#![cfg(feature = "encryption")]
#![forbid(unsafe_code)]
use chacha20poly1305::aead::{AeadInPlace, KeyInit};
use chacha20poly1305::{Key, Tag, XChaCha20Poly1305, XNonce};
use hkdf::Hkdf;
use sha2::Sha256;
use crate::error::{Error, Result};
pub const LOGICAL_PAGE_SIZE: usize = 4096;
pub const NONCE_SIZE: usize = 24;
pub const TAG_SIZE: usize = 16;
pub const ENCRYPTION_OVERHEAD: usize = NONCE_SIZE + TAG_SIZE;
pub const PHYSICAL_ENCRYPTED_PAGE_SIZE: usize = LOGICAL_PAGE_SIZE + ENCRYPTION_OVERHEAD;
pub const KDF_SALT_SIZE: usize = 32;
pub const KEY_SIZE: usize = 32;
pub const HKDF_INFO: &[u8] = b"obj-page-encryption-v1";
#[must_use]
pub fn derive_page_key(user_key: &[u8; KEY_SIZE], salt: &[u8; KDF_SALT_SIZE]) -> [u8; KEY_SIZE] {
let hk = Hkdf::<Sha256>::new(Some(salt), user_key);
let mut out = [0u8; KEY_SIZE];
if hk.expand(HKDF_INFO, &mut out).is_ok() {
out
} else {
debug_assert!(
false,
"derive_page_key: HKDF-Expand failed — KEY_SIZE must stay <= 255 * HashLen"
);
[0u8; KEY_SIZE]
}
}
pub fn encrypt_page(
key: &[u8; KEY_SIZE],
page_id: u64,
plaintext: &[u8; LOGICAL_PAGE_SIZE],
out: &mut [u8; PHYSICAL_ENCRYPTED_PAGE_SIZE],
) -> Result<()> {
let mut nonce_bytes = [0u8; NONCE_SIZE];
getrandom::getrandom(&mut nonce_bytes).map_err(|e| {
Error::Io(std::io::Error::other(format!("getrandom failure: {e}")))
})?;
let nonce = XNonce::from_slice(&nonce_bytes);
out[..LOGICAL_PAGE_SIZE].copy_from_slice(plaintext);
let cipher = XChaCha20Poly1305::new(Key::from_slice(key));
let ad = page_id.to_le_bytes();
let tag = cipher
.encrypt_in_place_detached(nonce, &ad, &mut out[..LOGICAL_PAGE_SIZE])
.map_err(|_| Error::EncryptionKeyInvalid)?;
out[LOGICAL_PAGE_SIZE..LOGICAL_PAGE_SIZE + NONCE_SIZE].copy_from_slice(&nonce_bytes);
out[LOGICAL_PAGE_SIZE + NONCE_SIZE..].copy_from_slice(&tag);
Ok(())
}
pub fn decrypt_page(
key: &[u8; KEY_SIZE],
page_id: u64,
ciphertext: &[u8; PHYSICAL_ENCRYPTED_PAGE_SIZE],
out: &mut [u8; LOGICAL_PAGE_SIZE],
) -> Result<()> {
let mut nonce_bytes = [0u8; NONCE_SIZE];
nonce_bytes.copy_from_slice(&ciphertext[LOGICAL_PAGE_SIZE..LOGICAL_PAGE_SIZE + NONCE_SIZE]);
let nonce = XNonce::from_slice(&nonce_bytes);
let mut tag_bytes = [0u8; TAG_SIZE];
tag_bytes.copy_from_slice(&ciphertext[LOGICAL_PAGE_SIZE + NONCE_SIZE..]);
let tag = Tag::from_slice(&tag_bytes);
out.copy_from_slice(&ciphertext[..LOGICAL_PAGE_SIZE]);
let cipher = XChaCha20Poly1305::new(Key::from_slice(key));
let ad = page_id.to_le_bytes();
cipher
.decrypt_in_place_detached(nonce, &ad, out, tag)
.map_err(|_| Error::EncryptionKeyInvalid)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
decrypt_page, derive_page_key, encrypt_page, KDF_SALT_SIZE, KEY_SIZE, LOGICAL_PAGE_SIZE,
NONCE_SIZE, PHYSICAL_ENCRYPTED_PAGE_SIZE,
};
fn test_key() -> [u8; KEY_SIZE] {
let mut k = [0u8; KEY_SIZE];
for (i, b) in k.iter_mut().enumerate() {
*b = u8::try_from(i & 0xFF).unwrap_or(0);
}
k
}
fn test_salt() -> [u8; KDF_SALT_SIZE] {
let mut s = [0u8; KDF_SALT_SIZE];
for (i, b) in s.iter_mut().enumerate() {
*b = u8::try_from((i ^ 0xA5) & 0xFF).unwrap_or(0);
}
s
}
fn test_plaintext() -> [u8; LOGICAL_PAGE_SIZE] {
let mut p = [0u8; LOGICAL_PAGE_SIZE];
for (i, b) in p.iter_mut().enumerate() {
*b = u8::try_from(i & 0xFF).unwrap_or(0);
}
p
}
#[test]
fn derive_page_key_is_deterministic() {
let k = test_key();
let s = test_salt();
assert_eq!(derive_page_key(&k, &s), derive_page_key(&k, &s));
}
#[test]
fn derive_page_key_changes_with_salt() {
let k = test_key();
let s1 = test_salt();
let mut s2 = s1;
s2[0] ^= 0xFF;
assert_ne!(derive_page_key(&k, &s1), derive_page_key(&k, &s2));
}
#[test]
fn derive_page_key_changes_with_user_key() {
let s = test_salt();
let k1 = test_key();
let mut k2 = k1;
k2[0] ^= 0x55;
assert_ne!(derive_page_key(&k1, &s), derive_page_key(&k2, &s));
}
#[test]
fn round_trip_round_trips() {
let key = derive_page_key(&test_key(), &test_salt());
let pt = test_plaintext();
let mut ct = [0u8; PHYSICAL_ENCRYPTED_PAGE_SIZE];
encrypt_page(&key, 7, &pt, &mut ct).expect("encrypt");
let mut decrypted = [0u8; LOGICAL_PAGE_SIZE];
decrypt_page(&key, 7, &ct, &mut decrypted).expect("decrypt");
assert_eq!(decrypted, pt);
}
#[test]
fn wrong_key_fails_decryption() {
let key = derive_page_key(&test_key(), &test_salt());
let mut wrong_user = test_key();
wrong_user[0] ^= 0x42;
let wrong_key = derive_page_key(&wrong_user, &test_salt());
let pt = test_plaintext();
let mut ct = [0u8; PHYSICAL_ENCRYPTED_PAGE_SIZE];
encrypt_page(&key, 7, &pt, &mut ct).expect("encrypt");
let mut decrypted = [0u8; LOGICAL_PAGE_SIZE];
let err =
decrypt_page(&wrong_key, 7, &ct, &mut decrypted).expect_err("wrong key must fail");
assert!(matches!(err, crate::error::Error::EncryptionKeyInvalid));
}
#[test]
fn bit_flip_in_ciphertext_fails_poly1305() {
let key = derive_page_key(&test_key(), &test_salt());
let pt = test_plaintext();
let mut ct = [0u8; PHYSICAL_ENCRYPTED_PAGE_SIZE];
encrypt_page(&key, 11, &pt, &mut ct).expect("encrypt");
ct[100] ^= 0x01;
let mut decrypted = [0u8; LOGICAL_PAGE_SIZE];
let err = decrypt_page(&key, 11, &ct, &mut decrypted).expect_err("bit flip must fail");
assert!(matches!(err, crate::error::Error::EncryptionKeyInvalid));
}
#[test]
fn bit_flip_in_nonce_fails_poly1305() {
let key = derive_page_key(&test_key(), &test_salt());
let pt = test_plaintext();
let mut ct = [0u8; PHYSICAL_ENCRYPTED_PAGE_SIZE];
encrypt_page(&key, 3, &pt, &mut ct).expect("encrypt");
ct[LOGICAL_PAGE_SIZE] ^= 0x40;
let mut decrypted = [0u8; LOGICAL_PAGE_SIZE];
let err = decrypt_page(&key, 3, &ct, &mut decrypted).expect_err("nonce flip must fail");
assert!(matches!(err, crate::error::Error::EncryptionKeyInvalid));
}
#[test]
fn bit_flip_in_tag_fails_poly1305() {
let key = derive_page_key(&test_key(), &test_salt());
let pt = test_plaintext();
let mut ct = [0u8; PHYSICAL_ENCRYPTED_PAGE_SIZE];
encrypt_page(&key, 3, &pt, &mut ct).expect("encrypt");
ct[PHYSICAL_ENCRYPTED_PAGE_SIZE - 1] ^= 0x80;
let mut decrypted = [0u8; LOGICAL_PAGE_SIZE];
let err = decrypt_page(&key, 3, &ct, &mut decrypted).expect_err("tag flip must fail");
assert!(matches!(err, crate::error::Error::EncryptionKeyInvalid));
}
#[test]
fn wrong_page_id_fails_decryption() {
let key = derive_page_key(&test_key(), &test_salt());
let pt = test_plaintext();
let mut ct = [0u8; PHYSICAL_ENCRYPTED_PAGE_SIZE];
encrypt_page(&key, 42, &pt, &mut ct).expect("encrypt");
let mut decrypted = [0u8; LOGICAL_PAGE_SIZE];
let err =
decrypt_page(&key, 43, &ct, &mut decrypted).expect_err("swapped page-id must fail");
assert!(matches!(err, crate::error::Error::EncryptionKeyInvalid));
}
#[test]
fn fresh_nonce_per_encryption() {
let key = derive_page_key(&test_key(), &test_salt());
let pt = test_plaintext();
let mut ct1 = [0u8; PHYSICAL_ENCRYPTED_PAGE_SIZE];
let mut ct2 = [0u8; PHYSICAL_ENCRYPTED_PAGE_SIZE];
encrypt_page(&key, 1, &pt, &mut ct1).expect("encrypt 1");
encrypt_page(&key, 1, &pt, &mut ct2).expect("encrypt 2");
assert_ne!(ct1[..LOGICAL_PAGE_SIZE], ct2[..LOGICAL_PAGE_SIZE]);
assert_ne!(
ct1[LOGICAL_PAGE_SIZE..LOGICAL_PAGE_SIZE + NONCE_SIZE],
ct2[LOGICAL_PAGE_SIZE..LOGICAL_PAGE_SIZE + NONCE_SIZE]
);
}
}