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]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.13.0"
|
version = "0.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
|
@ -2286,7 +2286,7 @@ source = "git+https://github.com/oscartbeaumont/httpz.git?rev=1ddbd9ad594ac7ee3e
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
"axum",
|
"axum",
|
||||||
"base64 0.13.0",
|
"base64 0.13.1",
|
||||||
"cookie",
|
"cookie",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures",
|
"futures",
|
||||||
|
@ -3816,7 +3816,7 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4"
|
checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64 0.13.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -4021,7 +4021,7 @@ version = "1.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225"
|
checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64 0.13.1",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"line-wrap",
|
"line-wrap",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -4114,7 +4114,7 @@ name = "prisma-client-rust"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
source = "git+https://github.com/Brendonovich/prisma-client-rust.git?tag=0.6.3#c81f22fb287a2801da5a5961ed1ec9e99f5bee34"
|
source = "git+https://github.com/Brendonovich/prisma-client-rust.git?tag=0.6.3#c81f22fb287a2801da5a5961ed1ec9e99f5bee34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64 0.13.1",
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"chrono",
|
"chrono",
|
||||||
"datamodel",
|
"datamodel",
|
||||||
|
@ -4822,7 +4822,7 @@ version = "0.11.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
|
checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64 0.13.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
@ -5022,7 +5022,7 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9"
|
checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64 0.13.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -5031,7 +5031,7 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
|
checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64 0.13.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -5144,7 +5144,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.13.0",
|
"base64 0.13.1",
|
||||||
"blake3",
|
"blake3",
|
||||||
"chrono",
|
"chrono",
|
||||||
"ctor",
|
"ctor",
|
||||||
|
@ -5208,6 +5208,7 @@ dependencies = [
|
||||||
"aead",
|
"aead",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
"base64 0.13.1",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
@ -5960,7 +5961,7 @@ name = "swift-rs"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
source = "git+https://github.com/Brendonovich/swift-rs.git?branch=autorelease#b16ba936ca2330bb27c6b9b7a84ad0d583ef0caa"
|
source = "git+https://github.com/Brendonovich/swift-rs.git?branch=autorelease#b16ba936ca2330bb27c6b9b7a84ad0d583ef0caa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64 0.13.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"swift-rs-macros",
|
"swift-rs-macros",
|
||||||
|
@ -6187,7 +6188,7 @@ version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "356fa253e40ae4d6ff02075011f2f2bb4066f5c9d8c1e16ca6912d7b75903ba6"
|
checksum = "356fa253e40ae4d6ff02075011f2f2bb4066f5c9d8c1e16ca6912d7b75903ba6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64 0.13.1",
|
||||||
"brotli",
|
"brotli",
|
||||||
"ico",
|
"ico",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
|
@ -6729,7 +6730,7 @@ version = "0.17.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
|
checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64 0.13.1",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http",
|
||||||
|
@ -7495,7 +7496,7 @@ version = "0.21.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff5c1352b4266fdf92c63479d2f58ab4cd29dc4e78fbc1b62011ed1227926945"
|
checksum = "ff5c1352b4266fdf92c63479d2f58ab4cd29dc4e78fbc1b62011ed1227926945"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64 0.13.1",
|
||||||
"block",
|
"block",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
|
|
|
@ -54,7 +54,7 @@ image = "0.24.4"
|
||||||
webp = "0.2.2"
|
webp = "0.2.2"
|
||||||
ffmpeg-next = { version = "5.1.1", optional = true, features = [] }
|
ffmpeg-next = { version = "5.1.1", optional = true, features = [] }
|
||||||
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
|
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
|
||||||
sd-crypto = { path = "../crates/crypto", features = ["rspc"] }
|
sd-crypto = { path = "../crates/crypto", features = ["rspc", "serde"] }
|
||||||
sd-file-ext = { path = "../crates/file-ext"}
|
sd-file-ext = { path = "../crates/file-ext"}
|
||||||
fs_extra = "1.2.0"
|
fs_extra = "1.2.0"
|
||||||
tracing = "0.1.36"
|
tracing = "0.1.36"
|
||||||
|
|
|
@ -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
|
algorithm Bytes
|
||||||
// hashing algorithm used for hashing the master password
|
// hashing algorithm used for hashing the master password
|
||||||
hashing_algorithm Bytes
|
hashing_algorithm Bytes
|
||||||
// salt to hash the master password with
|
|
||||||
salt Bytes
|
|
||||||
// salt used for encrypting data with this key
|
// salt used for encrypting data with this key
|
||||||
content_salt Bytes
|
content_salt Bytes
|
||||||
// the *encrypted* master key (48 bytes)
|
// the *encrypted* master key (48 bytes)
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
use std::str::FromStr;
|
use std::io::{Read, Write};
|
||||||
|
use std::{path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
use sd_crypto::{crypto::stream::Algorithm, keys::hashing::HashingAlgorithm, Protected};
|
use sd_crypto::keys::keymanager::StoredKey;
|
||||||
use serde::Deserialize;
|
use sd_crypto::{
|
||||||
|
crypto::stream::Algorithm,
|
||||||
|
keys::{hashing::HashingAlgorithm, keymanager::KeyManager},
|
||||||
|
Protected,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
|
|
||||||
use crate::{invalidate_query, prisma::key};
|
use crate::{invalidate_query, prisma::key};
|
||||||
|
@ -13,6 +19,7 @@ pub struct KeyAddArgs {
|
||||||
algorithm: Algorithm,
|
algorithm: Algorithm,
|
||||||
hashing_algorithm: HashingAlgorithm,
|
hashing_algorithm: HashingAlgorithm,
|
||||||
key: String,
|
key: String,
|
||||||
|
library_sync: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Type, Deserialize)]
|
#[derive(Type, Deserialize)]
|
||||||
|
@ -21,15 +28,65 @@ pub struct KeyNameUpdateArgs {
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Type, Deserialize)]
|
||||||
|
pub struct SetMasterPasswordArgs {
|
||||||
|
password: String,
|
||||||
|
secret_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Type, Deserialize)]
|
||||||
|
pub struct RestoreBackupArgs {
|
||||||
|
password: String,
|
||||||
|
secret_key: String,
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Type, Deserialize)]
|
||||||
|
pub struct OnboardingArgs {
|
||||||
|
algorithm: Algorithm,
|
||||||
|
hashing_algorithm: HashingAlgorithm,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Type, Deserialize)]
|
||||||
|
pub struct MasterPasswordChangeArgs {
|
||||||
|
password: String,
|
||||||
|
algorithm: Algorithm,
|
||||||
|
hashing_algorithm: HashingAlgorithm,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Type, Serialize)]
|
||||||
|
pub struct OnboardingKeys {
|
||||||
|
master_password: String,
|
||||||
|
secret_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn mount() -> RouterBuilder {
|
pub(crate) fn mount() -> RouterBuilder {
|
||||||
RouterBuilder::new()
|
RouterBuilder::new()
|
||||||
.library_query("list", |t| {
|
.library_query("list", |t| {
|
||||||
t(|_, _: (), library| async move { Ok(library.key_manager.dump_keystore()) })
|
t(|_, _: (), library| async move { Ok(library.key_manager.dump_keystore()) })
|
||||||
})
|
})
|
||||||
|
// do not unlock the key manager until this route returns true
|
||||||
|
.library_query("hasMasterPassword", |t| {
|
||||||
|
t(|_, _: (), library| async move { Ok(library.key_manager.has_master_password()?) })
|
||||||
|
})
|
||||||
// this is so we can show the key as mounted in the UI
|
// this is so we can show the key as mounted in the UI
|
||||||
.library_query("listMounted", |t| {
|
.library_query("listMounted", |t| {
|
||||||
t(|_, _: (), library| async move { Ok(library.key_manager.get_mounted_uuids()) })
|
t(|_, _: (), library| async move { Ok(library.key_manager.get_mounted_uuids()) })
|
||||||
})
|
})
|
||||||
|
.library_query("getKey", |t| {
|
||||||
|
t(|_, key_uuid: uuid::Uuid, library| async move {
|
||||||
|
let key = library.key_manager.get_key(key_uuid)?;
|
||||||
|
|
||||||
|
let key_string = String::from_utf8(key.expose().clone()).map_err(|_| {
|
||||||
|
rspc::Error::new(
|
||||||
|
rspc::ErrorCode::InternalServerError,
|
||||||
|
"Error serializing bytes to String".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(key_string)
|
||||||
|
})
|
||||||
|
})
|
||||||
.library_mutation("mount", |t| {
|
.library_mutation("mount", |t| {
|
||||||
t(|_, key_uuid: uuid::Uuid, library| async move {
|
t(|_, key_uuid: uuid::Uuid, library| async move {
|
||||||
library.key_manager.mount(key_uuid)?;
|
library.key_manager.mount(key_uuid)?;
|
||||||
|
@ -61,6 +118,14 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.library_mutation("clearMasterPassword", |t| {
|
||||||
|
t(|_, _: (), library| async move {
|
||||||
|
library.key_manager.clear_master_password()?;
|
||||||
|
|
||||||
|
invalidate_query!(library, "keys.hasMasterPassword");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
.library_mutation("deleteFromLibrary", |t| {
|
.library_mutation("deleteFromLibrary", |t| {
|
||||||
t(|_, key_uuid: uuid::Uuid, library| async move {
|
t(|_, key_uuid: uuid::Uuid, library| async move {
|
||||||
library.key_manager.remove_key(key_uuid)?;
|
library.key_manager.remove_key(key_uuid)?;
|
||||||
|
@ -79,16 +144,53 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.library_mutation("setMasterPassword", |t| {
|
.library_mutation("onboarding", |t| {
|
||||||
t(|_, password: String, library| async move {
|
t(|_, args: OnboardingArgs, library| async move {
|
||||||
// need to add master password checks in the keymanager itself to make sure it's correct
|
let bundle = KeyManager::onboarding(args.algorithm, args.hashing_algorithm)?;
|
||||||
// this can either unwrap&fail, or we can return the error. either way, the user will have to correct this
|
|
||||||
// by entering the correct password
|
let verification_key = bundle.verification_key;
|
||||||
// for now, automounting might have to serve as the master password checks
|
|
||||||
|
// remove old nil-id keys if they were set
|
||||||
|
// they possibly won't be, but we CANNOT have multiple
|
||||||
|
library
|
||||||
|
.db
|
||||||
|
.key()
|
||||||
|
.delete_many(vec![key::uuid::equals(uuid::Uuid::nil().to_string())])
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
|
||||||
library
|
library
|
||||||
.key_manager
|
.db
|
||||||
.set_master_password(Protected::new(password.as_bytes().to_vec()))?;
|
.key()
|
||||||
|
.create(
|
||||||
|
verification_key.uuid.to_string(),
|
||||||
|
verification_key.algorithm.serialize().to_vec(),
|
||||||
|
verification_key.hashing_algorithm.serialize().to_vec(),
|
||||||
|
verification_key.content_salt.to_vec(),
|
||||||
|
verification_key.master_key.to_vec(),
|
||||||
|
verification_key.master_key_nonce.to_vec(),
|
||||||
|
verification_key.key_nonce.to_vec(),
|
||||||
|
verification_key.key.to_vec(),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let keys = OnboardingKeys {
|
||||||
|
master_password: bundle.master_password.expose().clone(),
|
||||||
|
secret_key: base64::encode(bundle.secret_key.expose()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(keys)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.library_mutation("setMasterPassword", |t| {
|
||||||
|
t(|_, args: SetMasterPasswordArgs, library| async move {
|
||||||
|
// if this returns an error, the user MUST re-enter the correct password
|
||||||
|
library.key_manager.set_master_password(
|
||||||
|
Protected::new(args.password),
|
||||||
|
Protected::new(args.secret_key),
|
||||||
|
)?;
|
||||||
|
|
||||||
let automount = library
|
let automount = library
|
||||||
.db
|
.db
|
||||||
|
@ -108,6 +210,8 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||||
})?)?;
|
})?)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invalidate_query!(library, "keys.hasMasterPassword");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -161,17 +265,10 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||||
})
|
})
|
||||||
.library_query("getDefault", |t| {
|
.library_query("getDefault", |t| {
|
||||||
t(|_, _: (), library| async move {
|
t(|_, _: (), library| async move {
|
||||||
// `find_first` should be okay here as only one default key should ever be set
|
let default = library.key_manager.get_default();
|
||||||
// this is also stored in the keymanager but it's probably easier to get it from the DB
|
|
||||||
let default = library
|
|
||||||
.db
|
|
||||||
.key()
|
|
||||||
.find_first(vec![key::default::equals(true)])
|
|
||||||
.exec()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let Some(default_key) = default {
|
if let Ok(default_key) = default {
|
||||||
Ok(Some(default_key.uuid))
|
Ok(Some(default_key))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
@ -196,23 +293,24 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||||
|
|
||||||
let stored_key = library.key_manager.access_keystore(uuid)?;
|
let stored_key = library.key_manager.access_keystore(uuid)?;
|
||||||
|
|
||||||
library
|
if args.library_sync {
|
||||||
.db
|
library
|
||||||
.key()
|
.db
|
||||||
.create(
|
.key()
|
||||||
uuid.to_string(),
|
.create(
|
||||||
args.algorithm.serialize().to_vec(),
|
uuid.to_string(),
|
||||||
args.hashing_algorithm.serialize().to_vec(),
|
args.algorithm.serialize().to_vec(),
|
||||||
stored_key.salt.to_vec(),
|
args.hashing_algorithm.serialize().to_vec(),
|
||||||
stored_key.content_salt.to_vec(),
|
stored_key.content_salt.to_vec(),
|
||||||
stored_key.master_key.to_vec(),
|
stored_key.master_key.to_vec(),
|
||||||
stored_key.master_key_nonce.to_vec(),
|
stored_key.master_key_nonce.to_vec(),
|
||||||
stored_key.key_nonce.to_vec(),
|
stored_key.key_nonce.to_vec(),
|
||||||
stored_key.key.to_vec(),
|
stored_key.key.to_vec(),
|
||||||
vec![],
|
vec![],
|
||||||
)
|
)
|
||||||
.exec()
|
.exec()
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
// mount the key
|
// mount the key
|
||||||
library.key_manager.mount(uuid)?;
|
library.key_manager.mount(uuid)?;
|
||||||
|
@ -222,4 +320,146 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.library_mutation("backupKeystore", |t| {
|
||||||
|
t(|_, path: PathBuf, library| async move {
|
||||||
|
// dump all stored keys that are in the key manager (maybe these should be taken from prisma as this will include even "non-sync with library" keys)
|
||||||
|
let mut stored_keys = library.key_manager.dump_keystore();
|
||||||
|
// include the verification key at the time of backup
|
||||||
|
stored_keys.push(library.key_manager.get_verification_key()?);
|
||||||
|
|
||||||
|
let mut output_file = std::fs::File::create(path).map_err(|_| {
|
||||||
|
rspc::Error::new(
|
||||||
|
rspc::ErrorCode::InternalServerError,
|
||||||
|
"Error creating file".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
output_file
|
||||||
|
.write_all(&serde_json::to_vec(&stored_keys).map_err(|_| {
|
||||||
|
rspc::Error::new(
|
||||||
|
rspc::ErrorCode::InternalServerError,
|
||||||
|
"Error serializing keystore".into(),
|
||||||
|
)
|
||||||
|
})?)
|
||||||
|
.map_err(|_| {
|
||||||
|
rspc::Error::new(
|
||||||
|
rspc::ErrorCode::InternalServerError,
|
||||||
|
"Error writing key backup to file".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.library_mutation("restoreKeystore", |t| {
|
||||||
|
t(|_, args: RestoreBackupArgs, library| async move {
|
||||||
|
let mut input_file = std::fs::File::open(args.path).map_err(|_| {
|
||||||
|
rspc::Error::new(
|
||||||
|
rspc::ErrorCode::InternalServerError,
|
||||||
|
"Error opening backup file".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut backup = Vec::new();
|
||||||
|
|
||||||
|
input_file.read_to_end(&mut backup).map_err(|_| {
|
||||||
|
rspc::Error::new(
|
||||||
|
rspc::ErrorCode::InternalServerError,
|
||||||
|
"Error reading backup file".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let stored_keys: Vec<StoredKey> =
|
||||||
|
serde_json::from_slice(&backup).map_err(|_| {
|
||||||
|
rspc::Error::new(
|
||||||
|
rspc::ErrorCode::InternalServerError,
|
||||||
|
"Error deserializing backup".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let updated_keys = library.key_manager.import_keystore_backup(
|
||||||
|
Protected::new(args.password),
|
||||||
|
Protected::new(args.secret_key),
|
||||||
|
&stored_keys,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for key in &updated_keys {
|
||||||
|
library
|
||||||
|
.db
|
||||||
|
.key()
|
||||||
|
.create(
|
||||||
|
key.uuid.to_string(),
|
||||||
|
key.algorithm.serialize().to_vec(),
|
||||||
|
key.hashing_algorithm.serialize().to_vec(),
|
||||||
|
key.content_salt.to_vec(),
|
||||||
|
key.master_key.to_vec(),
|
||||||
|
key.master_key_nonce.to_vec(),
|
||||||
|
key.key_nonce.to_vec(),
|
||||||
|
key.key.to_vec(),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate_query!(library, "keys.list");
|
||||||
|
invalidate_query!(library, "keys.listMounted");
|
||||||
|
|
||||||
|
Ok(updated_keys.len())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.library_mutation("changeMasterPassword", |t| {
|
||||||
|
t(|_, args: MasterPasswordChangeArgs, library| async move {
|
||||||
|
let bundle = library.key_manager.change_master_password(
|
||||||
|
Protected::new(args.password),
|
||||||
|
args.algorithm,
|
||||||
|
args.hashing_algorithm,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let verification_key = bundle.verification_key;
|
||||||
|
|
||||||
|
// remove old nil-id keys if they were set
|
||||||
|
// they possibly won't be, but we CANNOT have multiple
|
||||||
|
library.db.key().delete_many(vec![]).exec().await?;
|
||||||
|
|
||||||
|
library
|
||||||
|
.db
|
||||||
|
.key()
|
||||||
|
.create(
|
||||||
|
verification_key.uuid.to_string(),
|
||||||
|
verification_key.algorithm.serialize().to_vec(),
|
||||||
|
verification_key.hashing_algorithm.serialize().to_vec(),
|
||||||
|
verification_key.content_salt.to_vec(),
|
||||||
|
verification_key.master_key.to_vec(),
|
||||||
|
verification_key.master_key_nonce.to_vec(),
|
||||||
|
verification_key.key_nonce.to_vec(),
|
||||||
|
verification_key.key.to_vec(),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// sync new changes with prisma
|
||||||
|
// note, this will write keys that were potentially marked as "don't sync to db"
|
||||||
|
// i think the way around this will be to include a marker in the `StoredKey` struct, as that means we can exclude them from `dump_keystore()` commands
|
||||||
|
for key in bundle.updated_keystore {
|
||||||
|
library
|
||||||
|
.db
|
||||||
|
.key()
|
||||||
|
.create(
|
||||||
|
key.uuid.to_string(),
|
||||||
|
key.algorithm.serialize().to_vec(),
|
||||||
|
key.hashing_algorithm.serialize().to_vec(),
|
||||||
|
key.content_salt.to_vec(),
|
||||||
|
key.master_key.to_vec(),
|
||||||
|
key.master_key_nonce.to_vec(),
|
||||||
|
key.key_nonce.to_vec(),
|
||||||
|
key.key.to_vec(),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bundle.secret_key.expose().clone())
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
invalidate_query,
|
invalidate_query,
|
||||||
node::Platform,
|
node::Platform,
|
||||||
prisma::{node, PrismaClient},
|
prisma::{key, node, PrismaClient},
|
||||||
util::{
|
util::{
|
||||||
db::load_and_migrate,
|
db::load_and_migrate,
|
||||||
seeder::{indexer_rules_seeder, SeederError},
|
seeder::{indexer_rules_seeder, SeederError},
|
||||||
|
@ -12,11 +12,10 @@ use crate::{
|
||||||
use sd_crypto::{
|
use sd_crypto::{
|
||||||
crypto::stream::Algorithm,
|
crypto::stream::Algorithm,
|
||||||
keys::{
|
keys::{
|
||||||
hashing::HashingAlgorithm,
|
hashing::{HashingAlgorithm, Params},
|
||||||
keymanager::{KeyManager, StoredKey},
|
keymanager::{KeyManager, StoredKey},
|
||||||
},
|
},
|
||||||
primitives::to_array,
|
primitives::to_array,
|
||||||
Protected,
|
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
env, fs, io,
|
env, fs, io,
|
||||||
|
@ -74,11 +73,51 @@ impl From<LibraryManagerError> for rspc::Error {
|
||||||
|
|
||||||
pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, LibraryManagerError> {
|
pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, LibraryManagerError> {
|
||||||
// retrieve all stored keys from the DB
|
// retrieve all stored keys from the DB
|
||||||
let key_manager = KeyManager::new(vec![], None);
|
let key_manager = KeyManager::new(vec![]);
|
||||||
|
|
||||||
|
// BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE
|
||||||
|
// this is so if there's no verification key set, we set one so users can use the key manager
|
||||||
|
// it will be done during onboarding, but for now things are statically set (unless they were changed)
|
||||||
|
if client
|
||||||
|
.key()
|
||||||
|
.find_many(vec![key::uuid::equals(uuid::Uuid::nil().to_string())])
|
||||||
|
.exec()
|
||||||
|
.await?
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
client
|
||||||
|
.key()
|
||||||
|
.delete_many(vec![key::uuid::equals(uuid::Uuid::nil().to_string())])
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
// BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE
|
||||||
|
let verification_key = KeyManager::onboarding(
|
||||||
|
Algorithm::XChaCha20Poly1305,
|
||||||
|
HashingAlgorithm::Argon2id(Params::Standard),
|
||||||
|
)?
|
||||||
|
.verification_key;
|
||||||
|
|
||||||
|
// BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE
|
||||||
|
client
|
||||||
|
.key()
|
||||||
|
.create(
|
||||||
|
verification_key.uuid.to_string(),
|
||||||
|
verification_key.algorithm.serialize().to_vec(),
|
||||||
|
verification_key.hashing_algorithm.serialize().to_vec(),
|
||||||
|
verification_key.content_salt.to_vec(),
|
||||||
|
verification_key.master_key.to_vec(),
|
||||||
|
verification_key.master_key_nonce.to_vec(),
|
||||||
|
verification_key.key_nonce.to_vec(),
|
||||||
|
verification_key.key.to_vec(),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
let db_stored_keys = client.key().find_many(vec![]).exec().await?;
|
let db_stored_keys = client.key().find_many(vec![]).exec().await?;
|
||||||
|
|
||||||
let mut default = Uuid::default();
|
let mut default = Uuid::nil();
|
||||||
|
|
||||||
// collect and serialize the stored keys
|
// collect and serialize the stored keys
|
||||||
// shouldn't call unwrap so much here
|
// shouldn't call unwrap so much here
|
||||||
|
@ -95,7 +134,6 @@ pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, Libr
|
||||||
|
|
||||||
StoredKey {
|
StoredKey {
|
||||||
uuid,
|
uuid,
|
||||||
salt: to_array(key.salt).unwrap(),
|
|
||||||
algorithm: Algorithm::deserialize(to_array(key.algorithm).unwrap()).unwrap(),
|
algorithm: Algorithm::deserialize(to_array(key.algorithm).unwrap()).unwrap(),
|
||||||
content_salt: to_array(key.content_salt).unwrap(),
|
content_salt: to_array(key.content_salt).unwrap(),
|
||||||
master_key: to_array(key.master_key).unwrap(),
|
master_key: to_array(key.master_key).unwrap(),
|
||||||
|
@ -118,9 +156,6 @@ pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, Libr
|
||||||
key_manager.set_default(default)?;
|
key_manager.set_default(default)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
////!!!! THIS IS FOR TESTING ONLY, REMOVE IT ONCE WE HAVE THE UI IN PLACE
|
|
||||||
key_manager.set_master_password(Protected::new(b"password".to_vec()))?;
|
|
||||||
|
|
||||||
Ok(key_manager)
|
Ok(key_manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,16 +27,19 @@ zeroize = "1.5.7"
|
||||||
thiserror = "1.0.37"
|
thiserror = "1.0.37"
|
||||||
|
|
||||||
# metadata de/serialization
|
# metadata de/serialization
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||||
serde_json = "1.0"
|
serde_json = { version = "1.0", optional = true }
|
||||||
serde-big-array = "0.4.1"
|
serde-big-array = { version = "0.4.1", optional = true }
|
||||||
|
|
||||||
uuid = { version = "1.1.2", features = ["v4", "serde"] }
|
uuid = { version = "1.1.2", features = ["v4"] }
|
||||||
|
|
||||||
dashmap = "5.4.0"
|
dashmap = "5.4.0"
|
||||||
|
|
||||||
rspc = { workspace = true, optional = true }
|
rspc = { workspace = true, optional = true }
|
||||||
specta = { workspace = true }
|
specta = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
base64 = "0.13.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
rpsc = ["rspc"]
|
rspc = ["dep:rspc", "dep:specta"]
|
||||||
|
serde = ["dep:serde", "dep:serde_json", "dep:serde-big-array", "uuid/serde"]
|
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::{
|
use sd_crypto::{
|
||||||
crypto::stream::{Algorithm, StreamEncryption},
|
crypto::stream::{Algorithm, StreamEncryption},
|
||||||
|
@ -11,12 +11,11 @@ use sd_crypto::{
|
||||||
primitives::{generate_master_key, generate_salt},
|
primitives::{generate_master_key, generate_salt},
|
||||||
Protected,
|
Protected,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use std::fs::File;
|
||||||
|
|
||||||
const ALGORITHM: Algorithm = Algorithm::XChaCha20Poly1305;
|
const ALGORITHM: Algorithm = Algorithm::XChaCha20Poly1305;
|
||||||
const HASHING_ALGORITHM: HashingAlgorithm = HashingAlgorithm::Argon2id(Params::Standard);
|
const HASHING_ALGORITHM: HashingAlgorithm = HashingAlgorithm::Argon2id(Params::Standard);
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
pub struct FileInformation {
|
pub struct FileInformation {
|
||||||
pub file_name: String,
|
pub file_name: String,
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,18 @@ use aead::{
|
||||||
};
|
};
|
||||||
use aes_gcm::Aes256Gcm;
|
use aes_gcm::Aes256Gcm;
|
||||||
use chacha20poly1305::XChaCha20Poly1305;
|
use chacha20poly1305::XChaCha20Poly1305;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use specta::Type;
|
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
use crate::{primitives::BLOCK_SIZE, Error, Protected, Result};
|
use crate::{primitives::BLOCK_SIZE, Error, Protected, Result};
|
||||||
|
|
||||||
/// These are all possible algorithms that can be used for encryption and decryption
|
/// These are all possible algorithms that can be used for encryption and decryption
|
||||||
#[derive(Clone, Copy, Eq, PartialEq, Type, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serde",
|
||||||
|
derive(serde::Serialize),
|
||||||
|
derive(serde::Deserialize)
|
||||||
|
)]
|
||||||
|
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||||
#[allow(clippy::use_self)]
|
#[allow(clippy::use_self)]
|
||||||
pub enum Algorithm {
|
pub enum Algorithm {
|
||||||
XChaCha20Poly1305,
|
XChaCha20Poly1305,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
//! This module contains all possible errors that this crate can return.
|
//! This module contains all possible errors that this crate can return.
|
||||||
|
|
||||||
|
use std::string::FromUtf8Error;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[cfg(feature = "rspc")]
|
#[cfg(feature = "rspc")]
|
||||||
|
@ -48,6 +50,10 @@ pub enum Error {
|
||||||
TooManyKeyslots,
|
TooManyKeyslots,
|
||||||
#[error("requested key wasn't found in the key manager")]
|
#[error("requested key wasn't found in the key manager")]
|
||||||
KeyNotFound,
|
KeyNotFound,
|
||||||
|
#[error("key is already mounted")]
|
||||||
|
KeyAlreadyMounted,
|
||||||
|
#[error("key not mounted")]
|
||||||
|
KeyNotMounted,
|
||||||
#[error("no default key has been set")]
|
#[error("no default key has been set")]
|
||||||
NoDefaultKeySet,
|
NoDefaultKeySet,
|
||||||
#[error("no master password has been provided to the keymanager")]
|
#[error("no master password has been provided to the keymanager")]
|
||||||
|
@ -56,6 +62,12 @@ pub enum Error {
|
||||||
KeystoreMismatch,
|
KeystoreMismatch,
|
||||||
#[error("mutex lock error")]
|
#[error("mutex lock error")]
|
||||||
MutexLock,
|
MutexLock,
|
||||||
|
#[error("no master password verification key")]
|
||||||
|
NoVerificationKey,
|
||||||
|
#[error("wrong information provided to the key manager")]
|
||||||
|
IncorrectKeymanagerDetails,
|
||||||
|
#[error("string parse error")]
|
||||||
|
StringParse(#[from] FromUtf8Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<std::sync::PoisonError<T>> for Error {
|
impl<T> From<std::sync::PoisonError<T>> for Error {
|
||||||
|
|
|
@ -29,12 +29,15 @@
|
||||||
//! ```
|
//! ```
|
||||||
use std::io::{Read, Seek};
|
use std::io::{Read, Seek};
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
use crate::{
|
use crate::{
|
||||||
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
|
crypto::stream::{StreamDecryption, StreamEncryption},
|
||||||
primitives::{generate_nonce, MASTER_KEY_LEN},
|
primitives::{generate_nonce, MASTER_KEY_LEN},
|
||||||
Error, Protected, Result,
|
Protected,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::{crypto::stream::Algorithm, Error, Result};
|
||||||
|
|
||||||
use super::file::FileHeader;
|
use super::file::FileHeader;
|
||||||
|
|
||||||
/// This is a metadata header item. You may add it to a header, and this will be stored with the file.
|
/// This is a metadata header item. You may add it to a header, and this will be stored with the file.
|
||||||
|
@ -63,6 +66,7 @@ impl FileHeader {
|
||||||
/// You will need to provide the user's password, and a semi-universal salt for hashing the user's password. This allows for extremely fast decryption.
|
/// You will need to provide the user's password, and a semi-universal salt for hashing the user's password. This allows for extremely fast decryption.
|
||||||
///
|
///
|
||||||
/// Metadata needs to be accessed switfly, so a key management system should handle the salt generation.
|
/// Metadata needs to be accessed switfly, so a key management system should handle the salt generation.
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
pub fn add_metadata<T>(
|
pub fn add_metadata<T>(
|
||||||
&mut self,
|
&mut self,
|
||||||
version: MetadataVersion,
|
version: MetadataVersion,
|
||||||
|
@ -100,6 +104,7 @@ impl FileHeader {
|
||||||
/// All it requires is pre-hashed keys returned from the key manager
|
/// All it requires is pre-hashed keys returned from the key manager
|
||||||
///
|
///
|
||||||
/// A deserialized data type will be returned from this function
|
/// A deserialized data type will be returned from this function
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
pub fn decrypt_metadata_from_prehashed<T>(
|
pub fn decrypt_metadata_from_prehashed<T>(
|
||||||
&self,
|
&self,
|
||||||
hashed_keys: Vec<Protected<[u8; 32]>>,
|
hashed_keys: Vec<Protected<[u8; 32]>>,
|
||||||
|
@ -130,6 +135,7 @@ impl FileHeader {
|
||||||
/// All it requires is a password. Hashing is handled for you.
|
/// All it requires is a password. Hashing is handled for you.
|
||||||
///
|
///
|
||||||
/// A deserialized data type will be returned from this function
|
/// A deserialized data type will be returned from this function
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
pub fn decrypt_metadata<T>(&self, password: Protected<Vec<u8>>) -> Result<T>
|
pub fn decrypt_metadata<T>(&self, password: Protected<Vec<u8>>) -> Result<T>
|
||||||
where
|
where
|
||||||
T: serde::de::DeserializeOwned,
|
T: serde::de::DeserializeOwned,
|
||||||
|
|
|
@ -13,13 +13,17 @@
|
||||||
use crate::Protected;
|
use crate::Protected;
|
||||||
use crate::{primitives::SALT_LEN, Error, Result};
|
use crate::{primitives::SALT_LEN, Error, Result};
|
||||||
use argon2::Argon2;
|
use argon2::Argon2;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use specta::Type;
|
|
||||||
|
|
||||||
/// These parameters define the password-hashing level.
|
/// These parameters define the password-hashing level.
|
||||||
///
|
///
|
||||||
/// The harder the parameter, the longer the password will take to hash.
|
/// The harder the parameter, the longer the password will take to hash.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Type, Serialize, Deserialize)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serde",
|
||||||
|
derive(serde::Serialize),
|
||||||
|
derive(serde::Deserialize)
|
||||||
|
)]
|
||||||
|
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||||
#[allow(clippy::use_self)]
|
#[allow(clippy::use_self)]
|
||||||
pub enum Params {
|
pub enum Params {
|
||||||
Standard,
|
Standard,
|
||||||
|
@ -28,7 +32,13 @@ pub enum Params {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This defines all available password hashing algorithms.
|
/// This defines all available password hashing algorithms.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Type, Serialize, Deserialize)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serde",
|
||||||
|
derive(serde::Serialize),
|
||||||
|
derive(serde::Deserialize)
|
||||||
|
)]
|
||||||
|
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||||
pub enum HashingAlgorithm {
|
pub enum HashingAlgorithm {
|
||||||
Argon2id(Params),
|
Argon2id(Params),
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::crypto::stream::{StreamDecryption, StreamEncryption};
|
use crate::crypto::stream::{StreamDecryption, StreamEncryption};
|
||||||
use crate::primitives::{
|
use crate::primitives::{
|
||||||
generate_master_key, generate_nonce, generate_salt, to_array, MASTER_KEY_LEN,
|
generate_master_key, generate_nonce, generate_passphrase, generate_salt, to_array,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
crypto::stream::Algorithm,
|
crypto::stream::Algorithm,
|
||||||
|
@ -49,11 +49,11 @@ use crate::{
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use serde::Serialize;
|
|
||||||
use serde_big_array::BigArray;
|
|
||||||
use specta::Type;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde_big_array::BigArray;
|
||||||
|
|
||||||
use super::hashing::HashingAlgorithm;
|
use super::hashing::HashingAlgorithm;
|
||||||
|
|
||||||
// The terminology in this file is very confusing.
|
// The terminology in this file is very confusing.
|
||||||
|
@ -63,17 +63,19 @@ use super::hashing::HashingAlgorithm;
|
||||||
// The `hashed_key` refers to the value you'd pass to PVM/MD decryption functions. It has been pre-hashed with the content salt.
|
// The `hashed_key` refers to the value you'd pass to PVM/MD decryption functions. It has been pre-hashed with the content salt.
|
||||||
// The content salt refers to the semi-universal salt that's used for metadata/preview media (unique to each key in the manager)
|
// The content salt refers to the semi-universal salt that's used for metadata/preview media (unique to each key in the manager)
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Type, Serialize)]
|
/// This is a stored key, and can be freely written to Prisma/another database.
|
||||||
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||||
pub struct StoredKey {
|
pub struct StoredKey {
|
||||||
pub uuid: uuid::Uuid, // uuid for identification. shared with mounted keys
|
pub uuid: uuid::Uuid, // uuid for identification. shared with mounted keys
|
||||||
pub algorithm: Algorithm, // encryption algorithm for encrypting the master key. can be changed (requires a re-encryption though)
|
pub algorithm: Algorithm, // encryption algorithm for encrypting the master key. can be changed (requires a re-encryption though)
|
||||||
pub hashing_algorithm: HashingAlgorithm, // hashing algorithm to use for hashing everything related to this key. can't be changed once set.
|
pub hashing_algorithm: HashingAlgorithm, // hashing algorithm used for hashing the key with the content salt
|
||||||
pub salt: [u8; SALT_LEN], // salt to hash the master password with
|
pub content_salt: [u8; SALT_LEN],
|
||||||
pub content_salt: [u8; SALT_LEN], // salt used for file data
|
#[cfg_attr(feature = "serde", serde(with = "BigArray"))] // salt used for file data
|
||||||
#[serde(with = "BigArray")]
|
|
||||||
pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], // this is for encrypting the `key`
|
pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], // this is for encrypting the `key`
|
||||||
pub master_key_nonce: Vec<u8>, // nonce for encrypting the master key
|
pub master_key_nonce: Vec<u8>, // nonce for encrypting the master key
|
||||||
pub key_nonce: Vec<u8>, // nonce used for encrypting the main key
|
pub key_nonce: Vec<u8>, // nonce used for encrypting the main key
|
||||||
pub key: Vec<u8>, // encrypted. the key stored in spacedrive (e.g. generated 64 char key)
|
pub key: Vec<u8>, // encrypted. the key stored in spacedrive (e.g. generated 64 char key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,9 +84,7 @@ pub struct StoredKey {
|
||||||
/// This contains the plaintext key, and the same key hashed with the content salt.
|
/// This contains the plaintext key, and the same key hashed with the content salt.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MountedKey {
|
pub struct MountedKey {
|
||||||
pub uuid: Uuid, // used for identification. shared with stored keys
|
pub uuid: Uuid, // used for identification. shared with stored keys
|
||||||
pub key: Protected<Vec<u8>>, // the actual key itself, text format encodable (so it can be viewed with an UI)
|
|
||||||
pub content_salt: [u8; SALT_LEN], // the salt used for file data
|
|
||||||
pub hashed_key: Protected<[u8; 32]>, // this is hashed with the content salt, for instant access
|
pub hashed_key: Protected<[u8; 32]>, // this is hashed with the content salt, for instant access
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,26 +94,114 @@ pub struct MountedKey {
|
||||||
///
|
///
|
||||||
/// Use the associated functions to interact with it.
|
/// Use the associated functions to interact with it.
|
||||||
pub struct KeyManager {
|
pub struct KeyManager {
|
||||||
master_password: Mutex<Option<Protected<Vec<u8>>>>, // the user's. we take ownership here to prevent other functions attempting to manage/pass it to us
|
master_password: Mutex<Option<Protected<[u8; 32]>>>, // the *hashed* master password+secret key combo
|
||||||
|
verification_key: Mutex<Option<StoredKey>>,
|
||||||
keystore: DashMap<Uuid, StoredKey>,
|
keystore: DashMap<Uuid, StoredKey>,
|
||||||
keymount: DashMap<Uuid, MountedKey>,
|
keymount: DashMap<Uuid, MountedKey>,
|
||||||
default: Mutex<Option<Uuid>>,
|
default: Mutex<Option<Uuid>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bundle returned during onboarding
|
||||||
|
// nil key should be stored within prisma
|
||||||
|
// secret key should be written down by the user (along with the master password)
|
||||||
|
/// This bundle is returned during onboarding.
|
||||||
|
///
|
||||||
|
/// The verification key should be written to the database, and only one nil-UUID key should exist at any given point for a library.
|
||||||
|
///
|
||||||
|
/// The secret key needs to be given to the user, and should be written down.
|
||||||
|
pub struct OnboardingBundle {
|
||||||
|
pub verification_key: StoredKey, // nil UUID key that is only ever used for verifying the master password is correct
|
||||||
|
pub master_password: Protected<String>,
|
||||||
|
pub secret_key: Protected<String>, // base64 encoded string that is required along with the master password
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MasterPasswordChangeBundle {
|
||||||
|
pub verification_key: StoredKey, // nil UUID key that is only ever used for verifying the master password is correct
|
||||||
|
pub secret_key: Protected<String>, // base64 encoded string that is required along with the master password
|
||||||
|
pub updated_keystore: Vec<StoredKey>,
|
||||||
|
}
|
||||||
|
|
||||||
/// The `KeyManager` functions should be used for all key-related management.
|
/// The `KeyManager` functions should be used for all key-related management.
|
||||||
impl KeyManager {
|
impl KeyManager {
|
||||||
/// Initialize the Key Manager with the user's master password, and `StoredKeys` retrieved from Prisma
|
/// This should be used to generate everything for the user during onboarding.
|
||||||
|
///
|
||||||
|
/// This will create a master password (a 7-word diceware passphrase), and a secret key (16 bytes, encoded in base64)
|
||||||
|
///
|
||||||
|
/// It will also generate a verification key, which should be written to the database.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub fn onboarding(
|
||||||
|
algorithm: Algorithm,
|
||||||
|
hashing_algorithm: HashingAlgorithm,
|
||||||
|
) -> Result<OnboardingBundle> {
|
||||||
|
let _master_password = generate_passphrase();
|
||||||
|
let _salt = generate_salt();
|
||||||
|
|
||||||
|
// BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE
|
||||||
|
let master_password = Protected::new("password".to_string());
|
||||||
|
let salt = *b"0000000000000000";
|
||||||
|
|
||||||
|
// Hash the master password
|
||||||
|
let hashed_password = hashing_algorithm.hash(
|
||||||
|
Protected::new(master_password.expose().as_bytes().to_vec()),
|
||||||
|
salt,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let uuid = uuid::Uuid::nil();
|
||||||
|
|
||||||
|
// Generate items we'll need for encryption
|
||||||
|
let master_key = generate_master_key();
|
||||||
|
let master_key_nonce = generate_nonce(algorithm);
|
||||||
|
|
||||||
|
// Encrypt the master key with the hashed master password
|
||||||
|
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
|
||||||
|
hashed_password,
|
||||||
|
&master_key_nonce,
|
||||||
|
algorithm,
|
||||||
|
master_key.expose(),
|
||||||
|
&[],
|
||||||
|
)?)?;
|
||||||
|
|
||||||
|
let verification_key = StoredKey {
|
||||||
|
uuid,
|
||||||
|
algorithm,
|
||||||
|
hashing_algorithm,
|
||||||
|
content_salt: [0u8; 16],
|
||||||
|
master_key: encrypted_master_key,
|
||||||
|
master_key_nonce,
|
||||||
|
key_nonce: Vec::new(),
|
||||||
|
key: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret_key = Protected::new(base64::encode(salt));
|
||||||
|
|
||||||
|
let onboarding_bundle = OnboardingBundle {
|
||||||
|
verification_key,
|
||||||
|
master_password,
|
||||||
|
secret_key,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(onboarding_bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the Key Manager with `StoredKeys` retrieved from Prisma
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(stored_keys: Vec<StoredKey>, master_password: Option<Protected<Vec<u8>>>) -> Self {
|
pub fn new(stored_keys: Vec<StoredKey>) -> Self {
|
||||||
let keystore = DashMap::new();
|
let keystore = DashMap::new();
|
||||||
|
let mut verification_key = None;
|
||||||
|
|
||||||
for key in stored_keys {
|
for key in stored_keys {
|
||||||
keystore.insert(key.uuid, key);
|
if key.uuid.is_nil() {
|
||||||
|
verification_key = Some(key);
|
||||||
|
} else {
|
||||||
|
keystore.insert(key.uuid, key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let keymount: DashMap<Uuid, MountedKey> = DashMap::new();
|
let keymount: DashMap<Uuid, MountedKey> = DashMap::new();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
master_password: Mutex::new(master_password),
|
master_password: Mutex::new(None),
|
||||||
|
verification_key: Mutex::new(verification_key),
|
||||||
keystore,
|
keystore,
|
||||||
keymount,
|
keymount,
|
||||||
default: Mutex::new(None),
|
default: Mutex::new(None),
|
||||||
|
@ -123,9 +211,15 @@ impl KeyManager {
|
||||||
/// This function should be used to populate the keystore with multiple stored keys at a time.
|
/// This function should be used to populate the keystore with multiple stored keys at a time.
|
||||||
///
|
///
|
||||||
/// It's suitable for when you created the key manager without populating it.
|
/// It's suitable for when you created the key manager without populating it.
|
||||||
|
///
|
||||||
|
/// This also detects the nil-UUID master passphrase verification key
|
||||||
pub fn populate_keystore(&self, stored_keys: Vec<StoredKey>) -> Result<()> {
|
pub fn populate_keystore(&self, stored_keys: Vec<StoredKey>) -> Result<()> {
|
||||||
for key in stored_keys {
|
for key in stored_keys {
|
||||||
self.keystore.insert(key.uuid, key);
|
if key.uuid.is_nil() {
|
||||||
|
*self.verification_key.lock()? = Some(key);
|
||||||
|
} else {
|
||||||
|
self.keystore.insert(key.uuid, key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -174,6 +268,7 @@ impl KeyManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This allows you to clear the default key
|
||||||
pub fn clear_default(&self) -> Result<()> {
|
pub fn clear_default(&self) -> Result<()> {
|
||||||
let mut default = self.default.lock()?;
|
let mut default = self.default.lock()?;
|
||||||
|
|
||||||
|
@ -186,19 +281,277 @@ impl KeyManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This should ONLY be used internally.
|
/// This should ONLY be used internally.
|
||||||
fn get_master_password(&self) -> Result<Protected<Vec<u8>>> {
|
fn get_master_password(&self) -> Result<Protected<[u8; 32]>> {
|
||||||
let master_password = self.master_password.lock()?;
|
match &*self.master_password.lock()? {
|
||||||
match &*master_password {
|
|
||||||
Some(k) => Ok(k.clone()),
|
Some(k) => Ok(k.clone()),
|
||||||
None => Err(Error::NoMasterPassword),
|
None => Err(Error::NoMasterPassword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_master_password(&self, master_password: Protected<Vec<u8>>) -> Result<()> {
|
/// This should ONLY be used internally.
|
||||||
// this returns a result, so we can potentially implement password checking functionality
|
pub fn get_verification_key(&self) -> Result<StoredKey> {
|
||||||
*self.master_password.lock()? = Some(master_password);
|
match &*self.verification_key.lock()? {
|
||||||
|
Some(k) => Ok(k.clone()),
|
||||||
|
None => Err(Error::NoMasterPassword),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
/// This is used to change a master password.
|
||||||
|
///
|
||||||
|
/// The entire keystore is re-encrypted with the new master password, and will require dumping and syncing with Prisma.
|
||||||
|
pub fn change_master_password(
|
||||||
|
&self,
|
||||||
|
master_password: Protected<String>,
|
||||||
|
algorithm: Algorithm,
|
||||||
|
hashing_algorithm: HashingAlgorithm,
|
||||||
|
) -> Result<MasterPasswordChangeBundle> {
|
||||||
|
// Generate a new secret key
|
||||||
|
let salt = generate_salt();
|
||||||
|
|
||||||
|
// Hash the master password
|
||||||
|
let hashed_password = hashing_algorithm.hash(
|
||||||
|
Protected::new(master_password.expose().as_bytes().to_vec()),
|
||||||
|
salt,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Iterate over the keystore - decrypt each master key, re-encrypt it with the same algorithm, and collect them into a vec
|
||||||
|
let updated_keystore: Result<Vec<StoredKey>> = self
|
||||||
|
.dump_keystore()
|
||||||
|
.iter()
|
||||||
|
.map(|stored_key| {
|
||||||
|
let mut stored_key = stored_key.clone();
|
||||||
|
|
||||||
|
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes(
|
||||||
|
self.get_master_password()?,
|
||||||
|
&stored_key.master_key_nonce,
|
||||||
|
stored_key.algorithm,
|
||||||
|
&stored_key.master_key,
|
||||||
|
&[],
|
||||||
|
) {
|
||||||
|
Ok(Protected::new(to_array::<32>(
|
||||||
|
decrypted_master_key.expose().clone(),
|
||||||
|
)?))
|
||||||
|
} else {
|
||||||
|
Err(Error::IncorrectPassword)
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let master_key_nonce = generate_nonce(algorithm);
|
||||||
|
|
||||||
|
// Encrypt the master key with the user's hashed password
|
||||||
|
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
|
||||||
|
hashed_password.clone(),
|
||||||
|
&master_key_nonce,
|
||||||
|
stored_key.algorithm,
|
||||||
|
master_key.expose(),
|
||||||
|
&[],
|
||||||
|
)?)?;
|
||||||
|
|
||||||
|
stored_key.master_key = encrypted_master_key;
|
||||||
|
stored_key.master_key_nonce = master_key_nonce;
|
||||||
|
|
||||||
|
Ok(stored_key)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// should use ? above
|
||||||
|
let updated_keystore = updated_keystore?;
|
||||||
|
|
||||||
|
// Clear the current keystore and update it with our re-encrypted keystore
|
||||||
|
self.empty_keystore();
|
||||||
|
self.populate_keystore(updated_keystore.clone())?;
|
||||||
|
|
||||||
|
// Create a new verification key for the master password/secret key combination
|
||||||
|
let uuid = uuid::Uuid::nil();
|
||||||
|
let master_key = generate_master_key();
|
||||||
|
let master_key_nonce = generate_nonce(algorithm);
|
||||||
|
|
||||||
|
// Encrypt the master key with the hashed master password
|
||||||
|
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
|
||||||
|
hashed_password,
|
||||||
|
&master_key_nonce,
|
||||||
|
algorithm,
|
||||||
|
master_key.expose(),
|
||||||
|
&[],
|
||||||
|
)?)?;
|
||||||
|
|
||||||
|
let verification_key = StoredKey {
|
||||||
|
uuid,
|
||||||
|
algorithm,
|
||||||
|
hashing_algorithm,
|
||||||
|
content_salt: [0u8; 16],
|
||||||
|
master_key: encrypted_master_key,
|
||||||
|
master_key_nonce,
|
||||||
|
key_nonce: Vec::new(),
|
||||||
|
key: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret_key = Protected::new(base64::encode(salt));
|
||||||
|
|
||||||
|
let mpc_bundle = MasterPasswordChangeBundle {
|
||||||
|
verification_key,
|
||||||
|
secret_key,
|
||||||
|
updated_keystore,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the internal verification key, and then set the master password
|
||||||
|
*self.verification_key.lock()? = Some(mpc_bundle.verification_key.clone());
|
||||||
|
self.set_master_password(master_password, mpc_bundle.secret_key.clone())?;
|
||||||
|
|
||||||
|
// Return the verification key so it can be written to Prisma and return the secret key so it can be shown to the user
|
||||||
|
Ok(mpc_bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used internally to convert a `Protected<String>` to a `Protected<Vec<u8>>`
|
||||||
|
#[allow(clippy::unused_self)]
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
fn convert_master_password_string(
|
||||||
|
&self,
|
||||||
|
master_password: Protected<String>,
|
||||||
|
) -> Protected<Vec<u8>> {
|
||||||
|
Protected::new(master_password.expose().as_bytes().to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used internally to convert from a base64-encoded `Protected<String>` to a `Protected<[u8; SALT_LEN]>` in a secretive manner.
|
||||||
|
///
|
||||||
|
/// If the secret key is wrong (not base64 or not the correct length), a filler secret key will be inserted secretly.
|
||||||
|
#[allow(clippy::unused_self)]
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
fn convert_secret_key_string(
|
||||||
|
&self,
|
||||||
|
secret_key: Protected<String>,
|
||||||
|
) -> Protected<[u8; SALT_LEN]> {
|
||||||
|
let secret_key = if let Ok(secret_key) = base64::decode(secret_key.expose()) {
|
||||||
|
secret_key
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// we shouldn't be letting on to *what* failed so we use a random secret key here if it's still invalid
|
||||||
|
// could maybe do this better (and make use of the subtle crate)
|
||||||
|
if let Ok(secret_key) = to_array(secret_key) {
|
||||||
|
Protected::new(secret_key)
|
||||||
|
} else {
|
||||||
|
Protected::new(generate_salt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opting to leave ser/de to external functions - the key manager isn't the right place to handle this.
|
||||||
|
/// This re-encrypts master keys so they can be imported from a key backup into the current key manager.
|
||||||
|
///
|
||||||
|
/// It returns a `Vec<StoredKey>` so they can be written to Prisma
|
||||||
|
pub fn import_keystore_backup(
|
||||||
|
&self,
|
||||||
|
master_password: Protected<String>, // at the time of the backup
|
||||||
|
secret_key: Protected<String>, // at the time of the backup
|
||||||
|
stored_keys: &[StoredKey], // from the backup
|
||||||
|
) -> Result<Vec<StoredKey>> {
|
||||||
|
// this backup should contain a verification key, which will tell us the algorithm+hashing algorithm
|
||||||
|
let master_password = self.convert_master_password_string(master_password);
|
||||||
|
let secret_key = self.convert_secret_key_string(secret_key);
|
||||||
|
|
||||||
|
let mut verification_key = None;
|
||||||
|
|
||||||
|
let keys: Vec<StoredKey> = stored_keys
|
||||||
|
.iter()
|
||||||
|
.filter_map(|key| {
|
||||||
|
if key.uuid.is_nil() {
|
||||||
|
verification_key = Some(key.clone());
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(key.clone())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let hashed_master_password = if let Some(verification_key) = verification_key {
|
||||||
|
verification_key
|
||||||
|
.hashing_algorithm
|
||||||
|
.hash(master_password, *secret_key.expose())
|
||||||
|
} else {
|
||||||
|
Err(Error::NoVerificationKey)
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let mut reencrypted_keys = Vec::new();
|
||||||
|
|
||||||
|
for key in keys {
|
||||||
|
if !self.keystore.contains_key(&key.uuid) {
|
||||||
|
// could check the key material itself? if they match, attach the content salt
|
||||||
|
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes(
|
||||||
|
hashed_master_password.clone(),
|
||||||
|
&key.master_key_nonce,
|
||||||
|
key.algorithm,
|
||||||
|
&key.master_key,
|
||||||
|
&[],
|
||||||
|
) {
|
||||||
|
Ok(Protected::new(to_array::<32>(
|
||||||
|
decrypted_master_key.expose().clone(),
|
||||||
|
)?))
|
||||||
|
} else {
|
||||||
|
Err(Error::IncorrectPassword)
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let master_key_nonce = generate_nonce(key.algorithm);
|
||||||
|
|
||||||
|
// Encrypt the master key with the user's hashed password
|
||||||
|
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
|
||||||
|
self.get_master_password()?,
|
||||||
|
&master_key_nonce,
|
||||||
|
key.algorithm,
|
||||||
|
master_key.expose(),
|
||||||
|
&[],
|
||||||
|
)?)?;
|
||||||
|
|
||||||
|
let mut updated_key = key.clone();
|
||||||
|
updated_key.master_key_nonce = master_key_nonce;
|
||||||
|
updated_key.master_key = encrypted_master_key;
|
||||||
|
|
||||||
|
reencrypted_keys.push(updated_key.clone());
|
||||||
|
self.keystore.insert(updated_key.uuid, updated_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(reencrypted_keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// requires master password and the secret key
|
||||||
|
/// This requires both the master password and the secret key
|
||||||
|
///
|
||||||
|
/// The master password and secret key are hashed together.
|
||||||
|
/// This minimises the risk of an attacker obtaining the master password, as both of these are required to unlock the vault (and both should be stored separately).
|
||||||
|
///
|
||||||
|
/// Both values need to be correct, otherwise this function will return a generic error.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub fn set_master_password(
|
||||||
|
&self,
|
||||||
|
master_password: Protected<String>,
|
||||||
|
secret_key: Protected<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let verification_key = match &*self.verification_key.lock()? {
|
||||||
|
Some(k) => Ok(k.clone()),
|
||||||
|
None => Err(Error::NoVerificationKey),
|
||||||
|
}?;
|
||||||
|
let master_password = self.convert_master_password_string(master_password);
|
||||||
|
let secret_key = self.convert_secret_key_string(secret_key);
|
||||||
|
|
||||||
|
let hashed_master_password = verification_key
|
||||||
|
.hashing_algorithm
|
||||||
|
.hash(master_password, *secret_key.expose())?;
|
||||||
|
|
||||||
|
// Decrypt the StoredKey's master key using the user's hashed password
|
||||||
|
let decryption_result = StreamDecryption::decrypt_bytes(
|
||||||
|
hashed_master_password.clone(),
|
||||||
|
&verification_key.master_key_nonce,
|
||||||
|
verification_key.algorithm,
|
||||||
|
&verification_key.master_key,
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
|
||||||
|
if decryption_result.is_ok() {
|
||||||
|
*self.master_password.lock()? = Some(hashed_master_password);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::IncorrectKeymanagerDetails)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This function is for removing a previously-added master password
|
/// This function is for removing a previously-added master password
|
||||||
|
@ -208,6 +561,7 @@ impl KeyManager {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This function is used for seeing if the key manager has a master password.
|
||||||
pub fn has_master_password(&self) -> Result<bool> {
|
pub fn has_master_password(&self) -> Result<bool> {
|
||||||
Ok(self.master_password.lock()?.is_some())
|
Ok(self.master_password.lock()?.is_some())
|
||||||
}
|
}
|
||||||
|
@ -253,7 +607,7 @@ impl KeyManager {
|
||||||
self.keymount.remove(&uuid);
|
self.keymount.remove(&uuid);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(Error::KeyNotFound)
|
Err(Error::KeyNotMounted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,26 +632,23 @@ impl KeyManager {
|
||||||
///
|
///
|
||||||
/// We could add a log to this, so that the user can view mounts
|
/// We could add a log to this, so that the user can view mounts
|
||||||
pub fn mount(&self, uuid: Uuid) -> Result<()> {
|
pub fn mount(&self, uuid: Uuid) -> Result<()> {
|
||||||
|
if self.keymount.get(&uuid).is_some() {
|
||||||
|
return Err(Error::KeyAlreadyMounted);
|
||||||
|
}
|
||||||
|
|
||||||
match self.keystore.get(&uuid) {
|
match self.keystore.get(&uuid) {
|
||||||
Some(stored_key) => {
|
Some(stored_key) => {
|
||||||
let master_password = self.get_master_password()?;
|
|
||||||
|
|
||||||
let hashed_password = stored_key
|
|
||||||
.hashing_algorithm
|
|
||||||
.hash(master_password, stored_key.salt)?;
|
|
||||||
|
|
||||||
let mut master_key = [0u8; MASTER_KEY_LEN];
|
|
||||||
|
|
||||||
// Decrypt the StoredKey's master key using the user's hashed password
|
// Decrypt the StoredKey's master key using the user's hashed password
|
||||||
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes(
|
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes(
|
||||||
hashed_password,
|
self.get_master_password()?,
|
||||||
&stored_key.master_key_nonce,
|
&stored_key.master_key_nonce,
|
||||||
stored_key.algorithm,
|
stored_key.algorithm,
|
||||||
&stored_key.master_key,
|
&stored_key.master_key,
|
||||||
&[],
|
&[],
|
||||||
) {
|
) {
|
||||||
master_key.copy_from_slice(&decrypted_master_key);
|
Ok(Protected::new(to_array(
|
||||||
Ok(Protected::new(master_key))
|
decrypted_master_key.expose().clone(),
|
||||||
|
)?))
|
||||||
} else {
|
} else {
|
||||||
Err(Error::IncorrectPassword)
|
Err(Error::IncorrectPassword)
|
||||||
}?;
|
}?;
|
||||||
|
@ -314,13 +665,11 @@ impl KeyManager {
|
||||||
// Hash the key once with the parameters/algorithm the user selected during first mount
|
// Hash the key once with the parameters/algorithm the user selected during first mount
|
||||||
let hashed_key = stored_key
|
let hashed_key = stored_key
|
||||||
.hashing_algorithm
|
.hashing_algorithm
|
||||||
.hash(key.clone(), stored_key.content_salt)?;
|
.hash(key, stored_key.content_salt)?;
|
||||||
|
|
||||||
// Construct the MountedKey and insert it into the Keymount
|
// Construct the MountedKey and insert it into the Keymount
|
||||||
let mounted_key = MountedKey {
|
let mounted_key = MountedKey {
|
||||||
uuid: stored_key.uuid,
|
uuid: stored_key.uuid,
|
||||||
key,
|
|
||||||
content_salt: stored_key.content_salt,
|
|
||||||
hashed_key,
|
hashed_key,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -332,6 +681,42 @@ impl KeyManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This function is used for getting the key itself, from a given UUID.
|
||||||
|
///
|
||||||
|
/// The master password/salt needs to be present, so we are able to decrypt the key itself from the stored key.
|
||||||
|
pub fn get_key(&self, uuid: Uuid) -> Result<Protected<Vec<u8>>> {
|
||||||
|
match self.keystore.get(&uuid) {
|
||||||
|
Some(stored_key) => {
|
||||||
|
// Decrypt the StoredKey's master key using the user's hashed password
|
||||||
|
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes(
|
||||||
|
self.get_master_password()?,
|
||||||
|
&stored_key.master_key_nonce,
|
||||||
|
stored_key.algorithm,
|
||||||
|
&stored_key.master_key,
|
||||||
|
&[],
|
||||||
|
) {
|
||||||
|
Ok(Protected::new(to_array(
|
||||||
|
decrypted_master_key.expose().clone(),
|
||||||
|
)?))
|
||||||
|
} else {
|
||||||
|
Err(Error::IncorrectPassword)
|
||||||
|
}?;
|
||||||
|
|
||||||
|
// Decrypt the StoredKey using the decrypted master key
|
||||||
|
let key = StreamDecryption::decrypt_bytes(
|
||||||
|
master_key,
|
||||||
|
&stored_key.key_nonce,
|
||||||
|
stored_key.algorithm,
|
||||||
|
&stored_key.key,
|
||||||
|
&[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
None => Err(Error::KeyNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// This function is for accessing the internal keymount.
|
/// This function is for accessing the internal keymount.
|
||||||
///
|
///
|
||||||
/// We could add a log to this, so that the user can view accesses
|
/// We could add a log to this, so that the user can view accesses
|
||||||
|
@ -379,23 +764,17 @@ impl KeyManager {
|
||||||
algorithm: Algorithm,
|
algorithm: Algorithm,
|
||||||
hashing_algorithm: HashingAlgorithm,
|
hashing_algorithm: HashingAlgorithm,
|
||||||
) -> Result<Uuid> {
|
) -> Result<Uuid> {
|
||||||
let master_password = self.get_master_password()?;
|
|
||||||
|
|
||||||
let uuid = uuid::Uuid::new_v4();
|
let uuid = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
// Generate items we'll need for encryption
|
// Generate items we'll need for encryption
|
||||||
let key_nonce = generate_nonce(algorithm);
|
let key_nonce = generate_nonce(algorithm);
|
||||||
let master_key = generate_master_key();
|
let master_key = generate_master_key();
|
||||||
let master_key_nonce = generate_nonce(algorithm);
|
let master_key_nonce = generate_nonce(algorithm);
|
||||||
let salt = generate_salt();
|
|
||||||
let content_salt = generate_salt(); // for PVM/MD
|
let content_salt = generate_salt(); // for PVM/MD
|
||||||
|
|
||||||
// Hash the user's master password
|
// Encrypt the master key with the user's hashed password
|
||||||
let hashed_password = hashing_algorithm.hash(master_password, salt)?;
|
|
||||||
|
|
||||||
// Encrypted the master key with the user's hashed password
|
|
||||||
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
|
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
|
||||||
hashed_password,
|
self.get_master_password()?,
|
||||||
&master_key_nonce,
|
&master_key_nonce,
|
||||||
algorithm,
|
algorithm,
|
||||||
master_key.expose(),
|
master_key.expose(),
|
||||||
|
@ -411,7 +790,6 @@ impl KeyManager {
|
||||||
uuid,
|
uuid,
|
||||||
algorithm,
|
algorithm,
|
||||||
hashing_algorithm,
|
hashing_algorithm,
|
||||||
salt,
|
|
||||||
content_salt,
|
content_salt,
|
||||||
master_key: encrypted_master_key,
|
master_key: encrypted_master_key,
|
||||||
master_key_nonce,
|
master_key_nonce,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! This includes things such as cryptographically-secure random salt/master key/nonce generation,
|
//! This includes things such as cryptographically-secure random salt/master key/nonce generation,
|
||||||
//! lengths for master keys and even the streaming block size.
|
//! lengths for master keys and even the streaming block size.
|
||||||
use rand::{RngCore, SeedableRng};
|
use rand::{seq::SliceRandom, RngCore, SeedableRng};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
use crate::{crypto::stream::Algorithm, Error, Protected, Result};
|
use crate::{crypto::stream::Algorithm, Error, Protected, Result};
|
||||||
|
@ -21,6 +21,8 @@ pub const ENCRYPTED_MASTER_KEY_LEN: usize = 48;
|
||||||
/// The length of the (unencrypted) master key
|
/// The length of the (unencrypted) master key
|
||||||
pub const MASTER_KEY_LEN: usize = 32;
|
pub const MASTER_KEY_LEN: usize = 32;
|
||||||
|
|
||||||
|
pub const PASSPHRASE_LEN: usize = 7;
|
||||||
|
|
||||||
/// This should be used for generating nonces for encryption.
|
/// This should be used for generating nonces for encryption.
|
||||||
///
|
///
|
||||||
/// An algorithm is required so this function can calculate the length of the nonce.
|
/// An algorithm is required so this function can calculate the length of the nonce.
|
||||||
|
@ -68,3 +70,34 @@ pub fn to_array<const I: usize>(bytes: Vec<u8>) -> Result<[u8; I]> {
|
||||||
Error::VecArrSizeMismatch
|
Error::VecArrSizeMismatch
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This generates a 7 word diceware passphrase, separated with `-`
|
||||||
|
#[must_use]
|
||||||
|
pub fn generate_passphrase() -> Protected<String> {
|
||||||
|
let wordlist = include_str!("../assets/eff_large_wordlist.txt")
|
||||||
|
.lines()
|
||||||
|
.collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
let words: Vec<String> = wordlist
|
||||||
|
.choose_multiple(
|
||||||
|
&mut rand_chacha::ChaCha20Rng::from_entropy(),
|
||||||
|
PASSPHRASE_LEN,
|
||||||
|
)
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let passphrase = words
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, word)| {
|
||||||
|
if i < PASSPHRASE_LEN - 1 {
|
||||||
|
word.clone() + "-"
|
||||||
|
} else {
|
||||||
|
word.clone()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Protected::new(passphrase)
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ export type Procedures = {
|
||||||
{ key: "jobs.getRunning", input: LibraryArgs<null>, result: Array<JobReport> } |
|
{ key: "jobs.getRunning", input: LibraryArgs<null>, result: Array<JobReport> } |
|
||||||
{ key: "jobs.isRunning", input: LibraryArgs<null>, result: boolean } |
|
{ key: "jobs.isRunning", input: LibraryArgs<null>, result: boolean } |
|
||||||
{ key: "keys.getDefault", input: LibraryArgs<null>, result: string | null } |
|
{ key: "keys.getDefault", input: LibraryArgs<null>, result: string | null } |
|
||||||
|
{ key: "keys.getKey", input: LibraryArgs<string>, result: string } |
|
||||||
|
{ key: "keys.hasMasterPassword", input: LibraryArgs<null>, result: boolean } |
|
||||||
{ key: "keys.list", input: LibraryArgs<null>, result: Array<StoredKey> } |
|
{ key: "keys.list", input: LibraryArgs<null>, result: Array<StoredKey> } |
|
||||||
{ key: "keys.listMounted", input: LibraryArgs<null>, result: Array<string> } |
|
{ key: "keys.listMounted", input: LibraryArgs<null>, result: Array<string> } |
|
||||||
{ key: "library.getStatistics", input: LibraryArgs<null>, result: Statistics } |
|
{ key: "library.getStatistics", input: LibraryArgs<null>, result: Statistics } |
|
||||||
|
@ -37,10 +39,15 @@ export type Procedures = {
|
||||||
{ key: "jobs.identifyUniqueFiles", input: LibraryArgs<IdentifyUniqueFilesArgs>, result: null } |
|
{ key: "jobs.identifyUniqueFiles", input: LibraryArgs<IdentifyUniqueFilesArgs>, result: null } |
|
||||||
{ key: "jobs.objectValidator", input: LibraryArgs<ObjectValidatorArgs>, result: null } |
|
{ key: "jobs.objectValidator", input: LibraryArgs<ObjectValidatorArgs>, result: null } |
|
||||||
{ key: "keys.add", input: LibraryArgs<KeyAddArgs>, result: null } |
|
{ key: "keys.add", input: LibraryArgs<KeyAddArgs>, result: null } |
|
||||||
|
{ key: "keys.backupKeystore", input: LibraryArgs<string>, result: null } |
|
||||||
|
{ key: "keys.changeMasterPassword", input: LibraryArgs<MasterPasswordChangeArgs>, result: string } |
|
||||||
|
{ key: "keys.clearMasterPassword", input: LibraryArgs<null>, result: null } |
|
||||||
{ key: "keys.deleteFromLibrary", input: LibraryArgs<string>, result: null } |
|
{ key: "keys.deleteFromLibrary", input: LibraryArgs<string>, result: null } |
|
||||||
{ key: "keys.mount", input: LibraryArgs<string>, result: null } |
|
{ key: "keys.mount", input: LibraryArgs<string>, result: null } |
|
||||||
|
{ key: "keys.onboarding", input: LibraryArgs<OnboardingArgs>, result: OnboardingKeys } |
|
||||||
|
{ key: "keys.restoreKeystore", input: LibraryArgs<RestoreBackupArgs>, result: number } |
|
||||||
{ key: "keys.setDefault", input: LibraryArgs<string>, result: null } |
|
{ key: "keys.setDefault", input: LibraryArgs<string>, result: null } |
|
||||||
{ key: "keys.setMasterPassword", input: LibraryArgs<string>, result: null } |
|
{ key: "keys.setMasterPassword", input: LibraryArgs<SetMasterPasswordArgs>, result: null } |
|
||||||
{ key: "keys.unmount", input: LibraryArgs<string>, result: null } |
|
{ key: "keys.unmount", input: LibraryArgs<string>, result: null } |
|
||||||
{ key: "keys.unmountAll", input: LibraryArgs<null>, result: null } |
|
{ key: "keys.unmountAll", input: LibraryArgs<null>, result: null } |
|
||||||
{ key: "keys.updateKeyName", input: LibraryArgs<KeyNameUpdateArgs>, result: null } |
|
{ key: "keys.updateKeyName", input: LibraryArgs<KeyNameUpdateArgs>, result: null } |
|
||||||
|
@ -95,7 +102,7 @@ export interface JobReport { id: string, name: string, data: Array<number> | nul
|
||||||
|
|
||||||
export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused"
|
export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused"
|
||||||
|
|
||||||
export interface KeyAddArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, key: string }
|
export interface KeyAddArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, key: string, library_sync: boolean }
|
||||||
|
|
||||||
export interface KeyNameUpdateArgs { uuid: string, name: string }
|
export interface KeyNameUpdateArgs { uuid: string, name: string }
|
||||||
|
|
||||||
|
@ -113,6 +120,8 @@ export interface LocationExplorerArgs { location_id: number, path: string, limit
|
||||||
|
|
||||||
export interface LocationUpdateArgs { id: number, name: string | null, indexer_rules_ids: Array<number> }
|
export interface LocationUpdateArgs { id: number, name: string | null, indexer_rules_ids: Array<number> }
|
||||||
|
|
||||||
|
export interface MasterPasswordChangeArgs { password: string, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm }
|
||||||
|
|
||||||
export interface Node { id: number, pub_id: Array<number>, name: string, platform: number, version: string | null, last_seen: string, timezone: string | null, date_created: string }
|
export interface Node { id: number, pub_id: Array<number>, name: string, platform: number, version: string | null, last_seen: string, timezone: string | null, date_created: string }
|
||||||
|
|
||||||
export interface NodeConfig { version: string | null, id: string, name: string, p2p_port: number | null }
|
export interface NodeConfig { version: string | null, id: string, name: string, p2p_port: number | null }
|
||||||
|
@ -131,17 +140,25 @@ export interface Object { id: number, cas_id: string, integrity_checksum: string
|
||||||
|
|
||||||
export interface ObjectValidatorArgs { id: number, path: string }
|
export interface ObjectValidatorArgs { id: number, path: string }
|
||||||
|
|
||||||
|
export interface OnboardingArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm }
|
||||||
|
|
||||||
|
export interface OnboardingKeys { master_password: string, secret_key: string }
|
||||||
|
|
||||||
export type Params = "Standard" | "Hardened" | "Paranoid"
|
export type Params = "Standard" | "Hardened" | "Paranoid"
|
||||||
|
|
||||||
|
export interface RestoreBackupArgs { password: string, secret_key: string, path: string }
|
||||||
|
|
||||||
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
|
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
|
||||||
|
|
||||||
export interface SetFavoriteArgs { id: number, favorite: boolean }
|
export interface SetFavoriteArgs { id: number, favorite: boolean }
|
||||||
|
|
||||||
|
export interface SetMasterPasswordArgs { password: string, secret_key: string }
|
||||||
|
|
||||||
export interface SetNoteArgs { id: number, note: string | null }
|
export interface SetNoteArgs { id: number, note: string | null }
|
||||||
|
|
||||||
export interface Statistics { id: number, date_captured: string, total_object_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string }
|
export interface Statistics { id: number, date_captured: string, total_object_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string }
|
||||||
|
|
||||||
export interface StoredKey { uuid: string, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, salt: Array<number>, content_salt: Array<number>, master_key: Array<number>, master_key_nonce: Array<number>, key_nonce: Array<number>, key: Array<number> }
|
export interface StoredKey { uuid: string, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, content_salt: Array<number>, master_key: Array<number>, master_key_nonce: Array<number>, key_nonce: Array<number>, key: Array<number> }
|
||||||
|
|
||||||
export interface Tag { id: number, pub_id: Array<number>, name: string | null, color: string | null, total_objects: number | null, redundancy_goal: number | null, date_created: string, date_modified: string }
|
export interface Tag { id: number, pub_id: Array<number>, name: string | null, color: string | null, total_objects: number | null, redundancy_goal: number | null, date_created: string, date_modified: string }
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,9 @@
|
||||||
"@tanstack/react-query-devtools": "^4.12.0",
|
"@tanstack/react-query-devtools": "^4.12.0",
|
||||||
"@tanstack/react-virtual": "3.0.0-beta.18",
|
"@tanstack/react-virtual": "3.0.0-beta.18",
|
||||||
"@vitejs/plugin-react": "^2.1.0",
|
"@vitejs/plugin-react": "^2.1.0",
|
||||||
|
"@zxcvbn-ts/core": "^2.1.0",
|
||||||
|
"@zxcvbn-ts/language-common": "^2.0.1",
|
||||||
|
"@zxcvbn-ts/language-en": "^2.1.0",
|
||||||
"autoprefixer": "^10.4.12",
|
"autoprefixer": "^10.4.12",
|
||||||
"byte-size": "^8.1.0",
|
"byte-size": "^8.1.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
|
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;
|
export type KeyListProps = DefaultProps;
|
||||||
|
|
||||||
const ListKeys = () => {
|
export const ListOfKeys = (props: { noKeysMessage: boolean }) => {
|
||||||
|
const { noKeysMessage } = props;
|
||||||
|
|
||||||
const keys = useLibraryQuery(['keys.list']);
|
const keys = useLibraryQuery(['keys.list']);
|
||||||
const mounted_uuids = useLibraryQuery(['keys.listMounted']);
|
const mounted_uuids = useLibraryQuery(['keys.listMounted']);
|
||||||
|
|
||||||
|
@ -20,7 +22,7 @@ const ListKeys = () => {
|
||||||
[keys, mounted_uuids]
|
[keys, mounted_uuids]
|
||||||
);
|
);
|
||||||
|
|
||||||
if(keys.data?.length === 0) {
|
if(keys.data?.length === 0 && noKeysMessage) {
|
||||||
return (
|
return (
|
||||||
<CategoryHeading>No keys available.</CategoryHeading>
|
<CategoryHeading>No keys available.</CategoryHeading>
|
||||||
)
|
)
|
||||||
|
@ -44,7 +46,7 @@ const ListKeys = () => {
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
export function KeyList(props: KeyListProps) {
|
export const KeyList = (props: KeyListProps) => {
|
||||||
const unmountAll = useLibraryMutation(['keys.unmountAll']);
|
const unmountAll = useLibraryMutation(['keys.unmountAll']);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -53,7 +55,7 @@ export function KeyList(props: KeyListProps) {
|
||||||
<div className="">
|
<div className="">
|
||||||
{/* <CategoryHeading>Mounted keys</CategoryHeading> */}
|
{/* <CategoryHeading>Mounted keys</CategoryHeading> */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<ListKeys></ListKeys>
|
<ListOfKeys noKeysMessage />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import { Tabs } from '@sd/ui';
|
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||||
|
import { Button, ButtonLink, Input, Tabs } from '@sd/ui';
|
||||||
|
import { Eye, EyeSlash, Gear, Lock } from 'phosphor-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { DefaultProps } from '../primitive/types';
|
import { DefaultProps } from '../primitive/types';
|
||||||
import { KeyList } from './KeyList';
|
import { KeyList } from './KeyList';
|
||||||
|
@ -7,24 +10,123 @@ import { KeyMounter } from './KeyMounter';
|
||||||
export type KeyManagerProps = DefaultProps;
|
export type KeyManagerProps = DefaultProps;
|
||||||
|
|
||||||
export function KeyManager(props: KeyManagerProps) {
|
export function KeyManager(props: KeyManagerProps) {
|
||||||
return (
|
const hasMasterPw = useLibraryQuery(['keys.hasMasterPassword']);
|
||||||
<div>
|
const setMasterPasswordMutation = useLibraryMutation('keys.setMasterPassword');
|
||||||
<Tabs.Root defaultValue="mount">
|
const unmountAll = useLibraryMutation('keys.unmountAll');
|
||||||
<Tabs.List>
|
const clearMasterPassword = useLibraryMutation('keys.clearMasterPassword');
|
||||||
<Tabs.Trigger className="text-sm font-medium" value="mount">
|
|
||||||
Mount
|
const [showMasterPassword, setShowMasterPassword] = useState(false);
|
||||||
</Tabs.Trigger>
|
const [showSecretKey, setShowSecretKey] = useState(false);
|
||||||
<Tabs.Trigger className="text-sm font-medium" value="keys">
|
|
||||||
Keys
|
const [masterPassword, setMasterPassword] = useState('');
|
||||||
</Tabs.Trigger>
|
const [secretKey, setSecretKey] = useState('');
|
||||||
</Tabs.List>
|
|
||||||
<Tabs.Content value="keys">
|
if (!hasMasterPw?.data) {
|
||||||
<KeyList />
|
const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
|
||||||
</Tabs.Content>
|
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
|
||||||
<Tabs.Content value="mount">
|
|
||||||
<KeyMounter />
|
return (
|
||||||
</Tabs.Content>
|
<div className="p-2">
|
||||||
</Tabs.Root>
|
<div className="relative flex flex-grow mb-2">
|
||||||
</div>
|
<Input
|
||||||
);
|
value={masterPassword}
|
||||||
|
onChange={(e) => setMasterPassword(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
type={showMasterPassword ? 'text' : 'password'}
|
||||||
|
className="flex-grow !py-0.5"
|
||||||
|
placeholder="Master Password"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowMasterPassword(!showMasterPassword)}
|
||||||
|
size="icon"
|
||||||
|
className="border-none absolute right-[5px] top-[5px]"
|
||||||
|
>
|
||||||
|
<MPCurrentEyeIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative flex flex-grow mb-2">
|
||||||
|
<Input
|
||||||
|
value={secretKey}
|
||||||
|
onChange={(e) => setSecretKey(e.target.value)}
|
||||||
|
type={showSecretKey ? 'text' : 'password'}
|
||||||
|
className="flex-grow !py-0.5"
|
||||||
|
placeholder="Secret Key"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||||
|
size="icon"
|
||||||
|
className="border-none absolute right-[5px] top-[5px]"
|
||||||
|
>
|
||||||
|
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
variant="accent"
|
||||||
|
disabled={setMasterPasswordMutation.isLoading}
|
||||||
|
onClick={() => {
|
||||||
|
if (masterPassword !== '' && secretKey !== '') {
|
||||||
|
setMasterPassword('');
|
||||||
|
setSecretKey('');
|
||||||
|
setMasterPasswordMutation.mutate(
|
||||||
|
{ password: masterPassword, secret_key: secretKey },
|
||||||
|
{
|
||||||
|
onError: () => {
|
||||||
|
alert('Incorrect information provided.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Unlock
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Tabs.Root defaultValue="mount">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Trigger className="text-sm font-medium" value="mount">
|
||||||
|
Mount
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger className="text-sm font-medium" value="keys">
|
||||||
|
Keys
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<div className="flex-grow" />
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
unmountAll.mutate(null);
|
||||||
|
clearMasterPassword.mutate(null);
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
className="text-ink-faint"
|
||||||
|
>
|
||||||
|
<Lock className="w-4 h-4 text-ink-faint" />
|
||||||
|
</Button>
|
||||||
|
<ButtonLink
|
||||||
|
to="/settings/keys"
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
className="text-ink-faint"
|
||||||
|
>
|
||||||
|
<Gear className="w-4 h-4 text-ink-faint" />
|
||||||
|
</ButtonLink>
|
||||||
|
</Tabs.List>
|
||||||
|
</div>
|
||||||
|
<Tabs.Content value="keys">
|
||||||
|
<KeyList />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="mount">
|
||||||
|
<KeyMounter />
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||||
|
import { Algorithm, HashingAlgorithm, Params } from '@sd/client';
|
||||||
import { Button, CategoryHeading, Input, Select, SelectOption, Switch, cva, tw } from '@sd/ui';
|
import { Button, CategoryHeading, Input, Select, SelectOption, Switch, cva, tw } from '@sd/ui';
|
||||||
import { Eye, EyeSlash, Info } from 'phosphor-react';
|
import { Eye, EyeSlash, Info } from 'phosphor-react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
|
||||||
import { Algorithm, HashingAlgorithm, Params } from '@sd/client';
|
|
||||||
|
|
||||||
|
import { getCryptoSettings } from '../../screens/settings/library/KeysSetting';
|
||||||
import { Tooltip } from '../tooltip/Tooltip';
|
import { Tooltip } from '../tooltip/Tooltip';
|
||||||
|
|
||||||
const KeyHeading = tw(CategoryHeading)`mb-1`;
|
const KeyHeading = tw(CategoryHeading)`mb-1`;
|
||||||
|
@ -18,7 +19,7 @@ export function KeyMounter() {
|
||||||
const mounted_uuids = useLibraryQuery(['keys.listMounted']);
|
const mounted_uuids = useLibraryQuery(['keys.listMounted']);
|
||||||
|
|
||||||
const [showKey, setShowKey] = useState(false);
|
const [showKey, setShowKey] = useState(false);
|
||||||
const [toggle, setToggle] = useState(true);
|
const [librarySync, setLibrarySync] = useState(true);
|
||||||
|
|
||||||
const [key, setKey] = useState('');
|
const [key, setKey] = useState('');
|
||||||
const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305');
|
const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305');
|
||||||
|
@ -63,12 +64,12 @@ export function KeyMounter() {
|
||||||
<Switch
|
<Switch
|
||||||
className="bg-app-selected"
|
className="bg-app-selected"
|
||||||
size="sm"
|
size="sm"
|
||||||
checked={toggle}
|
checked={librarySync}
|
||||||
onCheckedChange={setToggle}
|
onCheckedChange={setLibrarySync}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-3 text-xs font-medium">Sync with Library</span>
|
<span className="ml-3 text-xs font-medium">Sync with Library</span>
|
||||||
<Tooltip label="This key will be mounted on all devices running your Library">
|
<Tooltip label="This key will be registered with all devices running your Library">
|
||||||
<Info className="w-4 h-4 ml-1.5 text-ink-faint" />
|
<Info className="w-4 h-4 ml-1.5 text-ink-faint" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,26 +94,20 @@ export function KeyMounter() {
|
||||||
<p className="pt-1.5 ml-0.5 text-[8pt] leading-snug text-ink-faint w-[90%]">
|
<p className="pt-1.5 ml-0.5 text-[8pt] leading-snug text-ink-faint w-[90%]">
|
||||||
Files encrypted with this key will be revealed and decrypted on the fly.
|
Files encrypted with this key will be revealed and decrypted on the fly.
|
||||||
</p>
|
</p>
|
||||||
<Button className="w-full mt-2" variant="accent" onClick={() => {
|
<Button
|
||||||
let algorithm = encryptionAlgo as Algorithm;
|
className="w-full mt-2"
|
||||||
let hashing_algorithm: HashingAlgorithm = { Argon2id: "Standard" };
|
variant="accent"
|
||||||
|
disabled={key === ''}
|
||||||
|
onClick={() => {
|
||||||
|
if (key !== '') {
|
||||||
|
setKey('');
|
||||||
|
|
||||||
switch(hashingAlgo) {
|
const [algorithm, hashing_algorithm] = getCryptoSettings(encryptionAlgo, hashingAlgo);
|
||||||
case "Argon2id-s":
|
|
||||||
hashing_algorithm = { Argon2id: "Standard" as Params };
|
|
||||||
break;
|
|
||||||
case "Argon2id-h":
|
|
||||||
hashing_algorithm = { Argon2id: "Hardened" as Params };
|
|
||||||
break;
|
|
||||||
case "Argon2id-p":
|
|
||||||
hashing_algorithm = { Argon2id: "Paranoid" as Params };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
createKey.mutate({algorithm, hashing_algorithm, key });
|
createKey.mutate({ algorithm, hashing_algorithm, key, library_sync: librarySync });
|
||||||
setKey("");
|
}
|
||||||
}
|
}}
|
||||||
}>
|
>
|
||||||
Mount Key
|
Mount Key
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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 { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||||
|
import { SettingsSubHeader } from '../../../components/settings/SettingsSubHeader';
|
||||||
|
|
||||||
|
interface Props extends DropdownMenu.MenuContentProps {
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
transformOrigin?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KeyMounterDropdown = ({
|
||||||
|
trigger,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
transformOrigin,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<Props>) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const transitions = useTransition(open, {
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: `scale(0.9)`,
|
||||||
|
transformOrigin: transformOrigin || 'top'
|
||||||
|
},
|
||||||
|
enter: { opacity: 1, transform: 'scale(1)' },
|
||||||
|
leave: { opacity: -0.5, transform: 'scale(0.95)' },
|
||||||
|
config: { mass: 0.4, tension: 200, friction: 10 }
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
||||||
|
<DropdownMenu.Trigger>{trigger}</DropdownMenu.Trigger>
|
||||||
|
{transitions(
|
||||||
|
(styles, show) =>
|
||||||
|
show && (
|
||||||
|
<DropdownMenu.Portal forceMount>
|
||||||
|
<DropdownMenu.Content forceMount asChild>
|
||||||
|
<animated.div
|
||||||
|
// most of this is copied over from the `OverlayPanel`
|
||||||
|
className={clsx(
|
||||||
|
'flex flex-col',
|
||||||
|
'z-50 m-2 space-y-1',
|
||||||
|
'select-none cursor-default rounded-lg',
|
||||||
|
'text-left text-sm text-ink',
|
||||||
|
'bg-app-overlay/80 backdrop-blur',
|
||||||
|
// 'border border-app-overlay',
|
||||||
|
'shadow-2xl shadow-black/60 ',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={styles}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</animated.div>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function KeysSettings() {
|
export default function KeysSettings() {
|
||||||
return (
|
const hasMasterPw = useLibraryQuery(['keys.hasMasterPassword']);
|
||||||
<SettingsContainer>
|
const setMasterPasswordMutation = useLibraryMutation('keys.setMasterPassword');
|
||||||
<SettingsHeader title="Keys" description="Manage your keys." />
|
const unmountAll = useLibraryMutation('keys.unmountAll');
|
||||||
</SettingsContainer>
|
const clearMasterPassword = useLibraryMutation('keys.clearMasterPassword');
|
||||||
);
|
const backupKeystore = useLibraryMutation('keys.backupKeystore');
|
||||||
|
|
||||||
|
const [showMasterPassword, setShowMasterPassword] = useState(false);
|
||||||
|
const [showSecretKey, setShowSecretKey] = useState(false);
|
||||||
|
const [masterPassword, setMasterPassword] = useState('');
|
||||||
|
const [secretKey, setSecretKey] = useState('');
|
||||||
|
const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
|
||||||
|
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
|
||||||
|
|
||||||
|
if (!hasMasterPw?.data) {
|
||||||
|
return (
|
||||||
|
<div className="p-2 mr-20 ml-20 mt-10">
|
||||||
|
<div className="relative flex flex-grow mb-2">
|
||||||
|
<Input
|
||||||
|
value={masterPassword}
|
||||||
|
onChange={(e) => setMasterPassword(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
type={showMasterPassword ? 'text' : 'password'}
|
||||||
|
className="flex-grow !py-0.5"
|
||||||
|
placeholder="Master Password"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowMasterPassword(!showMasterPassword)}
|
||||||
|
size="icon"
|
||||||
|
className="border-none absolute right-[5px] top-[5px]"
|
||||||
|
>
|
||||||
|
<MPCurrentEyeIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative flex flex-grow mb-2">
|
||||||
|
<Input
|
||||||
|
value={secretKey}
|
||||||
|
onChange={(e) => setSecretKey(e.target.value)}
|
||||||
|
type={showSecretKey ? 'text' : 'password'}
|
||||||
|
className="flex-grow !py-0.5"
|
||||||
|
placeholder="Secret Key"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||||
|
size="icon"
|
||||||
|
className="border-none absolute right-[5px] top-[5px]"
|
||||||
|
>
|
||||||
|
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
variant="accent"
|
||||||
|
disabled={setMasterPasswordMutation.isLoading}
|
||||||
|
onClick={() => {
|
||||||
|
if (masterPassword !== '' && secretKey !== '') {
|
||||||
|
setMasterPassword('');
|
||||||
|
setSecretKey('');
|
||||||
|
setMasterPasswordMutation.mutate(
|
||||||
|
{ password: masterPassword, secret_key: secretKey },
|
||||||
|
{
|
||||||
|
onError: () => {
|
||||||
|
alert('Incorrect information provided.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Unlock
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<SettingsContainer>
|
||||||
|
<SettingsHeader
|
||||||
|
title="Keys"
|
||||||
|
description="Manage your keys."
|
||||||
|
rightArea={
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
unmountAll.mutate(null);
|
||||||
|
clearMasterPassword.mutate(null);
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
className="text-ink-faint"
|
||||||
|
>
|
||||||
|
<Lock className="w-4 h-4 text-ink-faint" />
|
||||||
|
</Button>
|
||||||
|
<KeyMounterDropdown
|
||||||
|
trigger={
|
||||||
|
<Button size="icon" variant="outline" className="text-ink-faint">
|
||||||
|
<Plus className="w-4 h-4 text-ink-faint" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<KeyMounter />
|
||||||
|
</KeyMounterDropdown>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{hasMasterPw.data ? (
|
||||||
|
<div className="grid space-y-2">
|
||||||
|
<ListOfKeys noKeysMessage={false} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<SettingsSubHeader title="Password Options" />
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<PasswordChangeDialog
|
||||||
|
trigger={
|
||||||
|
<Button size="sm" variant="gray" className="mr-2">
|
||||||
|
Change Master Password
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingsSubHeader title="Data Recovery" />
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="gray"
|
||||||
|
className="mr-2"
|
||||||
|
onClick={() => {
|
||||||
|
// not platform-safe, probably will break on web but `platform` doesn't have a save dialog option
|
||||||
|
save()?.then((result) => {
|
||||||
|
if (result) backupKeystore.mutate(result as string);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Backup
|
||||||
|
</Button>
|
||||||
|
<BackupRestoreDialog
|
||||||
|
trigger={
|
||||||
|
<Button size="sm" variant="gray" className="mr-2">
|
||||||
|
Restore
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// not sure of a suitable place for this function
|
||||||
|
export const getCryptoSettings = (
|
||||||
|
encryptionAlgorithm: string,
|
||||||
|
hashingAlgorithm: string
|
||||||
|
): [Algorithm, HashingAlgorithm] => {
|
||||||
|
const algorithm = encryptionAlgorithm as Algorithm;
|
||||||
|
let hashing_algorithm: HashingAlgorithm = { Argon2id: 'Standard' };
|
||||||
|
|
||||||
|
switch (hashingAlgorithm) {
|
||||||
|
case 'Argon2id-s':
|
||||||
|
hashing_algorithm = { Argon2id: 'Standard' as Params };
|
||||||
|
break;
|
||||||
|
case 'Argon2id-h':
|
||||||
|
hashing_algorithm = { Argon2id: 'Hardened' as Params };
|
||||||
|
break;
|
||||||
|
case 'Argon2id-p':
|
||||||
|
hashing_algorithm = { Argon2id: 'Paranoid' as Params };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [algorithm, hashing_algorithm];
|
||||||
|
};
|
||||||
|
|
|
@ -414,6 +414,9 @@ importers:
|
||||||
'@types/react-router-dom': ^5.3.3
|
'@types/react-router-dom': ^5.3.3
|
||||||
'@types/tailwindcss': ^3.1.0
|
'@types/tailwindcss': ^3.1.0
|
||||||
'@vitejs/plugin-react': ^2.1.0
|
'@vitejs/plugin-react': ^2.1.0
|
||||||
|
'@zxcvbn-ts/core': ^2.1.0
|
||||||
|
'@zxcvbn-ts/language-common': ^2.0.1
|
||||||
|
'@zxcvbn-ts/language-en': ^2.1.0
|
||||||
autoprefixer: ^10.4.12
|
autoprefixer: ^10.4.12
|
||||||
byte-size: ^8.1.0
|
byte-size: ^8.1.0
|
||||||
clsx: ^1.2.1
|
clsx: ^1.2.1
|
||||||
|
@ -456,6 +459,9 @@ importers:
|
||||||
'@tanstack/react-query-devtools': 4.12.0_pqnxmwujmmnpcx44ucekqkefny
|
'@tanstack/react-query-devtools': 4.12.0_pqnxmwujmmnpcx44ucekqkefny
|
||||||
'@tanstack/react-virtual': 3.0.0-beta.18_react@18.2.0
|
'@tanstack/react-virtual': 3.0.0-beta.18_react@18.2.0
|
||||||
'@vitejs/plugin-react': 2.1.0_vite@3.1.8
|
'@vitejs/plugin-react': 2.1.0_vite@3.1.8
|
||||||
|
'@zxcvbn-ts/core': 2.1.0
|
||||||
|
'@zxcvbn-ts/language-common': 2.0.1
|
||||||
|
'@zxcvbn-ts/language-en': 2.1.0
|
||||||
autoprefixer: 10.4.12
|
autoprefixer: 10.4.12
|
||||||
byte-size: 8.1.0
|
byte-size: 8.1.0
|
||||||
clsx: 1.2.1
|
clsx: 1.2.1
|
||||||
|
@ -8461,7 +8467,7 @@ packages:
|
||||||
'@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.19.3
|
'@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.19.3
|
||||||
magic-string: 0.26.7
|
magic-string: 0.26.7
|
||||||
react-refresh: 0.14.0
|
react-refresh: 0.14.0
|
||||||
vite: 3.1.8_sass@1.55.0
|
vite: 3.1.8
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
@ -8718,6 +8724,18 @@ packages:
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@zxcvbn-ts/core/2.1.0:
|
||||||
|
resolution: {integrity: sha512-doxol9xrO7LgyVJhguXe7vO0xthnIYmsOKoDwrLg0Ho2kkpQaVtM+AOQw+BkEiKIqNg1V48eUf4/cTzMElXdiA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zxcvbn-ts/language-common/2.0.1:
|
||||||
|
resolution: {integrity: sha512-P+v5MA/UNc9nb3FEOEoDgTyIGQc2vLc6m04pdf5YyuNOzrL0iNANhECk2TUp62JbrjouJVodqhMH0j1a8/24Bg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@zxcvbn-ts/language-en/2.1.0:
|
||||||
|
resolution: {integrity: sha512-I3n4AAbArjPAZtwCrk9MQnSrcj5+9rq8sic2rUU44fP5QaR17Vk8zDt61+R9dnP9ZRsj09aAUYML4Ash05qZjQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/abort-controller/3.0.0:
|
/abort-controller/3.0.0:
|
||||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
engines: {node: '>=6.5'}
|
engines: {node: '>=6.5'}
|
||||||
|
@ -22083,7 +22101,7 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 5.0.1
|
'@rollup/pluginutils': 5.0.1
|
||||||
'@svgr/core': 6.5.0
|
'@svgr/core': 6.5.0
|
||||||
vite: 3.1.8_sass@1.55.0
|
vite: 3.1.8
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
Loading…
Reference in a new issue