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
|
```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
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
|
// 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,24 +483,18 @@ 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if eof && len(p) != 0 {
|
p, err = d.ae.Open(d.buf[:0], nonce, d.buf[:n], b[:])
|
||||||
return nil, false, fmt.Errorf("decrypt: EOF set on blk %d of len %d", i, m)
|
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
|
// 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()
|
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
|
||||||
|
}
|
||||||
|
|
10
sign/sign.go
10
sign/sign.go
|
@ -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 (
|
||||||
|
|
2
version
2
version
|
@ -1 +1 @@
|
||||||
0.7.2
|
0.8.0
|
||||||
|
|
Loading…
Add table
Reference in a new issue