Skip to content

Commit

Permalink
Merge pull request #70 from spacemeshos/feat-use-argon2
Browse files Browse the repository at this point in the history
Use AES-CTR + ARGON2 + HMAC instead of AES-GCM
  • Loading branch information
brusherru authored Aug 26, 2024
2 parents 97ffc98 + 3fb5e8b commit 4f4f16f
Show file tree
Hide file tree
Showing 16 changed files with 446 additions and 187 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@
"@uidotdev/usehooks": "^2.4.1",
"@zondax/ledger-js": "^0.10.0",
"@zondax/ledger-spacemesh": "^0.2.2",
"aes-js": "^3.1.2",
"bech32": "^2.0.0",
"crypto-js": "^4.2.0",
"detect-browser": "^5.3.0",
"eventemitter3": "^5.0.1",
"framer-motion": "^8.5.2",
"hash-wasm": "^4.11.0",
"install": "^0.13.0",
"js-file-download": "^0.4.12",
"npm": "^10.8.1",
Expand All @@ -66,6 +69,8 @@
"@commitlint/config-conventional": "^17.4.2",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^13.4.0",
"@types/aes-js": "^3.1.4",
"@types/crypto-js": "^4.2.2",
"@types/jest": "^29.2.6",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
Expand Down
51 changes: 51 additions & 0 deletions src/__tests__/aes-ctr-argon2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { decrypt, encrypt } from '../utils/aes-ctr-argon2';

// Polyfill for browser's crypto
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto');

Object.defineProperty(globalThis, 'crypto', {
value: {
getRandomValues: (arr: Uint8Array) => crypto.randomBytes(arr.length),
},
});

describe('AES-CTR-Argon2', () => {
const ENCRYPTED_FIXTURE = {
kdf: 'ARGON2',
kdfparams: {
salt: '4ae49fea810d755484e6d5fbddc8b6df',
iterations: 64,
memorySize: 65536,
parallelism: 1,
},
cipher: 'AES-256-CTR',
cipherParams: { iv: '811e60e5e564774b16806a0bdf671e2b' },
cipherText: 'd0e97cd11c2bb1b1f7102b',
mac: 'd0fcd7eaf6ee6404c02be79f97fc5898fe1cf7fd167d95a20b01c5a950c62778',
} as const;

it('should encrypt', async () => {
const encryptedMsg = await encrypt('hello world', 'pass@123');
expect(encryptedMsg).toHaveProperty('kdf');
expect(encryptedMsg).toHaveProperty('kdfparams');
expect(encryptedMsg).toHaveProperty('cipher');
expect(encryptedMsg).toHaveProperty('cipherParams');
expect(encryptedMsg).toHaveProperty('cipherText');
expect(encryptedMsg).toHaveProperty('mac');
});
it('should decrypt', async () => {
const decrypted = await decrypt(ENCRYPTED_FIXTURE, 'pass@123');
expect(decrypted).toBe('hello world');
});
it('should encrypt and decrypt', async () => {
const encryptedMsg = await encrypt('hello world', 'pass@123');
const decryptedPlainText = await decrypt(encryptedMsg, 'pass@123');
expect(decryptedPlainText).toBe('hello world');
});
it('should fail on wrong password', async () => {
await expect(() =>
decrypt(ENCRYPTED_FIXTURE, 'wrong!Pass')
).rejects.toThrow();
});
});
13 changes: 2 additions & 11 deletions src/components/CreateKeyPairModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
ModalOverlay,
Text,
} from '@chakra-ui/react';
import { StdPublicKeys } from '@spacemesh/sm-codec';

import usePassword from '../store/usePassword';
import useWallet from '../store/useWallet';
Expand All @@ -36,7 +35,7 @@ function CreateKeyPairModal({
isOpen,
onClose,
}: CreateKeyPairModalProps): JSX.Element {
const { createKeyPair, createAccount, wallet } = useWallet();
const { createKeyPair, wallet } = useWallet();
const { withPassword } = usePassword();
const {
register,
Expand All @@ -55,15 +54,7 @@ function CreateKeyPairModal({
async ({ displayName, path, createSingleSig }) => {
const success = await withPassword(
async (password) => {
const key = await createKeyPair(displayName, path, password);
if (createSingleSig) {
await createAccount(
displayName,
StdPublicKeys.SingleSig,
{ PublicKey: key.publicKey },
password
);
}
await createKeyPair(displayName, path, password, createSingleSig);
return true;
},
'Create a new Key Pair',
Expand Down
16 changes: 4 additions & 12 deletions src/components/ImportKeyFromLedgerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
ModalOverlay,
Text,
} from '@chakra-ui/react';
import { StdPublicKeys } from '@spacemesh/sm-codec';

import useHardwareWallet from '../store/useHardwareWallet';
import usePassword from '../store/usePassword';
Expand All @@ -36,7 +35,7 @@ function ImportKeyFromLedgerModal({
isOpen,
onClose,
}: ImportKeyFromLedgerModalProps): JSX.Element {
const { addForeignKey, createAccount } = useWallet();
const { addForeignKey } = useWallet();
const { withPassword } = usePassword();
const { checkDeviceConnection, connectedDevice, modalConnect } =
useHardwareWallet();
Expand Down Expand Up @@ -67,18 +66,11 @@ function ImportKeyFromLedgerModal({
const publicKey = await connectedDevice.actions.getPubKey(path);
const success = await withPassword(
async (password) => {
const key = await addForeignKey(
await addForeignKey(
{ displayName, path, publicKey },
password
password,
createSingleSig
);
if (createSingleSig) {
await createAccount(
displayName,
StdPublicKeys.SingleSig,
{ PublicKey: key.publicKey },
password
);
}
return true;
},
'Import PublicKey from Ledger device',
Expand Down
18 changes: 7 additions & 11 deletions src/components/ImportKeyPairModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
ModalOverlay,
Text,
} from '@chakra-ui/react';
import { StdPublicKeys } from '@spacemesh/sm-codec';

import usePassword from '../store/usePassword';
import useWallet from '../store/useWallet';
Expand All @@ -38,7 +37,7 @@ function ImportKeyPairModal({
onClose,
keys,
}: ImportKeyPairModalProps): JSX.Element {
const { importKeyPair, createAccount } = useWallet();
const { importKeyPair } = useWallet();
const { withPassword } = usePassword();
const {
register,
Expand All @@ -57,15 +56,12 @@ function ImportKeyPairModal({
async ({ displayName, secretKey, createSingleSig }) => {
const success = await withPassword(
async (password) => {
const key = await importKeyPair(displayName, secretKey, password);
if (createSingleSig) {
await createAccount(
displayName,
StdPublicKeys.SingleSig,
{ PublicKey: key.publicKey },
password
);
}
await importKeyPair(
displayName,
secretKey,
password,
createSingleSig
);
return true;
},
'Importing the Key Pair',
Expand Down
14 changes: 11 additions & 3 deletions src/components/PasswordAlert.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef } from 'react';
import { useRef, useState } from 'react';
import { Form } from 'react-hook-form';

import {
Expand All @@ -21,12 +21,19 @@ import usePassword from '../store/usePassword';
import PasswordInput from './PasswordInput';

function PasswordAlert(): JSX.Element {
const [isLoading, setIsLoading] = useState(false);
const { form } = usePassword();
const cancelRef = useRef<HTMLButtonElement>(null);
if (!form.register.password || !form.register.remember) {
throw new Error('PasswordAlert: password or remember is not registered');
}

const onSubmit = async () => {
setIsLoading(true);
await form.onSubmit();
setIsLoading(false);
};

return (
<AlertDialog
leastDestructiveRef={cancelRef}
Expand Down Expand Up @@ -75,10 +82,11 @@ function PasswordAlert(): JSX.Element {
<Button
type="submit"
colorScheme="purple"
onClick={form.onSubmit}
onClick={onSubmit}
ml={3}
disabled={isLoading}
>
{form.actionLabel}
{isLoading ? 'Checking password...' : form.actionLabel}
</Button>
</AlertDialogFooter>
</Form>
Expand Down
14 changes: 12 additions & 2 deletions src/screens/UnlockScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useState } from 'react';
import { Form, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';

Expand Down Expand Up @@ -32,15 +33,18 @@ function UnlockScreen(): JSX.Element {
reset,
formState: { errors },
} = useForm<FormValues>();
const [isLoading, setIsLoading] = useState(false);

const submit = handleSubmit(async (data) => {
setIsLoading(true);
const success = await unlockWallet(data.password);
if (!success) {
setError('password', { type: 'value', message: 'Invalid password' });
return;
}
setValue('password', '');
reset();
setIsLoading(false);
navigate('/wallet');
});

Expand All @@ -64,8 +68,14 @@ function UnlockScreen(): JSX.Element {
<PasswordInput register={register('password')} />
<FormErrorMessage>{errors.password?.message}</FormErrorMessage>
</FormControl>
<Button type="submit" mt={4} onClick={() => submit()} size="lg">
Unlock
<Button
type="submit"
mt={4}
onClick={() => submit()}
size="lg"
disabled={isLoading}
>
{isLoading ? 'Unlocking...' : 'Unlock'}
</Button>
</Form>
</Flex>
Expand Down
14 changes: 12 additions & 2 deletions src/screens/welcome/ImportScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import BackButton from '../../components/BackButton';
import PasswordInput from '../../components/PasswordInput';
import useWallet from '../../store/useWallet';
import { WalletFile } from '../../types/wallet';
import { postpone } from '../../utils/promises';

type FormValues = {
password: string;
Expand All @@ -38,6 +39,7 @@ function ImportScreen(): JSX.Element {
} = useForm<FormValues>();
const { openWallet } = useWallet();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);

const readFile = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
Expand Down Expand Up @@ -75,11 +77,18 @@ function ImportScreen(): JSX.Element {
setError('root', { type: 'manual', message: 'No wallet file loaded' });
return;
}
const success = await openWallet(walletFileContent, password);
setIsLoading(true);
const success = await postpone(
// We need to postpone it for one tick
// to allow component to re-render
() => openWallet(walletFileContent, password),
1
);
if (!success) {
setError('password', { type: 'value', message: 'Invalid password' });
return;
}
setIsLoading(false);
navigate('/wallet');
});

Expand Down Expand Up @@ -135,8 +144,9 @@ function ImportScreen(): JSX.Element {
mt={4}
width="100%"
onClick={onSubmit}
disabled={isLoading}
>
Import wallet
{isLoading ? 'Importing...' : 'Import wallet'}
</Button>
</Form>
</CardBody>
Expand Down
25 changes: 15 additions & 10 deletions src/store/usePassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useDisclosure } from '@chakra-ui/react';

import { MINUTE } from '../utils/constants';
import { noop } from '../utils/func';
import { postpone } from '../utils/promises';

const REMEMBER_PASSWORD_TIME = 5 * MINUTE;

Expand Down Expand Up @@ -123,16 +124,20 @@ const usePassword = (): UsePasswordReturnType => {
);
return;
}
try {
const res = await passwordCallback(password);
setPassword(password, remember);
_onClose();
setValue('password', '');
reset();
eventEmitter.emit('success', res);
} catch (err) {
setError('password', { message: 'Incorrect password' });
}
await postpone(async () => {
// We need to postpone it for one tick to allow
// the form to re-render before start checking the password
try {
const res = await passwordCallback(password);
setPassword(password, remember);
_onClose();
setValue('password', '');
reset();
eventEmitter.emit('success', res);
} catch (err) {
setError('password', { message: 'Incorrect password' });
}
}, 1);
});

const onClose = () => {
Expand Down
Loading

0 comments on commit 4f4f16f

Please sign in to comment.