feat: add XChaCha20-Poly1305 encrypt/decrypt with binary format
This commit is contained in:
@@ -1,8 +1,64 @@
|
||||
use argon2::{Algorithm, Argon2, Params, Version};
|
||||
use chacha20poly1305::{
|
||||
aead::{Aead, KeyInit},
|
||||
XChaCha20Poly1305, XNonce,
|
||||
};
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{IdfotoError, Result};
|
||||
|
||||
const VERSION_BYTE: u8 = 0x01;
|
||||
const NONCE_LEN: usize = 24;
|
||||
const TAG_LEN: usize = 16;
|
||||
const HEADER_LEN: usize = 1 + NONCE_LEN; // version + nonce
|
||||
|
||||
pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
||||
let cipher = XChaCha20Poly1305::new(key.into());
|
||||
|
||||
let mut nonce_bytes = [0u8; NONCE_LEN];
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
let nonce = XNonce::from(nonce_bytes);
|
||||
|
||||
let ciphertext = cipher
|
||||
.encrypt(&nonce, plaintext)
|
||||
.map_err(|e| IdfotoError::Encrypt(e.to_string()))?;
|
||||
|
||||
// Output: version(1) || nonce(24) || ciphertext+tag
|
||||
let mut output = Vec::with_capacity(HEADER_LEN + ciphertext.len());
|
||||
output.push(VERSION_BYTE);
|
||||
output.extend_from_slice(&nonce_bytes);
|
||||
output.extend_from_slice(&ciphertext);
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
||||
if data.len() < HEADER_LEN + TAG_LEN {
|
||||
return Err(IdfotoError::Format(
|
||||
"data too short to be valid ciphertext".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let version = data[0];
|
||||
if version != VERSION_BYTE {
|
||||
return Err(IdfotoError::Format(format!(
|
||||
"unknown version byte: 0x{:02x}",
|
||||
version
|
||||
)));
|
||||
}
|
||||
|
||||
let nonce = XNonce::from_slice(&data[1..1 + NONCE_LEN]);
|
||||
let ciphertext = &data[HEADER_LEN..];
|
||||
|
||||
let cipher = XChaCha20Poly1305::new(key.into());
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce, ciphertext)
|
||||
.map_err(|_| IdfotoError::Decrypt)?;
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KdfParams {
|
||||
pub argon2_m: u32,
|
||||
@@ -100,4 +156,57 @@ mod tests {
|
||||
|
||||
assert_ne!(key1, key2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypt_decrypt_round_trip() {
|
||||
let key = [0xABu8; 32];
|
||||
let plaintext = b"hello, idfoto!";
|
||||
|
||||
let ciphertext = encrypt(&key, plaintext).unwrap();
|
||||
let decrypted = decrypt(&key, &ciphertext).unwrap();
|
||||
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_wrong_key_fails() {
|
||||
let key = [0xABu8; 32];
|
||||
let wrong_key = [0xCDu8; 32];
|
||||
let plaintext = b"sensitive data";
|
||||
|
||||
let ciphertext = encrypt(&key, plaintext).unwrap();
|
||||
let result = decrypt(&wrong_key, &ciphertext);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), IdfotoError::Decrypt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_tampered_data_fails() {
|
||||
let key = [0xABu8; 32];
|
||||
let plaintext = b"sensitive data";
|
||||
|
||||
let mut ciphertext = encrypt(&key, plaintext).unwrap();
|
||||
// Flip a byte in the ciphertext portion (after header)
|
||||
let flip_pos = HEADER_LEN + 2;
|
||||
ciphertext[flip_pos] ^= 0xFF;
|
||||
|
||||
let result = decrypt(&key, &ciphertext);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ciphertext_format_has_correct_structure() {
|
||||
let key = [0x11u8; 32];
|
||||
let plaintext = b"test plaintext for structure check";
|
||||
|
||||
let ciphertext = encrypt(&key, plaintext).unwrap();
|
||||
|
||||
// Expected length: 1 (version) + 24 (nonce) + plaintext_len + 16 (tag)
|
||||
let expected_len = 1 + 24 + plaintext.len() + 16;
|
||||
assert_eq!(ciphertext.len(), expected_len);
|
||||
|
||||
// Version byte must be 0x01
|
||||
assert_eq!(ciphertext[0], 0x01);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ pub mod error;
|
||||
pub use error::{IdfotoError, Result};
|
||||
|
||||
pub mod crypto;
|
||||
pub use crypto::{derive_master_key, KdfParams};
|
||||
pub use crypto::{decrypt, derive_master_key, encrypt, KdfParams};
|
||||
|
||||
Reference in New Issue
Block a user