From da65ef59007e67a99b73b08a277b9ce74ee019aa Mon Sep 17 00:00:00 2001 From: jake <77554505+brxken128@users.noreply.github.com> Date: Mon, 12 Dec 2022 17:13:52 +0000 Subject: [PATCH] [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 --- core/src/api/keys.rs | 17 +- packages/client/src/core.ts | 2 +- packages/interface/package.json | 1 + .../src/components/dialog/AlertDialog.tsx | 63 ++++ .../components/dialog/BackupRestoreDialog.tsx | 68 ++-- .../components/dialog/DecryptFileDialog.tsx | 18 +- .../components/dialog/EncryptFileDialog.tsx | 45 +-- .../components/dialog/ExplorerAlertDialog.tsx | 27 -- .../src/components/dialog/KeyViewerDialog.tsx | 85 +++++ ...log.tsx => MasterPasswordChangeDialog.tsx} | 61 ++-- .../src/components/explorer/Explorer.tsx | 22 +- .../explorer/ExplorerContextMenu.tsx | 28 +- packages/interface/src/components/key/Key.tsx | 3 +- .../interface/src/components/key/KeyList.tsx | 22 +- .../src/components/key/KeyMounter.tsx | 72 ++++- .../src/components/onboarding/Onboarding.tsx | 23 +- .../screens/settings/library/KeysSetting.tsx | 293 ++++++++++-------- pnpm-lock.yaml | Bin 832303 -> 832840 bytes 18 files changed, 553 insertions(+), 297 deletions(-) create mode 100644 packages/interface/src/components/dialog/AlertDialog.tsx delete mode 100644 packages/interface/src/components/dialog/ExplorerAlertDialog.tsx create mode 100644 packages/interface/src/components/dialog/KeyViewerDialog.tsx rename packages/interface/src/components/dialog/{PasswordChangeDialog.tsx => MasterPasswordChangeDialog.tsx} (84%) diff --git a/core/src/api/keys.rs b/core/src/api/keys.rs index 47dcc94f5..0afc5a785 100644 --- a/core/src/api/keys.rs +++ b/core/src/api/keys.rs @@ -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)?; diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index b679afe18..1dbf8b261 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -108,7 +108,7 @@ export interface JobReport { id: string, name: string, data: Array | 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 } diff --git a/packages/interface/package.json b/packages/interface/package.json index d470b7f74..ede6ef8bf 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -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", diff --git a/packages/interface/src/components/dialog/AlertDialog.tsx b/packages/interface/src/components/dialog/AlertDialog.tsx new file mode 100644 index 000000000..ff6e00580 --- /dev/null +++ b/packages/interface/src/components/dialog/AlertDialog.tsx @@ -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 ( + { + props.setOpen(false); + }} + ctaLabel={props.label !== undefined ? props.label : 'Done'} + > + {props.inputBox && ( +
+ + +
+ )} + + {!props.inputBox &&
{props.value}
} +
+ ); +}; diff --git a/packages/interface/src/components/dialog/BackupRestoreDialog.tsx b/packages/interface/src/components/dialog/BackupRestoreDialog.tsx index f43580238..2f72c68a3 100644 --- a/packages/interface/src/components/dialog/BackupRestoreDialog.tsx +++ b/packages/interface/src/components/dialog/BackupRestoreDialog.tsx @@ -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({ defaultValues: { masterPassword: '', - secretKey: '', - filePath: '' + secretKey: '' } }); const onSubmit: SubmitHandler = (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} >
{
- - { - setShowRestorationFinalizationDialog(false); - }} - ctaLabel="Done" - trigger={<>} - > -
- {totalKeysImported}{' '} - {totalKeysImported !== 1 ? 'keys were imported.' : 'key was imported.'} -
-
); }; diff --git a/packages/interface/src/components/dialog/DecryptFileDialog.tsx b/packages/interface/src/components/dialog/DecryptFileDialog.tsx index 62abe3aa4..d187cc7fc 100644 --- a/packages/interface/src/components/dialog/DecryptFileDialog.tsx +++ b/packages/interface/src/components/dialog/DecryptFileDialog.tsx @@ -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); }} >
diff --git a/packages/interface/src/components/dialog/EncryptFileDialog.tsx b/packages/interface/src/components/dialog/EncryptFileDialog.tsx index 8abb23016..d15061342 100644 --- a/packages/interface/src/components/dialog/EncryptFileDialog.tsx +++ b/packages/interface/src/components/dialog/EncryptFileDialog.tsx @@ -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 ( - Key {key.uuid.substring(0, 8).toUpperCase()} - ); - })} - - ); -}; +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); }} >
@@ -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 && ( - - )} + {mountedUuids.data && }
diff --git a/packages/interface/src/components/dialog/ExplorerAlertDialog.tsx b/packages/interface/src/components/dialog/ExplorerAlertDialog.tsx deleted file mode 100644 index 97e15cf9d..000000000 --- a/packages/interface/src/components/dialog/ExplorerAlertDialog.tsx +++ /dev/null @@ -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 ( - <> - { - setOpen(false); - }} - > -
{text}
-
- - ); -}; diff --git a/packages/interface/src/components/dialog/KeyViewerDialog.tsx b/packages/interface/src/components/dialog/KeyViewerDialog.tsx new file mode 100644 index 000000000..93fd1c4da --- /dev/null +++ b/packages/interface/src/components/dialog/KeyViewerDialog.tsx @@ -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 ( + <> + { + setShowKeyViewerDialog(false); + }} + > +
+
+ Key + +
+
+
+
+ Value +
+ + +
+ +
+
+
+ + ); +}; diff --git a/packages/interface/src/components/dialog/PasswordChangeDialog.tsx b/packages/interface/src/components/dialog/MasterPasswordChangeDialog.tsx similarity index 84% rename from packages/interface/src/components/dialog/PasswordChangeDialog.tsx rename to packages/interface/src/components/dialog/MasterPasswordChangeDialog.tsx index f796d3177..aa87f1939 100644 --- a/packages/interface/src/components/dialog/PasswordChangeDialog.tsx +++ b/packages/interface/src/components/dialog/MasterPasswordChangeDialog.tsx @@ -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({ + const { register, handleSubmit, reset } = useForm({ defaultValues: { masterPassword: '', masterPassword2: '' @@ -27,7 +30,13 @@ export const PasswordChangeDialog = (props: { trigger: ReactNode }) => { const onSubmit: SubmitHandler = (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 }) => {
- { - setShowSecretKeyDialog(false); - }} - ctaLabel="Done" - trigger={<>} - > - - ); }; diff --git a/packages/interface/src/components/explorer/Explorer.tsx b/packages/interface/src/components/explorer/Explorer.tsx index c50fa98d6..96d1662f7 100644 --- a/packages/interface/src/components/explorer/Explorer.tsx +++ b/packages/interface/src/components/explorer/Explorer.tsx @@ -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) {
@@ -93,18 +92,18 @@ export default function Explorer(props: Props) {
- diff --git a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx index 3097884fd..9a5de9100 100644 --- a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx +++ b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx @@ -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); } }} /> diff --git a/packages/interface/src/components/key/Key.tsx b/packages/interface/src/components/key/Key.tsx index 9ee2d6b57..7cc4ece38 100644 --- a/packages/interface/src/components/key/Key.tsx +++ b/packages/interface/src/components/key/Key.tsx @@ -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 } diff --git a/packages/interface/src/components/key/KeyList.tsx b/packages/interface/src/components/key/KeyList.tsx index c0394e314..0371da203 100644 --- a/packages/interface/src/components/key/KeyList.tsx +++ b/packages/interface/src/components/key/KeyList.tsx @@ -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 Key {key.substring(0, 8).toUpperCase()}; + })} + + ); +}; + 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 }} /> diff --git a/packages/interface/src/components/key/KeyMounter.tsx b/packages/interface/src/components/key/KeyMounter.tsx index 523d3fdee..9f3ab51e7 100644 --- a/packages/interface/src/components/key/KeyMounter.tsx +++ b/packages/interface/src/components/key/KeyMounter.tsx @@ -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(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() {
+
+
+ { + setSliderValue(e); + setKey(GeneratePassword(e[0])); + }} + /> +
+ {sliderValue} +
+
{ + if (autoMount && e) setAutoMount(false); + setLibrarySync(e); + }} />
Sync with Library +
+
+ { + if (librarySync && e) setLibrarySync(false); + setAutoMount(e); + }} + /> +
+ Automount + + +
@@ -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 diff --git a/packages/interface/src/components/onboarding/Onboarding.tsx b/packages/interface/src/components/onboarding/Onboarding.tsx index 8067bd3f0..627ad333a 100644 --- a/packages/interface/src/components/onboarding/Onboarding.tsx +++ b/packages/interface/src/components/onboarding/Onboarding.tsx @@ -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() { )} >

Welcome to Spacedrive

+
+

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

+
+
+

Password:

+ +
+
+

Secret Key:

+ +
+
+
navigate('/overview')}> -
+ <> +
+
+ setMasterPassword(e.target.value)} + autoFocus + type={showMasterPassword ? 'text' : 'password'} + className="flex-grow !py-0.5" + placeholder="Master Password" + /> + +
-
- setSecretKey(e.target.value)} - type={showSecretKey ? 'text' : 'password'} - className="flex-grow !py-0.5" - placeholder="Secret Key" - /> - -
+
+ setSecretKey(e.target.value)} + type={showSecretKey ? 'text' : 'password'} + className="flex-grow !py-0.5" + placeholder="Secret Key" + /> + +
- -
+ ); + } + }} + > + Unlock + +
+ + ); } else { return ( - - - - - - - } - > - - - - } + <> + + + + + + + } + > + + + + } + /> +
+ +
+ + +
+ + Change Master Password + + } + /> +
+ + +
+ + + Restore + + } + /> +
+
+ -
- -
- - -
- - Change Master Password - - } - /> -
- - -
- - - Restore - - } - /> -
-
+ ); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49a9ff6e15d3412f6aa593f6165f03bc3afdfa07..6f895dfbd6f371e562131c072d4b871a167ddd59 100644 GIT binary patch delta 415 zcmZ4g%;>}`qYbX+GRZ}i1ts~qMTvPS`MJ8qB}JKe=~fDHrg{c?2Al27d)TD0s{kvQ zu5g+|y7``M`#oDmAZ7w$=I!@vSvD`9zTbmKV!HZEZk5Rm?IP_rpR#Pf`IL25#`G^f zY$DS)-R0B@@ea=li3l&Ra7!^uv^2>zbxsL#57#fTFbXNMaLIQ{woLKM3vhN!a`QBF zbt+HwGb=MJDYK|Fi8RbL&r8WJ(9g^EbPB2nt#GU=NOZ~zGRihin*Q+>r`+_@``G2D ze{khunI6BLot?!<&(L!EL}oVe_F_LaAZ7<*4j|?PVy^AQe%wKenbT5>r`OwX>V^h6 zdRQ146*#6ir6>FOI;Mr^IXi_KJG(_Cr$y;!8m2~NmAi%Nrxg2mm*!TMmIfH-WtOI< z8-+TSW=BN28MqobI_4)^7)1pKr&)R#ga4Zi6j(LBQ2n^Sn!_yGWLK$I8& delta 121 zcmX@{%4q#FqYbX+n={N?*`{Zn;Sgz7wrf|mV+3L*AZFgKY{#;B`Siz;JkFC1;#k|& zp0R9Kd&W8|qkXGC8xXStF$WNH0x{S2t^V9Wi?^Tn%pIEo(qghbF@i^fZ+d+)4=Ynn Q;q*iwChhGf(|GFr07wxolmGw#