Skip to content

Commit 27d8580

Browse files
authored
build(deps): DEP-3 — crypto / vault modernization (WALLET-1330) (#1379)
* build(deps): bump Node 18 → 22 LTS + build-time RCE fix (WALLET-1327) Bump .nvmrc to v22.23.1 (Jod LTS, >=22.18 for future Babel 8) and Node in all 4 CI workflows; add engines field. Land the Node-gated build-tool bumps that transitively resolve the serialize-javascript advisory: web-ext 8->10.4, copy-webpack-plugin 11->14, terser-webpack-plugin 5.3.6->5.6.1 (serialize-javascript now 7.0.6 only). Switch ci-check install to npm ci. No app-code changes. * fix(deps): remediate npm audit vulnerabilities (WALLET-1328) Only one finding reaches the shipped bundle — i18next-http-backend's path-traversal via unsanitised lng/ns — so it is upgraded outright; the build-time serializer is treated as production-impacting and pinned via overrides. Everything else is dev/build-toolchain and accepted dev-only. - i18next-http-backend 2.5.0 -> ^3.0.5 (v3 uses global fetch; zero source change — i18n.ts has no backend:{} options block) - overrides += serialize-javascript ^7.0.5 (CVE-2026-34043), tmp ^0.2.7 (CVE-2026-44705); the override is mandatory — the webpack plugins resolve serialize-javascript to a vulnerable 6.x with no patch - add audit:ci script (npm audit --omit=dev --audit-level=high) wired into CI as a blocking runtime gate plus a non-blocking full-tree audit - regenerate package-lock.json under Node 22 Runtime audit clean (0). Residual is 16 dev-only advisories (0 critical), all under @redux-devtools/* and webpack-dev-server — never bundled. * test(crypto): pin vault AES-GCM blob byte-format before micro-aes-gcm swap (WALLET-1330) * refactor(crypto): replace deprecated micro-aes-gcm with @noble/ciphers gcm (WALLET-1330) * build(deps): migrate @lapo/asn1js to v2 ESM named exports (WALLET-1330) * build(deps): bump @noble/ciphers to v2 + libsodium floor; record @Scure v1 hold (WALLET-1330) * fix(deps): scope router to path-to-regexp v8 to unbreak start:chrome (WALLET-1328) * fix(deps): move i18next-parser to devDependencies (WALLET-1328) It's a build-time-only CLI used solely by locale:extract_pot and never shipped in the extension bundle. Keeping it in dependencies made the audit:ci gate (npm audit --omit=dev) treat its subtree (undici, node-fetch, cross-fetch) as runtime, which is why undici had to be floated to 7.28.0 and why any future advisory in that tree would have failed the gate as a false positive.
1 parent 78ff3ca commit 27d8580

10 files changed

Lines changed: 125 additions & 39 deletions

File tree

jest.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ module.exports = {
44
transform: {
55
'^.+\\.(j|t)sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.json' }]
66
},
7-
transformIgnorePatterns: ['<rootDir>/node_modules/(?!micro-aes-gcm/.*)'],
7+
transformIgnorePatterns: [
8+
'<rootDir>/node_modules/(?!(@lapo/asn1js|@noble/ciphers)/)'
9+
],
810
coveragePathIgnorePatterns: ['/node_modules/'],
911
testRegex: '(/tests?/.*|(\\.|/)(test|spec))\\.tsx?$',
1012
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],

package-lock.json

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

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,18 @@
5656
"resolutions": {
5757
"styled-components": "^5"
5858
},
59+
"//dependencyNotes": "@scure/bip32 and @scure/bip39 are intentionally held at v1 (1.6.2 / 1.2.1): casper-js-sdk@5.0.12 pins @scure/* ^1 and shares the top-level copy. Bumping to v2 would fork BIP32/39 into two impls and pull @noble/curves@2/@noble/hashes@2 (wrong-address risk, no upside). Bump in lockstep only when the SDK/core move off @scure ^1. See WALLET-1330.",
5960
"dependencies": {
6061
"@bringweb3/chrome-extension-kit": "1.6.4",
6162
"@formatjs/intl": "2.10.4",
6263
"@hookform/resolvers": "2.9.10",
63-
"@lapo/asn1js": "1.2.4",
64+
"@lapo/asn1js": "^2.1.3",
6465
"@ledgerhq/hw-transport": "^6.31.12",
6566
"@ledgerhq/hw-transport-web-ble": "^6.29.12",
6667
"@ledgerhq/hw-transport-webhid": "^6.30.8",
6768
"@ledgerhq/hw-transport-webusb": "^6.29.12",
6869
"@lottiefiles/react-lottie-player": "3.6.0",
69-
"@noble/ciphers": "^1.3.0",
70+
"@noble/ciphers": "^2.2.0",
7071
"@scure/bip32": "1.6.2",
7172
"@scure/bip39": "1.2.1",
7273
"@tanstack/react-query": "^5.100.5",
@@ -83,12 +84,11 @@
8384
"i18next-browser-languagedetector": "^7.2.1",
8485
"i18next-http-backend": "^3.0.5",
8586
"jszip": "^3.10.1",
86-
"libsodium-wrappers-sumo": "^0.8.2",
87+
"libsodium-wrappers-sumo": "^0.8.4",
8788
"lodash.debounce": "^4.0.8",
8889
"lodash.throttle": "4.1.1",
8990
"mac-scrollbar": "^0.13.6",
9091
"md5": "^2.3.0",
91-
"micro-aes-gcm": "0.3.3",
9292
"qrcode.react": "^3.1.0",
9393
"react": "^18.2.0",
9494
"react-dom": "^18.3.1",

src/@types/lapo/lapo.d.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1-
declare module '@lapo/asn1js';
2-
declare module '@lapo/asn1js/hex';
3-
declare module '@lapo/asn1js/base64';
1+
declare module '@lapo/asn1js' {
2+
export class ASN1 {
3+
static decode(data: string | ArrayBuffer | Uint8Array): {
4+
toPrettyString(): string;
5+
};
6+
}
7+
}
8+
9+
declare module '@lapo/asn1js/base64.js' {
10+
export class Base64 {
11+
static unarmor(input: string): Uint8Array;
12+
}
13+
}

src/background/workers/generate-sync-wallet-qr-data-worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { cbc } from '@noble/ciphers/aes';
1+
import { cbc } from '@noble/ciphers/aes.js';
22
import { scryptAsync } from '@noble/hashes/scrypt';
33
import { randomBytes } from '@noble/hashes/utils';
44
import { PublicKey } from 'casper-js-sdk';
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// src/libs/crypto/aes.back-compat.test.ts
2+
import { createCipheriv } from 'crypto';
3+
4+
import {
5+
FIXED_ENCRYPTION_CIPHER_TEXT,
6+
FIXED_ENCRYPTION_KEY_HASH,
7+
FIXED_ENCRYPTION_PLAIN_TEXT
8+
} from './__fixtures';
9+
import { aesDecryptString, aesEncryptString } from './aes';
10+
11+
const IV_LENGTH = 12;
12+
const TAG_LENGTH = 16;
13+
14+
describe('crypto.aes vault byte-compat', () => {
15+
it('decrypts a legacy micro-aes-gcm vault blob unchanged', async () => {
16+
const decrypted = await aesDecryptString(
17+
FIXED_ENCRYPTION_KEY_HASH,
18+
FIXED_ENCRYPTION_CIPHER_TEXT
19+
);
20+
21+
expect(decrypted).toBe(FIXED_ENCRYPTION_PLAIN_TEXT);
22+
});
23+
24+
it('produces the documented base64(iv[12] || ciphertext || tag[16]) layout', async () => {
25+
const cipherBase64 = await aesEncryptString(
26+
FIXED_ENCRYPTION_KEY_HASH,
27+
FIXED_ENCRYPTION_PLAIN_TEXT
28+
);
29+
const bytes = Buffer.from(cipherBase64, 'base64');
30+
const plaintextLength = Buffer.byteLength(
31+
FIXED_ENCRYPTION_PLAIN_TEXT,
32+
'utf8'
33+
);
34+
35+
expect(bytes.length).toBe(IV_LENGTH + plaintextLength + TAG_LENGTH);
36+
});
37+
38+
it('decrypts a blob built by an independent AES-256-GCM oracle (fixed key + iv)', async () => {
39+
const key = Buffer.from(FIXED_ENCRYPTION_KEY_HASH, 'hex'); // 32 bytes
40+
const iv = Buffer.alloc(IV_LENGTH, 7); // deterministic 12-byte nonce
41+
const plaintext = 'casper vault back-compat probe';
42+
43+
const cipher = createCipheriv('aes-256-gcm', key, iv);
44+
const ciphertext = Buffer.concat([
45+
cipher.update(plaintext, 'utf8'),
46+
cipher.final()
47+
]);
48+
const tag = cipher.getAuthTag(); // 16 bytes
49+
const blob = Buffer.concat([iv, ciphertext, tag]).toString('base64');
50+
51+
const decrypted = await aesDecryptString(FIXED_ENCRYPTION_KEY_HASH, blob);
52+
53+
expect(decrypted).toBe(plaintext);
54+
});
55+
56+
it('round-trips encrypt → decrypt', async () => {
57+
const plaintext = 'round trip';
58+
const blob = await aesEncryptString(FIXED_ENCRYPTION_KEY_HASH, plaintext);
59+
60+
const decrypted = await aesDecryptString(FIXED_ENCRYPTION_KEY_HASH, blob);
61+
62+
expect(decrypted).toBe(plaintext);
63+
});
64+
});

src/libs/crypto/aes.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import * as aes from 'micro-aes-gcm/index';
1+
import { gcm } from '@noble/ciphers/aes.js';
2+
import {
3+
bytesToUtf8,
4+
concatBytes,
5+
randomBytes,
6+
utf8ToBytes
7+
} from '@noble/ciphers/utils.js';
28

39
import {
410
convertBase64ToBytes,
@@ -10,16 +16,22 @@ export async function aesEncryptString(
1016
keyHash: string,
1117
str: string
1218
): Promise<string> {
13-
const key = convertHexToBytes(keyHash);
14-
const bytes = await aes.encrypt(key, str);
15-
return convertBytesToBase64(bytes);
19+
const iv = randomBytes(12);
20+
const ciphertext = gcm(convertHexToBytes(keyHash), iv).encrypt(
21+
utf8ToBytes(str)
22+
);
23+
24+
return convertBytesToBase64(concatBytes(iv, ciphertext));
1625
}
1726

1827
export async function aesDecryptString(
1928
keyHash: string,
2029
cipherBase64: string
2130
): Promise<string> {
22-
const key = convertHexToBytes(keyHash);
23-
const jsonBytes = await aes.decrypt(key, convertBase64ToBytes(cipherBase64));
24-
return aes.utils.bytesToUtf8(jsonBytes);
31+
const data = convertBase64ToBytes(cipherBase64);
32+
const plaintext = gcm(convertHexToBytes(keyHash), data.slice(0, 12)).decrypt(
33+
data.slice(12)
34+
);
35+
36+
return bytesToUtf8(plaintext);
2537
}

src/libs/crypto/parse-secret-key-string.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import ASN1 from '@lapo/asn1js';
2-
import Base64 from '@lapo/asn1js/base64';
1+
import { ASN1 } from '@lapo/asn1js';
2+
import { Base64 } from '@lapo/asn1js/base64.js';
33
import { Conversions, KeyAlgorithm, PrivateKey } from 'casper-js-sdk';
44
// These libraries are required for backward compatibility with Legacy Signer
55
import { t } from 'i18next';

src/libs/crypto/sign-deploy.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { concatBytes } from '@noble/ciphers/utils';
1+
import { concatBytes } from '@noble/ciphers/utils.js';
22
import {
33
CasperNetworkName,
44
Conversions,

src/libs/crypto/sign-message.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { concatBytes } from '@noble/ciphers/utils';
1+
import { concatBytes } from '@noble/ciphers/utils.js';
22
import { Conversions, KeyAlgorithm, PrivateKey } from 'casper-js-sdk';
33

44
import {

0 commit comments

Comments
 (0)