diff --git a/Cargo.lock b/Cargo.lock index ab5ebdd2f..d22252418 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5186,6 +5186,7 @@ dependencies = [ "rmp", "rmp-serde", "rspc", + "sd-crypto", "sd-ffmpeg", "sd-file-ext", "serde", @@ -5235,6 +5236,7 @@ dependencies = [ "serde", "serde_json", "thiserror", + "uuid 1.2.1", "zeroize", ] diff --git a/core/Cargo.toml b/core/Cargo.toml index b43659da7..16a6622d9 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -54,7 +54,8 @@ image = "0.24.4" webp = "0.2.2" ffmpeg-next = { version = "5.1.1", optional = true, features = [] } sd-ffmpeg = { path = "../crates/ffmpeg", optional = true } -sd-file-ext = { path = "../crates/file-ext" } +sd-crypto = { path = "../crates/crypto" } +sd-file-ext = { path = "../crates/file-ext"} fs_extra = "1.2.0" tracing = "0.1.36" tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index a6cd7f97d..a1f71b731 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -189,13 +189,30 @@ model FileConflict { // they can be "mounted" to a client, and then used to decrypt files automatically model Key { id Int @id @default(autoincrement()) - // used to identify the key when it is entered by user - checksum String @unique + // uuid to identify the key + uuid String @unique + // the name that the user sets name String? + // is this key the default for encryption? + default Boolean // nullable if concealed for security date_created DateTime? @default(now()) - // so we know which algorithm to use, can be null if user must select - algorithm Int? @default(0) + // encryption algorithm used to encrypt the key + algorithm Bytes + // hashing algorithm used for hashing the master password + hashing_algorithm Bytes + // salt to hash the master password with + salt Bytes + // salt used for encrypting data with this key + content_salt Bytes + // the *encrypted* master key (48 bytes) + master_key Bytes + // the nonce used for encrypting the master key + master_key_nonce Bytes + // the nonce used for encrypting the key + key_nonce Bytes + // the *encrypted* key + key Bytes objects Object[] file_paths FilePath[] diff --git a/core/src/library/library_ctx.rs b/core/src/library/library_ctx.rs index 01d3d8c86..f56f432a0 100644 --- a/core/src/library/library_ctx.rs +++ b/core/src/library/library_ctx.rs @@ -1,5 +1,7 @@ use crate::job::DynJob; +use sd_crypto::keys::keymanager::KeyManager; use std::sync::Arc; +use tokio::sync::Mutex; use tracing::warn; use uuid::Uuid; @@ -16,6 +18,8 @@ pub struct LibraryContext { pub config: LibraryConfig, /// db holds the database client for the current library. pub db: Arc, + /// key manager that provides encryption keys to functions that require them + pub key_manager: Arc>, /// node_local_id holds the local ID of the node which is running the library. pub node_local_id: i32, /// node_context holds the node context for the node which this library is running on. diff --git a/core/src/library/library_manager.rs b/core/src/library/library_manager.rs index 70577b536..9499400e0 100644 --- a/core/src/library/library_manager.rs +++ b/core/src/library/library_manager.rs @@ -1,7 +1,7 @@ use crate::{ invalidate_query, node::Platform, - prisma::node, + prisma::{node, PrismaClient}, util::{ db::load_and_migrate, seeder::{indexer_rules_seeder, SeederError}, @@ -9,6 +9,14 @@ use crate::{ NodeContext, }; +use sd_crypto::{ + crypto::stream::Algorithm, + keys::{ + hashing::HashingAlgorithm, + keymanager::{KeyManager, StoredKey}, + }, + primitives::to_array, +}; use std::{ env, fs, io, path::{Path, PathBuf}, @@ -16,7 +24,7 @@ use std::{ sync::Arc, }; use thiserror::Error; -use tokio::sync::RwLock; +use tokio::sync::{Mutex, RwLock}; use uuid::Uuid; use super::{LibraryConfig, LibraryConfigWrapped, LibraryContext}; @@ -31,6 +39,53 @@ pub struct LibraryManager { node_context: NodeContext, } +pub async fn create_keymanager(client: &PrismaClient) -> Result { + // retrieve all stored keys from the DB + let mut key_manager = KeyManager::new(vec![], None); + + let db_stored_keys = client.key().find_many(vec![]).exec().await?; + + let mut default = Uuid::default(); + + // collect and serialize the stored keys + let stored_keys: Vec = db_stored_keys + .iter() + .map(|d| { + let d = d.clone(); + let uuid = uuid::Uuid::from_str(&d.uuid).unwrap(); + + if d.default { + default = uuid; + } + + StoredKey { + uuid, + salt: to_array(d.salt).unwrap(), + algorithm: Algorithm::deserialize(to_array(d.algorithm).unwrap()).unwrap(), + content_salt: to_array(d.content_salt).unwrap(), + master_key: to_array(d.master_key).unwrap(), + master_key_nonce: d.master_key_nonce, + key_nonce: d.key_nonce, + key: d.key, + hashing_algorithm: HashingAlgorithm::deserialize( + to_array(d.hashing_algorithm).unwrap(), + ) + .unwrap(), + } + }) + .collect(); + + // insert all keys from the DB into the keymanager's keystore + key_manager.populate_keystore(stored_keys).unwrap(); + + // if any key had an associated default tag + if !default.is_nil() { + key_manager.set_default(default).unwrap(); + } + + Ok(key_manager) +} + #[derive(Error, Debug)] pub enum LibraryManagerError { #[error("error saving or loading the config from the filesystem")] @@ -263,10 +318,13 @@ impl LibraryManager { // Run seeders indexer_rules_seeder(&db).await?; + let key_manager = Arc::new(Mutex::new(create_keymanager(&db).await?)); + Ok(LibraryContext { id, config, db, + key_manager, node_local_id: node_data.id, node_context, }) diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index d75a28096..726bf3de5 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -28,4 +28,7 @@ thiserror = "1.0.37" # metadata de/serialization serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" \ No newline at end of file +serde_json = "1.0" + +uuid = { version = "1.1.2", features = ["v4", "serde"] } + diff --git a/crates/crypto/examples/single_file.rs b/crates/crypto/examples/single_file.rs index b5a469807..c7d04276e 100644 --- a/crates/crypto/examples/single_file.rs +++ b/crates/crypto/examples/single_file.rs @@ -7,7 +7,7 @@ use sd_crypto::{ keyslot::{Keyslot, KeyslotVersion}, }, keys::hashing::{HashingAlgorithm, Params}, - primitives::generate_master_key, + primitives::{generate_master_key, generate_salt}, Protected, }; @@ -24,6 +24,9 @@ pub fn encrypt() { // This needs to be generated here, otherwise we won't have access to it for encryption let master_key = generate_master_key(); + // This ideally should be done by the KMS + let salt = generate_salt(); + // Create a keyslot to be added to the header let mut keyslots: Vec = Vec::new(); keyslots.push( @@ -31,6 +34,7 @@ pub fn encrypt() { KeyslotVersion::V1, ALGORITHM, HASHING_ALGORITHM, + salt, password, &master_key, ) @@ -38,7 +42,7 @@ pub fn encrypt() { ); // Create the header for the encrypted file - let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, None, None); + let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots); // Write the header to the file header.write(&mut writer).unwrap(); diff --git a/crates/crypto/examples/single_file_with_metadata.rs b/crates/crypto/examples/single_file_with_metadata.rs index 8a5ed2213..0a2e7e48b 100644 --- a/crates/crypto/examples/single_file_with_metadata.rs +++ b/crates/crypto/examples/single_file_with_metadata.rs @@ -5,7 +5,7 @@ use sd_crypto::{ header::{ file::{FileHeader, FileHeaderVersion}, keyslot::{Keyslot, KeyslotVersion}, - metadata::{Metadata, MetadataVersion}, + metadata::MetadataVersion, }, keys::hashing::{HashingAlgorithm, Params}, primitives::{generate_master_key, generate_salt}, @@ -28,6 +28,9 @@ fn encrypt() { let password = Protected::new(b"password".to_vec()); + // This ideally should be done by the KMS + let salt = generate_salt(); + // Open both the source and the output file let mut reader = File::open("test").unwrap(); let mut writer = File::create("test.encrypted").unwrap(); @@ -43,27 +46,24 @@ fn encrypt() { KeyslotVersion::V1, ALGORITHM, HASHING_ALGORITHM, + salt, 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); + let mut header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots); + + header + .add_metadata( + MetadataVersion::V1, + ALGORITHM, + &master_key, + &embedded_metadata, + ) + .unwrap(); // Write the header to the file header.write(&mut writer).unwrap(); @@ -87,18 +87,8 @@ pub fn decrypt_metadata() { // 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(); + let file_info: FileInformation = header.decrypt_metadata(password).unwrap(); println!("file name: {}", file_info.file_name); } diff --git a/crates/crypto/examples/single_file_with_preview_media.rs b/crates/crypto/examples/single_file_with_preview_media.rs index 4e06e296b..3cac60bb5 100644 --- a/crates/crypto/examples/single_file_with_preview_media.rs +++ b/crates/crypto/examples/single_file_with_preview_media.rs @@ -5,7 +5,7 @@ use sd_crypto::{ header::{ file::{FileHeader, FileHeaderVersion}, keyslot::{Keyslot, KeyslotVersion}, - preview_media::{PreviewMedia, PreviewMediaVersion}, + preview_media::PreviewMediaVersion, }, keys::hashing::{HashingAlgorithm, Params}, primitives::{generate_master_key, generate_salt}, @@ -25,6 +25,9 @@ fn encrypt() { // This needs to be generated here, otherwise we won't have access to it for encryption let master_key = generate_master_key(); + // This ideally should be done by the KMS + let salt = generate_salt(); + // 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(); @@ -33,29 +36,21 @@ fn encrypt() { KeyslotVersion::V1, ALGORITHM, HASHING_ALGORITHM, + salt, 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)); + let mut header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots); + + header + .add_preview_media(PreviewMediaVersion::V1, ALGORITHM, &master_key, &pvm_media) + .unwrap(); // Write the header to the file header.write(&mut writer).unwrap(); @@ -79,15 +74,8 @@ pub fn decrypt_preview_media() { // 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(); + let media = header.decrypt_preview_media(password).unwrap(); println!("{:?}", media.expose()); } diff --git a/crates/crypto/src/crypto/stream.rs b/crates/crypto/src/crypto/stream.rs index aa086480f..750041e8f 100644 --- a/crates/crypto/src/crypto/stream.rs +++ b/crates/crypto/src/crypto/stream.rs @@ -13,6 +13,7 @@ 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)] +#[allow(clippy::use_self)] pub enum Algorithm { XChaCha20Poly1305, Aes256Gcm, @@ -127,6 +128,8 @@ impl StreamEncryption { })?; // zeroize before writing, so any potential errors won't result in a potential data leak + // this specific zeroize technically isn't needed due to the boxed slice, but performance impact is + // negligible and it's good practice either way read_buffer.zeroize(); // Using `write` instead of `write_all` so we can check the amount of bytes written diff --git a/crates/crypto/src/error.rs b/crates/crypto/src/error.rs index f5d0860dc..9d4f801f5 100644 --- a/crates/crypto/src/error.rs +++ b/crates/crypto/src/error.rs @@ -36,4 +36,12 @@ pub enum Error { NoMetadata, #[error("tried adding too many keyslots to a header")] TooManyKeyslots, + #[error("requested key wasn't found in the key manager")] + KeyNotFound, + #[error("no default key has been set")] + NoDefaultKeySet, + #[error("no master password has been provided to the keymanager")] + NoMasterPassword, + #[error("mismatch between supplied keys and the keystore")] + KeystoreMismatch, } diff --git a/crates/crypto/src/header/file.rs b/crates/crypto/src/header/file.rs index a6b1f6724..7b7a25d3a 100644 --- a/crates/crypto/src/header/file.rs +++ b/crates/crypto/src/header/file.rs @@ -82,8 +82,8 @@ impl FileHeader { version: FileHeaderVersion, algorithm: Algorithm, keyslots: Vec, - metadata: Option, - preview_media: Option, + //metadata: Option, + //preview_media: Option, ) -> Self { let nonce = generate_nonce(algorithm); @@ -92,12 +92,12 @@ impl FileHeader { algorithm, nonce, keyslots, - metadata, - preview_media, + metadata: None, + preview_media: None, } } - /// This is a helper function to decrypt a master key from keyslots that are attached to a header. + /// This is a helper function to decrypt a master key from keyslots that are attached to a header, from a user-supplied password. /// /// You receive an error if the password doesn't match or if there are no keyslots. #[allow(clippy::needless_pass_by_value)] @@ -124,6 +124,7 @@ impl FileHeader { } } + /// This is a helper function to serialize and write a header to a file. pub fn write(&self, writer: &mut W) -> Result<(), Error> where W: Write + Seek, @@ -132,6 +133,39 @@ impl FileHeader { Ok(()) } + /// This is a helper function to decrypt a master key from keyslots that are attached to a header. + /// + /// It takes in a Vec of pre-hashed keys, which is what the key manager returns + /// + /// 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_from_prehashed( + &self, + hashed_keys: Vec>, + ) -> Result, Error> { + let mut master_key = [0u8; MASTER_KEY_LEN]; + + if self.keyslots.is_empty() { + return Err(Error::NoKeyslots); + } + + for key in hashed_keys { + for keyslot in &self.keyslots { + if let Ok(decrypted_master_key) = + keyslot.decrypt_master_key_from_prehashed(key.clone()) + { + master_key.copy_from_slice(&decrypted_master_key); + } + } + } + + if master_key == [0u8; MASTER_KEY_LEN] { + Err(Error::IncorrectPassword) + } else { + Ok(Protected::new(master_key)) + } + } + /// This function should be used for generating AAD before encryption /// /// Use the return value from `FileHeader::deserialize()` for decryption diff --git a/crates/crypto/src/header/keyslot.rs b/crates/crypto/src/header/keyslot.rs index 87b24cbf6..e5ebf3d2d 100644 --- a/crates/crypto/src/header/keyslot.rs +++ b/crates/crypto/src/header/keyslot.rs @@ -27,9 +27,7 @@ use crate::{ crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, error::Error, keys::hashing::HashingAlgorithm, - primitives::{ - generate_nonce, generate_salt, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN, SALT_LEN, - }, + primitives::{generate_nonce, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN, SALT_LEN}, Protected, }; @@ -64,13 +62,13 @@ impl Keyslot { version: KeyslotVersion, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, + salt: [u8; SALT_LEN], 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 hashed_password = hashing_algorithm.hash(password, salt)?; let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes( hashed_password, @@ -107,6 +105,20 @@ impl Keyslot { StreamDecryption::decrypt_bytes(key, &self.nonce, self.algorithm, &self.master_key, &[]) } + /// This function should not be used directly, use `header.decrypt_master_key()` instead + /// + /// This attempts to decrypt the master key for a single keyslot, using a pre-hashed key + /// + /// No hashing is done internally. + /// + /// An error will be returned on failure. + pub fn decrypt_master_key_from_prehashed( + &self, + key: Protected<[u8; 32]>, + ) -> Result>, Error> { + 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 { diff --git a/crates/crypto/src/header/metadata.rs b/crates/crypto/src/header/metadata.rs index 4b993b08c..8b30d993e 100644 --- a/crates/crypto/src/header/metadata.rs +++ b/crates/crypto/src/header/metadata.rs @@ -32,14 +32,12 @@ 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, - }, + primitives::{generate_nonce, MASTER_KEY_LEN}, Protected, }; +use super::file::FileHeader; + /// 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. @@ -48,11 +46,7 @@ use crate::{ #[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 algorithm: Algorithm, // encryption algorithm pub metadata_nonce: Vec, pub metadata: Vec, } @@ -62,7 +56,7 @@ pub enum MetadataVersion { V1, } -impl Metadata { +impl FileHeader { /// This should be used for creating a header metadata item. /// /// It handles encrypting the master key and metadata. @@ -70,55 +64,101 @@ impl 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( + pub fn add_metadata( + &mut self, version: MetadataVersion, algorithm: Algorithm, - hashing_algorithm: HashingAlgorithm, - password: Protected>, - salt: &[u8; SALT_LEN], - media: &T, - ) -> Result + master_key: &Protected<[u8; MASTER_KEY_LEN]>, + metadata: &T, + ) -> Result<(), Error> 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, + master_key.clone(), &metadata_nonce, algorithm, - &serde_json::to_vec(media).map_err(|_| Error::MetadataDeSerialization)?, + &serde_json::to_vec(metadata).map_err(|_| Error::MetadataDeSerialization)?, &[], )?; - Ok(Self { + let metadata = Metadata { version, algorithm, - hashing_algorithm, - salt: *salt, - master_key: encrypted_master_key, - master_key_nonce, metadata_nonce, metadata: encrypted_metadata, - }) + }; + + self.metadata = Some(metadata); + + Ok(()) } + /// This function should be used to retrieve the metadata for a file + /// + /// All it requires is pre-hashed keys returned from the key manager + /// + /// A deserialized data type will be returned from this function + pub fn decrypt_metadata_from_prehashed( + &self, + hashed_keys: Vec>, + ) -> Result + where + T: serde::de::DeserializeOwned, + { + let master_key = self.decrypt_master_key_from_prehashed(hashed_keys)?; + + // could be an expensive clone (a few MiB at most) + if let Some(metadata) = self.metadata.clone() { + let metadata = StreamDecryption::decrypt_bytes( + master_key, + &metadata.metadata_nonce, + metadata.algorithm, + &metadata.metadata, + &[], + )?; + + serde_json::from_slice::(&metadata).map_err(|_| Error::MetadataDeSerialization) + } else { + Err(Error::NoMetadata) + } + } + + /// This function should be used to retrieve the metadata for a file + /// + /// All it requires is a password. Hashing is handled for you. + /// + /// A deserialized data type will be returned from this function + pub fn decrypt_metadata(&self, password: Protected>) -> Result + where + T: serde::de::DeserializeOwned, + { + let master_key = self.decrypt_master_key(password)?; + + // could be an expensive clone (a few MiB at most) + if let Some(metadata) = self.metadata.clone() { + let metadata = StreamDecryption::decrypt_bytes( + master_key, + &metadata.metadata_nonce, + metadata.algorithm, + &metadata.metadata, + &[], + )?; + + serde_json::from_slice::(&metadata).map_err(|_| Error::MetadataDeSerialization) + } else { + Err(Error::NoMetadata) + } + } +} + +impl Metadata { #[must_use] pub fn get_length(&self) -> usize { match self.version { - MetadataVersion::V1 => 128 + self.metadata.len(), + MetadataVersion::V1 => 36 + self.metadata.len(), } } @@ -132,55 +172,15 @@ impl Metadata { 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_nonce); // 24 max + metadata.extend_from_slice(&vec![0u8; 24 - self.metadata_nonce.len()]); // 28 + metadata.extend_from_slice(&self.metadata.len().to_le_bytes()); // 36 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 @@ -200,23 +200,6 @@ impl Metadata { 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)?; @@ -235,10 +218,6 @@ impl Metadata { let metadata = Self { version, algorithm, - hashing_algorithm, - salt, - master_key, - master_key_nonce, metadata_nonce, metadata, }; diff --git a/crates/crypto/src/header/preview_media.rs b/crates/crypto/src/header/preview_media.rs index cca29f208..9d316a2f6 100644 --- a/crates/crypto/src/header/preview_media.rs +++ b/crates/crypto/src/header/preview_media.rs @@ -25,14 +25,12 @@ 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, - }, + primitives::{generate_nonce, MASTER_KEY_LEN}, Protected, }; +use super::file::FileHeader; + /// 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. @@ -41,11 +39,7 @@ use crate::{ #[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 algorithm: Algorithm, // encryption algorithm pub media_nonce: Vec, pub media: Vec, } @@ -55,7 +49,7 @@ pub enum PreviewMediaVersion { V1, } -impl PreviewMedia { +impl FileHeader { /// This should be used for creating a header preview media item. /// /// This handles encrypting the master key and preview media. @@ -63,47 +57,95 @@ impl PreviewMedia { /// 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( + pub fn add_preview_media( + &mut self, version: PreviewMediaVersion, algorithm: Algorithm, - hashing_algorithm: HashingAlgorithm, - password: Protected>, - salt: &[u8; SALT_LEN], + master_key: &Protected<[u8; MASTER_KEY_LEN]>, media: &[u8], - ) -> Result { + ) -> Result<(), Error> { 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, + let encrypted_media = StreamEncryption::encrypt_bytes( + master_key.clone(), + &media_nonce, algorithm, - master_key.expose(), + media, &[], - )?)?; + )?; - let encrypted_media = - StreamEncryption::encrypt_bytes(master_key, &media_nonce, algorithm, media, &[])?; - - Ok(Self { + let pvm = PreviewMedia { version, algorithm, - hashing_algorithm, - salt: *salt, - master_key: encrypted_master_key, - master_key_nonce, media_nonce, media: encrypted_media, - }) + }; + + self.preview_media = Some(pvm); + + Ok(()) } + /// This function is what you'll want to use to get the preview media for a file + /// + /// All it requires is pre-hashed keys returned from the key manager + /// + /// Once provided, a `Vec` is returned that contains the preview media + pub fn decrypt_preview_media_from_prehashed( + &self, + hashed_keys: Vec>, + ) -> Result>, Error> { + let master_key = self.decrypt_master_key_from_prehashed(hashed_keys)?; + + // could be an expensive clone (a few MiB at most) + if let Some(pvm) = self.preview_media.clone() { + let media = StreamDecryption::decrypt_bytes( + master_key, + &pvm.media_nonce, + pvm.algorithm, + &pvm.media, + &[], + )?; + + Ok(media) + } else { + Err(Error::NoPreviewMedia) + } + } + + /// This function is what you'll want to use to get the preview media for a file + /// + /// All it requires is the user's password. Hashing is handled for you. + /// + /// Once provided, a `Vec` is returned that contains the preview media + pub fn decrypt_preview_media( + &self, + password: Protected>, + ) -> Result>, Error> { + let master_key = self.decrypt_master_key(password)?; + + // could be an expensive clone (a few MiB at most) + if let Some(pvm) = self.preview_media.clone() { + let media = StreamDecryption::decrypt_bytes( + master_key, + &pvm.media_nonce, + pvm.algorithm, + &pvm.media, + &[], + )?; + + Ok(media) + } else { + Err(Error::NoPreviewMedia) + } + } +} + +impl PreviewMedia { #[must_use] pub fn get_length(&self) -> usize { match self.version { - PreviewMediaVersion::V1 => 128 + self.media.len(), + PreviewMediaVersion::V1 => 36 + self.media.len(), } } @@ -117,55 +159,15 @@ impl PreviewMedia { 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_nonce); // 24 max + preview_media.extend_from_slice(&vec![0u8; 24 - self.media_nonce.len()]); // 28 total bytes + preview_media.extend_from_slice(&self.media.len().to_le_bytes()); // 36 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 @@ -186,23 +188,6 @@ impl PreviewMedia { 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)?; @@ -221,10 +206,6 @@ impl PreviewMedia { let preview_media = Self { version, algorithm, - hashing_algorithm, - salt, - master_key, - master_key_nonce, media_nonce, media, }; diff --git a/crates/crypto/src/keys/hashing.rs b/crates/crypto/src/keys/hashing.rs index 530b794eb..6ffde0955 100644 --- a/crates/crypto/src/keys/hashing.rs +++ b/crates/crypto/src/keys/hashing.rs @@ -17,7 +17,8 @@ use argon2::Argon2; /// These parameters define the password-hashing level. /// /// The harder the parameter, the longer the password will take to hash. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] +#[allow(clippy::use_self)] pub enum Params { Standard, Hardened, @@ -25,11 +26,20 @@ pub enum Params { } /// This defines all available password hashing algorithms. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum HashingAlgorithm { Argon2id(Params), } +/// This is so we can iterate over all hashing algorithms and parameters. +/// +/// The main usage is for pre-hashing a key during mounting. +pub const HASHING_ALGORITHM_LIST: [HashingAlgorithm; 3] = [ + HashingAlgorithm::Argon2id(Params::Standard), + HashingAlgorithm::Argon2id(Params::Hardened), + HashingAlgorithm::Argon2id(Params::Paranoid), +]; + impl HashingAlgorithm { /// This function should be used to hash passwords /// diff --git a/crates/crypto/src/keys/keymanager.rs b/crates/crypto/src/keys/keymanager.rs new file mode 100644 index 000000000..badca789f --- /dev/null +++ b/crates/crypto/src/keys/keymanager.rs @@ -0,0 +1,323 @@ +use std::collections::HashMap; + +use crate::crypto::stream::{StreamDecryption, StreamEncryption}; +use crate::error::Error; +use crate::primitives::{ + generate_master_key, generate_nonce, generate_salt, to_array, MASTER_KEY_LEN, +}; +use crate::{ + crypto::stream::Algorithm, + primitives::{ENCRYPTED_MASTER_KEY_LEN, SALT_LEN}, + Protected, +}; + +use uuid::Uuid; + +use super::hashing::{HashingAlgorithm, HASHING_ALGORITHM_LIST}; + +// The terminology in this file is very confusing. +// The `master_key` is specific to the `StoredKey`, and is just used internally for encryption. +// The `key` is what the user added/generated within their Spacedrive key manager. +// The `password` in this sense is the user's "master password", similar to a password manager's main password +// The `hashed_key` refers to the value you'd pass to PVM/MD decryption functions. It has been pre-hashed with the content salt. +// The content salt refers to the semi-universal salt that's used for metadata/preview media (unique to each key in the manager) + +#[derive(Clone, PartialEq, Eq)] +pub struct StoredKey { + pub uuid: uuid::Uuid, // uuid for identification. shared with mounted keys + pub algorithm: Algorithm, // encryption algorithm for encrypting the master key. can be changed (requires a re-encryption though) + pub hashing_algorithm: HashingAlgorithm, // hashing algorithm to use for hashing everything related to this key. can't be changed once set. + pub salt: [u8; SALT_LEN], // salt to hash the master password with + pub content_salt: [u8; SALT_LEN], // salt used for file data + pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], // this is for encrypting the `key` + pub master_key_nonce: Vec, // nonce for encrypting the master key + pub key_nonce: Vec, // nonce used for encrypting the main key + pub key: Vec, // encrypted. the key stored in spacedrive (e.g. generated 64 char key) +} + +pub struct KeyManager { + master_password: Option>>, // the user's. we take ownership here to prevent other functions attempting to manage/pass it to us + keystore: HashMap, + keymount: HashMap, + default: Option, +} + +/// The `KeyManager` functions should be used for all key-related management. +impl KeyManager { + /// Initialize the Key Manager with the user's master password, and `StoredKeys` retrieved from Prisma + #[must_use] + pub fn new(stored_keys: Vec, master_password: Option>>) -> Self { + let mut keystore = HashMap::new(); + for key in stored_keys { + keystore.insert(key.uuid, key); + } + + let keymount: HashMap = HashMap::new(); + + Self { + master_password, + keystore, + keymount, + default: None, + } + } + + /// This function should be used to populate the keystore with multiple stored keys at a time. + /// + /// It's suitable for when you created the key manager without populating it. + pub fn populate_keystore(&mut self, stored_keys: Vec) -> Result<(), Error> { + for key in stored_keys { + self.keystore.insert(key.uuid, key); + } + + Ok(()) + } + + /// This allows you to set the default key + pub fn set_default(&mut self, uuid: Uuid) -> Result<(), Error> { + if self.keystore.contains_key(&uuid) { + self.default = Some(uuid); + Ok(()) + } else { + Err(Error::KeyNotFound) + } + } + + /// This allows you to get the default key's ID + pub const fn get_default(&self) -> Result { + if let Some(default) = self.default { + Ok(default) + } else { + Err(Error::NoDefaultKeySet) + } + } + + /// This should ONLY be used within the key manager + fn get_master_password(&self) -> Result>, Error> { + match &self.master_password { + Some(k) => Ok(k.clone()), + None => Err(Error::NoMasterPassword), + } + } + + pub fn set_master_password( + &mut self, + master_password: Protected>, + ) -> Result<(), Error> { + // this returns a result, so we can potentially implement password checking functionality + self.master_password = Some(master_password); + Ok(()) + } + + #[must_use] + pub const fn has_master_password(&self) -> bool { + self.master_password.is_some() + } + + /// This function is used for emptying the entire keystore. + pub fn empty_keystore(&mut self) { + self.keystore.clear(); + } + + /// This function is used for unmounting all keys at once. + pub fn empty_keymount(&mut self) { + // i'm unsure whether or not `.clear()` also calls drop + // if it doesn't, we're going to need to find another way to call drop on these values + // that way they will be zeroized and removed from memory fully + self.keymount.clear(); + } + + /// This function can be used for comparing an array of `StoredKeys` to the currently loaded keystore. + pub fn compare_keystore(&self, supplied_keys: &[StoredKey]) -> Result<(), Error> { + if supplied_keys.len() != self.keystore.len() { + return Err(Error::KeystoreMismatch); + } + + for key in supplied_keys { + let keystore_key = match self.keystore.get(&key.uuid) { + Some(key) => key, + None => return Err(Error::KeystoreMismatch), + }; + + if key != keystore_key { + return Err(Error::KeystoreMismatch); + } + } + + Ok(()) + } + + pub fn unmount(&mut self, uuid: Uuid) -> Result<(), Error> { + if self.keymount.contains_key(&uuid) { + self.keymount.remove(&uuid); + Ok(()) + } else { + Err(Error::KeyNotFound) + } + } + + /// This function returns a Vec of `StoredKey`s, so you can write them somewhere/update the database with them/etc + /// + /// The database and keystore should be in sync at ALL times + pub fn dump_keystore(&self) -> Result, Error> { + let mut keys = Vec::::new(); + + for key in self.keystore.values() { + keys.push(key.clone()); + } + + Ok(keys) + } + + /// This function does not return a value by design. + /// Once a key is mounted, access it with `KeyManager::access()` + /// This is to ensure that only functions which require access to the mounted key receive it. + /// We could add a log to this, so that the user can view mounts + pub fn mount(&mut self, uuid: Uuid) -> Result<(), Error> { + match self.keystore.get(&uuid) { + Some(stored_key) => { + let master_password = self.get_master_password()?; + + let hashed_password = stored_key + .hashing_algorithm + .hash(master_password, stored_key.salt)?; + + let mut master_key = [0u8; MASTER_KEY_LEN]; + + // Decrypt the StoredKey's master key using the user's hashed password + let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes( + hashed_password, + &stored_key.master_key_nonce, + stored_key.algorithm, + &stored_key.master_key, + &[], + ) { + master_key.copy_from_slice(&decrypted_master_key); + Ok(Protected::new(master_key)) + } else { + Err(Error::IncorrectPassword) + }?; + + // Decrypt the StoredKey using the decrypted master key + let key = StreamDecryption::decrypt_bytes( + master_key, + &stored_key.key_nonce, + stored_key.algorithm, + &stored_key.key, + &[], + )?; + + let mut hashed_keys = Vec::>::new(); + + // Hash the StoredKey using each available password hashing parameter, so all content is accessible no matter the settings. + // It makes key mounting more expensive, but it allows for greater UX and customizability. + for hashing_algorithm in HASHING_ALGORITHM_LIST { + hashed_keys.push(hashing_algorithm.hash(key.clone(), stored_key.content_salt)?); + } + + // Construct the MountedKey and insert it into the Keymount + let mounted_key = MountedKey { + uuid: stored_key.uuid, + key, + content_salt: stored_key.content_salt, + hashed_keys, + }; + + self.keymount.insert(uuid, mounted_key); + + Ok(()) + } + None => Err(Error::KeyNotFound), + } + } + + /// This function is for accessing the internal keymount. + /// + /// We could add a log to this, so that the user can view accesses + pub fn access_keymount(&self, uuid: Uuid) -> Result { + match self.keymount.get(&uuid) { + Some(key) => Ok(key.clone()), + None => Err(Error::KeyNotFound), + } + } + + /// This function is for accessing a `StoredKey` from an ID. + pub fn access_keystore(&self, uuid: Uuid) -> Result { + match self.keystore.get(&uuid) { + Some(key) => Ok(key.clone()), + None => Err(Error::KeyNotFound), + } + } + + /// This function is used to add a new key/password to the keystore. + /// + /// You should use this when a new key is added, as it will generate salts/nonces/etc. + /// + /// It does not mount the key, it just registers it. + /// + /// Once added, you will need to use `KeyManager::access_keystore()` to retrieve it and add it to Prisma. + /// + /// You may use the returned ID to identify this key. + #[allow(clippy::needless_pass_by_value)] + pub fn add_to_keystore( + &mut self, + key: Protected>, + algorithm: Algorithm, + hashing_algorithm: HashingAlgorithm, + ) -> Result { + let master_password = self.get_master_password()?; + + let uuid = uuid::Uuid::new_v4(); + + // Generate items we'll need for encryption + let key_nonce = generate_nonce(algorithm); + let master_key = generate_master_key(); + let master_key_nonce = generate_nonce(algorithm); + let salt = generate_salt(); + let content_salt = generate_salt(); // for PVM/MD + + // Hash the user's master password + let hashed_password = hashing_algorithm.hash(master_password, salt)?; + + // Encrypted the master key with the user's hashed password + let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes( + hashed_password, + &master_key_nonce, + algorithm, + master_key.expose(), + &[], + )?)?; + + // Encrypt the actual key (e.g. user-added/autogenerated, text-encodable) + let encrypted_key = + StreamEncryption::encrypt_bytes(master_key, &key_nonce, algorithm, &key, &[])?; + + // Construct the StoredKey + let stored_key = StoredKey { + uuid, + algorithm, + hashing_algorithm, + salt, + content_salt, + master_key: encrypted_master_key, + master_key_nonce, + key_nonce, + key: encrypted_key, + }; + + // Insert it into the Keystore + self.keystore.insert(stored_key.uuid, stored_key); + + // Return the ID so it can be identified + Ok(uuid) + } +} + +// derive explicit CLONES only +#[derive(Clone)] +pub struct MountedKey { + pub uuid: Uuid, // used for identification. shared with stored keys + pub key: Protected>, // the actual key itself, text format encodable (so it can be viewed with an UI) + pub content_salt: [u8; SALT_LEN], // the salt used for file data + pub hashed_keys: Vec>, // this is hashed with the content salt, for instant access +} diff --git a/crates/crypto/src/keys/mod.rs b/crates/crypto/src/keys/mod.rs index dc007c115..9c7da697a 100644 --- a/crates/crypto/src/keys/mod.rs +++ b/crates/crypto/src/keys/mod.rs @@ -1,2 +1,3 @@ //! This module contains all key and hashing related functions. pub mod hashing; +pub mod keymanager;