From 190d0e677c812967a92ca63f979555e67ecaaf44 Mon Sep 17 00:00:00 2001 From: Anthony Potdevin <31413433+apotdevin@users.noreply.github.com> Date: Sat, 20 Jul 2024 14:22:03 -0500 Subject: [PATCH] fix: correct encryption key (#25) * fix: correct encryption key * refactor: params --- src/app/(app)/(layout)/temp/recover/page.tsx | 100 ++++++ src/components/button/VaultButton.tsx | 91 +++-- src/components/form/LoginForm.tsx | 8 +- src/components/wallet/SendAddressForm.tsx | 8 +- src/components/wallet/SendInvoiceForm.tsx | 8 +- .../queries/__generated__/user.generated.tsx | 2 + src/graphql/queries/user.ts | 1 + src/graphql/types.ts | 33 ++ src/hooks/message.ts | 7 +- src/stores/keys.ts | 12 +- src/utils/crypto.ts | 83 ++++- src/views/contacts/Messages.tsx | 8 +- src/views/contacts/boxes/PayMessageBox.tsx | 19 +- src/views/contacts/boxes/SendMessageBox.tsx | 8 +- src/views/wallet/NewWallet.tsx | 10 +- src/views/wallet/RestoreWallet.tsx | 10 +- src/views/wallet/Settings.tsx | 22 +- src/workers/account/account.ts | 128 +++---- src/workers/account/types.ts | 25 +- src/workers/crypto/crypto.ts | 327 ++++++++++-------- src/workers/crypto/types.ts | 14 +- 21 files changed, 594 insertions(+), 330 deletions(-) create mode 100644 src/app/(app)/(layout)/temp/recover/page.tsx diff --git a/src/app/(app)/(layout)/temp/recover/page.tsx b/src/app/(app)/(layout)/temp/recover/page.tsx new file mode 100644 index 00000000..640d2488 --- /dev/null +++ b/src/app/(app)/(layout)/temp/recover/page.tsx @@ -0,0 +1,100 @@ +'use client'; + +import { hexToBytes } from '@noble/hashes/utils'; +import { nip44 } from 'nostr-tools'; +import { useCallback, useEffect, useState } from 'react'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useToast } from '@/components/ui/use-toast'; +import { useUserQuery } from '@/graphql/queries/__generated__/user.generated'; +import { generateMasterKeyAndHash } from '@/utils/crypto'; + +export default function Page() { + const { data } = useUserQuery(); + + const { toast } = useToast(); + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [protectedMnemonic, setProtectedMnemonic] = useState(''); + const [mnemonic, setMnemonic] = useState(''); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (!!email) return; + if (!data?.user.email) return; + setEmail(data.user.email); + }, [data, email]); + + const decrypt = useCallback(async () => { + if (loading) return; + + setLoading(true); + + try { + const { masterKey } = await generateMasterKeyAndHash({ email, password }); + + const decrypted = nip44.v2.decrypt( + protectedMnemonic, + hexToBytes(masterKey) + ); + + setMnemonic(decrypted); + } catch (error) { + toast({ + variant: 'destructive', + title: 'Error decrypting.', + description: + 'Check your password and email and make sure you are copying the complete encrypted mnemonic.', + }); + setMnemonic(''); + } + + setLoading(false); + }, [email, loading, password, protectedMnemonic, toast]); + + return ( +
+
+ + setEmail(value.target.value)} + placeholder="Type your email" + /> +
+
+ + setPassword(value.target.value)} + placeholder="Type your password" + /> +
+
+ + setProtectedMnemonic(value.target.value)} + placeholder="Type your encrypted mnemonic" + /> +
+ + {!!mnemonic ? ( +
+ + +
+ ) : null} +
+ ); +} diff --git a/src/components/button/VaultButton.tsx b/src/components/button/VaultButton.tsx index eb9edb4f..ecea448c 100644 --- a/src/components/button/VaultButton.tsx +++ b/src/components/button/VaultButton.tsx @@ -1,14 +1,20 @@ 'use client'; +import { ApolloError, useApolloClient } from '@apollo/client'; import { zodResolver } from '@hookform/resolvers/zod'; import { Loader2, Lock, Unlock } from 'lucide-react'; import { FC, useEffect, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; -import { useCheckPasswordMutation } from '@/graphql/mutations/__generated__/checkPassword.generated'; +import { + CheckPasswordDocument, + CheckPasswordMutation, + CheckPasswordMutationVariables, +} from '@/graphql/mutations/__generated__/checkPassword.generated'; import { useUserQuery } from '@/graphql/queries/__generated__/user.generated'; import { useKeyStore } from '@/stores/keys'; +import { toWithError } from '@/utils/async'; import { cn } from '@/utils/cn'; import { handleApolloError } from '@/utils/error'; import { WorkerMessage, WorkerResponse } from '@/workers/account/types'; @@ -42,6 +48,8 @@ const formSchema = z.object({ const UnlockDialogContent: FC<{ callback: () => void }> = ({ callback }) => { const workerRef = useRef(); + const client = useApolloClient(); + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -51,35 +59,24 @@ const UnlockDialogContent: FC<{ callback: () => void }> = ({ callback }) => { const { toast } = useToast(); - const setMasterKey = useKeyStore(s => s.setMasterKey); + const setKeys = useKeyStore(s => s.setKeys); const [loading, setLoading] = useState(true); - const [tempMasterKey, setTempMasterKey] = useState(''); - - const { data } = useUserQuery({ errorPolicy: 'ignore' }); - const [checkPassword] = useCheckPasswordMutation({ - onCompleted: () => { - setMasterKey(tempMasterKey); - callback(); - }, - onError: error => { - const messages = handleApolloError(error); - - toast({ - variant: 'destructive', - title: 'Unable to unlock.', - description: messages.join(', '), - }); - - setTempMasterKey(''); - form.reset(); - }, - }); + const { data, loading: userLoading, error } = useUserQuery(); const handleSubmit = (values: z.infer) => { setLoading(true); - if (loading || !data?.user.email || !workerRef.current) { + + if ( + loading || + userLoading || + error || + !data?.user.email || + !data?.user.protected_symmetric_key || + !workerRef.current + ) { + setLoading(false); return; } @@ -88,6 +85,7 @@ const UnlockDialogContent: FC<{ callback: () => void }> = ({ callback }) => { payload: { email: data.user.email, password: values.password, + protectedSymmetricKey: data.user.protected_symmetric_key, }, }; @@ -99,18 +97,41 @@ const UnlockDialogContent: FC<{ callback: () => void }> = ({ callback }) => { new URL('../../workers/account/account.ts', import.meta.url) ); - workerRef.current.onmessage = event => { + workerRef.current.onmessage = async event => { const message: WorkerResponse = event.data; switch (message.type) { - case 'generateMaster': - setTempMasterKey(message.payload.masterKey); - - checkPassword({ - variables: { password: message.payload.masterPasswordHash }, - }); + case 'generateMaster': { + const [, error] = await toWithError( + client.mutate< + CheckPasswordMutation, + CheckPasswordMutationVariables + >({ + mutation: CheckPasswordDocument, + variables: { password: message.payload.masterPasswordHash }, + }) + ); + + if (error) { + const messages = handleApolloError(error as ApolloError); + + toast({ + variant: 'destructive', + title: 'Unable to unlock.', + description: messages.join(', '), + }); + + form.reset(); + } else { + setKeys({ + masterKey: message.payload.masterKey, + protectedSymmetricKey: message.payload.protectedSymmetricKey, + }); + callback(); + } break; + } case 'loaded': setLoading(false); @@ -128,7 +149,7 @@ const UnlockDialogContent: FC<{ callback: () => void }> = ({ callback }) => { return () => { if (workerRef.current) workerRef.current.terminate(); }; - }, [checkPassword]); + }, [client, toast, callback, form, setKeys]); return ( <> @@ -189,7 +210,7 @@ export const VaultButton: FC<{ className?: string; size?: 'sm'; }> = ({ lockedTitle = 'Locked', className, size }) => { - const masterKey = useKeyStore(s => s.masterKey); + const keys = useKeyStore(s => s.keys); const clearKeys = useKeyStore(s => s.clear); @@ -203,7 +224,7 @@ export const VaultButton: FC<{ return ( - {!!masterKey ? ( + {!!keys ? (