mirror of https://github.com/yeka/zip.git
defer authentication/buffering to Read not Open
This commit is contained in:
parent
37ce86c83e
commit
cf103a6528
13
README.txt
13
README.txt
|
@ -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
109
crypto.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
25
reader.go
25
reader.go
|
@ -16,10 +16,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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(),
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
Loading…
Reference in New Issue