defer authentication/buffering to Read not Open

This commit is contained in:
alexmullins 2015-11-05 23:12:14 -06:00
parent 37ce86c83e
commit cf103a6528
4 changed files with 113 additions and 128 deletions

View File

@ -6,14 +6,12 @@ This package DOES NOT intend to implement the encryption methods
mentioned in the original PKWARE spec (sections 6.0 and 7.0): mentioned in the original PKWARE spec (sections 6.0 and 7.0):
https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
The process The process
============================================================================== ============
hello.txt -> compressed -> encrypted -> .zip -> decrypted -> decompressed -> hello.txt hello.txt -> compressed -> encrypted -> .zip -> decrypted -> decompressed -> hello.txt
Roadmap Roadmap
============================================================================== ========
Reading - Done. Reading - Done.
TODO: TODO:
1. Change to streaming authentication and decryption. (Maybe not such a good 1. Change to streaming authentication and decryption. (Maybe not such a good
@ -22,9 +20,8 @@ Reading - Done.
Writing - Not started. Writing - Not started.
Testing - Needs more. Testing - Needs more.
WinZip AES specifies WinZip AES specifies
============================================================================== =====================
1. Encryption-Decryption w/ AES-CTR (128, 192, or 256 bits) 1. Encryption-Decryption w/ AES-CTR (128, 192, or 256 bits)
2. Key generation with PBKDF2-HMAC-SHA1 (1000 iteration count) that 2. Key generation with PBKDF2-HMAC-SHA1 (1000 iteration count) that
generates a master key broken into the following: generates a master key broken into the following:
@ -71,8 +68,8 @@ used that was replaced by the encryption process mentioned in #8.
15. AE-1 keeps the CRC and should be verified after decompression. 15. AE-1 keeps the CRC and should be verified after decompression.
AE-2 removes the CRC and shouldn't 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. Refer to http://www.winzip.com/aes_info.htm#winzip11 for the reasoning.
16. Storage Format (file data payload) totals CompressedSize64 bytes: 16. Storage Format (file data payload totals CompressedSize64 bytes):
a. Salt - 8, 12, or 16 bytes depending on keysize a. Salt - 8, 12, or 16 bytes depending on keysize
b. Password Verification Value - 2 bytes b. Password Verification Value - 2 bytes
c. Encrypted Data - compressed size - salt - pwv - auth lengths c. Encrypted Data - compressed size - salt - pwv - auth code lengths
d. Authentication code - 10 bytes d. Authentication code - 10 bytes

109
crypto.go
View File

@ -10,13 +10,20 @@ import (
"crypto/cipher" "crypto/cipher"
"crypto/hmac" "crypto/hmac"
"crypto/sha1" "crypto/sha1"
"crypto/subtle"
"errors"
"io" "io"
"io/ioutil"
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
) )
// Counter (CTR) mode. // Decryption Errors
var (
ErrDecryption = errors.New("zip: decryption error")
)
// Counter (CTR) mode.
// CTR converts a block cipher into a stream cipher by // CTR converts a block cipher into a stream cipher by
// repeatedly encrypting an incrementing counter and // repeatedly encrypting an incrementing counter and
// xoring the resulting stream of data with the input. // xoring the resulting stream of data with the input.
@ -111,6 +118,61 @@ func xorBytes(dst, a, b []byte) int {
return n return n
} }
type authReader struct {
data io.Reader // data to be authenticated
adata io.Reader // the authentication code to read
akey []byte // authentication key
buf *bytes.Buffer // buffer to store data to authenticate
err error
auth bool
}
func newAuthReader(akey []byte, data, adata io.Reader) io.Reader {
return &authReader{
data: data,
adata: adata,
akey: akey,
buf: new(bytes.Buffer),
err: nil,
auth: false,
}
}
// Read will fully buffer the file data payload to authenticate first.
// If authentication fails, returns ErrDecryption immediately.
// Else, sends data along for decryption.
func (a *authReader) Read(b []byte) (int, error) {
// check for sticky error
if a.err != nil {
return 0, a.err
}
// make sure we have auth'ed before we send any data
if !a.auth {
nn, err := io.Copy(a.buf, a.data)
if err != nil {
a.err = ErrDecryption
return 0, a.err
}
ab := new(bytes.Buffer)
nn, err = io.Copy(ab, a.adata)
if err != nil || nn != 10 {
a.err = ErrDecryption
return 0, a.err
}
a.auth = checkAuthentication(a.buf.Bytes(), ab.Bytes(), a.akey)
if !a.auth {
a.err = ErrDecryption
return 0, a.err
}
}
// so we've authenticated the data, now just pass it on.
n, err := a.buf.Read(b)
if err != nil {
a.err = err
}
return n, a.err
}
func checkAuthentication(message, authcode, key []byte) bool { func checkAuthentication(message, authcode, key []byte) bool {
mac := hmac.New(sha1.New, key) mac := hmac.New(sha1.New, key)
mac.Write(message) mac.Write(message)
@ -118,7 +180,13 @@ func checkAuthentication(message, authcode, key []byte) bool {
// Truncate at the first 10 bytes // Truncate at the first 10 bytes
expectedAuthCode = expectedAuthCode[:10] expectedAuthCode = expectedAuthCode[:10]
// Change to use crypto/subtle for constant time comparison // Change to use crypto/subtle for constant time comparison
return bytes.Equal(expectedAuthCode, authcode) b := subtle.ConstantTimeCompare(expectedAuthCode, authcode) > 0
return b
}
func checkPasswordVerification(pwvv, pwv []byte) bool {
b := subtle.ConstantTimeCompare(pwvv, pwv) > 0
return b
} }
func generateKeys(password, salt []byte, keySize int) (encKey, authKey, pwv []byte) { func generateKeys(password, salt []byte, keySize int) (encKey, authKey, pwv []byte) {
@ -130,7 +198,7 @@ func generateKeys(password, salt []byte, keySize int) (encKey, authKey, pwv []by
return return
} }
func newDecryptionReader(r io.Reader, f *File) (io.Reader, error) { func newDecryptionReader(r *io.SectionReader, f *File) (io.ReadCloser, error) {
keyLen := aesKeyLen(f.aesStrength) keyLen := aesKeyLen(f.aesStrength)
saltLen := keyLen / 2 // salt is half of key len saltLen := keyLen / 2 // salt is half of key len
if saltLen == 0 { if saltLen == 0 {
@ -141,38 +209,39 @@ func newDecryptionReader(r io.Reader, f *File) (io.Reader, error) {
// See: // See:
// https://www.imperialviolet.org/2014/06/27/streamingencryption.html // https://www.imperialviolet.org/2014/06/27/streamingencryption.html
// https://www.imperialviolet.org/2015/05/16/aeads.html // https://www.imperialviolet.org/2015/05/16/aeads.html
content := make([]byte, f.CompressedSize64) // grab the salt, pwvv, data, and authcode
if _, err := io.ReadFull(r, content); err != nil { saltpwvv := make([]byte, saltLen+2)
if _, err := r.Read(saltpwvv); err != nil {
return nil, ErrDecryption return nil, ErrDecryption
} }
// grab the salt, pwvv, data, and authcode salt := saltpwvv[:saltLen]
salt := content[:saltLen] pwvv := saltpwvv[saltLen : saltLen+2]
pwvv := content[saltLen : saltLen+2] dataOff := int64(saltLen + 2)
content = content[saltLen+2:] dataLen := int64(f.CompressedSize64 - uint64(saltLen) - 2 - 10)
size := f.CompressedSize64 - uint64(saltLen) - 2 - 10 data := io.NewSectionReader(r, dataOff, dataLen)
data := content[:size] authOff := dataOff + dataLen
authcode := content[size:] authcode := io.NewSectionReader(r, authOff, 10)
// generate keys // generate keys
decKey, authKey, pwv := generateKeys(f.password, salt, keyLen) decKey, authKey, pwv := generateKeys(f.password, salt, keyLen)
// check password verifier (pwv) // check password verifier (pwv)
// Change to use crypto/subtle for constant time comparison // Change to use crypto/subtle for constant time comparison
if !bytes.Equal(pwv, pwvv) { if !checkPasswordVerification(pwv, pwvv) {
return nil, ErrDecryption return nil, ErrDecryption
} }
// check authentication // setup auth reader
if !checkAuthentication(data, authcode, authKey) { ar := newAuthReader(authKey, data, authcode)
return nil, ErrDecryption // return decryption reader
} dr := decryptStream(decKey, ar)
return decryptStream(data, decKey), nil return ioutil.NopCloser(dr), nil
} }
func decryptStream(ciphertext, key []byte) io.Reader { func decryptStream(key []byte, ciphertext io.Reader) io.Reader {
block, err := aes.NewCipher(key) block, err := aes.NewCipher(key)
if err != nil { if err != nil {
return nil return nil
} }
stream := newWinZipCTR(block) stream := newWinZipCTR(block)
reader := cipher.StreamReader{S: stream, R: bytes.NewReader(ciphertext)} reader := &cipher.StreamReader{S: stream, R: ciphertext}
return reader return reader
} }

View File

@ -19,7 +19,6 @@ var (
ErrFormat = errors.New("zip: not a valid zip file") ErrFormat = errors.New("zip: not a valid zip file")
ErrAlgorithm = errors.New("zip: unsupported compression algorithm") ErrAlgorithm = errors.New("zip: unsupported compression algorithm")
ErrChecksum = errors.New("zip: checksum error") ErrChecksum = errors.New("zip: checksum error")
ErrDecryption = errors.New("zip: decryption error")
) )
type Reader struct { type Reader struct {
@ -53,6 +52,10 @@ func (f *File) IsEncrypted() bool {
return f.Flags&0x1 == 1 return f.Flags&0x1 == 1
} }
func (f *File) isAE2() bool {
return f.ae == 2
}
func (f *File) hasDataDescriptor() bool { func (f *File) hasDataDescriptor() bool {
return f.Flags&0x8 != 0 return f.Flags&0x8 != 0
} }
@ -156,12 +159,14 @@ func (f *File) Open() (rc io.ReadCloser, err error) {
// and auth code lengths // and auth code lengths
size := int64(f.CompressedSize64) size := int64(f.CompressedSize64)
var r io.Reader var r io.Reader
r = io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size) rr := io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size)
// check for encryption // check for encryption
if f.IsEncrypted() { if f.IsEncrypted() {
if r, err = newDecryptionReader(r, f); err != nil { if r, err = newDecryptionReader(rr, f); err != nil {
return return
} }
} else {
r = rr
} }
dcomp := decompressor(f.Method) dcomp := decompressor(f.Method)
if dcomp == nil { if dcomp == nil {
@ -174,6 +179,14 @@ func (f *File) Open() (rc io.ReadCloser, err error) {
if f.hasDataDescriptor() { if f.hasDataDescriptor() {
desr = io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset+size, dataDescriptorLen) desr = io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset+size, dataDescriptorLen)
} }
// if !f.isAE2() {
// rc = &checksumReader{
// rc: rc,
// hash: crc32.NewIEEE(),
// f: f,
// desr: desr,
// }
// }
rc = &checksumReader{ rc = &checksumReader{
rc: rc, rc: rc,
hash: crc32.NewIEEE(), hash: crc32.NewIEEE(),

View File

@ -605,97 +605,3 @@ func TestIssue11146(t *testing.T) {
} }
r.Close() 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.")
}
}
func TestHelloWorldAes(t *testing.T) {
file := "world-aes.zip"
expecting := "helloworld"
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) != 2 {
t.Errorf("Expected %s to contain two files.", file)
}
var b bytes.Buffer
for _, f := range r.File {
if !f.IsEncrypted() {
t.Errorf("Expected %s to be encrypted.", f.FileInfo().Name)
}
f.SetPassword([]byte("golang"))
rc, err := f.Open()
if err != nil {
t.Errorf("Expected to open readcloser: %v", err)
}
defer rc.Close()
if _, err := io.Copy(&b, rc); err != nil {
t.Errorf("Expected to copy bytes to buffer: %v", err)
}
}
if !bytes.Equal([]byte(expecting), b.Bytes()) {
t.Errorf("Expected ending content to be %s instead of %s", expecting, b.Bytes())
}
}
func TestMacbethAct1(t *testing.T) {
file := "macbeth-act1.zip"
expecting := "Exeunt"
var b bytes.Buffer
r, err := OpenReader(filepath.Join("testdata", file))
if err != nil {
t.Errorf("Expected %s to open: %v", file, err)
}
defer r.Close()
for _, f := range r.File {
if !f.IsEncrypted() {
t.Errorf("Expected %s to be encrypted.", f.Name)
}
f.SetPassword([]byte("golang"))
rc, err := f.Open()
if err != nil {
t.Errorf("Expected to open readcloser: %v", err)
}
defer rc.Close()
if _, err := io.Copy(&b, rc); err != nil {
t.Errorf("Expected to copy bytes to buffer: %v", err)
}
}
if !bytes.Contains(b.Bytes(), []byte(expecting)) {
t.Errorf("Expected to find %s in the buffer %v", expecting, b.Bytes())
}
}
// Test for AE-1 vs AE-2
// Test for tampered data payload, use messWith