Skip to content
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
4 changes: 3 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ module.exports = {
transform: {
'^.+\\.(j|t)sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.json' }]
},
transformIgnorePatterns: ['<rootDir>/node_modules/(?!micro-aes-gcm/.*)'],
transformIgnorePatterns: [
'<rootDir>/node_modules/(?!(@lapo/asn1js|@noble/ciphers)/)'
],
coveragePathIgnorePatterns: ['/node_modules/'],
testRegex: '(/tests?/.*|(\\.|/)(test|spec))\\.tsx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
Expand Down
36 changes: 17 additions & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,18 @@
"resolutions": {
"styled-components": "^5"
},
"//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.",
"dependencies": {
"@bringweb3/chrome-extension-kit": "1.6.4",
"@formatjs/intl": "2.10.4",
"@hookform/resolvers": "2.9.10",
"@lapo/asn1js": "1.2.4",
"@lapo/asn1js": "^2.1.3",
"@ledgerhq/hw-transport": "^6.31.12",
"@ledgerhq/hw-transport-web-ble": "^6.29.12",
"@ledgerhq/hw-transport-webhid": "^6.30.8",
"@ledgerhq/hw-transport-webusb": "^6.29.12",
"@lottiefiles/react-lottie-player": "3.6.0",
"@noble/ciphers": "^1.3.0",
"@noble/ciphers": "^2.2.0",
"@scure/bip32": "1.6.2",
"@scure/bip39": "1.2.1",
"@tanstack/react-query": "^5.100.5",
Expand All @@ -83,12 +84,11 @@
"i18next-browser-languagedetector": "^7.2.1",
"i18next-http-backend": "^3.0.5",
"jszip": "^3.10.1",
"libsodium-wrappers-sumo": "^0.8.2",
"libsodium-wrappers-sumo": "^0.8.4",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "4.1.1",
"mac-scrollbar": "^0.13.6",
"md5": "^2.3.0",
"micro-aes-gcm": "0.3.3",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.3.1",
Expand Down
16 changes: 13 additions & 3 deletions src/@types/lapo/lapo.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
declare module '@lapo/asn1js';
declare module '@lapo/asn1js/hex';
declare module '@lapo/asn1js/base64';
declare module '@lapo/asn1js' {
export class ASN1 {
static decode(data: string | ArrayBuffer | Uint8Array): {
toPrettyString(): string;
};
}
}

declare module '@lapo/asn1js/base64.js' {
export class Base64 {
static unarmor(input: string): Uint8Array;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cbc } from '@noble/ciphers/aes';
import { cbc } from '@noble/ciphers/aes.js';
import { scryptAsync } from '@noble/hashes/scrypt';
import { randomBytes } from '@noble/hashes/utils';
import { PublicKey } from 'casper-js-sdk';
Expand Down
64 changes: 64 additions & 0 deletions src/libs/crypto/aes.back-compat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// src/libs/crypto/aes.back-compat.test.ts
import { createCipheriv } from 'crypto';

import {
FIXED_ENCRYPTION_CIPHER_TEXT,
FIXED_ENCRYPTION_KEY_HASH,
FIXED_ENCRYPTION_PLAIN_TEXT
} from './__fixtures';
import { aesDecryptString, aesEncryptString } from './aes';

const IV_LENGTH = 12;
const TAG_LENGTH = 16;

describe('crypto.aes vault byte-compat', () => {
it('decrypts a legacy micro-aes-gcm vault blob unchanged', async () => {
const decrypted = await aesDecryptString(
FIXED_ENCRYPTION_KEY_HASH,
FIXED_ENCRYPTION_CIPHER_TEXT
);

expect(decrypted).toBe(FIXED_ENCRYPTION_PLAIN_TEXT);
});

it('produces the documented base64(iv[12] || ciphertext || tag[16]) layout', async () => {
const cipherBase64 = await aesEncryptString(
FIXED_ENCRYPTION_KEY_HASH,
FIXED_ENCRYPTION_PLAIN_TEXT
);
const bytes = Buffer.from(cipherBase64, 'base64');
const plaintextLength = Buffer.byteLength(
FIXED_ENCRYPTION_PLAIN_TEXT,
'utf8'
);

expect(bytes.length).toBe(IV_LENGTH + plaintextLength + TAG_LENGTH);
});

it('decrypts a blob built by an independent AES-256-GCM oracle (fixed key + iv)', async () => {
const key = Buffer.from(FIXED_ENCRYPTION_KEY_HASH, 'hex'); // 32 bytes
const iv = Buffer.alloc(IV_LENGTH, 7); // deterministic 12-byte nonce
const plaintext = 'casper vault back-compat probe';

const cipher = createCipheriv('aes-256-gcm', key, iv);
const ciphertext = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final()
]);
const tag = cipher.getAuthTag(); // 16 bytes
const blob = Buffer.concat([iv, ciphertext, tag]).toString('base64');

const decrypted = await aesDecryptString(FIXED_ENCRYPTION_KEY_HASH, blob);

expect(decrypted).toBe(plaintext);
});

it('round-trips encrypt → decrypt', async () => {
const plaintext = 'round trip';
const blob = await aesEncryptString(FIXED_ENCRYPTION_KEY_HASH, plaintext);

const decrypted = await aesDecryptString(FIXED_ENCRYPTION_KEY_HASH, blob);

expect(decrypted).toBe(plaintext);
});
});
26 changes: 19 additions & 7 deletions src/libs/crypto/aes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import * as aes from 'micro-aes-gcm/index';
import { gcm } from '@noble/ciphers/aes.js';
import {
bytesToUtf8,
concatBytes,
randomBytes,
utf8ToBytes
} from '@noble/ciphers/utils.js';

import {
convertBase64ToBytes,
Expand All @@ -10,16 +16,22 @@ export async function aesEncryptString(
keyHash: string,
str: string
): Promise<string> {
const key = convertHexToBytes(keyHash);
const bytes = await aes.encrypt(key, str);
return convertBytesToBase64(bytes);
const iv = randomBytes(12);
const ciphertext = gcm(convertHexToBytes(keyHash), iv).encrypt(
utf8ToBytes(str)
);

return convertBytesToBase64(concatBytes(iv, ciphertext));
}

export async function aesDecryptString(
keyHash: string,
cipherBase64: string
): Promise<string> {
const key = convertHexToBytes(keyHash);
const jsonBytes = await aes.decrypt(key, convertBase64ToBytes(cipherBase64));
return aes.utils.bytesToUtf8(jsonBytes);
const data = convertBase64ToBytes(cipherBase64);
const plaintext = gcm(convertHexToBytes(keyHash), data.slice(0, 12)).decrypt(
data.slice(12)
);

return bytesToUtf8(plaintext);
}
4 changes: 2 additions & 2 deletions src/libs/crypto/parse-secret-key-string.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ASN1 from '@lapo/asn1js';
import Base64 from '@lapo/asn1js/base64';
import { ASN1 } from '@lapo/asn1js';
import { Base64 } from '@lapo/asn1js/base64.js';
import { Conversions, KeyAlgorithm, PrivateKey } from 'casper-js-sdk';
// These libraries are required for backward compatibility with Legacy Signer
import { t } from 'i18next';
Expand Down
2 changes: 1 addition & 1 deletion src/libs/crypto/sign-deploy.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { concatBytes } from '@noble/ciphers/utils';
import { concatBytes } from '@noble/ciphers/utils.js';
import {
CasperNetworkName,
Conversions,
Expand Down
2 changes: 1 addition & 1 deletion src/libs/crypto/sign-message.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { concatBytes } from '@noble/ciphers/utils';
import { concatBytes } from '@noble/ciphers/utils.js';
import { Conversions, KeyAlgorithm, PrivateKey } from 'casper-js-sdk';

import {
Expand Down
Loading