[ENG-307] Key manager features (#467)

* working key management in settings page

* sync with library button

* fix `rspc` feature and add passphrase generation

* untested key manager rework

* trying to return values from mutations

* update library manager and remove settiong master PW

* update bindings

* set static secret key/master password

* prompt user for master password if correct one hasn't been provided yet

* add `hasMasterPassword` route

* add `clearMasterPassword` route + remove dead code

* tweak `set_master_password()` and add dedicated error

* tweak UI, fix `few hooks than expected`, add unmount+lock button

* remove old comment

* fmt

* clippy

* move static key/password setting so it doesn't fail sometimes

* add dedicated `get_key()` and remove keys from memory

* add `getKey` route

* update bindings

* use `const` instead of `let`

* comment updates

* update schema to remove salt

* add string parse error

* generate passphrase within key manager

* generate new migrations

* feature gate serde support in `crypto` crate

* fmt

* more specific error types

* foramatting

* add locking mechanism to keysettings page (not working?)

* fix react hook issues

Co-authored-by: maxichrome <maxichrome@users.noreply.github.com>

* remove empty onclick

* add keymanager dropdown menu

* working key backup functionality

* add experimental master password changing support

* update bindings

* semi-working change master password dialog

* use gear/lock icons to clean up key manager UI

* make settings button functional

* make buttons uniform and format code

* fix double base64 encode

* add change master password dialog and secret key dialog

* code cleanup

* restore backup dialog

* change UI wording

* make a start on restoring from a backup

* potentially working keystore restore

* don't overwrite verification key if one is set

* working backup restore + fix master password changing

* fix typo in static password/verification key check logic

* change wording to make UI clearer

* disable mount button if key is empty

* handle errors+remove type annotations

* show total imported keys on backup restoration

* add zxcvbn package

* change input border colour based on zxcvbn score

* clippy and formatting

* password strength meter

* remove nbsp

* add button type to stop early form submission

* use `react-hook-form` for backup restoration dialog

* more `react-hook-form` stuff

* attempt to fix password meter

* small cleanup

* Fix password meter

* update colours

Co-authored-by: maxichrome <maxichrome@users.noreply.github.com>
Co-authored-by: Utku Bakir <74243531+utkubakir@users.noreply.github.com>
This commit is contained in:
jake 2022-12-02 10:18:21 +00:00 committed by GitHub
parent 4dc7c4571a
commit 2baf16d982
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 9478 additions and 194 deletions

29
Cargo.lock generated
View file

@ -421,9 +421,9 @@ checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.0" version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64ct" name = "base64ct"
@ -2286,7 +2286,7 @@ source = "git+https://github.com/oscartbeaumont/httpz.git?rev=1ddbd9ad594ac7ee3e
dependencies = [ dependencies = [
"async-tungstenite", "async-tungstenite",
"axum", "axum",
"base64 0.13.0", "base64 0.13.1",
"cookie", "cookie",
"form_urlencoded", "form_urlencoded",
"futures", "futures",
@ -3816,7 +3816,7 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4"
dependencies = [ dependencies = [
"base64 0.13.0", "base64 0.13.1",
] ]
[[package]] [[package]]
@ -4021,7 +4021,7 @@ version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225" checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225"
dependencies = [ dependencies = [
"base64 0.13.0", "base64 0.13.1",
"indexmap", "indexmap",
"line-wrap", "line-wrap",
"serde", "serde",
@ -4114,7 +4114,7 @@ name = "prisma-client-rust"
version = "0.6.3" version = "0.6.3"
source = "git+https://github.com/Brendonovich/prisma-client-rust.git?tag=0.6.3#c81f22fb287a2801da5a5961ed1ec9e99f5bee34" source = "git+https://github.com/Brendonovich/prisma-client-rust.git?tag=0.6.3#c81f22fb287a2801da5a5961ed1ec9e99f5bee34"
dependencies = [ dependencies = [
"base64 0.13.0", "base64 0.13.1",
"bigdecimal", "bigdecimal",
"chrono", "chrono",
"datamodel", "datamodel",
@ -4822,7 +4822,7 @@ version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
dependencies = [ dependencies = [
"base64 0.13.0", "base64 0.13.1",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@ -5022,7 +5022,7 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9"
dependencies = [ dependencies = [
"base64 0.13.0", "base64 0.13.1",
] ]
[[package]] [[package]]
@ -5031,7 +5031,7 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
dependencies = [ dependencies = [
"base64 0.13.0", "base64 0.13.1",
] ]
[[package]] [[package]]
@ -5144,7 +5144,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"async-stream", "async-stream",
"async-trait", "async-trait",
"base64 0.13.0", "base64 0.13.1",
"blake3", "blake3",
"chrono", "chrono",
"ctor", "ctor",
@ -5208,6 +5208,7 @@ dependencies = [
"aead", "aead",
"aes-gcm", "aes-gcm",
"argon2", "argon2",
"base64 0.13.1",
"chacha20poly1305", "chacha20poly1305",
"dashmap", "dashmap",
"rand 0.8.5", "rand 0.8.5",
@ -5960,7 +5961,7 @@ name = "swift-rs"
version = "0.3.0" version = "0.3.0"
source = "git+https://github.com/Brendonovich/swift-rs.git?branch=autorelease#b16ba936ca2330bb27c6b9b7a84ad0d583ef0caa" source = "git+https://github.com/Brendonovich/swift-rs.git?branch=autorelease#b16ba936ca2330bb27c6b9b7a84ad0d583ef0caa"
dependencies = [ dependencies = [
"base64 0.13.0", "base64 0.13.1",
"serde", "serde",
"serde_json", "serde_json",
"swift-rs-macros", "swift-rs-macros",
@ -6187,7 +6188,7 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "356fa253e40ae4d6ff02075011f2f2bb4066f5c9d8c1e16ca6912d7b75903ba6" checksum = "356fa253e40ae4d6ff02075011f2f2bb4066f5c9d8c1e16ca6912d7b75903ba6"
dependencies = [ dependencies = [
"base64 0.13.0", "base64 0.13.1",
"brotli", "brotli",
"ico", "ico",
"json-patch", "json-patch",
@ -6729,7 +6730,7 @@ version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
dependencies = [ dependencies = [
"base64 0.13.0", "base64 0.13.1",
"byteorder", "byteorder",
"bytes", "bytes",
"http", "http",
@ -7495,7 +7496,7 @@ version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff5c1352b4266fdf92c63479d2f58ab4cd29dc4e78fbc1b62011ed1227926945" checksum = "ff5c1352b4266fdf92c63479d2f58ab4cd29dc4e78fbc1b62011ed1227926945"
dependencies = [ dependencies = [
"base64 0.13.0", "base64 0.13.1",
"block", "block",
"cocoa", "cocoa",
"core-graphics", "core-graphics",

View file

@ -54,7 +54,7 @@ image = "0.24.4"
webp = "0.2.2" webp = "0.2.2"
ffmpeg-next = { version = "5.1.1", optional = true, features = [] } ffmpeg-next = { version = "5.1.1", optional = true, features = [] }
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true } 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"} sd-file-ext = { path = "../crates/file-ext"}
fs_extra = "1.2.0" fs_extra = "1.2.0"
tracing = "0.1.36" tracing = "0.1.36"

View file

@ -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;

View file

@ -203,8 +203,6 @@ model Key {
algorithm Bytes algorithm Bytes
// hashing algorithm used for hashing the master password // hashing algorithm used for hashing the master password
hashing_algorithm Bytes hashing_algorithm Bytes
// salt to hash the master password with
salt Bytes
// salt used for encrypting data with this key // salt used for encrypting data with this key
content_salt Bytes content_salt Bytes
// the *encrypted* master key (48 bytes) // the *encrypted* master key (48 bytes)

View file

@ -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 sd_crypto::keys::keymanager::StoredKey;
use serde::Deserialize; use sd_crypto::{
crypto::stream::Algorithm,
keys::{hashing::HashingAlgorithm, keymanager::KeyManager},
Protected,
};
use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
use crate::{invalidate_query, prisma::key}; use crate::{invalidate_query, prisma::key};
@ -13,6 +19,7 @@ pub struct KeyAddArgs {
algorithm: Algorithm, algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm, hashing_algorithm: HashingAlgorithm,
key: String, key: String,
library_sync: bool,
} }
#[derive(Type, Deserialize)] #[derive(Type, Deserialize)]
@ -21,15 +28,65 @@ pub struct KeyNameUpdateArgs {
name: String, 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 { pub(crate) fn mount() -> RouterBuilder {
RouterBuilder::new() RouterBuilder::new()
.library_query("list", |t| { .library_query("list", |t| {
t(|_, _: (), library| async move { Ok(library.key_manager.dump_keystore()) }) 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 // this is so we can show the key as mounted in the UI
.library_query("listMounted", |t| { .library_query("listMounted", |t| {
t(|_, _: (), library| async move { Ok(library.key_manager.get_mounted_uuids()) }) 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| { .library_mutation("mount", |t| {
t(|_, key_uuid: uuid::Uuid, library| async move { t(|_, key_uuid: uuid::Uuid, library| async move {
library.key_manager.mount(key_uuid)?; library.key_manager.mount(key_uuid)?;
@ -61,6 +118,14 @@ pub(crate) fn mount() -> RouterBuilder {
Ok(()) 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| { .library_mutation("deleteFromLibrary", |t| {
t(|_, key_uuid: uuid::Uuid, library| async move { t(|_, key_uuid: uuid::Uuid, library| async move {
library.key_manager.remove_key(key_uuid)?; library.key_manager.remove_key(key_uuid)?;
@ -79,16 +144,53 @@ pub(crate) fn mount() -> RouterBuilder {
Ok(()) Ok(())
}) })
}) })
.library_mutation("setMasterPassword", |t| { .library_mutation("onboarding", |t| {
t(|_, password: String, library| async move { t(|_, args: OnboardingArgs, library| async move {
// need to add master password checks in the keymanager itself to make sure it's correct let bundle = KeyManager::onboarding(args.algorithm, args.hashing_algorithm)?;
// 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 let verification_key = bundle.verification_key;
// for now, automounting might have to serve as the master password checks
// 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 library
.key_manager .db
.set_master_password(Protected::new(password.as_bytes().to_vec()))?; .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 let automount = library
.db .db
@ -108,6 +210,8 @@ pub(crate) fn mount() -> RouterBuilder {
})?)?; })?)?;
} }
invalidate_query!(library, "keys.hasMasterPassword");
Ok(()) Ok(())
}) })
}) })
@ -161,17 +265,10 @@ pub(crate) fn mount() -> RouterBuilder {
}) })
.library_query("getDefault", |t| { .library_query("getDefault", |t| {
t(|_, _: (), library| async move { t(|_, _: (), library| async move {
// `find_first` should be okay here as only one default key should ever be set let default = library.key_manager.get_default();
// 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?;
if let Some(default_key) = default { if let Ok(default_key) = default {
Ok(Some(default_key.uuid)) Ok(Some(default_key))
} else { } else {
Ok(None) Ok(None)
} }
@ -196,23 +293,24 @@ pub(crate) fn mount() -> RouterBuilder {
let stored_key = library.key_manager.access_keystore(uuid)?; let stored_key = library.key_manager.access_keystore(uuid)?;
library if args.library_sync {
.db library
.key() .db
.create( .key()
uuid.to_string(), .create(
args.algorithm.serialize().to_vec(), uuid.to_string(),
args.hashing_algorithm.serialize().to_vec(), args.algorithm.serialize().to_vec(),
stored_key.salt.to_vec(), args.hashing_algorithm.serialize().to_vec(),
stored_key.content_salt.to_vec(), stored_key.content_salt.to_vec(),
stored_key.master_key.to_vec(), stored_key.master_key.to_vec(),
stored_key.master_key_nonce.to_vec(), stored_key.master_key_nonce.to_vec(),
stored_key.key_nonce.to_vec(), stored_key.key_nonce.to_vec(),
stored_key.key.to_vec(), stored_key.key.to_vec(),
vec![], vec![],
) )
.exec() .exec()
.await?; .await?;
}
// mount the key // mount the key
library.key_manager.mount(uuid)?; library.key_manager.mount(uuid)?;
@ -222,4 +320,146 @@ pub(crate) fn mount() -> RouterBuilder {
Ok(()) 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<StoredKey> =
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())
})
})
} }

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
invalidate_query, invalidate_query,
node::Platform, node::Platform,
prisma::{node, PrismaClient}, prisma::{key, node, PrismaClient},
util::{ util::{
db::load_and_migrate, db::load_and_migrate,
seeder::{indexer_rules_seeder, SeederError}, seeder::{indexer_rules_seeder, SeederError},
@ -12,11 +12,10 @@ use crate::{
use sd_crypto::{ use sd_crypto::{
crypto::stream::Algorithm, crypto::stream::Algorithm,
keys::{ keys::{
hashing::HashingAlgorithm, hashing::{HashingAlgorithm, Params},
keymanager::{KeyManager, StoredKey}, keymanager::{KeyManager, StoredKey},
}, },
primitives::to_array, primitives::to_array,
Protected,
}; };
use std::{ use std::{
env, fs, io, env, fs, io,
@ -74,11 +73,51 @@ impl From<LibraryManagerError> for rspc::Error {
pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, LibraryManagerError> { pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, LibraryManagerError> {
// retrieve all stored keys from the DB // 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 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 // collect and serialize the stored keys
// shouldn't call unwrap so much here // shouldn't call unwrap so much here
@ -95,7 +134,6 @@ pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, Libr
StoredKey { StoredKey {
uuid, uuid,
salt: to_array(key.salt).unwrap(),
algorithm: Algorithm::deserialize(to_array(key.algorithm).unwrap()).unwrap(), algorithm: Algorithm::deserialize(to_array(key.algorithm).unwrap()).unwrap(),
content_salt: to_array(key.content_salt).unwrap(), content_salt: to_array(key.content_salt).unwrap(),
master_key: to_array(key.master_key).unwrap(), master_key: to_array(key.master_key).unwrap(),
@ -118,9 +156,6 @@ pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, Libr
key_manager.set_default(default)?; key_manager.set_default(default)?;
} }
////!!!! THIS IS FOR TESTING ONLY, REMOVE IT ONCE WE HAVE THE UI IN PLACE
key_manager.set_master_password(Protected::new(b"password".to_vec()))?;
Ok(key_manager) Ok(key_manager)
} }

View file

@ -27,16 +27,19 @@ zeroize = "1.5.7"
thiserror = "1.0.37" thiserror = "1.0.37"
# metadata de/serialization # metadata de/serialization
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = "1.0" serde_json = { version = "1.0", optional = true }
serde-big-array = "0.4.1" serde-big-array = { version = "0.4.1", optional = true }
uuid = { version = "1.1.2", features = ["v4", "serde"] } uuid = { version = "1.1.2", features = ["v4"] }
dashmap = "5.4.0" dashmap = "5.4.0"
rspc = { workspace = true, optional = true } rspc = { workspace = true, optional = true }
specta = { workspace = true } specta = { workspace = true, optional = true }
base64 = "0.13.1"
[features] [features]
rpsc = ["rspc"] rspc = ["dep:rspc", "dep:specta"]
serde = ["dep:serde", "dep:serde_json", "dep:serde-big-array", "uuid/serde"]

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
use std::fs::File; #![cfg(feature = "serde")]
use sd_crypto::{ use sd_crypto::{
crypto::stream::{Algorithm, StreamEncryption}, crypto::stream::{Algorithm, StreamEncryption},
@ -11,12 +11,11 @@ use sd_crypto::{
primitives::{generate_master_key, generate_salt}, primitives::{generate_master_key, generate_salt},
Protected, Protected,
}; };
use serde::{Deserialize, Serialize}; use std::fs::File;
const ALGORITHM: Algorithm = Algorithm::XChaCha20Poly1305; const ALGORITHM: Algorithm = Algorithm::XChaCha20Poly1305;
const HASHING_ALGORITHM: HashingAlgorithm = HashingAlgorithm::Argon2id(Params::Standard); const HASHING_ALGORITHM: HashingAlgorithm = HashingAlgorithm::Argon2id(Params::Standard);
#[derive(Serialize, Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
pub struct FileInformation { pub struct FileInformation {
pub file_name: String, pub file_name: String,
} }

View file

@ -7,14 +7,18 @@ use aead::{
}; };
use aes_gcm::Aes256Gcm; use aes_gcm::Aes256Gcm;
use chacha20poly1305::XChaCha20Poly1305; use chacha20poly1305::XChaCha20Poly1305;
use serde::{Deserialize, Serialize};
use specta::Type;
use zeroize::Zeroize; use zeroize::Zeroize;
use crate::{primitives::BLOCK_SIZE, Error, Protected, Result}; use crate::{primitives::BLOCK_SIZE, Error, Protected, Result};
/// These are all possible algorithms that can be used for encryption and decryption /// These are all possible algorithms that can be used for encryption and decryption
#[derive(Clone, Copy, Eq, PartialEq, Type, Serialize, Deserialize)] #[derive(Clone, Copy, Eq, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize),
derive(serde::Deserialize)
)]
#[cfg_attr(feature = "rspc", derive(specta::Type))]
#[allow(clippy::use_self)] #[allow(clippy::use_self)]
pub enum Algorithm { pub enum Algorithm {
XChaCha20Poly1305, XChaCha20Poly1305,

View file

@ -1,5 +1,7 @@
//! This module contains all possible errors that this crate can return. //! This module contains all possible errors that this crate can return.
use std::string::FromUtf8Error;
use thiserror::Error; use thiserror::Error;
#[cfg(feature = "rspc")] #[cfg(feature = "rspc")]
@ -48,6 +50,10 @@ pub enum Error {
TooManyKeyslots, TooManyKeyslots,
#[error("requested key wasn't found in the key manager")] #[error("requested key wasn't found in the key manager")]
KeyNotFound, KeyNotFound,
#[error("key is already mounted")]
KeyAlreadyMounted,
#[error("key not mounted")]
KeyNotMounted,
#[error("no default key has been set")] #[error("no default key has been set")]
NoDefaultKeySet, NoDefaultKeySet,
#[error("no master password has been provided to the keymanager")] #[error("no master password has been provided to the keymanager")]
@ -56,6 +62,12 @@ pub enum Error {
KeystoreMismatch, KeystoreMismatch,
#[error("mutex lock error")] #[error("mutex lock error")]
MutexLock, MutexLock,
#[error("no master password verification key")]
NoVerificationKey,
#[error("wrong information provided to the key manager")]
IncorrectKeymanagerDetails,
#[error("string parse error")]
StringParse(#[from] FromUtf8Error),
} }
impl<T> From<std::sync::PoisonError<T>> for Error { impl<T> From<std::sync::PoisonError<T>> for Error {

View file

@ -29,12 +29,15 @@
//! ``` //! ```
use std::io::{Read, Seek}; use std::io::{Read, Seek};
#[cfg(feature = "serde")]
use crate::{ use crate::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, crypto::stream::{StreamDecryption, StreamEncryption},
primitives::{generate_nonce, MASTER_KEY_LEN}, primitives::{generate_nonce, MASTER_KEY_LEN},
Error, Protected, Result, Protected,
}; };
use crate::{crypto::stream::Algorithm, Error, Result};
use super::file::FileHeader; 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. /// 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. /// 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. /// Metadata needs to be accessed switfly, so a key management system should handle the salt generation.
#[cfg(feature = "serde")]
pub fn add_metadata<T>( pub fn add_metadata<T>(
&mut self, &mut self,
version: MetadataVersion, version: MetadataVersion,
@ -100,6 +104,7 @@ impl FileHeader {
/// All it requires is pre-hashed keys returned from the key manager /// All it requires is pre-hashed keys returned from the key manager
/// ///
/// A deserialized data type will be returned from this function /// A deserialized data type will be returned from this function
#[cfg(feature = "serde")]
pub fn decrypt_metadata_from_prehashed<T>( pub fn decrypt_metadata_from_prehashed<T>(
&self, &self,
hashed_keys: Vec<Protected<[u8; 32]>>, hashed_keys: Vec<Protected<[u8; 32]>>,
@ -130,6 +135,7 @@ impl FileHeader {
/// All it requires is a password. Hashing is handled for you. /// All it requires is a password. Hashing is handled for you.
/// ///
/// A deserialized data type will be returned from this function /// A deserialized data type will be returned from this function
#[cfg(feature = "serde")]
pub fn decrypt_metadata<T>(&self, password: Protected<Vec<u8>>) -> Result<T> pub fn decrypt_metadata<T>(&self, password: Protected<Vec<u8>>) -> Result<T>
where where
T: serde::de::DeserializeOwned, T: serde::de::DeserializeOwned,

View file

@ -13,13 +13,17 @@
use crate::Protected; use crate::Protected;
use crate::{primitives::SALT_LEN, Error, Result}; use crate::{primitives::SALT_LEN, Error, Result};
use argon2::Argon2; use argon2::Argon2;
use serde::{Deserialize, Serialize};
use specta::Type;
/// These parameters define the password-hashing level. /// These parameters define the password-hashing level.
/// ///
/// The harder the parameter, the longer the password will take to hash. /// 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)] #[allow(clippy::use_self)]
pub enum Params { pub enum Params {
Standard, Standard,
@ -28,7 +32,13 @@ pub enum Params {
} }
/// This defines all available password hashing algorithms. /// 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 { pub enum HashingAlgorithm {
Argon2id(Params), Argon2id(Params),
} }

View file

@ -39,7 +39,7 @@ use std::sync::Mutex;
use crate::crypto::stream::{StreamDecryption, StreamEncryption}; use crate::crypto::stream::{StreamDecryption, StreamEncryption};
use crate::primitives::{ 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::{ use crate::{
crypto::stream::Algorithm, crypto::stream::Algorithm,
@ -49,11 +49,11 @@ use crate::{
use crate::{Error, Result}; use crate::{Error, Result};
use dashmap::DashMap; use dashmap::DashMap;
use serde::Serialize;
use serde_big_array::BigArray;
use specta::Type;
use uuid::Uuid; use uuid::Uuid;
#[cfg(feature = "serde")]
use serde_big_array::BigArray;
use super::hashing::HashingAlgorithm; use super::hashing::HashingAlgorithm;
// The terminology in this file is very confusing. // 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 `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) // 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 struct StoredKey {
pub uuid: uuid::Uuid, // uuid for identification. shared with mounted keys 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 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 hashing_algorithm: HashingAlgorithm, // hashing algorithm used for hashing the key with the content salt
pub salt: [u8; SALT_LEN], // salt to hash the master password with pub content_salt: [u8; SALT_LEN],
pub content_salt: [u8; SALT_LEN], // salt used for file data #[cfg_attr(feature = "serde", serde(with = "BigArray"))] // salt used for file data
#[serde(with = "BigArray")]
pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], // this is for encrypting the `key` pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], // this is for encrypting the `key`
pub master_key_nonce: Vec<u8>, // nonce for encrypting the master key pub master_key_nonce: Vec<u8>, // nonce for encrypting the master key
pub key_nonce: Vec<u8>, // nonce used for encrypting the main key pub key_nonce: Vec<u8>, // nonce used for encrypting the main key
pub key: Vec<u8>, // encrypted. the key stored in spacedrive (e.g. generated 64 char key) pub key: Vec<u8>, // 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. /// This contains the plaintext key, and the same key hashed with the content salt.
#[derive(Clone)] #[derive(Clone)]
pub struct MountedKey { pub struct MountedKey {
pub uuid: Uuid, // used for identification. shared with stored keys pub uuid: Uuid, // used for identification. shared with stored keys
pub key: Protected<Vec<u8>>, // the actual key itself, text format encodable (so it can be viewed with an UI)
pub content_salt: [u8; SALT_LEN], // the salt used for file data
pub hashed_key: Protected<[u8; 32]>, // this is hashed with the content salt, for instant access 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. /// Use the associated functions to interact with it.
pub struct KeyManager { pub struct KeyManager {
master_password: Mutex<Option<Protected<Vec<u8>>>>, // the user's. we take ownership here to prevent other functions attempting to manage/pass it to us master_password: Mutex<Option<Protected<[u8; 32]>>>, // the *hashed* master password+secret key combo
verification_key: Mutex<Option<StoredKey>>,
keystore: DashMap<Uuid, StoredKey>, keystore: DashMap<Uuid, StoredKey>,
keymount: DashMap<Uuid, MountedKey>, keymount: DashMap<Uuid, MountedKey>,
default: Mutex<Option<Uuid>>, default: Mutex<Option<Uuid>>,
} }
// 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<String>,
pub secret_key: Protected<String>, // 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<String>, // base64 encoded string that is required along with the master password
pub updated_keystore: Vec<StoredKey>,
}
/// The `KeyManager` functions should be used for all key-related management. /// The `KeyManager` functions should be used for all key-related management.
impl KeyManager { 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<OnboardingBundle> {
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] #[must_use]
pub fn new(stored_keys: Vec<StoredKey>, master_password: Option<Protected<Vec<u8>>>) -> Self { pub fn new(stored_keys: Vec<StoredKey>) -> Self {
let keystore = DashMap::new(); let keystore = DashMap::new();
let mut verification_key = None;
for key in stored_keys { 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<Uuid, MountedKey> = DashMap::new(); let keymount: DashMap<Uuid, MountedKey> = DashMap::new();
Self { Self {
master_password: Mutex::new(master_password), master_password: Mutex::new(None),
verification_key: Mutex::new(verification_key),
keystore, keystore,
keymount, keymount,
default: Mutex::new(None), 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. /// 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. /// 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<StoredKey>) -> Result<()> { pub fn populate_keystore(&self, stored_keys: Vec<StoredKey>) -> Result<()> {
for key in stored_keys { 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(()) Ok(())
@ -174,6 +268,7 @@ impl KeyManager {
} }
} }
/// This allows you to clear the default key
pub fn clear_default(&self) -> Result<()> { pub fn clear_default(&self) -> Result<()> {
let mut default = self.default.lock()?; let mut default = self.default.lock()?;
@ -186,19 +281,277 @@ impl KeyManager {
} }
/// This should ONLY be used internally. /// This should ONLY be used internally.
fn get_master_password(&self) -> Result<Protected<Vec<u8>>> { fn get_master_password(&self) -> Result<Protected<[u8; 32]>> {
let master_password = self.master_password.lock()?; match &*self.master_password.lock()? {
match &*master_password {
Some(k) => Ok(k.clone()), Some(k) => Ok(k.clone()),
None => Err(Error::NoMasterPassword), None => Err(Error::NoMasterPassword),
} }
} }
pub fn set_master_password(&self, master_password: Protected<Vec<u8>>) -> Result<()> { /// This should ONLY be used internally.
// this returns a result, so we can potentially implement password checking functionality pub fn get_verification_key(&self) -> Result<StoredKey> {
*self.master_password.lock()? = Some(master_password); 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<String>,
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
) -> Result<MasterPasswordChangeBundle> {
// 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<Vec<StoredKey>> = 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<String>` to a `Protected<Vec<u8>>`
#[allow(clippy::unused_self)]
#[allow(clippy::needless_pass_by_value)]
fn convert_master_password_string(
&self,
master_password: Protected<String>,
) -> Protected<Vec<u8>> {
Protected::new(master_password.expose().as_bytes().to_vec())
}
/// Used internally to convert from a base64-encoded `Protected<String>` 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<String>,
) -> 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<StoredKey>` so they can be written to Prisma
pub fn import_keystore_backup(
&self,
master_password: Protected<String>, // at the time of the backup
secret_key: Protected<String>, // at the time of the backup
stored_keys: &[StoredKey], // from the backup
) -> Result<Vec<StoredKey>> {
// 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<StoredKey> = 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<String>,
secret_key: Protected<String>,
) -> 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 /// This function is for removing a previously-added master password
@ -208,6 +561,7 @@ impl KeyManager {
Ok(()) Ok(())
} }
/// This function is used for seeing if the key manager has a master password.
pub fn has_master_password(&self) -> Result<bool> { pub fn has_master_password(&self) -> Result<bool> {
Ok(self.master_password.lock()?.is_some()) Ok(self.master_password.lock()?.is_some())
} }
@ -253,7 +607,7 @@ impl KeyManager {
self.keymount.remove(&uuid); self.keymount.remove(&uuid);
Ok(()) Ok(())
} else { } 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 /// We could add a log to this, so that the user can view mounts
pub fn mount(&self, uuid: Uuid) -> Result<()> { pub fn mount(&self, uuid: Uuid) -> Result<()> {
if self.keymount.get(&uuid).is_some() {
return Err(Error::KeyAlreadyMounted);
}
match self.keystore.get(&uuid) { match self.keystore.get(&uuid) {
Some(stored_key) => { 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 // Decrypt the StoredKey's master key using the user's hashed password
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes( 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.master_key_nonce,
stored_key.algorithm, stored_key.algorithm,
&stored_key.master_key, &stored_key.master_key,
&[], &[],
) { ) {
master_key.copy_from_slice(&decrypted_master_key); Ok(Protected::new(to_array(
Ok(Protected::new(master_key)) decrypted_master_key.expose().clone(),
)?))
} else { } else {
Err(Error::IncorrectPassword) Err(Error::IncorrectPassword)
}?; }?;
@ -314,13 +665,11 @@ impl KeyManager {
// Hash the key once with the parameters/algorithm the user selected during first mount // Hash the key once with the parameters/algorithm the user selected during first mount
let hashed_key = stored_key let hashed_key = stored_key
.hashing_algorithm .hashing_algorithm
.hash(key.clone(), stored_key.content_salt)?; .hash(key, stored_key.content_salt)?;
// Construct the MountedKey and insert it into the Keymount // Construct the MountedKey and insert it into the Keymount
let mounted_key = MountedKey { let mounted_key = MountedKey {
uuid: stored_key.uuid, uuid: stored_key.uuid,
key,
content_salt: stored_key.content_salt,
hashed_key, 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<Protected<Vec<u8>>> {
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. /// This function is for accessing the internal keymount.
/// ///
/// We could add a log to this, so that the user can view accesses /// We could add a log to this, so that the user can view accesses
@ -379,23 +764,17 @@ impl KeyManager {
algorithm: Algorithm, algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm, hashing_algorithm: HashingAlgorithm,
) -> Result<Uuid> { ) -> Result<Uuid> {
let master_password = self.get_master_password()?;
let uuid = uuid::Uuid::new_v4(); let uuid = uuid::Uuid::new_v4();
// Generate items we'll need for encryption // Generate items we'll need for encryption
let key_nonce = generate_nonce(algorithm); let key_nonce = generate_nonce(algorithm);
let master_key = generate_master_key(); let master_key = generate_master_key();
let master_key_nonce = generate_nonce(algorithm); let master_key_nonce = generate_nonce(algorithm);
let salt = generate_salt();
let content_salt = generate_salt(); // for PVM/MD let content_salt = generate_salt(); // for PVM/MD
// Hash the user's master password // Encrypt the master key with the user's hashed password
let hashed_password = hashing_algorithm.hash(master_password, salt)?;
// Encrypted the master key with the user's hashed password
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes( let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
hashed_password, self.get_master_password()?,
&master_key_nonce, &master_key_nonce,
algorithm, algorithm,
master_key.expose(), master_key.expose(),
@ -411,7 +790,6 @@ impl KeyManager {
uuid, uuid,
algorithm, algorithm,
hashing_algorithm, hashing_algorithm,
salt,
content_salt, content_salt,
master_key: encrypted_master_key, master_key: encrypted_master_key,
master_key_nonce, master_key_nonce,

View file

@ -2,7 +2,7 @@
//! //!
//! This includes things such as cryptographically-secure random salt/master key/nonce generation, //! This includes things such as cryptographically-secure random salt/master key/nonce generation,
//! lengths for master keys and even the streaming block size. //! lengths for master keys and even the streaming block size.
use rand::{RngCore, SeedableRng}; use rand::{seq::SliceRandom, RngCore, SeedableRng};
use zeroize::Zeroize; use zeroize::Zeroize;
use crate::{crypto::stream::Algorithm, Error, Protected, Result}; 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 /// The length of the (unencrypted) master key
pub const MASTER_KEY_LEN: usize = 32; pub const MASTER_KEY_LEN: usize = 32;
pub const PASSPHRASE_LEN: usize = 7;
/// This should be used for generating nonces for encryption. /// This should be used for generating nonces for encryption.
/// ///
/// An algorithm is required so this function can calculate the length of the nonce. /// An algorithm is required so this function can calculate the length of the nonce.
@ -68,3 +70,34 @@ pub fn to_array<const I: usize>(bytes: Vec<u8>) -> Result<[u8; I]> {
Error::VecArrSizeMismatch Error::VecArrSizeMismatch
}) })
} }
/// This generates a 7 word diceware passphrase, separated with `-`
#[must_use]
pub fn generate_passphrase() -> Protected<String> {
let wordlist = include_str!("../assets/eff_large_wordlist.txt")
.lines()
.collect::<Vec<&str>>();
let words: Vec<String> = 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)
}

View file

@ -9,6 +9,8 @@ export type Procedures = {
{ key: "jobs.getRunning", input: LibraryArgs<null>, result: Array<JobReport> } | { key: "jobs.getRunning", input: LibraryArgs<null>, result: Array<JobReport> } |
{ key: "jobs.isRunning", input: LibraryArgs<null>, result: boolean } | { key: "jobs.isRunning", input: LibraryArgs<null>, result: boolean } |
{ key: "keys.getDefault", input: LibraryArgs<null>, result: string | null } | { key: "keys.getDefault", input: LibraryArgs<null>, result: string | null } |
{ key: "keys.getKey", input: LibraryArgs<string>, result: string } |
{ key: "keys.hasMasterPassword", input: LibraryArgs<null>, result: boolean } |
{ key: "keys.list", input: LibraryArgs<null>, result: Array<StoredKey> } | { key: "keys.list", input: LibraryArgs<null>, result: Array<StoredKey> } |
{ key: "keys.listMounted", input: LibraryArgs<null>, result: Array<string> } | { key: "keys.listMounted", input: LibraryArgs<null>, result: Array<string> } |
{ key: "library.getStatistics", input: LibraryArgs<null>, result: Statistics } | { key: "library.getStatistics", input: LibraryArgs<null>, result: Statistics } |
@ -37,10 +39,15 @@ export type Procedures = {
{ key: "jobs.identifyUniqueFiles", input: LibraryArgs<IdentifyUniqueFilesArgs>, result: null } | { key: "jobs.identifyUniqueFiles", input: LibraryArgs<IdentifyUniqueFilesArgs>, result: null } |
{ key: "jobs.objectValidator", input: LibraryArgs<ObjectValidatorArgs>, result: null } | { key: "jobs.objectValidator", input: LibraryArgs<ObjectValidatorArgs>, result: null } |
{ key: "keys.add", input: LibraryArgs<KeyAddArgs>, result: null } | { key: "keys.add", input: LibraryArgs<KeyAddArgs>, result: null } |
{ key: "keys.backupKeystore", input: LibraryArgs<string>, result: null } |
{ key: "keys.changeMasterPassword", input: LibraryArgs<MasterPasswordChangeArgs>, result: string } |
{ key: "keys.clearMasterPassword", input: LibraryArgs<null>, result: null } |
{ key: "keys.deleteFromLibrary", input: LibraryArgs<string>, result: null } | { key: "keys.deleteFromLibrary", input: LibraryArgs<string>, result: null } |
{ key: "keys.mount", input: LibraryArgs<string>, result: null } | { key: "keys.mount", input: LibraryArgs<string>, result: null } |
{ key: "keys.onboarding", input: LibraryArgs<OnboardingArgs>, result: OnboardingKeys } |
{ key: "keys.restoreKeystore", input: LibraryArgs<RestoreBackupArgs>, result: number } |
{ key: "keys.setDefault", input: LibraryArgs<string>, result: null } | { key: "keys.setDefault", input: LibraryArgs<string>, result: null } |
{ key: "keys.setMasterPassword", input: LibraryArgs<string>, result: null } | { key: "keys.setMasterPassword", input: LibraryArgs<SetMasterPasswordArgs>, result: null } |
{ key: "keys.unmount", input: LibraryArgs<string>, result: null } | { key: "keys.unmount", input: LibraryArgs<string>, result: null } |
{ key: "keys.unmountAll", input: LibraryArgs<null>, result: null } | { key: "keys.unmountAll", input: LibraryArgs<null>, result: null } |
{ key: "keys.updateKeyName", input: LibraryArgs<KeyNameUpdateArgs>, result: null } | { key: "keys.updateKeyName", input: LibraryArgs<KeyNameUpdateArgs>, result: null } |
@ -95,7 +102,7 @@ export interface JobReport { id: string, name: string, data: Array<number> | nul
export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused" 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 } 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<number> } export interface LocationUpdateArgs { id: number, name: string | null, indexer_rules_ids: Array<number> }
export interface MasterPasswordChangeArgs { password: string, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm }
export interface Node { id: number, pub_id: Array<number>, name: string, platform: number, version: string | null, last_seen: string, timezone: string | null, date_created: string } export interface Node { id: number, pub_id: Array<number>, 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 } 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 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 type Params = "Standard" | "Hardened" | "Paranoid"
export interface RestoreBackupArgs { password: string, secret_key: string, path: string }
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent" export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
export interface SetFavoriteArgs { id: number, favorite: boolean } 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 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 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<number>, content_salt: Array<number>, master_key: Array<number>, master_key_nonce: Array<number>, key_nonce: Array<number>, key: Array<number> } export interface StoredKey { uuid: string, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, content_salt: Array<number>, master_key: Array<number>, master_key_nonce: Array<number>, key_nonce: Array<number>, key: Array<number> }
export interface Tag { id: number, pub_id: Array<number>, name: string | null, color: string | null, total_objects: number | null, redundancy_goal: number | null, date_created: string, date_modified: string } export interface Tag { id: number, pub_id: Array<number>, name: string | null, color: string | null, total_objects: number | null, redundancy_goal: number | null, date_created: string, date_modified: string }

View file

@ -33,6 +33,9 @@
"@tanstack/react-query-devtools": "^4.12.0", "@tanstack/react-query-devtools": "^4.12.0",
"@tanstack/react-virtual": "3.0.0-beta.18", "@tanstack/react-virtual": "3.0.0-beta.18",
"@vitejs/plugin-react": "^2.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", "autoprefixer": "^10.4.12",
"byte-size": "^8.1.0", "byte-size": "^8.1.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",

View file

@ -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<FormValues>({
defaultValues: {
masterPassword: '',
secretKey: '',
filePath: ''
}
});
const onSubmit: SubmitHandler<FormValues> = (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 (
<>
<form onSubmit={handleSubmit(onSubmit)}>
<Dialog
open={showBackupRestoreDialog}
setOpen={setShowBackupRestoreDialog}
title="Restore Keys"
description="Restore keys from a backup."
loading={restoreKeystoreMutation.isLoading}
ctaLabel="Restore"
trigger={trigger}
>
<div className="relative flex flex-grow mt-3 mb-2">
<Input
className="flex-grow !py-0.5"
placeholder="Master Password"
required
type={showMasterPassword ? 'text' : 'password'}
{...register('masterPassword', { required: true })}
/>
<Button
onClick={() => setShowMasterPassword(!showMasterPassword)}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
type="button"
>
<MPCurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<div className="relative flex flex-grow mb-3">
<Input
className="flex-grow !py-0.5"
placeholder="Secret Key"
{...register('secretKey', { required: true })}
required
type={showSecretKey ? 'text' : 'password'}
/>
<Button
onClick={() => setShowSecretKey(!showSecretKey)}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
type="button"
>
<SKCurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<div className="relative flex flex-grow mb-2">
<Button
size="sm"
variant={getValues('filePath') !== '' ? 'accent' : 'gray'}
type="button"
onClick={() => {
open()?.then((result) => {
if (result) setValue('filePath', result as string);
});
}}
>
Select File
</Button>
</div>
</Dialog>
</form>
<Dialog
open={showRestorationFinalizationDialog}
setOpen={setShowRestorationFinalizationDialog}
title="Import Successful"
description=""
ctaAction={() => {
setShowRestorationFinalizationDialog(false);
}}
ctaLabel="Done"
trigger={<></>}
>
<div className="text-sm">
{totalKeysImported}{' '}
{totalKeysImported !== 1 ? 'keys were imported.' : 'key was imported.'}
</div>
</Dialog>
</>
);
};

View file

@ -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<FormValues>({
defaultValues: {
masterPassword: '',
masterPassword2: '',
encryptionAlgo: 'XChaCha20Poly1305',
hashingAlgo: 'Argon2id-s'
}
});
const onSubmit: SubmitHandler<FormValues> = (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 (
<>
<form onSubmit={handleSubmit(onSubmit)}>
<Dialog
open={showMasterPasswordDialog}
setOpen={setShowMasterPasswordDialog}
title="Change Master Password"
description="Select a new master password for your key manager."
ctaDanger={true}
loading={changeMasterPassword.isLoading}
ctaLabel="Change"
trigger={trigger}
>
<div className="relative flex flex-grow mt-3 mb-2">
<Input
className={`flex-grow w-max !py-0.5`}
placeholder="New Password"
required
{...register('masterPassword', { required: true })}
onChange={(e) => setPasswordMeterMasterPw(e.target.value)}
value={passwordMeterMasterPw}
type={showMasterPassword1 ? 'text' : 'password'}
/>
<Button
onClick={() => setShowMasterPassword1(!showMasterPassword1)}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
type="button"
>
<MP1CurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<div className="relative flex flex-grow mb-2">
<Input
className={`flex-grow !py-0.5}`}
placeholder="New Password (again)"
required
{...register('masterPassword2', { required: true })}
type={showMasterPassword2 ? 'text' : 'password'}
/>
<Button
onClick={() => setShowMasterPassword2(!showMasterPassword2)}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
type="button"
>
<MP2CurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<PasswordMeter password={passwordMeterMasterPw} />
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
<div className="flex flex-col">
<span className="text-xs font-bold">Encryption</span>
<Select
className="mt-2"
value={getValues('encryptionAlgo')}
onChange={(e) => setValue('encryptionAlgo', e)}
>
<SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption>
<SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption>
</Select>
</div>
<div className="flex flex-col">
<span className="text-xs font-bold">Hashing</span>
<Select
className="mt-2"
value={getValues('hashingAlgo')}
onChange={(e) => setValue('hashingAlgo', e)}
>
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
</Select>
</div>
</div>
</Dialog>
</form>
<Dialog
open={showSecretKeyDialog}
setOpen={setShowSecretKeyDialog}
title="Secret Key"
description="Please store this secret key securely as it is needed to access your key manager."
ctaAction={() => {
setShowSecretKeyDialog(false);
}}
ctaLabel="Done"
trigger={<></>}
>
<Input
className="flex-grow w-full mt-3"
value={secretKey}
placeholder="Secret Key"
disabled={true}
/>
</Dialog>
</>
);
};
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 (
<div className="mt-4 mb-5 relative flex flex-grow">
<div className="mt-2 w-4/5 h-[5px] rounded-[80px]">
<div
style={innerDiv}
className={clsx(
zx.score === 0 && 'bg-red-700',
zx.score === 1 && 'bg-red-500',
zx.score === 2 && 'bg-amber-400',
zx.score === 3 && 'bg-lime-500',
zx.score === 4 && 'bg-accent'
)}
/>
</div>
<span
className={clsx(
'absolute font-[750] right-[5px] text-sm pr-1 pl-1',
zx.score === 0 && 'text-red-700',
zx.score === 1 && 'text-red-500',
zx.score === 2 && 'text-amber-400',
zx.score === 3 && 'text-lime-500',
zx.score === 4 && 'text-accent'
)}
>
{ratings[zx.score]}
</span>
</div>
);
};

View file

@ -7,7 +7,9 @@ import { useMemo } from 'react';
export type KeyListProps = DefaultProps; export type KeyListProps = DefaultProps;
const ListKeys = () => { export const ListOfKeys = (props: { noKeysMessage: boolean }) => {
const { noKeysMessage } = props;
const keys = useLibraryQuery(['keys.list']); const keys = useLibraryQuery(['keys.list']);
const mounted_uuids = useLibraryQuery(['keys.listMounted']); const mounted_uuids = useLibraryQuery(['keys.listMounted']);
@ -20,7 +22,7 @@ const ListKeys = () => {
[keys, mounted_uuids] [keys, mounted_uuids]
); );
if(keys.data?.length === 0) { if(keys.data?.length === 0 && noKeysMessage) {
return ( return (
<CategoryHeading>No keys available.</CategoryHeading> <CategoryHeading>No keys available.</CategoryHeading>
) )
@ -44,7 +46,7 @@ const ListKeys = () => {
) )
}; };
export function KeyList(props: KeyListProps) { export const KeyList = (props: KeyListProps) => {
const unmountAll = useLibraryMutation(['keys.unmountAll']); const unmountAll = useLibraryMutation(['keys.unmountAll']);
return ( return (
@ -53,7 +55,7 @@ export function KeyList(props: KeyListProps) {
<div className=""> <div className="">
{/* <CategoryHeading>Mounted keys</CategoryHeading> */} {/* <CategoryHeading>Mounted keys</CategoryHeading> */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<ListKeys></ListKeys> <ListOfKeys noKeysMessage />
</div> </div>
</div> </div>
</div> </div>

View file

@ -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 { DefaultProps } from '../primitive/types';
import { KeyList } from './KeyList'; import { KeyList } from './KeyList';
@ -7,24 +10,123 @@ import { KeyMounter } from './KeyMounter';
export type KeyManagerProps = DefaultProps; export type KeyManagerProps = DefaultProps;
export function KeyManager(props: KeyManagerProps) { export function KeyManager(props: KeyManagerProps) {
return ( const hasMasterPw = useLibraryQuery(['keys.hasMasterPassword']);
<div> const setMasterPasswordMutation = useLibraryMutation('keys.setMasterPassword');
<Tabs.Root defaultValue="mount"> const unmountAll = useLibraryMutation('keys.unmountAll');
<Tabs.List> const clearMasterPassword = useLibraryMutation('keys.clearMasterPassword');
<Tabs.Trigger className="text-sm font-medium" value="mount">
Mount const [showMasterPassword, setShowMasterPassword] = useState(false);
</Tabs.Trigger> const [showSecretKey, setShowSecretKey] = useState(false);
<Tabs.Trigger className="text-sm font-medium" value="keys">
Keys const [masterPassword, setMasterPassword] = useState('');
</Tabs.Trigger> const [secretKey, setSecretKey] = useState('');
</Tabs.List>
<Tabs.Content value="keys"> if (!hasMasterPw?.data) {
<KeyList /> const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
</Tabs.Content> const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
<Tabs.Content value="mount">
<KeyMounter /> return (
</Tabs.Content> <div className="p-2">
</Tabs.Root> <div className="relative flex flex-grow mb-2">
</div> <Input
); value={masterPassword}
onChange={(e) => setMasterPassword(e.target.value)}
autoFocus
type={showMasterPassword ? 'text' : 'password'}
className="flex-grow !py-0.5"
placeholder="Master Password"
/>
<Button
onClick={() => setShowMasterPassword(!showMasterPassword)}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
>
<MPCurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<div className="relative flex flex-grow mb-2">
<Input
value={secretKey}
onChange={(e) => setSecretKey(e.target.value)}
type={showSecretKey ? 'text' : 'password'}
className="flex-grow !py-0.5"
placeholder="Secret Key"
/>
<Button
onClick={() => setShowSecretKey(!showSecretKey)}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
>
<SKCurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<Button
className="w-full"
variant="accent"
disabled={setMasterPasswordMutation.isLoading}
onClick={() => {
if (masterPassword !== '' && secretKey !== '') {
setMasterPassword('');
setSecretKey('');
setMasterPasswordMutation.mutate(
{ password: masterPassword, secret_key: secretKey },
{
onError: () => {
alert('Incorrect information provided.');
}
}
);
}
}}
>
Unlock
</Button>
</div>
);
} else {
return (
<div>
<Tabs.Root defaultValue="mount">
<div className="flex flex-col">
<Tabs.List>
<Tabs.Trigger className="text-sm font-medium" value="mount">
Mount
</Tabs.Trigger>
<Tabs.Trigger className="text-sm font-medium" value="keys">
Keys
</Tabs.Trigger>
<div className="flex-grow" />
<Button
size="icon"
onClick={() => {
unmountAll.mutate(null);
clearMasterPassword.mutate(null);
}}
variant="outline"
className="text-ink-faint"
>
<Lock className="w-4 h-4 text-ink-faint" />
</Button>
<ButtonLink
to="/settings/keys"
size="icon"
variant="outline"
className="text-ink-faint"
>
<Gear className="w-4 h-4 text-ink-faint" />
</ButtonLink>
</Tabs.List>
</div>
<Tabs.Content value="keys">
<KeyList />
</Tabs.Content>
<Tabs.Content value="mount">
<KeyMounter />
</Tabs.Content>
</Tabs.Root>
</div>
);
}
} }

View file

@ -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 { Button, CategoryHeading, Input, Select, SelectOption, Switch, cva, tw } from '@sd/ui';
import { Eye, EyeSlash, Info } from 'phosphor-react'; import { Eye, EyeSlash, Info } from 'phosphor-react';
import { useEffect, useRef, useState } from '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'; import { Tooltip } from '../tooltip/Tooltip';
const KeyHeading = tw(CategoryHeading)`mb-1`; const KeyHeading = tw(CategoryHeading)`mb-1`;
@ -18,7 +19,7 @@ export function KeyMounter() {
const mounted_uuids = useLibraryQuery(['keys.listMounted']); const mounted_uuids = useLibraryQuery(['keys.listMounted']);
const [showKey, setShowKey] = useState(false); const [showKey, setShowKey] = useState(false);
const [toggle, setToggle] = useState(true); const [librarySync, setLibrarySync] = useState(true);
const [key, setKey] = useState(''); const [key, setKey] = useState('');
const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305'); const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305');
@ -63,12 +64,12 @@ export function KeyMounter() {
<Switch <Switch
className="bg-app-selected" className="bg-app-selected"
size="sm" size="sm"
checked={toggle} checked={librarySync}
onCheckedChange={setToggle} onCheckedChange={setLibrarySync}
/> />
</div> </div>
<span className="ml-3 text-xs font-medium">Sync with Library</span> <span className="ml-3 text-xs font-medium">Sync with Library</span>
<Tooltip label="This key will be mounted on all devices running your Library"> <Tooltip label="This key will be registered with all devices running your Library">
<Info className="w-4 h-4 ml-1.5 text-ink-faint" /> <Info className="w-4 h-4 ml-1.5 text-ink-faint" />
</Tooltip> </Tooltip>
</div> </div>
@ -93,26 +94,20 @@ export function KeyMounter() {
<p className="pt-1.5 ml-0.5 text-[8pt] leading-snug text-ink-faint w-[90%]"> <p className="pt-1.5 ml-0.5 text-[8pt] leading-snug text-ink-faint w-[90%]">
Files encrypted with this key will be revealed and decrypted on the fly. Files encrypted with this key will be revealed and decrypted on the fly.
</p> </p>
<Button className="w-full mt-2" variant="accent" onClick={() => { <Button
let algorithm = encryptionAlgo as Algorithm; className="w-full mt-2"
let hashing_algorithm: HashingAlgorithm = { Argon2id: "Standard" }; variant="accent"
disabled={key === ''}
onClick={() => {
if (key !== '') {
setKey('');
switch(hashingAlgo) { const [algorithm, hashing_algorithm] = getCryptoSettings(encryptionAlgo, hashingAlgo);
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;
}
createKey.mutate({algorithm, hashing_algorithm, key }); createKey.mutate({ algorithm, hashing_algorithm, key, library_sync: librarySync });
setKey(""); }
} }}
}> >
Mount Key Mount Key
</Button> </Button>
</div> </div>

View file

@ -0,0 +1,17 @@
import { ReactNode } from 'react';
export interface SettingsSubHeaderProps {
title: string;
rightArea?: ReactNode;
}
export const SettingsSubHeader: React.FC<SettingsSubHeaderProps> = (props) => {
return (
<div className="flex">
<div className="flex-grow">
<h1 className="text-xl font-bold">{props.title}</h1>
</div>
{props.rightArea}
</div>
);
};

View file

@ -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 { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader'; 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<Props>) => {
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 (
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
<DropdownMenu.Trigger>{trigger}</DropdownMenu.Trigger>
{transitions(
(styles, show) =>
show && (
<DropdownMenu.Portal forceMount>
<DropdownMenu.Content forceMount asChild>
<animated.div
// most of this is copied over from the `OverlayPanel`
className={clsx(
'flex flex-col',
'z-50 m-2 space-y-1',
'select-none cursor-default rounded-lg',
'text-left text-sm text-ink',
'bg-app-overlay/80 backdrop-blur',
// 'border border-app-overlay',
'shadow-2xl shadow-black/60 ',
className
)}
style={styles}
>
{children}
</animated.div>
</DropdownMenu.Content>
</DropdownMenu.Portal>
)
)}
</DropdownMenu.Root>
);
};
export default function KeysSettings() { export default function KeysSettings() {
return ( const hasMasterPw = useLibraryQuery(['keys.hasMasterPassword']);
<SettingsContainer> const setMasterPasswordMutation = useLibraryMutation('keys.setMasterPassword');
<SettingsHeader title="Keys" description="Manage your keys." /> const unmountAll = useLibraryMutation('keys.unmountAll');
</SettingsContainer> 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 (
<div className="p-2 mr-20 ml-20 mt-10">
<div className="relative flex flex-grow mb-2">
<Input
value={masterPassword}
onChange={(e) => setMasterPassword(e.target.value)}
autoFocus
type={showMasterPassword ? 'text' : 'password'}
className="flex-grow !py-0.5"
placeholder="Master Password"
/>
<Button
onClick={() => setShowMasterPassword(!showMasterPassword)}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
>
<MPCurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<div className="relative flex flex-grow mb-2">
<Input
value={secretKey}
onChange={(e) => setSecretKey(e.target.value)}
type={showSecretKey ? 'text' : 'password'}
className="flex-grow !py-0.5"
placeholder="Secret Key"
/>
<Button
onClick={() => setShowSecretKey(!showSecretKey)}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
>
<SKCurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<Button
className="w-full"
variant="accent"
disabled={setMasterPasswordMutation.isLoading}
onClick={() => {
if (masterPassword !== '' && secretKey !== '') {
setMasterPassword('');
setSecretKey('');
setMasterPasswordMutation.mutate(
{ password: masterPassword, secret_key: secretKey },
{
onError: () => {
alert('Incorrect information provided.');
}
}
);
}
}}
>
Unlock
</Button>
</div>
);
} else {
return (
<SettingsContainer>
<SettingsHeader
title="Keys"
description="Manage your keys."
rightArea={
<div className="flex flex-row items-center">
<Button
size="icon"
onClick={() => {
unmountAll.mutate(null);
clearMasterPassword.mutate(null);
}}
variant="outline"
className="text-ink-faint"
>
<Lock className="w-4 h-4 text-ink-faint" />
</Button>
<KeyMounterDropdown
trigger={
<Button size="icon" variant="outline" className="text-ink-faint">
<Plus className="w-4 h-4 text-ink-faint" />
</Button>
}
>
<KeyMounter />
</KeyMounterDropdown>
</div>
}
/>
{hasMasterPw.data ? (
<div className="grid space-y-2">
<ListOfKeys noKeysMessage={false} />
</div>
) : null}
<SettingsSubHeader title="Password Options" />
<div className="flex flex-row">
<PasswordChangeDialog
trigger={
<Button size="sm" variant="gray" className="mr-2">
Change Master Password
</Button>
}
/>
</div>
<SettingsSubHeader title="Data Recovery" />
<div className="flex flex-row">
<Button
size="sm"
variant="gray"
className="mr-2"
onClick={() => {
// not platform-safe, probably will break on web but `platform` doesn't have a save dialog option
save()?.then((result) => {
if (result) backupKeystore.mutate(result as string);
});
}}
>
Backup
</Button>
<BackupRestoreDialog
trigger={
<Button size="sm" variant="gray" className="mr-2">
Restore
</Button>
}
/>
</div>
</SettingsContainer>
);
}
} }
// 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];
};

View file

@ -414,6 +414,9 @@ importers:
'@types/react-router-dom': ^5.3.3 '@types/react-router-dom': ^5.3.3
'@types/tailwindcss': ^3.1.0 '@types/tailwindcss': ^3.1.0
'@vitejs/plugin-react': ^2.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 autoprefixer: ^10.4.12
byte-size: ^8.1.0 byte-size: ^8.1.0
clsx: ^1.2.1 clsx: ^1.2.1
@ -456,6 +459,9 @@ importers:
'@tanstack/react-query-devtools': 4.12.0_pqnxmwujmmnpcx44ucekqkefny '@tanstack/react-query-devtools': 4.12.0_pqnxmwujmmnpcx44ucekqkefny
'@tanstack/react-virtual': 3.0.0-beta.18_react@18.2.0 '@tanstack/react-virtual': 3.0.0-beta.18_react@18.2.0
'@vitejs/plugin-react': 2.1.0_vite@3.1.8 '@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 autoprefixer: 10.4.12
byte-size: 8.1.0 byte-size: 8.1.0
clsx: 1.2.1 clsx: 1.2.1
@ -8461,7 +8467,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.19.3 '@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.19.3
magic-string: 0.26.7 magic-string: 0.26.7
react-refresh: 0.14.0 react-refresh: 0.14.0
vite: 3.1.8_sass@1.55.0 vite: 3.1.8
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -8718,6 +8724,18 @@ packages:
rimraf: 3.0.2 rimraf: 3.0.2
dev: false 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: /abort-controller/3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'} engines: {node: '>=6.5'}
@ -22083,7 +22101,7 @@ packages:
dependencies: dependencies:
'@rollup/pluginutils': 5.0.1 '@rollup/pluginutils': 5.0.1
'@svgr/core': 6.5.0 '@svgr/core': 6.5.0
vite: 3.1.8_sass@1.55.0 vite: 3.1.8
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
- supports-color - supports-color