diff --git a/README.md b/README.md new file mode 100755 index 0000000..9cad5fd --- /dev/null +++ b/README.md @@ -0,0 +1,149 @@ +This fork add support for Standard Zip Encryption. + +The work is based on https://github.com/alexmullins/zip + +Available encryption: +``` +zip.StandardEncryption +zip.AES128Encryption +zip.AES192Encryption +zip.AES256Encryption +``` + +## Warning + +Zip Standard Encryption isn't actually secure. +Unless you have to work with it, please use AES encryption instead. + +## Example Encrypt Zip + +``` +package main + +import ( + "bytes" + "io" + "log" + "os" + + "github.com/yeka/zip" +) + +func main() { + contents := []byte("Hello World") + fzip, err := os.Create(`./test.zip`) + if err != nil { + log.Fatalln(err) + } + zipw := zip.NewWriter(fzip) + defer zipw.Close() + w, err := zipw.Encrypt(`test.txt`, `golang`, zip.AES256Encryption) + if err != nil { + log.Fatal(err) + } + _, err = io.Copy(w, bytes.NewReader(contents)) + if err != nil { + log.Fatal(err) + } + zipw.Flush() +} +``` + +## Example Decrypt Zip + +``` +package main + +import ( + "fmt" + "io/ioutil" + "log" + + "github.com/yeka/zip" +) + +func main() { + r, err := zip.OpenReader("encrypted.zip") + if err != nil { + log.Fatal(err) + } + defer r.Close() + + for _, f := range r.File { + if f.IsEncrypted() { + f.SetPassword("12345") + } + + r, err := f.Open() + if err != nil { + log.Fatal(err) + } + + buf, err := ioutil.ReadAll(r) + if err != nil { + log.Fatal(err) + } + defer r.Close() + + fmt.Printf("Size of %v: %v byte(s)\n", f.Name, len(buf)) + } +} +``` + +### fileheader 设置文件的编码格式 + +``` + +func StepDealZip(tmpFilesForZip []string, zipFilePath string, job YellowDogJob) (err error) { + archive, err := os.Create(zipFilePath) + if err != nil { + return err + } + defer archive.Close() + zipWriter := zip.NewWriter(archive) + for _, file := range tmpFilesForZip { + fileInfo, err := os.Stat(file) + if err != nil { + return err + } + body, err := ioutil.ReadFile(file) + if err != nil { + return err + } + var filename string + fileNameSplit := strings.Split(fileInfo.Name(), ".") + if fileNameSplit[2] == `pdf` { + filename = "报表_" + fileNameSplit[1] + `.pdf` + } else { + for _, values := range job.RptTableDict { + if values.Dish == fileNameSplit[0] { + filename = fileNameSplit[1] + `_` + values.DishHuman + `.xlsx` + } + } + } + header := &zip.FileHeader{ + Name: filename, + Flags: 1 << 11, // 使用utf8编码 + Method: zip.Deflate, + } + if job.RptPwd != `` { + header.SetPassword(job.RptPwd) + header.SetEncryptionMethod(zip.StandardEncryption) + } + f, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + _, err = f.Write([]byte(body)) + if err != nil { + return err + } + } + if err = zipWriter.Close(); err != nil { + return err + } + return nil +} + +``` + diff --git a/README.txt b/README.txt deleted file mode 100644 index 24a6265..0000000 --- a/README.txt +++ /dev/null @@ -1,77 +0,0 @@ -This is a fork of the Go archive/zip package to add support -for reading/writing password protected .zip files. -Only supports Winzip's AES extension: http://www.winzip.com/aes_info.htm. - -This package DOES NOT intend to implement the encryption methods -mentioned in the original PKWARE spec (sections 6.0 and 7.0): -https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT - -Status - Alpha. More tests and code clean up next. - -Documentation - -https://godoc.org/github.com/alexmullins/zip - -Roadmap -======== -Reading - Done. -Writing - Done. -Testing - Needs more. - -The process -============ -1. hello.txt -> compressed -> encrypted -> .zip -2. .zip -> decrypted -> decompressed -> hello.txt - -WinZip AES specifies -===================== -1. Encryption-Decryption w/ AES-CTR (128, 192, or 256 bits) -2. Key generation with PBKDF2-HMAC-SHA1 (1000 iteration count) that -generates a master key broken into the following: - a. First m bytes is for the encryption key - b. Next n bytes is for the authentication key - c. Last 2 bytes is the password verification value. -3. Following salt lengths are used w/ password during keygen: - ------------------------------ - AES Key Size | Salt Size - ------------------------------ - 128bit(16bytes) | 8 bytes - 192bit(24bytes) | 12 bytes - 256bit(32bytes) | 16 bytes - ------------------------------- -4. Master key len = AESKeyLen + AuthKeyLen + PWVLen: - a. AES 128 = 16 + 16 + 2 = 34 bytes of key material - b. AES 192 = 24 + 24 + 2 = 50 bytes of key material - c. AES 256 = 32 + 32 + 2 = 66 bytes of key material -5. Authentication Key is same size as AES key. -6. Authentication with HMAC-SHA1-80 (truncated to 80bits). -7. A new master key is generated for every file. -8. The file header and directory header compression method will -be 99 (decimal) indicating Winzip AES encryption. The actual -compression method will be in the extra's payload at the end -of the headers. -9. A extra field will be added to the file header and directory -header identified by the ID 0x9901 and contains the following info: - a. Header ID (2 bytes) - b. Data Size (2 bytes) - c. Vendor Version (2 bytes) - d. Vendor ID (2 bytes) - e. AES Strength (1 byte) - f. Compression Method (2 bytes) -10. The Data Size is always 7. -11. The Vendor Version can be either 0x0001 (AE-1) or -0x0002 (AE-2). -12. Vendor ID is ASCII "AE" -13. AES Strength: - a. 0x01 - AES-128 - b. 0x02 - AES-192 - c. 0x03 - AES-256 -14. Compression Method is the actual compression method -used that was replaced by the encryption process mentioned in #8. -15. AE-1 keeps the CRC and should be verified after decompression. -AE-2 removes the CRC and shouldn't be verified after decompression. -Refer to http://www.winzip.com/aes_info.htm#winzip11 for the reasoning. -16. Storage Format (file data payload totals CompressedSize64 bytes): - a. Salt - 8, 12, or 16 bytes depending on keysize - b. Password Verification Value - 2 bytes - c. Encrypted Data - compressed size - salt - pwv - auth code lengths - d. Authentication code - 10 bytes diff --git a/crypto.go b/crypto.go old mode 100644 new mode 100755 index 137b674..82a7202 --- a/crypto.go +++ b/crypto.go @@ -19,7 +19,14 @@ import ( "golang.org/x/crypto/pbkdf2" ) +type EncryptionMethod int + const ( + StandardEncryption EncryptionMethod = 1 + AES128Encryption EncryptionMethod = 2 + AES192Encryption EncryptionMethod = 3 + AES256Encryption EncryptionMethod = 4 + // AES key lengths aes128 = 16 aes192 = 24 @@ -379,18 +386,20 @@ func encryptStream(key []byte, w io.Writer) (io.Writer, error) { // newEncryptionWriter returns an io.Writer that when written to, 1. writes // out the salt, 2. writes out pwv, and 3. writes out authenticated, encrypted // data. The authcode will be written out in fileWriter.close(). -func newEncryptionWriter(w io.Writer, password passwordFn, fw *fileWriter) (io.Writer, error) { - var salt [16]byte +func newEncryptionWriter(w io.Writer, password passwordFn, fw *fileWriter, aesstrength byte) (io.Writer, error) { + keysize := aesKeyLen(aesstrength) + salt := make([]byte, keysize/2) _, err := rand.Read(salt[:]) if err != nil { return nil, errors.New("zip: unable to generate random salt") } - ekey, akey, pwv := generateKeys(password(), salt[:], aes256) + ekey, akey, pwv := generateKeys(password(), salt[:], keysize) fw.hmac = hmac.New(sha1.New, akey) aw := &authWriter{ hmac: fw.hmac, w: w, } + es, err := encryptStream(ekey, aw) if err != nil { return nil, err @@ -424,11 +433,23 @@ func (h *FileHeader) writeWinZipExtra() { eb.uint16(7) // following data size is 7 eb.uint16(2) // ae 2 eb.uint16(0x4541) // "AE" - eb.uint8(3) // aes256 + eb.uint8(h.aesStrength) // aes256 eb.uint16(h.Method) // original compression method h.Extra = append(h.Extra, buf[:]...) } +func (h *FileHeader) SetEncryptionMethod(enc EncryptionMethod) { + h.encryption = enc + switch enc { + case AES128Encryption: + h.aesStrength = 1 + case AES192Encryption: + h.aesStrength = 2 + case AES256Encryption: + h.aesStrength = 3 + } +} + func (h *FileHeader) setEncryptionBit() { h.Flags |= 0x1 } @@ -452,11 +473,12 @@ type passwordFn func() []byte // contents will be encrypted with AES-256 using the given password. The // file's contents must be written to the io.Writer before the next call // to Create, CreateHeader, or Close. -func (w *Writer) Encrypt(name string, password string) (io.Writer, error) { +func (w *Writer) Encrypt(name string, password string, enc EncryptionMethod) (io.Writer, error) { fh := &FileHeader{ Name: name, Method: Deflate, } fh.SetPassword(password) + fh.SetEncryptionMethod(enc) return w.CreateHeader(fh) } diff --git a/crypto_test.go b/crypto_test.go old mode 100644 new mode 100755 index e7b7277..cc5e2de --- a/crypto_test.go +++ b/crypto_test.go @@ -175,9 +175,55 @@ func TestPasswordWriteSimple(t *testing.T) { contents := []byte("Hello World") conLen := len(contents) + for _, enc := range []EncryptionMethod{StandardEncryption, AES128Encryption, AES192Encryption, AES256Encryption} { + raw := new(bytes.Buffer) + zipw := NewWriter(raw) + w, err := zipw.Encrypt("hello.txt", "golang", enc) + if err != nil { + t.Errorf("Expected to create a new FileHeader") + } + n, err := io.Copy(w, bytes.NewReader(contents)) + if err != nil || n != int64(conLen) { + t.Errorf("Expected to write the full contents to the writer.") + } + zipw.Close() + + // Read the zip + buf := new(bytes.Buffer) + zipr, err := NewReader(bytes.NewReader(raw.Bytes()), int64(raw.Len())) + if err != nil { + t.Errorf("Expected to open a new zip reader: %v", err) + } + nn := len(zipr.File) + if nn != 1 { + t.Errorf("Expected to have one file in the zip archive, but has %d files", nn) + } + z := zipr.File[0] + z.SetPassword("golang") + rr, err := z.Open() + if err != nil { + t.Errorf("Expected to open the readcloser: %v", err) + } + n, err = io.Copy(buf, rr) + if err != nil { + t.Errorf("Expected to write to temporary buffer: %v", err) + } + if n != int64(conLen) { + t.Errorf("Expected to copy %d bytes to temp buffer, but copied %d bytes instead", conLen, n) + } + if !bytes.Equal(contents, buf.Bytes()) { + t.Errorf("Expected the unzipped contents to equal '%s', but was '%s' instead", contents, buf.Bytes()) + } + } +} + +func TestZipCrypto(t *testing.T) { + contents := []byte("Hello World") + conLen := len(contents) + raw := new(bytes.Buffer) zipw := NewWriter(raw) - w, err := zipw.Encrypt("hello.txt", "golang") + w, err := zipw.Encrypt("hello.txt", "golang", StandardEncryption) if err != nil { t.Errorf("Expected to create a new FileHeader") } @@ -187,30 +233,14 @@ func TestPasswordWriteSimple(t *testing.T) { } zipw.Close() - // Read the zip - buf := new(bytes.Buffer) - zipr, err := NewReader(bytes.NewReader(raw.Bytes()), int64(raw.Len())) - if err != nil { - t.Errorf("Expected to open a new zip reader: %v", err) - } - nn := len(zipr.File) - if nn != 1 { - t.Errorf("Expected to have one file in the zip archive, but has %d files", nn) - } - z := zipr.File[0] - z.SetPassword("golang") - rr, err := z.Open() - if err != nil { - t.Errorf("Expected to open the readcloser: %v", err) - } - n, err = io.Copy(buf, rr) - if err != nil { - t.Errorf("Expected to write to temporary buffer: %v", err) - } - if n != int64(conLen) { - t.Errorf("Expected to copy %d bytes to temp buffer, but copied %d bytes instead", conLen, n) - } - if !bytes.Equal(contents, buf.Bytes()) { - t.Errorf("Expected the unzipped contents to equal '%s', but was '%s' instead", contents, buf.Bytes()) + zipr, _ := NewReader(bytes.NewReader(raw.Bytes()), int64(raw.Len())) + zipr.File[0].SetPassword("golang") + r, _ := zipr.File[0].Open() + res := new(bytes.Buffer) + io.Copy(res, r) + r.Close() + + if !bytes.Equal(contents, res.Bytes()) { + t.Errorf("Expected the unzipped contents to equal '%s', but was '%s' instead", contents, res.Bytes()) } } diff --git a/example_test.go b/example_test.go old mode 100644 new mode 100755 index d625b7e..dce27ee --- a/example_test.go +++ b/example_test.go @@ -11,7 +11,7 @@ import ( "log" "os" - "github.com/alexmullins/zip" + "github.com/yeka/zip" ) func ExampleWriter() { @@ -81,7 +81,7 @@ func ExampleWriter_Encrypt() { // write a password zip raw := new(bytes.Buffer) zipw := zip.NewWriter(raw) - w, err := zipw.Encrypt("hello.txt", "golang") + w, err := zipw.Encrypt("hello.txt", "golang", zip.AES256Encryption) if err != nil { log.Fatal(err) } diff --git a/reader.go b/reader.go old mode 100644 new mode 100755 index a9e3f6b..48a7c17 --- a/reader.go +++ b/reader.go @@ -145,7 +145,12 @@ 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 r, err = newDecryptionReader(rr, f); err != nil { + + if f.ae == 0 { + if r, err = ZipCryptoDecryptor(rr, f.password()); err != nil { + return + } + } else if r, err = newDecryptionReader(rr, f); err != nil { return } } else { diff --git a/reader_test.go b/reader_test.go old mode 100644 new mode 100755 diff --git a/register.go b/register.go old mode 100644 new mode 100755 diff --git a/struct.go b/struct.go old mode 100644 new mode 100755 index 1e0cba2..7e50a5c --- a/struct.go +++ b/struct.go @@ -101,6 +101,7 @@ type FileHeader struct { // https://www.imperialviolet.org/2015/05/16/aeads.html DeferAuth bool + encryption EncryptionMethod password passwordFn // Returns the password to use when reading/writing ae uint16 aesStrength byte diff --git a/testdata/crc32-not-streamed.zip b/testdata/crc32-not-streamed.zip deleted file mode 100644 index f268d88..0000000 Binary files a/testdata/crc32-not-streamed.zip and /dev/null differ diff --git a/testdata/dd.zip b/testdata/dd.zip deleted file mode 100644 index e53378b..0000000 Binary files a/testdata/dd.zip and /dev/null differ diff --git a/testdata/go-no-datadesc-sig.zip b/testdata/go-no-datadesc-sig.zip deleted file mode 100644 index c3d593f..0000000 Binary files a/testdata/go-no-datadesc-sig.zip and /dev/null differ diff --git a/testdata/go-with-datadesc-sig.zip b/testdata/go-with-datadesc-sig.zip deleted file mode 100644 index bcfe121..0000000 Binary files a/testdata/go-with-datadesc-sig.zip and /dev/null differ diff --git a/testdata/gophercolor16x16.png b/testdata/gophercolor16x16.png deleted file mode 100644 index 48854ff..0000000 Binary files a/testdata/gophercolor16x16.png and /dev/null differ diff --git a/testdata/hello-aes.zip b/testdata/hello-aes.zip deleted file mode 100644 index 341a408..0000000 Binary files a/testdata/hello-aes.zip and /dev/null differ diff --git a/testdata/macbeth-act1.zip b/testdata/macbeth-act1.zip deleted file mode 100644 index 8ee2aa2..0000000 Binary files a/testdata/macbeth-act1.zip and /dev/null differ diff --git a/testdata/readme.notzip b/testdata/readme.notzip deleted file mode 100644 index 8173727..0000000 Binary files a/testdata/readme.notzip and /dev/null differ diff --git a/testdata/readme.zip b/testdata/readme.zip deleted file mode 100644 index 5642a67..0000000 Binary files a/testdata/readme.zip and /dev/null differ diff --git a/testdata/symlink.zip b/testdata/symlink.zip deleted file mode 100644 index af84693..0000000 Binary files a/testdata/symlink.zip and /dev/null differ diff --git a/testdata/test-trailing-junk.zip b/testdata/test-trailing-junk.zip deleted file mode 100644 index 42281b4..0000000 Binary files a/testdata/test-trailing-junk.zip and /dev/null differ diff --git a/testdata/test.zip b/testdata/test.zip deleted file mode 100644 index 03890c0..0000000 Binary files a/testdata/test.zip and /dev/null differ diff --git a/testdata/unix.zip b/testdata/unix.zip deleted file mode 100644 index ce1a981..0000000 Binary files a/testdata/unix.zip and /dev/null differ diff --git a/testdata/winxp.zip b/testdata/winxp.zip deleted file mode 100644 index 3919322..0000000 Binary files a/testdata/winxp.zip and /dev/null differ diff --git a/testdata/world-aes.zip b/testdata/world-aes.zip deleted file mode 100644 index ac0b729..0000000 Binary files a/testdata/world-aes.zip and /dev/null differ diff --git a/testdata/zip64-2.zip b/testdata/zip64-2.zip deleted file mode 100644 index f844e35..0000000 Binary files a/testdata/zip64-2.zip and /dev/null differ diff --git a/testdata/zip64.zip b/testdata/zip64.zip deleted file mode 100644 index a2ee1fa..0000000 Binary files a/testdata/zip64.zip and /dev/null differ diff --git a/writer.go b/writer.go old mode 100644 new mode 100755 index 27d125e..55b3595 --- a/writer.go +++ b/writer.go @@ -229,14 +229,23 @@ func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, error) { // check for password var sw io.Writer = fw.compCount if fh.password != nil { - // we have a password and need to encrypt. - fh.writeWinZipExtra() - fh.Method = 99 // ok to change, we've gotten the comp and wrote extra - ew, err := newEncryptionWriter(sw, fh.password, fw) - if err != nil { - return nil, err + if fh.encryption == StandardEncryption { + ew, err := ZipCryptoEncryptor(sw, fh.password, fw) + if err != nil { + return nil, err + } + sw = ew + } else { + // we have a password and need to encrypt. + fh.writeWinZipExtra() + fh.Method = 99 // ok to change, we've gotten the comp and wrote extra + ew, err := newEncryptionWriter(sw, fh.password, fw, fh.aesStrength) + if err != nil { + //fmt.Println(`HJAHKJHKJHJKHSKJSHKJHKDH`, err) + return nil, err + } + sw = ew } - sw = ew } var err error fw.comp, err = comp(sw) @@ -313,7 +322,7 @@ func (w *fileWriter) close() error { return err } // if encrypted grab the hmac and write it out - if w.header.IsEncrypted() { + if w.header.IsEncrypted() && w.header.encryption != StandardEncryption { authCode := w.hmac.Sum(nil) authCode = authCode[:10] _, err := w.compCount.Write(authCode) @@ -324,7 +333,7 @@ func (w *fileWriter) close() error { // update FileHeader fh := w.header.FileHeader // ae-2 we don't write out CRC - if !fh.IsEncrypted() { + if !fh.IsEncrypted() || fh.encryption == StandardEncryption { fh.CRC32 = w.crc32.Sum32() } fh.CompressedSize64 = uint64(w.compCount.count) diff --git a/writer_test.go b/writer_test.go old mode 100644 new mode 100755 diff --git a/zip_test.go b/zip_test.go old mode 100644 new mode 100755 diff --git a/zipcrypto.go b/zipcrypto.go new file mode 100755 index 0000000..b3d87dc --- /dev/null +++ b/zipcrypto.go @@ -0,0 +1,109 @@ +package zip + +import ( + "bytes" + "hash/crc32" + "io" +) + +type ZipCrypto struct { + password []byte + Keys [3]uint32 +} + +func NewZipCrypto(passphrase []byte) *ZipCrypto { + z := &ZipCrypto{} + z.password = passphrase + z.init() + return z +} + +func (z *ZipCrypto) init() { + z.Keys[0] = 0x12345678 + z.Keys[1] = 0x23456789 + z.Keys[2] = 0x34567890 + + for i := 0; i < len(z.password); i++ { + z.updateKeys(z.password[i]) + } +} + +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)) +} + +func (z *ZipCrypto) magicByte() byte { + var t uint32 = z.Keys[2] | 2 + return byte((t * (t ^ 1)) >> 8) +} + +func (z *ZipCrypto) Encrypt(data []byte) []byte { + length := len(data) + chiper := make([]byte, length) + for i := 0; i < length; i++ { + v := data[i] + chiper[i] = v ^ z.magicByte() + z.updateKeys(v) + } + return chiper +} + +func (z *ZipCrypto) Decrypt(chiper []byte) []byte { + length := len(chiper) + plain := make([]byte, length) + for i, c := range chiper { + v := c ^ z.magicByte() + z.updateKeys(v) + plain[i] = v + } + return plain +} + +func crc32update(pCrc32 uint32, bval byte) uint32 { + return crc32.IEEETable[(pCrc32^uint32(bval))&0xff] ^ (pCrc32 >> 8) +} + +func ZipCryptoDecryptor(r *io.SectionReader, password []byte) (*io.SectionReader, error) { + z := NewZipCrypto(password) + b := make([]byte, r.Size()) + + r.Read(b) + + m := z.Decrypt(b) + return io.NewSectionReader(bytes.NewReader(m), 12, int64(len(m))), nil +} + +type zipCryptoWriter struct { + w io.Writer + z *ZipCrypto + first bool + fw *fileWriter +} + +func (z *zipCryptoWriter) Write(p []byte) (n int, err error) { + err = nil + if z.first { + z.first = false + header := []byte{0xF8, 0x53, 0xCF, 0x05, 0x2D, 0xDD, 0xAD, 0xC8, 0x66, 0x3F, 0x8C, 0xAC} + header = z.z.Encrypt(header) + + crc := z.fw.ModifiedTime + header[10] = byte(crc) + header[11] = byte(crc >> 8) + + z.z.init() + z.w.Write(z.z.Encrypt(header)) + n += 12 + } + z.w.Write(z.z.Encrypt(p)) + return +} + +func ZipCryptoEncryptor(i io.Writer, pass passwordFn, fw *fileWriter) (io.Writer, error) { + z := NewZipCrypto(pass()) + zc := &zipCryptoWriter{i, z, true, fw} + return zc, nil +} diff --git a/zipwriters.png b/zipwriters.png old mode 100644 new mode 100755