mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-07 03:13:30 +00:00
[ENG-262] Key Manager Integration (#450)
* add keys router
* make progress on keymanager integration
* make name non-negotiable
* hyphenate encryption algorithm names
* Revert "make name non-negotiable"
This reverts commit 9c0f51329e
.
* add some more keymanager queries
* add master password and default key routes
* add newly registered key to db + fmt
* clippy, formatting and `updateKeyName` route
* add automount to schema+automount keys where `true`
* update bindings
* working key add/mount
* working keylist
* mounted keys show first
* cleanup code
* add comments, code cleanup, more functions
* unmount all keys button
* comment and keymanager `clear_master_password()`
* add no keys available message
* fix unmount button
* use dashmap for concurrency
* fix missing keylist issue and add invalidate query macro
* set correct RSPC types
* statically set master password (TEMPORARILY)
* add remove key function within the keymanager
* key dropdown menu and impl
* formatting
* allow `option_if_let_else`
* add comment about key stats
* add additional comment
* rpsc error handling for the keys route
* fix rspc errors with an impl
* crypto crate errors to `sd-crypto::Error`
* remove `map_err`
* use custom result type
* cargo fmt
* clippy
* fix builds
* remove `Error::MutexLock`
* fix unnecessary unwrap
* mutex error handling (buggy for some reason)
* clean default key logic
* fix default key clearing
* allow a key to be removed without bugs
* implement requested changes
* use a single `useMemo`
* update schema with defaults
* re-generate migrations
* use rust enums in TS
* remove dead code
* remove mutate expansion
* read key list from keymanager, not prisma
* add "Default" key marker and cleanup TS
* rustfmt
* remove dead code
This commit is contained in:
parent
d851880ac4
commit
a403224b3b
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -5209,10 +5209,14 @@ dependencies = [
|
|||
"aes-gcm",
|
||||
"argon2",
|
||||
"chacha20poly1305",
|
||||
"dashmap",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rspc",
|
||||
"serde",
|
||||
"serde-big-array",
|
||||
"serde_json",
|
||||
"specta 0.0.4",
|
||||
"thiserror",
|
||||
"uuid 1.2.1",
|
||||
"zeroize",
|
||||
|
@ -5368,6 +5372,15 @@ dependencies = [
|
|||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-big-array"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3323f09a748af288c3dc2474ea6803ee81f118321775bffa3ac8f7e65c5e90e7"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-value"
|
||||
version = "0.7.0"
|
||||
|
|
|
@ -54,7 +54,7 @@ image = "0.24.4"
|
|||
webp = "0.2.2"
|
||||
ffmpeg-next = { version = "5.1.1", optional = true, features = [] }
|
||||
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
|
||||
sd-crypto = { path = "../crates/crypto" }
|
||||
sd-crypto = { path = "../crates/crypto", features = ["rspc"] }
|
||||
sd-file-ext = { path = "../crates/file-ext"}
|
||||
fs_extra = "1.2.0"
|
||||
tracing = "0.1.36"
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
-- 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,
|
||||
"salt" 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", "content_salt", "date_created", "default", "hashing_algorithm", "id", "key", "key_nonce", "master_key", "master_key_nonce", "name", "salt", "uuid") SELECT "algorithm", "content_salt", "date_created", "default", "hashing_algorithm", "id", "key", "key_nonce", "master_key", "master_key_nonce", "name", "salt", "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;
|
|
@ -194,7 +194,9 @@ model Key {
|
|||
// the name that the user sets
|
||||
name String?
|
||||
// is this key the default for encryption?
|
||||
default Boolean
|
||||
// was not tagged as unique as i'm not too sure if PCR will handle it
|
||||
// can always be tagged as unique, the keys API will need updating to use `find_unique()`
|
||||
default Boolean @default(false)
|
||||
// nullable if concealed for security
|
||||
date_created DateTime? @default(now())
|
||||
// encryption algorithm used to encrypt the key
|
||||
|
@ -214,6 +216,8 @@ model Key {
|
|||
// the *encrypted* key
|
||||
key Bytes
|
||||
|
||||
automount Boolean @default(false)
|
||||
|
||||
objects Object[]
|
||||
file_paths FilePath[]
|
||||
|
||||
|
|
225
core/src/api/keys.rs
Normal file
225
core/src/api/keys.rs
Normal file
|
@ -0,0 +1,225 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use sd_crypto::{crypto::stream::Algorithm, keys::hashing::HashingAlgorithm, Protected};
|
||||
use serde::Deserialize;
|
||||
use specta::Type;
|
||||
|
||||
use crate::{invalidate_query, prisma::key};
|
||||
|
||||
use super::{utils::LibraryRequest, RouterBuilder};
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct KeyAddArgs {
|
||||
algorithm: Algorithm,
|
||||
hashing_algorithm: HashingAlgorithm,
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct KeyNameUpdateArgs {
|
||||
uuid: uuid::Uuid,
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub(crate) fn mount() -> RouterBuilder {
|
||||
RouterBuilder::new()
|
||||
.library_query("list", |t| {
|
||||
t(|_, _: (), library| async move { Ok(library.key_manager.dump_keystore()) })
|
||||
})
|
||||
// this is so we can show the key as mounted in the UI
|
||||
.library_query("listMounted", |t| {
|
||||
t(|_, _: (), library| async move { Ok(library.key_manager.get_mounted_uuids()) })
|
||||
})
|
||||
.library_mutation("mount", |t| {
|
||||
t(|_, key_uuid: uuid::Uuid, library| async move {
|
||||
library.key_manager.mount(key_uuid)?;
|
||||
// we also need to dispatch jobs that automatically decrypt preview media and metadata here
|
||||
invalidate_query!(library, "keys.listMounted");
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_mutation("updateKeyName", |t| {
|
||||
t(|_, args: KeyNameUpdateArgs, library| async move {
|
||||
library
|
||||
.db
|
||||
.key()
|
||||
.update(
|
||||
key::uuid::equals(args.uuid.to_string()),
|
||||
vec![key::SetParam::SetName(Some(args.name))],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_mutation("unmount", |t| {
|
||||
t(|_, key_uuid: uuid::Uuid, library| async move {
|
||||
library.key_manager.unmount(key_uuid)?;
|
||||
// we also need to delete all in-memory decrypted data associated with this key
|
||||
invalidate_query!(library, "keys.listMounted");
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_mutation("deleteFromLibrary", |t| {
|
||||
t(|_, key_uuid: uuid::Uuid, library| async move {
|
||||
library.key_manager.remove_key(key_uuid)?;
|
||||
|
||||
library
|
||||
.db
|
||||
.key()
|
||||
.delete(key::uuid::equals(key_uuid.to_string()))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
// we also need to delete all in-memory decrypted data associated with this key
|
||||
invalidate_query!(library, "keys.list");
|
||||
invalidate_query!(library, "keys.listMounted");
|
||||
invalidate_query!(library, "keys.getDefault");
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_mutation("setMasterPassword", |t| {
|
||||
t(|_, password: String, library| async move {
|
||||
// need to add master password checks in the keymanager itself to make sure it's correct
|
||||
// this can either unwrap&fail, or we can return the error. either way, the user will have to correct this
|
||||
// by entering the correct password
|
||||
// for now, automounting might have to serve as the master password checks
|
||||
|
||||
library
|
||||
.key_manager
|
||||
.set_master_password(Protected::new(password.as_bytes().to_vec()))?;
|
||||
|
||||
let automount = library
|
||||
.db
|
||||
.key()
|
||||
.find_many(vec![key::automount::equals(true)])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
for key in automount {
|
||||
library
|
||||
.key_manager
|
||||
.mount(uuid::Uuid::from_str(&key.uuid).map_err(|_| {
|
||||
rspc::Error::new(
|
||||
rspc::ErrorCode::InternalServerError,
|
||||
"Error deserializing UUID from string".into(),
|
||||
)
|
||||
})?)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_mutation("setDefault", |t| {
|
||||
t(|_, key_uuid: uuid::Uuid, library| async move {
|
||||
library.key_manager.set_default(key_uuid)?;
|
||||
|
||||
// if an old default is set, unset it as the default
|
||||
let old_default = library
|
||||
.db
|
||||
.key()
|
||||
.find_first(vec![key::default::equals(true)])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
if let Some(key) = old_default {
|
||||
library
|
||||
.db
|
||||
.key()
|
||||
.update(
|
||||
key::uuid::equals(key.uuid),
|
||||
vec![key::SetParam::SetDefault(false)],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_default = library
|
||||
.db
|
||||
.key()
|
||||
.find_unique(key::uuid::equals(key_uuid.to_string()))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
// if the new default key is stored in the library, update it as the default
|
||||
if let Some(default) = new_default {
|
||||
library
|
||||
.db
|
||||
.key()
|
||||
.update(
|
||||
key::uuid::equals(default.uuid),
|
||||
vec![key::SetParam::SetDefault(true)],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
}
|
||||
|
||||
invalidate_query!(library, "keys.getDefault");
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_query("getDefault", |t| {
|
||||
t(|_, _: (), library| async move {
|
||||
// `find_first` should be okay here as only one default key should ever be set
|
||||
// this is also stored in the keymanager but it's probably easier to get it from the DB
|
||||
let default = library
|
||||
.db
|
||||
.key()
|
||||
.find_first(vec![key::default::equals(true)])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
if let Some(default_key) = default {
|
||||
Ok(Some(default_key.uuid))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
})
|
||||
.library_mutation("unmountAll", |t| {
|
||||
t(|_, _: (), library| async move {
|
||||
library.key_manager.empty_keymount();
|
||||
invalidate_query!(library, "keys.listMounted");
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
// this also mounts the key
|
||||
.library_mutation("add", |t| {
|
||||
t(|_, args: KeyAddArgs, library| async move {
|
||||
// register the key with the keymanager
|
||||
let uuid = library.key_manager.add_to_keystore(
|
||||
Protected::new(args.key.as_bytes().to_vec()),
|
||||
args.algorithm,
|
||||
args.hashing_algorithm,
|
||||
)?;
|
||||
|
||||
let stored_key = library.key_manager.access_keystore(uuid)?;
|
||||
|
||||
library
|
||||
.db
|
||||
.key()
|
||||
.create(
|
||||
uuid.to_string(),
|
||||
args.algorithm.serialize().to_vec(),
|
||||
args.hashing_algorithm.serialize().to_vec(),
|
||||
stored_key.salt.to_vec(),
|
||||
stored_key.content_salt.to_vec(),
|
||||
stored_key.master_key.to_vec(),
|
||||
stored_key.master_key_nonce.to_vec(),
|
||||
stored_key.key_nonce.to_vec(),
|
||||
stored_key.key.to_vec(),
|
||||
vec![],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
// mount the key
|
||||
library.key_manager.mount(uuid)?;
|
||||
|
||||
invalidate_query!(library, "keys.list");
|
||||
invalidate_query!(library, "keys.listMounted");
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
|
@ -36,6 +36,7 @@ pub struct Ctx {
|
|||
|
||||
mod files;
|
||||
mod jobs;
|
||||
mod keys;
|
||||
mod libraries;
|
||||
mod locations;
|
||||
mod normi;
|
||||
|
@ -85,6 +86,7 @@ pub(crate) fn mount() -> Arc<Router> {
|
|||
.merge("library.", libraries::mount())
|
||||
.merge("volumes.", volumes::mount())
|
||||
.merge("tags.", tags::mount())
|
||||
.merge("keys.", keys::mount())
|
||||
.merge("locations.", locations::mount())
|
||||
.merge("files.", files::mount())
|
||||
.merge("jobs.", jobs::mount())
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use crate::job::DynJob;
|
||||
use sd_crypto::keys::keymanager::KeyManager;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -19,7 +18,7 @@ pub struct LibraryContext {
|
|||
/// db holds the database client for the current library.
|
||||
pub db: Arc<PrismaClient>,
|
||||
/// key manager that provides encryption keys to functions that require them
|
||||
pub key_manager: Arc<Mutex<KeyManager>>,
|
||||
pub key_manager: Arc<KeyManager>,
|
||||
/// node_local_id holds the local ID of the node which is running the library.
|
||||
pub node_local_id: i32,
|
||||
/// node_context holds the node context for the node which this library is running on.
|
||||
|
|
|
@ -16,6 +16,7 @@ use sd_crypto::{
|
|||
keymanager::{KeyManager, StoredKey},
|
||||
},
|
||||
primitives::to_array,
|
||||
Protected,
|
||||
};
|
||||
use std::{
|
||||
env, fs, io,
|
||||
|
@ -24,7 +25,7 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{LibraryConfig, LibraryConfigWrapped, LibraryContext};
|
||||
|
@ -39,53 +40,6 @@ pub struct LibraryManager {
|
|||
node_context: NodeContext,
|
||||
}
|
||||
|
||||
pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, SeederError> {
|
||||
// retrieve all stored keys from the DB
|
||||
let mut key_manager = KeyManager::new(vec![], None);
|
||||
|
||||
let db_stored_keys = client.key().find_many(vec![]).exec().await?;
|
||||
|
||||
let mut default = Uuid::default();
|
||||
|
||||
// collect and serialize the stored keys
|
||||
let stored_keys: Vec<StoredKey> = db_stored_keys
|
||||
.iter()
|
||||
.map(|d| {
|
||||
let d = d.clone();
|
||||
let uuid = uuid::Uuid::from_str(&d.uuid).unwrap();
|
||||
|
||||
if d.default {
|
||||
default = uuid;
|
||||
}
|
||||
|
||||
StoredKey {
|
||||
uuid,
|
||||
salt: to_array(d.salt).unwrap(),
|
||||
algorithm: Algorithm::deserialize(to_array(d.algorithm).unwrap()).unwrap(),
|
||||
content_salt: to_array(d.content_salt).unwrap(),
|
||||
master_key: to_array(d.master_key).unwrap(),
|
||||
master_key_nonce: d.master_key_nonce,
|
||||
key_nonce: d.key_nonce,
|
||||
key: d.key,
|
||||
hashing_algorithm: HashingAlgorithm::deserialize(
|
||||
to_array(d.hashing_algorithm).unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// insert all keys from the DB into the keymanager's keystore
|
||||
key_manager.populate_keystore(stored_keys).unwrap();
|
||||
|
||||
// if any key had an associated default tag
|
||||
if !default.is_nil() {
|
||||
key_manager.set_default(default).unwrap();
|
||||
}
|
||||
|
||||
Ok(key_manager)
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LibraryManagerError {
|
||||
#[error("error saving or loading the config from the filesystem")]
|
||||
|
@ -104,6 +58,8 @@ pub enum LibraryManagerError {
|
|||
InvalidDatabasePath(PathBuf),
|
||||
#[error("Failed to run seeder: {0}")]
|
||||
Seeder(#[from] SeederError),
|
||||
#[error("failed to initialise the key manager")]
|
||||
KeyManager(#[from] sd_crypto::Error),
|
||||
}
|
||||
|
||||
impl From<LibraryManagerError> for rspc::Error {
|
||||
|
@ -116,6 +72,58 @@ impl From<LibraryManagerError> for rspc::Error {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, LibraryManagerError> {
|
||||
// retrieve all stored keys from the DB
|
||||
let key_manager = KeyManager::new(vec![], None);
|
||||
|
||||
let db_stored_keys = client.key().find_many(vec![]).exec().await?;
|
||||
|
||||
let mut default = Uuid::default();
|
||||
|
||||
// collect and serialize the stored keys
|
||||
// shouldn't call unwrap so much here
|
||||
let stored_keys: Vec<StoredKey> = db_stored_keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
let key = key.clone();
|
||||
|
||||
let uuid = uuid::Uuid::from_str(&key.uuid).unwrap();
|
||||
|
||||
if key.default {
|
||||
default = uuid;
|
||||
}
|
||||
|
||||
StoredKey {
|
||||
uuid,
|
||||
salt: to_array(key.salt).unwrap(),
|
||||
algorithm: Algorithm::deserialize(to_array(key.algorithm).unwrap()).unwrap(),
|
||||
content_salt: to_array(key.content_salt).unwrap(),
|
||||
master_key: to_array(key.master_key).unwrap(),
|
||||
master_key_nonce: key.master_key_nonce,
|
||||
key_nonce: key.key_nonce,
|
||||
key: key.key,
|
||||
hashing_algorithm: HashingAlgorithm::deserialize(
|
||||
to_array(key.hashing_algorithm).unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// insert all keys from the DB into the keymanager's keystore
|
||||
key_manager.populate_keystore(stored_keys)?;
|
||||
|
||||
// if any key had an associated default tag
|
||||
if !default.is_nil() {
|
||||
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)
|
||||
}
|
||||
|
||||
impl LibraryManager {
|
||||
pub(crate) async fn new(
|
||||
libraries_dir: PathBuf,
|
||||
|
@ -318,7 +326,7 @@ impl LibraryManager {
|
|||
// Run seeders
|
||||
indexer_rules_seeder(&db).await?;
|
||||
|
||||
let key_manager = Arc::new(Mutex::new(create_keymanager(&db).await?));
|
||||
let key_manager = Arc::new(create_keymanager(&db).await?);
|
||||
|
||||
Ok(LibraryContext {
|
||||
id,
|
||||
|
|
|
@ -29,6 +29,14 @@ thiserror = "1.0.37"
|
|||
# metadata de/serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde-big-array = "0.4.1"
|
||||
|
||||
uuid = { version = "1.1.2", features = ["v4", "serde"] }
|
||||
|
||||
dashmap = "5.4.0"
|
||||
|
||||
rspc = { workspace = true, optional = true }
|
||||
specta = { workspace = true }
|
||||
|
||||
[features]
|
||||
rpsc = ["rspc"]
|
|
@ -7,12 +7,14 @@ use aead::{
|
|||
};
|
||||
use aes_gcm::Aes256Gcm;
|
||||
use chacha20poly1305::XChaCha20Poly1305;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{error::Error, primitives::BLOCK_SIZE, Protected};
|
||||
use crate::{primitives::BLOCK_SIZE, Error, Protected, Result};
|
||||
|
||||
/// These are all possible algorithms that can be used for encryption and decryption
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Type, Serialize, Deserialize)]
|
||||
#[allow(clippy::use_self)]
|
||||
pub enum Algorithm {
|
||||
XChaCha20Poly1305,
|
||||
|
@ -45,11 +47,7 @@ impl StreamEncryption {
|
|||
///
|
||||
/// The master key, a suitable nonce, and a specific algorithm should be provided.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(
|
||||
key: Protected<[u8; 32]>,
|
||||
nonce: &[u8],
|
||||
algorithm: Algorithm,
|
||||
) -> Result<Self, Error> {
|
||||
pub fn new(key: Protected<[u8; 32]>, nonce: &[u8], algorithm: Algorithm) -> Result<Self> {
|
||||
if nonce.len() != algorithm.nonce_len() {
|
||||
return Err(Error::NonceLengthMismatch);
|
||||
}
|
||||
|
@ -103,12 +101,7 @@ impl StreamEncryption {
|
|||
/// It requires a reader, a writer, and any AAD to go with it.
|
||||
///
|
||||
/// The AAD will be authenticated with each block of data.
|
||||
pub fn encrypt_streams<R, W>(
|
||||
mut self,
|
||||
mut reader: R,
|
||||
mut writer: W,
|
||||
aad: &[u8],
|
||||
) -> Result<(), Error>
|
||||
pub fn encrypt_streams<R, W>(mut self, mut reader: R, mut writer: W, aad: &[u8]) -> Result<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
W: Write + Seek,
|
||||
|
@ -181,7 +174,7 @@ impl StreamEncryption {
|
|||
algorithm: Algorithm,
|
||||
bytes: &[u8],
|
||||
aad: &[u8],
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
) -> Result<Vec<u8>> {
|
||||
let mut reader = Cursor::new(bytes);
|
||||
let mut writer = Cursor::new(Vec::<u8>::new());
|
||||
|
||||
|
@ -199,11 +192,7 @@ impl StreamDecryption {
|
|||
///
|
||||
/// The master key, nonce and algorithm that were used for encryption should be provided.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(
|
||||
key: Protected<[u8; 32]>,
|
||||
nonce: &[u8],
|
||||
algorithm: Algorithm,
|
||||
) -> Result<Self, Error> {
|
||||
pub fn new(key: Protected<[u8; 32]>, nonce: &[u8], algorithm: Algorithm) -> Result<Self> {
|
||||
if nonce.len() != algorithm.nonce_len() {
|
||||
return Err(Error::NonceLengthMismatch);
|
||||
}
|
||||
|
@ -257,12 +246,7 @@ impl StreamDecryption {
|
|||
/// It requires a reader, a writer, and any AAD that was used.
|
||||
///
|
||||
/// The AAD will be authenticated with each block of data - if the AAD doesn't match what was used during encryption, an error will be returned.
|
||||
pub fn decrypt_streams<R, W>(
|
||||
mut self,
|
||||
mut reader: R,
|
||||
mut writer: W,
|
||||
aad: &[u8],
|
||||
) -> Result<(), Error>
|
||||
pub fn decrypt_streams<R, W>(mut self, mut reader: R, mut writer: W, aad: &[u8]) -> Result<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
W: Write + Seek,
|
||||
|
@ -332,7 +316,7 @@ impl StreamDecryption {
|
|||
algorithm: Algorithm,
|
||||
bytes: &[u8],
|
||||
aad: &[u8],
|
||||
) -> Result<Protected<Vec<u8>>, Error> {
|
||||
) -> Result<Protected<Vec<u8>>> {
|
||||
let mut reader = Cursor::new(bytes);
|
||||
let mut writer = Cursor::new(Vec::<u8>::new());
|
||||
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
//! This module contains all possible errors that this crate can return.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "rspc")]
|
||||
impl From<Error> for rspc::Error {
|
||||
fn from(err: Error) -> Self {
|
||||
Self::new(rspc::ErrorCode::InternalServerError, err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// This enum defines all possible errors that this crate can give
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
|
@ -44,4 +54,12 @@ pub enum Error {
|
|||
NoMasterPassword,
|
||||
#[error("mismatch between supplied keys and the keystore")]
|
||||
KeystoreMismatch,
|
||||
#[error("mutex lock error")]
|
||||
MutexLock,
|
||||
}
|
||||
|
||||
impl<T> From<std::sync::PoisonError<T>> for Error {
|
||||
fn from(_: std::sync::PoisonError<T>) -> Self {
|
||||
Self::MutexLock
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,9 +33,8 @@ use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
|||
|
||||
use crate::{
|
||||
crypto::stream::Algorithm,
|
||||
error::Error,
|
||||
primitives::{generate_nonce, MASTER_KEY_LEN},
|
||||
Protected,
|
||||
Error, Protected, Result,
|
||||
};
|
||||
|
||||
use super::{keyslot::Keyslot, metadata::Metadata, preview_media::PreviewMedia};
|
||||
|
@ -104,7 +103,7 @@ impl FileHeader {
|
|||
pub fn decrypt_master_key(
|
||||
&self,
|
||||
password: Protected<Vec<u8>>,
|
||||
) -> Result<Protected<[u8; MASTER_KEY_LEN]>, Error> {
|
||||
) -> Result<Protected<[u8; MASTER_KEY_LEN]>> {
|
||||
let mut master_key = [0u8; MASTER_KEY_LEN];
|
||||
|
||||
if self.keyslots.is_empty() {
|
||||
|
@ -125,7 +124,7 @@ impl FileHeader {
|
|||
}
|
||||
|
||||
/// This is a helper function to serialize and write a header to a file.
|
||||
pub fn write<W>(&self, writer: &mut W) -> Result<(), Error>
|
||||
pub fn write<W>(&self, writer: &mut W) -> Result<()>
|
||||
where
|
||||
W: Write + Seek,
|
||||
{
|
||||
|
@ -142,7 +141,7 @@ impl FileHeader {
|
|||
pub fn decrypt_master_key_from_prehashed(
|
||||
&self,
|
||||
hashed_keys: Vec<Protected<[u8; 32]>>,
|
||||
) -> Result<Protected<[u8; MASTER_KEY_LEN]>, Error> {
|
||||
) -> Result<Protected<[u8; MASTER_KEY_LEN]>> {
|
||||
let mut master_key = [0u8; MASTER_KEY_LEN];
|
||||
|
||||
if self.keyslots.is_empty() {
|
||||
|
@ -189,7 +188,7 @@ impl FileHeader {
|
|||
/// This will include keyslots, metadata and preview media (if provided)
|
||||
///
|
||||
/// An error will be returned if there are no keyslots/more than two keyslots attached.
|
||||
pub fn serialize(&self) -> Result<Vec<u8>, Error> {
|
||||
pub fn serialize(&self) -> Result<Vec<u8>> {
|
||||
match self.version {
|
||||
FileHeaderVersion::V1 => {
|
||||
if self.keyslots.len() > 2 {
|
||||
|
@ -231,7 +230,7 @@ impl FileHeader {
|
|||
/// On error, the cursor will not be rewound.
|
||||
///
|
||||
/// It returns both the header, and the AAD that should be used for decryption.
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<(Self, Vec<u8>), Error>
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<(Self, Vec<u8>)>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
|
|
|
@ -25,10 +25,9 @@ use std::io::{Read, Seek};
|
|||
|
||||
use crate::{
|
||||
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
|
||||
error::Error,
|
||||
keys::hashing::HashingAlgorithm,
|
||||
primitives::{generate_nonce, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN, SALT_LEN},
|
||||
Protected,
|
||||
Error, Protected, Result,
|
||||
};
|
||||
|
||||
/// A keyslot - 96 bytes (as of V1), and contains all the information for future-proofing while keeping the size reasonable
|
||||
|
@ -65,7 +64,7 @@ impl Keyslot {
|
|||
salt: [u8; SALT_LEN],
|
||||
password: Protected<Vec<u8>>,
|
||||
master_key: &Protected<[u8; MASTER_KEY_LEN]>,
|
||||
) -> Result<Self, Error> {
|
||||
) -> Result<Self> {
|
||||
let nonce = generate_nonce(algorithm);
|
||||
|
||||
let hashed_password = hashing_algorithm.hash(password, salt)?;
|
||||
|
@ -93,10 +92,7 @@ impl Keyslot {
|
|||
/// This attempts to decrypt the master key for a single keyslot
|
||||
///
|
||||
/// An error will be returned on failure.
|
||||
pub fn decrypt_master_key(
|
||||
&self,
|
||||
password: &Protected<Vec<u8>>,
|
||||
) -> Result<Protected<Vec<u8>>, Error> {
|
||||
pub fn decrypt_master_key(&self, password: &Protected<Vec<u8>>) -> Result<Protected<Vec<u8>>> {
|
||||
let key = self
|
||||
.hashing_algorithm
|
||||
.hash(password.clone(), self.salt)
|
||||
|
@ -115,7 +111,7 @@ impl Keyslot {
|
|||
pub fn decrypt_master_key_from_prehashed(
|
||||
&self,
|
||||
key: Protected<[u8; 32]>,
|
||||
) -> Result<Protected<Vec<u8>>, Error> {
|
||||
) -> Result<Protected<Vec<u8>>> {
|
||||
StreamDecryption::decrypt_bytes(key, &self.nonce, self.algorithm, &self.master_key, &[])
|
||||
}
|
||||
|
||||
|
@ -142,7 +138,7 @@ impl Keyslot {
|
|||
/// It will leave the cursor at the end of the keyslot on success
|
||||
///
|
||||
/// The cursor will not be rewound on error.
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<Self, Error>
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
|
|
|
@ -31,9 +31,8 @@ use std::io::{Read, Seek};
|
|||
|
||||
use crate::{
|
||||
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
|
||||
error::Error,
|
||||
primitives::{generate_nonce, MASTER_KEY_LEN},
|
||||
Protected,
|
||||
Error, Protected, Result,
|
||||
};
|
||||
|
||||
use super::file::FileHeader;
|
||||
|
@ -70,7 +69,7 @@ impl FileHeader {
|
|||
algorithm: Algorithm,
|
||||
master_key: &Protected<[u8; MASTER_KEY_LEN]>,
|
||||
metadata: &T,
|
||||
) -> Result<(), Error>
|
||||
) -> Result<()>
|
||||
where
|
||||
T: ?Sized + serde::Serialize,
|
||||
{
|
||||
|
@ -104,7 +103,7 @@ impl FileHeader {
|
|||
pub fn decrypt_metadata_from_prehashed<T>(
|
||||
&self,
|
||||
hashed_keys: Vec<Protected<[u8; 32]>>,
|
||||
) -> Result<T, Error>
|
||||
) -> Result<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
|
@ -131,7 +130,7 @@ impl FileHeader {
|
|||
/// All it requires is a password. Hashing is handled for you.
|
||||
///
|
||||
/// A deserialized data type will be returned from this function
|
||||
pub fn decrypt_metadata<T>(&self, password: Protected<Vec<u8>>) -> Result<T, Error>
|
||||
pub fn decrypt_metadata<T>(&self, password: Protected<Vec<u8>>) -> Result<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
|
@ -189,7 +188,7 @@ impl Metadata {
|
|||
/// The cursor will be left at the end of the metadata item on success
|
||||
///
|
||||
/// The cursor will not be rewound on error.
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<Self, Error>
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
|
|
|
@ -24,9 +24,8 @@ use std::io::{Read, Seek};
|
|||
|
||||
use crate::{
|
||||
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
|
||||
error::Error,
|
||||
primitives::{generate_nonce, MASTER_KEY_LEN},
|
||||
Protected,
|
||||
Error, Protected, Result,
|
||||
};
|
||||
|
||||
use super::file::FileHeader;
|
||||
|
@ -63,7 +62,7 @@ impl FileHeader {
|
|||
algorithm: Algorithm,
|
||||
master_key: &Protected<[u8; MASTER_KEY_LEN]>,
|
||||
media: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<()> {
|
||||
let media_nonce = generate_nonce(algorithm);
|
||||
|
||||
let encrypted_media = StreamEncryption::encrypt_bytes(
|
||||
|
@ -94,7 +93,7 @@ impl FileHeader {
|
|||
pub fn decrypt_preview_media_from_prehashed(
|
||||
&self,
|
||||
hashed_keys: Vec<Protected<[u8; 32]>>,
|
||||
) -> Result<Protected<Vec<u8>>, Error> {
|
||||
) -> Result<Protected<Vec<u8>>> {
|
||||
let master_key = self.decrypt_master_key_from_prehashed(hashed_keys)?;
|
||||
|
||||
// could be an expensive clone (a few MiB at most)
|
||||
|
@ -121,7 +120,7 @@ impl FileHeader {
|
|||
pub fn decrypt_preview_media(
|
||||
&self,
|
||||
password: Protected<Vec<u8>>,
|
||||
) -> Result<Protected<Vec<u8>>, Error> {
|
||||
) -> Result<Protected<Vec<u8>>> {
|
||||
let master_key = self.decrypt_master_key(password)?;
|
||||
|
||||
// could be an expensive clone (a few MiB at most)
|
||||
|
@ -176,7 +175,7 @@ impl PreviewMedia {
|
|||
/// The cursor will be left at the end of the preview media item on success
|
||||
///
|
||||
/// The cursor will not be rewound on error.
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<Self, Error>
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
//! It contains `byte -> enum` and `enum -> byte` conversions for everything that could be written to a header (except headers, keyslots, and other header items)
|
||||
use crate::{
|
||||
crypto::stream::Algorithm,
|
||||
error::Error,
|
||||
keys::hashing::{HashingAlgorithm, Params},
|
||||
Error, Result,
|
||||
};
|
||||
|
||||
use super::{
|
||||
|
@ -20,7 +20,7 @@ impl FileHeaderVersion {
|
|||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self> {
|
||||
match bytes {
|
||||
[0x0A, 0x01] => Ok(Self::V1),
|
||||
_ => Err(Error::FileHeader),
|
||||
|
@ -36,7 +36,7 @@ impl KeyslotVersion {
|
|||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self> {
|
||||
match bytes {
|
||||
[0x0D, 0x01] => Ok(Self::V1),
|
||||
_ => Err(Error::FileHeader),
|
||||
|
@ -52,7 +52,7 @@ impl PreviewMediaVersion {
|
|||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self> {
|
||||
match bytes {
|
||||
[0x0E, 0x01] => Ok(Self::V1),
|
||||
_ => Err(Error::FileHeader),
|
||||
|
@ -68,7 +68,7 @@ impl MetadataVersion {
|
|||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self> {
|
||||
match bytes {
|
||||
[0x1F, 0x01] => Ok(Self::V1),
|
||||
_ => Err(Error::FileHeader),
|
||||
|
@ -88,7 +88,7 @@ impl HashingAlgorithm {
|
|||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self> {
|
||||
match bytes {
|
||||
[0x0F, 0x01] => Ok(Self::Argon2id(Params::Standard)),
|
||||
[0x0F, 0x02] => Ok(Self::Argon2id(Params::Hardened)),
|
||||
|
@ -107,7 +107,7 @@ impl Algorithm {
|
|||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self> {
|
||||
match bytes {
|
||||
[0x0B, 0x01] => Ok(Self::XChaCha20Poly1305),
|
||||
[0x0B, 0x02] => Ok(Self::Aes256Gcm),
|
||||
|
|
|
@ -11,13 +11,15 @@
|
|||
//! let hashed_password = hashing_algorithm.hash(password, salt).unwrap();
|
||||
//! ```
|
||||
use crate::Protected;
|
||||
use crate::{error::Error, primitives::SALT_LEN};
|
||||
use crate::{primitives::SALT_LEN, Error, Result};
|
||||
use argon2::Argon2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
/// These parameters define the password-hashing level.
|
||||
///
|
||||
/// The harder the parameter, the longer the password will take to hash.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Type, Serialize, Deserialize)]
|
||||
#[allow(clippy::use_self)]
|
||||
pub enum Params {
|
||||
Standard,
|
||||
|
@ -26,20 +28,11 @@ pub enum Params {
|
|||
}
|
||||
|
||||
/// This defines all available password hashing algorithms.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Type, Serialize, Deserialize)]
|
||||
pub enum HashingAlgorithm {
|
||||
Argon2id(Params),
|
||||
}
|
||||
|
||||
/// This is so we can iterate over all hashing algorithms and parameters.
|
||||
///
|
||||
/// The main usage is for pre-hashing a key during mounting.
|
||||
pub const HASHING_ALGORITHM_LIST: [HashingAlgorithm; 3] = [
|
||||
HashingAlgorithm::Argon2id(Params::Standard),
|
||||
HashingAlgorithm::Argon2id(Params::Hardened),
|
||||
HashingAlgorithm::Argon2id(Params::Paranoid),
|
||||
];
|
||||
|
||||
impl HashingAlgorithm {
|
||||
/// This function should be used to hash passwords
|
||||
///
|
||||
|
@ -48,7 +41,7 @@ impl HashingAlgorithm {
|
|||
&self,
|
||||
password: Protected<Vec<u8>>,
|
||||
salt: [u8; SALT_LEN],
|
||||
) -> Result<Protected<[u8; 32]>, Error> {
|
||||
) -> Result<Protected<[u8; 32]>> {
|
||||
match self {
|
||||
Self::Argon2id(params) => password_hash_argon2id(password, salt, *params),
|
||||
}
|
||||
|
@ -91,7 +84,7 @@ pub fn password_hash_argon2id(
|
|||
password: Protected<Vec<u8>>,
|
||||
salt: [u8; SALT_LEN],
|
||||
params: Params,
|
||||
) -> Result<Protected<[u8; 32]>, Error> {
|
||||
) -> Result<Protected<[u8; 32]>> {
|
||||
let mut key = [0u8; 32];
|
||||
|
||||
let argon2 = Argon2::new(
|
||||
|
|
|
@ -1,7 +1,43 @@
|
|||
use std::collections::HashMap;
|
||||
//! This module contains Spacedrive's key manager implementation.
|
||||
//!
|
||||
//! The key manager is used for keeping track of keys within memory, and mounting them on demand.
|
||||
//!
|
||||
//! The key manager is initialised, and added to a global state so it is accessible everywhere.
|
||||
//! It is also populated with all keys from the Prisma database.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! use sd_crypto::keys::keymanager::KeyManager;
|
||||
//! use sd_crypto::Protected;
|
||||
//! use sd_crypto::crypto::stream::Algorithm;
|
||||
//! use sd_crypto::keys::hashing::{HashingAlgorithm, Params};
|
||||
//!
|
||||
//! let master_password = Protected::new(b"password".to_vec());
|
||||
//!
|
||||
//! // Initialise a `Keymanager` with no stored keys and no master password
|
||||
//! let mut key_manager = KeyManager::new(vec![], None);
|
||||
//!
|
||||
//! // Set the master password
|
||||
//! key_manager.set_master_password(master_password);
|
||||
//!
|
||||
//! let new_password = Protected::new(b"super secure".to_vec());
|
||||
//!
|
||||
//! // Register the new key with the key manager
|
||||
//! let added_key = key_manager.add_to_keystore(new_password, Algorithm::XChaCha20Poly1305, HashingAlgorithm::Argon2id(Params::Standard)).unwrap();
|
||||
//!
|
||||
//! // Write the stored key to the database here (with `KeyManager::access_keystore()`)
|
||||
//!
|
||||
//! // Mount the key we just added (with the returned UUID)
|
||||
//! key_manager.mount(added_key);
|
||||
//!
|
||||
//! // Retrieve all currently mounted, hashed keys to pass to a decryption function.
|
||||
//! let keys = key_manager.enumerate_hashed_keys();
|
||||
//! ```
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::crypto::stream::{StreamDecryption, StreamEncryption};
|
||||
use crate::error::Error;
|
||||
use crate::primitives::{
|
||||
generate_master_key, generate_nonce, generate_salt, to_array, MASTER_KEY_LEN,
|
||||
};
|
||||
|
@ -10,10 +46,15 @@ use crate::{
|
|||
primitives::{ENCRYPTED_MASTER_KEY_LEN, SALT_LEN},
|
||||
Protected,
|
||||
};
|
||||
use crate::{Error, Result};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use serde::Serialize;
|
||||
use serde_big_array::BigArray;
|
||||
use specta::Type;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::hashing::{HashingAlgorithm, HASHING_ALGORITHM_LIST};
|
||||
use super::hashing::HashingAlgorithm;
|
||||
|
||||
// The terminology in this file is very confusing.
|
||||
// The `master_key` is specific to the `StoredKey`, and is just used internally for encryption.
|
||||
|
@ -22,24 +63,41 @@ use super::hashing::{HashingAlgorithm, HASHING_ALGORITHM_LIST};
|
|||
// The `hashed_key` refers to the value you'd pass to PVM/MD decryption functions. It has been pre-hashed with the content salt.
|
||||
// The content salt refers to the semi-universal salt that's used for metadata/preview media (unique to each key in the manager)
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[derive(Clone, PartialEq, Eq, Type, Serialize)]
|
||||
pub struct StoredKey {
|
||||
pub uuid: uuid::Uuid, // uuid for identification. shared with mounted keys
|
||||
pub algorithm: Algorithm, // encryption algorithm for encrypting the master key. can be changed (requires a re-encryption though)
|
||||
pub hashing_algorithm: HashingAlgorithm, // hashing algorithm to use for hashing everything related to this key. can't be changed once set.
|
||||
pub salt: [u8; SALT_LEN], // salt to hash the master password with
|
||||
pub content_salt: [u8; SALT_LEN], // salt used for file data
|
||||
#[serde(with = "BigArray")]
|
||||
pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], // this is for encrypting the `key`
|
||||
pub master_key_nonce: Vec<u8>, // nonce for encrypting the master key
|
||||
pub key_nonce: Vec<u8>, // nonce used for encrypting the main key
|
||||
pub key: Vec<u8>, // encrypted. the key stored in spacedrive (e.g. generated 64 char key)
|
||||
}
|
||||
|
||||
/// This is a mounted key, and needs to be kept somewhat hidden.
|
||||
///
|
||||
/// This contains the plaintext key, and the same key hashed with the content salt.
|
||||
#[derive(Clone)]
|
||||
pub struct MountedKey {
|
||||
pub uuid: Uuid, // used for identification. shared with stored keys
|
||||
pub key: Protected<Vec<u8>>, // the actual key itself, text format encodable (so it can be viewed with an UI)
|
||||
pub content_salt: [u8; SALT_LEN], // the salt used for file data
|
||||
pub hashed_key: Protected<[u8; 32]>, // this is hashed with the content salt, for instant access
|
||||
}
|
||||
|
||||
/// This is the key manager itself.
|
||||
///
|
||||
/// It contains the keystore, the keymount, the master password and the default key.
|
||||
///
|
||||
/// Use the associated functions to interact with it.
|
||||
pub struct KeyManager {
|
||||
master_password: Option<Protected<Vec<u8>>>, // the user's. we take ownership here to prevent other functions attempting to manage/pass it to us
|
||||
keystore: HashMap<Uuid, StoredKey>,
|
||||
keymount: HashMap<Uuid, MountedKey>,
|
||||
default: Option<Uuid>,
|
||||
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
|
||||
keystore: DashMap<Uuid, StoredKey>,
|
||||
keymount: DashMap<Uuid, MountedKey>,
|
||||
default: Mutex<Option<Uuid>>,
|
||||
}
|
||||
|
||||
/// The `KeyManager` functions should be used for all key-related management.
|
||||
|
@ -47,25 +105,25 @@ impl KeyManager {
|
|||
/// Initialize the Key Manager with the user's master password, and `StoredKeys` retrieved from Prisma
|
||||
#[must_use]
|
||||
pub fn new(stored_keys: Vec<StoredKey>, master_password: Option<Protected<Vec<u8>>>) -> Self {
|
||||
let mut keystore = HashMap::new();
|
||||
let keystore = DashMap::new();
|
||||
for key in stored_keys {
|
||||
keystore.insert(key.uuid, key);
|
||||
}
|
||||
|
||||
let keymount: HashMap<Uuid, MountedKey> = HashMap::new();
|
||||
let keymount: DashMap<Uuid, MountedKey> = DashMap::new();
|
||||
|
||||
Self {
|
||||
master_password,
|
||||
master_password: Mutex::new(master_password),
|
||||
keystore,
|
||||
keymount,
|
||||
default: None,
|
||||
default: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn populate_keystore(&mut self, stored_keys: Vec<StoredKey>) -> Result<(), Error> {
|
||||
pub fn populate_keystore(&self, stored_keys: Vec<StoredKey>) -> Result<()> {
|
||||
for key in stored_keys {
|
||||
self.keystore.insert(key.uuid, key);
|
||||
}
|
||||
|
@ -73,10 +131,34 @@ impl KeyManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// This allows you to set the default key
|
||||
pub fn set_default(&mut self, uuid: Uuid) -> Result<(), Error> {
|
||||
/// This function removes a key from the keystore, the keymount and it's unset as the default.
|
||||
pub fn remove_key(&self, uuid: Uuid) -> Result<()> {
|
||||
if self.keystore.contains_key(&uuid) {
|
||||
self.default = Some(uuid);
|
||||
// if key is default, clear it
|
||||
// do this manually to prevent deadlocks
|
||||
let mut default = self.default.lock()?;
|
||||
if *default == Some(uuid) {
|
||||
*default = None;
|
||||
}
|
||||
drop(default);
|
||||
|
||||
// unmount if mounted
|
||||
if self.keymount.contains_key(&uuid) {
|
||||
// use remove as unmount calls the checks that we just did
|
||||
self.keymount.remove(&uuid);
|
||||
}
|
||||
|
||||
// remove from keystore
|
||||
self.keystore.remove(&uuid);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This allows you to set the default key
|
||||
pub fn set_default(&self, uuid: Uuid) -> Result<()> {
|
||||
if self.keystore.contains_key(&uuid) {
|
||||
*self.default.lock()? = Some(uuid);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::KeyNotFound)
|
||||
|
@ -84,43 +166,59 @@ impl KeyManager {
|
|||
}
|
||||
|
||||
/// This allows you to get the default key's ID
|
||||
pub const fn get_default(&self) -> Result<Uuid, Error> {
|
||||
if let Some(default) = self.default {
|
||||
pub fn get_default(&self) -> Result<Uuid> {
|
||||
if let Some(default) = *self.default.lock()? {
|
||||
Ok(default)
|
||||
} else {
|
||||
Err(Error::NoDefaultKeySet)
|
||||
}
|
||||
}
|
||||
|
||||
/// This should ONLY be used within the key manager
|
||||
fn get_master_password(&self) -> Result<Protected<Vec<u8>>, Error> {
|
||||
match &self.master_password {
|
||||
pub fn clear_default(&self) -> Result<()> {
|
||||
let mut default = self.default.lock()?;
|
||||
|
||||
if default.is_some() {
|
||||
*default = None;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::NoDefaultKeySet)
|
||||
}
|
||||
}
|
||||
|
||||
/// This should ONLY be used internally.
|
||||
fn get_master_password(&self) -> Result<Protected<Vec<u8>>> {
|
||||
let master_password = self.master_password.lock()?;
|
||||
match &*master_password {
|
||||
Some(k) => Ok(k.clone()),
|
||||
None => Err(Error::NoMasterPassword),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_master_password(
|
||||
&mut self,
|
||||
master_password: Protected<Vec<u8>>,
|
||||
) -> Result<(), Error> {
|
||||
pub fn set_master_password(&self, master_password: Protected<Vec<u8>>) -> Result<()> {
|
||||
// this returns a result, so we can potentially implement password checking functionality
|
||||
self.master_password = Some(master_password);
|
||||
*self.master_password.lock()? = Some(master_password);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn has_master_password(&self) -> bool {
|
||||
self.master_password.is_some()
|
||||
/// This function is for removing a previously-added master password
|
||||
pub fn clear_master_password(&self) -> Result<()> {
|
||||
*self.master_password.lock()? = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_master_password(&self) -> Result<bool> {
|
||||
Ok(self.master_password.lock()?.is_some())
|
||||
}
|
||||
|
||||
/// This function is used for emptying the entire keystore.
|
||||
pub fn empty_keystore(&mut self) {
|
||||
pub fn empty_keystore(&self) {
|
||||
self.keystore.clear();
|
||||
}
|
||||
|
||||
/// This function is used for unmounting all keys at once.
|
||||
pub fn empty_keymount(&mut self) {
|
||||
pub fn empty_keymount(&self) {
|
||||
// i'm unsure whether or not `.clear()` also calls drop
|
||||
// if it doesn't, we're going to need to find another way to call drop on these values
|
||||
// that way they will be zeroized and removed from memory fully
|
||||
|
@ -128,18 +226,18 @@ impl KeyManager {
|
|||
}
|
||||
|
||||
/// This function can be used for comparing an array of `StoredKeys` to the currently loaded keystore.
|
||||
pub fn compare_keystore(&self, supplied_keys: &[StoredKey]) -> Result<(), Error> {
|
||||
pub fn compare_keystore(&self, supplied_keys: &[StoredKey]) -> Result<()> {
|
||||
if supplied_keys.len() != self.keystore.len() {
|
||||
return Err(Error::KeystoreMismatch);
|
||||
}
|
||||
|
||||
for key in supplied_keys {
|
||||
let keystore_key = match self.keystore.get(&key.uuid) {
|
||||
Some(key) => key,
|
||||
Some(key) => key.clone(),
|
||||
None => return Err(Error::KeystoreMismatch),
|
||||
};
|
||||
|
||||
if key != keystore_key {
|
||||
if *key != keystore_key {
|
||||
return Err(Error::KeystoreMismatch);
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +245,10 @@ impl KeyManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unmount(&mut self, uuid: Uuid) -> Result<(), Error> {
|
||||
/// This function is for unmounting a key from the key manager
|
||||
///
|
||||
/// This does not remove the key from the key store
|
||||
pub fn unmount(&self, uuid: Uuid) -> Result<()> {
|
||||
if self.keymount.contains_key(&uuid) {
|
||||
self.keymount.remove(&uuid);
|
||||
Ok(())
|
||||
|
@ -158,22 +259,25 @@ impl KeyManager {
|
|||
|
||||
/// This function returns a Vec of `StoredKey`s, so you can write them somewhere/update the database with them/etc
|
||||
///
|
||||
/// The database and keystore should be in sync at ALL times
|
||||
pub fn dump_keystore(&self) -> Result<Vec<StoredKey>, Error> {
|
||||
let mut keys = Vec::<StoredKey>::new();
|
||||
|
||||
for key in self.keystore.values() {
|
||||
keys.push(key.clone());
|
||||
/// The database and keystore should be in sync at ALL times (unless the user chose an in-memory only key)
|
||||
#[must_use]
|
||||
pub fn dump_keystore(&self) -> Vec<StoredKey> {
|
||||
self.keystore.iter().map(|key| key.clone()).collect()
|
||||
}
|
||||
|
||||
Ok(keys)
|
||||
#[must_use]
|
||||
pub fn get_mounted_uuids(&self) -> Vec<Uuid> {
|
||||
self.keymount.iter().map(|key| key.uuid).collect()
|
||||
}
|
||||
|
||||
/// This function does not return a value by design.
|
||||
///
|
||||
/// Once a key is mounted, access it with `KeyManager::access()`
|
||||
///
|
||||
/// This is to ensure that only functions which require access to the mounted key receive it.
|
||||
///
|
||||
/// We could add a log to this, so that the user can view mounts
|
||||
pub fn mount(&mut self, uuid: Uuid) -> Result<(), Error> {
|
||||
pub fn mount(&self, uuid: Uuid) -> Result<()> {
|
||||
match self.keystore.get(&uuid) {
|
||||
Some(stored_key) => {
|
||||
let master_password = self.get_master_password()?;
|
||||
|
@ -207,20 +311,17 @@ impl KeyManager {
|
|||
&[],
|
||||
)?;
|
||||
|
||||
let mut hashed_keys = Vec::<Protected<[u8; 32]>>::new();
|
||||
|
||||
// Hash the StoredKey using each available password hashing parameter, so all content is accessible no matter the settings.
|
||||
// It makes key mounting more expensive, but it allows for greater UX and customizability.
|
||||
for hashing_algorithm in HASHING_ALGORITHM_LIST {
|
||||
hashed_keys.push(hashing_algorithm.hash(key.clone(), stored_key.content_salt)?);
|
||||
}
|
||||
// Hash the key once with the parameters/algorithm the user selected during first mount
|
||||
let hashed_key = stored_key
|
||||
.hashing_algorithm
|
||||
.hash(key.clone(), stored_key.content_salt)?;
|
||||
|
||||
// Construct the MountedKey and insert it into the Keymount
|
||||
let mounted_key = MountedKey {
|
||||
uuid: stored_key.uuid,
|
||||
key,
|
||||
content_salt: stored_key.content_salt,
|
||||
hashed_keys,
|
||||
hashed_key,
|
||||
};
|
||||
|
||||
self.keymount.insert(uuid, mounted_key);
|
||||
|
@ -234,21 +335,34 @@ impl KeyManager {
|
|||
/// This function is for accessing the internal keymount.
|
||||
///
|
||||
/// We could add a log to this, so that the user can view accesses
|
||||
pub fn access_keymount(&self, uuid: Uuid) -> Result<MountedKey, Error> {
|
||||
pub fn access_keymount(&self, uuid: Uuid) -> Result<MountedKey> {
|
||||
match self.keymount.get(&uuid) {
|
||||
Some(key) => Ok(key.clone()),
|
||||
None => Err(Error::KeyNotFound),
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is for accessing a `StoredKey` from an ID.
|
||||
pub fn access_keystore(&self, uuid: Uuid) -> Result<StoredKey, Error> {
|
||||
/// This function is for accessing a `StoredKey`.
|
||||
pub fn access_keystore(&self, uuid: Uuid) -> Result<StoredKey> {
|
||||
match self.keystore.get(&uuid) {
|
||||
Some(key) => Ok(key.clone()),
|
||||
None => Err(Error::KeyNotFound),
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is for getting an entire collection of hashed keys.
|
||||
///
|
||||
/// These are ideal for passing over to decryption functions, as each decryption attempt is negligible, performance wise.
|
||||
///
|
||||
/// This means we don't need to keep super specific track of which key goes to which file, and we can just throw all of them at it.
|
||||
#[must_use]
|
||||
pub fn enumerate_hashed_keys(&self) -> Vec<Protected<[u8; 32]>> {
|
||||
self.keymount
|
||||
.iter()
|
||||
.map(|mounted_key| mounted_key.hashed_key.clone())
|
||||
.collect::<Vec<Protected<[u8; 32]>>>()
|
||||
}
|
||||
|
||||
/// This function is used to add a new key/password to the keystore.
|
||||
///
|
||||
/// You should use this when a new key is added, as it will generate salts/nonces/etc.
|
||||
|
@ -260,11 +374,11 @@ impl KeyManager {
|
|||
/// You may use the returned ID to identify this key.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn add_to_keystore(
|
||||
&mut self,
|
||||
&self,
|
||||
key: Protected<Vec<u8>>,
|
||||
algorithm: Algorithm,
|
||||
hashing_algorithm: HashingAlgorithm,
|
||||
) -> Result<Uuid, Error> {
|
||||
) -> Result<Uuid> {
|
||||
let master_password = self.get_master_password()?;
|
||||
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
|
@ -312,12 +426,3 @@ impl KeyManager {
|
|||
Ok(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
// derive explicit CLONES only
|
||||
#[derive(Clone)]
|
||||
pub struct MountedKey {
|
||||
pub uuid: Uuid, // used for identification. shared with stored keys
|
||||
pub key: Protected<Vec<u8>>, // the actual key itself, text format encodable (so it can be viewed with an UI)
|
||||
pub content_salt: [u8; SALT_LEN], // the salt used for file data
|
||||
pub hashed_keys: Vec<Protected<[u8; 32]>>, // this is hashed with the content salt, for instant access
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#![allow(clippy::missing_errors_doc)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
#![allow(clippy::similar_names)]
|
||||
#![allow(clippy::option_if_let_else)]
|
||||
|
||||
pub mod crypto;
|
||||
pub mod error;
|
||||
|
@ -26,3 +27,5 @@ pub use protected::Protected;
|
|||
|
||||
// Re-export zeroize so it can be used elsewhere
|
||||
pub use zeroize::Zeroize;
|
||||
|
||||
pub use self::error::{Error, Result};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
use rand::{RngCore, SeedableRng};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{crypto::stream::Algorithm, error::Error, Protected};
|
||||
use crate::{crypto::stream::Algorithm, Error, Protected, Result};
|
||||
|
||||
/// This is the default salt size, and the recommended size for argon2id.
|
||||
pub const SALT_LEN: usize = 16;
|
||||
|
@ -62,7 +62,7 @@ pub fn generate_master_key() -> Protected<[u8; MASTER_KEY_LEN]> {
|
|||
/// As the master key is encrypted at this point, it does not need to be `Protected<>`
|
||||
///
|
||||
/// This function still `zeroize`s any data it can
|
||||
pub fn to_array<const I: usize>(bytes: Vec<u8>) -> Result<[u8; I], Error> {
|
||||
pub fn to_array<const I: usize>(bytes: Vec<u8>) -> Result<[u8; I]> {
|
||||
bytes.try_into().map_err(|mut b: Vec<u8>| {
|
||||
b.zeroize();
|
||||
Error::VecArrSizeMismatch
|
||||
|
|
|
@ -8,6 +8,9 @@ export type Procedures = {
|
|||
{ key: "jobs.getHistory", input: LibraryArgs<null>, result: Array<JobReport> } |
|
||||
{ key: "jobs.getRunning", input: LibraryArgs<null>, result: Array<JobReport> } |
|
||||
{ key: "jobs.isRunning", input: LibraryArgs<null>, result: boolean } |
|
||||
{ key: "keys.getDefault", input: LibraryArgs<null>, result: string | null } |
|
||||
{ key: "keys.list", input: LibraryArgs<null>, result: Array<StoredKey> } |
|
||||
{ key: "keys.listMounted", input: LibraryArgs<null>, result: Array<string> } |
|
||||
{ key: "library.getStatistics", input: LibraryArgs<null>, result: Statistics } |
|
||||
{ key: "library.list", input: never, result: Array<LibraryConfigWrapped> } |
|
||||
{ key: "locations.getById", input: LibraryArgs<number>, result: Location | null } |
|
||||
|
@ -33,6 +36,14 @@ export type Procedures = {
|
|||
{ key: "jobs.generateThumbsForLocation", input: LibraryArgs<GenerateThumbsForLocationArgs>, result: null } |
|
||||
{ key: "jobs.identifyUniqueFiles", input: LibraryArgs<IdentifyUniqueFilesArgs>, result: null } |
|
||||
{ key: "jobs.objectValidator", input: LibraryArgs<ObjectValidatorArgs>, result: null } |
|
||||
{ key: "keys.add", input: LibraryArgs<KeyAddArgs>, result: null } |
|
||||
{ key: "keys.deleteFromLibrary", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "keys.mount", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "keys.setDefault", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "keys.setMasterPassword", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "keys.unmount", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "keys.unmountAll", input: LibraryArgs<null>, result: null } |
|
||||
{ key: "keys.updateKeyName", input: LibraryArgs<KeyNameUpdateArgs>, result: null } |
|
||||
{ key: "library.create", input: string, result: LibraryConfigWrapped } |
|
||||
{ key: "library.delete", input: string, result: null } |
|
||||
{ key: "library.edit", input: EditLibraryArgs, result: null } |
|
||||
|
@ -52,6 +63,8 @@ export type Procedures = {
|
|||
{ key: "jobs.newThumbnail", input: LibraryArgs<null>, result: string }
|
||||
};
|
||||
|
||||
export type Algorithm = "XChaCha20Poly1305" | "Aes256Gcm"
|
||||
|
||||
export interface BuildInfo { version: string, commit: string }
|
||||
|
||||
export interface ConfigMetadata { version: string | null }
|
||||
|
@ -68,6 +81,8 @@ export interface FilePath { id: number, is_dir: boolean, location_id: number, ma
|
|||
|
||||
export interface GenerateThumbsForLocationArgs { id: number, path: string }
|
||||
|
||||
export type HashingAlgorithm = { Argon2id: Params }
|
||||
|
||||
export interface IdentifyUniqueFilesArgs { id: number, path: string }
|
||||
|
||||
export interface IndexerRule { id: number, kind: number, name: string, parameters: Array<number>, date_created: string, date_modified: string }
|
||||
|
@ -80,6 +95,10 @@ export interface JobReport { id: string, name: string, data: Array<number> | nul
|
|||
|
||||
export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused"
|
||||
|
||||
export interface KeyAddArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, key: string }
|
||||
|
||||
export interface KeyNameUpdateArgs { uuid: string, name: string }
|
||||
|
||||
export interface LibraryArgs<T> { library_id: string, arg: T }
|
||||
|
||||
export interface LibraryConfig { version: string | null, name: string, description: string }
|
||||
|
@ -112,6 +131,8 @@ export interface Object { id: number, cas_id: string, integrity_checksum: string
|
|||
|
||||
export interface ObjectValidatorArgs { id: number, path: string }
|
||||
|
||||
export type Params = "Standard" | "Hardened" | "Paranoid"
|
||||
|
||||
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
|
||||
|
||||
export interface SetFavoriteArgs { id: number, favorite: boolean }
|
||||
|
@ -120,6 +141,8 @@ export interface SetNoteArgs { id: number, note: string | null }
|
|||
|
||||
export interface Statistics { id: number, date_captured: string, total_object_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string }
|
||||
|
||||
export interface StoredKey { uuid: string, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, salt: Array<number>, content_salt: Array<number>, master_key: Array<number>, master_key_nonce: Array<number>, key_nonce: Array<number>, key: Array<number> }
|
||||
|
||||
export interface 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 TagAssignArgs { object_id: number, tag_id: number, unassign: boolean }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Button } from '@sd/ui';
|
||||
import { Button, ContextMenu } from '@sd/ui';
|
||||
import clsx from 'clsx';
|
||||
import { DotsThree, Eye, Key as KeyIcon } from 'phosphor-react';
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { DefaultProps } from '../primitive/types';
|
||||
import { Tooltip } from '../tooltip/Tooltip';
|
||||
|
||||
|
@ -17,11 +17,81 @@ export interface Key {
|
|||
objectCount?: number;
|
||||
containerCount?: number;
|
||||
};
|
||||
default?: boolean; // need to make use of this within the UI
|
||||
// Nodes this key is mounted on
|
||||
nodes?: string[]; // will be node object
|
||||
}
|
||||
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { animated, config, useTransition } from 'react-spring';
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
|
||||
interface Props extends DropdownMenu.MenuContentProps {
|
||||
trigger: React.ReactNode;
|
||||
transformOrigin?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const KeyDropdown = ({
|
||||
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",
|
||||
"pl-4 pr-4 pt-2 pb-2 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 const Key: React.FC<{ data: Key; index: number }> = ({ data, index }) => {
|
||||
const mountKey = useLibraryMutation('keys.mount');
|
||||
const unmountKey = useLibraryMutation('keys.unmount');
|
||||
const deleteKey = useLibraryMutation('keys.deleteFromLibrary');
|
||||
const setDefaultKey = useLibraryMutation('keys.setDefault');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
|
@ -43,6 +113,11 @@ export const Key: React.FC<{ data: Key; index: number }> = ({ data, index }) =>
|
|||
{data.nodes?.length || 0 > 0 ? `${data.nodes?.length || 0} nodes` : 'This node'}
|
||||
</div>
|
||||
)}
|
||||
{data.default && (
|
||||
<div className="inline ml-2 px-1 text-[8pt] font-medium text-gray-300 bg-gray-500 rounded">
|
||||
Default
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* <div className="text-xs text-gray-300 opacity-30">#{data.id}</div> */}
|
||||
{data.stats ? (
|
||||
|
@ -73,9 +148,24 @@ export const Key: React.FC<{ data: Key; index: number }> = ({ data, index }) =>
|
|||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<KeyDropdown trigger={
|
||||
<Button size="icon">
|
||||
<DotsThree className="w-4 h-4 text-ink-faint" />
|
||||
</Button>
|
||||
</Button> }>
|
||||
{data.mounted && (
|
||||
<DropdownMenu.DropdownMenuItem className="!cursor-default select-none text-menu-ink focus:outline-none py-0.5 active:opacity-80" onClick={(e) => { unmountKey.mutate(data.id) }}>Unmount</DropdownMenu.DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{!data.mounted && (
|
||||
<DropdownMenu.DropdownMenuItem className="!cursor-default select-none text-menu-ink focus:outline-none py-0.5 active:opacity-80" onClick={(e) => { mountKey.mutate(data.id) }}>Mount</DropdownMenu.DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenu.DropdownMenuItem className="!cursor-default select-none text-menu-ink focus:outline-none py-0.5 active:opacity-80" onClick={(e) => { deleteKey.mutate(data.id) }}>Delete from Library</DropdownMenu.DropdownMenuItem>
|
||||
|
||||
{!data.default && (
|
||||
<DropdownMenu.DropdownMenuItem className="!cursor-default select-none text-menu-ink focus:outline-none py-0.5 active:opacity-80" onClick={(e) => { setDefaultKey.mutate(data.id) }}>Set as Default</DropdownMenu.DropdownMenuItem>
|
||||
)}
|
||||
</KeyDropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,47 +1,66 @@
|
|||
import { Button } from '@sd/ui';
|
||||
import { useLibraryQuery, useLibraryMutation } from '@sd/client';
|
||||
import { Button, CategoryHeading } from '@sd/ui';
|
||||
|
||||
import { DefaultProps } from '../primitive/types';
|
||||
import { Key } from './Key';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export type KeyListProps = DefaultProps;
|
||||
|
||||
const ListKeys = () => {
|
||||
const keys = useLibraryQuery(['keys.list']);
|
||||
const mounted_uuids = useLibraryQuery(['keys.listMounted']);
|
||||
|
||||
// use a separate route so we get the default key from the key manager, not the database
|
||||
// sometimes the key won't be stored in the database
|
||||
const default_key = useLibraryQuery(['keys.getDefault']);
|
||||
|
||||
const [mountedKeys, unmountedKeys] = useMemo(
|
||||
() => [keys.data?.filter((key) => mounted_uuids.data?.includes(key.uuid)) ?? [], keys.data?.filter(key => !mounted_uuids.data?.includes(key.uuid)) ?? []],
|
||||
[keys, mounted_uuids]
|
||||
);
|
||||
|
||||
if(keys.data?.length === 0) {
|
||||
return (
|
||||
<CategoryHeading>No keys available.</CategoryHeading>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{[...mountedKeys, ...unmountedKeys]?.map((key, index) => {
|
||||
return (
|
||||
<Key index={index} data={{
|
||||
id: key.uuid,
|
||||
// could probably do with a better way to number these, maybe something that doesn't change
|
||||
name: `Key ${index + 1}`,
|
||||
mounted: mountedKeys.includes(key),
|
||||
default: default_key.data === key.uuid,
|
||||
// key stats need including here at some point
|
||||
}} />
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export function KeyList(props: KeyListProps) {
|
||||
const unmountAll = useLibraryMutation(['keys.unmountAll']);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full max-h-[360px]">
|
||||
<div className="p-3 custom-scroll overlay-scroll">
|
||||
<div className="">
|
||||
{/* <CategoryHeading>Mounted keys</CategoryHeading> */}
|
||||
<div className="space-y-1.5">
|
||||
<Key
|
||||
index={0}
|
||||
data={{
|
||||
id: 'af5570f5a1810b7a',
|
||||
name: 'OBS Recordings',
|
||||
mounted: true,
|
||||
|
||||
nodes: ['node1', 'node2'],
|
||||
stats: { objectCount: 235, containerCount: 2 }
|
||||
}}
|
||||
/>
|
||||
<Key
|
||||
index={1}
|
||||
data={{
|
||||
id: 'af5570f5a1810b7a',
|
||||
name: 'Unknown Key',
|
||||
locked: true,
|
||||
mounted: true,
|
||||
stats: { objectCount: 45 }
|
||||
}}
|
||||
/>
|
||||
<Key index={2} data={{ id: '7324695a52da67b1', name: 'Spacedrive Company' }} />
|
||||
<Key index={3} data={{ id: 'b02303d68d05a562', name: 'Key 4' }} />
|
||||
<Key index={3} data={{ id: 'b02303d68d05a562', name: 'Key 5' }} />
|
||||
<Key index={3} data={{ id: 'b02303d68d05a562', name: 'Key 6' }} />
|
||||
<ListKeys></ListKeys>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full p-2 border-t border-app-line rounded-b-md">
|
||||
<Button size="sm" variant="gray">
|
||||
<Button size="sm" variant="gray" onClick={() => {
|
||||
unmountAll.mutate(null);
|
||||
}}>
|
||||
Unmount All
|
||||
</Button>
|
||||
<div className="flex-grow" />
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { Button, CategoryHeading, Input, Select, SelectOption, Switch, cva, tw } from '@sd/ui';
|
||||
import { Eye, EyeSlash, Info } from 'phosphor-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Algorithm, HashingAlgorithm, Params } from '@sd/client';
|
||||
|
||||
import { Tooltip } from '../tooltip/Tooltip';
|
||||
|
||||
|
@ -9,13 +11,20 @@ const KeyHeading = tw(CategoryHeading)`mb-1`;
|
|||
export function KeyMounter() {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
// we need to call these at least once somewhere
|
||||
// if we don't, if a user mounts a key before first viewing the key list, no key will show in the list
|
||||
// either call it in here or in the keymanager itself
|
||||
const keys = useLibraryQuery(['keys.list']);
|
||||
const mounted_uuids = useLibraryQuery(['keys.listMounted']);
|
||||
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [toggle, setToggle] = useState(true);
|
||||
|
||||
const [key, setKey] = useState('');
|
||||
const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305');
|
||||
const [hashingAlgo, setHashingAlgo] = useState('Argon2id');
|
||||
const [hashingAlgo, setHashingAlgo] = useState('Argon2id-s');
|
||||
|
||||
const createKey = useLibraryMutation('keys.add');
|
||||
const CurrentEyeIcon = showKey ? EyeSlash : Eye;
|
||||
|
||||
// this keeps the input focused when switching tabs
|
||||
|
@ -68,22 +77,42 @@ export function KeyMounter() {
|
|||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Encryption</span>
|
||||
<Select className="mt-2" onChange={setEncryptionAlgo} value={encryptionAlgo}>
|
||||
<SelectOption value="XChaCha20Poly1305">XChaCha20Poly1305</SelectOption>
|
||||
<SelectOption value="Aes256Gcm">Aes256Gcm</SelectOption>
|
||||
<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" onChange={setHashingAlgo} value={hashingAlgo}>
|
||||
<SelectOption value="Argon2id">Argon2id</SelectOption>
|
||||
<SelectOption value="Bcrypt">Bcrypt</SelectOption>
|
||||
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
|
||||
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
|
||||
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<p className="pt-1.5 ml-0.5 text-[8pt] leading-snug text-ink-faint w-[90%]">
|
||||
Files encrypted with this key will be revealed and decrypted on the fly.
|
||||
</p>
|
||||
<Button className="w-full mt-2" variant="accent">
|
||||
<Button className="w-full mt-2" variant="accent" onClick={() => {
|
||||
let algorithm = encryptionAlgo as Algorithm;
|
||||
let hashing_algorithm: HashingAlgorithm = { Argon2id: "Standard" };
|
||||
|
||||
switch(hashingAlgo) {
|
||||
case "Argon2id-s":
|
||||
hashing_algorithm = { Argon2id: "Standard" as Params };
|
||||
break;
|
||||
case "Argon2id-h":
|
||||
hashing_algorithm = { Argon2id: "Hardened" as Params };
|
||||
break;
|
||||
case "Argon2id-p":
|
||||
hashing_algorithm = { Argon2id: "Paranoid" as Params };
|
||||
break;
|
||||
}
|
||||
|
||||
createKey.mutate({algorithm, hashing_algorithm, key });
|
||||
setKey("");
|
||||
}
|
||||
}>
|
||||
Mount Key
|
||||
</Button>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue