UNPKG

node-aescrypt

Version:

A node implementation of the AES Crypt <https://www.aescrypt.com/> file encryption format.

207 lines 16.4 kB
import { createDecipheriv } from 'crypto'; import { Transform } from 'stream'; import { AESCRYPT_FILE_FORMAT_VERSION, getHMAC, getKey, toStream, withStream, } from './util'; /** * Decrypt a Buffer that is in the AES Crypt file format. * * Create a stream transformer that takes [Readable stream](https://nodejs.org/api/stream.html) * that is encrypted in the * [AES Crypt file format](https://www.aescrypt.com/aes_file_format.html) and * decrypts it passing it on as a Readable stream. */ export class Decrypt extends Transform { constructor(password, options) { super(options); this.password = password; this.decipher = null; this.hmac = null; this.mode = 0; this.buffer = Buffer.alloc(0); } static get MODE_FILE_HEADER() { return 0; } static get MODE_EXTESIONS() { return 1; } static get MODE_CREDENTIALS() { return 2; } static get MODE_DECRYPT() { return 3; } // Create a small helper static method if you just want to decrypt a whole // Buffer all at once. static buffer(password, buffer) { return new Promise((resolve, reject) => { toStream(buffer) .pipe(new Decrypt(password)) .pipe(withStream(contents => { resolve(contents); })) .on('error', reject); }); } _transform(chunk, _, callback) { this.buffer = Buffer.concat([this.buffer, chunk]); let error = null; // Move through the various sections of the file format and raise an error // If anything is malformed. if (this.mode === Decrypt.MODE_FILE_HEADER) { error = this._modeFileHeader(); } if (!error && this.mode === Decrypt.MODE_EXTESIONS) { error = this._modeExtensions(); } if (!error && this.mode === Decrypt.MODE_CREDENTIALS) { error = this._modeCredentials(); } // Finally ready to decrypt the contents. if (!error && this.mode === Decrypt.MODE_DECRYPT) { // We need to reserve 33 bytes (+ 16 for the padding) of the end for the file-size-modulo-16 and HMAC. if (this.buffer.length > 49) { const encChunk = this.buffer.slice(0, -49); // This is unnecessary, but makes tslint keep quiet. if (this.decipher == null) { return; } if (this.hmac == null) { return; } this.hmac.update(encChunk); this.push(this.decipher.update(encChunk)); this.buffer = this.buffer.slice(-49); } } if (error) { callback(error); } else { callback(); } } _flush(callback) { let error = null; // If we never got to the decryption mode, something went terribly wrong. // Most likely, there is a problem in the extensions, and we never found // the end. if (this.mode !== Decrypt.MODE_DECRYPT) { error = new Error('Error: Message has been altered or password is incorrect'); } else { // We are at the end of the file. Let's hash that last remaining cipher text // and check the HMAC. const encChunk = this.buffer.slice(0, 16); const lenMod16 = this.buffer.readUInt8(16); const encHMACActual = this.buffer.slice(17); // This is unnecessary, but makes tslint keep quiet. if (this.decipher == null) { return; } if (this.hmac == null) { return; } this.hmac.update(encChunk); const encHMACExpected = this.hmac.digest(); // Validately the integrity of the cipher text. if (encHMACExpected.compare(encHMACActual) !== 0) { error = new Error('Error: Message has been altered or password is incorrect'); } // Validate the padding length (or more accurately, the length of the last block minus the padding). else if (lenMod16 > 16) { error = new Error('Error: Message has been altered or password is incorrect'); } else { // Decrypt the last block and send it on its way. const decChunk = Buffer.concat([ this.decipher.update(encChunk), this.decipher.final(), ]).slice(0, lenMod16); this.push(decChunk); } } if (error) { callback(error); } else { callback(); } } _modeFileHeader() { if (this.buffer.length >= 5) { const type = this.buffer.slice(0, 3).toString(); const version = this.buffer.readUInt8(3); if (type !== 'AES') { return new Error('Error: Bad file header (not aescrypt file or is corrupted?'); } // We only understand the version 2 of the AES Crypt file format as described // at https://www.aescrypt.com/aes_file_format.html. if (version !== AESCRYPT_FILE_FORMAT_VERSION) { return new Error('Error: Unsupported AES file version'); } this.buffer = this.buffer.slice(5); this.mode = Decrypt.MODE_EXTESIONS; } return null; } _modeExtensions() { let i = 0; // Search through the buffer to the length. // If we can't find the end of the extensions in the current buffer, let it // buffer a little more. while (i + 1 < this.buffer.length) { const extLen = this.buffer.readUInt16BE(i); i += 2; // If this extension has a length, fast-forward past it. if (extLen > 0) { i += extLen; } // If this is a zero length extension, we are done. else { this.buffer = this.buffer.slice(i); this.mode = Decrypt.MODE_CREDENTIALS; break; } } return null; } _modeCredentials() { if (this.buffer.length >= 96) { const credIV = this.buffer.slice(0, 16); const credKey = getKey(credIV, this.password); const credDecipher = this._getDecipher(credKey, credIV); credDecipher.setAutoPadding(false); const credBlock = this.buffer.slice(16, 64); const credHMACActual = this.buffer.slice(64, 96); const credHMACExpected = getHMAC(credKey) .update(credBlock) .digest(); // First we check the HMAC signature of the encrypted credentials block. // This ensures nothing was tampered with. It also has the added benefit // of checking the password early on in the decryption process. if (credHMACExpected.compare(credHMACActual) !== 0) { return new Error('Error: Message has been altered or password is incorrect'); } // Decrypt the credentials we need for the rest of the contents. const decryptedCredBlock = Buffer.concat([ credDecipher.update(credBlock), credDecipher.final(), ]); const encIV = decryptedCredBlock.slice(0, 16); const encKey = decryptedCredBlock.slice(16, 48); // Create our main workhorses using the decrypted credentials. this.decipher = this._getDecipher(encKey, encIV); this.hmac = getHMAC(encKey); delete this.password; // Don't need this anymore. this.buffer = this.buffer.slice(96); this.mode = Decrypt.MODE_DECRYPT; } return null; } _getDecipher(key, iv) { const encDecipher = createDecipheriv('aes-256-cbc', key, iv); encDecipher.setAutoPadding(false); return encDecipher; } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVjcnlwdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9saWIvZGVjcnlwdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsZ0JBQWdCLEVBQWtCLE1BQU0sUUFBUSxDQUFDO0FBQzFELE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxRQUFRLENBQUM7QUFDbkMsT0FBTyxFQUNMLDRCQUE0QixFQUM1QixPQUFPLEVBQ1AsTUFBTSxFQUNOLFFBQVEsRUFFUixVQUFVLEdBQ1gsTUFBTSxRQUFRLENBQUM7QUFFaEI7Ozs7Ozs7R0FPRztBQUNILE1BQU0sT0FBTyxPQUFRLFNBQVEsU0FBUztJQWtDcEMsWUFBWSxRQUFnQixFQUFFLE9BQWE7UUFDekMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ2YsSUFBSSxDQUFDLFFBQVEsR0FBRyxRQUFRLENBQUM7UUFDekIsSUFBSSxDQUFDLFFBQVEsR0FBRyxJQUFJLENBQUM7UUFDckIsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7UUFDakIsSUFBSSxDQUFDLElBQUksR0FBRyxDQUFDLENBQUM7UUFDZCxJQUFJLENBQUMsTUFBTSxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDaEMsQ0FBQztJQXhDRCxNQUFNLEtBQUssZ0JBQWdCO1FBQ3pCLE9BQU8sQ0FBQyxDQUFDO0lBQ1gsQ0FBQztJQUNELE1BQU0sS0FBSyxjQUFjO1FBQ3ZCLE9BQU8sQ0FBQyxDQUFDO0lBQ1gsQ0FBQztJQUNELE1BQU0sS0FBSyxnQkFBZ0I7UUFDekIsT0FBTyxDQUFDLENBQUM7SUFDWCxDQUFDO0lBQ0QsTUFBTSxLQUFLLFlBQVk7UUFDckIsT0FBTyxDQUFDLENBQUM7SUFDWCxDQUFDO0lBQ0QsMEVBQTBFO0lBQzFFLHNCQUFzQjtJQUNmLE1BQU0sQ0FBQyxNQUFNLENBQUMsUUFBZ0IsRUFBRSxNQUFjO1FBQ25ELE9BQU8sSUFBSSxPQUFPLENB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