From c8343f223bfa6a2d1001df27df173617066a7730 Mon Sep 17 00:00:00 2001 From: Satchmo Date: Thu, 5 Sep 2024 13:14:02 -0400 Subject: [PATCH 1/4] update ecies, support electrum and bitcore. remove redundant encryption.go. update examples. --- compat/ecies/ecies.go | 312 ++++++++++++ compat/ecies/ecies_test.go | 121 +++++ docs/examples/aes/aes.go | 17 +- docs/examples/ecies/ecies.go | 23 - .../ecies_electrum_binary.go | 26 + docs/examples/ecies_shared/ecies_shared.go | 40 ++ docs/examples/ecies_single/ecies_single.go | 25 + .../{message => encrypted_message}/message.go | 0 message/encrypted.go | 14 +- primitives/aescbc/cbc.go | 75 +++ primitives/aescbc/cbc_test.go | 153 ++++++ primitives/aesgcm/aesgcm.go | 20 +- primitives/aesgcm/aesgcm_test.go | 446 +++++++++--------- primitives/ec/encryption.go | 114 ----- primitives/ec/encryption_test.go | 108 ----- primitives/ec/publickey.go | 8 +- primitives/ec/symmetric.go | 4 +- primitives/hash/hash.go | 14 + 18 files changed, 1020 insertions(+), 500 deletions(-) create mode 100644 compat/ecies/ecies.go create mode 100644 compat/ecies/ecies_test.go delete mode 100644 docs/examples/ecies/ecies.go create mode 100644 docs/examples/ecies_electrum_binary/ecies_electrum_binary.go create mode 100644 docs/examples/ecies_shared/ecies_shared.go create mode 100644 docs/examples/ecies_single/ecies_single.go rename docs/examples/{message => encrypted_message}/message.go (100%) create mode 100644 primitives/aescbc/cbc.go create mode 100644 primitives/aescbc/cbc_test.go delete mode 100644 primitives/ec/encryption.go delete mode 100644 primitives/ec/encryption_test.go diff --git a/compat/ecies/ecies.go b/compat/ecies/ecies.go new file mode 100644 index 0000000..6bac6a8 --- /dev/null +++ b/compat/ecies/ecies.go @@ -0,0 +1,312 @@ +package compat + +import ( + "crypto/hmac" + "log" + "math/big" + "reflect" + + "encoding/base64" + "errors" + + ecies "github.com/bitcoin-sv/go-sdk/primitives/aescbc" + ec "github.com/bitcoin-sv/go-sdk/primitives/ec" + c "github.com/bitcoin-sv/go-sdk/primitives/hash" +) + +// +// ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac +// + +func EncryptSingle(message string, privateKey *ec.PrivateKey) (string, error) { + messageBytes := []byte(message) + return ElectrumEncrypt(messageBytes, privateKey.PubKey(), privateKey, false) +} + +func DecryptSingle(encryptedData string, privateKey *ec.PrivateKey) (string, error) { + plainBytes, err := ElectrumDecrypt(encryptedData, privateKey, nil) + if err != nil { + return "", err + } + return string(plainBytes), nil +} + +func EncryptShared(message string, toPublicKey *ec.PublicKey, fromPrivateKey *ec.PrivateKey) (string, error) { + messageBytes := []byte(message) + return ElectrumEncrypt(messageBytes, toPublicKey, nil, false) +} + +func DecryptShared(encryptedData string, toPrivateKey *ec.PrivateKey, fromPublicKey *ec.PublicKey) (string, error) { + plainBytes, err := ElectrumDecrypt(encryptedData, toPrivateKey, fromPublicKey) + if err != nil { + return "", err + } + return string(plainBytes), nil +} + +func ElectrumEncrypt(message []byte, toPublicKey *ec.PublicKey, fromPrivateKey *ec.PrivateKey, noKey bool) (string, error) { + // Generate an ephemeral EC private key if fromPrivateKey is nil + var ephemeralPrivateKey *ec.PrivateKey + if fromPrivateKey == nil { + ephemeralPrivateKey, _ = ec.NewPrivateKey() + } else { + ephemeralPrivateKey = fromPrivateKey + } + + // Derive ECDH key + x, y := toPublicKey.Curve.ScalarMult(toPublicKey.X, toPublicKey.Y, ephemeralPrivateKey.D.Bytes()) + ecdhKey := (&ec.PublicKey{X: x, Y: y}).SerializeCompressed() + + // SHA512(ECDH_KEY) + key := c.Sha512(ecdhKey) + iv, keyE, keyM := key[0:16], key[16:32], key[32:] + + // AES encryption + cipherText, err := ecies.AESCBCEncrypt(message, keyE, iv, false) + if err != nil { + return "", err + } + + ephemeralPublicKey := ephemeralPrivateKey.PubKey() + var encrypted []byte + if noKey { + // encrypted = magic_bytes(4 bytes) + cipher + encrypted = append([]byte("BIE1"), cipherText...) + } else { + // encrypted = magic_bytes(4 bytes) + ephemeral_public_key(33 bytes) + cipher + encrypted = append(append([]byte("BIE1"), ephemeralPublicKey.SerializeCompressed()...), cipherText...) + } + + mac := c.Sha256HMAC(encrypted, keyM) + + return base64.StdEncoding.EncodeToString(append(encrypted, mac...)), nil +} +func ElectrumDecrypt(encryptedData string, toPrivateKey *ec.PrivateKey, fromPublicKey *ec.PublicKey) ([]byte, error) { + encrypted, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return nil, err + } + if len(encrypted) < 52 { // Minimum length: 4 (magic) + 16 (min cipher) + 32 (mac) + return nil, errors.New("invalid encrypted text: length") + } + magic := encrypted[:4] + if string(magic) != "BIE1" { + return nil, errors.New("invalid cipher text: invalid magic bytes") + } + + var sharedSecret []byte + var cipherText []byte + var ephemeralPublicKey *ec.PublicKey + + if fromPublicKey != nil { + // Use counterparty public key to derive shared secret + x, y := toPrivateKey.Curve.ScalarMult(fromPublicKey.X, fromPublicKey.Y, toPrivateKey.D.Bytes()) + sharedSecret = (&ec.PublicKey{X: x, Y: y}).SerializeCompressed() + if len(encrypted) > 69 { // 4 (magic) + 33 (pubkey) + 32 (mac) + cipherText = encrypted[37 : len(encrypted)-32] + } else { + cipherText = encrypted[4 : len(encrypted)-32] + } + } else { + // Use ephemeral public key to derive shared secret + ephemeralPublicKey, err = ec.ParsePubKey(encrypted[4:37]) + if err != nil { + return nil, err + } + x, y := ephemeralPublicKey.Curve.ScalarMult(ephemeralPublicKey.X, ephemeralPublicKey.Y, toPrivateKey.D.Bytes()) + sharedSecret = (&ec.PublicKey{X: x, Y: y}).SerializeCompressed() + cipherText = encrypted[37 : len(encrypted)-32] + } + + // Derive key_e, iv and key_m + key := c.Sha512(sharedSecret) + iv, keyE, keyM := key[0:16], key[16:32], key[32:] + + // Verify mac + mac := encrypted[len(encrypted)-32:] + macRecalculated := c.Sha256HMAC(encrypted[:len(encrypted)-32], keyM) + if !reflect.DeepEqual(mac, macRecalculated) { + return nil, errors.New("incorrect password") + } + + // AES decryption + plain, err := ecies.AESCBCDecrypt(cipherText, keyE, iv) + if err != nil { + return nil, err + } + log.Printf("IV: %x, plain text: %x", iv, plain) + return plain, nil +} + +// func BitcoreEncrypt(message []byte, publicKey *ec.PublicKey) (string, error) { +// // Generate an ephemeral EC private key +// ephemeralPrivateKey, err := ec.NewPrivateKey() +// if err != nil { +// return "", err +// } + +// // Derive shared secret +// x, _ := publicKey.Curve.ScalarMult(publicKey.X, publicKey.Y, ephemeralPrivateKey.D.Bytes()) +// sharedSecret := x.Bytes() + +// // Key derivation +// keyMaterial := c.Sha512(sharedSecret) + +// keyE := keyMaterial[:32] // AES-256 key +// keyM := keyMaterial[32:] // HMAC key +// iv := make([]byte, 16) // IV for AES (all zeros in Bitcore) + +// // Encrypt the message +// cipherText, err := aescbc.AESEncryptWithIV(message, keyE, iv) +// if err != nil { +// return "", err +// } + +// // Prepare the output +// ephemeralPublicKey := ephemeralPrivateKey.PubKey().SerializeCompressed() +// encryptedData := append(ephemeralPublicKey, cipherText...) + +// // Calculate HMAC +// hmacSum := c.Sha256HMAC(encryptedData, keyM) + +// // Combine all parts +// result := append(encryptedData, hmacSum...) + +// return base64.StdEncoding.EncodeToString(result), nil +// } + +func BitcoreEncrypt(message []byte, toPublicKey *ec.PublicKey, fromPrivateKey *ec.PrivateKey, iv []byte) (string, error) { + + // JS Implementation + // if (!fromPrivateKey) { + // fromPrivateKey = PrivateKey.fromRandom() + // } + // const r = fromPrivateKey + // const RPublicKey = fromPrivateKey.toPublicKey() + // const RBuf = RPublicKey.encode(true) as number[] + // const KB = toPublicKey + // const P = KB.mul(r) + // const S = P.getX() + // const Sbuf = S.toArray('be', 32) + // const kEkM = Hash.sha512(Sbuf) + // const kE = kEkM.slice(0, 32) + // const kM = kEkM.slice(32, 64) + // const c = AESCBC.encrypt(messageBuf, kE, ivBuf) + // const d = Hash.sha256hmac(kM, [...c]) + // const encBuf = [...RBuf, ...c, ...d] + // return encBuf + // If IV is not provided, generate a random one + + if iv == nil { + iv = make([]byte, 16) + } + + // If fromPrivateKey is not provided, generate a random one + if fromPrivateKey == nil { + var err error + fromPrivateKey, err = ec.NewPrivateKey() + if err != nil { + return "", err + } + } + + RBuf := fromPrivateKey.PubKey().ToDERBytes() + P := toPublicKey.Mul(fromPrivateKey.D) + + Sbuf := P.X.Bytes() + kEkM := c.Sha512(Sbuf) + kE := kEkM[:32] + kM := kEkM[32:] + cc, err := ecies.AESCBCEncrypt(message, kE, iv, true) + if err != nil { + return "", err + } + d := c.Sha256HMAC(cc, kM) + encBuf := append(RBuf, cc...) + encBuf = append(encBuf, d...) + + log.Printf("encBuf: %x", encBuf) + result := base64.StdEncoding.EncodeToString(encBuf) + return result, nil +} + +func BitcoreDecrypt(encryptedMessage string, toPrivatKey *ec.PrivateKey) ([]byte, error) { + // const kB = toPrivateKey + // const fromPublicKey = PublicKey.fromString(toHex(encBuf.slice(0, 33))) + // const R = fromPublicKey + // const P = R.mul(kB) + // if (P.eq(new Point(0, 0))) { + // throw new Error('P equals 0') + // } + // const S = P.getX() + // const Sbuf = S.toArray('be', 32) + // const kEkM = Hash.sha512(Sbuf) + // const kE = kEkM.slice(0, 32) + // const kM = kEkM.slice(32, 64) + // const c = encBuf.slice(33, encBuf.length - 32) + // const d = encBuf.slice(encBuf.length - 32, encBuf.length) + // const d2 = Hash.sha256hmac(kM, c) + // if (toHex(d) !== toHex(d2)) { + // throw new Error('Invalid checksum') + // } + // const messageBuf = AESCBC.decrypt(c, kE) + // return [...messageBuf] + + data, err := base64.StdEncoding.DecodeString(encryptedMessage) + if err != nil { + return nil, err + } + + fromPublicKey, err := ec.ParsePubKey(data[:33]) + if err != nil { + return nil, err + } + + P := fromPublicKey.Mul(toPrivatKey.D) + if P.X.Cmp(big.NewInt(0)) == 0 && P.Y.Cmp(big.NewInt(0)) == 0 { + return nil, errors.New("p equals 0") + } + + Sbuf := P.X.Bytes() + kEkM := c.Sha512(Sbuf) + kE := kEkM[:32] + kM := kEkM[32:] + + cipherText := data[33 : len(data)-32] + mac := data[len(data)-32:] + expectedMAC := c.Sha256HMAC(cipherText, kM) + if !hmac.Equal(mac, expectedMAC) { + return nil, errors.New("invalid ciphertext: HMAC mismatch") + } + iv := cipherText[:16] + return ecies.AESCBCDecrypt(cipherText[16:], kE, iv) + + // Derive shared secret + // x, _ := privateKey.Curve.ScalarMult(fromPublicKey.X, fromPublicKey.Y, privateKey.D.Bytes()) + // sharedSecret := x.Bytes() + + // Key derivation + // keyMaterial := c.Sha512(sharedSecret) + + // keyE := keyMaterial[:32] // AES-256 key + // keyM := keyMaterial[32:] // HMAC key + + // // Verify HMAC + // mac := data[len(data)-32:] + // encryptedData := data[:len(data)-32] + // expectedMAC := c.Sha256HMAC(encryptedData, keyM) + + // if !hmac.Equal(mac, expectedMAC) { + // return nil, errors.New("invalid ciphertext: HMAC mismatch") + // } + + // // Decrypt + // iv := make([]byte, 16) // In Bitcore, IV is usually all zeros + // cipherText := encryptedData[33:] + // plainText, err := ecies.AESCBCDecrypt(cipherText, keyE, iv) + // if err != nil { + // return nil, err + // } + + // return plainText, nil +} diff --git a/compat/ecies/ecies_test.go b/compat/ecies/ecies_test.go new file mode 100644 index 0000000..6eba51f --- /dev/null +++ b/compat/ecies/ecies_test.go @@ -0,0 +1,121 @@ +package compat + +import ( + "encoding/base64" + "log" + "testing" + + ec "github.com/bitcoin-sv/go-sdk/primitives/ec" + "github.com/stretchr/testify/require" +) + +const msgString = "hello world" + +func TestEncryptDecryptSingle(t *testing.T) { + + pk, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + + // Electrum Encrypt + encryptedData, err := EncryptSingle(msgString, pk) + require.NoError(t, err) + require.Equal(t, "QklFMQO7zpX/GS4XpthCy6/hT38ZKsBGbn8JKMGHOY5ifmaoT890Krt9cIRk/ULXaB5uC08owRICzenFbm31pZGu0gCM2uOxpofwHacKidwZ0Q7aEw==", encryptedData) + + // Electrum Decrypt + decryptedData, _ := DecryptSingle(encryptedData, pk) + require.Equal(t, msgString, decryptedData) +} + +func TestElectrumEncryptDecryptSingle(t *testing.T) { + + pk, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + + // Electrum Encrypt + encryptedData, err := ElectrumEncrypt([]byte(msgString), pk.PubKey(), pk, false) + require.NoError(t, err) + require.Equal(t, "QklFMQO7zpX/GS4XpthCy6/hT38ZKsBGbn8JKMGHOY5ifmaoT890Krt9cIRk/ULXaB5uC08owRICzenFbm31pZGu0gCM2uOxpofwHacKidwZ0Q7aEw==", encryptedData) + + // Electrum Decrypt + decryptedData, _ := ElectrumDecrypt(encryptedData, pk, nil) + require.Equal(t, []byte(msgString), decryptedData) +} + +func TestElectrumEncryptDecryptSingleNokey(t *testing.T) { + + pk, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + + // Electrum Encrypt + encryptedData, err := ElectrumEncrypt([]byte(msgString), pk.PubKey(), pk, true) + require.NoError(t, err) + + // Electrum Decrypt + decryptedData, _ := ElectrumDecrypt(encryptedData, pk, pk.PubKey()) + require.Equal(t, []byte(msgString), decryptedData) +} + +func TestElectrumEncryptDecryptWithCounterparty(t *testing.T) { + pk1, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + counterparty, err := ec.PublicKeyFromString("03121a7afe56fc8e25bca4bb2c94f35eb67ebe5b84df2e149d65b9423ee65b8b4b") + require.NoError(t, err) + + // Electrum Encrypt + encryptedData, _ := ElectrumEncrypt([]byte(msgString), counterparty, pk1, false) + require.NoError(t, err) + log.Println("Encrypted data: ", encryptedData) + + // Electrum Decrypt + decryptedData, err := ElectrumDecrypt(encryptedData, pk1, counterparty) + require.NoError(t, err) + require.Equal(t, msgString, string(decryptedData)) +} + +func TestElectrumEncryptDecryptWithCounterpartyNoKey(t *testing.T) { + pk1, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + counterparty, err := ec.PublicKeyFromString("03121a7afe56fc8e25bca4bb2c94f35eb67ebe5b84df2e149d65b9423ee65b8b4b") + require.NoError(t, err) + + // Electrum Encrypt + encryptedData, _ := ElectrumEncrypt([]byte(msgString), counterparty, pk1, true) + require.NoError(t, err) + log.Println("Encrypted data: ", encryptedData) + + // Electrum Decrypt + decryptedData, err := ElectrumDecrypt(encryptedData, pk1, counterparty) + require.NoError(t, err) + require.Equal(t, msgString, string(decryptedData)) +} + +func TestBitcoreEncryptDecryptSolo(t *testing.T) { + expectedEncryptedData := "A7vOlf8ZLhem2ELLr+FPfxkqwEZufwkowYc5jmJ+ZqhPAAAAAAAAAAAAAAAAAAAAAB27kUY/HpNbiwhYSpEoEZZDW+wEjMmPNcAAxnc0kiuQ73FpFzf6p6afe4wwVtKAAg==" + decodedExpectedEncryptedData, _ := base64.StdEncoding.DecodeString(expectedEncryptedData) + log.Printf("Decoded expected encrypted data: %x\n", decodedExpectedEncryptedData) + pk, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + + // Bitcore Encrypt + encryptedData, err := BitcoreEncrypt([]byte(msgString), pk.PubKey(), pk, nil) + require.NoError(t, err) + require.Len(t, encryptedData, 132) + require.Equal(t, expectedEncryptedData, encryptedData) + + // Bitcore Decrypt + decryptedData, err := BitcoreDecrypt(string(encryptedData), pk) + require.NoError(t, err) + require.Equal(t, msgString, string(decryptedData)) +} + +func TestBitcoreEncryptDecryptWithCounterparty(t *testing.T) { + pk1, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + counterpartyPk, err := ec.PrivateKeyFromWif("L27ZSAC1xTsZrghYHqnxwAQZ12bH57piaAdoGaLizTp3JZrjkZjK") + require.NoError(t, err) + + // Bitcore Encrypt + encryptedData, err := BitcoreEncrypt([]byte(msgString), counterpartyPk.PubKey(), pk1, nil) + expectedEncryptedData := "A7vOlf8ZLhem2ELLr+FPfxkqwEZufwkowYc5jmJ+ZqhPAAAAAAAAAAAAAAAAAAAAAAmFslNpNc4TrjaMPmPLdooZwoP6/fE7GN3AeyLpFf2f+QGYRKIke8zbhxu8FcLOsA==" + + require.NoError(t, err) + require.Equal(t, expectedEncryptedData, encryptedData) + + // Bitcore Decrypt + decryptedData, err := BitcoreDecrypt(encryptedData, counterpartyPk) + require.NoError(t, err) + require.Equal(t, msgString, string(decryptedData)) +} diff --git a/docs/examples/aes/aes.go b/docs/examples/aes/aes.go index b508cc5..ecaa365 100644 --- a/docs/examples/aes/aes.go +++ b/docs/examples/aes/aes.go @@ -1,19 +1,26 @@ package main import ( + "encoding/hex" "fmt" - ec "github.com/bitcoin-sv/go-sdk/primitives/ec" + aes "github.com/bitcoin-sv/go-sdk/primitives/aesgcm" ) +// Vanilla AES encryption and decryption func main() { - pk, _ := ec.NewPrivateKey() + key, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f") // Encrypt using the public key of the given private key - encryptedData, _ := ec.EncryptWithPrivateKey(pk, "this is a test") + encryptedData, err := aes.AESEncrypt([]byte("0123456789abcdef"), key) + if err != nil { + fmt.Println(err) + } // Decrypt using the private key - decryptedData, _ := ec.DecryptWithPrivateKey(pk, encryptedData) - + decryptedData, err := aes.AESDecrypt(encryptedData, key) + if err != nil { + fmt.Println(err) + } fmt.Printf("decryptedData: %s\n", decryptedData) } diff --git a/docs/examples/ecies/ecies.go b/docs/examples/ecies/ecies.go deleted file mode 100644 index e3b1f82..0000000 --- a/docs/examples/ecies/ecies.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "encoding/hex" - "fmt" - - ec "github.com/bitcoin-sv/go-sdk/primitives/ec" -) - -// Example of using ECIES to encrypt and decrypt data -func main() { - // user 1 - user1Pk, _ := ec.NewPrivateKey() - - // user 2 - user2Pk, _ := ec.PublicKeyFromString("03121a7afe56fc8e25bca4bb2c94f35eb67ebe5b84df2e149d65b9423ee65b8b4b") - - priv, _, encryptedData, _ := ec.EncryptShared(user1Pk, user2Pk, []byte("this is a test")) - - decryptedData, _ := ec.DecryptWithPrivateKey(priv, hex.EncodeToString(encryptedData)) - - fmt.Printf("decryptedData: %s\n", decryptedData) -} diff --git a/docs/examples/ecies_electrum_binary/ecies_electrum_binary.go b/docs/examples/ecies_electrum_binary/ecies_electrum_binary.go new file mode 100644 index 0000000..e44703b --- /dev/null +++ b/docs/examples/ecies_electrum_binary/ecies_electrum_binary.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + + ecies "github.com/bitcoin-sv/go-sdk/compat/ecies" + ec "github.com/bitcoin-sv/go-sdk/primitives/ec" +) + +// Example of using ECIES to encrypt and decrypt data +func main() { + + // user 1 + user1Pk, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + + // user 2 + user2Pk, _ := ec.PublicKeyFromString("03121a7afe56fc8e25bca4bb2c94f35eb67ebe5b84df2e149d65b9423ee65b8b4b") + + encryptedData, _ := ecies.ElectrumEncrypt([]byte("hello world"), user2Pk, user1Pk, false) + + fmt.Println(encryptedData) + decryptedData, _ := ecies.ElectrumDecrypt(encryptedData, user1Pk, user2Pk) + + fmt.Printf("decryptedData: %s\n", decryptedData) + +} diff --git a/docs/examples/ecies_shared/ecies_shared.go b/docs/examples/ecies_shared/ecies_shared.go new file mode 100644 index 0000000..8a4d5c0 --- /dev/null +++ b/docs/examples/ecies_shared/ecies_shared.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + + ecies "github.com/bitcoin-sv/go-sdk/compat/ecies" + ec "github.com/bitcoin-sv/go-sdk/primitives/ec" +) + +// Example of using ECIES to encrypt and decrypt data between two users + +func main() { + + myPrivateKey, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + recipientPublicKey, _ := ec.PublicKeyFromString("03121a7afe56fc8e25bca4bb2c94f35eb67ebe5b84df2e149d65b9423ee65b8b4b") + + encryptedData, _ := ecies.EncryptShared("hello world", recipientPublicKey, myPrivateKey) + + fmt.Println(encryptedData) + // Prints: + // QklFMQO7zpX/GS4XpthCy6/hT38ZKsBGbn8JKMGHOY5ifmaoT+nbjXrzxPofyG94/QHgX8QZ3+a/DfQbTJ+Qvm1KtZWZISHww7MM5oRZybxHjtAa+Q== + + decryptedData, _ := ecies.DecryptShared(encryptedData, myPrivateKey, recipientPublicKey) + fmt.Printf("decryptedData: %s\n", decryptedData) + // Prints: + // decryptedData: hello world +} + +// // user 1 +// user1Pk, _ := ec.NewPrivateKey() + +// // user 2 +// user2Pk, _ := ec.PublicKeyFromString("03121a7afe56fc8e25bca4bb2c94f35eb67ebe5b84df2e149d65b9423ee65b8b4b") + +// priv, _, encryptedData, _ := ec.EncryptShared(user1Pk, user2Pk, []byte("this is a test")) + +// decryptedData, _ := ec.DecryptWithPrivateKey(priv, hex.EncodeToString(encryptedData)) + +// fmt.Printf("decryptedData: %s\n", decryptedData) +// } diff --git a/docs/examples/ecies_single/ecies_single.go b/docs/examples/ecies_single/ecies_single.go new file mode 100644 index 0000000..3ecb0b5 --- /dev/null +++ b/docs/examples/ecies_single/ecies_single.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + + ecies "github.com/bitcoin-sv/go-sdk/compat/ecies" + ec "github.com/bitcoin-sv/go-sdk/primitives/ec" +) + +// Example of using ECIES to encrypt and decrypt data for a single user +func main() { + myPrivateKey, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + + encryptedData, _ := ecies.EncryptSingle("hello world", myPrivateKey) + + fmt.Println(encryptedData) + // Prints: + // QklFMQLoYyD2A6LA9Pd342B7Z5q4agY+r674wbq6Vu2YLtVqNU5RpP1SQZNkJ22FOQt9LmXHYgMFkORAJ1nD/JVGmbmmDCx4rbYfZBVh/aa9B4imlA== + + decryptedData, _ := ecies.DecryptSingle(encryptedData, myPrivateKey) + + fmt.Printf("decryptedData: %s\n", decryptedData) + // Prints: + // decryptedData: hello world +} diff --git a/docs/examples/message/message.go b/docs/examples/encrypted_message/message.go similarity index 100% rename from docs/examples/message/message.go rename to docs/examples/encrypted_message/message.go diff --git a/message/encrypted.go b/message/encrypted.go index 6434270..1f74edf 100644 --- a/message/encrypted.go +++ b/message/encrypted.go @@ -43,18 +43,6 @@ func Encrypt(message []byte, sender *ec.PrivateKey, recipient *ec.PublicKey) ([] return nil, err } - // const symmetricKey = new SymmetricKey(sharedSecret.encode(true).slice(1)) - // const encrypted = symmetricKey.encrypt(message) as number[] - // const senderPublicKey = sender.toPublicKey().encode(true) - // const version = toArray(VERSION, 'hex') - // return [ - // ...version, - // ...senderPublicKey, - // ...recipient.encode(true), - // ...keyID, - // ...encrypted - // ] - version, err := hex.DecodeString(VERSION) if err != nil { return nil, err @@ -73,7 +61,7 @@ func Encrypt(message []byte, sender *ec.PrivateKey, recipient *ec.PublicKey) ([] // /** // - Decrypts a message from one party to another using the BRC-78 message encryption protocol. // - @param message The message to decrypt -// - @param sender The private key of the recipient +// - @param recipient The private key of the recipient // * // - @returns The decrypted message // */ diff --git a/primitives/aescbc/cbc.go b/primitives/aescbc/cbc.go new file mode 100644 index 0000000..254063a --- /dev/null +++ b/primitives/aescbc/cbc.go @@ -0,0 +1,75 @@ +package primitives + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "errors" + "log" +) + +// AESCBCEncrypt encrypts data using AES in CBC mode with an IV +func AESCBCEncrypt(data, key, iv []byte, concatIv bool) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + data = PKCS7Padd(data, block.BlockSize()) + blockModel := cipher.NewCBCEncrypter(block, iv) + cipherText := make([]byte, len(data)) + blockModel.CryptBlocks(cipherText, data) + if concatIv { + cipherText = append(iv, cipherText...) + } + log.Printf("cipherText: %x", cipherText) + return cipherText, nil +} + +// AESCBCDecrypt decrypts data using AES in CBC mode with an IV +func AESCBCDecrypt(data, key, iv []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + blockModel := cipher.NewCBCDecrypter(block, iv) + plantText := make([]byte, len(data)) + blockModel.CryptBlocks(plantText, data) + plantText, err = PKCS7Unpad(plantText, block.BlockSize()) + if err != nil { + return nil, err + } + return plantText, nil +} + +func PKCS7Padd(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + return append(data, bytes.Repeat([]byte{byte(padding)}, padding)...) +} + +// PKCS7UnPadding removes padding from the plaintext +func PKCS7Unpad(data []byte, blockSize int) ([]byte, error) { + length := len(data) + + // Check if the data length is a multiple of the block size or if it's empty + if length%blockSize != 0 || length == 0 { + return nil, errors.New("invalid padding length") + } + + // Get the padding length from the last byte + padding := int(data[length-1]) + + // Check if the padding length is larger than the block size + if padding > blockSize { + return nil, errors.New("invalid padding byte (large)") + } + + // Check all padding bytes to ensure they are consistent + for _, v := range data[len(data)-padding:] { + if int(v) != padding { + return nil, errors.New("invalid padding byte (inconsistent)") + } + } + + // Return the data without padding + return data[:(length - padding)], nil +} diff --git a/primitives/aescbc/cbc_test.go b/primitives/aescbc/cbc_test.go new file mode 100644 index 0000000..6f7abe5 --- /dev/null +++ b/primitives/aescbc/cbc_test.go @@ -0,0 +1,153 @@ +package primitives + +import ( + "crypto/aes" + "crypto/rand" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAESCBCEncryptDecrypt(t *testing.T) { + validTestData := "Test data" + // Test cases + testCases := []struct { + name string + data string + key string + iv string + expectError bool + }{ + { + name: "Valid short message", + data: validTestData, + key: "0123456789abcdef0123456789abcdef", // 32 bytes (256 bits) + iv: "0123456789abcdef0123456789abcdef", // 32 hex chars = 16 bytes + expectError: false, + }, + { + name: "Valid long message", + data: "This is a longer message that spans multiple AES blocks.", + key: "0123456789abcdef0123456789abcdef", // 32 bytes (256 bits) + iv: "fedcba9876543210fedcba9876543210", // 32 hex chars = 16 bytes + expectError: false, + }, + { + name: "Invalid key length", + data: validTestData, + key: "0123456789abcdef", // 16 bytes (128 bits) - too short for AES-256 + iv: "0123456789abcdef0123456789abcdef", + expectError: true, + }, + { + name: "Invalid IV length", + data: validTestData, + key: "0123456789abcdef0123456789abcdef", + iv: "211234560123456789abcdefababababab", // More than 16 bytes + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + key, _ := hex.DecodeString(tc.key) + + iv, _ := hex.DecodeString(tc.iv) + + // Check IV length + if len(iv) != aes.BlockSize { + if tc.expectError { + return // Expected error due to invalid IV length + } + t.Fatalf("IV length must be %d bytes, got %d bytes", aes.BlockSize, len(iv)) + } + + data := []byte(tc.data) + + // Encrypt + encrypted, err := AESCBCEncrypt(data, key, iv, false) + if tc.expectError { + require.Error(t, err, "Expected an error but got none") + return + } + require.NoError(t, err, "Encryption failed") + + // Decrypt + decrypted, err := AESCBCDecrypt(encrypted, key, iv) + require.NoError(t, err, "Decryption failed") + + // Compare + require.Equal(t, data, decrypted, "Decrypted data doesn't match original") + }) + } +} + +func TestPKCS7Padding(t *testing.T) { + testCases := []struct { + name string + data string + blockSize int + }{ + {"Short data", "Hello", 16}, + {"Block-sized data", "1234567890123456", 16}, + {"Long data", "This is a longer string for testing", 16}, + {"Empty data", "", 16}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + data := []byte(tc.data) + padded := PKCS7Padd(data, tc.blockSize) + + // Check if padded data length is multiple of block size + require.Equal(t, 0, len(padded)%tc.blockSize, "Padded data length is not a multiple of block size") + + // Strip padding + stripped, err := PKCS7Unpad(padded, tc.blockSize) + require.NoError(t, err, "StripPKCS7Padding failed") + + // Compare stripped data with original + require.Equal(t, data, stripped, "Stripped data doesn't match original") + }) + } +} + +func TestStripPKCS7PaddingInvalidPadding(t *testing.T) { + testCases := []struct { + name string + data []byte + blockSize int + }{ + {"Invalid length", []byte{1, 2, 3}, 16}, + {"Large padding byte", []byte{1, 2, 3, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17}, 16}, + {"Inconsistent padding", []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 4}, 16}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := PKCS7Unpad(tc.data, tc.blockSize) + require.Error(t, err, "Expected an error, but got nil") + }) + } +} + +func TestAESEncryptDecryptWithRandomIV(t *testing.T) { + key := make([]byte, 32) + _, err := rand.Read(key) + require.NoError(t, err, "Failed to generate random key") + + iv := make([]byte, aes.BlockSize) + _, err = rand.Read(iv) + require.NoError(t, err, "Failed to generate random IV") + + data := []byte("This is a test message with random key and IV.") + + encrypted, err := AESCBCEncrypt(data, key, iv, false) + require.NoError(t, err, "Encryption failed") + + decrypted, err := AESCBCDecrypt(encrypted, key, iv) + require.NoError(t, err, "Decryption failed") + + require.Equal(t, data, decrypted, "Decrypted data doesn't match original") +} diff --git a/primitives/aesgcm/aesgcm.go b/primitives/aesgcm/aesgcm.go index ee6a659..e6066b0 100644 --- a/primitives/aesgcm/aesgcm.go +++ b/primitives/aesgcm/aesgcm.go @@ -41,39 +41,37 @@ func AESDecrypt(ciphertext, key []byte) ([]byte, error) { } // EncryptGCM encrypts plaintext using AES-GCM with the provided key and additional data -func EncryptGCM(plaintext, nonce, key, additionalData []byte) (ciphertext, tag []byte, err error) { +func AESGCMEncrypt(plaintext, key, initializationVector, additionalAuthenticatedData []byte) (ciphertext, authenticationTag []byte, err error) { block, err := aes.NewCipher(key) if err != nil { return nil, nil, err } - // Automatically adjust GCM to the nonce size - gcm, err := cipher.NewGCMWithNonceSize(block, len(nonce)) + gcm, err := cipher.NewGCMWithNonceSize(block, len(initializationVector)) if err != nil { return nil, nil, err } - ciphertext = gcm.Seal(nil, nonce, plaintext, additionalData) - tag = ciphertext[len(ciphertext)-gcm.Overhead():] + ciphertext = gcm.Seal(nil, initializationVector, plaintext, additionalAuthenticatedData) + authenticationTag = ciphertext[len(ciphertext)-gcm.Overhead():] - return ciphertext[:len(ciphertext)-gcm.Overhead()], tag, nil + return ciphertext[:len(ciphertext)-gcm.Overhead()], authenticationTag, nil } // DecryptGCM decrypts ciphertext using AES-GCM with the provided key, nonce, additional data, and tag -func DecryptGCM(ciphertext, key, nonce, additionalData, tag []byte) (plaintext []byte, err error) { +func AESGCMDecrypt(ciphertext, key, initializationVector, additionalAuthenticatedData, authenticationTag []byte) (plaintext []byte, err error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } - // Automatically adjust GCM to the nonce size - gcm, err := cipher.NewGCMWithNonceSize(block, len(nonce)) + gcm, err := cipher.NewGCMWithNonceSize(block, len(initializationVector)) if err != nil { return nil, err } - ciphertextWithTag := append(ciphertext, tag...) - plaintext, err = gcm.Open(nil, nonce, ciphertextWithTag, additionalData) + ciphertextWithTag := append(ciphertext, authenticationTag...) + plaintext, err = gcm.Open(nil, initializationVector, ciphertextWithTag, additionalAuthenticatedData) if err != nil { return nil, fmt.Errorf("decryption failed: %w", err) } diff --git a/primitives/aesgcm/aesgcm_test.go b/primitives/aesgcm/aesgcm_test.go index f39258a..45411db 100644 --- a/primitives/aesgcm/aesgcm_test.go +++ b/primitives/aesgcm/aesgcm_test.go @@ -1,286 +1,218 @@ package primitives import ( + "bytes" + "crypto/aes" "encoding/hex" "reflect" "testing" -) - -// Helper function to decode a hex string to a byte slice. -func hexDecode(s string) []byte { - bytes, err := hex.DecodeString(s) - if err != nil { - panic(err) // Can use t.Fatalf in the test case instead of panic. - } - return bytes -} -func TestAES(t *testing.T) { - tests := []struct { - name string - plaintextHex string - keyHex string - expectedCiphertext string - nonceHex string - additionalDataHex string - expectedTagHex string - }{ - { - name: "AES-128", - plaintextHex: "00112233445566778899aabbccddeeff", - keyHex: "000102030405060708090a0b0c0d0e0f", - expectedCiphertext: "69c4e0d86a7b0430d8cdb78070b4c55a", - }, - { - name: "AES-192", - plaintextHex: "00112233445566778899aabbccddeeff", - keyHex: "000102030405060708090a0b0c0d0e0f1011121314151617", - expectedCiphertext: "dda97ca4864cdfe06eaf70a0ec0d7191", - }, - { - name: "AES-256", - plaintextHex: "00112233445566778899aabbccddeeff", - keyHex: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", - expectedCiphertext: "8ea2b7ca516745bfeafc49904b496089", - }, - { - name: "AES with zero plaintext and key", - plaintextHex: "00000000000000000000000000000000", - keyHex: "00000000000000000000000000000000", - expectedCiphertext: "66e94bd4ef8a2c3b884cfa59ca342b2e", - }, - { - name: "AES with zero plaintext", - plaintextHex: "00000000000000000000000000000000", - keyHex: "000102030405060708090a0b0c0d0e0f", - expectedCiphertext: "c6a13b37878f5b826f4f8162a1c8d879", - }, - { - name: "AES with specific key", - plaintextHex: "00000000000000000000000000000000", - keyHex: "ad7a2bd03eac835a6f620fdcb506b345", - expectedCiphertext: "73a23d80121de2d5a850253fcf43120e", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - plaintext := hexDecode(tt.plaintextHex) - key := hexDecode(tt.keyHex) - expectedCiphertext := hexDecode(tt.expectedCiphertext) - ciphertext, err := AESEncrypt(plaintext, key) - if err != nil { - t.Errorf("AESEncrypt failed: %v", err) - } - if !reflect.DeepEqual(ciphertext, expectedCiphertext) { - t.Errorf("Ciphertext mismatch:\n got: %x\nwant: %x", ciphertext, expectedCiphertext) - } - }) - } -} + "github.com/stretchr/testify/require" +) -func TestEncryptGCM(t *testing.T) { +func TestAESGCM(t *testing.T) { tests := []struct { - name string - plaintextHex string - additionalDataHex string - nonceHex string - keyHex string - expectedCiphertextHex string - expectedTagHex string + name string + plaintext string + additionalAuthenticatedData string + initializationVector string + key string + expectedCiphertext string + expectedAuthenticationTag string }{ { - name: "Test Case 1", - plaintextHex: "", - additionalDataHex: "", - nonceHex: "000000000000000000000000", - keyHex: "00000000000000000000000000000000", - expectedCiphertextHex: "", - expectedTagHex: "58e2fccefa7e3061367f1d57a4e7455a", + name: "Test Case 1", + plaintext: "", + additionalAuthenticatedData: "", + initializationVector: "000000000000000000000000", + key: "00000000000000000000000000000000", + expectedCiphertext: "", + expectedAuthenticationTag: "58e2fccefa7e3061367f1d57a4e7455a", }, { - name: "Test Case 2", - plaintextHex: "00000000000000000000000000000000", - additionalDataHex: "", - nonceHex: "000000000000000000000000", - keyHex: "00000000000000000000000000000000", - expectedCiphertextHex: "0388dace60b6a392f328c2b971b2fe78", - expectedTagHex: "ab6e47d42cec13bdf53a67b21257bddf", + name: "Test Case 2", + plaintext: "00000000000000000000000000000000", + additionalAuthenticatedData: "", + initializationVector: "000000000000000000000000", + key: "00000000000000000000000000000000", + expectedCiphertext: "0388dace60b6a392f328c2b971b2fe78", + expectedAuthenticationTag: "ab6e47d42cec13bdf53a67b21257bddf", }, { - name: "Test Case 3", - plaintextHex: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255", - additionalDataHex: "", - nonceHex: "cafebabefacedbaddecaf888", - keyHex: "feffe9928665731c6d6a8f9467308308", - expectedCiphertextHex: "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091473f5985", - expectedTagHex: "4d5c2af327cd64a62cf35abd2ba6fab4", + name: "Test Case 3", + plaintext: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255", + additionalAuthenticatedData: "", + initializationVector: "cafebabefacedbaddecaf888", + key: "feffe9928665731c6d6a8f9467308308", + expectedCiphertext: "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091473f5985", + expectedAuthenticationTag: "4d5c2af327cd64a62cf35abd2ba6fab4", }, { - name: "Test Case 4", - plaintextHex: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", - additionalDataHex: "feedfacedeadbeeffeedfacedeadbeefabaddad2", - nonceHex: "cafebabefacedbaddecaf888", - keyHex: "feffe9928665731c6d6a8f9467308308", - expectedCiphertextHex: "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091", - expectedTagHex: "5bc94fbc3221a5db94fae95ae7121a47", + name: "Test Case 4", + plaintext: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + additionalAuthenticatedData: "feedfacedeadbeeffeedfacedeadbeefabaddad2", + initializationVector: "cafebabefacedbaddecaf888", + key: "feffe9928665731c6d6a8f9467308308", + expectedCiphertext: "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091", + expectedAuthenticationTag: "5bc94fbc3221a5db94fae95ae7121a47", }, { - name: "Test Case 5", - plaintextHex: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", - additionalDataHex: "feedfacedeadbeeffeedfacedeadbeefabaddad2", - nonceHex: "cafebabefacedbad", - keyHex: "feffe9928665731c6d6a8f9467308308", - expectedCiphertextHex: "61353b4c2806934a777ff51fa22a4755699b2a714fcdc6f83766e5f97b6c742373806900e49f24b22b097544d4896b424989b5e1ebac0f07c23f4598", - expectedTagHex: "3612d2e79e3b0785561be14aaca2fccb", + name: "Test Case 5", + plaintext: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + additionalAuthenticatedData: "feedfacedeadbeeffeedfacedeadbeefabaddad2", + initializationVector: "cafebabefacedbad", + key: "feffe9928665731c6d6a8f9467308308", + expectedCiphertext: "61353b4c2806934a777ff51fa22a4755699b2a714fcdc6f83766e5f97b6c742373806900e49f24b22b097544d4896b424989b5e1ebac0f07c23f4598", + expectedAuthenticationTag: "3612d2e79e3b0785561be14aaca2fccb", }, { - name: "Test Case 6", - plaintextHex: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", - additionalDataHex: "feedfacedeadbeeffeedfacedeadbeefabaddad2", - nonceHex: "9313225df88406e555909c5aff5269aa6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b525416aedbf5a0de6a57a637b39b", - keyHex: "feffe9928665731c6d6a8f9467308308", - expectedCiphertextHex: "8ce24998625615b603a033aca13fb894be9112a5c3a211a8ba262a3cca7e2ca701e4a9a4fba43c90ccdcb281d48c7c6fd62875d2aca417034c34aee5", - expectedTagHex: "619cc5aefffe0bfa462af43c1699d050", + name: "Test Case 6", + plaintext: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + additionalAuthenticatedData: "feedfacedeadbeeffeedfacedeadbeefabaddad2", + initializationVector: "9313225df88406e555909c5aff5269aa6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b525416aedbf5a0de6a57a637b39b", + key: "feffe9928665731c6d6a8f9467308308", + expectedCiphertext: "8ce24998625615b603a033aca13fb894be9112a5c3a211a8ba262a3cca7e2ca701e4a9a4fba43c90ccdcb281d48c7c6fd62875d2aca417034c34aee5", + expectedAuthenticationTag: "619cc5aefffe0bfa462af43c1699d050", }, { - name: "Test Case 7", - plaintextHex: "", - additionalDataHex: "", - nonceHex: "000000000000000000000000", - keyHex: "000000000000000000000000000000000000000000000000", - expectedCiphertextHex: "", - expectedTagHex: "cd33b28ac773f74ba00ed1f312572435", + name: "Test Case 7", + plaintext: "", + additionalAuthenticatedData: "", + initializationVector: "000000000000000000000000", + key: "000000000000000000000000000000000000000000000000", + expectedCiphertext: "", + expectedAuthenticationTag: "cd33b28ac773f74ba00ed1f312572435", }, { - name: "Test Case 8", - plaintextHex: "00000000000000000000000000000000", - additionalDataHex: "", - nonceHex: "000000000000000000000000", - keyHex: "000000000000000000000000000000000000000000000000", - expectedCiphertextHex: "98e7247c07f0fe411c267e4384b0f600", - expectedTagHex: "2ff58d80033927ab8ef4d4587514f0fb", + name: "Test Case 8", + plaintext: "00000000000000000000000000000000", + additionalAuthenticatedData: "", + initializationVector: "000000000000000000000000", + key: "000000000000000000000000000000000000000000000000", + expectedCiphertext: "98e7247c07f0fe411c267e4384b0f600", + expectedAuthenticationTag: "2ff58d80033927ab8ef4d4587514f0fb", }, { - name: "Test Case 9", - plaintextHex: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255", - additionalDataHex: "", - nonceHex: "cafebabefacedbaddecaf888", - keyHex: "feffe9928665731c6d6a8f9467308308feffe9928665731c", - expectedCiphertextHex: "3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710acade256", - expectedTagHex: "9924a7c8587336bfb118024db8674a14", + name: "Test Case 9", + plaintext: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255", + additionalAuthenticatedData: "", + initializationVector: "cafebabefacedbaddecaf888", + key: "feffe9928665731c6d6a8f9467308308feffe9928665731c", + expectedCiphertext: "3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710acade256", + expectedAuthenticationTag: "9924a7c8587336bfb118024db8674a14", }, { - name: "Test Case 10", - plaintextHex: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", - additionalDataHex: "feedfacedeadbeeffeedfacedeadbeefabaddad2", - nonceHex: "cafebabefacedbaddecaf888", - keyHex: "feffe9928665731c6d6a8f9467308308feffe9928665731c", - expectedCiphertextHex: "3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710", - expectedTagHex: "2519498e80f1478f37ba55bd6d27618c", + name: "Test Case 10", + plaintext: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + additionalAuthenticatedData: "feedfacedeadbeeffeedfacedeadbeefabaddad2", + initializationVector: "cafebabefacedbaddecaf888", + key: "feffe9928665731c6d6a8f9467308308feffe9928665731c", + expectedCiphertext: "3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710", + expectedAuthenticationTag: "2519498e80f1478f37ba55bd6d27618c", }, { - name: "Test Case 11", - plaintextHex: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", - additionalDataHex: "feedfacedeadbeeffeedfacedeadbeefabaddad2", - nonceHex: "cafebabefacedbad", - keyHex: "feffe9928665731c6d6a8f9467308308feffe9928665731c", - expectedCiphertextHex: "0f10f599ae14a154ed24b36e25324db8c566632ef2bbb34f8347280fc4507057fddc29df9a471f75c66541d4d4dad1c9e93a19a58e8b473fa0f062f7", - expectedTagHex: "65dcc57fcf623a24094fcca40d3533f8", + name: "Test Case 11", + plaintext: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + additionalAuthenticatedData: "feedfacedeadbeeffeedfacedeadbeefabaddad2", + initializationVector: "cafebabefacedbad", + key: "feffe9928665731c6d6a8f9467308308feffe9928665731c", + expectedCiphertext: "0f10f599ae14a154ed24b36e25324db8c566632ef2bbb34f8347280fc4507057fddc29df9a471f75c66541d4d4dad1c9e93a19a58e8b473fa0f062f7", + expectedAuthenticationTag: "65dcc57fcf623a24094fcca40d3533f8", }, { - name: "Test Case 12", - plaintextHex: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", - additionalDataHex: "feedfacedeadbeeffeedfacedeadbeefabaddad2", - nonceHex: "9313225df88406e555909c5aff5269aa6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b525416aedbf5a0de6a57a637b39b", - keyHex: "feffe9928665731c6d6a8f9467308308feffe9928665731c", - expectedCiphertextHex: "d27e88681ce3243c4830165a8fdcf9ff1de9a1d8e6b447ef6ef7b79828666e4581e79012af34ddd9e2f037589b292db3e67c036745fa22e7e9b7373b", - expectedTagHex: "dcf566ff291c25bbb8568fc3d376a6d9", + name: "Test Case 12", + plaintext: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + additionalAuthenticatedData: "feedfacedeadbeeffeedfacedeadbeefabaddad2", + initializationVector: "9313225df88406e555909c5aff5269aa6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b525416aedbf5a0de6a57a637b39b", + key: "feffe9928665731c6d6a8f9467308308feffe9928665731c", + expectedCiphertext: "d27e88681ce3243c4830165a8fdcf9ff1de9a1d8e6b447ef6ef7b79828666e4581e79012af34ddd9e2f037589b292db3e67c036745fa22e7e9b7373b", + expectedAuthenticationTag: "dcf566ff291c25bbb8568fc3d376a6d9", }, { - name: "Test Case 13", - plaintextHex: "", - additionalDataHex: "", - nonceHex: "000000000000000000000000", - keyHex: "0000000000000000000000000000000000000000000000000000000000000000", - expectedCiphertextHex: "", - expectedTagHex: "530f8afbc74536b9a963b4f1c4cb738b", + name: "Test Case 13", + plaintext: "", + additionalAuthenticatedData: "", + initializationVector: "000000000000000000000000", + key: "0000000000000000000000000000000000000000000000000000000000000000", + expectedCiphertext: "", + expectedAuthenticationTag: "530f8afbc74536b9a963b4f1c4cb738b", }, { - name: "Test Case 14", - plaintextHex: "00000000000000000000000000000000", - additionalDataHex: "", - nonceHex: "000000000000000000000000", - keyHex: "0000000000000000000000000000000000000000000000000000000000000000", - expectedCiphertextHex: "cea7403d4d606b6e074ec5d3baf39d18", - expectedTagHex: "d0d1c8a799996bf0265b98b5d48ab919", + name: "Test Case 14", + plaintext: "00000000000000000000000000000000", + additionalAuthenticatedData: "", + initializationVector: "000000000000000000000000", + key: "0000000000000000000000000000000000000000000000000000000000000000", + expectedCiphertext: "cea7403d4d606b6e074ec5d3baf39d18", + expectedAuthenticationTag: "d0d1c8a799996bf0265b98b5d48ab919", }, { - name: "Test Case 15", - plaintextHex: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255", - additionalDataHex: "", - nonceHex: "cafebabefacedbaddecaf888", - keyHex: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", - expectedCiphertextHex: "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015ad", - expectedTagHex: "b094dac5d93471bdec1a502270e3cc6c", + name: "Test Case 15", + plaintext: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255", + additionalAuthenticatedData: "", + initializationVector: "cafebabefacedbaddecaf888", + key: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", + expectedCiphertext: "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015ad", + expectedAuthenticationTag: "b094dac5d93471bdec1a502270e3cc6c", }, { - name: "Test Case 16", - plaintextHex: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", - additionalDataHex: "feedfacedeadbeeffeedfacedeadbeefabaddad2", - nonceHex: "cafebabefacedbaddecaf888", - keyHex: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", - expectedCiphertextHex: "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662", - expectedTagHex: "76fc6ece0f4e1768cddf8853bb2d551b", + name: "Test Case 16", + plaintext: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + additionalAuthenticatedData: "feedfacedeadbeeffeedfacedeadbeefabaddad2", + initializationVector: "cafebabefacedbaddecaf888", + key: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", + expectedCiphertext: "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662", + expectedAuthenticationTag: "76fc6ece0f4e1768cddf8853bb2d551b", }, { - name: "Test Case 17", - plaintextHex: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", - additionalDataHex: "feedfacedeadbeeffeedfacedeadbeefabaddad2", - nonceHex: "cafebabefacedbad", - keyHex: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", - expectedCiphertextHex: "c3762df1ca787d32ae47c13bf19844cbaf1ae14d0b976afac52ff7d79bba9de0feb582d33934a4f0954cc2363bc73f7862ac430e64abe499f47c9b1f", - expectedTagHex: "3a337dbf46a792c45e454913fe2ea8f2", + name: "Test Case 17", + plaintext: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + additionalAuthenticatedData: "feedfacedeadbeeffeedfacedeadbeefabaddad2", + initializationVector: "cafebabefacedbad", + key: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", + expectedCiphertext: "c3762df1ca787d32ae47c13bf19844cbaf1ae14d0b976afac52ff7d79bba9de0feb582d33934a4f0954cc2363bc73f7862ac430e64abe499f47c9b1f", + expectedAuthenticationTag: "3a337dbf46a792c45e454913fe2ea8f2", }, { - name: "Test Case 18", - plaintextHex: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", - additionalDataHex: "feedfacedeadbeeffeedfacedeadbeefabaddad2", - nonceHex: "9313225df88406e555909c5aff5269aa6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b525416aedbf5a0de6a57a637b39b", - keyHex: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", - expectedCiphertextHex: "5a8def2f0c9e53f1f75d7853659e2a20eeb2b22aafde6419a058ab4f6f746bf40fc0c3b780f244452da3ebf1c5d82cdea2418997200ef82e44ae7e3f", - expectedTagHex: "a44a8266ee1c8eb0c8b5d4cf5ae9f19a", + name: "Test Case 18", + plaintext: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + additionalAuthenticatedData: "feedfacedeadbeeffeedfacedeadbeefabaddad2", + initializationVector: "9313225df88406e555909c5aff5269aa6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b525416aedbf5a0de6a57a637b39b", + key: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", + expectedCiphertext: "5a8def2f0c9e53f1f75d7853659e2a20eeb2b22aafde6419a058ab4f6f746bf40fc0c3b780f244452da3ebf1c5d82cdea2418997200ef82e44ae7e3f", + expectedAuthenticationTag: "a44a8266ee1c8eb0c8b5d4cf5ae9f19a", }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - plaintext := hexDecode(tt.plaintextHex) - additionalData := hexDecode(tt.additionalDataHex) - nonce := hexDecode(tt.nonceHex) - key := hexDecode(tt.keyHex) - expectedCiphertext := hexDecode(tt.expectedCiphertextHex) - expectedTag := hexDecode(tt.expectedTagHex) + plaintext, _ := hex.DecodeString(tt.plaintext) + aad, _ := hex.DecodeString(tt.additionalAuthenticatedData) + iv, _ := hex.DecodeString(tt.initializationVector) + key, _ := hex.DecodeString(tt.key) + expectedCiphertext, _ := hex.DecodeString(tt.expectedCiphertext) + expectedAuthTag, _ := hex.DecodeString(tt.expectedAuthenticationTag) - actualCiphertext, actualTag, err := EncryptGCM(plaintext, nonce, key, additionalData) + ciphertext, authTag, err := AESGCMEncrypt(plaintext, key, iv, aad) if err != nil { - t.Errorf("EncryptGCM() error = %v, wantErr false", err) - return + t.Fatalf("AESGCMEncrypt failed: %v", err) } - if !reflect.DeepEqual(actualCiphertext, expectedCiphertext) { - t.Errorf("Ciphertext mismatch in %v:\n got: %x\nwant: %x", tt.name, actualCiphertext, expectedCiphertext) + if !bytes.Equal(ciphertext, expectedCiphertext) { + t.Errorf("Ciphertext mismatch.\nGot: %x\nWant: %x", ciphertext, expectedCiphertext) } - if !reflect.DeepEqual(actualTag, expectedTag) { - t.Errorf("Tag mismatch in %v:\n got: %x\nwant: %x", tt.name, actualTag, expectedTag) + if !bytes.Equal(authTag, expectedAuthTag) { + t.Errorf("Authentication tag mismatch.\nGot: %x\nWant: %x", authTag, expectedAuthTag) } }) } } func TestGhash(t *testing.T) { - input := hexDecode("000000000000000000000000000000000388dace60b6a392f328c2b971b2fe7800000000000000000000000000000080") - hashSubKey := hexDecode("66e94bd4ef8a2c3b884cfa59ca342b2e") - expected := hexDecode("f38cbb1ad69223dcc3457ae5b6b0f885") + input, _ := hex.DecodeString("000000000000000000000000000000000388dace60b6a392f328c2b971b2fe7800000000000000000000000000000080") + hashSubKey, _ := hex.DecodeString("66e94bd4ef8a2c3b884cfa59ca342b2e") + expected, _ := hex.DecodeString("f38cbb1ad69223dcc3457ae5b6b0f885") actual := Ghash(input, hashSubKey) @@ -288,3 +220,73 @@ func TestGhash(t *testing.T) { t.Errorf("ghash mismatch:\n got: %x\nwant: %x", actual, expected) } } + +func TestAES(t *testing.T) { + t.Run("AES-128", func(t *testing.T) { + plaintext, _ := hex.DecodeString("00112233445566778899aabbccddeeff") + key, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f") + expected, _ := hex.DecodeString("69c4e0d86a7b0430d8cdb78070b4c55a") + + block, err := aes.NewCipher(key) + require.NoError(t, err) + + result := make([]byte, len(plaintext)) + block.Encrypt(result, plaintext) + + require.Equal(t, expected, result) + }) + + t.Run("AES-192", func(t *testing.T) { + plaintext, _ := hex.DecodeString("00112233445566778899aabbccddeeff") + key, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f1011121314151617") + expected, _ := hex.DecodeString("dda97ca4864cdfe06eaf70a0ec0d7191") + + block, err := aes.NewCipher(key) + require.NoError(t, err) + + result := make([]byte, len(plaintext)) + block.Encrypt(result, plaintext) + + require.Equal(t, expected, result) + }) + + t.Run("AES-256", func(t *testing.T) { + plaintext, _ := hex.DecodeString("00112233445566778899aabbccddeeff") + key, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") + expected, _ := hex.DecodeString("8ea2b7ca516745bfeafc49904b496089") + + block, err := aes.NewCipher(key) + require.NoError(t, err) + + result := make([]byte, len(plaintext)) + block.Encrypt(result, plaintext) + + require.Equal(t, expected, result) + }) + + t.Run("Additional AES tests", func(t *testing.T) { + testCases := []struct { + plaintext string + key string + expected string + }{ + {"00000000000000000000000000000000", "00000000000000000000000000000000", "66e94bd4ef8a2c3b884cfa59ca342b2e"}, + {"00000000000000000000000000000000", "000102030405060708090a0b0c0d0e0f", "c6a13b37878f5b826f4f8162a1c8d879"}, + {"00000000000000000000000000000000", "ad7a2bd03eac835a6f620fdcb506b345", "73a23d80121de2d5a850253fcf43120e"}, + } + + for _, tc := range testCases { + plaintext, _ := hex.DecodeString(tc.plaintext) + key, _ := hex.DecodeString(tc.key) + expected, _ := hex.DecodeString(tc.expected) + + block, err := aes.NewCipher(key) + require.NoError(t, err) + + result := make([]byte, len(plaintext)) + block.Encrypt(result, plaintext) + + require.Equal(t, expected, result) + } + }) +} diff --git a/primitives/ec/encryption.go b/primitives/ec/encryption.go deleted file mode 100644 index d4a25e2..0000000 --- a/primitives/ec/encryption.go +++ /dev/null @@ -1,114 +0,0 @@ -package primitives - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/base64" - "encoding/hex" - "errors" - "io" -) - -// AES / ECIES encryption - -// EncryptWithPrivateKey will encrypt the data using a given private key -func EncryptWithPrivateKey(privateKey *PrivateKey, data string) (string, error) { - var block cipher.Block - block, err := aes.NewCipher(privateKey.PublicKey.X.Bytes()) - if err != nil { - return "", err - } - - // Encrypt using bec - encryptedData, err := Encrypt(block, []byte(data)) - if err != nil { - return "", err - } - - // Return the hex encoded value - return hex.EncodeToString(encryptedData), nil -} - -// DecryptWithPrivateKey is a wrapper to decrypt the previously encrypted -// information, given a corresponding private key -func DecryptWithPrivateKey(privateKey *PrivateKey, data string) (string, error) { - - // Decode the hex encoded string - rawData, err := hex.DecodeString(data) - if err != nil { - return "", err - } - - var block cipher.Block - block, err = aes.NewCipher(privateKey.X.Bytes()) - if err != nil { - return "", err - } - - // Decrypt the data - var decrypted []byte - if decrypted, err = Decrypt(block, rawData); err != nil { - return "", err - } - return string(decrypted), nil -} - -// EncryptShared will ECIES encrypt data and provide shared keys for decryption -func EncryptShared(user1PrivateKey *PrivateKey, user2PubKey *PublicKey, data []byte) ( - *PrivateKey, *PublicKey, []byte, error) { - - // Generate shared keys that can be decrypted by either user - sharedPrivKey, sharedPubKey := GenerateSharedKeyPair(user1PrivateKey, user2PubKey) - - // var block cipher.Block - block, err := aes.NewCipher(sharedPubKey.X.Bytes()) - if err != nil { - return nil, nil, nil, err - } - // Encrypt data with shared key - encryptedData, err := Encrypt(block, data) - return sharedPrivKey, sharedPubKey, encryptedData, err -} - -// Encrypt is an encrypt function -func Encrypt(cipherBlock cipher.Block, text []byte) ([]byte, error) { - b := base64.StdEncoding.EncodeToString(text) - ciphertext := make([]byte, aes.BlockSize+len(b)) - iv := ciphertext[:aes.BlockSize] - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return nil, err - } - cfb := cipher.NewCFBEncrypter(cipherBlock, iv) - cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b)) - return ciphertext, nil -} - -// Decrypt is a decrypt function -func Decrypt(cipherBlock cipher.Block, ciphertext []byte) ([]byte, error) { - if len(ciphertext) < aes.BlockSize { - return nil, errors.New("ciphertext too short") - } - iv := ciphertext[:aes.BlockSize] - cfb := cipher.NewCFBDecrypter(cipherBlock, iv) - text := ciphertext[aes.BlockSize:] - cfb.XORKeyStream(text, text) - return base64.StdEncoding.DecodeString(string(text)) -} - -// GenerateSharedKeyPair creates shared keys that can be used to encrypt/decrypt data -// that can be decrypted by yourself (privateKey) and also the owner of the given public key -func GenerateSharedKeyPair(privateKey *PrivateKey, - pubKey *PublicKey) (*PrivateKey, *PublicKey) { - return PrivateKeyFromBytes( - GenerateSharedSecret(privateKey, pubKey), - ) -} - -// GenerateSharedSecret generates a shared secret based on a private key and a -// public 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() -} diff --git a/primitives/ec/encryption_test.go b/primitives/ec/encryption_test.go deleted file mode 100644 index ca2a7ff..0000000 --- a/primitives/ec/encryption_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package primitives - -import ( - "crypto/aes" - "crypto/cipher" - "encoding/hex" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const testKey = "2b7e151628aed2a6abf7158809cf4f3c" - -func TestEncrypt(t *testing.T) { - t.Parallel() - - t.Run("valid aes encryption", func(t *testing.T) { - key, err := hex.DecodeString(testKey) - require.NoError(t, err) - - testStr := "7468697320697320612074657374" - var testData []byte - testData, err = hex.DecodeString(testStr) - require.NoError(t, err) - - var block cipher.Block - block, err = aes.NewCipher(key) - require.NoError(t, err) - require.NotNil(t, block) - - var encrypted []byte - encrypted, err = Encrypt(block, testData) - require.NoError(t, err) - // t.Logf("%x", encrypted) - - var decrypted []byte - decrypted, err = Decrypt(block, encrypted) - require.NoError(t, err) - assert.Equal(t, "this is a test", string(decrypted)) - assert.Equal(t, "7468697320697320612074657374", hex.EncodeToString(decrypted)) - }) -} - -func TestDecrypt(t *testing.T) { - t.Parallel() - - t.Run("valid aes decryption", func(t *testing.T) { - key, err := hex.DecodeString(testKey) - require.NoError(t, err) - - encryptedString := "16c9de9d806edf8bf7512f1654f0d72c63e4698d61714d1e7c394ada99ef10d8e43c0b22" - var encryptedData []byte - encryptedData, err = hex.DecodeString(encryptedString) - require.NoError(t, err) - - var block cipher.Block - block, err = aes.NewCipher(key) - require.NoError(t, err) - require.NotNil(t, block) - - var decrypted []byte - decrypted, err = Decrypt(block, encryptedData) - require.NoError(t, err) - assert.Equal(t, "this is a test", string(decrypted)) - assert.Equal(t, "7468697320697320612074657374", hex.EncodeToString(decrypted)) - }) - - t.Run("invalid cipher text", func(t *testing.T) { - - key, err := hex.DecodeString(testKey) - require.NoError(t, err) - - encryptedString := "000000" - var encryptedData []byte - encryptedData, err = hex.DecodeString(encryptedString) - require.NoError(t, err) - - var block cipher.Block - block, err = aes.NewCipher(key) - require.NoError(t, err) - require.NotNil(t, block) - - _, err = Decrypt(block, encryptedData) - assert.Error(t, err) - }) -} - -func TestEncryptDecrypt(t *testing.T) { - t.Parallel() - - t.Run("encrypt / decrypt using keys", func(t *testing.T) { - pk, err := NewPrivateKey() - require.NoError(t, err) - - // Encrypt using the public key of the given private key - encryptedData, err := EncryptWithPrivateKey(pk, "this is a test") - - require.NoError(t, err) - - // Decrypt using the private key - decryptedData, err := DecryptWithPrivateKey(pk, encryptedData) - require.NoError(t, err) - - assert.Equal(t, "this is a test", decryptedData) - - }) -} diff --git a/primitives/ec/publickey.go b/primitives/ec/publickey.go index 6c63100..647fb77 100644 --- a/primitives/ec/publickey.go +++ b/primitives/ec/publickey.go @@ -259,9 +259,13 @@ func (p *PublicKey) encode(compact bool) []byte { return append(append([]byte{0x04}, xBytes...), yBytes...) } -func (p *PublicKey) ToDER() string { +func (p *PublicKey) ToDERBytes() []byte { encoded := p.encode(true) - return hex.EncodeToString(encoded) + return encoded +} + +func (p *PublicKey) ToDER() string { + return hex.EncodeToString(p.ToDERBytes()) } func (p *PublicKey) DeriveChild(privateKey *PrivateKey, invoiceNumber string) (*PublicKey, error) { diff --git a/primitives/ec/symmetric.go b/primitives/ec/symmetric.go index 47d806a..f0b699d 100644 --- a/primitives/ec/symmetric.go +++ b/primitives/ec/symmetric.go @@ -19,7 +19,7 @@ func (s *SymmetricKey) Encrypt(message []byte) (ciphertext []byte, err error) { if err != nil { return nil, err } - ciphertext, tag, err := aesgcm.EncryptGCM(message, iv, s.ToBytes(), []byte{}) + ciphertext, tag, err := aesgcm.AESGCMEncrypt(message, s.ToBytes(), iv, []byte{}) if err != nil { return nil, err } @@ -37,7 +37,7 @@ func (s *SymmetricKey) Decrypt(message []byte) (plaintext []byte, err error) { iv := message[:32] ciphertext := message[32 : len(message)-16] tag := message[len(message)-16:] - plaintext, err = aesgcm.DecryptGCM(ciphertext, s.ToBytes(), iv, []byte{}, tag) + plaintext, err = aesgcm.AESGCMDecrypt(ciphertext, s.ToBytes(), iv, []byte{}, tag) if err != nil { return nil, err } diff --git a/primitives/hash/hash.go b/primitives/hash/hash.go index 96c2bc0..b0585c2 100644 --- a/primitives/hash/hash.go +++ b/primitives/hash/hash.go @@ -3,6 +3,7 @@ package primitives import ( "crypto/hmac" "crypto/sha256" + "crypto/sha512" "golang.org/x/crypto/ripemd160" ) @@ -19,6 +20,12 @@ func Sha256d(b []byte) []byte { return Sha256(first[:]) } +// Sha512 calculates hash(b) and returns the resulting 64 bytes. +func Sha512(b []byte) []byte { + data := sha512.Sum512(b) + return data[:] +} + // Sha256HMAC - HMAC with SHA256 func Sha256HMAC(b, key []byte) []byte { mac := hmac.New(sha256.New, key) @@ -26,6 +33,13 @@ func Sha256HMAC(b, key []byte) []byte { return mac.Sum(nil) } +// Sha512HMAC - HMAC with SHA512 +func Sha512HMAC(b, key []byte) []byte { + mac := hmac.New(sha512.New, key) + mac.Write(b) + return mac.Sum(nil) +} + // Ripemd160 hashes with RIPEMD160 func Ripemd160(b []byte) []byte { ripe := ripemd160.New() From 18d54f9ee8468bc6f6ba70c7c117a81644691b02 Mon Sep 17 00:00:00 2001 From: Satchmo Date: Thu, 5 Sep 2024 13:29:37 -0400 Subject: [PATCH 2/4] update ecies to use binary, add helpers for strings, fix lint issues --- compat/ecies/ecies.go | 200 ++++++++++-------------------------- compat/ecies/ecies_test.go | 12 ++- primitives/aescbc/cbc.go | 2 - primitives/aesgcm/aesgcm.go | 13 ++- 4 files changed, 72 insertions(+), 155 deletions(-) diff --git a/compat/ecies/ecies.go b/compat/ecies/ecies.go index 6bac6a8..dbed9f0 100644 --- a/compat/ecies/ecies.go +++ b/compat/ecies/ecies.go @@ -2,11 +2,10 @@ package compat import ( "crypto/hmac" - "log" + "encoding/base64" "math/big" "reflect" - "encoding/base64" "errors" ecies "github.com/bitcoin-sv/go-sdk/primitives/aescbc" @@ -20,11 +19,20 @@ import ( func EncryptSingle(message string, privateKey *ec.PrivateKey) (string, error) { messageBytes := []byte(message) - return ElectrumEncrypt(messageBytes, privateKey.PubKey(), privateKey, false) + + decryptedBytes, err := ElectrumEncrypt(messageBytes, privateKey.PubKey(), privateKey, false) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(decryptedBytes), nil } func DecryptSingle(encryptedData string, privateKey *ec.PrivateKey) (string, error) { - plainBytes, err := ElectrumDecrypt(encryptedData, privateKey, nil) + encryptedBytes, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return "", err + } + plainBytes, err := ElectrumDecrypt(encryptedBytes, privateKey, nil) if err != nil { return "", err } @@ -33,18 +41,30 @@ func DecryptSingle(encryptedData string, privateKey *ec.PrivateKey) (string, err func EncryptShared(message string, toPublicKey *ec.PublicKey, fromPrivateKey *ec.PrivateKey) (string, error) { messageBytes := []byte(message) - return ElectrumEncrypt(messageBytes, toPublicKey, nil, false) + decryptedBytes, err := ElectrumEncrypt(messageBytes, toPublicKey, nil, false) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(decryptedBytes), nil } func DecryptShared(encryptedData string, toPrivateKey *ec.PrivateKey, fromPublicKey *ec.PublicKey) (string, error) { - plainBytes, err := ElectrumDecrypt(encryptedData, toPrivateKey, fromPublicKey) + encryptedBytes, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return "", err + } + plainBytes, err := ElectrumDecrypt(encryptedBytes, toPrivateKey, fromPublicKey) if err != nil { return "", err } return string(plainBytes), nil } -func ElectrumEncrypt(message []byte, toPublicKey *ec.PublicKey, fromPrivateKey *ec.PrivateKey, noKey bool) (string, error) { +func ElectrumEncrypt(message []byte, + toPublicKey *ec.PublicKey, + fromPrivateKey *ec.PrivateKey, + noKey bool, +) ([]byte, error) { // Generate an ephemeral EC private key if fromPrivateKey is nil var ephemeralPrivateKey *ec.PrivateKey if fromPrivateKey == nil { @@ -64,7 +84,7 @@ func ElectrumEncrypt(message []byte, toPublicKey *ec.PublicKey, fromPrivateKey * // AES encryption cipherText, err := ecies.AESCBCEncrypt(message, keyE, iv, false) if err != nil { - return "", err + return nil, err } ephemeralPublicKey := ephemeralPrivateKey.PubKey() @@ -79,43 +99,39 @@ func ElectrumEncrypt(message []byte, toPublicKey *ec.PublicKey, fromPrivateKey * mac := c.Sha256HMAC(encrypted, keyM) - return base64.StdEncoding.EncodeToString(append(encrypted, mac...)), nil + return append(encrypted, mac...), nil } -func ElectrumDecrypt(encryptedData string, toPrivateKey *ec.PrivateKey, fromPublicKey *ec.PublicKey) ([]byte, error) { - encrypted, err := base64.StdEncoding.DecodeString(encryptedData) - if err != nil { - return nil, err - } - if len(encrypted) < 52 { // Minimum length: 4 (magic) + 16 (min cipher) + 32 (mac) +func ElectrumDecrypt(encryptedData []byte, toPrivateKey *ec.PrivateKey, fromPublicKey *ec.PublicKey) ([]byte, error) { + + if len(encryptedData) < 52 { // Minimum length: 4 (magic) + 16 (min cipher) + 32 (mac) return nil, errors.New("invalid encrypted text: length") } - magic := encrypted[:4] + magic := encryptedData[:4] if string(magic) != "BIE1" { return nil, errors.New("invalid cipher text: invalid magic bytes") } var sharedSecret []byte var cipherText []byte - var ephemeralPublicKey *ec.PublicKey if fromPublicKey != nil { // Use counterparty public key to derive shared secret x, y := toPrivateKey.Curve.ScalarMult(fromPublicKey.X, fromPublicKey.Y, toPrivateKey.D.Bytes()) sharedSecret = (&ec.PublicKey{X: x, Y: y}).SerializeCompressed() - if len(encrypted) > 69 { // 4 (magic) + 33 (pubkey) + 32 (mac) - cipherText = encrypted[37 : len(encrypted)-32] + if len(encryptedData) > 69 { // 4 (magic) + 33 (pubkey) + 32 (mac) + cipherText = encryptedData[37 : len(encryptedData)-32] } else { - cipherText = encrypted[4 : len(encrypted)-32] + cipherText = encryptedData[4 : len(encryptedData)-32] } } else { // Use ephemeral public key to derive shared secret - ephemeralPublicKey, err = ec.ParsePubKey(encrypted[4:37]) + ephemeralPublicKey, err := ec.ParsePubKey(encryptedData[4:37]) if err != nil { return nil, err } x, y := ephemeralPublicKey.Curve.ScalarMult(ephemeralPublicKey.X, ephemeralPublicKey.Y, toPrivateKey.D.Bytes()) sharedSecret = (&ec.PublicKey{X: x, Y: y}).SerializeCompressed() - cipherText = encrypted[37 : len(encrypted)-32] + cipherText = encryptedData[37 : len(encryptedData)-32] } // Derive key_e, iv and key_m @@ -123,8 +139,8 @@ func ElectrumDecrypt(encryptedData string, toPrivateKey *ec.PrivateKey, fromPubl iv, keyE, keyM := key[0:16], key[16:32], key[32:] // Verify mac - mac := encrypted[len(encrypted)-32:] - macRecalculated := c.Sha256HMAC(encrypted[:len(encrypted)-32], keyM) + mac := encryptedData[len(encryptedData)-32:] + macRecalculated := c.Sha256HMAC(encryptedData[:len(encryptedData)-32], keyM) if !reflect.DeepEqual(mac, macRecalculated) { return nil, errors.New("incorrect password") } @@ -134,69 +150,17 @@ func ElectrumDecrypt(encryptedData string, toPrivateKey *ec.PrivateKey, fromPubl if err != nil { return nil, err } - log.Printf("IV: %x, plain text: %x", iv, plain) return plain, nil } -// func BitcoreEncrypt(message []byte, publicKey *ec.PublicKey) (string, error) { -// // Generate an ephemeral EC private key -// ephemeralPrivateKey, err := ec.NewPrivateKey() -// if err != nil { -// return "", err -// } - -// // Derive shared secret -// x, _ := publicKey.Curve.ScalarMult(publicKey.X, publicKey.Y, ephemeralPrivateKey.D.Bytes()) -// sharedSecret := x.Bytes() - -// // Key derivation -// keyMaterial := c.Sha512(sharedSecret) - -// keyE := keyMaterial[:32] // AES-256 key -// keyM := keyMaterial[32:] // HMAC key -// iv := make([]byte, 16) // IV for AES (all zeros in Bitcore) - -// // Encrypt the message -// cipherText, err := aescbc.AESEncryptWithIV(message, keyE, iv) -// if err != nil { -// return "", err -// } - -// // Prepare the output -// ephemeralPublicKey := ephemeralPrivateKey.PubKey().SerializeCompressed() -// encryptedData := append(ephemeralPublicKey, cipherText...) - -// // Calculate HMAC -// hmacSum := c.Sha256HMAC(encryptedData, keyM) - -// // Combine all parts -// result := append(encryptedData, hmacSum...) - -// return base64.StdEncoding.EncodeToString(result), nil -// } - -func BitcoreEncrypt(message []byte, toPublicKey *ec.PublicKey, fromPrivateKey *ec.PrivateKey, iv []byte) (string, error) { - - // JS Implementation - // if (!fromPrivateKey) { - // fromPrivateKey = PrivateKey.fromRandom() - // } - // const r = fromPrivateKey - // const RPublicKey = fromPrivateKey.toPublicKey() - // const RBuf = RPublicKey.encode(true) as number[] - // const KB = toPublicKey - // const P = KB.mul(r) - // const S = P.getX() - // const Sbuf = S.toArray('be', 32) - // const kEkM = Hash.sha512(Sbuf) - // const kE = kEkM.slice(0, 32) - // const kM = kEkM.slice(32, 64) - // const c = AESCBC.encrypt(messageBuf, kE, ivBuf) - // const d = Hash.sha256hmac(kM, [...c]) - // const encBuf = [...RBuf, ...c, ...d] - // return encBuf - // If IV is not provided, generate a random one +// BitcoreEncrypt encrypts a message using ECIES +func BitcoreEncrypt(message []byte, + toPublicKey *ec.PublicKey, + fromPrivateKey *ec.PrivateKey, + iv []byte, +) ([]byte, error) { + // If IV is not provided, fill it with zeros if iv == nil { iv = make([]byte, 16) } @@ -206,7 +170,7 @@ func BitcoreEncrypt(message []byte, toPublicKey *ec.PublicKey, fromPrivateKey *e var err error fromPrivateKey, err = ec.NewPrivateKey() if err != nil { - return "", err + return nil, err } } @@ -219,45 +183,18 @@ func BitcoreEncrypt(message []byte, toPublicKey *ec.PublicKey, fromPrivateKey *e kM := kEkM[32:] cc, err := ecies.AESCBCEncrypt(message, kE, iv, true) if err != nil { - return "", err + return nil, err } d := c.Sha256HMAC(cc, kM) encBuf := append(RBuf, cc...) encBuf = append(encBuf, d...) - log.Printf("encBuf: %x", encBuf) - result := base64.StdEncoding.EncodeToString(encBuf) - return result, nil + return encBuf, nil } -func BitcoreDecrypt(encryptedMessage string, toPrivatKey *ec.PrivateKey) ([]byte, error) { - // const kB = toPrivateKey - // const fromPublicKey = PublicKey.fromString(toHex(encBuf.slice(0, 33))) - // const R = fromPublicKey - // const P = R.mul(kB) - // if (P.eq(new Point(0, 0))) { - // throw new Error('P equals 0') - // } - // const S = P.getX() - // const Sbuf = S.toArray('be', 32) - // const kEkM = Hash.sha512(Sbuf) - // const kE = kEkM.slice(0, 32) - // const kM = kEkM.slice(32, 64) - // const c = encBuf.slice(33, encBuf.length - 32) - // const d = encBuf.slice(encBuf.length - 32, encBuf.length) - // const d2 = Hash.sha256hmac(kM, c) - // if (toHex(d) !== toHex(d2)) { - // throw new Error('Invalid checksum') - // } - // const messageBuf = AESCBC.decrypt(c, kE) - // return [...messageBuf] - - data, err := base64.StdEncoding.DecodeString(encryptedMessage) - if err != nil { - return nil, err - } +func BitcoreDecrypt(encryptedMessage []byte, toPrivatKey *ec.PrivateKey) ([]byte, error) { - fromPublicKey, err := ec.ParsePubKey(data[:33]) + fromPublicKey, err := ec.ParsePubKey(encryptedMessage[:33]) if err != nil { return nil, err } @@ -272,41 +209,12 @@ func BitcoreDecrypt(encryptedMessage string, toPrivatKey *ec.PrivateKey) ([]byte kE := kEkM[:32] kM := kEkM[32:] - cipherText := data[33 : len(data)-32] - mac := data[len(data)-32:] + cipherText := encryptedMessage[33 : len(encryptedMessage)-32] + mac := encryptedMessage[len(encryptedMessage)-32:] expectedMAC := c.Sha256HMAC(cipherText, kM) if !hmac.Equal(mac, expectedMAC) { return nil, errors.New("invalid ciphertext: HMAC mismatch") } iv := cipherText[:16] return ecies.AESCBCDecrypt(cipherText[16:], kE, iv) - - // Derive shared secret - // x, _ := privateKey.Curve.ScalarMult(fromPublicKey.X, fromPublicKey.Y, privateKey.D.Bytes()) - // sharedSecret := x.Bytes() - - // Key derivation - // keyMaterial := c.Sha512(sharedSecret) - - // keyE := keyMaterial[:32] // AES-256 key - // keyM := keyMaterial[32:] // HMAC key - - // // Verify HMAC - // mac := data[len(data)-32:] - // encryptedData := data[:len(data)-32] - // expectedMAC := c.Sha256HMAC(encryptedData, keyM) - - // if !hmac.Equal(mac, expectedMAC) { - // return nil, errors.New("invalid ciphertext: HMAC mismatch") - // } - - // // Decrypt - // iv := make([]byte, 16) // In Bitcore, IV is usually all zeros - // cipherText := encryptedData[33:] - // plainText, err := ecies.AESCBCDecrypt(cipherText, keyE, iv) - // if err != nil { - // return nil, err - // } - - // return plainText, nil } diff --git a/compat/ecies/ecies_test.go b/compat/ecies/ecies_test.go index 6eba51f..b5b3e39 100644 --- a/compat/ecies/ecies_test.go +++ b/compat/ecies/ecies_test.go @@ -32,7 +32,9 @@ func TestElectrumEncryptDecryptSingle(t *testing.T) { // Electrum Encrypt encryptedData, err := ElectrumEncrypt([]byte(msgString), pk.PubKey(), pk, false) require.NoError(t, err) - require.Equal(t, "QklFMQO7zpX/GS4XpthCy6/hT38ZKsBGbn8JKMGHOY5ifmaoT890Krt9cIRk/ULXaB5uC08owRICzenFbm31pZGu0gCM2uOxpofwHacKidwZ0Q7aEw==", encryptedData) + expectedB64, err := base64.StdEncoding.DecodeString("QklFMQO7zpX/GS4XpthCy6/hT38ZKsBGbn8JKMGHOY5ifmaoT890Krt9cIRk/ULXaB5uC08owRICzenFbm31pZGu0gCM2uOxpofwHacKidwZ0Q7aEw==") + require.NoError(t, err) + require.Equal(t, expectedB64, encryptedData) // Electrum Decrypt decryptedData, _ := ElectrumDecrypt(encryptedData, pk, nil) @@ -93,11 +95,10 @@ func TestBitcoreEncryptDecryptSolo(t *testing.T) { // Bitcore Encrypt encryptedData, err := BitcoreEncrypt([]byte(msgString), pk.PubKey(), pk, nil) require.NoError(t, err) - require.Len(t, encryptedData, 132) - require.Equal(t, expectedEncryptedData, encryptedData) + require.Equal(t, decodedExpectedEncryptedData, encryptedData) // Bitcore Decrypt - decryptedData, err := BitcoreDecrypt(string(encryptedData), pk) + decryptedData, err := BitcoreDecrypt(encryptedData, pk) require.NoError(t, err) require.Equal(t, msgString, string(decryptedData)) } @@ -110,9 +111,10 @@ func TestBitcoreEncryptDecryptWithCounterparty(t *testing.T) { // Bitcore Encrypt encryptedData, err := BitcoreEncrypt([]byte(msgString), counterpartyPk.PubKey(), pk1, nil) expectedEncryptedData := "A7vOlf8ZLhem2ELLr+FPfxkqwEZufwkowYc5jmJ+ZqhPAAAAAAAAAAAAAAAAAAAAAAmFslNpNc4TrjaMPmPLdooZwoP6/fE7GN3AeyLpFf2f+QGYRKIke8zbhxu8FcLOsA==" + decodedExpectedEncryptedData, _ := base64.StdEncoding.DecodeString(expectedEncryptedData) require.NoError(t, err) - require.Equal(t, expectedEncryptedData, encryptedData) + require.Equal(t, decodedExpectedEncryptedData, encryptedData) // Bitcore Decrypt decryptedData, err := BitcoreDecrypt(encryptedData, counterpartyPk) diff --git a/primitives/aescbc/cbc.go b/primitives/aescbc/cbc.go index 254063a..5b2893f 100644 --- a/primitives/aescbc/cbc.go +++ b/primitives/aescbc/cbc.go @@ -5,7 +5,6 @@ import ( "crypto/aes" "crypto/cipher" "errors" - "log" ) // AESCBCEncrypt encrypts data using AES in CBC mode with an IV @@ -21,7 +20,6 @@ func AESCBCEncrypt(data, key, iv []byte, concatIv bool) ([]byte, error) { if concatIv { cipherText = append(iv, cipherText...) } - log.Printf("cipherText: %x", cipherText) return cipherText, nil } diff --git a/primitives/aesgcm/aesgcm.go b/primitives/aesgcm/aesgcm.go index e6066b0..8870c67 100644 --- a/primitives/aesgcm/aesgcm.go +++ b/primitives/aesgcm/aesgcm.go @@ -41,7 +41,11 @@ func AESDecrypt(ciphertext, key []byte) ([]byte, error) { } // EncryptGCM encrypts plaintext using AES-GCM with the provided key and additional data -func AESGCMEncrypt(plaintext, key, initializationVector, additionalAuthenticatedData []byte) (ciphertext, authenticationTag []byte, err error) { +func AESGCMEncrypt(plaintext, + key, + initializationVector, + additionalAuthenticatedData []byte, +) (ciphertext, authenticationTag []byte, err error) { block, err := aes.NewCipher(key) if err != nil { return nil, nil, err @@ -59,7 +63,12 @@ func AESGCMEncrypt(plaintext, key, initializationVector, additionalAuthenticated } // DecryptGCM decrypts ciphertext using AES-GCM with the provided key, nonce, additional data, and tag -func AESGCMDecrypt(ciphertext, key, initializationVector, additionalAuthenticatedData, authenticationTag []byte) (plaintext []byte, err error) { +func AESGCMDecrypt(ciphertext, + key, + initializationVector, + additionalAuthenticatedData, + authenticationTag []byte, +) (plaintext []byte, err error) { block, err := aes.NewCipher(key) if err != nil { return nil, err From 97933d0ae37455a854139e63036dead77518f2ba Mon Sep 17 00:00:00 2001 From: Satchmo Date: Thu, 5 Sep 2024 14:18:08 -0400 Subject: [PATCH 3/4] add test for shared ecies encryption helper --- CHANGELOG.md | 24 +++++++++++++++ compat/ecies/ecies.go | 15 ++++------ compat/ecies/ecies_test.go | 55 +++++++++++++++++++++++++++------- primitives/hash/hash_test.go | 57 ++++++++++++++++++++++++++++++++---- 4 files changed, 126 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8385294..406a9dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,36 @@ All notable changes to this project will be documented in this file. The format ## Table of Contents - [Unreleased](#unreleased) +- [1.1.4 - 2024-09-05](#113---2024-09-05) - [1.1.3 - 2024-09-04](#113---2024-09-04) - [1.1.2 - 2024-09-02](#112---2024-09-02) - [1.1.1 - 2024-08-28](#111---2024-08-28) - [1.1.0 - 2024-08-19](#110---2024-08-19) - [1.0.0 - 2024-06-06](#100---2024-06-06) +## [1.1.4] - 2024-09-05 + + - Update ECIES implementation to align with + + ### Added + - `primitives/aescbc` directory + - `AESCBCEncrypt`, `AESCBCDecrypt`, `PKCS7Padd`, `PKCS7Unpad` + - `compat/ecies` + - `EncryptSingle`, `DecryptSingle`, `EncryptShared` and `DecryptShared` convenience functions that deal with strings, uses Electrum ECIES and typical defaults + - `ElectrumEncrypt`, `ElectrumDecrypt`, `BitcoreEncrypt`, `BitcoreDecrypt` + - `docs/examples` + - `ecies_shared`, `ecies_single`, `ecies_electrum_binary` + - Tests for different ECIES encryption implementations + + ### Removed + - Previous ecies implementation + - Outdated ecies example + - encryption.go for vanilla AES encryption (to align with typescript library) + + ### Changed + - Renamed `message` example to `encrypt_message` for clarity + - Change vanilla `aes` example to use existing encrypt/decrypt functions from `aesgcm` directory + ## [1.1.3] - 2024-09-04 - Add shamir key splitting diff --git a/compat/ecies/ecies.go b/compat/ecies/ecies.go index dbed9f0..2741710 100644 --- a/compat/ecies/ecies.go +++ b/compat/ecies/ecies.go @@ -19,11 +19,10 @@ import ( func EncryptSingle(message string, privateKey *ec.PrivateKey) (string, error) { messageBytes := []byte(message) - - decryptedBytes, err := ElectrumEncrypt(messageBytes, privateKey.PubKey(), privateKey, false) - if err != nil { - return "", err + if privateKey == nil { + return "", errors.New("private key is required") } + decryptedBytes, _ := ElectrumEncrypt(messageBytes, privateKey.PubKey(), privateKey, false) return base64.StdEncoding.EncodeToString(decryptedBytes), nil } @@ -41,7 +40,7 @@ func DecryptSingle(encryptedData string, privateKey *ec.PrivateKey) (string, err func EncryptShared(message string, toPublicKey *ec.PublicKey, fromPrivateKey *ec.PrivateKey) (string, error) { messageBytes := []byte(message) - decryptedBytes, err := ElectrumEncrypt(messageBytes, toPublicKey, nil, false) + decryptedBytes, err := ElectrumEncrypt(messageBytes, toPublicKey, fromPrivateKey, false) if err != nil { return "", err } @@ -167,11 +166,7 @@ func BitcoreEncrypt(message []byte, // If fromPrivateKey is not provided, generate a random one if fromPrivateKey == nil { - var err error - fromPrivateKey, err = ec.NewPrivateKey() - if err != nil { - return nil, err - } + fromPrivateKey, _ = ec.NewPrivateKey() } RBuf := fromPrivateKey.PubKey().ToDERBytes() diff --git a/compat/ecies/ecies_test.go b/compat/ecies/ecies_test.go index b5b3e39..101b487 100644 --- a/compat/ecies/ecies_test.go +++ b/compat/ecies/ecies_test.go @@ -10,10 +10,12 @@ import ( ) const msgString = "hello world" +const wrongData = "wrong data" +const wif = "L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu" +const counterpartyWif = "L27ZSAC1xTsZrghYHqnxwAQZ12bH57piaAdoGaLizTp3JZrjkZjK" func TestEncryptDecryptSingle(t *testing.T) { - - pk, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + pk, _ := ec.PrivateKeyFromWif(wif) // Electrum Encrypt encryptedData, err := EncryptSingle(msgString, pk) @@ -23,11 +25,37 @@ func TestEncryptDecryptSingle(t *testing.T) { // Electrum Decrypt decryptedData, _ := DecryptSingle(encryptedData, pk) require.Equal(t, msgString, decryptedData) + + // Encrypt without private key + _, err = EncryptSingle(msgString, nil) + require.Error(t, err) + + // Decrypt with non base64 data + _, err = DecryptSingle(wrongData, pk) + require.Error(t, err) + + // Decrypt with wrong data + wrongData := base64.StdEncoding.EncodeToString([]byte(wrongData)) + _, err = DecryptSingle(wrongData, pk) + require.Error(t, err) + +} + +func TestEncryptDecryptShared(t *testing.T) { + pk, _ := ec.PrivateKeyFromWif(wif) + pk2, _ := ec.PrivateKeyFromWif(counterpartyWif) + + encryptedString, err := EncryptShared(msgString, pk2.PubKey(), pk) + require.NoError(t, err) + + decryptedString, err := DecryptShared(encryptedString, pk2, pk.PubKey()) + require.NoError(t, err) + require.Equal(t, msgString, decryptedString) } func TestElectrumEncryptDecryptSingle(t *testing.T) { - pk, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + pk, _ := ec.PrivateKeyFromWif(wif) // Electrum Encrypt encryptedData, err := ElectrumEncrypt([]byte(msgString), pk.PubKey(), pk, false) @@ -39,11 +67,18 @@ func TestElectrumEncryptDecryptSingle(t *testing.T) { // Electrum Decrypt decryptedData, _ := ElectrumDecrypt(encryptedData, pk, nil) require.Equal(t, []byte(msgString), decryptedData) + + // Encrypt without private key + _, err = ElectrumEncrypt([]byte(msgString), pk.PubKey(), nil, false) + require.NoError(t, err) + + // Decrypt with wrong data + _, err = ElectrumDecrypt([]byte(wrongData), pk, nil) + require.Error(t, err) } func TestElectrumEncryptDecryptSingleNokey(t *testing.T) { - - pk, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + pk, _ := ec.PrivateKeyFromWif(wif) // Electrum Encrypt encryptedData, err := ElectrumEncrypt([]byte(msgString), pk.PubKey(), pk, true) @@ -55,7 +90,7 @@ func TestElectrumEncryptDecryptSingleNokey(t *testing.T) { } func TestElectrumEncryptDecryptWithCounterparty(t *testing.T) { - pk1, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + pk1, _ := ec.PrivateKeyFromWif(wif) counterparty, err := ec.PublicKeyFromString("03121a7afe56fc8e25bca4bb2c94f35eb67ebe5b84df2e149d65b9423ee65b8b4b") require.NoError(t, err) @@ -71,7 +106,7 @@ func TestElectrumEncryptDecryptWithCounterparty(t *testing.T) { } func TestElectrumEncryptDecryptWithCounterpartyNoKey(t *testing.T) { - pk1, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + pk1, _ := ec.PrivateKeyFromWif(wif) counterparty, err := ec.PublicKeyFromString("03121a7afe56fc8e25bca4bb2c94f35eb67ebe5b84df2e149d65b9423ee65b8b4b") require.NoError(t, err) @@ -90,7 +125,7 @@ func TestBitcoreEncryptDecryptSolo(t *testing.T) { expectedEncryptedData := "A7vOlf8ZLhem2ELLr+FPfxkqwEZufwkowYc5jmJ+ZqhPAAAAAAAAAAAAAAAAAAAAAB27kUY/HpNbiwhYSpEoEZZDW+wEjMmPNcAAxnc0kiuQ73FpFzf6p6afe4wwVtKAAg==" decodedExpectedEncryptedData, _ := base64.StdEncoding.DecodeString(expectedEncryptedData) log.Printf("Decoded expected encrypted data: %x\n", decodedExpectedEncryptedData) - pk, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") + pk, _ := ec.PrivateKeyFromWif(wif) // Bitcore Encrypt encryptedData, err := BitcoreEncrypt([]byte(msgString), pk.PubKey(), pk, nil) @@ -104,8 +139,8 @@ func TestBitcoreEncryptDecryptSolo(t *testing.T) { } func TestBitcoreEncryptDecryptWithCounterparty(t *testing.T) { - pk1, _ := ec.PrivateKeyFromWif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu") - counterpartyPk, err := ec.PrivateKeyFromWif("L27ZSAC1xTsZrghYHqnxwAQZ12bH57piaAdoGaLizTp3JZrjkZjK") + pk1, _ := ec.PrivateKeyFromWif(wif) + counterpartyPk, err := ec.PrivateKeyFromWif(counterpartyWif) require.NoError(t, err) // Bitcore Encrypt diff --git a/primitives/hash/hash_test.go b/primitives/hash/hash_test.go index 927b66d..69614a9 100644 --- a/primitives/hash/hash_test.go +++ b/primitives/hash/hash_test.go @@ -9,6 +9,9 @@ import ( "github.com/stretchr/testify/require" ) +const testData = "I am a test" +const testData2 = "this is the data I want to hash" + func TestHashFunctions(t *testing.T) { t.Parallel() @@ -26,7 +29,7 @@ func TestHashFunctions(t *testing.T) { }, { "Test Ripemd160 String", - "I am a test", + testData, "09a23f506b4a37cabab8a9e49b541de582fca96b", Ripemd160, }, @@ -38,7 +41,7 @@ func TestHashFunctions(t *testing.T) { }, { "Test Sha256 d String", - "this is the data I want to hash", + testData2, "2209ddda5914a3fbad507ff2284c4b6e559c18a669f9fc3ad3b5826a2a999d58", Sha256d, }, @@ -50,7 +53,7 @@ func TestHashFunctions(t *testing.T) { }, { "Test Sha256 String", - "this is the data I want to hash", + testData2, "f88eec7ecabf88f9a64c4100cac1e0c0c4581100492137d1b656ea626cad63e3", Sha256, }, @@ -62,10 +65,22 @@ func TestHashFunctions(t *testing.T) { }, { "Test Hash160 String", - "this is the data I want to hash", + testData2, "e7fb13ef86fef4203f042fbfc2703fa628301e90", Hash160, }, + { + "Test Sha512 Empty String", + "", + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + Sha512, + }, + { + "Test Sha512 String", + testData2, + "fe917669df24482f19e9fdd305a846ab5778708d75e05bef0eb9b349c22c21c0168892058b26fe9ae0e3488f6b05b5cc6b356f4dd6093cdf9329ed800de3a165", + Sha512, + }, } for _, hashTest := range hashTests { @@ -104,7 +119,6 @@ func TestSha256HMAC(t *testing.T) { "a28cf43130ee696a98f14a37678b56bcfcbdd9e5cf69717fecf5480f0ebdf790", Sha256HMAC, }, - // ... (Other test cases) } for _, tc := range tests { @@ -118,3 +132,36 @@ func TestSha256HMAC(t *testing.T) { }) } } + +func TestSha512HMAC(t *testing.T) { + tests := []struct { + name string + keyHex string + msgHex string + expected string + }{ + { + "Test case 1", + "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "4869205468657265", + "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854", + }, + { + "Test case 2", + "4a656665", + "7768617420646f2079612077616e7420666f72206e6f7468696e673f", + "164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + key, _ := hex.DecodeString(tc.keyHex) + msg, _ := hex.DecodeString(tc.msgHex) + expected, _ := hex.DecodeString(tc.expected) + + result := Sha512HMAC(msg, key) + assert.Equal(t, expected, result) + }) + } +} From 0eab629cb39bab9dd19fd2daf420684c750035fc Mon Sep 17 00:00:00 2001 From: Satchmo Date: Thu, 5 Sep 2024 14:21:51 -0400 Subject: [PATCH 4/4] add comments for ecies methods --- compat/ecies/ecies.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/compat/ecies/ecies.go b/compat/ecies/ecies.go index 2741710..33dde91 100644 --- a/compat/ecies/ecies.go +++ b/compat/ecies/ecies.go @@ -13,10 +13,7 @@ import ( c "github.com/bitcoin-sv/go-sdk/primitives/hash" ) -// -// ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac -// - +// EncryptSingle is a helper that uses Electrum ECIES method to encrypt a message func EncryptSingle(message string, privateKey *ec.PrivateKey) (string, error) { messageBytes := []byte(message) if privateKey == nil { @@ -26,6 +23,7 @@ func EncryptSingle(message string, privateKey *ec.PrivateKey) (string, error) { return base64.StdEncoding.EncodeToString(decryptedBytes), nil } +// DecryptSingle is a helper that uses Electrum ECIES method to decrypt a message func DecryptSingle(encryptedData string, privateKey *ec.PrivateKey) (string, error) { encryptedBytes, err := base64.StdEncoding.DecodeString(encryptedData) if err != nil { @@ -38,6 +36,7 @@ func DecryptSingle(encryptedData string, privateKey *ec.PrivateKey) (string, err return string(plainBytes), nil } +// EncryptShared is a helper that uses Electrum ECIES method to encrypt a message for a target public key func EncryptShared(message string, toPublicKey *ec.PublicKey, fromPrivateKey *ec.PrivateKey) (string, error) { messageBytes := []byte(message) decryptedBytes, err := ElectrumEncrypt(messageBytes, toPublicKey, fromPrivateKey, false) @@ -47,6 +46,7 @@ func EncryptShared(message string, toPublicKey *ec.PublicKey, fromPrivateKey *ec return base64.StdEncoding.EncodeToString(decryptedBytes), nil } +// DecryptShared is a helper that uses Electrum ECIES method to decrypt a message from a target public key func DecryptShared(encryptedData string, toPrivateKey *ec.PrivateKey, fromPublicKey *ec.PublicKey) (string, error) { encryptedBytes, err := base64.StdEncoding.DecodeString(encryptedData) if err != nil { @@ -59,6 +59,7 @@ func DecryptShared(encryptedData string, toPrivateKey *ec.PrivateKey, fromPublic return string(plainBytes), nil } +// ElectrumEncrypt encrypts a message using ECIES using Electrum encryption method func ElectrumEncrypt(message []byte, toPublicKey *ec.PublicKey, fromPrivateKey *ec.PrivateKey, @@ -100,6 +101,8 @@ func ElectrumEncrypt(message []byte, return append(encrypted, mac...), nil } + +// ElectrumDecrypt decrypts a message using ECIES using Electrum decryption method func ElectrumDecrypt(encryptedData []byte, toPrivateKey *ec.PrivateKey, fromPublicKey *ec.PublicKey) ([]byte, error) { if len(encryptedData) < 52 { // Minimum length: 4 (magic) + 16 (min cipher) + 32 (mac) @@ -152,7 +155,7 @@ func ElectrumDecrypt(encryptedData []byte, toPrivateKey *ec.PrivateKey, fromPubl return plain, nil } -// BitcoreEncrypt encrypts a message using ECIES +// BitcoreEncrypt encrypts a message using ECIES using Bitcore encryption method func BitcoreEncrypt(message []byte, toPublicKey *ec.PublicKey, fromPrivateKey *ec.PrivateKey, @@ -187,6 +190,7 @@ func BitcoreEncrypt(message []byte, return encBuf, nil } +// BitcoreDecrypt decrypts a message using ECIES using Bitcore decryption method func BitcoreDecrypt(encryptedMessage []byte, toPrivatKey *ec.PrivateKey) ([]byte, error) { fromPublicKey, err := ec.ParsePubKey(encryptedMessage[:33])