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/compressedObject.js b/lib/compressedObject.js index d4a7f5e9..134bd362 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/ctrGladman.js b/lib/ctrGladman.js new file mode 100644 index 00000000..655b8fd2 --- /dev/null +++ b/lib/ctrGladman.js @@ -0,0 +1,110 @@ +/** @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 < l; i += 4) { + this.incCounter(c); + e = prf.encrypt(c); + d[i] ^= e[0]; + d[i + 1] ^= e[1]; + d[i + 2] ^= e[2]; + d[i + 3] ^= e[3]; + } + return sjcl.bitArray.clamp(d, bl); + } +}; diff --git a/lib/generate/ZipFileWorker.js b/lib/generate/ZipFileWorker.js index 4c7a6112..95492b3c 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,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 @@ -183,7 +187,7 @@ var generateZipParts = function(streamInfo, streamedContent, streamingEnded, off unicodePathExtraField; } - if(useUTF8ForComment) { + if (useUTF8ForComment) { unicodeCommentExtraField = // Version @@ -202,14 +206,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 +339,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 +368,8 @@ function ZipFileWorker(streamFiles, comment, platform, encodeFileName) { // Used for the emitted metadata. this.currentFile = null; + this.encryptOptions = encryptOptions; + this._sources = []; @@ -390,7 +412,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,10 +430,10 @@ 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) { + if (streamedContent) { // after the streamed file, we put data descriptors this.push({ data : generateDataDescriptors(streamInfo), diff --git a/lib/generate/index.js b/lib/generate/index.js index 083ddeb1..04550636 100644 --- a/lib/generate/index.js +++ b/lib/generate/index.js @@ -26,8 +26,14 @@ 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 + }; + 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 { @@ -37,7 +43,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/load.js b/lib/load.js index 53cae42a..56733cd1 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: null, base64: false, checkCRC32: false, optimizedBinaryString: false, diff --git a/lib/object.js b/lib/object.js index 1c9d8e80..9b4cd8a7 100644 --- a/lib/object.js +++ b/lib/object.js @@ -325,6 +325,8 @@ var out = { streamFiles: false, compression: "STORE", compressionOptions : null, + password: null, + encryptStrength: 3, type: "", platform: "DOS", comment: null, diff --git a/lib/stream/AesWorker.js b/lib/stream/AesWorker.js new file mode 100644 index 00000000..8d52935c --- /dev/null +++ b/lib/stream/AesWorker.js @@ -0,0 +1,152 @@ +'use strict'; +var sjcl = require('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'); +require('sjcl/core/codecBytes'); +require('sjcl/core/random'); +require('sjcl/core/aes'); +require('sjcl/core/pbkdf2'); +require('sjcl/core/hmac'); +require('../ctrGladman'); + +sjcl.misc.hmacSha1 = function (key) { + sjcl.misc.hmac.call(this, key, sjcl.hash.sha1); +}; +sjcl.misc.hmacSha1.prototype = new sjcl.misc.hmac(""); +sjcl.misc.hmacSha1.prototype.constructor = sjcl.misc.hmacSha1; + +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 sjcl to encrypt/decrypt. + * @constructor + * @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); + + this._crypto = sjcl; + 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 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 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); + var macKey = this._crypto.bitArray.bitSlice(derivedKey, this._keyLen * 8, (this._keyLen + this._macLen) * 8); + var derivedPassVerifier = this._crypto.bitArray.bitSlice(derivedKey, (this._keyLen + this._macLen) * 8); + + if (this._aesAction === "Encrypt") { + var encryptedData = this._crypto.mode.ctrGladman.encrypt(new this._crypto.cipher.aes(aesKey), this._crypto.codec.bytes.toBits(data), iv); + encryptedData = this._crypto.bitArray.clamp(encryptedData, (data.length) * 8); + + hmac = new this._crypto.misc.hmacSha1(macKey); + var macData = hmac.encrypt(encryptedData); + macData = this._crypto.bitArray.clamp(macData, 10 * 8); + + fileData = this._crypto.bitArray.concat(salt, derivedPassVerifier); + fileData = this._crypto.bitArray.concat(fileData, encryptedData); + fileData = this._crypto.bitArray.concat(fileData, macData); + fileData = Uint8Array.from(this._crypto.codec.bytes.fromBits(fileData)); + } else { + var passVerifyValue = this._crypto.codec.bytes.toBits(data.slice(this._saltLen, this._saltLen + this._passVerifyLen)); + if (!this._crypto.bitArray.equal(passVerifyValue, derivedPassVerifier)) { + throw new Error("Encrypted zip: incorrect password"); + } + + var encryptedValue = this._crypto.codec.bytes.toBits(data.slice(this._saltLen + this._passVerifyLen, -10)); + var macValue = this._crypto.codec.bytes.toBits(data.slice(-10)); + // if AE-2 format check mac + if (this._version === 2) { + hmac = new this._crypto.misc.hmacSha1(macKey); + var macVerifier = hmac.encrypt(encryptedValue); + macVerifier = this._crypto.bitArray.clamp(macVerifier, 10 * 8); + if (!this._crypto.bitArray.equal(macValue, macVerifier)) { + throw new Error("Corrupted zip: CRC failed"); + } + } + + var decryptData = this._crypto.mode.ctrGladman.decrypt(new this._crypto.cipher.aes(aesKey), encryptedValue, iv); + fileData = Uint8Array.from(this._crypto.codec.bytes.fromBits(decryptData)); + } + + this.push({ + data: fileData, + meta: this.meta + }); +}; + +/** + * @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/zipEntries.js b/lib/zipEntries.js index 2e3aba90..28e8d74a 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..6c38c883 100644 --- a/lib/zipEntry.js +++ b/lib/zipEntry.js @@ -93,7 +93,22 @@ 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()) { + 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, + 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)); + } }, /** @@ -118,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); @@ -203,13 +214,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 + }; + } } }, /** 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); } }, /** diff --git a/package.json b/package.json index a2ca6451..a8a9a5e1 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "main": "./lib/index", "browser": { "readable-stream": "./lib/readable-stream-browser.js", + "sjcl": "./node_modules/sjcl/core/sjcl.js", ".": "./dist/jszip.min.js" }, "types": "./index.d.ts", @@ -40,6 +41,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", @@ -57,7 +59,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)" } 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 00000000..3de44000 Binary files /dev/null and b/test/ref/aes.zip differ