## API
-- pack(contents, version, flags)
-- unpack(box)
+- `async pack(contents, version, flags)`
+- `async unpack(box)`
+
+## Example
+
+```js
+const { MysteryBox } = require('@squeep/mystery-box');
+const assert = require('assert');
+
+const mb = new MysteryBox(console, {
+ encryptionSecret: 'very secret',
+});
+
+(async () => {
+ const data = { important: 'to keep secret' };
+ const encrypted = await mb.pack(data);
+ const decrypted = await mb.unpack(encrypted);
+ assert.deepStrictEqual(decrypted, data);
+})()
+ .then(() => console.log('data retrieved!'));
+```
+
+## Details
+
+This relies on AEAD ciphers, such as `aes-256-gcm` and `chacha20-poly1305`, to encrypt the payload and authenticate the additional metadata (version identifier, flags indicating payload details, the iv of the cipher, and the salt used to create the key) needed to decrypt the payload.
+
+For each box, a new key is generated using the stored secret and a securely-random salt by way of a mechanism such as an XOF such as `shake256`, a hash such as `blake2b512`, or a more time-consuming multi-round hash such as `scrypt`. This key is used to encrypt and authenticate the data and metadata, which is then encoded as a base64url string.
// Filter any unavailable algorithms
const availableCiphers = crypto.getCiphers();
+ const availableHashes = crypto.getHashes();
+ // Add legacy scrypt to available hashes for filtering key derivations
+ availableHashes.push('scrypt');
this.versionParameters = Object.entries(allVersions).reduce((acc, [v, p]) => {
- if (availableCiphers.includes(p.algorithm)) {
+ const validCipher = availableCiphers.includes(p.algorithm);
+ const validKeyDeriver = availableHashes.includes(p.keyDeriver);
+ if (validCipher && validKeyDeriver) {
acc[v] = p; // eslint-disable-line security/detect-object-injection
}
return acc;
}
+ /**
+ * Generate key data.
+ */
+ static async _keyFromSecret(deriver, secret, salt, keyBytes) {
+ switch (deriver) {
+ case 'shake256': {
+ const hash = crypto.createHash('shake256', { outputLength: keyBytes });
+ hash.update(salt);
+ hash.update(secret);
+ return hash.digest();
+ }
+
+ case 'blake2b512': {
+ const hash = crypto.createHash('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);
+ }
+
+ case 'scrypt':
+ return scryptAsync(secret, salt, keyBytes);
+
+ default:
+ throw new RangeError('unsupported key deriver');
+ }
+ }
+
+
/**
* Put contents into a mysterious box.
* @param {Object|Buffer} contents
// Always encrypt with first secret
const secret = this.secrets[0];
- const key = await scryptAsync(secret, salt, v.keyBytes);
+ const key = await MysteryBox._keyFromSecret(v.keyDeriver, secret, salt, v.keyBytes);
const cipher = crypto.createCipheriv(v.algorithm, key, iv, v.algOptions);
cipher.setAAD(aadBuffer);
const encrypted = cipher.update(payload);
let err;
let success = false;
for await (const secret of this.secrets) {
- const key = await scryptAsync(secret, salt, v.keyBytes);
+ const key = await MysteryBox._keyFromSecret(v.keyDeriver, secret, salt, v.keyBytes);
const decipher = crypto.createDecipheriv(v.algorithm, key, iv, v.algOptions);
decipher.setAAD(aad);
decipher.setAuthTag(tag);
ivBytes: 12,
saltBytes: 16,
tagBytes: 16,
+ keyDeriver: 'scrypt',
keyBytes: 32,
},
2: {
ivBytes: 12,
saltBytes: 16,
tagBytes: 16,
+ keyDeriver: 'scrypt',
keyBytes: 32,
},
3: {
version: 3,
- algorithm: 'xchacha20-poly1305', // Not yet available...
+ algorithm: 'xchacha20-poly1305', // Not yet available, but would prefer even more...
algOptions: {
authTagLength: 16,
},
ivBytes: 24,
saltBytes: 16,
tagBytes: 16,
+ keyDeriver: 'scrypt',
+ keyBytes: 32,
+ },
+ 4: {
+ version: 4,
+ algorithm: 'aes-256-gcm',
+ algOptions: {},
+ versionBytes: 1,
+ flagsBytes: 1,
+ ivBytes: 12,
+ saltBytes: 16,
+ tagBytes: 16,
+ keyDeriver: 'shake256',
+ keyBytes: 32,
+ },
+ 5: {
+ version: 5,
+ algorithm: 'chacha20-poly1305',
+ algOptions: {
+ authTagLength: 16,
+ },
+ versionBytes: 1,
+ flagsBytes: 1,
+ ivBytes: 12,
+ saltBytes: 16,
+ tagBytes: 16,
+ keyDeriver: 'shake256',
+ keyBytes: 32,
+ },
+ 6: {
+ version: 6,
+ algorithm: 'xchacha20-poly1305', // Not yet available, but would prefer even more...
+ algOptions: {
+ authTagLength: 16,
+ },
+ versionBytes: 1,
+ flagsBytes: 1,
+ ivBytes: 24,
+ saltBytes: 16,
+ tagBytes: 16,
+ keyDeriver: 'shake256',
+ keyBytes: 32,
+ },
+ 7: {
+ version: 7,
+ algorithm: 'aes-256-gcm',
+ algOptions: {},
+ versionBytes: 1,
+ flagsBytes: 1,
+ ivBytes: 12,
+ saltBytes: 16,
+ tagBytes: 16,
+ keyDeriver: 'blake2b512',
+ keyBytes: 32,
+ },
+ 8: {
+ version: 8,
+ algorithm: 'chacha20-poly1305',
+ algOptions: {
+ authTagLength: 16,
+ },
+ versionBytes: 1,
+ flagsBytes: 1,
+ ivBytes: 12,
+ saltBytes: 16,
+ tagBytes: 16,
+ keyDeriver: 'blake2b512',
+ keyBytes: 32,
+ },
+ 9: {
+ version: 9,
+ algorithm: 'xchacha20-poly1305', // Not yet available, but would prefer even more...
+ algOptions: {
+ authTagLength: 16,
+ },
+ versionBytes: 1,
+ flagsBytes: 1,
+ ivBytes: 24,
+ saltBytes: 16,
+ tagBytes: 16,
+ keyDeriver: 'blake2b512',
keyBytes: 32,
},
};
const encryptedResult = await mb.pack(object);
const decryptedResult = await mb.unpack(encryptedResult);
assert.deepStrictEqual(decryptedResult, object);
-
});
it('covers options', function () {
});
}); // constructor
+ describe('_keyFromSecret', function () {
+ it('covers invalid', async function () {
+ try {
+ await MysteryBox._keyFromSecret('unknown deriver', 'secret', 'salt', 32);
+ } catch (e) {
+ assert(e instanceof RangeError);
+ }
+ });
+ }); // _keyFromSecret
+
describe('pack, unpack', function () {
beforeEach(function () {
mb = new MysteryBox(stubLogger, options);