feat(core): add encrypt_attachment + decrypt_attachment
AttachmentId is derived from sha256(plaintext) so identical content deduplicates naturally. Size cap enforced at encrypt time, returning IdfotoError::AttachmentTooLarge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,96 @@ impl From<&AttachmentRef> for AttachmentSummary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
use crate::crypto::{decrypt, encrypt};
|
||||||
|
use crate::error::{IdfotoError, Result};
|
||||||
|
|
||||||
|
/// Encrypted attachment with the AID derived from plaintext content.
|
||||||
|
pub struct EncryptedAttachment {
|
||||||
|
pub id: AttachmentId,
|
||||||
|
pub bytes: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypt raw attachment bytes, deriving the [`AttachmentId`] from `sha256(plaintext)`.
|
||||||
|
///
|
||||||
|
/// Returns [`IdfotoError::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(IdfotoError::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(IdfotoError::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(IdfotoError::Decrypt)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ pub mod item;
|
|||||||
pub use item::{Field, FieldHistoryEntry, FieldKind, FieldValue, Item, Section};
|
pub use item::{Field, FieldHistoryEntry, FieldKind, FieldValue, Item, Section};
|
||||||
|
|
||||||
pub mod attachment;
|
pub mod attachment;
|
||||||
pub use attachment::{AttachmentRef, AttachmentSummary};
|
pub use attachment::{decrypt_attachment, encrypt_attachment, AttachmentRef, AttachmentSummary, EncryptedAttachment};
|
||||||
|
|
||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
pub use manifest::{Manifest, ManifestEntry, MANIFEST_SCHEMA_VERSION};
|
pub use manifest::{Manifest, ManifestEntry, MANIFEST_SCHEMA_VERSION};
|
||||||
|
|||||||
Reference in New Issue
Block a user