Merge branch 'main'

Conflicts:
	packages/interface/src/screens/settings/library/LibraryGeneralSettings.tsx
This commit is contained in:
Jamie Pine committed 2022-10-23 18:43:27 -07:00
commit 3a3ec6251f
22 files changed
+721 -353

No files matched your search

+8
View File
@@ -2,6 +2,14 @@
# disabled for now because we have some dead code that doesn't really need reviewed yet
# * @spacedriveapp
# crates
/crates/crypto @brxken128
/crates/ffmpeg @fogodev
/crates/file-ext @jamiepine
/crates/p2p @oscartbeaumont
/crates/prisma-cli @brendonovich
/crates/sync @brendonovich
# documentation
/apps/landing/src/pages/docs/ @jamiepine
/docs/ @jamiepine
Generated
+2 -1
View File
@@ -5150,11 +5150,11 @@ dependencies = [
"itertools",
"normi",
"once_cell",
"openssl-sys",
"prisma-client-rust",
"rmp",
"rmp-serde",
"rspc",
"sd-crypto",
"sd-ffmpeg",
"sd-file-ext",
"serde",
@@ -5204,6 +5204,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror",
"uuid 1.2.1",
"zeroize",
]
+4 -2
View File
@@ -24,14 +24,16 @@ prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-
"migrations",
"sqlite",
], default-features = false }
rspc = { version = "0.1.2" }
normi = { version = "0.0.1" }
specta = { version = "0.0.4" }
[patch.crates-io]
# We use this patch so we can compile for the IOS simulator on M1
openssl-sys = { git = "https://github.com/spacedriveapp/rust-openssl", rev = "92c3dec225a9e984884d5b30a517e5d44a24d03b" }
rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "7c0a67c1176a8af33b604c68d8edcbf0d70b8429" } # TODO: Move back to crates.io when new jsonrpc executor is released
normi = { git = "https://github.com/oscartbeaumont/rspc", rev = "7c0a67c1176a8af33b604c68d8edcbf0d70b8429" } # TODO: When normi is released on crates.io
rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "7c0a67c1176a8af33b604c68d8edcbf0d70b8429" } # TODO: Move back to crates.io when new jsonrpc executor is released
normi = { git = "https://github.com/oscartbeaumont/rspc", rev = "7c0a67c1176a8af33b604c68d8edcbf0d70b8429" } # TODO: When normi is released on crates.io
specta = { git = "https://github.com/oscartbeaumont/rspc", rev = "7c0a67c1176a8af33b604c68d8edcbf0d70b8429" } # TODO: When normi is released on crates.io
+2 -1
View File
@@ -35,4 +35,5 @@ objc-foundation = "0.1.1"
jni = "0.19.0"
[target.'cfg(not(target_os = "ios"))'.features]
default = ["sd-core/android"]
default = ["sd-core/android"]
+2 -3
View File
@@ -54,7 +54,8 @@ 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-file-ext = { path = "../crates/file-ext" }
sd-crypto = { path = "../crates/crypto" }
sd-file-ext = { path = "../crates/file-ext"}
fs_extra = "1.2.0"
tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
@@ -66,8 +67,6 @@ globset = { version = "^0.4.9", features = ["serde1"] }
itertools = "^0.10.5"
enumflags2 = "0.7.5"
openssl-sys = "0.9.76" # We don't use this in the core but it exists so that the workspace level patch doesn't complain that the dependency isn't used.
[dev-dependencies]
tempfile = "^3.3.0"
tracing-test = "^0.2.3"
+21 -4
View File
@@ -189,13 +189,30 @@ model FileConflict {
// they can be "mounted" to a client, and then used to decrypt files automatically
model Key {
id Int @id @default(autoincrement())
// used to identify the key when it is entered by user
checksum String @unique
// uuid to identify the key
uuid String @unique
// the name that the user sets
name String?
// is this key the default for encryption?
default Boolean
// nullable if concealed for security
date_created DateTime? @default(now())
// so we know which algorithm to use, can be null if user must select
algorithm Int? @default(0)
// encryption algorithm used to encrypt the key
algorithm Bytes
// hashing algorithm used for hashing the master password
hashing_algorithm Bytes
// salt to hash the master password with
salt Bytes
// salt used for encrypting data with this key
content_salt Bytes
// the *encrypted* master key (48 bytes)
master_key Bytes
// the nonce used for encrypting the master key
master_key_nonce Bytes
// the nonce used for encrypting the key
key_nonce Bytes
// the *encrypted* key
key Bytes
objects Object[]
file_paths FilePath[]
+4
View File
@@ -1,5 +1,7 @@
use crate::job::DynJob;
use sd_crypto::keys::keymanager::KeyManager;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::warn;
use uuid::Uuid;
@@ -16,6 +18,8 @@ pub struct LibraryContext {
pub config: LibraryConfig,
/// 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>>,
/// 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.
+60 -2
View File
@@ -1,7 +1,7 @@
use crate::{
invalidate_query,
node::Platform,
prisma::node,
prisma::{node, PrismaClient},
util::{
db::load_and_migrate,
seeder::{indexer_rules_seeder, SeederError},
@@ -9,6 +9,14 @@ use crate::{
NodeContext,
};
use sd_crypto::{
crypto::stream::Algorithm,
keys::{
hashing::HashingAlgorithm,
keymanager::{KeyManager, StoredKey},
},
primitives::to_array,
};
use std::{
env, fs, io,
path::{Path, PathBuf},
@@ -16,7 +24,7 @@ use std::{
sync::Arc,
};
use thiserror::Error;
use tokio::sync::RwLock;
use tokio::sync::{Mutex, RwLock};
use uuid::Uuid;
use super::{LibraryConfig, LibraryConfigWrapped, LibraryContext};
@@ -31,6 +39,53 @@ 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")]
@@ -263,10 +318,13 @@ impl LibraryManager {
// Run seeders
indexer_rules_seeder(&db).await?;
let key_manager = Arc::new(Mutex::new(create_keymanager(&db).await?));
Ok(LibraryContext {
id,
config,
db,
key_manager,
node_local_id: node_data.id,
node_context,
})
+4 -1
View File
@@ -28,4 +28,7 @@ thiserror = "1.0.37"
# metadata de/serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_json = "1.0"
uuid = { version = "1.1.2", features = ["v4", "serde"] }
+6 -2
View File
@@ -7,7 +7,7 @@ use sd_crypto::{
keyslot::{Keyslot, KeyslotVersion},
},
keys::hashing::{HashingAlgorithm, Params},
primitives::generate_master_key,
primitives::{generate_master_key, generate_salt},
Protected,
};
@@ -24,6 +24,9 @@ pub fn encrypt() {
// This needs to be generated here, otherwise we won't have access to it for encryption
let master_key = generate_master_key();
// This ideally should be done by the KMS
let salt = generate_salt();
// Create a keyslot to be added to the header
let mut keyslots: Vec<Keyslot> = Vec::new();
keyslots.push(
@@ -31,6 +34,7 @@ pub fn encrypt() {
KeyslotVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
salt,
password,
&master_key,
)
@@ -38,7 +42,7 @@ pub fn encrypt() {
);
// Create the header for the encrypted file
let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, None, None);
let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots);
// Write the header to the file
header.write(&mut writer).unwrap();
@@ -5,7 +5,7 @@ use sd_crypto::{
header::{
file::{FileHeader, FileHeaderVersion},
keyslot::{Keyslot, KeyslotVersion},
metadata::{Metadata, MetadataVersion},
metadata::MetadataVersion,
},
keys::hashing::{HashingAlgorithm, Params},
primitives::{generate_master_key, generate_salt},
@@ -28,6 +28,9 @@ fn encrypt() {
let password = Protected::new(b"password".to_vec());
// This ideally should be done by the KMS
let salt = generate_salt();
// Open both the source and the output file
let mut reader = File::open("test").unwrap();
let mut writer = File::create("test.encrypted").unwrap();
@@ -43,27 +46,24 @@ fn encrypt() {
KeyslotVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
salt,
password.clone(),
&master_key,
)
.unwrap(),
);
// Ideally this will be generated via the key management system
let md_salt = generate_salt();
let md = Metadata::new(
MetadataVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
password,
&md_salt,
&embedded_metadata,
)
.unwrap();
// Create the header for the encrypted file (and include our metadata)
let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, Some(md), None);
let mut header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots);
header
.add_metadata(
MetadataVersion::V1,
ALGORITHM,
&master_key,
&embedded_metadata,
)
.unwrap();
// Write the header to the file
header.write(&mut writer).unwrap();
@@ -87,18 +87,8 @@ pub fn decrypt_metadata() {
// Deserialize the header, keyslots, etc from the encrypted file
let (header, _) = FileHeader::deserialize(&mut reader).unwrap();
// Checks should be made to ensure the file actually contains any metadata
let metadata = header.metadata.unwrap();
// Hash the user's password with the metadata salt
// This should be done by a key management system
let hashed_key = metadata
.hashing_algorithm
.hash(password, metadata.salt)
.unwrap();
// Decrypt the metadata
let file_info: FileInformation = metadata.decrypt_metadata(hashed_key).unwrap();
let file_info: FileInformation = header.decrypt_metadata(password).unwrap();
println!("file name: {}", file_info.file_name);
}
@@ -5,7 +5,7 @@ use sd_crypto::{
header::{
file::{FileHeader, FileHeaderVersion},
keyslot::{Keyslot, KeyslotVersion},
preview_media::{PreviewMedia, PreviewMediaVersion},
preview_media::PreviewMediaVersion,
},
keys::hashing::{HashingAlgorithm, Params},
primitives::{generate_master_key, generate_salt},
@@ -25,6 +25,9 @@ fn encrypt() {
// This needs to be generated here, otherwise we won't have access to it for encryption
let master_key = generate_master_key();
// This ideally should be done by the KMS
let salt = generate_salt();
// Create a keyslot to be added to the header
// The password is cloned as we also need to provide this for the preview media
let mut keyslots: Vec<Keyslot> = Vec::new();
@@ -33,29 +36,21 @@ fn encrypt() {
KeyslotVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
salt,
password.clone(),
&master_key,
)
.unwrap(),
);
// Ideally this will be generated via the key management system
let pvm_salt = generate_salt();
let pvm_media = b"a nice mountain".to_vec();
let pvm = PreviewMedia::new(
PreviewMediaVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
password,
&pvm_salt,
&pvm_media,
)
.unwrap();
// Create the header for the encrypted file (and include our preview media)
let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, None, Some(pvm));
let mut header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots);
header
.add_preview_media(PreviewMediaVersion::V1, ALGORITHM, &master_key, &pvm_media)
.unwrap();
// Write the header to the file
header.write(&mut writer).unwrap();
@@ -79,15 +74,8 @@ pub fn decrypt_preview_media() {
// Deserialize the header, keyslots, etc from the encrypted file
let (header, _) = FileHeader::deserialize(&mut reader).unwrap();
// Checks should be made to ensure the file actually contains any preview media
let pvm = header.preview_media.unwrap();
// Hash the user's password with the preview media salt
// This should be done by a key management system
let hashed_key = pvm.hashing_algorithm.hash(password, pvm.salt).unwrap();
// Decrypt the preview media
let media = pvm.decrypt_preview_media(hashed_key).unwrap();
let media = header.decrypt_preview_media(password).unwrap();
println!("{:?}", media.expose());
}
+3
View File
@@ -13,6 +13,7 @@ use crate::{error::Error, primitives::BLOCK_SIZE, Protected};
/// These are all possible algorithms that can be used for encryption and decryption
#[derive(Clone, Copy, Eq, PartialEq)]
#[allow(clippy::use_self)]
pub enum Algorithm {
XChaCha20Poly1305,
Aes256Gcm,
@@ -127,6 +128,8 @@ impl StreamEncryption {
})?;
// zeroize before writing, so any potential errors won't result in a potential data leak
// this specific zeroize technically isn't needed due to the boxed slice, but performance impact is
// negligible and it's good practice either way
read_buffer.zeroize();
// Using `write` instead of `write_all` so we can check the amount of bytes written
+8
View File
@@ -36,4 +36,12 @@ pub enum Error {
NoMetadata,
#[error("tried adding too many keyslots to a header")]
TooManyKeyslots,
#[error("requested key wasn't found in the key manager")]
KeyNotFound,
#[error("no default key has been set")]
NoDefaultKeySet,
#[error("no master password has been provided to the keymanager")]
NoMasterPassword,
#[error("mismatch between supplied keys and the keystore")]
KeystoreMismatch,
}
+39 -5
View File
@@ -82,8 +82,8 @@ impl FileHeader {
version: FileHeaderVersion,
algorithm: Algorithm,
keyslots: Vec<Keyslot>,
metadata: Option<Metadata>,
preview_media: Option<PreviewMedia>,
//metadata: Option<Metadata>,
//preview_media: Option<PreviewMedia>,
) -> Self {
let nonce = generate_nonce(algorithm);
@@ -92,12 +92,12 @@ impl FileHeader {
algorithm,
nonce,
keyslots,
metadata,
preview_media,
metadata: None,
preview_media: None,
}
}
/// This is a helper function to decrypt a master key from keyslots that are attached to a header.
/// This is a helper function to decrypt a master key from keyslots that are attached to a header, from a user-supplied password.
///
/// You receive an error if the password doesn't match or if there are no keyslots.
#[allow(clippy::needless_pass_by_value)]
@@ -124,6 +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>
where
W: Write + Seek,
@@ -132,6 +133,39 @@ impl FileHeader {
Ok(())
}
/// This is a helper function to decrypt a master key from keyslots that are attached to a header.
///
/// It takes in a Vec of pre-hashed keys, which is what the key manager returns
///
/// You receive an error if the password doesn't match or if there are no keyslots.
#[allow(clippy::needless_pass_by_value)]
pub fn decrypt_master_key_from_prehashed(
&self,
hashed_keys: Vec<Protected<[u8; 32]>>,
) -> Result<Protected<[u8; MASTER_KEY_LEN]>, Error> {
let mut master_key = [0u8; MASTER_KEY_LEN];
if self.keyslots.is_empty() {
return Err(Error::NoKeyslots);
}
for key in hashed_keys {
for keyslot in &self.keyslots {
if let Ok(decrypted_master_key) =
keyslot.decrypt_master_key_from_prehashed(key.clone())
{
master_key.copy_from_slice(&decrypted_master_key);
}
}
}
if master_key == [0u8; MASTER_KEY_LEN] {
Err(Error::IncorrectPassword)
} else {
Ok(Protected::new(master_key))
}
}
/// This function should be used for generating AAD before encryption
///
/// Use the return value from `FileHeader::deserialize()` for decryption
+17 -5
View File
@@ -27,9 +27,7 @@ use crate::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
error::Error,
keys::hashing::HashingAlgorithm,
primitives::{
generate_nonce, generate_salt, 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,
};
@@ -64,13 +62,13 @@ impl Keyslot {
version: KeyslotVersion,
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
salt: [u8; SALT_LEN],
password: Protected<Vec<u8>>,
master_key: &Protected<[u8; MASTER_KEY_LEN]>,
) -> Result<Self, Error> {
let salt = generate_salt();
let nonce = generate_nonce(algorithm);
let hashed_password = hashing_algorithm.hash(password, salt).unwrap();
let hashed_password = hashing_algorithm.hash(password, salt)?;
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
hashed_password,
@@ -107,6 +105,20 @@ impl Keyslot {
StreamDecryption::decrypt_bytes(key, &self.nonce, self.algorithm, &self.master_key, &[])
}
/// This function should not be used directly, use `header.decrypt_master_key()` instead
///
/// This attempts to decrypt the master key for a single keyslot, using a pre-hashed key
///
/// No hashing is done internally.
///
/// An error will be returned on failure.
pub fn decrypt_master_key_from_prehashed(
&self,
key: Protected<[u8; 32]>,
) -> Result<Protected<Vec<u8>>, Error> {
StreamDecryption::decrypt_bytes(key, &self.nonce, self.algorithm, &self.master_key, &[])
}
/// This function is used to serialize a keyslot into bytes
#[must_use]
pub fn serialize(&self) -> Vec<u8> {
+81 -102
View File
@@ -32,14 +32,12 @@ use std::io::{Read, Seek};
use crate::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
error::Error,
keys::hashing::HashingAlgorithm,
primitives::{
generate_master_key, generate_nonce, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN,
SALT_LEN,
},
primitives::{generate_nonce, MASTER_KEY_LEN},
Protected,
};
use super::file::FileHeader;
/// This is a metadata header item. You may add it to a header, and this will be stored with the file.
///
/// The `Metadata::new()` function handles master key and metadata encryption.
@@ -48,11 +46,7 @@ use crate::{
#[derive(Clone)]
pub struct Metadata {
pub version: MetadataVersion,
pub algorithm: Algorithm, // encryption algorithm
pub hashing_algorithm: HashingAlgorithm, // password hashing algorithm
pub salt: [u8; SALT_LEN],
pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN],
pub master_key_nonce: Vec<u8>,
pub algorithm: Algorithm, // encryption algorithm
pub metadata_nonce: Vec<u8>,
pub metadata: Vec<u8>,
}
@@ -62,7 +56,7 @@ pub enum MetadataVersion {
V1,
}
impl Metadata {
impl FileHeader {
/// This should be used for creating a header metadata item.
///
/// It handles encrypting the master key and metadata.
@@ -70,55 +64,101 @@ impl Metadata {
/// You will need to provide the user's password, and a semi-universal salt for hashing the user's password. This allows for extremely fast decryption.
///
/// Metadata needs to be accessed switfly, so a key management system should handle the salt generation.
pub fn new<T>(
pub fn add_metadata<T>(
&mut self,
version: MetadataVersion,
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
password: Protected<Vec<u8>>,
salt: &[u8; SALT_LEN],
media: &T,
) -> Result<Self, Error>
master_key: &Protected<[u8; MASTER_KEY_LEN]>,
metadata: &T,
) -> Result<(), Error>
where
T: ?Sized + serde::Serialize,
{
let metadata_nonce = generate_nonce(algorithm);
let master_key_nonce = generate_nonce(algorithm);
let master_key = generate_master_key();
let hashed_password = hashing_algorithm.hash(password, *salt)?;
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
hashed_password,
&master_key_nonce,
algorithm,
master_key.expose(),
&[],
)?)?;
let encrypted_metadata = StreamEncryption::encrypt_bytes(
master_key,
master_key.clone(),
&metadata_nonce,
algorithm,
&serde_json::to_vec(media).map_err(|_| Error::MetadataDeSerialization)?,
&serde_json::to_vec(metadata).map_err(|_| Error::MetadataDeSerialization)?,
&[],
)?;
Ok(Self {
let metadata = Metadata {
version,
algorithm,
hashing_algorithm,
salt: *salt,
master_key: encrypted_master_key,
master_key_nonce,
metadata_nonce,
metadata: encrypted_metadata,
})
};
self.metadata = Some(metadata);
Ok(())
}
/// This function should be used to retrieve the metadata for a file
///
/// All it requires is pre-hashed keys returned from the key manager
///
/// A deserialized data type will be returned from this function
pub fn decrypt_metadata_from_prehashed<T>(
&self,
hashed_keys: Vec<Protected<[u8; 32]>>,
) -> Result<T, Error>
where
T: serde::de::DeserializeOwned,
{
let master_key = self.decrypt_master_key_from_prehashed(hashed_keys)?;
// could be an expensive clone (a few MiB at most)
if let Some(metadata) = self.metadata.clone() {
let metadata = StreamDecryption::decrypt_bytes(
master_key,
&metadata.metadata_nonce,
metadata.algorithm,
&metadata.metadata,
&[],
)?;
serde_json::from_slice::<T>(&metadata).map_err(|_| Error::MetadataDeSerialization)
} else {
Err(Error::NoMetadata)
}
}
/// This function should be used to retrieve the metadata for a file
///
/// 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>
where
T: serde::de::DeserializeOwned,
{
let master_key = self.decrypt_master_key(password)?;
// could be an expensive clone (a few MiB at most)
if let Some(metadata) = self.metadata.clone() {
let metadata = StreamDecryption::decrypt_bytes(
master_key,
&metadata.metadata_nonce,
metadata.algorithm,
&metadata.metadata,
&[],
)?;
serde_json::from_slice::<T>(&metadata).map_err(|_| Error::MetadataDeSerialization)
} else {
Err(Error::NoMetadata)
}
}
}
impl Metadata {
#[must_use]
pub fn get_length(&self) -> usize {
match self.version {
MetadataVersion::V1 => 128 + self.metadata.len(),
MetadataVersion::V1 => 36 + self.metadata.len(),
}
}
@@ -132,55 +172,15 @@ impl Metadata {
let mut metadata: Vec<u8> = Vec::new();
metadata.extend_from_slice(&self.version.serialize()); // 2
metadata.extend_from_slice(&self.algorithm.serialize()); // 4
metadata.extend_from_slice(&self.hashing_algorithm.serialize()); // 6
metadata.extend_from_slice(&self.salt); // 22
metadata.extend_from_slice(&self.master_key); // 70
metadata.extend_from_slice(&self.master_key_nonce); // 82 or 94
metadata.extend_from_slice(&vec![0u8; 26 - self.master_key_nonce.len()]); // 96
metadata.extend_from_slice(&self.metadata_nonce); // 108 or 120
metadata.extend_from_slice(&vec![0u8; 24 - self.metadata_nonce.len()]); // 120
metadata.extend_from_slice(&self.metadata.len().to_le_bytes()); // 128 total bytes
metadata.extend_from_slice(&self.metadata_nonce); // 24 max
metadata.extend_from_slice(&vec![0u8; 24 - self.metadata_nonce.len()]); // 28
metadata.extend_from_slice(&self.metadata.len().to_le_bytes()); // 36 total bytes
metadata.extend_from_slice(&self.metadata); // this can vary in length
metadata
}
}
}
/// This function should be used to retrieve the metadata for a file
///
/// All it requires is a pre-hashed key (hashed with the salt provided on creation)
///
/// A deserialized data type will be returned from this function
pub fn decrypt_metadata<T>(&self, hashed_key: Protected<[u8; 32]>) -> Result<T, Error>
where
T: serde::de::DeserializeOwned,
{
let mut master_key = [0u8; MASTER_KEY_LEN];
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes(
hashed_key,
&self.master_key_nonce,
self.algorithm,
&self.master_key,
&[],
) {
master_key.copy_from_slice(&decrypted_master_key);
Ok(Protected::new(master_key))
} else {
Err(Error::IncorrectPassword)
}?;
let metadata = StreamDecryption::decrypt_bytes(
master_key,
&self.metadata_nonce,
self.algorithm,
&self.metadata,
&[],
)?;
serde_json::from_slice::<T>(&metadata).map_err(|_| Error::MetadataDeSerialization)
}
/// This function reads a metadata header item from a reader
///
/// The cursor will be left at the end of the metadata item on success
@@ -200,23 +200,6 @@ impl Metadata {
reader.read(&mut algorithm).map_err(Error::Io)?;
let algorithm = Algorithm::deserialize(algorithm)?;
let mut hashing_algorithm = [0u8; 2];
reader.read(&mut hashing_algorithm).map_err(Error::Io)?;
let hashing_algorithm = HashingAlgorithm::deserialize(hashing_algorithm)?;
let mut salt = [0u8; SALT_LEN];
reader.read(&mut salt).map_err(Error::Io)?;
let mut master_key = [0u8; ENCRYPTED_MASTER_KEY_LEN];
reader.read(&mut master_key).map_err(Error::Io)?;
let mut master_key_nonce = vec![0u8; algorithm.nonce_len()];
reader.read(&mut master_key_nonce).map_err(Error::Io)?;
reader
.read(&mut vec![0u8; 26 - master_key_nonce.len()])
.map_err(Error::Io)?;
let mut metadata_nonce = vec![0u8; algorithm.nonce_len()];
reader.read(&mut metadata_nonce).map_err(Error::Io)?;
@@ -235,10 +218,6 @@ impl Metadata {
let metadata = Self {
version,
algorithm,
hashing_algorithm,
salt,
master_key,
master_key_nonce,
metadata_nonce,
metadata,
};
+80 -99
View File
@@ -25,14 +25,12 @@ use std::io::{Read, Seek};
use crate::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
error::Error,
keys::hashing::HashingAlgorithm,
primitives::{
generate_master_key, generate_nonce, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN,
SALT_LEN,
},
primitives::{generate_nonce, MASTER_KEY_LEN},
Protected,
};
use super::file::FileHeader;
/// This is a preview media header item. You may add it to a header, and this will be stored with the file.
///
/// The `Metadata::new()` function handles master key and metadata encryption.
@@ -41,11 +39,7 @@ use crate::{
#[derive(Clone)]
pub struct PreviewMedia {
pub version: PreviewMediaVersion,
pub algorithm: Algorithm, // encryption algorithm
pub hashing_algorithm: HashingAlgorithm, // password hashing algorithm
pub salt: [u8; SALT_LEN],
pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN],
pub master_key_nonce: Vec<u8>,
pub algorithm: Algorithm, // encryption algorithm
pub media_nonce: Vec<u8>,
pub media: Vec<u8>,
}
@@ -55,7 +49,7 @@ pub enum PreviewMediaVersion {
V1,
}
impl PreviewMedia {
impl FileHeader {
/// This should be used for creating a header preview media item.
///
/// This handles encrypting the master key and preview media.
@@ -63,47 +57,95 @@ impl PreviewMedia {
/// You will need to provide the user's password, and a semi-universal salt for hashing the user's password. This allows for extremely fast decryption.
///
/// Preview media needs to be accessed switfly, so a key management system should handle the salt generation.
pub fn new(
pub fn add_preview_media(
&mut self,
version: PreviewMediaVersion,
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
password: Protected<Vec<u8>>,
salt: &[u8; SALT_LEN],
master_key: &Protected<[u8; MASTER_KEY_LEN]>,
media: &[u8],
) -> Result<Self, Error> {
) -> Result<(), Error> {
let media_nonce = generate_nonce(algorithm);
let master_key_nonce = generate_nonce(algorithm);
let master_key = generate_master_key();
let hashed_password = hashing_algorithm.hash(password, *salt)?;
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
hashed_password,
&master_key_nonce,
let encrypted_media = StreamEncryption::encrypt_bytes(
master_key.clone(),
&media_nonce,
algorithm,
master_key.expose(),
media,
&[],
)?)?;
)?;
let encrypted_media =
StreamEncryption::encrypt_bytes(master_key, &media_nonce, algorithm, media, &[])?;
Ok(Self {
let pvm = PreviewMedia {
version,
algorithm,
hashing_algorithm,
salt: *salt,
master_key: encrypted_master_key,
master_key_nonce,
media_nonce,
media: encrypted_media,
})
};
self.preview_media = Some(pvm);
Ok(())
}
/// This function is what you'll want to use to get the preview media for a file
///
/// All it requires is pre-hashed keys returned from the key manager
///
/// Once provided, a `Vec<u8>` is returned that contains the preview media
pub fn decrypt_preview_media_from_prehashed(
&self,
hashed_keys: Vec<Protected<[u8; 32]>>,
) -> Result<Protected<Vec<u8>>, Error> {
let master_key = self.decrypt_master_key_from_prehashed(hashed_keys)?;
// could be an expensive clone (a few MiB at most)
if let Some(pvm) = self.preview_media.clone() {
let media = StreamDecryption::decrypt_bytes(
master_key,
&pvm.media_nonce,
pvm.algorithm,
&pvm.media,
&[],
)?;
Ok(media)
} else {
Err(Error::NoPreviewMedia)
}
}
/// This function is what you'll want to use to get the preview media for a file
///
/// All it requires is the user's password. Hashing is handled for you.
///
/// Once provided, a `Vec<u8>` is returned that contains the preview media
pub fn decrypt_preview_media(
&self,
password: Protected<Vec<u8>>,
) -> Result<Protected<Vec<u8>>, Error> {
let master_key = self.decrypt_master_key(password)?;
// could be an expensive clone (a few MiB at most)
if let Some(pvm) = self.preview_media.clone() {
let media = StreamDecryption::decrypt_bytes(
master_key,
&pvm.media_nonce,
pvm.algorithm,
&pvm.media,
&[],
)?;
Ok(media)
} else {
Err(Error::NoPreviewMedia)
}
}
}
impl PreviewMedia {
#[must_use]
pub fn get_length(&self) -> usize {
match self.version {
PreviewMediaVersion::V1 => 128 + self.media.len(),
PreviewMediaVersion::V1 => 36 + self.media.len(),
}
}
@@ -117,55 +159,15 @@ impl PreviewMedia {
let mut preview_media: Vec<u8> = Vec::new();
preview_media.extend_from_slice(&self.version.serialize()); // 2
preview_media.extend_from_slice(&self.algorithm.serialize()); // 4
preview_media.extend_from_slice(&self.hashing_algorithm.serialize()); // 6
preview_media.extend_from_slice(&self.salt); // 22
preview_media.extend_from_slice(&self.master_key); // 70
preview_media.extend_from_slice(&self.master_key_nonce); // 82 or 94
preview_media.extend_from_slice(&vec![0u8; 26 - self.master_key_nonce.len()]); // 96
preview_media.extend_from_slice(&self.media_nonce); // 108 or 120
preview_media.extend_from_slice(&vec![0u8; 24 - self.media_nonce.len()]); // 120
preview_media.extend_from_slice(&self.media.len().to_le_bytes()); // 128 total bytes
preview_media.extend_from_slice(&self.media_nonce); // 24 max
preview_media.extend_from_slice(&vec![0u8; 24 - self.media_nonce.len()]); // 28 total bytes
preview_media.extend_from_slice(&self.media.len().to_le_bytes()); // 36 total bytes
preview_media.extend_from_slice(&self.media); // this can vary in length
preview_media
}
}
}
/// This function is what you'll want to use to get the preview media for a file
///
/// All it requires is a pre-hashed key (hashed with the salt provided on creation)
///
/// Once provided, a `Vec<u8>` is returned that contains the preview media
pub fn decrypt_preview_media(
&self,
hashed_key: Protected<[u8; 32]>,
) -> Result<Protected<Vec<u8>>, Error> {
let mut master_key = [0u8; MASTER_KEY_LEN];
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes(
hashed_key,
&self.master_key_nonce,
self.algorithm,
&self.master_key,
&[],
) {
master_key.copy_from_slice(&decrypted_master_key);
Ok(Protected::new(master_key))
} else {
Err(Error::IncorrectPassword)
}?;
let media = StreamDecryption::decrypt_bytes(
master_key,
&self.media_nonce,
self.algorithm,
&self.media,
&[],
)?;
Ok(media)
}
/// This function reads a preview media header item from a reader
///
/// The cursor will be left at the end of the preview media item on success
@@ -186,23 +188,6 @@ impl PreviewMedia {
reader.read(&mut algorithm).map_err(Error::Io)?;
let algorithm = Algorithm::deserialize(algorithm)?;
let mut hashing_algorithm = [0u8; 2];
reader.read(&mut hashing_algorithm).map_err(Error::Io)?;
let hashing_algorithm = HashingAlgorithm::deserialize(hashing_algorithm)?;
let mut salt = [0u8; SALT_LEN];
reader.read(&mut salt).map_err(Error::Io)?;
let mut master_key = [0u8; ENCRYPTED_MASTER_KEY_LEN];
reader.read(&mut master_key).map_err(Error::Io)?;
let mut master_key_nonce = vec![0u8; algorithm.nonce_len()];
reader.read(&mut master_key_nonce).map_err(Error::Io)?;
reader
.read(&mut vec![0u8; 26 - master_key_nonce.len()])
.map_err(Error::Io)?;
let mut media_nonce = vec![0u8; algorithm.nonce_len()];
reader.read(&mut media_nonce).map_err(Error::Io)?;
@@ -221,10 +206,6 @@ impl PreviewMedia {
let preview_media = Self {
version,
algorithm,
hashing_algorithm,
salt,
master_key,
master_key_nonce,
media_nonce,
media,
};
+12 -2
View File
@@ -17,7 +17,8 @@ use argon2::Argon2;
/// These parameters define the password-hashing level.
///
/// The harder the parameter, the longer the password will take to hash.
#[derive(Clone, Copy)]
#[derive(Clone, Copy, PartialEq, Eq)]
#[allow(clippy::use_self)]
pub enum Params {
Standard,
Hardened,
@@ -25,11 +26,20 @@ pub enum Params {
}
/// This defines all available password hashing algorithms.
#[derive(Clone, Copy)]
#[derive(Clone, Copy, PartialEq, Eq)]
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
///
+323
View File
@@ -0,0 +1,323 @@
use std::collections::HashMap;
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,
};
use crate::{
crypto::stream::Algorithm,
primitives::{ENCRYPTED_MASTER_KEY_LEN, SALT_LEN},
Protected,
};
use uuid::Uuid;
use super::hashing::{HashingAlgorithm, HASHING_ALGORITHM_LIST};
// The terminology in this file is very confusing.
// The `master_key` is specific to the `StoredKey`, and is just used internally for encryption.
// The `key` is what the user added/generated within their Spacedrive key manager.
// The `password` in this sense is the user's "master password", similar to a password manager's main password
// 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)]
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
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)
}
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>,
}
/// The `KeyManager` functions should be used for all key-related management.
impl KeyManager {
/// Initialize the Key Manager with the user's master password, and `StoredKeys` retrieved from Prisma
#[must_use]
pub fn new(stored_keys: Vec<StoredKey>, master_password: Option<Protected<Vec<u8>>>) -> Self {
let mut keystore = HashMap::new();
for key in stored_keys {
keystore.insert(key.uuid, key);
}
let keymount: HashMap<Uuid, MountedKey> = HashMap::new();
Self {
master_password,
keystore,
keymount,
default: 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> {
for key in stored_keys {
self.keystore.insert(key.uuid, key);
}
Ok(())
}
/// This allows you to set the default key
pub fn set_default(&mut self, uuid: Uuid) -> Result<(), Error> {
if self.keystore.contains_key(&uuid) {
self.default = Some(uuid);
Ok(())
} else {
Err(Error::KeyNotFound)
}
}
/// 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 {
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 {
Some(k) => Ok(k.clone()),
None => Err(Error::NoMasterPassword),
}
}
pub fn set_master_password(
&mut self,
master_password: Protected<Vec<u8>>,
) -> Result<(), Error> {
// this returns a result, so we can potentially implement password checking functionality
self.master_password = Some(master_password);
Ok(())
}
#[must_use]
pub const fn has_master_password(&self) -> bool {
self.master_password.is_some()
}
/// This function is used for emptying the entire keystore.
pub fn empty_keystore(&mut self) {
self.keystore.clear();
}
/// This function is used for unmounting all keys at once.
pub fn empty_keymount(&mut 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
self.keymount.clear();
}
/// 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> {
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,
None => return Err(Error::KeystoreMismatch),
};
if key != keystore_key {
return Err(Error::KeystoreMismatch);
}
}
Ok(())
}
pub fn unmount(&mut self, uuid: Uuid) -> Result<(), Error> {
if self.keymount.contains_key(&uuid) {
self.keymount.remove(&uuid);
Ok(())
} else {
Err(Error::KeyNotFound)
}
}
/// This function returns a Vec of `StoredKey`s, so you can write them somewhere/update the database with them/etc
///
/// The database and keystore should be in sync at ALL times
pub fn dump_keystore(&self) -> Result<Vec<StoredKey>, Error> {
let mut keys = Vec::<StoredKey>::new();
for key in self.keystore.values() {
keys.push(key.clone());
}
Ok(keys)
}
/// 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> {
match self.keystore.get(&uuid) {
Some(stored_key) => {
let master_password = self.get_master_password()?;
let hashed_password = stored_key
.hashing_algorithm
.hash(master_password, stored_key.salt)?;
let mut master_key = [0u8; MASTER_KEY_LEN];
// Decrypt the StoredKey's master key using the user's hashed password
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes(
hashed_password,
&stored_key.master_key_nonce,
stored_key.algorithm,
&stored_key.master_key,
&[],
) {
master_key.copy_from_slice(&decrypted_master_key);
Ok(Protected::new(master_key))
} else {
Err(Error::IncorrectPassword)
}?;
// Decrypt the StoredKey using the decrypted master key
let key = StreamDecryption::decrypt_bytes(
master_key,
&stored_key.key_nonce,
stored_key.algorithm,
&stored_key.key,
&[],
)?;
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)?);
}
// 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,
};
self.keymount.insert(uuid, mounted_key);
Ok(())
}
None => Err(Error::KeyNotFound),
}
}
/// This function is for accessing the internal keymount.
///
/// We could add a log to this, so that the user can view accesses
pub fn access_keymount(&self, uuid: Uuid) -> Result<MountedKey, Error> {
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> {
match self.keystore.get(&uuid) {
Some(key) => Ok(key.clone()),
None => Err(Error::KeyNotFound),
}
}
/// 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.
///
/// It does not mount the key, it just registers it.
///
/// Once added, you will need to use `KeyManager::access_keystore()` to retrieve it and add it to Prisma.
///
/// You may use the returned ID to identify this key.
#[allow(clippy::needless_pass_by_value)]
pub fn add_to_keystore(
&mut self,
key: Protected<Vec<u8>>,
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
) -> Result<Uuid, Error> {
let master_password = self.get_master_password()?;
let uuid = uuid::Uuid::new_v4();
// Generate items we'll need for encryption
let key_nonce = generate_nonce(algorithm);
let master_key = generate_master_key();
let master_key_nonce = generate_nonce(algorithm);
let salt = generate_salt();
let content_salt = generate_salt(); // for PVM/MD
// Hash the user's master password
let hashed_password = hashing_algorithm.hash(master_password, salt)?;
// Encrypted the master key with the user's hashed password
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
hashed_password,
&master_key_nonce,
algorithm,
master_key.expose(),
&[],
)?)?;
// Encrypt the actual key (e.g. user-added/autogenerated, text-encodable)
let encrypted_key =
StreamEncryption::encrypt_bytes(master_key, &key_nonce, algorithm, &key, &[])?;
// Construct the StoredKey
let stored_key = StoredKey {
uuid,
algorithm,
hashing_algorithm,
salt,
content_salt,
master_key: encrypted_master_key,
master_key_nonce,
key_nonce,
key: encrypted_key,
};
// Insert it into the Keystore
self.keystore.insert(stored_key.uuid, stored_key);
// Return the ID so it can be identified
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
}
+1
View File
@@ -1,2 +1,3 @@
//! This module contains all key and hashing related functions.
pub mod hashing;
pub mod keymanager;
@@ -1,94 +1,36 @@
import { useBridgeMutation } from '@sd/client';
import { useCurrentLibrary } from '@sd/client';
import { Button, Input, Switch } from '@sd/ui';
import { useCallback, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useDebounce } from 'rooks';
import { useDebouncedCallback } from 'use-debounce';
import { InputContainer } from '../../../components/primitive/InputContainer';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
export default function LibraryGeneralSettings() {
const { library, libraries } = useCurrentLibrary();
const { library } = useCurrentLibrary();
const { mutate: editLibrary } = useBridgeMutation('library.edit');
const [encryptLibrary, setEncryptLibrary] = useState(false);
const debounced = useDebouncedCallback((value) => {
editLibrary({
id: library!.uuid,
name: value.name,
description: value.description
});
}, 500);
const editLibrary = useBridgeMutation('library.edit');
const { register, reset, handleSubmit, watch } = useForm({
const { register, watch } = useForm({
defaultValues: {
name: library?.config.name,
description: library?.config.description
}
});
// reset form when library changes
useEffect(() => {
reset({
name: library?.config.name,
description: library?.config.description
});
console.log('libraries changed, resetting form', library, libraries);
}, [libraries, library, reset]);
const handleEditLibrary = handleSubmit((data) => {
console.log("updating library's name and description", library?.uuid, data);
if (library?.uuid) {
console.log('library.uuid', library.uuid);
editLibrary.mutate({
id: library.uuid,
name: data.name || null,
description: data.description || null
});
}
});
// @ts-ignore
// eslint-disable-next-line react-hooks/exhaustive-deps
const update = useCallback(useDebounce(handleEditLibrary, 500), []);
useEffect(() => {
const subscription = watch(() => update());
return () => subscription.unsubscribe();
});
// const [name, setName] = useState('');
// const [description, setDescription] = useState('');
// const [nameDebounced] = useDebounce(name, 500);
// const [descriptionDebounced] = useDebounce(description, 500);
// prevent auto update when switching library
// useEffect(() => {
// if (library) {
// const { name, description } = library.config;
// // currentLibrary must be loaded, name must not be empty, and must be different from the current
// if (nameDebounced && (nameDebounced !== name || descriptionDebounced !== description)) {
// editLibrary({
// id: library.uuid!,
// name: nameDebounced,
// description: descriptionDebounced
// });
// }
// }
// }, [nameDebounced, descriptionDebounced, library, editLibrary]);
// useEffect(() => {
// if (library) {
// setName(library.config.name);
// setDescription(library.config.description);
// }
// }, [libraries, library]);
// useEffect(() => {
// if (library) {
// setBlockAutoUpdate(true);
// setName(library.config.name);
// setDescription(library.config.description);
// }
// }, [library]);
// useEffect(() => {
// if (blockAutoUpdate) setBlockAutoUpdate(false);
// }, [blockAutoUpdate]);
watch(debounced); // Listen for form changes
// This forces the debounce to run when the component is unmounted
useEffect(() => () => debounced.flush(), [debounced]);
return (
<SettingsContainer>
@@ -99,7 +41,7 @@ export default function LibraryGeneralSettings() {
<div className="flex flex-row pb-3 space-x-5">
<div className="flex flex-col flex-grow">
<span className="mb-1 text-sm font-medium">Name</span>
<Input {...register('name')} defaultValue="My Default Library" />
<Input {...register('name', { required: true })} defaultValue="My Default Library" />
</div>
<div className="flex flex-col flex-grow">
<span className="mb-1 text-sm font-medium">Description</span>
@@ -113,7 +55,7 @@ export default function LibraryGeneralSettings() {
description="Enable encryption for this library, this will only encrypt the Spacedrive database, not the files themselves."
>
<div className="flex items-center ml-3">
<Switch checked={encryptLibrary} onCheckedChange={setEncryptLibrary} />
<Switch checked={false} />
</div>
</InputContainer>
<InputContainer mini title="Export Library" description="Export this library to a file.">