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