[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:
jake 2022-11-05 11:18:01 +00:00 committed by GitHub
parent d851880ac4
commit a403224b3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 774 additions and 234 deletions

13
Cargo.lock generated
View file

@ -5209,10 +5209,14 @@ dependencies = [
"aes-gcm", "aes-gcm",
"argon2", "argon2",
"chacha20poly1305", "chacha20poly1305",
"dashmap",
"rand 0.8.5", "rand 0.8.5",
"rand_chacha 0.3.1", "rand_chacha 0.3.1",
"rspc",
"serde", "serde",
"serde-big-array",
"serde_json", "serde_json",
"specta 0.0.4",
"thiserror", "thiserror",
"uuid 1.2.1", "uuid 1.2.1",
"zeroize", "zeroize",
@ -5368,6 +5372,15 @@ dependencies = [
"serde_derive", "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]] [[package]]
name = "serde-value" name = "serde-value"
version = "0.7.0" version = "0.7.0"

View file

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

View file

@ -0,0 +1,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;

View file

@ -194,7 +194,9 @@ model Key {
// the name that the user sets // the name that the user sets
name String? name String?
// is this key the default for encryption? // 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 // nullable if concealed for security
date_created DateTime? @default(now()) date_created DateTime? @default(now())
// encryption algorithm used to encrypt the key // encryption algorithm used to encrypt the key
@ -214,6 +216,8 @@ model Key {
// the *encrypted* key // the *encrypted* key
key Bytes key Bytes
automount Boolean @default(false)
objects Object[] objects Object[]
file_paths FilePath[] file_paths FilePath[]

225
core/src/api/keys.rs Normal file
View 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(())
})
})
}

View file

@ -36,6 +36,7 @@ pub struct Ctx {
mod files; mod files;
mod jobs; mod jobs;
mod keys;
mod libraries; mod libraries;
mod locations; mod locations;
mod normi; mod normi;
@ -85,6 +86,7 @@ pub(crate) fn mount() -> Arc<Router> {
.merge("library.", libraries::mount()) .merge("library.", libraries::mount())
.merge("volumes.", volumes::mount()) .merge("volumes.", volumes::mount())
.merge("tags.", tags::mount()) .merge("tags.", tags::mount())
.merge("keys.", keys::mount())
.merge("locations.", locations::mount()) .merge("locations.", locations::mount())
.merge("files.", files::mount()) .merge("files.", files::mount())
.merge("jobs.", jobs::mount()) .merge("jobs.", jobs::mount())

View file

@ -1,7 +1,6 @@
use crate::job::DynJob; use crate::job::DynJob;
use sd_crypto::keys::keymanager::KeyManager; use sd_crypto::keys::keymanager::KeyManager;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::warn; use tracing::warn;
use uuid::Uuid; use uuid::Uuid;
@ -19,7 +18,7 @@ pub struct LibraryContext {
/// db holds the database client for the current library. /// db holds the database client for the current library.
pub db: Arc<PrismaClient>, pub db: Arc<PrismaClient>,
/// key manager that provides encryption keys to functions that require them /// 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. /// node_local_id holds the local ID of the node which is running the library.
pub node_local_id: i32, pub node_local_id: i32,
/// node_context holds the node context for the node which this library is running on. /// node_context holds the node context for the node which this library is running on.

View file

@ -16,6 +16,7 @@ use sd_crypto::{
keymanager::{KeyManager, StoredKey}, keymanager::{KeyManager, StoredKey},
}, },
primitives::to_array, primitives::to_array,
Protected,
}; };
use std::{ use std::{
env, fs, io, env, fs, io,
@ -24,7 +25,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use thiserror::Error; use thiserror::Error;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::RwLock;
use uuid::Uuid; use uuid::Uuid;
use super::{LibraryConfig, LibraryConfigWrapped, LibraryContext}; use super::{LibraryConfig, LibraryConfigWrapped, LibraryContext};
@ -39,53 +40,6 @@ pub struct LibraryManager {
node_context: NodeContext, 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)] #[derive(Error, Debug)]
pub enum LibraryManagerError { pub enum LibraryManagerError {
#[error("error saving or loading the config from the filesystem")] #[error("error saving or loading the config from the filesystem")]
@ -104,6 +58,8 @@ pub enum LibraryManagerError {
InvalidDatabasePath(PathBuf), InvalidDatabasePath(PathBuf),
#[error("Failed to run seeder: {0}")] #[error("Failed to run seeder: {0}")]
Seeder(#[from] SeederError), Seeder(#[from] SeederError),
#[error("failed to initialise the key manager")]
KeyManager(#[from] sd_crypto::Error),
} }
impl From<LibraryManagerError> for rspc::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 { impl LibraryManager {
pub(crate) async fn new( pub(crate) async fn new(
libraries_dir: PathBuf, libraries_dir: PathBuf,
@ -318,7 +326,7 @@ impl LibraryManager {
// Run seeders // Run seeders
indexer_rules_seeder(&db).await?; 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 { Ok(LibraryContext {
id, id,

View file

@ -29,6 +29,14 @@ thiserror = "1.0.37"
# metadata de/serialization # metadata de/serialization
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde-big-array = "0.4.1"
uuid = { version = "1.1.2", features = ["v4", "serde"] } uuid = { version = "1.1.2", features = ["v4", "serde"] }
dashmap = "5.4.0"
rspc = { workspace = true, optional = true }
specta = { workspace = true }
[features]
rpsc = ["rspc"]

View file

@ -7,12 +7,14 @@ 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::{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 /// 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)] #[allow(clippy::use_self)]
pub enum Algorithm { pub enum Algorithm {
XChaCha20Poly1305, XChaCha20Poly1305,
@ -45,11 +47,7 @@ impl StreamEncryption {
/// ///
/// The master key, a suitable nonce, and a specific algorithm should be provided. /// The master key, a suitable nonce, and a specific algorithm should be provided.
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn new( pub fn new(key: Protected<[u8; 32]>, nonce: &[u8], algorithm: Algorithm) -> Result<Self> {
key: Protected<[u8; 32]>,
nonce: &[u8],
algorithm: Algorithm,
) -> Result<Self, Error> {
if nonce.len() != algorithm.nonce_len() { if nonce.len() != algorithm.nonce_len() {
return Err(Error::NonceLengthMismatch); return Err(Error::NonceLengthMismatch);
} }
@ -103,12 +101,7 @@ impl StreamEncryption {
/// It requires a reader, a writer, and any AAD to go with it. /// It requires a reader, a writer, and any AAD to go with it.
/// ///
/// The AAD will be authenticated with each block of data. /// The AAD will be authenticated with each block of data.
pub fn encrypt_streams<R, W>( pub fn encrypt_streams<R, W>(mut self, mut reader: R, mut writer: W, aad: &[u8]) -> Result<()>
mut self,
mut reader: R,
mut writer: W,
aad: &[u8],
) -> Result<(), Error>
where where
R: Read + Seek, R: Read + Seek,
W: Write + Seek, W: Write + Seek,
@ -181,7 +174,7 @@ impl StreamEncryption {
algorithm: Algorithm, algorithm: Algorithm,
bytes: &[u8], bytes: &[u8],
aad: &[u8], aad: &[u8],
) -> Result<Vec<u8>, Error> { ) -> Result<Vec<u8>> {
let mut reader = Cursor::new(bytes); let mut reader = Cursor::new(bytes);
let mut writer = Cursor::new(Vec::<u8>::new()); 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. /// The master key, nonce and algorithm that were used for encryption should be provided.
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn new( pub fn new(key: Protected<[u8; 32]>, nonce: &[u8], algorithm: Algorithm) -> Result<Self> {
key: Protected<[u8; 32]>,
nonce: &[u8],
algorithm: Algorithm,
) -> Result<Self, Error> {
if nonce.len() != algorithm.nonce_len() { if nonce.len() != algorithm.nonce_len() {
return Err(Error::NonceLengthMismatch); return Err(Error::NonceLengthMismatch);
} }
@ -257,12 +246,7 @@ impl StreamDecryption {
/// It requires a reader, a writer, and any AAD that was used. /// 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. /// 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>( pub fn decrypt_streams<R, W>(mut self, mut reader: R, mut writer: W, aad: &[u8]) -> Result<()>
mut self,
mut reader: R,
mut writer: W,
aad: &[u8],
) -> Result<(), Error>
where where
R: Read + Seek, R: Read + Seek,
W: Write + Seek, W: Write + Seek,
@ -332,7 +316,7 @@ impl StreamDecryption {
algorithm: Algorithm, algorithm: Algorithm,
bytes: &[u8], bytes: &[u8],
aad: &[u8], aad: &[u8],
) -> Result<Protected<Vec<u8>>, Error> { ) -> Result<Protected<Vec<u8>>> {
let mut reader = Cursor::new(bytes); let mut reader = Cursor::new(bytes);
let mut writer = Cursor::new(Vec::<u8>::new()); let mut writer = Cursor::new(Vec::<u8>::new());

View file

@ -1,6 +1,16 @@
//! This module contains all possible errors that this crate can return. //! This module contains all possible errors that this crate can return.
use thiserror::Error; 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 /// This enum defines all possible errors that this crate can give
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
@ -44,4 +54,12 @@ pub enum Error {
NoMasterPassword, NoMasterPassword,
#[error("mismatch between supplied keys and the keystore")] #[error("mismatch between supplied keys and the keystore")]
KeystoreMismatch, KeystoreMismatch,
#[error("mutex lock error")]
MutexLock,
}
impl<T> From<std::sync::PoisonError<T>> for Error {
fn from(_: std::sync::PoisonError<T>) -> Self {
Self::MutexLock
}
} }

View file

@ -33,9 +33,8 @@ use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use crate::{ use crate::{
crypto::stream::Algorithm, crypto::stream::Algorithm,
error::Error,
primitives::{generate_nonce, MASTER_KEY_LEN}, primitives::{generate_nonce, MASTER_KEY_LEN},
Protected, Error, Protected, Result,
}; };
use super::{keyslot::Keyslot, metadata::Metadata, preview_media::PreviewMedia}; use super::{keyslot::Keyslot, metadata::Metadata, preview_media::PreviewMedia};
@ -104,7 +103,7 @@ impl FileHeader {
pub fn decrypt_master_key( pub fn decrypt_master_key(
&self, &self,
password: Protected<Vec<u8>>, 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]; let mut master_key = [0u8; MASTER_KEY_LEN];
if self.keyslots.is_empty() { 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. /// 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 where
W: Write + Seek, W: Write + Seek,
{ {
@ -142,7 +141,7 @@ impl FileHeader {
pub fn decrypt_master_key_from_prehashed( pub fn decrypt_master_key_from_prehashed(
&self, &self,
hashed_keys: Vec<Protected<[u8; 32]>>, 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]; let mut master_key = [0u8; MASTER_KEY_LEN];
if self.keyslots.is_empty() { if self.keyslots.is_empty() {
@ -189,7 +188,7 @@ impl FileHeader {
/// This will include keyslots, metadata and preview media (if provided) /// 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. /// 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 { match self.version {
FileHeaderVersion::V1 => { FileHeaderVersion::V1 => {
if self.keyslots.len() > 2 { if self.keyslots.len() > 2 {
@ -231,7 +230,7 @@ impl FileHeader {
/// On error, the cursor will not be rewound. /// On error, the cursor will not be rewound.
/// ///
/// It returns both the header, and the AAD that should be used for decryption. /// 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 where
R: Read + Seek, R: Read + Seek,
{ {

View file

@ -25,10 +25,9 @@ use std::io::{Read, Seek};
use crate::{ use crate::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
error::Error,
keys::hashing::HashingAlgorithm, keys::hashing::HashingAlgorithm,
primitives::{generate_nonce, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN, SALT_LEN}, 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 /// 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], salt: [u8; SALT_LEN],
password: Protected<Vec<u8>>, password: Protected<Vec<u8>>,
master_key: &Protected<[u8; MASTER_KEY_LEN]>, master_key: &Protected<[u8; MASTER_KEY_LEN]>,
) -> Result<Self, Error> { ) -> Result<Self> {
let nonce = generate_nonce(algorithm); let nonce = generate_nonce(algorithm);
let hashed_password = hashing_algorithm.hash(password, salt)?; 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 /// This attempts to decrypt the master key for a single keyslot
/// ///
/// An error will be returned on failure. /// An error will be returned on failure.
pub fn decrypt_master_key( pub fn decrypt_master_key(&self, password: &Protected<Vec<u8>>) -> Result<Protected<Vec<u8>>> {
&self,
password: &Protected<Vec<u8>>,
) -> Result<Protected<Vec<u8>>, Error> {
let key = self let key = self
.hashing_algorithm .hashing_algorithm
.hash(password.clone(), self.salt) .hash(password.clone(), self.salt)
@ -115,7 +111,7 @@ impl Keyslot {
pub fn decrypt_master_key_from_prehashed( pub fn decrypt_master_key_from_prehashed(
&self, &self,
key: Protected<[u8; 32]>, key: Protected<[u8; 32]>,
) -> Result<Protected<Vec<u8>>, Error> { ) -> Result<Protected<Vec<u8>>> {
StreamDecryption::decrypt_bytes(key, &self.nonce, self.algorithm, &self.master_key, &[]) 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 /// It will leave the cursor at the end of the keyslot on success
/// ///
/// The cursor will not be rewound on error. /// 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 where
R: Read + Seek, R: Read + Seek,
{ {

View file

@ -31,9 +31,8 @@ use std::io::{Read, Seek};
use crate::{ use crate::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
error::Error,
primitives::{generate_nonce, MASTER_KEY_LEN}, primitives::{generate_nonce, MASTER_KEY_LEN},
Protected, Error, Protected, Result,
}; };
use super::file::FileHeader; use super::file::FileHeader;
@ -70,7 +69,7 @@ impl FileHeader {
algorithm: Algorithm, algorithm: Algorithm,
master_key: &Protected<[u8; MASTER_KEY_LEN]>, master_key: &Protected<[u8; MASTER_KEY_LEN]>,
metadata: &T, metadata: &T,
) -> Result<(), Error> ) -> Result<()>
where where
T: ?Sized + serde::Serialize, T: ?Sized + serde::Serialize,
{ {
@ -104,7 +103,7 @@ impl FileHeader {
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]>>,
) -> Result<T, Error> ) -> Result<T>
where where
T: serde::de::DeserializeOwned, T: serde::de::DeserializeOwned,
{ {
@ -131,7 +130,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
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 where
T: serde::de::DeserializeOwned, 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 be left at the end of the metadata item on success
/// ///
/// The cursor will not be rewound on error. /// 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 where
R: Read + Seek, R: Read + Seek,
{ {

View file

@ -24,9 +24,8 @@ use std::io::{Read, Seek};
use crate::{ use crate::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption}, crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
error::Error,
primitives::{generate_nonce, MASTER_KEY_LEN}, primitives::{generate_nonce, MASTER_KEY_LEN},
Protected, Error, Protected, Result,
}; };
use super::file::FileHeader; use super::file::FileHeader;
@ -63,7 +62,7 @@ impl FileHeader {
algorithm: Algorithm, algorithm: Algorithm,
master_key: &Protected<[u8; MASTER_KEY_LEN]>, master_key: &Protected<[u8; MASTER_KEY_LEN]>,
media: &[u8], media: &[u8],
) -> Result<(), Error> { ) -> Result<()> {
let media_nonce = generate_nonce(algorithm); let media_nonce = generate_nonce(algorithm);
let encrypted_media = StreamEncryption::encrypt_bytes( let encrypted_media = StreamEncryption::encrypt_bytes(
@ -94,7 +93,7 @@ impl FileHeader {
pub fn decrypt_preview_media_from_prehashed( pub fn decrypt_preview_media_from_prehashed(
&self, &self,
hashed_keys: Vec<Protected<[u8; 32]>>, 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)?; let master_key = self.decrypt_master_key_from_prehashed(hashed_keys)?;
// could be an expensive clone (a few MiB at most) // could be an expensive clone (a few MiB at most)
@ -121,7 +120,7 @@ impl FileHeader {
pub fn decrypt_preview_media( pub fn decrypt_preview_media(
&self, &self,
password: Protected<Vec<u8>>, password: Protected<Vec<u8>>,
) -> Result<Protected<Vec<u8>>, Error> { ) -> Result<Protected<Vec<u8>>> {
let master_key = self.decrypt_master_key(password)?; let master_key = self.decrypt_master_key(password)?;
// could be an expensive clone (a few MiB at most) // 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 be left at the end of the preview media item on success
/// ///
/// The cursor will not be rewound on error. /// 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 where
R: Read + Seek, R: Read + Seek,
{ {

View file

@ -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) //! 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::{ use crate::{
crypto::stream::Algorithm, crypto::stream::Algorithm,
error::Error,
keys::hashing::{HashingAlgorithm, Params}, keys::hashing::{HashingAlgorithm, Params},
Error, Result,
}; };
use super::{ 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 { match bytes {
[0x0A, 0x01] => Ok(Self::V1), [0x0A, 0x01] => Ok(Self::V1),
_ => Err(Error::FileHeader), _ => 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 { match bytes {
[0x0D, 0x01] => Ok(Self::V1), [0x0D, 0x01] => Ok(Self::V1),
_ => Err(Error::FileHeader), _ => 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 { match bytes {
[0x0E, 0x01] => Ok(Self::V1), [0x0E, 0x01] => Ok(Self::V1),
_ => Err(Error::FileHeader), _ => 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 { match bytes {
[0x1F, 0x01] => Ok(Self::V1), [0x1F, 0x01] => Ok(Self::V1),
_ => Err(Error::FileHeader), _ => 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 { match bytes {
[0x0F, 0x01] => Ok(Self::Argon2id(Params::Standard)), [0x0F, 0x01] => Ok(Self::Argon2id(Params::Standard)),
[0x0F, 0x02] => Ok(Self::Argon2id(Params::Hardened)), [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 { match bytes {
[0x0B, 0x01] => Ok(Self::XChaCha20Poly1305), [0x0B, 0x01] => Ok(Self::XChaCha20Poly1305),
[0x0B, 0x02] => Ok(Self::Aes256Gcm), [0x0B, 0x02] => Ok(Self::Aes256Gcm),

View file

@ -11,13 +11,15 @@
//! let hashed_password = hashing_algorithm.hash(password, salt).unwrap(); //! let hashed_password = hashing_algorithm.hash(password, salt).unwrap();
//! ``` //! ```
use crate::Protected; use crate::Protected;
use crate::{error::Error, primitives::SALT_LEN}; 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)] #[derive(Clone, Copy, PartialEq, Eq, Type, Serialize, Deserialize)]
#[allow(clippy::use_self)] #[allow(clippy::use_self)]
pub enum Params { pub enum Params {
Standard, Standard,
@ -26,20 +28,11 @@ pub enum Params {
} }
/// This defines all available password hashing algorithms. /// This defines all available password hashing algorithms.
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq, Type, Serialize, Deserialize)]
pub enum HashingAlgorithm { pub enum HashingAlgorithm {
Argon2id(Params), 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 { impl HashingAlgorithm {
/// This function should be used to hash passwords /// This function should be used to hash passwords
/// ///
@ -48,7 +41,7 @@ impl HashingAlgorithm {
&self, &self,
password: Protected<Vec<u8>>, password: Protected<Vec<u8>>,
salt: [u8; SALT_LEN], salt: [u8; SALT_LEN],
) -> Result<Protected<[u8; 32]>, Error> { ) -> Result<Protected<[u8; 32]>> {
match self { match self {
Self::Argon2id(params) => password_hash_argon2id(password, salt, *params), Self::Argon2id(params) => password_hash_argon2id(password, salt, *params),
} }
@ -91,7 +84,7 @@ pub fn password_hash_argon2id(
password: Protected<Vec<u8>>, password: Protected<Vec<u8>>,
salt: [u8; SALT_LEN], salt: [u8; SALT_LEN],
params: Params, params: Params,
) -> Result<Protected<[u8; 32]>, Error> { ) -> Result<Protected<[u8; 32]>> {
let mut key = [0u8; 32]; let mut key = [0u8; 32];
let argon2 = Argon2::new( let argon2 = Argon2::new(

View file

@ -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::crypto::stream::{StreamDecryption, StreamEncryption};
use crate::error::Error;
use crate::primitives::{ use crate::primitives::{
generate_master_key, generate_nonce, generate_salt, to_array, MASTER_KEY_LEN, 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}, primitives::{ENCRYPTED_MASTER_KEY_LEN, SALT_LEN},
Protected, Protected,
}; };
use crate::{Error, Result};
use dashmap::DashMap;
use serde::Serialize;
use serde_big_array::BigArray;
use specta::Type;
use uuid::Uuid; use uuid::Uuid;
use super::hashing::{HashingAlgorithm, HASHING_ALGORITHM_LIST}; use super::hashing::HashingAlgorithm;
// The terminology in this file is very confusing. // The terminology in this file is very confusing.
// The `master_key` is specific to the `StoredKey`, and is just used internally for encryption. // 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 `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)] #[derive(Clone, PartialEq, Eq, Type, Serialize)]
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 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 salt: [u8; SALT_LEN], // salt to hash the master password with
pub content_salt: [u8; SALT_LEN], // salt used for file data 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: [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)
} }
/// 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 { 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 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: HashMap<Uuid, StoredKey>, keystore: DashMap<Uuid, StoredKey>,
keymount: HashMap<Uuid, MountedKey>, keymount: DashMap<Uuid, MountedKey>,
default: Option<Uuid>, default: Mutex<Option<Uuid>>,
} }
/// The `KeyManager` functions should be used for all key-related management. /// 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 /// Initialize the Key Manager with the user's master password, and `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>, master_password: Option<Protected<Vec<u8>>>) -> Self {
let mut keystore = HashMap::new(); let keystore = DashMap::new();
for key in stored_keys { for key in stored_keys {
keystore.insert(key.uuid, key); keystore.insert(key.uuid, key);
} }
let keymount: HashMap<Uuid, MountedKey> = HashMap::new(); let keymount: DashMap<Uuid, MountedKey> = DashMap::new();
Self { Self {
master_password, master_password: Mutex::new(master_password),
keystore, keystore,
keymount, keymount,
default: None, default: Mutex::new(None),
} }
} }
/// 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.
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 { for key in stored_keys {
self.keystore.insert(key.uuid, key); self.keystore.insert(key.uuid, key);
} }
@ -73,10 +131,34 @@ impl KeyManager {
Ok(()) Ok(())
} }
/// This allows you to set the default key /// This function removes a key from the keystore, the keymount and it's unset as the default.
pub fn set_default(&mut self, uuid: Uuid) -> Result<(), Error> { pub fn remove_key(&self, uuid: Uuid) -> Result<()> {
if self.keystore.contains_key(&uuid) { 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(()) Ok(())
} else { } else {
Err(Error::KeyNotFound) Err(Error::KeyNotFound)
@ -84,43 +166,59 @@ impl KeyManager {
} }
/// This allows you to get the default key's ID /// This allows you to get the default key's ID
pub const fn get_default(&self) -> Result<Uuid, Error> { pub fn get_default(&self) -> Result<Uuid> {
if let Some(default) = self.default { if let Some(default) = *self.default.lock()? {
Ok(default) Ok(default)
} else { } else {
Err(Error::NoDefaultKeySet) Err(Error::NoDefaultKeySet)
} }
} }
/// This should ONLY be used within the key manager pub fn clear_default(&self) -> Result<()> {
fn get_master_password(&self) -> Result<Protected<Vec<u8>>, Error> { let mut default = self.default.lock()?;
match &self.master_password {
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()), Some(k) => Ok(k.clone()),
None => Err(Error::NoMasterPassword), None => Err(Error::NoMasterPassword),
} }
} }
pub fn set_master_password( pub fn set_master_password(&self, master_password: Protected<Vec<u8>>) -> Result<()> {
&mut self,
master_password: Protected<Vec<u8>>,
) -> Result<(), Error> {
// this returns a result, so we can potentially implement password checking functionality // 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(()) Ok(())
} }
#[must_use] /// This function is for removing a previously-added master password
pub const fn has_master_password(&self) -> bool { pub fn clear_master_password(&self) -> Result<()> {
self.master_password.is_some() *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. /// This function is used for emptying the entire keystore.
pub fn empty_keystore(&mut self) { pub fn empty_keystore(&self) {
self.keystore.clear(); self.keystore.clear();
} }
/// This function is used for unmounting all keys at once. /// 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 // 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 // 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 // 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. /// 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() { if supplied_keys.len() != self.keystore.len() {
return Err(Error::KeystoreMismatch); return Err(Error::KeystoreMismatch);
} }
for key in supplied_keys { for key in supplied_keys {
let keystore_key = match self.keystore.get(&key.uuid) { let keystore_key = match self.keystore.get(&key.uuid) {
Some(key) => key, Some(key) => key.clone(),
None => return Err(Error::KeystoreMismatch), None => return Err(Error::KeystoreMismatch),
}; };
if key != keystore_key { if *key != keystore_key {
return Err(Error::KeystoreMismatch); return Err(Error::KeystoreMismatch);
} }
} }
@ -147,7 +245,10 @@ impl KeyManager {
Ok(()) 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) { if self.keymount.contains_key(&uuid) {
self.keymount.remove(&uuid); self.keymount.remove(&uuid);
Ok(()) 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 /// 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 /// The database and keystore should be in sync at ALL times (unless the user chose an in-memory only key)
pub fn dump_keystore(&self) -> Result<Vec<StoredKey>, Error> { #[must_use]
let mut keys = Vec::<StoredKey>::new(); pub fn dump_keystore(&self) -> Vec<StoredKey> {
self.keystore.iter().map(|key| key.clone()).collect()
for key in self.keystore.values() {
keys.push(key.clone());
} }
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. /// This function does not return a value by design.
///
/// Once a key is mounted, access it with `KeyManager::access()` /// 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. /// 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 /// 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) { match self.keystore.get(&uuid) {
Some(stored_key) => { Some(stored_key) => {
let master_password = self.get_master_password()?; let master_password = self.get_master_password()?;
@ -207,20 +311,17 @@ impl KeyManager {
&[], &[],
)?; )?;
let mut hashed_keys = Vec::<Protected<[u8; 32]>>::new(); // Hash the key once with the parameters/algorithm the user selected during first mount
let hashed_key = stored_key
// Hash the StoredKey using each available password hashing parameter, so all content is accessible no matter the settings. .hashing_algorithm
// It makes key mounting more expensive, but it allows for greater UX and customizability. .hash(key.clone(), stored_key.content_salt)?;
for hashing_algorithm in HASHING_ALGORITHM_LIST {
hashed_keys.push(hashing_algorithm.hash(key.clone(), 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, key,
content_salt: stored_key.content_salt, content_salt: stored_key.content_salt,
hashed_keys, hashed_key,
}; };
self.keymount.insert(uuid, mounted_key); self.keymount.insert(uuid, mounted_key);
@ -234,21 +335,34 @@ impl KeyManager {
/// 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
pub fn access_keymount(&self, uuid: Uuid) -> Result<MountedKey, Error> { pub fn access_keymount(&self, uuid: Uuid) -> Result<MountedKey> {
match self.keymount.get(&uuid) { match self.keymount.get(&uuid) {
Some(key) => Ok(key.clone()), Some(key) => Ok(key.clone()),
None => Err(Error::KeyNotFound), None => Err(Error::KeyNotFound),
} }
} }
/// This function is for accessing a `StoredKey` from an ID. /// This function is for accessing a `StoredKey`.
pub fn access_keystore(&self, uuid: Uuid) -> Result<StoredKey, Error> { pub fn access_keystore(&self, uuid: Uuid) -> Result<StoredKey> {
match self.keystore.get(&uuid) { match self.keystore.get(&uuid) {
Some(key) => Ok(key.clone()), Some(key) => Ok(key.clone()),
None => Err(Error::KeyNotFound), 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. /// 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. /// 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. /// You may use the returned ID to identify this key.
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn add_to_keystore( pub fn add_to_keystore(
&mut self, &self,
key: Protected<Vec<u8>>, key: Protected<Vec<u8>>,
algorithm: Algorithm, algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm, hashing_algorithm: HashingAlgorithm,
) -> Result<Uuid, Error> { ) -> Result<Uuid> {
let master_password = self.get_master_password()?; let master_password = self.get_master_password()?;
let uuid = uuid::Uuid::new_v4(); let uuid = uuid::Uuid::new_v4();
@ -312,12 +426,3 @@ impl KeyManager {
Ok(uuid) 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
}

View file

@ -10,6 +10,7 @@
#![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_errors_doc)]
#![allow(clippy::module_name_repetitions)] #![allow(clippy::module_name_repetitions)]
#![allow(clippy::similar_names)] #![allow(clippy::similar_names)]
#![allow(clippy::option_if_let_else)]
pub mod crypto; pub mod crypto;
pub mod error; pub mod error;
@ -26,3 +27,5 @@ pub use protected::Protected;
// Re-export zeroize so it can be used elsewhere // Re-export zeroize so it can be used elsewhere
pub use zeroize::Zeroize; pub use zeroize::Zeroize;
pub use self::error::{Error, Result};

View file

@ -5,7 +5,7 @@
use rand::{RngCore, SeedableRng}; use rand::{RngCore, SeedableRng};
use zeroize::Zeroize; 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. /// This is the default salt size, and the recommended size for argon2id.
pub const SALT_LEN: usize = 16; 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<>` /// 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 /// 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>| { bytes.try_into().map_err(|mut b: Vec<u8>| {
b.zeroize(); b.zeroize();
Error::VecArrSizeMismatch Error::VecArrSizeMismatch

View file

@ -8,6 +8,9 @@ export type Procedures = {
{ key: "jobs.getHistory", input: LibraryArgs<null>, result: Array<JobReport> } | { key: "jobs.getHistory", input: LibraryArgs<null>, result: Array<JobReport> } |
{ 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.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.getStatistics", input: LibraryArgs<null>, result: Statistics } |
{ key: "library.list", input: never, result: Array<LibraryConfigWrapped> } | { key: "library.list", input: never, result: Array<LibraryConfigWrapped> } |
{ key: "locations.getById", input: LibraryArgs<number>, result: Location | null } | { 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.generateThumbsForLocation", input: LibraryArgs<GenerateThumbsForLocationArgs>, result: null } |
{ 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.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.create", input: string, result: LibraryConfigWrapped } |
{ key: "library.delete", input: string, result: null } | { key: "library.delete", input: string, result: null } |
{ key: "library.edit", input: EditLibraryArgs, result: null } | { key: "library.edit", input: EditLibraryArgs, result: null } |
@ -52,6 +63,8 @@ export type Procedures = {
{ key: "jobs.newThumbnail", input: LibraryArgs<null>, result: string } { key: "jobs.newThumbnail", input: LibraryArgs<null>, result: string }
}; };
export type Algorithm = "XChaCha20Poly1305" | "Aes256Gcm"
export interface BuildInfo { version: string, commit: string } export interface BuildInfo { version: string, commit: string }
export interface ConfigMetadata { version: string | null } 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 interface GenerateThumbsForLocationArgs { id: number, path: string }
export type HashingAlgorithm = { Argon2id: Params }
export interface IdentifyUniqueFilesArgs { id: number, path: string } 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 } 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 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 LibraryArgs<T> { library_id: string, arg: T }
export interface LibraryConfig { version: string | null, name: string, description: string } 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 interface ObjectValidatorArgs { id: number, path: string }
export type Params = "Standard" | "Hardened" | "Paranoid"
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 }
@ -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 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 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 } export interface TagAssignArgs { object_id: number, tag_id: number, unassign: boolean }

View file

@ -1,7 +1,7 @@
import { Button } from '@sd/ui'; import { Button, ContextMenu } from '@sd/ui';
import clsx from 'clsx'; import clsx from 'clsx';
import { DotsThree, Eye, Key as KeyIcon } from 'phosphor-react'; import { DotsThree, Eye, Key as KeyIcon } from 'phosphor-react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { DefaultProps } from '../primitive/types'; import { DefaultProps } from '../primitive/types';
import { Tooltip } from '../tooltip/Tooltip'; import { Tooltip } from '../tooltip/Tooltip';
@ -17,11 +17,81 @@ export interface Key {
objectCount?: number; objectCount?: number;
containerCount?: number; containerCount?: number;
}; };
default?: boolean; // need to make use of this within the UI
// Nodes this key is mounted on // Nodes this key is mounted on
nodes?: string[]; // will be node object 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 }) => { 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 ( return (
<div <div
className={clsx( 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'} {data.nodes?.length || 0 > 0 ? `${data.nodes?.length || 0} nodes` : 'This node'}
</div> </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>
{/* <div className="text-xs text-gray-300 opacity-30">#{data.id}</div> */} {/* <div className="text-xs text-gray-300 opacity-30">#{data.id}</div> */}
{data.stats ? ( {data.stats ? (
@ -73,9 +148,24 @@ export const Key: React.FC<{ data: Key; index: number }> = ({ data, index }) =>
</Button> </Button>
</Tooltip> </Tooltip>
)} )}
<KeyDropdown trigger={
<Button size="icon"> <Button size="icon">
<DotsThree className="w-4 h-4 text-ink-faint" /> <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>
</div> </div>
); );

View file

@ -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 { DefaultProps } from '../primitive/types';
import { Key } from './Key'; import { Key } from './Key';
import { useMemo } from 'react';
export type KeyListProps = DefaultProps; 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) { export function KeyList(props: KeyListProps) {
const unmountAll = useLibraryMutation(['keys.unmountAll']);
return ( return (
<div className="flex flex-col h-full max-h-[360px]"> <div className="flex flex-col h-full max-h-[360px]">
<div className="p-3 custom-scroll overlay-scroll"> <div className="p-3 custom-scroll overlay-scroll">
<div className=""> <div className="">
{/* <CategoryHeading>Mounted keys</CategoryHeading> */} {/* <CategoryHeading>Mounted keys</CategoryHeading> */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<Key <ListKeys></ListKeys>
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' }} />
</div> </div>
</div> </div>
</div> </div>
<div className="flex w-full p-2 border-t border-app-line rounded-b-md"> <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 Unmount All
</Button> </Button>
<div className="flex-grow" /> <div className="flex-grow" />

View file

@ -1,6 +1,8 @@
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 { Tooltip } from '../tooltip/Tooltip'; import { Tooltip } from '../tooltip/Tooltip';
@ -9,13 +11,20 @@ const KeyHeading = tw(CategoryHeading)`mb-1`;
export function KeyMounter() { export function KeyMounter() {
const ref = useRef<HTMLInputElement>(null); 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 [showKey, setShowKey] = useState(false);
const [toggle, setToggle] = useState(true); const [toggle, setToggle] = useState(true);
const [key, setKey] = useState(''); const [key, setKey] = useState('');
const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305'); 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; const CurrentEyeIcon = showKey ? EyeSlash : Eye;
// this keeps the input focused when switching tabs // this keeps the input focused when switching tabs
@ -68,22 +77,42 @@ export function KeyMounter() {
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-xs font-bold">Encryption</span> <span className="text-xs font-bold">Encryption</span>
<Select className="mt-2" onChange={setEncryptionAlgo} value={encryptionAlgo}> <Select className="mt-2" onChange={setEncryptionAlgo} value={encryptionAlgo}>
<SelectOption value="XChaCha20Poly1305">XChaCha20Poly1305</SelectOption> <SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption>
<SelectOption value="Aes256Gcm">Aes256Gcm</SelectOption> <SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption>
</Select> </Select>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-xs font-bold">Hashing</span> <span className="text-xs font-bold">Hashing</span>
<Select className="mt-2" onChange={setHashingAlgo} value={hashingAlgo}> <Select className="mt-2" onChange={setHashingAlgo} value={hashingAlgo}>
<SelectOption value="Argon2id">Argon2id</SelectOption> <SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
<SelectOption value="Bcrypt">Bcrypt</SelectOption> <SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
</Select> </Select>
</div> </div>
</div> </div>
<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"> <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 Mount Key
</Button> </Button>
</div> </div>