Skip to content

Commit 3c6a97d

Browse files
authored
Wallet 1215 (#1279)
* Add `libsodium-wrappers-sumo` dependency and update Content Security Policy * Add support for Ed25519 and secp256k1 encryption/decryption utilities * Add support for message encryption and decryption in Casper Wallet SDK * Add support for decrypt message request and enforce message encryption length limit * Add support for decrypt message functionality in signature request module * Rename `message` to `decryptedMessage` for consistency in decrypt message response * Simplify decrypted message rendering logic in decrypt message content component. * Add support for `messageDecryption` in SDK types and capability checks * Add support for `messageDecryption` in SDK types and capability checks
1 parent a63b210 commit 3c6a97d

21 files changed

Lines changed: 788 additions & 11 deletions

File tree

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"i18next-http-backend": "2.5.0",
8080
"i18next-parser": "^9.3.0",
8181
"jszip": "^3.10.1",
82+
"libsodium-wrappers-sumo": "^0.8.2",
8283
"lodash.debounce": "^4.0.8",
8384
"lodash.throttle": "4.1.1",
8485
"mac-scrollbar": "^0.13.6",

src/apps/signature-request/app-router.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { selectVaultIsLocked } from '@background/redux/session/selectors';
1414

1515
import { ErrorPath, LockedRouter, WindowErrorPage } from '@libs/layout';
1616

17+
import { DecryptMessagePage } from './pages/decrypt-message';
1718
import { SignMessagePage } from './pages/sign-message';
1819
import { SignTransactionPage } from './pages/sign-transaction';
1920

@@ -30,6 +31,10 @@ export function AppRouter() {
3031
<Routes>
3132
<Route path={RouterPath.SignDeploy} element={<SignTransactionPage />} />
3233
<Route path={RouterPath.SignMessage} element={<SignMessagePage />} />
34+
<Route
35+
path={RouterPath.DecryptMessage}
36+
element={<DecryptMessagePage />}
37+
/>
3338
<Route
3439
path={ErrorPath}
3540
element={
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React from 'react';
2+
import { Trans, useTranslation } from 'react-i18next';
3+
import { useTheme } from 'styled-components';
4+
5+
import {
6+
ContentContainer,
7+
PageContainer,
8+
ParagraphContainer,
9+
SpacingSize
10+
} from '@libs/layout';
11+
import { FormField, Input, TextArea, Typography } from '@libs/ui/components';
12+
import { truncateKey } from '@libs/ui/components/hash/utils';
13+
14+
export interface SignMessageContentProps {
15+
message: string;
16+
hasDecryptionError: boolean;
17+
decryptedMessage: string;
18+
publicKeyHex: string;
19+
}
20+
21+
export function DecryptMessageContent({
22+
message,
23+
hasDecryptionError,
24+
decryptedMessage,
25+
publicKeyHex
26+
}: SignMessageContentProps) {
27+
const { t } = useTranslation();
28+
const theme = useTheme();
29+
30+
return (
31+
<PageContainer>
32+
<ContentContainer>
33+
<ParagraphContainer top={SpacingSize.XL}>
34+
<Typography type="header">
35+
<Trans t={t}>Decrypt Message Request</Trans>
36+
</Typography>
37+
</ParagraphContainer>
38+
{hasDecryptionError && (
39+
<ParagraphContainer top={SpacingSize.XL}>
40+
<FormField label={t('Decryption error:')}>
41+
<Typography type="body" color="contentWarning">
42+
<TextArea
43+
value={'Unable to decrypt message with provided publicKey'}
44+
readOnly
45+
style={{
46+
maxHeight: 125,
47+
color: theme.color.contentActionCritical
48+
}}
49+
/>
50+
</Typography>
51+
</FormField>
52+
</ParagraphContainer>
53+
)}
54+
<ParagraphContainer top={SpacingSize.XL}>
55+
<FormField
56+
label={t(decryptedMessage ? 'Decrypted message:' : 'Message:')}
57+
>
58+
<Typography type="body">
59+
<TextArea
60+
value={decryptedMessage || message}
61+
readOnly
62+
style={{ minHeight: 220 }}
63+
/>
64+
</Typography>
65+
</FormField>
66+
</ParagraphContainer>
67+
<ParagraphContainer top={SpacingSize.Small}>
68+
<FormField label={t('Signing Key:')}>
69+
<Typography type="body">
70+
<Input
71+
value={truncateKey(publicKeyHex, { size: 'max' })}
72+
monotype
73+
readOnly
74+
/>
75+
</Typography>
76+
</FormField>
77+
</ParagraphContainer>
78+
</ContentContainer>
79+
</PageContainer>
80+
);
81+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2+
import { Trans, useTranslation } from 'react-i18next';
3+
import { shallowEqual, useSelector } from 'react-redux';
4+
5+
import { ErrorMessages } from '@src/constants';
6+
import { getSigningAccount } from '@src/utils';
7+
8+
import { closeCurrentWindow } from '@background/close-current-window';
9+
import {
10+
selectConnectedAccountNamesWithActiveOrigin,
11+
selectVaultAccounts
12+
} from '@background/redux/vault/selectors';
13+
import { sendSdkResponseToSpecificTab } from '@background/send-sdk-response-to-specific-tab';
14+
15+
import { sdkMethod } from '@content/sdk-method';
16+
17+
import { decryptEncryptedBase64PrivateKey } from '@libs/crypto';
18+
import {
19+
FooterButtonsContainer,
20+
HeaderPopup,
21+
LayoutWindow
22+
} from '@libs/layout';
23+
import { HardwareWalletType } from '@libs/types/account';
24+
import { Button } from '@libs/ui/components';
25+
26+
import { DecryptMessageContent } from './decrypt-message-content';
27+
28+
export function DecryptMessagePage() {
29+
const { t } = useTranslation();
30+
const searchParams = new URLSearchParams(document.location.search);
31+
const [decryptedMessage, setDecryptedMessage] = useState('');
32+
const [hasDecryptionError, setHasDecryptionError] = useState(false);
33+
34+
const requestId = searchParams.get('requestId');
35+
const message = searchParams.get('message');
36+
const signingPublicKeyHex = searchParams.get('signingPublicKeyHex');
37+
38+
if (!requestId || !message || !signingPublicKeyHex) {
39+
throw Error(
40+
`${ErrorMessages.signTransaction.MISSING_SEARCH_PARAM.description} ${requestId} ${message} ${signingPublicKeyHex}`
41+
);
42+
}
43+
44+
const vaultAccounts = useSelector(selectVaultAccounts, shallowEqual);
45+
46+
const connectedAccountNamesWithOrigin = useSelector(
47+
selectConnectedAccountNamesWithActiveOrigin
48+
);
49+
50+
const connectedAccountNames = useMemo(
51+
() => connectedAccountNamesWithOrigin,
52+
[connectedAccountNamesWithOrigin]
53+
);
54+
55+
const signingAccount = getSigningAccount(vaultAccounts, signingPublicKeyHex);
56+
57+
if (!signingAccount) {
58+
const error = Error(
59+
ErrorMessages.signTransaction.SIGNING_ACCOUNT_MISSING.description
60+
);
61+
sendSdkResponseToSpecificTab(
62+
sdkMethod.signMessageError(error, { requestId })
63+
);
64+
throw error;
65+
}
66+
67+
if (signingAccount.hardware === HardwareWalletType.Ledger) {
68+
const error = Error(
69+
ErrorMessages.decryptMessage.LEDGER_NOT_SUPPORTED.description
70+
);
71+
sendSdkResponseToSpecificTab(
72+
sdkMethod.signMessageError(error, { requestId })
73+
);
74+
throw error;
75+
}
76+
77+
if (
78+
connectedAccountNames &&
79+
!connectedAccountNames.includes(signingAccount.name)
80+
) {
81+
const error = Error(
82+
ErrorMessages.signTransaction.ACCOUNT_NOT_CONNECTED.description
83+
);
84+
sendSdkResponseToSpecificTab(
85+
sdkMethod.signMessageError(error, { requestId })
86+
);
87+
throw error;
88+
}
89+
90+
const handleCancel = useCallback(() => {
91+
sendSdkResponseToSpecificTab(
92+
sdkMethod.decryptMessageResponse({ cancelled: true }, { requestId })
93+
);
94+
closeCurrentWindow();
95+
}, [requestId]);
96+
97+
useEffect(() => {
98+
window.addEventListener('beforeunload', handleCancel);
99+
100+
return () => window.removeEventListener('beforeunload', handleCancel);
101+
}, [handleCancel]);
102+
103+
const handleDecrypt = useCallback(async () => {
104+
try {
105+
if (!message) {
106+
return;
107+
}
108+
109+
const mdg = await decryptEncryptedBase64PrivateKey(
110+
message,
111+
signingAccount.publicKey,
112+
signingAccount.secretKey
113+
);
114+
115+
setHasDecryptionError(false);
116+
setDecryptedMessage(mdg);
117+
} catch (e) {
118+
setHasDecryptionError(true);
119+
console.error(e);
120+
}
121+
}, [message, signingAccount.publicKey, signingAccount.secretKey]);
122+
123+
const handleSendResponse = useCallback(async () => {
124+
if (!decryptedMessage) {
125+
return;
126+
}
127+
128+
sendSdkResponseToSpecificTab(
129+
sdkMethod.decryptMessageResponse(
130+
{ decryptedMessage, cancelled: false },
131+
{ requestId }
132+
)
133+
);
134+
closeCurrentWindow();
135+
}, [decryptedMessage, requestId]);
136+
137+
const renderFooter = useCallback(() => {
138+
return (
139+
<FooterButtonsContainer>
140+
<Button
141+
color="primaryRed"
142+
onClick={decryptedMessage ? handleSendResponse : handleDecrypt}
143+
>
144+
<Trans t={t}>
145+
{decryptedMessage ? 'Send response to app' : 'Decrypt'}
146+
</Trans>
147+
</Button>
148+
<Button color="secondaryBlue" onClick={handleCancel}>
149+
<Trans t={t}>Cancel</Trans>
150+
</Button>
151+
</FooterButtonsContainer>
152+
);
153+
}, [decryptedMessage, handleCancel, handleDecrypt, handleSendResponse, t]);
154+
155+
return (
156+
<LayoutWindow
157+
renderHeader={() => <HeaderPopup />}
158+
renderContent={() => (
159+
<DecryptMessageContent
160+
message={message}
161+
hasDecryptionError={hasDecryptionError}
162+
decryptedMessage={decryptedMessage}
163+
publicKeyHex={signingPublicKeyHex}
164+
/>
165+
)}
166+
renderFooter={renderFooter}
167+
/>
168+
);
169+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export enum RouterPath {
22
Any = '*',
33
SignDeploy = '/sign-deploy',
4-
SignMessage = '/sign-message'
4+
SignMessage = '/sign-message',
5+
DecryptMessage = '/decrypt-message'
56
}

src/background/create-open-window.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export enum WindowApp {
77
ConnectToApp = 'ConnectToApp',
88
SwitchAccount = 'SwitchAccount',
99
SignatureRequestDeploy = 'SignatureRequestDeploy',
10-
SignatureRequestMessage = 'SignatureRequestMessage'
10+
SignatureRequestMessage = 'SignatureRequestMessage',
11+
DecryptMessageRequest = 'DecryptMessageRequest'
1112
}
1213

1314
export type WindowSearchParams = Record<string, string>;
@@ -33,6 +34,8 @@ function getUrlByWindowApp(
3334
return `signature-request.html${searchParamsWithPrefix}#${RouterPath.SignDeploy}`;
3435
case WindowApp.SignatureRequestMessage:
3536
return `signature-request.html${searchParamsWithPrefix}#${RouterPath.SignMessage}`;
37+
case WindowApp.DecryptMessageRequest:
38+
return `signature-request.html${searchParamsWithPrefix}#${RouterPath.DecryptMessage}`;
3639
default:
3740
return 'popup.html';
3841
}

0 commit comments

Comments
 (0)