From 9cf3e9a8efd225c218aa6bd7d01696c6f90e46cf Mon Sep 17 00:00:00 2001 From: Mike Wiacek Date: Mon, 29 Mar 2021 16:27:26 +0000 Subject: [PATCH 1/9] Backport zip64 extra field improvements from Go's stdlib. This fix allows zip to read archives that include files larger than 4GB and/or those that use zip64 extra fields. This is simply a backport of fixes that were made in http://golang.org/issue/13367. --- reader.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/reader.go b/reader.go index 48a7c17..981aa36 100644 --- a/reader.go +++ b/reader.go @@ -281,6 +281,10 @@ func readDirectoryHeader(f *File, r io.Reader) error { f.Extra = d[filenameLen : filenameLen+extraLen] f.Comment = string(d[filenameLen+extraLen:]) + needUSize := f.UncompressedSize == ^uint32(0) + needCSize := f.CompressedSize == ^uint32(0) + needHeaderOffset := f.headerOffset == int64(^uint32(0)) + if len(f.Extra) > 0 { b := readBuf(f.Extra) for len(b) >= 4 { // need at least tag and size @@ -290,16 +294,32 @@ func readDirectoryHeader(f *File, r io.Reader) error { return ErrFormat } eb := readBuf(b[:size]) + switch tag { case zip64ExtraId: - // update directory values from the zip64 extra block - if len(eb) >= 8 { + // update directory values from the zip64 extra block. + // They should only be consulted if the sizes read earlier + // are maxed out. + // See golang.org/issue/13367. + if needUSize { + needUSize = false + if len(eb) < 8 { + return ErrFormat + } f.UncompressedSize64 = eb.uint64() } - if len(eb) >= 8 { + if needCSize { + needCSize = false + if len(eb) < 8 { + return ErrFormat + } f.CompressedSize64 = eb.uint64() } - if len(eb) >= 8 { + if needHeaderOffset { + needHeaderOffset = false + if len(eb) < 8 { + return ErrFormat + } f.headerOffset = int64(eb.uint64()) } case winzipAesExtraId: From 7dde4a9554346c8a90c1fa0570b77f37ff97d81d Mon Sep 17 00:00:00 2001 From: Andy Walker Date: Tue, 4 Nov 2025 03:53:31 +0000 Subject: [PATCH 2/9] regression test: SectionReader length for ZipCryptoReader --- reader_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/reader_test.go b/reader_test.go index 547dd39..3b37a97 100644 --- a/reader_test.go +++ b/reader_test.go @@ -605,3 +605,56 @@ func TestIssue11146(t *testing.T) { } r.Close() } + +// Verify that the length is correct when using zipcrypto (standard encryption) +func TestZipCryptoLength(t *testing.T) { + contents := []byte{0} + password := "password" + buf := new(bytes.Buffer) + zipw := NewWriter(buf) + w, err := zipw.Encrypt("test.txt", password, StandardEncryption) + if err != nil { + t.Fatal("Encrypt():", err) + } + if _, err := io.Copy(w, bytes.NewReader(contents)); err != nil { + t.Fatal("io.Copy():", err) + } + zipw.Close() + zipr, err := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) + if err != nil { + t.Fatal("NewReader():", err) + } + f := zipr.File[0] + if !f.IsEncrypted() { + t.Fatal("Expected file to be encrypted") + } + f.SetPassword(password) + + bodyOffset, err := f.findBodyOffset() + if err != nil { + t.Fatal("findBodyOffset():", err) + } + size := int64(f.CompressedSize64) + sr := io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size) + r, err := ZipCryptoDecryptor(sr, []byte(password)) + if err != nil { + t.Fatal("ZipCryptoDecryptor():", err) + } + + // SectionReader should have Size() = (encrypted_size - 12 byte header) + expectedSize := size - 12 + + if r.Size() != expectedSize { + t.Errorf("SeectionReader.Size() incorrect - want: %d, got: %d", expectedSize, r.Size()) + } + + readBuf := make([]byte, r.Size()) + n, err := io.ReadFull(r, readBuf) + if err != nil { + t.Errorf("ReadFull(): unexpected error: %v", err) + } + + if n != int(r.Size()) { + t.Errorf("ReadFull(): expected promised %d bytes, got: %d", r.Size(), n) + } +} From 1b0f894e6316443db9d8124a29c6209d6189ea90 Mon Sep 17 00:00:00 2001 From: Andy Walker Date: Tue, 4 Nov 2025 03:54:27 +0000 Subject: [PATCH 3/9] fix: length bug for ZipCryptoReader SectionReader --- zipcrypto.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zipcrypto.go b/zipcrypto.go index 309bc32..1d1b7da 100644 --- a/zipcrypto.go +++ b/zipcrypto.go @@ -73,7 +73,8 @@ func ZipCryptoDecryptor(r *io.SectionReader, password []byte) (*io.SectionReader r.Read(b) m := z.Decrypt(b) - return io.NewSectionReader(bytes.NewReader(m), 12, int64(len(m))), nil + return io.NewSectionReader(bytes.NewReader(m), 12, int64(len(m)-12)), nil +} } type zipCryptoWriter struct { From a9809adc0ce03baa93ff8477895cc796cbae25e2 Mon Sep 17 00:00:00 2001 From: Andy Walker Date: Tue, 4 Nov 2025 03:55:14 +0000 Subject: [PATCH 4/9] regression test: ensure all encryption methods properly test for bad passwords --- reader_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/reader_test.go b/reader_test.go index 3b37a97..cd91af8 100644 --- a/reader_test.go +++ b/reader_test.go @@ -658,3 +658,73 @@ func TestZipCryptoLength(t *testing.T) { t.Errorf("ReadFull(): expected promised %d bytes, got: %d", r.Size(), n) } } + +// Ensure that password verification works. +func TestPasswordVerification(t *testing.T) { + contents := []byte("Hello World") + goodPassword := "password" + + tryPasswords := []string{ + "badpassword1", + "badpassword2", + goodPassword, + } + + type PWTest struct { + methodName string + method EncryptionMethod + } + + for _, pwTest := range []PWTest{ + {"StandardEncryption", StandardEncryption}, + {"AES128Encryption", AES128Encryption}, + {"AES192Encryption", AES192Encryption}, + {"AES256Encryption", AES256Encryption}, + } { + t.Run(pwTest.methodName, func(t *testing.T) { + raw := new(bytes.Buffer) + zipw := NewWriter(raw) + w, err := zipw.Encrypt("hello.txt", "password", pwTest.method) + if err != nil { + t.Fatal("Encrypt():", err) + } + _, err = io.Copy(w, bytes.NewReader(contents)) + if err != nil { + t.Fatal("io.Copy():", err) + } + zipw.Close() + + zipr, err := NewReader(bytes.NewReader(raw.Bytes()), int64(raw.Len())) + if err != nil { + t.Fatal("NewReader():", err) + } + f := zipr.File[0] + if !f.IsEncrypted() { + t.Fatal("Expected file to be encrypted") + } + var rc io.ReadCloser + for _, password := range tryPasswords { + f.SetPassword(password) + rc, err = f.Open() + if password == goodPassword { + if err != nil { + t.Fatal("Expect nil error for good password - got:", err) + } + } else { + t.Log("bad password:", password) + if !errors.Is(err, ErrPassword) { + t.Error("Expected ErrPassword for bad password - got:", err) + } + } + } + got, err := io.ReadAll(rc) + if err != nil { + t.Fatal("ReadALL():", err) + } + if !bytes.Equal(got, contents) { + t.Errorf("File contents different - want: %q, got %q", contents, got) + } + }) + + } +} From 7d1894791f466b707c794e8b9ba7af0d38a101a3 Mon Sep 17 00:00:00 2001 From: Andy Walker Date: Tue, 4 Nov 2025 03:58:22 +0000 Subject: [PATCH 5/9] fix: deprecate ZipCryptoDecryptor and replace with compliant internal version - Fix ZipCrypto readers to correctly verify password using high byte of ModifiedTime or CRC (for legacy archives) - Add deprecation note and recommend Open, which calls the new, correct function. --- reader.go | 3 +-- zipcrypto.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/reader.go b/reader.go index 981aa36..39766f2 100644 --- a/reader.go +++ b/reader.go @@ -145,9 +145,8 @@ func (f *File) Open() (rc io.ReadCloser, err error) { rr := io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size) // check for encryption if f.IsEncrypted() { - if f.ae == 0 { - if r, err = ZipCryptoDecryptor(rr, f.password()); err != nil { + if r, err = newZipCryptoReader(rr, f); err != nil { return } } else if r, err = newDecryptionReader(rr, f); err != nil { diff --git a/zipcrypto.go b/zipcrypto.go index 1d1b7da..4b819ec 100644 --- a/zipcrypto.go +++ b/zipcrypto.go @@ -66,6 +66,10 @@ func crc32update(pCrc32 uint32, bval byte) uint32 { return crc32.IEEETable[(pCrc32 ^ uint32(bval)) & 0xff] ^ (pCrc32 >> 8) } +// ZipCryptoDecryptor decrypts a section reader with standard zip decryption +// +// Deprecated: ZipCryptoDecryptor exists for historical compatibility and does not do any password validation. Please +// use [Open] instead, which will automatically detect a ZipCrypto-encrypted file and check the password automatically. func ZipCryptoDecryptor(r *io.SectionReader, password []byte) (*io.SectionReader, error) { z := NewZipCrypto(password) b := make([]byte, r.Size()) @@ -75,6 +79,36 @@ func ZipCryptoDecryptor(r *io.SectionReader, password []byte) (*io.SectionReader m := z.Decrypt(b) return io.NewSectionReader(bytes.NewReader(m), 12, int64(len(m)-12)), nil } + +func newZipCryptoReader(r *io.SectionReader, f *File) (io.Reader, error) { + z := NewZipCrypto(f.password()) + b := make([]byte, r.Size()) + r.Read(b) + + m := z.Decrypt(b) + + // ZipCrypto password verification is done by comparing byte 11 of the decrypted encryption header with either the + // high byte of ModifiedTime (used most implementation) or the high byte of the CRC value (antiquated, but retained + // for compatibility). + var expectedByte byte + + // If bit 3 is set in the general purpose bitflags (almost always), there's no legacy CRC, and we use the high byte + // of ModifiedTime for password verification. + if f.Flags&0x8 != 0 { + expectedByte = byte(f.ModifiedTime >> 8) + } else { + // Otherwise use the high byte of the CRC for legacy compatibility. + if f.CRC32 == 0 { + return nil, ErrFormat + } + expectedByte = byte(f.CRC32 >> 24) + } + + if m[11] != expectedByte { + return nil, ErrPassword + } + + return io.NewSectionReader(bytes.NewReader(m), 12, int64(len(m)-12)), nil } type zipCryptoWriter struct { From 9027ec74aa03a7f59d1b0acc0fa9154c7612c130 Mon Sep 17 00:00:00 2001 From: Andy Walker Date: Tue, 4 Nov 2025 03:58:42 +0000 Subject: [PATCH 6/9] chore: errors import --- reader_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/reader_test.go b/reader_test.go index cd91af8..e9c9ee4 100644 --- a/reader_test.go +++ b/reader_test.go @@ -8,6 +8,7 @@ import ( "bytes" "encoding/binary" "encoding/hex" + "errors" "io" "io/ioutil" "os" From c0051c13da8c9f593fa7cded1eb3832b6486c5b8 Mon Sep 17 00:00:00 2001 From: Andy Walker Date: Tue, 4 Nov 2025 03:59:03 +0000 Subject: [PATCH 7/9] testfix: wrong check logic --- reader_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reader_test.go b/reader_test.go index e9c9ee4..e4b7695 100644 --- a/reader_test.go +++ b/reader_test.go @@ -579,7 +579,7 @@ func TestIssue10956(t *testing.T) { "0000\v\x00000\x00\x00\x00\x00\x00\x00\x000") _, err := NewReader(bytes.NewReader(data), int64(len(data))) const want = "TOC declares impossible 3472328296227680304 files in 57 byte" - if err == nil && !strings.Contains(err.Error(), want) { + if err == nil || !strings.Contains(err.Error(), want) { t.Errorf("error = %v; want %q", err, want) } } From bc28cd8e7aec44f99617e9b3f5727cd51e77fa1f Mon Sep 17 00:00:00 2001 From: Andy Walker Date: Tue, 4 Nov 2025 04:00:52 +0000 Subject: [PATCH 8/9] chore: gofmt --- zipcrypto.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/zipcrypto.go b/zipcrypto.go index 4b819ec..e74604c 100644 --- a/zipcrypto.go +++ b/zipcrypto.go @@ -1,14 +1,14 @@ package zip import ( - "io" "bytes" "hash/crc32" + "io" ) type ZipCrypto struct { password []byte - Keys [3]uint32 + Keys [3]uint32 } func NewZipCrypto(passphrase []byte) *ZipCrypto { @@ -29,10 +29,10 @@ func (z *ZipCrypto) init() { } func (z *ZipCrypto) updateKeys(byteValue byte) { - z.Keys[0] = crc32update(z.Keys[0], byteValue); - z.Keys[1] += z.Keys[0] & 0xff; - z.Keys[1] = z.Keys[1] * 134775813 + 1; - z.Keys[2] = crc32update(z.Keys[2], (byte) (z.Keys[1] >> 24)); + z.Keys[0] = crc32update(z.Keys[0], byteValue) + z.Keys[1] += z.Keys[0] & 0xff + z.Keys[1] = z.Keys[1]*134775813 + 1 + z.Keys[2] = crc32update(z.Keys[2], (byte)(z.Keys[1]>>24)) } func (z *ZipCrypto) magicByte() byte { @@ -55,7 +55,7 @@ func (z *ZipCrypto) Decrypt(chiper []byte) []byte { length := len(chiper) plain := make([]byte, length) for i, c := range chiper { - v := c ^ z.magicByte(); + v := c ^ z.magicByte() z.updateKeys(v) plain[i] = v } @@ -63,7 +63,7 @@ func (z *ZipCrypto) Decrypt(chiper []byte) []byte { } func crc32update(pCrc32 uint32, bval byte) uint32 { - return crc32.IEEETable[(pCrc32 ^ uint32(bval)) & 0xff] ^ (pCrc32 >> 8) + return crc32.IEEETable[(pCrc32^uint32(bval))&0xff] ^ (pCrc32 >> 8) } // ZipCryptoDecryptor decrypts a section reader with standard zip decryption @@ -137,8 +137,8 @@ func (z *zipCryptoWriter) Write(p []byte) (n int, err error) { return } -func ZipCryptoEncryptor(i io.Writer, pass passwordFn, fw *fileWriter) (io.Writer, error) { +func ZipCryptoEncryptor(i io.Writer, pass passwordFn, fw *fileWriter) (io.Writer, error) { z := NewZipCrypto(pass()) zc := &zipCryptoWriter{i, z, true, fw} return zc, nil -} \ No newline at end of file +} From 023c607323e1dc91ad4797e182f1e6aef7323179 Mon Sep 17 00:00:00 2001 From: Andy Walker Date: Tue, 4 Nov 2025 04:03:47 +0000 Subject: [PATCH 9/9] chore: make module --- example_test.go | 2 +- go.mod | 5 +++++ go.sum | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 go.mod create mode 100644 go.sum diff --git a/example_test.go b/example_test.go index dce27ee..dec841b 100644 --- a/example_test.go +++ b/example_test.go @@ -11,7 +11,7 @@ import ( "log" "os" - "github.com/yeka/zip" + "github.com/mikewiacek/zip" ) func ExampleWriter() { diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..465a594 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/mikewiacek/zip + +go 1.24.6 + +require golang.org/x/crypto v0.43.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2f7f61e --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=