From e1cddb33fdc94a1b4e210d8c22e1d3ffc6a6ad53 Mon Sep 17 00:00:00 2001 From: Luke Malcolm Date: Tue, 5 Mar 2024 23:01:08 +1300 Subject: [PATCH 1/2] Add support for Info-Zip password check spec for ZipCrypto. (Uses high byte of header modified time, rather than crc). Updates current tests to handle. --- headers/entryHeader.js | 4 +++- methods/zipcrypto.js | 8 ++++++-- test/methods/zipcrypto.test.js | 22 ++++++++++++++++++---- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/headers/entryHeader.js b/headers/entryHeader.js index 572b9a7..051d389 100644 --- a/headers/entryHeader.js +++ b/headers/entryHeader.js @@ -83,7 +83,9 @@ module.exports = function () { set time(val) { setTime(val); }, - + get timeHighByte() { + return (_time >>> 8) & 0xff; + }, get crc() { return _crc; }, diff --git a/methods/zipcrypto.js b/methods/zipcrypto.js index 701b5ce..79768f4 100644 --- a/methods/zipcrypto.js +++ b/methods/zipcrypto.js @@ -118,8 +118,12 @@ function decrypt(/*Buffer*/ data, /*Object*/ header, /*String, Buffer*/ pwd) { // 2. decrypt salt what is always 12 bytes and is a part of file content const salt = decrypter(data.slice(0, 12)); - // 3. does password meet expectations - if (salt[11] !== header.crc >>> 24) { + // if bit 3 (0x08) of the general-purpose flags field is set, check salt[11] with the high byte of the header time + // 2 byte data block (as per Info-Zip spec), otherwise check with the high byte of the header entry + const verifyByte = ((header.flags & 0x8) === 0x8) ? header.timeHighByte : header.crc >>> 24; + + //3. does password meet expectations + if (salt[11] !== verifyByte) { throw "ADM-ZIP: Wrong Password"; } diff --git a/test/methods/zipcrypto.test.js b/test/methods/zipcrypto.test.js index 4936a50..de63476 100644 --- a/test/methods/zipcrypto.test.js +++ b/test/methods/zipcrypto.test.js @@ -15,6 +15,9 @@ describe("method - zipcrypto decrypt", () => { md5: "wYHjota6dQNazueWO9/uDg==", pwdok: "secret", pwdbad: "Secret", + flagsencrypted: 0x01, + flagsinfozipencrypted: 0x09, + timeHighByte: 0xD8, // result result: Buffer.from("test", "ascii") }; @@ -40,22 +43,33 @@ describe("method - zipcrypto decrypt", () => { // is error thrown if invalid password was provided it("should throw if invalid password is provided", () => { expect(function badpassword() { - decrypt(source.data, { crc: source.crc }, source.pwdbad); + decrypt(source.data, { crc: source.crc, flags: source.flagsencrypted }, source.pwdbad); }).to.throw(); expect(function okpassword() { - decrypt(source.data, { crc: source.crc }, source.pwdok); + decrypt(source.data, { crc: source.crc, flags: source.flagsencrypted }, source.pwdok); + }).to.not.throw(); + }); + + // is error thrown if invalid password was provided + it("should throw if invalid password is provided for Info-Zip bit 3 flag", () => { + expect(function badpassword() { + decrypt(source.data, { crc: source.crc, flags: source.flagsinfozipencrypted, timeHighByte: source.timeHighByte }, source.pwdbad); + }).to.throw(); + + expect(function okpassword() { + decrypt(source.data, { crc: source.crc, flags: source.flagsinfozipencrypted, timeHighByte: source.timeHighByte }, source.pwdok); }).to.not.throw(); }); // test decryption with both password types it("test decrypted data with password", () => { // test password, string - const result1 = decrypt(source.data, { crc: source.crc }, source.pwdok); + const result1 = decrypt(source.data, { crc: source.crc, flags: source.flagsencrypted }, source.pwdok); 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, flags: source.flagsencrypted }, Buffer.from(source.pwdok, "ascii")); expect(result2.compare(source.result)).to.equal(0); }); }); From 1bfa3bb76257f4b35210f97008f0369774358592 Mon Sep 17 00:00:00 2001 From: Luke Malcolm Date: Tue, 5 Mar 2024 23:03:07 +1300 Subject: [PATCH 2/2] Additional test for Info-Zip generated ZipCrypto encrypted file. Relates to Issue 471. --- test/assets/issue-471-infozip-encrypted.zip | Bin 0 -> 331 bytes test/issue_471/infozip-password.test.js | 35 ++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 test/assets/issue-471-infozip-encrypted.zip create mode 100644 test/issue_471/infozip-password.test.js diff --git a/test/assets/issue-471-infozip-encrypted.zip b/test/assets/issue-471-infozip-encrypted.zip new file mode 100644 index 0000000000000000000000000000000000000000..7cbbf3edf9cb3f63d31b83be8c850859a75451a3 GIT binary patch literal 331 zcmWIWW@Zs#;AG%n$Xk&bv312uE-wZKhN(cz$&ka4Qkt7vsaH}_66_MrummUwRM+3K z>SS_CLZ(1sLV`g;1B1LmS_-Riq-&$g-TD5KZ~k1hmR7wwBhKjY^yH+DgakJ4Zi7GR zQ_>SsT2_cM1q%rns&&j@nh?OSIaJ|M#TizH&=6h*_6rxErG{R4mbw;5OE2i!9Cp~= z&Pm2uOE0F7W7n}@AqC+XV%CTMI7wwa-}~ZJ4#N~~h7a@KKab71V;H(r`ToSL{WrRp zE2=ByG(=QpI`g-8b~yen?F#T_=Ro#SfHxzPI5#fu3WEH{z{sG$u&oiqLh?Kd*z=(w WtPqz5c(bxW)G{)702yyV90mY?DQS=Z literal 0 HcmV?d00001 diff --git a/test/issue_471/infozip-password.test.js b/test/issue_471/infozip-password.test.js new file mode 100644 index 0000000..38d536f --- /dev/null +++ b/test/issue_471/infozip-password.test.js @@ -0,0 +1,35 @@ +"use strict"; + +// Tests for github issue 471: https://github.com/cthackers/adm-zip/issues/471 + +const assert = require("assert"); +const path = require("path"); +const Zip = require("../../adm-zip"); + +describe("decryption with info-zip spec password check", () => { + + + // test decryption with both password types + it("test decrypted data with password", () => { + // the issue-471-infozip-encrypted.zip file has been generated with Info-Zip Zip 2.32, but the Info-Zip + // standard is used by other zip generators as well. + const infoZip = new Zip(path.join(__dirname, "../assets/issue-471-infozip-encrypted.zip")); + const entries = infoZip.getEntries(); + assert(entries.length === 1, "Good: Test archive contains exactly 1 file"); + + const testFile = entries.filter(function (entry) { + return entry.entryName === "dummy.txt"; + }); + assert(testFile.length === 1, "Good: dummy.txt file exists as archive entry"); + + const readData = entries[0].getData('secret'); + assert(readData.toString('utf8').startsWith('How much wood could a woodchuck chuck'), "Good: buffer matches expectations"); + + // assert that the following call throws an exception + assert.throws(() => { + const readDataBad = entries[0].getData('badpassword'); + }, "Good: error thrown for bad password"); + + }); +}); +