From 689f0e6fdc7dc5113c2b66c1ff51cd9d628b31a4 Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 9 Jan 2014 19:16:02 -0700 Subject: [PATCH] Add ECDH package. The package encrypts data using ECDHE with AES-128-CBC-HMAC-SHA1; this matches the other components. The curve used is P256 to match the use of AES-128. The Go ECDSA package is used; no signatures are done, but it presents usable PublicKey and PrivateKey types that are useful for this system. --- ecdh/ecdh.go | 141 ++++++++++++++++++++++++++++++++++++++++++++++ ecdh/ecdh_test.go | 36 ++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 ecdh/ecdh.go create mode 100644 ecdh/ecdh_test.go diff --git a/ecdh/ecdh.go b/ecdh/ecdh.go new file mode 100644 index 0000000..2264293 --- /dev/null +++ b/ecdh/ecdh.go @@ -0,0 +1,141 @@ +// Package ecdh encrypts and decrypts data using elliptic curve keys. Data +// is encrypted with AES-128-CBC with HMAC-SHA1 message tags using +// ECDHE to generate a shared key. The P256 curve is chosen in +// keeping with the use of AES-128 for encryption. +package ecdh + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/hmac" + "crypto/rand" + "crypto/sha1" + "errors" + "github.com/cloudflare/redoctober/padding" +) + +var Curve = elliptic.P256 + +func zero(in []byte) { + inLen := len(in) + for i := 0; i < inLen; i++ { + in[i] ^= in[i] + } +} + +// Encrypt secures and authenticates its input using the public key +// using ECDHE with AES-128-CBC-HMAC-SHA1. +func Encrypt(pub ecdsa.PublicKey, in []byte) (out []byte, err error) { + ephemeral, err := ecdsa.GenerateKey(Curve(), rand.Reader) + if err != nil { + return + } + x, _ := pub.Curve.ScalarMult(pub.X, pub.Y, ephemeral.D.Bytes()) + if x == nil { + return nil, errors.New("Failed to generate encryption key") + } + shared := x.Bytes() + iv, err := makeRandom(16) + if err != nil { + return + } + + paddedIn := padding.AddPadding(in) + ct, err := encryptCBC(paddedIn, iv, shared[:16]) + if err != nil { + return + } + + ephPub := elliptic.Marshal(pub.Curve, ephemeral.PublicKey.X, ephemeral.PublicKey.Y) + out = make([]byte, 1+len(ephPub)+16) + out[0] = byte(len(ephPub)) + copy(out[1:], ephPub) + copy(out[1+len(ephPub):], iv) + out = append(out, ct...) + + h := hmac.New(sha1.New, shared[16:]) + h.Write(iv) + h.Write(ct) + out = h.Sum(out) + return +} + +// Decrypt authentications and recovers the original message from +// its input using the private key and the ephemeral key included in +// the message. +func Decrypt(priv *ecdsa.PrivateKey, in []byte) (out []byte, err error) { + ephLen := int(in[0]) + ephPub := in[1 : 1+ephLen] + ct := in[1+ephLen:] + if len(ct) < (sha1.Size + aes.BlockSize) { + return nil, errors.New("Invalid ciphertext") + } + + x, y := elliptic.Unmarshal(Curve(), ephPub) + if x == nil { + return nil, errors.New("Invalid public key") + } + + x, _ = priv.Curve.ScalarMult(x, y, priv.D.Bytes()) + if x == nil { + return nil, errors.New("Failed to generate encryption key") + } + shared := x.Bytes() + + tagStart := len(ct) - sha1.Size + h := hmac.New(sha1.New, shared[16:]) + h.Write(ct[:tagStart]) + mac := h.Sum(nil) + if !hmac.Equal(mac, ct[tagStart:]) { + return nil, errors.New("Invalid MAC") + } + + paddedOut, err := decryptCBC(ct[aes.BlockSize:tagStart], ct[:aes.BlockSize], shared[:16]) + if err != nil { + return + } + out, err = padding.RemovePadding(paddedOut) + return +} + +// Utility functions copied from passvault. These handle encryption +// and decryption of data using AES-128-CBC. + +// decryptCBC decrypt bytes using a key and IV with AES in CBC mode. +func decryptCBC(data, iv, key []byte) (decryptedData []byte, err error) { + aesCrypt, err := aes.NewCipher(key) + if err != nil { + return + } + ivBytes := append([]byte{}, iv...) + + decryptedData = make([]byte, len(data)) + aesCBC := cipher.NewCBCDecrypter(aesCrypt, ivBytes) + aesCBC.CryptBlocks(decryptedData, data) + + return +} + +// encryptCBC encrypt data using a key and IV with AES in CBC mode. +func encryptCBC(data, iv, key []byte) (encryptedData []byte, err error) { + aesCrypt, err := aes.NewCipher(key) + if err != nil { + return + } + ivBytes := append([]byte{}, iv...) + + encryptedData = make([]byte, len(data)) + aesCBC := cipher.NewCBCEncrypter(aesCrypt, ivBytes) + aesCBC.CryptBlocks(encryptedData, data) + + return +} + +// makeRandom is a helper that makes a new buffer full of random data +func makeRandom(length int) ([]byte, error) { + bytes := make([]byte, length) + _, err := rand.Read(bytes) + return bytes, err +} diff --git a/ecdh/ecdh_test.go b/ecdh/ecdh_test.go new file mode 100644 index 0000000..0efbd28 --- /dev/null +++ b/ecdh/ecdh_test.go @@ -0,0 +1,36 @@ +package ecdh + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" +) + +var testKey *ecdsa.PrivateKey + +func TestGenerateKey(t *testing.T) { + var err error + testKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("%v", err) + } +} + +func TestCrypt(t *testing.T) { + message := []byte("One ping only, please.") + out, err := Encrypt(testKey.PublicKey, message) + if err != nil { + t.Fatalf("%v", err) + } + + out, err = Decrypt(testKey, out) + if err != nil { + t.Fatalf("%v", err) + } + + if !bytes.Equal(out, message) { + t.Fatal("Decryption return different plaintext than original message.") + } +}