diff --git a/README.md b/README.md index ebd56f2..51c4b35 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ e.g., to verify the signature of *archive.tar.gz* against If the sender wishes to prove to the recipient that they encrypted a file: - sigtool encrypt -s sender.key to.pub -o archive.tar.gz.enc archive.tar.gz + sigtool encrypt -s sender.key to.pub -o archive.tar.gz.enc archive.tar.gz This will create an encrypted file *archive.tar.gz.enc* such that the @@ -101,7 +101,7 @@ who they expect. If the receiver has the public key of the sender, they can verify that they indeed sent the file by cryptographically checking the output: - sigtool decrypt -o archive.tar.gz -v sender.pub to.key archive.tar.gz.enc + sigtool decrypt -o archive.tar.gz -v sender.pub to.key archive.tar.gz.enc Note that the verification is optional and if the `-v` option is not used, then decryption will proceed without verifying the sender. @@ -110,7 +110,7 @@ used, then decryption will proceed without verifying the sender. `sigtool` can generate ephemeral keys for encrypting a file such that the receiver doesn't need to authenticate the sender: - sigtool encrypt to.pub -o archive.tar.gz.enc archive.tar.gz + sigtool encrypt to.pub -o archive.tar.gz.enc archive.tar.gz This will create an encrypted file *archive.tar.gz.enc* such that the recipient in possession of *to.key* can decrypt it. diff --git a/crypt.go b/crypt.go index ebcb23e..236d2dd 100644 --- a/crypt.go +++ b/crypt.go @@ -35,17 +35,24 @@ func encrypt(args []string) { var keyfile string var envpw string var pw bool + var sblksize string fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`") fs.StringVarP(&keyfile, "sign", "s", "", "Sign using private key `S`") fs.BoolVarP(&pw, "password", "p", false, "Ask for passphrase to decrypt the private key") fs.StringVarP(&envpw, "env-password", "", "", "Use passphrase from environment variable `E`") + fs.StringVarP(&sblksize, "block-size", "B", "4M", "Use `S` as the encryption block size") err := fs.Parse(args) if err != nil { die("%s", err) } + blksize, err := utils.ParseSize(sblksize) + if err != nil { + die("%s", err) + } + var pws, infile string if len(envpw) > 0 { @@ -108,7 +115,7 @@ func encrypt(args []string) { outfd = outf } - en, err := sign.NewEncryptor(sk) + en, err := sign.NewEncryptor(sk, blksize) if err != nil { die("%s", err) } diff --git a/go.mod b/go.mod index ef96194..39cbc19 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/gogo/protobuf v1.3.1 - github.com/opencoff/go-utils v0.3.0 + github.com/opencoff/go-utils v0.4.0 github.com/opencoff/pflag v0.3.3 golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc gopkg.in/yaml.v2 v2.2.4 diff --git a/go.sum b/go.sum index e318adb..aff4d00 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/opencoff/go-utils v0.3.0 h1:/TQXjf50o3GSB9MItog5L8Gf4GWJ4B5+rmqjB4g2RZQ= github.com/opencoff/go-utils v0.3.0/go.mod h1:c+7QUAiCCHcNH6OGvsZ0fviG7cgse8Y3ucg+xy7sGXM= +github.com/opencoff/go-utils v0.4.0 h1:pu08Om//u2+YGvLkHa2CyL6eI+/1J0bXih1Z6nuITp8= +github.com/opencoff/go-utils v0.4.0/go.mod h1:c+7QUAiCCHcNH6OGvsZ0fviG7cgse8Y3ucg+xy7sGXM= github.com/opencoff/pflag v0.3.3 h1:yohZkwYGPkB34WXvUQzU5GyLhImnjfePDARUaE8me3U= github.com/opencoff/pflag v0.3.3/go.mod h1:mTLzGGUGda1Av3d34iAJlh0JIlRxmFZtmc6qoWPspK0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/sign/crypt_test.go b/sign/crypt_test.go new file mode 100644 index 0000000..b65d18d --- /dev/null +++ b/sign/crypt_test.go @@ -0,0 +1,218 @@ +// crypt_test.go -- Test harness for encrypt/decrypt bits +// +// (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 + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "fmt" + "io" + "testing" +) + +// one sender, one receiver no verification of sender +func TestSimple(t *testing.T) { + assert := newAsserter(t) + + receiver, err := NewKeypair() + assert(err == nil, "receiver keypair gen failed: %s", err) + + // cleartext + buf := make([]byte, 64*1024) + for i := 0; i < len(buf); i++ { + buf[i] = byte(i & 0xff) + } + + ee, err := NewEncryptor(nil, 4096) + 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) + wr := bytes.Buffer{} + + err = ee.Encrypt(rd, &wr) + assert(err == nil, "encrypt fail: %s", err) + + rd = bytes.NewBuffer(wr.Bytes()) + + dd, err := NewDecryptor(rd) + assert(err == nil, "decryptor create fail: %s", err) + + err = dd.SetPrivateKey(&receiver.Sec, nil) + assert(err == nil, "decryptor can't add SK: %s", err) + + wr = bytes.Buffer{} + err = dd.Decrypt(&wr) + assert(err == nil, "decrypt fail: %s", err) + + b := wr.Bytes() + assert(len(b) == len(buf), "decrypt length mismatch: exp %d, saw %d", len(buf), len(b)) + + assert(byteEq(b, buf), "decrypt content mismatch") +} + +// test corrupted header or corrupted input +func TestCorrupted(t *testing.T) { + assert := newAsserter(t) + + receiver, err := NewKeypair() + assert(err == nil, "receiver keypair gen failed: %s", err) + + // cleartext + buf := make([]byte, 64*1024) + for i := 0; i < len(buf); i++ { + buf[i] = byte(i & 0xff) + } + + ee, err := NewEncryptor(nil, 4096) + 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) + wr := bytes.Buffer{} + + err = ee.Encrypt(rd, &wr) + assert(err == nil, "encrypt fail: %s", err) + + rb := wr.Bytes() + n := len(rb) + + for i := 0; i < n; i++ { + j := randint() % n + rb[j] = byte(randint() & 0xff) + } + + rd = bytes.NewBuffer(rb) + dd, err := NewDecryptor(rd) + assert(err != nil, "decryptor works on bad input") + assert(dd == nil, "decryptor not nil for bad input") +} + +// one sender, one receiver with verification of sender +func TestSenderVerified(t *testing.T) { + assert := newAsserter(t) + + sender, err := NewKeypair() + assert(err == nil, "sender keypair gen failed: %s", err) + + receiver, err := NewKeypair() + assert(err == nil, "receiver keypair gen failed: %s", err) + + // cleartext + buf := make([]byte, 64*1024) + for i := 0; i < len(buf); i++ { + buf[i] = byte(i & 0xff) + } + + ee, err := NewEncryptor(&sender.Sec, 4096) + 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) + wr := bytes.Buffer{} + + err = ee.Encrypt(rd, &wr) + assert(err == nil, "encrypt fail: %s", err) + + rd = bytes.NewBuffer(wr.Bytes()) + + dd, err := NewDecryptor(rd) + assert(err == nil, "decryptor create fail: %s", err) + + err = dd.SetPrivateKey(&receiver.Sec, &sender.Pub) + assert(err == nil, "decryptor can't add SK: %s", err) + + wr = bytes.Buffer{} + err = dd.Decrypt(&wr) + assert(err == nil, "decrypt fail: %s", err) + + b := wr.Bytes() + assert(len(b) == len(buf), "decrypt length mismatch: exp %d, saw %d", len(buf), len(b)) + + assert(byteEq(b, buf), "decrypt content mismatch") +} + +// one sender, multiple receivers, each decrypting the blob +func TestMultiReceiver(t *testing.T) { + assert := newAsserter(t) + + sender, err := NewKeypair() + assert(err == nil, "sender keypair gen failed: %s", err) + + // cleartext + buf := make([]byte, 64*1024) + for i := 0; i < len(buf); i++ { + buf[i] = byte(i & 0xff) + } + + ee, err := NewEncryptor(&sender.Sec, 4096) + assert(err == nil, "encryptor create fail: %s", err) + + n := 4 + rx := make([]*Keypair, n) + for i := 0; i < n; i++ { + r, err := NewKeypair() + assert(err == nil, "can't make receiver key %d: %s", i, err) + rx[i] = r + + err = ee.AddRecipient(&r.Pub) + assert(err == nil, "can't add recipient %d: %s", i, err) + } + + rd := bytes.NewBuffer(buf) + wr := bytes.Buffer{} + + err = ee.Encrypt(rd, &wr) + assert(err == nil, "encrypt fail: %s", err) + + encBytes := wr.Bytes() + for i := 0; i < n; i++ { + rd = bytes.NewBuffer(encBytes) + + dd, err := NewDecryptor(rd) + assert(err == nil, "decryptor %d create fail: %s", i, err) + + err = dd.SetPrivateKey(&rx[i].Sec, &sender.Pub) + assert(err == nil, "decryptor can't add SK %d: %s", i, err) + + wr = bytes.Buffer{} + err = dd.Decrypt(&wr) + assert(err == nil, "decrypt %d fail: %s", i, err) + + b := wr.Bytes() + assert(len(b) == len(buf), "decrypt %d length mismatch: exp %d, saw %d", i, len(buf), len(b)) + + assert(byteEq(b, buf), "decrypt %d content mismatch", i) + } +} + +func randint() int { + var b [4]byte + + _, err := io.ReadFull(rand.Reader, b[:]) + if err != nil { + panic(fmt.Sprintf("can't read 4 rand bytes: %s", err)) + } + + u := binary.BigEndian.Uint32(b[:]) + + return int(u & 0x7fffffff) +} diff --git a/sign/encrypt.go b/sign/encrypt.go index a421287..4615ee1 100644 --- a/sign/encrypt.go +++ b/sign/encrypt.go @@ -1,4 +1,4 @@ -// cipher.go -- Ed25519 based encrypt/decrypt +// encrypt.go -- Ed25519 based encrypt/decrypt // // (c) 2016 Sudhi Herle // @@ -31,6 +31,7 @@ import ( // Encryption chunk size = 4MB const chunkSize int = 4 * 1048576 +const maxChunkSize int = 16 * 1048576 const _Magic = "SigTool" const _MagicLen = len(_Magic) @@ -51,11 +52,19 @@ type Encryptor struct { // Create a new Encryption context and use the optional private key 'sk' for // signing any recipient keys. If 'sk' is nil, then ephmeral Curve25519 keys // are generated and used with recipient's public key. -func NewEncryptor(sk *PrivateKey) (*Encryptor, error) { +func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) { + + if blksize >= uint64(maxChunkSize) { + return nil, fmt.Errorf("encrypt: Blocksize is too large (max 16M)") + } + + if blksize == 0 { + blksize = uint64(chunkSize) + } e := &Encryptor{ Header: Header{ - ChunkSize: uint32(chunkSize), + ChunkSize: uint32(blksize), Salt: make([]byte, _AEADNonceLen), }, @@ -282,7 +291,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) { return nil, fmt.Errorf("decrypt: decode error: %s", err) } - if d.ChunkSize == 0 || d.ChunkSize > (16*1048576) { + if d.ChunkSize == 0 || d.ChunkSize >= uint32(maxChunkSize) { return nil, fmt.Errorf("decrypt: invalid chunkSize %d", d.ChunkSize) } @@ -397,7 +406,7 @@ func (d *Decryptor) decrypt(i int) ([]byte, error) { chunklen := int(binary.BigEndian.Uint32(b[:4])) // Sanity check - in case of corrupt header - if chunklen > (d.ae.Overhead()+chunkSize) { + if chunklen > (d.ae.Overhead() + chunkSize) { return nil, fmt.Errorf("decrypt: chunksize is too large (%d)", chunklen) } diff --git a/version b/version index 0c62199..9e11b32 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.2.1 +0.3.1