// Package crypto implements flit's identity and signaling crypto: Ed25519 // device identity, X25519 ECDH (static, derived from Ed25519; and ephemeral, // yaw/2.1 forward-secret signaling), and the nacl/box signaling seal. // Ported and trimmed from waste-go/internal/crypto — dropped chat/backup // helpers not needed for 1:1 file transfer. package crypto import ( "crypto/ed25519" "crypto/rand" "crypto/sha512" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "os" "path/filepath" "filippo.io/edwards25519" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/nacl/box" ) var b64 = base64.RawURLEncoding // ── Identity ────────────────────────────────────────────────────────────────── type Identity struct { privateKey ed25519.PrivateKey PublicKey ed25519.PublicKey } type identityFile struct { PrivateKeyB64 string `json:"private_key"` } // LoadOrCreate loads the identity from dataDir/identity.json, or generates a // fresh keypair if none exists yet. func LoadOrCreate(dataDir string) (*Identity, error) { if err := os.MkdirAll(dataDir, 0700); err != nil { return nil, fmt.Errorf("creating data dir: %w", err) } path := filepath.Join(dataDir, "identity.json") data, err := os.ReadFile(path) if err == nil { var f identityFile if err := json.Unmarshal(data, &f); err != nil { return nil, fmt.Errorf("parsing identity file: %w", err) } privBytes, err := b64.DecodeString(f.PrivateKeyB64) if err != nil { return nil, fmt.Errorf("decoding private key: %w", err) } priv := ed25519.PrivateKey(privBytes) return &Identity{privateKey: priv, PublicKey: priv.Public().(ed25519.PublicKey)}, nil } if !errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("reading identity file: %w", err) } pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf("generating keypair: %w", err) } f := identityFile{PrivateKeyB64: b64.EncodeToString(priv)} raw, _ := json.MarshalIndent(f, "", " ") if err := os.WriteFile(path, raw, 0600); err != nil { return nil, fmt.Errorf("saving identity: %w", err) } return &Identity{privateKey: priv, PublicKey: pub}, nil } // PeerID returns the lowercase hex encoding of the 32-byte Ed25519 public key. func (id *Identity) PeerID() string { return hex.EncodeToString(id.PublicKey) } func (id *Identity) Sign(data []byte) string { return hex.EncodeToString(ed25519.Sign(id.privateKey, data)) } func Verify(publicKeyHex string, data []byte, sigHex string) error { pubBytes, err := hex.DecodeString(publicKeyHex) if err != nil { return fmt.Errorf("decoding public key: %w", err) } sigBytes, err := hex.DecodeString(sigHex) if err != nil { return fmt.Errorf("decoding signature: %w", err) } if !ed25519.Verify(ed25519.PublicKey(pubBytes), data, sigBytes) { return errors.New("signature verification failed") } return nil } // CurvePublicKey returns the X25519 public key derived from this Ed25519 identity. func (id *Identity) CurvePublicKey() *[32]byte { edPoint, _ := new(edwards25519.Point).SetBytes(id.PublicKey) mont := edPoint.BytesMontgomery() var out [32]byte copy(out[:], mont) return &out } // CurvePrivateKey returns the X25519 private key derived from this Ed25519 identity. func (id *Identity) CurvePrivateKey() *[32]byte { h := sha512.Sum512(id.privateKey[:32]) h[0] &= 248 h[31] &= 127 h[31] |= 64 var out [32]byte copy(out[:], h[:32]) return &out } // CurveFromPeerID derives the X25519 public key from a peer's hex Ed25519 id. func CurveFromPeerID(idHex string) (*[32]byte, error) { pubBytes, err := hex.DecodeString(idHex) if err != nil || len(pubBytes) != 32 { return nil, fmt.Errorf("invalid peer id %q", idHex) } edPoint, err := new(edwards25519.Point).SetBytes(pubBytes) if err != nil { return nil, fmt.Errorf("ed25519 point: %w", err) } mont := edPoint.BytesMontgomery() var out [32]byte copy(out[:], mont) return &out, nil } // ── Signaling seal (nacl/box, base64 std-padded) ────────────────────────────── func SignalingBox(plaintext []byte, recipientPub, senderPriv *[32]byte) string { var nonce [24]byte rand.Read(nonce[:]) //nolint:errcheck ct := box.Seal(nonce[:], plaintext, &nonce, recipientPub, senderPriv) return base64.StdEncoding.EncodeToString(ct) } func SignalingOpen(b64box string, senderPub, recipientPriv *[32]byte) ([]byte, error) { raw, err := base64.StdEncoding.DecodeString(b64box) if err != nil || len(raw) < 24 { return nil, errors.New("invalid box") } var nonce [24]byte copy(nonce[:], raw[:24]) out, ok := box.Open(nil, raw[24:], &nonce, senderPub, recipientPriv) if !ok { return nil, errors.New("box open failed") } return out, nil } // ── X25519 ephemeral keys (yaw/2.1 forward secrecy) ─────────────────────────── type EphemeralKey struct { private [32]byte public [32]byte } func GenerateEphemeral() (*EphemeralKey, error) { ek := &EphemeralKey{} if _, err := rand.Read(ek.private[:]); err != nil { return nil, fmt.Errorf("generating ephemeral key: %w", err) } ek.private[0] &= 248 ek.private[31] &= 127 ek.private[31] |= 64 pub, err := curve25519.X25519(ek.private[:], curve25519.Basepoint) if err != nil { return nil, fmt.Errorf("computing public key: %w", err) } copy(ek.public[:], pub) return ek, nil } func (ek *EphemeralKey) PublicRaw() *[32]byte { return &ek.public } func (ek *EphemeralKey) PrivateRaw() *[32]byte { return &ek.private } func (ek *EphemeralKey) Wipe() { for i := range ek.private { ek.private[i] = 0 } }