BlueWallet/tests/unit/encryption.test.ts
Ivan Vershigora 31eca3cc2d REF: scrypt + AES-256-GCM (v2 envelope), legacy v1 read-only
Encrypt now emits a v2: envelope — scrypt (N=2^15, r=8, p=1) KDF plus
AES-256-GCM. Decrypt still reads legacy Salted__ (EVP_BytesToKey-MD5 +
AES-256-CBC) ciphertexts, and lazily rewrites them as v2 on the first
successful unlock. Public encrypt/decrypt are now async.
2026-06-22 11:20:56 +01:00

84 lines
4.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from 'assert';
import * as c from '../../blue_modules/encryption';
jest.setTimeout(30 * 1000); // scrypt KDF (N=2^15) is ~200400 ms per call
describe('unit - encryption', function () {
it('encrypts and decrypts', async function () {
const data2encrypt = 'really long data string bla bla really long data string bla bla really long data string bla bla';
const crypted = await c.encrypt(data2encrypt, 'password');
const decrypted = await c.decrypt(crypted, 'password');
assert.ok(crypted);
assert.ok(decrypted);
assert.strictEqual(decrypted, data2encrypt);
assert.ok(crypted !== data2encrypt);
assert.ok(crypted.startsWith('v2:'), 'new encryptions must use the v2 envelope');
const decryptedWithBadPassword = await c.decrypt(crypted, 'passwordBad');
assert.strictEqual(decryptedWithBadPassword, false);
let exceptionRaised = false;
try {
await c.encrypt('yolo', 'password');
} catch (_) {
exceptionRaised = true;
}
assert.ok(exceptionRaised);
});
it('handles ok malformed data', async function () {
const decrypted = await c.decrypt(
'U2FsdGVkX1/OSNdi0JrLANn9qdNEiXgP20MJgT13CMKC7xKe+sb7x0An6r8lzrYeL2vjoPm2Xi5I3UdBcsgjgh0TR4PypNdDaW1tW8LhFH1wVCh1hacrFsJjoKMBmdCn4IVMwtIffGPptqBrGZl+6kjOc3BBbgq4uaAavFIwTS86WdaRt9qAboBcoPJZxsj37othbZfZfl2GBTCWnR1tOYAbElKWv4lBwNQpX7HqX3wTQkAbamBslsH5FfZRY1c38lOHrZMwNSyxhgspydksTxKkhPqWQu3XWT4GpRoRuVvYlBNvJOCUu2JbiVSp4NiOMSfnA8ahvpCGRNy+qPWsXqmJtz9BwyzedzDkgg6QOqxXz4oOeEJa/XLKiuv3ItsLrZb+sSA6wjB1Cx6/Oh2vW7eiHjCITeC7KUK1fAxVwufLcprNkvG8qFzkOcHxDyzG+sNL0cMipAxhpMX7qIcYcZFoLYkQRQHpOZKZCIAdNTfPGJ7M4cxGM0V+Uuirjyn+KAPJwNElwmPpX8sTQyEqlIlEwVjFXBpz28N5RAGN2zzCzEjD8NVYQJ2QyHj0gfWe',
'fakePassword',
);
assert.ok(!decrypted);
});
it('can decrypt cipher created by CryptoJS@3.1.9-1 (legacy v1 path)', async () => {
const data2decrypt = 'really long data string bla bla really long data string bla bla really long data string bla bla';
const crypted =
'U2FsdGVkX19fJ4PcLum+tmBpEVNgGGsGKOhRS21cEcYAox+Df8VqmnnG9t2PvpM05eWImCRArorVUUegtcfSq314WMFzxKmiPIl9eqV1aOY+VFGuIBx0VIVsCWix2Q7sRZZwnOVpG5bdveZI0+Azyw==';
const decrypted = await c.decrypt(crypted, 'password');
assert.deepEqual(data2decrypt, decrypted);
});
it('can decrypt a ciphertext produced by the OpenSSL CLI (legacy v1 wire-format check)', async () => {
// Regenerate this fixture with (copy-pasteable, verified to reproduce the byte string below):
//
// { printf 'Salted__\x01\x02\x03\x04\x05\x06\x07\x08'; \
// printf 'hello world this is plaintext' \
// | openssl enc -aes-256-cbc -k mypassword -S 0102030405060708 -md md5; \
// } | base64
//
// OpenSSL's `enc` only emits the `Salted__` envelope when it picks the salt itself;
// passing `-S <hex>` suppresses the header, so we prepend it manually. Pins the
// on-disk format against an independent reference beyond crypto-js.
const crypted = 'U2FsdGVkX18BAgMEBQYHCMqtJuZaneiHrVN/oMPPLvFplovZbI1K+lulGJn7NAvn';
assert.strictEqual(await c.decrypt(crypted, 'mypassword'), 'hello world this is plaintext');
});
it('roundtrips multi-byte UTF-8 (emoji / CJK) under v2', async () => {
const data = '日本語テスト 🌅🔥🌊 multi-byte plaintext ✅';
const crypted = await c.encrypt(data, 'pässwörd中');
assert.ok(crypted.startsWith('v2:'));
const decrypted = await c.decrypt(crypted, 'pässwörd中');
assert.strictEqual(decrypted, data);
});
it('returns false on tampered v2 ciphertext (AEAD auth-tag mismatch)', async () => {
const crypted = await c.encrypt('legitimate payload bytes here', 'password');
assert.ok(crypted.startsWith('v2:'));
// Flip one base64 char near the end (inside the auth tag region) — must not decrypt
const tampered = `${crypted.slice(0, -2)}${crypted.slice(-2) === 'AA' ? 'BB' : 'AA'}`;
assert.strictEqual(await c.decrypt(tampered, 'password'), false);
});
it('returns false on empty / non-string input', async () => {
assert.strictEqual(await c.decrypt('', 'password'), false);
// @ts-expect-error — runtime guard for non-string input
assert.strictEqual(await c.decrypt(undefined, 'password'), false);
});
});