[ENG-313] Key auto-generation and viewing (#478)

* add generic dialog for keys settings

* revert artifact failed key viewing attempt

* move `Select` key list component

* rename dialog

* remove unused imports and add new select option for *all* keys

* add WIP but broken key viewer dialog

* cleanup code and fix key viewer dialog

* add clipboard icon and copy functionality

* generalise the `AlertDialog` and refactor `BackupRestoreDialog` to use it

* use new alert dialog in place of JS/tauri alerts

* use generic alerts everywhere and bring generic alert props/default state

* make `SelectOptionKeyList` generic for mounted/unmounted keys (with the use of `map` for the latter)

* add clipboard to generic alert dialog + clean up

* fix accent colour button for backup restoration

* remove unneeded props from components

* add slider+automount button

* tweak password gen function

* add password autogeneration

* clippy

* tweak password generation

* use `crypto-random-string` and drop rust password generation

* add default TEMPORARY keymanager pass/secret key to library creation screen

* make key automounting functional

* clean up key viewer

* change dialog name

* remove slider as that wasn't even being used?

* make requested changes and hide key viewer if no keys are in the key manager

* prevent automount and library sync from being enabled simultaneously

* include `memoryOnly` in key

* mark keys as memoryOnly
This commit is contained in:
jake 2022-12-12 17:13:52 +00:00 committed by GitHub
parent 8ee2d18053
commit 3ce8c74a0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 570 additions and 299 deletions

View file

@ -21,6 +21,7 @@ pub struct KeyAddArgs {
hashing_algorithm: HashingAlgorithm,
key: String,
library_sync: bool,
automount: bool,
}
#[derive(Type, Deserialize)]
@ -283,7 +284,21 @@ pub(crate) fn mount() -> RouterBuilder {
let stored_key = library.key_manager.access_keystore(uuid)?;
write_storedkey_to_db(library.db.clone(), &stored_key).await?;
if args.library_sync {
write_storedkey_to_db(library.db.clone(), &stored_key).await?;
if args.automount {
library
.db
.key()
.update(
key::uuid::equals(uuid.to_string()),
vec![key::SetParam::SetAutomount(true)],
)
.exec()
.await?;
}
}
// mount the key
library.key_manager.mount(uuid)?;

View file

@ -108,7 +108,7 @@ 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, library_sync: boolean }
export interface KeyAddArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, key: string, library_sync: boolean, automount: boolean }
export interface KeyNameUpdateArgs { uuid: string, name: string }

View file

@ -39,6 +39,7 @@
"autoprefixer": "^10.4.12",
"byte-size": "^8.1.0",
"clsx": "^1.2.1",
"crypto-random-string": "^5.0.0",
"dayjs": "^1.11.5",
"phosphor-react": "^1.4.1",
"react": "^18.2.0",

View file

@ -0,0 +1,63 @@
import { Button, Dialog, Input } from '@sd/ui';
import { writeText } from '@tauri-apps/api/clipboard';
import { Clipboard } from 'phosphor-react';
export const GenericAlertDialogState = {
open: false,
title: '',
description: '',
value: '',
inputBox: false
};
export interface GenericAlertDialogProps {
open: boolean;
title: string;
description: string;
value: string;
inputBox: boolean;
}
export interface AlertDialogProps {
open: boolean;
setOpen: (isShowing: boolean) => void;
title: string; // dialog title
description?: string; // description of the dialog
value: string; // value to be displayed as text or in an input box
label?: string; // button label
inputBox: boolean; // whether the dialog should display the `value` in a disabled input box or as text
}
export const AlertDialog = (props: AlertDialogProps) => {
// maybe a copy-to-clipboard button would be beneficial too
return (
<Dialog
open={props.open}
setOpen={props.setOpen}
title={props.title}
description={props.description}
ctaAction={() => {
props.setOpen(false);
}}
ctaLabel={props.label !== undefined ? props.label : 'Done'}
>
{props.inputBox && (
<div className="relative flex flex-grow mt-3">
<Input value={props.value} disabled className="flex-grow !py-0.5" />
<Button
type="button"
onClick={() => {
writeText(props.value);
}}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
>
<Clipboard className="w-4 h-4" />
</Button>
</div>
)}
{!props.inputBox && <div className="text-sm">{props.value}</div>}
</Dialog>
);
};

View file

@ -5,56 +5,69 @@ import { Eye, EyeSlash } from 'phosphor-react';
import { ReactNode, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { GenericAlertDialogProps } from './AlertDialog';
type FormValues = {
masterPassword: string;
secretKey: string;
filePath: string;
};
export const BackupRestoreDialog = (props: { trigger: ReactNode }) => {
const { trigger } = props;
export interface BackupRestorationDialogProps {
trigger: ReactNode;
setDialogData: (data: GenericAlertDialogProps) => void;
}
export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => {
const { register, handleSubmit, getValues, setValue } = useForm<FormValues>({
defaultValues: {
masterPassword: '',
secretKey: '',
filePath: ''
secretKey: ''
}
});
const onSubmit: SubmitHandler<FormValues> = (data) => {
if (data.filePath !== '') {
setValue('masterPassword', '');
setValue('secretKey', '');
setValue('filePath', '');
if (filePath !== '') {
restoreKeystoreMutation.mutate(
{
password: data.masterPassword,
secret_key: data.secretKey,
path: data.filePath
path: filePath
},
{
onSuccess: (total) => {
setTotalKeysImported(total);
setShowBackupRestoreDialog(false);
setShowRestorationFinalizationDialog(true);
props.setDialogData({
open: true,
title: 'Import Successful',
description: '',
value: `${total} ${total !== 1 ? 'keys were imported.' : 'key was imported.'}`,
inputBox: false
});
},
onError: () => {
alert('There was an error while restoring your backup.');
setShowBackupRestoreDialog(false);
props.setDialogData({
open: true,
title: 'Import Error',
description: '',
value: 'There was an error while restoring your backup.',
inputBox: false
});
}
}
);
setValue('masterPassword', '');
setValue('secretKey', '');
setFilePath('');
}
};
const [showBackupRestoreDialog, setShowBackupRestoreDialog] = useState(false);
const [showRestorationFinalizationDialog, setShowRestorationFinalizationDialog] = useState(false);
const restoreKeystoreMutation = useLibraryMutation('keys.restoreKeystore');
const [showMasterPassword, setShowMasterPassword] = useState(false);
const [showSecretKey, setShowSecretKey] = useState(false);
const [totalKeysImported, setTotalKeysImported] = useState(0);
const [filePath, setFilePath] = useState('');
const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
@ -69,7 +82,7 @@ export const BackupRestoreDialog = (props: { trigger: ReactNode }) => {
description="Restore keys from a backup."
loading={restoreKeystoreMutation.isLoading}
ctaLabel="Restore"
trigger={trigger}
trigger={props.trigger}
>
<div className="relative flex flex-grow mt-3 mb-2">
<Input
@ -108,11 +121,11 @@ export const BackupRestoreDialog = (props: { trigger: ReactNode }) => {
<div className="relative flex flex-grow mb-2">
<Button
size="sm"
variant={getValues('filePath') !== '' ? 'accent' : 'gray'}
variant={filePath !== '' ? 'accent' : 'gray'}
type="button"
onClick={() => {
open()?.then((result) => {
if (result) setValue('filePath', result as string);
if (result) setFilePath(result as string);
});
}}
>
@ -121,23 +134,6 @@ export const BackupRestoreDialog = (props: { trigger: ReactNode }) => {
</div>
</Dialog>
</form>
<Dialog
open={showRestorationFinalizationDialog}
setOpen={setShowRestorationFinalizationDialog}
title="Import Successful"
description=""
ctaAction={() => {
setShowRestorationFinalizationDialog(false);
}}
ctaLabel="Done"
trigger={<></>}
>
<div className="text-sm">
{totalKeysImported}{' '}
{totalKeysImported !== 1 ? 'keys were imported.' : 'key was imported.'}
</div>
</Dialog>
</>
);
};

View file

@ -3,13 +3,14 @@ import { Button, Dialog } from '@sd/ui';
import { save } from '@tauri-apps/api/dialog';
import { useState } from 'react';
import { GenericAlertDialogProps } from './AlertDialog';
interface DecryptDialogProps {
open: boolean;
setOpen: (isShowing: boolean) => void;
location_id: number | null;
object_id: number | null;
setShowAlertDialog: (isShowing: boolean) => void;
setAlertDialogData: (data: { title: string; text: string }) => void;
setAlertDialogData: (data: GenericAlertDialogProps) => void;
}
export const DecryptFileDialog = (props: DecryptDialogProps) => {
@ -41,20 +42,25 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
{
onSuccess: () => {
props.setAlertDialogData({
open: true,
title: 'Info',
text: 'The decryption job has started successfully. You may track the progress in the job overview panel.'
value:
'The decryption job has started successfully. You may track the progress in the job overview panel.',
inputBox: false,
description: ''
});
},
onError: () => {
props.setAlertDialogData({
open: true,
title: 'Error',
text: 'The decryption job failed to start.'
value: 'The decryption job failed to start.',
inputBox: false,
description: ''
});
}
}
);
props.setShowAlertDialog(true);
}}
>
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">

View file

@ -1,40 +1,22 @@
import { StoredKey, useLibraryMutation, useLibraryQuery } from '@sd/client';
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
import { Button, Dialog, Select, SelectOption } from '@sd/ui';
import { save } from '@tauri-apps/api/dialog';
import { useMemo, useState } from 'react';
import { useState } from 'react';
import {
getCryptoSettings,
getHashingAlgorithmString
} from '../../screens/settings/library/KeysSetting';
import { SelectOptionKeyList } from '../key/KeyList';
import { Checkbox } from '../primitive/Checkbox';
export const ListOfMountedKeys = (props: { keys: StoredKey[]; mountedUuids: string[] }) => {
const { keys, mountedUuids } = props;
const [mountedKeys] = useMemo(
() => [keys.filter((key) => mountedUuids.includes(key.uuid)) ?? []],
[keys, mountedUuids]
);
return (
<>
{[...mountedKeys]?.map((key) => {
return (
<SelectOption value={key.uuid}>Key {key.uuid.substring(0, 8).toUpperCase()}</SelectOption>
);
})}
</>
);
};
import { GenericAlertDialogProps } from './AlertDialog';
interface EncryptDialogProps {
open: boolean;
setOpen: (isShowing: boolean) => void;
location_id: number | null;
object_id: number | null;
setShowAlertDialog: (isShowing: boolean) => void;
setAlertDialogData: (data: { title: string; text: string }) => void;
setAlertDialogData: (data: GenericAlertDialogProps) => void;
}
export const EncryptFileDialog = (props: EncryptDialogProps) => {
@ -99,20 +81,25 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
{
onSuccess: () => {
props.setAlertDialogData({
open: true,
title: 'Success',
text: 'The encryption job has started successfully. You may track the progress in the job overview panel.'
value:
'The encryption job has started successfully. You may track the progress in the job overview panel.',
inputBox: false,
description: ''
});
},
onError: () => {
props.setAlertDialogData({
open: true,
title: 'Error',
text: 'The encryption job failed to start.'
value: 'The encryption job failed to start.',
inputBox: false,
description: ''
});
}
}
);
props.setShowAlertDialog(true);
}}
>
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
@ -126,9 +113,7 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
}}
>
{/* this only returns MOUNTED keys. we could include unmounted keys, but then we'd have to prompt the user to mount them too */}
{keys.data && mountedUuids.data && (
<ListOfMountedKeys keys={keys.data} mountedUuids={mountedUuids.data} />
)}
{mountedUuids.data && <SelectOptionKeyList keys={mountedUuids.data} />}
</Select>
</div>
<div className="flex flex-col">

View file

@ -1,27 +0,0 @@
import { Dialog } from '@sd/ui';
export const ExplorerAlertDialog = (props: {
open: boolean;
setOpen: (isShowing: boolean) => void;
title: string;
description?: string;
text: string;
}) => {
const { open, setOpen, title, description, text } = props;
return (
<>
<Dialog
open={open}
setOpen={setOpen}
title={title}
description={description}
ctaLabel="Done"
ctaAction={() => {
setOpen(false);
}}
>
<div className="text-sm">{text}</div>
</Dialog>
</>
);
};

View file

@ -0,0 +1,85 @@
import { useLibraryQuery } from '@sd/client';
import { Button, Dialog, Input, Select } from '@sd/ui';
import { writeText } from '@tauri-apps/api/clipboard';
import { Clipboard } from 'phosphor-react';
import { ReactNode, useEffect, useState } from 'react';
import { SelectOptionKeyList } from '../key/KeyList';
interface KeyViewerDialogProps {
trigger: ReactNode;
}
export const KeyTextBox = (props: { uuid: string; setKey: (value: string) => void }) => {
useLibraryQuery(['keys.getKey', props.uuid], {
onSuccess: (data) => {
props.setKey(data);
}
});
return <></>;
};
export const KeyViewerDialog = (props: KeyViewerDialogProps) => {
const keys = useLibraryQuery(['keys.list'], {
onSuccess: (data) => {
if (key === '' && data.length !== 0) {
setKey(data[0].uuid);
}
}
});
const [showKeyViewerDialog, setShowKeyViewerDialog] = useState(false);
const [key, setKey] = useState('');
const [keyValue, setKeyValue] = useState('');
return (
<>
<Dialog
open={showKeyViewerDialog}
setOpen={setShowKeyViewerDialog}
trigger={props.trigger}
title="View Key Values"
description="Here you can view the values of your keys."
ctaLabel="Done"
ctaAction={() => {
setShowKeyViewerDialog(false);
}}
>
<div className="grid w-full gap-4 mt-4 mb-3">
<div className="flex flex-col">
<span className="text-xs font-bold">Key</span>
<Select
className="mt-2 flex-grow"
value={key}
onChange={(e) => {
setKey(e);
}}
>
{keys.data && <SelectOptionKeyList keys={keys.data.map((key) => key.uuid)} />}
</Select>
</div>
</div>
<div className="grid w-full gap-4 mt-4 mb-3">
<div className="flex flex-col">
<span className="text-xs font-bold">Value</span>
<div className="relative flex flex-grow">
<Input value={keyValue} disabled className="flex-grow !py-0.5" />
<Button
type="button"
onClick={() => {
writeText(keyValue);
}}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
>
<Clipboard className="w-4 h-4" />
</Button>
</div>
<KeyTextBox uuid={key} setKey={setKeyValue} />
</div>
</div>
</Dialog>
</>
);
};

View file

@ -9,16 +9,19 @@ import { ReactNode, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { getCryptoSettings } from '../../screens/settings/library/KeysSetting';
import { GenericAlertDialogProps } from './AlertDialog';
export const PasswordChangeDialog = (props: { trigger: ReactNode }) => {
export interface MasterPasswordChangeDialogProps {
trigger: ReactNode;
setDialogData: (data: GenericAlertDialogProps) => void;
}
export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProps) => {
type FormValues = {
masterPassword: string;
masterPassword2: string;
};
const [secretKey, setSecretKey] = useState('');
const { register, handleSubmit, getValues, setValue } = useForm<FormValues>({
const { register, handleSubmit, reset } = useForm<FormValues>({
defaultValues: {
masterPassword: '',
masterPassword2: ''
@ -27,7 +30,13 @@ export const PasswordChangeDialog = (props: { trigger: ReactNode }) => {
const onSubmit: SubmitHandler<FormValues> = (data) => {
if (data.masterPassword !== data.masterPassword2) {
alert('Passwords are not the same.');
props.setDialogData({
open: true,
title: 'Error',
description: '',
value: 'Passwords are not the same, please try again.',
inputBox: false
});
} else {
const [algorithm, hashing_algorithm] = getCryptoSettings(encryptionAlgo, hashingAlgo);
@ -35,17 +44,31 @@ export const PasswordChangeDialog = (props: { trigger: ReactNode }) => {
{ algorithm, hashing_algorithm, password: data.masterPassword },
{
onSuccess: (sk) => {
setSecretKey(sk);
setShowMasterPasswordDialog(false);
setShowSecretKeyDialog(true);
props.setDialogData({
open: true,
title: 'Secret Key',
description:
'Please store this secret key securely as it is needed to access your key manager.',
value: sk,
inputBox: true
});
},
onError: () => {
// this should never really happen
alert('There was an error while changing your master password.');
setShowMasterPasswordDialog(false);
props.setDialogData({
open: true,
title: 'Master Password Change Error',
description: '',
value: 'There was an error while changing your master password.',
inputBox: false
});
}
}
);
reset();
}
};
@ -53,7 +76,7 @@ export const PasswordChangeDialog = (props: { trigger: ReactNode }) => {
const [hashingAlgo, setHashingAlgo] = useState('Argon2id-s');
const [passwordMeterMasterPw, setPasswordMeterMasterPw] = useState(''); // this is needed as the password meter won't update purely with react-hook-for
const [showMasterPasswordDialog, setShowMasterPasswordDialog] = useState(false);
const [showSecretKeyDialog, setShowSecretKeyDialog] = useState(false);
// const [showSecretKeyDialog, setShowSecretKeyDialog] = useState(false);
const changeMasterPassword = useLibraryMutation('keys.changeMasterPassword');
const [showMasterPassword1, setShowMasterPassword1] = useState(false);
const [showMasterPassword2, setShowMasterPassword2] = useState(false);
@ -136,24 +159,6 @@ export const PasswordChangeDialog = (props: { trigger: ReactNode }) => {
</div>
</Dialog>
</form>
<Dialog
open={showSecretKeyDialog}
setOpen={setShowSecretKeyDialog}
title="Secret Key"
description="Please store this secret key securely as it is needed to access your key manager."
ctaAction={() => {
setShowSecretKeyDialog(false);
}}
ctaLabel="Done"
trigger={<></>}
>
<Input
className="flex-grow w-full mt-3"
value={secretKey}
placeholder="Secret Key"
disabled={true}
/>
</Dialog>
</>
);
};

View file

@ -2,9 +2,9 @@ import { ExplorerData, rspc, useCurrentLibrary } from '@sd/client';
import { useEffect, useState } from 'react';
import { useExplorerStore } from '../../util/explorerStore';
import { AlertDialog, GenericAlertDialogState } from '../dialog/AlertDialog';
import { DecryptFileDialog } from '../dialog/DecryptFileDialog';
import { EncryptFileDialog } from '../dialog/EncryptFileDialog';
import { ExplorerAlertDialog } from '../dialog/ExplorerAlertDialog';
import { Inspector } from '../explorer/Inspector';
import ExplorerContextMenu from './ExplorerContextMenu';
import { TopBar } from './ExplorerTopBar';
@ -23,11 +23,11 @@ export default function Explorer(props: Props) {
const [showEncryptDialog, setShowEncryptDialog] = useState(false);
const [showDecryptDialog, setShowDecryptDialog] = useState(false);
const [showAlertDialog, setShowAlertDialog] = useState(false);
const [alertDialogData, setAlertDialogData] = useState({
title: '',
text: ''
});
const [alertDialogData, setAlertDialogData] = useState(GenericAlertDialogState);
const setShowAlertDialog = (state: boolean) => {
setAlertDialogData({ ...alertDialogData, open: state });
};
useEffect(() => {
setSeparateTopBar((oldValue) => {
@ -50,7 +50,6 @@ export default function Explorer(props: Props) {
<ExplorerContextMenu
setShowEncryptDialog={setShowEncryptDialog}
setShowDecryptDialog={setShowDecryptDialog}
setShowAlertDialog={setShowAlertDialog}
setAlertDialogData={setAlertDialogData}
>
<div className="relative flex flex-col w-full">
@ -93,18 +92,18 @@ export default function Explorer(props: Props) {
</div>
</ExplorerContextMenu>
</div>
<ExplorerAlertDialog
open={showAlertDialog}
<AlertDialog
open={alertDialogData.open}
setOpen={setShowAlertDialog}
title={alertDialogData.title}
text={alertDialogData.text}
value={alertDialogData.value}
inputBox={alertDialogData.inputBox}
/>
<EncryptFileDialog
location_id={expStore.locationId}
object_id={expStore.contextMenuObjectId}
open={showEncryptDialog}
setOpen={setShowEncryptDialog}
setShowAlertDialog={setShowAlertDialog}
setAlertDialogData={setAlertDialogData}
/>
<DecryptFileDialog
@ -112,7 +111,6 @@ export default function Explorer(props: Props) {
object_id={expStore.contextMenuObjectId}
open={showDecryptDialog}
setOpen={setShowDecryptDialog}
setShowAlertDialog={setShowAlertDialog}
setAlertDialogData={setAlertDialogData}
/>
</>

View file

@ -16,6 +16,7 @@ import { PropsWithChildren, useMemo } from 'react';
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
import { usePlatform } from '../../util/Platform';
import { getExplorerStore } from '../../util/explorerStore';
import { GenericAlertDialogProps } from '../dialog/AlertDialog';
import { EncryptFileDialog } from '../dialog/EncryptFileDialog';
const AssignTagMenuItems = (props: { objectId: number }) => {
@ -61,8 +62,7 @@ const AssignTagMenuItems = (props: { objectId: number }) => {
export interface ExplorerContextMenuProps extends PropsWithChildren {
setShowEncryptDialog: (isShowing: boolean) => void;
setShowDecryptDialog: (isShowing: boolean) => void;
setShowAlertDialog: (isShowing: boolean) => void;
setAlertDialogData: (data: { title: string; text: string }) => void;
setAlertDialogData: (data: GenericAlertDialogProps) => void;
}
export default function ExplorerContextMenu(props: ExplorerContextMenuProps) {
@ -150,16 +150,20 @@ export default function ExplorerContextMenu(props: ExplorerContextMenuProps) {
props.setShowEncryptDialog(true);
} else if (!hasMasterPassword) {
props.setAlertDialogData({
open: true,
title: 'Key manager locked',
text: 'The key manager is currently locked. Please unlock it and try again.'
value: 'The key manager is currently locked. Please unlock it and try again.',
inputBox: false,
description: ''
});
props.setShowAlertDialog(true);
} else if (!hasMountedKeys) {
props.setAlertDialogData({
open: true,
title: 'No mounted keys',
text: 'No mounted keys were found. Please mount a key and try again.'
description: '',
value: 'No mounted keys were found. Please mount a key and try again.',
inputBox: false
});
props.setShowAlertDialog(true);
}
}}
/>
@ -173,16 +177,20 @@ export default function ExplorerContextMenu(props: ExplorerContextMenuProps) {
props.setShowDecryptDialog(true);
} else if (!hasMasterPassword) {
props.setAlertDialogData({
open: true,
title: 'Key manager locked',
text: 'The key manager is currently locked. Please unlock it and try again.'
value: 'The key manager is currently locked. Please unlock it and try again.',
inputBox: false,
description: ''
});
props.setShowAlertDialog(true);
} else if (!hasMountedKeys) {
props.setAlertDialogData({
open: true,
title: 'No mounted keys',
text: 'No mounted keys were found. Please mount a key and try again.'
value: 'No mounted keys were found. Please mount a key and try again.',
inputBox: false,
description: ''
});
props.setShowAlertDialog(true);
}
}}
/>

View file

@ -21,7 +21,8 @@ export interface Key {
objectCount?: number;
containerCount?: number;
};
default?: boolean; // need to make use of this within the UI
default?: boolean;
memoryOnly?: boolean;
// Nodes this key is mounted on
nodes?: string[]; // will be node object
}

View file

@ -1,5 +1,5 @@
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
import { Button, CategoryHeading } from '@sd/ui';
import { StoredKey, useLibraryMutation, useLibraryQuery } from '@sd/client';
import { Button, CategoryHeading, SelectOption } from '@sd/ui';
import { useMemo } from 'react';
import { DefaultProps } from '../primitive/types';
@ -7,12 +7,21 @@ import { DummyKey, Key } from './Key';
export type KeyListProps = DefaultProps;
// ideal for going within a select box
// can use mounted or unmounted keys, just provide different inputs
export const SelectOptionKeyList = (props: { keys: string[] }) => {
return (
<>
{props.keys.map((key) => {
return <SelectOption value={key}>Key {key.substring(0, 8).toUpperCase()}</SelectOption>;
})}
</>
);
};
export const ListOfKeys = () => {
const keys = useLibraryQuery(['keys.list']);
const mountedUuids = 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 defaultKey = useLibraryQuery(['keys.getDefault']);
const [mountedKeys, unmountedKeys] = useMemo(
@ -37,7 +46,8 @@ export const ListOfKeys = () => {
id: key.uuid,
name: `Key ${key.uuid.substring(0, 8).toUpperCase()}`,
mounted: mountedKeys.includes(key),
default: defaultKey.data === key.uuid
default: defaultKey.data === key.uuid,
memoryOnly: key.memory_only
// key stats need including here at some point
}}
/>

View file

@ -1,25 +1,29 @@
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
import { Algorithm, HashingAlgorithm, Params } from '@sd/client';
import { Button, CategoryHeading, Input, Select, SelectOption, Switch, cva, tw } from '@sd/ui';
import cryptoRandomString from 'crypto-random-string';
import { Eye, EyeSlash, Info } from 'phosphor-react';
import { useEffect, useRef, useState } from 'react';
import { getCryptoSettings } from '../../screens/settings/library/KeysSetting';
import Slider from '../primitive/Slider';
import { Tooltip } from '../tooltip/Tooltip';
const KeyHeading = tw(CategoryHeading)`mb-1`;
const PasswordCharset =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-={}[]:"\';<>?,./\\|`~';
const GeneratePassword = (length: number) => {
return cryptoRandomString({ length, characters: PasswordCharset });
};
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 [librarySync, setLibrarySync] = useState(true);
const [autoMount, setAutoMount] = useState(false);
const [sliderValue, setSliderValue] = useState([64]);
const [key, setKey] = useState('');
const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305');
@ -59,19 +63,55 @@ export function KeyMounter() {
</div>
</div>
<div className="flex flex-row space-x-2">
<div className="relative flex flex-grow mt-2 mb-2">
<Slider
value={sliderValue}
max={128}
min={8}
step={4}
defaultValue={[64]}
onValueChange={(e) => {
setSliderValue(e);
setKey(GeneratePassword(e[0]));
}}
/>
</div>
<span className="text-sm mt-2.5 font-medium">{sliderValue}</span>
</div>
<div className="flex flex-row items-center mt-3 mb-1">
<div className="space-x-2">
<Switch
className="bg-app-selected"
size="sm"
checked={librarySync}
onCheckedChange={setLibrarySync}
onCheckedChange={(e) => {
if (autoMount && e) setAutoMount(false);
setLibrarySync(e);
}}
/>
</div>
<span className="ml-3 text-xs font-medium">Sync with Library</span>
<Tooltip label="This key will be registered with all devices running your Library">
<Info className="w-4 h-4 ml-1.5 text-ink-faint" />
</Tooltip>
<div className="flex-grow" />
<div className="space-x-2">
<Switch
className="bg-app-selected"
size="sm"
checked={autoMount}
onCheckedChange={(e) => {
if (librarySync && e) setLibrarySync(false);
setAutoMount(e);
}}
/>
</div>
<span className="ml-3 text-xs font-medium">Automount</span>
<Tooltip label="This key will be automatically mounted every time you unlock the key manager">
<Info className="w-4 h-4 ml-1.5 text-ink-faint" />
</Tooltip>
</div>
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
@ -99,13 +139,17 @@ export function KeyMounter() {
variant="accent"
disabled={key === ''}
onClick={() => {
if (key !== '') {
setKey('');
setKey('');
const [algorithm, hashing_algorithm] = getCryptoSettings(encryptionAlgo, hashingAlgo);
const [algorithm, hashing_algorithm] = getCryptoSettings(encryptionAlgo, hashingAlgo);
createKey.mutate({ algorithm, hashing_algorithm, key, library_sync: librarySync });
}
createKey.mutate({
algorithm,
hashing_algorithm,
key,
library_sync: librarySync,
automount: autoMount
});
}}
>
Mount Key

View file

@ -2,7 +2,7 @@ import clsx from 'clsx';
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { Button } from '../../../../ui/src';
import { Button, Input } from '../../../../ui/src';
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
import CreateLibraryDialog from '../dialog/CreateLibraryDialog';
@ -20,6 +20,27 @@ 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">

View file

@ -13,8 +13,10 @@ import { Eye, EyeSlash, Lock, Plus } from 'phosphor-react';
import { PropsWithChildren, useState } from 'react';
import { animated, useTransition } from 'react-spring';
import { AlertDialog, GenericAlertDialogState } from '../../../components/dialog/AlertDialog';
import { BackupRestoreDialog } from '../../../components/dialog/BackupRestoreDialog';
import { PasswordChangeDialog } from '../../../components/dialog/PasswordChangeDialog';
import { KeyViewerDialog } from '../../../components/dialog/KeyViewerDialog';
import { MasterPasswordChangeDialog } from '../../../components/dialog/MasterPasswordChangeDialog';
import { ListOfKeys } from '../../../components/key/KeyList';
import { KeyMounter } from '../../../components/key/KeyMounter';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
@ -91,141 +93,184 @@ export default function KeysSettings() {
const [showSecretKey, setShowSecretKey] = useState(false);
const [masterPassword, setMasterPassword] = useState('');
const [secretKey, setSecretKey] = useState('');
const keys = useLibraryQuery(['keys.list']);
const [alertDialogData, setAlertDialogData] = useState(GenericAlertDialogState);
const setShowAlertDialog = (state: boolean) => {
setAlertDialogData({ ...alertDialogData, open: state });
};
const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
if (!hasMasterPw?.data) {
return (
<div className="p-2 mr-20 ml-20 mt-10">
<div className="relative flex flex-grow mb-2">
<Input
value={masterPassword}
onChange={(e) => setMasterPassword(e.target.value)}
autoFocus
type={showMasterPassword ? 'text' : 'password'}
className="flex-grow !py-0.5"
placeholder="Master Password"
/>
<Button
onClick={() => setShowMasterPassword(!showMasterPassword)}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
>
<MPCurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<>
<div className="p-2 mr-20 ml-20 mt-10">
<div className="relative flex flex-grow mb-2">
<Input
value={masterPassword}
onChange={(e) => setMasterPassword(e.target.value)}
autoFocus
type={showMasterPassword ? 'text' : 'password'}
className="flex-grow !py-0.5"
placeholder="Master Password"
/>
<Button
onClick={() => setShowMasterPassword(!showMasterPassword)}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
>
<MPCurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<div className="relative flex flex-grow mb-2">
<Input
value={secretKey}
onChange={(e) => setSecretKey(e.target.value)}
type={showSecretKey ? 'text' : 'password'}
className="flex-grow !py-0.5"
placeholder="Secret Key"
/>
<Button
onClick={() => setShowSecretKey(!showSecretKey)}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
>
<SKCurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<div className="relative flex flex-grow mb-2">
<Input
value={secretKey}
onChange={(e) => setSecretKey(e.target.value)}
type={showSecretKey ? 'text' : 'password'}
className="flex-grow !py-0.5"
placeholder="Secret Key"
/>
<Button
onClick={() => setShowSecretKey(!showSecretKey)}
size="icon"
className="border-none absolute right-[5px] top-[5px]"
>
<SKCurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<Button
className="w-full"
variant="accent"
disabled={setMasterPasswordMutation.isLoading}
onClick={() => {
if (masterPassword !== '' && secretKey !== '') {
setMasterPassword('');
setSecretKey('');
setMasterPasswordMutation.mutate(
{ password: masterPassword, secret_key: secretKey },
{
onError: () => {
alert('Incorrect information provided.');
<Button
className="w-full"
variant="accent"
disabled={setMasterPasswordMutation.isLoading}
onClick={() => {
if (masterPassword !== '' && secretKey !== '') {
setMasterPassword('');
setSecretKey('');
setMasterPasswordMutation.mutate(
{ password: masterPassword, secret_key: secretKey },
{
onError: () => {
setAlertDialogData({
open: true,
title: 'Unlock Error',
description: '',
value: 'The information provided to the key manager was incorrect',
inputBox: false
});
}
}
}
);
}
}}
>
Unlock
</Button>
</div>
);
}
}}
>
Unlock
</Button>
</div>
<AlertDialog
open={alertDialogData.open}
setOpen={setShowAlertDialog}
title={alertDialogData.title}
description={alertDialogData.description}
value={alertDialogData.value}
inputBox={alertDialogData.inputBox}
/>
</>
);
} else {
return (
<SettingsContainer>
<SettingsHeader
title="Keys"
description="Manage your keys."
rightArea={
<div className="flex flex-row items-center">
<Button
size="icon"
onClick={() => {
unmountAll.mutate(null);
clearMasterPassword.mutate(null);
}}
variant="outline"
className="text-ink-faint"
>
<Lock className="w-4 h-4 text-ink-faint" />
</Button>
<KeyMounterDropdown
trigger={
<Button size="icon" variant="outline" className="text-ink-faint">
<Plus className="w-4 h-4 text-ink-faint" />
</Button>
}
>
<KeyMounter />
</KeyMounterDropdown>
</div>
}
<>
<SettingsContainer>
<SettingsHeader
title="Keys"
description="Manage your keys."
rightArea={
<div className="flex flex-row items-center">
<Button
size="icon"
onClick={() => {
unmountAll.mutate(null);
clearMasterPassword.mutate(null);
}}
variant="outline"
className="text-ink-faint"
>
<Lock className="w-4 h-4 text-ink-faint" />
</Button>
<KeyMounterDropdown
trigger={
<Button size="icon" variant="outline" className="text-ink-faint">
<Plus className="w-4 h-4 text-ink-faint" />
</Button>
}
>
<KeyMounter />
</KeyMounterDropdown>
</div>
}
/>
<div className="grid space-y-2">
<ListOfKeys />
</div>
<SettingsSubHeader title="Password Options" />
<div className="flex flex-row">
<MasterPasswordChangeDialog
setDialogData={setAlertDialogData}
trigger={
<Button size="sm" variant="gray" className="mr-2">
Change Master Password
</Button>
}
/>
<KeyViewerDialog
trigger={
<Button size="sm" variant="gray" className="mr-2" hidden={keys.data?.length === 0}>
View Key Values
</Button>
}
/>
</div>
<SettingsSubHeader title="Data Recovery" />
<div className="flex flex-row">
<Button
size="sm"
variant="gray"
className="mr-2"
type="button"
onClick={() => {
// not platform-safe, probably will break on web but `platform` doesn't have a save dialog option
save()?.then((result) => {
if (result) backupKeystore.mutate(result as string);
});
}}
>
Backup
</Button>
<BackupRestoreDialog
setDialogData={setAlertDialogData}
trigger={
<Button size="sm" variant="gray" className="mr-2">
Restore
</Button>
}
/>
</div>
</SettingsContainer>
<AlertDialog
open={alertDialogData.open}
setOpen={setShowAlertDialog}
title={alertDialogData.title}
description={alertDialogData.description}
value={alertDialogData.value}
inputBox={alertDialogData.inputBox}
/>
<div className="grid space-y-2">
<ListOfKeys />
</div>
<SettingsSubHeader title="Password Options" />
<div className="flex flex-row">
<PasswordChangeDialog
trigger={
<Button size="sm" variant="gray" className="mr-2">
Change Master Password
</Button>
}
/>
</div>
<SettingsSubHeader title="Data Recovery" />
<div className="flex flex-row">
<Button
size="sm"
variant="gray"
className="mr-2"
type="button"
onClick={() => {
// not platform-safe, probably will break on web but `platform` doesn't have a save dialog option
save()?.then((result) => {
if (result) backupKeystore.mutate(result as string);
});
}}
>
Backup
</Button>
<BackupRestoreDialog
trigger={
<Button size="sm" variant="gray" className="mr-2">
Restore
</Button>
}
/>
</div>
</SettingsContainer>
</>
);
}
}

View file

@ -423,6 +423,7 @@ importers:
autoprefixer: ^10.4.12
byte-size: ^8.1.0
clsx: ^1.2.1
crypto-random-string: ^5.0.0
dayjs: ^1.11.5
phosphor-react: ^1.4.1
prettier: ^2.7.1
@ -468,6 +469,7 @@ importers:
autoprefixer: 10.4.13
byte-size: 8.1.0
clsx: 1.2.1
crypto-random-string: 5.0.0
dayjs: 1.11.6
phosphor-react: 1.4.1_react@18.2.0
react: 18.2.0
@ -8474,7 +8476,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.19.6_@babel+core@7.20.5
magic-string: 0.26.7
react-refresh: 0.14.0
vite: 3.2.4_sass@1.56.1
vite: 3.2.4_@types+node@16.18.4
transitivePeerDependencies:
- supports-color
@ -11027,6 +11029,13 @@ packages:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'}
/crypto-random-string/5.0.0:
resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==}
engines: {node: '>=14.16'}
dependencies:
type-fest: 2.19.0
dev: false
/cspell-dictionary/6.15.1:
resolution: {integrity: sha512-VCx8URiNgOCYZkG6ThQKYMJ6jXyv4RC7C5H8yD8r3tBU81snYV4KWNqEdQCU9ClM+uHjz8FEANrF9hggB+KzuA==}
engines: {node: '>=14'}
@ -21567,6 +21576,11 @@ packages:
engines: {node: '>=8'}
dev: true
/type-fest/2.19.0:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
dev: false
/type-is/1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@ -22300,7 +22314,7 @@ packages:
dependencies:
'@rollup/pluginutils': 5.0.2
'@svgr/core': 6.5.1
vite: 3.2.4_sass@1.56.1
vite: 3.2.4_@types+node@16.18.4
transitivePeerDependencies:
- rollup
- supports-color
@ -22452,6 +22466,7 @@ packages:
sass: 1.56.1
optionalDependencies:
fsevents: 2.3.2
dev: true
/vlq/1.0.1:
resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==}