archiver-zip-encrypted
Version:
AES-256 and legacy Zip 2.0 encryption for Zip files
105 lines (84 loc) • 3.12 kB
JavaScript
'use strict';
const inherits = require('util').inherits;
const {Transform} = require('stream');
const crypto = require('crypto');
const aes = require('aes-js');
const SALT_LENGTH = 16;
const KEY_LENGTH = 32;
const COMPOSITE_KEY_LENGTH = 2 * KEY_LENGTH + 2;
const PBKDF2_ITERATION_COUNT = 1000;
/**
* Override increment to behave like in Zip implementation
*/
aes.Counter.prototype.increment = function() {
for (var i = 0; i < 16; i++) {
if (this._counter[i] === 255) {
this._counter[i] = 0;
} else {
this._counter[i]++;
break;
}
}
};
/**
* Transform stream that encodes data with AES stream and prepends additional data for authentication and verification:
* ${salt}${passwordVerifier}${encodedData}${hmac}
*/
function AesHmacEtmStream(options = {key: null, salt: null, transformOptions: {}}) {
if (!(this instanceof AesHmacEtmStream)) {
return new AesHmacEtmStream(options);
}
Transform.call(this, options);
this.salt = options.salt;
if (!this.salt) {
this.salt = crypto.randomBytes(SALT_LENGTH);
}
this.key = this.deriveKey(this.salt, options.key);
// apparently WinZip's AES-CTR implementation uses nonce with 1st byte set to 1 and zeroes to others
this.cipher = new aes.ModeOfOperation.ctr(new Uint8Array(this.key.aesKey),
new aes.Counter(new Uint8Array(Buffer.from('01000000000000000000000000000000', 'hex'))));
this.hmac = crypto.createHmac('sha1', this.key.hmacKey);
this.passwordVerifier = this.key.passwordVerifier;
this.headerSent = false;
this.totalSize = 0;
}
inherits(AesHmacEtmStream, Transform);
AesHmacEtmStream.prototype.deriveKey = function (salt, key) {
const compositeKey = crypto.pbkdf2Sync(key, salt, PBKDF2_ITERATION_COUNT, COMPOSITE_KEY_LENGTH, 'sha1');
return {
aesKey: compositeKey.slice(0, KEY_LENGTH),
hmacKey: compositeKey.slice(KEY_LENGTH, 2 * KEY_LENGTH),
passwordVerifier: compositeKey.slice(2 * KEY_LENGTH)
};
};
AesHmacEtmStream.prototype.getTotalSize = function () {
return this.totalSize;
};
AesHmacEtmStream.prototype.ensureHeaderSent = function() {
if (!this.headerSent) {
this.push(this.salt);
this.totalSize += this.salt.length;
this.push(this.passwordVerifier);
this.totalSize += this.passwordVerifier.length;
this.headerSent = true;
}
};
AesHmacEtmStream.prototype._transform = function (data, encoding, cb) {
this.ensureHeaderSent();
let encoded = this.cipher.encrypt(new Uint8Array(data));
if (encoded.length > 0) {
this.hmac.update(encoded);
this.push(Buffer.from(encoded)); // TODO check for return value?
this.totalSize += encoded.length;
}
cb();
};
AesHmacEtmStream.prototype._flush = function (cb) {
// _transform is not called for non-file entries, such as directories
this.ensureHeaderSent();
let hmacData = this.hmac.digest().slice(0, 10);
this.push(hmacData);
this.totalSize += hmacData.length;
cb();
};
module.exports = AesHmacEtmStream;