diff --git a/Cargo.lock b/Cargo.lock index bf62fa30b..dd874bb85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5199,6 +5199,8 @@ dependencies = [ "chacha20poly1305", "rand 0.8.5", "rand_chacha 0.3.1", + "serde", + "serde_json", "thiserror", "zeroize", ] diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index e4719d58a..d75a28096 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -24,4 +24,8 @@ aead = { version = "0.5.1", features = ["stream"] } zeroize = "1.5.7" # error handling -thiserror = "1.0.37" \ No newline at end of file +thiserror = "1.0.37" + +# metadata de/serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" \ No newline at end of file diff --git a/crates/crypto/examples/single_file.rs b/crates/crypto/examples/single_file.rs new file mode 100644 index 000000000..b5a469807 --- /dev/null +++ b/crates/crypto/examples/single_file.rs @@ -0,0 +1,82 @@ +use std::fs::File; + +use sd_crypto::{ + crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, + header::{ + file::{FileHeader, FileHeaderVersion}, + keyslot::{Keyslot, KeyslotVersion}, + }, + keys::hashing::{HashingAlgorithm, Params}, + primitives::generate_master_key, + Protected, +}; + +const ALGORITHM: Algorithm = Algorithm::XChaCha20Poly1305; +const HASHING_ALGORITHM: HashingAlgorithm = HashingAlgorithm::Argon2id(Params::Standard); + +pub fn encrypt() { + let password = Protected::new(b"password".to_vec()); + + // Open both the source and the output file + let mut reader = File::open("test").unwrap(); + let mut writer = File::create("test.encrypted").unwrap(); + + // This needs to be generated here, otherwise we won't have access to it for encryption + let master_key = generate_master_key(); + + // Create a keyslot to be added to the header + let mut keyslots: Vec = Vec::new(); + keyslots.push( + Keyslot::new( + KeyslotVersion::V1, + ALGORITHM, + HASHING_ALGORITHM, + password, + &master_key, + ) + .unwrap(), + ); + + // Create the header for the encrypted file + let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, None, None); + + // Write the header to the file + header.write(&mut writer).unwrap(); + + // Use the nonce created by the header to initialize a stream encryption object + let encryptor = StreamEncryption::new(master_key, &header.nonce, header.algorithm).unwrap(); + + // Encrypt the data from the reader, and write it to the writer + // Use AAD so the header can be authenticated against every block of data + encryptor + .encrypt_streams(&mut reader, &mut writer, &header.generate_aad()) + .unwrap(); +} + +pub fn decrypt() { + let password = Protected::new(b"password".to_vec()); + + // Open both the encrypted file and the output file + let mut reader = File::open("test.encrypted").unwrap(); + let mut writer = File::create("test.original").unwrap(); + + // Deserialize the header, keyslots, etc from the encrypted file + let (header, aad) = FileHeader::deserialize(&mut reader).unwrap(); + + // Decrypt the master key with the user's password + let master_key = header.decrypt_master_key(password).unwrap(); + + // Initialize a stream decryption object using data provided by the header + let decryptor = StreamDecryption::new(master_key, &header.nonce, header.algorithm).unwrap(); + + // Decrypt data the from the writer, and write it to the writer + decryptor + .decrypt_streams(&mut reader, &mut writer, &aad) + .unwrap(); +} + +fn main() { + encrypt(); + + decrypt(); +} diff --git a/crates/crypto/examples/single_file_with_metadata.rs b/crates/crypto/examples/single_file_with_metadata.rs new file mode 100644 index 000000000..8a5ed2213 --- /dev/null +++ b/crates/crypto/examples/single_file_with_metadata.rs @@ -0,0 +1,110 @@ +use std::fs::File; + +use sd_crypto::{ + crypto::stream::{Algorithm, StreamEncryption}, + header::{ + file::{FileHeader, FileHeaderVersion}, + keyslot::{Keyslot, KeyslotVersion}, + metadata::{Metadata, MetadataVersion}, + }, + keys::hashing::{HashingAlgorithm, Params}, + primitives::{generate_master_key, generate_salt}, + Protected, +}; +use serde::{Deserialize, Serialize}; + +const ALGORITHM: Algorithm = Algorithm::XChaCha20Poly1305; +const HASHING_ALGORITHM: HashingAlgorithm = HashingAlgorithm::Argon2id(Params::Standard); + +#[derive(Serialize, Deserialize)] +pub struct FileInformation { + pub file_name: String, +} + +fn encrypt() { + let embedded_metadata = FileInformation { + file_name: "filename.txt".to_string(), + }; + + let password = Protected::new(b"password".to_vec()); + + // Open both the source and the output file + let mut reader = File::open("test").unwrap(); + let mut writer = File::create("test.encrypted").unwrap(); + + // This needs to be generated here, otherwise we won't have access to it for encryption + let master_key = generate_master_key(); + + // Create a keyslot to be added to the header + // The password is cloned as we also need to provide this for the metadata + let mut keyslots: Vec = Vec::new(); + keyslots.push( + Keyslot::new( + KeyslotVersion::V1, + ALGORITHM, + HASHING_ALGORITHM, + password.clone(), + &master_key, + ) + .unwrap(), + ); + + // Ideally this will be generated via the key management system + let md_salt = generate_salt(); + + let md = Metadata::new( + MetadataVersion::V1, + ALGORITHM, + HASHING_ALGORITHM, + password, + &md_salt, + &embedded_metadata, + ) + .unwrap(); + + // Create the header for the encrypted file (and include our metadata) + let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, Some(md), None); + + // Write the header to the file + header.write(&mut writer).unwrap(); + + // Use the nonce created by the header to initialise a stream encryption object + let encryptor = StreamEncryption::new(master_key, &header.nonce, header.algorithm).unwrap(); + + // Encrypt the data from the reader, and write it to the writer + // Use AAD so the header can be authenticated against every block of data + encryptor + .encrypt_streams(&mut reader, &mut writer, &header.generate_aad()) + .unwrap(); +} + +pub fn decrypt_metadata() { + let password = Protected::new(b"password".to_vec()); + + // Open the encrypted file + let mut reader = File::open("test.encrypted").unwrap(); + + // Deserialize the header, keyslots, etc from the encrypted file + let (header, _) = FileHeader::deserialize(&mut reader).unwrap(); + + // Checks should be made to ensure the file actually contains any metadata + let metadata = header.metadata.unwrap(); + + // Hash the user's password with the metadata salt + // This should be done by a key management system + let hashed_key = metadata + .hashing_algorithm + .hash(password, metadata.salt) + .unwrap(); + + // Decrypt the metadata + let file_info: FileInformation = metadata.decrypt_metadata(hashed_key).unwrap(); + + println!("file name: {}", file_info.file_name); +} + +fn main() { + encrypt(); + + decrypt_metadata(); +} diff --git a/crates/crypto/examples/single_file_with_preview_media.rs b/crates/crypto/examples/single_file_with_preview_media.rs new file mode 100644 index 000000000..4e06e296b --- /dev/null +++ b/crates/crypto/examples/single_file_with_preview_media.rs @@ -0,0 +1,99 @@ +use std::fs::File; + +use sd_crypto::{ + crypto::stream::{Algorithm, StreamEncryption}, + header::{ + file::{FileHeader, FileHeaderVersion}, + keyslot::{Keyslot, KeyslotVersion}, + preview_media::{PreviewMedia, PreviewMediaVersion}, + }, + keys::hashing::{HashingAlgorithm, Params}, + primitives::{generate_master_key, generate_salt}, + Protected, +}; + +const ALGORITHM: Algorithm = Algorithm::XChaCha20Poly1305; +const HASHING_ALGORITHM: HashingAlgorithm = HashingAlgorithm::Argon2id(Params::Standard); + +fn encrypt() { + let password = Protected::new(b"password".to_vec()); + + // Open both the source and the output file + let mut reader = File::open("test").unwrap(); + let mut writer = File::create("test.encrypted").unwrap(); + + // This needs to be generated here, otherwise we won't have access to it for encryption + let master_key = generate_master_key(); + + // Create a keyslot to be added to the header + // The password is cloned as we also need to provide this for the preview media + let mut keyslots: Vec = Vec::new(); + keyslots.push( + Keyslot::new( + KeyslotVersion::V1, + ALGORITHM, + HASHING_ALGORITHM, + password.clone(), + &master_key, + ) + .unwrap(), + ); + + // Ideally this will be generated via the key management system + let pvm_salt = generate_salt(); + + let pvm_media = b"a nice mountain".to_vec(); + + let pvm = PreviewMedia::new( + PreviewMediaVersion::V1, + ALGORITHM, + HASHING_ALGORITHM, + password, + &pvm_salt, + &pvm_media, + ) + .unwrap(); + + // Create the header for the encrypted file (and include our preview media) + let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, None, Some(pvm)); + + // Write the header to the file + header.write(&mut writer).unwrap(); + + // Use the nonce created by the header to initialise a stream encryption object + let encryptor = StreamEncryption::new(master_key, &header.nonce, header.algorithm).unwrap(); + + // Encrypt the data from the reader, and write it to the writer + // Use AAD so the header can be authenticated against every block of data + encryptor + .encrypt_streams(&mut reader, &mut writer, &header.generate_aad()) + .unwrap(); +} + +pub fn decrypt_preview_media() { + let password = Protected::new(b"password".to_vec()); + + // Open the encrypted file + let mut reader = File::open("test.encrypted").unwrap(); + + // Deserialize the header, keyslots, etc from the encrypted file + let (header, _) = FileHeader::deserialize(&mut reader).unwrap(); + + // Checks should be made to ensure the file actually contains any preview media + let pvm = header.preview_media.unwrap(); + + // Hash the user's password with the preview media salt + // This should be done by a key management system + let hashed_key = pvm.hashing_algorithm.hash(password, pvm.salt).unwrap(); + + // Decrypt the preview media + let media = pvm.decrypt_preview_media(hashed_key).unwrap(); + + println!("{:?}", media.expose()); +} + +fn main() { + encrypt(); + + decrypt_preview_media(); +} diff --git a/crates/crypto/src/crypto/mod.rs b/crates/crypto/src/crypto/mod.rs new file mode 100644 index 000000000..a66c1fbb1 --- /dev/null +++ b/crates/crypto/src/crypto/mod.rs @@ -0,0 +1,2 @@ +//! This module contains all encryption and decryption items. These are used throughout the crate for all encryption/decryption needs. +pub mod stream; diff --git a/crates/crypto/src/crypto/stream.rs b/crates/crypto/src/crypto/stream.rs new file mode 100644 index 000000000..aa086480f --- /dev/null +++ b/crates/crypto/src/crypto/stream.rs @@ -0,0 +1,343 @@ +//! This module contains the crate's STREAM implementation, and wrappers that allow us to support multiple AEADs. +use std::io::{Cursor, 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::BLOCK_SIZE, Protected}; + +/// These are all possible algorithms that can be used for encryption and decryption +#[derive(Clone, Copy, Eq, PartialEq)] +pub enum Algorithm { + XChaCha20Poly1305, + Aes256Gcm, +} + +impl Algorithm { + /// This function allows us to calculate the nonce length for a given algorithm + #[must_use] + pub const fn nonce_len(&self) -> usize { + match self { + Self::XChaCha20Poly1305 => 20, + Self::Aes256Gcm => 8, + } + } +} + +pub enum StreamEncryption { + XChaCha20Poly1305(Box>), + Aes256Gcm(Box>), +} + +pub enum StreamDecryption { + Aes256Gcm(Box>), + XChaCha20Poly1305(Box>), +} + +impl StreamEncryption { + /// This should be used to initialize a stream encryption object. + /// + /// The master key, a suitable nonce, and a specific algorithm should be provided. + #[allow(clippy::needless_pass_by_value)] + pub fn new( + key: Protected<[u8; 32]>, + nonce: &[u8], + algorithm: Algorithm, + ) -> Result { + if nonce.len() != algorithm.nonce_len() { + 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) + } + + 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), + } + } + + 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), + } + } + + /// This function should be used for encrypting large amounts of data. + /// + /// The streaming implementation reads blocks of data in `BLOCK_SIZE`, encrypts, and writes to the writer. + /// + /// Measures are in place to zeroize any buffers that may contain sensitive information. + /// + /// It requires a reader, a writer, and any AAD to go with it. + /// + /// The AAD will be authenticated with each block of data. + 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].into_boxed_slice(); + loop { + 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); + } + + break; + } + } + + writer.flush().map_err(Error::Io)?; + + Ok(()) + } + + /// This should ideally only be used for small amounts of data + /// + /// It is just a thin wrapper around `encrypt_streams()`, but reduces the amount of code needed elsewhere. + #[allow(unused_mut)] + pub fn encrypt_bytes( + key: Protected<[u8; 32]>, + nonce: &[u8], + algorithm: Algorithm, + bytes: &[u8], + aad: &[u8], + ) -> Result, Error> { + let mut reader = Cursor::new(bytes); + let mut writer = Cursor::new(Vec::::new()); + + let encryptor = Self::new(key, nonce, algorithm)?; + + match encryptor.encrypt_streams(&mut reader, &mut writer, aad) { + Ok(_) => Ok(writer.into_inner()), + Err(e) => Err(e), + } + } +} + +impl StreamDecryption { + /// This should be used to initialize a stream decryption object. + /// + /// The master key, nonce and algorithm that were used for encryption should be provided. + #[allow(clippy::needless_pass_by_value)] + pub fn new( + key: Protected<[u8; 32]>, + nonce: &[u8], + algorithm: Algorithm, + ) -> Result { + if nonce.len() != algorithm.nonce_len() { + 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) + } + + 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), + } + } + + 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), + } + } + + /// This function should be used for decrypting large amounts of data. + /// + /// The streaming implementation reads blocks of data in `BLOCK_SIZE`, decrypts, and writes to the writer. + /// + /// Measures are in place to zeroize any buffers that may contain sensitive information. + /// + /// It requires a reader, a writer, and any AAD that was used. + /// + /// The AAD will be authenticated with each block of data - if the AAD doesn't match what was used during encryption, an error will be returned. + 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 + 16].into_boxed_slice(); + loop { + 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(|_| Error::Decrypt)?; + + // Using `write` instead of `write_all` so we can check the amount of bytes written + // Zeroize buffer on write error + let write_count = writer.write(&decrypted_data).map_err(|e| { + decrypted_data.zeroize(); + Error::Io(e) + })?; + + 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(|_| Error::Decrypt)?; + + // Using `write` instead of `write_all` so we can check the amount of bytes written + // Zeroize buffer on write error + let write_count = writer.write(&decrypted_data).map_err(|e| { + decrypted_data.zeroize(); + Error::Io(e) + })?; + + decrypted_data.zeroize(); + + if read_count - 16 != write_count { + // -16 to account for the AEAD tag + return Err(Error::WriteMismatch); + } + + break; + } + } + + writer.flush().map_err(Error::Io)?; + + Ok(()) + } + + /// This should ideally only be used for small amounts of data + /// + /// It is just a thin wrapper around `decrypt_streams()`, but reduces the amount of code needed elsewhere. + #[allow(unused_mut)] + pub fn decrypt_bytes( + key: Protected<[u8; 32]>, + nonce: &[u8], + algorithm: Algorithm, + bytes: &[u8], + aad: &[u8], + ) -> Result>, Error> { + let mut reader = Cursor::new(bytes); + let mut writer = Cursor::new(Vec::::new()); + + let decryptor = Self::new(key, nonce, algorithm)?; + + match decryptor.decrypt_streams(&mut reader, &mut writer, aad) { + Ok(_) => Ok(Protected::new(writer.into_inner())), + Err(e) => Err(e), + } + } +} diff --git a/crates/crypto/src/error.rs b/crates/crypto/src/error.rs index 2553376df..f5d0860dc 100644 --- a/crates/crypto/src/error.rs +++ b/crates/crypto/src/error.rs @@ -1,3 +1,4 @@ +//! This module contains all possible errors that this crate can return. use thiserror::Error; /// This enum defines all possible errors that this crate can give @@ -19,12 +20,20 @@ pub enum Error { 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, + #[error("error while parsing preview media length")] + MediaLengthParse, + #[error("no preview media found")] + NoPreviewMedia, + #[error("error while serializing/deserializing the metadata")] + MetadataDeSerialization, + #[error("no metadata found")] + NoMetadata, + #[error("tried adding too many keyslots to a header")] + TooManyKeyslots, } diff --git a/crates/crypto/src/header/container.rs b/crates/crypto/src/header/container.rs index 18ce80bb0..4af01ffab 100644 --- a/crates/crypto/src/header/container.rs +++ b/crates/crypto/src/header/container.rs @@ -1 +1 @@ -/// This is a placeholder file \ No newline at end of file +// 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 index 2a2fedddb..a6b1f6724 100644 --- a/crates/crypto/src/header/file.rs +++ b/crates/crypto/src/header/file.rs @@ -1,78 +1,119 @@ -use std::io::{Read, Seek, Write}; - -use zeroize::Zeroize; +//! This module contains a standard file header, and the functions needed to serialize/deserialize it. +//! +//! # Examples +//! +//! ```rust,ignore +//! let password = Protected::new(b"password".to_vec()); +//! +//! let mut writer = File::create("test.encrypted").unwrap(); +//! +//! // This needs to be generated here, otherwise we won't have access to it for encryption +//! let master_key = generate_master_key(); +//! +//! // Create a keyslot to be added to the header +//! let mut keyslots: Vec = Vec::new(); +//! keyslots.push( +//! Keyslot::new( +//! KeyslotVersion::V1, +//! ALGORITHM, +//! HASHING_ALGORITHM, +//! password, +//! &master_key, +//! ) +//! .unwrap(), +//! ); +//! +//! // Create the header for the encrypted file +//! let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, None, None); +//! +//! // Write the header to the file +//! header.write(&mut writer).unwrap(); +//! ``` +use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use crate::{ + crypto::stream::Algorithm, error::Error, - objects::memory::MemoryDecryption, - primitives::{Algorithm, Mode, MASTER_KEY_LEN}, - protected::Protected, + primitives::{generate_nonce, MASTER_KEY_LEN}, + Protected, }; -use super::keyslot::Keyslot; +use super::{keyslot::Keyslot, metadata::Metadata, preview_media::PreviewMedia}; /// These are used to quickly and easily identify Spacedrive-encrypted files -/// Random values - can be changed (up until 0.1.0) +/// These currently are set as "ballapp" 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 alleviate 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 +/// This header is primarily used for encrypting/decrypting single files. +/// +/// It has support for 2 keyslots (maximum). +/// +/// You may optionally attach `Metadata` and `PreviewMedia` structs to this header, and they will be accessible on deserialization. +/// +/// This contains everything necessary for decryption, and the entire header can be flaunted with no worries (provided a suitable password was selected by the user). +#[derive(Clone)] pub struct FileHeader { pub version: FileHeaderVersion, pub algorithm: Algorithm, - pub mode: Mode, pub nonce: Vec, pub keyslots: Vec, + pub metadata: Option, + pub preview_media: Option, } -/// This defines the main file header version +/// This defines the main file header version. +#[derive(Clone, Copy)] pub enum FileHeaderVersion { V1, } +/// This includes the magic bytes at the start of the file, and remainder of the header itself (excluding keyslots, metadata, and preview media as these can all change) +#[must_use] +pub const fn aad_length(version: FileHeaderVersion) -> usize { + match version { + FileHeaderVersion::V1 => 36, + } +} + impl FileHeader { + /// This function is used for creating a file header. #[must_use] pub fn new( version: FileHeaderVersion, algorithm: Algorithm, - nonce: Vec, keyslots: Vec, + metadata: Option, + preview_media: Option, ) -> Self { + let nonce = generate_nonce(algorithm); + Self { version, algorithm, - mode: Mode::Stream, nonce, keyslots, + metadata, + preview_media, } } - /// 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 + /// This is a helper function to decrypt a master key from keyslots that are attached to a header. + /// + /// You receive an error if the password doesn't match or if there are no keyslots. #[allow(clippy::needless_pass_by_value)] pub fn decrypt_master_key( &self, password: Protected>, - ) -> Result, Error> { + ) -> 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)?; + if self.keyslots.is_empty() { + return Err(Error::NoKeyslots); + } - 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) - { + for keyslot in &self.keyslots { + if let Ok(decrypted_master_key) = keyslot.decrypt_master_key(&password) { master_key.copy_from_slice(&decrypted_master_key); - decrypted_master_key.zeroize(); } } @@ -87,37 +128,48 @@ impl FileHeader { where W: Write + Seek, { - writer.write(&self.serialize()).map_err(Error::Io)?; + writer.write(&self.serialize()?).map_err(Error::Io)?; Ok(()) } + /// This function should be used for generating AAD before encryption + /// + /// Use the return value from `FileHeader::deserialize()` for decryption #[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.extend_from_slice(&MAGIC_BYTES); // 7 + aad.extend_from_slice(&self.version.serialize()); // 9 + aad.extend_from_slice(&self.algorithm.serialize()); // 11 + aad.extend_from_slice(&self.nonce); // 19 OR 31 + aad.extend_from_slice(&vec![0u8; 25 - self.nonce.len()]); // padded until 36 bytes aad } } } - #[must_use] - pub fn serialize(&self) -> Vec { + /// This function serializes a full header. + /// + /// This will include keyslots, metadata and preview media (if provided) + /// + /// An error will be returned if there are no keyslots/more than two keyslots attached. + pub fn serialize(&self) -> Result, Error> { match self.version { FileHeaderVersion::V1 => { + if self.keyslots.len() > 2 { + return Err(Error::TooManyKeyslots); + } else if self.keyslots.is_empty() { + return Err(Error::NoKeyslots); + } + 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 + header.extend_from_slice(&MAGIC_BYTES); // 7 + header.extend_from_slice(&self.version.serialize()); // 9 + header.extend_from_slice(&self.algorithm.serialize()); // 11 + header.extend_from_slice(&self.nonce); // 19 OR 31 + header.extend_from_slice(&vec![0u8; 25 - self.nonce.len()]); // padded until 36 bytes for keyslot in &self.keyslots { header.extend_from_slice(&keyslot.serialize()); @@ -127,30 +179,24 @@ impl FileHeader { header.extend_from_slice(&[0u8; 96]); } - header + if let Some(metadata) = self.metadata.clone() { + header.extend_from_slice(&metadata.serialize()); + } + + if let Some(preview_media) = self.preview_media.clone() { + header.extend_from_slice(&preview_media.serialize()); + } + + Ok(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 + /// This deserializes a header directly from a reader, and leaves the reader at the start of the encrypted data. + /// + /// On error, the cursor will not be rewound. + /// + /// 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, @@ -167,54 +213,225 @@ impl FileHeader { reader.read(&mut version).map_err(Error::Io)?; let version = FileHeaderVersion::deserialize(version)?; + // Rewind so we can get the AAD + reader.rewind().map_err(Error::Io)?; + + let mut aad = vec![0u8; aad_length(version)]; + reader.read(&mut aad).map_err(Error::Io)?; + + reader + .seek(SeekFrom::Start(MAGIC_BYTES.len() as u64 + 2)) + .map_err(Error::Io)?; + 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)]; + let mut nonce = vec![0u8; algorithm.nonce_len()]; reader.read(&mut nonce).map_err(Error::Io)?; // read and discard the padding reader - .read(&mut vec![0u8; 24 - nonce.len()]) + .read(&mut vec![0u8; 25 - nonce.len()]) .map_err(Error::Io)?; + let mut keyslot_bytes = [0u8; 192]; // length of 2x keyslots let mut keyslots: Vec = Vec::new(); + reader.read(&mut keyslot_bytes).map_err(Error::Io)?; + let mut keyslot_reader = Cursor::new(keyslot_bytes); + for _ in 0..2 { - if let Ok(keyslot) = Keyslot::deserialize(reader) { + if let Ok(keyslot) = Keyslot::deserialize(&mut keyslot_reader) { keyslots.push(keyslot); } } + let metadata = if let Ok(metadata) = Metadata::deserialize(reader) { + Some(metadata) + } else { + // header/aad area, keyslot area + reader.seek(SeekFrom::Start(36 + 192)).map_err(Error::Io)?; + None + }; + + let preview_media = if let Ok(preview_media) = PreviewMedia::deserialize(reader) { + Some(preview_media) + } else { + // header/aad area, keyslot area, full metadata length + if metadata.is_some() { + reader + .seek(SeekFrom::Start( + 36 + 192 + metadata.clone().unwrap().get_length() as u64, + )) + .map_err(Error::Io)?; + } else { + // header/aad area, keyslot area + reader.seek(SeekFrom::Start(36 + 192)).map_err(Error::Io)?; + } + None + }; + Self { version, algorithm, - mode, nonce, keyslots, + metadata, + preview_media, } } }; - // 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)) } } + +#[cfg(test)] +mod test { + use crate::{ + crypto::stream::Algorithm, + header::keyslot::{Keyslot, KeyslotVersion}, + keys::hashing::{HashingAlgorithm, Params}, + }; + use std::io::Cursor; + + use super::{FileHeader, FileHeaderVersion}; + + const HEADER_BYTES_NO_ADDITIONAL_OBJECTS: [u8; 228] = [ + 98, 97, 108, 108, 97, 112, 112, 10, 1, 11, 1, 230, 47, 48, 63, 225, 227, 15, 211, 115, 69, + 169, 184, 184, 18, 110, 189, 167, 0, 144, 26, 0, 0, 0, 0, 0, 13, 1, 11, 1, 15, 1, 104, 176, + 135, 146, 133, 75, 34, 155, 165, 148, 179, 133, 114, 245, 235, 117, 160, 55, 36, 93, 100, + 83, 164, 171, 19, 57, 66, 65, 253, 42, 160, 239, 74, 205, 239, 253, 48, 239, 249, 203, 121, + 126, 231, 52, 38, 49, 154, 254, 234, 41, 113, 169, 25, 195, 84, 78, 180, 212, 54, 4, 198, + 109, 33, 216, 163, 148, 79, 207, 121, 142, 102, 39, 169, 31, 55, 41, 231, 248, 65, 131, + 184, 216, 175, 202, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + + #[test] + fn deserialize_header() { + let mut reader = Cursor::new(HEADER_BYTES_NO_ADDITIONAL_OBJECTS); + FileHeader::deserialize(&mut reader).unwrap(); + } + + #[test] + fn serialize_header() { + let header: FileHeader = FileHeader { + version: FileHeaderVersion::V1, + algorithm: Algorithm::XChaCha20Poly1305, + nonce: [ + 230, 47, 48, 63, 225, 227, 15, 211, 115, 69, 169, 184, 184, 18, 110, 189, 167, 0, + 144, 26, + ] + .to_vec(), + keyslots: [Keyslot { + version: KeyslotVersion::V1, + algorithm: Algorithm::XChaCha20Poly1305, + hashing_algorithm: HashingAlgorithm::Argon2id(Params::Standard), + salt: [ + 104, 176, 135, 146, 133, 75, 34, 155, 165, 148, 179, 133, 114, 245, 235, 117, + ], + master_key: [ + 160, 55, 36, 93, 100, 83, 164, 171, 19, 57, 66, 65, 253, 42, 160, 239, 74, 205, + 239, 253, 48, 239, 249, 203, 121, 126, 231, 52, 38, 49, 154, 254, 234, 41, 113, + 169, 25, 195, 84, 78, 180, 212, 54, 4, 198, 109, 33, 216, + ], + nonce: [ + 163, 148, 79, 207, 121, 142, 102, 39, 169, 31, 55, 41, 231, 248, 65, 131, 184, + 216, 175, 202, + ] + .to_vec(), + }] + .to_vec(), + metadata: None, + preview_media: None, + }; + + let header_bytes = header.serialize().unwrap(); + + assert_eq!(HEADER_BYTES_NO_ADDITIONAL_OBJECTS.to_vec(), header_bytes) + } + + #[test] + #[should_panic] + fn serialize_header_with_too_many_keyslots() { + let header: FileHeader = FileHeader { + version: FileHeaderVersion::V1, + algorithm: Algorithm::XChaCha20Poly1305, + nonce: [ + 230, 47, 48, 63, 225, 227, 15, 211, 115, 69, 169, 184, 184, 18, 110, 189, 167, 0, + 144, 26, + ] + .to_vec(), + keyslots: [ + Keyslot { + version: KeyslotVersion::V1, + algorithm: Algorithm::XChaCha20Poly1305, + hashing_algorithm: HashingAlgorithm::Argon2id(Params::Standard), + salt: [ + 104, 176, 135, 146, 133, 75, 34, 155, 165, 148, 179, 133, 114, 245, 235, + 117, + ], + master_key: [ + 160, 55, 36, 93, 100, 83, 164, 171, 19, 57, 66, 65, 253, 42, 160, 239, 74, + 205, 239, 253, 48, 239, 249, 203, 121, 126, 231, 52, 38, 49, 154, 254, 234, + 41, 113, 169, 25, 195, 84, 78, 180, 212, 54, 4, 198, 109, 33, 216, + ], + nonce: [ + 163, 148, 79, 207, 121, 142, 102, 39, 169, 31, 55, 41, 231, 248, 65, 131, + 184, 216, 175, 202, + ] + .to_vec(), + }, + Keyslot { + version: KeyslotVersion::V1, + algorithm: Algorithm::XChaCha20Poly1305, + hashing_algorithm: HashingAlgorithm::Argon2id(Params::Standard), + salt: [ + 104, 176, 135, 146, 133, 75, 34, 155, 165, 148, 179, 133, 114, 245, 235, + 117, + ], + master_key: [ + 160, 55, 36, 93, 100, 83, 164, 171, 19, 57, 66, 65, 253, 42, 160, 239, 74, + 205, 239, 253, 48, 239, 249, 203, 121, 126, 231, 52, 38, 49, 154, 254, 234, + 41, 113, 169, 25, 195, 84, 78, 180, 212, 54, 4, 198, 109, 33, 216, + ], + nonce: [ + 163, 148, 79, 207, 121, 142, 102, 39, 169, 31, 55, 41, 231, 248, 65, 131, + 184, 216, 175, 202, + ] + .to_vec(), + }, + Keyslot { + version: KeyslotVersion::V1, + algorithm: Algorithm::XChaCha20Poly1305, + hashing_algorithm: HashingAlgorithm::Argon2id(Params::Standard), + salt: [ + 104, 176, 135, 146, 133, 75, 34, 155, 165, 148, 179, 133, 114, 245, 235, + 117, + ], + master_key: [ + 160, 55, 36, 93, 100, 83, 164, 171, 19, 57, 66, 65, 253, 42, 160, 239, 74, + 205, 239, 253, 48, 239, 249, 203, 121, 126, 231, 52, 38, 49, 154, 254, 234, + 41, 113, 169, 25, 195, 84, 78, 180, 212, 54, 4, 198, 109, 33, 216, + ], + nonce: [ + 163, 148, 79, 207, 121, 142, 102, 39, 169, 31, 55, 41, 231, 248, 65, 131, + 184, 216, 175, 202, + ] + .to_vec(), + }, + ] + .to_vec(), + metadata: None, + preview_media: None, + }; + + header.serialize().unwrap(); + } +} diff --git a/crates/crypto/src/header/keyslot.rs b/crates/crypto/src/header/keyslot.rs index cbd7ecba9..87b24cbf6 100644 --- a/crates/crypto/src/header/keyslot.rs +++ b/crates/crypto/src/header/keyslot.rs @@ -1,50 +1,112 @@ +//! This module contains the keyslot header item. +//! +//! At least one keyslot needs to be attached to a main header. +//! +//! Headers have limitations on the maximum amount of keyslots, and you should double check before usage. +//! +//! The `Keyslot::new()` function should always be used to create a keyslot, as it handles encrypting the master key. +//! +//! # Examples +//! +//! ```rust,ignore +//! use sd_crypto::header::keyslot::{Keyslot, KeyslotVersion}; +//! use sd_crypto::Protected; +//! use sd_crypto::keys::hashing::{HashingAlgorithm, Params}; +//! use sd_crypto::crypto::stream::Algorithm; +//! use sd_crypto::primitives::generate_master_key; +//! +//! +//! let user_password = Protected::new(b"password".to_vec()); +//! let master_key = generate_master_key(); +//! +//! let keyslot = Keyslot::new(KeyslotVersion::V1, Algorithm::XChaCha20Poly1305, HashingAlgorithm::Argon2id(Params::Standard), user_password, &master_key).unwrap(); +//! ``` use std::io::{Read, Seek}; use crate::{ + crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, error::Error, - primitives::{Algorithm, HashingAlgorithm, Mode, ENCRYPTED_MASTER_KEY_LEN, SALT_LEN}, + keys::hashing::HashingAlgorithm, + primitives::{ + generate_nonce, generate_salt, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN, SALT_LEN, + }, + Protected, }; -/// A keyslot. 96 bytes, and contains all the information for future-proofing while keeping the size reasonable +/// A keyslot - 96 bytes (as of V1), 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 +/// The algorithm (should) be inherited from the parent (the header, in this case), but that's not a guarantee so we include it here too +#[derive(Clone)] 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 +/// +/// The goal is to not increment this much, but it's here in case we need to make breaking changes +#[derive(Clone, Copy)] pub enum KeyslotVersion { V1, } impl Keyslot { - #[must_use] + /// This should be used for creating a keyslot. + /// + /// This handles generating the nonce/salt, and encrypting the master key. + /// + /// You will need to provide the password, and a generated master key (this can't generate it, otherwise it can't be used elsewhere) 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 { + password: Protected>, + master_key: &Protected<[u8; MASTER_KEY_LEN]>, + ) -> Result { + let salt = generate_salt(); + let nonce = generate_nonce(algorithm); + + let hashed_password = hashing_algorithm.hash(password, salt).unwrap(); + + let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes( + hashed_password, + &nonce, + algorithm, + master_key.expose(), + &[], + )?)?; + + Ok(Self { version, algorithm, hashing_algorithm, - mode: Mode::Memory, salt, master_key: encrypted_master_key, nonce, - } + }) } + + /// This function should not be used directly, use `header.decrypt_master_key()` instead + /// + /// This attempts to decrypt the master key for a single keyslot + /// + /// An error will be returned on failure. + pub fn decrypt_master_key( + &self, + password: &Protected>, + ) -> Result>, Error> { + let key = self + .hashing_algorithm + .hash(password.clone(), self.salt) + .map_err(|_| Error::PasswordHash)?; + + StreamDecryption::decrypt_bytes(key, &self.nonce, self.algorithm, &self.master_key, &[]) + } + /// This function is used to serialize a keyslot into bytes #[must_use] pub fn serialize(&self) -> Vec { @@ -54,17 +116,20 @@ impl Keyslot { 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.extend_from_slice(&self.salt); // 22 + keyslot.extend_from_slice(&self.master_key); // 70 + keyslot.extend_from_slice(&self.nonce); // 78 or 90 + keyslot.extend_from_slice(&vec![0u8; 26 - self.nonce.len()]); // 96 total bytes keyslot } } } - /// This function reads a keyslot from a reader, and attempts to serialize a keyslot + /// This function reads a keyslot from a reader + /// + /// It will leave the cursor at the end of the keyslot on success + /// + /// The cursor will not be rewound on error. pub fn deserialize(reader: &mut R) -> Result where R: Read + Seek, @@ -83,17 +148,13 @@ impl Keyslot { 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)]; + let mut nonce = vec![0u8; algorithm.nonce_len()]; reader.read(&mut nonce).map_err(Error::Io)?; reader @@ -104,7 +165,6 @@ impl Keyslot { version, algorithm, hashing_algorithm, - mode, salt, master_key, nonce, diff --git a/crates/crypto/src/header/metadata.rs b/crates/crypto/src/header/metadata.rs new file mode 100644 index 000000000..4b993b08c --- /dev/null +++ b/crates/crypto/src/header/metadata.rs @@ -0,0 +1,250 @@ +//! This module contains the metadata header item. +//! +//! This is an optional item, and anything that may be serialized with `serde` can be used here. +//! +//! # Examples +//! +//! ```rust,ignore +//! #[derive(Serialize, Deserialize)] +//! pub struct FileInformation { +//! pub file_name: String, +//! } +//! +//! let embedded_metadata = FileInformation { +//! file_name: "filename.txt".to_string(), +//! }; +//! +//! // Ideally this will be generated via a key management system +//! let md_salt = generate_salt(); +//! +//! let md = Metadata::new( +//! MetadataVersion::V1, +//! ALGORITHM, +//! HASHING_ALGORITHM, +//! password, +//! &md_salt, +//! &embedded_metadata, +//! ) +//! .unwrap(); +//! ``` +use std::io::{Read, Seek}; + +use crate::{ + crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, + error::Error, + keys::hashing::HashingAlgorithm, + primitives::{ + generate_master_key, generate_nonce, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN, + SALT_LEN, + }, + Protected, +}; + +/// This is a metadata header item. You may add it to a header, and this will be stored with the file. +/// +/// The `Metadata::new()` function handles master key and metadata encryption. +/// +/// The salt should be generated elsewhere (e.g. a key management system). +#[derive(Clone)] +pub struct Metadata { + pub version: MetadataVersion, + pub algorithm: Algorithm, // encryption algorithm + pub hashing_algorithm: HashingAlgorithm, // password hashing algorithm + pub salt: [u8; SALT_LEN], + pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], + pub master_key_nonce: Vec, + pub metadata_nonce: Vec, + pub metadata: Vec, +} + +#[derive(Clone, Copy)] +pub enum MetadataVersion { + V1, +} + +impl Metadata { + /// This should be used for creating a header metadata item. + /// + /// It handles encrypting the master key and metadata. + /// + /// You will need to provide the user's password, and a semi-universal salt for hashing the user's password. This allows for extremely fast decryption. + /// + /// Metadata needs to be accessed switfly, so a key management system should handle the salt generation. + pub fn new( + version: MetadataVersion, + algorithm: Algorithm, + hashing_algorithm: HashingAlgorithm, + password: Protected>, + salt: &[u8; SALT_LEN], + media: &T, + ) -> Result + where + T: ?Sized + serde::Serialize, + { + let metadata_nonce = generate_nonce(algorithm); + let master_key_nonce = generate_nonce(algorithm); + let master_key = generate_master_key(); + + let hashed_password = hashing_algorithm.hash(password, *salt)?; + + let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes( + hashed_password, + &master_key_nonce, + algorithm, + master_key.expose(), + &[], + )?)?; + + let encrypted_metadata = StreamEncryption::encrypt_bytes( + master_key, + &metadata_nonce, + algorithm, + &serde_json::to_vec(media).map_err(|_| Error::MetadataDeSerialization)?, + &[], + )?; + + Ok(Self { + version, + algorithm, + hashing_algorithm, + salt: *salt, + master_key: encrypted_master_key, + master_key_nonce, + metadata_nonce, + metadata: encrypted_metadata, + }) + } + + #[must_use] + pub fn get_length(&self) -> usize { + match self.version { + MetadataVersion::V1 => 128 + self.metadata.len(), + } + } + + /// This function is used to serialize a metadata item into bytes + /// + /// This also includes the encrypted metadata itself, so this may be sizeable + #[must_use] + pub fn serialize(&self) -> Vec { + match self.version { + MetadataVersion::V1 => { + let mut metadata: Vec = Vec::new(); + metadata.extend_from_slice(&self.version.serialize()); // 2 + metadata.extend_from_slice(&self.algorithm.serialize()); // 4 + metadata.extend_from_slice(&self.hashing_algorithm.serialize()); // 6 + metadata.extend_from_slice(&self.salt); // 22 + metadata.extend_from_slice(&self.master_key); // 70 + metadata.extend_from_slice(&self.master_key_nonce); // 82 or 94 + metadata.extend_from_slice(&vec![0u8; 26 - self.master_key_nonce.len()]); // 96 + metadata.extend_from_slice(&self.metadata_nonce); // 108 or 120 + metadata.extend_from_slice(&vec![0u8; 24 - self.metadata_nonce.len()]); // 120 + metadata.extend_from_slice(&self.metadata.len().to_le_bytes()); // 128 total bytes + metadata.extend_from_slice(&self.metadata); // this can vary in length + metadata + } + } + } + + /// This function should be used to retrieve the metadata for a file + /// + /// All it requires is a pre-hashed key (hashed with the salt provided on creation) + /// + /// A deserialized data type will be returned from this function + pub fn decrypt_metadata(&self, hashed_key: Protected<[u8; 32]>) -> Result + where + T: serde::de::DeserializeOwned, + { + let mut master_key = [0u8; MASTER_KEY_LEN]; + + let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes( + hashed_key, + &self.master_key_nonce, + self.algorithm, + &self.master_key, + &[], + ) { + master_key.copy_from_slice(&decrypted_master_key); + Ok(Protected::new(master_key)) + } else { + Err(Error::IncorrectPassword) + }?; + + let metadata = StreamDecryption::decrypt_bytes( + master_key, + &self.metadata_nonce, + self.algorithm, + &self.metadata, + &[], + )?; + + serde_json::from_slice::(&metadata).map_err(|_| Error::MetadataDeSerialization) + } + + /// This function reads a metadata header item from a reader + /// + /// The cursor will be left at the end of the metadata item on success + /// + /// The cursor will not be rewound on error. + 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 = MetadataVersion::deserialize(version).map_err(|_| Error::NoMetadata)?; + + match version { + MetadataVersion::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 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 master_key_nonce = vec![0u8; algorithm.nonce_len()]; + reader.read(&mut master_key_nonce).map_err(Error::Io)?; + + reader + .read(&mut vec![0u8; 26 - master_key_nonce.len()]) + .map_err(Error::Io)?; + + let mut metadata_nonce = vec![0u8; algorithm.nonce_len()]; + reader.read(&mut metadata_nonce).map_err(Error::Io)?; + + reader + .read(&mut vec![0u8; 24 - metadata_nonce.len()]) + .map_err(Error::Io)?; + + let mut metadata_length = [0u8; 8]; + reader.read(&mut metadata_length).map_err(Error::Io)?; + + let metadata_length: usize = usize::from_le_bytes(metadata_length); + + let mut metadata = vec![0u8; metadata_length]; + reader.read(&mut metadata).map_err(Error::Io)?; + + let metadata = Self { + version, + algorithm, + hashing_algorithm, + salt, + master_key, + master_key_nonce, + metadata_nonce, + metadata, + }; + + Ok(metadata) + } + } + } +} diff --git a/crates/crypto/src/header/mod.rs b/crates/crypto/src/header/mod.rs index f4fe3e77e..d7a889594 100644 --- a/crates/crypto/src/header/mod.rs +++ b/crates/crypto/src/header/mod.rs @@ -1,6 +1,8 @@ -//! This module will contain all encrypted header related functions, information, etc. -//! It'll handle serialisation, deserialisation, AAD, keyslots and everything else - +//! This module will contains all header related functions. +//! +//! It handles serialisation, deserialisation, AAD, keyslots and metadata, preview media. pub mod file; pub mod keyslot; +pub mod metadata; +pub mod preview_media; pub mod serialization; diff --git a/crates/crypto/src/header/preview_media.rs b/crates/crypto/src/header/preview_media.rs new file mode 100644 index 000000000..cca29f208 --- /dev/null +++ b/crates/crypto/src/header/preview_media.rs @@ -0,0 +1,236 @@ +//! This module contains the preview media header item. +//! +//! It is an optional extension to a header, and is intended for video/image thumbnails. +//! +//! # Examples +//! +//! ```rust,ignore +//! // Ideally this will be generated via a key management system +//! let pvm_salt = generate_salt(); +//! +//! let pvm_media = b"a nice mountain".to_vec(); +//! +//! let pvm = PreviewMedia::new( +//! PreviewMediaVersion::V1, +//! ALGORITHM, +//! HASHING_ALGORITHM, +//! password, +//! &pvm_salt, +//! &pvm_media, +//! ) +//! .unwrap(); +//! ``` +use std::io::{Read, Seek}; + +use crate::{ + crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, + error::Error, + keys::hashing::HashingAlgorithm, + primitives::{ + generate_master_key, generate_nonce, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN, + SALT_LEN, + }, + Protected, +}; + +/// This is a preview media header item. You may add it to a header, and this will be stored with the file. +/// +/// The `Metadata::new()` function handles master key and metadata encryption. +/// +/// The salt should be generated elsewhere (e.g. a key management system). +#[derive(Clone)] +pub struct PreviewMedia { + pub version: PreviewMediaVersion, + pub algorithm: Algorithm, // encryption algorithm + pub hashing_algorithm: HashingAlgorithm, // password hashing algorithm + pub salt: [u8; SALT_LEN], + pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], + pub master_key_nonce: Vec, + pub media_nonce: Vec, + pub media: Vec, +} + +#[derive(Clone, Copy)] +pub enum PreviewMediaVersion { + V1, +} + +impl PreviewMedia { + /// This should be used for creating a header preview media item. + /// + /// This handles encrypting the master key and preview media. + /// + /// You will need to provide the user's password, and a semi-universal salt for hashing the user's password. This allows for extremely fast decryption. + /// + /// Preview media needs to be accessed switfly, so a key management system should handle the salt generation. + pub fn new( + version: PreviewMediaVersion, + algorithm: Algorithm, + hashing_algorithm: HashingAlgorithm, + password: Protected>, + salt: &[u8; SALT_LEN], + media: &[u8], + ) -> Result { + let media_nonce = generate_nonce(algorithm); + let master_key_nonce = generate_nonce(algorithm); + let master_key = generate_master_key(); + + let hashed_password = hashing_algorithm.hash(password, *salt)?; + + let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes( + hashed_password, + &master_key_nonce, + algorithm, + master_key.expose(), + &[], + )?)?; + + let encrypted_media = + StreamEncryption::encrypt_bytes(master_key, &media_nonce, algorithm, media, &[])?; + + Ok(Self { + version, + algorithm, + hashing_algorithm, + salt: *salt, + master_key: encrypted_master_key, + master_key_nonce, + media_nonce, + media: encrypted_media, + }) + } + + #[must_use] + pub fn get_length(&self) -> usize { + match self.version { + PreviewMediaVersion::V1 => 128 + self.media.len(), + } + } + + /// This function is used to serialize a preview media header item into bytes + /// + /// This also includes the encrypted preview media itself, so this may be sizeable + #[must_use] + pub fn serialize(&self) -> Vec { + match self.version { + PreviewMediaVersion::V1 => { + let mut preview_media: Vec = Vec::new(); + preview_media.extend_from_slice(&self.version.serialize()); // 2 + preview_media.extend_from_slice(&self.algorithm.serialize()); // 4 + preview_media.extend_from_slice(&self.hashing_algorithm.serialize()); // 6 + preview_media.extend_from_slice(&self.salt); // 22 + preview_media.extend_from_slice(&self.master_key); // 70 + preview_media.extend_from_slice(&self.master_key_nonce); // 82 or 94 + preview_media.extend_from_slice(&vec![0u8; 26 - self.master_key_nonce.len()]); // 96 + preview_media.extend_from_slice(&self.media_nonce); // 108 or 120 + preview_media.extend_from_slice(&vec![0u8; 24 - self.media_nonce.len()]); // 120 + preview_media.extend_from_slice(&self.media.len().to_le_bytes()); // 128 total bytes + preview_media.extend_from_slice(&self.media); // this can vary in length + preview_media + } + } + } + + /// This function is what you'll want to use to get the preview media for a file + /// + /// All it requires is a pre-hashed key (hashed with the salt provided on creation) + /// + /// Once provided, a `Vec` is returned that contains the preview media + pub fn decrypt_preview_media( + &self, + hashed_key: Protected<[u8; 32]>, + ) -> Result>, Error> { + let mut master_key = [0u8; MASTER_KEY_LEN]; + + let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes( + hashed_key, + &self.master_key_nonce, + self.algorithm, + &self.master_key, + &[], + ) { + master_key.copy_from_slice(&decrypted_master_key); + Ok(Protected::new(master_key)) + } else { + Err(Error::IncorrectPassword) + }?; + + let media = StreamDecryption::decrypt_bytes( + master_key, + &self.media_nonce, + self.algorithm, + &self.media, + &[], + )?; + + Ok(media) + } + + /// This function reads a preview media header item from a reader + /// + /// The cursor will be left at the end of the preview media item on success + /// + /// The cursor will not be rewound on error. + 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 = + PreviewMediaVersion::deserialize(version).map_err(|_| Error::NoPreviewMedia)?; + + match version { + PreviewMediaVersion::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 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 master_key_nonce = vec![0u8; algorithm.nonce_len()]; + reader.read(&mut master_key_nonce).map_err(Error::Io)?; + + reader + .read(&mut vec![0u8; 26 - master_key_nonce.len()]) + .map_err(Error::Io)?; + + let mut media_nonce = vec![0u8; algorithm.nonce_len()]; + reader.read(&mut media_nonce).map_err(Error::Io)?; + + reader + .read(&mut vec![0u8; 24 - media_nonce.len()]) + .map_err(Error::Io)?; + + let mut media_length = [0u8; 8]; + reader.read(&mut media_length).map_err(Error::Io)?; + + let media_length: usize = usize::from_le_bytes(media_length); + + let mut media = vec![0u8; media_length]; + reader.read(&mut media).map_err(Error::Io)?; + + let preview_media = Self { + version, + algorithm, + hashing_algorithm, + salt, + master_key, + master_key_nonce, + media_nonce, + media, + }; + + Ok(preview_media) + } + } + } +} diff --git a/crates/crypto/src/header/serialization.rs b/crates/crypto/src/header/serialization.rs index abd1bc861..2453354e6 100644 --- a/crates/crypto/src/header/serialization.rs +++ b/crates/crypto/src/header/serialization.rs @@ -1,14 +1,16 @@ -//! This module defines all of the serialization and deserialization rules for the headers +//! This module defines all of the serialization and deserialization rules for the header items //! -//! It contains byte -> enum and enum -> byte conversions for everything that could be written to a header (except headers and keyslots themselves) - +//! It contains `byte -> enum` and `enum -> byte` conversions for everything that could be written to a header (except headers, keyslots, and other header items) use crate::{ + crypto::stream::Algorithm, error::Error, - keys::hashing::Params, - primitives::{Algorithm, HashingAlgorithm, Mode}, + keys::hashing::{HashingAlgorithm, Params}, }; -use super::{file::FileHeaderVersion, keyslot::KeyslotVersion}; +use super::{ + file::FileHeaderVersion, keyslot::KeyslotVersion, metadata::MetadataVersion, + preview_media::PreviewMediaVersion, +}; impl FileHeaderVersion { #[must_use] @@ -42,6 +44,38 @@ impl KeyslotVersion { } } +impl PreviewMediaVersion { + #[must_use] + pub const fn serialize(&self) -> [u8; 2] { + match self { + Self::V1 => [0x0E, 0x01], + } + } + + pub const fn deserialize(bytes: [u8; 2]) -> Result { + match bytes { + [0x0E, 0x01] => Ok(Self::V1), + _ => Err(Error::FileHeader), + } + } +} + +impl MetadataVersion { + #[must_use] + pub const fn serialize(&self) -> [u8; 2] { + match self { + Self::V1 => [0x1F, 0x01], + } + } + + pub const fn deserialize(bytes: [u8; 2]) -> Result { + match bytes { + [0x1F, 0x01] => Ok(Self::V1), + _ => Err(Error::FileHeader), + } + } +} + impl HashingAlgorithm { #[must_use] pub const fn serialize(&self) -> [u8; 2] { @@ -81,21 +115,3 @@ impl Algorithm { } } } - -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 index 9fd05852f..530b794eb 100644 --- a/crates/crypto/src/keys/hashing.rs +++ b/crates/crypto/src/keys/hashing.rs @@ -1,10 +1,22 @@ -use crate::protected::Protected; +//! This module contains all password-hashing related functions. +//! +//! Everything contained within is used to hash a user's password into strong key material, suitable for encrypting master keys. +//! +//! # Examples +//! +//! ```rust,ignore +//! let password = Protected::new(b"password".to_vec()); +//! let hashing_algorithm = HashingAlgorithm::Argon2id(Params::Standard); +//! let salt = generate_salt(); +//! let hashed_password = hashing_algorithm.hash(password, salt).unwrap(); +//! ``` +use crate::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 +/// These parameters define the password-hashing level. +/// +/// The harder the parameter, the longer the password will take to hash. #[derive(Clone, Copy)] pub enum Params { Standard, @@ -12,13 +24,37 @@ pub enum Params { Paranoid, } +/// This defines all available password hashing algorithms. +#[derive(Clone, Copy)] +pub enum HashingAlgorithm { + Argon2id(Params), +} + +impl HashingAlgorithm { + /// This function should be used to hash passwords + /// + /// It also handles all the password hashing parameters. + pub fn hash( + &self, + password: Protected>, + salt: [u8; SALT_LEN], + ) -> Result, Error> { + match self { + Self::Argon2id(params) => password_hash_argon2id(password, salt, *params), + } + } +} + impl Params { + /// This function is used to generate parameters for password hashing. + /// + /// This should not be called directly. Call it via the `HashingAlgorithm` struct (e.g. `HashingAlgorithm::Argon2id(Params::Standard).hash()`) #[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 + // It's very hardware dependant but we should aim for at least 64MB 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 => { @@ -37,7 +73,6 @@ impl Params { } } -// 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()`) diff --git a/crates/crypto/src/keys/mod.rs b/crates/crypto/src/keys/mod.rs index 62c68590b..dc007c115 100644 --- a/crates/crypto/src/keys/mod.rs +++ b/crates/crypto/src/keys/mod.rs @@ -1 +1,2 @@ +//! This module contains all key and hashing related functions. pub mod hashing; diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index 56ca2da09..c426cdd4e 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -11,13 +11,18 @@ #![allow(clippy::module_name_repetitions)] #![allow(clippy::similar_names)] +pub mod crypto; 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; + +// Make this easier to use (e.g. `sd_crypto::Protected`) +pub use protected::Protected; + +// Re-export zeroize so it can be used elsewhere pub use zeroize::Zeroize; diff --git a/crates/crypto/src/objects/memory.rs b/crates/crypto/src/objects/memory.rs deleted file mode 100644 index 23d8fd01a..000000000 --- a/crates/crypto/src/objects/memory.rs +++ /dev/null @@ -1,84 +0,0 @@ -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 deleted file mode 100644 index ed9ed57d9..000000000 --- a/crates/crypto/src/objects/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod memory; -pub mod stream; diff --git a/crates/crypto/src/objects/stream.rs b/crates/crypto/src/objects/stream.rs deleted file mode 100644 index 809aeef72..000000000 --- a/crates/crypto/src/objects/stream.rs +++ /dev/null @@ -1,258 +0,0 @@ -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 index 6a2c7fe36..05c1d7ab9 100644 --- a/crates/crypto/src/primitives.rs +++ b/crates/crypto/src/primitives.rs @@ -1,17 +1,18 @@ +//! This module contains constant values and functions that are used around the crate. +//! +//! This includes things such as cryptographically-secure random salt/master key/nonce generation, +//! lengths for master keys and even the streaming block size. use rand::{RngCore, SeedableRng}; use zeroize::Zeroize; -use crate::{ - error::Error, - keys::hashing::{password_hash_argon2id, Params}, - protected::Protected, -}; +use crate::{crypto::stream::Algorithm, error::Error, Protected}; -// This is the default salt size, and the recommended size for argon2id. +/// 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) +/// The size used for streaming encryption/decryption. This size seems to offer the best performance compared to alternatives. +/// +/// The file size gain is 16 bytes per 1048576 bytes (due to the AEAD tag) pub const BLOCK_SIZE: usize = 1_048_576; /// The length of the encrypted master key @@ -20,75 +21,21 @@ 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) +/// This should be used for generating nonces for encryption. /// -/// Memory loads all data into memory before encryption, and encrypts it in one pass +/// An algorithm is required so this function can calculate the length of the nonce. /// -/// 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 +/// This function uses `ChaCha20Rng` for generating cryptographically-secure random data #[must_use] -pub fn generate_nonce(len: usize) -> Vec { - let mut nonce = vec![0u8; len]; +pub fn generate_nonce(algorithm: Algorithm) -> Vec { + let mut nonce = vec![0u8; algorithm.nonce_len()]; rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut nonce); nonce } -/// This function uses `ChaCha20Rng` for cryptographically-securely generating random data +/// This should be used for generating salts for hashing. +/// +/// This function uses `ChaCha20Rng` for generating cryptographically-secure random data #[must_use] pub fn generate_salt() -> [u8; SALT_LEN] { let mut salt = [0u8; SALT_LEN]; @@ -98,9 +45,9 @@ pub fn generate_salt() -> [u8; SALT_LEN] { /// This generates a master key, which should be used for encrypting the data /// -/// This is then stored encrypted in the header +/// This is then stored (encrypted) within the header. /// -/// This function uses `ChaCha20Rng` for cryptographically-securely generating random data +/// This function uses `ChaCha20Rng` for generating cryptographically-secure random data #[must_use] pub fn generate_master_key() -> Protected<[u8; MASTER_KEY_LEN]> { let mut master_key = [0u8; MASTER_KEY_LEN]; diff --git a/crates/crypto/src/protected.rs b/crates/crypto/src/protected.rs index 33bfbc361..3fdadf024 100644 --- a/crates/crypto/src/protected.rs +++ b/crates/crypto/src/protected.rs @@ -17,7 +17,9 @@ //! //! # Examples //! -//! ```rust,ignore +//! ```rust +//! use sd_crypto::Protected; +//! //! let secret_data = "this is classified information".to_string(); //! let protected_data = Protected::new(secret_data); //! @@ -26,7 +28,6 @@ //! let value = protected_data.expose(); //! ``` //! - use std::fmt::Debug; use zeroize::Zeroize; @@ -60,6 +61,10 @@ where pub const fn expose(&self) -> &T { &self.data } + + pub fn zeroize(mut self) { + self.data.zeroize(); + } } impl Drop for Protected