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

View file

@ -54,7 +54,7 @@ image = "0.24.4"
webp = "0.2.2"
ffmpeg-next = { version = "5.1.1", optional = true, features = [] }
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
sd-crypto = { path = "../crates/crypto", features = ["rspc"] }
sd-crypto = { path = "../crates/crypto", features = ["rspc", "serde"] }
sd-file-ext = { path = "../crates/file-ext"}
fs_extra = "1.2.0"
tracing = "0.1.36"

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
// hashing algorithm used for hashing the master password
hashing_algorithm Bytes
// salt to hash the master password with
salt Bytes
// salt used for encrypting data with this key
content_salt Bytes
// the *encrypted* master key (48 bytes)

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 serde::Deserialize;
use sd_crypto::keys::keymanager::StoredKey;
use sd_crypto::{
crypto::stream::Algorithm,
keys::{hashing::HashingAlgorithm, keymanager::KeyManager},
Protected,
};
use serde::{Deserialize, Serialize};
use specta::Type;
use crate::{invalidate_query, prisma::key};
@ -13,6 +19,7 @@ pub struct KeyAddArgs {
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
key: String,
library_sync: bool,
}
#[derive(Type, Deserialize)]
@ -21,15 +28,65 @@ pub struct KeyNameUpdateArgs {
name: String,
}
#[derive(Type, Deserialize)]
pub struct SetMasterPasswordArgs {
password: String,
secret_key: String,
}
#[derive(Type, Deserialize)]
pub struct RestoreBackupArgs {
password: String,
secret_key: String,
path: PathBuf,
}
#[derive(Type, Deserialize)]
pub struct OnboardingArgs {
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
}
#[derive(Type, Deserialize)]
pub struct MasterPasswordChangeArgs {
password: String,
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
}
#[derive(Type, Serialize)]
pub struct OnboardingKeys {
master_password: String,
secret_key: String,
}
pub(crate) fn mount() -> RouterBuilder {
RouterBuilder::new()
.library_query("list", |t| {
t(|_, _: (), library| async move { Ok(library.key_manager.dump_keystore()) })
})
// do not unlock the key manager until this route returns true
.library_query("hasMasterPassword", |t| {
t(|_, _: (), library| async move { Ok(library.key_manager.has_master_password()?) })
})
// this is so we can show the key as mounted in the UI
.library_query("listMounted", |t| {
t(|_, _: (), library| async move { Ok(library.key_manager.get_mounted_uuids()) })
})
.library_query("getKey", |t| {
t(|_, key_uuid: uuid::Uuid, library| async move {
let key = library.key_manager.get_key(key_uuid)?;
let key_string = String::from_utf8(key.expose().clone()).map_err(|_| {
rspc::Error::new(
rspc::ErrorCode::InternalServerError,
"Error serializing bytes to String".into(),
)
})?;
Ok(key_string)
})
})
.library_mutation("mount", |t| {
t(|_, key_uuid: uuid::Uuid, library| async move {
library.key_manager.mount(key_uuid)?;
@ -61,6 +118,14 @@ pub(crate) fn mount() -> RouterBuilder {
Ok(())
})
})
.library_mutation("clearMasterPassword", |t| {
t(|_, _: (), library| async move {
library.key_manager.clear_master_password()?;
invalidate_query!(library, "keys.hasMasterPassword");
Ok(())
})
})
.library_mutation("deleteFromLibrary", |t| {
t(|_, key_uuid: uuid::Uuid, library| async move {
library.key_manager.remove_key(key_uuid)?;
@ -79,16 +144,53 @@ pub(crate) fn mount() -> RouterBuilder {
Ok(())
})
})
.library_mutation("setMasterPassword", |t| {
t(|_, password: String, library| async move {
// need to add master password checks in the keymanager itself to make sure it's correct
// this can either unwrap&fail, or we can return the error. either way, the user will have to correct this
// by entering the correct password
// for now, automounting might have to serve as the master password checks
.library_mutation("onboarding", |t| {
t(|_, args: OnboardingArgs, library| async move {
let bundle = KeyManager::onboarding(args.algorithm, args.hashing_algorithm)?;
let verification_key = bundle.verification_key;
// remove old nil-id keys if they were set
// they possibly won't be, but we CANNOT have multiple
library
.db
.key()
.delete_many(vec![key::uuid::equals(uuid::Uuid::nil().to_string())])
.exec()
.await?;
library
.key_manager
.set_master_password(Protected::new(password.as_bytes().to_vec()))?;
.db
.key()
.create(
verification_key.uuid.to_string(),
verification_key.algorithm.serialize().to_vec(),
verification_key.hashing_algorithm.serialize().to_vec(),
verification_key.content_salt.to_vec(),
verification_key.master_key.to_vec(),
verification_key.master_key_nonce.to_vec(),
verification_key.key_nonce.to_vec(),
verification_key.key.to_vec(),
vec![],
)
.exec()
.await?;
let keys = OnboardingKeys {
master_password: bundle.master_password.expose().clone(),
secret_key: base64::encode(bundle.secret_key.expose()),
};
Ok(keys)
})
})
.library_mutation("setMasterPassword", |t| {
t(|_, args: SetMasterPasswordArgs, library| async move {
// if this returns an error, the user MUST re-enter the correct password
library.key_manager.set_master_password(
Protected::new(args.password),
Protected::new(args.secret_key),
)?;
let automount = library
.db
@ -108,6 +210,8 @@ pub(crate) fn mount() -> RouterBuilder {
})?)?;
}
invalidate_query!(library, "keys.hasMasterPassword");
Ok(())
})
})
@ -161,17 +265,10 @@ pub(crate) fn mount() -> RouterBuilder {
})
.library_query("getDefault", |t| {
t(|_, _: (), library| async move {
// `find_first` should be okay here as only one default key should ever be set
// this is also stored in the keymanager but it's probably easier to get it from the DB
let default = library
.db
.key()
.find_first(vec![key::default::equals(true)])
.exec()
.await?;
let default = library.key_manager.get_default();
if let Some(default_key) = default {
Ok(Some(default_key.uuid))
if let Ok(default_key) = default {
Ok(Some(default_key))
} else {
Ok(None)
}
@ -196,23 +293,24 @@ pub(crate) fn mount() -> RouterBuilder {
let stored_key = library.key_manager.access_keystore(uuid)?;
library
.db
.key()
.create(
uuid.to_string(),
args.algorithm.serialize().to_vec(),
args.hashing_algorithm.serialize().to_vec(),
stored_key.salt.to_vec(),
stored_key.content_salt.to_vec(),
stored_key.master_key.to_vec(),
stored_key.master_key_nonce.to_vec(),
stored_key.key_nonce.to_vec(),
stored_key.key.to_vec(),
vec![],
)
.exec()
.await?;
if args.library_sync {
library
.db
.key()
.create(
uuid.to_string(),
args.algorithm.serialize().to_vec(),
args.hashing_algorithm.serialize().to_vec(),
stored_key.content_salt.to_vec(),
stored_key.master_key.to_vec(),
stored_key.master_key_nonce.to_vec(),
stored_key.key_nonce.to_vec(),
stored_key.key.to_vec(),
vec![],
)
.exec()
.await?;
}
// mount the key
library.key_manager.mount(uuid)?;
@ -222,4 +320,146 @@ pub(crate) fn mount() -> RouterBuilder {
Ok(())
})
})
.library_mutation("backupKeystore", |t| {
t(|_, path: PathBuf, library| async move {
// dump all stored keys that are in the key manager (maybe these should be taken from prisma as this will include even "non-sync with library" keys)
let mut stored_keys = library.key_manager.dump_keystore();
// include the verification key at the time of backup
stored_keys.push(library.key_manager.get_verification_key()?);
let mut output_file = std::fs::File::create(path).map_err(|_| {
rspc::Error::new(
rspc::ErrorCode::InternalServerError,
"Error creating file".into(),
)
})?;
output_file
.write_all(&serde_json::to_vec(&stored_keys).map_err(|_| {
rspc::Error::new(
rspc::ErrorCode::InternalServerError,
"Error serializing keystore".into(),
)
})?)
.map_err(|_| {
rspc::Error::new(
rspc::ErrorCode::InternalServerError,
"Error writing key backup to file".into(),
)
})?;
Ok(())
})
})
.library_mutation("restoreKeystore", |t| {
t(|_, args: RestoreBackupArgs, library| async move {
let mut input_file = std::fs::File::open(args.path).map_err(|_| {
rspc::Error::new(
rspc::ErrorCode::InternalServerError,
"Error opening backup file".into(),
)
})?;
let mut backup = Vec::new();
input_file.read_to_end(&mut backup).map_err(|_| {
rspc::Error::new(
rspc::ErrorCode::InternalServerError,
"Error reading backup file".into(),
)
})?;
let stored_keys: Vec<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::{
invalidate_query,
node::Platform,
prisma::{node, PrismaClient},
prisma::{key, node, PrismaClient},
util::{
db::load_and_migrate,
seeder::{indexer_rules_seeder, SeederError},
@ -12,11 +12,10 @@ use crate::{
use sd_crypto::{
crypto::stream::Algorithm,
keys::{
hashing::HashingAlgorithm,
hashing::{HashingAlgorithm, Params},
keymanager::{KeyManager, StoredKey},
},
primitives::to_array,
Protected,
};
use std::{
env, fs, io,
@ -74,11 +73,51 @@ impl From<LibraryManagerError> for rspc::Error {
pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, LibraryManagerError> {
// retrieve all stored keys from the DB
let key_manager = KeyManager::new(vec![], None);
let key_manager = KeyManager::new(vec![]);
// BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE
// this is so if there's no verification key set, we set one so users can use the key manager
// it will be done during onboarding, but for now things are statically set (unless they were changed)
if client
.key()
.find_many(vec![key::uuid::equals(uuid::Uuid::nil().to_string())])
.exec()
.await?
.is_empty()
{
client
.key()
.delete_many(vec![key::uuid::equals(uuid::Uuid::nil().to_string())])
.exec()
.await?;
// BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE
let verification_key = KeyManager::onboarding(
Algorithm::XChaCha20Poly1305,
HashingAlgorithm::Argon2id(Params::Standard),
)?
.verification_key;
// BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE
client
.key()
.create(
verification_key.uuid.to_string(),
verification_key.algorithm.serialize().to_vec(),
verification_key.hashing_algorithm.serialize().to_vec(),
verification_key.content_salt.to_vec(),
verification_key.master_key.to_vec(),
verification_key.master_key_nonce.to_vec(),
verification_key.key_nonce.to_vec(),
verification_key.key.to_vec(),
vec![],
)
.exec()
.await?;
}
let db_stored_keys = client.key().find_many(vec![]).exec().await?;
let mut default = Uuid::default();
let mut default = Uuid::nil();
// collect and serialize the stored keys
// shouldn't call unwrap so much here
@ -95,7 +134,6 @@ pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, Libr
StoredKey {
uuid,
salt: to_array(key.salt).unwrap(),
algorithm: Algorithm::deserialize(to_array(key.algorithm).unwrap()).unwrap(),
content_salt: to_array(key.content_salt).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)?;
}
////!!!! 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)
}

View file

@ -27,16 +27,19 @@ zeroize = "1.5.7"
thiserror = "1.0.37"
# metadata de/serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-big-array = "0.4.1"
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
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"
rspc = { workspace = true, optional = true }
specta = { workspace = true }
specta = { workspace = true, optional = true }
base64 = "0.13.1"
[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::{
crypto::stream::{Algorithm, StreamEncryption},
@ -11,12 +11,11 @@ use sd_crypto::{
primitives::{generate_master_key, generate_salt},
Protected,
};
use serde::{Deserialize, Serialize};
use std::fs::File;
const ALGORITHM: Algorithm = Algorithm::XChaCha20Poly1305;
const HASHING_ALGORITHM: HashingAlgorithm = HashingAlgorithm::Argon2id(Params::Standard);
#[derive(Serialize, Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct FileInformation {
pub file_name: String,
}

View file

@ -7,14 +7,18 @@ use aead::{
};
use aes_gcm::Aes256Gcm;
use chacha20poly1305::XChaCha20Poly1305;
use serde::{Deserialize, Serialize};
use specta::Type;
use zeroize::Zeroize;
use crate::{primitives::BLOCK_SIZE, Error, Protected, Result};
/// 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)]
pub enum Algorithm {
XChaCha20Poly1305,

View file

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

View file

@ -29,12 +29,15 @@
//! ```
use std::io::{Read, Seek};
#[cfg(feature = "serde")]
use crate::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
crypto::stream::{StreamDecryption, StreamEncryption},
primitives::{generate_nonce, MASTER_KEY_LEN},
Error, Protected, Result,
Protected,
};
use crate::{crypto::stream::Algorithm, Error, Result};
use super::file::FileHeader;
/// This is a metadata header item. You may add it to a header, and this will be stored with the file.
@ -63,6 +66,7 @@ impl FileHeader {
/// You will need to provide the user's password, and a semi-universal salt for hashing the user's password. This allows for extremely fast decryption.
///
/// Metadata needs to be accessed switfly, so a key management system should handle the salt generation.
#[cfg(feature = "serde")]
pub fn add_metadata<T>(
&mut self,
version: MetadataVersion,
@ -100,6 +104,7 @@ impl FileHeader {
/// All it requires is pre-hashed keys returned from the key manager
///
/// A deserialized data type will be returned from this function
#[cfg(feature = "serde")]
pub fn decrypt_metadata_from_prehashed<T>(
&self,
hashed_keys: Vec<Protected<[u8; 32]>>,
@ -130,6 +135,7 @@ impl FileHeader {
/// All it requires is a password. Hashing is handled for you.
///
/// A deserialized data type will be returned from this function
#[cfg(feature = "serde")]
pub fn decrypt_metadata<T>(&self, password: Protected<Vec<u8>>) -> Result<T>
where
T: serde::de::DeserializeOwned,

View file

@ -13,13 +13,17 @@
use crate::Protected;
use crate::{primitives::SALT_LEN, Error, Result};
use argon2::Argon2;
use serde::{Deserialize, Serialize};
use specta::Type;
/// These parameters define the password-hashing level.
///
/// The harder the parameter, the longer the password will take to hash.
#[derive(Clone, Copy, PartialEq, Eq, Type, Serialize, Deserialize)]
#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize),
derive(serde::Deserialize)
)]
#[cfg_attr(feature = "rspc", derive(specta::Type))]
#[allow(clippy::use_self)]
pub enum Params {
Standard,
@ -28,7 +32,13 @@ pub enum Params {
}
/// This defines all available password hashing algorithms.
#[derive(Clone, Copy, PartialEq, Eq, Type, Serialize, Deserialize)]
#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize),
derive(serde::Deserialize)
)]
#[cfg_attr(feature = "rspc", derive(specta::Type))]
pub enum HashingAlgorithm {
Argon2id(Params),
}

View file

@ -39,7 +39,7 @@ use std::sync::Mutex;
use crate::crypto::stream::{StreamDecryption, StreamEncryption};
use crate::primitives::{
generate_master_key, generate_nonce, generate_salt, to_array, MASTER_KEY_LEN,
generate_master_key, generate_nonce, generate_passphrase, generate_salt, to_array,
};
use crate::{
crypto::stream::Algorithm,
@ -49,11 +49,11 @@ use crate::{
use crate::{Error, Result};
use dashmap::DashMap;
use serde::Serialize;
use serde_big_array::BigArray;
use specta::Type;
use uuid::Uuid;
#[cfg(feature = "serde")]
use serde_big_array::BigArray;
use super::hashing::HashingAlgorithm;
// The terminology in this file is very confusing.
@ -63,17 +63,19 @@ use super::hashing::HashingAlgorithm;
// The `hashed_key` refers to the value you'd pass to PVM/MD decryption functions. It has been pre-hashed with the content salt.
// The content salt refers to the semi-universal salt that's used for metadata/preview media (unique to each key in the manager)
#[derive(Clone, PartialEq, Eq, Type, Serialize)]
/// This is a stored key, and can be freely written to Prisma/another database.
#[derive(Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "rspc", derive(specta::Type))]
pub struct StoredKey {
pub uuid: uuid::Uuid, // uuid for identification. shared with mounted keys
pub algorithm: Algorithm, // encryption algorithm for encrypting the master key. can be changed (requires a re-encryption though)
pub hashing_algorithm: HashingAlgorithm, // hashing algorithm to use for hashing everything related to this key. can't be changed once set.
pub salt: [u8; SALT_LEN], // salt to hash the master password with
pub content_salt: [u8; SALT_LEN], // salt used for file data
#[serde(with = "BigArray")]
pub hashing_algorithm: HashingAlgorithm, // hashing algorithm used for hashing the key with the content salt
pub content_salt: [u8; SALT_LEN],
#[cfg_attr(feature = "serde", serde(with = "BigArray"))] // salt used for file data
pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], // this is for encrypting the `key`
pub master_key_nonce: Vec<u8>, // nonce for encrypting the master key
pub key_nonce: Vec<u8>, // nonce used for encrypting the main 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: 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.
#[derive(Clone)]
pub struct MountedKey {
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 uuid: Uuid, // used for identification. shared with stored keys
pub hashed_key: Protected<[u8; 32]>, // this is hashed with the content salt, for instant access
}
@ -94,26 +94,114 @@ pub struct MountedKey {
///
/// Use the associated functions to interact with it.
pub struct KeyManager {
master_password: Mutex<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>,
keymount: DashMap<Uuid, MountedKey>,
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.
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]
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 mut verification_key = None;
for key in stored_keys {
keystore.insert(key.uuid, key);
if key.uuid.is_nil() {
verification_key = Some(key);
} else {
keystore.insert(key.uuid, key);
}
}
let keymount: DashMap<Uuid, MountedKey> = DashMap::new();
Self {
master_password: Mutex::new(master_password),
master_password: Mutex::new(None),
verification_key: Mutex::new(verification_key),
keystore,
keymount,
default: Mutex::new(None),
@ -123,9 +211,15 @@ impl KeyManager {
/// This function should be used to populate the keystore with multiple stored keys at a time.
///
/// It's suitable for when you created the key manager without populating it.
///
/// This also detects the nil-UUID master passphrase verification key
pub fn populate_keystore(&self, stored_keys: Vec<StoredKey>) -> Result<()> {
for key in stored_keys {
self.keystore.insert(key.uuid, key);
if key.uuid.is_nil() {
*self.verification_key.lock()? = Some(key);
} else {
self.keystore.insert(key.uuid, key);
}
}
Ok(())
@ -174,6 +268,7 @@ impl KeyManager {
}
}
/// This allows you to clear the default key
pub fn clear_default(&self) -> Result<()> {
let mut default = self.default.lock()?;
@ -186,19 +281,277 @@ impl KeyManager {
}
/// This should ONLY be used internally.
fn get_master_password(&self) -> Result<Protected<Vec<u8>>> {
let master_password = self.master_password.lock()?;
match &*master_password {
fn get_master_password(&self) -> Result<Protected<[u8; 32]>> {
match &*self.master_password.lock()? {
Some(k) => Ok(k.clone()),
None => Err(Error::NoMasterPassword),
}
}
pub fn set_master_password(&self, master_password: Protected<Vec<u8>>) -> Result<()> {
// this returns a result, so we can potentially implement password checking functionality
*self.master_password.lock()? = Some(master_password);
/// This should ONLY be used internally.
pub fn get_verification_key(&self) -> Result<StoredKey> {
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
@ -208,6 +561,7 @@ impl KeyManager {
Ok(())
}
/// This function is used for seeing if the key manager has a master password.
pub fn has_master_password(&self) -> Result<bool> {
Ok(self.master_password.lock()?.is_some())
}
@ -253,7 +607,7 @@ impl KeyManager {
self.keymount.remove(&uuid);
Ok(())
} else {
Err(Error::KeyNotFound)
Err(Error::KeyNotMounted)
}
}
@ -278,26 +632,23 @@ impl KeyManager {
///
/// We could add a log to this, so that the user can view mounts
pub fn mount(&self, uuid: Uuid) -> Result<()> {
if self.keymount.get(&uuid).is_some() {
return Err(Error::KeyAlreadyMounted);
}
match self.keystore.get(&uuid) {
Some(stored_key) => {
let master_password = self.get_master_password()?;
let hashed_password = stored_key
.hashing_algorithm
.hash(master_password, stored_key.salt)?;
let mut master_key = [0u8; MASTER_KEY_LEN];
// Decrypt the StoredKey's master key using the user's hashed password
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes(
hashed_password,
self.get_master_password()?,
&stored_key.master_key_nonce,
stored_key.algorithm,
&stored_key.master_key,
&[],
) {
master_key.copy_from_slice(&decrypted_master_key);
Ok(Protected::new(master_key))
Ok(Protected::new(to_array(
decrypted_master_key.expose().clone(),
)?))
} else {
Err(Error::IncorrectPassword)
}?;
@ -314,13 +665,11 @@ impl KeyManager {
// Hash the key once with the parameters/algorithm the user selected during first mount
let hashed_key = stored_key
.hashing_algorithm
.hash(key.clone(), stored_key.content_salt)?;
.hash(key, stored_key.content_salt)?;
// Construct the MountedKey and insert it into the Keymount
let mounted_key = MountedKey {
uuid: stored_key.uuid,
key,
content_salt: stored_key.content_salt,
hashed_key,
};
@ -332,6 +681,42 @@ impl KeyManager {
}
}
/// This function is used for getting the key itself, from a given UUID.
///
/// The master password/salt needs to be present, so we are able to decrypt the key itself from the stored key.
pub fn get_key(&self, uuid: Uuid) -> Result<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.
///
/// We could add a log to this, so that the user can view accesses
@ -379,23 +764,17 @@ impl KeyManager {
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
) -> Result<Uuid> {
let master_password = self.get_master_password()?;
let uuid = uuid::Uuid::new_v4();
// Generate items we'll need for encryption
let key_nonce = generate_nonce(algorithm);
let master_key = generate_master_key();
let master_key_nonce = generate_nonce(algorithm);
let salt = generate_salt();
let content_salt = generate_salt(); // for PVM/MD
// Hash the user's master password
let hashed_password = hashing_algorithm.hash(master_password, salt)?;
// Encrypted the master key with the user's hashed password
// Encrypt the master key with the user's hashed password
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
hashed_password,
self.get_master_password()?,
&master_key_nonce,
algorithm,
master_key.expose(),
@ -411,7 +790,6 @@ impl KeyManager {
uuid,
algorithm,
hashing_algorithm,
salt,
content_salt,
master_key: encrypted_master_key,
master_key_nonce,

View file

@ -2,7 +2,7 @@
//!
//! This includes things such as cryptographically-secure random salt/master key/nonce generation,
//! lengths for master keys and even the streaming block size.
use rand::{RngCore, SeedableRng};
use rand::{seq::SliceRandom, RngCore, SeedableRng};
use zeroize::Zeroize;
use crate::{crypto::stream::Algorithm, Error, Protected, Result};
@ -21,6 +21,8 @@ pub const ENCRYPTED_MASTER_KEY_LEN: usize = 48;
/// The length of the (unencrypted) master key
pub const MASTER_KEY_LEN: usize = 32;
pub const PASSPHRASE_LEN: usize = 7;
/// This should be used for generating nonces for encryption.
///
/// An algorithm is required so this function can calculate the length of the nonce.
@ -68,3 +70,34 @@ pub fn to_array<const I: usize>(bytes: Vec<u8>) -> Result<[u8; I]> {
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.isRunning", input: LibraryArgs<null>, result: boolean } |
{ 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.listMounted", input: LibraryArgs<null>, result: Array<string> } |
{ 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.objectValidator", input: LibraryArgs<ObjectValidatorArgs>, 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.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.setMasterPassword", input: LibraryArgs<string>, result: null } |
{ key: "keys.setMasterPassword", input: LibraryArgs<SetMasterPasswordArgs>, result: null } |
{ key: "keys.unmount", input: LibraryArgs<string>, result: null } |
{ key: "keys.unmountAll", input: LibraryArgs<null>, 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 interface KeyAddArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, key: string }
export interface KeyAddArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, key: string, library_sync: boolean }
export interface KeyNameUpdateArgs { uuid: string, name: string }
@ -113,6 +120,8 @@ export interface LocationExplorerArgs { location_id: number, path: string, limit
export interface LocationUpdateArgs { id: number, name: string | null, indexer_rules_ids: Array<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 NodeConfig { version: string | null, id: string, name: string, p2p_port: number | null }
@ -131,17 +140,25 @@ export interface Object { id: number, cas_id: string, integrity_checksum: string
export interface ObjectValidatorArgs { id: number, path: string }
export interface OnboardingArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm }
export interface OnboardingKeys { master_password: string, secret_key: string }
export type Params = "Standard" | "Hardened" | "Paranoid"
export interface RestoreBackupArgs { password: string, secret_key: string, path: string }
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
export interface SetFavoriteArgs { id: number, favorite: boolean }
export interface SetMasterPasswordArgs { password: string, secret_key: string }
export interface SetNoteArgs { id: number, note: string | null }
export interface Statistics { id: number, date_captured: string, total_object_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string }
export interface StoredKey { uuid: string, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, salt: Array<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 }

View file

@ -33,6 +33,9 @@
"@tanstack/react-query-devtools": "^4.12.0",
"@tanstack/react-virtual": "3.0.0-beta.18",
"@vitejs/plugin-react": "^2.1.0",
"@zxcvbn-ts/core": "^2.1.0",
"@zxcvbn-ts/language-common": "^2.0.1",
"@zxcvbn-ts/language-en": "^2.1.0",
"autoprefixer": "^10.4.12",
"byte-size": "^8.1.0",
"clsx": "^1.2.1",

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;
const ListKeys = () => {
export const ListOfKeys = (props: { noKeysMessage: boolean }) => {
const { noKeysMessage } = props;
const keys = useLibraryQuery(['keys.list']);
const mounted_uuids = useLibraryQuery(['keys.listMounted']);
@ -20,7 +22,7 @@ const ListKeys = () => {
[keys, mounted_uuids]
);
if(keys.data?.length === 0) {
if(keys.data?.length === 0 && noKeysMessage) {
return (
<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']);
return (
@ -53,7 +55,7 @@ export function KeyList(props: KeyListProps) {
<div className="">
{/* <CategoryHeading>Mounted keys</CategoryHeading> */}
<div className="space-y-1.5">
<ListKeys></ListKeys>
<ListOfKeys noKeysMessage />
</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 { KeyList } from './KeyList';
@ -7,24 +10,123 @@ import { KeyMounter } from './KeyMounter';
export type KeyManagerProps = DefaultProps;
export function KeyManager(props: KeyManagerProps) {
return (
<div>
<Tabs.Root defaultValue="mount">
<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>
</Tabs.List>
<Tabs.Content value="keys">
<KeyList />
</Tabs.Content>
<Tabs.Content value="mount">
<KeyMounter />
</Tabs.Content>
</Tabs.Root>
</div>
);
const hasMasterPw = useLibraryQuery(['keys.hasMasterPassword']);
const setMasterPasswordMutation = useLibraryMutation('keys.setMasterPassword');
const unmountAll = useLibraryMutation('keys.unmountAll');
const clearMasterPassword = useLibraryMutation('keys.clearMasterPassword');
const [showMasterPassword, setShowMasterPassword] = useState(false);
const [showSecretKey, setShowSecretKey] = useState(false);
const [masterPassword, setMasterPassword] = useState('');
const [secretKey, setSecretKey] = useState('');
if (!hasMasterPw?.data) {
const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
return (
<div className="p-2">
<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 (
<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 { Eye, EyeSlash, Info } from 'phosphor-react';
import { useEffect, useRef, useState } from 'react';
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
import { Algorithm, HashingAlgorithm, Params } from '@sd/client';
import { getCryptoSettings } from '../../screens/settings/library/KeysSetting';
import { Tooltip } from '../tooltip/Tooltip';
const KeyHeading = tw(CategoryHeading)`mb-1`;
@ -18,7 +19,7 @@ export function KeyMounter() {
const mounted_uuids = useLibraryQuery(['keys.listMounted']);
const [showKey, setShowKey] = useState(false);
const [toggle, setToggle] = useState(true);
const [librarySync, setLibrarySync] = useState(true);
const [key, setKey] = useState('');
const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305');
@ -63,12 +64,12 @@ export function KeyMounter() {
<Switch
className="bg-app-selected"
size="sm"
checked={toggle}
onCheckedChange={setToggle}
checked={librarySync}
onCheckedChange={setLibrarySync}
/>
</div>
<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" />
</Tooltip>
</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%]">
Files encrypted with this key will be revealed and decrypted on the fly.
</p>
<Button className="w-full mt-2" variant="accent" onClick={() => {
let algorithm = encryptionAlgo as Algorithm;
let hashing_algorithm: HashingAlgorithm = { Argon2id: "Standard" };
<Button
className="w-full mt-2"
variant="accent"
disabled={key === ''}
onClick={() => {
if (key !== '') {
setKey('');
switch(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;
}
const [algorithm, hashing_algorithm] = getCryptoSettings(encryptionAlgo, hashingAlgo);
createKey.mutate({algorithm, hashing_algorithm, key });
setKey("");
}
}>
createKey.mutate({ algorithm, hashing_algorithm, key, library_sync: librarySync });
}
}}
>
Mount Key
</Button>
</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 { 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() {
return (
<SettingsContainer>
<SettingsHeader title="Keys" description="Manage your keys." />
</SettingsContainer>
);
const hasMasterPw = useLibraryQuery(['keys.hasMasterPassword']);
const setMasterPasswordMutation = useLibraryMutation('keys.setMasterPassword');
const unmountAll = useLibraryMutation('keys.unmountAll');
const clearMasterPassword = useLibraryMutation('keys.clearMasterPassword');
const backupKeystore = useLibraryMutation('keys.backupKeystore');
const [showMasterPassword, setShowMasterPassword] = useState(false);
const [showSecretKey, setShowSecretKey] = useState(false);
const [masterPassword, setMasterPassword] = useState('');
const [secretKey, setSecretKey] = useState('');
const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
if (!hasMasterPw?.data) {
return (
<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/tailwindcss': ^3.1.0
'@vitejs/plugin-react': ^2.1.0
'@zxcvbn-ts/core': ^2.1.0
'@zxcvbn-ts/language-common': ^2.0.1
'@zxcvbn-ts/language-en': ^2.1.0
autoprefixer: ^10.4.12
byte-size: ^8.1.0
clsx: ^1.2.1
@ -456,6 +459,9 @@ importers:
'@tanstack/react-query-devtools': 4.12.0_pqnxmwujmmnpcx44ucekqkefny
'@tanstack/react-virtual': 3.0.0-beta.18_react@18.2.0
'@vitejs/plugin-react': 2.1.0_vite@3.1.8
'@zxcvbn-ts/core': 2.1.0
'@zxcvbn-ts/language-common': 2.0.1
'@zxcvbn-ts/language-en': 2.1.0
autoprefixer: 10.4.12
byte-size: 8.1.0
clsx: 1.2.1
@ -8461,7 +8467,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.19.3
magic-string: 0.26.7
react-refresh: 0.14.0
vite: 3.1.8_sass@1.55.0
vite: 3.1.8
transitivePeerDependencies:
- supports-color
@ -8718,6 +8724,18 @@ packages:
rimraf: 3.0.2
dev: false
/@zxcvbn-ts/core/2.1.0:
resolution: {integrity: sha512-doxol9xrO7LgyVJhguXe7vO0xthnIYmsOKoDwrLg0Ho2kkpQaVtM+AOQw+BkEiKIqNg1V48eUf4/cTzMElXdiA==}
dev: false
/@zxcvbn-ts/language-common/2.0.1:
resolution: {integrity: sha512-P+v5MA/UNc9nb3FEOEoDgTyIGQc2vLc6m04pdf5YyuNOzrL0iNANhECk2TUp62JbrjouJVodqhMH0j1a8/24Bg==}
dev: false
/@zxcvbn-ts/language-en/2.1.0:
resolution: {integrity: sha512-I3n4AAbArjPAZtwCrk9MQnSrcj5+9rq8sic2rUU44fP5QaR17Vk8zDt61+R9dnP9ZRsj09aAUYML4Ash05qZjQ==}
dev: false
/abort-controller/3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
@ -22083,7 +22101,7 @@ packages:
dependencies:
'@rollup/pluginutils': 5.0.1
'@svgr/core': 6.5.0
vite: 3.1.8_sass@1.55.0
vite: 3.1.8
transitivePeerDependencies:
- rollup
- supports-color