[ENG-329] Crypto dialog refactor (#509)

* update backup restoration dialog

* restructure MPC dialog

* refactor `EncryptFileDialog`

* dialog changes

Co-authored-by: Brendan Allan <brendonovich@outlook.com>
This commit is contained in:
jake 2023-01-11 08:38:45 +00:00 committed by GitHub
parent 50b788f9e6
commit 434bc81deb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 427 additions and 396 deletions

View file

@ -2,7 +2,7 @@ import { useLibraryMutation } from '@sd/client';
import { Button, Dialog, Input } from '@sd/ui'; import { Button, Dialog, Input } from '@sd/ui';
import { Eye, EyeSlash } from 'phosphor-react'; import { Eye, EyeSlash } from 'phosphor-react';
import { ReactNode, useState } from 'react'; import { ReactNode, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { usePlatform } from '../../util/Platform'; import { usePlatform } from '../../util/Platform';
import { GenericAlertDialogProps } from './AlertDialog'; import { GenericAlertDialogProps } from './AlertDialog';
@ -20,6 +20,39 @@ export interface BackupRestorationDialogProps {
export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => { export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => {
const platform = usePlatform(); const platform = usePlatform();
const restoreKeystoreMutation = useLibraryMutation('keys.restoreKeystore', {
onSuccess: (total) => {
setShow((old) => ({ ...old, backupRestoreDialog: false }));
props.setAlertDialogData({
open: true,
title: 'Import Successful',
description: '',
value: `${total} ${total !== 1 ? 'keys were imported.' : 'key was imported.'}`,
inputBox: false
});
},
onError: () => {
setShow((old) => ({ ...old, backupRestoreDialog: false }));
props.setAlertDialogData({
open: true,
title: 'Import Error',
description: '',
value: 'There was an error while restoring your backup.',
inputBox: false
});
}
});
const [show, setShow] = useState({
backupRestoreDialog: false,
masterPassword: false,
secretKey: false
});
const MPCurrentEyeIcon = show.masterPassword ? EyeSlash : Eye;
const SKCurrentEyeIcon = show.secretKey ? EyeSlash : Eye;
const form = useForm<FormValues>({ const form = useForm<FormValues>({
defaultValues: { defaultValues: {
masterPassword: '', masterPassword: '',
@ -28,124 +61,89 @@ export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => {
} }
}); });
const onSubmit: SubmitHandler<FormValues> = (data) => { const onSubmit = form.handleSubmit((data) => {
const sk = data.secretKey || null; const sk = data.secretKey || null;
if (data.filePath !== '') { if (data.filePath !== '') {
restoreKeystoreMutation.mutate( restoreKeystoreMutation.mutate({
{ password: data.masterPassword,
password: data.masterPassword, secret_key: sk,
secret_key: sk, path: data.filePath
path: data.filePath });
},
{
onSuccess: (total) => {
setShowBackupRestoreDialog(false);
props.setAlertDialogData({
open: true,
title: 'Import Successful',
description: '',
value: `${total} ${total !== 1 ? 'keys were imported.' : 'key was imported.'}`,
inputBox: false
});
},
onError: () => {
setShowBackupRestoreDialog(false);
props.setAlertDialogData({
open: true,
title: 'Import Error',
description: '',
value: 'There was an error while restoring your backup.',
inputBox: false
});
}
}
);
form.reset(); form.reset();
} }
}; });
const [showBackupRestoreDialog, setShowBackupRestoreDialog] = useState(false);
const restoreKeystoreMutation = useLibraryMutation('keys.restoreKeystore');
const [showMasterPassword, setShowMasterPassword] = useState(false);
const [showSecretKey, setShowSecretKey] = useState(false);
const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
return ( return (
<> <form onSubmit={onSubmit}>
<form onSubmit={form.handleSubmit(onSubmit)}> <Dialog
<Dialog open={show.backupRestoreDialog}
open={showBackupRestoreDialog} setOpen={(e) => setShow((old) => ({ ...old, backupRestoreDialog: e }))}
setOpen={setShowBackupRestoreDialog} title="Restore Keys"
title="Restore Keys" description="Restore keys from a backup."
description="Restore keys from a backup." loading={restoreKeystoreMutation.isLoading}
loading={restoreKeystoreMutation.isLoading} ctaLabel="Restore"
ctaLabel="Restore" trigger={props.trigger}
trigger={props.trigger} >
> <div className="relative flex flex-grow mt-3 mb-2">
<div className="relative flex flex-grow mt-3 mb-2"> <Input
<Input className="flex-grow !py-0.5"
className="flex-grow !py-0.5" placeholder="Master Password"
placeholder="Master Password" required
required type={show.masterPassword ? 'text' : 'password'}
type={showMasterPassword ? 'text' : 'password'} {...form.register('masterPassword', { required: true })}
{...form.register('masterPassword', { required: true })} />
/> <Button
<Button onClick={() => setShow((old) => ({ ...old, masterPassword: !old.masterPassword }))}
onClick={() => setShowMasterPassword(!showMasterPassword)} size="icon"
size="icon" className="border-none absolute right-[5px] top-[5px]"
className="border-none absolute right-[5px] top-[5px]" type="button"
type="button" >
> <MPCurrentEyeIcon className="w-4 h-4" />
<MPCurrentEyeIcon className="w-4 h-4" /> </Button>
</Button> </div>
</div> <div className="relative flex flex-grow mb-3">
<div className="relative flex flex-grow mb-3"> <Input
<Input className="flex-grow !py-0.5"
className="flex-grow !py-0.5" placeholder="Secret Key"
placeholder="Secret Key" {...form.register('secretKey', { required: false })}
{...form.register('secretKey', { required: false })} type={show.secretKey ? 'text' : 'password'}
type={showSecretKey ? 'text' : 'password'} />
/> <Button
<Button onClick={() => setShow((old) => ({ ...old, secretKey: !old.secretKey }))}
onClick={() => setShowSecretKey(!showSecretKey)} size="icon"
size="icon" className="border-none absolute right-[5px] top-[5px]"
className="border-none absolute right-[5px] top-[5px]" type="button"
type="button" >
> <SKCurrentEyeIcon className="w-4 h-4" />
<SKCurrentEyeIcon className="w-4 h-4" /> </Button>
</Button> </div>
</div> <div className="relative flex flex-grow mb-2">
<div className="relative flex flex-grow mb-2"> <Button
<Button size="sm"
size="sm" variant={form.watch('filePath') !== '' ? 'accent' : 'gray'}
variant={form.watch('filePath') !== '' ? 'accent' : 'gray'} type="button"
type="button" onClick={() => {
onClick={() => { if (!platform.openFilePickerDialog) {
if (!platform.openFilePickerDialog) { // TODO: Support opening locations on web
// TODO: Support opening locations on web props.setAlertDialogData({
props.setAlertDialogData({ open: true,
open: true, title: 'Error',
title: 'Error', description: '',
description: '', value: "System dialogs aren't supported on this platform.",
value: "System dialogs aren't supported on this platform.", inputBox: false
inputBox: false
});
return;
}
platform.openFilePickerDialog().then((result) => {
if (result) form.setValue('filePath', result as string);
}); });
}} return;
> }
Select File platform.openFilePickerDialog().then((result) => {
</Button> if (result) form.setValue('filePath', result as string);
</div> });
</Dialog> }}
</form> >
</> Select File
</Button>
</div>
</Dialog>
</form>
); );
}; };

View file

@ -3,6 +3,7 @@ import { useLibraryMutation, useLibraryQuery } from '@sd/client';
import { Button, Dialog, Input, Switch } from '@sd/ui'; import { Button, Dialog, Input, Switch } from '@sd/ui';
import { Eye, EyeSlash, Info } from 'phosphor-react'; import { Eye, EyeSlash, Info } from 'phosphor-react';
import { useState } from 'react'; import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { usePlatform } from '../../util/Platform'; import { usePlatform } from '../../util/Platform';
import { Tooltip } from '../tooltip/Tooltip'; import { Tooltip } from '../tooltip/Tooltip';
@ -16,23 +17,23 @@ interface DecryptDialogProps {
setAlertDialogData: (data: GenericAlertDialogProps) => void; setAlertDialogData: (data: GenericAlertDialogProps) => void;
} }
type FormValues = {
type: 'password' | 'key';
outputPath: string;
password: string;
saveToKeyManager: boolean;
};
export const DecryptFileDialog = (props: DecryptDialogProps) => { export const DecryptFileDialog = (props: DecryptDialogProps) => {
const platform = usePlatform(); const platform = usePlatform();
const { location_id, path_id } = props;
const decryptFile = useLibraryMutation('files.decryptFiles');
const [outputPath, setOutputpath] = useState('');
const [password, setPassword] = useState('');
const [saveToKeyManager, setSaveToKeyManager] = useState(true);
const [showPassword, setShowPassword] = useState(false);
const PasswordCurrentEyeIcon = showPassword ? EyeSlash : Eye;
const mountedUuids = useLibraryQuery(['keys.listMounted'], { const mountedUuids = useLibraryQuery(['keys.listMounted'], {
onSuccess: (data) => { onSuccess: (data) => {
hasMountedKeys = data.length > 0 ? true : false; hasMountedKeys = data.length > 0 ? true : false;
if (!hasMountedKeys) { if (!hasMountedKeys) {
setDecryptType('password'); form.setValue('type', 'password');
} else { } else {
setDecryptType('key'); form.setValue('type', 'key');
} }
} }
}); });
@ -40,10 +41,63 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
let hasMountedKeys = let hasMountedKeys =
mountedUuids.data !== undefined && mountedUuids.data.length > 0 ? true : false; mountedUuids.data !== undefined && mountedUuids.data.length > 0 ? true : false;
const [decryptType, setDecryptType] = useState(hasMountedKeys ? 'key' : 'password'); const decryptFile = useLibraryMutation('files.decryptFiles', {
onSuccess: () => {
props.setAlertDialogData({
open: true,
title: 'Info',
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',
value: 'The decryption job failed to start.',
inputBox: false,
description: ''
});
}
});
const [show, setShow] = useState({ password: false });
const PasswordCurrentEyeIcon = show.password ? EyeSlash : Eye;
const form = useForm<FormValues>({
defaultValues: {
type: hasMountedKeys ? 'key' : 'password',
outputPath: '',
password: '',
saveToKeyManager: true
}
});
const onSubmit = form.handleSubmit((data) => {
const output = data.outputPath !== '' ? data.outputPath : null;
const pw = data.type === 'password' ? data.password : null;
const save = data.type === 'password' ? data.saveToKeyManager : null;
props.setOpen(false);
props.location_id &&
props.path_id &&
decryptFile.mutate({
location_id: props.location_id,
path_id: props.path_id,
output_path: output,
password: pw,
save_to_library: save
});
form.reset();
});
return ( return (
<> <form onSubmit={onSubmit}>
<Dialog <Dialog
open={props.open} open={props.open}
setOpen={props.setOpen} setOpen={props.setOpen}
@ -51,48 +105,12 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
description="Leave the output file blank for the default." description="Leave the output file blank for the default."
loading={decryptFile.isLoading} loading={decryptFile.isLoading}
ctaLabel="Decrypt" ctaLabel="Decrypt"
ctaAction={() => {
const output = outputPath !== '' ? outputPath : null;
const pw = decryptType === 'password' ? password : null;
const save = decryptType === 'password' ? saveToKeyManager : null;
props.setOpen(false);
location_id &&
path_id &&
decryptFile.mutate(
{
location_id,
path_id,
output_path: output,
password: pw,
save_to_library: save
},
{
onSuccess: () => {
props.setAlertDialogData({
open: true,
title: 'Info',
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',
value: 'The decryption job failed to start.',
inputBox: false,
description: ''
});
}
}
);
}}
> >
<RadioGroup value={decryptType} onChange={setDecryptType} className="mt-2"> <RadioGroup
value={form.watch('type')}
onChange={(e: 'key' | 'password') => form.setValue('type', e)}
className="mt-2"
>
<span className="text-xs font-bold">Key Type</span> <span className="text-xs font-bold">Key Type</span>
<div className="flex flex-row gap-2 mt-2"> <div className="flex flex-row gap-2 mt-2">
<RadioGroup.Option disabled={!hasMountedKeys} value="key"> <RadioGroup.Option disabled={!hasMountedKeys} value="key">
@ -117,19 +135,18 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
</div> </div>
</RadioGroup> </RadioGroup>
{decryptType === 'password' && ( {form.watch('type') === 'password' && (
<> <>
<div className="relative flex flex-grow mt-3 mb-2"> <div className="relative flex flex-grow mt-3 mb-2">
<Input <Input
className={`flex-grow w-max !py-0.5`} className={`flex-grow w-max !py-0.5`}
placeholder="Password" placeholder="Password"
onChange={(e) => setPassword(e.target.value)} {...form.register('password', { required: false })}
value={password} type={show.password ? 'text' : 'password'}
type={showPassword ? 'text' : 'password'}
required required
/> />
<Button <Button
onClick={() => setShowPassword(!showPassword)} onClick={() => setShow((old) => ({ ...old, password: !old.password }))}
size="icon" size="icon"
className="border-none absolute right-[5px] top-[5px]" className="border-none absolute right-[5px] top-[5px]"
type="button" type="button"
@ -143,8 +160,8 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
<Switch <Switch
className="bg-app-selected" className="bg-app-selected"
size="sm" size="sm"
checked={saveToKeyManager} checked={form.watch('saveToKeyManager')}
onCheckedChange={setSaveToKeyManager} onCheckedChange={(e) => form.setValue('saveToKeyManager', e)}
/> />
</div> </div>
<span className="ml-3 text-xs font-medium mt-0.5">Save to Key Manager</span> <span className="ml-3 text-xs font-medium mt-0.5">Save to Key Manager</span>
@ -161,7 +178,7 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
<Button <Button
size="sm" size="sm"
variant={outputPath !== '' ? 'accent' : 'gray'} variant={form.watch('outputPath') !== '' ? 'accent' : 'gray'}
className="h-[23px] text-xs leading-3 mt-2" className="h-[23px] text-xs leading-3 mt-2"
type="button" type="button"
onClick={() => { onClick={() => {
@ -178,7 +195,7 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
return; return;
} }
platform.saveFilePickerDialog().then((result) => { platform.saveFilePickerDialog().then((result) => {
if (result) setOutputpath(result as string); if (result) form.setValue('outputPath', result as string);
}); });
}} }}
> >
@ -187,6 +204,6 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
</div> </div>
</div> </div>
</Dialog> </Dialog>
</> </form>
); );
}; };

View file

@ -1,6 +1,7 @@
import { Algorithm, useLibraryMutation, useLibraryQuery } from '@sd/client'; import { Algorithm, useLibraryMutation, useLibraryQuery } from '@sd/client';
import { Button, Dialog, Select, SelectOption } from '@sd/ui'; import { Button, Dialog, Select, SelectOption } from '@sd/ui';
import { useState } from 'react'; import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { getHashingAlgorithmString } from '../../screens/settings/library/KeysSetting'; import { getHashingAlgorithmString } from '../../screens/settings/library/KeysSetting';
import { usePlatform } from '../../util/Platform'; import { usePlatform } from '../../util/Platform';
@ -16,19 +17,25 @@ interface EncryptDialogProps {
setAlertDialogData: (data: GenericAlertDialogProps) => void; setAlertDialogData: (data: GenericAlertDialogProps) => void;
} }
type FormValues = {
key: string;
encryptionAlgo: string;
hashingAlgo: string;
metadata: boolean;
previewMedia: boolean;
outputPath: string;
};
export const EncryptFileDialog = (props: EncryptDialogProps) => { export const EncryptFileDialog = (props: EncryptDialogProps) => {
const { location_id, path_id } = props;
const platform = usePlatform(); const platform = usePlatform();
// the selected key will be random, we should prioritise the default const UpdateKey = (uuid: string) => {
const [key, setKey] = useState(''); form.setValue('key', uuid);
const hashAlg = keys.data?.find((key) => {
// decided against react-hook-form, as it doesn't allow us to work with select boxes and such return key.uuid === uuid;
const [metadata, setMetadata] = useState(false); })?.hashing_algorithm;
const [previewMedia, setPreviewMedia] = useState(false); hashAlg && form.setValue('hashingAlgo', getHashingAlgorithmString(hashAlg));
const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305'); };
const [hashingAlgo, setHashingAlgo] = useState('');
const [outputPath, setOutputpath] = useState('');
const keys = useLibraryQuery(['keys.list']); const keys = useLibraryQuery(['keys.list']);
const mountedUuids = useLibraryQuery(['keys.listMounted'], { const mountedUuids = useLibraryQuery(['keys.listMounted'], {
@ -37,18 +44,60 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
} }
}); });
const UpdateKey = (uuid: string) => { const encryptFile = useLibraryMutation('files.encryptFiles', {
setKey(uuid); onSuccess: () => {
const hashAlg = keys.data?.find((key) => { props.setAlertDialogData({
return key.uuid === uuid; open: true,
})?.hashing_algorithm; title: 'Success',
hashAlg && setHashingAlgo(getHashingAlgorithmString(hashAlg)); 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',
value: 'The encryption job failed to start.',
inputBox: false,
description: ''
});
}
});
const encryptFile = useLibraryMutation('files.encryptFiles'); const form = useForm<FormValues>({
defaultValues: {
key: '',
encryptionAlgo: 'XChaCha20Poly1305',
hashingAlgo: 'Argon2id-s',
metadata: false,
previewMedia: false,
outputPath: ''
}
});
const onSubmit = form.handleSubmit((data) => {
const output = data.outputPath !== '' ? data.outputPath : null;
props.setOpen(false);
props.location_id &&
props.path_id &&
encryptFile.mutate({
algorithm: data.encryptionAlgo as Algorithm,
key_uuid: data.key,
location_id: props.location_id,
path_id: props.path_id,
metadata: data.metadata,
preview_media: data.previewMedia,
output_path: output
});
form.reset();
});
return ( return (
<> <form onSubmit={onSubmit}>
<Dialog <Dialog
open={props.open} open={props.open}
setOpen={props.setOpen} setOpen={props.setOpen}
@ -56,52 +105,13 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
description="Configure your encryption settings. Leave the output file blank for the default." description="Configure your encryption settings. Leave the output file blank for the default."
loading={encryptFile.isLoading} loading={encryptFile.isLoading}
ctaLabel="Encrypt" ctaLabel="Encrypt"
ctaAction={() => {
const output = outputPath !== '' ? outputPath : null;
props.setOpen(false);
location_id &&
path_id &&
encryptFile.mutate(
{
algorithm: encryptionAlgo as Algorithm,
key_uuid: key,
location_id,
path_id,
metadata,
preview_media: previewMedia,
output_path: output
},
{
onSuccess: () => {
props.setAlertDialogData({
open: true,
title: 'Success',
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',
value: 'The encryption job failed to start.',
inputBox: false,
description: ''
});
}
}
);
}}
> >
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3"> <div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-xs font-bold">Key</span> <span className="text-xs font-bold">Key</span>
<Select <Select
className="mt-2" className="mt-2"
value={key} value={form.watch('key')}
onChange={(e) => { onChange={(e) => {
UpdateKey(e); UpdateKey(e);
}} }}
@ -114,7 +124,7 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
<Button <Button
size="sm" size="sm"
variant={outputPath !== '' ? 'accent' : 'gray'} variant={form.watch('outputPath') !== '' ? 'accent' : 'gray'}
className="h-[23px] text-xs leading-3 mt-2" className="h-[23px] text-xs leading-3 mt-2"
type="button" type="button"
onClick={() => { onClick={() => {
@ -131,7 +141,7 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
return; return;
} }
platform.saveFilePickerDialog().then((result) => { platform.saveFilePickerDialog().then((result) => {
if (result) setOutputpath(result as string); if (result) form.setValue('outputPath', result as string);
}); });
}} }}
> >
@ -143,7 +153,11 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3"> <div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-xs font-bold">Encryption</span> <span className="text-xs font-bold">Encryption</span>
<Select className="mt-2" value={encryptionAlgo} onChange={(e) => setEncryptionAlgo(e)}> <Select
className="mt-2"
value={form.watch('encryptionAlgo')}
onChange={(e) => form.setValue('encryptionAlgo', e)}
>
<SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption> <SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption>
<SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption> <SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption>
</Select> </Select>
@ -154,7 +168,7 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
className="mt-2 text-gray-400/80" className="mt-2 text-gray-400/80"
onChange={() => {}} onChange={() => {}}
disabled disabled
value={hashingAlgo} value={form.watch('hashingAlgo')}
> >
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption> <SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption> <SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
@ -169,14 +183,20 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3"> <div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
<div className="flex"> <div className="flex">
<span className="text-sm font-bold mr-3 ml-0.5 mt-0.5">Metadata</span> <span className="text-sm font-bold mr-3 ml-0.5 mt-0.5">Metadata</span>
<Checkbox checked={metadata} onChange={(e) => setMetadata(e.target.checked)} /> <Checkbox
checked={form.watch('metadata')}
onChange={(e) => form.setValue('metadata', e.target.checked)}
/>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-sm font-bold mr-3 ml-0.5 mt-0.5">Preview Media</span> <span className="text-sm font-bold mr-3 ml-0.5 mt-0.5">Preview Media</span>
<Checkbox checked={previewMedia} onChange={(e) => setPreviewMedia(e.target.checked)} /> <Checkbox
checked={form.watch('previewMedia')}
onChange={(e) => form.setValue('previewMedia', e.target.checked)}
/>
</div> </div>
</div> </div>
</Dialog> </Dialog>
</> </form>
); );
}; };

View file

@ -24,18 +24,6 @@ type FormValues = {
}; };
export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProps) => { export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProps) => {
const { trigger } = props;
const form = useForm<FormValues>({
defaultValues: {
masterPassword: '',
masterPassword2: '',
secretKey: '',
encryptionAlgo: 'XChaCha20Poly1305',
hashingAlgo: 'Argon2id-s'
}
});
const changeMasterPassword = useLibraryMutation('keys.changeMasterPassword', { const changeMasterPassword = useLibraryMutation('keys.changeMasterPassword', {
onSuccess: () => { onSuccess: () => {
setShow((old) => ({ ...old, masterPasswordDialog: false })); setShow((old) => ({ ...old, masterPasswordDialog: false }));
@ -71,6 +59,16 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp
const MP2CurrentEyeIcon = show.masterPassword2 ? EyeSlash : Eye; const MP2CurrentEyeIcon = show.masterPassword2 ? EyeSlash : Eye;
const SKCurrentEyeIcon = show.secretKey ? EyeSlash : Eye; const SKCurrentEyeIcon = show.secretKey ? EyeSlash : Eye;
const form = useForm<FormValues>({
defaultValues: {
masterPassword: '',
masterPassword2: '',
secretKey: '',
encryptionAlgo: 'XChaCha20Poly1305',
hashingAlgo: 'Argon2id-s'
}
});
const onSubmit = form.handleSubmit((data) => { const onSubmit = form.handleSubmit((data) => {
if (data.masterPassword !== data.masterPassword2) { if (data.masterPassword !== data.masterPassword2) {
props.setAlertDialogData({ props.setAlertDialogData({
@ -96,153 +94,151 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp
}); });
return ( return (
<> <form onSubmit={onSubmit}>
<form onSubmit={onSubmit}> <Dialog
<Dialog open={show.masterPasswordDialog}
open={show.masterPasswordDialog} setOpen={(e) => {
setOpen={(e) => { setShow((old) => ({ ...old, masterPasswordDialog: e }));
setShow((old) => ({ ...old, masterPasswordDialog: e })); }}
}} title="Change Master Password"
title="Change Master Password" description="Select a new master password for your key manager. Leave the key secret blank to disable it."
description="Select a new master password for your key manager. Leave the key secret blank to disable it." ctaDanger={true}
ctaDanger={true} loading={changeMasterPassword.isLoading}
loading={changeMasterPassword.isLoading} ctaLabel="Change"
ctaLabel="Change" trigger={props.trigger}
trigger={trigger} >
> <div className="relative flex flex-grow mt-3 mb-2">
<div className="relative flex flex-grow mt-3 mb-2"> <Input
<Input className={`flex-grow w-max !py-0.5`}
className={`flex-grow w-max !py-0.5`} placeholder="New password"
placeholder="New password" required
required {...form.register('masterPassword', { required: true })}
{...form.register('masterPassword', { required: true })} type={show.masterPassword ? 'text' : 'password'}
type={show.masterPassword ? 'text' : 'password'} />
/> <Button
<Button onClick={() => {
onClick={() => { const password = generatePassword(32);
const password = generatePassword(32); form.setValue('masterPassword', password);
form.setValue('masterPassword', password); form.setValue('masterPassword2', password);
form.setValue('masterPassword2', password); setShow((old) => ({
setShow((old) => ({ ...old,
...old, masterPassword: true,
masterPassword: true, masterPassword2: true
masterPassword2: true }));
})); }}
}} size="icon"
size="icon" className="border-none absolute right-[65px] top-[5px]"
className="border-none absolute right-[65px] top-[5px]" type="button"
type="button" >
> <ArrowsClockwise className="w-4 h-4" />
<ArrowsClockwise className="w-4 h-4" /> </Button>
</Button> <Button
<Button type="button"
type="button" onClick={() => {
onClick={() => { navigator.clipboard.writeText(form.watch('masterPassword') as string);
navigator.clipboard.writeText(form.watch('masterPassword') as string); }}
}} size="icon"
size="icon" className="border-none absolute right-[35px] top-[5px]"
className="border-none absolute right-[35px] top-[5px]" >
> <Clipboard className="w-4 h-4" />
<Clipboard className="w-4 h-4" /> </Button>
</Button> <Button
<Button onClick={() => setShow((old) => ({ ...old, masterPassword: !old.masterPassword }))}
onClick={() => setShow((old) => ({ ...old, masterPassword: !old.masterPassword }))} size="icon"
size="icon" className="border-none absolute right-[5px] top-[5px]"
className="border-none absolute right-[5px] top-[5px]" type="button"
type="button" >
> <MP1CurrentEyeIcon className="w-4 h-4" />
<MP1CurrentEyeIcon className="w-4 h-4" /> </Button>
</Button> </div>
</div> <div className="relative flex flex-grow mb-2">
<div className="relative flex flex-grow mb-2"> <Input
<Input className={`flex-grow !py-0.5}`}
className={`flex-grow !py-0.5}`} placeholder="New password (again)"
placeholder="New password (again)" required
required {...form.register('masterPassword2', { required: true })}
{...form.register('masterPassword2', { required: true })} type={show.masterPassword2 ? 'text' : 'password'}
type={show.masterPassword2 ? 'text' : 'password'} />
/> <Button
<Button onClick={() => setShow((old) => ({ ...old, masterPassword2: !old.masterPassword2 }))}
onClick={() => setShow((old) => ({ ...old, masterPassword2: !old.masterPassword2 }))} size="icon"
size="icon" className="border-none absolute right-[5px] top-[5px]"
className="border-none absolute right-[5px] top-[5px]" type="button"
type="button" >
> <MP2CurrentEyeIcon className="w-4 h-4" />
<MP2CurrentEyeIcon className="w-4 h-4" /> </Button>
</Button> </div>
</div>
<div className="relative flex flex-grow mb-2"> <div className="relative flex flex-grow mb-2">
<Input <Input
className={`flex-grow !py-0.5}`} className={`flex-grow !py-0.5}`}
placeholder="Key secret" placeholder="Key secret"
{...form.register('secretKey', { required: false })} {...form.register('secretKey', { required: false })}
type={show.secretKey ? 'text' : 'password'} type={show.secretKey ? 'text' : 'password'}
/> />
<Button <Button
// onClick={() => setmasterPassword2(!masterPassword2)} // onClick={() => setmasterPassword2(!masterPassword2)}
onClick={() => { onClick={() => {
form.setValue('secretKey', cryptoRandomString({ length: 24 })); form.setValue('secretKey', cryptoRandomString({ length: 24 }));
setShow((old) => ({ ...old, secretKey: true })); setShow((old) => ({ ...old, secretKey: true }));
}} }}
size="icon" size="icon"
className="border-none absolute right-[65px] top-[5px]" className="border-none absolute right-[65px] top-[5px]"
type="button" type="button"
> >
<ArrowsClockwise className="w-4 h-4" /> <ArrowsClockwise className="w-4 h-4" />
</Button> </Button>
<Button <Button
type="button" type="button"
onClick={() => { onClick={() => {
navigator.clipboard.writeText(form.watch('secretKey') as string); navigator.clipboard.writeText(form.watch('secretKey') as string);
}} }}
size="icon" size="icon"
className="border-none absolute right-[35px] top-[5px]" className="border-none absolute right-[35px] top-[5px]"
> >
<Clipboard className="w-4 h-4" /> <Clipboard className="w-4 h-4" />
</Button> </Button>
<Button <Button
onClick={() => setShow((old) => ({ ...old, secretKey: !old.secretKey }))} onClick={() => setShow((old) => ({ ...old, secretKey: !old.secretKey }))}
size="icon" size="icon"
className="border-none absolute right-[5px] top-[5px]" className="border-none absolute right-[5px] top-[5px]"
type="button" type="button"
> >
<SKCurrentEyeIcon className="w-4 h-4" /> <SKCurrentEyeIcon className="w-4 h-4" />
</Button> </Button>
</div> </div>
<PasswordMeter password={form.watch('masterPassword')} /> <PasswordMeter password={form.watch('masterPassword')} />
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3"> <div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-xs font-bold">Encryption</span> <span className="text-xs font-bold">Encryption</span>
<Select <Select
className="mt-2" className="mt-2"
value={form.watch('encryptionAlgo')} value={form.watch('encryptionAlgo')}
onChange={(e) => form.setValue('encryptionAlgo', e)} onChange={(e) => form.setValue('encryptionAlgo', e)}
> >
<SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption> <SelectOption value="XChaCha20Poly1305">XChaCha20-Poly1305</SelectOption>
<SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption> <SelectOption value="Aes256Gcm">AES-256-GCM</SelectOption>
</Select> </Select>
</div>
<div className="flex flex-col">
<span className="text-xs font-bold">Hashing</span>
<Select
className="mt-2"
value={form.watch('hashingAlgo')}
onChange={(e) => form.setValue('hashingAlgo', e)}
>
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
<SelectOption value="BalloonBlake3-s">BLAKE3-Balloon (standard)</SelectOption>
<SelectOption value="BalloonBlake3-h">BLAKE3-Balloon (hardened)</SelectOption>
<SelectOption value="BalloonBlake3-p">BLAKE3-Balloon (paranoid)</SelectOption>
</Select>
</div>
</div> </div>
</Dialog> <div className="flex flex-col">
</form> <span className="text-xs font-bold">Hashing</span>
</> <Select
className="mt-2"
value={form.watch('hashingAlgo')}
onChange={(e) => form.setValue('hashingAlgo', e)}
>
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
<SelectOption value="BalloonBlake3-s">BLAKE3-Balloon (standard)</SelectOption>
<SelectOption value="BalloonBlake3-h">BLAKE3-Balloon (hardened)</SelectOption>
<SelectOption value="BalloonBlake3-p">BLAKE3-Balloon (paranoid)</SelectOption>
</Select>
</div>
</div>
</Dialog>
</form>
); );
}; };