diff --git a/lib/object.js b/lib/object.js index c22672a8..13ac2f72 100644 --- a/lib/object.js +++ b/lib/object.js @@ -347,8 +347,7 @@ var generateCompressedObjectFrom = function(file, compression) { * @return {object} the zip parts. */ var generateZipParts = function(name, file, compressedObject, offset) { - var data = compressedObject.compressedContent, - utfEncodedFileName = this.utf8encode(file.name), + var utfEncodedFileName = this.utf8encode(file.name), useUTF8 = utfEncodedFileName !== file.name, o = file.options, dosTime, @@ -751,96 +750,7 @@ var out = { * http://www.webtoolkit.info/ * */ - crc32: function crc32(input, crc) { - if (typeof input === "undefined" || !input.length) { - return 0; - } - - var isArray = utils.getTypeOf(input) !== "string"; - - var table = [ - 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, - 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, - 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, - 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, - 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, - 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, - 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, - 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, - 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, - 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, - 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, - 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, - 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, - 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, - 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, - 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, - 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, - 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, - 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, - 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, - 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, - 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, - 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, - 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, - 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, - 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, - 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, - 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, - 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, - 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, - 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, - 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, - 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, - 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, - 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, - 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, - 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, - 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, - 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, - 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, - 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, - 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, - 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, - 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, - 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, - 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, - 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, - 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, - 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, - 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, - 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, - 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, - 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, - 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, - 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, - 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, - 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, - 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, - 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, - 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, - 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, - 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, - 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, - 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D]; - - if (typeof(crc) == "undefined") { - crc = 0; - } - var x = 0; - var y = 0; - var b = 0; - - crc = crc ^ (-1); - for (var i = 0, iTop = input.length; i < iTop; i++) { - b = isArray ? input[i] : input.charCodeAt(i); - y = (crc ^ b) & 0xFF; - x = table[y]; - crc = (crc >>> 8) ^ x; - } - - return crc ^ (-1); - }, + crc32: utils.crc32, // Inspired by http://my.opera.com/GreyWyvern/blog/show.dml/1725165 diff --git a/lib/utils.js b/lib/utils.js index 219c53f9..73af6fe9 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -349,3 +349,95 @@ exports.isRegExp = function (object) { return Object.prototype.toString.call(object) === "[object RegExp]"; }; +exports.crcTable = [ + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, + 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, + 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, + 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, + 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, + 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, + 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, + 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, + 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, + 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, + 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, + 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, + 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, + 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, + 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, + 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, + 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, + 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, + 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, + 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, + 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, + 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, + 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, + 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, + 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, + 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, + 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, + 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, + 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, + 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, + 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, + 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, + 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D]; + +exports.crc32 = function crc32(input, crc) { + if (typeof input === "undefined" || !input.length) { + return 0; + } + + var isArray = exports.getTypeOf(input) !== "string"; + + var table = exports.crcTable; + + if (typeof(crc) == "undefined") { + crc = 0; + } + var x = 0; + var y = 0; + var b = 0; + + crc = crc ^ (-1); + for (var i = 0, iTop = input.length; i < iTop; i++) { + b = isArray ? input[i] : input.charCodeAt(i); + y = (crc ^ b) & 0xFF; + x = table[y]; + crc = (crc >>> 8) ^ x; + } + + return crc ^ (-1); +}; \ No newline at end of file diff --git a/lib/zipCrypto.js b/lib/zipCrypto.js new file mode 100644 index 00000000..10c22fb1 --- /dev/null +++ b/lib/zipCrypto.js @@ -0,0 +1,159 @@ +'use strict'; +var utils = require('./utils'); + +/** + * Based on the algorithm described in + * http://www.pkware.com/documents/casestudies/APPNOTE.TXT + */ + +var ZipCrypto = function(loadOptions) { + this.encryptionKeys = null; + this.retrievePasswordCallback = loadOptions.retrievePasswordCallback; + this.retrieveEncryptionKeysCallback = loadOptions.retrieveEncryptionKeysCallback; + this.invalidPasswordCallback = loadOptions.invalidPasswordCallback; +}; + +/** + * Requests password/encryption keys (once), primes the encryption keys, if necessary, + * and returns them to the caller. + * @return {number[]} An array of 3 32-bit numeric pre-primed encryption keys + * @throws {Error} if the password callbacks are not set or return incorrect data types + */ +ZipCrypto.prototype.getEncryptionKeys = function() { + if (!this.encryptionKeys) { + if (typeof this.retrievePasswordCallback != "function" && + typeof this.retrieveEncryptionKeysCallback != "function") { + throw new Error("retrievePasswordCallback or retrieveEncryptionKeysCallback must be set for encrypted ZIP files"); + } + + if (this.retrieveEncryptionKeysCallback) { + var keys = this.retrieveEncryptionKeysCallback(); + if(!(keys instanceof Array)) throw new Error("retrieveEncryptionKeysCallback must return an array of encryption keys"); + if(keys.length != 3) throw new Error("retrieveEncryptionKeysCallback must return an array of 3 encryption keys"); + for(var i=0; i>> 24) && + !this.invalidPassword(data.subarray(0,12), crc32) ) { + return; + } + } + } + return data.subarray(12); // First 12 bytes are encryption header +}; + +/** + * Updates a given CRC32 hash with an additional byte + * @param {number} crc CRC32 hash as a 32-bit number + * @param {number} b A single byte + * @return {number} Updated CRC32 hash + */ +ZipCrypto.crc32Byte = function(crc, b) { + var x = utils.crcTable[(crc ^ b) & 0xFF]; + return (crc >>> 8) ^ x; +}; + +/** + * Updates encyption keys + * @param {number[]} keys Encryption keys to be updated + * @param {number} b A single byte + */ +function updateKeys(keys, b) { + keys[0] = ZipCrypto.crc32Byte(keys[0], b); + keys[1] = u32Multiply(keys[1] + (keys[0] & 0xFF), 0x08088405) + 1; + keys[2] = ZipCrypto.crc32Byte(keys[2], keys[1] >> 24); +} + +/** + * Decrypts a single byte of data + * @param {number[]} keys Encryption keys + * @param {number} b A single byte + * @return {number} Decrypted byte + */ +function decryptByte(keys, b) { + var tmp = keys[2] | 2; + b = b ^ (u32Multiply(tmp,tmp ^ 1) >> 8); + updateKeys(keys, b); + return b; +} + +/** + * Performs 32-bit multiplication. + * Discards overflow bits and maintains accuracy for the low significance bits + * @param {number} a + * @param {number} b + * @return 32-bit product of a and b + */ +function u32Multiply(a, b) { + // We have a 52 bit mantissa, so we can safely multiply 32 bit and 16 bit + // numbers without losing accuracy (result cannot be more than 48 bits) + var a1 = a >>> 16; // MSB 16 bits + var a2 = a & 0xFFFF; // LSB 16 bits + // a1 and a2 are always positive here + b = b >>> 0; // Truncate MSBs past 32 bits + + return ( ( (b * a1) << 16 >>> 0) + b * a2 ) >>> 0; // Don't return negative numbers +} + +module.exports = ZipCrypto; \ No newline at end of file diff --git a/lib/zipEntries.js b/lib/zipEntries.js index b0f38e78..948b05d7 100644 --- a/lib/zipEntries.js +++ b/lib/zipEntries.js @@ -6,6 +6,7 @@ var utils = require('./utils'); var sig = require('./signature'); var ZipEntry = require('./zipEntry'); var support = require('./support'); +var ZipCrypto = require('./zipCrypto'); // class ZipEntries {{{ /** * All the entries in the zip file. @@ -16,6 +17,14 @@ var support = require('./support'); function ZipEntries(data, loadOptions) { this.files = []; this.loadOptions = loadOptions; + + if (ZipCrypto) { + var zc = new ZipCrypto(loadOptions); + this.loadOptions.decrypt = function() { + return zc.decryptData.apply(zc, arguments); + }; + } + if (data) { this.load(data); } diff --git a/lib/zipEntry.js b/lib/zipEntry.js index 8f56e809..33b8d041 100644 --- a/lib/zipEntry.js +++ b/lib/zipEntry.js @@ -20,9 +20,17 @@ ZipEntry.prototype = { * @return {boolean} true if the file is encrypted, false otherwise. */ isEncrypted: function() { - // bit 1 is set + // bit 0 is set return (this.bitFlag & 0x0001) === 0x0001; }, + /** + * say if the file uses PKWARE's strong encryption + * @return {boolean} true if the file is encryted using strong encryption, false otherwise + */ + isStrongEncryption: function() { + // bit 6 is set + return (this.bitFlag & 0x0040) === 0x0040; + }, /** * say if the file has utf-8 filename/comment. * @return {boolean} true if the filename/comment is in utf-8, false otherwise. @@ -61,6 +69,9 @@ ZipEntry.prototype = { return function() { var compressedFileData = utils.transformTo(compression.uncompressInputType, this.getCompressedContent()); + if (this.encrypted) { + compressedFileData = this.decrypt(compressedFileData, this.crc32); + } var uncompressedFileData = compression.uncompress(compressedFileData); if (uncompressedFileData.length !== uncompressedSize) { @@ -73,6 +84,7 @@ ZipEntry.prototype = { /** * Read the local part of a zip file and add the info in this object. * @param {DataReader} reader the reader to use. + * Pointer should be just past the local file header signature */ readLocalPart: function(reader) { var compression, localExtraFieldsLength; @@ -98,7 +110,8 @@ ZipEntry.prototype = { localExtraFieldsLength = reader.readInt(2); // can't be sure this will be the same as the central dir this.fileName = reader.readString(this.fileNameLength); reader.skip(localExtraFieldsLength); - + + this.encrypted = this.isEncrypted(); if (this.compressedSize == -1 || this.uncompressedSize == -1) { throw new Error("Bug or corrupted zip : didn't get enough informations from the central directory " + "(compressedSize == -1 || uncompressedSize == -1)"); } @@ -112,6 +125,11 @@ ZipEntry.prototype = { this.decompressed.uncompressedSize = this.uncompressedSize; this.decompressed.crc32 = this.crc32; this.decompressed.compressionMethod = this.compressionMethod; + this.decompressed.encrypted = this.encrypted; + if (this.encrypted) { + if(!this.loadOptions.decrypt) throw new Error("ZipCrypto library is required to read encrypted files"); + this.decompressed.decrypt = this.loadOptions.decrypt; + } this.decompressed.getCompressedContent = this.prepareCompressedContent(reader, reader.index, this.compressedSize, compression); this.decompressed.getContent = this.prepareContent(reader, reader.index, this.compressedSize, compression, this.uncompressedSize); @@ -145,8 +163,8 @@ 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() && this.isStrongEncryption()) { + throw new Error("ZIP files using Strong Encryption are not supported"); } this.fileName = reader.readString(this.fileNameLength); diff --git a/test/ref/encrypted.zip b/test/ref/encrypted.zip index 632a988b..c5d5fd22 100644 Binary files a/test/ref/encrypted.zip and b/test/ref/encrypted.zip differ diff --git a/test/test.js b/test/test.js index 0e076526..699b5f2c 100644 --- a/test/test.js +++ b/test/test.js @@ -911,20 +911,8 @@ test("unknown compression throws an exception", function () { ok(true, "an exception were thrown"); } }); -// }}} More advanced - -QUnit.module("Load file, not supported features"); // {{{ -// zip -0 -X -e encrypted.zip Hello.txt -testZipFile("basic encryption", "ref/encrypted.zip", function(file) { - try { - var zip = new JSZip(file); - ok(false, "Encryption is not supported, but no exception were thrown"); - } catch(e) { - equal(e.message, "Encrypted zip are not supported", "the error message is useful"); - } -}); -// }}} Load file, not supported features +// }}} More advanced QUnit.module("Load file, corrupted zip"); // {{{ @@ -1134,6 +1122,31 @@ test("A folder stays a folder", function () { ok(reloaded.files['folder/'].options.dir, "the folder is marked as a folder"); }); +// zip -0 -X -e encrypted.zip Hello.txt (password: test) +testZipFile("ZipCrypto encryption", "ref/encrypted.zip", function(file) { + var zip = new JSZip(file, { + retrievePasswordCallback: function() { + return 'test' + } + }); + equal(zip.file("Hello.txt").asText(), "Hello World\n", "ZipCrypto-encrypted file was correctly read."); +}); + +// zip -0 -X -e encrypted.zip Hello.txt (password: test) +testZipFile("ZipCrypto encryption: wrong password", "ref/encrypted.zip", function(file) { + var zip = new JSZip(file, { + retrievePasswordCallback: function() { + return 'wrong' + } + }); + try { + zip.file("Hello.txt").asText(); + ok(false, "no exception was thrown"); + } catch(e) { + ok(e.message.match("Supplied password is invalid"), "expected exception was thrown"); + } +}); + // }}} Load file QUnit.module("Load complex files"); // {{{