From 8ed3bff6db19f54540432f6f4407ccf8d4ea7ee9 Mon Sep 17 00:00:00 2001 From: Sudhi Herle Date: Wed, 29 Jan 2020 16:47:14 +0530 Subject: [PATCH] Cleaned up chunk header encoding during encrypt/decrypt. * encrypted chunk header now encodes _only_ plain text length * the AEAD tag length is implicitly added when reading/writing * added better sanity checks for short blocks during decrypt * io.ReadAtLeast() reports ErrUnexpectedEOF for less than a full chunk; use this signal correctly * major version bump to denote header incompatibility --- README.md | 5 ++- sign/doc.go | 33 ++++++++++++++++++ sign/encrypt.go | 82 ++++++++++++++++++++++++++++---------------- sign/encrypt_test.go | 36 +++++++++++++------ sign/sign.go | 10 +++--- version | 2 +- 6 files changed, 120 insertions(+), 48 deletions(-) create mode 100644 sign/doc.go diff --git a/README.md b/README.md index b713fd2..e60234e 100644 --- a/README.md +++ b/README.md @@ -199,10 +199,13 @@ chunk is encoded the same way: ```C 4 byte chunk length (big endian encoding) - chunk data + encrypted chunk data AEAD tag ``` +The chunk length does _not_ include the AEAD tag length; it is implicitly +computed. + The chunk data and AEAD tag are treated as an atomic unit for AEAD decryption. diff --git a/sign/doc.go b/sign/doc.go new file mode 100644 index 0000000..ada10b0 --- /dev/null +++ b/sign/doc.go @@ -0,0 +1,33 @@ +// doc.go -- Documentation for sign & encrypt +// +// (c) 2016 Sudhi Herle +// +// Licensing Terms: GPLv2 +// +// If you need a commercial license for this work, please contact +// the author. +// +// This software does not come with any express or implied +// warranty; it is provided "as is". No claim is made to its +// suitability for any purpose. + +// Package sign implements Ed25519 signing, verification on files. +// It builds upon golang.org/x/crypto/ed25519 by adding methods +// for serializing and deserializing Ed25519 private & public keys. +// +// It can sign and verify very large files - it prehashes the files +// with SHA-512 and then signs the SHA-512 checksum. The keys and signatures +// are YAML files and so, human readable. +// +// It can encrypt files for multiple recipients - each of whom is identified +// by their Ed25519 public key. The encryption by default generates ephmeral +// Curve25519 keys and creates pair-wise shared secret for each recipient of +// the encrypted file. The caller can optionally use a specific secret key +// during the encryption process - this has the benefit of also authenticating +// the sender (and the receiver can verify the sender if they possess the +// corresponding public key). +// +// The sign, verify, encrypt, decrypt operations can use OpenSSH Ed25519 keys +// *or* the keys generated by sigtool. This means, you can send encrypted +// files to any recipient identified by their comment in `~/.ssh/authorized_keys`. +package sign diff --git a/sign/encrypt.go b/sign/encrypt.go index f410615..e36a820 100644 --- a/sign/encrypt.go +++ b/sign/encrypt.go @@ -11,8 +11,9 @@ // warranty; it is provided "as is". No claim is made to its // suitability for any purpose. // -// Notes -// ===== + +// Implementation Notes for Encryption/Decryption: +// // Header: has 3 parts: // - Fixed sized header // - Variable sized protobuf encoded header @@ -32,6 +33,16 @@ // a protobuf message. This protobuf encoded message immediately // follows the fixed length header. // +// The input data is broken up into "chunks"; each no larger than +// maxChunkSize. The default block size is "chunkSize". Each block +// is AEAD encrypted: +// AEAD nonce = SHA256(header.salt || block# || block-size) +// +// The encrypted block (includes the AEAD tag) length is written +// as a big-endian 4-byte prefix. The high-order bit of this length +// field is set for the last-block (denoting EOF). +// +// The encrypted blocks use an opinionated nonce length of 32 (_AEADNonceLen). package sign @@ -153,7 +164,7 @@ func (e *Encryptor) Encrypt(rd io.Reader, wr io.Writer) error { var eof bool for !eof { n, err := io.ReadAtLeast(rd, buf, int(e.ChunkSize)) - eof = err == io.EOF || err == io.ErrClosedPipe + eof = err == io.EOF || err == io.ErrClosedPipe || err == io.ErrUnexpectedEOF if n >= 0 { err = e.encrypt(buf[:n], wr, i, eof) if err != nil { @@ -230,7 +241,7 @@ func fullwrite(buf []byte, wr io.Writer) error { func (e *Encryptor) encrypt(buf []byte, wr io.Writer, i uint32, eof bool) error { var b [8]byte var noncebuf [32]byte - var z uint32 = uint32(e.ae.Overhead() + len(buf)) + var z uint32 = uint32(len(buf)) // mark last block if eof { @@ -249,8 +260,8 @@ func (e *Encryptor) encrypt(buf []byte, wr io.Writer, i uint32, eof bool) error cbuf := e.buf[4:] c := e.ae.Seal(cbuf[:0], nonce, buf, b[:]) + // total number of bytes written n := len(c) + 4 - err := fullwrite(e.buf[:n], wr) if err != nil { return fmt.Errorf("encrypt: %s", err) @@ -268,6 +279,7 @@ type Decryptor struct { // Decrypted key key []byte + eof bool } // Create a new decryption context and if 'pk' is given, check that it matches @@ -330,7 +342,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) { return nil, fmt.Errorf("decrypt: invalid chunkSize %d", d.ChunkSize) } - if len(d.Salt) != 32 { + if len(d.Salt) != _AEADNonceLen { return nil, fmt.Errorf("decrypt: invalid nonce length %d", len(d.Salt)) } @@ -405,22 +417,28 @@ func (d *Decryptor) Decrypt(wr io.Writer) error { return fmt.Errorf("decrypt: wrapped-key not decrypted (missing SetPrivateKey()?") } + if d.eof { + return fmt.Errorf("decrypt: input stream has reached EOF") + } + var i uint32 for i = 0; ; i++ { c, eof, err := d.decrypt(i) if err != nil { return err } - if eof || len(c) == 0 { - return nil - } - if len(c) > 0 { err = fullwrite(c, wr) if err != nil { return fmt.Errorf("decrypt: %s", err) } } + + if eof { + d.eof = true + return nil + } + } return nil } @@ -429,24 +447,34 @@ func (d *Decryptor) Decrypt(wr io.Writer) error { func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) { var b [8]byte var nonceb [32]byte + var ovh uint32 = uint32(d.ae.Overhead()) + var p []byte n, err := io.ReadFull(d.rd, b[:4]) - if err == io.EOF || err == io.ErrClosedPipe || n == 0 { + if err != nil || n == 0 { return nil, false, fmt.Errorf("decrypt: premature EOF while reading header block %d", i) } - if err != nil { - return nil, false, fmt.Errorf("decrypt: can't read chunk %d length: %s", i, err) - } - m := binary.BigEndian.Uint32(b[:4]) eof := (m & _EOF) > 0 m &= (_EOF - 1) // Sanity check - in case of corrupt header - if m > (uint32(d.ae.Overhead()) + d.ChunkSize) { + switch { + case m > uint32(d.ChunkSize): return nil, false, fmt.Errorf("decrypt: chunksize is too large (%d)", m) + + case m == 0: + if !eof { + return nil, false, fmt.Errorf("decrypt: block %d: zero-sized chunk without EOF", i) + } + return p, eof, nil + + case m < ovh: + return nil, false, fmt.Errorf("decrypt: chunksize is too small (%d)", m) + + default: } binary.BigEndian.PutUint32(b[4:], i) @@ -455,24 +483,18 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) { h.Write(b[:]) nonce := h.Sum(nonceb[:0]) - var p []byte - if m > 0 { - n, err = io.ReadFull(d.rd, d.buf[:m]) - if err != nil { - return nil, false, fmt.Errorf("decrypt: premature EOF while reading block %d: %s", i, err) - } - - p, err = d.ae.Open(d.buf[:0], nonce, d.buf[:n], b[:]) - if err != nil { - return nil, false, fmt.Errorf("decrypt: can't decrypt chunk %d: %s", i, err) - } + z := m + ovh + n, err = io.ReadFull(d.rd, d.buf[:z]) + if err != nil { + return nil, false, fmt.Errorf("decrypt: premature EOF while reading block %d: %s", i, err) } - if eof && len(p) != 0 { - return nil, false, fmt.Errorf("decrypt: EOF set on blk %d of len %d", i, m) + p, err = d.ae.Open(d.buf[:0], nonce, d.buf[:n], b[:]) + if err != nil { + return nil, false, fmt.Errorf("decrypt: can't decrypt chunk %d: %s", i, err) } - return p, eof, nil + return p[:m], eof, nil } // Wrap a shared key with the recipient's public key 'pk' by generating an ephemeral diff --git a/sign/encrypt_test.go b/sign/encrypt_test.go index 73c2fd3..ce538af 100644 --- a/sign/encrypt_test.go +++ b/sign/encrypt_test.go @@ -29,13 +29,16 @@ func TestEncryptSimple(t *testing.T) { receiver, err := NewKeypair() assert(err == nil, "receiver keypair gen failed: %s", err) + var blkSize int = 1024 + var size int = (blkSize * 10) + // cleartext - buf := make([]byte, 64*1024) + buf := make([]byte, size) for i := 0; i < len(buf); i++ { buf[i] = byte(i & 0xff) } - ee, err := NewEncryptor(nil, 4096) + ee, err := NewEncryptor(nil, uint64(blkSize)) assert(err == nil, "encryptor create fail: %s", err) err = ee.AddRecipient(&receiver.Pub) @@ -72,19 +75,22 @@ func TestEncryptCorrupted(t *testing.T) { receiver, err := NewKeypair() assert(err == nil, "receiver keypair gen failed: %s", err) + var blkSize int = 1024 + var size int = (blkSize * 23) + randmod(blkSize) + // cleartext - buf := make([]byte, 64*1024) + buf := make([]byte, size) for i := 0; i < len(buf); i++ { buf[i] = byte(i & 0xff) } - ee, err := NewEncryptor(nil, 4096) + ee, err := NewEncryptor(nil, uint64(blkSize)) assert(err == nil, "encryptor create fail: %s", err) err = ee.AddRecipient(&receiver.Pub) assert(err == nil, "can't add recipient: %s", err) - rd := bytes.NewBuffer(buf) + rd := bytes.NewReader(buf) wr := bytes.Buffer{} err = ee.Encrypt(rd, &wr) @@ -98,7 +104,7 @@ func TestEncryptCorrupted(t *testing.T) { rb[j] = byte(randint() & 0xff) } - rd = bytes.NewBuffer(rb) + rd = bytes.NewReader(rb) dd, err := NewDecryptor(rd) assert(err != nil, "decryptor works on bad input") assert(dd == nil, "decryptor not nil for bad input") @@ -114,13 +120,16 @@ func TestEncryptSenderVerified(t *testing.T) { receiver, err := NewKeypair() assert(err == nil, "receiver keypair gen failed: %s", err) + var blkSize int = 1024 + var size int = (blkSize * 23) + randmod(blkSize) + // cleartext - buf := make([]byte, 64*1024) + buf := make([]byte, size) for i := 0; i < len(buf); i++ { buf[i] = byte(i & 0xff) } - ee, err := NewEncryptor(&sender.Sec, 4096) + ee, err := NewEncryptor(&sender.Sec, uint64(blkSize)) assert(err == nil, "encryptor create fail: %s", err) err = ee.AddRecipient(&receiver.Pub) @@ -157,13 +166,16 @@ func TestEncryptMultiReceiver(t *testing.T) { sender, err := NewKeypair() assert(err == nil, "sender keypair gen failed: %s", err) + var blkSize int = 1024 + var size int = (blkSize * 23) + randmod(blkSize) + // cleartext - buf := make([]byte, 64*1024) + buf := make([]byte, size) for i := 0; i < len(buf); i++ { buf[i] = byte(i & 0xff) } - ee, err := NewEncryptor(&sender.Sec, 4096) + ee, err := NewEncryptor(&sender.Sec, uint64(blkSize)) assert(err == nil, "encryptor create fail: %s", err) n := 4 @@ -216,3 +228,7 @@ func randint() int { return int(u & 0x7fffffff) } + +func randmod(m int) int { + return randint() % m +} diff --git a/sign/sign.go b/sign/sign.go index 96333b9..7c431c8 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -11,12 +11,10 @@ // warranty; it is provided "as is". No claim is made to its // suitability for any purpose. -// Package sign implements Ed25519 signing, verification on files. -// It builds upon golang.org/x/crypto/ed25519 by adding methods -// for serializing and deserializing Ed25519 private & public keys. -// In addition, it works with large files - by precalculating their -// SHA512 checksum in mmap'd mode and sending the 64 byte signature -// for Ed25519 signing. +// This file implements: +// - key generation, and key I/O +// - sign/verify of files and byte strings + package sign import ( diff --git a/version b/version index 7486fdb..a3df0a6 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.7.2 +0.8.0