Skip to content

Commit fd6b293

Browse files
authored
Merge pull request #49 from bitcoin-sv/update/ecies
update encryption implementations
2 parents f8436b5 + 0eab629 commit fd6b293

File tree

20 files changed

+1047
-505
lines changed

20 files changed

+1047
-505
lines changed

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,36 @@ All notable changes to this project will be documented in this file. The format
55
## Table of Contents
66

77
- [Unreleased](#unreleased)
8+
- [1.1.4 - 2024-09-05](#113---2024-09-05)
89
- [1.1.3 - 2024-09-04](#113---2024-09-04)
910
- [1.1.2 - 2024-09-02](#112---2024-09-02)
1011
- [1.1.1 - 2024-08-28](#111---2024-08-28)
1112
- [1.1.0 - 2024-08-19](#110---2024-08-19)
1213
- [1.0.0 - 2024-06-06](#100---2024-06-06)
1314

15+
## [1.1.4] - 2024-09-05
16+
17+
- Update ECIES implementation to align with
18+
19+
### Added
20+
- `primitives/aescbc` directory
21+
- `AESCBCEncrypt`, `AESCBCDecrypt`, `PKCS7Padd`, `PKCS7Unpad`
22+
- `compat/ecies`
23+
- `EncryptSingle`, `DecryptSingle`, `EncryptShared` and `DecryptShared` convenience functions that deal with strings, uses Electrum ECIES and typical defaults
24+
- `ElectrumEncrypt`, `ElectrumDecrypt`, `BitcoreEncrypt`, `BitcoreDecrypt`
25+
- `docs/examples`
26+
- `ecies_shared`, `ecies_single`, `ecies_electrum_binary`
27+
- Tests for different ECIES encryption implementations
28+
29+
### Removed
30+
- Previous ecies implementation
31+
- Outdated ecies example
32+
- encryption.go for vanilla AES encryption (to align with typescript library)
33+
34+
### Changed
35+
- Renamed `message` example to `encrypt_message` for clarity
36+
- Change vanilla `aes` example to use existing encrypt/decrypt functions from `aesgcm` directory
37+
1438
## [1.1.3] - 2024-09-04
1539

1640
- Add shamir key splitting

compat/ecies/ecies.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package compat
2+
3+
import (
4+
"crypto/hmac"
5+
"encoding/base64"
6+
"math/big"
7+
"reflect"
8+
9+
"errors"
10+
11+
ecies "github.com/bitcoin-sv/go-sdk/primitives/aescbc"
12+
ec "github.com/bitcoin-sv/go-sdk/primitives/ec"
13+
c "github.com/bitcoin-sv/go-sdk/primitives/hash"
14+
)
15+
16+
// EncryptSingle is a helper that uses Electrum ECIES method to encrypt a message
17+
func EncryptSingle(message string, privateKey *ec.PrivateKey) (string, error) {
18+
messageBytes := []byte(message)
19+
if privateKey == nil {
20+
return "", errors.New("private key is required")
21+
}
22+
decryptedBytes, _ := ElectrumEncrypt(messageBytes, privateKey.PubKey(), privateKey, false)
23+
return base64.StdEncoding.EncodeToString(decryptedBytes), nil
24+
}
25+
26+
// DecryptSingle is a helper that uses Electrum ECIES method to decrypt a message
27+
func DecryptSingle(encryptedData string, privateKey *ec.PrivateKey) (string, error) {
28+
encryptedBytes, err := base64.StdEncoding.DecodeString(encryptedData)
29+
if err != nil {
30+
return "", err
31+
}
32+
plainBytes, err := ElectrumDecrypt(encryptedBytes, privateKey, nil)
33+
if err != nil {
34+
return "", err
35+
}
36+
return string(plainBytes), nil
37+
}
38+
39+
// EncryptShared is a helper that uses Electrum ECIES method to encrypt a message for a target public key
40+
func EncryptShared(message string, toPublicKey *ec.PublicKey, fromPrivateKey *ec.PrivateKey) (string, error) {
41+
messageBytes := []byte(message)
42+
decryptedBytes, err := ElectrumEncrypt(messageBytes, toPublicKey, fromPrivateKey, false)
43+
if err != nil {
44+
return "", err
45+
}
46+
return base64.StdEncoding.EncodeToString(decryptedBytes), nil
47+
}
48+
49+
// DecryptShared is a helper that uses Electrum ECIES method to decrypt a message from a target public key
50+
func DecryptShared(encryptedData string, toPrivateKey *ec.PrivateKey, fromPublicKey *ec.PublicKey) (string, error) {
51+
encryptedBytes, err := base64.StdEncoding.DecodeString(encryptedData)
52+
if err != nil {
53+
return "", err
54+
}
55+
plainBytes, err := ElectrumDecrypt(encryptedBytes, toPrivateKey, fromPublicKey)
56+
if err != nil {
57+
return "", err
58+
}
59+
return string(plainBytes), nil
60+
}
61+
62+
// ElectrumEncrypt encrypts a message using ECIES using Electrum encryption method
63+
func ElectrumEncrypt(message []byte,
64+
toPublicKey *ec.PublicKey,
65+
fromPrivateKey *ec.PrivateKey,
66+
noKey bool,
67+
) ([]byte, error) {
68+
// Generate an ephemeral EC private key if fromPrivateKey is nil
69+
var ephemeralPrivateKey *ec.PrivateKey
70+
if fromPrivateKey == nil {
71+
ephemeralPrivateKey, _ = ec.NewPrivateKey()
72+
} else {
73+
ephemeralPrivateKey = fromPrivateKey
74+
}
75+
76+
// Derive ECDH key
77+
x, y := toPublicKey.Curve.ScalarMult(toPublicKey.X, toPublicKey.Y, ephemeralPrivateKey.D.Bytes())
78+
ecdhKey := (&ec.PublicKey{X: x, Y: y}).SerializeCompressed()
79+
80+
// SHA512(ECDH_KEY)
81+
key := c.Sha512(ecdhKey)
82+
iv, keyE, keyM := key[0:16], key[16:32], key[32:]
83+
84+
// AES encryption
85+
cipherText, err := ecies.AESCBCEncrypt(message, keyE, iv, false)
86+
if err != nil {
87+
return nil, err
88+
}
89+
90+
ephemeralPublicKey := ephemeralPrivateKey.PubKey()
91+
var encrypted []byte
92+
if noKey {
93+
// encrypted = magic_bytes(4 bytes) + cipher
94+
encrypted = append([]byte("BIE1"), cipherText...)
95+
} else {
96+
// encrypted = magic_bytes(4 bytes) + ephemeral_public_key(33 bytes) + cipher
97+
encrypted = append(append([]byte("BIE1"), ephemeralPublicKey.SerializeCompressed()...), cipherText...)
98+
}
99+
100+
mac := c.Sha256HMAC(encrypted, keyM)
101+
102+
return append(encrypted, mac...), nil
103+
}
104+
105+
// ElectrumDecrypt decrypts a message using ECIES using Electrum decryption method
106+
func ElectrumDecrypt(encryptedData []byte, toPrivateKey *ec.PrivateKey, fromPublicKey *ec.PublicKey) ([]byte, error) {
107+
108+
if len(encryptedData) < 52 { // Minimum length: 4 (magic) + 16 (min cipher) + 32 (mac)
109+
return nil, errors.New("invalid encrypted text: length")
110+
}
111+
magic := encryptedData[:4]
112+
if string(magic) != "BIE1" {
113+
return nil, errors.New("invalid cipher text: invalid magic bytes")
114+
}
115+
116+
var sharedSecret []byte
117+
var cipherText []byte
118+
119+
if fromPublicKey != nil {
120+
// Use counterparty public key to derive shared secret
121+
x, y := toPrivateKey.Curve.ScalarMult(fromPublicKey.X, fromPublicKey.Y, toPrivateKey.D.Bytes())
122+
sharedSecret = (&ec.PublicKey{X: x, Y: y}).SerializeCompressed()
123+
if len(encryptedData) > 69 { // 4 (magic) + 33 (pubkey) + 32 (mac)
124+
cipherText = encryptedData[37 : len(encryptedData)-32]
125+
} else {
126+
cipherText = encryptedData[4 : len(encryptedData)-32]
127+
}
128+
} else {
129+
// Use ephemeral public key to derive shared secret
130+
ephemeralPublicKey, err := ec.ParsePubKey(encryptedData[4:37])
131+
if err != nil {
132+
return nil, err
133+
}
134+
x, y := ephemeralPublicKey.Curve.ScalarMult(ephemeralPublicKey.X, ephemeralPublicKey.Y, toPrivateKey.D.Bytes())
135+
sharedSecret = (&ec.PublicKey{X: x, Y: y}).SerializeCompressed()
136+
cipherText = encryptedData[37 : len(encryptedData)-32]
137+
}
138+
139+
// Derive key_e, iv and key_m
140+
key := c.Sha512(sharedSecret)
141+
iv, keyE, keyM := key[0:16], key[16:32], key[32:]
142+
143+
// Verify mac
144+
mac := encryptedData[len(encryptedData)-32:]
145+
macRecalculated := c.Sha256HMAC(encryptedData[:len(encryptedData)-32], keyM)
146+
if !reflect.DeepEqual(mac, macRecalculated) {
147+
return nil, errors.New("incorrect password")
148+
}
149+
150+
// AES decryption
151+
plain, err := ecies.AESCBCDecrypt(cipherText, keyE, iv)
152+
if err != nil {
153+
return nil, err
154+
}
155+
return plain, nil
156+
}
157+
158+
// BitcoreEncrypt encrypts a message using ECIES using Bitcore encryption method
159+
func BitcoreEncrypt(message []byte,
160+
toPublicKey *ec.PublicKey,
161+
fromPrivateKey *ec.PrivateKey,
162+
iv []byte,
163+
) ([]byte, error) {
164+
165+
// If IV is not provided, fill it with zeros
166+
if iv == nil {
167+
iv = make([]byte, 16)
168+
}
169+
170+
// If fromPrivateKey is not provided, generate a random one
171+
if fromPrivateKey == nil {
172+
fromPrivateKey, _ = ec.NewPrivateKey()
173+
}
174+
175+
RBuf := fromPrivateKey.PubKey().ToDERBytes()
176+
P := toPublicKey.Mul(fromPrivateKey.D)
177+
178+
Sbuf := P.X.Bytes()
179+
kEkM := c.Sha512(Sbuf)
180+
kE := kEkM[:32]
181+
kM := kEkM[32:]
182+
cc, err := ecies.AESCBCEncrypt(message, kE, iv, true)
183+
if err != nil {
184+
return nil, err
185+
}
186+
d := c.Sha256HMAC(cc, kM)
187+
encBuf := append(RBuf, cc...)
188+
encBuf = append(encBuf, d...)
189+
190+
return encBuf, nil
191+
}
192+
193+
// BitcoreDecrypt decrypts a message using ECIES using Bitcore decryption method
194+
func BitcoreDecrypt(encryptedMessage []byte, toPrivatKey *ec.PrivateKey) ([]byte, error) {
195+
196+
fromPublicKey, err := ec.ParsePubKey(encryptedMessage[:33])
197+
if err != nil {
198+
return nil, err
199+
}
200+
201+
P := fromPublicKey.Mul(toPrivatKey.D)
202+
if P.X.Cmp(big.NewInt(0)) == 0 && P.Y.Cmp(big.NewInt(0)) == 0 {
203+
return nil, errors.New("p equals 0")
204+
}
205+
206+
Sbuf := P.X.Bytes()
207+
kEkM := c.Sha512(Sbuf)
208+
kE := kEkM[:32]
209+
kM := kEkM[32:]
210+
211+
cipherText := encryptedMessage[33 : len(encryptedMessage)-32]
212+
mac := encryptedMessage[len(encryptedMessage)-32:]
213+
expectedMAC := c.Sha256HMAC(cipherText, kM)
214+
if !hmac.Equal(mac, expectedMAC) {
215+
return nil, errors.New("invalid ciphertext: HMAC mismatch")
216+
}
217+
iv := cipherText[:16]
218+
return ecies.AESCBCDecrypt(cipherText[16:], kE, iv)
219+
}

0 commit comments

Comments
 (0)