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
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.

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
// 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

View file

@ -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
}

View file

@ -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 (

View file

@ -1 +1 @@
0.7.2
0.8.0