diff options
| author | Claude <noreply@anthropic.com> | 2025-11-23 10:07:55 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2025-11-23 10:07:55 +0000 |
| commit | 69a75acea56bbff39fa1c81d4813ebcb4a617f06 (patch) | |
| tree | ee1239320191bae0df39bdbe0d058069c27a0107 /pkg/identity | |
| parent | 9b4caf3e5ffad219ea55ea6db7ad19940c5f83d9 (diff) | |
| download | noshitalk-69a75acea56bbff39fa1c81d4813ebcb4a617f06.tar.gz noshitalk-69a75acea56bbff39fa1c81d4813ebcb4a617f06.tar.xz noshitalk-69a75acea56bbff39fa1c81d4813ebcb4a617f06.zip | |
Refactor: modular architecture with shared packages
Major code reorganization to eliminate duplication and improve maintainability:
- Created pkg/crypto: shared cryptographic functions (padding, AES-GCM, ECDH, HMAC auth)
- Created pkg/protocol: unified message structures and JSON serialization
- Created pkg/identity: persistent identity management (load/save/generate)
- Created pkg/tor: Tor SOCKS5 proxy connection utilities
- Refactored server to use shared packages (cmd/server)
- Refactored CLI client with shared packages (cmd/cli-client)
- Refactored GUI client with shared packages (cmd/gui-client)
- Refactored web client with embedded HTML template (cmd/web-client)
Security improvements:
- Replaced math/rand with crypto/rand for randomDelay()
- Added proper error handling for crypto operations
Code reduction: eliminated ~500+ lines of duplicated code
Diffstat (limited to 'pkg/identity')
| -rw-r--r-- | pkg/identity/identity.go | 254 |
1 files changed, 254 insertions, 0 deletions
diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go new file mode 100644 index 0000000..be48a44 --- /dev/null +++ b/pkg/identity/identity.go @@ -0,0 +1,254 @@ +// Package identity provides persistent cryptographic identity management for NoshiTalk. +package identity + +import ( + "crypto/ecdh" + cryptorand "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + + "noshitalk/pkg/crypto" +) + +const ( + // DefaultKeyDir is the default directory for storing keys + DefaultKeyDir = ".noshitalk" + + // IdentityVersion is the current identity file format version + IdentityVersion = "1.0" +) + +// Identity represents a user's cryptographic identity. +type Identity struct { + PrivateKey *ecdh.PrivateKey + PublicKey *ecdh.PublicKey + Username string + Fingerprint string +} + +// IdentityFile represents the on-disk format for storing identity. +type IdentityFile struct { + Version string `json:"version"` + Username string `json:"username"` + Fingerprint string `json:"fingerprint"` + SignKeyPair KeyPair `json:"signKeyPair,omitempty"` + EncryptKeyPair KeyPair `json:"encryptKeyPair,omitempty"` + PrivateKey string `json:"privateKey,omitempty"` // For simple format +} + +// KeyPair represents a public/private key pair in hex encoding. +type KeyPair struct { + PrivateKey string `json:"privateKey"` + PublicKey string `json:"publicKey"` +} + +// GetDefaultKeyPath returns the default path for storing identity keys. +func GetDefaultKeyPath(filename string) (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get home directory: %w", err) + } + return filepath.Join(homeDir, DefaultKeyDir, filename), nil +} + +// New generates a new random identity. +func New(usernamePrefix string) (*Identity, error) { + privateKey, err := crypto.GenerateX25519KeyPair() + if err != nil { + return nil, fmt.Errorf("failed to generate key pair: %w", err) + } + + publicKey := privateKey.PublicKey() + fingerprint := crypto.DeriveFingerprint(publicKey.Bytes()) + username := fmt.Sprintf("%s_%s", usernamePrefix, fingerprint[:8]) + + return &Identity{ + PrivateKey: privateKey, + PublicKey: publicKey, + Username: username, + Fingerprint: fingerprint, + }, nil +} + +// LoadOrCreate loads an existing identity from file or creates a new one. +func LoadOrCreate(keyPath, usernamePrefix string) (*Identity, error) { + // Try to load existing + if _, err := os.Stat(keyPath); err == nil { + return Load(keyPath) + } + + // Generate new + identity, err := New(usernamePrefix) + if err != nil { + return nil, err + } + + // Save it + if err := identity.Save(keyPath); err != nil { + return nil, fmt.Errorf("failed to save new identity: %w", err) + } + + return identity, nil +} + +// Load loads an identity from a file. +// Supports both simple format (just private key bytes) and JSON format. +func Load(keyPath string) (*Identity, error) { + data, err := os.ReadFile(keyPath) + if err != nil { + return nil, fmt.Errorf("failed to read identity file: %w", err) + } + + curve := ecdh.X25519() + + // Try JSON format first + var idFile IdentityFile + if err := json.Unmarshal(data, &idFile); err == nil { + // JSON format - check for different key storage formats + var privateKeyBytes []byte + + if idFile.EncryptKeyPair.PrivateKey != "" { + // GUI client format + privateKeyBytes, err = hex.DecodeString(idFile.EncryptKeyPair.PrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to decode encrypt private key: %w", err) + } + } else if idFile.SignKeyPair.PrivateKey != "" { + // Alternative format + privateKeyBytes, err = hex.DecodeString(idFile.SignKeyPair.PrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to decode sign private key: %w", err) + } + } else if idFile.PrivateKey != "" { + // Simple JSON format + privateKeyBytes, err = hex.DecodeString(idFile.PrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to decode private key: %w", err) + } + } else { + return nil, fmt.Errorf("no private key found in identity file") + } + + privateKey, err := curve.NewPrivateKey(privateKeyBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + + publicKey := privateKey.PublicKey() + fingerprint := crypto.DeriveFingerprint(publicKey.Bytes()) + + username := idFile.Username + if username == "" { + username = crypto.DeriveIdentity(publicKey.Bytes()) + } + + return &Identity{ + PrivateKey: privateKey, + PublicKey: publicKey, + Username: username, + Fingerprint: fingerprint, + }, nil + } + + // Try raw binary format (CLI client format) + if len(data) == 32 { + privateKey, err := curve.NewPrivateKey(data) + if err != nil { + return nil, fmt.Errorf("failed to parse raw private key: %w", err) + } + + publicKey := privateKey.PublicKey() + fingerprint := crypto.DeriveFingerprint(publicKey.Bytes()) + username := crypto.DeriveIdentity(publicKey.Bytes()) + + return &Identity{ + PrivateKey: privateKey, + PublicKey: publicKey, + Username: username, + Fingerprint: fingerprint, + }, nil + } + + return nil, fmt.Errorf("unrecognized identity file format") +} + +// Save saves the identity to a file in JSON format. +func (id *Identity) Save(keyPath string) error { + // Create directory if needed + dir := filepath.Dir(keyPath) + if err := os.MkdirAll(dir, 0700); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + idFile := IdentityFile{ + Version: IdentityVersion, + Username: id.Username, + Fingerprint: id.Fingerprint, + EncryptKeyPair: KeyPair{ + PrivateKey: hex.EncodeToString(id.PrivateKey.Bytes()), + PublicKey: hex.EncodeToString(id.PublicKey.Bytes()), + }, + } + + data, err := json.MarshalIndent(idFile, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal identity: %w", err) + } + + if err := os.WriteFile(keyPath, data, 0600); err != nil { + return fmt.Errorf("failed to write identity file: %w", err) + } + + return nil +} + +// SaveRaw saves only the private key bytes (for CLI client compatibility). +func (id *Identity) SaveRaw(keyPath string) error { + dir := filepath.Dir(keyPath) + if err := os.MkdirAll(dir, 0700); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + if err := os.WriteFile(keyPath, id.PrivateKey.Bytes(), 0600); err != nil { + return fmt.Errorf("failed to write key file: %w", err) + } + + return nil +} + +// DeriveSharedIdentity derives a deterministic identity from a seed. +// This is useful for testing or when you want reproducible identities. +func DeriveSharedIdentity(seed string, usernamePrefix string) (*Identity, error) { + // Hash the seed to get 32 bytes + hash := sha256.Sum256([]byte(seed)) + + curve := ecdh.X25519() + privateKey, err := curve.NewPrivateKey(hash[:]) + if err != nil { + return nil, fmt.Errorf("failed to derive private key: %w", err) + } + + publicKey := privateKey.PublicKey() + fingerprint := crypto.DeriveFingerprint(publicKey.Bytes()) + username := fmt.Sprintf("%s_%s", usernamePrefix, fingerprint[:8]) + + return &Identity{ + PrivateKey: privateKey, + PublicKey: publicKey, + Username: username, + Fingerprint: fingerprint, + }, nil +} + +// GenerateChallenge generates a random challenge for authentication. +func GenerateChallenge() ([]byte, error) { + challenge := make([]byte, 32) + if _, err := cryptorand.Read(challenge); err != nil { + return nil, fmt.Errorf("failed to generate challenge: %w", err) + } + return challenge, nil +} |
