From 58f29ad939c967e03838c1cc7e354ca6e6ed452b Mon Sep 17 00:00:00 2001 From: Ishbir Singh Date: Thu, 9 Apr 2015 18:13:35 -0400 Subject: [PATCH] Added ECDH and encryption/decryption support --- btcec/README.md | 10 ++ btcec/ciphering.go | 216 ++++++++++++++++++++++++++++++++++++++++ btcec/ciphering_test.go | 176 ++++++++++++++++++++++++++++++++ btcec/example_test.go | 80 +++++++++++++++ btcec/internal_test.go | 6 ++ btcec/pubkey.go | 2 +- 6 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 btcec/ciphering.go create mode 100644 btcec/ciphering_test.go diff --git a/btcec/README.md b/btcec/README.md index 1470907e..55ab1904 100644 --- a/btcec/README.md +++ b/btcec/README.md @@ -47,6 +47,16 @@ $ go get github.com/btcsuite/btcd/btcec Demonstrates verifying a secp256k1 signature against a public key that is first parsed from raw bytes. The signature is also parsed from raw bytes. +* [Encryption] + (http://godoc.org/github.com/btcsuite/btcd/btcec#example-package--EncryptMessage) + Demonstrates encrypting a message for a public key that is first parsed from + raw bytes, then decrypting it using the corresponding private key. + +* [Decryption] + (http://godoc.org/github.com/btcsuite/btcd/btcec#example-package--DecryptMessage) + Demonstrates decrypting a message using a private key that is first parsed + from raw bytes. + ## GPG Verification Key All official release tags are signed by Conformal so users can ensure the code diff --git a/btcec/ciphering.go b/btcec/ciphering.go new file mode 100644 index 00000000..959240b3 --- /dev/null +++ b/btcec/ciphering.go @@ -0,0 +1,216 @@ +// Copyright (c) 2015 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcec + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "errors" + "io" +) + +var ( + // ErrInvalidMAC occurs when Message Authentication Check (MAC) fails + // during decryption. This happens because of either invalid private key or + // corrupt ciphertext. + ErrInvalidMAC = errors.New("invalid mac hash") + + // errInputTooShort occurs when the input ciphertext to the Decrypt + // function is less than 134 bytes long. + errInputTooShort = errors.New("ciphertext too short") + + // errUnsupportedCurve occurs when the first two bytes of the encrypted + // text aren't 0x02CA (= 712 = secp256k1, from OpenSSL). + errUnsupportedCurve = errors.New("unsupported curve") + + errInvalidXLength = errors.New("invalid X length, must be 32") + errInvalidYLength = errors.New("invalid Y length, must be 32") + errInvalidPadding = errors.New("invalid PKCS#7 padding") + + // 0x02CA = 714 + ciphCurveBytes = [2]byte{0x02, 0xCA} + // 0x20 = 32 + ciphCoordLength = [2]byte{0x00, 0x20} +) + +// GenerateSharedSecret generates a shared secret based on a private key and a +// private key using Diffie-Hellman key exchange (ECDH) (RFC 4753). +// RFC5903 Section 9 states we should only return x. +func GenerateSharedSecret(privkey *PrivateKey, pubkey *PublicKey) []byte { + x, _ := pubkey.Curve.ScalarMult(pubkey.X, pubkey.Y, privkey.D.Bytes()) + return x.Bytes() +} + +// Encrypt encrypts data for the target public key using AES-256-CBC. It also +// generates a private key (the pubkey of which is also in the output). The only +// supported curve is secp256k1. The `structure' that it encodes everything into +// is: +// +// struct { +// // Initialization Vector used for AES-256-CBC +// IV [16]byte +// // Public Key: curve(2) + len_of_pubkeyX(2) + pubkeyX + +// // len_of_pubkeyY(2) + pubkeyY (curve = 714) +// PublicKey [70]byte +// // Cipher text +// Data []byte +// // HMAC-SHA-256 Message Authentication Code +// HMAC [32]byte +// } +// +// The primary aim is to ensure byte compatibility with Pyelliptic. Additionaly, +// refer to section 5.8.1 of ANSI X9.63 for rationale on this format. +func Encrypt(pubkey *PublicKey, in []byte) ([]byte, error) { + ephemeral, err := NewPrivateKey(S256()) + if err != nil { + return nil, err + } + ecdhKey := GenerateSharedSecret(ephemeral, pubkey) + derivedKey := sha512.Sum512(ecdhKey) + keyE := derivedKey[:32] + keyM := derivedKey[32:] + + paddedIn := addPKCSPadding(in) + // IV + Curve params/X/Y + padded plaintext/ciphertext + HMAC-256 + out := make([]byte, aes.BlockSize+70+len(paddedIn)+sha256.Size) + iv := out[:aes.BlockSize] + if _, err = io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + // start writing public key + pb := ephemeral.PubKey().SerializeUncompressed() + offset := aes.BlockSize + + // curve and X length + copy(out[offset:offset+4], append(ciphCurveBytes[:], ciphCoordLength[:]...)) + offset += 4 + // X + copy(out[offset:offset+32], pb[1:33]) + offset += 32 + // Y length + copy(out[offset:offset+2], ciphCoordLength[:]) + offset += 2 + // Y + copy(out[offset:offset+32], pb[33:]) + offset += 32 + + // start encryption + block, err := aes.NewCipher(keyE) + if err != nil { + return nil, err + } + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(out[offset:len(out)-sha256.Size], paddedIn) + + // start HMAC-SHA-256 + hm := hmac.New(sha256.New, keyM) + hm.Write(out[:len(out)-sha256.Size]) // everything is hashed + copy(out[len(out)-sha256.Size:], hm.Sum(nil)) // write checksum + + return out, nil +} + +// Decrypt decrypts data that was encrypted using the Encrypt function. +func Decrypt(priv *PrivateKey, in []byte) ([]byte, error) { + // IV + Curve params/X/Y + 1 block + HMAC-256 + if len(in) < aes.BlockSize+70+aes.BlockSize+sha256.Size { + return nil, errInputTooShort + } + + // read iv + iv := in[:aes.BlockSize] + offset := aes.BlockSize + + // start reading pubkey + if !bytes.Equal(in[offset:offset+2], ciphCurveBytes[:]) { + return nil, errUnsupportedCurve + } + offset += 2 + + if !bytes.Equal(in[offset:offset+2], ciphCoordLength[:]) { + return nil, errInvalidXLength + } + offset += 2 + + xBytes := in[offset : offset+32] + offset += 32 + + if !bytes.Equal(in[offset:offset+2], ciphCoordLength[:]) { + return nil, errInvalidYLength + } + offset += 2 + + yBytes := in[offset : offset+32] + offset += 32 + + pb := make([]byte, 65) + pb[0] = byte(0x04) // uncompressed + copy(pb[1:33], xBytes) + copy(pb[33:], yBytes) + // check if (X, Y) lies on the curve and create a Pubkey if it does + pubkey, err := ParsePubKey(pb, S256()) + if err != nil { + return nil, err + } + + // check for cipher text length + if (len(in)-aes.BlockSize-offset-sha256.Size)%aes.BlockSize != 0 { + return nil, errInvalidPadding // not padded to 16 bytes + } + + // read hmac + messageMAC := in[len(in)-sha256.Size:] + + // generate shared secret + ecdhKey := GenerateSharedSecret(priv, pubkey) + derivedKey := sha512.Sum512(ecdhKey) + keyE := derivedKey[:32] + keyM := derivedKey[32:] + + // verify mac + hm := hmac.New(sha256.New, keyM) + hm.Write(in[:len(in)-sha256.Size]) // everything is hashed + expectedMAC := hm.Sum(nil) + if !bytes.Equal(messageMAC, expectedMAC) { + return nil, ErrInvalidMAC + } + + // start decryption + block, err := aes.NewCipher(keyE) + if err != nil { + return nil, err + } + mode := cipher.NewCBCDecrypter(block, iv) + // same length as ciphertext + plaintext := make([]byte, len(in)-offset-sha256.Size) + mode.CryptBlocks(plaintext, in[offset:len(in)-sha256.Size]) + + return removePKCSPadding(plaintext) +} + +// Implement PKCS#7 padding with block size of 16 (AES block size). + +// addPKCSPadding adds padding to a block of data +func addPKCSPadding(src []byte) []byte { + padding := aes.BlockSize - len(src)%aes.BlockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(src, padtext...) +} + +// removePKCSPadding removes padding from data that was added with addPKCSPadding +func removePKCSPadding(src []byte) ([]byte, error) { + length := len(src) + padLength := int(src[length-1]) + if padLength > aes.BlockSize || length < aes.BlockSize { + return nil, errInvalidPadding + } + + return src[:length-padLength], nil +} diff --git a/btcec/ciphering_test.go b/btcec/ciphering_test.go new file mode 100644 index 00000000..0160f062 --- /dev/null +++ b/btcec/ciphering_test.go @@ -0,0 +1,176 @@ +// Copyright (c) 2015 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcec_test + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/btcec" +) + +func TestGenerateSharedSecret(t *testing.T) { + privKey1, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Errorf("private key generation error: %s", err) + return + } + privKey2, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Errorf("private key generation error: %s", err) + return + } + + secret1 := btcec.GenerateSharedSecret(privKey1, privKey2.PubKey()) + secret2 := btcec.GenerateSharedSecret(privKey2, privKey1.PubKey()) + + if !bytes.Equal(secret1, secret2) { + t.Errorf("ECDH failed, secrets mismatch - first: %x, second: %x", + secret1, secret2) + } +} + +// Test 1: Encryption and decryption +func TestCipheringBasic(t *testing.T) { + privkey, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Fatal("failed to generate private key") + } + + in := []byte("Hey there dude. How are you doing? This is a test.") + + out, err := btcec.Encrypt(privkey.PubKey(), in) + if err != nil { + t.Fatal("failed to encrypt:", err) + } + + dec, err := btcec.Decrypt(privkey, out) + if err != nil { + t.Fatal("failed to decrypt:", err) + } + + if !bytes.Equal(in, dec) { + t.Error("decrypted data doesn't match original") + } +} + +// Test 2: Byte compatibility with Pyelliptic +func TestCiphering(t *testing.T) { + pb, _ := hex.DecodeString("fe38240982f313ae5afb3e904fb8215fb11af1200592b" + + "fca26c96c4738e4bf8f") + privkey, _ := btcec.PrivKeyFromBytes(btcec.S256(), pb) + + in := []byte("This is just a test.") + out, _ := hex.DecodeString("b0d66e5adaa5ed4e2f0ca68e17b8f2fc02ca002009e3" + + "3487e7fa4ab505cf34d98f131be7bd258391588ca7804acb30251e71a04e0020ecf" + + "df0f84608f8add82d7353af780fbb28868c713b7813eb4d4e61f7b75d7534dd9856" + + "9b0ba77cf14348fcff80fee10e11981f1b4be372d93923e9178972f69937ec850ed" + + "6c3f11ff572ddd5b2bedf9f9c0b327c54da02a28fcdce1f8369ffec") + + dec, err := btcec.Decrypt(privkey, out) + if err != nil { + t.Fatal("failed to decrypt:", err) + } + + if !bytes.Equal(in, dec) { + t.Error("decrypted data doesn't match original") + } +} + +func TestCipheringErrors(t *testing.T) { + privkey, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Fatal("failed to generate private key") + } + + tests1 := []struct { + ciphertext []byte // input ciphertext + }{ + {bytes.Repeat([]byte{0x00}, 133)}, // errInputTooShort + {bytes.Repeat([]byte{0x00}, 134)}, // errUnsupportedCurve + {bytes.Repeat([]byte{0x02, 0xCA}, 134)}, // errInvalidXLength + {bytes.Repeat([]byte{0x02, 0xCA, 0x00, 0x20}, 134)}, // errInvalidYLength + {[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // IV + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0xCA, 0x00, 0x20, // curve and X length + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // X + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x20, // Y length + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Y + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ciphertext + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // MAC + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }}, // invalid pubkey + {[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // IV + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0xCA, 0x00, 0x20, // curve and X length + 0x11, 0x5C, 0x42, 0xE7, 0x57, 0xB2, 0xEF, 0xB7, // X + 0x67, 0x1C, 0x57, 0x85, 0x30, 0xEC, 0x19, 0x1A, + 0x13, 0x59, 0x38, 0x1E, 0x6A, 0x71, 0x12, 0x7A, + 0x9D, 0x37, 0xC4, 0x86, 0xFD, 0x30, 0xDA, 0xE5, + 0x00, 0x20, // Y length + 0x7E, 0x76, 0xDC, 0x58, 0xF6, 0x93, 0xBD, 0x7E, // Y + 0x70, 0x10, 0x35, 0x8C, 0xE6, 0xB1, 0x65, 0xE4, + 0x83, 0xA2, 0x92, 0x10, 0x10, 0xDB, 0x67, 0xAC, + 0x11, 0xB1, 0xB5, 0x1B, 0x65, 0x19, 0x53, 0xD2, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ciphertext + // padding not aligned to 16 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // MAC + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }}, // errInvalidPadding + {[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // IV + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0xCA, 0x00, 0x20, // curve and X length + 0x11, 0x5C, 0x42, 0xE7, 0x57, 0xB2, 0xEF, 0xB7, // X + 0x67, 0x1C, 0x57, 0x85, 0x30, 0xEC, 0x19, 0x1A, + 0x13, 0x59, 0x38, 0x1E, 0x6A, 0x71, 0x12, 0x7A, + 0x9D, 0x37, 0xC4, 0x86, 0xFD, 0x30, 0xDA, 0xE5, + 0x00, 0x20, // Y length + 0x7E, 0x76, 0xDC, 0x58, 0xF6, 0x93, 0xBD, 0x7E, // Y + 0x70, 0x10, 0x35, 0x8C, 0xE6, 0xB1, 0x65, 0xE4, + 0x83, 0xA2, 0x92, 0x10, 0x10, 0xDB, 0x67, 0xAC, + 0x11, 0xB1, 0xB5, 0x1B, 0x65, 0x19, 0x53, 0xD2, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ciphertext + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // MAC + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }}, // ErrInvalidMAC + } + + for i, test := range tests1 { + _, err = btcec.Decrypt(privkey, test.ciphertext) + if err == nil { + t.Errorf("Decrypt #%d did not get error", i) + } + } + + // test error from removePKCSPadding + tests2 := []struct { + in []byte // input data + }{ + {bytes.Repeat([]byte{0x11}, 17)}, + {bytes.Repeat([]byte{0x07}, 15)}, + } + for i, test := range tests2 { + _, err = btcec.TstRemovePKCSPadding(test.in) + if err == nil { + t.Errorf("removePKCSPadding #%d did not get error", i) + } + } +} diff --git a/btcec/example_test.go b/btcec/example_test.go index cf68e42f..00fe7f84 100644 --- a/btcec/example_test.go +++ b/btcec/example_test.go @@ -86,3 +86,83 @@ func Example_verifySignature() { // Output: // Signature Verified? true } + +// This example demonstrates encrypting a message for a public key that is first +// parsed from raw bytes, then decrypting it using the corresponding private key. +func Example_encryptMessage() { + // Decode the hex-encoded pubkey of the recipient. + pubKeyBytes, err := hex.DecodeString("04115c42e757b2efb7671c578530ec191a1" + + "359381e6a71127a9d37c486fd30dae57e76dc58f693bd7e7010358ce6b165e483a29" + + "21010db67ac11b1b51b651953d2") // uncompressed pubkey + if err != nil { + fmt.Println(err) + return + } + pubKey, err := btcec.ParsePubKey(pubKeyBytes, btcec.S256()) + if err != nil { + fmt.Println(err) + return + } + + // Encrypt a message decryptable by the private key corresponding to pubKey + message := "test message" + ciphertext, err := btcec.Encrypt(pubKey, []byte(message)) + if err != nil { + fmt.Println(err) + return + } + + // Decode the hex-encoded private key. + pkBytes, err := hex.DecodeString("a11b0a4e1a132305652ee7a8eb7848f6ad" + + "5ea381e3ce20a2c086a2e388230811") + if err != nil { + fmt.Println(err) + return + } + // note that we already have corresponding pubKey + privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), pkBytes) + + // Try decrypting and verify if it's the same message. + plaintext, err := btcec.Decrypt(privKey, ciphertext) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(string(plaintext)) + + // Output: + // test message +} + +// This example demonstrates decrypting a message using a private key that is +// first parsed from raw bytes. +func Example_decryptMessage() { + // Decode the hex-encoded private key. + pkBytes, err := hex.DecodeString("a11b0a4e1a132305652ee7a8eb7848f6ad" + + "5ea381e3ce20a2c086a2e388230811") + if err != nil { + fmt.Println(err) + return + } + + privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), pkBytes) + + ciphertext, err := hex.DecodeString("35f644fbfb208bc71e57684c3c8b437402ca" + + "002047a2f1b38aa1a8f1d5121778378414f708fe13ebf7b4a7bb74407288c1958969" + + "00207cf4ac6057406e40f79961c973309a892732ae7a74ee96cd89823913b8b8d650" + + "a44166dc61ea1c419d47077b748a9c06b8d57af72deb2819d98a9d503efc59fc8307" + + "d14174f8b83354fac3ff56075162") + + // Try decrypting the message. + plaintext, err := btcec.Decrypt(privKey, ciphertext) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(string(plaintext)) + + // Output: + // test message +} diff --git a/btcec/internal_test.go b/btcec/internal_test.go index 8e3b50b5..23f35684 100644 --- a/btcec/internal_test.go +++ b/btcec/internal_test.go @@ -74,3 +74,9 @@ func NewFieldVal() *fieldVal { func TstNonceRFC6979(privkey *big.Int, hash []byte) *big.Int { return nonceRFC6979(privkey, hash) } + +// TstRemovePKCSPadding makes the internal removePKCSPadding function available +// to the test package. +func TstRemovePKCSPadding(src []byte) ([]byte, error) { + return removePKCSPadding(src) +} diff --git a/btcec/pubkey.go b/btcec/pubkey.go index 950c8859..d8b06bfd 100644 --- a/btcec/pubkey.go +++ b/btcec/pubkey.go @@ -107,7 +107,7 @@ func ParsePubKey(pubKeyStr []byte, curve *KoblitzCurve) (key *PublicKey, err err return nil, fmt.Errorf("pubkey Y parameter is >= to P") } if !pubkey.Curve.IsOnCurve(pubkey.X, pubkey.Y) { - return nil, fmt.Errorf("pubkey isn't on secp265k1 curve") + return nil, fmt.Errorf("pubkey isn't on secp256k1 curve") } return &pubkey, nil }