Skip to content

Commit cf103a6

Browse files
author
alexmullins
committed
defer authentication/buffering to Read not Open
1 parent 37ce86c commit cf103a6

File tree

4 files changed

+113
-128
lines changed

4 files changed

+113
-128
lines changed

README.txt

+5-8
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ This package DOES NOT intend to implement the encryption methods
66
mentioned in the original PKWARE spec (sections 6.0 and 7.0):
77
https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
88

9-
109
The process
11-
==============================================================================
10+
============
1211
hello.txt -> compressed -> encrypted -> .zip -> decrypted -> decompressed -> hello.txt
1312

14-
1513
Roadmap
16-
==============================================================================
14+
========
1715
Reading - Done.
1816
TODO:
1917
1. Change to streaming authentication and decryption. (Maybe not such a good
@@ -22,9 +20,8 @@ Reading - Done.
2220
Writing - Not started.
2321
Testing - Needs more.
2422

25-
2623
WinZip AES specifies
27-
==============================================================================
24+
=====================
2825
1. Encryption-Decryption w/ AES-CTR (128, 192, or 256 bits)
2926
2. Key generation with PBKDF2-HMAC-SHA1 (1000 iteration count) that
3027
generates a master key broken into the following:
@@ -71,8 +68,8 @@ used that was replaced by the encryption process mentioned in #8.
7168
15. AE-1 keeps the CRC and should be verified after decompression.
7269
AE-2 removes the CRC and shouldn't be verified after decompression.
7370
Refer to http://www.winzip.com/aes_info.htm#winzip11 for the reasoning.
74-
16. Storage Format (file data payload) totals CompressedSize64 bytes:
71+
16. Storage Format (file data payload totals CompressedSize64 bytes):
7572
a. Salt - 8, 12, or 16 bytes depending on keysize
7673
b. Password Verification Value - 2 bytes
77-
c. Encrypted Data - compressed size - salt - pwv - auth lengths
74+
c. Encrypted Data - compressed size - salt - pwv - auth code lengths
7875
d. Authentication code - 10 bytes

crypto.go

+89-20
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,20 @@ import (
1010
"crypto/cipher"
1111
"crypto/hmac"
1212
"crypto/sha1"
13+
"crypto/subtle"
14+
"errors"
1315
"io"
16+
"io/ioutil"
1417

1518
"golang.org/x/crypto/pbkdf2"
1619
)
1720

18-
// Counter (CTR) mode.
21+
// Decryption Errors
22+
var (
23+
ErrDecryption = errors.New("zip: decryption error")
24+
)
1925

26+
// Counter (CTR) mode.
2027
// CTR converts a block cipher into a stream cipher by
2128
// repeatedly encrypting an incrementing counter and
2229
// xoring the resulting stream of data with the input.
@@ -111,14 +118,75 @@ func xorBytes(dst, a, b []byte) int {
111118
return n
112119
}
113120

121+
type authReader struct {
122+
data io.Reader // data to be authenticated
123+
adata io.Reader // the authentication code to read
124+
akey []byte // authentication key
125+
buf *bytes.Buffer // buffer to store data to authenticate
126+
err error
127+
auth bool
128+
}
129+
130+
func newAuthReader(akey []byte, data, adata io.Reader) io.Reader {
131+
return &authReader{
132+
data: data,
133+
adata: adata,
134+
akey: akey,
135+
buf: new(bytes.Buffer),
136+
err: nil,
137+
auth: false,
138+
}
139+
}
140+
141+
// Read will fully buffer the file data payload to authenticate first.
142+
// If authentication fails, returns ErrDecryption immediately.
143+
// Else, sends data along for decryption.
144+
func (a *authReader) Read(b []byte) (int, error) {
145+
// check for sticky error
146+
if a.err != nil {
147+
return 0, a.err
148+
}
149+
// make sure we have auth'ed before we send any data
150+
if !a.auth {
151+
nn, err := io.Copy(a.buf, a.data)
152+
if err != nil {
153+
a.err = ErrDecryption
154+
return 0, a.err
155+
}
156+
ab := new(bytes.Buffer)
157+
nn, err = io.Copy(ab, a.adata)
158+
if err != nil || nn != 10 {
159+
a.err = ErrDecryption
160+
return 0, a.err
161+
}
162+
a.auth = checkAuthentication(a.buf.Bytes(), ab.Bytes(), a.akey)
163+
if !a.auth {
164+
a.err = ErrDecryption
165+
return 0, a.err
166+
}
167+
}
168+
// so we've authenticated the data, now just pass it on.
169+
n, err := a.buf.Read(b)
170+
if err != nil {
171+
a.err = err
172+
}
173+
return n, a.err
174+
}
175+
114176
func checkAuthentication(message, authcode, key []byte) bool {
115177
mac := hmac.New(sha1.New, key)
116178
mac.Write(message)
117179
expectedAuthCode := mac.Sum(nil)
118180
// Truncate at the first 10 bytes
119181
expectedAuthCode = expectedAuthCode[:10]
120182
// Change to use crypto/subtle for constant time comparison
121-
return bytes.Equal(expectedAuthCode, authcode)
183+
b := subtle.ConstantTimeCompare(expectedAuthCode, authcode) > 0
184+
return b
185+
}
186+
187+
func checkPasswordVerification(pwvv, pwv []byte) bool {
188+
b := subtle.ConstantTimeCompare(pwvv, pwv) > 0
189+
return b
122190
}
123191

124192
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
130198
return
131199
}
132200

133-
func newDecryptionReader(r io.Reader, f *File) (io.Reader, error) {
201+
func newDecryptionReader(r *io.SectionReader, f *File) (io.ReadCloser, error) {
134202
keyLen := aesKeyLen(f.aesStrength)
135203
saltLen := keyLen / 2 // salt is half of key len
136204
if saltLen == 0 {
@@ -141,38 +209,39 @@ func newDecryptionReader(r io.Reader, f *File) (io.Reader, error) {
141209
// See:
142210
// https://www.imperialviolet.org/2014/06/27/streamingencryption.html
143211
// https://www.imperialviolet.org/2015/05/16/aeads.html
144-
content := make([]byte, f.CompressedSize64)
145-
if _, err := io.ReadFull(r, content); err != nil {
212+
// grab the salt, pwvv, data, and authcode
213+
saltpwvv := make([]byte, saltLen+2)
214+
if _, err := r.Read(saltpwvv); err != nil {
146215
return nil, ErrDecryption
147216
}
148-
// grab the salt, pwvv, data, and authcode
149-
salt := content[:saltLen]
150-
pwvv := content[saltLen : saltLen+2]
151-
content = content[saltLen+2:]
152-
size := f.CompressedSize64 - uint64(saltLen) - 2 - 10
153-
data := content[:size]
154-
authcode := content[size:]
217+
salt := saltpwvv[:saltLen]
218+
pwvv := saltpwvv[saltLen : saltLen+2]
219+
dataOff := int64(saltLen + 2)
220+
dataLen := int64(f.CompressedSize64 - uint64(saltLen) - 2 - 10)
221+
data := io.NewSectionReader(r, dataOff, dataLen)
222+
authOff := dataOff + dataLen
223+
authcode := io.NewSectionReader(r, authOff, 10)
155224
// generate keys
156225
decKey, authKey, pwv := generateKeys(f.password, salt, keyLen)
157226
// check password verifier (pwv)
158227
// Change to use crypto/subtle for constant time comparison
159-
if !bytes.Equal(pwv, pwvv) {
160-
return nil, ErrDecryption
161-
}
162-
// check authentication
163-
if !checkAuthentication(data, authcode, authKey) {
228+
if !checkPasswordVerification(pwv, pwvv) {
164229
return nil, ErrDecryption
165230
}
166-
return decryptStream(data, decKey), nil
231+
// setup auth reader
232+
ar := newAuthReader(authKey, data, authcode)
233+
// return decryption reader
234+
dr := decryptStream(decKey, ar)
235+
return ioutil.NopCloser(dr), nil
167236
}
168237

169-
func decryptStream(ciphertext, key []byte) io.Reader {
238+
func decryptStream(key []byte, ciphertext io.Reader) io.Reader {
170239
block, err := aes.NewCipher(key)
171240
if err != nil {
172241
return nil
173242
}
174243
stream := newWinZipCTR(block)
175-
reader := cipher.StreamReader{S: stream, R: bytes.NewReader(ciphertext)}
244+
reader := &cipher.StreamReader{S: stream, R: ciphertext}
176245
return reader
177246
}
178247

reader.go

+19-6
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ import (
1616
)
1717

1818
var (
19-
ErrFormat = errors.New("zip: not a valid zip file")
20-
ErrAlgorithm = errors.New("zip: unsupported compression algorithm")
21-
ErrChecksum = errors.New("zip: checksum error")
22-
ErrDecryption = errors.New("zip: decryption error")
19+
ErrFormat = errors.New("zip: not a valid zip file")
20+
ErrAlgorithm = errors.New("zip: unsupported compression algorithm")
21+
ErrChecksum = errors.New("zip: checksum error")
2322
)
2423

2524
type Reader struct {
@@ -53,6 +52,10 @@ func (f *File) IsEncrypted() bool {
5352
return f.Flags&0x1 == 1
5453
}
5554

55+
func (f *File) isAE2() bool {
56+
return f.ae == 2
57+
}
58+
5659
func (f *File) hasDataDescriptor() bool {
5760
return f.Flags&0x8 != 0
5861
}
@@ -156,12 +159,14 @@ func (f *File) Open() (rc io.ReadCloser, err error) {
156159
// and auth code lengths
157160
size := int64(f.CompressedSize64)
158161
var r io.Reader
159-
r = io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size)
162+
rr := io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size)
160163
// check for encryption
161164
if f.IsEncrypted() {
162-
if r, err = newDecryptionReader(r, f); err != nil {
165+
if r, err = newDecryptionReader(rr, f); err != nil {
163166
return
164167
}
168+
} else {
169+
r = rr
165170
}
166171
dcomp := decompressor(f.Method)
167172
if dcomp == nil {
@@ -174,6 +179,14 @@ func (f *File) Open() (rc io.ReadCloser, err error) {
174179
if f.hasDataDescriptor() {
175180
desr = io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset+size, dataDescriptorLen)
176181
}
182+
// if !f.isAE2() {
183+
// rc = &checksumReader{
184+
// rc: rc,
185+
// hash: crc32.NewIEEE(),
186+
// f: f,
187+
// desr: desr,
188+
// }
189+
// }
177190
rc = &checksumReader{
178191
rc: rc,
179192
hash: crc32.NewIEEE(),

reader_test.go

-94
Original file line numberDiff line numberDiff line change
@@ -605,97 +605,3 @@ func TestIssue11146(t *testing.T) {
605605
}
606606
r.Close()
607607
}
608-
609-
func TestSimplePassword(t *testing.T) {
610-
file := "hello-aes.zip"
611-
var buf bytes.Buffer
612-
r, err := OpenReader(filepath.Join("testdata", file))
613-
if err != nil {
614-
t.Errorf("Expected %s to open: %v.", file, err)
615-
}
616-
defer r.Close()
617-
if len(r.File) != 1 {
618-
t.Errorf("Expected %s to contain one file.", file)
619-
}
620-
f := r.File[0]
621-
if f.FileInfo().Name() != "hello.txt" {
622-
t.Errorf("Expected %s to have a file named hello.txt", file)
623-
}
624-
if f.Method != 0 {
625-
t.Errorf("Expected %s to have its Method set to 0.", file)
626-
}
627-
f.SetPassword([]byte("golang"))
628-
rc, err := f.Open()
629-
if err != nil {
630-
t.Errorf("Expected to open the readcloser: %v.", err)
631-
}
632-
_, err = io.Copy(&buf, rc)
633-
if err != nil {
634-
t.Errorf("Expected to copy bytes: %v.", err)
635-
}
636-
if !bytes.Contains(buf.Bytes(), []byte("Hello World\r\n")) {
637-
t.Errorf("Expected contents were not found.")
638-
}
639-
}
640-
641-
func TestHelloWorldAes(t *testing.T) {
642-
file := "world-aes.zip"
643-
expecting := "helloworld"
644-
r, err := OpenReader(filepath.Join("testdata", file))
645-
if err != nil {
646-
t.Errorf("Expected %s to open: %v", file, err)
647-
}
648-
defer r.Close()
649-
if len(r.File) != 2 {
650-
t.Errorf("Expected %s to contain two files.", file)
651-
}
652-
var b bytes.Buffer
653-
for _, f := range r.File {
654-
if !f.IsEncrypted() {
655-
t.Errorf("Expected %s to be encrypted.", f.FileInfo().Name)
656-
}
657-
f.SetPassword([]byte("golang"))
658-
rc, err := f.Open()
659-
if err != nil {
660-
t.Errorf("Expected to open readcloser: %v", err)
661-
}
662-
defer rc.Close()
663-
if _, err := io.Copy(&b, rc); err != nil {
664-
t.Errorf("Expected to copy bytes to buffer: %v", err)
665-
}
666-
}
667-
if !bytes.Equal([]byte(expecting), b.Bytes()) {
668-
t.Errorf("Expected ending content to be %s instead of %s", expecting, b.Bytes())
669-
}
670-
}
671-
672-
func TestMacbethAct1(t *testing.T) {
673-
file := "macbeth-act1.zip"
674-
expecting := "Exeunt"
675-
var b bytes.Buffer
676-
r, err := OpenReader(filepath.Join("testdata", file))
677-
if err != nil {
678-
t.Errorf("Expected %s to open: %v", file, err)
679-
}
680-
defer r.Close()
681-
for _, f := range r.File {
682-
if !f.IsEncrypted() {
683-
t.Errorf("Expected %s to be encrypted.", f.Name)
684-
}
685-
f.SetPassword([]byte("golang"))
686-
rc, err := f.Open()
687-
if err != nil {
688-
t.Errorf("Expected to open readcloser: %v", err)
689-
}
690-
defer rc.Close()
691-
if _, err := io.Copy(&b, rc); err != nil {
692-
t.Errorf("Expected to copy bytes to buffer: %v", err)
693-
}
694-
}
695-
if !bytes.Contains(b.Bytes(), []byte(expecting)) {
696-
t.Errorf("Expected to find %s in the buffer %v", expecting, b.Bytes())
697-
}
698-
}
699-
700-
// Test for AE-1 vs AE-2
701-
// Test for tampered data payload, use messWith

0 commit comments

Comments
 (0)