'use strict'; // Refs: https://github.com/nodejs/node/issues/31733 const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const stream = require('stream'); const tmpdir = require('../common/tmpdir'); class Sink extends stream.Writable { constructor() { super(); this.chunks = []; } _write(chunk, encoding, cb) { this.chunks.push(chunk); cb(); } } function direct(config) { const { cipher, key, iv, aad, authTagLength, plaintextLength } = config; const expected = Buffer.alloc(plaintextLength); const c = crypto.createCipheriv(cipher, key, iv, { authTagLength }); c.setAAD(aad, { plaintextLength }); const ciphertext = Buffer.concat([c.update(expected), c.final()]); const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength }); d.setAAD(aad, { plaintextLength }); d.setAuthTag(c.getAuthTag()); const actual = Buffer.concat([d.update(ciphertext), d.final()]); assert.deepStrictEqual(expected, actual); } function mstream(config) { const { cipher, key, iv, aad, authTagLength, plaintextLength } = config; const expected = Buffer.alloc(plaintextLength); const c = crypto.createCipheriv(cipher, key, iv, { authTagLength }); c.setAAD(aad, { plaintextLength }); const plain = new stream.PassThrough(); const crypt = new Sink(); const chunks = crypt.chunks; plain.pipe(c).pipe(crypt); plain.end(expected); crypt.on('close', common.mustCall(() => { const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength }); d.setAAD(aad, { plaintextLength }); d.setAuthTag(c.getAuthTag()); const crypt = new stream.PassThrough(); const plain = new Sink(); crypt.pipe(d).pipe(plain); for (const chunk of chunks) crypt.write(chunk); crypt.end(); plain.on('close', common.mustCall(() => { const actual = Buffer.concat(plain.chunks); assert.deepStrictEqual(expected, actual); })); })); } function fstream(config) { const count = fstream.count++; const filename = (name) => path.join(tmpdir.path, `${name}${count}`); const { cipher, key, iv, aad, authTagLength, plaintextLength } = config; const expected = Buffer.alloc(plaintextLength); fs.writeFileSync(filename('a'), expected); const c = crypto.createCipheriv(cipher, key, iv, { authTagLength }); c.setAAD(aad, { plaintextLength }); const plain = fs.createReadStream(filename('a')); const crypt = fs.createWriteStream(filename('b')); plain.pipe(c).pipe(crypt); // Observation: 'close' comes before 'end' on |c|, which definitely feels // wrong. Switching to `c.on('end', ...)` doesn't fix the test though. crypt.on('close', common.mustCall(() => { // Just to drive home the point that decryption does actually work: // reading the file synchronously, then decrypting it, works. { const ciphertext = fs.readFileSync(filename('b')); const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength }); d.setAAD(aad, { plaintextLength }); d.setAuthTag(c.getAuthTag()); const actual = Buffer.concat([d.update(ciphertext), d.final()]); assert.deepStrictEqual(expected, actual); } const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength }); d.setAAD(aad, { plaintextLength }); d.setAuthTag(c.getAuthTag()); const crypt = fs.createReadStream(filename('b')); const plain = fs.createWriteStream(filename('c')); crypt.pipe(d).pipe(plain); plain.on('close', common.mustCall(() => { const actual = fs.readFileSync(filename('c')); assert.deepStrictEqual(expected, actual); })); })); } fstream.count = 0; function test(config) { direct(config); mstream(config); fstream(config); } tmpdir.refresh(); test({ cipher: 'aes-128-ccm', aad: Buffer.alloc(1), iv: Buffer.alloc(8), key: Buffer.alloc(16), authTagLength: 16, plaintextLength: 32768, }); test({ cipher: 'aes-128-ccm', aad: Buffer.alloc(1), iv: Buffer.alloc(8), key: Buffer.alloc(16), authTagLength: 16, plaintextLength: 32769, });