chore: reconcile Plan 1A branch with idfoto→relicario rename
Renames crate directories and sweeps identifiers so Plan 1B can reference
the post-rename names throughout.
- git mv crates/idfoto-{core,cli,wasm} → crates/relicario-{core,cli,wasm}
- sed sweep: idfoto_core/idfoto-core/IdfotoError/IDFOTO_IMAGE/.idfoto/ etc.
- All 128 relicario-core tests pass post-sweep
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
[package]
|
||||
name = "idfoto-cli"
|
||||
name = "relicario-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "CLI for idfoto password manager"
|
||||
|
||||
[[bin]]
|
||||
name = "idfoto"
|
||||
name = "relicario"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
idfoto-core = { path = "../idfoto-core" }
|
||||
relicario-core = { path = "../relicario-core" }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
anyhow = "1"
|
||||
rpassword = "5"
|
||||
@@ -1,14 +1,14 @@
|
||||
//! idfoto CLI -- the platform layer for the idfoto password manager.
|
||||
//!
|
||||
//! This binary provides the filesystem, git, and terminal I/O that
|
||||
//! [`idfoto_core`] intentionally excludes. It is the "glue" between the
|
||||
//! [`relicario_core`] intentionally excludes. It is the "glue" between the
|
||||
//! platform-agnostic core library and the user's local environment.
|
||||
//!
|
||||
//! ## Vault layout on disk
|
||||
//!
|
||||
//! ```text
|
||||
//! <vault_dir>/
|
||||
//! .idfoto/
|
||||
//! .relicario/
|
||||
//! salt # 32-byte random salt for Argon2id KDF
|
||||
//! params.json # KDF tuning parameters (m, t, p)
|
||||
//! devices.json # registered device public keys
|
||||
@@ -23,10 +23,10 @@
|
||||
//!
|
||||
//! Every command that accesses vault data follows this sequence:
|
||||
//!
|
||||
//! 1. Locate the reference image (via `IDFOTO_IMAGE` env var or interactive prompt).
|
||||
//! 1. Locate the reference image (via `RELICARIO_IMAGE` env var or interactive prompt).
|
||||
//! 2. Prompt for the passphrase (read from stderr, not echoed).
|
||||
//! 3. Extract the 32-byte image secret from the reference JPEG via DCT steganography.
|
||||
//! 4. Read the vault salt and KDF params from `.idfoto/`.
|
||||
//! 4. Read the vault salt and KDF params from `.relicario/`.
|
||||
//! 5. Derive the master key: `Argon2id(passphrase || image_secret, salt, params)`.
|
||||
//! 6. Use the master key to decrypt the manifest and/or individual entries.
|
||||
//!
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use idfoto_core::{
|
||||
use relicario_core::{
|
||||
decrypt_entry, decrypt_manifest, encrypt_entry, encrypt_manifest, generate_entry_id,
|
||||
Entry, KdfParams, Manifest, ManifestEntry,
|
||||
};
|
||||
@@ -57,7 +57,7 @@ use std::process::Command;
|
||||
/// Top-level CLI argument parser.
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "idfoto",
|
||||
name = "relicario",
|
||||
version,
|
||||
about = "Git-backed password manager with reference image authentication"
|
||||
)]
|
||||
@@ -133,7 +133,7 @@ enum DeviceCommands {
|
||||
|
||||
// ─── Device entry ───────────────────────────────────────────────────────────
|
||||
|
||||
/// A registered device, stored in `.idfoto/devices.json`.
|
||||
/// A registered device, stored in `.relicario/devices.json`.
|
||||
///
|
||||
/// Each device has an ed25519 keypair. The private key lives on the device
|
||||
/// itself (in the user's config directory); only the public key is stored
|
||||
@@ -155,12 +155,12 @@ fn vault_dir() -> PathBuf {
|
||||
std::env::current_dir().expect("failed to get current directory")
|
||||
}
|
||||
|
||||
/// Returns the path to the `.idfoto/` configuration directory within the vault.
|
||||
/// Returns the path to the `.relicario/` configuration directory within the vault.
|
||||
fn idfoto_dir() -> PathBuf {
|
||||
vault_dir().join(".idfoto")
|
||||
}
|
||||
|
||||
/// Read the 32-byte vault salt from `.idfoto/salt`.
|
||||
/// Read the 32-byte vault salt from `.relicario/salt`.
|
||||
///
|
||||
/// The salt is generated once during `init` and is unique per vault. It is
|
||||
/// not secret (stored in plaintext) -- its purpose is to prevent precomputed
|
||||
@@ -175,7 +175,7 @@ fn read_salt() -> Result<[u8; 32]> {
|
||||
Ok(salt)
|
||||
}
|
||||
|
||||
/// Read the KDF parameters from `.idfoto/params.json`.
|
||||
/// Read the KDF parameters from `.relicario/params.json`.
|
||||
fn read_params() -> Result<KdfParams> {
|
||||
let data = fs::read_to_string(idfoto_dir().join("params.json"))
|
||||
.context("failed to read params.json")?;
|
||||
@@ -185,10 +185,10 @@ fn read_params() -> Result<KdfParams> {
|
||||
|
||||
/// Locate the reference image path.
|
||||
///
|
||||
/// First checks the `IDFOTO_IMAGE` environment variable (useful for scripting
|
||||
/// First checks the `RELICARIO_IMAGE` environment variable (useful for scripting
|
||||
/// and testing). If not set, prompts the user interactively.
|
||||
fn get_image_path() -> Result<PathBuf> {
|
||||
if let Ok(path) = std::env::var("IDFOTO_IMAGE") {
|
||||
if let Ok(path) = std::env::var("RELICARIO_IMAGE") {
|
||||
return Ok(PathBuf::from(path));
|
||||
}
|
||||
let path = prompt("Reference image path")?;
|
||||
@@ -207,12 +207,12 @@ fn unlock(image_path: &PathBuf) -> Result<Zeroizing<[u8; 32]>> {
|
||||
|
||||
let jpeg_data = fs::read(image_path).context("failed to read reference image")?;
|
||||
let image_secret =
|
||||
idfoto_core::imgsecret::extract(&jpeg_data).context("failed to extract image secret")?;
|
||||
relicario_core::imgsecret::extract(&jpeg_data).context("failed to extract image secret")?;
|
||||
|
||||
let salt = read_salt()?;
|
||||
let params = read_params()?;
|
||||
|
||||
let master_key = idfoto_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)
|
||||
let master_key = relicario_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)
|
||||
.context("failed to derive master key")?;
|
||||
|
||||
Ok(master_key)
|
||||
@@ -329,7 +329,7 @@ fn generate_password(length: usize) -> String {
|
||||
/// 5. Prompt for a passphrase (minimum 8 characters, with confirmation).
|
||||
/// 6. Generate a random 32-byte salt.
|
||||
/// 7. Derive the master key from passphrase + image_secret + salt.
|
||||
/// 8. Create the vault directory structure (.idfoto/, entries/).
|
||||
/// 8. Create the vault directory structure (.relicario/, entries/).
|
||||
/// 9. Write salt, KDF params, empty devices list, and encrypted empty manifest.
|
||||
/// 10. Initialize git and create the first commit.
|
||||
fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
|
||||
@@ -342,7 +342,7 @@ fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
|
||||
|
||||
// 3. Embed secret into carrier
|
||||
let reference_jpeg =
|
||||
idfoto_core::imgsecret::embed(&carrier, &image_secret).context("failed to embed secret")?;
|
||||
relicario_core::imgsecret::embed(&carrier, &image_secret).context("failed to embed secret")?;
|
||||
|
||||
// 4. Save reference JPEG
|
||||
fs::write(&output, &reference_jpeg).context("failed to write reference image")?;
|
||||
@@ -371,7 +371,7 @@ fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
|
||||
|
||||
// 7. Derive master key
|
||||
let params = KdfParams::default();
|
||||
let master_key = idfoto_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)
|
||||
let master_key = relicario_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)
|
||||
.context("failed to derive master key")?;
|
||||
|
||||
// 8. Create directory structure
|
||||
@@ -758,7 +758,7 @@ fn cmd_sync() -> Result<()> {
|
||||
|
||||
// ─── Device management ──────────────────────────────────────────────────────
|
||||
|
||||
/// Read the device registry from `.idfoto/devices.json`.
|
||||
/// Read the device registry from `.relicario/devices.json`.
|
||||
fn read_devices() -> Result<Vec<DeviceEntry>> {
|
||||
let path = idfoto_dir().join("devices.json");
|
||||
let data = fs::read_to_string(&path).context("failed to read devices.json")?;
|
||||
@@ -766,7 +766,7 @@ fn read_devices() -> Result<Vec<DeviceEntry>> {
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
/// Write the device registry to `.idfoto/devices.json`.
|
||||
/// Write the device registry to `.relicario/devices.json`.
|
||||
fn write_devices(devices: &[DeviceEntry]) -> Result<()> {
|
||||
let data = serde_json::to_string_pretty(devices)?;
|
||||
fs::write(idfoto_dir().join("devices.json"), data).context("failed to write devices.json")?;
|
||||
@@ -801,7 +801,7 @@ fn cmd_device_add(name: String) -> Result<()> {
|
||||
// Save private key to the user's config directory (NOT in the vault)
|
||||
let config_dir = dirs::config_dir()
|
||||
.context("failed to find config directory")?
|
||||
.join("idfoto");
|
||||
.join("relicario");
|
||||
fs::create_dir_all(&config_dir).context("failed to create config directory")?;
|
||||
let key_path = config_dir.join(format!("{}.key", name));
|
||||
fs::write(&key_path, &private_key_hex).context("failed to write private key")?;
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "idfoto-core"
|
||||
name = "relicario-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Core library for idfoto password manager"
|
||||
@@ -43,7 +43,7 @@ impl From<&AttachmentRef> for AttachmentSummary {
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::crypto::{decrypt, encrypt};
|
||||
use crate::error::{IdfotoError, Result};
|
||||
use crate::error::{RelicarioError, Result};
|
||||
|
||||
/// Encrypted attachment with the AID derived from plaintext content.
|
||||
#[derive(Debug)]
|
||||
@@ -54,7 +54,7 @@ pub struct EncryptedAttachment {
|
||||
|
||||
/// Encrypt raw attachment bytes, deriving the [`AttachmentId`] from `sha256(plaintext)`.
|
||||
///
|
||||
/// Returns [`IdfotoError::AttachmentTooLarge`] immediately if `plaintext.len() > max_bytes`,
|
||||
/// Returns [`RelicarioError::AttachmentTooLarge`] immediately if `plaintext.len() > max_bytes`,
|
||||
/// before any crypto work is done.
|
||||
///
|
||||
/// ## Call-site adaptation
|
||||
@@ -67,7 +67,7 @@ pub fn encrypt_attachment(
|
||||
max_bytes: u64,
|
||||
) -> Result<EncryptedAttachment> {
|
||||
if plaintext.len() as u64 > max_bytes {
|
||||
return Err(IdfotoError::AttachmentTooLarge {
|
||||
return Err(RelicarioError::AttachmentTooLarge {
|
||||
size: plaintext.len() as u64,
|
||||
max: max_bytes,
|
||||
});
|
||||
@@ -118,7 +118,7 @@ mod crypto_tests {
|
||||
fn oversize_attachment_rejected() {
|
||||
let plaintext = vec![0u8; 11_000_000];
|
||||
let err = encrypt_attachment(&plaintext, &key(), 10 * 1024 * 1024);
|
||||
assert!(matches!(err, Err(IdfotoError::AttachmentTooLarge { .. })));
|
||||
assert!(matches!(err, Err(RelicarioError::AttachmentTooLarge { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -127,7 +127,7 @@ mod crypto_tests {
|
||||
let enc = encrypt_attachment(plaintext, &key(), 1024).unwrap();
|
||||
let wrong = Zeroizing::new([0u8; 32]);
|
||||
let err = decrypt_attachment(&enc.bytes, &wrong);
|
||||
assert!(matches!(err, Err(IdfotoError::Decrypt)));
|
||||
assert!(matches!(err, Err(RelicarioError::Decrypt)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
//! ```
|
||||
//!
|
||||
//! Both factors contribute to the derived key -- compromising one without the
|
||||
//! other is insufficient. The salt is vault-specific and stored in `.idfoto/salt`.
|
||||
//! other is insufficient. The salt is vault-specific and stored in `.relicario/salt`.
|
||||
|
||||
use argon2::{Algorithm, Argon2, Params, Version};
|
||||
use chacha20poly1305::{
|
||||
@@ -53,7 +53,7 @@ use serde::{Deserialize, Serialize};
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::error::{IdfotoError, Result};
|
||||
use crate::error::{RelicarioError, Result};
|
||||
|
||||
/// Current binary format version. Increment this if the ciphertext layout changes.
|
||||
pub const VERSION_BYTE: u8 = 0x02;
|
||||
@@ -76,7 +76,7 @@ const HEADER_LEN: usize = 1 + NONCE_LEN; // version + nonce
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`IdfotoError::Encrypt`] if the underlying AEAD operation fails
|
||||
/// Returns [`RelicarioError::Encrypt`] if the underlying AEAD operation fails
|
||||
/// (extremely unlikely in practice).
|
||||
pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
||||
let cipher = XChaCha20Poly1305::new(key.into());
|
||||
@@ -90,7 +90,7 @@ pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
||||
|
||||
let ciphertext = cipher
|
||||
.encrypt(&nonce, plaintext)
|
||||
.map_err(|e| IdfotoError::Encrypt(e.to_string()))?;
|
||||
.map_err(|e| RelicarioError::Encrypt(e.to_string()))?;
|
||||
|
||||
// Output: version(1) || nonce(24) || ciphertext+tag
|
||||
let mut output = Vec::with_capacity(HEADER_LEN + ciphertext.len());
|
||||
@@ -105,27 +105,27 @@ pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
||||
///
|
||||
/// Validates the version byte and minimum blob length before attempting
|
||||
/// authenticated decryption. If the key is wrong or the data has been
|
||||
/// tampered with, the Poly1305 tag verification fails and [`IdfotoError::Decrypt`]
|
||||
/// tampered with, the Poly1305 tag verification fails and [`RelicarioError::Decrypt`]
|
||||
/// is returned -- with no information about which bytes were wrong (preventing
|
||||
/// padding oracle / chosen-ciphertext attacks).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`IdfotoError::Format`] if the data is too short or has an unknown version byte.
|
||||
/// - [`IdfotoError::Decrypt`] if the AEAD tag verification fails (wrong key or
|
||||
/// - [`RelicarioError::Format`] if the data is too short or has an unknown version byte.
|
||||
/// - [`RelicarioError::Decrypt`] if the AEAD tag verification fails (wrong key or
|
||||
/// tampered data).
|
||||
pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
||||
// Minimum valid blob: 1 (version) + 24 (nonce) + 16 (tag) = 41 bytes.
|
||||
// A zero-length plaintext produces exactly 41 bytes of output.
|
||||
if data.len() < HEADER_LEN + TAG_LEN {
|
||||
return Err(IdfotoError::Format(
|
||||
return Err(RelicarioError::Format(
|
||||
"data too short to be valid ciphertext".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let found = data[0];
|
||||
if found != VERSION_BYTE {
|
||||
return Err(IdfotoError::UnsupportedFormatVersion {
|
||||
return Err(RelicarioError::UnsupportedFormatVersion {
|
||||
found,
|
||||
expected: VERSION_BYTE,
|
||||
});
|
||||
@@ -137,14 +137,14 @@ pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
||||
let cipher = XChaCha20Poly1305::new(key.into());
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce, ciphertext)
|
||||
.map_err(|_| IdfotoError::Decrypt)?;
|
||||
.map_err(|_| RelicarioError::Decrypt)?;
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
/// Tunable parameters for the Argon2id key derivation function.
|
||||
///
|
||||
/// These are stored in the vault's `.idfoto/params.json` so that every client
|
||||
/// These are stored in the vault's `.relicario/params.json` so that every client
|
||||
/// derives the same master key from the same inputs. Making them configurable
|
||||
/// lets tests use fast params (m=256, t=1, p=1) while production uses strong
|
||||
/// params (m=64MiB, t=3, p=4).
|
||||
@@ -193,8 +193,8 @@ impl Default for KdfParams {
|
||||
/// - `passphrase`: the user's passphrase as raw UTF-8 bytes.
|
||||
/// - `image_secret`: the 32-byte secret extracted from the reference JPEG via
|
||||
/// [`crate::imgsecret::extract`].
|
||||
/// - `salt`: a 32-byte vault-specific salt (stored in `.idfoto/salt`).
|
||||
/// - `params`: the Argon2id tuning parameters (stored in `.idfoto/params.json`).
|
||||
/// - `salt`: a 32-byte vault-specific salt (stored in `.relicario/salt`).
|
||||
/// - `params`: the Argon2id tuning parameters (stored in `.relicario/params.json`).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
@@ -202,7 +202,7 @@ impl Default for KdfParams {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`IdfotoError::Kdf`] if the Argon2id parameters are invalid (e.g.,
|
||||
/// Returns [`RelicarioError::Kdf`] if the Argon2id parameters are invalid (e.g.,
|
||||
/// memory cost below the library's minimum).
|
||||
pub fn derive_master_key(
|
||||
passphrase: &[u8],
|
||||
@@ -216,7 +216,7 @@ pub fn derive_master_key(
|
||||
params.argon2_p,
|
||||
Some(32),
|
||||
)
|
||||
.map_err(|e| IdfotoError::Kdf(e.to_string()))?;
|
||||
.map_err(|e| RelicarioError::Kdf(e.to_string()))?;
|
||||
|
||||
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
|
||||
|
||||
@@ -238,7 +238,7 @@ pub fn derive_master_key(
|
||||
let mut output = Zeroizing::new([0u8; 32]);
|
||||
argon2
|
||||
.hash_password_into(password.as_slice(), salt, output.as_mut())
|
||||
.map_err(|e| IdfotoError::Kdf(e.to_string()))?;
|
||||
.map_err(|e| RelicarioError::Kdf(e.to_string()))?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
@@ -316,7 +316,7 @@ mod tests {
|
||||
let result = decrypt(&wrong_key, &ciphertext);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), IdfotoError::Decrypt));
|
||||
assert!(matches!(result.unwrap_err(), RelicarioError::Decrypt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -410,7 +410,7 @@ mod tests {
|
||||
let key = Zeroizing::new([0u8; 32]);
|
||||
let err = decrypt(&*key, &blob).expect_err("v1 blob should fail decrypt");
|
||||
match err {
|
||||
IdfotoError::UnsupportedFormatVersion { found, expected } => {
|
||||
RelicarioError::UnsupportedFormatVersion { found, expected } => {
|
||||
assert_eq!(found, 0x01);
|
||||
assert_eq!(expected, 0x02);
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
//! Unified error type for the idfoto-core crate.
|
||||
//! Unified error type for the relicario-core crate.
|
||||
//!
|
||||
//! Every fallible function in this crate returns [`Result<T>`], which is an alias
|
||||
//! for `std::result::Result<T, IdfotoError>`. Using a single error enum keeps the
|
||||
//! for `std::result::Result<T, RelicarioError>`. Using a single error enum keeps the
|
||||
//! public API surface predictable and makes error handling in callers (CLI, WASM
|
||||
//! bindings, mobile FFI) straightforward.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// All errors that can originate from idfoto-core operations.
|
||||
/// All errors that can originate from relicario-core operations.
|
||||
///
|
||||
/// Variants are ordered roughly by the pipeline stage where they occur:
|
||||
/// KDF -> encryption -> decryption -> format parsing -> item lookup -> image
|
||||
/// steganography -> serialization -> device keys.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum IdfotoError {
|
||||
pub enum RelicarioError {
|
||||
#[error("key derivation failed: {0}")]
|
||||
Kdf(String),
|
||||
|
||||
@@ -64,7 +64,7 @@ pub enum IdfotoError {
|
||||
}
|
||||
|
||||
/// Crate-wide result alias, reducing boilerplate in function signatures.
|
||||
pub type Result<T> = std::result::Result<T, IdfotoError>;
|
||||
pub type Result<T> = std::result::Result<T, RelicarioError>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -72,13 +72,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn decrypt_error_message_is_opaque() {
|
||||
let err = IdfotoError::Decrypt;
|
||||
let err = RelicarioError::Decrypt;
|
||||
assert_eq!(format!("{}", err), "decryption failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weak_passphrase_carries_score() {
|
||||
let err = IdfotoError::WeakPassphrase { score: 1 };
|
||||
let err = RelicarioError::WeakPassphrase { score: 1 };
|
||||
let s = format!("{}", err);
|
||||
assert!(s.contains("passphrase"));
|
||||
assert!(s.contains("strength"));
|
||||
@@ -86,7 +86,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn attachment_too_large_reports_sizes() {
|
||||
let err = IdfotoError::AttachmentTooLarge { size: 11_000_000, max: 10_485_760 };
|
||||
let err = RelicarioError::AttachmentTooLarge { size: 11_000_000, max: 10_485_760 };
|
||||
let s = format!("{}", err);
|
||||
assert!(s.contains("11000000"));
|
||||
assert!(s.contains("10485760"));
|
||||
@@ -94,13 +94,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn item_not_found_carries_id() {
|
||||
let err = IdfotoError::ItemNotFound("abc123".to_string());
|
||||
let err = RelicarioError::ItemNotFound("abc123".to_string());
|
||||
assert!(format!("{}", err).contains("abc123"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_format_version_reports_byte() {
|
||||
let err = IdfotoError::UnsupportedFormatVersion { found: 0x01, expected: 0x02 };
|
||||
let err = RelicarioError::UnsupportedFormatVersion { found: 0x01, expected: 0x02 };
|
||||
let s = format!("{}", err);
|
||||
assert!(s.contains("01") || s.contains("1"));
|
||||
assert!(s.contains("02") || s.contains("2"));
|
||||
@@ -7,7 +7,7 @@ use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::error::{IdfotoError, Result};
|
||||
use crate::error::{RelicarioError, Result};
|
||||
use crate::settings::{Capitalization, CharClasses, GeneratorRequest, SymbolCharset};
|
||||
|
||||
const SAFE_SYMBOLS: &[u8] = b"!@#$%^&*-_=+";
|
||||
@@ -21,7 +21,7 @@ pub fn generate_password(req: &GeneratorRequest) -> Result<Zeroizing<String>> {
|
||||
GeneratorRequest::Random { length, classes, symbol_charset } => {
|
||||
random_password(*length, classes, symbol_charset)
|
||||
}
|
||||
GeneratorRequest::Bip39 { .. } => Err(IdfotoError::Format(
|
||||
GeneratorRequest::Bip39 { .. } => Err(RelicarioError::Format(
|
||||
"use generate_passphrase() for BIP39 requests".into(),
|
||||
)),
|
||||
}
|
||||
@@ -33,7 +33,7 @@ fn random_password(
|
||||
symbol_charset: &SymbolCharset,
|
||||
) -> Result<Zeroizing<String>> {
|
||||
if length == 0 || length > 128 {
|
||||
return Err(IdfotoError::Format("length must be 1..=128".into()));
|
||||
return Err(RelicarioError::Format("length must be 1..=128".into()));
|
||||
}
|
||||
let mut charset: Vec<u8> = Vec::new();
|
||||
if classes.lower { charset.extend_from_slice(LOWER); }
|
||||
@@ -45,7 +45,7 @@ fn random_password(
|
||||
SymbolCharset::Extended => EXTENDED_SYMBOLS,
|
||||
SymbolCharset::Custom(s) => {
|
||||
if !s.is_ascii() {
|
||||
return Err(IdfotoError::Format(
|
||||
return Err(RelicarioError::Format(
|
||||
"SymbolCharset::Custom must be ASCII-only".into(),
|
||||
));
|
||||
}
|
||||
@@ -55,7 +55,7 @@ fn random_password(
|
||||
charset.extend_from_slice(symbols);
|
||||
}
|
||||
if charset.is_empty() {
|
||||
return Err(IdfotoError::Format("at least one character class required".into()));
|
||||
return Err(RelicarioError::Format("at least one character class required".into()));
|
||||
}
|
||||
|
||||
let dist = Uniform::from(0..charset.len());
|
||||
@@ -69,7 +69,7 @@ pub fn generate_passphrase(req: &GeneratorRequest) -> Result<Zeroizing<String>>
|
||||
GeneratorRequest::Bip39 { word_count, separator, capitalization } => {
|
||||
bip39_passphrase(*word_count, separator, *capitalization)
|
||||
}
|
||||
GeneratorRequest::Random { .. } => Err(IdfotoError::Format(
|
||||
GeneratorRequest::Random { .. } => Err(RelicarioError::Format(
|
||||
"use generate_password() for Random requests".into(),
|
||||
)),
|
||||
}
|
||||
@@ -77,7 +77,7 @@ pub fn generate_passphrase(req: &GeneratorRequest) -> Result<Zeroizing<String>>
|
||||
|
||||
fn bip39_passphrase(word_count: u32, separator: &str, cap: Capitalization) -> Result<Zeroizing<String>> {
|
||||
if !matches!(word_count, 3..=12) {
|
||||
return Err(IdfotoError::Format("word_count must be 3..=12".into()));
|
||||
return Err(RelicarioError::Format("word_count must be 3..=12".into()));
|
||||
}
|
||||
// bip39 v2 requires entropy 128–256 bits in multiples of 32 bits (4 bytes).
|
||||
// We always generate 128 bits (16 bytes) → 12 words, then take the first
|
||||
@@ -85,7 +85,7 @@ fn bip39_passphrase(word_count: u32, separator: &str, cap: Capitalization) -> Re
|
||||
let mut entropy = Zeroizing::new([0u8; 16]);
|
||||
OsRng.fill_bytes(entropy.as_mut_slice());
|
||||
let m = Mnemonic::from_entropy_in(Language::English, entropy.as_slice())
|
||||
.map_err(|e| IdfotoError::Format(format!("bip39: {e}")))?;
|
||||
.map_err(|e| RelicarioError::Format(format!("bip39: {e}")))?;
|
||||
let words: Vec<String> = m.words().take(word_count as usize).map(|w| {
|
||||
match cap {
|
||||
Capitalization::Lower => w.to_ascii_lowercase(),
|
||||
@@ -124,7 +124,7 @@ pub fn rate_passphrase(p: &str) -> StrengthEstimate {
|
||||
pub fn validate_passphrase_strength(p: &str) -> Result<()> {
|
||||
let est = rate_passphrase(p);
|
||||
if est.score < 3 {
|
||||
return Err(IdfotoError::WeakPassphrase { score: est.score });
|
||||
return Err(RelicarioError::WeakPassphrase { score: est.score });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -39,7 +39,7 @@
|
||||
//! - Mild cropping (up to ~10% from edges, within the 15% crumple zone)
|
||||
//! - Color space conversions (embedding is in luminance only)
|
||||
|
||||
use crate::error::{IdfotoError, Result};
|
||||
use crate::error::{RelicarioError, Result};
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::ImageReader;
|
||||
use image::{ImageEncoder, Rgb, RgbImage};
|
||||
@@ -179,10 +179,10 @@ struct EmbedRegion {
|
||||
fn extract_y_channel(jpeg_bytes: &[u8]) -> Result<YChannel> {
|
||||
let reader = ImageReader::new(Cursor::new(jpeg_bytes))
|
||||
.with_guessed_format()
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to read image: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to read image: {e}")))?;
|
||||
let img = reader
|
||||
.decode()
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to decode image: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to decode image: {e}")))?;
|
||||
let rgb = img.to_rgb8();
|
||||
let (width, height) = (rgb.width() as usize, rgb.height() as usize);
|
||||
let mut data = Vec::with_capacity(width * height);
|
||||
@@ -527,10 +527,10 @@ fn select_embed_blocks(region: &EmbedRegion, target_count: usize) -> Vec<(usize,
|
||||
fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result<Vec<u8>> {
|
||||
let reader = ImageReader::new(Cursor::new(original_jpeg))
|
||||
.with_guessed_format()
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to read image: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to read image: {e}")))?;
|
||||
let img = reader
|
||||
.decode()
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to decode image: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to decode image: {e}")))?;
|
||||
let rgb = img.to_rgb8();
|
||||
let (width, height) = (rgb.width(), rgb.height());
|
||||
|
||||
@@ -572,7 +572,7 @@ fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result<Vec<u
|
||||
let encoder = JpegEncoder::new_with_quality(&mut buf, 92);
|
||||
encoder
|
||||
.write_image(output.as_raw(), width, height, image::ExtendedColorType::Rgb8)
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to encode JPEG: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to encode JPEG: {e}")))?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
@@ -597,14 +597,14 @@ fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result<Vec<u
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`IdfotoError::ImageTooSmall`] if the image is below minimum dimensions
|
||||
/// - [`RelicarioError::ImageTooSmall`] if the image is below minimum dimensions
|
||||
/// or does not have enough blocks for reliable embedding.
|
||||
/// - [`IdfotoError::ImgSecret`] if the image cannot be decoded or re-encoded.
|
||||
/// - [`RelicarioError::ImgSecret`] if the image cannot be decoded or re-encoded.
|
||||
pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result<Vec<u8>> {
|
||||
let mut y = extract_y_channel(carrier_jpeg)?;
|
||||
|
||||
if (y.width as u32) < MIN_DIMENSION || (y.height as u32) < MIN_DIMENSION {
|
||||
return Err(IdfotoError::ImageTooSmall {
|
||||
return Err(RelicarioError::ImageTooSmall {
|
||||
min_width: MIN_DIMENSION,
|
||||
min_height: MIN_DIMENSION,
|
||||
actual_width: y.width as u32,
|
||||
@@ -616,7 +616,7 @@ pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result<Vec<u8>> {
|
||||
let total_blocks = region.blocks_x * region.blocks_y;
|
||||
|
||||
if total_blocks < BLOCKS_PER_COPY * MIN_COPIES {
|
||||
return Err(IdfotoError::ImageTooSmall {
|
||||
return Err(RelicarioError::ImageTooSmall {
|
||||
min_width: MIN_DIMENSION,
|
||||
min_height: MIN_DIMENSION,
|
||||
actual_width: y.width as u32,
|
||||
@@ -669,7 +669,7 @@ pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result<Vec<u8>> {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`IdfotoError::ExtractionFailed`] if no valid secret could be recovered
|
||||
/// - [`RelicarioError::ExtractionFailed`] if no valid secret could be recovered
|
||||
/// (image was never watermarked, or was too heavily recompressed/cropped).
|
||||
pub fn extract(jpeg_bytes: &[u8]) -> Result<[u8; 32]> {
|
||||
extract_with_crop_recovery(jpeg_bytes)
|
||||
@@ -695,7 +695,7 @@ fn try_extract_with_layout(
|
||||
) -> Result<[u8; 32]> {
|
||||
let positions = compute_embed_positions(orig_w, orig_h);
|
||||
if positions.is_empty() {
|
||||
return Err(IdfotoError::ExtractionFailed);
|
||||
return Err(RelicarioError::ExtractionFailed);
|
||||
}
|
||||
|
||||
let region = compute_region(orig_w, orig_h);
|
||||
@@ -750,14 +750,14 @@ fn try_extract_with_layout(
|
||||
let mut result_bits = vec![0u8; SECRET_BITS];
|
||||
for i in 0..SECRET_BITS {
|
||||
if votes_total[i] == 0 {
|
||||
return Err(IdfotoError::ExtractionFailed);
|
||||
return Err(RelicarioError::ExtractionFailed);
|
||||
}
|
||||
let ones = votes_one[i];
|
||||
let zeros = votes_total[i] - ones;
|
||||
let majority = ones.max(zeros);
|
||||
let confidence = majority as f64 / votes_total[i] as f64;
|
||||
if confidence < 0.60 {
|
||||
return Err(IdfotoError::ExtractionFailed);
|
||||
return Err(RelicarioError::ExtractionFailed);
|
||||
}
|
||||
result_bits[i] = if ones > zeros { 1 } else { 0 };
|
||||
}
|
||||
@@ -785,7 +785,7 @@ fn extract_with_crop_recovery(jpeg_bytes: &[u8]) -> Result<[u8; 32]> {
|
||||
let y = extract_y_channel(jpeg_bytes)?;
|
||||
|
||||
if (y.width as u32) < MIN_DIMENSION || (y.height as u32) < MIN_DIMENSION {
|
||||
return Err(IdfotoError::ExtractionFailed);
|
||||
return Err(RelicarioError::ExtractionFailed);
|
||||
}
|
||||
|
||||
// Try 1: assume the image is uncropped (original size = current size)
|
||||
@@ -830,7 +830,7 @@ fn extract_with_crop_recovery(jpeg_bytes: &[u8]) -> Result<[u8; 32]> {
|
||||
}
|
||||
}
|
||||
|
||||
Err(IdfotoError::ExtractionFailed)
|
||||
Err(RelicarioError::ExtractionFailed)
|
||||
}
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────────────
|
||||
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::error::{IdfotoError, Result};
|
||||
use crate::error::{RelicarioError, Result};
|
||||
use crate::ids::{AttachmentId, FieldId};
|
||||
use crate::item_types::TotpConfig;
|
||||
use crate::time::MonthYear;
|
||||
@@ -96,7 +96,7 @@ impl Field {
|
||||
/// Verify kind/value discriminants match. Called after deserialization.
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.kind != self.value.kind() {
|
||||
return Err(IdfotoError::Format(format!(
|
||||
return Err(RelicarioError::Format(format!(
|
||||
"field {}: kind {:?} does not match value discriminant {:?}",
|
||||
self.id.as_str(),
|
||||
self.kind,
|
||||
@@ -182,7 +182,7 @@ impl Item {
|
||||
for section in &mut self.sections {
|
||||
if let Some(field) = section.fields.iter_mut().find(|f| &f.id == field_id) {
|
||||
if field.value.kind() != new_value.kind() {
|
||||
return Err(IdfotoError::Format(format!(
|
||||
return Err(RelicarioError::Format(format!(
|
||||
"field {}: cannot change kind from {:?} to {:?}",
|
||||
field.id.as_str(), field.value.kind(), new_value.kind()
|
||||
)));
|
||||
@@ -199,7 +199,7 @@ impl Item {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(IdfotoError::Format(format!("field {} not found", field_id.as_str())))
|
||||
Err(RelicarioError::Format(format!("field {} not found", field_id.as_str())))
|
||||
}
|
||||
|
||||
pub fn soft_delete(&mut self) {
|
||||
@@ -247,7 +247,7 @@ fn serialize_history_value(value: &FieldValue) -> Result<Zeroizing<String>> {
|
||||
let s = base32_encode(&cfg.secret);
|
||||
Zeroizing::new(s)
|
||||
}
|
||||
_ => return Err(IdfotoError::Format("not a history-tracked kind".into())),
|
||||
_ => return Err(RelicarioError::Format("not a history-tracked kind".into())),
|
||||
};
|
||||
Ok(s)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//! # idfoto-core
|
||||
//! # relicario-core
|
||||
//!
|
||||
//! Platform-agnostic core library for the idfoto password manager.
|
||||
//!
|
||||
@@ -10,7 +10,7 @@
|
||||
//!
|
||||
//! ## Modules
|
||||
//!
|
||||
//! - [`error`] — The unified error type ([`IdfotoError`]).
|
||||
//! - [`error`] — The unified error type ([`RelicarioError`]).
|
||||
//! - [`crypto`] — Argon2id KDF (length-prefixed inputs, Zeroizing output) and
|
||||
//! XChaCha20-Poly1305 AEAD with VERSION_BYTE 0x02.
|
||||
//! - [`ids`] — `ItemId`, `FieldId`, and content-addressed `AttachmentId`.
|
||||
@@ -38,7 +38,7 @@
|
||||
//! ```
|
||||
|
||||
pub mod error;
|
||||
pub use error::{IdfotoError, Result};
|
||||
pub use error::{RelicarioError, Result};
|
||||
|
||||
pub mod crypto;
|
||||
pub use crypto::{decrypt, derive_master_key, encrypt, KdfParams, VERSION_BYTE};
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Attachment encrypt/decrypt + content-addressed AID + cap enforcement.
|
||||
|
||||
use idfoto_core::{
|
||||
AttachmentId, IdfotoError,
|
||||
use relicario_core::{
|
||||
AttachmentId, RelicarioError,
|
||||
crypto::KdfParams,
|
||||
decrypt_attachment, derive_master_key, encrypt_attachment,
|
||||
};
|
||||
@@ -43,7 +43,7 @@ fn cap_enforcement_at_exact_max() {
|
||||
// One byte over — should fail
|
||||
let err = encrypt_attachment(&plaintext, &key, 1023);
|
||||
match err {
|
||||
Err(IdfotoError::AttachmentTooLarge { size, max }) => {
|
||||
Err(RelicarioError::AttachmentTooLarge { size, max }) => {
|
||||
assert_eq!(size, 1024);
|
||||
assert_eq!(max, 1023);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
//! Field history end-to-end: capture on update, prune by retention policy,
|
||||
//! survive encrypt/decrypt round-trip.
|
||||
|
||||
use idfoto_core::{
|
||||
use relicario_core::{
|
||||
Field, FieldValue, HistoryRetention, Item, ItemCore, Section,
|
||||
crypto::KdfParams,
|
||||
derive_master_key, decrypt_item, encrypt_item,
|
||||
};
|
||||
use idfoto_core::item_types::LoginCore;
|
||||
use relicario_core::item_types::LoginCore;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
fn key() -> Zeroizing<[u8; 32]> {
|
||||
@@ -2,8 +2,8 @@
|
||||
//! UnsupportedFormatVersion, length-prefix construction guarantees domain
|
||||
//! separation.
|
||||
|
||||
use idfoto_core::{
|
||||
IdfotoError,
|
||||
use relicario_core::{
|
||||
RelicarioError,
|
||||
crypto::{KdfParams, VERSION_BYTE},
|
||||
decrypt, derive_master_key, encrypt,
|
||||
};
|
||||
@@ -33,7 +33,7 @@ fn v1_blob_is_rejected_with_unsupported_format_version() {
|
||||
// decrypt(key: &[u8; 32], data: &[u8])
|
||||
let err = decrypt(&key, &blob);
|
||||
match err {
|
||||
Err(IdfotoError::UnsupportedFormatVersion { found, expected }) => {
|
||||
Err(RelicarioError::UnsupportedFormatVersion { found, expected }) => {
|
||||
assert_eq!(found, 0x01);
|
||||
assert_eq!(expected, 0x02);
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
//! counts before asserting proportions. The ±5pp tolerance is unchanged because
|
||||
//! sample size is the same (~10k chars).
|
||||
|
||||
use idfoto_core::{
|
||||
use relicario_core::{
|
||||
Capitalization, CharClasses, GeneratorRequest, SymbolCharset,
|
||||
generate_passphrase, generate_password, validate_passphrase_strength,
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
//! End-to-end integration tests for the typed-item core.
|
||||
|
||||
use idfoto_core::{
|
||||
use relicario_core::{
|
||||
crypto::KdfParams,
|
||||
derive_master_key, encrypt_item, decrypt_item,
|
||||
encrypt_manifest, decrypt_manifest,
|
||||
encrypt_settings, decrypt_settings,
|
||||
Field, FieldValue, Item, ItemCore, Manifest, Section, VaultSettings,
|
||||
};
|
||||
use idfoto_core::item_types::{LoginCore, SecureNoteCore};
|
||||
use relicario_core::item_types::{LoginCore, SecureNoteCore};
|
||||
use url::Url;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
@@ -97,7 +97,7 @@ fn field_history_persists_through_round_trip() {
|
||||
|
||||
#[test]
|
||||
fn wrong_key_fails_with_opaque_decrypt() {
|
||||
use idfoto_core::IdfotoError;
|
||||
use relicario_core::RelicarioError;
|
||||
|
||||
let salt = [0u8; 32];
|
||||
let img = [0u8; 32];
|
||||
@@ -107,5 +107,5 @@ fn wrong_key_fails_with_opaque_decrypt() {
|
||||
let item = Item::new("x".into(), ItemCore::SecureNote(SecureNoteCore::default()));
|
||||
let blob = encrypt_item(&item, &right).unwrap();
|
||||
let err = decrypt_item(&blob, &wrong);
|
||||
assert!(matches!(err, Err(IdfotoError::Decrypt)));
|
||||
assert!(matches!(err, Err(RelicarioError::Decrypt)));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "idfoto-wasm"
|
||||
name = "relicario-wasm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "WASM bindings for idfoto password manager"
|
||||
@@ -8,7 +8,7 @@ description = "WASM bindings for idfoto password manager"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
idfoto-core = { path = "../idfoto-core" }
|
||||
relicario-core = { path = "../relicario-core" }
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
serde_json = "1"
|
||||
@@ -1,6 +1,6 @@
|
||||
//! WASM bindings for the idfoto password manager.
|
||||
//!
|
||||
//! This crate wraps [`idfoto_core`] for use in a Chrome MV3 browser extension via
|
||||
//! This crate wraps [`relicario_core`] for use in a Chrome MV3 browser extension via
|
||||
//! `wasm-bindgen`. Every function marked `#[wasm_bindgen]` is callable from
|
||||
//! JavaScript after loading the compiled `.wasm` module.
|
||||
//!
|
||||
@@ -21,10 +21,10 @@
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use idfoto_core::crypto::{self, KdfParams};
|
||||
use idfoto_core::entry::Entry;
|
||||
use idfoto_core::vault;
|
||||
use idfoto_core::imgsecret;
|
||||
use relicario_core::crypto::{self, KdfParams};
|
||||
use relicario_core::entry::Entry;
|
||||
use relicario_core::vault;
|
||||
use relicario_core::imgsecret;
|
||||
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha1::Sha1;
|
||||
@@ -103,7 +103,7 @@ pub fn embed_image_secret(carrier_jpeg: &[u8], secret: &[u8]) -> Result<Vec<u8>,
|
||||
let secret: [u8; 32] = secret
|
||||
.try_into()
|
||||
.map_err(|_| JsValue::from_str("secret must be exactly 32 bytes"))?;
|
||||
idfoto_core::imgsecret::embed(carrier_jpeg, &secret)
|
||||
relicario_core::imgsecret::embed(carrier_jpeg, &secret)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ pub fn encrypt_manifest(manifest_json: &str, key: &[u8]) -> Result<Vec<u8>, JsVa
|
||||
let key: &[u8; 32] = key
|
||||
.try_into()
|
||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||
let manifest: idfoto_core::entry::Manifest =
|
||||
let manifest: relicario_core::entry::Manifest =
|
||||
serde_json::from_str(manifest_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
vault::encrypt_manifest(key, &manifest).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
Reference in New Issue
Block a user