Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: clear sensitive data in memory #704

Merged
merged 1 commit into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/adena-extension/src/common/utils/encoding-util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
export function bytesToBase64(bytes: number[]): string {
return Buffer.from(bytes).toString('base64');
}

export function base64ToBytes(base64: string): number[] {
return Array.from(Buffer.from(base64, 'base64'));
}

export function stringFromBase64(base64: string): string {
return Buffer.from(base64, 'base64').toString('utf-8');
}

export function stringToBase64(str: string): string {
return Buffer.from(str).toString('base64');
}
5 changes: 5 additions & 0 deletions packages/adena-extension/src/common/utils/rand-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import crypto from 'crypto';

export function generateRandomHex(): string {
return crypto.randomBytes(32).toString('hex');
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from 'react';
import { WebTextarea } from '@components/atoms/web-textarea';
import { useEffect, useState } from 'react';
import styled from 'styled-components';

import { stringFromBase64 } from '@common/utils/encoding-util';
import { generateRandomHex } from '@common/utils/rand-utils';
import { View } from '../../atoms';

interface WebPrivateKeyBoxProps {
Expand All @@ -11,7 +13,7 @@ interface WebPrivateKeyBoxProps {
error?: boolean;
}

const StyledContainer = styled(View) <{ showBlur: boolean }>`
const StyledContainer = styled(View)<{ showBlur: boolean }>`
position: relative;
overflow: hidden;
height: 80px;
Expand All @@ -27,17 +29,41 @@ const StyledBlurScreen = styled(View)`
border-radius: 10px;
`;

export const WebPrivateKeyBox = ({ privateKey, showBlur = true, readOnly = false, error = false }: WebPrivateKeyBoxProps): JSX.Element => {
export const WebPrivateKeyBox = ({
privateKey,
showBlur = true,
readOnly = false,
error = false,
}: WebPrivateKeyBoxProps): JSX.Element => {
const randomHexString = generateRandomHex();
const [displayPrivateKey, setDisplayPrivateKey] = useState<string>(randomHexString);

useEffect(() => {
if (!showBlur) {
setDisplayPrivateKey(stringFromBase64(privateKey));
return;
}

setDisplayPrivateKey(randomHexString);
}, [showBlur, privateKey]);

useEffect(() => {
return () => {
setDisplayPrivateKey('');
};
}, []);

return (
<StyledContainer showBlur={showBlur}>
<WebTextarea
value={privateKey}
value={displayPrivateKey}
placeholder='Private Key'
readOnly={readOnly}
error={error}
style={{ height: '100%' }}
onChange={(): void => { return; }}
onChange={(): void => {
return;
}}
spellCheck={false}
/>
{showBlur && <StyledBlurScreen />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect, useRef } from 'react';
import { stringFromBase64 } from '@common/utils/encoding-util';
import { useEffect, useMemo, useRef } from 'react';
import styled from 'styled-components';

interface WebSeedBoxProps {
seeds: string[];
seedString: string;
showBlur?: boolean;
}

Expand Down Expand Up @@ -31,12 +32,20 @@ const BOX_WIDTH = 185;
const BOX_HEIGHT = 40;
const NUMBER_BOX_WIDTH = 40;

export const WebSeedBox = ({ seeds, showBlur = true }: WebSeedBoxProps): JSX.Element => {
export const WebSeedBox = ({ seedString, showBlur = true }: WebSeedBoxProps): JSX.Element => {
const canvasRefs = useRef<(HTMLCanvasElement | null)[]>([]);

const seedSize = useMemo(() => {
return `${stringFromBase64(seedString)}`.split(' ').length;
}, [seedString]);

useEffect(() => {
if (seedSize === 0) return;

const dpr = window?.devicePixelRatio || 1;

const seeds = `${stringFromBase64(seedString)}`.split(' ');

seeds.forEach((seed, index) => {
const canvas = canvasRefs.current[index];
if (!canvas) return;
Expand Down Expand Up @@ -87,11 +96,11 @@ export const WebSeedBox = ({ seeds, showBlur = true }: WebSeedBoxProps): JSX.Ele

ctx.scale(dpr, dpr);
});
}, [seeds, showBlur, window?.devicePixelRatio]);
}, [seedSize, seedString, showBlur, window?.devicePixelRatio]);

return (
<CanvasContainer>
{seeds.map((_, index) => (
{Array.from({ length: seedSize }).map((_, index) => (
<CanvasWrapper key={index} blur={showBlur}>
<canvas
ref={(el): HTMLCanvasElement | null => (canvasRefs.current[index] = el)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@ import theme from '@styles/theme';
import { GlobalWebStyle } from '@styles/global-style';
import { WebSeedBox } from '.';

const seeds = ['seed', 'seed', 'seed', 'seed'];

describe('WebSeedBox Component', () => {
it('WebSeedBox render', () => {

render(
<RecoilRoot>
<GlobalWebStyle />
<ThemeProvider theme={theme}>
<WebSeedBox seeds={seeds} />
<WebSeedBox seedString='' />
</ThemeProvider>
</RecoilRoot>,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default {

export const Default: StoryObj<typeof WebSeedBox> = {
args: {
seeds: ['seed', 'seed', 'seed', 'seed'],
seedString: '',
showBlur: true,
},
};
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { PasswordValidationError } from '@common/errors';
import { useAdenaContext } from '@hooks/use-context';
import { evaluatePassword, EvaluatePasswordResult } from '@common/utils/password-utils';
import {
validateEmptyPassword,
validateNotMatchConfirmPassword,
validatePasswordComplexity,
} from '@common/validation';
import { AdenaWallet } from 'adena-module';
import useAppNavigate from '@hooks/use-app-navigate';
import { CreateAccountState, GoogleState, LedgerState, SeedState, RoutePath } from '@types';
import { evaluatePassword, EvaluatePasswordResult } from '@common/utils/password-utils';
import { useAdenaContext } from '@hooks/use-context';
import { CreateAccountState, GoogleState, LedgerState, RoutePath, SeedState } from '@types';
import { AdenaWallet } from 'adena-module';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

export type UseCreatePasswordReturn = {
pwdState: {
Expand Down Expand Up @@ -169,6 +169,7 @@ export const useCreatePassword = (): UseCreatePasswordReturn => {
});
await accountService.changeCurrentAccount(wallet.currentAccount);
await walletService.changePassword(pwd);
clearPassword();
} catch (error) {
console.error(error);
return 'FAIL';
Expand All @@ -183,6 +184,7 @@ export const useCreatePassword = (): UseCreatePasswordReturn => {
const wallet = await AdenaWallet.createByWeb3Auth(googleState.privateKey);
await accountService.changeCurrentAccount(wallet.currentAccount);
await walletService.saveWallet(wallet, pwd);
clearPassword();
} catch (error) {
console.error(error);
return 'FAIL';
Expand Down Expand Up @@ -213,6 +215,10 @@ export const useCreatePassword = (): UseCreatePasswordReturn => {
setActivated(true);
};

const clearPassword = (): void => {
setInputs({ pwd: '', confirmPwd: '' });
};

return {
pwdState: {
value: pwd,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AdenaWallet } from 'adena-module';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { RoutePath } from '@types';
import { PasswordValidationError } from '@common/errors';
import { evaluatePassword, EvaluatePasswordResult } from '@common/utils/password-utils';
import {
validateEmptyPassword,
validateNotMatchConfirmPassword,
Expand All @@ -13,7 +13,7 @@ import { useAdenaContext } from '@hooks/use-context';
import useIndicatorStep, {
UseIndicatorStepReturn,
} from '@hooks/wallet/broadcast-transaction/use-indicator-step';
import { evaluatePassword, EvaluatePasswordResult } from '@common/utils/password-utils';
import { RoutePath } from '@types';

export type UseCreatePasswordScreenReturn = {
passwordState: {
Expand Down Expand Up @@ -41,6 +41,7 @@ export type UseCreatePasswordScreenReturn = {
};
indicatorInfo: UseIndicatorStepReturn;
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
clearPassword: () => void;
};

export const useCreatePasswordScreen = (): UseCreatePasswordScreenReturn => {
Expand Down Expand Up @@ -126,11 +127,16 @@ export const useCreatePasswordScreen = (): UseCreatePasswordScreenReturn => {
return false;
};

const clearPassword = (): void => {
setInputs({ password: '', confirmPassword: '' });
};

const _saveWalletByPassword = async (password: string): Promise<void> => {
const { serializedWallet } = params;
const wallet = await AdenaWallet.deserialize(serializedWallet, '');
await walletService.saveWallet(wallet, password);
await accountService.changeCurrentAccount(wallet.currentAccount);
await setInputs({ password: '', confirmPassword: '' });
};

const onChangePassword = useCallback(
Expand All @@ -157,14 +163,15 @@ export const useCreatePasswordScreen = (): UseCreatePasswordScreenReturn => {
};

const onKeyDownInput = useCallback(
() => (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
if (disabledCreateButton) {
return;
() =>
(e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
if (disabledCreateButton) {
return;
}
onClickCreateButton();
}
onClickCreateButton();
}
},
},
[disabledCreateButton, onClickCreateButton],
);

Expand Down Expand Up @@ -207,5 +214,6 @@ export const useCreatePasswordScreen = (): UseCreatePasswordScreenReturn => {
disabled: disabledCreateButton,
},
onKeyDown: onKeyDownInput,
clearPassword,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ const useAccountImportScreen = ({ wallet }: { wallet: Wallet }): UseAccountImpor
const { account, keyring } = result;
const resultWallet = await addAccountWith(wallet.clone(), keyring, account);
await updateWallet(resultWallet);
setInputValue('');
}).then(() => navigate(RoutePath.WebAccountAddedComplete));
return;
} else {
Expand Down Expand Up @@ -224,6 +225,7 @@ const useAccountImportScreen = ({ wallet }: { wallet: Wallet }): UseAccountImpor
}

await updateWallet(resultWallet);
setInputValue('');
navigate(RoutePath.WebAccountAddedComplete);
}
}, [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AdenaWallet, HDWalletKeyring, SeedAccount } from 'adena-module';
import { useCallback, useMemo, useState } from 'react';

import { stringFromBase64, stringToBase64 } from '@common/utils/encoding-util';
import useAppNavigate from '@hooks/use-app-navigate';
import { useWalletContext } from '@hooks/use-context';
import { useCurrentAccount } from '@hooks/use-current-account';
Expand Down Expand Up @@ -42,7 +43,9 @@ const useWalletCreateScreen = (): UseWalletCreateReturn => {
hasQuestionnaire: true,
});

const seeds = useMemo(() => AdenaWallet.generateMnemonic(), []);
const seeds = useMemo(() => {
return stringToBase64(AdenaWallet.generateMnemonic());
}, []);

const onClickGoBack = useCallback(() => {
if (step === 'INIT') {
Expand All @@ -65,7 +68,10 @@ const useWalletCreateScreen = (): UseWalletCreateReturn => {
}
} else if (step === 'GET_SEED_PHRASE') {
if (wallet) {
const keyring = await HDWalletKeyring.fromMnemonic(seeds);
let rawSeeds = stringFromBase64(seeds);
const keyring = await HDWalletKeyring.fromMnemonic(rawSeeds);
rawSeeds = '';

const account = await SeedAccount.createBy(
keyring,
`Account ${wallet.lastAccountIndex + 1}`,
Expand All @@ -85,7 +91,10 @@ const useWalletCreateScreen = (): UseWalletCreateReturn => {
await updateWallet(clone);
navigate(RoutePath.WebAccountAddedComplete);
} else {
const createdWallet = await AdenaWallet.createByMnemonic(seeds);
let rawSeeds = stringFromBase64(seeds);
const createdWallet = await AdenaWallet.createByMnemonic(rawSeeds);
rawSeeds = '';

const serializedWallet = await createdWallet.serialize('');

navigate(RoutePath.WebCreatePassword, {
Expand Down
Loading