use chacha20poly1305::{ AeadCore as _, ChaCha20Poly1305, KeyInit as _, aead::{Aead as _, OsRng, Payload, rand_core::RngCore as _}, }; use hmac::{Hmac, Mac}; use sha2::Sha256; use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::Error; const NONCE_BYTES: usize = 12; /// Identifies a chunk by a keyed hash (HMAC-SHA256) of its plaintext. /// /// Using a keyed hash rather than a plain digest means identical content /// deduplicates within a repository, while someone holding the repository /// cannot test whether it contains a known plaintext. #[derive(Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct ChunkId([u8; 32]); impl ChunkId { pub(crate) fn compute(id_key: &[u8; 32], plaintext: &[u8]) -> ChunkId { let mut mac = as Mac>::new_from_slice(id_key) .expect("HMAC-SHA256 accepts keys of any length"); mac.update(plaintext); ChunkId(mac.finalize().into_bytes().into()) } pub(crate) fn as_bytes(&self) -> &[u8; 32] { &self.0 } pub(crate) fn from_bytes(bytes: [u8; 32]) -> ChunkId { ChunkId(bytes) } pub fn to_hex(&self) -> String { hex::encode(self.0) } pub fn from_hex(text: impl AsRef) -> Result { let text = text.as_ref(); let mut bytes = [0u8; 32]; hex::decode_to_slice(text, &mut bytes) .map_err(|_| Error::InvalidObjectName(text.to_string()))?; Ok(ChunkId(bytes)) } } impl std::fmt::Debug for ChunkId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "ChunkId({})", self.to_hex()) } } impl std::fmt::Display for ChunkId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.to_hex()) } } /// The secrets protecting a repository. /// /// Nothing here is ever stored, anywhere: the keys are re-derived /// idempotently from the passphrase and the repository's public /// [`Header`] whenever the repository is opened. Consequently the /// passphrase *is* the key material — it cannot be changed without /// re-encrypting everything, and losing it loses the repository. #[derive(Zeroize, ZeroizeOnDrop)] pub(crate) struct MasterKeys { /// ChaCha20-Poly1305 key sealing every object except the header. encryption: [u8; 32], /// HMAC key from which chunk ids are computed. id: [u8; 32], /// Key for the header's passphrase verifier. verify: [u8; 32], } impl MasterKeys { /// Derives the key set from a passphrase: Argon2id over the /// repository's public salt yields a master secret, and independent /// subkeys are split from it by domain-separated HMAC. pub(crate) fn derive(password: &str, header: &Header) -> Result { let argon2 = argon2::Argon2::new( argon2::Algorithm::Argon2id, argon2::Version::V0x13, argon2::Params::new( header.argon2_memory_kib, header.argon2_iterations, header.argon2_parallelism, Some(32), ) .map_err(|_| Error::InvalidHeader)?, ); let mut master = [0u8; 32]; argon2 .hash_password_into(password.as_bytes(), &header.salt, &mut master) .map_err(|_| Error::InvalidHeader)?; let keys = MasterKeys { encryption: keyed_digest(&master, &[b"beeping encryption key".as_slice()]), id: keyed_digest(&master, &[b"beeping chunk id key".as_slice()]), verify: keyed_digest(&master, &[b"beeping verify key".as_slice()]), }; master.zeroize(); Ok(keys) } /// The public check value proving a later derivation used the same /// passphrase *and* that the header it was derived against is the /// one the repository was created with. Committing every header /// field means a storage-level attacker cannot silently swap in /// weaker KDF parameters or different chunker parameters (the /// latter would quietly destroy deduplication): any tampering makes /// the verifier mismatch, indistinguishable from a wrong passphrase /// and equally fatal to the attempt. /// /// Knowing the verifier helps an attacker no more than any /// ciphertext object does: either way they must brute-force the /// passphrase through Argon2. pub(crate) fn verifier(&self, header: &Header) -> [u8; 32] { keyed_digest( &self.verify, &[ b"beeping passphrase verifier".as_slice(), &header.version.to_be_bytes(), &header.salt, &header.argon2_memory_kib.to_be_bytes(), &header.argon2_iterations.to_be_bytes(), &header.argon2_parallelism.to_be_bytes(), &header.config.chunk_min.to_be_bytes(), &header.config.chunk_avg.to_be_bytes(), &header.config.chunk_max.to_be_bytes(), ], ) } pub(crate) fn id_key(&self) -> &[u8; 32] { &self.id } } /// A keyed digest over several byte strings, used for deriving /// deterministic identifiers. Each part's length is mixed in, so distinct /// part lists never collide by concatenation. pub(crate) fn keyed_digest(key: &[u8; 32], parts: &[&[u8]]) -> [u8; 32] { let mut mac = as Mac>::new_from_slice(key).expect("HMAC-SHA256 accepts keys of any length"); for part in parts { mac.update(&(part.len() as u64).to_be_bytes()); mac.update(part); } mac.finalize().into_bytes().into() } /// Parameters that must stay constant for the life of a repository so /// that identical content keeps producing identical chunks. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub(crate) struct RepoConfig { pub chunk_min: u32, pub chunk_avg: u32, pub chunk_max: u32, } impl Default for RepoConfig { fn default() -> Self { RepoConfig { chunk_min: 256 * 1024, chunk_avg: 1024 * 1024, chunk_max: 4 * 1024 * 1024, } } } /// The repository's public, write-once `header` object. /// /// It deliberately contains no secret material — only what a client /// needs to re-derive the keys from the passphrase (salt and KDF /// parameters), detect a wrong passphrase (the verifier), and chunk /// content compatibly with every other client of the repository. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub(crate) struct Header { pub version: u16, pub salt: [u8; 16], pub argon2_memory_kib: u32, pub argon2_iterations: u32, pub argon2_parallelism: u32, pub verifier: [u8; 32], pub config: RepoConfig, } pub(crate) const HEADER_VERSION: u16 = 1; impl Header { /// A fresh header with a random salt and current cost parameters. /// The verifier starts zeroed and is filled in after the first key /// derivation. pub(crate) fn generate() -> Header { let mut salt = [0u8; 16]; OsRng.fill_bytes(&mut salt); let defaults = argon2::Params::DEFAULT; Header { version: HEADER_VERSION, salt, argon2_memory_kib: defaults.m_cost(), argon2_iterations: defaults.t_cost(), argon2_parallelism: defaults.p_cost(), verifier: [0u8; 32], config: RepoConfig::default(), } } } /// Encrypts `plaintext` with the repository's master key, producing /// `nonce || ciphertext`. The AAD binds the result to the object it names, /// so an object substituted for another fails to open. pub(crate) fn seal(keys: &MasterKeys, aad: &[u8], plaintext: &[u8]) -> Result, Error> { let cipher = ChaCha20Poly1305::new((&keys.encryption).into()); let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng); let ciphertext = cipher .encrypt( &nonce, Payload { msg: plaintext, aad, }, ) .map_err(|_| Error::Crypto)?; let mut sealed = Vec::with_capacity(NONCE_BYTES + ciphertext.len()); sealed.extend_from_slice(&nonce); sealed.extend_from_slice(&ciphertext); Ok(sealed) } /// Reverses [`seal`], authenticating the data and its AAD in the process. pub(crate) fn open(keys: &MasterKeys, aad: &[u8], sealed: &[u8]) -> Result, Error> { if sealed.len() < NONCE_BYTES { return Err(Error::Crypto); } let (nonce, ciphertext) = sealed.split_at(NONCE_BYTES); let cipher = ChaCha20Poly1305::new((&keys.encryption).into()); cipher .decrypt( nonce.into(), Payload { msg: ciphertext, aad, }, ) .map_err(|_| Error::Crypto) } /// Defines a 16-byte object identifier: a newtype over the bytes with /// hex parsing, display, and the derives every one of them needs. /// /// Snapshots, prune records, and locks are all named this way, and the /// distinct types keep one from being passed where another belongs. macro_rules! object_id { ($name:ident, $doc:expr) => { #[doc = $doc] #[derive(Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct $name([u8; 16]); impl $name { pub(crate) fn from_bytes(bytes: [u8; 16]) -> $name { $name(bytes) } pub fn to_hex(&self) -> String { hex::encode(self.0) } pub fn from_hex(text: impl AsRef) -> Result<$name, crate::Error> { let text = text.as_ref(); let mut bytes = [0u8; 16]; hex::decode_to_slice(text, &mut bytes) .map_err(|_| crate::Error::InvalidObjectName(text.to_string()))?; Ok($name(bytes)) } pub(crate) fn as_bytes(&self) -> &[u8; 16] { &self.0 } } impl std::fmt::Debug for $name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}({})", stringify!($name), self.to_hex()) } } impl std::fmt::Display for $name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.to_hex()) } } }; } /// Gives an id type a source of fresh, unpredictable values. /// /// Only for the objects named by nothing but themselves. A snapshot's id /// is derived from what it contains instead, so that a backup redone /// after an interruption converges on one object rather than piling up /// duplicates — which is why this is not part of [`object_id`]. macro_rules! random_id { ($name:ident) => { impl $name { pub(crate) fn generate() -> $name { let mut bytes = [0u8; 16]; OsRng.fill_bytes(&mut bytes); $name::from_bytes(bytes) } } }; } object_id!( SnapshotId, "Identifies a snapshot record within a repository." ); object_id!( PruneId, "Identifies the record one prune leaves behind. Their ids, taken \ together, are what tells a local cache whether anything has been \ removed from the repository since it was written." ); object_id!( LockId, "Identifies one running backup's lock. A lock means the run has \ references that no snapshot names yet, so pruning would sweep them." ); random_id!(PruneId); random_id!(LockId);