// Package identity manages Ed25519 cryptographic identities package identity import ( "crypto/ed25519" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "os" "golang.org/x/crypto/scrypt" "golang.org/x/crypto/nacl/secretbox" ) // Identity represents a cryptographic identity type Identity struct { PublicKey ed25519.PublicKey PrivateKey ed25519.PrivateKey } // encryptedIdentity is the encrypted on-disk format type encryptedIdentity struct { Nonce [24]byte `json:"nonce"` Ciphertext []byte `json:"ciphertext"` Salt []byte `json:"salt"` } // NewIdentity generates a new Ed25519 identity func NewIdentity() (*Identity, error) { pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf("failed to generate keypair: %w", err) } return &Identity{ PublicKey: pubKey, PrivateKey: privKey, }, nil } // LoadIdentity loads an encrypted identity from disk func LoadIdentity(path string, passphrase []byte) (*Identity, error) { // Read encrypted file data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read identity file: %w", err) } // Parse JSON var encrypted encryptedIdentity if err := json.Unmarshal(data, &encrypted); err != nil { return nil, fmt.Errorf("failed to parse identity file: %w", err) } // Derive encryption key from passphrase key, err := scrypt.Key(passphrase, encrypted.Salt, 32768, 8, 1, 32) if err != nil { return nil, fmt.Errorf("failed to derive key: %w", err) } // Decrypt private key var keyArray [32]byte copy(keyArray[:], key) plaintext, ok := secretbox.Open(nil, encrypted.Ciphertext, &encrypted.Nonce, &keyArray) if !ok { return nil, fmt.Errorf("decryption failed: wrong passphrase or corrupted file") } // Parse private key if len(plaintext) != ed25519.PrivateKeySize { return nil, fmt.Errorf("invalid private key size") } privKey := ed25519.PrivateKey(plaintext) pubKey := privKey.Public().(ed25519.PublicKey) return &Identity{ PublicKey: pubKey, PrivateKey: privKey, }, nil } // Save encrypts and saves the identity to disk func (id *Identity) Save(path string, passphrase []byte) error { // Generate random salt salt := make([]byte, 32) if _, err := rand.Read(salt); err != nil { return fmt.Errorf("failed to generate salt: %w", err) } // Derive encryption key key, err := scrypt.Key(passphrase, salt, 32768, 8, 1, 32) if err != nil { return fmt.Errorf("failed to derive key: %w", err) } // Generate random nonce var nonce [24]byte if _, err := rand.Read(nonce[:]); err != nil { return fmt.Errorf("failed to generate nonce: %w", err) } // Encrypt private key var keyArray [32]byte copy(keyArray[:], key) ciphertext := secretbox.Seal(nil, id.PrivateKey, &nonce, &keyArray) // Create encrypted structure encrypted := encryptedIdentity{ Nonce: nonce, Ciphertext: ciphertext, Salt: salt, } // Marshal to JSON data, err := json.MarshalIndent(encrypted, "", " ") if err != nil { return fmt.Errorf("failed to marshal identity: %w", err) } // Write to file with secure permissions if err := os.WriteFile(path, data, 0600); err != nil { return fmt.Errorf("failed to write identity file: %w", err) } return nil } // GetPublicKeyHex returns the public key as hex string func (id *Identity) GetPublicKeyHex() string { return hex.EncodeToString(id.PublicKey) } // GetPrivateKeyHex returns the private key as hex string (use with caution!) func (id *Identity) GetPrivateKeyHex() string { return hex.EncodeToString(id.PrivateKey) } // Sign signs a message with the private key func (id *Identity) Sign(message []byte) []byte { return ed25519.Sign(id.PrivateKey, message) } // Verify verifies a signature func (id *Identity) Verify(message, signature []byte) bool { return ed25519.Verify(id.PublicKey, message, signature) } // SecureWipe zeros out sensitive key material from memory func (id *Identity) SecureWipe() { // Zero out private key for i := range id.PrivateKey { id.PrivateKey[i] = 0 } // Public key is not sensitive, but wipe it anyway for i := range id.PublicKey { id.PublicKey[i] = 0 } } // String returns a safe string representation func (id *Identity) String() string { return fmt.Sprintf("Identity(pubkey=%s...)", id.GetPublicKeyHex()[:16]) }