From 66d38bbd6e61d4fd4645b200fc2c2007db3c7cb8 Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Sat, 26 Jul 2025 13:55:20 -0700 Subject: [PATCH] use KeyObject instead of buffers for all sensitive data, use HKDF for new blake2b512 versions, sanity tests, doc updates --- CHANGELOG.md | 8 +++ README.md | 13 +++- lib/mystery-box.js | 140 ++++++++++++++++++++++++++++---------- lib/version-parameters.js | 84 +++++++++++++++++++++-- test/lib/mystery-box.js | 46 +++++++++++-- 5 files changed, 238 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 430fddd..ce00075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ Releases and notable changes to this project are documented here. +## [v2.2.0] - TBD + +- support including additional data in events +- include simple secret generator convenience script +- internal: add hkdf as preferred key deriver +- internal: use KeyObject instead of plain Buffer for keys and secrets +- updated devDependencies + ## [v2.1.0] - 2024-11-26 - Added new preferred versions which store payload flags encrypted rather than tagged, to avoid leaking any payload metadata. diff --git a/README.md b/README.md index 3104107..41c2bf9 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,17 @@ In our case, this results in a Base64URL encoded string containing a bespoke pac ## API -- `async pack(contents, version, flags)` -- `async unpack(box)` +### `async pack(contents, version, flags, statsExtra)` + + - `contents`: a JSON-serializable object or Buffer + - `version`: optional, override the best-available version + - `flags`: optional, override the default flags of enabling Flate compression + - `statsExtra`: optional, additional data included in the statistics event for this call + +### `async unpack(box, statsExtra)` + + - `box`: base64url encoded string + - `statsExtra`: optional, additional data included in the statistics event for this call ## Example diff --git a/lib/mystery-box.js b/lib/mystery-box.js index 3bf9b84..cb2aa4e 100644 --- a/lib/mystery-box.js +++ b/lib/mystery-box.js @@ -25,6 +25,7 @@ const deflateRawAsync = promisify(zlib.deflateRaw); const inflateRawAsync = promisify(zlib.inflateRaw); const scryptAsync = promisify(crypto.scrypt); const randomBytesAsync = promisify(crypto.randomBytes); +const hkdfAsync = promisify(crypto.hkdf); /** * Only you will know what's inside your... @@ -48,6 +49,7 @@ const randomBytesAsync = promisify(crypto.randomBytes); * - payload, encrypted version of possibly compressed and serialized object * * Older versions exposed the payload state flags, later versions encrypt them. + * All bytes up to the encrypted content are verified as AAD. */ @@ -72,23 +74,34 @@ const payloadFlagsShift = 7; /** * @typedef {string|Buffer} Secret * @typedef {object} MysteryBoxOptions - * @property {Secret|Secret[]} encryptionSecret if an array, will always encrypt with first secret, will attempt to decrypt with all in ordere + * @property {Secret|Secret[]} encryptionSecret if an array, will always encrypt with first secret, will attempt to decrypt with all in order * @property {number=} defaultFlags default flags to use when packing boxes + * @property {string|Buffer=} hkdfSalt domain-specific salt for HKDF key derivation * @event MysteryBox#statistics * @type {object} * @property {...PackageInfo} package name and version * @property {...Stats} details of operation * @property {...TimingsPhases} timings of operation + * @property {any=} extra extra data */ class MysteryBox extends EventEmitter { + static statisticsEvent = 'statistics'; + secrets; + versionParameters; + bestVersion; + Flags; + defaultFlags; + hkdfSalt; + /** * @param {MysteryBoxOptions} options options * @param {any[]} args passed on to EventEmitter constructor */ constructor(options = {}, ...args) { super(...args); - this.secrets = MysteryBox._ensureArray(options.encryptionSecret); + this.secrets = MysteryBox._ensureArray(options.encryptionSecret) + .map((secret) => crypto.createSecretKey(secret)); if (!this.secrets.length) { throw new MysteryBoxError('missing encryption secret'); } @@ -98,6 +111,9 @@ class MysteryBox extends EventEmitter { const availableHashes = crypto.getHashes(); // Add legacy scrypt to available hashes for filtering key derivations availableHashes.push(allVersions.KD.SCRYPT); + /** + * @type {Record} + */ this.versionParameters = Object.entries(allVersions).reduce((acc, [v, p]) => { const validCipher = availableCiphers.includes(p.algorithm); const validKeyDeriver = availableHashes.includes(p.keyDeriver); @@ -118,6 +134,8 @@ class MysteryBox extends EventEmitter { if (this.defaultFlags < 0 || this.defaultFlags > 255) { throw new MysteryBoxError('Invalid default flag value'); } + + this.hkdfSalt = options.hkdfSalt ?? Buffer.from('squeep-mystery-box'); } @@ -137,10 +155,15 @@ class MysteryBox extends EventEmitter { } + /** + * @typedef {object} FlagsDecoded + * @property {number} compression compression type + * @property {boolean} payloadIsBuffer true if payload is a buffer + */ /** * Parse the bits out of the flags. * @param {number} flags flags - * @returns {object} decoded flags + * @returns {FlagsDecoded} decoded flags */ static _decodeFlags(flags) { return { @@ -152,45 +175,61 @@ class MysteryBox extends EventEmitter { /** * Generate key data. - * @param {string} deriver key deriver enum - * @param {Buffer} secret secret + * @param {string} deriver key deriver enum string + * @param {crypto.KeyObject} secret secret * @param {Buffer} salt salt * @param {number} keyBytes bytes in key - * @returns {Promise} key + * @param {boolean} useHKDF use HKDF for derivation + * @returns {Promise} key */ - static async _keyFromSecret(deriver, secret, salt, keyBytes) { + async _keyFromSecret(deriver, secret, salt, keyBytes, useHKDF) { + let keyBuffer; switch (deriver) { case allVersions.KD.SHAKE256: { const hash = crypto.createHash(allVersions.KD.SHAKE256, { outputLength: keyBytes }); hash.update(salt); - hash.update(secret); - return hash.digest(); + hash.update(secret.export()); + keyBuffer = hash.digest(); + break; } case allVersions.KD.BLAKE2B512: { - const hash = crypto.createHash(allVersions.KD.BLAKE2B512); - hash.update(salt); - hash.update(secret); - const digest = hash.digest(); - // should assert that keyBytes <= 64 - // but not concerned about that for now - // until we have a new algorithm with bigger key size - return digest.subarray(0, keyBytes); + if (useHKDF) { + keyBuffer = await hkdfAsync(allVersions.KD.BLAKE2B512, secret.export(), this.hkdfSalt, salt, keyBytes); + } else { + const hash = crypto.createHash(allVersions.KD.BLAKE2B512); + hash.update(salt); + hash.update(secret.export()); + const digest = hash.digest(); + // should assert that keyBytes <= 64 + // but not concerned about that for now + // until we have a new algorithm with bigger key size + keyBuffer = digest.subarray(0, keyBytes); + } + break; } case allVersions.KD.SCRYPT: - return scryptAsync(secret, salt, keyBytes); + keyBuffer = await scryptAsync(secret.export(), salt, keyBytes); + break; default: throw new MysteryBoxError('unsupported key deriver'); } + + return crypto.createSecretKey(keyBuffer); } + /** + * @typedef {object} VersionHeaderBits + * @property {number} headerValue value + * @property {number} headerMask mask + */ /** * Return bits and bit mask for given number of encoded bytes. * @param {number} numBytes number of bytes - * @returns {object} bits value and mask + * @returns {VersionHeaderBits} bits value and mask */ static _versionHeaderBits(numBytes) { // Round up to 8 bits in result, just to be proper. @@ -231,6 +270,11 @@ class MysteryBox extends EventEmitter { } + /** + * @typedef {object} VersionDecoded + * @property {number} version version number + * @property {number} versionBytes number of bytes used to encode version + */ /** * Decode leading bytes of buffer as version identifier. * In the first byte, the position of the first unset bit indicates how @@ -238,7 +282,7 @@ class MysteryBox extends EventEmitter { * Returns decoded version and number of bytes used to decode. * Only supports up to 6-byte numbers, and of those, only up to 4398046511103. * @param {Buffer} buf N.B. will be mogrified - * @returns {object} decoded version and version byte length + * @returns {VersionDecoded} decoded version and version byte length */ static _versionDecode(buf) { const headerByte = buf.readUInt8(0); @@ -263,10 +307,15 @@ class MysteryBox extends EventEmitter { } + /** + * @typedef {object} VersionEncoded + * @property {Buffer} buffer encoded version + * @property {number} versionBytes number of bytes used to encode version + */ /** * Encode a version identifier into a buffer of a variable number of bytes. * @param {number} version version number to encode - * @returns {object} encoded version buffer and number of bytes + * @returns {VersionEncoded} encoded version buffer and number of bytes */ static _versionEncode(version) { let versionBytes = 0; @@ -350,35 +399,40 @@ class MysteryBox extends EventEmitter { * @param {object|Buffer} contents object or buffer * @param {number=} version version * @param {number=} flags flags + * @param {any=} statsExtra extra field added to stats * @returns {Promise} encoded contents * @fires MysteryBox#statistics */ - async pack(contents, version = this.bestVersion, flags = this.defaultFlags) { + async pack(contents, version, flags, statsExtra) { const { stats, timingsMs } = MysteryBox._newStats('pack'); + version = version ?? this.bestVersion; + flags = flags ?? this.defaultFlags; + if (!(version in this.versionParameters)) { throw new MysteryBoxError(`MysteryBox format version ${version} not supported`); } // eslint-disable-next-line security/detect-object-injection const v = this.versionParameters[version]; - stats.version = version; + stats.version = v.version; + // version specific details const visibleFlags = (version <= 9); + const useHKDF = (version >= 19); const { compression, payloadIsBuffer } = MysteryBox._decodeFlags(flags); if (Buffer.isBuffer(contents)) { // Ensure payloadIsBuffer flag is set when contents are indeed a Buffer flags |= this.Flags.BufferPayload; + } else if (payloadIsBuffer) { + // Flag is set, but contents are not a Buffer? Try to coerce contents. + contents = Buffer.from(contents); } else { - if (payloadIsBuffer) { - // Flag is set, but contents are not a Buffer? Try to coerce contents. - contents = Buffer.from(contents); - } else { - // Otherwise attempt to serialize the object - contents = JSON.stringify(contents); - } + // Otherwise attempt to serialize the object + contents = JSON.stringify(contents); } + stats.serializedBytes = Buffer.byteLength(contents); const { buffer: versionBuffer, versionBytes } = MysteryBox._versionEncode(v.version); @@ -424,7 +478,7 @@ class MysteryBox extends EventEmitter { // Always encrypt with first secret const secret = this.secrets[0]; - const key = await MysteryBox._keyFromSecret(v.keyDeriver, secret, salt, v.keyBytes); + const key = await this._keyFromSecret(v.keyDeriver, secret, salt, v.keyBytes, useHKDF); const cipher = crypto.createCipheriv(v.algorithm, key, iv, v.algOptions); let result; @@ -454,7 +508,12 @@ class MysteryBox extends EventEmitter { } timingsMs.end = timingsMs.postCrypt = performance.now(); - this.emit('statistics', { ...stats, ...MysteryBox._timingsLog(timingsMs), ...packageInfo }); + this.emit('statistics', { + ...stats, + ...MysteryBox._timingsLog(timingsMs), + ...packageInfo, + ...(statsExtra && { extra: statsExtra }), + }); return result; } @@ -462,11 +521,12 @@ class MysteryBox extends EventEmitter { /** * Take contents out of a mysterious box. - * @param {string} box - Base64URL encoded payload + * @param {string} box Base64URL encoded payload + * @param {any=} statsExtra extra fieild added to stats * @returns {Promise} decoded contents * @fires MysteryBox#statistics */ - async unpack(box) { + async unpack(box, statsExtra) { const { stats, timingsMs } = MysteryBox._newStats('unpack'); if (!box) { @@ -490,6 +550,7 @@ class MysteryBox extends EventEmitter { stats.version = version; const visibleFlags = (version <= 9); + const useHKDF = (version >= 19); const minBytes = v.versionBytes + (visibleFlags ? v.flagsBytes : 0) + v.ivBytes + v.saltBytes + v.tagBytes; if (raw.length < minBytes) { @@ -521,8 +582,8 @@ class MysteryBox extends EventEmitter { let decrypted; let err; let success = false; - for await (const secret of this.secrets) { - const key = await MysteryBox._keyFromSecret(v.keyDeriver, secret, salt, v.keyBytes); + for (const secret of this.secrets) { + const key = await this._keyFromSecret(v.keyDeriver, secret, salt, v.keyBytes, useHKDF); const decipher = crypto.createDecipheriv(v.algorithm, key, iv, v.algOptions); decipher.setAAD(aad); decipher.setAuthTag(tag); @@ -572,7 +633,12 @@ class MysteryBox extends EventEmitter { } stats.flags = this._prettyFlags(flags); - this.emit('statistics', { ...stats, ...MysteryBox._timingsLog(timingsMs), ...packageInfo }); + this.emit('statistics', { + ...stats, + ...MysteryBox._timingsLog(timingsMs), + ...packageInfo, + ...(statsExtra && { extra: statsExtra }), + }); return payload; } diff --git a/lib/version-parameters.js b/lib/version-parameters.js index bfbb116..3d67e6e 100644 --- a/lib/version-parameters.js +++ b/lib/version-parameters.js @@ -5,7 +5,7 @@ const ALG = { AES_256_GCM: 'aes-256-gcm', CHACHA20_POLY1305: 'chacha20-poly1305', - XCHACHA20_POLY1305: 'xchacha20-poly1305', + XCHACHA20_POLY1305: 'xchacha20-poly1305', // Not currently available, but we optimistically allocate for a future where it is }; const KD = { @@ -16,10 +16,32 @@ const KD = { BLAKE2B512: 'blake2b512', }; +/** + * @typedef {object} VersionParameters + * @property {number} version version number + * @property {string} algorithm encryption algorithm, from ALG + * @property {object} algOptions algorithm specific options + * @property {number=} algOptions.authTagLength length of authentication tag + * @property {number} versionBytes number of bytes used for version + * @property {number} flagsBytes number of bytes used for flags + * @property {number} ivBytes number of bytes used for IV + * @property {number} saltBytes number of bytes used for salt + * @property {number} tagBytes number of bytes used for tag + * @property {string} keyDeriver key derivation function, from KD + * @property {object} keyDeriverOptions options for key derivation function + * @property {number} keyBytes number of bytes used for key + */ + +/** + * @typedef {Record} Versions + * @property {Record} ALG encryption algorithms + * @property {Record} KD key derivation functions + */ /** * Supported packings/cipher types. * To be useful, any cipher included here must be Authenticated Encryption with Additional Data (AEAD). * More preferable versions are numbered higher. + * @type {Versions} */ const allVersions = { __proto__: null, @@ -54,7 +76,7 @@ const allVersions = { }, 3: { version: 3, - algorithm: ALG.XCHACHA20_POLY1305, // Not yet available, but would prefer even more... + algorithm: ALG.XCHACHA20_POLY1305, // Not yet available, but would prefer this, allocating for hopeful future use algOptions: { authTagLength: 16, }, @@ -66,6 +88,7 @@ const allVersions = { keyDeriver: KD.SCRYPT, keyBytes: 32, }, + 4: { version: 4, algorithm: ALG.AES_256_GCM, @@ -94,7 +117,7 @@ const allVersions = { }, 6: { version: 6, - algorithm: ALG.XCHACHA20_POLY1305, // Not yet available, but would prefer even more... + algorithm: ALG.XCHACHA20_POLY1305, algOptions: { authTagLength: 16, }, @@ -106,6 +129,7 @@ const allVersions = { keyDeriver: KD.SHAKE256, keyBytes: 32, }, + 7: { version: 7, algorithm: ALG.AES_256_GCM, @@ -134,7 +158,7 @@ const allVersions = { }, 9: { version: 9, - algorithm: ALG.XCHACHA20_POLY1305, // Not yet available, but would prefer even more... + algorithm: ALG.XCHACHA20_POLY1305, algOptions: { authTagLength: 16, }, @@ -177,7 +201,7 @@ const allVersions = { }, 12: { version: 12, - algorithm: ALG.XCHACHA20_POLY1305, // Not yet available, but would prefer even more... + algorithm: ALG.XCHACHA20_POLY1305, algOptions: { authTagLength: 16, }, @@ -189,6 +213,7 @@ const allVersions = { keyDeriver: KD.SCRYPT, keyBytes: 32, }, + 13: { version: 13, algorithm: ALG.AES_256_GCM, @@ -217,7 +242,7 @@ const allVersions = { }, 15: { version: 15, - algorithm: ALG.XCHACHA20_POLY1305, // Not yet available, but would prefer even more... + algorithm: ALG.XCHACHA20_POLY1305, algOptions: { authTagLength: 16, }, @@ -229,6 +254,7 @@ const allVersions = { keyDeriver: KD.SHAKE256, keyBytes: 32, }, + 16: { version: 16, algorithm: ALG.AES_256_GCM, @@ -257,7 +283,50 @@ const allVersions = { }, 18: { version: 18, - algorithm: ALG.XCHACHA20_POLY1305, // Not yet available, but would prefer even more... + algorithm: ALG.XCHACHA20_POLY1305, + algOptions: { + authTagLength: 16, + }, + versionBytes: 1, + flagsBytes: 1, + ivBytes: 24, + saltBytes: 16, + tagBytes: 16, + keyDeriver: KD.BLAKE2B512, + keyBytes: 32, + }, + + // Prior to version 19, blake2b512 used plain hash digest. + // Versions 19 and later use HKDF for blake2b512 key derivation. + 19: { + version: 19, + algorithm: ALG.AES_256_GCM, + algOptions: {}, + versionBytes: 1, + flagsBytes: 1, + ivBytes: 12, + saltBytes: 16, + tagBytes: 16, + keyDeriver: KD.BLAKE2B512, + keyBytes: 32, + }, + 20: { + version: 20, + algorithm: ALG.CHACHA20_POLY1305, + algOptions: { + authTagLength: 16, + }, + versionBytes: 1, + flagsBytes: 1, + ivBytes: 12, + saltBytes: 16, + tagBytes: 16, + keyDeriver: KD.BLAKE2B512, + keyBytes: 32, + }, + 21: { + version: 21, + algorithm: ALG.XCHACHA20_POLY1305, algOptions: { authTagLength: 16, }, @@ -269,6 +338,7 @@ const allVersions = { keyDeriver: KD.BLAKE2B512, keyBytes: 32, }, + }; Object.defineProperties(allVersions, { diff --git a/test/lib/mystery-box.js b/test/lib/mystery-box.js index c0b5d9a..865cec6 100644 --- a/test/lib/mystery-box.js +++ b/test/lib/mystery-box.js @@ -13,11 +13,12 @@ function _verbose(mb) { } describe('MysteryBox', function () { - let mb, options, object; + let mb, options, object, statsStub; beforeEach(function () { options = { encryptionSecret: 'this is not a very good secret', }; + statsStub = sinon.stub(); }); afterEach(function () { sinon.restore(); @@ -35,14 +36,16 @@ describe('MysteryBox', function () { encryptionSecret: ['first poor secret', 'second poor secret'], }; mb = new MysteryBox(options); + mb.on('statistics', statsStub); _verbose(mb); object = { foo: 'bar', baz: 'quux', flarp: 13, }; - const encryptedResult = await mb.pack(object); - const decryptedResult = await mb.unpack(encryptedResult); + const encryptedResult = await mb.pack(object, undefined, undefined, 'extra data'); + const decryptedResult = await mb.unpack(encryptedResult, 'extra data'); + assert.strictEqual(statsStub.callCount, 2); assert.deepStrictEqual(decryptedResult, object); }); @@ -80,7 +83,7 @@ describe('MysteryBox', function () { describe('_keyFromSecret', function () { it('covers invalid', async function () { - assert.rejects(() => MysteryBox._keyFromSecret('unknown deriver', 'secret', 'salt', 32), MysteryBoxError); + assert.rejects(() => mb._keyFromSecret('unknown deriver', 'secret', 'salt', 32), MysteryBoxError); }); }); // _keyFromSecret @@ -338,7 +341,7 @@ describe('MysteryBox', function () { it('encrypts and decrypts all available versions no compression', async function () { this.slow(Object.keys(mb.versionParameters).length * 200); const availableVersions = Object.keys(mb.versionParameters).map((v) => Number(v)); - for await (const version of availableVersions) { + for (const version of availableVersions) { object = { foo: 'bar', baz: 'quux', @@ -353,7 +356,7 @@ describe('MysteryBox', function () { it('encrypts and decrypts all available versions +brotli', async function () { this.slow(Object.keys(mb.versionParameters).length * 200); const availableVersions = Object.keys(mb.versionParameters).map((v) => Number(v)); - for await (const version of availableVersions) { + for (const version of availableVersions) { object = { foo: 'bar', baz: 'quux', @@ -368,7 +371,7 @@ describe('MysteryBox', function () { it('encrypts and decrypts all available versions +flate', async function () { this.slow(Object.keys(mb.versionParameters).length * 200); const availableVersions = Object.keys(mb.versionParameters).map((v) => Number(v)); - for await (const version of availableVersions) { + for (const version of availableVersions) { object = { foo: 'bar', baz: 'quux', @@ -437,4 +440,33 @@ describe('MysteryBox', function () { }); // pack, unpack + describe('sanity check', function () { + const encryptionSecret = 'jived fox nymph grabs quick waltz'; + const data = 'Jackdaws love my big sphinx of quartz.'; + const testVectors = { + '1': 'AQDrdybNo2AZM1YWGhWWMDp5V4ukBz_rm9PCSreBu3GkpLuegLWHZ4YKW2ObxA6QQowDixNdGcTzJLxFxyl5sojeZwM3FcXQlje920CqhVkaj5hs_xA', + '2': 'AgBRg3sswTRkYAP67KfbTvt14V2smjqT9E5i7exkiy3C8qv6LzXWahoPBKW1fOYTbT9n4nIwRjCmpZMiswWfiES2VqQHd8JPuQAvkfgGy2xoE2PmoxM', + '4': 'BADNJyfzTimCZm84ltW7Iipta4cBOPk3SnGaUmUD9sXralAFlYG_iSmZrEbqE7Ywc1XI5-CFn-yTsCd9-gNYisainPr1IBRErpyM_T5LE5tTxqGVmy8', + '5': 'BQBh2VkCGJflQcGCDDrKNmb2g8NszYawXYNKRoeyyPAXah0YV_h_2SPzQ11YswwGVR3zlbhlg-8x-bhbe_mDDbYwZs_PRq3YZ6OV2TOhR1y7XBODdDE', + '7': 'BwBKSO5LwBGUjPeD0m3tYe-L9U_4rwDAe6Tj6g02G9_2CnZ3dqRozDWi0ITD4YiDz3x1VQHMV7r6Zl_7SnBg-4D1UMt0dZyxGfL4XqQlglNrJazyFBQ', + '8': 'CACkXxst6JGfec1pdBW1N1z9DG9bbITMJ0ZO4u1nmfKuEwwyxewutucajVwirUThI4ZYjJMGfudnqwZAIs6FzoJ8XQna-IJw3bwORNXRliq4Lj3ZEM4', + '10': 'Ck8zfSTdZIpi7pucKuV4dt97zDz-blVk4IC3ou8dmvIHukVJwCepBPeByBalR2krxqDLHIfI86JJBauHIbkn6-20hGhV69DMaD11OahewlfqSEgdbjA', + '11': 'C0F3TPLnpGjtfA0XbDZ735Q9X5vw2P6Stcn44peZnBk8qzdxldGLTR5QBuRVOk7HWc8oWEdO9yc4wpULmfCvw5NeaA2dwAeZ-cgWnESvu5RSVUv3Ajs', + '13': 'DUxzFf148hjckC7uAcmOgRrGLV8N5Zku5afpggewFsSPDiKRzhE_rQoPTtjE6b42QgSag0RDu5wA-r9RobNg6CiCHeEaW_6krdjTkqNlxv_KO9rhUs8', + '14': 'Dparydjh1muUkxEDyywlAV2YIbaxLoJB2KZ8BqVubmPFQ4G9qtydpAcdD3eSzFHrU_npRN8UrLpLW13Bp56Pj70hbsRhw15ncxId104ZfiHZbmdWm2A', + '16': 'EN96MLAnjIdggkaBLUpdzLq2hArVtiLkkl8ih8UTrp-xGQlOQndnPMPUzlAbKGFKn2Rya0C7NmbFTKGWP5ZIwkt6ayT97aaZrp_K2XYSK4KvZnHp_CU', + '17': 'EUuC_-KVGtWqnV51NwOTPj-uRWoEMM_-YlqG7F9zblx2W3qFrKgEMzRpYNhyh_zqbCvuDR9CV9-b1Rj4OUPD1OtrHwcVEEpuiUcPVa2XA2ulupbPHdo', + '19': 'E_et0lNMOpI5k1FAI2LX85C5WbisN9cONnL99g-yDPm2IYGNZjQIygWxJ4gTQcxQGrfnce_JQjlY5nHs-OgJb2nunyqD2T9S46wghHU_9aBZ3p-U6pg', + '20': 'FFBZIC71_4Hn94rS-oJhOXA6y-yRFnW4ROtviSDYXLanaFEDrHthNFeglju0XnBEQLHB4kXXMsQYcwOn4Av88cfgEV7mQiLr5IAWavM0CQbkHsPYCk8', + }; + it('decrypts existing, known good, boxes', async function () { + this.slow(Object.keys(testVectors).length * 200); + mb = new MysteryBox({ encryptionSecret }); + for (const box of Object.values(testVectors)) { + const result = await mb.unpack(box); + assert.strictEqual(result, data); + } + }); + }); // sanity check + }); // MysteryBox \ No newline at end of file -- 2.49.1