diff --git a/Cargo.lock b/Cargo.lock index 26cdb2243..80b51cf1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -421,9 +421,9 @@ checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64ct" @@ -2286,7 +2286,7 @@ source = "git+https://github.com/oscartbeaumont/httpz.git?rev=1ddbd9ad594ac7ee3e dependencies = [ "async-tungstenite", "axum", - "base64 0.13.0", + "base64 0.13.1", "cookie", "form_urlencoded", "futures", @@ -3816,7 +3816,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", ] [[package]] @@ -4021,7 +4021,7 @@ version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "indexmap", "line-wrap", "serde", @@ -4114,7 +4114,7 @@ name = "prisma-client-rust" version = "0.6.3" source = "git+https://github.com/Brendonovich/prisma-client-rust.git?tag=0.6.3#c81f22fb287a2801da5a5961ed1ec9e99f5bee34" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "bigdecimal", "chrono", "datamodel", @@ -4822,7 +4822,7 @@ version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "bytes", "encoding_rs", "futures-core", @@ -5022,7 +5022,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", ] [[package]] @@ -5031,7 +5031,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", ] [[package]] @@ -5144,7 +5144,7 @@ version = "0.1.0" dependencies = [ "async-stream", "async-trait", - "base64 0.13.0", + "base64 0.13.1", "blake3", "chrono", "ctor", @@ -5208,6 +5208,7 @@ dependencies = [ "aead", "aes-gcm", "argon2", + "base64 0.13.1", "chacha20poly1305", "dashmap", "rand 0.8.5", @@ -5960,7 +5961,7 @@ name = "swift-rs" version = "0.3.0" source = "git+https://github.com/Brendonovich/swift-rs.git?branch=autorelease#b16ba936ca2330bb27c6b9b7a84ad0d583ef0caa" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "serde", "serde_json", "swift-rs-macros", @@ -6187,7 +6188,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "356fa253e40ae4d6ff02075011f2f2bb4066f5c9d8c1e16ca6912d7b75903ba6" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "brotli", "ico", "json-patch", @@ -6729,7 +6730,7 @@ version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "byteorder", "bytes", "http", @@ -7495,7 +7496,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff5c1352b4266fdf92c63479d2f58ab4cd29dc4e78fbc1b62011ed1227926945" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "block", "cocoa", "core-graphics", diff --git a/core/Cargo.toml b/core/Cargo.toml index ec8d91611..70d45e0b9 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -54,7 +54,7 @@ image = "0.24.4" webp = "0.2.2" ffmpeg-next = { version = "5.1.1", optional = true, features = [] } sd-ffmpeg = { path = "../crates/ffmpeg", optional = true } -sd-crypto = { path = "../crates/crypto", features = ["rspc"] } +sd-crypto = { path = "../crates/crypto", features = ["rspc", "serde"] } sd-file-ext = { path = "../crates/file-ext"} fs_extra = "1.2.0" tracing = "0.1.36" diff --git a/core/prisma/migrations/20221108102622_salt_removal/migration.sql b/core/prisma/migrations/20221108102622_salt_removal/migration.sql new file mode 100644 index 000000000..4e4930f8e --- /dev/null +++ b/core/prisma/migrations/20221108102622_salt_removal/migration.sql @@ -0,0 +1,29 @@ +/* + Warnings: + + - You are about to drop the column `salt` on the `key` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_key" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "uuid" TEXT NOT NULL, + "name" TEXT, + "default" BOOLEAN NOT NULL DEFAULT false, + "date_created" DATETIME DEFAULT CURRENT_TIMESTAMP, + "algorithm" BLOB NOT NULL, + "hashing_algorithm" BLOB NOT NULL, + "content_salt" BLOB NOT NULL, + "master_key" BLOB NOT NULL, + "master_key_nonce" BLOB NOT NULL, + "key_nonce" BLOB NOT NULL, + "key" BLOB NOT NULL, + "automount" BOOLEAN NOT NULL DEFAULT false +); +INSERT INTO "new_key" ("algorithm", "automount", "content_salt", "date_created", "default", "hashing_algorithm", "id", "key", "key_nonce", "master_key", "master_key_nonce", "name", "uuid") SELECT "algorithm", "automount", "content_salt", "date_created", "default", "hashing_algorithm", "id", "key", "key_nonce", "master_key", "master_key_nonce", "name", "uuid" FROM "key"; +DROP TABLE "key"; +ALTER TABLE "new_key" RENAME TO "key"; +CREATE UNIQUE INDEX "key_uuid_key" ON "key"("uuid"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index 2b1610168..9a7dc9b1f 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -203,8 +203,6 @@ model Key { algorithm Bytes // hashing algorithm used for hashing the master password hashing_algorithm Bytes - // salt to hash the master password with - salt Bytes // salt used for encrypting data with this key content_salt Bytes // the *encrypted* master key (48 bytes) diff --git a/core/src/api/keys.rs b/core/src/api/keys.rs index 56bbed01d..838e22484 100644 --- a/core/src/api/keys.rs +++ b/core/src/api/keys.rs @@ -1,7 +1,13 @@ -use std::str::FromStr; +use std::io::{Read, Write}; +use std::{path::PathBuf, str::FromStr}; -use sd_crypto::{crypto::stream::Algorithm, keys::hashing::HashingAlgorithm, Protected}; -use serde::Deserialize; +use sd_crypto::keys::keymanager::StoredKey; +use sd_crypto::{ + crypto::stream::Algorithm, + keys::{hashing::HashingAlgorithm, keymanager::KeyManager}, + Protected, +}; +use serde::{Deserialize, Serialize}; use specta::Type; use crate::{invalidate_query, prisma::key}; @@ -13,6 +19,7 @@ pub struct KeyAddArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, key: String, + library_sync: bool, } #[derive(Type, Deserialize)] @@ -21,15 +28,65 @@ pub struct KeyNameUpdateArgs { name: String, } +#[derive(Type, Deserialize)] +pub struct SetMasterPasswordArgs { + password: String, + secret_key: String, +} + +#[derive(Type, Deserialize)] +pub struct RestoreBackupArgs { + password: String, + secret_key: String, + path: PathBuf, +} + +#[derive(Type, Deserialize)] +pub struct OnboardingArgs { + algorithm: Algorithm, + hashing_algorithm: HashingAlgorithm, +} + +#[derive(Type, Deserialize)] +pub struct MasterPasswordChangeArgs { + password: String, + algorithm: Algorithm, + hashing_algorithm: HashingAlgorithm, +} + +#[derive(Type, Serialize)] +pub struct OnboardingKeys { + master_password: String, + secret_key: String, +} + pub(crate) fn mount() -> RouterBuilder { RouterBuilder::new() .library_query("list", |t| { 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()?) }) + }) // this is so we can show the key as mounted in the UI .library_query("listMounted", |t| { t(|_, _: (), library| async move { Ok(library.key_manager.get_mounted_uuids()) }) }) + .library_query("getKey", |t| { + t(|_, key_uuid: uuid::Uuid, library| async move { + let key = library.key_manager.get_key(key_uuid)?; + + let key_string = String::from_utf8(key.expose().clone()).map_err(|_| { + rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Error serializing bytes to String".into(), + ) + })?; + + Ok(key_string) + }) + }) .library_mutation("mount", |t| { t(|_, key_uuid: uuid::Uuid, library| async move { library.key_manager.mount(key_uuid)?; @@ -61,6 +118,14 @@ pub(crate) fn mount() -> RouterBuilder { Ok(()) }) }) + .library_mutation("clearMasterPassword", |t| { + t(|_, _: (), library| async move { + library.key_manager.clear_master_password()?; + + invalidate_query!(library, "keys.hasMasterPassword"); + Ok(()) + }) + }) .library_mutation("deleteFromLibrary", |t| { t(|_, key_uuid: uuid::Uuid, library| async move { library.key_manager.remove_key(key_uuid)?; @@ -79,16 +144,53 @@ pub(crate) fn mount() -> RouterBuilder { Ok(()) }) }) - .library_mutation("setMasterPassword", |t| { - t(|_, password: String, library| async move { - // need to add master password checks in the keymanager itself to make sure it's correct - // this can either unwrap&fail, or we can return the error. either way, the user will have to correct this - // by entering the correct password - // for now, automounting might have to serve as the master password checks + .library_mutation("onboarding", |t| { + t(|_, args: OnboardingArgs, library| async move { + let bundle = KeyManager::onboarding(args.algorithm, args.hashing_algorithm)?; + + let verification_key = bundle.verification_key; + + // remove old nil-id keys if they were set + // they possibly won't be, but we CANNOT have multiple + library + .db + .key() + .delete_many(vec![key::uuid::equals(uuid::Uuid::nil().to_string())]) + .exec() + .await?; library - .key_manager - .set_master_password(Protected::new(password.as_bytes().to_vec()))?; + .db + .key() + .create( + verification_key.uuid.to_string(), + verification_key.algorithm.serialize().to_vec(), + verification_key.hashing_algorithm.serialize().to_vec(), + verification_key.content_salt.to_vec(), + verification_key.master_key.to_vec(), + verification_key.master_key_nonce.to_vec(), + verification_key.key_nonce.to_vec(), + verification_key.key.to_vec(), + vec![], + ) + .exec() + .await?; + + let keys = OnboardingKeys { + master_password: bundle.master_password.expose().clone(), + secret_key: base64::encode(bundle.secret_key.expose()), + }; + + Ok(keys) + }) + }) + .library_mutation("setMasterPassword", |t| { + t(|_, args: SetMasterPasswordArgs, library| async move { + // if this returns an error, the user MUST re-enter the correct password + library.key_manager.set_master_password( + Protected::new(args.password), + Protected::new(args.secret_key), + )?; let automount = library .db @@ -108,6 +210,8 @@ pub(crate) fn mount() -> RouterBuilder { })?)?; } + invalidate_query!(library, "keys.hasMasterPassword"); + Ok(()) }) }) @@ -161,17 +265,10 @@ pub(crate) fn mount() -> RouterBuilder { }) .library_query("getDefault", |t| { t(|_, _: (), library| async move { - // `find_first` should be okay here as only one default key should ever be set - // this is also stored in the keymanager but it's probably easier to get it from the DB - let default = library - .db - .key() - .find_first(vec![key::default::equals(true)]) - .exec() - .await?; + let default = library.key_manager.get_default(); - if let Some(default_key) = default { - Ok(Some(default_key.uuid)) + if let Ok(default_key) = default { + Ok(Some(default_key)) } else { Ok(None) } @@ -196,23 +293,24 @@ pub(crate) fn mount() -> RouterBuilder { let stored_key = library.key_manager.access_keystore(uuid)?; - library - .db - .key() - .create( - uuid.to_string(), - args.algorithm.serialize().to_vec(), - args.hashing_algorithm.serialize().to_vec(), - stored_key.salt.to_vec(), - stored_key.content_salt.to_vec(), - stored_key.master_key.to_vec(), - stored_key.master_key_nonce.to_vec(), - stored_key.key_nonce.to_vec(), - stored_key.key.to_vec(), - vec![], - ) - .exec() - .await?; + if args.library_sync { + library + .db + .key() + .create( + uuid.to_string(), + args.algorithm.serialize().to_vec(), + args.hashing_algorithm.serialize().to_vec(), + stored_key.content_salt.to_vec(), + stored_key.master_key.to_vec(), + stored_key.master_key_nonce.to_vec(), + stored_key.key_nonce.to_vec(), + stored_key.key.to_vec(), + vec![], + ) + .exec() + .await?; + } // mount the key library.key_manager.mount(uuid)?; @@ -222,4 +320,146 @@ pub(crate) fn mount() -> RouterBuilder { Ok(()) }) }) + .library_mutation("backupKeystore", |t| { + t(|_, path: PathBuf, library| async move { + // dump all stored keys that are in the key manager (maybe these should be taken from prisma as this will include even "non-sync with library" keys) + let mut stored_keys = library.key_manager.dump_keystore(); + // include the verification key at the time of backup + stored_keys.push(library.key_manager.get_verification_key()?); + + let mut output_file = std::fs::File::create(path).map_err(|_| { + rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Error creating file".into(), + ) + })?; + output_file + .write_all(&serde_json::to_vec(&stored_keys).map_err(|_| { + rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Error serializing keystore".into(), + ) + })?) + .map_err(|_| { + rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Error writing key backup to file".into(), + ) + })?; + Ok(()) + }) + }) + .library_mutation("restoreKeystore", |t| { + t(|_, args: RestoreBackupArgs, library| async move { + let mut input_file = std::fs::File::open(args.path).map_err(|_| { + rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Error opening backup file".into(), + ) + })?; + + let mut backup = Vec::new(); + + input_file.read_to_end(&mut backup).map_err(|_| { + rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Error reading backup file".into(), + ) + })?; + + let stored_keys: Vec = + serde_json::from_slice(&backup).map_err(|_| { + rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Error deserializing backup".into(), + ) + })?; + + let updated_keys = library.key_manager.import_keystore_backup( + Protected::new(args.password), + Protected::new(args.secret_key), + &stored_keys, + )?; + + for key in &updated_keys { + library + .db + .key() + .create( + key.uuid.to_string(), + key.algorithm.serialize().to_vec(), + key.hashing_algorithm.serialize().to_vec(), + key.content_salt.to_vec(), + key.master_key.to_vec(), + key.master_key_nonce.to_vec(), + key.key_nonce.to_vec(), + key.key.to_vec(), + vec![], + ) + .exec() + .await?; + } + + invalidate_query!(library, "keys.list"); + invalidate_query!(library, "keys.listMounted"); + + Ok(updated_keys.len()) + }) + }) + .library_mutation("changeMasterPassword", |t| { + t(|_, args: MasterPasswordChangeArgs, library| async move { + let bundle = library.key_manager.change_master_password( + Protected::new(args.password), + args.algorithm, + args.hashing_algorithm, + )?; + + let verification_key = bundle.verification_key; + + // remove old nil-id keys if they were set + // they possibly won't be, but we CANNOT have multiple + library.db.key().delete_many(vec![]).exec().await?; + + library + .db + .key() + .create( + verification_key.uuid.to_string(), + verification_key.algorithm.serialize().to_vec(), + verification_key.hashing_algorithm.serialize().to_vec(), + verification_key.content_salt.to_vec(), + verification_key.master_key.to_vec(), + verification_key.master_key_nonce.to_vec(), + verification_key.key_nonce.to_vec(), + verification_key.key.to_vec(), + vec![], + ) + .exec() + .await?; + + // sync new changes with prisma + // note, this will write keys that were potentially marked as "don't sync to db" + // i think the way around this will be to include a marker in the `StoredKey` struct, as that means we can exclude them from `dump_keystore()` commands + for key in bundle.updated_keystore { + library + .db + .key() + .create( + key.uuid.to_string(), + key.algorithm.serialize().to_vec(), + key.hashing_algorithm.serialize().to_vec(), + key.content_salt.to_vec(), + key.master_key.to_vec(), + key.master_key_nonce.to_vec(), + key.key_nonce.to_vec(), + key.key.to_vec(), + vec![], + ) + .exec() + .await?; + } + + Ok(bundle.secret_key.expose().clone()) + }) + }) } diff --git a/core/src/library/library_manager.rs b/core/src/library/library_manager.rs index 699360a60..8cf5a6f4f 100644 --- a/core/src/library/library_manager.rs +++ b/core/src/library/library_manager.rs @@ -1,7 +1,7 @@ use crate::{ invalidate_query, node::Platform, - prisma::{node, PrismaClient}, + prisma::{key, node, PrismaClient}, util::{ db::load_and_migrate, seeder::{indexer_rules_seeder, SeederError}, @@ -12,11 +12,10 @@ use crate::{ use sd_crypto::{ crypto::stream::Algorithm, keys::{ - hashing::HashingAlgorithm, + hashing::{HashingAlgorithm, Params}, keymanager::{KeyManager, StoredKey}, }, primitives::to_array, - Protected, }; use std::{ env, fs, io, @@ -74,11 +73,51 @@ impl From for rspc::Error { pub async fn create_keymanager(client: &PrismaClient) -> Result { // retrieve all stored keys from the DB - let key_manager = KeyManager::new(vec![], None); + let key_manager = KeyManager::new(vec![]); + + // BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE + // this is so if there's no verification key set, we set one so users can use the key manager + // it will be done during onboarding, but for now things are statically set (unless they were changed) + if client + .key() + .find_many(vec![key::uuid::equals(uuid::Uuid::nil().to_string())]) + .exec() + .await? + .is_empty() + { + client + .key() + .delete_many(vec![key::uuid::equals(uuid::Uuid::nil().to_string())]) + .exec() + .await?; + // BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE + let verification_key = KeyManager::onboarding( + Algorithm::XChaCha20Poly1305, + HashingAlgorithm::Argon2id(Params::Standard), + )? + .verification_key; + + // BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE + client + .key() + .create( + verification_key.uuid.to_string(), + verification_key.algorithm.serialize().to_vec(), + verification_key.hashing_algorithm.serialize().to_vec(), + verification_key.content_salt.to_vec(), + verification_key.master_key.to_vec(), + verification_key.master_key_nonce.to_vec(), + verification_key.key_nonce.to_vec(), + verification_key.key.to_vec(), + vec![], + ) + .exec() + .await?; + } let db_stored_keys = client.key().find_many(vec![]).exec().await?; - let mut default = Uuid::default(); + let mut default = Uuid::nil(); // collect and serialize the stored keys // shouldn't call unwrap so much here @@ -95,7 +134,6 @@ pub async fn create_keymanager(client: &PrismaClient) -> Result Result From> for Error { diff --git a/crates/crypto/src/header/metadata.rs b/crates/crypto/src/header/metadata.rs index 5414ae95a..80d203210 100644 --- a/crates/crypto/src/header/metadata.rs +++ b/crates/crypto/src/header/metadata.rs @@ -29,12 +29,15 @@ //! ``` use std::io::{Read, Seek}; +#[cfg(feature = "serde")] use crate::{ - crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, + crypto::stream::{StreamDecryption, StreamEncryption}, primitives::{generate_nonce, MASTER_KEY_LEN}, - Error, Protected, Result, + Protected, }; +use crate::{crypto::stream::Algorithm, Error, Result}; + use super::file::FileHeader; /// This is a metadata header item. You may add it to a header, and this will be stored with the file. @@ -63,6 +66,7 @@ impl FileHeader { /// You will need to provide the user's password, and a semi-universal salt for hashing the user's password. This allows for extremely fast decryption. /// /// Metadata needs to be accessed switfly, so a key management system should handle the salt generation. + #[cfg(feature = "serde")] pub fn add_metadata( &mut self, version: MetadataVersion, @@ -100,6 +104,7 @@ impl FileHeader { /// 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 fn decrypt_metadata_from_prehashed( &self, hashed_keys: Vec>, @@ -130,6 +135,7 @@ impl FileHeader { /// 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 fn decrypt_metadata(&self, password: Protected>) -> Result where T: serde::de::DeserializeOwned, diff --git a/crates/crypto/src/keys/hashing.rs b/crates/crypto/src/keys/hashing.rs index b41f5534f..91ed36c4e 100644 --- a/crates/crypto/src/keys/hashing.rs +++ b/crates/crypto/src/keys/hashing.rs @@ -13,13 +13,17 @@ use crate::Protected; use crate::{primitives::SALT_LEN, Error, Result}; use argon2::Argon2; -use serde::{Deserialize, Serialize}; -use specta::Type; /// These parameters define the password-hashing level. /// /// The harder the parameter, the longer the password will take to hash. -#[derive(Clone, Copy, PartialEq, Eq, Type, Serialize, Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize), + derive(serde::Deserialize) +)] +#[cfg_attr(feature = "rspc", derive(specta::Type))] #[allow(clippy::use_self)] pub enum Params { Standard, @@ -28,7 +32,13 @@ pub enum Params { } /// This defines all available password hashing algorithms. -#[derive(Clone, Copy, PartialEq, Eq, Type, Serialize, Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize), + derive(serde::Deserialize) +)] +#[cfg_attr(feature = "rspc", derive(specta::Type))] pub enum HashingAlgorithm { Argon2id(Params), } diff --git a/crates/crypto/src/keys/keymanager.rs b/crates/crypto/src/keys/keymanager.rs index e748dc7b7..7127d7778 100644 --- a/crates/crypto/src/keys/keymanager.rs +++ b/crates/crypto/src/keys/keymanager.rs @@ -39,7 +39,7 @@ use std::sync::Mutex; use crate::crypto::stream::{StreamDecryption, StreamEncryption}; use crate::primitives::{ - generate_master_key, generate_nonce, generate_salt, to_array, MASTER_KEY_LEN, + generate_master_key, generate_nonce, generate_passphrase, generate_salt, to_array, }; use crate::{ crypto::stream::Algorithm, @@ -49,11 +49,11 @@ use crate::{ use crate::{Error, Result}; use dashmap::DashMap; -use serde::Serialize; -use serde_big_array::BigArray; -use specta::Type; use uuid::Uuid; +#[cfg(feature = "serde")] +use serde_big_array::BigArray; + use super::hashing::HashingAlgorithm; // The terminology in this file is very confusing. @@ -63,17 +63,19 @@ use super::hashing::HashingAlgorithm; // The `hashed_key` refers to the value you'd pass to PVM/MD decryption functions. It has been pre-hashed with the content salt. // The content salt refers to the semi-universal salt that's used for metadata/preview media (unique to each key in the manager) -#[derive(Clone, PartialEq, Eq, Type, Serialize)] +/// 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 algorithm: Algorithm, // encryption algorithm for encrypting the master key. can be changed (requires a re-encryption though) - pub hashing_algorithm: HashingAlgorithm, // hashing algorithm to use for hashing everything related to this key. can't be changed once set. - pub salt: [u8; SALT_LEN], // salt to hash the master password with - pub content_salt: [u8; SALT_LEN], // salt used for file data - #[serde(with = "BigArray")] + pub hashing_algorithm: HashingAlgorithm, // hashing algorithm used for hashing the key with the content salt + pub content_salt: [u8; SALT_LEN], + #[cfg_attr(feature = "serde", serde(with = "BigArray"))] // salt used for file data pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], // this is for encrypting the `key` - pub master_key_nonce: Vec, // nonce for encrypting the master key - pub key_nonce: Vec, // nonce used for encrypting the main key + pub 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) } @@ -82,9 +84,7 @@ pub struct StoredKey { /// 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 key: Protected>, // the actual key itself, text format encodable (so it can be viewed with an UI) - pub content_salt: [u8; SALT_LEN], // the salt used for file data + pub uuid: Uuid, // used for identification. shared with stored keys pub hashed_key: Protected<[u8; 32]>, // this is hashed with the content salt, for instant access } @@ -94,26 +94,114 @@ pub struct MountedKey { /// /// Use the associated functions to interact with it. pub struct KeyManager { - master_password: Mutex>>>, // the user's. we take ownership here to prevent other functions attempting to manage/pass it to us + master_password: Mutex>>, // the *hashed* master password+secret key combo + verification_key: Mutex>, keystore: DashMap, keymount: DashMap, default: Mutex>, } +// bundle returned during onboarding +// nil key should be stored within prisma +// secret key should be written down by the user (along with the master password) +/// This bundle is returned during onboarding. +/// +/// The verification key should be written to the database, and only one nil-UUID key should exist at any given point for a library. +/// +/// The secret key needs to be given to the user, and should be written down. +pub struct OnboardingBundle { + pub verification_key: StoredKey, // nil UUID key that is only ever used for verifying the master password is correct + pub master_password: Protected, + pub secret_key: Protected, // base64 encoded string that is required along with the master password +} + +pub struct MasterPasswordChangeBundle { + pub verification_key: StoredKey, // nil UUID key that is only ever used for verifying the master password is correct + pub secret_key: Protected, // base64 encoded string that is required along with the master password + pub updated_keystore: Vec, +} + /// The `KeyManager` functions should be used for all key-related management. impl KeyManager { - /// Initialize the Key Manager with the user's master password, and `StoredKeys` retrieved from Prisma + /// 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, encoded in base64) + /// + /// It will also generate a verification key, which should be written to the database. + #[allow(clippy::needless_pass_by_value)] + pub fn onboarding( + algorithm: Algorithm, + hashing_algorithm: HashingAlgorithm, + ) -> Result { + let _master_password = generate_passphrase(); + let _salt = generate_salt(); + + // BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE + let master_password = Protected::new("password".to_string()); + let salt = *b"0000000000000000"; + + // Hash the master password + let hashed_password = hashing_algorithm.hash( + Protected::new(master_password.expose().as_bytes().to_vec()), + salt, + )?; + + 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); + + // Encrypt the master key with the hashed master password + let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes( + hashed_password, + &master_key_nonce, + algorithm, + master_key.expose(), + &[], + )?)?; + + let verification_key = StoredKey { + uuid, + algorithm, + hashing_algorithm, + content_salt: [0u8; 16], + master_key: encrypted_master_key, + master_key_nonce, + key_nonce: Vec::new(), + key: Vec::new(), + }; + + let secret_key = Protected::new(base64::encode(salt)); + + let onboarding_bundle = OnboardingBundle { + verification_key, + master_password, + secret_key, + }; + + Ok(onboarding_bundle) + } + + /// Initialize the Key Manager with `StoredKeys` retrieved from Prisma #[must_use] - pub fn new(stored_keys: Vec, master_password: Option>>) -> Self { + pub fn new(stored_keys: Vec) -> Self { let keystore = DashMap::new(); + let mut verification_key = None; + for key in stored_keys { - keystore.insert(key.uuid, key); + if key.uuid.is_nil() { + verification_key = Some(key); + } else { + keystore.insert(key.uuid, key); + } } let keymount: DashMap = DashMap::new(); Self { - master_password: Mutex::new(master_password), + master_password: Mutex::new(None), + verification_key: Mutex::new(verification_key), keystore, keymount, default: Mutex::new(None), @@ -123,9 +211,15 @@ impl KeyManager { /// This function should be used to populate the keystore with multiple stored keys at a time. /// /// It's suitable for when you created the key manager without populating it. + /// + /// This also detects the nil-UUID master passphrase verification key pub fn populate_keystore(&self, stored_keys: Vec) -> Result<()> { for key in stored_keys { - self.keystore.insert(key.uuid, key); + if key.uuid.is_nil() { + *self.verification_key.lock()? = Some(key); + } else { + self.keystore.insert(key.uuid, key); + } } Ok(()) @@ -174,6 +268,7 @@ impl KeyManager { } } + /// This allows you to clear the default key pub fn clear_default(&self) -> Result<()> { let mut default = self.default.lock()?; @@ -186,19 +281,277 @@ impl KeyManager { } /// This should ONLY be used internally. - fn get_master_password(&self) -> Result>> { - let master_password = self.master_password.lock()?; - match &*master_password { + fn get_master_password(&self) -> Result> { + match &*self.master_password.lock()? { Some(k) => Ok(k.clone()), None => Err(Error::NoMasterPassword), } } - pub fn set_master_password(&self, master_password: Protected>) -> Result<()> { - // this returns a result, so we can potentially implement password checking functionality - *self.master_password.lock()? = Some(master_password); + /// This should ONLY be used internally. + pub fn get_verification_key(&self) -> Result { + match &*self.verification_key.lock()? { + Some(k) => Ok(k.clone()), + None => Err(Error::NoMasterPassword), + } + } - Ok(()) + /// This is used to change a master password. + /// + /// The entire keystore is re-encrypted with the new master password, and will require dumping and syncing with Prisma. + pub fn change_master_password( + &self, + master_password: Protected, + algorithm: Algorithm, + hashing_algorithm: HashingAlgorithm, + ) -> Result { + // Generate a new secret key + let salt = generate_salt(); + + // Hash the master password + let hashed_password = hashing_algorithm.hash( + Protected::new(master_password.expose().as_bytes().to_vec()), + salt, + )?; + + // Iterate over the keystore - decrypt each master key, re-encrypt it with the same algorithm, and collect them into a vec + let updated_keystore: Result> = self + .dump_keystore() + .iter() + .map(|stored_key| { + let mut stored_key = stored_key.clone(); + + let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes( + self.get_master_password()?, + &stored_key.master_key_nonce, + stored_key.algorithm, + &stored_key.master_key, + &[], + ) { + Ok(Protected::new(to_array::<32>( + decrypted_master_key.expose().clone(), + )?)) + } else { + Err(Error::IncorrectPassword) + }?; + + let master_key_nonce = generate_nonce(algorithm); + + // Encrypt the master key with the user's hashed password + let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes( + hashed_password.clone(), + &master_key_nonce, + stored_key.algorithm, + master_key.expose(), + &[], + )?)?; + + stored_key.master_key = encrypted_master_key; + stored_key.master_key_nonce = master_key_nonce; + + Ok(stored_key) + }) + .collect(); + + // should use ? above + let updated_keystore = updated_keystore?; + + // Clear the current keystore and update it with our re-encrypted keystore + self.empty_keystore(); + self.populate_keystore(updated_keystore.clone())?; + + // Create a new verification key for the master password/secret key combination + let uuid = uuid::Uuid::nil(); + let master_key = generate_master_key(); + let master_key_nonce = generate_nonce(algorithm); + + // Encrypt the master key with the hashed master password + let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes( + hashed_password, + &master_key_nonce, + algorithm, + master_key.expose(), + &[], + )?)?; + + let verification_key = StoredKey { + uuid, + algorithm, + hashing_algorithm, + content_salt: [0u8; 16], + master_key: encrypted_master_key, + master_key_nonce, + key_nonce: Vec::new(), + key: Vec::new(), + }; + + let secret_key = Protected::new(base64::encode(salt)); + + let mpc_bundle = MasterPasswordChangeBundle { + verification_key, + secret_key, + updated_keystore, + }; + + // Update the internal verification key, and then set the master password + *self.verification_key.lock()? = Some(mpc_bundle.verification_key.clone()); + self.set_master_password(master_password, mpc_bundle.secret_key.clone())?; + + // Return the verification key so it can be written to Prisma and return the secret key so it can be shown to the user + Ok(mpc_bundle) + } + + /// Used internally to convert a `Protected` to a `Protected>` + #[allow(clippy::unused_self)] + #[allow(clippy::needless_pass_by_value)] + fn convert_master_password_string( + &self, + master_password: Protected, + ) -> Protected> { + Protected::new(master_password.expose().as_bytes().to_vec()) + } + + /// Used internally to convert from a base64-encoded `Protected` to a `Protected<[u8; SALT_LEN]>` in a secretive manner. + /// + /// If the secret key is wrong (not base64 or not the correct length), a filler secret key will be inserted secretly. + #[allow(clippy::unused_self)] + #[allow(clippy::needless_pass_by_value)] + fn convert_secret_key_string( + &self, + secret_key: Protected, + ) -> Protected<[u8; SALT_LEN]> { + let secret_key = if let Ok(secret_key) = base64::decode(secret_key.expose()) { + secret_key + } else { + Vec::new() + }; + + // 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) + if let Ok(secret_key) = to_array(secret_key) { + Protected::new(secret_key) + } else { + Protected::new(generate_salt()) + } + } + + // Opting to leave ser/de to external functions - the key manager isn't the right place to handle this. + /// This re-encrypts master keys so they can be imported from a key backup into the current key manager. + /// + /// It returns a `Vec` so they can be written to Prisma + pub fn import_keystore_backup( + &self, + master_password: Protected, // at the time of the backup + secret_key: Protected, // 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 master_password = self.convert_master_password_string(master_password); + let secret_key = self.convert_secret_key_string(secret_key); + + let mut verification_key = None; + + let keys: Vec = stored_keys + .iter() + .filter_map(|key| { + if key.uuid.is_nil() { + verification_key = Some(key.clone()); + None + } else { + Some(key.clone()) + } + }) + .collect(); + + let hashed_master_password = if let Some(verification_key) = verification_key { + verification_key + .hashing_algorithm + .hash(master_password, *secret_key.expose()) + } else { + Err(Error::NoVerificationKey) + }?; + + let mut reencrypted_keys = Vec::new(); + + for key in keys { + if !self.keystore.contains_key(&key.uuid) { + // could check the key material itself? if they match, attach the content salt + let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes( + hashed_master_password.clone(), + &key.master_key_nonce, + key.algorithm, + &key.master_key, + &[], + ) { + Ok(Protected::new(to_array::<32>( + decrypted_master_key.expose().clone(), + )?)) + } else { + Err(Error::IncorrectPassword) + }?; + + let master_key_nonce = generate_nonce(key.algorithm); + + // Encrypt the master key with the user's hashed password + let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes( + self.get_master_password()?, + &master_key_nonce, + key.algorithm, + master_key.expose(), + &[], + )?)?; + + let mut updated_key = key.clone(); + updated_key.master_key_nonce = master_key_nonce; + updated_key.master_key = encrypted_master_key; + + reencrypted_keys.push(updated_key.clone()); + self.keystore.insert(updated_key.uuid, updated_key); + } + } + + Ok(reencrypted_keys) + } + + // requires master password and the secret key + /// This requires both the master password and the secret key + /// + /// The master password and secret key are hashed together. + /// 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. + #[allow(clippy::needless_pass_by_value)] + pub fn set_master_password( + &self, + master_password: Protected, + secret_key: Protected, + ) -> Result<()> { + let verification_key = match &*self.verification_key.lock()? { + Some(k) => Ok(k.clone()), + None => Err(Error::NoVerificationKey), + }?; + let master_password = self.convert_master_password_string(master_password); + let secret_key = self.convert_secret_key_string(secret_key); + + let hashed_master_password = verification_key + .hashing_algorithm + .hash(master_password, *secret_key.expose())?; + + // Decrypt the StoredKey's master key using the user's hashed password + let decryption_result = StreamDecryption::decrypt_bytes( + hashed_master_password.clone(), + &verification_key.master_key_nonce, + verification_key.algorithm, + &verification_key.master_key, + &[], + ); + + if decryption_result.is_ok() { + *self.master_password.lock()? = Some(hashed_master_password); + Ok(()) + } else { + Err(Error::IncorrectKeymanagerDetails) + } } /// This function is for removing a previously-added master password @@ -208,6 +561,7 @@ impl KeyManager { Ok(()) } + /// This function is used for seeing if the key manager has a master password. pub fn has_master_password(&self) -> Result { Ok(self.master_password.lock()?.is_some()) } @@ -253,7 +607,7 @@ impl KeyManager { self.keymount.remove(&uuid); Ok(()) } else { - Err(Error::KeyNotFound) + Err(Error::KeyNotMounted) } } @@ -278,26 +632,23 @@ impl KeyManager { /// /// We could add a log to this, so that the user can view mounts pub fn mount(&self, uuid: Uuid) -> Result<()> { + if self.keymount.get(&uuid).is_some() { + return Err(Error::KeyAlreadyMounted); + } + match self.keystore.get(&uuid) { Some(stored_key) => { - let master_password = self.get_master_password()?; - - let hashed_password = stored_key - .hashing_algorithm - .hash(master_password, stored_key.salt)?; - - let mut master_key = [0u8; MASTER_KEY_LEN]; - // Decrypt the StoredKey's master key using the user's hashed password let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes( - hashed_password, + self.get_master_password()?, &stored_key.master_key_nonce, stored_key.algorithm, &stored_key.master_key, &[], ) { - master_key.copy_from_slice(&decrypted_master_key); - Ok(Protected::new(master_key)) + Ok(Protected::new(to_array( + decrypted_master_key.expose().clone(), + )?)) } else { Err(Error::IncorrectPassword) }?; @@ -314,13 +665,11 @@ impl KeyManager { // Hash the key once with the parameters/algorithm the user selected during first mount let hashed_key = stored_key .hashing_algorithm - .hash(key.clone(), stored_key.content_salt)?; + .hash(key, stored_key.content_salt)?; // Construct the MountedKey and insert it into the Keymount let mounted_key = MountedKey { uuid: stored_key.uuid, - key, - content_salt: stored_key.content_salt, hashed_key, }; @@ -332,6 +681,42 @@ impl KeyManager { } } + /// This function is used for getting the key 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 fn get_key(&self, uuid: Uuid) -> Result>> { + match self.keystore.get(&uuid) { + Some(stored_key) => { + // Decrypt the StoredKey's master key using the user's hashed password + let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes( + self.get_master_password()?, + &stored_key.master_key_nonce, + stored_key.algorithm, + &stored_key.master_key, + &[], + ) { + Ok(Protected::new(to_array( + decrypted_master_key.expose().clone(), + )?)) + } else { + Err(Error::IncorrectPassword) + }?; + + // Decrypt the StoredKey using the decrypted master key + let key = StreamDecryption::decrypt_bytes( + master_key, + &stored_key.key_nonce, + stored_key.algorithm, + &stored_key.key, + &[], + )?; + + Ok(key) + } + None => Err(Error::KeyNotFound), + } + } + /// This function is for accessing the internal keymount. /// /// We could add a log to this, so that the user can view accesses @@ -379,23 +764,17 @@ impl KeyManager { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, ) -> Result { - let master_password = self.get_master_password()?; - let uuid = uuid::Uuid::new_v4(); // Generate items we'll need for encryption let key_nonce = generate_nonce(algorithm); let master_key = generate_master_key(); let master_key_nonce = generate_nonce(algorithm); - let salt = generate_salt(); let content_salt = generate_salt(); // for PVM/MD - // Hash the user's master password - let hashed_password = hashing_algorithm.hash(master_password, salt)?; - - // Encrypted the master key with the user's hashed password + // Encrypt the master key with the user's hashed password let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes( - hashed_password, + self.get_master_password()?, &master_key_nonce, algorithm, master_key.expose(), @@ -411,7 +790,6 @@ impl KeyManager { uuid, algorithm, hashing_algorithm, - salt, content_salt, master_key: encrypted_master_key, master_key_nonce, diff --git a/crates/crypto/src/primitives.rs b/crates/crypto/src/primitives.rs index 458806055..87cf2b9c8 100644 --- a/crates/crypto/src/primitives.rs +++ b/crates/crypto/src/primitives.rs @@ -2,7 +2,7 @@ //! //! 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 rand::{seq::SliceRandom, RngCore, SeedableRng}; use zeroize::Zeroize; use crate::{crypto::stream::Algorithm, Error, Protected, Result}; @@ -21,6 +21,8 @@ pub const ENCRYPTED_MASTER_KEY_LEN: usize = 48; /// The length of the (unencrypted) master key pub const MASTER_KEY_LEN: usize = 32; +pub const PASSPHRASE_LEN: usize = 7; + /// This should be used for generating nonces for encryption. /// /// An algorithm is required so this function can calculate the length of the nonce. @@ -68,3 +70,34 @@ pub fn to_array(bytes: Vec) -> Result<[u8; I]> { 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/packages/client/src/core.ts b/packages/client/src/core.ts index a9ff0d7cd..1844cefd8 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -9,6 +9,8 @@ export type Procedures = { { key: "jobs.getRunning", input: LibraryArgs, result: Array } | { 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.list", input: LibraryArgs, result: Array } | { key: "keys.listMounted", input: LibraryArgs, result: Array } | { key: "library.getStatistics", input: LibraryArgs, result: Statistics } | @@ -37,10 +39,15 @@ export type Procedures = { { key: "jobs.identifyUniqueFiles", input: LibraryArgs, result: null } | { key: "jobs.objectValidator", input: LibraryArgs, result: null } | { key: "keys.add", input: LibraryArgs, result: null } | + { key: "keys.backupKeystore", input: LibraryArgs, result: null } | + { key: "keys.changeMasterPassword", input: LibraryArgs, result: string } | + { key: "keys.clearMasterPassword", input: LibraryArgs, result: null } | { key: "keys.deleteFromLibrary", input: LibraryArgs, result: null } | { key: "keys.mount", input: LibraryArgs, result: null } | + { key: "keys.onboarding", input: LibraryArgs, result: OnboardingKeys } | + { key: "keys.restoreKeystore", input: LibraryArgs, result: number } | { key: "keys.setDefault", input: LibraryArgs, result: null } | - { key: "keys.setMasterPassword", input: LibraryArgs, result: null } | + { key: "keys.setMasterPassword", input: LibraryArgs, result: null } | { key: "keys.unmount", input: LibraryArgs, result: null } | { key: "keys.unmountAll", input: LibraryArgs, result: null } | { key: "keys.updateKeyName", input: LibraryArgs, result: null } | @@ -95,7 +102,7 @@ export interface JobReport { id: string, name: string, data: Array | nul export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused" -export interface KeyAddArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, key: string } +export interface KeyAddArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, key: string, library_sync: boolean } export interface KeyNameUpdateArgs { uuid: string, name: string } @@ -113,6 +120,8 @@ 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, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm } + export interface Node { id: number, pub_id: Array, name: string, platform: number, version: string | null, last_seen: string, timezone: string | null, date_created: string } export interface NodeConfig { version: string | null, id: string, name: string, p2p_port: number | null } @@ -131,17 +140,25 @@ export interface Object { id: number, cas_id: string, integrity_checksum: string export interface ObjectValidatorArgs { id: number, path: string } +export interface OnboardingArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm } + +export interface OnboardingKeys { master_password: string, secret_key: string } + export type Params = "Standard" | "Hardened" | "Paranoid" +export interface RestoreBackupArgs { password: string, secret_key: string, path: string } + export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent" export interface SetFavoriteArgs { id: number, favorite: boolean } +export interface SetMasterPasswordArgs { password: string, secret_key: string } + 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, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, salt: Array, content_salt: Array, master_key: Array, master_key_nonce: Array, key_nonce: Array, key: Array } +export interface StoredKey { uuid: string, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, content_salt: Array, master_key: Array, master_key_nonce: Array, key_nonce: Array, key: Array } export interface Tag { id: number, pub_id: Array, name: string | null, color: string | null, total_objects: number | null, redundancy_goal: number | null, date_created: string, date_modified: string } diff --git a/packages/interface/package.json b/packages/interface/package.json index 11916b835..02aedf8cb 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -33,6 +33,9 @@ "@tanstack/react-query-devtools": "^4.12.0", "@tanstack/react-virtual": "3.0.0-beta.18", "@vitejs/plugin-react": "^2.1.0", + "@zxcvbn-ts/core": "^2.1.0", + "@zxcvbn-ts/language-common": "^2.0.1", + "@zxcvbn-ts/language-en": "^2.1.0", "autoprefixer": "^10.4.12", "byte-size": "^8.1.0", "clsx": "^1.2.1", diff --git a/packages/interface/src/components/dialog/BackupRestoreDialog.tsx b/packages/interface/src/components/dialog/BackupRestoreDialog.tsx new file mode 100644 index 000000000..f43580238 --- /dev/null +++ b/packages/interface/src/components/dialog/BackupRestoreDialog.tsx @@ -0,0 +1,143 @@ +import { useLibraryMutation } from '@sd/client'; +import { Button, Dialog, Input } from '@sd/ui'; +import { open } from '@tauri-apps/api/dialog'; +import { Eye, EyeSlash } from 'phosphor-react'; +import { ReactNode, useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; + +type FormValues = { + masterPassword: string; + secretKey: string; + filePath: string; +}; + +export const BackupRestoreDialog = (props: { trigger: ReactNode }) => { + const { trigger } = props; + + const { register, handleSubmit, getValues, setValue } = useForm({ + defaultValues: { + masterPassword: '', + secretKey: '', + filePath: '' + } + }); + + const onSubmit: SubmitHandler = (data) => { + if (data.filePath !== '') { + setValue('masterPassword', ''); + setValue('secretKey', ''); + setValue('filePath', ''); + restoreKeystoreMutation.mutate( + { + password: data.masterPassword, + secret_key: data.secretKey, + path: data.filePath + }, + { + onSuccess: (total) => { + setTotalKeysImported(total); + setShowBackupRestoreDialog(false); + setShowRestorationFinalizationDialog(true); + }, + onError: () => { + alert('There was an error while restoring your backup.'); + } + } + ); + } + }; + + const [showBackupRestoreDialog, setShowBackupRestoreDialog] = useState(false); + const [showRestorationFinalizationDialog, setShowRestorationFinalizationDialog] = useState(false); + const restoreKeystoreMutation = useLibraryMutation('keys.restoreKeystore'); + + const [showMasterPassword, setShowMasterPassword] = useState(false); + const [showSecretKey, setShowSecretKey] = useState(false); + + const [totalKeysImported, setTotalKeysImported] = useState(0); + + const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye; + const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye; + + return ( + <> +
+ +
+ + +
+
+ + +
+
+ +
+
+
+ + { + setShowRestorationFinalizationDialog(false); + }} + ctaLabel="Done" + trigger={<>} + > +
+ {totalKeysImported}{' '} + {totalKeysImported !== 1 ? 'keys were imported.' : 'key was imported.'} +
+
+ + ); +}; diff --git a/packages/interface/src/components/dialog/PasswordChangeDialog.tsx b/packages/interface/src/components/dialog/PasswordChangeDialog.tsx new file mode 100644 index 000000000..389da66bd --- /dev/null +++ b/packages/interface/src/components/dialog/PasswordChangeDialog.tsx @@ -0,0 +1,218 @@ +import { useLibraryMutation } from '@sd/client'; +import { Button, Dialog, Input, Select, SelectOption } from '@sd/ui'; +import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core'; +import zxcvbnCommonPackage from '@zxcvbn-ts/language-common'; +import zxcvbnEnPackage from '@zxcvbn-ts/language-en'; +import clsx from 'clsx'; +import { Eye, EyeSlash } from 'phosphor-react'; +import { ReactNode, useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; + +import { getCryptoSettings } from '../../screens/settings/library/KeysSetting'; + +export const PasswordChangeDialog = (props: { trigger: ReactNode }) => { + type FormValues = { + masterPassword: string; + masterPassword2: string; + encryptionAlgo: string; + hashingAlgo: string; + }; + + const [secretKey, setSecretKey] = useState(''); + + const { register, handleSubmit, getValues, setValue } = useForm({ + defaultValues: { + masterPassword: '', + masterPassword2: '', + encryptionAlgo: 'XChaCha20Poly1305', + hashingAlgo: 'Argon2id-s' + } + }); + + const onSubmit: SubmitHandler = (data) => { + if (data.masterPassword !== data.masterPassword2) { + alert('Passwords are not the same.'); + } else { + const [algorithm, hashing_algorithm] = getCryptoSettings( + data.encryptionAlgo, + data.hashingAlgo + ); + + changeMasterPassword.mutate( + { algorithm, hashing_algorithm, password: data.masterPassword }, + { + onSuccess: (sk) => { + setSecretKey(sk); + + setShowSecretKeyDialog(true); + setShowMasterPasswordDialog(false); + }, + onError: () => { + // this should never really happen + alert('There was an error while changing your master password.'); + } + } + ); + } + }; + + const [passwordMeterMasterPw, setPasswordMeterMasterPw] = useState(''); // this is needed as the password meter won't update purely with react-hook-for + const [showMasterPasswordDialog, setShowMasterPasswordDialog] = useState(false); + const [showSecretKeyDialog, setShowSecretKeyDialog] = useState(false); + const changeMasterPassword = useLibraryMutation('keys.changeMasterPassword'); + const [showMasterPassword1, setShowMasterPassword1] = useState(false); + const [showMasterPassword2, setShowMasterPassword2] = useState(false); + const MP1CurrentEyeIcon = showMasterPassword1 ? EyeSlash : Eye; + const MP2CurrentEyeIcon = showMasterPassword2 ? EyeSlash : Eye; + const { trigger } = props; + + return ( + <> +
+ +
+ setPasswordMeterMasterPw(e.target.value)} + value={passwordMeterMasterPw} + type={showMasterPassword1 ? 'text' : 'password'} + /> + +
+
+ + +
+ + + +
+
+ Encryption + +
+
+ Hashing + +
+
+
+
+ { + setShowSecretKeyDialog(false); + }} + ctaLabel="Done" + trigger={<>} + > + + + + ); +}; + +const PasswordMeter = (props: { password: string }) => { + const ratings = ['Poor', 'Weak', 'Good', 'Strong', 'Perfect']; + + const options = { + dictionary: { + ...zxcvbnCommonPackage.dictionary, + ...zxcvbnEnPackage.dictionary + }, + graps: zxcvbnCommonPackage.adjacencyGraphs, + translations: zxcvbnEnPackage.translations + }; + zxcvbnOptions.setOptions(options); + const zx = zxcvbn(props.password); + + const innerDiv = { + width: `${zx.score !== 0 ? zx.score * 25 : 12.5}%`, + height: '5px', + borderRadius: 80 + }; + + return ( +
+
+
+
+ + {ratings[zx.score]} + +
+ ); +}; diff --git a/packages/interface/src/components/key/KeyList.tsx b/packages/interface/src/components/key/KeyList.tsx index 63e3d71ca..7bd22fa04 100644 --- a/packages/interface/src/components/key/KeyList.tsx +++ b/packages/interface/src/components/key/KeyList.tsx @@ -7,7 +7,9 @@ import { useMemo } from 'react'; export type KeyListProps = DefaultProps; -const ListKeys = () => { +export const ListOfKeys = (props: { noKeysMessage: boolean }) => { + const { noKeysMessage } = props; + const keys = useLibraryQuery(['keys.list']); const mounted_uuids = useLibraryQuery(['keys.listMounted']); @@ -20,7 +22,7 @@ const ListKeys = () => { [keys, mounted_uuids] ); - if(keys.data?.length === 0) { + if(keys.data?.length === 0 && noKeysMessage) { return ( No keys available. ) @@ -44,7 +46,7 @@ const ListKeys = () => { ) }; -export function KeyList(props: KeyListProps) { +export const KeyList = (props: KeyListProps) => { const unmountAll = useLibraryMutation(['keys.unmountAll']); return ( @@ -53,7 +55,7 @@ export function KeyList(props: KeyListProps) {
{/* Mounted keys */}
- +
diff --git a/packages/interface/src/components/key/KeyManager.tsx b/packages/interface/src/components/key/KeyManager.tsx index 509c48c8f..10cf2591d 100644 --- a/packages/interface/src/components/key/KeyManager.tsx +++ b/packages/interface/src/components/key/KeyManager.tsx @@ -1,4 +1,7 @@ -import { Tabs } from '@sd/ui'; +import { useLibraryMutation, useLibraryQuery } from '@sd/client'; +import { Button, ButtonLink, Input, Tabs } from '@sd/ui'; +import { Eye, EyeSlash, Gear, Lock } from 'phosphor-react'; +import { useState } from 'react'; import { DefaultProps } from '../primitive/types'; import { KeyList } from './KeyList'; @@ -7,24 +10,123 @@ import { KeyMounter } from './KeyMounter'; export type KeyManagerProps = DefaultProps; export function KeyManager(props: KeyManagerProps) { - return ( -
- - - - Mount - - - Keys - - - - - - - - - -
- ); + const hasMasterPw = useLibraryQuery(['keys.hasMasterPassword']); + const setMasterPasswordMutation = useLibraryMutation('keys.setMasterPassword'); + const unmountAll = useLibraryMutation('keys.unmountAll'); + const clearMasterPassword = useLibraryMutation('keys.clearMasterPassword'); + + const [showMasterPassword, setShowMasterPassword] = useState(false); + const [showSecretKey, setShowSecretKey] = useState(false); + + const [masterPassword, setMasterPassword] = useState(''); + const [secretKey, setSecretKey] = useState(''); + + if (!hasMasterPw?.data) { + const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye; + const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye; + + return ( +
+
+ setMasterPassword(e.target.value)} + autoFocus + type={showMasterPassword ? 'text' : 'password'} + className="flex-grow !py-0.5" + placeholder="Master Password" + /> + +
+ +
+ setSecretKey(e.target.value)} + type={showSecretKey ? 'text' : 'password'} + className="flex-grow !py-0.5" + placeholder="Secret Key" + /> + +
+ + +
+ ); + } else { + return ( +
+ +
+ + + Mount + + + Keys + +
+ + + + + +
+ + + + + + + +
+ ); + } } diff --git a/packages/interface/src/components/key/KeyMounter.tsx b/packages/interface/src/components/key/KeyMounter.tsx index 751430423..523d3fdee 100644 --- a/packages/interface/src/components/key/KeyMounter.tsx +++ b/packages/interface/src/components/key/KeyMounter.tsx @@ -1,9 +1,10 @@ +import { useLibraryMutation, useLibraryQuery } from '@sd/client'; +import { Algorithm, HashingAlgorithm, Params } from '@sd/client'; import { Button, CategoryHeading, Input, Select, SelectOption, Switch, cva, tw } from '@sd/ui'; import { Eye, EyeSlash, Info } from 'phosphor-react'; import { useEffect, useRef, useState } from 'react'; -import { useLibraryMutation, useLibraryQuery } from '@sd/client'; -import { Algorithm, HashingAlgorithm, Params } from '@sd/client'; +import { getCryptoSettings } from '../../screens/settings/library/KeysSetting'; import { Tooltip } from '../tooltip/Tooltip'; const KeyHeading = tw(CategoryHeading)`mb-1`; @@ -18,7 +19,7 @@ export function KeyMounter() { const mounted_uuids = useLibraryQuery(['keys.listMounted']); const [showKey, setShowKey] = useState(false); - const [toggle, setToggle] = useState(true); + const [librarySync, setLibrarySync] = useState(true); const [key, setKey] = useState(''); const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305'); @@ -63,12 +64,12 @@ export function KeyMounter() {
Sync with Library - + @@ -93,26 +94,20 @@ export function KeyMounter() {

Files encrypted with this key will be revealed and decrypted on the fly.

- diff --git a/packages/interface/src/components/settings/SettingsSubHeader.tsx b/packages/interface/src/components/settings/SettingsSubHeader.tsx new file mode 100644 index 000000000..7fd4ea53b --- /dev/null +++ b/packages/interface/src/components/settings/SettingsSubHeader.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; + +export interface SettingsSubHeaderProps { + title: string; + rightArea?: ReactNode; +} + +export const SettingsSubHeader: React.FC = (props) => { + return ( +
+
+

{props.title}

+
+ {props.rightArea} +
+ ); +}; diff --git a/packages/interface/src/screens/settings/library/KeysSetting.tsx b/packages/interface/src/screens/settings/library/KeysSetting.tsx index 21adfdde2..d6cb19021 100644 --- a/packages/interface/src/screens/settings/library/KeysSetting.tsx +++ b/packages/interface/src/screens/settings/library/KeysSetting.tsx @@ -1,10 +1,255 @@ +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { + Algorithm, + HashingAlgorithm, + Params, + useLibraryMutation, + useLibraryQuery +} from '@sd/client'; +import { Button, Input } from '@sd/ui'; +import { save } from '@tauri-apps/api/dialog'; +import clsx from 'clsx'; +import { Eye, EyeSlash, Lock, Plus } from 'phosphor-react'; +import { PropsWithChildren, useState } from 'react'; +import { animated, useTransition } from 'react-spring'; + +import { BackupRestoreDialog } from '../../../components/dialog/BackupRestoreDialog'; +import { PasswordChangeDialog } from '../../../components/dialog/PasswordChangeDialog'; +import { ListOfKeys } from '../../../components/key/KeyList'; +import { KeyMounter } from '../../../components/key/KeyMounter'; import { SettingsContainer } from '../../../components/settings/SettingsContainer'; import { SettingsHeader } from '../../../components/settings/SettingsHeader'; +import { SettingsSubHeader } from '../../../components/settings/SettingsSubHeader'; + +interface Props extends DropdownMenu.MenuContentProps { + trigger: React.ReactNode; + transformOrigin?: string; + disabled?: boolean; +} + +export const KeyMounterDropdown = ({ + trigger, + children, + disabled, + transformOrigin, + className, + ...props +}: PropsWithChildren) => { + const [open, setOpen] = useState(false); + + const transitions = useTransition(open, { + from: { + opacity: 0, + transform: `scale(0.9)`, + transformOrigin: transformOrigin || 'top' + }, + enter: { opacity: 1, transform: 'scale(1)' }, + leave: { opacity: -0.5, transform: 'scale(0.95)' }, + config: { mass: 0.4, tension: 200, friction: 10 } + }); + + return ( + + {trigger} + {transitions( + (styles, show) => + show && ( + + + + {children} + + + + ) + )} + + ); +}; export default function KeysSettings() { - return ( - - - - ); + const hasMasterPw = useLibraryQuery(['keys.hasMasterPassword']); + const setMasterPasswordMutation = useLibraryMutation('keys.setMasterPassword'); + const unmountAll = useLibraryMutation('keys.unmountAll'); + const clearMasterPassword = useLibraryMutation('keys.clearMasterPassword'); + const backupKeystore = useLibraryMutation('keys.backupKeystore'); + + const [showMasterPassword, setShowMasterPassword] = useState(false); + const [showSecretKey, setShowSecretKey] = useState(false); + const [masterPassword, setMasterPassword] = useState(''); + const [secretKey, setSecretKey] = useState(''); + const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye; + const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye; + + if (!hasMasterPw?.data) { + return ( +
+
+ setMasterPassword(e.target.value)} + autoFocus + type={showMasterPassword ? 'text' : 'password'} + className="flex-grow !py-0.5" + placeholder="Master Password" + /> + +
+ +
+ setSecretKey(e.target.value)} + type={showSecretKey ? 'text' : 'password'} + className="flex-grow !py-0.5" + placeholder="Secret Key" + /> + +
+ + +
+ ); + } else { + return ( + + + + + + + } + > + + + + } + /> + {hasMasterPw.data ? ( +
+ +
+ ) : null} + + +
+ + Change Master Password + + } + /> +
+ + +
+ + + Restore + + } + /> +
+
+ ); + } } + +// not sure of a suitable place for this function +export const getCryptoSettings = ( + encryptionAlgorithm: string, + hashingAlgorithm: string +): [Algorithm, HashingAlgorithm] => { + const algorithm = encryptionAlgorithm as Algorithm; + let hashing_algorithm: HashingAlgorithm = { Argon2id: 'Standard' }; + + switch (hashingAlgorithm) { + case 'Argon2id-s': + hashing_algorithm = { Argon2id: 'Standard' as Params }; + break; + case 'Argon2id-h': + hashing_algorithm = { Argon2id: 'Hardened' as Params }; + break; + case 'Argon2id-p': + hashing_algorithm = { Argon2id: 'Paranoid' as Params }; + break; + } + + return [algorithm, hashing_algorithm]; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a72b2bbf0..94fdde4d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -414,6 +414,9 @@ importers: '@types/react-router-dom': ^5.3.3 '@types/tailwindcss': ^3.1.0 '@vitejs/plugin-react': ^2.1.0 + '@zxcvbn-ts/core': ^2.1.0 + '@zxcvbn-ts/language-common': ^2.0.1 + '@zxcvbn-ts/language-en': ^2.1.0 autoprefixer: ^10.4.12 byte-size: ^8.1.0 clsx: ^1.2.1 @@ -456,6 +459,9 @@ importers: '@tanstack/react-query-devtools': 4.12.0_pqnxmwujmmnpcx44ucekqkefny '@tanstack/react-virtual': 3.0.0-beta.18_react@18.2.0 '@vitejs/plugin-react': 2.1.0_vite@3.1.8 + '@zxcvbn-ts/core': 2.1.0 + '@zxcvbn-ts/language-common': 2.0.1 + '@zxcvbn-ts/language-en': 2.1.0 autoprefixer: 10.4.12 byte-size: 8.1.0 clsx: 1.2.1 @@ -8461,7 +8467,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.19.3 magic-string: 0.26.7 react-refresh: 0.14.0 - vite: 3.1.8_sass@1.55.0 + vite: 3.1.8 transitivePeerDependencies: - supports-color @@ -8718,6 +8724,18 @@ packages: rimraf: 3.0.2 dev: false + /@zxcvbn-ts/core/2.1.0: + resolution: {integrity: sha512-doxol9xrO7LgyVJhguXe7vO0xthnIYmsOKoDwrLg0Ho2kkpQaVtM+AOQw+BkEiKIqNg1V48eUf4/cTzMElXdiA==} + dev: false + + /@zxcvbn-ts/language-common/2.0.1: + resolution: {integrity: sha512-P+v5MA/UNc9nb3FEOEoDgTyIGQc2vLc6m04pdf5YyuNOzrL0iNANhECk2TUp62JbrjouJVodqhMH0j1a8/24Bg==} + dev: false + + /@zxcvbn-ts/language-en/2.1.0: + resolution: {integrity: sha512-I3n4AAbArjPAZtwCrk9MQnSrcj5+9rq8sic2rUU44fP5QaR17Vk8zDt61+R9dnP9ZRsj09aAUYML4Ash05qZjQ==} + dev: false + /abort-controller/3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -22083,7 +22101,7 @@ packages: dependencies: '@rollup/pluginutils': 5.0.1 '@svgr/core': 6.5.0 - vite: 3.1.8_sass@1.55.0 + vite: 3.1.8 transitivePeerDependencies: - rollup - supports-color