Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for AES encryption zip file #696

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions lib/aes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"use strict";
var sjcl = require("./sjcl");
var utils = require("./utils");
var GenericWorker = require("./stream/GenericWorker");

var passwordVerifierLen = 2;
var authCodeLen = 10;

/**
* Create a worker that uses sjcl to process file data.
* @constructor
* @param dir The direction, 0 for decrypt and 1 for encrypt.
* @param {Object|bitArray} param the aesKey for decrypt or the options for encrypt.
*/
function AesWorker(dir, param) {
GenericWorker.call(this, "AesWorker");

this._aes = null;
this._aesKey = null;
this._mac = null;
this._dir = dir;

if (this._dir) {
this._password = param.password;
this._keyLen = this._macLen = 8 * param.strength + 8;
this._saltLen = this._keyLen /2;
} else {
this._aesKey = param;
}

// the `meta` object from the last chunk received
// this allow this worker to pass around metadata
this.meta = {};
}

utils.inherits(AesWorker, GenericWorker);

/**
* @see GenericWorker.processChunk
*/
AesWorker.prototype.processChunk = function (chunk) {
this.meta = chunk.meta;

if (this._aes === null) {
this._createAes();
}
var result = this._aes.update(sjcl.codec.bytes.toBits(chunk.data));
if (this._dir) {
this._mac.update(result);
}

this.push({
data : new Uint8Array(sjcl.codec.bytes.fromBits(result)),
meta : this.meta
});
};

/**
* @see GenericWorker.flush
*/
AesWorker.prototype.flush = function () {
GenericWorker.prototype.flush.call(this);

if (this._dir) {
if (this._aes === null) {
this._createAes();
}
var macData = this._mac.digest();
macData = sjcl.bitArray.clamp(macData, authCodeLen * 8);

this.push({
data : new Uint8Array(sjcl.codec.bytes.fromBits(macData)),
meta : {percent: 100}
});
}
};

/**
* @see GenericWorker.cleanUp
*/
AesWorker.prototype.cleanUp = function () {
GenericWorker.prototype.cleanUp.call(this);
this._aes = null;
this._aesKey = null;
this._mac = null;
};

/**
* Create the _aes object.
*/
AesWorker.prototype._createAes = function () {
if (this._dir) {
var salt = sjcl.random.randomWords(this._saltLen);
var derivedKey = sjcl.misc.pbkdf2(this._password, salt, 1000, (this._macLen + this._keyLen + passwordVerifierLen) * 8);
this._aesKey = sjcl.bitArray.bitSlice(derivedKey, 0, this._keyLen * 8);
var macKey = sjcl.bitArray.bitSlice(derivedKey, this._keyLen * 8, (this._keyLen + this._macLen) * 8);
var derivedPassVerifier = sjcl.bitArray.bitSlice(derivedKey, (this._keyLen + this._macLen) * 8);
this._mac = new sjcl.misc.hmac(macKey);

this.push({
data : new Uint8Array(sjcl.codec.bytes.fromBits(sjcl.bitArray.concat(salt, derivedPassVerifier))),
meta : {percent: 0}
});
}

this._aes = new sjcl.mode.ctrGladman(new sjcl.cipher.aes(this._aesKey), [0, 0, 0, 0]);
};

exports.EncryptWorker = function (options) {
return new AesWorker(1, options);
};

exports.DecryptWorker = function (key) {
return new AesWorker(0, key);
};

/**
* Verify the password of file using sjcl.
* @param {Uint8Array} data the data to verify.
* @param {Object} options the options when verifying.
* @return {Object} the aes key and encrypted file data.
*/
exports.verifyPassword = function (data, options) {
var password = options.password;
var keyLen = 8 * options.strength + 8;
var macLen = keyLen;
var saltLen = keyLen / 2;

var salt = sjcl.codec.bytes.toBits(data.subarray(0, saltLen));
var derivedKey = sjcl.misc.pbkdf2(password, salt, 1000, (macLen + keyLen + passwordVerifierLen) * 8);
var derivedPassVerifier = sjcl.bitArray.bitSlice(derivedKey, (keyLen + macLen) * 8);
var passVerifyValue = sjcl.codec.bytes.toBits(data.subarray(saltLen, saltLen + passwordVerifierLen));
if (!sjcl.bitArray.equal(passVerifyValue, derivedPassVerifier)) {
throw new Error("Encrypted zip: incorrect password");
}

return {
key: sjcl.bitArray.bitSlice(derivedKey, 0, keyLen * 8),
data: data.subarray(saltLen + passwordVerifierLen, -authCodeLen)
};
};
47 changes: 36 additions & 11 deletions lib/compressedObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var external = require("./external");
var DataWorker = require("./stream/DataWorker");
var Crc32Probe = require("./stream/Crc32Probe");
var DataLengthProbe = require("./stream/DataLengthProbe");
var aes = require("./aes");

/**
* Represent a compressed object, with everything needed to decompress it.
Expand All @@ -13,13 +14,15 @@ var DataLengthProbe = require("./stream/DataLengthProbe");
* @param {number} crc32 the crc32 of the decompressed file.
* @param {object} compression the type of compression, see lib/compressions.js.
* @param {String|ArrayBuffer|Uint8Array|Buffer} data the compressed data.
* @param {Object} decryptOptions the compressed object decrypt options.
*/
function CompressedObject(compressedSize, uncompressedSize, crc32, compression, data) {
function CompressedObject(compressedSize, uncompressedSize, crc32, compression, data, decryptOptions) {
this.compressedSize = compressedSize;
this.uncompressedSize = uncompressedSize;
this.crc32 = crc32;
this.compression = compression;
this.compressedContent = data;
this.decryptOptions = decryptOptions;
}

CompressedObject.prototype = {
Expand All @@ -28,9 +31,22 @@ CompressedObject.prototype = {
* @return {GenericWorker} the worker.
*/
getContentWorker: function () {
var worker = new DataWorker(external.Promise.resolve(this.compressedContent))
.pipe(this.compression.uncompressWorker())
.pipe(new DataLengthProbe("data_length"));
var worker;
if (this.decryptOptions.strength) {
if (!(this.decryptOptions.password && typeof this.decryptOptions.password === "string" )) {
throw new Error("Encrypted zip: need password");
}
var result = aes.verifyPassword(this.compressedContent, this.decryptOptions);

worker = new DataWorker(external.Promise.resolve(result.data))
.pipe(aes.DecryptWorker(result.key))
.pipe(this.compression.uncompressWorker())
.pipe(new DataLengthProbe("data_length"));
} else {
worker = new DataWorker(external.Promise.resolve(this.compressedContent))
.pipe(this.compression.uncompressWorker())
.pipe(new DataLengthProbe("data_length"));
}

var that = this;
worker.on("end", function () {
Expand Down Expand Up @@ -62,13 +78,22 @@ CompressedObject.prototype = {
* @param {Object} compressionOptions the options to use when compressing.
* @return {GenericWorker} the new worker compressing the content.
*/
CompressedObject.createWorkerFrom = function (uncompressedWorker, compression, compressionOptions) {
return uncompressedWorker
.pipe(new Crc32Probe())
.pipe(new DataLengthProbe("uncompressedSize"))
.pipe(compression.compressWorker(compressionOptions))
.pipe(new DataLengthProbe("compressedSize"))
.withStreamInfo("compression", compression);
CompressedObject.createWorkerFrom = function (uncompressedWorker, compression, compressionOptions, encryptOptions) {
if (encryptOptions.password !== null) {
return uncompressedWorker
.pipe(new DataLengthProbe("uncompressedSize"))
.pipe(compression.compressWorker(compressionOptions))
.pipe(aes.EncryptWorker(encryptOptions))
.pipe(new DataLengthProbe("compressedSize"))
.withStreamInfo("compression", compression);
} else {
return uncompressedWorker
.pipe(new Crc32Probe())
.pipe(new DataLengthProbe("uncompressedSize"))
.pipe(compression.compressWorker(compressionOptions))
.pipe(new DataLengthProbe("compressedSize"))
.withStreamInfo("compression", compression);
}
};

module.exports = CompressedObject;
38 changes: 29 additions & 9 deletions lib/generate/ZipFileWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off
unicodePathExtraField = "",
unicodeCommentExtraField = "",
dir = file.dir,
date = file.date;
date = file.date,
encryptOptions = streamInfo.file.encryptOptions,
isEncrypt = encryptOptions.password !== null;


var dataInfo = {
Expand All @@ -112,6 +114,9 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off
}

var bitflag = 0;
if (isEncrypt) {
bitflag |= 0x0001;
}
if (streamedContent) {
// Bit 3: the sizes/crc32 are set to zero in the local header.
// The correct values are put in the data descriptor immediately
Expand All @@ -130,7 +135,7 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off
// dos or unix, we set the dos dir flag
extFileAttr |= 0x00010;
}
if(platform === "UNIX") {
if (platform === "UNIX") {
versionMadeBy = 0x031E; // UNIX, version 3.0
extFileAttr |= generateUnixExternalFileAttr(file.unixPermissions, dir);
} else { // DOS or other, fallback to DOS
Expand Down Expand Up @@ -182,7 +187,7 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off
unicodePathExtraField;
}

if(useUTF8ForComment) {
if (useUTF8ForComment) {

unicodeCommentExtraField =
// Version
Expand All @@ -201,14 +206,31 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off
unicodeCommentExtraField;
}

if (isEncrypt) {
extraFields += "\x01" + String.fromCharCode(0x99);
extraFields += "\x07\x00";
extraFields += "\x02\x00";
extraFields += "AE";
extraFields += String.fromCharCode(encryptOptions.strength);
extraFields += compression.magic;
}

var header = "";

// version needed to extract
header += "\x0A\x00";
if (isEncrypt) {
header += "\x33\x00";
} else {
header += "\x0A\x00";
}
// general purpose bit flag
header += decToHex(bitflag, 2);
// compression method
header += compression.magic;
if (isEncrypt) {
header += "\x63\x00";
} else {
header += compression.magic;
}
// last mod file time
header += decToHex(dosTime, 2);
// last mod file date
Expand Down Expand Up @@ -347,8 +369,6 @@ function ZipFileWorker(streamFiles, comment, platform, encodeFileName) {
// Used for the emitted metadata.
this.currentFile = null;



this._sources = [];
}
utils.inherits(ZipFileWorker, GenericWorker);
Expand All @@ -362,7 +382,7 @@ ZipFileWorker.prototype.push = function (chunk) {
var entriesCount = this.entriesCount;
var remainingFiles = this._sources.length;

if(this.accumulate) {
if (this.accumulate) {
this.contentBuffer.push(chunk);
} else {
this.bytesWritten += chunk.data.length;
Expand Down Expand Up @@ -410,7 +430,7 @@ ZipFileWorker.prototype.closedSource = function (streamInfo) {
var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName);

this.dirRecords.push(record.dirRecord);
if(streamedContent) {
if (streamedContent) {
// after the streamed file, we put data descriptors
this.push({
data : generateDataDescriptors(streamInfo),
Expand Down
19 changes: 16 additions & 3 deletions lib/generate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

var compressions = require("../compressions");
var ZipFileWorker = require("./ZipFileWorker");
var utils = require("../utils");

/**
* Find the compression to use.
Expand All @@ -26,7 +27,10 @@ var getCompression = function (fileCompression, zipCompression) {
* @param {String} comment the comment to use.
*/
exports.generateWorker = function (zip, options, comment) {

var encryptOptions = {
password: options.password,
strength: options.encryptStrength
};
var zipFileWorker = new ZipFileWorker(options.streamFiles, comment, options.platform, options.encodeFileName);
var entriesCount = 0;
try {
Expand All @@ -36,15 +40,24 @@ exports.generateWorker = function (zip, options, comment) {
var compression = getCompression(file.options.compression, options.compression);
var compressionOptions = file.options.compressionOptions || options.compressionOptions || {};
var dir = file.dir, date = file.date;
var fileEncryptOptions = utils.extend(file.encryptOptions || {}, encryptOptions);
if (!fileEncryptOptions.password) {
fileEncryptOptions.password = null;
} else if (typeof fileEncryptOptions.password !== "string") {
throw new Error("Password is not a valid string.");
}else{
fileEncryptOptions.strength = fileEncryptOptions.strength || 3;
}

file._compressWorker(compression, compressionOptions)
file._compressWorker(compression, compressionOptions, fileEncryptOptions)
.withStreamInfo("file", {
name : relativePath,
dir : dir,
date : date,
comment : file.comment || "",
unixPermissions : file.unixPermissions,
dosPermissions : file.dosPermissions
dosPermissions : file.dosPermissions,
encryptOptions : fileEncryptOptions
})
.pipe(zipFileWorker);
});
Expand Down
5 changes: 5 additions & 0 deletions lib/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ var nodejsUtils = require("./nodejsUtils");
*/
function checkEntryCRC32(zipEntry) {
return new external.Promise(function (resolve, reject) {
if (zipEntry.options.aes.version === 2) {
reject(new Error("Encrypted zip : no CRC32 stored"));
return;
}
var worker = zipEntry.decompressed.getContentWorker().pipe(new Crc32Probe());
worker.on("error", function (e) {
reject(e);
Expand All @@ -32,6 +36,7 @@ module.exports = function (data, options) {
var zip = this;
options = utils.extend(options || {}, {
base64: false,
password: null,
checkCRC32: false,
optimizedBinaryString: false,
createFolders: false,
Expand Down
Loading