From b5c4a8d7396880e1ccdc9814df56959e34066bb9 Mon Sep 17 00:00:00 2001 From: xqdoo00o Date: Wed, 11 Mar 2020 22:03:24 +0800 Subject: [PATCH 01/18] Add support for AES decryption --- lib/aes.js | 70 +++++++++++++++++++++++++++++++++++++++++++++++ lib/load.js | 1 + lib/zipEntries.js | 4 ++- lib/zipEntry.js | 35 +++++++++++++++++------- 4 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 lib/aes.js diff --git a/lib/aes.js b/lib/aes.js new file mode 100644 index 00000000..51e9e4aa --- /dev/null +++ b/lib/aes.js @@ -0,0 +1,70 @@ +'use strict'; +var CryptoJS = require('crypto-js'); +var AesKeyStrength = { + 1:{ + saltLength: 8, + macLength: 16, + keyLength: 16 + }, + 2:{ + saltLength: 12, + macLength: 24, + keyLength: 24 + }, + 3:{ + saltLength: 16, + macLength: 32, + keyLength: 32 + } +}; +var AesObject = function(data, password, options) { + this.passwordVerifyLen = 2; + this.data = data; + this.version = options.version; + this.strength = options.strength; + this.password = password; + this.saltLength = AesKeyStrength[this.strength].saltLength; + this.macLength = AesKeyStrength[this.strength].macLength; + this.keyLength = AesKeyStrength[this.strength].keyLength; +}; +AesObject.prototype ={ + decrypt:function(){ + var salt = CryptoJS.lib.WordArray.create(this.data.slice(0,this.saltLength)); + var passVerifyValue = CryptoJS.lib.WordArray.create(this.data.slice(this.saltLength,this.saltLength + this.passwordVerifyLen)).toString(CryptoJS.enc.Hex); + var encryptedData = CryptoJS.lib.WordArray.create(this.data.slice(this.saltLength + this.passwordVerifyLen, -10)); + var macData = CryptoJS.lib.WordArray.create(this.data.slice(-10)).toString(CryptoJS.enc.Hex); + var derivedKey = CryptoJS.PBKDF2(this.password, salt,{ + keySize: (this.macLength + this.keyLength + this.passwordVerifyLen) / 4, + iterations: 1000 + }).toString(); + var aesKey = CryptoJS.enc.Hex.parse(derivedKey.slice(0,this.keyLength * 2)); + var macKey = CryptoJS.enc.Hex.parse(derivedKey.slice(this.keyLength * 2, (this.keyLength + this.macLength) * 2)); + var derivedPasswordVerifier = derivedKey.slice(-this.passwordVerifyLen * 2); + + if(passVerifyValue !== derivedPasswordVerifier){ + throw new Error("Wrong Password!"); + } + + // if AE-2 format check mac + if(this.version === 2){ + var macDataVerifier = CryptoJS.HmacSHA1(encryptedData,macKey).toString().slice(0,20); + if(macData !== macDataVerifier){ + throw new Error("Corrupted encrypted data!"); + } + } + + var decryptData = CryptoJS.AES.decrypt({ + ciphertext: encryptedData + },aesKey,{ + iv: CryptoJS.enc.Hex.parse("0"), + mode: CryptoJS.mode.CTRGladman, + padding: CryptoJS.pad.NoPadding + }).toString(); + + return new Uint8Array(decryptData.match(/.{1,2}/g).map(function(b){ + return parseInt(b, 16); + })); + } +}; + +module.exports = AesObject; \ No newline at end of file diff --git a/lib/load.js b/lib/load.js index 53cae42a..0c2e3a8e 100644 --- a/lib/load.js +++ b/lib/load.js @@ -32,6 +32,7 @@ function checkEntryCRC32(zipEntry) { module.exports = function(data, options) { var zip = this; options = utils.extend(options || {}, { + password: "", base64: false, checkCRC32: false, optimizedBinaryString: false, diff --git a/lib/zipEntries.js b/lib/zipEntries.js index 2e3aba90..35a03f5b 100644 --- a/lib/zipEntries.js +++ b/lib/zipEntries.js @@ -131,9 +131,11 @@ ZipEntries.prototype = { var file; this.reader.setIndex(this.centralDirOffset); + this.aes={}; while (this.reader.readAndCheckSignature(sig.CENTRAL_FILE_HEADER)) { file = new ZipEntry({ - zip64: this.zip64 + zip64: this.zip64, + aes: this.aes }, this.loadOptions); file.readCentralPart(this.reader); this.files.push(file); diff --git a/lib/zipEntry.js b/lib/zipEntry.js index 39afdbd3..de32b26d 100644 --- a/lib/zipEntry.js +++ b/lib/zipEntry.js @@ -6,6 +6,7 @@ var crc32fn = require('./crc32'); var utf8 = require('./utf8'); var compressions = require('./compressions'); var support = require('./support'); +var AesObject = require('./aes'); var MADE_BY_DOS = 0x00; var MADE_BY_UNIX = 0x03; @@ -93,7 +94,13 @@ ZipEntry.prototype = { if (compression === null) { // no compression found throw new Error("Corrupted zip : compression " + utils.pretty(this.compressionMethod) + " unknown (inner file : " + utils.transformTo("string", this.fileName) + ")"); } - this.decompressed = new CompressedObject(this.compressedSize, this.uncompressedSize, this.crc32, compression, reader.readData(this.compressedSize)); + if(this.isEncrypted()){ + this.aesObj = new AesObject(reader.readData(this.compressedSize), this.loadOptions.password, this.options.aes); + var decryptData = this.aesObj.decrypt(); + this.decompressed = new CompressedObject(decryptData.length, this.uncompressedSize, this.crc32, compression, decryptData); + }else{ + this.decompressed = new CompressedObject(this.compressedSize, this.uncompressedSize, this.crc32, compression, reader.readData(this.compressedSize)); + } }, /** @@ -118,9 +125,9 @@ ZipEntry.prototype = { this.externalFileAttributes = reader.readInt(4); this.localHeaderOffset = reader.readInt(4); - if (this.isEncrypted()) { - throw new Error("Encrypted zip are not supported"); - } + // if (this.isEncrypted()) { + // throw new Error("Encrypted zip are not supported"); + // } // will be read in the local part, see the comments there reader.skip(fileNameLength); @@ -203,13 +210,21 @@ ZipEntry.prototype = { while (reader.index < end) { extraFieldId = reader.readInt(2); extraFieldLength = reader.readInt(2); - extraFieldValue = reader.readData(extraFieldLength); - this.extraFields[extraFieldId] = { - id: extraFieldId, - length: extraFieldLength, - value: extraFieldValue - }; + // AES encrypt extra field header id + if(extraFieldId === 0x9901){ + this.options.aes.version = reader.readInt(2); + reader.skip(2); + this.options.aes.strength = reader.readInt(1); + this.compressionMethod = this.options.aes.compressionMethod = reader.readString(2); + }else{ + extraFieldValue = reader.readData(extraFieldLength); + this.extraFields[extraFieldId] = { + id: extraFieldId, + length: extraFieldLength, + value: extraFieldValue + }; + } } }, /** From 2c4674fe33c62380805d510d5ca0085bbd70f43c Mon Sep 17 00:00:00 2001 From: David xu Date: Wed, 11 Mar 2020 22:05:19 +0800 Subject: [PATCH 02/18] Update package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index de104997..15edf1a2 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "tmp": "0.0.28" }, "dependencies": { + "crypto-js": "^4.0.0", "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", From 1475d39d3ac6c9afac4babdeae7756b1124e809b Mon Sep 17 00:00:00 2001 From: David xu Date: Thu, 12 Mar 2020 15:40:36 +0800 Subject: [PATCH 03/18] Only require needed library module --- lib/aes.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/aes.js b/lib/aes.js index 51e9e4aa..8cb202bf 100644 --- a/lib/aes.js +++ b/lib/aes.js @@ -1,5 +1,11 @@ 'use strict'; -var CryptoJS = require('crypto-js'); +var CryptoJS = require('crypto-js/core'); +require('crypto-js/lib-typedarrays'); +require('crypto-js/aes'); +require('crypto-js/pbkdf2'); +require('crypto-js/hmac-sha1'); +require('crypto-js/mode-ctr-gladman'); +require('crypto-js/pad-nopadding'); var AesKeyStrength = { 1:{ saltLength: 8, @@ -67,4 +73,4 @@ AesObject.prototype ={ } }; -module.exports = AesObject; \ No newline at end of file +module.exports = AesObject; From b5bab2a5cc9f1f5b4d1296d5e93a986220ed0ab8 Mon Sep 17 00:00:00 2001 From: David xu Date: Tue, 31 Mar 2020 09:57:38 +0800 Subject: [PATCH 04/18] update algorithm --- lib/aes.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/aes.js b/lib/aes.js index 8cb202bf..40e7e429 100644 --- a/lib/aes.js +++ b/lib/aes.js @@ -34,7 +34,29 @@ var AesObject = function(data, password, options) { this.keyLength = AesKeyStrength[this.strength].keyLength; }; AesObject.prototype ={ - decrypt:function(){ + wordArrayToUint8Array: function(wordArray) { + var l = wordArray.sigBytes; + var words = wordArray.words; + var result = new Uint8Array(l); + var i=0, j=0; + while(true) { + if (i==l) + break; + var w = words[j++]; + result[i++] = (w & 0xff000000) >>> 24; + if (i==l) + break; + result[i++] = (w & 0x00ff0000) >>> 16; + if (i==l) + break; + result[i++] = (w & 0x0000ff00) >>> 8; + if (i==l) + break; + result[i++] = (w & 0x000000ff); + } + return result + }, + decrypt: function(){ var salt = CryptoJS.lib.WordArray.create(this.data.slice(0,this.saltLength)); var passVerifyValue = CryptoJS.lib.WordArray.create(this.data.slice(this.saltLength,this.saltLength + this.passwordVerifyLen)).toString(CryptoJS.enc.Hex); var encryptedData = CryptoJS.lib.WordArray.create(this.data.slice(this.saltLength + this.passwordVerifyLen, -10)); @@ -65,11 +87,9 @@ AesObject.prototype ={ iv: CryptoJS.enc.Hex.parse("0"), mode: CryptoJS.mode.CTRGladman, padding: CryptoJS.pad.NoPadding - }).toString(); + }) - return new Uint8Array(decryptData.match(/.{1,2}/g).map(function(b){ - return parseInt(b, 16); - })); + return this.wordArrayToUint8Array(decryptData) } }; From 307df73bb0f824847e3f439670b2012d4a644abc Mon Sep 17 00:00:00 2001 From: David xu Date: Tue, 31 Mar 2020 10:18:28 +0800 Subject: [PATCH 05/18] fix format --- lib/aes.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/aes.js b/lib/aes.js index 40e7e429..a46db65c 100644 --- a/lib/aes.js +++ b/lib/aes.js @@ -40,21 +40,17 @@ AesObject.prototype ={ var result = new Uint8Array(l); var i=0, j=0; while(true) { - if (i==l) - break; + if (i===l){ break;} var w = words[j++]; result[i++] = (w & 0xff000000) >>> 24; - if (i==l) - break; + if (i===l){ break;} result[i++] = (w & 0x00ff0000) >>> 16; - if (i==l) - break; + if (i===l){ break;} result[i++] = (w & 0x0000ff00) >>> 8; - if (i==l) - break; + if (i===l){ break;} result[i++] = (w & 0x000000ff); } - return result + return result; }, decrypt: function(){ var salt = CryptoJS.lib.WordArray.create(this.data.slice(0,this.saltLength)); @@ -87,9 +83,9 @@ AesObject.prototype ={ iv: CryptoJS.enc.Hex.parse("0"), mode: CryptoJS.mode.CTRGladman, padding: CryptoJS.pad.NoPadding - }) + }); - return this.wordArrayToUint8Array(decryptData) + return this.wordArrayToUint8Array(decryptData); } }; From 8f1b4c547645b9c72cc9eaf3b9d917d7dab0f319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=90=AF=E8=BF=AA?= Date: Thu, 23 Apr 2020 16:12:55 +0800 Subject: [PATCH 06/18] add Encryption --- lib/aes.js | 92 -------------------- lib/compressedObject.js | 43 ++++++--- lib/generate/ZipFileWorker.js | 47 +++++++--- lib/generate/index.js | 9 +- lib/object.js | 2 + lib/stream/AesWorker.js | 159 ++++++++++++++++++++++++++++++++++ lib/zipEntry.js | 13 ++- lib/zipObject.js | 4 +- 8 files changed, 245 insertions(+), 124 deletions(-) delete mode 100644 lib/aes.js create mode 100644 lib/stream/AesWorker.js diff --git a/lib/aes.js b/lib/aes.js deleted file mode 100644 index a46db65c..00000000 --- a/lib/aes.js +++ /dev/null @@ -1,92 +0,0 @@ -'use strict'; -var CryptoJS = require('crypto-js/core'); -require('crypto-js/lib-typedarrays'); -require('crypto-js/aes'); -require('crypto-js/pbkdf2'); -require('crypto-js/hmac-sha1'); -require('crypto-js/mode-ctr-gladman'); -require('crypto-js/pad-nopadding'); -var AesKeyStrength = { - 1:{ - saltLength: 8, - macLength: 16, - keyLength: 16 - }, - 2:{ - saltLength: 12, - macLength: 24, - keyLength: 24 - }, - 3:{ - saltLength: 16, - macLength: 32, - keyLength: 32 - } -}; -var AesObject = function(data, password, options) { - this.passwordVerifyLen = 2; - this.data = data; - this.version = options.version; - this.strength = options.strength; - this.password = password; - this.saltLength = AesKeyStrength[this.strength].saltLength; - this.macLength = AesKeyStrength[this.strength].macLength; - this.keyLength = AesKeyStrength[this.strength].keyLength; -}; -AesObject.prototype ={ - wordArrayToUint8Array: function(wordArray) { - var l = wordArray.sigBytes; - var words = wordArray.words; - var result = new Uint8Array(l); - var i=0, j=0; - while(true) { - if (i===l){ break;} - var w = words[j++]; - result[i++] = (w & 0xff000000) >>> 24; - if (i===l){ break;} - result[i++] = (w & 0x00ff0000) >>> 16; - if (i===l){ break;} - result[i++] = (w & 0x0000ff00) >>> 8; - if (i===l){ break;} - result[i++] = (w & 0x000000ff); - } - return result; - }, - decrypt: function(){ - var salt = CryptoJS.lib.WordArray.create(this.data.slice(0,this.saltLength)); - var passVerifyValue = CryptoJS.lib.WordArray.create(this.data.slice(this.saltLength,this.saltLength + this.passwordVerifyLen)).toString(CryptoJS.enc.Hex); - var encryptedData = CryptoJS.lib.WordArray.create(this.data.slice(this.saltLength + this.passwordVerifyLen, -10)); - var macData = CryptoJS.lib.WordArray.create(this.data.slice(-10)).toString(CryptoJS.enc.Hex); - var derivedKey = CryptoJS.PBKDF2(this.password, salt,{ - keySize: (this.macLength + this.keyLength + this.passwordVerifyLen) / 4, - iterations: 1000 - }).toString(); - var aesKey = CryptoJS.enc.Hex.parse(derivedKey.slice(0,this.keyLength * 2)); - var macKey = CryptoJS.enc.Hex.parse(derivedKey.slice(this.keyLength * 2, (this.keyLength + this.macLength) * 2)); - var derivedPasswordVerifier = derivedKey.slice(-this.passwordVerifyLen * 2); - - if(passVerifyValue !== derivedPasswordVerifier){ - throw new Error("Wrong Password!"); - } - - // if AE-2 format check mac - if(this.version === 2){ - var macDataVerifier = CryptoJS.HmacSHA1(encryptedData,macKey).toString().slice(0,20); - if(macData !== macDataVerifier){ - throw new Error("Corrupted encrypted data!"); - } - } - - var decryptData = CryptoJS.AES.decrypt({ - ciphertext: encryptedData - },aesKey,{ - iv: CryptoJS.enc.Hex.parse("0"), - mode: CryptoJS.mode.CTRGladman, - padding: CryptoJS.pad.NoPadding - }); - - return this.wordArrayToUint8Array(decryptData); - } -}; - -module.exports = AesObject; diff --git a/lib/compressedObject.js b/lib/compressedObject.js index d4a7f5e9..cad708cf 100644 --- a/lib/compressedObject.js +++ b/lib/compressedObject.js @@ -5,6 +5,7 @@ var DataWorker = require('./stream/DataWorker'); var DataLengthProbe = require('./stream/DataLengthProbe'); var Crc32Probe = require('./stream/Crc32Probe'); var DataLengthProbe = require('./stream/DataLengthProbe'); +var AesWorker = require('./stream/AesWorker'); /** * Represent a compressed object, with everything needed to decompress it. @@ -15,12 +16,13 @@ var DataLengthProbe = require('./stream/DataLengthProbe'); * @param {object} compression the type of compression, see lib/compressions.js. * @param {String|ArrayBuffer|Uint8Array|Buffer} data the compressed data. */ -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 = { @@ -29,9 +31,18 @@ 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){ + worker = new DataWorker(external.Promise.resolve(this.compressedContent)) + .pipe(AesWorker.decryptWorker(this.decryptOptions)) + .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 () { @@ -63,13 +74,23 @@ 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 Crc32Probe()) + .pipe(new DataLengthProbe("uncompressedSize")) + .pipe(compression.compressWorker(compressionOptions)) + .pipe(AesWorker.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; diff --git a/lib/generate/ZipFileWorker.js b/lib/generate/ZipFileWorker.js index 4c7a6112..44ee7bbb 100644 --- a/lib/generate/ZipFileWorker.js +++ b/lib/generate/ZipFileWorker.js @@ -78,7 +78,7 @@ var generateDosExternalFileAttr = function (dosPermissions, isDir) { * @param {Function} encodeFileName the function to encode the file name / comment. * @return {Object} the zip parts. */ -var generateZipParts = function(streamInfo, streamedContent, streamingEnded, offset, platform, encodeFileName) { +var generateZipParts = function(streamInfo, streamedContent, streamingEnded, offset, platform, encodeFileName, encryptOptions) { var file = streamInfo['file'], compression = streamInfo['compression'], useCustomEncoding = encodeFileName !== utf8.utf8encode, @@ -95,7 +95,8 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off unicodePathExtraField = "", unicodeCommentExtraField = "", dir = file.dir, - date = file.date; + date = file.date, + isEncrypt = encryptOptions.password !== null; var dataInfo = { @@ -113,7 +114,9 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off } var bitflag = 0; - if (streamedContent) { + if(isEncrypt){ + bitflag |= 0x0001; + }else 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 // following the compressed data. @@ -202,14 +205,30 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off unicodeCommentExtraField; } + if(isEncrypt){ + extraFields += "\x01\x99"; + 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 @@ -319,7 +338,7 @@ var generateDataDescriptors = function (streamInfo) { * @param {String} platform the platform to use, "UNIX" or "DOS". * @param {Function} encodeFileName the function to encode file names and comments. */ -function ZipFileWorker(streamFiles, comment, platform, encodeFileName) { +function ZipFileWorker(streamFiles, comment, platform, encodeFileName, encryptOptions) { GenericWorker.call(this, "ZipFileWorker"); // The number of bytes written so far. This doesn't count accumulated chunks. this.bytesWritten = 0; @@ -348,6 +367,8 @@ function ZipFileWorker(streamFiles, comment, platform, encodeFileName) { // Used for the emitted metadata. this.currentFile = null; + this.encryptOptions = encryptOptions; + this._sources = []; @@ -390,7 +411,7 @@ ZipFileWorker.prototype.openedSource = function (streamInfo) { // don't stream folders (because they don't have any content) if(streamedContent) { - var record = generateZipParts(streamInfo, streamedContent, false, this.currentSourceOffset, this.zipPlatform, this.encodeFileName); + var record = generateZipParts(streamInfo, streamedContent, false, this.currentSourceOffset, this.zipPlatform, this.encodeFileName,this.encryptOptions); this.push({ data : record.fileRecord, meta : {percent:0} @@ -408,15 +429,17 @@ ZipFileWorker.prototype.openedSource = function (streamInfo) { ZipFileWorker.prototype.closedSource = function (streamInfo) { this.accumulate = false; var streamedContent = this.streamFiles && !streamInfo['file'].dir; - var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName); + var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName,this.encryptOptions); this.dirRecords.push(record.dirRecord); if(streamedContent) { // after the streamed file, we put data descriptors - this.push({ - data : generateDataDescriptors(streamInfo), - meta : {percent:100} - }); + if(this.encryptOptions.password === null){ + this.push({ + data : generateDataDescriptors(streamInfo), + meta : {percent:100} + }); + } } else { // the content wasn't streamed, we need to push everything now // first the file record, then the content diff --git a/lib/generate/index.js b/lib/generate/index.js index 083ddeb1..ac84eb9d 100644 --- a/lib/generate/index.js +++ b/lib/generate/index.js @@ -26,8 +26,11 @@ var getCompression = function (fileCompression, zipCompression) { * @param {String} comment the comment to use. */ exports.generateWorker = function (zip, options, comment) { - - var zipFileWorker = new ZipFileWorker(options.streamFiles, comment, options.platform, options.encodeFileName); + var encryptOptions = { + password: options.password, + strength: options.encryptStrength || 3 + }; + var zipFileWorker = new ZipFileWorker(options.streamFiles, comment, options.platform, options.encodeFileName, encryptOptions); var entriesCount = 0; try { @@ -37,7 +40,7 @@ exports.generateWorker = function (zip, options, comment) { var compressionOptions = file.options.compressionOptions || options.compressionOptions || {}; var dir = file.dir, date = file.date; - file._compressWorker(compression, compressionOptions) + file._compressWorker(compression, compressionOptions, encryptOptions) .withStreamInfo("file", { name : relativePath, dir : dir, diff --git a/lib/object.js b/lib/object.js index 1c9d8e80..0cea3387 100644 --- a/lib/object.js +++ b/lib/object.js @@ -325,6 +325,8 @@ var out = { streamFiles: false, compression: "STORE", compressionOptions : null, + password: null, + encryptStrength: null, type: "", platform: "DOS", comment: null, diff --git a/lib/stream/AesWorker.js b/lib/stream/AesWorker.js new file mode 100644 index 00000000..e2e7a13f --- /dev/null +++ b/lib/stream/AesWorker.js @@ -0,0 +1,159 @@ +'use strict'; +var CryptoJS = require('crypto-js/core'); +require('crypto-js/lib-typedarrays'); +require('crypto-js/aes'); +require('crypto-js/pbkdf2'); +require('crypto-js/hmac-sha1'); +require('crypto-js/mode-ctr-gladman'); +require('crypto-js/pad-nopadding'); + +var utils = require("../utils"); +var GenericWorker = require("./GenericWorker"); +var AesKeyStrength = { + 1:{ + saltLength: 8, + macLength: 16, + keyLength: 16 + }, + 2:{ + saltLength: 12, + macLength: 24, + keyLength: 24 + }, + 3:{ + saltLength: 16, + macLength: 32, + keyLength: 32 + } +}; + +/** + * Create a worker that uses CryptoJS to encrypt/decrypt. + * @constructor + * @param {String} action the name of the CryptoJS to call : either "Encrypt" or "Decrypt". + * @param {Object} options the options to use when (de)crypting. + */ +function AesWorker(action, options) { + GenericWorker.call(this, "AesWorker/" + action); + + this._crypto = CryptoJS; + this._aesAction = action; + this._password = options.password; + this._saltLen = AesKeyStrength[options.strength].saltLength; + this._macLen = AesKeyStrength[options.strength].macLength; + this._keyLen = AesKeyStrength[options.strength].keyLength; + this._version = options.version; + this._passVerifyLen = 2; + + // 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; + var data = chunk.data; + + var fileData; + var salt = this._aesAction === "Encrypt" ? this._crypto.lib.WordArray.random(this._saltLen) :this._crypto.lib.WordArray.create(data.slice(0,this._saltLen)); + var derivedKey = this._crypto.PBKDF2(this._password, salt,{ + keySize: (this._macLen + this._keyLen + this._passVerifyLen) / 4, + iterations: 1000 + }).toString(); + var aesKey = this._crypto.enc.Hex.parse(derivedKey.slice(0,this._keyLen * 2)); + var macKey = this._crypto.enc.Hex.parse(derivedKey.slice(this._keyLen * 2, (this._keyLen + this._macLen) * 2)); + var derivedPassVerifier = derivedKey.slice(-this._passVerifyLen * 2); + if(this._aesAction === "Encrypt"){ + derivedPassVerifier = this._crypto.enc.Hex.parse(derivedPassVerifier); + + var encryptedData = this._crypto.AES.encrypt(this._crypto.lib.WordArray.create(data), + aesKey, + { + iv: this._crypto.enc.Hex.parse("0"), + mode: this._crypto.mode.CTRGladman, + padding: this._crypto.pad.NoPadding + } + ).ciphertext; + encryptedData.clamp(); + + var macData = this._crypto.HmacSHA1(encryptedData, macKey); + macData.sigBytes = 10; + + fileData = this.wordArrayToUint8Array(salt.concat(derivedPassVerifier).concat(encryptedData).concat(macData)); + }else{ + var passVerifyValue = this._crypto.lib.WordArray.create(data.slice(this._saltLen,this._saltLen + this._passVerifyLen)).toString(this._crypto.enc.Hex); + if(passVerifyValue !== derivedPassVerifier){ + throw new Error("Wrong Password!"); + } + + var encryptedValue = this._crypto.lib.WordArray.create(data.slice(this._saltLen + this._passVerifyLen, -10)); + var macValue = this._crypto.lib.WordArray.create(data.slice(-10)).toString(this._crypto.enc.Hex); + // if AE-2 format check mac + if(this._version === 2){ + var macVerifier = this._crypto.HmacSHA1(encryptedValue, macKey).toString().slice(0,20); + if(macValue !== macVerifier){ + throw new Error("Corrupted encrypted data!"); + } + } + + var decryptData = this._crypto.AES.decrypt({ciphertext: encryptedValue}, + aesKey, + { + iv: this._crypto.enc.Hex.parse("0"), + mode: this._crypto.mode.CTRGladman, + padding: this._crypto.pad.NoPadding + } + ); + fileData = this.wordArrayToUint8Array(decryptData); + } + + this.push({ + data : fileData, + meta : this.meta + }); +}; + +AesWorker.prototype.wordArrayToUint8Array = function(wordArray) { + var l = wordArray.sigBytes; + var words = wordArray.words; + var result = new Uint8Array(l); + var i=0, j=0; + while(true) { + if (i===l){ break;} + var w = words[j++]; + result[i++] = (w & 0xff000000) >>> 24; + if (i===l){ break;} + result[i++] = (w & 0x00ff0000) >>> 16; + if (i===l){ break;} + result[i++] = (w & 0x0000ff00) >>> 8; + if (i===l){ break;} + result[i++] = (w & 0x000000ff); + } + return result; +}; + +/** + * @see GenericWorker.flush + */ +AesWorker.prototype.flush = function () { + GenericWorker.prototype.flush.call(this); +}; +/** + * @see GenericWorker.cleanUp + */ +AesWorker.prototype.cleanUp = function () { + GenericWorker.prototype.cleanUp.call(this); + this._crypto = null; +}; + +exports.encryptWorker = function (encryptOptions) { + return new AesWorker("Encrypt", encryptOptions); +}; +exports.decryptWorker = function (decryptOptions) { + return new AesWorker("Decrypt", decryptOptions); +}; diff --git a/lib/zipEntry.js b/lib/zipEntry.js index de32b26d..1b0d9298 100644 --- a/lib/zipEntry.js +++ b/lib/zipEntry.js @@ -6,7 +6,6 @@ var crc32fn = require('./crc32'); var utf8 = require('./utf8'); var compressions = require('./compressions'); var support = require('./support'); -var AesObject = require('./aes'); var MADE_BY_DOS = 0x00; var MADE_BY_UNIX = 0x03; @@ -95,9 +94,15 @@ ZipEntry.prototype = { throw new Error("Corrupted zip : compression " + utils.pretty(this.compressionMethod) + " unknown (inner file : " + utils.transformTo("string", this.fileName) + ")"); } if(this.isEncrypted()){ - this.aesObj = new AesObject(reader.readData(this.compressedSize), this.loadOptions.password, this.options.aes); - var decryptData = this.aesObj.decrypt(); - this.decompressed = new CompressedObject(decryptData.length, this.uncompressedSize, this.crc32, compression, decryptData); + if(this.loadOptions.password === undefined){ + throw new Error("Encrypted zip: need password !"); + } + var decryptOptions = { + password: this.loadOptions.password, + strength: this.options.aes.strength, + version: this.options.aes.version + }; + this.decompressed = new CompressedObject(this.compressedSize, this.uncompressedSize, this.crc32, compression, reader.readData(this.compressedSize), decryptOptions); }else{ this.decompressed = new CompressedObject(this.compressedSize, this.uncompressedSize, this.crc32, compression, reader.readData(this.compressedSize)); } diff --git a/lib/zipObject.js b/lib/zipObject.js index 04d7b618..7f957d14 100644 --- a/lib/zipObject.js +++ b/lib/zipObject.js @@ -92,7 +92,7 @@ ZipObject.prototype = { * @param {Object} compressionOptions the options to use when compressing. * @return Worker the worker. */ - _compressWorker: function (compression, compressionOptions) { + _compressWorker: function (compression, compressionOptions, encryptOptions) { if ( this._data instanceof CompressedObject && this._data.compression.magic === compression.magic @@ -103,7 +103,7 @@ ZipObject.prototype = { if(!this._dataBinary) { result = result.pipe(new utf8.Utf8EncodeWorker()); } - return CompressedObject.createWorkerFrom(result, compression, compressionOptions); + return CompressedObject.createWorkerFrom(result, compression, compressionOptions, encryptOptions); } }, /** From e449c2b1f9a8d6b9bd431876e6b1c289756031d0 Mon Sep 17 00:00:00 2001 From: David xu Date: Sun, 26 Apr 2020 08:52:49 +0800 Subject: [PATCH 07/18] Update lib/zipEntry.js Co-Authored-By: Stuart Knightley --- lib/zipEntry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/zipEntry.js b/lib/zipEntry.js index 1b0d9298..ee1123ce 100644 --- a/lib/zipEntry.js +++ b/lib/zipEntry.js @@ -222,7 +222,7 @@ ZipEntry.prototype = { reader.skip(2); this.options.aes.strength = reader.readInt(1); this.compressionMethod = this.options.aes.compressionMethod = reader.readString(2); - }else{ + } else { extraFieldValue = reader.readData(extraFieldLength); this.extraFields[extraFieldId] = { id: extraFieldId, From ae07475b676a0a9ca1979a849185f044da622b80 Mon Sep 17 00:00:00 2001 From: David xu Date: Sun, 26 Apr 2020 08:53:00 +0800 Subject: [PATCH 08/18] Update lib/zipEntry.js Co-Authored-By: Stuart Knightley --- lib/zipEntry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/zipEntry.js b/lib/zipEntry.js index ee1123ce..00e771e2 100644 --- a/lib/zipEntry.js +++ b/lib/zipEntry.js @@ -217,7 +217,7 @@ ZipEntry.prototype = { extraFieldLength = reader.readInt(2); // AES encrypt extra field header id - if(extraFieldId === 0x9901){ + if (extraFieldId === 0x9901){ this.options.aes.version = reader.readInt(2); reader.skip(2); this.options.aes.strength = reader.readInt(1); From e6bbb2e425f9085a054b8c5f545dbff4e5f73616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=90=AF=E8=BF=AA?= Date: Sun, 26 Apr 2020 11:26:49 +0800 Subject: [PATCH 09/18] fix format, add tests --- lib/compressedObject.js | 8 ++++---- lib/generate/ZipFileWorker.js | 12 ++++++------ lib/generate/index.js | 3 +++ lib/load.js | 2 +- lib/stream/AesWorker.js | 20 ++++++++++---------- lib/zipEntries.js | 2 +- lib/zipEntry.js | 11 +++++------ test/asserts/generate.js | 16 ++++++++++++++++ test/asserts/load.js | 13 ++++++++++++- test/ref/aes.zip | Bin 0 -> 175 bytes 10 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 test/ref/aes.zip diff --git a/lib/compressedObject.js b/lib/compressedObject.js index cad708cf..134bd362 100644 --- a/lib/compressedObject.js +++ b/lib/compressedObject.js @@ -32,12 +32,12 @@ CompressedObject.prototype = { */ getContentWorker : function () { var worker; - if(this.decryptOptions){ + if (this.decryptOptions) { worker = new DataWorker(external.Promise.resolve(this.compressedContent)) .pipe(AesWorker.decryptWorker(this.decryptOptions)) .pipe(this.compression.uncompressWorker()) .pipe(new DataLengthProbe("data_length")); - }else{ + } else { worker = new DataWorker(external.Promise.resolve(this.compressedContent)) .pipe(this.compression.uncompressWorker()) .pipe(new DataLengthProbe("data_length")); @@ -75,7 +75,7 @@ CompressedObject.prototype = { * @return {GenericWorker} the new worker compressing the content. */ CompressedObject.createWorkerFrom = function (uncompressedWorker, compression, compressionOptions, encryptOptions) { - if(encryptOptions.password !== null){ + if (encryptOptions.password !== null) { return uncompressedWorker .pipe(new Crc32Probe()) .pipe(new DataLengthProbe("uncompressedSize")) @@ -83,7 +83,7 @@ CompressedObject.createWorkerFrom = function (uncompressedWorker, compression, c .pipe(AesWorker.encryptWorker(encryptOptions)) .pipe(new DataLengthProbe("compressedSize")) .withStreamInfo("compression", compression); - }else{ + } else { return uncompressedWorker .pipe(new Crc32Probe()) .pipe(new DataLengthProbe("uncompressedSize")) diff --git a/lib/generate/ZipFileWorker.js b/lib/generate/ZipFileWorker.js index 44ee7bbb..d79998c8 100644 --- a/lib/generate/ZipFileWorker.js +++ b/lib/generate/ZipFileWorker.js @@ -216,17 +216,17 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off var header = ""; // version needed to extract - if(isEncrypt){ + if (isEncrypt) { header += "\x33\x00"; - }else{ + } else { header += "\x0A\x00"; } // general purpose bit flag header += decToHex(bitflag, 2); // compression method - if(isEncrypt){ + if (isEncrypt) { header += "\x63\x00"; - }else{ + } else { header += compression.magic; } // last mod file time @@ -411,7 +411,7 @@ ZipFileWorker.prototype.openedSource = function (streamInfo) { // don't stream folders (because they don't have any content) if(streamedContent) { - var record = generateZipParts(streamInfo, streamedContent, false, this.currentSourceOffset, this.zipPlatform, this.encodeFileName,this.encryptOptions); + var record = generateZipParts(streamInfo, streamedContent, false, this.currentSourceOffset, this.zipPlatform, this.encodeFileName, this.encryptOptions); this.push({ data : record.fileRecord, meta : {percent:0} @@ -429,7 +429,7 @@ ZipFileWorker.prototype.openedSource = function (streamInfo) { ZipFileWorker.prototype.closedSource = function (streamInfo) { this.accumulate = false; var streamedContent = this.streamFiles && !streamInfo['file'].dir; - var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName,this.encryptOptions); + var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName, this.encryptOptions); this.dirRecords.push(record.dirRecord); if(streamedContent) { diff --git a/lib/generate/index.js b/lib/generate/index.js index ac84eb9d..6dd4670d 100644 --- a/lib/generate/index.js +++ b/lib/generate/index.js @@ -30,6 +30,9 @@ exports.generateWorker = function (zip, options, comment) { password: options.password, strength: options.encryptStrength || 3 }; + if (encryptOptions.password && typeof encryptOptions.password !== "string") { + throw new Error("Password is not a valid string !"); + } var zipFileWorker = new ZipFileWorker(options.streamFiles, comment, options.platform, options.encodeFileName, encryptOptions); var entriesCount = 0; try { diff --git a/lib/load.js b/lib/load.js index 0c2e3a8e..56733cd1 100644 --- a/lib/load.js +++ b/lib/load.js @@ -32,7 +32,7 @@ function checkEntryCRC32(zipEntry) { module.exports = function(data, options) { var zip = this; options = utils.extend(options || {}, { - password: "", + password: null, base64: false, checkCRC32: false, optimizedBinaryString: false, diff --git a/lib/stream/AesWorker.js b/lib/stream/AesWorker.js index e2e7a13f..8942bb13 100644 --- a/lib/stream/AesWorker.js +++ b/lib/stream/AesWorker.js @@ -60,15 +60,15 @@ AesWorker.prototype.processChunk = function (chunk) { var data = chunk.data; var fileData; - var salt = this._aesAction === "Encrypt" ? this._crypto.lib.WordArray.random(this._saltLen) :this._crypto.lib.WordArray.create(data.slice(0,this._saltLen)); - var derivedKey = this._crypto.PBKDF2(this._password, salt,{ + var salt = this._aesAction === "Encrypt" ? this._crypto.lib.WordArray.random(this._saltLen) : this._crypto.lib.WordArray.create(data.slice(0, this._saltLen)); + var derivedKey = this._crypto.PBKDF2(this._password, salt, { keySize: (this._macLen + this._keyLen + this._passVerifyLen) / 4, iterations: 1000 }).toString(); - var aesKey = this._crypto.enc.Hex.parse(derivedKey.slice(0,this._keyLen * 2)); + var aesKey = this._crypto.enc.Hex.parse(derivedKey.slice(0, this._keyLen * 2)); var macKey = this._crypto.enc.Hex.parse(derivedKey.slice(this._keyLen * 2, (this._keyLen + this._macLen) * 2)); var derivedPassVerifier = derivedKey.slice(-this._passVerifyLen * 2); - if(this._aesAction === "Encrypt"){ + if (this._aesAction === "Encrypt") { derivedPassVerifier = this._crypto.enc.Hex.parse(derivedPassVerifier); var encryptedData = this._crypto.AES.encrypt(this._crypto.lib.WordArray.create(data), @@ -86,7 +86,7 @@ AesWorker.prototype.processChunk = function (chunk) { fileData = this.wordArrayToUint8Array(salt.concat(derivedPassVerifier).concat(encryptedData).concat(macData)); }else{ - var passVerifyValue = this._crypto.lib.WordArray.create(data.slice(this._saltLen,this._saltLen + this._passVerifyLen)).toString(this._crypto.enc.Hex); + var passVerifyValue = this._crypto.lib.WordArray.create(data.slice(this._saltLen, this._saltLen + this._passVerifyLen)).toString(this._crypto.enc.Hex); if(passVerifyValue !== derivedPassVerifier){ throw new Error("Wrong Password!"); } @@ -95,7 +95,7 @@ AesWorker.prototype.processChunk = function (chunk) { var macValue = this._crypto.lib.WordArray.create(data.slice(-10)).toString(this._crypto.enc.Hex); // if AE-2 format check mac if(this._version === 2){ - var macVerifier = this._crypto.HmacSHA1(encryptedValue, macKey).toString().slice(0,20); + var macVerifier = this._crypto.HmacSHA1(encryptedValue, macKey).toString().slice(0, 20); if(macValue !== macVerifier){ throw new Error("Corrupted encrypted data!"); } @@ -124,14 +124,14 @@ AesWorker.prototype.wordArrayToUint8Array = function(wordArray) { var result = new Uint8Array(l); var i=0, j=0; while(true) { - if (i===l){ break;} + if (i===l) {break;} var w = words[j++]; result[i++] = (w & 0xff000000) >>> 24; - if (i===l){ break;} + if (i===l) {break;} result[i++] = (w & 0x00ff0000) >>> 16; - if (i===l){ break;} + if (i===l) {break;} result[i++] = (w & 0x0000ff00) >>> 8; - if (i===l){ break;} + if (i===l) {break;} result[i++] = (w & 0x000000ff); } return result; diff --git a/lib/zipEntries.js b/lib/zipEntries.js index 35a03f5b..28e8d74a 100644 --- a/lib/zipEntries.js +++ b/lib/zipEntries.js @@ -131,7 +131,7 @@ ZipEntries.prototype = { var file; this.reader.setIndex(this.centralDirOffset); - this.aes={}; + this.aes = {}; while (this.reader.readAndCheckSignature(sig.CENTRAL_FILE_HEADER)) { file = new ZipEntry({ zip64: this.zip64, diff --git a/lib/zipEntry.js b/lib/zipEntry.js index 00e771e2..d40eb8ee 100644 --- a/lib/zipEntry.js +++ b/lib/zipEntry.js @@ -94,8 +94,11 @@ ZipEntry.prototype = { throw new Error("Corrupted zip : compression " + utils.pretty(this.compressionMethod) + " unknown (inner file : " + utils.transformTo("string", this.fileName) + ")"); } if(this.isEncrypted()){ - if(this.loadOptions.password === undefined){ - throw new Error("Encrypted zip: need password !"); + if(this.options.aes.strength === undefined){ + throw new Error("Encrypted zip: unsupported encrypt method"); + } + if(!(this.loadOptions.password && this.loadOptions.password.length)){ + throw new Error("Encrypted zip: need password"); } var decryptOptions = { password: this.loadOptions.password, @@ -130,10 +133,6 @@ ZipEntry.prototype = { this.externalFileAttributes = reader.readInt(4); this.localHeaderOffset = reader.readInt(4); - // if (this.isEncrypted()) { - // throw new Error("Encrypted zip are not supported"); - // } - // will be read in the local part, see the comments there reader.skip(fileNameLength); this.readExtraFields(reader); diff --git a/test/asserts/generate.js b/test/asserts/generate.js index 4830be82..8552728d 100644 --- a/test/asserts/generate.js +++ b/test/asserts/generate.js @@ -245,6 +245,22 @@ JSZipTestUtils.testZipFile("STORE is the default method", "ref/text.zip", functi })['catch'](JSZipTestUtils.assertNoError); }); +JSZipTestUtils.testZipFile("AES-256 encrypted", "ref/aes.zip", function(assert, expected) { + var zip = new JSZip(); + zip.file("aes.txt", "aes encrypted"); + var done = assert.async(); + zip.generateAsync({type:"arraybuffer", password:"12345678", encryptStrength: 3}) + .then(function(content) { + JSZip.loadAsync(content, {password:"12345678"}) + .then(function success(zip) { + return zip.file("aes.txt").async("string"); + }).then(function (content) { + assert.equal(content, "aes encrypted", "Generated ZIP matches reference ZIP"); + done(); + }); + })['catch'](JSZipTestUtils.assertNoError); +}); + function testLazyDecompression(assert, from, to) { var done = assert.async(); diff --git a/test/asserts/load.js b/test/asserts/load.js index 5b229661..0884c7ab 100644 --- a/test/asserts/load.js +++ b/test/asserts/load.js @@ -425,6 +425,17 @@ QUnit.module("load", function () { })['catch'](JSZipTestUtils.assertNoError); }); + JSZipTestUtils.testZipFile("aes encrypted zip file", "ref/aes.zip", function(assert, file) { + var done = assert.async(); + JSZip.loadAsync(file, {password: "12345678"}) + .then(function success(zip) { + return zip.file("aes.txt").async("string"); + }).then(function (content) { + assert.equal(content, "aes encrypted", "the zip was correctly read."); + done(); + })['catch'](JSZipTestUtils.assertNoError); + }); + JSZipTestUtils.testZipFile("load(promise) works", "ref/all.zip", function(assert, fileAsString) { var done = assert.async(); @@ -525,7 +536,7 @@ QUnit.module("load", function () { assert.ok(false, "Encryption is not supported, but no exception were thrown"); done(); }, function failure(e) { - assert.equal(e.message, "Encrypted zip are not supported", "the error message is useful"); + assert.equal(e.message, "Encrypted zip: unsupported encrypt method", "the error message is useful"); done(); }); }); diff --git a/test/ref/aes.zip b/test/ref/aes.zip new file mode 100644 index 0000000000000000000000000000000000000000..3de440006d56f0379e1dbbdaf46f8543adb72424 GIT binary patch literal 175 zcmWIWW@a&FU}Q*UI4(FVV4p3|S4{>623{a$XW(W?OfA+csVHHb$ Date: Sun, 26 Apr 2020 20:12:31 +0800 Subject: [PATCH 10/18] fix code format --- lib/generate/ZipFileWorker.js | 12 ++++++------ lib/zipEntry.js | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/generate/ZipFileWorker.js b/lib/generate/ZipFileWorker.js index d79998c8..9dc16e56 100644 --- a/lib/generate/ZipFileWorker.js +++ b/lib/generate/ZipFileWorker.js @@ -114,9 +114,9 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off } var bitflag = 0; - if(isEncrypt){ + if (isEncrypt) { bitflag |= 0x0001; - }else if (streamedContent) { + } else 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 // following the compressed data. @@ -186,7 +186,7 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off unicodePathExtraField; } - if(useUTF8ForComment) { + if (useUTF8ForComment) { unicodeCommentExtraField = // Version @@ -205,7 +205,7 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off unicodeCommentExtraField; } - if(isEncrypt){ + if (isEncrypt) { extraFields += "\x01\x99"; extraFields += "\x07\x00"; extraFields += "\x02\x00"; @@ -432,9 +432,9 @@ ZipFileWorker.prototype.closedSource = function (streamInfo) { var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName, this.encryptOptions); this.dirRecords.push(record.dirRecord); - if(streamedContent) { + if (streamedContent) { // after the streamed file, we put data descriptors - if(this.encryptOptions.password === null){ + if (this.encryptOptions.password === null) { this.push({ data : generateDataDescriptors(streamInfo), meta : {percent:100} diff --git a/lib/zipEntry.js b/lib/zipEntry.js index d40eb8ee..6c38c883 100644 --- a/lib/zipEntry.js +++ b/lib/zipEntry.js @@ -93,11 +93,11 @@ ZipEntry.prototype = { if (compression === null) { // no compression found throw new Error("Corrupted zip : compression " + utils.pretty(this.compressionMethod) + " unknown (inner file : " + utils.transformTo("string", this.fileName) + ")"); } - if(this.isEncrypted()){ - if(this.options.aes.strength === undefined){ + if (this.isEncrypted()) { + if (this.options.aes.strength === undefined) { throw new Error("Encrypted zip: unsupported encrypt method"); } - if(!(this.loadOptions.password && this.loadOptions.password.length)){ + if (!(this.loadOptions.password && this.loadOptions.password.length)) { throw new Error("Encrypted zip: need password"); } var decryptOptions = { @@ -106,7 +106,7 @@ ZipEntry.prototype = { version: this.options.aes.version }; this.decompressed = new CompressedObject(this.compressedSize, this.uncompressedSize, this.crc32, compression, reader.readData(this.compressedSize), decryptOptions); - }else{ + } else { this.decompressed = new CompressedObject(this.compressedSize, this.uncompressedSize, this.crc32, compression, reader.readData(this.compressedSize)); } }, @@ -216,7 +216,7 @@ ZipEntry.prototype = { extraFieldLength = reader.readInt(2); // AES encrypt extra field header id - if (extraFieldId === 0x9901){ + if (extraFieldId === 0x9901) { this.options.aes.version = reader.readInt(2); reader.skip(2); this.options.aes.strength = reader.readInt(1); From e4fd809c7ede3ddd0da06350fc7b33724c8dbf91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=90=AF=E8=BF=AA?= Date: Mon, 27 Apr 2020 15:53:22 +0800 Subject: [PATCH 11/18] fix error tip --- lib/generate/index.js | 2 +- lib/stream/AesWorker.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/generate/index.js b/lib/generate/index.js index 6dd4670d..ee90c136 100644 --- a/lib/generate/index.js +++ b/lib/generate/index.js @@ -31,7 +31,7 @@ exports.generateWorker = function (zip, options, comment) { strength: options.encryptStrength || 3 }; if (encryptOptions.password && typeof encryptOptions.password !== "string") { - throw new Error("Password is not a valid string !"); + throw new Error("Password is not a valid string."); } var zipFileWorker = new ZipFileWorker(options.streamFiles, comment, options.platform, options.encodeFileName, encryptOptions); var entriesCount = 0; diff --git a/lib/stream/AesWorker.js b/lib/stream/AesWorker.js index 8942bb13..6509d767 100644 --- a/lib/stream/AesWorker.js +++ b/lib/stream/AesWorker.js @@ -88,7 +88,7 @@ AesWorker.prototype.processChunk = function (chunk) { }else{ var passVerifyValue = this._crypto.lib.WordArray.create(data.slice(this._saltLen, this._saltLen + this._passVerifyLen)).toString(this._crypto.enc.Hex); if(passVerifyValue !== derivedPassVerifier){ - throw new Error("Wrong Password!"); + throw new Error("Encrypted zip: incorrect password"); } var encryptedValue = this._crypto.lib.WordArray.create(data.slice(this._saltLen + this._passVerifyLen, -10)); @@ -97,7 +97,7 @@ AesWorker.prototype.processChunk = function (chunk) { if(this._version === 2){ var macVerifier = this._crypto.HmacSHA1(encryptedValue, macKey).toString().slice(0, 20); if(macValue !== macVerifier){ - throw new Error("Corrupted encrypted data!"); + throw new Error("Corrupted zip: CRC failed"); } } From 614c0f95c25733ed9bb1d20f13ab03eb42dfcb4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=90=AF=E8=BF=AA?= Date: Tue, 28 Apr 2020 12:02:27 +0800 Subject: [PATCH 12/18] fix stream --- lib/generate/ZipFileWorker.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/generate/ZipFileWorker.js b/lib/generate/ZipFileWorker.js index 9dc16e56..95492b3c 100644 --- a/lib/generate/ZipFileWorker.js +++ b/lib/generate/ZipFileWorker.js @@ -116,7 +116,8 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off var bitflag = 0; if (isEncrypt) { bitflag |= 0x0001; - } else if (streamedContent) { + } + 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 // following the compressed data. @@ -434,12 +435,10 @@ ZipFileWorker.prototype.closedSource = function (streamInfo) { this.dirRecords.push(record.dirRecord); if (streamedContent) { // after the streamed file, we put data descriptors - if (this.encryptOptions.password === null) { - this.push({ - data : generateDataDescriptors(streamInfo), - meta : {percent:100} - }); - } + this.push({ + data : generateDataDescriptors(streamInfo), + meta : {percent:100} + }); } else { // the content wasn't streamed, we need to push everything now // first the file record, then the content From 8d57b4a2ed436e1667cd14952c1ef1b04d520b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=90=AF=E8=BF=AA?= Date: Sat, 9 May 2020 20:22:32 +0800 Subject: [PATCH 13/18] replace crypto-js with sjcl --- .jshintignore | 1 + Gruntfile.js | 3 ++ lib/ctrGladman.js | 111 ++++++++++++++++++++++++++++++++++++++++ lib/stream/AesWorker.js | 110 +++++++++++++++++---------------------- package.json | 15 +++++- 5 files changed, 174 insertions(+), 66 deletions(-) create mode 100644 lib/ctrGladman.js diff --git a/.jshintignore b/.jshintignore index 3c3629e6..e05b4f9a 100644 --- a/.jshintignore +++ b/.jshintignore @@ -1 +1,2 @@ node_modules +lib/ctrGladman.js \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 45e7c4c5..45a4bd77 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -85,6 +85,9 @@ module.exports = function(grunt) { }, builtins: false }, + transform: [ + 'browserify-shim' + ], banner: grunt.file.read('lib/license_header.js').replace(/__VERSION__/, version) } } diff --git a/lib/ctrGladman.js b/lib/ctrGladman.js new file mode 100644 index 00000000..f24f019f --- /dev/null +++ b/lib/ctrGladman.js @@ -0,0 +1,111 @@ +/** @fileOverview CTR mode implementation. + * + * Special thanks to Roy Nicholson for pointing out a bug in our + * implementation. + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh + */ + +/** + * CTR mode with CBC MAC. + * @namespace + */ +sjcl.mode.ctrGladman = { + /** The name of the mode. + * @constant + */ + name: "ctrGladman", + + /** Encrypt in CTR mode. + * @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes. + * @param {bitArray} plaintext The plaintext data. + * @param {bitArray} iv The initialization value. It must be 128 bits. + * @param {bitArray} [adata=[]] The authenticated data. Must be empty. + * @return The encrypted data, an array of bytes. + * @throws {sjcl.exception.invalid} if the IV isn't exactly 128 bits or if any adata is specified. + */ + encrypt: function(prf, plaintext, iv, adata) { + return sjcl.mode.ctrGladman._calculate(prf, plaintext, iv, adata); + }, + + /** Decrypt in CTR mode. + * @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes. + * @param {bitArray} ciphertext The ciphertext data. + * @param {bitArray} iv The initialization value. It must be 128 bits. + * @param {bitArray} [adata=[]] The authenticated data. It must be empty. + * @return The decrypted data, an array of bytes. + * @throws {sjcl.exception.invalid} if the IV isn't exactly 128 bits or if any adata is specified. + * @throws {sjcl.exception.corrupt} if if the message is corrupt. + */ + decrypt: function(prf, ciphertext, iv, adata) { + return sjcl.mode.ctrGladman._calculate(prf, ciphertext, iv, adata); + }, + + incWord: function(word) { + if (((word >> 24) & 0xff) === 0xff) { //overflow + var b1 = (word >> 16) & 0xff; + var b2 = (word >> 8) & 0xff; + var b3 = word & 0xff; + + if (b1 === 0xff) { // overflow b1 + b1 = 0; + if (b2 === 0xff) { + b2 = 0; + if (b3 === 0xff) { + b3 = 0; + } else { + ++b3; + } + } else { + ++b2; + } + } else { + ++b1; + } + + word = 0; + word += (b1 << 16); + word += (b2 << 8); + word += b3; + } else { + word += (0x01 << 24); + } + return word; + }, + + incCounter: function(counter) { + if ((counter[0] = this.incWord(counter[0])) === 0) + { + // encr_data in fileenc.c from Dr Brian Gladman's counts only with DWORD j < 8 + counter[1] = this.incWord(counter[1]); + } + return counter; + }, + + _calculate: function(prf, data, iv, adata) { + var l, bl, res, c, d, e, i; + if (adata && adata.length) { + throw new sjcl.exception.invalid("ctr can't authenticate data"); + } + if (sjcl.bitArray.bitLength(iv) !== 128) { + throw new sjcl.exception.invalid("ctr iv must be 128 bits"); + } + if (!(l = data.length)) { + return []; + } + c = iv.slice(0); + d = data.slice(0); + bl = sjcl.bitArray.bitLength(d); + for (i=0; i>> 24; - if (i===l) {break;} - result[i++] = (w & 0x00ff0000) >>> 16; - if (i===l) {break;} - result[i++] = (w & 0x0000ff00) >>> 8; - if (i===l) {break;} - result[i++] = (w & 0x000000ff); - } - return result; -}; - /** * @see GenericWorker.flush */ diff --git a/package.json b/package.json index 15edf1a2..ae173e17 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ ], "main": "./lib/index", "browser": { - "readable-stream": "./lib/readable-stream-browser.js" + "readable-stream": "./lib/readable-stream-browser.js", + "sjcl": "./node_modules/sjcl/core/sjcl.js" }, "repository": { "type": "git", @@ -38,6 +39,7 @@ ], "devDependencies": { "browserify": "~13.0.0", + "browserify-shim": "^3.8.14", "grunt": "~0.4.1", "grunt-browserify": "~5.0.0", "grunt-cli": "~1.1.0", @@ -56,7 +58,16 @@ "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" + "set-immediate-shim": "~1.0.1", + "sjcl": "^1.0.8" + }, + "browserify": { + "transform": [ + "browserify-shim" + ] + }, + "browserify-shim": { + "sjcl": "sjcl" }, "license": "(MIT OR GPL-3.0)" } From 8b03d923bdbd37328003b3dea4974ab031066d9d Mon Sep 17 00:00:00 2001 From: David xu Date: Sun, 10 May 2020 10:17:33 +0800 Subject: [PATCH 14/18] Update lib/stream/AesWorker.js Co-authored-by: Deliang Yang <623601391@qq.com> --- lib/stream/AesWorker.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/stream/AesWorker.js b/lib/stream/AesWorker.js index 8526ab4f..aa9ce324 100644 --- a/lib/stream/AesWorker.js +++ b/lib/stream/AesWorker.js @@ -72,7 +72,9 @@ AesWorker.prototype.processChunk = function (chunk) { var fileData; var hmac; var iv = [0, 0, 0, 0]; - var salt = this._aesAction === "Encrypt" ? this._crypto.random.randomWords(this._saltLen / 4) : this._crypto.codec.bytes.toBits(data.slice(0, this._saltLen)); +var salt = this._aesAction === "Encrypt" + ? this._crypto.random.randomWords(this._saltLen / 4) + : this._crypto.codec.bytes.toBits(data.slice(0, this._saltLen)); var derivedKey = this._crypto.misc.pbkdf2(this._password, salt, 1000, (this._macLen + this._keyLen + this._passVerifyLen) * 8, this._crypto.misc.hmacSha1); var aesKey = this._crypto.bitArray.bitSlice(derivedKey, 0, this._keyLen * 8); From 2dbc0eff3e6e5b523bd76a7c0801f4e7add4d76e Mon Sep 17 00:00:00 2001 From: David xu Date: Sun, 10 May 2020 10:17:45 +0800 Subject: [PATCH 15/18] Update lib/ctrGladman.js Co-authored-by: Deliang Yang <623601391@qq.com> --- lib/ctrGladman.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/ctrGladman.js b/lib/ctrGladman.js index f24f019f..a38322ca 100644 --- a/lib/ctrGladman.js +++ b/lib/ctrGladman.js @@ -76,8 +76,7 @@ sjcl.mode.ctrGladman = { }, incCounter: function(counter) { - if ((counter[0] = this.incWord(counter[0])) === 0) - { + if ((counter[0] = this.incWord(counter[0])) === 0) { // encr_data in fileenc.c from Dr Brian Gladman's counts only with DWORD j < 8 counter[1] = this.incWord(counter[1]); } From 586cc72174055a380a007b22236295a9c4c3287b Mon Sep 17 00:00:00 2001 From: xqdoo00o Date: Sun, 10 May 2020 10:45:00 +0800 Subject: [PATCH 16/18] fix formats --- lib/ctrGladman.js | 172 ++++++++++++++++++++-------------------- lib/stream/AesWorker.js | 32 ++++---- package.json | 1 - 3 files changed, 102 insertions(+), 103 deletions(-) diff --git a/lib/ctrGladman.js b/lib/ctrGladman.js index a38322ca..655b8fd2 100644 --- a/lib/ctrGladman.js +++ b/lib/ctrGladman.js @@ -13,98 +13,98 @@ * @namespace */ sjcl.mode.ctrGladman = { - /** The name of the mode. - * @constant - */ - name: "ctrGladman", - - /** Encrypt in CTR mode. - * @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes. - * @param {bitArray} plaintext The plaintext data. - * @param {bitArray} iv The initialization value. It must be 128 bits. - * @param {bitArray} [adata=[]] The authenticated data. Must be empty. - * @return The encrypted data, an array of bytes. - * @throws {sjcl.exception.invalid} if the IV isn't exactly 128 bits or if any adata is specified. - */ - encrypt: function(prf, plaintext, iv, adata) { - return sjcl.mode.ctrGladman._calculate(prf, plaintext, iv, adata); - }, + /** The name of the mode. + * @constant + */ + name: "ctrGladman", - /** Decrypt in CTR mode. - * @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes. - * @param {bitArray} ciphertext The ciphertext data. - * @param {bitArray} iv The initialization value. It must be 128 bits. - * @param {bitArray} [adata=[]] The authenticated data. It must be empty. - * @return The decrypted data, an array of bytes. - * @throws {sjcl.exception.invalid} if the IV isn't exactly 128 bits or if any adata is specified. - * @throws {sjcl.exception.corrupt} if if the message is corrupt. - */ - decrypt: function(prf, ciphertext, iv, adata) { - return sjcl.mode.ctrGladman._calculate(prf, ciphertext, iv, adata); - }, + /** Encrypt in CTR mode. + * @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes. + * @param {bitArray} plaintext The plaintext data. + * @param {bitArray} iv The initialization value. It must be 128 bits. + * @param {bitArray} [adata=[]] The authenticated data. Must be empty. + * @return The encrypted data, an array of bytes. + * @throws {sjcl.exception.invalid} if the IV isn't exactly 128 bits or if any adata is specified. + */ + encrypt: function (prf, plaintext, iv, adata) { + return sjcl.mode.ctrGladman._calculate(prf, plaintext, iv, adata); + }, - incWord: function(word) { - if (((word >> 24) & 0xff) === 0xff) { //overflow - var b1 = (word >> 16) & 0xff; - var b2 = (word >> 8) & 0xff; - var b3 = word & 0xff; + /** Decrypt in CTR mode. + * @param {Object} prf The pseudorandom function. It must have a block size of 16 bytes. + * @param {bitArray} ciphertext The ciphertext data. + * @param {bitArray} iv The initialization value. It must be 128 bits. + * @param {bitArray} [adata=[]] The authenticated data. It must be empty. + * @return The decrypted data, an array of bytes. + * @throws {sjcl.exception.invalid} if the IV isn't exactly 128 bits or if any adata is specified. + * @throws {sjcl.exception.corrupt} if if the message is corrupt. + */ + decrypt: function (prf, ciphertext, iv, adata) { + return sjcl.mode.ctrGladman._calculate(prf, ciphertext, iv, adata); + }, - if (b1 === 0xff) { // overflow b1 - b1 = 0; - if (b2 === 0xff) { - b2 = 0; - if (b3 === 0xff) { - b3 = 0; - } else { - ++b3; - } + incWord: function (word) { + if (((word >> 24) & 0xff) === 0xff) { //overflow + var b1 = (word >> 16) & 0xff; + var b2 = (word >> 8) & 0xff; + var b3 = word & 0xff; + + if (b1 === 0xff) { // overflow b1 + b1 = 0; + if (b2 === 0xff) { + b2 = 0; + if (b3 === 0xff) { + b3 = 0; + } else { + ++b3; + } + } else { + ++b2; + } + } else { + ++b1; + } + + word = 0; + word += (b1 << 16); + word += (b2 << 8); + word += b3; } else { - ++b2; + word += (0x01 << 24); } - } else { - ++b1; - } - - word = 0; - word += (b1 << 16); - word += (b2 << 8); - word += b3; - } else { - word += (0x01 << 24); - } - return word; - }, + return word; + }, - incCounter: function(counter) { - if ((counter[0] = this.incWord(counter[0])) === 0) { - // encr_data in fileenc.c from Dr Brian Gladman's counts only with DWORD j < 8 - counter[1] = this.incWord(counter[1]); - } - return counter; - }, + incCounter: function (counter) { + if ((counter[0] = this.incWord(counter[0])) === 0) { + // encr_data in fileenc.c from Dr Brian Gladman's counts only with DWORD j < 8 + counter[1] = this.incWord(counter[1]); + } + return counter; + }, - _calculate: function(prf, data, iv, adata) { - var l, bl, res, c, d, e, i; - if (adata && adata.length) { - throw new sjcl.exception.invalid("ctr can't authenticate data"); - } - if (sjcl.bitArray.bitLength(iv) !== 128) { - throw new sjcl.exception.invalid("ctr iv must be 128 bits"); - } - if (!(l = data.length)) { - return []; - } - c = iv.slice(0); - d = data.slice(0); - bl = sjcl.bitArray.bitLength(d); - for (i=0; i Date: Sun, 10 May 2020 11:17:49 +0800 Subject: [PATCH 17/18] fix env --- lib/stream/AesWorker.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/stream/AesWorker.js b/lib/stream/AesWorker.js index 1bcba490..5bb3e306 100644 --- a/lib/stream/AesWorker.js +++ b/lib/stream/AesWorker.js @@ -1,6 +1,15 @@ 'use strict'; var sjcl = require('sjcl'); -window.sjcl = sjcl; + +var getGlobal = function () { + if (typeof self !== 'undefined') { return self; } + if (typeof window !== 'undefined') { return window; } + if (typeof global !== 'undefined') { return global; } + throw new Error('unable to locate global object'); +}; +var global = getGlobal(); +global.sjcl = sjcl; + require('sjcl/core/bitArray'); require('sjcl/core/sha1'); require('sjcl/core/sha256'); @@ -72,9 +81,9 @@ AesWorker.prototype.processChunk = function (chunk) { var fileData; var hmac; var iv = [0, 0, 0, 0]; - var salt = this._aesAction === "Encrypt" - ? this._crypto.random.randomWords(this._saltLen / 4) - : this._crypto.codec.bytes.toBits(data.slice(0, this._saltLen)); + var salt = this._aesAction === "Encrypt" ? + this._crypto.random.randomWords(this._saltLen / 4) : + this._crypto.codec.bytes.toBits(data.slice(0, this._saltLen)); var derivedKey = this._crypto.misc.pbkdf2(this._password, salt, 1000, (this._macLen + this._keyLen + this._passVerifyLen) * 8, this._crypto.misc.hmacSha1); var aesKey = this._crypto.bitArray.bitSlice(derivedKey, 0, this._keyLen * 8); From d6a8a30183fb1676f466e6b3a596898fdd0b778e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=90=AF=E8=BF=AA?= Date: Tue, 12 May 2020 17:53:29 +0800 Subject: [PATCH 18/18] fix comment --- lib/generate/index.js | 2 +- lib/object.js | 2 +- lib/stream/AesWorker.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/generate/index.js b/lib/generate/index.js index ee90c136..04550636 100644 --- a/lib/generate/index.js +++ b/lib/generate/index.js @@ -28,7 +28,7 @@ var getCompression = function (fileCompression, zipCompression) { exports.generateWorker = function (zip, options, comment) { var encryptOptions = { password: options.password, - strength: options.encryptStrength || 3 + strength: options.encryptStrength }; if (encryptOptions.password && typeof encryptOptions.password !== "string") { throw new Error("Password is not a valid string."); diff --git a/lib/object.js b/lib/object.js index 0cea3387..9b4cd8a7 100644 --- a/lib/object.js +++ b/lib/object.js @@ -326,7 +326,7 @@ var out = { compression: "STORE", compressionOptions : null, password: null, - encryptStrength: null, + encryptStrength: 3, type: "", platform: "DOS", comment: null, diff --git a/lib/stream/AesWorker.js b/lib/stream/AesWorker.js index 5bb3e306..8d52935c 100644 --- a/lib/stream/AesWorker.js +++ b/lib/stream/AesWorker.js @@ -47,10 +47,10 @@ var AesKeyStrength = { }; /** - * Create a worker that uses CryptoJS to encrypt/decrypt. + * Create a worker that uses sjcl to encrypt/decrypt. * @constructor - * @param {String} action the name of the CryptoJS to call : either "Encrypt" or "Decrypt". - * @param {Object} options the options to use when (de)crypting. + * @param {String} action the name of the sjcl to call : either "Encrypt" or "Decrypt". + * @param {Object} options the options to use when (en/de)crypt. */ function AesWorker(action, options) { GenericWorker.call(this, "AesWorker/" + action);