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:
parent
d18b7a05bc
commit
8ed3bff6db
6 changed files with 120 additions and 48 deletions
|
@ -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
33
sign/doc.go
Normal 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
|
|
@ -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,9 +483,8 @@ 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])
|
||||
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)
|
||||
}
|
||||
|
@ -466,13 +493,8 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
|
|||
if err != nil {
|
||||
return nil, false, fmt.Errorf("decrypt: can't decrypt chunk %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)
|
||||
}
|
||||
|
||||
return p, eof, nil
|
||||
return p[:m], eof, nil
|
||||
}
|
||||
|
||||
// Wrap a shared key with the recipient's public key 'pk' by generating an ephemeral
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
10
sign/sign.go
10
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 (
|
||||
|
|
2
version
2
version
|
@ -1 +1 @@
|
|||
0.7.2
|
||||
0.8.0
|
||||
|
|
Loading…
Add table
Reference in a new issue