diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72747d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +.DS_Store + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/README.txt b/README.txt index c1e6392..1cfd1c9 100644 --- a/README.txt +++ b/README.txt @@ -5,6 +5,17 @@ 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 +The process +============================================================================== +hello.txt -> compress -> encrypt -> .zip -> decrypt -> decompress -> hello.txt + +Roadmap +================================================ +Reading - Almost done (TODO: check for AE-2 and skip CRC). +Writing - Not started. +Testing - Needs more. + + WinZip AES specifies ==================================================================== 1. Encryption-Decryption w/ AES-CTR (128, 192, or 256 bits) diff --git a/reader.go b/reader.go index 519748b..1226a49 100644 --- a/reader.go +++ b/reader.go @@ -6,19 +6,28 @@ package zip import ( "bufio" + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha1" "encoding/binary" "errors" "fmt" "hash" "hash/crc32" "io" + "io/ioutil" "os" + + "golang.org/x/crypto/pbkdf2" ) var ( - ErrFormat = errors.New("zip: not a valid zip file") - ErrAlgorithm = errors.New("zip: unsupported compression algorithm") - ErrChecksum = errors.New("zip: checksum error") + ErrFormat = errors.New("zip: not a valid zip file") + ErrAlgorithm = errors.New("zip: unsupported compression algorithm") + ErrChecksum = errors.New("zip: checksum error") + ErrDecryption = errors.New("zip: decryption error") ) type Reader struct { @@ -37,6 +46,32 @@ type File struct { zipr io.ReaderAt zipsize int64 headerOffset int64 + password []byte + ae uint16 + aesStrength byte +} + +func aesKeyLen(strength byte) int { + switch strength { + case 1: + return aes128 + case 2: + return aes192 + case 3: + return aes256 + default: + return 0 + } +} + +// SetPassword must be called before calling Open on the file. +func (f *File) SetPassword(password []byte) { + f.password = password +} + +// IsEncrypted indicates whether this file's data is encrypted. +func (f *File) IsEncrypted() bool { + return f.Flags&0x1 == 1 } func (f *File) hasDataDescriptor() bool { @@ -138,8 +173,17 @@ func (f *File) Open() (rc io.ReadCloser, err error) { if err != nil { return } + // If f is encrypted, CompressedSize64 includes salt, pwvv, encrypted data, + // and auth code lengths size := int64(f.CompressedSize64) - r := io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size) + var r io.Reader + r = io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size) + // check for encryption + if f.IsEncrypted() { + if r, err = newDecryptionReader(r, f); err != nil { + return + } + } dcomp := decompressor(f.Method) if dcomp == nil { err = ErrAlgorithm @@ -150,6 +194,7 @@ func (f *File) Open() (rc io.ReadCloser, err error) { if f.hasDataDescriptor() { desr = io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset+size, dataDescriptorLen) } + // TODO: if AE-2, skip CRC rc = &checksumReader{ rc: rc, hash: crc32.NewIEEE(), @@ -159,6 +204,75 @@ func (f *File) Open() (rc io.ReadCloser, err error) { return } +func newDecryptionReader(r io.Reader, f *File) (io.ReadCloser, error) { + keyLen := aesKeyLen(f.aesStrength) + saltLen := keyLen / 2 // salt is half of key len + if saltLen == 0 { + return nil, ErrDecryption + } + + content := make([]byte, f.CompressedSize64) + if _, err := io.ReadFull(r, content); err != nil { + return nil, ErrDecryption + } + + // grab the salt, pwvv, data, and authcode + salt := content[:saltLen] + pwvv := content[saltLen : saltLen+2] + content = content[saltLen+2:] + size := f.UncompressedSize64 + data := content[:size] + authcode := content[size:] + + // generate keys + decKey, authKey, pwv := generateKeys(f.password, salt, keyLen) + + // check password verifier (pwv) + if !bytes.Equal(pwv, pwvv) { + return nil, ErrDecryption + } + + // check authentication + if !checkAuthentication(data, authcode, authKey) { + return nil, ErrDecryption + } + + // set the IV + var iv [aes.BlockSize]byte + iv[0] = 1 + + return decryptStream(data, decKey, iv[:]), nil +} + +func decryptStream(ciphertext, key, iv []byte) io.ReadCloser { + block, err := aes.NewCipher(key) + if err != nil { + return nil + } + stream := cipher.NewCTR(block, iv) + reader := cipher.StreamReader{S: stream, R: bytes.NewReader(ciphertext)} + return ioutil.NopCloser(reader) +} + +func checkAuthentication(message, authcode, key []byte) bool { + mac := hmac.New(sha1.New, key) + mac.Write(message) + expectedAuthCode := mac.Sum(nil) + // Truncate at the first 10 bytes + expectedAuthCode = expectedAuthCode[:10] + return bytes.Equal(expectedAuthCode, authcode) +} + +func generateKeys(password, salt []byte, keySize int) (encKey, authKey, pwv []byte) { + totalSize := (keySize * 2) + 2 // enc + auth + pv sizes + + key := pbkdf2.Key(password, salt, 1000, totalSize, sha1.New) + encKey = key[:keySize] + authKey = key[keySize : keySize*2] + pwv = key[keySize*2:] + return +} + type checksumReader struct { rc io.ReadCloser hash hash.Hash32 @@ -269,9 +383,10 @@ func readDirectoryHeader(f *File, r io.Reader) error { if int(size) > len(b) { return ErrFormat } - if tag == zip64ExtraId { + eb := readBuf(b[:size]) + switch tag { + case zip64ExtraId: // update directory values from the zip64 extra block - eb := readBuf(b[:size]) if len(eb) >= 8 { f.UncompressedSize64 = eb.uint64() } @@ -281,6 +396,18 @@ func readDirectoryHeader(f *File, r io.Reader) error { if len(eb) >= 8 { f.headerOffset = int64(eb.uint64()) } + case winzipAesExtraId: + // grab the AE version + f.ae = eb.uint16() + + // skip vendor ID + _ = eb.uint16() + + // AES strength + f.aesStrength = eb.uint8() + + // set the actual compression method. + f.Method = eb.uint16() } b = b[size:] } @@ -452,6 +579,12 @@ func findSignatureInBlock(b []byte) int { type readBuf []byte +func (b *readBuf) uint8() byte { + v := (*b)[0] + *b = (*b)[1:] + return v +} + func (b *readBuf) uint16() uint16 { v := binary.LittleEndian.Uint16(*b) *b = (*b)[2:] diff --git a/reader_test.go b/reader_test.go index 547dd39..6eba4cf 100644 --- a/reader_test.go +++ b/reader_test.go @@ -605,3 +605,42 @@ func TestIssue11146(t *testing.T) { } r.Close() } + +func TestSimplePassword(t *testing.T) { + file := "hello-aes.zip" + var buf bytes.Buffer + r, err := OpenReader(filepath.Join("testdata", file)) + if err != nil { + t.Errorf("Expected %s to open: %v.", file, err) + } + defer r.Close() + + if len(r.File) != 1 { + t.Errorf("Expected %s to contain one file.", file) + } + + f := r.File[0] + + if f.FileInfo().Name() != "hello.txt" { + t.Errorf("Expected %s to have a file named hello.txt", file) + } + + if f.Method != 0 { + t.Errorf("Expected %s to have its Method set to 0.", file) + } + + f.SetPassword([]byte("golang")) + + rc, err := f.Open() + if err != nil { + t.Errorf("Expected to open the readcloser: %v.", err) + } + _, err = io.Copy(&buf, rc) + if err != nil { + t.Errorf("Expected to copy bytes: %v.", err) + } + + if !bytes.Contains(buf.Bytes(), []byte("Hello World\r\n")) { + t.Errorf("Expected contents were not found.") + } +} diff --git a/struct.go b/struct.go index 137d049..cf2f4af 100644 --- a/struct.go +++ b/struct.go @@ -62,7 +62,13 @@ const ( uint32max = (1 << 32) - 1 // extra header id's - zip64ExtraId = 0x0001 // zip64 Extended Information Extra Field + zip64ExtraId = 0x0001 // zip64 Extended Information Extra Field + winzipAesExtraId = 0x9901 // winzip AES Extra Field + + // AES key lengths + aes128 = 16 + aes192 = 24 + aes256 = 32 ) // FileHeader describes a file within a zip file. diff --git a/testdata/hello-aes.zip b/testdata/hello-aes.zip new file mode 100644 index 0000000..341a408 Binary files /dev/null and b/testdata/hello-aes.zip differ