mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 12:13:27 +00:00
[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:
parent
4dc7c4571a
commit
2baf16d982
29
Cargo.lock
generated
29
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
|
@ -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)
|
||||
|
|
|
@ -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,6 +293,7 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
|
||||
let stored_key = library.key_manager.access_keystore(uuid)?;
|
||||
|
||||
if args.library_sync {
|
||||
library
|
||||
.db
|
||||
.key()
|
||||
|
@ -203,7 +301,6 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
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(),
|
||||
|
@ -213,6 +310,7 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
)
|
||||
.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())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"]
|
7776
crates/crypto/assets/eff_large_wordlist.txt
Normal file
7776
crates/crypto/assets/eff_large_wordlist.txt
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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,14 +63,16 @@ 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
|
||||
|
@ -83,8 +85,6 @@ pub struct StoredKey {
|
|||
#[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 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 {
|
||||
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,10 +211,16 @@ 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 {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
143
packages/interface/src/components/dialog/BackupRestoreDialog.tsx
Normal file
143
packages/interface/src/components/dialog/BackupRestoreDialog.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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,9 +10,86 @@ import { KeyMounter } from './KeyMounter';
|
|||
export type KeyManagerProps = DefaultProps;
|
||||
|
||||
export function KeyManager(props: KeyManagerProps) {
|
||||
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
|
||||
|
@ -17,7 +97,28 @@ export function KeyManager(props: KeyManagerProps) {
|
|||
<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>
|
||||
|
@ -28,3 +129,4 @@ export function KeyManager(props: KeyManagerProps) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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() {
|
||||
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." />
|
||||
<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];
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue