forked from mirror/zip
Added initial support for reading pw protected files.
This commit is contained in:
parent
5ca6f99620
commit
ea2bc2cf67
|
@ -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
|
11
README.txt
11
README.txt
|
@ -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):
|
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
|
||||||
|
==============================================================================
|
||||||
|
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
|
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)
|
||||||
|
|
145
reader.go
145
reader.go
|
@ -6,19 +6,28 @@ package zip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
)
|
)
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -37,6 +46,32 @@ type File struct {
|
||||||
zipr io.ReaderAt
|
zipr io.ReaderAt
|
||||||
zipsize int64
|
zipsize int64
|
||||||
headerOffset 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 {
|
func (f *File) hasDataDescriptor() bool {
|
||||||
|
@ -138,8 +173,17 @@ func (f *File) Open() (rc io.ReadCloser, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// If f is encrypted, CompressedSize64 includes salt, pwvv, encrypted data,
|
||||||
|
// and auth code lengths
|
||||||
size := int64(f.CompressedSize64)
|
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)
|
dcomp := decompressor(f.Method)
|
||||||
if dcomp == nil {
|
if dcomp == nil {
|
||||||
err = ErrAlgorithm
|
err = ErrAlgorithm
|
||||||
|
@ -150,6 +194,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
// TODO: if AE-2, skip CRC
|
||||||
rc = &checksumReader{
|
rc = &checksumReader{
|
||||||
rc: rc,
|
rc: rc,
|
||||||
hash: crc32.NewIEEE(),
|
hash: crc32.NewIEEE(),
|
||||||
|
@ -159,6 +204,75 @@ func (f *File) Open() (rc io.ReadCloser, err error) {
|
||||||
return
|
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 {
|
type checksumReader struct {
|
||||||
rc io.ReadCloser
|
rc io.ReadCloser
|
||||||
hash hash.Hash32
|
hash hash.Hash32
|
||||||
|
@ -269,9 +383,10 @@ func readDirectoryHeader(f *File, r io.Reader) error {
|
||||||
if int(size) > len(b) {
|
if int(size) > len(b) {
|
||||||
return ErrFormat
|
return ErrFormat
|
||||||
}
|
}
|
||||||
if tag == zip64ExtraId {
|
eb := readBuf(b[:size])
|
||||||
|
switch tag {
|
||||||
|
case zip64ExtraId:
|
||||||
// update directory values from the zip64 extra block
|
// update directory values from the zip64 extra block
|
||||||
eb := readBuf(b[:size])
|
|
||||||
if len(eb) >= 8 {
|
if len(eb) >= 8 {
|
||||||
f.UncompressedSize64 = eb.uint64()
|
f.UncompressedSize64 = eb.uint64()
|
||||||
}
|
}
|
||||||
|
@ -281,6 +396,18 @@ func readDirectoryHeader(f *File, r io.Reader) error {
|
||||||
if len(eb) >= 8 {
|
if len(eb) >= 8 {
|
||||||
f.headerOffset = int64(eb.uint64())
|
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:]
|
b = b[size:]
|
||||||
}
|
}
|
||||||
|
@ -452,6 +579,12 @@ func findSignatureInBlock(b []byte) int {
|
||||||
|
|
||||||
type readBuf []byte
|
type readBuf []byte
|
||||||
|
|
||||||
|
func (b *readBuf) uint8() byte {
|
||||||
|
v := (*b)[0]
|
||||||
|
*b = (*b)[1:]
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
func (b *readBuf) uint16() uint16 {
|
func (b *readBuf) uint16() uint16 {
|
||||||
v := binary.LittleEndian.Uint16(*b)
|
v := binary.LittleEndian.Uint16(*b)
|
||||||
*b = (*b)[2:]
|
*b = (*b)[2:]
|
||||||
|
|
|
@ -605,3 +605,42 @@ 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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -62,7 +62,13 @@ const (
|
||||||
uint32max = (1 << 32) - 1
|
uint32max = (1 << 32) - 1
|
||||||
|
|
||||||
// extra header id's
|
// 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.
|
// FileHeader describes a file within a zip file.
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue