From d1b6263ae7e2efaf1bceb9bc5171450caaee8921 Mon Sep 17 00:00:00 2001 From: jake <77554505+brxken128@users.noreply.github.com> Date: Tue, 7 Feb 2023 12:03:12 +0000 Subject: [PATCH] [ENG-355] Keychain integration (and some typesafety) (#558) * update crypto MSRV * rename `keychain` to `keyring` * make a start on the keymanager unlock refactor/keychain integration * update routes * update bindings * add const identifiers * add UI/front-end support for unlocking KM with OS keychains * remove SK from lib creation dialog * update query name * add keyring functions * attempt to update `change_master_password()` to use the keychain * cleanup, fix master password change ui, better secret key in keyring detection * cleanup TS a little * add route for getting secret key from keyring * update bindings * update var names + show secret key in keys settings * add `react-qr-code` and option to view the secret key (if it's in the OS keyring) * allow copying SK to clipboard * add `key_type` so we're not reliant on specific UUIDs for root/verification key handling * clippy * fix mobile typecheck * fix typecheck, fix typo and tweak balloon hash parameters * minor cleanup + typo fix * use newtype structs * WIP type refactoring (major readability boost!) * update `use` * add tokio `sync` feature * too many structs? idk * more cleanup * add `generate` and `Nonce` * `Nonce` and `Key` typesafety (beautiful) * clippy + cleanup * update code & examples * fix bug & remove `ProtectedVec` as it looked out of place * use `Key` * add a query invalidation to make the UI extremely responsive * ci pls work * remove `keyringHasSk` route --- Cargo.lock | Bin 190980 -> 190988 bytes apps/cli/src/main.rs | 10 +- .../containers/dialog/CreateLibraryDialog.tsx | 1 - .../20230202133507_keytype/migration.sql | 74 ++++ core/prisma/schema.prisma | 1 + core/src/api/keys.rs | 91 ++-- core/src/api/libraries.rs | 6 +- core/src/library/library_manager.rs | 16 +- core/src/object/fs/decrypt.rs | 17 +- core/src/object/fs/encrypt.rs | 9 +- core/src/util/db.rs | 3 +- crates/crypto/Cargo.toml | 10 +- crates/crypto/examples/single_file.rs | 15 +- .../examples/single_file_with_metadata.rs | 13 +- .../single_file_with_preview_media.rs | 13 +- crates/crypto/src/crypto/stream.rs | 32 +- crates/crypto/src/header/file.rs | 93 ++-- crates/crypto/src/header/keyslot.rs | 65 +-- crates/crypto/src/header/metadata.rs | 80 ++-- crates/crypto/src/header/preview_media.rs | 38 +- crates/crypto/src/keys/hashing.rs | 47 +- crates/crypto/src/keys/keymanager.rs | 406 ++++++++++++------ .../src/keys/{keychain => keyring}/apple.rs | 6 +- .../src/keys/{keychain => keyring}/linux.rs | 7 +- .../src/keys/{keychain => keyring}/mod.rs | 6 +- crates/crypto/src/keys/mod.rs | 2 +- crates/crypto/src/lib.rs | 1 - crates/crypto/src/primitives.rs | 151 ------- crates/crypto/src/primitives/mod.rs | 64 +++ crates/crypto/src/primitives/types.rs | 282 ++++++++++++ crates/crypto/src/protected.rs | 13 - packages/client/src/core.ts | 23 +- packages/interface/package.json | 1 + .../components/dialog/BackupRestoreDialog.tsx | 4 +- .../components/dialog/CreateLibraryDialog.tsx | 40 -- .../dialog/MasterPasswordChangeDialog.tsx | 49 +-- .../explorer/ExplorerContextMenu.tsx | 14 +- .../src/components/key/KeyManager.tsx | 64 ++- .../screens/settings/library/KeysSetting.tsx | 98 +++-- pnpm-lock.yaml | Bin 780136 -> 780769 bytes 40 files changed, 1146 insertions(+), 719 deletions(-) create mode 100644 core/prisma/migrations/20230202133507_keytype/migration.sql rename crates/crypto/src/keys/{keychain => keyring}/apple.rs (78%) rename crates/crypto/src/keys/{keychain => keyring}/linux.rs (84%) rename crates/crypto/src/keys/{keychain => keyring}/mod.rs (88%) delete mode 100644 crates/crypto/src/primitives.rs create mode 100644 crates/crypto/src/primitives/mod.rs create mode 100644 crates/crypto/src/primitives/types.rs diff --git a/Cargo.lock b/Cargo.lock index 8eb8ab7c51b026bc5e5cdae8d6b0e34e7a3762db..980e738f125836cbebb992eae754367d2d3e3dbc 100644 GIT binary patch delta 21 dcmZpVM{p)Qas1{~4FO0sw4{3RM6A delta 22 ecmeCV!rgL(yI~7s>i_AikxYWy>;5zLy#fGom, library_sync: bool, automount: bool, } #[derive(Type, Deserialize)] pub struct UnlockKeyManagerArgs { - password: String, - secret_key: Option, + password: Protected, + secret_key: Protected, } #[derive(Type, Deserialize)] pub struct RestoreBackupArgs { - password: String, - secret_key: Option, + password: Protected, + secret_key: Protected, path: PathBuf, } #[derive(Type, Deserialize)] pub struct MasterPasswordChangeArgs { - password: String, - secret_key: Option, + password: Protected, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, } @@ -55,10 +56,8 @@ pub(crate) fn mount() -> RouterBuilder { t(|_, _: (), library| async move { Ok(library.key_manager.dump_keystore()) }) }) // do not unlock the key manager until this route returns true - .library_query("hasMasterPassword", |t| { - t( - |_, _: (), library| async move { Ok(library.key_manager.has_master_password().await?) }, - ) + .library_query("isUnlocked", |t| { + t(|_, _: (), library| async move { Ok(library.key_manager.is_unlocked().await?) }) }) // this is so we can show the key as mounted in the UI .library_query("listMounted", |t| { @@ -66,9 +65,12 @@ pub(crate) fn mount() -> RouterBuilder { }) .library_query("getKey", |t| { t(|_, key_uuid: Uuid, library| async move { - let key = library.key_manager.get_key(key_uuid).await?; - - Ok(String::from_utf8(key.into_inner()).map_err(Error::StringParse)?) + Ok(library + .key_manager + .get_key(key_uuid) + .await? + .expose() + .clone()) }) }) .library_mutation("mount", |t| { @@ -79,6 +81,27 @@ pub(crate) fn mount() -> RouterBuilder { Ok(()) }) }) + .library_query("getSecretKey", |t| { + t(|_, _: (), library| async move { + if library + .key_manager + .keyring_contains_valid_secret_key(library.id) + .await + .is_ok() + { + Ok(Some( + library + .key_manager + .keyring_retrieve(library.id, SECRET_KEY_IDENTIFIER.to_string()) + .await? + .expose() + .clone(), + )) + } else { + Ok(None) + } + }) + }) .library_mutation("unmount", |t| { t(|_, key_uuid: Uuid, library| async move { library.key_manager.unmount(key_uuid)?; @@ -92,7 +115,7 @@ pub(crate) fn mount() -> RouterBuilder { // This technically clears the root key, but it means the same thing to the frontend library.key_manager.clear_root_key().await?; - invalidate_query!(library, "keys.hasMasterPassword"); + invalidate_query!(library, "keys.isUnlocked"); Ok(()) }) }) @@ -152,17 +175,19 @@ pub(crate) fn mount() -> RouterBuilder { }) .library_mutation("unlockKeyManager", |t| { t(|_, args: UnlockKeyManagerArgs, library| async move { - // if this returns an error, the user MUST re-enter the correct password + let secret_key = (!args.secret_key.expose().is_empty()).then_some(args.secret_key); + library .key_manager .unlock( - Protected::new(args.password), - args.secret_key.map(Protected::new), + Password(args.password), + secret_key.map(SecretKeyString), + library.id, || invalidate_query!(library, "keys.isKeyManagerUnlocking"), ) .await?; - invalidate_query!(library, "keys.hasMasterPassword"); + invalidate_query!(library, "keys.isUnlocked"); let automount = library .db @@ -215,7 +240,7 @@ pub(crate) fn mount() -> RouterBuilder { t(|_, _: (), library| async move { library.key_manager.get_default().await.ok() }) }) .library_query("isKeyManagerUnlocking", |t| { - t(|_, _: (), library| async move { library.key_manager.is_queued(Uuid::nil()) }) + t(|_, _: (), library| async move { library.key_manager.is_unlocking().await.ok() }) }) .library_mutation("unmountAll", |t| { t(|_, _: (), library| async move { @@ -231,7 +256,7 @@ pub(crate) fn mount() -> RouterBuilder { let uuid = library .key_manager .add_to_keystore( - Protected::new(args.key.as_bytes().to_vec()), + Password(args.key), args.algorithm, args.hashing_algorithm, !args.library_sync, @@ -297,11 +322,13 @@ pub(crate) fn mount() -> RouterBuilder { let stored_keys: Vec = serde_json::from_slice(&backup).map_err(|_| Error::Serialization)?; - let secret_key = args.secret_key.map(Protected::new); - let updated_keys = library .key_manager - .import_keystore_backup(Protected::new(args.password), secret_key, &stored_keys) + .import_keystore_backup( + args.password, + SecretKeyString(args.secret_key), + &stored_keys, + ) .await?; for key in &updated_keys { @@ -316,23 +343,25 @@ pub(crate) fn mount() -> RouterBuilder { }) .library_mutation("changeMasterPassword", |t| { t(|_, args: MasterPasswordChangeArgs, library| async move { - let secret_key = args.secret_key.map(Protected::new); - let verification_key = library .key_manager .change_master_password( - Protected::new(args.password), + args.password, args.algorithm, args.hashing_algorithm, - secret_key, + library.id, ) .await?; - // remove old nil-id keys if they were set + invalidate_query!(library, "keys.getSecretKey"); + + // remove old root key if present library .db .key() - .delete_many(vec![key::uuid::equals(Uuid::nil().to_string())]) + .delete_many(vec![key::key_type::equals( + serde_json::to_string(&StoredKeyType::Root).unwrap(), + )]) .exec() .await?; diff --git a/core/src/api/libraries.rs b/core/src/api/libraries.rs index aface8af7..6732556c6 100644 --- a/core/src/api/libraries.rs +++ b/core/src/api/libraries.rs @@ -9,8 +9,8 @@ use chrono::Utc; use fs_extra::dir::get_size; // TODO: Remove this dependency as it is sync instead of async use rspc::Type; use sd_crypto::{ - crypto::stream::Algorithm, keys::hashing::HashingAlgorithm, primitives::OnboardingConfig, - Protected, + crypto::stream::Algorithm, keys::hashing::HashingAlgorithm, + primitives::types::OnboardingConfig, Protected, }; use serde::Deserialize; use tokio::fs; @@ -81,7 +81,6 @@ pub(crate) fn mount() -> RouterBuilder { pub struct CreateLibraryArgs { name: String, password: Protected, - secret_key: Option>, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, } @@ -96,7 +95,6 @@ pub(crate) fn mount() -> RouterBuilder { }, OnboardingConfig { password: args.password, - secret_key: args.secret_key, algorithm: args.algorithm, hashing_algorithm: args.hashing_algorithm, }, diff --git a/core/src/library/library_manager.rs b/core/src/library/library_manager.rs index 24d476b14..5e93ed30c 100644 --- a/core/src/library/library_manager.rs +++ b/core/src/library/library_manager.rs @@ -12,7 +12,7 @@ use crate::{ use sd_crypto::{ keys::keymanager::{KeyManager, StoredKey}, - primitives::{to_array, OnboardingConfig}, + primitives::types::{EncryptedKey, Nonce, OnboardingConfig, Salt}, }; use std::{ env, fs, io, @@ -93,16 +93,18 @@ pub async fn seed_keymanager( uuid, version: serde_json::from_str(&key.version) .map_err(|_| sd_crypto::Error::Serialization)?, + key_type: serde_json::from_str(&key.key_type) + .map_err(|_| sd_crypto::Error::Serialization)?, algorithm: serde_json::from_str(&key.algorithm) .map_err(|_| sd_crypto::Error::Serialization)?, - content_salt: to_array(key.content_salt)?, - master_key: to_array(key.master_key)?, - master_key_nonce: key.master_key_nonce, - key_nonce: key.key_nonce, + content_salt: Salt::try_from(key.content_salt)?, + master_key: EncryptedKey::try_from(key.master_key)?, + master_key_nonce: Nonce::try_from(key.master_key_nonce)?, + key_nonce: Nonce::try_from(key.key_nonce)?, key: key.key, hashing_algorithm: serde_json::from_str(&key.hashing_algorithm) .map_err(|_| sd_crypto::Error::Serialization)?, - salt: to_array(key.salt)?, + salt: Salt::try_from(key.salt)?, memory_only: false, automount: key.automount, }) @@ -197,7 +199,7 @@ impl LibraryManager { indexer_rules_seeder(&library.db).await?; // setup master password - let verification_key = KeyManager::onboarding(km_config).await?; + let verification_key = KeyManager::onboarding(km_config, library.id).await?; write_storedkey_to_db(&library.db, &verification_key).await?; diff --git a/core/src/object/fs/decrypt.rs b/core/src/object/fs/decrypt.rs index d99219786..9b93dd37d 100644 --- a/core/src/object/fs/decrypt.rs +++ b/core/src/object/fs/decrypt.rs @@ -1,4 +1,7 @@ -use sd_crypto::{crypto::stream::StreamDecryption, header::file::FileHeader, Protected}; +use sd_crypto::{ + crypto::stream::StreamDecryption, header::file::FileHeader, primitives::types::Password, + Protected, +}; use serde::{Deserialize, Serialize}; use specta::Type; use std::{collections::VecDeque, path::PathBuf}; @@ -88,17 +91,17 @@ impl StatefulJob for FileDecryptorJob { let master_key = if let Some(password) = state.init.password.clone() { if let Some(save_to_library) = state.init.save_to_library { - let password = Protected::new(password.into_bytes()); - // we can do this first, as `find_key_index` requires a successful decryption (just like `decrypt_master_key`) + let password_bytes = Protected::new(password.as_bytes().to_vec()); + if save_to_library { - let index = header.find_key_index(password.clone()).await?; + let index = header.find_key_index(password_bytes.clone()).await?; // inherit the encryption algorithm from the keyslot ctx.library_ctx .key_manager .add_to_keystore( - password.clone(), + Password::new(password), header.algorithm, header.keyslots[index].hashing_algorithm, false, @@ -108,7 +111,7 @@ impl StatefulJob for FileDecryptorJob { .await?; } - header.decrypt_master_key(password).await? + header.decrypt_master_key(password_bytes).await? } else { return Err(JobError::JobDataNotFound(String::from( "Password decryption selected, but save to library boolean was not included", @@ -120,7 +123,7 @@ impl StatefulJob for FileDecryptorJob { header.decrypt_master_key_from_prehashed(keys).await? }; - let decryptor = StreamDecryption::new(master_key, &header.nonce, header.algorithm)?; + let decryptor = StreamDecryption::new(master_key, header.nonce, header.algorithm)?; decryptor .decrypt_streams(&mut reader, &mut writer, &aad) diff --git a/core/src/object/fs/encrypt.rs b/core/src/object/fs/encrypt.rs index e7aa225a5..9dc1f8783 100644 --- a/core/src/object/fs/encrypt.rs +++ b/core/src/object/fs/encrypt.rs @@ -7,8 +7,7 @@ use sd_crypto::{ crypto::stream::{Algorithm, StreamEncryption}, header::{file::FileHeader, keyslot::Keyslot}, primitives::{ - generate_master_key, LATEST_FILE_HEADER, LATEST_KEYSLOT, LATEST_METADATA, - LATEST_PREVIEW_MEDIA, + types::Key, LATEST_FILE_HEADER, LATEST_KEYSLOT, LATEST_METADATA, LATEST_PREVIEW_MEDIA, }, }; use serde::{Deserialize, Serialize}; @@ -130,7 +129,7 @@ impl StatefulJob for FileEncryptorJob { let mut reader = File::open(&info.fs_path).await?; let mut writer = File::create(output_path).await?; - let master_key = generate_master_key(); + let master_key = Key::generate(); let mut header = FileHeader::new( LATEST_FILE_HEADER, @@ -146,7 +145,7 @@ impl StatefulJob for FileEncryptorJob { ) .await?, ], - ); + )?; if state.init.metadata || state.init.preview_media { // if any are requested, we can make the query as it'll be used at least once @@ -208,7 +207,7 @@ impl StatefulJob for FileEncryptorJob { header.write(&mut writer).await?; - let encryptor = StreamEncryption::new(master_key, &header.nonce, header.algorithm)?; + let encryptor = StreamEncryption::new(master_key, header.nonce, header.algorithm)?; encryptor .encrypt_streams(&mut reader, &mut writer, &header.generate_aad()) diff --git a/core/src/util/db.rs b/core/src/util/db.rs index e1483b9a6..fab8524d5 100644 --- a/core/src/util/db.rs +++ b/core/src/util/db.rs @@ -54,9 +54,10 @@ pub async fn write_storedkey_to_db( .create( key.uuid.to_string(), serde_json::to_string(&key.version)?, + serde_json::to_string(&key.key_type)?, serde_json::to_string(&key.algorithm)?, serde_json::to_string(&key.hashing_algorithm)?, - key.content_salt.to_vec(), + key.content_salt.0.to_vec(), key.master_key.to_vec(), key.master_key_nonce.to_vec(), key.key_nonce.to_vec(), diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index 1281de2b3..498a80832 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Jake Robinson "] readme = "README.md" description = "A library to handle cryptographic functions within Spacedrive" edition = "2021" -rust-version = "1.64.0" +rust-version = "1.67.0" [dependencies] # rng @@ -44,13 +44,15 @@ rspc = { workspace = true, features = ["uuid"], optional = true } specta = { workspace = true, optional = true } # for asynchronous crypto -tokio = { workspace = true, features = ["io-util", "rt-multi-thread"] } +tokio = { workspace = true, features = ["io-util", "rt-multi-thread", "sync"] } -# linux OS keychain +hex = "0.4.3" + +# linux OS keyring [target.'cfg(target_os = "linux")'.dependencies] secret-service = "2.0.2" -# macos/ios OS keychain +# macos/ios OS keyring [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] security-framework = "2.8.1" diff --git a/crates/crypto/examples/single_file.rs b/crates/crypto/examples/single_file.rs index 142d6a0e6..3db4d3a9b 100644 --- a/crates/crypto/examples/single_file.rs +++ b/crates/crypto/examples/single_file.rs @@ -4,7 +4,10 @@ use sd_crypto::{ crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, header::{file::FileHeader, keyslot::Keyslot}, keys::hashing::{HashingAlgorithm, Params}, - primitives::{generate_master_key, generate_salt, LATEST_FILE_HEADER, LATEST_KEYSLOT}, + primitives::{ + types::{Key, Salt}, + LATEST_FILE_HEADER, LATEST_KEYSLOT, + }, Protected, }; @@ -19,10 +22,10 @@ async fn encrypt() { let mut writer = File::create("test.encrypted").await.unwrap(); // This needs to be generated here, otherwise we won't have access to it for encryption - let master_key = generate_master_key(); + let master_key = Key::generate(); // These should ideally be done by a key management system - let content_salt = generate_salt(); + let content_salt = Salt::generate(); let hashed_password = HASHING_ALGORITHM .hash(password, content_salt, None) .unwrap(); @@ -40,13 +43,13 @@ async fn encrypt() { .unwrap()]; // Create the header for the encrypted file - let header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots); + let header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots).unwrap(); // Write the header to the file header.write(&mut writer).await.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(); + 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 @@ -70,7 +73,7 @@ async fn decrypt() { let master_key = header.decrypt_master_key(password).await.unwrap(); // Initialize a stream decryption object using data provided by the header - let decryptor = StreamDecryption::new(master_key, &header.nonce, header.algorithm).unwrap(); + let decryptor = StreamDecryption::new(master_key, header.nonce, header.algorithm).unwrap(); // Decrypt data the from the writer, and write it to the writer decryptor diff --git a/crates/crypto/examples/single_file_with_metadata.rs b/crates/crypto/examples/single_file_with_metadata.rs index ee48c1e00..73bf31e95 100644 --- a/crates/crypto/examples/single_file_with_metadata.rs +++ b/crates/crypto/examples/single_file_with_metadata.rs @@ -4,7 +4,10 @@ use sd_crypto::{ crypto::stream::{Algorithm, StreamEncryption}, header::{file::FileHeader, keyslot::Keyslot, metadata::MetadataVersion}, keys::hashing::{HashingAlgorithm, Params}, - primitives::{generate_master_key, generate_salt, LATEST_FILE_HEADER, LATEST_KEYSLOT}, + primitives::{ + types::{Key, Salt}, + LATEST_FILE_HEADER, LATEST_KEYSLOT, + }, Protected, }; use tokio::fs::File; @@ -28,10 +31,10 @@ async fn encrypt() { let mut writer = File::create("test.encrypted").await.unwrap(); // This needs to be generated here, otherwise we won't have access to it for encryption - let master_key = generate_master_key(); + let master_key = Key::generate(); // These should ideally be done by a key management system - let content_salt = generate_salt(); + let content_salt = Salt::generate(); let hashed_password = HASHING_ALGORITHM .hash(password, content_salt, None) .unwrap(); @@ -49,7 +52,7 @@ async fn encrypt() { .unwrap()]; // Create the header for the encrypted file (and include our metadata) - let mut header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots); + let mut header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots).unwrap(); header .add_metadata( @@ -65,7 +68,7 @@ async fn encrypt() { header.write(&mut writer).await.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(); + 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 diff --git a/crates/crypto/examples/single_file_with_preview_media.rs b/crates/crypto/examples/single_file_with_preview_media.rs index 032f87447..de1c1a596 100644 --- a/crates/crypto/examples/single_file_with_preview_media.rs +++ b/crates/crypto/examples/single_file_with_preview_media.rs @@ -4,7 +4,10 @@ use sd_crypto::{ crypto::stream::{Algorithm, StreamEncryption}, header::{file::FileHeader, keyslot::Keyslot, preview_media::PreviewMediaVersion}, keys::hashing::{HashingAlgorithm, Params}, - primitives::{generate_master_key, generate_salt, LATEST_FILE_HEADER, LATEST_KEYSLOT}, + primitives::{ + types::{Key, Salt}, + LATEST_FILE_HEADER, LATEST_KEYSLOT, + }, Protected, }; @@ -19,10 +22,10 @@ async fn encrypt() { let mut writer = File::create("test.encrypted").await.unwrap(); // This needs to be generated here, otherwise we won't have access to it for encryption - let master_key = generate_master_key(); + let master_key = Key::generate(); // These should ideally be done by a key management system - let content_salt = generate_salt(); + let content_salt = Salt::generate(); let hashed_password = HASHING_ALGORITHM .hash(password, content_salt, None) .unwrap(); @@ -42,7 +45,7 @@ async fn encrypt() { let pvm_media = b"a nice mountain".to_vec(); // Create the header for the encrypted file (and include our preview media) - let mut header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots); + let mut header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots).unwrap(); header .add_preview_media( @@ -58,7 +61,7 @@ async fn encrypt() { header.write(&mut writer).await.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(); + 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 diff --git a/crates/crypto/src/crypto/stream.rs b/crates/crypto/src/crypto/stream.rs index e6e431e90..15a4f75be 100644 --- a/crates/crypto/src/crypto/stream.rs +++ b/crates/crypto/src/crypto/stream.rs @@ -4,8 +4,10 @@ use std::io::Cursor; use crate::{ - primitives::{Key, AEAD_TAG_SIZE, BLOCK_SIZE}, - protected::ProtectedVec, + primitives::{ + types::{Key, Nonce}, + AEAD_TAG_SIZE, BLOCK_SIZE, + }, Error, Protected, Result, }; use aead::{ @@ -55,7 +57,7 @@ impl StreamEncryption { /// /// The master key, a suitable nonce, and a specific algorithm should be provided. #[allow(clippy::needless_pass_by_value)] - pub fn new(key: Protected, nonce: &[u8], algorithm: Algorithm) -> Result { + pub fn new(key: Key, nonce: Nonce, algorithm: Algorithm) -> Result { if nonce.len() != algorithm.nonce_len() { return Err(Error::NonceLengthMismatch); } @@ -65,14 +67,14 @@ impl StreamEncryption { let cipher = XChaCha20Poly1305::new_from_slice(key.expose()) .map_err(|_| Error::StreamModeInit)?; - let stream = EncryptorLE31::from_aead(cipher, nonce.into()); + 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()); + let stream = EncryptorLE31::from_aead(cipher, (&*nonce).into()); Self::Aes256Gcm(Box::new(stream)) } }; @@ -137,7 +139,6 @@ impl StreamEncryption { }; let encrypted_data = self.encrypt_next(payload).map_err(|_| Error::Encrypt)?; - writer.write_all(&encrypted_data).await?; } else { // we use `..read_count` in order to only use the read data, and not zeroes also @@ -148,7 +149,6 @@ impl StreamEncryption { let encrypted_data = self.encrypt_last(payload).map_err(|_| Error::Encrypt)?; writer.write_all(&encrypted_data).await?; - break; } } @@ -163,8 +163,8 @@ impl StreamEncryption { /// It is just a thin wrapper around `encrypt_streams()`, but reduces the amount of code needed elsewhere. #[allow(unused_mut)] pub async fn encrypt_bytes( - key: Protected, - nonce: &[u8], + key: Key, + nonce: Nonce, algorithm: Algorithm, bytes: &[u8], aad: &[u8], @@ -184,7 +184,7 @@ impl StreamDecryption { /// /// 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, nonce: &[u8], algorithm: Algorithm) -> Result { + pub fn new(key: Key, nonce: Nonce, algorithm: Algorithm) -> Result { if nonce.len() != algorithm.nonce_len() { return Err(Error::NonceLengthMismatch); } @@ -194,14 +194,14 @@ impl StreamDecryption { let cipher = XChaCha20Poly1305::new_from_slice(key.expose()) .map_err(|_| Error::StreamModeInit)?; - let stream = DecryptorLE31::from_aead(cipher, nonce.into()); + 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()); + let stream = DecryptorLE31::from_aead(cipher, (&*nonce).into()); Self::Aes256Gcm(Box::new(stream)) } }; @@ -266,7 +266,6 @@ impl StreamDecryption { }; let decrypted_data = self.decrypt_next(payload).map_err(|_| Error::Decrypt)?; - writer.write_all(&decrypted_data).await?; } else { let payload = Payload { @@ -276,7 +275,6 @@ impl StreamDecryption { let decrypted_data = self.decrypt_last(payload).map_err(|_| Error::Decrypt)?; writer.write_all(&decrypted_data).await?; - break; } } @@ -291,12 +289,12 @@ impl StreamDecryption { /// It is just a thin wrapper around `decrypt_streams()`, but reduces the amount of code needed elsewhere. #[allow(unused_mut)] pub async fn decrypt_bytes( - key: Protected, - nonce: &[u8], + key: Key, + nonce: Nonce, algorithm: Algorithm, bytes: &[u8], aad: &[u8], - ) -> Result> { + ) -> Result>> { let mut writer = Cursor::new(Vec::::new()); let decryptor = Self::new(key, nonce, algorithm)?; diff --git a/crates/crypto/src/header/file.rs b/crates/crypto/src/header/file.rs index 60b3659ef..3ae4accde 100644 --- a/crates/crypto/src/header/file.rs +++ b/crates/crypto/src/header/file.rs @@ -35,8 +35,7 @@ use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; use crate::{ crypto::stream::Algorithm, - primitives::{generate_nonce, to_array, Key, KEY_LEN}, - protected::ProtectedVec, + primitives::types::{Key, Nonce}, Error, Protected, Result, }; @@ -61,7 +60,7 @@ pub const MAGIC_BYTES: [u8; 7] = [0x62, 0x61, 0x6C, 0x6C, 0x61, 0x70, 0x70]; pub struct FileHeader { pub version: FileHeaderVersion, pub algorithm: Algorithm, - pub nonce: Vec, + pub nonce: Nonce, pub keyslots: Vec, pub metadata: Option, pub preview_media: Option, @@ -75,16 +74,21 @@ pub enum FileHeaderVersion { impl FileHeader { /// This function is used for creating a file header. - #[must_use] - pub fn new(version: FileHeaderVersion, algorithm: Algorithm, keyslots: Vec) -> Self { - Self { + pub fn new( + version: FileHeaderVersion, + algorithm: Algorithm, + keyslots: Vec, + ) -> Result { + let f = Self { version, algorithm, - nonce: generate_nonce(algorithm), + nonce: Nonce::generate(algorithm)?, keyslots, metadata: None, preview_media: None, - } + }; + + Ok(f) } /// 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) @@ -101,18 +105,13 @@ impl FileHeader { /// /// You receive an error if the password doesn't match or if there are no keyslots. #[allow(clippy::needless_pass_by_value)] - pub async fn decrypt_master_key(&self, password: ProtectedVec) -> Result> { + pub async fn decrypt_master_key(&self, password: Protected>) -> Result { if self.keyslots.is_empty() { return Err(Error::NoKeyslots); } for v in &self.keyslots { - if let Some(key) = v - .decrypt_master_key(password.clone()) - .await - .ok() - .map(|v| Protected::new(to_array::(v.into_inner()).unwrap())) - { + if let Ok(key) = v.decrypt_master_key(password.clone()).await { return Ok(key); } } @@ -120,6 +119,31 @@ impl FileHeader { Err(Error::IncorrectPassword) } + /// 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 async fn decrypt_master_key_from_prehashed(&self, hashed_keys: Vec) -> Result { + if self.keyslots.is_empty() { + return Err(Error::NoKeyslots); + } + + for hashed_key in hashed_keys { + for v in &self.keyslots { + if let Ok(key) = v + .decrypt_master_key_from_prehashed(hashed_key.clone()) + .await + { + return Ok(key); + } + } + } + + Err(Error::IncorrectPassword) + } + /// This is a helper function to serialize and write a header to a file. pub async fn write(&self, writer: &mut W) -> Result<()> where @@ -129,41 +153,11 @@ 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 async fn decrypt_master_key_from_prehashed( - &self, - hashed_keys: Vec>, - ) -> Result> { - if self.keyslots.is_empty() { - return Err(Error::NoKeyslots); - } - - for hashed_key in hashed_keys { - for v in &self.keyslots { - if let Some(key) = v - .decrypt_master_key_from_prehashed(hashed_key.clone()) - .await - .ok() - .map(|v| Protected::new(to_array::(v.into_inner()).unwrap())) - { - return Ok(key); - } - } - } - - Err(Error::IncorrectPassword) - } - /// This is a helper function to find which keyslot a key belongs to. /// /// You receive an error if the password doesn't match or if there are no keyslots. #[allow(clippy::needless_pass_by_value)] - pub async fn find_key_index(&self, password: ProtectedVec) -> Result { + pub async fn find_key_index(&self, password: Protected>) -> Result { if self.keyslots.is_empty() { return Err(Error::NoKeyslots); } @@ -185,9 +179,9 @@ impl FileHeader { match self.version { FileHeaderVersion::V1 => [ MAGIC_BYTES.as_ref(), - self.version.to_bytes().as_ref(), - self.algorithm.to_bytes().as_ref(), - self.nonce.as_ref(), + &self.version.to_bytes(), + &self.algorithm.to_bytes(), + &self.nonce, &vec![0u8; 25 - self.nonce.len()], ] .into_iter() @@ -291,6 +285,7 @@ impl FileHeader { let mut nonce = vec![0u8; algorithm.nonce_len()]; reader.read_exact(&mut nonce).await?; + let nonce = Nonce::try_from(nonce)?; // read and discard the padding reader.read_exact(&mut vec![0u8; 25 - nonce.len()]).await?; diff --git a/crates/crypto/src/header/keyslot.rs b/crates/crypto/src/header/keyslot.rs index e15b1b7c4..1127d99a4 100644 --- a/crates/crypto/src/header/keyslot.rs +++ b/crates/crypto/src/header/keyslot.rs @@ -27,10 +27,9 @@ use crate::{ crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, keys::hashing::HashingAlgorithm, primitives::{ - derive_key, generate_nonce, generate_salt, to_array, EncryptedKey, Key, Salt, + types::{EncryptedKey, Key, Nonce, Salt}, ENCRYPTED_KEY_LEN, FILE_KEY_CONTEXT, SALT_LEN, }, - protected::ProtectedVec, Error, Protected, Result, }; @@ -45,7 +44,7 @@ pub struct Keyslot { pub salt: Salt, // the salt used for deriving a KEK from a (key/content salt) hash pub content_salt: Salt, pub master_key: EncryptedKey, // this is encrypted so we can store it - pub nonce: Vec, + pub nonce: Nonce, } pub const KEYSLOT_SIZE: usize = 112; @@ -70,17 +69,17 @@ impl Keyslot { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, content_salt: Salt, - hashed_key: Protected, - master_key: Protected, + hashed_key: Key, + master_key: Key, ) -> Result { - let nonce = generate_nonce(algorithm); + let nonce = Nonce::generate(algorithm)?; - let salt = generate_salt(); + let salt = Salt::generate(); - let encrypted_master_key = to_array::( + let encrypted_master_key = EncryptedKey::try_from( StreamEncryption::encrypt_bytes( - derive_key(hashed_key, salt, FILE_KEY_CONTEXT), - &nonce, + Key::derive(hashed_key, salt, FILE_KEY_CONTEXT), + nonce, algorithm, master_key.expose(), &[], @@ -105,20 +104,22 @@ impl Keyslot { /// /// An error will be returned on failure. #[allow(clippy::needless_pass_by_value)] - pub async fn decrypt_master_key(&self, password: ProtectedVec) -> Result> { + pub async fn decrypt_master_key(&self, password: Protected>) -> Result { let key = self .hashing_algorithm .hash(password, self.content_salt, None) .map_err(|_| Error::PasswordHash)?; - StreamDecryption::decrypt_bytes( - derive_key(key, self.salt, FILE_KEY_CONTEXT), - &self.nonce, - self.algorithm, - &self.master_key, - &[], + Key::try_from( + StreamDecryption::decrypt_bytes( + Key::derive(key, self.salt, FILE_KEY_CONTEXT), + self.nonce, + self.algorithm, + &self.master_key, + &[], + ) + .await?, ) - .await } /// This function should not be used directly, use `header.decrypt_master_key()` instead @@ -128,18 +129,17 @@ impl Keyslot { /// No hashing is done internally. /// /// An error will be returned on failure. - pub async fn decrypt_master_key_from_prehashed( - &self, - key: Protected, - ) -> Result> { - StreamDecryption::decrypt_bytes( - derive_key(key, self.salt, FILE_KEY_CONTEXT), - &self.nonce, - self.algorithm, - &self.master_key, - &[], + pub async fn decrypt_master_key_from_prehashed(&self, key: Key) -> Result { + Key::try_from( + StreamDecryption::decrypt_bytes( + Key::derive(key, self.salt, FILE_KEY_CONTEXT), + self.nonce, + self.algorithm, + &self.master_key, + &[], + ) + .await?, ) - .await } /// This function is used to serialize a keyslot into bytes @@ -197,6 +197,7 @@ impl Keyslot { let mut nonce = vec![0u8; algorithm.nonce_len()]; reader.read_exact(&mut nonce)?; + let nonce = Nonce::try_from(nonce)?; reader.read_exact(&mut vec![0u8; 26 - nonce.len()])?; @@ -204,9 +205,9 @@ impl Keyslot { version, algorithm, hashing_algorithm, - salt, - content_salt, - master_key, + salt: Salt(salt), + content_salt: Salt(content_salt), + master_key: EncryptedKey(master_key), nonce, }; diff --git a/crates/crypto/src/header/metadata.rs b/crates/crypto/src/header/metadata.rs index e6965c9ea..c9456a55c 100644 --- a/crates/crypto/src/header/metadata.rs +++ b/crates/crypto/src/header/metadata.rs @@ -31,13 +31,13 @@ #[cfg(feature = "serde")] use crate::{ crypto::stream::{StreamDecryption, StreamEncryption}, - primitives::{generate_nonce, Key}, - Protected, ProtectedVec, + primitives::types::Key, + Protected, }; use tokio::io::AsyncReadExt; -use crate::{crypto::stream::Algorithm, Error, Result}; +use crate::{crypto::stream::Algorithm, primitives::types::Nonce, Error, Result}; use super::file::FileHeader; @@ -50,7 +50,7 @@ use super::file::FileHeader; pub struct Metadata { pub version: MetadataVersion, pub algorithm: Algorithm, // encryption algorithm - pub metadata_nonce: Vec, + pub metadata_nonce: Nonce, pub metadata: Vec, } @@ -73,17 +73,17 @@ impl FileHeader { &mut self, version: MetadataVersion, algorithm: Algorithm, - master_key: Protected, + master_key: Key, metadata: &T, ) -> Result<()> where T: ?Sized + serde::Serialize + Sync + Send, { - let metadata_nonce = generate_nonce(algorithm); + let metadata_nonce = Nonce::generate(algorithm)?; let encrypted_metadata = StreamEncryption::encrypt_bytes( master_key, - &metadata_nonce, + metadata_nonce, algorithm, &serde_json::to_vec(metadata).map_err(|_| Error::Serialization)?, &[], @@ -100,44 +100,13 @@ impl FileHeader { 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 - #[cfg(feature = "serde")] - pub async 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).await?; - - if let Some(metadata) = self.metadata.as_ref() { - let metadata = StreamDecryption::decrypt_bytes( - master_key, - &metadata.metadata_nonce, - metadata.algorithm, - &metadata.metadata, - &[], - ) - .await?; - - serde_json::from_slice::(&metadata).map_err(|_| Error::Serialization) - } 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 #[cfg(feature = "serde")] - pub async fn decrypt_metadata(&self, password: ProtectedVec) -> Result + pub async fn decrypt_metadata(&self, password: Protected>) -> Result where T: serde::de::DeserializeOwned, { @@ -146,14 +115,42 @@ impl FileHeader { if let Some(metadata) = self.metadata.as_ref() { let metadata = StreamDecryption::decrypt_bytes( master_key, - &metadata.metadata_nonce, + metadata.metadata_nonce, metadata.algorithm, &metadata.metadata, &[], ) .await?; - serde_json::from_slice::(&metadata).map_err(|_| Error::Serialization) + serde_json::from_slice::(metadata.expose()).map_err(|_| Error::Serialization) + } else { + Err(Error::NoMetadata) + } + } + + /// 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 + #[cfg(feature = "serde")] + pub async 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).await?; + + if let Some(metadata) = self.metadata.as_ref() { + let metadata = StreamDecryption::decrypt_bytes( + master_key, + metadata.metadata_nonce, + metadata.algorithm, + &metadata.metadata, + &[], + ) + .await?; + + serde_json::from_slice::(metadata.expose()).map_err(|_| Error::Serialization) } else { Err(Error::NoMetadata) } @@ -208,6 +205,7 @@ impl Metadata { let mut metadata_nonce = vec![0u8; algorithm.nonce_len()]; reader.read_exact(&mut metadata_nonce).await?; + let metadata_nonce = Nonce::try_from(metadata_nonce)?; reader .read_exact(&mut vec![0u8; 24 - metadata_nonce.len()]) diff --git a/crates/crypto/src/header/preview_media.rs b/crates/crypto/src/header/preview_media.rs index 4481a5009..dd746132c 100644 --- a/crates/crypto/src/header/preview_media.rs +++ b/crates/crypto/src/header/preview_media.rs @@ -24,8 +24,8 @@ use tokio::io::AsyncReadExt; use crate::{ crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, - primitives::{generate_nonce, Key}, - Error, Protected, ProtectedVec, Result, + primitives::types::{Key, Nonce}, + Error, Protected, Result, }; use super::file::FileHeader; @@ -39,7 +39,7 @@ use super::file::FileHeader; pub struct PreviewMedia { pub version: PreviewMediaVersion, pub algorithm: Algorithm, // encryption algorithm - pub media_nonce: Vec, + pub media_nonce: Nonce, pub media: Vec, } @@ -61,14 +61,13 @@ impl FileHeader { &mut self, version: PreviewMediaVersion, algorithm: Algorithm, - master_key: Protected, + master_key: Key, media: &[u8], ) -> Result<()> { - let media_nonce = generate_nonce(algorithm); + let media_nonce = Nonce::generate(algorithm)?; let encrypted_media = - StreamEncryption::encrypt_bytes(master_key, &media_nonce, algorithm, media, &[]) - .await?; + StreamEncryption::encrypt_bytes(master_key, media_nonce, algorithm, media, &[]).await?; self.preview_media = Some(PreviewMedia { version, @@ -82,19 +81,19 @@ impl FileHeader { /// 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 + /// 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 async fn decrypt_preview_media_from_prehashed( + pub async fn decrypt_preview_media( &self, - hashed_keys: Vec>, - ) -> Result> { - let master_key = self.decrypt_master_key_from_prehashed(hashed_keys).await?; + password: Protected>, + ) -> Result>> { + let master_key = self.decrypt_master_key(password).await?; if let Some(pvm) = self.preview_media.as_ref() { let pvm = StreamDecryption::decrypt_bytes( master_key, - &pvm.media_nonce, + pvm.media_nonce, pvm.algorithm, &pvm.media, &[], @@ -109,19 +108,19 @@ impl FileHeader { /// 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. + /// All it requires is pre-hashed keys returned from the key manager /// /// Once provided, a `Vec` is returned that contains the preview media - pub async fn decrypt_preview_media( + pub async fn decrypt_preview_media_from_prehashed( &self, - password: ProtectedVec, - ) -> Result> { - let master_key = self.decrypt_master_key(password).await?; + hashed_keys: Vec, + ) -> Result>> { + let master_key = self.decrypt_master_key_from_prehashed(hashed_keys).await?; if let Some(pvm) = self.preview_media.as_ref() { let pvm = StreamDecryption::decrypt_bytes( master_key, - &pvm.media_nonce, + pvm.media_nonce, pvm.algorithm, &pvm.media, &[], @@ -184,6 +183,7 @@ impl PreviewMedia { let mut media_nonce = vec![0u8; algorithm.nonce_len()]; reader.read_exact(&mut media_nonce).await?; + let media_nonce = Nonce::try_from(media_nonce)?; reader .read_exact(&mut vec![0u8; 24 - media_nonce.len()]) diff --git a/crates/crypto/src/keys/hashing.rs b/crates/crypto/src/keys/hashing.rs index ccc810156..d1b6a66b8 100644 --- a/crates/crypto/src/keys/hashing.rs +++ b/crates/crypto/src/keys/hashing.rs @@ -12,8 +12,11 @@ //! ``` use crate::{ - primitives::{Key, Salt, KEY_LEN}, - Error, Protected, ProtectedVec, Result, + primitives::{ + types::{Key, Salt, SecretKey}, + KEY_LEN, + }, + Error, Protected, Result, }; use argon2::Argon2; use balloon_hash::Balloon; @@ -53,10 +56,10 @@ impl HashingAlgorithm { #[allow(clippy::needless_pass_by_value)] pub fn hash( &self, - password: ProtectedVec, + password: Protected>, salt: Salt, - secret: Option>, - ) -> Result> { + secret: Option, + ) -> Result { match self { Self::Argon2id(params) => PasswordHasher::argon2id(password, salt, secret, *params), Self::BalloonBlake3(params) => { @@ -79,8 +82,8 @@ impl Params { // Provided they all take one (ish) second or longer, and less than 3/4 seconds (for paranoid), they will be fine // It's not so much the parameters themselves that matter, it's the duration (and ensuring that they use enough RAM to hinder ASIC brute-force attacks) Self::Standard => argon2::Params::new(131_072, 8, 4, None).unwrap(), - Self::Paranoid => argon2::Params::new(262_144, 8, 4, None).unwrap(), - Self::Hardened => argon2::Params::new(524_288, 8, 4, None).unwrap(), + Self::Hardened => argon2::Params::new(262_144, 8, 4, None).unwrap(), + Self::Paranoid => argon2::Params::new(524_288, 8, 4, None).unwrap(), } } @@ -95,9 +98,9 @@ impl Params { // 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 => balloon_hash::Params::new(131_072, 1, 1).unwrap(), - Self::Paranoid => balloon_hash::Params::new(262_144, 1, 1).unwrap(), - Self::Hardened => balloon_hash::Params::new(524_288, 1, 1).unwrap(), + Self::Standard => balloon_hash::Params::new(131_072, 2, 1).unwrap(), + Self::Hardened => balloon_hash::Params::new(262_144, 2, 1).unwrap(), + Self::Paranoid => balloon_hash::Params::new(524_288, 2, 1).unwrap(), } } } @@ -107,12 +110,14 @@ struct PasswordHasher; impl PasswordHasher { #[allow(clippy::needless_pass_by_value)] fn argon2id( - password: ProtectedVec, + password: Protected>, salt: Salt, - secret: Option>, + secret: Option, params: Params, - ) -> Result> { - let secret = secret.map_or(Protected::new(vec![]), |k| k); + ) -> Result { + let secret = secret.map_or(Protected::new(vec![]), |k| { + Protected::new(k.expose().to_vec()) + }); let mut key = [0u8; KEY_LEN]; let argon2 = Argon2::new_with_secret( @@ -125,17 +130,19 @@ impl PasswordHasher { argon2 .hash_password_into(password.expose(), &salt, &mut key) - .map_or(Err(Error::PasswordHash), |_| Ok(Protected::new(key))) + .map_or(Err(Error::PasswordHash), |_| Ok(Key::new(key))) } #[allow(clippy::needless_pass_by_value)] fn balloon_blake3( - password: ProtectedVec, + password: Protected>, salt: Salt, - secret: Option>, + secret: Option, params: Params, - ) -> Result> { - let secret = secret.map_or(Protected::new(vec![]), |k| k); + ) -> Result { + let secret = secret.map_or(Protected::new(vec![]), |k| { + Protected::new(k.expose().to_vec()) + }); let mut key = [0u8; KEY_LEN]; @@ -147,6 +154,6 @@ impl PasswordHasher { balloon .hash_into(password.expose(), &salt, &mut key) - .map_or(Err(Error::PasswordHash), |_| Ok(Protected::new(key))) + .map_or(Err(Error::PasswordHash), |_| Ok(Key::new(key))) } } diff --git a/crates/crypto/src/keys/keymanager.rs b/crates/crypto/src/keys/keymanager.rs index ba4145e86..262bc3c99 100644 --- a/crates/crypto/src/keys/keymanager.rs +++ b/crates/crypto/src/keys/keymanager.rs @@ -35,50 +35,58 @@ //! let keys = key_manager.enumerate_hashed_keys(); //! ``` +use std::sync::Arc; + use tokio::sync::Mutex; -// use crate::primitives::{ -// derive_key, generate_master_key, generate_nonce, generate_salt, to_array, EncryptedKey, Key, -// OnboardingConfig, Salt, KEY_LEN, LATEST_STORED_KEY, MASTER_PASSWORD_CONTEXT, ROOT_KEY_CONTEXT, -// }; use crate::{ crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, primitives::{ - derive_key, generate_master_key, generate_nonce, generate_salt, to_array, EncryptedKey, - Key, OnboardingConfig, Salt, ENCRYPTED_KEY_LEN, KEY_LEN, LATEST_STORED_KEY, - MASTER_PASSWORD_CONTEXT, ROOT_KEY_CONTEXT, + types::{ + EncryptedKey, Key, Nonce, OnboardingConfig, Password, Salt, SecretKey, SecretKeyString, + }, + APP_IDENTIFIER, LATEST_STORED_KEY, MASTER_PASSWORD_CONTEXT, ROOT_KEY_CONTEXT, + SECRET_KEY_IDENTIFIER, }, - Error, Protected, ProtectedVec, Result, + Error, Protected, Result, }; use dashmap::{DashMap, DashSet}; use uuid::Uuid; -#[cfg(feature = "serde")] -use serde_big_array::BigArray; - -use super::hashing::HashingAlgorithm; +use super::{ + hashing::HashingAlgorithm, + keyring::{Identifier, KeyringInterface}, +}; /// This is a stored key, and can be freely written to Prisma/another database. #[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "rspc", derive(specta::Type))] pub struct StoredKey { - pub uuid: uuid::Uuid, // uuid for identification. shared with mounted keys + pub uuid: Uuid, // uuid for identification. shared with mounted keys pub version: StoredKeyVersion, + pub key_type: StoredKeyType, pub algorithm: Algorithm, // encryption algorithm for encrypting the master key. can be changed (requires a re-encryption though) pub hashing_algorithm: HashingAlgorithm, // hashing algorithm used for hashing the key with the content salt pub content_salt: Salt, - #[cfg_attr(feature = "serde", serde(with = "BigArray"))] // salt used for file data pub master_key: EncryptedKey, // 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 master_key_nonce: Nonce, // nonce for encrypting the master key + pub key_nonce: Nonce, // nonce used for encrypting the main key + pub key: Vec, // encrypted. the password stored in spacedrive (e.g. generated 64 char key) pub salt: Salt, pub memory_only: bool, pub automount: bool, } +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "rspc", derive(specta::Type))] +pub enum StoredKeyType { + User, + Root, +} + #[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "rspc", derive(specta::Type))] @@ -91,8 +99,8 @@ pub enum StoredKeyVersion { /// This contains the plaintext key, and the same key hashed with the content salt. #[derive(Clone)] pub struct MountedKey { - pub uuid: Uuid, // used for identification. shared with stored keys - pub hashed_key: Protected, // this is hashed with the content salt, for instant access + pub uuid: Uuid, // used for identification. shared with stored keys + pub hashed_key: Key, // this is hashed with the content salt, for instant access } /// This is the key manager itself. @@ -101,18 +109,23 @@ pub struct MountedKey { /// /// Use the associated functions to interact with it. pub struct KeyManager { - root_key: Mutex>>, // the root key for the vault + root_key: Mutex>, // the root key for the vault verification_key: Mutex>, keystore: DashMap, keymount: DashMap, default: Mutex>, mounting_queue: DashSet, + keyring: Option>>, } /// The `KeyManager` functions should be used for all key-related management. impl KeyManager { /// Initialize the Key Manager with `StoredKeys` retrieved from Prisma pub async fn new(stored_keys: Vec) -> Result { + let keyring = KeyringInterface::new() + .map(|k| Arc::new(Mutex::new(k))) + .ok(); + let keymanager = Self { root_key: Mutex::new(None), verification_key: Mutex::new(None), @@ -120,6 +133,7 @@ impl KeyManager { keymount: DashMap::new(), default: Mutex::new(None), mounting_queue: DashSet::new(), + keyring, }; keymanager.populate_keystore(stored_keys).await?; @@ -127,15 +141,89 @@ impl KeyManager { Ok(keymanager) } + // A returned error here should be treated as `false` + pub async fn keyring_contains(&self, library_uuid: Uuid, usage: String) -> Result<()> { + self.get_keyring()?.lock().await.retrieve(Identifier { + application: APP_IDENTIFIER, + library_uuid: &library_uuid.to_string(), + usage: &usage, + })?; + + Ok(()) + } + + pub async fn keyring_retrieve( + &self, + library_uuid: Uuid, + usage: String, + ) -> Result> { + let value = self.get_keyring()?.lock().await.retrieve(Identifier { + application: APP_IDENTIFIER, + library_uuid: &library_uuid.to_string(), + usage: &usage, + })?; + + Ok(Protected::new(String::from_utf8(value.expose().clone())?)) + } + + /// This checks to see if the keyring is active, and if the keyring has a valid secret key. + /// + /// For a secret key to be considered valid, it must be 18 bytes encoded in hex. It can be separated with `-`. + /// + /// We can use this to detect if a secret key is technically present in the keyring, but not valid/has been tampered with. + pub async fn keyring_contains_valid_secret_key(&self, library_uuid: Uuid) -> Result<()> { + let secret_key = self + .keyring_retrieve(library_uuid, SECRET_KEY_IDENTIFIER.to_string()) + .await?; + + let mut secret_key_sanitized = secret_key.expose().clone(); + secret_key_sanitized.retain(|c| c != '-' && !c.is_whitespace()); + + if hex::decode(secret_key_sanitized) + .map_err(|_| Error::IncorrectPassword)? + .len() != 18 + { + return Err(Error::IncorrectPassword); + } + + Ok(()) + } + + async fn keyring_insert( + &self, + library_uuid: Uuid, + usage: String, + value: SecretKeyString, + ) -> Result<()> { + self.get_keyring()?.lock().await.insert( + Identifier { + application: APP_IDENTIFIER, + library_uuid: &library_uuid.to_string(), + usage: &usage, + }, + value, + )?; + + Ok(()) + } + + fn get_keyring(&self) -> Result>> { + self.keyring + .as_ref() + .map_or(Err(Error::KeyringNotSupported), |k| Ok(k.clone())) + } + /// This should be used to generate everything for the user during onboarding. /// /// This will create a master password (a 7-word diceware passphrase), and a secret key (16 bytes, hex encoded) /// /// It will also generate a verification key, which should be written to the database. #[allow(clippy::needless_pass_by_value)] - pub async fn onboarding(config: OnboardingConfig) -> Result { - let content_salt = generate_salt(); - let secret_key = config.secret_key.map(Self::convert_secret_key_string); + pub async fn onboarding(config: OnboardingConfig, library_uuid: Uuid) -> Result { + let content_salt = Salt::generate(); + let secret_key = SecretKey::generate(); + + dbg!(SecretKeyString::from(secret_key.clone()).expose()); let algorithm = config.algorithm; let hashing_algorithm = config.hashing_algorithm; @@ -144,24 +232,23 @@ impl KeyManager { let hashed_password = hashing_algorithm.hash( Protected::new(config.password.expose().as_bytes().to_vec()), content_salt, - secret_key, + Some(secret_key.clone()), )?; - let salt = generate_salt(); - let uuid = uuid::Uuid::nil(); + let salt = Salt::generate(); // Generate items we'll need for encryption - let master_key = generate_master_key(); - let master_key_nonce = generate_nonce(algorithm); + let master_key = Key::generate(); + let master_key_nonce = Nonce::generate(algorithm)?; - let root_key = generate_master_key(); - let root_key_nonce = generate_nonce(algorithm); + let root_key = Key::generate(); + let root_key_nonce = Nonce::generate(algorithm)?; // Encrypt the master key with the hashed master password - let encrypted_master_key = to_array::( + let encrypted_master_key = EncryptedKey::try_from( StreamEncryption::encrypt_bytes( - derive_key(hashed_password, salt, MASTER_PASSWORD_CONTEXT), - &master_key_nonce, + Key::derive(hashed_password, salt, MASTER_PASSWORD_CONTEXT), + master_key_nonce, algorithm, master_key.expose(), &[], @@ -171,16 +258,29 @@ impl KeyManager { let encrypted_root_key = StreamEncryption::encrypt_bytes( master_key, - &root_key_nonce, + root_key_nonce, algorithm, root_key.expose(), &[], ) .await?; + // attempt to insert into the OS keyring + // can ignore false here as we want to silently error + if let Ok(keyring) = KeyringInterface::new() { + let identifier = Identifier { + application: APP_IDENTIFIER, + library_uuid: &library_uuid.to_string(), + usage: SECRET_KEY_IDENTIFIER, + }; + + keyring.insert(identifier, secret_key.into()).ok(); + } + let verification_key = StoredKey { - uuid, + uuid: Uuid::new_v4(), version: LATEST_STORED_KEY, + key_type: StoredKeyType::Root, algorithm, hashing_algorithm, content_salt, // salt used for hashing @@ -207,7 +307,7 @@ impl KeyManager { continue; } - if key.uuid.is_nil() { + if key.key_type == StoredKeyType::Root { *self.verification_key.lock().await = Some(key); } else { self.keystore.insert(key.uuid, key); @@ -246,33 +346,33 @@ impl KeyManager { master_password: Protected, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, - secret_key: Option>, + library_uuid: Uuid, ) -> Result { - let secret_key = secret_key.map(Self::convert_secret_key_string); - let content_salt = generate_salt(); + let secret_key = SecretKey::generate(); + let content_salt = Salt::generate(); + + dbg!(SecretKeyString::from(secret_key.clone()).expose()); let hashed_password = hashing_algorithm.hash( Protected::new(master_password.expose().as_bytes().to_vec()), content_salt, - secret_key, + Some(secret_key.clone()), )?; - let uuid = uuid::Uuid::nil(); - // Generate items we'll need for encryption - let master_key = generate_master_key(); - let master_key_nonce = generate_nonce(algorithm); + let master_key = Key::generate(); + let master_key_nonce = Nonce::generate(algorithm)?; let root_key = self.get_root_key().await?; - let root_key_nonce = generate_nonce(algorithm); + let root_key_nonce = Nonce::generate(algorithm)?; - let salt = generate_salt(); + let salt = Salt::generate(); // Encrypt the master key with the hashed master password - let encrypted_master_key = to_array::( + let encrypted_master_key = EncryptedKey::try_from( StreamEncryption::encrypt_bytes( - derive_key(hashed_password, salt, MASTER_PASSWORD_CONTEXT), - &master_key_nonce, + Key::derive(hashed_password, salt, MASTER_PASSWORD_CONTEXT), + master_key_nonce, algorithm, master_key.expose(), &[], @@ -282,16 +382,26 @@ impl KeyManager { let encrypted_root_key = StreamEncryption::encrypt_bytes( master_key, - &root_key_nonce, + root_key_nonce, algorithm, root_key.expose(), &[], ) .await?; + // will update if it's already present + self.keyring_insert( + library_uuid, + SECRET_KEY_IDENTIFIER.to_string(), + secret_key.into(), + ) + .await + .ok(); + let verification_key = StoredKey { - uuid, + uuid: Uuid::new_v4(), version: LATEST_STORED_KEY, + key_type: StoredKeyType::Root, algorithm, hashing_algorithm, content_salt, @@ -315,19 +425,19 @@ impl KeyManager { #[allow(clippy::needless_pass_by_value)] pub async fn import_keystore_backup( &self, - master_password: Protected, // at the time of the backup - secret_key: Option>, // at the time of the backup - stored_keys: &[StoredKey], // from the backup + master_password: Protected, // at the time of the backup + secret_key: SecretKeyString, // at the time of the backup + stored_keys: &[StoredKey], // from the backup ) -> Result> { // this backup should contain a verification key, which will tell us the algorithm+hashing algorithm - let secret_key = secret_key.map(Self::convert_secret_key_string); + let secret_key = secret_key.into(); let mut old_verification_key = None; let keys: Vec = stored_keys .iter() .filter_map(|key| { - if key.uuid.is_nil() { + if key.key_type == StoredKeyType::Root { old_verification_key = Some(key.clone()); None } else { @@ -343,17 +453,17 @@ impl KeyManager { let hashed_password = old_verification_key.hashing_algorithm.hash( Protected::new(master_password.expose().as_bytes().to_vec()), old_verification_key.content_salt, - secret_key, + Some(secret_key), )?; // decrypt the root key's KEK let master_key = StreamDecryption::decrypt_bytes( - derive_key( + Key::derive( hashed_password, old_verification_key.salt, MASTER_PASSWORD_CONTEXT, ), - &old_verification_key.master_key_nonce, + old_verification_key.master_key_nonce, old_verification_key.algorithm, &old_verification_key.master_key, &[], @@ -362,15 +472,15 @@ impl KeyManager { // get the root key from the backup let old_root_key = StreamDecryption::decrypt_bytes( - Protected::new(to_array(master_key.into_inner())?), - &old_verification_key.key_nonce, + Key::try_from(master_key)?, + old_verification_key.key_nonce, old_verification_key.algorithm, &old_verification_key.key, &[], ) .await?; - Protected::new(to_array(old_root_key.into_inner())?) + Key::try_from(old_root_key)? } }; @@ -385,27 +495,25 @@ impl KeyManager { StoredKeyVersion::V1 => { // decrypt the key's master key let master_key = StreamDecryption::decrypt_bytes( - derive_key(old_root_key.clone(), key.salt, ROOT_KEY_CONTEXT), - &key.master_key_nonce, + Key::derive(old_root_key.clone(), key.salt, ROOT_KEY_CONTEXT), + key.master_key_nonce, key.algorithm, &key.master_key, &[], ) .await - .map_or(Err(Error::IncorrectPassword), |v| { - Ok(Protected::new(to_array::(v.into_inner())?)) - })?; + .map_or(Err(Error::IncorrectPassword), Key::try_from)?; // generate a new nonce - let master_key_nonce = generate_nonce(key.algorithm); + let master_key_nonce = Nonce::generate(key.algorithm)?; - let salt = generate_salt(); + let salt = Salt::generate(); // encrypt the master key with the current root key - let encrypted_master_key = to_array( + let encrypted_master_key = EncryptedKey::try_from( StreamEncryption::encrypt_bytes( - derive_key(self.get_root_key().await?, salt, ROOT_KEY_CONTEXT), - &master_key_nonce, + Key::derive(self.get_root_key().await?, salt, ROOT_KEY_CONTEXT), + master_key_nonce, key.algorithm, master_key.expose(), &[], @@ -430,6 +538,9 @@ impl KeyManager { /// This is used for unlocking the key manager, and requires both the master password and the secret key. /// /// The master password and secret key are hashed together. + /// + /// Only provide the secret key if it should not/can not be sourced from an OS keychain (e.g. web, OS keychains not enabled/available, etc). + /// /// This minimises the risk of an attacker obtaining the master password, as both of these are required to unlock the vault (and both should be stored separately). /// /// Both values need to be correct, otherwise this function will return a generic error. @@ -440,28 +551,40 @@ impl KeyManager { #[allow(clippy::needless_pass_by_value)] pub async fn unlock( &self, - master_password: Protected, - secret_key: Option>, + master_password: Password, + provided_secret_key: Option, + library_uuid: Uuid, invalidate: F, ) -> Result<()> where F: Fn() + Send, { - let uuid = Uuid::nil(); - - if self.has_master_password().await? { - return Err(Error::KeyAlreadyMounted); - } else if self.is_queued(uuid) { - return Err(Error::KeyAlreadyQueued); - } - let verification_key = (*self.verification_key.lock().await) .as_ref() .map_or(Err(Error::NoVerificationKey), |k| Ok(k.clone()))?; - let secret_key = secret_key.map(Self::convert_secret_key_string); + if self.is_unlocked().await? { + return Err(Error::KeyAlreadyMounted); + } else if self.is_queued(verification_key.uuid) { + return Err(Error::KeyAlreadyQueued); + } - self.mounting_queue.insert(uuid); + let secret_key = if let Some(secret_key) = provided_secret_key.clone() { + secret_key.into() + } else { + self.get_keyring()? + .lock() + .await + .retrieve(Identifier { + application: APP_IDENTIFIER, + library_uuid: &library_uuid.to_string(), + usage: SECRET_KEY_IDENTIFIER, + }) + .map(|x| SecretKeyString::new(String::from_utf8(x.expose().clone()).unwrap()))? + .into() + }; + + self.mounting_queue.insert(verification_key.uuid); invalidate(); match verification_key.version { @@ -471,53 +594,62 @@ impl KeyManager { .hash( Protected::new(master_password.expose().as_bytes().to_vec()), verification_key.content_salt, - secret_key, + Some(secret_key), ) .map_err(|e| { - self.remove_from_queue(uuid).ok(); + self.remove_from_queue(verification_key.uuid).ok(); e })?; let master_key = StreamDecryption::decrypt_bytes( - derive_key( + Key::derive( hashed_password, verification_key.salt, MASTER_PASSWORD_CONTEXT, ), - &verification_key.master_key_nonce, + verification_key.master_key_nonce, verification_key.algorithm, &verification_key.master_key, &[], ) .await .map_err(|_| { - self.remove_from_queue(uuid).ok(); + self.remove_from_queue(verification_key.uuid).ok(); Error::IncorrectKeymanagerDetails })?; - *self.root_key.lock().await = Some(Protected::new( - to_array( + *self.root_key.lock().await = Some( + Key::try_from( StreamDecryption::decrypt_bytes( - Protected::new(to_array(master_key.into_inner())?), - &verification_key.key_nonce, + Key::try_from(master_key)?, + verification_key.key_nonce, verification_key.algorithm, &verification_key.key, &[], ) - .await? - .expose() - .clone(), + .await?, ) .map_err(|e| { - self.remove_from_queue(uuid).ok(); + self.remove_from_queue(verification_key.uuid).ok(); e })?, - )); + ); - self.remove_from_queue(uuid)?; + self.remove_from_queue(verification_key.uuid)?; } } + if let Some(secret_key) = provided_secret_key { + // converting twice ensures it's formatted correctly + self.keyring_insert( + library_uuid, + SECRET_KEY_IDENTIFIER.to_string(), + SecretKeyString::from(SecretKey::from(secret_key)), + ) + .await + .ok(); + } + invalidate(); Ok(()) @@ -543,12 +675,12 @@ impl KeyManager { self.mounting_queue.insert(uuid); let master_key = StreamDecryption::decrypt_bytes( - derive_key( + Key::derive( self.get_root_key().await?, stored_key.salt, ROOT_KEY_CONTEXT, ), - &stored_key.master_key_nonce, + stored_key.master_key_nonce, stored_key.algorithm, &stored_key.master_key, &[], @@ -559,12 +691,12 @@ impl KeyManager { self.remove_from_queue(uuid).ok(); Err(Error::IncorrectPassword) }, - |v| Ok(Protected::new(to_array(v.into_inner())?)), + Key::try_from, )?; // Decrypt the StoredKey using the decrypted master key let key = StreamDecryption::decrypt_bytes( master_key, - &stored_key.key_nonce, + stored_key.key_nonce, stored_key.algorithm, &stored_key.key, &[], @@ -605,35 +737,33 @@ impl KeyManager { /// This function is used for getting the key value itself, from a given UUID. /// /// The master password/salt needs to be present, so we are able to decrypt the key itself from the stored key. - pub async fn get_key(&self, uuid: Uuid) -> Result> { + pub async fn get_key(&self, uuid: Uuid) -> Result { if let Some(stored_key) = self.keystore.get(&uuid) { let master_key = StreamDecryption::decrypt_bytes( - derive_key( + Key::derive( self.get_root_key().await?, stored_key.salt, ROOT_KEY_CONTEXT, ), - &stored_key.master_key_nonce, + stored_key.master_key_nonce, stored_key.algorithm, &stored_key.master_key, &[], ) .await - .map_or(Err(Error::IncorrectPassword), |k| { - Ok(Protected::new(to_array(k.into_inner())?)) - })?; + .map_or(Err(Error::IncorrectPassword), Key::try_from)?; // Decrypt the StoredKey using the decrypted master key let key = StreamDecryption::decrypt_bytes( master_key, - &stored_key.key_nonce, + stored_key.key_nonce, stored_key.algorithm, &stored_key.key, &[], ) .await?; - Ok(key) + Ok(Password::new(String::from_utf8(key.expose().clone())?)) } else { Err(Error::KeyNotFound) } @@ -653,30 +783,30 @@ impl KeyManager { #[allow(clippy::needless_pass_by_value)] pub async fn add_to_keystore( &self, - key: ProtectedVec, + key: Password, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, memory_only: bool, automount: bool, content_salt: Option, ) -> Result { - let uuid = uuid::Uuid::new_v4(); + let 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 key_nonce = Nonce::generate(algorithm)?; + let master_key = Key::generate(); + let master_key_nonce = Nonce::generate(algorithm)?; - let content_salt = content_salt.map_or_else(generate_salt, |v| v); + let content_salt = content_salt.map_or_else(Salt::generate, |v| v); // salt used for the kdf - let salt = generate_salt(); + let salt = Salt::generate(); // Encrypt the master key with a derived key (derived from the root key) - let encrypted_master_key = to_array::( + let encrypted_master_key = EncryptedKey::try_from( StreamEncryption::encrypt_bytes( - derive_key(self.get_root_key().await?, salt, ROOT_KEY_CONTEXT), - &master_key_nonce, + Key::derive(self.get_root_key().await?, salt, ROOT_KEY_CONTEXT), + master_key_nonce, algorithm, master_key.expose(), &[], @@ -685,8 +815,14 @@ impl KeyManager { )?; // Encrypt the actual key (e.g. user-added/autogenerated, text-encodable) - let encrypted_key = - StreamEncryption::encrypt_bytes(master_key, &key_nonce, algorithm, &key, &[]).await?; + let encrypted_key = StreamEncryption::encrypt_bytes( + master_key, + key_nonce, + algorithm, + key.expose().as_bytes(), + &[], + ) + .await?; // Insert it into the Keystore self.keystore.insert( @@ -694,6 +830,7 @@ impl KeyManager { StoredKey { uuid, version: LATEST_STORED_KEY, + key_type: StoredKeyType::User, algorithm, hashing_algorithm, content_salt, @@ -711,11 +848,6 @@ impl KeyManager { Ok(uuid) } - #[allow(clippy::needless_pass_by_value)] - fn convert_secret_key_string(secret_key: Protected) -> ProtectedVec { - Protected::new(secret_key.expose().as_bytes().to_vec()) - } - /// This function is for accessing the internal keymount. /// /// We could add a log to this, so that the user can view accesses @@ -748,7 +880,7 @@ impl KeyManager { } /// This should ONLY be used internally. - async fn get_root_key(&self) -> Result> { + async fn get_root_key(&self) -> Result { self.root_key .lock() .await @@ -791,11 +923,11 @@ impl KeyManager { /// /// This means we don't need to keep super specific track of which key goes to which file, and we can just throw all of them at it. #[must_use] - pub fn enumerate_hashed_keys(&self) -> Vec> { + pub fn enumerate_hashed_keys(&self) -> Vec { self.keymount .iter() .map(|mounted_key| mounted_key.hashed_key.clone()) - .collect::>>() + .collect::>() } /// This function is for converting a memory-only key to a saved key which syncs to the library. @@ -828,10 +960,8 @@ impl KeyManager { Ok(()) } - /// This function is used for seeing if the key manager has a master password. - /// - /// Technically this checks for the root key, but it makes no difference to the front end. - pub async fn has_master_password(&self) -> Result { + /// This function is used for checking if the key manager is unlocked. + pub async fn is_unlocked(&self) -> Result { Ok(self.root_key.lock().await.is_some()) } @@ -873,6 +1003,12 @@ impl KeyManager { self.mounting_queue.contains(&uuid) } + pub async fn is_unlocking(&self) -> Result { + Ok(self + .mounting_queue + .contains(&self.get_verification_key().await?.uuid)) + } + pub fn remove_from_queue(&self, uuid: Uuid) -> Result<()> { self.mounting_queue .remove(&uuid) diff --git a/crates/crypto/src/keys/keychain/apple.rs b/crates/crypto/src/keys/keyring/apple.rs similarity index 78% rename from crates/crypto/src/keys/keychain/apple.rs rename to crates/crypto/src/keys/keyring/apple.rs index 8ed606d9a..bbe387180 100644 --- a/crates/crypto/src/keys/keychain/apple.rs +++ b/crates/crypto/src/keys/keyring/apple.rs @@ -1,9 +1,9 @@ -//! This is Spacedrive's Apple OS keychain integration. It has no strict dependencies. +//! This is Spacedrive's Apple OS keyring integration. It has no strict dependencies. //! //! This has been tested on MacOS, but should work just the same for iOS (according to the `security_framework` documentation) use super::{Identifier, Keyring}; -use crate::{Error, Protected, Result}; +use crate::{primitives::types::SecretKeyString, Error, Protected, Result}; use security_framework::passwords::{ delete_generic_password, get_generic_password, set_generic_password, }; @@ -11,7 +11,7 @@ use security_framework::passwords::{ pub struct AppleKeyring; impl Keyring for AppleKeyring { - fn insert(&self, identifier: Identifier, value: Protected) -> Result<()> { + fn insert(&self, identifier: Identifier, value: SecretKeyString) -> Result<()> { set_generic_password( &identifier.application, &identifier.to_apple_account(), diff --git a/crates/crypto/src/keys/keychain/linux.rs b/crates/crypto/src/keys/keyring/linux.rs similarity index 84% rename from crates/crypto/src/keys/keychain/linux.rs rename to crates/crypto/src/keys/keyring/linux.rs index ff9d8d75f..8c2d7dfcf 100644 --- a/crates/crypto/src/keys/keychain/linux.rs +++ b/crates/crypto/src/keys/keyring/linux.rs @@ -1,11 +1,12 @@ -//! This is Spacedrive's Linux keychain implementation, which makes use of the Secret Service API. +//! This is Spacedrive's Linux keyring implementation, which makes use of the Secret Service API. //! //! This does strictly require `DBus`, and either `gnome-keyring`, `kwallet` or another implementor of the Secret Service API. use secret_service::{Collection, EncryptionType, SecretService}; use crate::{ - keys::keychain::{Identifier, Keyring}, + keys::keyring::{Identifier, Keyring}, + primitives::types::SecretKeyString, Error, Protected, Result, }; @@ -33,7 +34,7 @@ impl<'a> LinuxKeyring<'a> { } impl<'a> Keyring for LinuxKeyring<'a> { - fn insert(&self, identifier: Identifier, value: Protected) -> Result<()> { + fn insert(&self, identifier: Identifier, value: SecretKeyString) -> Result<()> { self.get_collection()?.create_item( &identifier.generate_linux_label(), identifier.to_hashmap(), diff --git a/crates/crypto/src/keys/keychain/mod.rs b/crates/crypto/src/keys/keyring/mod.rs similarity index 88% rename from crates/crypto/src/keys/keychain/mod.rs rename to crates/crypto/src/keys/keyring/mod.rs index 5d258d6b8..9aa989774 100644 --- a/crates/crypto/src/keys/keychain/mod.rs +++ b/crates/crypto/src/keys/keyring/mod.rs @@ -1,4 +1,4 @@ -use crate::{Protected, Result}; +use crate::{primitives::types::SecretKeyString, Protected, Result}; #[cfg(target_os = "linux")] pub mod linux; @@ -40,7 +40,7 @@ impl<'a> Identifier<'a> { } pub trait Keyring { - fn insert(&self, identifier: Identifier, value: Protected) -> Result<()>; + fn insert(&self, identifier: Identifier, value: SecretKeyString) -> Result<()>; fn retrieve(&self, identifier: Identifier) -> Result>>; fn delete(&self, identifier: Identifier) -> Result<()>; } @@ -64,7 +64,7 @@ impl KeyringInterface { Ok(Self { keyring }) } - pub fn insert(&self, identifier: Identifier, value: Protected) -> Result<()> { + pub fn insert(&self, identifier: Identifier, value: SecretKeyString) -> Result<()> { self.keyring.insert(identifier, value) } diff --git a/crates/crypto/src/keys/mod.rs b/crates/crypto/src/keys/mod.rs index ba1f81e20..cd9bfbd37 100644 --- a/crates/crypto/src/keys/mod.rs +++ b/crates/crypto/src/keys/mod.rs @@ -1,5 +1,5 @@ //! This module contains all key and hashing related functions. pub mod hashing; -pub mod keychain; pub mod keymanager; +pub mod keyring; diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index 03b048a79..87be04556 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -24,7 +24,6 @@ pub use aead::Payload; // Make this easier to use (e.g. `sd_crypto::Protected`) pub use protected::Protected; -pub use protected::ProtectedVec; // Re-export zeroize so it can be used elsewhere pub use zeroize::Zeroize; diff --git a/crates/crypto/src/primitives.rs b/crates/crypto/src/primitives.rs deleted file mode 100644 index e15d2cf8f..000000000 --- a/crates/crypto/src/primitives.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! 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::{ - crypto::stream::Algorithm, - header::{ - file::FileHeaderVersion, keyslot::KeyslotVersion, metadata::MetadataVersion, - preview_media::PreviewMediaVersion, - }, - keys::{hashing::HashingAlgorithm, keymanager::StoredKeyVersion}, - Error, Protected, Result, -}; - -/// This is the default salt size, and the recommended size for argon2id. -pub const SALT_LEN: usize = 16; - -/// 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). Plus the size of the header. -pub const BLOCK_SIZE: usize = 1_048_576; - -pub const AEAD_TAG_SIZE: usize = 16; - -/// The length of the encrypted master key -pub const ENCRYPTED_KEY_LEN: usize = 48; - -/// The length of the (unencrypted) master key -pub const KEY_LEN: usize = 32; - -pub const PASSPHRASE_LEN: usize = 7; - -pub const LATEST_FILE_HEADER: FileHeaderVersion = FileHeaderVersion::V1; -pub const LATEST_KEYSLOT: KeyslotVersion = KeyslotVersion::V1; -pub const LATEST_METADATA: MetadataVersion = MetadataVersion::V1; -pub const LATEST_PREVIEW_MEDIA: PreviewMediaVersion = PreviewMediaVersion::V1; -pub const LATEST_STORED_KEY: StoredKeyVersion = StoredKeyVersion::V1; - -pub const ROOT_KEY_CONTEXT: &str = "spacedrive 2022-12-14 12:53:54 root key derivation"; // used for deriving keys from the root key -pub const MASTER_PASSWORD_CONTEXT: &str = - "spacedrive 2022-12-14 15:35:41 master password hash derivation"; // used for deriving keys from the master password hash -pub const FILE_KEY_CONTEXT: &str = "spacedrive 2022-12-14 12:54:12 file key derivation"; // used for deriving keys from user key/content salt hashes (for file encryption) - -pub type Key = [u8; KEY_LEN]; -pub type EncryptedKey = [u8; ENCRYPTED_KEY_LEN]; -pub type Salt = [u8; SALT_LEN]; - -#[derive(Clone)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[cfg_attr(feature = "rspc", derive(specta::Type))] -pub struct OnboardingConfig { - pub password: Protected, - pub secret_key: Option>, - pub algorithm: Algorithm, - pub hashing_algorithm: HashingAlgorithm, -} - -/// This should be used for generating nonces for encryption. -/// -/// An algorithm is required so this function can calculate the length of the nonce. -/// -/// This function uses `ChaCha20Rng` for generating cryptographically-secure random data -#[must_use] -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 should be used for generating salts for hashing. -/// -/// This function uses `ChaCha20Rng` for generating cryptographically-secure random data -#[must_use] -pub fn generate_salt() -> Salt { - let mut salt = [0u8; SALT_LEN]; - rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut salt); - salt -} - -/// This generates a master key, which should be used for encrypting the data -/// -/// This is then stored (encrypted) within the header. -/// -/// This function uses `ChaCha20Rng` for generating cryptographically-secure random data -#[must_use] -pub fn generate_master_key() -> Protected { - let mut master_key = [0u8; KEY_LEN]; - rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut master_key); - Protected::new(master_key) -} - -#[must_use] -#[allow(clippy::needless_pass_by_value)] -pub fn derive_key(key: Protected, salt: Salt, context: &str) -> Protected { - let mut input = key.expose().to_vec(); - input.extend_from_slice(&salt); - - let key = blake3::derive_key(context, &input); - - input.zeroize(); - - Protected::new(key) -} - -/// This is used for converting a `Vec` to an array of bytes -/// -/// It's main usage is for converting an encrypted master key from a `Vec` to `EncryptedKey` -/// -/// As the master key is encrypted at this point, it does not need to be `Protected<>` -/// -/// This function still `zeroize`s any data it can -pub fn to_array(bytes: Vec) -> Result<[u8; I]> { - bytes.try_into().map_err(|mut b: Vec| { - b.zeroize(); - Error::VecArrSizeMismatch - }) -} - -// /// This generates a 7 word diceware passphrase, separated with `-` -// #[must_use] -// pub fn generate_passphrase() -> Protected { -// let wordlist = include_str!("../assets/eff_large_wordlist.txt") -// .lines() -// .collect::>(); - -// let words: Vec = wordlist -// .choose_multiple( -// &mut rand_chacha::ChaCha20Rng::from_entropy(), -// PASSPHRASE_LEN, -// ) -// .map(ToString::to_string) -// .collect(); - -// let passphrase = words -// .iter() -// .enumerate() -// .map(|(i, word)| { -// if i < PASSPHRASE_LEN - 1 { -// word.clone() + "-" -// } else { -// word.clone() -// } -// }) -// .into_iter() -// .collect(); - -// Protected::new(passphrase) -// } diff --git a/crates/crypto/src/primitives/mod.rs b/crates/crypto/src/primitives/mod.rs new file mode 100644 index 000000000..217e684a6 --- /dev/null +++ b/crates/crypto/src/primitives/mod.rs @@ -0,0 +1,64 @@ +//! 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 zeroize::Zeroize; + +use crate::{ + header::{ + file::FileHeaderVersion, keyslot::KeyslotVersion, metadata::MetadataVersion, + preview_media::PreviewMediaVersion, + }, + keys::keymanager::StoredKeyVersion, + Error, Result, +}; + +pub mod types; + +/// This is the default salt size, and the recommended size for argon2id. +pub const SALT_LEN: usize = 16; + +pub const SECRET_KEY_LEN: usize = 18; + +/// 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). Plus the size of the header. +pub const BLOCK_SIZE: usize = 1_048_576; + +pub const AEAD_TAG_SIZE: usize = 16; + +/// The length of the encrypted master key +pub const ENCRYPTED_KEY_LEN: usize = 48; + +/// The length of the (unencrypted) master key +pub const KEY_LEN: usize = 32; + +pub const PASSPHRASE_LEN: usize = 7; + +pub const APP_IDENTIFIER: &str = "Spacedrive"; +pub const SECRET_KEY_IDENTIFIER: &str = "Secret key"; + +pub const LATEST_FILE_HEADER: FileHeaderVersion = FileHeaderVersion::V1; +pub const LATEST_KEYSLOT: KeyslotVersion = KeyslotVersion::V1; +pub const LATEST_METADATA: MetadataVersion = MetadataVersion::V1; +pub const LATEST_PREVIEW_MEDIA: PreviewMediaVersion = PreviewMediaVersion::V1; +pub const LATEST_STORED_KEY: StoredKeyVersion = StoredKeyVersion::V1; + +pub const ROOT_KEY_CONTEXT: &str = "spacedrive 2022-12-14 12:53:54 root key derivation"; // used for deriving keys from the root key +pub const MASTER_PASSWORD_CONTEXT: &str = + "spacedrive 2022-12-14 15:35:41 master password hash derivation"; // used for deriving keys from the master password hash +pub const FILE_KEY_CONTEXT: &str = "spacedrive 2022-12-14 12:54:12 file key derivation"; // used for deriving keys from user key/content salt hashes (for file encryption) + +/// This is used for converting a `Vec` to an array of bytes +/// +/// It's main usage is for converting an encrypted master key from a `Vec` to `EncryptedKey` +/// +/// As the master key is encrypted at this point, it does not need to be `Protected<>` +/// +/// This function still `zeroize`s any data it can +pub fn to_array(bytes: &[u8]) -> Result<[u8; I]> { + bytes.to_vec().try_into().map_err(|mut b: Vec| { + b.zeroize(); + Error::VecArrSizeMismatch + }) +} diff --git a/crates/crypto/src/primitives/types.rs b/crates/crypto/src/primitives/types.rs new file mode 100644 index 000000000..f62310275 --- /dev/null +++ b/crates/crypto/src/primitives/types.rs @@ -0,0 +1,282 @@ +use rand::{RngCore, SeedableRng}; +use std::ops::Deref; +use zeroize::Zeroize; + +use crate::{crypto::stream::Algorithm, keys::hashing::HashingAlgorithm, Error, Protected}; + +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "rspc", derive(specta::Type))] +pub enum Nonce { + XChaCha20Poly1305([u8; 20]), + Aes256Gcm([u8; 8]), +} + +impl Nonce { + pub fn generate(algorithm: Algorithm) -> crate::Result { + let mut nonce = vec![0u8; algorithm.nonce_len()]; + rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut nonce); + Self::try_from(nonce) + } + + #[must_use] + pub const fn len(&self) -> usize { + match self { + Self::Aes256Gcm(_) => 8, + Self::XChaCha20Poly1305(_) => 20, + } + } + + #[must_use] + pub const fn is_empty(&self) -> bool { + match self { + Self::Aes256Gcm(x) => x.is_empty(), + Self::XChaCha20Poly1305(x) => x.is_empty(), + } + } +} + +impl TryFrom> for Nonce { + type Error = Error; + + fn try_from(value: Vec) -> Result { + match value.len() { + 8 => Ok(Self::Aes256Gcm(to_array(&value)?)), + 20 => Ok(Self::XChaCha20Poly1305(to_array(&value)?)), + _ => Err(Error::VecArrSizeMismatch), + } + } +} + +impl AsRef<[u8]> for Nonce { + fn as_ref(&self) -> &[u8] { + match self { + Self::Aes256Gcm(x) => x, + Self::XChaCha20Poly1305(x) => x, + } + } +} + +impl Deref for Nonce { + type Target = [u8]; + fn deref(&self) -> &Self::Target { + match self { + Self::Aes256Gcm(x) => x, + Self::XChaCha20Poly1305(x) => x, + } + } +} + +#[derive(Clone)] +pub struct Key(pub Protected<[u8; KEY_LEN]>); + +impl Key { + #[must_use] + pub const fn new(v: [u8; KEY_LEN]) -> Self { + Self(Protected::new(v)) + } + + #[must_use] + #[allow(clippy::needless_pass_by_value)] + pub fn derive(key: Self, salt: Salt, context: &str) -> Self { + let mut input = key.expose().to_vec(); + input.extend_from_slice(&salt); + let key = blake3::derive_key(context, &input); + + input.zeroize(); + + Self::new(key) + } + + #[must_use] + pub const fn expose(&self) -> &[u8; KEY_LEN] { + self.0.expose() + } + + #[must_use] + pub fn generate() -> Self { + let mut key = [0u8; KEY_LEN]; + rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut key); + Self::new(key) + } +} + +impl TryFrom>> for Key { + type Error = Error; + + fn try_from(value: Protected>) -> Result { + Ok(Self::new(to_array(value.expose())?)) + } +} + +impl Deref for Key { + type Target = Protected<[u8; KEY_LEN]>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone)] +pub struct SecretKey(pub Protected<[u8; SECRET_KEY_LEN]>); + +impl SecretKey { + #[must_use] + pub const fn new(v: [u8; SECRET_KEY_LEN]) -> Self { + Self(Protected::new(v)) + } + + #[must_use] + pub const fn expose(&self) -> &[u8; SECRET_KEY_LEN] { + self.0.expose() + } + + #[must_use] + pub fn generate() -> Self { + let mut secret_key = [0u8; SECRET_KEY_LEN]; + rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut secret_key); + Self::new(secret_key) + } +} + +impl Deref for SecretKey { + type Target = Protected<[u8; SECRET_KEY_LEN]>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for SecretKeyString { + fn from(v: SecretKey) -> Self { + let hex_string: String = hex::encode_upper(v.0.expose()) + .chars() + .enumerate() + .map(|(i, c)| { + if (i + 1) % 6 == 0 && i != 35 { + c.to_string() + "-" + } else { + c.to_string() + } + }) + .into_iter() + .collect(); + + Self::new(hex_string) + } +} + +impl From for SecretKey { + fn from(v: SecretKeyString) -> Self { + let mut secret_key_sanitized = v.expose().clone(); + secret_key_sanitized.retain(|c| c != '-' && !c.is_whitespace()); + + // we shouldn't be letting on to *what* failed so we use a random secret key here if it's still invalid + // could maybe do this better (and make use of the subtle crate) + + let secret_key = hex::decode(secret_key_sanitized) + .ok() + .map_or(Vec::new(), |v| v); + + to_array(&secret_key) + .ok() + .map_or_else(Self::generate, Self::new) + } +} + +#[derive(Clone)] +pub struct Password(pub Protected); + +impl Password { + #[must_use] + pub const fn new(v: String) -> Self { + Self(Protected::new(v)) + } + + #[must_use] + pub const fn expose(&self) -> &String { + self.0.expose() + } +} + +#[derive(Clone)] +pub struct SecretKeyString(pub Protected); + +impl SecretKeyString { + #[must_use] + pub const fn new(v: String) -> Self { + Self(Protected::new(v)) + } + + #[must_use] + pub const fn expose(&self) -> &String { + self.0.expose() + } +} + +#[cfg(feature = "serde")] +use serde_big_array::BigArray; + +use super::{to_array, ENCRYPTED_KEY_LEN, KEY_LEN, SALT_LEN, SECRET_KEY_LEN}; +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "rspc", derive(specta::Type))] +pub struct EncryptedKey( + #[cfg_attr(feature = "serde", serde(with = "BigArray"))] // salt used for file data + pub [u8; ENCRYPTED_KEY_LEN], +); + +impl Deref for EncryptedKey { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TryFrom> for EncryptedKey { + type Error = Error; + + fn try_from(value: Vec) -> Result { + Ok(Self(to_array(&value)?)) + } +} + +#[derive(Clone, PartialEq, Eq, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "rspc", derive(specta::Type))] +pub struct Salt(pub [u8; SALT_LEN]); + +impl Salt { + #[must_use] + pub fn generate() -> Self { + let mut salt = [0u8; SALT_LEN]; + rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut salt); + Self(salt) + } +} + +impl Deref for Salt { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TryFrom> for Salt { + type Error = Error; + + fn try_from(value: Vec) -> Result { + Ok(Self(to_array(&value)?)) + } +} + +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[cfg_attr(feature = "rspc", derive(specta::Type))] +pub struct OnboardingConfig { + pub password: Protected, + pub algorithm: Algorithm, + pub hashing_algorithm: HashingAlgorithm, +} diff --git a/crates/crypto/src/protected.rs b/crates/crypto/src/protected.rs index 747f1a1bc..bfd38b01c 100644 --- a/crates/crypto/src/protected.rs +++ b/crates/crypto/src/protected.rs @@ -30,8 +30,6 @@ //! use std::{fmt::Debug, mem::swap}; use zeroize::Zeroize; - -pub type ProtectedVec = Protected>; #[derive(Clone)] pub struct Protected where @@ -40,17 +38,6 @@ where data: T, } -impl std::ops::Deref for Protected -where - T: Zeroize, -{ - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.data - } -} - impl Protected where T: Zeroize, diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 862d39b7e..675eee0ef 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -10,8 +10,9 @@ export type Procedures = { { key: "jobs.isRunning", input: LibraryArgs, result: boolean } | { key: "keys.getDefault", input: LibraryArgs, result: string | null } | { key: "keys.getKey", input: LibraryArgs, result: string } | - { key: "keys.hasMasterPassword", input: LibraryArgs, result: boolean } | - { key: "keys.isKeyManagerUnlocking", input: LibraryArgs, result: boolean } | + { key: "keys.getSecretKey", input: LibraryArgs, result: string | null } | + { key: "keys.isKeyManagerUnlocking", input: LibraryArgs, result: boolean | null } | + { key: "keys.isUnlocked", input: LibraryArgs, result: boolean } | { key: "keys.list", input: LibraryArgs, result: Array } | { key: "keys.listMounted", input: LibraryArgs, result: Array } | { key: "library.getStatistics", input: LibraryArgs, result: Statistics } | @@ -89,10 +90,12 @@ export interface BuildInfo { version: string, commit: string } export interface ConfigMetadata { version: string | null } -export interface CreateLibraryArgs { name: string, password: string, secret_key: string | null, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm } +export interface CreateLibraryArgs { name: string, password: string, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm } export interface EditLibraryArgs { id: string, name: string | null, description: string | null } +export type EncryptedKey = Array + export type ExplorerContext = { type: "Location" } & Location | { type: "Tag" } & Tag export interface ExplorerData { context: ExplorerContext, items: Array } @@ -147,7 +150,7 @@ export interface LocationExplorerArgs { location_id: number, path: string, limit export interface LocationUpdateArgs { id: number, name: string | null, indexer_rules_ids: Array } -export interface MasterPasswordChangeArgs { password: string, secret_key: string | null, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm } +export interface MasterPasswordChangeArgs { password: string, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm } export interface MediaData { id: number, pixel_width: number | null, pixel_height: number | null, longitude: number | null, latitude: number | null, fps: number | null, capture_device_make: string | null, capture_device_model: string | null, capture_device_software: string | null, duration_seconds: number | null, codecs: string | null, streams: number | null } @@ -157,6 +160,8 @@ export interface NodeConfig { version: string | null, id: string, name: string, export interface NodeState { version: string | null, id: string, name: string, p2p_port: number | null, data_path: string } +export type Nonce = { XChaCha20Poly1305: Array } | { Aes256Gcm: Array } + export interface NormalisedCompositeId { $type: string, $id: any, org_id: string, user_id: string } export interface NormalisedOrganisation { $type: string, $id: any, id: string, name: string, users: NormalizedVec, owner: NormalisedUser, non_normalised_data: Array } @@ -171,17 +176,21 @@ export interface ObjectValidatorArgs { id: number, path: string } export type Params = "Standard" | "Hardened" | "Paranoid" -export interface RestoreBackupArgs { password: string, secret_key: string | null, path: string } +export interface RestoreBackupArgs { password: string, secret_key: string, path: string } export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent" +export type Salt = Array + export interface SetFavoriteArgs { id: number, favorite: boolean } export interface SetNoteArgs { id: number, note: string | null } export interface Statistics { id: number, date_captured: string, total_object_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string } -export interface StoredKey { uuid: string, version: StoredKeyVersion, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, content_salt: Array, master_key: Array, master_key_nonce: Array, key_nonce: Array, key: Array, salt: Array, memory_only: boolean, automount: boolean } +export interface StoredKey { uuid: string, version: StoredKeyVersion, key_type: StoredKeyType, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, content_salt: Salt, master_key: EncryptedKey, master_key_nonce: Nonce, key_nonce: Nonce, key: Array, salt: Salt, memory_only: boolean, automount: boolean } + +export type StoredKeyType = "User" | "Root" export type StoredKeyVersion = "V1" @@ -193,7 +202,7 @@ export interface TagCreateArgs { name: string, color: string } export interface TagUpdateArgs { id: number, name: string | null, color: string | null } -export interface UnlockKeyManagerArgs { password: string, secret_key: string | null } +export interface UnlockKeyManagerArgs { password: string, secret_key: string } export interface Volume { name: string, mount_point: string, total_capacity: bigint, available_capacity: bigint, is_removable: boolean, disk_type: string | null, file_system: string | null, is_root_filesystem: boolean } diff --git a/packages/interface/package.json b/packages/interface/package.json index 94d25c89f..11d3bf59f 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -54,6 +54,7 @@ "react-hook-form": "^7.36.1", "react-json-view": "^1.21.3", "react-loading-skeleton": "^3.1.0", + "react-qr-code": "^2.0.11", "react-router": "6.4.2", "react-router-dom": "6.4.2", "rooks": "^5.14.0", diff --git a/packages/interface/src/components/dialog/BackupRestoreDialog.tsx b/packages/interface/src/components/dialog/BackupRestoreDialog.tsx index 32f11179e..f48f46e09 100644 --- a/packages/interface/src/components/dialog/BackupRestoreDialog.tsx +++ b/packages/interface/src/components/dialog/BackupRestoreDialog.tsx @@ -49,12 +49,10 @@ export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => { }); const onSubmit = form.handleSubmit((data) => { - const sk = data.secretKey || null; - if (data.filePath !== '') { restoreKeystoreMutation.mutate({ password: data.masterPassword, - secret_key: sk, + secret_key: data.secretKey, path: data.filePath }); form.reset(); diff --git a/packages/interface/src/components/dialog/CreateLibraryDialog.tsx b/packages/interface/src/components/dialog/CreateLibraryDialog.tsx index 1310409b3..4cd1b04ba 100644 --- a/packages/interface/src/components/dialog/CreateLibraryDialog.tsx +++ b/packages/interface/src/components/dialog/CreateLibraryDialog.tsx @@ -15,7 +15,6 @@ const schema = z.object({ name: z.string(), password: z.string(), password_validate: z.string(), - secret_key: z.string(), algorithm: z.string(), hashing_algorithm: z.string() }); @@ -36,10 +35,8 @@ export default function CreateLibraryDialog(props: Props) { const [showMasterPassword1, setShowMasterPassword1] = useState(false); const [showMasterPassword2, setShowMasterPassword2] = useState(false); - const [showSecretKey, setShowSecretKey] = useState(false); const MP1CurrentEyeIcon = showMasterPassword1 ? EyeSlash : Eye; const MP2CurrentEyeIcon = showMasterPassword2 ? EyeSlash : Eye; - const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye; const queryClient = useQueryClient(); const createLibrary = useBridgeMutation('library.create', { @@ -151,43 +148,6 @@ export default function CreateLibraryDialog(props: Props) { -
-

Key secret (optional)

-
- - - - -
-
diff --git a/packages/interface/src/components/dialog/MasterPasswordChangeDialog.tsx b/packages/interface/src/components/dialog/MasterPasswordChangeDialog.tsx index d6c16c778..3c28fda3d 100644 --- a/packages/interface/src/components/dialog/MasterPasswordChangeDialog.tsx +++ b/packages/interface/src/components/dialog/MasterPasswordChangeDialog.tsx @@ -1,4 +1,3 @@ -import cryptoRandomString from 'crypto-random-string'; import { ArrowsClockwise, Clipboard, Eye, EyeSlash } from 'phosphor-react'; import { useState } from 'react'; import { Algorithm, useLibraryMutation } from '@sd/client'; @@ -14,7 +13,6 @@ export type MasterPasswordChangeDialogProps = UseDialogProps; const schema = z.object({ masterPassword: z.string(), masterPassword2: z.string(), - secretKey: z.string().nullable(), encryptionAlgo: z.string(), hashingAlgo: z.string() }); @@ -38,15 +36,13 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp const [show, setShow] = useState({ masterPassword: false, - masterPassword2: false, - secretKey: false + masterPassword2: false }); const dialog = useDialog(props); const MP1CurrentEyeIcon = show.masterPassword ? EyeSlash : Eye; const MP2CurrentEyeIcon = show.masterPassword2 ? EyeSlash : Eye; - const SKCurrentEyeIcon = show.secretKey ? EyeSlash : Eye; const form = useZodForm({ schema, @@ -69,8 +65,7 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp return changeMasterPassword.mutateAsync({ algorithm: data.encryptionAlgo as Algorithm, hashing_algorithm, - password: data.masterPassword, - secret_key: data.secretKey || null + password: data.masterPassword }); } }); @@ -81,7 +76,7 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp onSubmit={onSubmit} dialog={dialog} title="Change Master Password" - description="Select a new master password for your key manager. Leave the key secret blank to disable it." + description="Select a new master password for your key manager." ctaDanger={true} ctaLabel="Change" > @@ -145,44 +140,6 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp
-
- - - - -
-
diff --git a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx index 5efa40e67..ac8b16fe2 100644 --- a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx +++ b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx @@ -211,11 +211,9 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps const params = useExplorerParams(); const objectData = data ? (isObject(data) ? data.item : data.item.object) : null; - const hasMasterPasswordQuery = useLibraryQuery(['keys.hasMasterPassword']); - const hasMasterPassword = - hasMasterPasswordQuery.data !== undefined && hasMasterPasswordQuery.data === true - ? true - : false; + const isUnlockedQuery = useLibraryQuery(['keys.isUnlocked']); + const isUnlocked = + isUnlockedQuery.data !== undefined && isUnlockedQuery.data === true ? true : false; const mountedUuids = useLibraryQuery(['keys.listMounted']); const hasMountedKeys = @@ -319,7 +317,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps icon={LockSimple} keybind="⌘E" onClick={() => { - if (hasMasterPassword && hasMountedKeys) { + if (isUnlocked && hasMountedKeys) { dialogManager.create((dp) => ( )); - } else if (!hasMasterPassword) { + } else if (!isUnlocked) { showAlertDialog({ title: 'Key manager locked', value: 'The key manager is currently locked. Please unlock it and try again.' @@ -346,7 +344,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps icon={LockSimpleOpen} keybind="⌘D" onClick={() => { - if (hasMasterPassword) { + if (isUnlocked) { dialogManager.create((dp) => ( { showAlertDialog({ title: 'Unlock Error', @@ -29,7 +30,9 @@ export function KeyManager(props: KeyManagerProps) { const [masterPassword, setMasterPassword] = useState(''); const [secretKey, setSecretKey] = useState(''); - if (!hasMasterPw?.data) { + const [enterSkManually, setEnterSkManually] = useState(keyringSk?.data === null); + + if (!isUnlocked?.data) { const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye; const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye; @@ -53,37 +56,54 @@ export function KeyManager(props: KeyManagerProps) {
-
- setSecretKey(e.target.value)} - type={showSecretKey ? 'text' : 'password'} - className="flex-grow !py-0.5" - placeholder="Secret Key" - /> - -
+ {enterSkManually && ( +
+ setSecretKey(e.target.value)} + type={showSecretKey ? 'text' : 'password'} + className="flex-grow !py-0.5" + placeholder="Secret Key" + /> + +
+ )} + {!enterSkManually && ( +
+

{ + setEnterSkManually(true); + }} + > + or enter secret key manually +

+
+ )}
); } else { diff --git a/packages/interface/src/screens/settings/library/KeysSetting.tsx b/packages/interface/src/screens/settings/library/KeysSetting.tsx index d0ef16870..9a96fa0aa 100644 --- a/packages/interface/src/screens/settings/library/KeysSetting.tsx +++ b/packages/interface/src/screens/settings/library/KeysSetting.tsx @@ -2,6 +2,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import clsx from 'clsx'; import { Eye, EyeSlash, Lock, Plus } from 'phosphor-react'; import { PropsWithChildren, useState } from 'react'; +import QRCode from 'react-qr-code'; import { animated, useTransition } from 'react-spring'; import { HashingAlgorithm, useLibraryMutation, useLibraryQuery } from '@sd/client'; import { Button, Input, dialogManager } from '@sd/ui'; @@ -10,6 +11,7 @@ import { KeyViewerDialog } from '~/components/dialog/KeyViewerDialog'; import { MasterPasswordChangeDialog } from '~/components/dialog/MasterPasswordChangeDialog'; import { ListOfKeys } from '~/components/key/KeyList'; import { KeyMounter } from '~/components/key/KeyMounter'; +import { DefaultProps } from '~/components/primitive/types'; import { SettingsContainer } from '~/components/settings/SettingsContainer'; import { SettingsHeader } from '~/components/settings/SettingsHeader'; import { SettingsSubHeader } from '~/components/settings/SettingsSubHeader'; @@ -75,8 +77,9 @@ export const KeyMounterDropdown = ({ export default function KeysSettings() { const platform = usePlatform(); - const hasMasterPw = useLibraryQuery(['keys.hasMasterPassword']); - const setMasterPasswordMutation = useLibraryMutation('keys.unlockKeyManager', { + const isUnlocked = useLibraryQuery(['keys.isUnlocked']); + const keyringSk = useLibraryQuery(['keys.getSecretKey'], { initialData: '' }); // assume true by default, as it will often be the case. need to fix this with an rspc subscription+such + const unlockKeyManager = useLibraryMutation('keys.unlockKeyManager', { onError: () => { showAlertDialog({ title: 'Unlock Error', @@ -84,6 +87,7 @@ export default function KeysSettings() { }); } }); + const unmountAll = useLibraryMutation('keys.unmountAll'); const clearMasterPassword = useLibraryMutation('keys.clearMasterPassword'); const backupKeystore = useLibraryMutation('keys.backupKeystore'); @@ -92,14 +96,17 @@ export default function KeysSettings() { const [showMasterPassword, setShowMasterPassword] = useState(false); const [showSecretKey, setShowSecretKey] = useState(false); const [masterPassword, setMasterPassword] = useState(''); - const [secretKey, setSecretKey] = useState(''); + const [secretKey, setSecretKey] = useState(''); // for the unlock form + const [viewSecretKey, setViewSecretKey] = useState(false); // for the settings page const keys = useLibraryQuery(['keys.list']); const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye; const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye; - if (!hasMasterPw?.data) { + const [enterSkManually, setEnterSkManually] = useState(keyringSk?.data === null); + + if (!isUnlocked?.data) { return (
@@ -119,39 +126,55 @@ export default function KeysSettings() {
- -
- setSecretKey(e.target.value)} - type={showSecretKey ? 'text' : 'password'} - className="flex-grow !py-0.5" - placeholder="Secret Key" - /> - -
+ {enterSkManually && ( +
+ setSecretKey(e.target.value)} + type={showSecretKey ? 'text' : 'password'} + className="flex-grow !py-0.5" + placeholder="Secret Key" + /> + +
+ )} + {!enterSkManually && ( +
+

{ + setEnterSkManually(true); + }} + > + or enter secret key manually +

+
+ )}
); } else { @@ -186,10 +209,37 @@ export default function KeysSettings() { } /> +
+ {keyringSk?.data && ( + <> + + {!viewSecretKey && ( +
+ +
+ )} + {viewSecretKey && ( +
{ + keyringSk.data && navigator.clipboard.writeText(keyringSk.data); + }} + > + <> + +

{keyringSk.data}

+ +
+ )} + + )} +