Skip to content

Commit ea2bc2c

Browse files
author
alexmullins
committed
Added initial support for reading pw protected files.
1 parent 5ca6f99 commit ea2bc2c

File tree

6 files changed

+222
-7
lines changed

6 files changed

+222
-7
lines changed

.gitignore

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
.DS_Store
2+
3+
# Compiled Object files, Static and Dynamic libs (Shared Objects)
4+
*.o
5+
*.a
6+
*.so
7+
8+
# Folders
9+
_obj
10+
_test
11+
12+
# Architecture specific extensions/prefixes
13+
*.[568vq]
14+
[568vq].out
15+
16+
*.cgo1.go
17+
*.cgo2.c
18+
_cgo_defun.c
19+
_cgo_gotypes.go
20+
_cgo_export.*
21+
22+
_testmain.go
23+
24+
*.exe
25+
*.test
26+
*.prof

README.txt

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

8+
The process
9+
==============================================================================
10+
hello.txt -> compress -> encrypt -> .zip -> decrypt -> decompress -> hello.txt
11+
12+
Roadmap
13+
================================================
14+
Reading - Almost done (TODO: check for AE-2 and skip CRC).
15+
Writing - Not started.
16+
Testing - Needs more.
17+
18+
819
WinZip AES specifies
920
====================================================================
1021
1. Encryption-Decryption w/ AES-CTR (128, 192, or 256 bits)

reader.go

+139-6
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,28 @@ package zip
66

77
import (
88
"bufio"
9+
"bytes"
10+
"crypto/aes"
11+
"crypto/cipher"
12+
"crypto/hmac"
13+
"crypto/sha1"
914
"encoding/binary"
1015
"errors"
1116
"fmt"
1217
"hash"
1318
"hash/crc32"
1419
"io"
20+
"io/ioutil"
1521
"os"
22+
23+
"golang.org/x/crypto/pbkdf2"
1624
)
1725

1826
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")
27+
ErrFormat = errors.New("zip: not a valid zip file")
28+
ErrAlgorithm = errors.New("zip: unsupported compression algorithm")
29+
ErrChecksum = errors.New("zip: checksum error")
30+
ErrDecryption = errors.New("zip: decryption error")
2231
)
2332

2433
type Reader struct {
@@ -37,6 +46,32 @@ type File struct {
3746
zipr io.ReaderAt
3847
zipsize int64
3948
headerOffset int64
49+
password []byte
50+
ae uint16
51+
aesStrength byte
52+
}
53+
54+
func aesKeyLen(strength byte) int {
55+
switch strength {
56+
case 1:
57+
return aes128
58+
case 2:
59+
return aes192
60+
case 3:
61+
return aes256
62+
default:
63+
return 0
64+
}
65+
}
66+
67+
// SetPassword must be called before calling Open on the file.
68+
func (f *File) SetPassword(password []byte) {
69+
f.password = password
70+
}
71+
72+
// IsEncrypted indicates whether this file's data is encrypted.
73+
func (f *File) IsEncrypted() bool {
74+
return f.Flags&0x1 == 1
4075
}
4176

4277
func (f *File) hasDataDescriptor() bool {
@@ -138,8 +173,17 @@ func (f *File) Open() (rc io.ReadCloser, err error) {
138173
if err != nil {
139174
return
140175
}
176+
// If f is encrypted, CompressedSize64 includes salt, pwvv, encrypted data,
177+
// and auth code lengths
141178
size := int64(f.CompressedSize64)
142-
r := io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size)
179+
var r io.Reader
180+
r = io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size)
181+
// check for encryption
182+
if f.IsEncrypted() {
183+
if r, err = newDecryptionReader(r, f); err != nil {
184+
return
185+
}
186+
}
143187
dcomp := decompressor(f.Method)
144188
if dcomp == nil {
145189
err = ErrAlgorithm
@@ -150,6 +194,7 @@ func (f *File) Open() (rc io.ReadCloser, err error) {
150194
if f.hasDataDescriptor() {
151195
desr = io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset+size, dataDescriptorLen)
152196
}
197+
// TODO: if AE-2, skip CRC
153198
rc = &checksumReader{
154199
rc: rc,
155200
hash: crc32.NewIEEE(),
@@ -159,6 +204,75 @@ func (f *File) Open() (rc io.ReadCloser, err error) {
159204
return
160205
}
161206

207+
func newDecryptionReader(r io.Reader, f *File) (io.ReadCloser, error) {
208+
keyLen := aesKeyLen(f.aesStrength)
209+
saltLen := keyLen / 2 // salt is half of key len
210+
if saltLen == 0 {
211+
return nil, ErrDecryption
212+
}
213+
214+
content := make([]byte, f.CompressedSize64)
215+
if _, err := io.ReadFull(r, content); err != nil {
216+
return nil, ErrDecryption
217+
}
218+
219+
// grab the salt, pwvv, data, and authcode
220+
salt := content[:saltLen]
221+
pwvv := content[saltLen : saltLen+2]
222+
content = content[saltLen+2:]
223+
size := f.UncompressedSize64
224+
data := content[:size]
225+
authcode := content[size:]
226+
227+
// generate keys
228+
decKey, authKey, pwv := generateKeys(f.password, salt, keyLen)
229+
230+
// check password verifier (pwv)
231+
if !bytes.Equal(pwv, pwvv) {
232+
return nil, ErrDecryption
233+
}
234+
235+
// check authentication
236+
if !checkAuthentication(data, authcode, authKey) {
237+
return nil, ErrDecryption
238+
}
239+
240+
// set the IV
241+
var iv [aes.BlockSize]byte
242+
iv[0] = 1
243+
244+
return decryptStream(data, decKey, iv[:]), nil
245+
}
246+
247+
func decryptStream(ciphertext, key, iv []byte) io.ReadCloser {
248+
block, err := aes.NewCipher(key)
249+
if err != nil {
250+
return nil
251+
}
252+
stream := cipher.NewCTR(block, iv)
253+
reader := cipher.StreamReader{S: stream, R: bytes.NewReader(ciphertext)}
254+
return ioutil.NopCloser(reader)
255+
}
256+
257+
func checkAuthentication(message, authcode, key []byte) bool {
258+
mac := hmac.New(sha1.New, key)
259+
mac.Write(message)
260+
expectedAuthCode := mac.Sum(nil)
261+
// Truncate at the first 10 bytes
262+
expectedAuthCode = expectedAuthCode[:10]
263+
return bytes.Equal(expectedAuthCode, authcode)
264+
}
265+
266+
func generateKeys(password, salt []byte, keySize int) (encKey, authKey, pwv []byte) {
267+
totalSize := (keySize * 2) + 2 // enc + auth + pv sizes
268+
269+
key := pbkdf2.Key(password, salt, 1000, totalSize, sha1.New)
270+
encKey = key[:keySize]
271+
authKey = key[keySize : keySize*2]
272+
pwv = key[keySize*2:]
273+
return
274+
}
275+
162276
type checksumReader struct {
163277
rc io.ReadCloser
164278
hash hash.Hash32
@@ -269,9 +383,10 @@ func readDirectoryHeader(f *File, r io.Reader) error {
269383
if int(size) > len(b) {
270384
return ErrFormat
271385
}
272-
if tag == zip64ExtraId {
386+
eb := readBuf(b[:size])
387+
switch tag {
388+
case zip64ExtraId:
273389
// update directory values from the zip64 extra block
274-
eb := readBuf(b[:size])
275390
if len(eb) >= 8 {
276391
f.UncompressedSize64 = eb.uint64()
277392
}
@@ -281,6 +396,18 @@ func readDirectoryHeader(f *File, r io.Reader) error {
281396
if len(eb) >= 8 {
282397
f.headerOffset = int64(eb.uint64())
283398
}
399+
case winzipAesExtraId:
400+
// grab the AE version
401+
f.ae = eb.uint16()
402+
403+
// skip vendor ID
404+
_ = eb.uint16()
405+
406+
// AES strength
407+
f.aesStrength = eb.uint8()
408+
409+
// set the actual compression method.
410+
f.Method = eb.uint16()
284411
}
285412
b = b[size:]
286413
}
@@ -452,6 +579,12 @@ func findSignatureInBlock(b []byte) int {
452579

453580
type readBuf []byte
454581

582+
func (b *readBuf) uint8() byte {
583+
v := (*b)[0]
584+
*b = (*b)[1:]
585+
return v
586+
}
587+
455588
func (b *readBuf) uint16() uint16 {
456589
v := binary.LittleEndian.Uint16(*b)
457590
*b = (*b)[2:]

reader_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -605,3 +605,42 @@ 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+
618+
if len(r.File) != 1 {
619+
t.Errorf("Expected %s to contain one file.", file)
620+
}
621+
622+
f := r.File[0]
623+
624+
if f.FileInfo().Name() != "hello.txt" {
625+
t.Errorf("Expected %s to have a file named hello.txt", file)
626+
}
627+
628+
if f.Method != 0 {
629+
t.Errorf("Expected %s to have its Method set to 0.", file)
630+
}
631+
632+
f.SetPassword([]byte("golang"))
633+
634+
rc, err := f.Open()
635+
if err != nil {
636+
t.Errorf("Expected to open the readcloser: %v.", err)
637+
}
638+
_, err = io.Copy(&buf, rc)
639+
if err != nil {
640+
t.Errorf("Expected to copy bytes: %v.", err)
641+
}
642+
643+
if !bytes.Contains(buf.Bytes(), []byte("Hello World\r\n")) {
644+
t.Errorf("Expected contents were not found.")
645+
}
646+
}

struct.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,13 @@ const (
6262
uint32max = (1 << 32) - 1
6363

6464
// extra header id's
65-
zip64ExtraId = 0x0001 // zip64 Extended Information Extra Field
65+
zip64ExtraId = 0x0001 // zip64 Extended Information Extra Field
66+
winzipAesExtraId = 0x9901 // winzip AES Extra Field
67+
68+
// AES key lengths
69+
aes128 = 16
70+
aes192 = 24
71+
aes256 = 32
6672
)
6773

6874
// FileHeader describes a file within a zip file.

testdata/hello-aes.zip

215 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)