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
This commit is contained in:
Sudhi Herle 2020-01-29 16:47:14 +05:30
parent d18b7a05bc
commit 8ed3bff6db
6 changed files with 120 additions and 48 deletions

View file

@ -199,10 +199,13 @@ chunk is encoded the same way:
```C ```C
4 byte chunk length (big endian encoding) 4 byte chunk length (big endian encoding)
chunk data encrypted chunk data
AEAD tag 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 The chunk data and AEAD tag are treated as an atomic unit for AEAD
decryption. decryption.

33
sign/doc.go Normal file
View file

@ -0,0 +1,33 @@
// doc.go -- Documentation for sign & encrypt
//
// (c) 2016 Sudhi Herle <sudhi@herle.net>
//
// 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

View file

@ -11,8 +11,9 @@
// warranty; it is provided "as is". No claim is made to its // warranty; it is provided "as is". No claim is made to its
// suitability for any purpose. // suitability for any purpose.
// //
// Notes
// ===== // Implementation Notes for Encryption/Decryption:
//
// Header: has 3 parts: // Header: has 3 parts:
// - Fixed sized header // - Fixed sized header
// - Variable sized protobuf encoded header // - Variable sized protobuf encoded header
@ -32,6 +33,16 @@
// a protobuf message. This protobuf encoded message immediately // a protobuf message. This protobuf encoded message immediately
// follows the fixed length header. // 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 package sign
@ -153,7 +164,7 @@ func (e *Encryptor) Encrypt(rd io.Reader, wr io.Writer) error {
var eof bool var eof bool
for !eof { for !eof {
n, err := io.ReadAtLeast(rd, buf, int(e.ChunkSize)) 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 { if n >= 0 {
err = e.encrypt(buf[:n], wr, i, eof) err = e.encrypt(buf[:n], wr, i, eof)
if err != nil { 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 { func (e *Encryptor) encrypt(buf []byte, wr io.Writer, i uint32, eof bool) error {
var b [8]byte var b [8]byte
var noncebuf [32]byte var noncebuf [32]byte
var z uint32 = uint32(e.ae.Overhead() + len(buf)) var z uint32 = uint32(len(buf))
// mark last block // mark last block
if eof { if eof {
@ -249,8 +260,8 @@ func (e *Encryptor) encrypt(buf []byte, wr io.Writer, i uint32, eof bool) error
cbuf := e.buf[4:] cbuf := e.buf[4:]
c := e.ae.Seal(cbuf[:0], nonce, buf, b[:]) c := e.ae.Seal(cbuf[:0], nonce, buf, b[:])
// total number of bytes written
n := len(c) + 4 n := len(c) + 4
err := fullwrite(e.buf[:n], wr) err := fullwrite(e.buf[:n], wr)
if err != nil { if err != nil {
return fmt.Errorf("encrypt: %s", err) return fmt.Errorf("encrypt: %s", err)
@ -268,6 +279,7 @@ type Decryptor struct {
// Decrypted key // Decrypted key
key []byte key []byte
eof bool
} }
// Create a new decryption context and if 'pk' is given, check that it matches // 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) 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)) 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()?") 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 var i uint32
for i = 0; ; i++ { for i = 0; ; i++ {
c, eof, err := d.decrypt(i) c, eof, err := d.decrypt(i)
if err != nil { if err != nil {
return err return err
} }
if eof || len(c) == 0 {
return nil
}
if len(c) > 0 { if len(c) > 0 {
err = fullwrite(c, wr) err = fullwrite(c, wr)
if err != nil { if err != nil {
return fmt.Errorf("decrypt: %s", err) return fmt.Errorf("decrypt: %s", err)
} }
} }
if eof {
d.eof = true
return nil
}
} }
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) { func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
var b [8]byte var b [8]byte
var nonceb [32]byte var nonceb [32]byte
var ovh uint32 = uint32(d.ae.Overhead())
var p []byte
n, err := io.ReadFull(d.rd, b[:4]) 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) 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]) m := binary.BigEndian.Uint32(b[:4])
eof := (m & _EOF) > 0 eof := (m & _EOF) > 0
m &= (_EOF - 1) m &= (_EOF - 1)
// Sanity check - in case of corrupt header // 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) 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) binary.BigEndian.PutUint32(b[4:], i)
@ -455,9 +483,8 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
h.Write(b[:]) h.Write(b[:])
nonce := h.Sum(nonceb[:0]) nonce := h.Sum(nonceb[:0])
var p []byte z := m + ovh
if m > 0 { n, err = io.ReadFull(d.rd, d.buf[:z])
n, err = io.ReadFull(d.rd, d.buf[:m])
if err != nil { if err != nil {
return nil, false, fmt.Errorf("decrypt: premature EOF while reading block %d: %s", i, err) return nil, false, fmt.Errorf("decrypt: premature EOF while reading block %d: %s", i, err)
} }
@ -466,13 +493,8 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
if err != nil { if err != nil {
return nil, false, fmt.Errorf("decrypt: can't decrypt chunk %d: %s", i, err) return nil, false, fmt.Errorf("decrypt: can't decrypt chunk %d: %s", i, err)
} }
}
if eof && len(p) != 0 { return p[:m], eof, nil
return nil, false, fmt.Errorf("decrypt: EOF set on blk %d of len %d", i, m)
}
return p, eof, nil
} }
// Wrap a shared key with the recipient's public key 'pk' by generating an ephemeral // Wrap a shared key with the recipient's public key 'pk' by generating an ephemeral

View file

@ -29,13 +29,16 @@ func TestEncryptSimple(t *testing.T) {
receiver, err := NewKeypair() receiver, err := NewKeypair()
assert(err == nil, "receiver keypair gen failed: %s", err) assert(err == nil, "receiver keypair gen failed: %s", err)
var blkSize int = 1024
var size int = (blkSize * 10)
// cleartext // cleartext
buf := make([]byte, 64*1024) buf := make([]byte, size)
for i := 0; i < len(buf); i++ { for i := 0; i < len(buf); i++ {
buf[i] = byte(i & 0xff) buf[i] = byte(i & 0xff)
} }
ee, err := NewEncryptor(nil, 4096) ee, err := NewEncryptor(nil, uint64(blkSize))
assert(err == nil, "encryptor create fail: %s", err) assert(err == nil, "encryptor create fail: %s", err)
err = ee.AddRecipient(&receiver.Pub) err = ee.AddRecipient(&receiver.Pub)
@ -72,19 +75,22 @@ func TestEncryptCorrupted(t *testing.T) {
receiver, err := NewKeypair() receiver, err := NewKeypair()
assert(err == nil, "receiver keypair gen failed: %s", err) assert(err == nil, "receiver keypair gen failed: %s", err)
var blkSize int = 1024
var size int = (blkSize * 23) + randmod(blkSize)
// cleartext // cleartext
buf := make([]byte, 64*1024) buf := make([]byte, size)
for i := 0; i < len(buf); i++ { for i := 0; i < len(buf); i++ {
buf[i] = byte(i & 0xff) buf[i] = byte(i & 0xff)
} }
ee, err := NewEncryptor(nil, 4096) ee, err := NewEncryptor(nil, uint64(blkSize))
assert(err == nil, "encryptor create fail: %s", err) assert(err == nil, "encryptor create fail: %s", err)
err = ee.AddRecipient(&receiver.Pub) err = ee.AddRecipient(&receiver.Pub)
assert(err == nil, "can't add recipient: %s", err) assert(err == nil, "can't add recipient: %s", err)
rd := bytes.NewBuffer(buf) rd := bytes.NewReader(buf)
wr := bytes.Buffer{} wr := bytes.Buffer{}
err = ee.Encrypt(rd, &wr) err = ee.Encrypt(rd, &wr)
@ -98,7 +104,7 @@ func TestEncryptCorrupted(t *testing.T) {
rb[j] = byte(randint() & 0xff) rb[j] = byte(randint() & 0xff)
} }
rd = bytes.NewBuffer(rb) rd = bytes.NewReader(rb)
dd, err := NewDecryptor(rd) dd, err := NewDecryptor(rd)
assert(err != nil, "decryptor works on bad input") assert(err != nil, "decryptor works on bad input")
assert(dd == nil, "decryptor not nil for bad input") assert(dd == nil, "decryptor not nil for bad input")
@ -114,13 +120,16 @@ func TestEncryptSenderVerified(t *testing.T) {
receiver, err := NewKeypair() receiver, err := NewKeypair()
assert(err == nil, "receiver keypair gen failed: %s", err) assert(err == nil, "receiver keypair gen failed: %s", err)
var blkSize int = 1024
var size int = (blkSize * 23) + randmod(blkSize)
// cleartext // cleartext
buf := make([]byte, 64*1024) buf := make([]byte, size)
for i := 0; i < len(buf); i++ { for i := 0; i < len(buf); i++ {
buf[i] = byte(i & 0xff) 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) assert(err == nil, "encryptor create fail: %s", err)
err = ee.AddRecipient(&receiver.Pub) err = ee.AddRecipient(&receiver.Pub)
@ -157,13 +166,16 @@ func TestEncryptMultiReceiver(t *testing.T) {
sender, err := NewKeypair() sender, err := NewKeypair()
assert(err == nil, "sender keypair gen failed: %s", err) assert(err == nil, "sender keypair gen failed: %s", err)
var blkSize int = 1024
var size int = (blkSize * 23) + randmod(blkSize)
// cleartext // cleartext
buf := make([]byte, 64*1024) buf := make([]byte, size)
for i := 0; i < len(buf); i++ { for i := 0; i < len(buf); i++ {
buf[i] = byte(i & 0xff) 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) assert(err == nil, "encryptor create fail: %s", err)
n := 4 n := 4
@ -216,3 +228,7 @@ func randint() int {
return int(u & 0x7fffffff) return int(u & 0x7fffffff)
} }
func randmod(m int) int {
return randint() % m
}

View file

@ -11,12 +11,10 @@
// warranty; it is provided "as is". No claim is made to its // warranty; it is provided "as is". No claim is made to its
// suitability for any purpose. // suitability for any purpose.
// Package sign implements Ed25519 signing, verification on files. // This file implements:
// It builds upon golang.org/x/crypto/ed25519 by adding methods // - key generation, and key I/O
// for serializing and deserializing Ed25519 private & public keys. // - sign/verify of files and byte strings
// 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.
package sign package sign
import ( import (

View file

@ -1 +1 @@
0.7.2 0.8.0