Integrate KeyManager with library creation flow (#491)

* integrate keymanager with library creation flow

* final fixes

* fix clippy recommendations

* minor fixes on library create dialog

* reenable `panic!` for no key + fix secret handling code

* prevent user setting secret , instead hardcode it

* clean library manager default key selection

* bring back wrongly removed `keys.onboarding` resolver

* fix types in `CreateLibraryDialog`
This commit is contained in:
Oscar Beaumont 2023-01-04 00:13:12 +08:00 committed by GitHub
parent a309d15a8d
commit 3964d44ce5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 214 additions and 183 deletions

View file

@ -43,7 +43,13 @@ const CreateLibraryDialog = ({ children, onSubmit, disableBackdropClose }: Props
title="Create New Library"
description="Choose a name for your new library, you can configure this and more settings from the library settings later on."
ctaLabel="Create"
ctaAction={() => createLibrary(libName)}
ctaAction={() =>
createLibrary({
name: libName,
// TODO: Support password and secret on mobile
password: undefined
})
}
loading={createLibLoading}
ctaDisabled={libName.length === 0}
trigger={children}

View file

@ -12,17 +12,12 @@ import { useAutoForm } from '~/hooks/useAutoForm';
import tw from '~/lib/tailwind';
import { SettingsStackScreenProps } from '~/navigation/SettingsNavigator';
type LibraryFormData = {
name: string;
description: string;
};
const LibraryGeneralSettingsScreen = ({
navigation
}: SettingsStackScreenProps<'LibraryGeneralSettings'>) => {
const { library } = useCurrentLibrary();
const form = useForm<LibraryFormData>({
const form = useForm({
defaultValues: { name: library.config.name, description: library.config.description }
});

View file

@ -1,12 +1,8 @@
use std::io::{Read, Write};
use std::{path::PathBuf, str::FromStr};
use sd_crypto::keys::keymanager::StoredKey;
use sd_crypto::{
crypto::stream::Algorithm,
keys::{hashing::HashingAlgorithm, keymanager::KeyManager},
Protected,
};
use sd_crypto::keys::keymanager::{KeyManager, StoredKey};
use sd_crypto::{crypto::stream::Algorithm, keys::hashing::HashingAlgorithm, Protected};
use serde::{Deserialize, Serialize};
use specta::Type;
use uuid::Uuid;
@ -48,6 +44,7 @@ pub struct RestoreBackupArgs {
pub struct OnboardingArgs {
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
password: Protected<String>,
}
#[derive(Type, Deserialize)]
@ -141,7 +138,7 @@ pub(crate) fn mount() -> RouterBuilder {
let key = library.key_manager.save_to_database(key_uuid)?;
// does not check that the key doesn't exist before writing
write_storedkey_to_db(library.db.clone(), &key).await?;
write_storedkey_to_db(&library.db, &key).await?;
invalidate_query!(library, "keys.list");
Ok(())
@ -192,7 +189,8 @@ pub(crate) fn mount() -> RouterBuilder {
})
.library_mutation("onboarding", |t| {
t(|_, args: OnboardingArgs, library| async move {
let bundle = KeyManager::onboarding(args.algorithm, args.hashing_algorithm)?;
let bundle =
KeyManager::onboarding(args.algorithm, args.hashing_algorithm, args.password)?;
let verification_key = bundle.verification_key;
@ -205,7 +203,7 @@ pub(crate) fn mount() -> RouterBuilder {
.exec()
.await?;
write_storedkey_to_db(library.db.clone(), &verification_key).await?;
write_storedkey_to_db(&library.db, &verification_key).await?;
let keys = OnboardingKeys {
master_password: bundle.master_password.expose().clone(),
@ -307,7 +305,7 @@ pub(crate) fn mount() -> RouterBuilder {
let stored_key = library.key_manager.access_keystore(uuid)?;
if args.library_sync {
write_storedkey_to_db(library.db.clone(), &stored_key).await?;
write_storedkey_to_db(&library.db, &stored_key).await?;
if args.automount {
library
@ -393,7 +391,7 @@ pub(crate) fn mount() -> RouterBuilder {
)?;
for key in &updated_keys {
write_storedkey_to_db(library.db.clone(), key).await?;
write_storedkey_to_db(&library.db, key).await?;
}
invalidate_query!(library, "keys.list");
@ -419,7 +417,7 @@ pub(crate) fn mount() -> RouterBuilder {
.await?;
// write the new verification key
write_storedkey_to_db(library.db.clone(), &bundle.verification_key).await?;
write_storedkey_to_db(&library.db, &bundle.verification_key).await?;
Ok(bundle.secret_key.expose().clone())
})

View file

@ -8,6 +8,7 @@ use super::{utils::LibraryRequest, RouterBuilder};
use chrono::Utc;
use fs_extra::dir::get_size; // TODO: Remove this dependency as it is sync instead of async
use rspc::Type;
use sd_crypto::Protected;
use serde::Deserialize;
use tokio::fs;
use uuid::Uuid;
@ -73,13 +74,22 @@ pub(crate) fn mount() -> RouterBuilder {
})
})
.mutation("create", |t| {
t(|ctx, name: String| async move {
#[derive(Deserialize, Type)]
pub struct CreateLibraryArgs {
name: String,
password: Option<Protected<String>>,
}
t(|ctx, args: CreateLibraryArgs| async move {
Ok(ctx
.library_manager
.create(LibraryConfig {
name: name.to_string(),
..Default::default()
})
.create(
LibraryConfig {
name: args.name.to_string(),
..Default::default()
},
args.password,
)
.await?)
})
})

View file

@ -22,6 +22,9 @@ pub struct LibraryConfig {
pub name: String,
/// description is a user set description of the library. This is used in the UI and is set by the user.
pub description: String,
// /// is_encrypted is a flag that is set to true if the library is encrypted.
// #[serde(default)]
// pub is_encrypted: bool,
}
impl LibraryConfig {

View file

@ -19,6 +19,8 @@ use super::LibraryConfig;
pub struct LibraryContext {
/// id holds the ID of the current library.
pub id: Uuid,
/// local_id holds the local ID of the current library.
pub local_id: i32,
/// config holds the configuration of the current library.
pub config: LibraryConfig,
/// db holds the database client for the current library.

View file

@ -1,9 +1,9 @@
use crate::{
invalidate_query,
node::Platform,
prisma::{key, node, PrismaClient},
prisma::{node, PrismaClient},
util::{
db::load_and_migrate,
db::{load_and_migrate, write_storedkey_to_db},
seeder::{indexer_rules_seeder, SeederError},
},
NodeContext,
@ -16,6 +16,7 @@ use sd_crypto::{
keymanager::{KeyManager, StoredKey},
},
primitives::to_array,
Protected,
};
use std::{
env, fs, io,
@ -71,58 +72,19 @@ impl From<LibraryManagerError> for rspc::Error {
}
}
pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, LibraryManagerError> {
// retrieve all stored keys from the DB
pub async fn create_keymanager(
client: &PrismaClient,
) -> Result<Arc<KeyManager>, LibraryManagerError> {
let key_manager = KeyManager::new(vec![])?;
// BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE
// this is so if there's no verification key set, we set one so users can use the key manager
// it will be done during onboarding, but for now things are statically set (unless they were changed)
if client
.key()
.find_many(vec![key::uuid::equals(uuid::Uuid::nil().to_string())])
.exec()
.await?
.is_empty()
{
client
.key()
.delete_many(vec![key::uuid::equals(uuid::Uuid::nil().to_string())])
.exec()
.await?;
// BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE
let verification_key = KeyManager::onboarding(
Algorithm::XChaCha20Poly1305,
HashingAlgorithm::Argon2id(Params::Standard),
)?
.verification_key;
// BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE
client
.key()
.create(
verification_key.uuid.to_string(),
verification_key.algorithm.serialize().to_vec(),
verification_key.hashing_algorithm.serialize().to_vec(),
verification_key.content_salt.to_vec(),
verification_key.master_key.to_vec(),
verification_key.master_key_nonce.to_vec(),
verification_key.key_nonce.to_vec(),
verification_key.key.to_vec(),
verification_key.salt.to_vec(),
vec![],
)
.exec()
.await?;
}
let db_stored_keys = client.key().find_many(vec![]).exec().await?;
let mut default = Uuid::nil();
let mut default: Option<Uuid> = None;
// collect and serialize the stored keys
// shouldn't call unwrap so much here
let stored_keys: Vec<StoredKey> = db_stored_keys
let stored_keys: Vec<StoredKey> = client
.key()
.find_many(vec![])
.exec()
.await?
.iter()
.map(|key| {
let key = key.clone();
@ -130,7 +92,7 @@ pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, Libr
let uuid = uuid::Uuid::from_str(&key.uuid).unwrap();
if key.default {
default = uuid;
default = Some(uuid);
}
let stored_key = StoredKey {
@ -156,11 +118,11 @@ pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, Libr
key_manager.populate_keystore(stored_keys)?;
// if any key had an associated default tag
if !default.is_nil() {
if let Some(default) = default {
key_manager.set_default(default)?;
}
Ok(key_manager)
Ok(Arc::new(key_manager))
}
impl LibraryManager {
@ -220,6 +182,7 @@ impl LibraryManager {
pub(crate) async fn create(
&self,
config: LibraryConfig,
password: Option<Protected<String>>,
) -> Result<LibraryConfigWrapped, LibraryManagerError> {
let id = Uuid::new_v4();
LibraryConfig::save(
@ -236,6 +199,23 @@ impl LibraryManager {
)
.await?;
// Run seeders
indexer_rules_seeder(&library.db).await?;
// Setup default key
if let Some(password) = password {
let verification_key = KeyManager::onboarding(
Algorithm::XChaCha20Poly1305,
HashingAlgorithm::Argon2id(Params::Standard),
password,
)?
.verification_key;
write_storedkey_to_db(&library.db, &verification_key).await?;
} else {
// TODO: Make setting up keys optional with rest of system before removing this.
todo!();
}
invalidate_query!(library, "library.list");
self.libraries.write().await.push(library);
@ -347,7 +327,6 @@ impl LibraryManager {
};
let uuid_vec = id.as_bytes().to_vec();
let node_data = db
.node()
.upsert(
@ -362,16 +341,12 @@ impl LibraryManager {
.exec()
.await?;
// Run seeders
indexer_rules_seeder(&db).await?;
let key_manager = Arc::new(create_keymanager(&db).await?);
Ok(LibraryContext {
id,
local_id: node_data.id,
config,
key_manager: create_keymanager(&db).await?,
db,
key_manager,
node_local_id: node_data.id,
node_context,
})

View file

@ -1,5 +1,6 @@
use crate::{
job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext},
location::indexer::rules::RuleKind,
prisma::{file_path, location},
};
@ -22,7 +23,7 @@ use super::{
create_many_file_paths, get_max_file_path_id, set_max_file_path_id,
FilePathBatchCreateEntry,
},
rules::{IndexerRule, RuleKind},
rules::IndexerRule,
walk::{walk, WalkEntry},
};

View file

@ -2,7 +2,6 @@ use crate::prisma::{self, PrismaClient};
use prisma_client_rust::QueryError;
use prisma_client_rust::{migrations::*, NewClientError};
use sd_crypto::keys::keymanager::StoredKey;
use std::sync::Arc;
use thiserror::Error;
/// MigrationError represents an error that occurring while opening a initialising and running migrations on the database.
@ -46,10 +45,7 @@ pub async fn load_and_migrate(db_url: &str) -> Result<PrismaClient, MigrationErr
/// This writes a `StoredKey` to prisma
/// If the key is marked as memory-only, it is skipped
pub async fn write_storedkey_to_db(
db: Arc<PrismaClient>,
key: &StoredKey,
) -> Result<(), QueryError> {
pub async fn write_storedkey_to_db(db: &PrismaClient, key: &StoredKey) -> Result<(), QueryError> {
if !key.memory_only {
db.key()
.create(

View file

@ -39,8 +39,8 @@ use std::sync::Mutex;
use crate::crypto::stream::{StreamDecryption, StreamEncryption};
use crate::primitives::{
derive_key, generate_master_key, generate_nonce, generate_passphrase, generate_salt, to_array,
KEY_LEN, MASTER_PASSWORD_CONTEXT, ROOT_KEY_CONTEXT,
derive_key, generate_master_key, generate_nonce, generate_salt, to_array, KEY_LEN,
MASTER_PASSWORD_CONTEXT, ROOT_KEY_CONTEXT,
};
use crate::{
crypto::stream::Algorithm,
@ -154,23 +154,18 @@ impl KeyManager {
pub fn onboarding(
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
password: Protected<String>,
) -> Result<OnboardingBundle> {
let _master_password = generate_passphrase();
let _content_salt = generate_salt(); // secret key
// BRXKEN128: REMOVE THIS ONCE ONBOARDING HAS BEEN DONE
let master_password = Protected::new("password".to_string());
let content_salt = *b"0000000000000000"; // secret key
let content_salt = *b"0000000000000000"; // secret key // TODO: Don't hardcode this
// Hash the master password
let hashed_password = hashing_algorithm.hash(
Protected::new(master_password.expose().as_bytes().to_vec()),
Protected::new(password.expose().as_bytes().to_vec()),
content_salt,
)?;
let salt = generate_salt();
let derived_key = derive_key(hashed_password, salt, MASTER_PASSWORD_CONTEXT);
let uuid = uuid::Uuid::nil();
// Generate items we'll need for encryption
@ -215,7 +210,7 @@ impl KeyManager {
let onboarding_bundle = OnboardingBundle {
verification_key,
master_password,
master_password: password,
secret_key,
};

View file

@ -2,7 +2,7 @@
//!
//! This includes things such as cryptographically-secure random salt/master key/nonce generation,
//! lengths for master keys and even the streaming block size.
use rand::{seq::SliceRandom, RngCore, SeedableRng};
use rand::{RngCore, SeedableRng};
use zeroize::Zeroize;
use crate::{
@ -107,33 +107,33 @@ pub fn to_array<const I: usize>(bytes: Vec<u8>) -> Result<[u8; I]> {
})
}
/// This generates a 7 word diceware passphrase, separated with `-`
#[must_use]
pub fn generate_passphrase() -> Protected<String> {
let wordlist = include_str!("../assets/eff_large_wordlist.txt")
.lines()
.collect::<Vec<&str>>();
// /// This generates a 7 word diceware passphrase, separated with `-`
// #[must_use]
// pub fn generate_passphrase() -> Protected<String> {
// let wordlist = include_str!("../assets/eff_large_wordlist.txt")
// .lines()
// .collect::<Vec<&str>>();
let words: Vec<String> = wordlist
.choose_multiple(
&mut rand_chacha::ChaCha20Rng::from_entropy(),
PASSPHRASE_LEN,
)
.map(ToString::to_string)
.collect();
// let words: Vec<String> = wordlist
// .choose_multiple(
// &mut rand_chacha::ChaCha20Rng::from_entropy(),
// PASSPHRASE_LEN,
// )
// .map(ToString::to_string)
// .collect();
let passphrase = words
.iter()
.enumerate()
.map(|(i, word)| {
if i < PASSPHRASE_LEN - 1 {
word.clone() + "-"
} else {
word.clone()
}
})
.into_iter()
.collect();
// let passphrase = words
// .iter()
// .enumerate()
// .map(|(i, word)| {
// if i < PASSPHRASE_LEN - 1 {
// word.clone() + "-"
// } else {
// word.clone()
// }
// })
// .into_iter()
// .collect();
Protected::new(passphrase)
}
// Protected::new(passphrase)
// }

View file

@ -84,3 +84,36 @@ where
f.write_str("[REDACTED]")
}
}
#[cfg(feature = "serde")]
impl<'de, T> serde::Deserialize<'de> for Protected<T>
where
T: serde::Deserialize<'de> + Zeroize,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Self::new(T::deserialize(deserializer)?))
}
}
#[cfg(feature = "rspc")]
impl<T> specta::Type for Protected<T>
where
T: specta::Type + Zeroize,
{
const NAME: &'static str = T::NAME;
fn inline(opts: specta::DefOpts, generics: &[specta::DataType]) -> specta::DataType {
T::inline(opts, generics)
}
fn reference(opts: specta::DefOpts, generics: &[specta::DataType]) -> specta::DataType {
T::reference(opts, generics)
}
fn definition(opts: specta::DefOpts) -> specta::DataType {
T::definition(opts)
}
}

View file

@ -56,7 +56,7 @@ export type Procedures = {
{ key: "keys.unmountAll", input: LibraryArgs<null>, result: null } |
{ key: "keys.updateAutomountStatus", input: LibraryArgs<AutomountUpdateArgs>, result: null } |
{ key: "keys.updateKeyName", input: LibraryArgs<KeyNameUpdateArgs>, result: null } |
{ key: "library.create", input: string, result: LibraryConfigWrapped } |
{ key: "library.create", input: CreateLibraryArgs, result: LibraryConfigWrapped } |
{ key: "library.delete", input: string, result: null } |
{ key: "library.edit", input: EditLibraryArgs, result: null } |
{ key: "locations.addLibrary", input: LibraryArgs<LocationCreateArgs>, result: null } |
@ -85,6 +85,8 @@ export interface BuildInfo { version: string, commit: string }
export interface ConfigMetadata { version: string | null }
export interface CreateLibraryArgs { name: string, password: string | null }
export interface EditLibraryArgs { id: string, name: string | null, description: string | null }
export type ExplorerContext = { type: "Location" } & Location | { type: "Tag" } & Tag
@ -157,7 +159,7 @@ export interface Object { id: number, cas_id: string, integrity_checksum: string
export interface ObjectValidatorArgs { id: number, path: string }
export interface OnboardingArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm }
export interface OnboardingArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, password: string }
export interface OnboardingKeys { master_password: string, secret_key: string }

View file

@ -2,7 +2,8 @@ import { useBridgeMutation } from '@sd/client';
import { Input } from '@sd/ui';
import { Dialog } from '@sd/ui';
import { useQueryClient } from '@tanstack/react-query';
import { PropsWithChildren, useState } from 'react';
import { PropsWithChildren } from 'react';
import { useForm } from 'react-hook-form';
export default function CreateLibraryDialog({
children,
@ -10,27 +11,38 @@ export default function CreateLibraryDialog({
open,
setOpen
}: PropsWithChildren<{ onSubmit?: () => void; open: boolean; setOpen: (state: boolean) => void }>) {
const [newLibName, setNewLibName] = useState('');
const queryClient = useQueryClient();
const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeMutation(
'library.create',
{
onSuccess: (library: any) => {
setOpen(false);
queryClient.setQueryData(['library.list'], (libraries: any) => [
...(libraries || []),
library
]);
if (onSubmit) onSubmit();
},
onError: (err: any) => {
console.error(err);
}
const form = useForm({
defaultValues: {
name: '',
// TODO: Remove these default values once we go to prod
password: 'password' as string | null
}
);
});
const createLibrary = useBridgeMutation('library.create', {
onSuccess: (library) => {
queryClient.setQueryData(['library.list'], (libraries: any) => [
...(libraries || []),
library
]);
if (onSubmit) onSubmit();
setOpen(false);
form.reset();
},
onError: (err: any) => {
console.error(err);
}
});
const doSubmit = form.handleSubmit((data) => {
// TODO: This is skechy, but will work for now.
if (data.password === '') {
data.password = null;
}
return createLibrary.mutateAsync(data);
});
return (
<Dialog
@ -38,19 +50,44 @@ export default function CreateLibraryDialog({
setOpen={setOpen}
title="Create New Library"
description="Choose a name for your new library, you can configure this and more settings from the library settings later on."
ctaAction={() => createLibrary(newLibName)}
loading={createLibLoading}
submitDisabled={!newLibName}
ctaAction={doSubmit}
loading={form.formState.isSubmitting}
submitDisabled={!form.formState.isValid}
ctaLabel="Create"
trigger={children}
>
<Input
className="flex-grow w-full mt-3"
value={newLibName}
placeholder="My Cool Library"
onChange={(e) => setNewLibName(e.target.value)}
required
/>
<form onSubmit={doSubmit}>
<div className="relative flex flex-col">
<p className="text-sm mt-3">Name:</p>
<Input
className="flex-grow w-full"
placeholder="My Cool Library"
disabled={form.formState.isSubmitting}
{...form.register('name', { required: true })}
/>
</div>
{/* TODO: Proper UI for this. Maybe checkbox for encrypted or not and then reveal these fields. Select encrypted by default. */}
<span className="text-sm">Make password field empty to skip key setup.</span>
<div className="relative flex flex-col">
<p className="text-sm mt-2">Password:</p>
<Input
className="flex-grow !py-0.5"
disabled={form.formState.isSubmitting}
{...form.register('password')}
placeholder="password"
/>
</div>
<div className="relative flex flex-col">
<p className="text-sm mt-2">Secret Key:</p>
<Input
className="flex-grow !py-0.5"
placeholder="00000000-00000000-00000000-00000000"
readOnly
/>
</div>
</form>
</Dialog>
);
}

View file

@ -20,28 +20,6 @@ export default function OnboardingPage() {
)}
>
<h1 className="text-red-500">Welcome to Spacedrive</h1>
<div className="text-white mt-2 mb-4">
<p className="text-sm mb-1">
The default keymanager details are below. This is only for development, and will be
completely random once onboarding has completed. The secret key is just 16x zeroes encoded
in hex.
</p>
<div className="flex space-x-2">
<div className="relative flex">
<p className="mr-2 text-sm mt-2">Password:</p>
<Input value="password" className="flex-grow !py-0.5" disabled />
</div>
<div className="relative flex w-[375px]">
<p className="mr-2 text-sm mt-2">Secret Key:</p>
<Input
value="30303030-30303030-30303030-30303030"
className="flex-grow !py-0.5"
disabled
/>
</div>
</div>
</div>
<CreateLibraryDialog open={open} setOpen={setOpen} onSubmit={() => navigate('/overview')}>
<Button variant="accent" size="sm">
Create your library