Skip to content

Commit dff173e

Browse files
author
alexmullins
committed
Add password protected writing
1 parent 1b10667 commit dff173e

File tree

5 files changed

+196
-12
lines changed

5 files changed

+196
-12
lines changed

crypto.go

+100-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"crypto/aes"
1010
"crypto/cipher"
1111
"crypto/hmac"
12+
"crypto/rand"
1213
"crypto/sha1"
1314
"crypto/subtle"
1415
"errors"
@@ -212,7 +213,7 @@ func (a *bufferedAuthReader) Read(b []byte) (int, error) {
212213
a.err = io.ErrUnexpectedEOF
213214
return 0, a.err
214215
}
215-
ab := new(bytes.Buffer)
216+
ab := new(bytes.Buffer) // remove this buffer and io.Copy to mac
216217
nn, err := io.Copy(ab, a.adata)
217218
if err != nil || nn != 10 {
218219
a.err = io.ErrUnexpectedEOF
@@ -266,10 +267,6 @@ func newDecryptionReader(r *io.SectionReader, f *File) (io.Reader, error) {
266267
if saltLen == 0 {
267268
return nil, ErrDecryption
268269
}
269-
// Change to a streaming implementation
270-
// Maybe not such a good idea after all. See:
271-
// https://www.imperialviolet.org/2014/06/27/streamingencryption.html
272-
// https://www.imperialviolet.org/2015/05/16/aeads.html
273270
// grab the salt, pwvv, data, and authcode
274271
saltpwvv := make([]byte, saltLen+2)
275272
if _, err := r.Read(saltpwvv); err != nil {
@@ -328,3 +325,101 @@ func aesKeyLen(strength byte) int {
328325
return 0
329326
}
330327
}
328+
329+
type authWriter struct {
330+
hmac hash.Hash // from fw.hmac
331+
w io.Writer // this will be the compCount writer
332+
}
333+
334+
func (aw *authWriter) Write(p []byte) (int, error) {
335+
_, err := aw.hmac.Write(p)
336+
if err != nil {
337+
return 0, err
338+
}
339+
return aw.w.Write(p)
340+
}
341+
342+
// writes out the salt, pwv, and then the encrypted file data
343+
type encryptionWriter struct {
344+
pwv []byte // password verification code to be written
345+
salt []byte // salt to be written
346+
w io.Writer // where to write the salt + pwv
347+
es io.Writer // where to write encrypted file data
348+
first bool // first write?
349+
err error // last error
350+
}
351+
352+
func (ew *encryptionWriter) Write(p []byte) (int, error) {
353+
if ew.err != nil {
354+
return 0, ew.err
355+
}
356+
if ew.first {
357+
// if our first time writing
358+
// must write out the salt and pwv first unencrypted
359+
_, err1 := ew.w.Write(ew.salt)
360+
_, err2 := ew.w.Write(ew.pwv)
361+
if err1 != nil || err2 != nil {
362+
ew.err = errors.New("zip: error writing salt or pwv")
363+
return 0, ew.err
364+
}
365+
ew.first = false
366+
}
367+
// now just pass on to the encryption stream
368+
return ew.es.Write(p)
369+
}
370+
371+
// newEncryptionWriter returns a io.Writer that when written to, 1. writes
372+
// out the salt, 2. writes out pwv, 3. writes out encrypted the data, and finally
373+
// 4. will write to hmac.
374+
func newEncryptionWriter(w io.Writer, fh *FileHeader, fw *fileWriter) (io.Writer, error) {
375+
var salt [16]byte
376+
_, err := rand.Read(salt[:])
377+
if err != nil {
378+
return nil, errors.New("zip: unable to generate random salt")
379+
}
380+
ekey, akey, pwv := generateKeys(fh.Password(), salt[:], aes256)
381+
fw.hmac = hmac.New(sha1.New, akey)
382+
aw := &authWriter{
383+
hmac: fw.hmac,
384+
w: w,
385+
}
386+
es, err := encryptStream(ekey, aw)
387+
if err != nil {
388+
return nil, err
389+
}
390+
ew := &encryptionWriter{
391+
pwv: pwv,
392+
salt: salt[:],
393+
w: w,
394+
es: es,
395+
first: true,
396+
}
397+
return ew, nil
398+
}
399+
400+
func encryptStream(key []byte, w io.Writer) (io.Writer, error) {
401+
block, err := aes.NewCipher(key)
402+
if err != nil {
403+
return nil, errors.New("zip: couldn't create AES cipher")
404+
}
405+
stream := newWinZipCTR(block)
406+
writer := &cipher.StreamWriter{S: stream, W: w}
407+
return writer, nil
408+
}
409+
410+
func (fh *FileHeader) writeWinZipExtra() {
411+
// total size is 11 bytes
412+
var buf [11]byte
413+
eb := writeBuf(buf[:])
414+
eb.uint16(winzipAesExtraId)
415+
eb.uint16(7) // following data size is 7
416+
eb.uint16(2) // ae 2
417+
eb.uint16(0x4541) // "AE"
418+
eb.uint8(3) // aes256
419+
eb.uint16(fh.Method) // original compression method
420+
fh.Extra = append(fh.Extra, buf[:]...)
421+
}
422+
423+
func (fh *FileHeader) setEncryptionBit() {
424+
fh.Flags |= 0x1
425+
}

crypto_test.go

+50-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func pwFn() []byte {
1212
}
1313

1414
// Test simple password reading.
15-
func TestPasswordSimple(t *testing.T) {
15+
func TestPasswordReadSimple(t *testing.T) {
1616
file := "hello-aes.zip"
1717
var buf bytes.Buffer
1818
r, err := OpenReader(filepath.Join("testdata", file))
@@ -173,3 +173,52 @@ func TestPasswordTamperedData(t *testing.T) {
173173
}
174174
}
175175
}
176+
177+
func TestPasswordWriteSimple(t *testing.T) {
178+
contents := []byte("Hello World")
179+
conLen := len(contents)
180+
181+
// Write a zip
182+
fh := &FileHeader{
183+
Name: "hello.txt",
184+
Password: pwFn,
185+
}
186+
raw := new(bytes.Buffer)
187+
zipw := NewWriter(raw)
188+
w, err := zipw.CreateHeader(fh)
189+
if err != nil {
190+
t.Errorf("Expected to create a new FileHeader")
191+
}
192+
n, err := io.Copy(w, bytes.NewReader(contents))
193+
if err != nil || n != int64(conLen) {
194+
t.Errorf("Expected to write the full contents to the writer.")
195+
}
196+
zipw.Close()
197+
198+
// Read the zip
199+
buf := new(bytes.Buffer)
200+
zipr, err := NewReader(bytes.NewReader(raw.Bytes()), int64(raw.Len()))
201+
if err != nil {
202+
t.Errorf("Expected to open a new zip reader: %v", err)
203+
}
204+
nn := len(zipr.File)
205+
if nn != 1 {
206+
t.Errorf("Expected to have one file in the zip archive, but has %d files", nn)
207+
}
208+
z := zipr.File[0]
209+
z.Password = pwFn
210+
rr, err := z.Open()
211+
if err != nil {
212+
t.Errorf("Expected to open the readcloser: %v", err)
213+
}
214+
n, err = io.Copy(buf, rr)
215+
if err != nil {
216+
t.Errorf("Expected to write to temporary buffer: %v", err)
217+
}
218+
if n != int64(conLen) {
219+
t.Errorf("Expected to copy %d bytes to temp buffer, but copied %d bytes instead", conLen, n)
220+
}
221+
if !bytes.Equal(contents, buf.Bytes()) {
222+
t.Errorf("Expected the unzipped contents to equal '%s', but was '%s' instead", contents, buf.Bytes())
223+
}
224+
}

struct.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,14 @@ type FileHeader struct {
9393
ExternalAttrs uint32 // Meaning depends on CreatorVersion
9494
Comment string
9595

96-
// encryption fields
97-
Password PasswordFn // The password to use when reading/writing
96+
// DeferAuth determines whether hmac checks happen before
97+
// any ciphertext is decrypted. It is recommended to leave this
98+
// set to false. For more detail:
99+
// https://www.imperialviolet.org/2014/06/27/streamingencryption.html
100+
// https://www.imperialviolet.org/2015/05/16/aeads.html
101+
DeferAuth bool
102+
103+
Password PasswordFn // Returns the password to use when reading/writing
98104
ae uint16
99105
aesStrength byte
100106
}

writer.go

+38-4
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,8 @@ func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, error) {
211211
}
212212

213213
fh.Flags |= 0x8 // we will write a data descriptor
214-
214+
// TODO(alex): Look at spec and see if these need to be changed
215+
// when using encryption.
215216
fh.CreatorVersion = fh.CreatorVersion&0xff00 | zipVersion20 // preserve compatibility byte
216217
fh.ReaderVersion = zipVersion20
217218

@@ -220,12 +221,27 @@ func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, error) {
220221
compCount: &countWriter{w: w.cw},
221222
crc32: crc32.NewIEEE(),
222223
}
224+
// Get the compressor before possibly changing Method to 99 due to password
223225
comp := compressor(fh.Method)
224226
if comp == nil {
225227
return nil, ErrAlgorithm
226228
}
229+
// check for password
230+
var sw io.Writer = fw.compCount
231+
if fh.Password != nil {
232+
// we have a password and need to encrypt.
233+
// 1. Set encryption bit in fh.Flags
234+
fh.setEncryptionBit()
235+
fh.writeWinZipExtra()
236+
fh.Method = 99 // ok to change, we've gotten the comp and wrote extra
237+
ew, err := newEncryptionWriter(sw, fh, fw)
238+
if err != nil {
239+
return nil, errors.New("zip: unable to create an encryption writer")
240+
}
241+
sw = ew
242+
}
227243
var err error
228-
fw.comp, err = comp(fw.compCount)
244+
fw.comp, err = comp(sw)
229245
if err != nil {
230246
return nil, err
231247
}
@@ -278,6 +294,8 @@ type fileWriter struct {
278294
compCount *countWriter
279295
crc32 hash.Hash32
280296
closed bool
297+
298+
hmac hash.Hash // possible hmac used for authentication when encrypting
281299
}
282300

283301
func (w *fileWriter) Write(p []byte) (int, error) {
@@ -296,10 +314,21 @@ func (w *fileWriter) close() error {
296314
if err := w.comp.Close(); err != nil {
297315
return err
298316
}
299-
317+
// if encrypted grab the hmac and write it out
318+
if w.header.IsEncrypted() {
319+
authCode := w.hmac.Sum(nil)
320+
authCode = authCode[:10]
321+
_, err := w.compCount.Write(authCode)
322+
if err != nil {
323+
return errors.New("zip: error writing authcode")
324+
}
325+
}
300326
// update FileHeader
301327
fh := w.header.FileHeader
302-
fh.CRC32 = w.crc32.Sum32()
328+
// ae-2 we don't write out CRC
329+
if !fh.IsEncrypted() {
330+
fh.CRC32 = w.crc32.Sum32()
331+
}
303332
fh.CompressedSize64 = uint64(w.compCount.count)
304333
fh.UncompressedSize64 = uint64(w.rawCount.count)
305334

@@ -358,6 +387,11 @@ func (w nopCloser) Close() error {
358387

359388
type writeBuf []byte
360389

390+
func (b *writeBuf) uint8(v uint8) {
391+
(*b)[0] = v
392+
*b = (*b)[1:]
393+
}
394+
361395
func (b *writeBuf) uint16(v uint16) {
362396
binary.LittleEndian.PutUint16(*b, v)
363397
*b = (*b)[2:]

zipwriters.png

19.8 KB
Loading

0 commit comments

Comments
 (0)