diff --git a/Cargo.lock b/Cargo.lock index 07ac4f273..3a1957d3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,41 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "aead" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c192eb8f11fc081b0fe4259ba5af04217d4e0faddd02417310a927911abd7c8" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe0133578c0986e1fe3dfcd4af1cc5b2dd6c3dbf534d69916ce16a2701d40ba" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e1366e0c69c9f927b1fa5ce2c7bf9eafc8f9268c0b9800729e8b267612447c" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.6" @@ -88,6 +123,17 @@ version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +[[package]] +name = "argon2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + [[package]] name = "ascii" version = "0.9.3" @@ -465,6 +511,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "blake2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +dependencies = [ + "digest 0.10.5", +] + [[package]] name = "block" version = "0.1.6" @@ -662,6 +717,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fc89c7c5b9e7a02dfe45cd2367bae382f9ed31c61ca8debe5f827c420a2f08" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.22" @@ -678,6 +757,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clang-sys" version = "1.4.0" @@ -949,6 +1039,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -989,6 +1080,15 @@ dependencies = [ "syn", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "ctrlc" version = "3.2.3" @@ -1750,6 +1850,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gif" version = "0.11.4" @@ -2355,6 +2465,15 @@ dependencies = [ "adler32", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -3578,6 +3697,17 @@ dependencies = [ "schema-ast", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.9" @@ -3853,6 +3983,29 @@ dependencies = [ "winapi", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -4927,14 +5080,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] -name = "sct" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +name = "sd-crypto" +version = "0.0.0" dependencies = [ - "ring 0.16.20", - "untrusted", -] + "aead", + "aes-gcm", + "argon2", + "chacha20poly1305", + "rand 0.8.5", + "rand_chacha 0.3.1", + "thiserror", + "zeroize", + [[package]] name = "sd-core" @@ -6576,6 +6733,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unreachable" version = "1.0.0" diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml new file mode 100644 index 000000000..e4719d58a --- /dev/null +++ b/crates/crypto/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "sd-crypto" +version = "0.0.0" +authors = ["Jake "] +readme = "README.md" +description = "A library to handle cryptographic functions within Spacedrive" +edition = "2021" +rust-version = "1.64.0" + +[dependencies] +# rng +rand = "0.8.5" +rand_chacha = "0.3.1" + +# password hashing +argon2 = "0.4.1" + +# aeads +aes-gcm = "0.10.1" +chacha20poly1305 = "0.10.1" +aead = { version = "0.5.1", features = ["stream"] } + +# cryptographic hygiene +zeroize = "1.5.7" + +# error handling +thiserror = "1.0.37" \ No newline at end of file diff --git a/crates/crypto/src/error.rs b/crates/crypto/src/error.rs new file mode 100644 index 000000000..2553376df --- /dev/null +++ b/crates/crypto/src/error.rs @@ -0,0 +1,30 @@ +use thiserror::Error; + +/// This enum defines all possible errors that this crate can give +#[derive(Error, Debug)] +pub enum Error { + #[error("not enough bytes were written to the output file")] + WriteMismatch, + #[error("there was an error hashing the password")] + PasswordHash, + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + #[error("error while encrypting")] + Encrypt, + #[error("error while decrypting")] + Decrypt, + #[error("nonce length mismatch")] + NonceLengthMismatch, + #[error("invalid file header")] + FileHeader, + #[error("error initialising stream encryption/decryption")] + StreamModeInit, + #[error("error initialising in-memory encryption/decryption")] + MemoryModeInit, + #[error("wrong password provided")] + IncorrectPassword, + #[error("no keyslots available")] + NoKeyslots, + #[error("mismatched data length while converting vec to array")] + VecArrSizeMismatch, +} diff --git a/crates/crypto/src/header/container.rs b/crates/crypto/src/header/container.rs new file mode 100644 index 000000000..18ce80bb0 --- /dev/null +++ b/crates/crypto/src/header/container.rs @@ -0,0 +1 @@ +/// This is a placeholder file \ No newline at end of file diff --git a/crates/crypto/src/header/file.rs b/crates/crypto/src/header/file.rs new file mode 100644 index 000000000..f104d4cd5 --- /dev/null +++ b/crates/crypto/src/header/file.rs @@ -0,0 +1,220 @@ +use std::io::{Read, Seek, Write}; + +use zeroize::Zeroize; + +use crate::{ + error::Error, + objects::memory::MemoryDecryption, + primitives::{Algorithm, Mode, MASTER_KEY_LEN}, + protected::Protected, +}; + +use super::keyslot::Keyslot; + +/// These are used to quickly and easily identify Spacedrive-encrypted files +/// Random values - can be changed (up until 0.1.0) +pub const MAGIC_BYTES: [u8; 7] = [0x62, 0x61, 0x6C, 0x6C, 0x61, 0x70, 0x70]; + +// Everything contained within this header can be flaunted around with minimal security risk +// The only way this could compromise any data is if a weak password/key was used +// Even then, `argon2id` helps alleiviate this somewhat (brute-forcing it is incredibly tough) +// We also use high memory parameters in order to hinder attacks with ASICs +// There should be no more than two keyslots in this header type +pub struct FileHeader { + pub version: FileHeaderVersion, + pub algorithm: Algorithm, + pub mode: Mode, + pub nonce: Vec, + pub keyslots: Vec, +} + +/// This defines the main file header version +pub enum FileHeaderVersion { + V1, +} + +impl FileHeader { + #[must_use] + pub fn new( + version: FileHeaderVersion, + algorithm: Algorithm, + nonce: Vec, + keyslots: Vec, + ) -> Self { + Self { + version, + algorithm, + mode: Mode::Stream, + nonce, + keyslots, + } + } + + /// This is a helper function to decrypt a master key from a set of keyslots + /// It's easier to call this on the header for now - but this may be changed in the future + /// You receive an error if the password doesn't match + #[allow(clippy::needless_pass_by_value)] + pub fn decrypt_master_key( + &self, + password: Protected>, + ) -> Result, Error> { + let mut master_key = [0u8; MASTER_KEY_LEN]; + + for keyslot in &self.keyslots { + let key = keyslot + .hashing_algorithm + .hash(password.clone(), keyslot.salt) + .map_err(|_| Error::PasswordHash)?; + + let decryptor = + MemoryDecryption::new(key, keyslot.algorithm).map_err(|_| Error::MemoryModeInit)?; + if let Ok(mut decrypted_master_key) = + decryptor.decrypt(keyslot.master_key.as_ref(), &keyslot.nonce) + { + master_key.copy_from_slice(&decrypted_master_key); + decrypted_master_key.zeroize(); + } + } + + if master_key == [0u8; MASTER_KEY_LEN] { + Err(Error::IncorrectPassword) + } else { + Ok(Protected::new(master_key)) + } + } + + pub fn write(&self, writer: &mut W) -> Result<(), Error> + where + W: Write + Seek, + { + writer.write(&self.serialize()).map_err(Error::Io)?; + Ok(()) + } + + #[must_use] + pub fn generate_aad(&self) -> Vec { + match self.version { + FileHeaderVersion::V1 => { + let mut aad: Vec = Vec::new(); + aad.extend_from_slice(&MAGIC_BYTES); // 6 + aad.extend_from_slice(&self.version.serialize()); // 8 + aad.extend_from_slice(&self.algorithm.serialize()); // 10 + aad.extend_from_slice(&self.mode.serialize()); // 12 + aad.extend_from_slice(&self.nonce); // 20 OR 32 + aad.extend_from_slice(&vec![0u8; 24 - self.nonce.len()]); // padded until 36 bytes + aad + } + } + } + + #[must_use] + pub fn serialize(&self) -> Vec { + match self.version { + FileHeaderVersion::V1 => { + let mut header: Vec = Vec::new(); + header.extend_from_slice(&MAGIC_BYTES); // 6 + header.extend_from_slice(&self.version.serialize()); // 8 + header.extend_from_slice(&self.algorithm.serialize()); // 10 + header.extend_from_slice(&self.mode.serialize()); // 12 + header.extend_from_slice(&self.nonce); // 20 OR 32 + header.extend_from_slice(&vec![0u8; 24 - self.nonce.len()]); // padded until 36 bytes + + for keyslot in &self.keyslots { + header.extend_from_slice(&keyslot.serialize()); + } + + for _ in 0..(2 - self.keyslots.len()) { + header.extend_from_slice(&[0u8; 96]); + } + + header + } + } + } + + // This includes the magic bytes at the start of the file + #[must_use] + pub const fn length(&self) -> usize { + match self.version { + FileHeaderVersion::V1 => 222 + MAGIC_BYTES.len(), + } + } + + // This includes the magic bytes at the start of the file + #[must_use] + pub const fn aad_length(&self) -> usize { + match self.version { + FileHeaderVersion::V1 => 30 + MAGIC_BYTES.len(), + } + } + + // The AAD retrieval here could be optimised - we do rewind a couple of times + /// This deserializes a header directly from a reader, and leaves the reader at the start of the encrypted data + /// It returns both the header, and the AAD that should be used for decryption + pub fn deserialize(reader: &mut R) -> Result<(Self, Vec), Error> + where + R: Read + Seek, + { + let mut magic_bytes = [0u8; MAGIC_BYTES.len()]; + reader.read(&mut magic_bytes).map_err(Error::Io)?; + + if magic_bytes != MAGIC_BYTES { + return Err(Error::FileHeader); + } + + let mut version = [0u8; 2]; + + reader.read(&mut version).map_err(Error::Io)?; + let version = FileHeaderVersion::deserialize(version)?; + + let header = match version { + FileHeaderVersion::V1 => { + let mut algorithm = [0u8; 2]; + reader.read(&mut algorithm).map_err(Error::Io)?; + let algorithm = Algorithm::deserialize(algorithm)?; + + let mut mode = [0u8; 2]; + reader.read(&mut mode).map_err(Error::Io)?; + let mode = Mode::deserialize(mode)?; + + let mut nonce = vec![0u8; algorithm.nonce_len(mode)]; + reader.read(&mut nonce).map_err(Error::Io)?; + + // read and discard the padding + reader + .read(&mut vec![0u8; 24 - nonce.len()]) + .map_err(Error::Io)?; + + let mut keyslots: Vec = Vec::new(); + + for _ in 0..2 { + if let Ok(keyslot) = Keyslot::deserialize(reader) { + keyslots.push(keyslot); + } + } + + Self { + version, + algorithm, + mode, + nonce, + keyslots, + } + } + }; + + // Rewind so we can get the AAD + reader.rewind().map_err(Error::Io)?; + + let mut aad = vec![0u8; header.aad_length()]; + reader.read(&mut aad).map_err(Error::Io)?; + + // We return the cursor position to the end of the header, + // So that the encrypted data can be read directly afterwards + reader + .seek(std::io::SeekFrom::Start(header.length() as u64)) + .map_err(Error::Io)?; + + Ok((header, aad)) + } +} diff --git a/crates/crypto/src/header/keyslot.rs b/crates/crypto/src/header/keyslot.rs new file mode 100644 index 000000000..cbd7ecba9 --- /dev/null +++ b/crates/crypto/src/header/keyslot.rs @@ -0,0 +1,117 @@ +use std::io::{Read, Seek}; + +use crate::{ + error::Error, + primitives::{Algorithm, HashingAlgorithm, Mode, ENCRYPTED_MASTER_KEY_LEN, SALT_LEN}, +}; + +/// A keyslot. 96 bytes, and contains all the information for future-proofing while keeping the size reasonable +/// +/// The mode was added so others can see that master keys are encrypted differently from data +/// +/// The algorithm (should) be inherited from the parent header, but that's not a guarantee +pub struct Keyslot { + pub version: KeyslotVersion, + pub algorithm: Algorithm, // encryption algorithm + pub hashing_algorithm: HashingAlgorithm, // password hashing algorithm + pub mode: Mode, + pub salt: [u8; SALT_LEN], + pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], // this is encrypted so we can store it + pub nonce: Vec, +} + +/// This defines the keyslot version +pub enum KeyslotVersion { + V1, +} + +impl Keyslot { + #[must_use] + pub fn new( + version: KeyslotVersion, + algorithm: Algorithm, + hashing_algorithm: HashingAlgorithm, + salt: [u8; SALT_LEN], + encrypted_master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], + nonce: Vec, + ) -> Self { + Self { + version, + algorithm, + hashing_algorithm, + mode: Mode::Memory, + salt, + master_key: encrypted_master_key, + nonce, + } + } + /// This function is used to serialize a keyslot into bytes + #[must_use] + pub fn serialize(&self) -> Vec { + match self.version { + KeyslotVersion::V1 => { + let mut keyslot: Vec = Vec::new(); + keyslot.extend_from_slice(&self.version.serialize()); // 2 + keyslot.extend_from_slice(&self.algorithm.serialize()); // 4 + keyslot.extend_from_slice(&self.hashing_algorithm.serialize()); // 6 + keyslot.extend_from_slice(&self.mode.serialize()); // 8 + keyslot.extend_from_slice(&self.salt); // 24 + keyslot.extend_from_slice(&self.master_key); // 72 + keyslot.extend_from_slice(&self.nonce); // 82 OR 94 + keyslot.extend_from_slice(&vec![0u8; 24 - self.nonce.len()]); // 96 total bytes + keyslot + } + } + } + + /// This function reads a keyslot from a reader, and attempts to serialize a keyslot + pub fn deserialize(reader: &mut R) -> Result + where + R: Read + Seek, + { + let mut version = [0u8; 2]; + reader.read(&mut version).map_err(Error::Io)?; + let version = KeyslotVersion::deserialize(version)?; + + match version { + KeyslotVersion::V1 => { + let mut algorithm = [0u8; 2]; + reader.read(&mut algorithm).map_err(Error::Io)?; + let algorithm = Algorithm::deserialize(algorithm)?; + + let mut hashing_algorithm = [0u8; 2]; + reader.read(&mut hashing_algorithm).map_err(Error::Io)?; + let hashing_algorithm = HashingAlgorithm::deserialize(hashing_algorithm)?; + + let mut mode = [0u8; 2]; + reader.read(&mut mode).map_err(Error::Io)?; + let mode = Mode::deserialize(mode)?; + + let mut salt = [0u8; SALT_LEN]; + reader.read(&mut salt).map_err(Error::Io)?; + + let mut master_key = [0u8; ENCRYPTED_MASTER_KEY_LEN]; + reader.read(&mut master_key).map_err(Error::Io)?; + + let mut nonce = vec![0u8; algorithm.nonce_len(mode)]; + reader.read(&mut nonce).map_err(Error::Io)?; + + reader + .read(&mut vec![0u8; 26 - nonce.len()]) + .map_err(Error::Io)?; + + let keyslot = Self { + version, + algorithm, + hashing_algorithm, + mode, + salt, + master_key, + nonce, + }; + + Ok(keyslot) + } + } + } +} diff --git a/crates/crypto/src/header/mod.rs b/crates/crypto/src/header/mod.rs new file mode 100644 index 000000000..f4fe3e77e --- /dev/null +++ b/crates/crypto/src/header/mod.rs @@ -0,0 +1,6 @@ +//! This module will contain all encrypted header related functions, information, etc. +//! It'll handle serialisation, deserialisation, AAD, keyslots and everything else + +pub mod file; +pub mod keyslot; +pub mod serialization; diff --git a/crates/crypto/src/header/serialization.rs b/crates/crypto/src/header/serialization.rs new file mode 100644 index 000000000..abd1bc861 --- /dev/null +++ b/crates/crypto/src/header/serialization.rs @@ -0,0 +1,101 @@ +//! This module defines all of the serialization and deserialization rules for the headers +//! +//! It contains byte -> enum and enum -> byte conversions for everything that could be written to a header (except headers and keyslots themselves) + +use crate::{ + error::Error, + keys::hashing::Params, + primitives::{Algorithm, HashingAlgorithm, Mode}, +}; + +use super::{file::FileHeaderVersion, keyslot::KeyslotVersion}; + +impl FileHeaderVersion { + #[must_use] + pub const fn serialize(&self) -> [u8; 2] { + match self { + Self::V1 => [0x0A, 0x01], + } + } + + pub const fn deserialize(bytes: [u8; 2]) -> Result { + match bytes { + [0x0A, 0x01] => Ok(Self::V1), + _ => Err(Error::FileHeader), + } + } +} + +impl KeyslotVersion { + #[must_use] + pub const fn serialize(&self) -> [u8; 2] { + match self { + Self::V1 => [0x0D, 0x01], + } + } + + pub const fn deserialize(bytes: [u8; 2]) -> Result { + match bytes { + [0x0D, 0x01] => Ok(Self::V1), + _ => Err(Error::FileHeader), + } + } +} + +impl HashingAlgorithm { + #[must_use] + pub const fn serialize(&self) -> [u8; 2] { + match self { + Self::Argon2id(p) => match p { + Params::Standard => [0x0F, 0x01], + Params::Hardened => [0x0F, 0x02], + Params::Paranoid => [0x0F, 0x03], + }, + } + } + + pub const fn deserialize(bytes: [u8; 2]) -> Result { + match bytes { + [0x0F, 0x01] => Ok(Self::Argon2id(Params::Standard)), + [0x0F, 0x02] => Ok(Self::Argon2id(Params::Hardened)), + [0x0F, 0x03] => Ok(Self::Argon2id(Params::Paranoid)), + _ => Err(Error::FileHeader), + } + } +} + +impl Algorithm { + #[must_use] + pub const fn serialize(&self) -> [u8; 2] { + match self { + Self::XChaCha20Poly1305 => [0x0B, 0x01], + Self::Aes256Gcm => [0x0B, 0x02], + } + } + + pub const fn deserialize(bytes: [u8; 2]) -> Result { + match bytes { + [0x0B, 0x01] => Ok(Self::XChaCha20Poly1305), + [0x0B, 0x02] => Ok(Self::Aes256Gcm), + _ => Err(Error::FileHeader), + } + } +} + +impl Mode { + #[must_use] + pub const fn serialize(&self) -> [u8; 2] { + match self { + Self::Stream => [0x0C, 0x01], + Self::Memory => [0x0C, 0x02], + } + } + + pub const fn deserialize(bytes: [u8; 2]) -> Result { + match bytes { + [0x0C, 0x01] => Ok(Self::Stream), + [0x0C, 0x02] => Ok(Self::Memory), + _ => Err(Error::FileHeader), + } + } +} diff --git a/crates/crypto/src/keys/hashing.rs b/crates/crypto/src/keys/hashing.rs new file mode 100644 index 000000000..9fd05852f --- /dev/null +++ b/crates/crypto/src/keys/hashing.rs @@ -0,0 +1,65 @@ +use crate::protected::Protected; +use crate::{error::Error, primitives::SALT_LEN}; +use argon2::Argon2; + +// These names are not final +// I'm considering adding an `(i32)` to each, to allow specific versioning of each parameter version +// These will be serializable/deserializable with regards to the header/storage of this information +#[derive(Clone, Copy)] +pub enum Params { + Standard, + Hardened, + Paranoid, +} + +impl Params { + #[must_use] + pub fn get_argon2_params(&self) -> argon2::Params { + match self { + // We can use `.unwrap()` here as the values are hardcoded, and this shouldn't error + // The values are NOT final, as we need to find a good average. + // It's very hardware dependant but we should aim for at least 16MB of RAM usage on standard + // Provided they all take one (ish) second or longer, and less than 3/4 seconds (for paranoid), they will be fine + // It's not so much the parameters themselves that matter, it's the duration (and ensuring that they use enough RAM to hinder ASIC brute-force attacks) + Self::Standard => { + argon2::Params::new(131_072, 8, 4, Some(argon2::Params::DEFAULT_OUTPUT_LEN)) + .unwrap() + } + Self::Paranoid => { + argon2::Params::new(262_144, 8, 4, Some(argon2::Params::DEFAULT_OUTPUT_LEN)) + .unwrap() + } + Self::Hardened => { + argon2::Params::new(524_288, 8, 4, Some(argon2::Params::DEFAULT_OUTPUT_LEN)) + .unwrap() + } + } + } +} + +// Shouldn't be called directly - call it on the `HashingAlgorithm` struct +/// This function should NOT be called directly! +/// +/// Call it via the `HashingAlgorithm` struct (e.g. `HashingAlgorithm::Argon2id(Params::Standard).hash()`) +#[allow(clippy::needless_pass_by_value)] +pub fn password_hash_argon2id( + password: Protected>, + salt: [u8; SALT_LEN], + params: Params, +) -> Result, Error> { + let mut key = [0u8; 32]; + + let argon2 = Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + params.get_argon2_params(), + ); + + let result = argon2.hash_password_into(password.expose(), &salt, &mut key); + + if result.is_ok() { + Ok(Protected::new(key)) + } else { + Err(Error::PasswordHash) + } +} diff --git a/crates/crypto/src/keys/mod.rs b/crates/crypto/src/keys/mod.rs new file mode 100644 index 000000000..62c68590b --- /dev/null +++ b/crates/crypto/src/keys/mod.rs @@ -0,0 +1 @@ +pub mod hashing; diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs new file mode 100644 index 000000000..56ca2da09 --- /dev/null +++ b/crates/crypto/src/lib.rs @@ -0,0 +1,23 @@ +#![forbid(unsafe_code)] +#![warn(clippy::pedantic)] +#![warn(clippy::correctness)] +#![warn(clippy::perf)] +#![warn(clippy::style)] +#![warn(clippy::suspicious)] +#![warn(clippy::nursery)] +#![warn(clippy::correctness)] +#![allow(clippy::missing_panics_doc)] +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::similar_names)] + +pub mod error; +pub mod header; +pub mod keys; +pub mod objects; +pub mod primitives; +pub mod protected; + +// Re-export this so that payloads can be generated elsewhere +pub use aead::Payload; +pub use zeroize::Zeroize; diff --git a/crates/crypto/src/objects/memory.rs b/crates/crypto/src/objects/memory.rs new file mode 100644 index 000000000..23d8fd01a --- /dev/null +++ b/crates/crypto/src/objects/memory.rs @@ -0,0 +1,84 @@ +use crate::protected::Protected; +use aead::{Aead, KeyInit, Payload}; +use aes_gcm::Aes256Gcm; +use chacha20poly1305::XChaCha20Poly1305; + +use crate::{error::Error, primitives::Algorithm}; + +// Although these two objects are identical, I think it'll be good practice to keep their usage separate. +// One for encryption, and one for decryption. This can easily be changed if needed. +pub enum MemoryEncryption { + XChaCha20Poly1305(Box), + Aes256Gcm(Box), +} + +pub enum MemoryDecryption { + XChaCha20Poly1305(Box), + Aes256Gcm(Box), +} + +impl MemoryEncryption { + #[allow(clippy::needless_pass_by_value)] + pub fn new(key: Protected<[u8; 32]>, algorithm: Algorithm) -> Result { + let encryption_object = match algorithm { + Algorithm::XChaCha20Poly1305 => { + let cipher = XChaCha20Poly1305::new_from_slice(key.expose()) + .map_err(|_| Error::MemoryModeInit)?; + + Self::XChaCha20Poly1305(Box::new(cipher)) + } + Algorithm::Aes256Gcm => { + let cipher = + Aes256Gcm::new_from_slice(key.expose()).map_err(|_| Error::MemoryModeInit)?; + + Self::Aes256Gcm(Box::new(cipher)) + } + }; + + Ok(encryption_object) + } + + pub fn encrypt<'msg, 'aad>( + &self, + plaintext: impl Into>, + nonce: &[u8], + ) -> aead::Result> { + match self { + Self::XChaCha20Poly1305(m) => m.encrypt(nonce.into(), plaintext), + Self::Aes256Gcm(m) => m.encrypt(nonce.into(), plaintext), + } + } +} + +impl MemoryDecryption { + #[allow(clippy::needless_pass_by_value)] + pub fn new(key: Protected<[u8; 32]>, algorithm: Algorithm) -> Result { + let decryption_object = match algorithm { + Algorithm::XChaCha20Poly1305 => { + let cipher = XChaCha20Poly1305::new_from_slice(key.expose()) + .map_err(|_| Error::MemoryModeInit)?; + + Self::XChaCha20Poly1305(Box::new(cipher)) + } + Algorithm::Aes256Gcm => { + let cipher = + Aes256Gcm::new_from_slice(key.expose()).map_err(|_| Error::MemoryModeInit)?; + + Self::Aes256Gcm(Box::new(cipher)) + } + }; + + Ok(decryption_object) + } + + pub fn decrypt<'msg, 'aad>( + &self, + ciphertext: impl Into>, + nonce: &[u8], + ) -> aead::Result> { + match self { + Self::XChaCha20Poly1305(m) => m.decrypt(nonce.into(), ciphertext), + Self::Aes256Gcm(m) => m.decrypt(nonce.into(), ciphertext), + } + } +} diff --git a/crates/crypto/src/objects/mod.rs b/crates/crypto/src/objects/mod.rs new file mode 100644 index 000000000..ed9ed57d9 --- /dev/null +++ b/crates/crypto/src/objects/mod.rs @@ -0,0 +1,2 @@ +pub mod memory; +pub mod stream; diff --git a/crates/crypto/src/objects/stream.rs b/crates/crypto/src/objects/stream.rs new file mode 100644 index 000000000..809aeef72 --- /dev/null +++ b/crates/crypto/src/objects/stream.rs @@ -0,0 +1,258 @@ +use std::io::{Read, Seek, Write}; + +use aead::{ + stream::{DecryptorLE31, EncryptorLE31}, + KeyInit, Payload, +}; +use aes_gcm::Aes256Gcm; +use chacha20poly1305::XChaCha20Poly1305; +use zeroize::Zeroize; + +use crate::{ + error::Error, + primitives::{Algorithm, Mode, BLOCK_SIZE}, + protected::Protected, +}; + +pub enum StreamEncryption { + XChaCha20Poly1305(Box>), + Aes256Gcm(Box>), +} + +pub enum StreamDecryption { + Aes256Gcm(Box>), + XChaCha20Poly1305(Box>), +} + +impl StreamEncryption { + #[allow(clippy::needless_pass_by_value)] + pub fn new( + key: Protected<[u8; 32]>, + nonce: &[u8], + algorithm: Algorithm, + ) -> Result { + if nonce.len() != algorithm.nonce_len(Mode::Stream) { + return Err(Error::NonceLengthMismatch); + } + + let encryption_object = match algorithm { + Algorithm::XChaCha20Poly1305 => { + let cipher = XChaCha20Poly1305::new_from_slice(key.expose()) + .map_err(|_| Error::StreamModeInit)?; + + let stream = EncryptorLE31::from_aead(cipher, nonce.into()); + Self::XChaCha20Poly1305(Box::new(stream)) + } + Algorithm::Aes256Gcm => { + let cipher = + Aes256Gcm::new_from_slice(key.expose()).map_err(|_| Error::StreamModeInit)?; + + let stream = EncryptorLE31::from_aead(cipher, nonce.into()); + Self::Aes256Gcm(Box::new(stream)) + } + }; + + Ok(encryption_object) + } + + // This should be used for every block, except the final block + pub fn encrypt_next<'msg, 'aad>( + &mut self, + payload: impl Into>, + ) -> aead::Result> { + match self { + Self::XChaCha20Poly1305(s) => s.encrypt_next(payload), + Self::Aes256Gcm(s) => s.encrypt_next(payload), + } + } + + // This should be used to encrypt the final block of data + // This takes ownership of `self` to prevent usage after finalization + pub fn encrypt_last<'msg, 'aad>( + self, + payload: impl Into>, + ) -> aead::Result> { + match self { + Self::XChaCha20Poly1305(s) => s.encrypt_last(payload), + Self::Aes256Gcm(s) => s.encrypt_last(payload), + } + } + + pub fn encrypt_streams( + mut self, + mut reader: R, + mut writer: W, + aad: &[u8], + ) -> Result<(), Error> + where + R: Read + Seek, + W: Write + Seek, + { + let mut read_buffer = vec![0u8; BLOCK_SIZE]; + let read_count = reader.read(&mut read_buffer).map_err(Error::Io)?; + if read_count == BLOCK_SIZE { + let payload = Payload { + aad, + msg: &read_buffer, + }; + + let encrypted_data = self.encrypt_next(payload).map_err(|_| { + read_buffer.zeroize(); + Error::Encrypt + })?; + + // zeroize before writing, so any potential errors won't result in a potential data leak + read_buffer.zeroize(); + + // Using `write` instead of `write_all` so we can check the amount of bytes written + let write_count = writer.write(&encrypted_data).map_err(Error::Io)?; + + if read_count != write_count - 16 { + // -16 to account for the AEAD tag + return Err(Error::WriteMismatch); + } + } else { + // we use `..read_count` in order to only use the read data, and not zeroes also + let payload = Payload { + aad, + msg: &read_buffer[..read_count], + }; + + let encrypted_data = self.encrypt_last(payload).map_err(|_| { + read_buffer.zeroize(); + Error::Encrypt + })?; + + // zeroize before writing, so any potential errors won't result in a potential data leak + read_buffer.zeroize(); + + // Using `write` instead of `write_all` so we can check the amount of bytes written + let write_count = writer.write(&encrypted_data).map_err(Error::Io)?; + + if read_count != write_count - 16 { + // -16 to account for the AEAD tag + return Err(Error::WriteMismatch); + } + } + + writer.flush().map_err(Error::Io)?; + + Ok(()) + } +} + +impl StreamDecryption { + #[allow(clippy::needless_pass_by_value)] + pub fn new( + key: Protected<[u8; 32]>, + nonce: &[u8], + algorithm: Algorithm, + ) -> Result { + if nonce.len() != algorithm.nonce_len(Mode::Stream) { + return Err(Error::NonceLengthMismatch); + } + + let decryption_object = match algorithm { + Algorithm::XChaCha20Poly1305 => { + let cipher = XChaCha20Poly1305::new_from_slice(key.expose()) + .map_err(|_| Error::StreamModeInit)?; + + let stream = DecryptorLE31::from_aead(cipher, nonce.into()); + Self::XChaCha20Poly1305(Box::new(stream)) + } + Algorithm::Aes256Gcm => { + let cipher = + Aes256Gcm::new_from_slice(key.expose()).map_err(|_| Error::StreamModeInit)?; + + let stream = DecryptorLE31::from_aead(cipher, nonce.into()); + Self::Aes256Gcm(Box::new(stream)) + } + }; + + Ok(decryption_object) + } + + // This should be used for every block, except the final block + pub fn decrypt_next<'msg, 'aad>( + &mut self, + payload: impl Into>, + ) -> aead::Result> { + match self { + Self::XChaCha20Poly1305(s) => s.decrypt_next(payload), + Self::Aes256Gcm(s) => s.decrypt_next(payload), + } + } + + // This should be used to decrypt the final block of data + // This takes ownership of `self` to prevent usage after finalization + pub fn decrypt_last<'msg, 'aad>( + self, + payload: impl Into>, + ) -> aead::Result> { + match self { + Self::XChaCha20Poly1305(s) => s.decrypt_last(payload), + Self::Aes256Gcm(s) => s.decrypt_last(payload), + } + } + + pub fn decrypt_streams( + mut self, + mut reader: R, + mut writer: W, + aad: &[u8], + ) -> Result<(), Error> + where + R: Read + Seek, + W: Write + Seek, + { + let mut read_buffer = vec![0u8; BLOCK_SIZE]; + let read_count = reader.read(&mut read_buffer).map_err(Error::Io)?; + if read_count == (BLOCK_SIZE + 16) { + let payload = Payload { + aad, + msg: &read_buffer, + }; + + let mut decrypted_data = self.decrypt_next(payload).map_err(|_| { + read_buffer.zeroize(); + Error::Decrypt + })?; + + // Using `write` instead of `write_all` so we can check the amount of bytes written + let write_count = writer.write(&decrypted_data).map_err(Error::Io)?; + + // zeroize before writing, so any potential errors won't result in a potential data leak + decrypted_data.zeroize(); + + if read_count - 16 != write_count { + // -16 to account for the AEAD tag + return Err(Error::WriteMismatch); + } + } else { + let payload = Payload { + aad, + msg: &read_buffer[..read_count], + }; + + let mut decrypted_data = self.decrypt_last(payload).map_err(|_| { + read_buffer.zeroize(); + Error::Decrypt + })?; + + // Using `write` instead of `write_all` so we can check the amount of bytes written + let write_count = writer.write(&decrypted_data).map_err(Error::Io)?; + + // zeroize before writing, so any potential errors won't result in a potential data leak + decrypted_data.zeroize(); + + if read_count - 16 != write_count { + // -16 to account for the AEAD tag + return Err(Error::WriteMismatch); + } + } + + writer.flush().map_err(Error::Io)?; + + Ok(()) + } +} diff --git a/crates/crypto/src/primitives.rs b/crates/crypto/src/primitives.rs new file mode 100644 index 000000000..6a2c7fe36 --- /dev/null +++ b/crates/crypto/src/primitives.rs @@ -0,0 +1,123 @@ +use rand::{RngCore, SeedableRng}; +use zeroize::Zeroize; + +use crate::{ + error::Error, + keys::hashing::{password_hash_argon2id, Params}, + protected::Protected, +}; + +// This is the default salt size, and the recommended size for argon2id. +pub const SALT_LEN: usize = 16; + +/// The size used for streaming blocks. This size seems to offer the best performance compared to alternatives. +/// The file size gain is 16 bytes per 1MiB (due to the AEAD tag) +pub const BLOCK_SIZE: usize = 1_048_576; + +/// The length of the encrypted master key +pub const ENCRYPTED_MASTER_KEY_LEN: usize = 48; + +/// The length of the (unencrypted) master key +pub const MASTER_KEY_LEN: usize = 32; + +/// These are all possible algorithms that can be used for encryption +#[derive(Clone, Copy, Eq, PartialEq)] +pub enum Algorithm { + XChaCha20Poly1305, + Aes256Gcm, +} + +/// These are the different "modes" for encryption +/// Stream works in "blocks", incrementing the nonce on each block (so the same nonce isn't used twice) +/// +/// Memory loads all data into memory before encryption, and encrypts it in one pass +/// +/// Stream mode is going to be the default for files, containers, etc. as memory usage is roughly equal to the `BLOCK_SIZE` +/// +/// Memory mode is only going to be used for small amounts of data (such as a master key) - streaming modes aren't viable here +#[derive(Clone, Copy, Eq, PartialEq)] +pub enum Mode { + Stream, + Memory, +} + +// (Password)HashingAlgorithm +pub enum HashingAlgorithm { + Argon2id(Params), +} + +impl HashingAlgorithm { + /// This function should be used to hash passwords + /// + /// It handles all of the security "levels" and paramaters + pub fn hash( + &self, + password: Protected>, + salt: [u8; SALT_LEN], + ) -> Result, Error> { + match self { + Self::Argon2id(params) => password_hash_argon2id(password, salt, *params), + } + } +} + +impl Algorithm { + // This function calculates the expected nonce length for a given algorithm + // 4 bytes are deducted for streaming mode, due to the LE31 counter being the last 4 bytes of the nonce + #[must_use] + pub const fn nonce_len(&self, mode: Mode) -> usize { + let base = match self { + Self::XChaCha20Poly1305 => 24, + Self::Aes256Gcm => 12, + }; + + match mode { + Mode::Stream => base - 4, + Mode::Memory => base, + } + } +} + +/// The length can easily be obtained via `algorithm.nonce_len(mode)` +/// +/// This function uses `ChaCha20Rng` for cryptographically-securely generating random data +#[must_use] +pub fn generate_nonce(len: usize) -> Vec { + let mut nonce = vec![0u8; len]; + rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut nonce); + nonce +} + +/// This function uses `ChaCha20Rng` for cryptographically-securely generating random data +#[must_use] +pub fn generate_salt() -> [u8; SALT_LEN] { + let mut salt = [0u8; SALT_LEN]; + rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut salt); + salt +} + +/// This generates a master key, which should be used for encrypting the data +/// +/// This is then stored encrypted in the header +/// +/// This function uses `ChaCha20Rng` for cryptographically-securely generating random data +#[must_use] +pub fn generate_master_key() -> Protected<[u8; MASTER_KEY_LEN]> { + let mut master_key = [0u8; MASTER_KEY_LEN]; + rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut master_key); + Protected::new(master_key) +} + +/// This is used for converting a `Vec` to an array of bytes +/// +/// It's main usage is for converting an encrypted master key from a `Vec` to `[u8; ENCRYPTED_MASTER_KEY_LEN]` +/// +/// As the master key is encrypted at this point, it does not need to be `Protected<>` +/// +/// This function still `zeroize`s any data it can +pub fn to_array(bytes: Vec) -> Result<[u8; I], Error> { + bytes.try_into().map_err(|mut b: Vec| { + b.zeroize(); + Error::VecArrSizeMismatch + }) +} diff --git a/crates/crypto/src/protected.rs b/crates/crypto/src/protected.rs new file mode 100644 index 000000000..33bfbc361 --- /dev/null +++ b/crates/crypto/src/protected.rs @@ -0,0 +1,81 @@ +//! This is a basic wrapper for secret/hidden values +//! +//! It's worth noting that this wrapper does not provide additional security that you can't get manually, it just makes it a LOT easier. +//! +//! It implements zeroize-on-drop, meaning the data is securely erased from memory once it goes out of scope. +//! You may call `drop()` prematurely if you wish to erase it sooner. +//! +//! `Protected` values are also hidden from `fmt::Debug`, and will display `[REDACTED]` instead. +//! +//! The only way to access the data within a `Protected` value is to call `.expose()` - this is to prevent accidental leakage. +//! This also makes any `Protected` value easier to audit, as you are able to quickly view wherever the data is accessed. +//! +//! `Protected` values are not able to be copied within memory, to prevent accidental leakage. They are able to be `cloned` however - but this is always explicit and you will be aware of it. +//! +//! I'd like to give a huge thank you to the authors of the [secrecy crate](https://crates.io/crates/secrecy), +//! as that crate's functionality inspired this implementation. +//! +//! # Examples +//! +//! ```rust,ignore +//! let secret_data = "this is classified information".to_string(); +//! let protected_data = Protected::new(secret_data); +//! +//! // the only way to access the data within the `Protected` wrapper +//! // is by calling `.expose()` +//! let value = protected_data.expose(); +//! ``` +//! + +use std::fmt::Debug; +use zeroize::Zeroize; + +#[derive(Clone)] +pub struct Protected +where + T: Zeroize, +{ + data: T, +} + +impl std::ops::Deref for Protected +where + T: Zeroize, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl Protected +where + T: Zeroize, +{ + pub const fn new(value: T) -> Self { + Self { data: value } + } + + pub const fn expose(&self) -> &T { + &self.data + } +} + +impl Drop for Protected +where + T: Zeroize, +{ + fn drop(&mut self) { + self.data.zeroize(); + } +} + +impl Debug for Protected +where + T: Zeroize, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("[REDACTED]") + } +}