Added initial support for reading pw protected files.

This commit is contained in:
alexmullins 2015-10-29 16:14:19 -05:00
parent 5ca6f99620
commit ea2bc2cf67
6 changed files with 222 additions and 7 deletions

26
.gitignore vendored Normal file
View File

@ -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

View File

@ -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)

145
reader.go
View File

@ -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:]

View File

@ -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.")
}
}

View File

@ -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.

BIN
testdata/hello-aes.zip vendored Normal file

Binary file not shown.