mirror of
https://github.com/sonr-io/crypto.git
synced 2026-09-17 01:26:23 +00:00
197 lines
8.4 KiB
Plaintext
197 lines
8.4 KiB
Plaintext
---
|
|||
|
|
title: ECIES
|
||
|
|
description: Encrypt a payload to a secp256k1 public key. A thin wrapper over github.com/ecies/go/v2 with one significant seed hazard.
|
||
|
|
sidebar:
|
||
|
|
order: 5
|
||
|
|
icon: mail
|
||
|
|
---
|
||
|
|
|
||
|
|
`github.com/sonr-io/crypto/ecies` is a **thin wrapper** — three files, 70 lines of code — over
|
||
|
|
[`github.com/ecies/go/v2`](https://github.com/ecies/go). ECIES (Elliptic Curve Integrated Encryption
|
||
|
|
Scheme) is hybrid public-key encryption: the sender generates an ephemeral keypair, does ECDH against
|
||
|
|
the recipient's static public key, derives a symmetric key, and encrypts the payload under an AEAD.
|
||
|
|
The recipient needs no prior interaction — just their own private key and the ciphertext.
|
||
|
|
|
||
|
|
**Reach for this when** you need to encrypt a payload to a public key you already have, with no
|
||
|
|
handshake and no shared state.
|
||
|
|
|
||
|
|
**Do not reach for this when** you need forward secrecy for the recipient, authenticated sender
|
||
|
|
identity (ECIES gives you confidentiality, not sender authentication — sign separately with
|
||
|
|
[`mpc`](/identity/mpc-enclave) or an [ECDSA](/signatures/ecdsa) key), or a symmetric key you already
|
||
|
|
share (use [AEAD](/symmetric/aead) directly).
|
||
|
|
|
||
|
|
## The API surface
|
||
|
|
|
||
|
|
```go
|
||
|
|
type PrivateKey = eciesgo.PrivateKey // type ALIAS, not a wrapper struct
|
||
|
|
type PublicKey = eciesgo.PublicKey // type ALIAS
|
||
|
|
|
||
|
|
func GenerateKey() (*PrivateKey, error)
|
||
|
|
func GenerateKeyFromSeed(seed []byte) (*PrivateKey, error)
|
||
|
|
func HashSeed(seed []byte) []byte
|
||
|
|
|
||
|
|
func Encrypt(pub *PublicKey, plaintext []byte) ([]byte, error)
|
||
|
|
func Decrypt(priv *PrivateKey, ciphertext []byte) ([]byte, error)
|
||
|
|
```
|
||
|
|
|
||
|
|
That is the entire package. `Encrypt` and `Decrypt` are one-line forwards to `eciesgo.Encrypt` and
|
||
|
|
`eciesgo.Decrypt`.
|
||
|
|
|
||
|
|
:::note[The key types are aliases, so the upstream API is yours]
|
||
|
|
`PrivateKey` and `PublicKey` are Go **type aliases** (`type PrivateKey = eciesgo.PrivateKey`), not
|
||
|
|
distinct named types. Everything the upstream library defines on those types is directly available:
|
||
|
|
`priv.Bytes()`, `priv.Hex()`, `priv.PublicKey`, `priv.ECDH(pub)`, `pub.Bytes(compressed bool)`,
|
||
|
|
`pub.Hex(compressed bool)`, `eciesgo.NewPrivateKeyFromHex`, `eciesgo.NewPublicKeyFromBytes`, and so
|
||
|
|
on.
|
||
|
|
|
||
|
|
Consult [`github.com/ecies/go/v2`](https://github.com/ecies/go) for:
|
||
|
|
|
||
|
|
- **key serialization** — this package exposes no marshal/unmarshal helpers of its own;
|
||
|
|
- **the ciphertext wire format** — the ephemeral-key encoding, KDF and AEAD choices are entirely
|
||
|
|
upstream's, and are not restated or pinned here.
|
||
|
|
:::
|
||
|
|
|
||
|
|
## Curve
|
||
|
|
|
||
|
|
`GenerateKey` and `GenerateKeyFromSeed` both build their key on `curves.SP256()`, which returns
|
||
|
|
`ecc.P256k1()` from `github.com/dustinxie/ecc` — i.e. **secp256k1**, the same curve as
|
||
|
|
[`mpc`](/identity/mpc-enclave) and secp256k1 `did:key` identifiers. See
|
||
|
|
[Foundations → Curves](/foundations/curves) for the curve abstraction.
|
||
|
|
|
||
|
|
Note the constructors bypass `eciesgo.GenerateKey` and assemble the struct by hand from
|
||
|
|
`ecdsa.GenerateKey(curve, rand.Reader)`:
|
||
|
|
|
||
|
|
```go
|
||
|
|
p, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||
|
|
return &PrivateKey{
|
||
|
|
PublicKey: &PublicKey{Curve: curve, X: p.X, Y: p.Y},
|
||
|
|
D: p.D,
|
||
|
|
}, nil
|
||
|
|
```
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
|
||
|
|
```go ecies_roundtrip.go
|
||
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
|
||
|
|
"github.com/sonr-io/crypto/ecies"
|
||
|
|
)
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
// Recipient generates a keypair and publishes the public key.
|
||
|
|
priv, err := ecies.GenerateKey()
|
||
|
|
if err != nil {
|
||
|
|
panic(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sender encrypts to the public key. No prior interaction needed.
|
||
|
|
ciphertext, err := ecies.Encrypt(priv.PublicKey, []byte("hello"))
|
||
|
|
if err != nil {
|
||
|
|
panic(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Recipient decrypts with the private key.
|
||
|
|
plaintext, err := ecies.Decrypt(priv, ciphertext)
|
||
|
|
if err != nil {
|
||
|
|
panic(err)
|
||
|
|
}
|
||
|
|
fmt.Println(string(plaintext)) // hello
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
`GenerateKey` is grounded in `TestGenerateKey` and `GenerateKeyFromSeed` in `TestGenerateFromSeed`
|
||
|
|
(`ecies/keys_test.go`). The encrypt/decrypt round trip above is **not** covered by any test in the
|
||
|
|
package — see the caveats.
|
||
|
|
|
||
|
|
## `HashSeed` and seeded keys
|
||
|
|
|
||
|
|
`HashSeed(seed []byte) []byte` is `blake3.Sum512(seed)` from `lukechampine.com/blake3`, returned as a
|
||
|
|
64-byte slice. Its purpose is to stretch an arbitrary-length input up to enough bytes for
|
||
|
|
`GenerateKeyFromSeed`, which reads from the seed as an entropy source:
|
||
|
|
|
||
|
|
```go
|
||
|
|
seed := ecies.HashSeed([]byte("some high-entropy passphrase or master secret"))
|
||
|
|
priv, err := ecies.GenerateKeyFromSeed(seed)
|
||
|
|
```
|
||
|
|
|
||
|
|
:::note[The seed is key material]
|
||
|
|
`GenerateKeyFromSeed` treats its argument as the sole entropy input. Whoever holds the seed can
|
||
|
|
recompute the private key. Store, transmit and destroy a seed exactly as you would a private key —
|
||
|
|
and note that `HashSeed` is a plain hash, **not** a password KDF: it has no salt, no work factor and
|
||
|
|
no memory hardness. Do not feed it a human-chosen password. For password-derived keys use a real KDF
|
||
|
|
from [Key Derivation](/symmetric/key-derivation).
|
||
|
|
:::
|
||
|
|
|
||
|
|
:::danger[`GenerateKeyFromSeed` is not deterministic]
|
||
|
|
Despite the name, this function does not reliably produce the same key from the same seed on current
|
||
|
|
Go toolchains. `ecdsa.GenerateKey(curve, bytes.NewReader(seed))` passes the seed reader into
|
||
|
|
`crypto/ecdsa`, but the standard library does not use it as given:
|
||
|
|
|
||
|
|
- On Go 1.26 and later, `crypto/ecdsa` routes a caller-supplied reader through
|
||
|
|
`crypto/internal/rand.CustomReader`, which **returns the system CSPRNG and discards the supplied
|
||
|
|
reader** unless the `GODEBUG` setting `cryptocustomrand=1` is active. The `cryptocustomrand`
|
||
|
|
default became `0` in Go 1.26, so a program whose main module declares `go 1.26` or later gets a
|
||
|
|
fully random key and the seed is ignored entirely.
|
||
|
|
- Under the older behaviour (`cryptocustomrand=1`, i.e. a main module declaring an earlier Go
|
||
|
|
version), `randutil.MaybeReadByte` consumes a byte from the reader with roughly 50% probability
|
||
|
|
before key generation, which shifts the whole byte stream. Measured against this package: 20
|
||
|
|
successive calls with an identical seed produced the same private key only **13 times out of 20**.
|
||
|
|
|
||
|
|
Both behaviours were confirmed empirically against this package on Go 1.27.
|
||
|
|
|
||
|
|
`ecies/keys_test.go`'s `TestGenerateFromSeed` calls `GenerateKeyFromSeed` twice with the same seed
|
||
|
|
but only asserts that neither call errors — it never compares the two keys, which is why the defect
|
||
|
|
is not caught.
|
||
|
|
|
||
|
|
**Do not use `GenerateKeyFromSeed` for deterministic key derivation.** If you need a key
|
||
|
|
reproducible from a seed, derive the scalar yourself with a KDF from
|
||
|
|
[Key Derivation](/symmetric/key-derivation) and construct the key from those bytes via
|
||
|
|
`eciesgo.NewPrivateKeyFromBytes`.
|
||
|
|
:::
|
||
|
|
|
||
|
|
## Caveats
|
||
|
|
|
||
|
|
:::warning[`GenerateKeyFromSeed` errors on a short seed]
|
||
|
|
The implementation slices `seed[:]` and hands it to `bytes.NewReader`. `randFieldElement` then calls
|
||
|
|
`io.ReadFull`, which returns `io.ErrUnexpectedEOF` on a seed shorter than 32 bytes — surfaced as
|
||
|
|
`"cannot generate key pair: unexpected EOF"`. A `nil` seed is worse: `seed[:]` on a nil slice is
|
||
|
|
legal, so you get the same EOF error rather than a clear "nil seed" message. Always pass
|
||
|
|
`HashSeed(...)` output (64 bytes) rather than a raw seed. Under the Go 1.26+ behaviour described
|
||
|
|
above the reader is never consulted, so short seeds succeed there — which makes the failure mode
|
||
|
|
toolchain-dependent.
|
||
|
|
:::
|
||
|
|
|
||
|
|
:::warning[No round-trip test]
|
||
|
|
`ecies/keys_test.go` is 24 lines and contains two tests: `TestGenerateKey` and
|
||
|
|
`TestGenerateFromSeed`. **Neither `Encrypt` nor `Decrypt` is tested at all**, and there is no test
|
||
|
|
that the hand-assembled `PrivateKey`/`PublicKey` structs are accepted by the upstream library. The
|
||
|
|
round trip does work — it was verified directly against this package — but the package ships no
|
||
|
|
regression coverage for its two most important functions.
|
||
|
|
:::
|
||
|
|
|
||
|
|
:::info[No authentication of the sender]
|
||
|
|
ECIES ciphertext is confidential and integrity-protected against tampering, but **anyone** with the
|
||
|
|
recipient's public key can produce a valid ciphertext. If the recipient needs to know who sent a
|
||
|
|
message, sign the plaintext (or the ciphertext) separately and transmit the signature alongside it.
|
||
|
|
:::
|
||
|
|
|
||
|
|
## Next
|
||
|
|
|
||
|
|
<CardGroup cols={2}>
|
||
|
|
<Card title="AEAD" href="/symmetric/aead" icon="lock">
|
||
|
|
The symmetric layer, for when you already share a key.
|
||
|
|
</Card>
|
||
|
|
<Card title="Key Derivation" href="/symmetric/key-derivation" icon="git-branch">
|
||
|
|
Real KDFs, for deriving keys from seeds or passwords.
|
||
|
|
</Card>
|
||
|
|
<Card title="MPC Enclave" href="/identity/mpc-enclave" icon="shield">
|
||
|
|
Signing, to pair with encryption for sender authentication.
|
||
|
|
</Card>
|
||
|
|
<Card title="Curves" href="/foundations/curves" icon="binary">
|
||
|
|
The secp256k1 curve this package builds on.
|
||
|
|
</Card>
|
||
|
|
</CardGroup>
|