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:
166
crates/relicario-core/src/attachment.rs
Normal file
166
crates/relicario-core/src/attachment.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
//! Attachment refs (carried on Item) and summaries (carried in Manifest).
|
||||
//!
|
||||
//! Encryption helpers (`encrypt_attachment`, `decrypt_attachment`) are added
|
||||
//! later in Task 22 once the crypto module is settled.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ids::AttachmentId;
|
||||
|
||||
/// Reference to an attachment, carried on the Item record.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AttachmentRef {
|
||||
pub id: AttachmentId,
|
||||
pub filename: String,
|
||||
pub mime_type: String,
|
||||
/// Plaintext size in bytes.
|
||||
pub size: u64,
|
||||
/// Unix-seconds when this attachment was added.
|
||||
pub created: i64,
|
||||
}
|
||||
|
||||
/// Compact summary of an attachment, carried in the Manifest so the popup
|
||||
/// can show attachment indicators without decrypting the item file.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AttachmentSummary {
|
||||
pub id: AttachmentId,
|
||||
pub filename: String,
|
||||
pub mime_type: String,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
impl From<&AttachmentRef> for AttachmentSummary {
|
||||
fn from(r: &AttachmentRef) -> Self {
|
||||
Self {
|
||||
id: r.id.clone(),
|
||||
filename: r.filename.clone(),
|
||||
mime_type: r.mime_type.clone(),
|
||||
size: r.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::crypto::{decrypt, encrypt};
|
||||
use crate::error::{RelicarioError, Result};
|
||||
|
||||
/// Encrypted attachment with the AID derived from plaintext content.
|
||||
#[derive(Debug)]
|
||||
pub struct EncryptedAttachment {
|
||||
pub id: AttachmentId,
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Encrypt raw attachment bytes, deriving the [`AttachmentId`] from `sha256(plaintext)`.
|
||||
///
|
||||
/// Returns [`RelicarioError::AttachmentTooLarge`] immediately if `plaintext.len() > max_bytes`,
|
||||
/// before any crypto work is done.
|
||||
///
|
||||
/// ## Call-site adaptation
|
||||
///
|
||||
/// `crypto::encrypt` accepts `&[u8; 32]`; we coerce `&Zeroizing<[u8; 32]>` via
|
||||
/// `&**master_key` (double-deref: `Zeroizing<[u8;32]>` → `[u8;32]` → `&[u8;32]`).
|
||||
pub fn encrypt_attachment(
|
||||
plaintext: &[u8],
|
||||
master_key: &Zeroizing<[u8; 32]>,
|
||||
max_bytes: u64,
|
||||
) -> Result<EncryptedAttachment> {
|
||||
if plaintext.len() as u64 > max_bytes {
|
||||
return Err(RelicarioError::AttachmentTooLarge {
|
||||
size: plaintext.len() as u64,
|
||||
max: max_bytes,
|
||||
});
|
||||
}
|
||||
let id = AttachmentId::from_plaintext(plaintext);
|
||||
let bytes = encrypt(master_key, plaintext)?;
|
||||
Ok(EncryptedAttachment { id, bytes })
|
||||
}
|
||||
|
||||
/// Decrypt a blob produced by [`encrypt_attachment`], returning the plaintext
|
||||
/// wrapped in [`Zeroizing`] so it is wiped on drop.
|
||||
///
|
||||
/// ## Call-site adaptation
|
||||
///
|
||||
/// `crypto::decrypt` accepts `&[u8; 32]`; we coerce via `&**master_key`.
|
||||
pub fn decrypt_attachment(
|
||||
encrypted: &[u8],
|
||||
master_key: &Zeroizing<[u8; 32]>,
|
||||
) -> Result<Zeroizing<Vec<u8>>> {
|
||||
let plaintext = decrypt(master_key, encrypted)?;
|
||||
Ok(Zeroizing::new(plaintext))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod crypto_tests {
|
||||
use super::*;
|
||||
|
||||
fn key() -> Zeroizing<[u8; 32]> {
|
||||
Zeroizing::new([0x42u8; 32])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_round_trip() {
|
||||
let plaintext = b"the quick brown fox jumps over the lazy dog";
|
||||
let enc = encrypt_attachment(plaintext, &key(), 1024).unwrap();
|
||||
let dec = decrypt_attachment(&enc.bytes, &key()).unwrap();
|
||||
assert_eq!(dec.as_slice(), plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_id_matches_sha256() {
|
||||
let plaintext = b"hello world";
|
||||
let enc = encrypt_attachment(plaintext, &key(), 1024).unwrap();
|
||||
assert_eq!(enc.id, AttachmentId::from_plaintext(plaintext));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversize_attachment_rejected() {
|
||||
let plaintext = vec![0u8; 11_000_000];
|
||||
let err = encrypt_attachment(&plaintext, &key(), 10 * 1024 * 1024);
|
||||
assert!(matches!(err, Err(RelicarioError::AttachmentTooLarge { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_key_fails_with_opaque_decrypt() {
|
||||
let plaintext = b"x";
|
||||
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(RelicarioError::Decrypt)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn attachment_ref_round_trip() {
|
||||
let r = AttachmentRef {
|
||||
id: AttachmentId("0123456789abcdef".into()),
|
||||
filename: "doc.pdf".into(),
|
||||
mime_type: "application/pdf".into(),
|
||||
size: 12345,
|
||||
created: 1_700_000_000,
|
||||
};
|
||||
let json = serde_json::to_string(&r).unwrap();
|
||||
let parsed: AttachmentRef = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.filename, "doc.pdf");
|
||||
assert_eq!(parsed.size, 12345);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_summary_from_ref() {
|
||||
let r = AttachmentRef {
|
||||
id: AttachmentId("aabb".into()),
|
||||
filename: "x.txt".into(),
|
||||
mime_type: "text/plain".into(),
|
||||
size: 5,
|
||||
created: 0,
|
||||
};
|
||||
let s: AttachmentSummary = (&r).into();
|
||||
assert_eq!(s.filename, "x.txt");
|
||||
assert_eq!(s.id, r.id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user