[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",
"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"

View file

@ -54,7 +54,7 @@ image = "0.24.4"
webp = "0.2.2"
ffmpeg-next = { version = "5.1.1", optional = true, features = [] }
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
sd-crypto = { path = "../crates/crypto" }
sd-crypto = { path = "../crates/crypto", features = ["rspc"] }
sd-file-ext = { path = "../crates/file-ext"}
fs_extra = "1.2.0"
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
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
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 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())

View file

@ -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.

View file

@ -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,

View file

@ -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"]

View file

@ -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());

View file

@ -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
}
}

View file

@ -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,
{

View file

@ -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,
{

View file

@ -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,
{

View file

@ -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,
{

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)
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),

View file

@ -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(

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::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();
/// 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()
}
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.
///
/// 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
}

View file

@ -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};

View file

@ -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

View file

@ -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 }

View file

@ -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>
)}
<Button size="icon">
<DotsThree className="w-4 h-4 text-ink-faint" />
</Button>
<KeyDropdown trigger={
<Button size="icon">
<DotsThree className="w-4 h-4 text-ink-faint" />
</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>
);

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 { 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" />

View file

@ -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>