Рыба проекта. Минимальная функциональность

This commit is contained in:
2026-08-03 22:22:24 +03:00
commit 8c8631ac9c
80 changed files with 10618 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
package mlkem768
import (
"bytes"
"crypto/mlkem"
"crypto/sha256"
"crypto/sha3"
"errors"
"fmt"
"io"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
)
// suiteID is the scheme identifier for ML-KEM-768.
const suiteID uint16 = 0x0006
// ErrDecapsulationFailed is returned when ciphertext decapsulation fails,
// typically due to a tampered or invalid ciphertext.
var ErrDecapsulationFailed = errors.New("decapsulation failed")
// DefaultRegistry is the package-level registry for ML-KEM-768.
var DefaultRegistry = crypto.NewRegistry()
// kemAdapter wraps the Go stdlib crypto/mlkem 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 ML-KEM-768 scheme identifier (0x0006).
func (k *kemAdapter) SchemeID() uint16 {
return suiteID
}
// GenerateKeyPair generates a new ML-KEM-768 key pair.
//
// Note: the rand parameter is part of the KEM interface contract but is
// ignored here because the stdlib crypto/mlkem.GenerateKey768 uses
// crypto/rand internally.
func (k *kemAdapter) GenerateKeyPair(
rand io.Reader,
) (
crypto.RecipientPub,
crypto.RecipientPriv,
error,
) {
dk, err := mlkem.GenerateKey768()
if err != nil {
return nil, nil, err
}
ek := dk.EncapsulationKey()
rawPub := ek.Bytes()
keyID := computeKeyID(rawPub)
pub := &pubKey{
key: ek,
keyID: keyID,
}
priv := &privKey{
key: dk,
keyID: keyID,
}
return pub, priv, nil
}
// Encapsulate generates a shared secret and ciphertext for the given public key.
//
// Note: the rand parameter is part of the KEM interface contract but is
// ignored here because the stdlib (*EncapsulationKey768).Encapsulate uses
// crypto/rand internally.
//
// CRITICAL: the stdlib returns (sharedKey, ciphertext) but this adapter
// swaps the order to (ciphertext, sharedSecret) to match the domain KEM
// interface contract.
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 := mlkem.NewEncapsulationKey768(raw)
if parseErr != nil {
return nil, nil, fmt.Errorf("invalid public key for ML-KEM-768: %w", parseErr)
}
p = &pubKey{key: ek, keyID: computeKeyID(raw)}
}
// stdlib returns (sharedKey, ciphertext); we swap to (ciphertext, sharedSecret).
ss, ct := p.key.Encapsulate()
return ct, ss, nil
}
// LoadPriv loads an ML-KEM-768 private key from raw bytes.
func (k *kemAdapter) LoadPriv(
raw []byte,
) (
crypto.RecipientPriv,
error,
) {
dk, err := mlkem.NewDecapsulationKey768(raw)
if err != nil {
return nil, fmt.Errorf("invalid ML-KEM-768 private key: %w", err)
}
pubRaw := dk.EncapsulationKey().Bytes()
return &privKey{key: dk, keyID: computeKeyID(pubRaw)}, nil
}
// Decapsulate recovers the shared secret from a ciphertext using the private key.
// For tampered ciphertexts, it returns ErrDecapsulationFailed.
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 ML-KEM-768")
}
ss, err := p.key.Decapsulate(ciphertext)
if err != nil {
return nil, errors.Join(ErrDecapsulationFailed, err)
}
// crypto/mlkem.Decapsulate implements implicit rejection: it returns a
// pseudorandom shared secret instead of an error for invalid ciphertexts.
// Perform explicit rejection by recomputing the implicit-rejection value
// Kout = SHAKE256(z || ciphertext) and comparing it with the result.
// If they match, the ciphertext was invalid.
seed := p.key.Bytes()
z := seed[32:]
shake := sha3.NewSHAKE256()
// sha3.ShakeHash.Write/Read never return an error; explicitly ignore to satisfy errcheck.
_, _ = shake.Write(z)
_, _ = shake.Write(ciphertext)
computedKout := make([]byte, mlkem.SharedKeySize)
_, _ = shake.Read(computedKout)
if bytes.Equal(ss, computedKout) {
return nil, ErrDecapsulationFailed
}
return ss, nil
}
// pubKey wraps *mlkem.EncapsulationKey768 to satisfy crypto.RecipientPub.
type pubKey struct {
key *mlkem.EncapsulationKey768
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 *mlkem.DecapsulationKey768 to satisfy crypto.RecipientPriv.
type privKey struct {
key *mlkem.DecapsulationKey768
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 first 8 bytes of raw key material.
func computeKeyID(raw []byte) []byte {
h := sha256.Sum256(raw[:8])
return h[:8]
}
// init registers the ML-KEM-768 factory under suiteID 0x0006.
func init() {
_ = DefaultRegistry.Register(
suiteID,
func() crypto.KEM {
return New()
},
)
}
@@ -0,0 +1,140 @@
package mlkem768
import (
"bytes"
"crypto/sha256"
"errors"
"testing"
)
func TestGenerateKeyPair(t *testing.T) {
adapter := New()
pub, priv, err := adapter.GenerateKeyPair(nil)
if err != nil {
t.Fatalf("GenerateKeyPair failed: %v", err)
}
if pub.SchemeID() != suiteID {
t.Errorf("pub.SchemeID() = 0x%04x, want 0x%04x", pub.SchemeID(), suiteID)
}
if priv.SchemeID() != suiteID {
t.Errorf("priv.SchemeID() = 0x%04x, want 0x%04x", priv.SchemeID(), suiteID)
}
rawPub := pub.Raw()
if len(rawPub) != 1184 {
t.Errorf("pub.Raw() len = %d, want 1184", len(rawPub))
}
rawPriv := priv.Raw()
if len(rawPriv) != 64 {
t.Errorf("priv.Raw() len = %d, want 64", len(rawPriv))
}
expectedKeyID := sha256.Sum256(rawPub[:8])
if !bytes.Equal(pub.KeyID(), expectedKeyID[:8]) {
t.Errorf("pub.KeyID() = %x, want %x", pub.KeyID(), expectedKeyID[:8])
}
if !bytes.Equal(priv.KeyID(), pub.KeyID()) {
t.Errorf("priv.KeyID() = %x, want %x", priv.KeyID(), pub.KeyID())
}
}
func TestEncapsulateReturnOrder(t *testing.T) {
adapter := New()
pub, _, err := adapter.GenerateKeyPair(nil)
if err != nil {
t.Fatalf("GenerateKeyPair failed: %v", err)
}
ct, ss, err := adapter.Encapsulate(pub, nil)
if err != nil {
t.Fatalf("Encapsulate failed: %v", err)
}
if len(ct) != 1088 {
t.Errorf("ciphertext len = %d, want 1088", len(ct))
}
if len(ss) != 32 {
t.Errorf("sharedSecret len = %d, want 32", len(ss))
}
}
func TestRoundTrip(t *testing.T) {
adapter := New()
pub, priv, err := adapter.GenerateKeyPair(nil)
if err != nil {
t.Fatalf("GenerateKeyPair failed: %v", err)
}
ct, ssEnc, err := adapter.Encapsulate(pub, nil)
if err != nil {
t.Fatalf("Encapsulate failed: %v", err)
}
ssDec, err := adapter.Decapsulate(priv, ct)
if err != nil {
t.Fatalf("Decapsulate failed: %v", err)
}
if !bytes.Equal(ssEnc, ssDec) {
t.Fatalf("shared secret mismatch: encapsulate=%x, decapsulate=%x", ssEnc, ssDec)
}
}
func TestDecapsulateTamperedCiphertext(t *testing.T) {
adapter := New()
pub, priv, err := adapter.GenerateKeyPair(nil)
if err != nil {
t.Fatalf("GenerateKeyPair failed: %v", err)
}
ct, _, err := adapter.Encapsulate(pub, nil)
if err != nil {
t.Fatalf("Encapsulate failed: %v", err)
}
ct[0] ^= 0xFF
_, err = adapter.Decapsulate(priv, ct)
if err == nil {
t.Fatal("Decapsulate with tampered ciphertext: expected error, got nil")
}
if !errors.Is(err, ErrDecapsulationFailed) {
t.Errorf("Decapsulate error = %v, want ErrDecapsulationFailed", err)
}
}
func TestRegistryRegistration(t *testing.T) {
factory, err := DefaultRegistry.Lookup(suiteID)
if err != nil {
t.Fatalf("Lookup suiteID 0x%04x failed: %v", suiteID, err)
}
instance := factory()
if instance.SchemeID() != suiteID {
t.Errorf("factory() SchemeID = 0x%04x, want 0x%04x", instance.SchemeID(), suiteID)
}
}
func TestFactoryReturnsIndependentInstances(t *testing.T) {
factory, err := DefaultRegistry.Lookup(suiteID)
if err != nil {
t.Fatalf("Lookup suiteID 0x%04x failed: %v", suiteID, err)
}
one := factory()
two := factory()
if one.SchemeID() != two.SchemeID() {
t.Error("factory() returned instances with different scheme IDs")
}
}