Skip to content

Commit

Permalink
Merge pull request #49 from bitcoin-sv/update/ecies
Browse files Browse the repository at this point in the history
update encryption implementations
  • Loading branch information
rohenaz authored Sep 5, 2024
2 parents f8436b5 + 0eab629 commit fd6b293
Show file tree
Hide file tree
Showing 20 changed files with 1,047 additions and 505 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
219 changes: 219 additions & 0 deletions compat/ecies/ecies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package compat

import (
"crypto/hmac"
"encoding/base64"
"math/big"
"reflect"

"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"
)

// 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 {
return "", errors.New("private key is required")
}
decryptedBytes, _ := ElectrumEncrypt(messageBytes, privateKey.PubKey(), privateKey, false)
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 {
return "", err
}
plainBytes, err := ElectrumDecrypt(encryptedBytes, privateKey, nil)
if err != nil {
return "", 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)
if err != nil {
return "", err
}
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 {
return "", err
}
plainBytes, err := ElectrumDecrypt(encryptedBytes, toPrivateKey, fromPublicKey)
if err != nil {
return "", err
}
return string(plainBytes), nil
}

// ElectrumEncrypt encrypts a message using ECIES using Electrum encryption method
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 {
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 nil, 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 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)
return nil, errors.New("invalid encrypted text: length")
}
magic := encryptedData[:4]
if string(magic) != "BIE1" {
return nil, errors.New("invalid cipher text: invalid magic bytes")
}

var sharedSecret []byte
var cipherText []byte

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(encryptedData) > 69 { // 4 (magic) + 33 (pubkey) + 32 (mac)
cipherText = encryptedData[37 : len(encryptedData)-32]
} else {
cipherText = encryptedData[4 : len(encryptedData)-32]
}
} else {
// Use ephemeral public key to derive shared secret
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 = encryptedData[37 : len(encryptedData)-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 := encryptedData[len(encryptedData)-32:]
macRecalculated := c.Sha256HMAC(encryptedData[:len(encryptedData)-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
}
return plain, nil
}

// BitcoreEncrypt encrypts a message using ECIES using Bitcore encryption method
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)
}

// If fromPrivateKey is not provided, generate a random one
if fromPrivateKey == nil {
fromPrivateKey, _ = ec.NewPrivateKey()
}

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 nil, err
}
d := c.Sha256HMAC(cc, kM)
encBuf := append(RBuf, cc...)
encBuf = append(encBuf, d...)

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])
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 := 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)
}
Loading

0 comments on commit fd6b293

Please sign in to comment.