Skip to content

Commit

Permalink
Implement XC message chunking (#680)
Browse files Browse the repository at this point in the history
Fixes #675
  • Loading branch information
dominikschulz authored Feb 24, 2018
1 parent 5e11e18 commit 899cb6c
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 88 deletions.
2 changes: 1 addition & 1 deletion backend/crypto/xc/compress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestCompress(t *testing.T) {
return pw
},
} {
for i := 0; i < 1024; i++ {
for i := 256; i < 512; i++ {
pw := pwg(i)
compPlain, compressed := compress([]byte(pw))
decompPlain := []byte(pw)
Expand Down
36 changes: 25 additions & 11 deletions backend/crypto/xc/decrypt.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package xc

import (
"bytes"
"context"
"encoding/binary"
"fmt"
"time"

Expand Down Expand Up @@ -32,23 +34,35 @@ func (x *XC) Decrypt(ctx context.Context, buf []byte) ([]byte, error) {
return nil, err
}

// initialize the AEAD cipher with the session key
cp, err := chacha20poly1305.New(sk)
if err != nil {
return nil, err
}
plainBuf := &bytes.Buffer{}

// decrypt and verify the ciphertext
plaintext, err := cp.Open(nil, msg.Header.Nonce, msg.Body, nil)
if err != nil {
return nil, err
for i, chunk := range msg.Chunks {
// initialize the AEAD cipher with the session key
cp, err := chacha20poly1305.New(sk)
if err != nil {
return nil, err
}

// reconstruct nonce from chunk number
// in case chunks have been reordered by some adversary
// decryption will fail
nonce := make([]byte, 12)
binary.BigEndian.PutUint64(nonce, uint64(i))

// decrypt and verify the ciphertext
plaintext, err := cp.Open(nil, nonce, chunk.Body, nil)
if err != nil {
return nil, err
}

plainBuf.Write(plaintext)
}

if !msg.Compressed {
return plaintext, nil
return plainBuf.Bytes(), nil
}

return decompress(plaintext)
return decompress(plainBuf.Bytes())
}

// findDecryptionKey tries to find a suiteable decryption key from the available
Expand Down
57 changes: 37 additions & 20 deletions backend/crypto/xc/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package xc

import (
"context"
"encoding/binary"
"fmt"
"io"
"sort"
Expand All @@ -20,6 +21,7 @@ import (
const (
// OnDiskVersion is the version of our on-disk format
OnDiskVersion = 1
chunkSizeMax = 1024 * 1024
)

// Encrypt encrypts the given plaintext for all the given recipients and returns the
Expand All @@ -34,22 +36,22 @@ func (x *XC) Encrypt(ctx context.Context, plaintext []byte, recipients []string)
var compressed bool
plaintext, compressed = compress(plaintext)

// encrypt body (als generates a random nonce and a random session key)
sk, nonce, body, err := encryptBody(plaintext)
// encrypt body (also generates a random session key)
sk, chunks, err := encryptBody(plaintext)
if err != nil {
return nil, errors.Wrapf(err, "failed to encrypt body: %s", err)
}

// encrypt the session key per recipient
header, err := x.encryptHeader(privKey, sk, nonce, recipients)
header, err := x.encryptHeader(privKey, sk, recipients)
if err != nil {
return nil, errors.Wrapf(err, "failed to encrypt header: %s", err)
}

msg := &xcpb.Message{
Version: OnDiskVersion,
Header: header,
Body: body,
Chunks: chunks,
Compressed: compressed,
}

Expand All @@ -58,10 +60,9 @@ func (x *XC) Encrypt(ctx context.Context, plaintext []byte, recipients []string)

// encrypt header creates and populates a header struct with the nonce (plain)
// and the session key encrypted per recipient
func (x *XC) encryptHeader(signKey *keyring.PrivateKey, sk, nonce []byte, recipients []string) (*xcpb.Header, error) {
func (x *XC) encryptHeader(signKey *keyring.PrivateKey, sk []byte, recipients []string) (*xcpb.Header, error) {
hdr := &xcpb.Header{
Sender: signKey.Fingerprint(),
Nonce: nonce,
Recipients: make(map[string][]byte, len(recipients)),
Metadata: make(map[string]string), // metadata is plaintext!
}
Expand Down Expand Up @@ -116,27 +117,43 @@ func (x *XC) encryptForRecipient(sender *keyring.PrivateKey, sk []byte, recipien

// encryptBody generates a random session key and a nonce and encrypts the given
// plaintext with those. it returns all three
func encryptBody(plaintext []byte) ([]byte, []byte, []byte, error) {
func encryptBody(plaintext []byte) ([]byte, []*xcpb.Chunk, error) {
// generate session / encryption key
sessionKey := make([]byte, 32)
if _, err := crypto_rand.Read(sessionKey); err != nil {
return nil, nil, nil, err
return nil, nil, err
}

// generate a random nonce
nonce := make([]byte, 12)
if _, err := crypto_rand.Read(nonce); err != nil {
return nil, nil, nil, err
}
chunks := make([]*xcpb.Chunk, 0, (len(plaintext)/chunkSizeMax)+1)
offset := 0

// initialize the AEAD with the generated session key
cp, err := chacha20poly1305.New(sessionKey)
if err != nil {
return nil, nil, nil, err
for offset < len(plaintext) {
// use a sequential nonce to prevent chunk reordering.
// since the pair of key and nonce has to be unique and we're
// generating a new random key for each message, this is OK
nonce := make([]byte, 12)
binary.BigEndian.PutUint64(nonce, uint64(len(chunks)))

// initialize the AEAD with the generated session key
cp, err := chacha20poly1305.New(sessionKey)
if err != nil {
return nil, nil, err
}

// encrypt the plaintext using the random nonce
ciphertext := cp.Seal(nil, nonce, plaintext[offset:min(len(plaintext), offset+chunkSizeMax)], nil)
chunks = append(chunks, &xcpb.Chunk{
Body: ciphertext,
})
offset += chunkSizeMax
}

// encrypt the plaintext using the random nonce
ciphertext := cp.Seal(nil, nonce, plaintext, nil)
return sessionKey, chunks, nil
}

return sessionKey, nonce, ciphertext, nil
func min(a, b int) int {
if a < b {
return a
}
return b
}
59 changes: 59 additions & 0 deletions backend/crypto/xc/encrypt_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package xc

import (
"bytes"
"context"
"crypto/rand"
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/golang/protobuf/proto"
"github.com/justwatchcom/gopass/backend/crypto/xc/keyring"
"github.com/justwatchcom/gopass/backend/crypto/xc/xcpb"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -110,3 +114,58 @@ func TestEncryptMultiKeys(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "foobar", string(buf))
}

func TestEncryptChunks(t *testing.T) {
ctx := context.Background()

td, err := ioutil.TempDir("", "gopass-")
assert.NoError(t, err)
defer func() {
_ = os.RemoveAll(td)
}()
assert.NoError(t, os.Setenv("GOPASS_CONFIG", filepath.Join(td, ".gopass.yml")))
assert.NoError(t, os.Setenv("GOPASS_HOMEDIR", td))

passphrase := "test"

k1, err := keyring.GenerateKeypair(passphrase)
assert.NoError(t, err)

skr := keyring.NewSecring()
assert.NoError(t, skr.Set(k1))

pkr := keyring.NewPubring(skr)

xc := &XC{
pubring: pkr,
secring: skr,
client: &fakeAgent{passphrase},
}

plaintext := &bytes.Buffer{}
p := make([]byte, 1024)
for i := 0; i < 10*(chunkSizeMax/1024); i++ {
_, _ = rand.Read(p)
plaintext.Write(p)
}
assert.Equal(t, 10485760, plaintext.Len())

buf, err := xc.Encrypt(ctx, plaintext.Bytes(), []string{k1.Fingerprint()})
assert.NoError(t, err)

// check recipients
recps, err := xc.RecipientIDs(ctx, buf)
assert.NoError(t, err)
assert.Equal(t, []string{k1.Fingerprint()}, recps)

// check number of chunks
msg := &xcpb.Message{}
assert.NoError(t, proto.Unmarshal(buf, msg))
assert.Equal(t, 10, len(msg.Chunks))

// check decryption works and yields exactly the input
buf, err = xc.Decrypt(ctx, buf)
assert.NoError(t, err)
assert.Equal(t, plaintext.String(), string(buf))

}
Loading

0 comments on commit 899cb6c

Please sign in to comment.