diff --git a/methods/zipcrypto.js b/methods/zipcrypto.js index 0cf8d32..d089e14 100644 --- a/methods/zipcrypto.js +++ b/methods/zipcrypto.js @@ -1,77 +1,169 @@ +// node crypt, we use it for generate salt +const { randomFillSync } = require("crypto"); + +"use strict"; + // generate CRC32 lookup table -const crctable = (new Uint32Array(256)).map((t,crc)=>{ - for(let j=0;j<8;j++){ - if (0 !== (crc & 1)){ - crc = (crc >>> 1) ^ 0xEDB88320 - }else{ - crc >>>= 1 +const crctable = new Uint32Array(256).map((t, crc) => { + for (let j = 0; j < 8; j++) { + if (0 !== (crc & 1)) { + crc = (crc >>> 1) ^ 0xedb88320; + } else { + crc >>>= 1; } } - return crc>>>0; + return crc >>> 0; }); -function make_decrypter(/*Buffer*/pwd){ - // C-style uInt32 Multiply - const uMul = (a,b) => Math.imul(a, b) >>> 0; - // Initialize keys with default values - const keys = new Uint32Array([0x12345678, 0x23456789, 0x34567890]); - // crc32 byte update - const crc32update = (pCrc32, bval) => { - return crctable[(pCrc32 ^ bval) & 0xff] ^ (pCrc32 >>> 8); +// C-style uInt32 Multiply (discards higher bits, when JS multiply discards lower bits) +const uMul = (a, b) => Math.imul(a, b) >>> 0; + +// crc32 byte single update (actually same function is part of utils.crc32 function :) ) +const crc32update = (pCrc32, bval) => { + return crctable[(pCrc32 ^ bval) & 0xff] ^ (pCrc32 >>> 8); +}; + +// function for generating salt for encrytion header +const genSalt = () => { + if ("function" === typeof randomFillSync) { + return randomFillSync(Buffer.alloc(12)); + } else { + // fallback if function is not defined + return genSalt.node(); } - // update keys with byteValues - const updateKeys = (byteValue) => { - keys[0] = crc32update(keys[0], byteValue); - keys[1] += keys[0] & 0xff; - keys[1] = uMul(keys[1], 134775813) + 1; - keys[2] = crc32update(keys[2], keys[1] >>> 24); +}; + +// salt generation with node random function (mainly as fallback) +genSalt.node = () => { + const salt = Buffer.alloc(12); + const len = salt.length; + for (let i = 0; i < len; i++) salt[i] = (Math.random() * 256) & 0xff; + return salt; +}; + +// general config +const config = { + genSalt +}; + +// Class Initkeys handles same basic ops with keys +function Initkeys(pw) { + const pass = Buffer.isBuffer(pw) ? pw : Buffer.from(pw); + this.keys = new Uint32Array([0x12345678, 0x23456789, 0x34567890]); + for (let i = 0; i < pass.length; i++) { + this.updateKeys(pass[i]); } +} +Initkeys.prototype.updateKeys = function (byteValue) { + const keys = this.keys; + keys[0] = crc32update(keys[0], byteValue); + keys[1] += keys[0] & 0xff; + keys[1] = uMul(keys[1], 134775813) + 1; + keys[2] = crc32update(keys[2], keys[1] >>> 24); + return byteValue; +}; + +Initkeys.prototype.next = function () { + const k = (this.keys[2] | 2) >>> 0; // key + return (uMul(k, k ^ 1) >> 8) & 0xff; // decode +}; + +function make_decrypter(/*Buffer*/ pwd) { // 1. Stage initialize key - const pass = (Buffer.isBuffer(pwd)) ? pwd : Buffer.from(pwd); - for(let i=0; i< pass.length; i++){ - updateKeys(pass[i]); - } + const keys = new Initkeys(pwd); // return decrypter function - return function (/*Buffer*/data){ - if (!Buffer.isBuffer(data)){ - throw 'decrypter needs Buffer' - } + return function (/*Buffer*/ data) { // result - we create new Buffer for results const result = Buffer.alloc(data.length); let pos = 0; // process input data - for(let c of data){ - const k = (keys[2] | 2) >>> 0; // key - c ^= (uMul(k, k^1) >> 8) & 0xff; // decode - result[pos++] = c; // Save Value - updateKeys(c); // update keys with decoded byte + for (let c of data) { + //c ^= keys.next(); + //result[pos++] = c; // decode & Save Value + result[pos++] = keys.updateKeys(c ^ keys.next()); // update keys with decoded byte } return result; - } + }; +} + +function make_encrypter(/*Buffer*/ pwd) { + // 1. Stage initialize key + const keys = new Initkeys(pwd); + + // return encrypting function, result and pos is here so we dont have to merge buffers later + return function (/*Buffer*/ data, /*Buffer*/ result, /* Number */ pos = 0) { + // result - we create new Buffer for results + if (!result) result = Buffer.alloc(data.length); + // process input data + for (let c of data) { + const k = keys.next(); // save key byte + result[pos++] = c ^ k; // save val + keys.updateKeys(c); // update keys with decoded byte + } + return result; + }; } -function decrypt(/*Buffer*/ data, /*Object*/header, /*String, Buffer*/ pwd){ +function decrypt(/*Buffer*/ data, /*Object*/ header, /*String, Buffer*/ pwd) { if (!data || !Buffer.isBuffer(data) || data.length < 12) { return Buffer.alloc(0); } - - // We Initialize and generate decrypting function + + // 1. We Initialize and generate decrypting function const decrypter = make_decrypter(pwd); - // check - for testing password - const check = header.crc >>> 24; - // decrypt salt what is always 12 bytes and is a part of file content - const testbyte = decrypter(data.slice(0, 12))[11]; + // 2. decrypt salt what is always 12 bytes and is a part of file content + const salt = decrypter(data.slice(0, 12)); - // does password meet expectations - if (check !== testbyte){ - throw 'ADM-ZIP: Wrong Password'; + // 3. does password meet expectations + if (salt[11] !== header.crc >>> 24) { + throw "ADM-ZIP: Wrong Password"; } - // decode content + // 4. decode content return decrypter(data.slice(12)); } -module.exports = {decrypt}; +// lets add way to populate salt, NOT RECOMMENDED for production but maybe useful for testing general functionality +function _salter(data) { + if (Buffer.isBuffer(data) && data.length >= 12) { + // be aware - currently salting buffer data is modified + config.genSalt = function () { + return data.slice(0, 12); + }; + } else if (data === "node") { + // test salt generation with node random function + config.genSalt = genSalt.node; + } else { + // if value is not acceptable config gets reset. + config.genSalt = genSalt; + } +} + +function encrypt(/*Buffer*/ data, /*Object*/ header, /*String, Buffer*/ pwd, /*Boolean*/ oldlike = false) { + // 1. test data if data is not Buffer we make buffer from it + if (data == null) data = Buffer.alloc(0); + // if data is not buffer be make buffer from it + if (!Buffer.isBuffer(data)) data = Buffer.from(data.toString()); + + // 2. We Initialize and generate encrypting function + const encrypter = make_encrypter(pwd); + + // 3. generate salt (12-bytes of random data) + const salt = config.genSalt(); + salt[11] = (header.crc >>> 24) & 0xff; + + // old implementations (before PKZip 2.04g) used two byte check + if (oldlike) salt[10] = (header.crc >>> 16) & 0xff; + + // 4. create output + const result = Buffer.alloc(data.length + 12); + encrypter(salt, result); + + // finally encode content + return encrypter(data, result, 12); +} + +module.exports = { decrypt, encrypt, _salter }; diff --git a/test/methods/zipcrypto.test.js b/test/methods/zipcrypto.test.js index 42d6cab..4936a50 100644 --- a/test/methods/zipcrypto.test.js +++ b/test/methods/zipcrypto.test.js @@ -1,6 +1,6 @@ "use strict"; const { expect } = require("chai"); -const { decrypt } = require("../../methods/zipcrypto"); +const { decrypt, encrypt, _salter } = require("../../methods/zipcrypto"); const { crc32 } = require("../../util/utils"); // node crypto @@ -16,12 +16,12 @@ describe("method - zipcrypto decrypt", () => { pwdok: "secret", pwdbad: "Secret", // result - result: Buffer.from("test", "ascii"), + result: Buffer.from("test", "ascii") }; // test invalid input data it("handles invalid data field values / types", () => { - for (const data of [ undefined, null, "str", true, false, 6, Buffer.alloc(4) ]) { + for (const data of [undefined, null, "str", true, false, 6, Buffer.alloc(4)]) { const result = decrypt(data, { crc: source.crc }, source.pwdok); expect(result).to.have.lengthOf(0); } @@ -55,7 +55,71 @@ describe("method - zipcrypto decrypt", () => { expect(result1.compare(source.result)).to.equal(0); // test password, buffer - const result2 = decrypt( source.data, { crc: source.crc }, Buffer.from(source.pwdok, "ascii")); + const result2 = decrypt(source.data, { crc: source.crc }, Buffer.from(source.pwdok, "ascii")); expect(result2.compare(source.result)).to.equal(0); }); }); + +describe("method - zipcrypto encrypt", () => { + const source = { + crc: 0xd87f7e0c, + // data + data_str: "test", + data_buffer: Buffer.from("test", "ascii"), + salt: Buffer.from("xx+OYQ1Pkvo0ztPY", "base64"), + // 16 byte buffer as test source + data: Buffer.from("D1Q5///EbpBY6rHIZXvd3A==", "base64"), + // just data integrity check + pwdok: "secret", + // result + result: Buffer.from("D1Q5///EbpBY6rHIZXvd3A==", "base64") + }; + + // test binary results with known salt + it("test binary results with known salt", () => { + const head = { crc: source.crc }; + // inject known salt + _salter(source.salt); + const result = encrypt(source.data_str, head, source.pwdok, false); + expect(result.compare(source.result)).to.equal(0); + // restore salting + _salter(); + }); + + // test decryption with both password types + it("test encryption and decrytion with node random salt", () => { + const head = { crc: source.crc }; + _salter("node"); + // test password, string + const data_buf = Buffer.from(source.data_str); + const result1 = encrypt(source.data_str, head, source.pwdok, false); + const result2 = decrypt(result1, head, source.pwdok); + expect(result2.compare(data_buf)).to.equal(0); + _salter(); + }); + + // test decryption with both password types + it("test encryption and decrytion with known source data", () => { + const head = { crc: source.crc }; + // test password, string + const data_buf = Buffer.from(source.data_str); + const result1 = encrypt(source.data_str, head, source.pwdok, false); + const result2 = decrypt(result1, head, source.pwdok); + expect(result2.compare(data_buf)).to.equal(0); + }); + + // test how encrytion will handle some random data + it("test encrypting and decryting with some javascript objects", () => { + const tests = [true, null, false, undefined, {}, [], 747, new Date(), [{}]]; + const head = {}; + + for (const test of tests) { + const data_buf = test == null ? Buffer.alloc(0) : Buffer.from(test.toString()); + head.crc = crc32(data_buf); + + const result1 = encrypt(test, head, source.pwdok, false); + const result2 = decrypt(result1, head, source.pwdok); + expect(result2.compare(data_buf)).to.equal(0); + } + }); +});