Skip to content

Commit 4368354

Browse files
authored
fix(extension): [LW-11298] [LW-11259] [LW-11396] [LW-11260] [LW-11258] [LW-11397] Misc paper wallet fixes (#1396)
* fix(extension): lw-11298 * fix(extension): lw-11259 * fix(extension): lw-11396 * fix(extension): lw-11260 * fix(extension): lw-11258 * fix(extension): lw-11397 * fix(extension): lw-11397 * feat(extension): add toast to copy to clipboard * fix(extension): allow pgp private key entry more than once * fix(extension): add missing translated strings * style(extension): remove unused style * fix: always cast PGP message as Uint8Array
1 parent 0e4aeaf commit 4368354

File tree

10 files changed

+94
-40
lines changed

10 files changed

+94
-40
lines changed
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { Message } from 'openpgp';
2-
31
export interface PublicPgpKeyData {
42
pgpPublicKey: string;
53
pgpKeyReference?: string;
@@ -8,6 +6,6 @@ export interface PublicPgpKeyData {
86
export interface ShieldedPgpKeyData {
97
pgpPrivateKey: string;
108
pgpKeyPassphrase?: string;
11-
shieldedMessage: Message<Uint8Array>;
9+
shieldedMessage: Uint8Array;
1210
privateKeyIsDecrypted: boolean;
1311
}

apps/browser-extension-wallet/src/utils/pgp.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { createMessage, decrypt, encrypt, readKey, readMessage, readPrivateKey, decryptKey } from 'openpgp';
33
import type { Key, MaybeArray, Message, PartialConfig, PrivateKey, PublicKey } from 'openpgp';
44
import { i18n } from '@lace/translation';
5-
import { PublicPgpKeyData } from '@src/types';
5+
import type { PublicPgpKeyData } from '@src/types';
66

77
export const WEAK_KEY_REGEX = new RegExp(/RSA keys shorter than 2047 bits are considered too weak./);
88
export const NO_ENCRYPTION_PACKET_REGEX = new RegExp(/Could not find valid encryption key packet in key/);
@@ -156,7 +156,7 @@ export const decryptMessageWithPgp = async ({
156156
decryptionKeys: privateKey
157157
});
158158

159-
if (checkSignatures) {
159+
if (checkSignatures && signatures?.length > 0) {
160160
signatures.map((signature) => {
161161
if (!signature.verified) throw new Error(`signature ${signature.keyID} not verified`);
162162
});
@@ -191,13 +191,13 @@ export const pgpPublicKeyVerification =
191191
.join(' ')
192192
});
193193
} catch (error) {
194-
if (
194+
if (WEAK_KEY_REGEX.test(error.message)) {
195+
setPgpValidation({ error: i18n.t('pgp.error.rsaKeyTooWeak') });
196+
} else if (
195197
error.message === 'no valid encryption key packet in key.' ||
196198
NO_ENCRYPTION_PACKET_REGEX.test(error.message)
197199
) {
198200
setPgpValidation({ error: i18n.t('pgp.error.noValidEncryptionKeyPacket') });
199-
} else if (WEAK_KEY_REGEX.test(error.message)) {
200-
setPgpValidation({ error: error.message });
201201
} else if (error.message === 'PGP key is not public') {
202202
setPgpValidation({ error: i18n.t('pgp.error.privateKeySuppliedInsteadOfPublic') });
203203
} else {

apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/create-wallet/steps/Setup.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ export const Setup = (): JSX.Element => {
3131
const onNext = async ({ walletName }: WalletSetupNamePasswordSubmitParams) => {
3232
if (recoveryMethod === 'mnemonic') {
3333
void analytics.sendEventToPostHog(postHogActions.create.ENTER_WALLET);
34+
} else {
35+
void analytics.sendEventToPostHog(postHogActions.create.WALLET_SETUP_GENERATE_PAPER_WALLET_CLICK);
3436
}
35-
void analytics.sendEventToPostHog(postHogActions.create.WALLET_SETUP_GENERATE_PAPER_WALLET_CLICK);
3637
await next({ name: walletName });
3738
};
3839

apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/steps/EnterPgpPrivateKey.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { Wallet } from '@lace/cardano';
33
import { WalletSetupStepLayoutRevamp, WalletTimelineSteps } from '@lace/core';
44
import { i18n } from '@lace/translation';
5-
import { decryptMessageWithPgp, decryptPgpPrivateKey, readPgpPrivateKey } from '@src/utils/pgp';
5+
import { decryptMessageWithPgp, decryptPgpPrivateKey, readPgpPrivateKey, readBinaryPgpMessage } from '@src/utils/pgp';
66
import React, { useState, VFC, useEffect, ChangeEvent, useCallback } from 'react';
77
import { useRestoreWallet } from '../context';
88
import styles from './EnterPgpPrivateKey.module.scss';
@@ -40,9 +40,8 @@ const decryptQrCodeMnemonicWithPrivateKey = async ({ pgpInfo }: DecryptProps): P
4040
passphrase: pgpInfo.pgpKeyPassphrase
4141
})
4242
: await readPgpPrivateKey({ privateKey: pgpInfo.pgpPrivateKey });
43-
4443
const decryptedMessage = await decryptMessageWithPgp({
45-
message: pgpInfo.shieldedMessage,
44+
message: await readBinaryPgpMessage(new Uint8Array(pgpInfo.shieldedMessage)),
4645
privateKey
4746
});
4847
if (
@@ -83,8 +82,6 @@ export const EnterPgpPrivateKey: VFC = () => {
8382
if (error.message === 'Armored text not of type private key') {
8483
setValidation({ error: i18n.t('paperWallet.enterPgpPrivateKey.keyNotPrivate') });
8584
}
86-
} finally {
87-
navigator.clipboard.writeText('');
8885
}
8986
},
9087
[setValidation, pgpInfo, setPgpInfo]
@@ -162,16 +159,22 @@ export const EnterPgpPrivateKey: VFC = () => {
162159
};
163160
if (
164161
((pgpInfo.pgpPrivateKey && pgpInfo.privateKeyIsDecrypted) ||
165-
(pgpInfo.pgpPrivateKey && pgpInfo.pgpKeyPassphrase && !pgpInfo.privateKeyIsDecrypted)) && // Only try to decrypt if we don't already have a wallet
162+
(pgpInfo.pgpPrivateKey && pgpInfo.pgpKeyPassphrase && !pgpInfo.privateKeyIsDecrypted)) &&
166163
!createWalletData.mnemonic.every((w) => !!w)
167-
)
164+
) {
165+
// Only try to decrypt if we don't already have a wallet
168166
getMnemonic();
167+
}
169168
}, [pgpInfo, createWalletData.mnemonic, setMnemonic, setValidation]);
170169

171170
useEffect(() => {
172171
setValidation({ error: null });
173172
}, [entryType]);
174173

174+
window.addEventListener('paste', () => {
175+
navigator.clipboard.writeText('');
176+
});
177+
175178
return (
176179
<>
177180
<WalletSetupStepLayoutRevamp
@@ -253,7 +256,6 @@ export const EnterPgpPrivateKey: VFC = () => {
253256
onChange={async (e) => {
254257
setValidation({ error: null, success: null });
255258
setPgpInfo({ ...pgpInfo, pgpKeyPassphrase: e.target.value });
256-
navigator.clipboard.writeText('');
257259
}}
258260
label={i18n.t('core.paperWallet.privatePgpKeyPassphraseLabel')}
259261
value={pgpInfo.pgpKeyPassphrase || ''}

apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/steps/ScanShieldedMessage.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import {
1616
CameraComponent as CameraIcon,
1717
WarningIconCircleComponent as WarningIcon
1818
} from '@input-output-hk/lace-ui-toolkit';
19-
import * as openpgp from 'openpgp';
20-
import { readBinaryPgpMessage } from '@src/utils/pgp';
2119
import { i18n } from '@lace/translation';
2220
import jsQR, { QRCode } from 'jsqr';
2321
import { Trans } from 'react-i18next';
@@ -41,7 +39,7 @@ const stripDetailFromVideoDeviceName = (str: string) => str.replace(/\s(camera\s
4139

4240
type QrCodeScanState = 'waiting' | 'blocked' | 'scanning' | 'validating' | 'scanned';
4341
interface ByteChunk<T> {
44-
bytes: number[];
42+
bytes: Uint8Array;
4543
text?: T;
4644
type: 'byte';
4745
}
@@ -60,8 +58,13 @@ export const ScanShieldedMessage: VFC = () => {
6058
const streamRef = useRef<MediaStream>(null);
6159
const [deviceId, setDeviceId] = useState<MediaDeviceInfo['deviceId'] | null>();
6260

63-
const handleDeviceChange = async (value: MediaDeviceInfo['deviceId']) => {
61+
const endVideoTracks = () => {
6462
streamRef.current.getVideoTracks().forEach((t) => t.stop());
63+
streamRef.current = null;
64+
};
65+
66+
const handleDeviceChange = async (value: MediaDeviceInfo['deviceId']) => {
67+
endVideoTracks();
6568
setDeviceId(value);
6669
};
6770

@@ -115,12 +118,11 @@ export const ScanShieldedMessage: VFC = () => {
115118
postHogActions.restore.SCAN_QR_CODE_CAMERA_ERROR
116119
]);
117120

118-
const onScanSuccess = async (message: openpgp.Message<Uint8Array>, address: string, chain: ChainName) => {
121+
const onScanSuccess = async (message: Uint8Array, address: string, chain: ChainName) => {
119122
setPgpInfo({ ...pgpInfo, shieldedMessage: message });
120123
setWalletMetadata({ chain, address });
121124
setValidation({ error: null });
122-
streamRef.current.getVideoTracks().forEach((t) => t.stop());
123-
streamRef.current = null;
125+
endVideoTracks();
124126
await analytics.sendEventToPostHog(postHogActions.restore.SCAN_QR_CODE_READ_SUCCESS);
125127
};
126128

@@ -145,16 +147,17 @@ export const ScanShieldedMessage: VFC = () => {
145147
async (code: QRCode) => {
146148
const [shieldedMessage, address, chain] = code.chunks as ScannedCode;
147149

150+
const shieldedQrCodeDataFormat = new RegExp(/^addr.*(Preprod|Preview|Mainnet)$/);
151+
const isCodeDataCorrectFormatForPaperWallet = shieldedQrCodeDataFormat.test(code.data);
148152
// User may have scanned the wallet address QR code
149153
if (Wallet.Cardano.Address.fromString(code.data)) {
150154
setValidation({
151155
error: { title: 'Wrong QR code identified', description: 'Scan paper wallet private QR code' }
152156
});
153157
void analytics.sendEventToPostHog(postHogActions.restore.SCAN_QR_CODE_READ_ERROR);
154-
} else if (!!shieldedMessage?.bytes || !!address?.text || !!chain?.text) {
158+
} else if (isCodeDataCorrectFormatForPaperWallet) {
155159
if (!shieldedMessage?.bytes || !address?.text || !chain?.text) return; // wait for code to be scanned in it's entirety
156-
const shieldedPgpMessage = await readBinaryPgpMessage(new Uint8Array(shieldedMessage.bytes));
157-
await onScanSuccess(shieldedPgpMessage, address.text, chain.text);
160+
await onScanSuccess(shieldedMessage.bytes, address.text, chain.text);
158161
next();
159162
// Immediately move to next step
160163
} else {
@@ -276,7 +279,14 @@ export const ScanShieldedMessage: VFC = () => {
276279
)}
277280
</Flex>
278281
<Flex justifyContent="space-between" h={'$48'} alignItems="center">
279-
<Button.Secondary onClick={back} label="Back" title="Back" />
282+
<Button.Secondary
283+
onClick={() => {
284+
endVideoTracks();
285+
back();
286+
}}
287+
label={i18n.t('core.walletSetupStep.back')}
288+
title={i18n.t('core.walletSetupStep.back')}
289+
/>
280290
{scanState === 'scanning' && !validation.error && (
281291
<Flex alignItems="center" gap="$8" h="$48">
282292
<Loader className={styles.loader} />

apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/steps/WalletOverview.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
import { Wallet } from '@lace/cardano';
1919
import { compactNumberWithUnit } from '@src/utils/format-number';
2020
import { PortfolioBalance } from '@src/views/browser-view/components';
21-
import { addEllipsis, WarningBanner } from '@lace/common';
21+
import { addEllipsis, WarningBanner, toast } from '@lace/common';
2222
import { getProviderByChain } from '@src/stores/slices';
2323
import { CARDANO_COIN_SYMBOL, COINGECKO_URL } from '@src/utils/constants';
2424
import BigNumber from 'bignumber.js';
@@ -27,6 +27,8 @@ import { useAnalyticsContext } from '@providers';
2727
import { useWalletOnboarding } from '../../walletOnboardingContext';
2828
import { useFetchCoinPrice } from '@hooks';
2929

30+
const TOAST_DEFAULT_DURATION = 3;
31+
3032
const handleOpenCoingeckoLink = () => {
3133
window.open(COINGECKO_URL, '_blank', 'noopener,noreferrer');
3234
};
@@ -178,13 +180,16 @@ export const WalletOverview = (): JSX.Element => {
178180
</Tooltip>
179181
<ControlButton.Icon
180182
icon={<Copy height={24} width={24} />}
181-
onClick={() => navigator.clipboard.writeText(walletMetadata.address)}
183+
onClick={() => {
184+
navigator.clipboard.writeText(walletMetadata.address);
185+
toast.notify({ duration: TOAST_DEFAULT_DURATION, text: i18n.t('general.clipboard.copiedToClipboard') });
186+
}}
182187
size="small"
183188
/>
184189
</Flex>
185190
</Flex>
186191
<Flex flexDirection="column">
187-
{walletBalances.fetched ? (
192+
{walletBalances.fetched && (
188193
<PortfolioBalance
189194
textSize="medium"
190195
loading={isLoading}
@@ -199,7 +204,8 @@ export const WalletOverview = (): JSX.Element => {
199204
isPercentage: false
200205
}}
201206
/>
202-
) : (
207+
)}
208+
{!walletBalances.fetched && !isLoading && (
203209
<WarningBanner message={i18n.t('general.warnings.cannotFetchPrice')} />
204210
)}
205211
</Flex>
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
.fullHeight {
2-
height: 100%;
2+
height: 100% !important;
3+
4+
:global(.ant-input) {
5+
height: 100% !important;
6+
}
37
}

apps/browser-extension-wallet/src/views/browser-view/features/settings/components/PaperWalletSettingsDrawer.tsx

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import styles from './SettingsLayout.module.scss';
1818
import { useAnalyticsContext } from '@providers';
1919
import { PassphraseStage, SaveStage, SecureStage } from './PaperWallet';
2020
import { useSecrets } from '@lace/core';
21+
22+
const INCORRECT_STAGE_ERROR = 'incorrect stage supplied';
2123
interface Props {
2224
isOpen: boolean;
2325
onClose: () => void;
@@ -123,7 +125,7 @@ export const PaperWalletSettingsDrawer = ({ isOpen, onClose, popupView = false }
123125
case 'save':
124126
return <SaveStage walletName={formattedWalletName} />;
125127
default:
126-
throw new Error('incorrect stage supplied');
128+
throw new Error(INCORRECT_STAGE_ERROR);
127129
}
128130
}, [stage, isPasswordValid, setPgpInfo, pgpInfo, setPassword, formattedWalletName]);
129131

@@ -163,7 +165,6 @@ export const PaperWalletSettingsDrawer = ({ isOpen, onClose, popupView = false }
163165
aria-disabled={pdfInstance.loading || !!pdfInstance.error}
164166
onClick={() => {
165167
analytics.sendEventToPostHog(PostHogAction.SettingsPaperWalletDownloadClick);
166-
handleClose();
167168
}}
168169
>
169170
<Button.Primary
@@ -178,7 +179,6 @@ export const PaperWalletSettingsDrawer = ({ isOpen, onClose, popupView = false }
178179
analytics.sendEventToPostHog(PostHogAction.SettingsPaperWalletPrintClick);
179180
const printWindow = window.open(URL.createObjectURL(pdfInstance.blob));
180181
printWindow.print();
181-
handleClose();
182182
}}
183183
w="$fill"
184184
disabled={pdfInstance.loading || !!pdfInstance.error}
@@ -189,9 +189,40 @@ export const PaperWalletSettingsDrawer = ({ isOpen, onClose, popupView = false }
189189
);
190190
}
191191
default:
192-
throw new Error('incorrect stage supplied');
192+
throw new Error(INCORRECT_STAGE_ERROR);
193+
}
194+
}, [stage, pgpInfo, setStage, handleVerifyPass, password, pdfInstance, formattedWalletName, analytics]);
195+
196+
const drawerHeader = useMemo(() => {
197+
switch (stage) {
198+
case 'secure':
199+
return (
200+
<DrawerHeader
201+
popupView={popupView}
202+
title={i18n.t('paperWallet.securePaperWallet.title')}
203+
subtitle={i18n.t('paperWallet.securePaperWallet.description')}
204+
/>
205+
);
206+
case 'passphrase':
207+
return (
208+
<DrawerHeader
209+
popupView={popupView}
210+
title={i18n.t('paperWallet.SettingsDrawer.passphraseStage.title')}
211+
subtitle={i18n.t('paperWallet.SettingsDrawer.passphraseStage.subtitle')}
212+
/>
213+
);
214+
case 'save':
215+
return (
216+
<DrawerHeader
217+
popupView={popupView}
218+
title={i18n.t('paperWallet.savePaperWallet.title')}
219+
subtitle={i18n.t('paperWallet.savePaperWallet.description')}
220+
/>
221+
);
222+
default:
223+
throw new Error(INCORRECT_STAGE_ERROR);
193224
}
194-
}, [stage, pgpInfo, setStage, handleVerifyPass, password, pdfInstance, formattedWalletName, handleClose, analytics]);
225+
}, [stage, popupView]);
195226

196227
return (
197228
<>
@@ -200,7 +231,7 @@ export const PaperWalletSettingsDrawer = ({ isOpen, onClose, popupView = false }
200231
dataTestId="paper-wallet-settings-drawer"
201232
onClose={handleClose}
202233
popupView={popupView}
203-
title={<DrawerHeader popupView={popupView} title={i18n.t('paperWallet.securePaperWallet.title')} />}
234+
title={drawerHeader}
204235
navigation={
205236
<DrawerNavigation
206237
title={i18n.t('browserView.settings.heading')}

packages/common/src/ui/components/Form/TextArea/TextArea.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const TextArea = ({
2626
className,
2727
dataTestId,
2828
invalid,
29-
isResizable,
29+
isResizable = false,
3030
value,
3131
onChange,
3232
label,
@@ -82,6 +82,7 @@ export const TextArea = ({
8282
[styles.invalid]: invalid
8383
})}
8484
{...props}
85+
spellCheck={false}
8586
/>
8687
</div>
8788
);

packages/translation/src/lib/translations/browser-extension-wallet/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,7 @@
842842
"pgp.error.misformedArmoredText": "Malformed PGP key block.",
843843
"pgp.error.privateKeySuppliedInsteadOfPublic": "Please use your public PGP key.",
844844
"pgp.error.noValidEncryptionKeyPacket": "Could not find valid encryption key packet in key.",
845+
"pgp.error.rsaKeyTooWeak": "RSA keys shorter than 2047 bits are considered too weak.",
845846
"paperWallet.enterPgpPrivateKey.title": "Enter your private PGP key",
846847
"paperWallet.enterPgpPrivateKey.description": "To decrypt your wallet's recovery details, provide the private key for the PGP keys used during paper wallet creation.",
847848
"paperWallet.enterPgpPrivateKey.toggleOption.fileUpload": "File upload",

0 commit comments

Comments
 (0)