Skip to content

Commit 2f31464

Browse files
committed
internal/ecies: add encrypt/decrypt with ECIES
This commit creates a simple encryption and decryption function that uses ChaCha20Poly1305 for tne symmetric encryption and HKDF with SHA256 for the key derivation. The shared key generation is not part of these functions, because we'll need to use lnd RPCs in some cases to be able to derive it.
1 parent f76d926 commit 2f31464

File tree

3 files changed

+401
-1
lines changed

3 files changed

+401
-1
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ require (
4141
github.com/prometheus/client_golang v1.14.0
4242
github.com/stretchr/testify v1.10.0
4343
github.com/urfave/cli v1.22.14
44+
golang.org/x/crypto v0.36.0
4445
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
4546
golang.org/x/net v0.38.0
4647
golang.org/x/sync v0.12.0
@@ -188,7 +189,6 @@ require (
188189
go.uber.org/atomic v1.10.0 // indirect
189190
go.uber.org/multierr v1.6.0 // indirect
190191
go.uber.org/zap v1.23.0 // indirect
191-
golang.org/x/crypto v0.36.0 // indirect
192192
golang.org/x/mod v0.21.0 // indirect
193193
golang.org/x/sys v0.31.0 // indirect
194194
golang.org/x/text v0.23.0 // indirect

internal/ecies/ecies.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// This package implements an ECIES (Elliptic Curve Integrated Encryption
2+
// Scheme) encryption. It uses ChaCha20Poly1305 for encryption and HKDF with
3+
// SHA256 for key derivation. The package provides functions to encrypt and
4+
// decrypt messages using a shared secret derived between two parties using ECDH
5+
// (Elliptic Curve Diffie-Hellman).
6+
7+
package ecies
8+
9+
import (
10+
"bytes"
11+
crand "crypto/rand"
12+
"crypto/sha256"
13+
"fmt"
14+
"io"
15+
"math"
16+
17+
"github.com/btcsuite/btcd/btcec/v2"
18+
"golang.org/x/crypto/chacha20poly1305"
19+
"golang.org/x/crypto/hkdf"
20+
)
21+
22+
const (
23+
// protocolName is the name of the protocol used for encryption and
24+
// decryption. This is used to salt the HKDF key derivation.
25+
protocolName = "ECIES-HKDF-SHA256-XCHA20POLY1305"
26+
)
27+
28+
// EncryptSha256ChaCha20Poly1305 encrypts the given message using
29+
// ChaCha20Poly1305 with a shared secret (usually derived using ECDH between the
30+
// sender's ephemeral key and the receiver's public key) that is hardened using
31+
// HKDF with SHA256. The cipher also authenticates the additional data and
32+
// prepends it to the returned encrypted message. The additional data is limited
33+
// to at most 255 bytes. The output is a byte slice containing:
34+
//
35+
// <1 byte AD length> <* bytes AD> <24 bytes nonce> <* bytes ciphertext>
36+
func EncryptSha256ChaCha20Poly1305(sharedSecret [32]byte, msg []byte,
37+
additionalData []byte) ([]byte, error) {
38+
39+
if len(additionalData) > math.MaxUint8 {
40+
return nil, fmt.Errorf("additional data too long: %d bytes "+
41+
"given, 255 bytes maximum", len(additionalData))
42+
}
43+
44+
// We begin by hardening the shared secret against brute forcing by
45+
// using HKDF with SHA256.
46+
stretchedKey, err := HkdfSha256(sharedSecret[:], []byte(protocolName))
47+
if err != nil {
48+
return nil, fmt.Errorf("cannot derive hkdf key: %w", err)
49+
}
50+
51+
// We can now create a new XChaCha20Poly1305 AEAD cipher using the
52+
// stretched key.
53+
aead, err := chacha20poly1305.NewX(stretchedKey[:])
54+
if err != nil {
55+
return nil, fmt.Errorf("cannot create new chacha20poly1305 "+
56+
"cipher: %w", err)
57+
}
58+
59+
// Select a random nonce, and leave capacity for the ciphertext.
60+
nonce := make(
61+
[]byte, aead.NonceSize(),
62+
aead.NonceSize()+len(msg)+aead.Overhead(),
63+
)
64+
65+
if _, err := crand.Read(nonce); err != nil {
66+
return nil, fmt.Errorf("cannot read random nonce: %w", err)
67+
}
68+
69+
ciphertext := aead.Seal(nonce, nonce, msg, additionalData)
70+
71+
var result bytes.Buffer
72+
result.WriteByte(byte(len(additionalData)))
73+
result.Write(additionalData)
74+
result.Write(ciphertext)
75+
76+
return result.Bytes(), nil
77+
}
78+
79+
// ExtractAdditionalData extracts the additional data and the ciphertext from
80+
// the given message. The message must be in the format:
81+
//
82+
// <1 byte AD length> <* bytes AD> <24 bytes nonce> <* bytes ciphertext>
83+
func ExtractAdditionalData(msg []byte) ([]byte, []byte, error) {
84+
// We need at least 1 byte for the additional data length.
85+
if len(msg) < 1 {
86+
return nil, nil, fmt.Errorf("ciphertext too short: %d bytes "+
87+
"given, 1 byte minimum", len(msg))
88+
}
89+
90+
// Extract the additional data length from the first byte of the
91+
// ciphertext.
92+
additionalDataLen := int(msg[0])
93+
94+
// Before we start, we check that the ciphertext is at least
95+
// 1+adLength+24+16 bytes long. This is the minimum size for a valid
96+
// ciphertext, as it contains the additional data length (1 byte), the
97+
// additional data (additionalDataLen bytes), the nonce (24 bytes) and
98+
// the overhead (16 bytes).
99+
minLength := 1 + additionalDataLen + chacha20poly1305.NonceSizeX +
100+
chacha20poly1305.Overhead
101+
if len(msg) < minLength {
102+
return nil, nil, fmt.Errorf("ciphertext too short: %d bytes "+
103+
"given, %d bytes minimum", len(msg), minLength)
104+
}
105+
106+
additionalData := msg[1 : 1+additionalDataLen]
107+
msg = msg[1+additionalDataLen:]
108+
109+
return additionalData, msg, nil
110+
}
111+
112+
// DecryptSha256ChaCha20Poly1305 decrypts the given ciphertext using
113+
// ChaCha20Poly1305 with a shared secret (usually derived using ECDH between the
114+
// sender's ephemeral key and the receiver's public key) that is hardened using
115+
// HKDF with SHA256. The cipher also authenticates the additional data and
116+
// prepends it to the returned encrypted message. The additional data is limited
117+
// to at most 255 bytes. The ciphertext must be in the format:
118+
//
119+
// <1 byte AD length> <* bytes AD> <24 bytes nonce> <* bytes ciphertext>
120+
func DecryptSha256ChaCha20Poly1305(sharedSecret [32]byte,
121+
msg []byte) ([]byte, error) {
122+
123+
// Make sure the message correctly encodes the additional data.
124+
additionalData, remainder, err := ExtractAdditionalData(msg)
125+
if err != nil {
126+
return nil, err
127+
}
128+
129+
// We begin by hardening the shared secret against brute forcing by
130+
// using HKDF with SHA256.
131+
stretchedKey, err := HkdfSha256(sharedSecret[:], []byte(protocolName))
132+
if err != nil {
133+
return nil, fmt.Errorf("cannot derive hkdf key: %w", err)
134+
}
135+
136+
// We can now create a new XChaCha20Poly1305 AEAD cipher using the
137+
// stretched key.
138+
aead, err := chacha20poly1305.NewX(stretchedKey[:])
139+
if err != nil {
140+
return nil, fmt.Errorf("cannot create new chacha20poly1305 "+
141+
"cipher: %w", err)
142+
}
143+
144+
// Split additional data, nonce and ciphertext.
145+
nonce := remainder[:aead.NonceSize()]
146+
ciphertext := remainder[aead.NonceSize():]
147+
148+
// Decrypt the message and check it wasn't tampered with.
149+
plaintext, err := aead.Open(nil, nonce, ciphertext, additionalData)
150+
if err != nil {
151+
return nil, fmt.Errorf("cannot decrypt message: %w", err)
152+
}
153+
154+
return plaintext, nil
155+
}
156+
157+
// HkdfSha256 derives a 32-byte key from the given secret and salt using HKDF
158+
// with SHA256.
159+
func HkdfSha256(secret, salt []byte) ([32]byte, error) {
160+
var key [32]byte
161+
kdf := hkdf.New(sha256.New, secret, salt, nil)
162+
if _, err := io.ReadFull(kdf, key[:]); err != nil {
163+
return [32]byte{}, fmt.Errorf("cannot read secret from HKDF "+
164+
"reader: %w", err)
165+
}
166+
167+
return key, nil
168+
}
169+
170+
// ECDH performs a scalar multiplication (ECDH-like operation) between the
171+
// target private key and remote public key. The output returned will be
172+
// the sha256 of the resulting shared point serialized in compressed format. If
173+
// k is our private key, and P is the public key, we perform the following
174+
// operation:
175+
//
176+
// sx = k*P
177+
// s = sha256(sx.SerializeCompressed())
178+
func ECDH(privKey *btcec.PrivateKey, pub *btcec.PublicKey) ([32]byte, error) {
179+
var (
180+
pubJacobian btcec.JacobianPoint
181+
s btcec.JacobianPoint
182+
)
183+
pub.AsJacobian(&pubJacobian)
184+
185+
btcec.ScalarMultNonConst(&privKey.Key, &pubJacobian, &s)
186+
s.ToAffine()
187+
sPubKey := btcec.NewPublicKey(&s.X, &s.Y)
188+
return sha256.Sum256(sPubKey.SerializeCompressed()), nil
189+
}

0 commit comments

Comments
 (0)