197 lines
4.5 KiB
Go
197 lines
4.5 KiB
Go
package x25519
|
|
|
|
import (
|
|
"crypto/ecdh"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
|
|
)
|
|
|
|
// suiteID is the scheme identifier for X25519 ECDH KEM.
|
|
const suiteID uint16 = 0x0007
|
|
|
|
// ErrDecapsulationFailed is returned when ciphertext decapsulation fails,
|
|
// typically because the ciphertext is not a valid X25519 public key.
|
|
var ErrDecapsulationFailed = errors.New("decapsulation failed")
|
|
|
|
// DefaultRegistry is the package-level registry for X25519.
|
|
var DefaultRegistry = crypto.NewRegistry()
|
|
|
|
// kemAdapter wraps the Go stdlib crypto/ecdh X25519 implementation to satisfy
|
|
// the pkg/domain/crypto.KEM interface.
|
|
type kemAdapter struct{}
|
|
|
|
// New creates a new KEM adapter instance.
|
|
func New() crypto.KEM {
|
|
return &kemAdapter{}
|
|
}
|
|
|
|
// SchemeID returns the X25519 scheme identifier (0x0007).
|
|
func (k *kemAdapter) SchemeID() uint16 {
|
|
return suiteID
|
|
}
|
|
|
|
// GenerateKeyPair generates a new X25519 key pair.
|
|
func (k *kemAdapter) GenerateKeyPair(
|
|
rand io.Reader,
|
|
) (
|
|
crypto.RecipientPub,
|
|
crypto.RecipientPriv,
|
|
error,
|
|
) {
|
|
ecdhPriv, err := ecdh.X25519().GenerateKey(rand)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
rawPub := ecdhPriv.PublicKey().Bytes()
|
|
keyID := computeKeyID(rawPub)
|
|
|
|
pub := &pubKey{
|
|
key: ecdhPriv.PublicKey(),
|
|
keyID: keyID,
|
|
}
|
|
priv := &privKey{
|
|
key: ecdhPriv,
|
|
keyID: keyID,
|
|
}
|
|
|
|
return pub, priv, nil
|
|
}
|
|
|
|
// Encapsulate generates a shared secret and ciphertext for the given public key.
|
|
// The ciphertext is the ephemeral public key (32 bytes).
|
|
func (k *kemAdapter) Encapsulate(
|
|
pub crypto.RecipientPub,
|
|
rand io.Reader,
|
|
) (
|
|
ciphertext []byte,
|
|
sharedSecret []byte,
|
|
err error,
|
|
) {
|
|
p, ok := pub.(*pubKey)
|
|
if !ok {
|
|
raw := pub.Raw()
|
|
ek, parseErr := ecdh.X25519().NewPublicKey(raw)
|
|
if parseErr != nil {
|
|
return nil, nil, fmt.Errorf("invalid public key for X25519: %w", parseErr)
|
|
}
|
|
p = &pubKey{key: ek, keyID: computeKeyID(raw)}
|
|
}
|
|
|
|
ephPriv, err := ecdh.X25519().GenerateKey(rand)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
ct := ephPriv.PublicKey().Bytes()
|
|
ss, err := ephPriv.ECDH(p.key)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return ct, ss, nil
|
|
}
|
|
|
|
// LoadPriv loads an X25519 private key from raw bytes.
|
|
func (k *kemAdapter) LoadPriv(
|
|
raw []byte,
|
|
) (
|
|
crypto.RecipientPriv,
|
|
error,
|
|
) {
|
|
dk, err := ecdh.X25519().NewPrivateKey(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid X25519 private key: %w", err)
|
|
}
|
|
pubRaw := dk.PublicKey().Bytes()
|
|
return &privKey{key: dk, keyID: computeKeyID(pubRaw)}, nil
|
|
}
|
|
|
|
// Decapsulate recovers the shared secret from a ciphertext using the private key.
|
|
// The ciphertext must be a valid 32-byte X25519 public key.
|
|
func (k *kemAdapter) Decapsulate(
|
|
priv crypto.RecipientPriv,
|
|
ciphertext []byte,
|
|
) (
|
|
sharedSecret []byte,
|
|
err error,
|
|
) {
|
|
p, ok := priv.(*privKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid private key type for X25519")
|
|
}
|
|
|
|
if len(ciphertext) != 32 {
|
|
return nil, errors.Join(ErrDecapsulationFailed, errors.New("invalid ciphertext length"))
|
|
}
|
|
|
|
// X25519 public keys are 255-bit Montgomery u-coordinates; bit 255 must be zero.
|
|
if ciphertext[31]&0x80 != 0 {
|
|
return nil, ErrDecapsulationFailed
|
|
}
|
|
|
|
// Reject the all-zero public key (identity point), which yields an all-zero shared secret.
|
|
allZero := true
|
|
for _, b := range ciphertext {
|
|
if b != 0 {
|
|
allZero = false
|
|
break
|
|
}
|
|
}
|
|
if allZero {
|
|
return nil, ErrDecapsulationFailed
|
|
}
|
|
|
|
ephPub, err := ecdh.X25519().NewPublicKey(ciphertext)
|
|
if err != nil {
|
|
return nil, errors.Join(ErrDecapsulationFailed, err)
|
|
}
|
|
|
|
ss, err := p.key.ECDH(ephPub)
|
|
if err != nil {
|
|
return nil, errors.Join(ErrDecapsulationFailed, err)
|
|
}
|
|
|
|
return ss, nil
|
|
}
|
|
|
|
// pubKey wraps *ecdh.PublicKey to satisfy crypto.RecipientPub.
|
|
type pubKey struct {
|
|
key *ecdh.PublicKey
|
|
keyID []byte
|
|
}
|
|
|
|
func (p *pubKey) SchemeID() uint16 { return suiteID }
|
|
func (p *pubKey) KeyID() []byte { return p.keyID }
|
|
func (p *pubKey) Raw() []byte { return p.key.Bytes() }
|
|
|
|
// privKey wraps *ecdh.PrivateKey to satisfy crypto.RecipientPriv.
|
|
type privKey struct {
|
|
key *ecdh.PrivateKey
|
|
keyID []byte
|
|
}
|
|
|
|
func (p *privKey) SchemeID() uint16 { return suiteID }
|
|
func (p *privKey) KeyID() []byte { return p.keyID }
|
|
func (p *privKey) Raw() []byte { return p.key.Bytes() }
|
|
|
|
// computeKeyID derives the first 8 bytes of SHA-256 over the raw public key.
|
|
func computeKeyID(raw []byte) []byte {
|
|
h := sha256.Sum256(raw)
|
|
return h[:8]
|
|
}
|
|
|
|
// init registers the X25519 factory under suiteID 0x0007.
|
|
func init() {
|
|
_ = DefaultRegistry.Register(
|
|
suiteID,
|
|
func() crypto.KEM {
|
|
return New()
|
|
},
|
|
)
|
|
}
|