mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
352 lines
9.1 KiB
Go
352 lines
9.1 KiB
Go
// Copyright (C) 2022-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package key provides a pluggable key storage backend system supporting:
|
|
// - Software encrypted storage (AES-256-GCM + Argon2id)
|
|
// - macOS Keychain with TouchID/Biometrics
|
|
// - Linux Secret Service (GNOME Keyring, KWallet)
|
|
// - Hardware security modules (Zymbit, Yubikey)
|
|
// - Remote signing via WalletConnect/QR codes
|
|
// - Ledger hardware wallet (optional)
|
|
package key
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// BackendType identifies the key storage backend
|
|
type BackendType string
|
|
|
|
const (
|
|
// BackendSoftware is the default encrypted file storage
|
|
BackendSoftware BackendType = "software"
|
|
|
|
// BackendKeychain uses macOS Keychain with optional TouchID
|
|
BackendKeychain BackendType = "keychain"
|
|
|
|
// BackendSecretService uses Linux Secret Service API (GNOME Keyring, KWallet)
|
|
BackendSecretService BackendType = "secret-service"
|
|
|
|
// BackendYubikey uses Yubikey for key storage/signing
|
|
BackendYubikey BackendType = "yubikey"
|
|
|
|
// BackendZymbit uses Zymbit HSM (Raspberry Pi hardware security)
|
|
BackendZymbit BackendType = "zymbit"
|
|
|
|
// BackendWalletConnect uses mobile wallet for remote signing
|
|
BackendWalletConnect BackendType = "walletconnect"
|
|
|
|
// BackendLedger uses Ledger hardware wallet (optional)
|
|
BackendLedger BackendType = "ledger"
|
|
|
|
// BackendEnv loads keys from environment variables
|
|
BackendEnv BackendType = "env"
|
|
)
|
|
|
|
var (
|
|
ErrBackendNotFound = errors.New("key backend not found")
|
|
ErrBackendNotSupported = errors.New("key backend not supported on this platform")
|
|
ErrBackendUnavailable = errors.New("key backend unavailable (check hardware/service)")
|
|
ErrSigningCancelled = errors.New("signing cancelled by user")
|
|
ErrAuthFailed = errors.New("authentication failed")
|
|
ErrKeyLocked = errors.New("key is locked, use 'lux key unlock' first")
|
|
ErrKeyNotFound = errors.New("key not found")
|
|
ErrInvalidPassword = errors.New("invalid password")
|
|
ErrKeyExists = errors.New("key already exists")
|
|
ErrNoPassword = errors.New("password required")
|
|
)
|
|
|
|
// KeyInfo represents information about a stored key
|
|
type KeyInfo struct {
|
|
Name string
|
|
Address string
|
|
NodeID string
|
|
Encrypted bool
|
|
Locked bool
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// SignRequest represents a transaction signing request
|
|
type SignRequest struct {
|
|
Type string // "transaction", "message", "auth"
|
|
ChainID uint64
|
|
Description string
|
|
Data []byte // Raw data to sign
|
|
DataHash [32]byte // Hash of data (for display)
|
|
}
|
|
|
|
// SignResponse contains the signature result
|
|
type SignResponse struct {
|
|
Signature []byte
|
|
PublicKey []byte
|
|
Address string
|
|
}
|
|
|
|
// KeyBackend defines the interface for all key storage backends
|
|
type KeyBackend interface {
|
|
// Type returns the backend type identifier
|
|
Type() BackendType
|
|
|
|
// Name returns a human-readable name
|
|
Name() string
|
|
|
|
// Available checks if this backend is available on the current system
|
|
Available() bool
|
|
|
|
// RequiresPassword returns true if password is needed
|
|
RequiresPassword() bool
|
|
|
|
// RequiresHardware returns true if hardware device is needed
|
|
RequiresHardware() bool
|
|
|
|
// SupportsRemoteSigning returns true if signing is done externally
|
|
SupportsRemoteSigning() bool
|
|
|
|
// Initialize sets up the backend (creates directories, connects to services, etc.)
|
|
Initialize(ctx context.Context) error
|
|
|
|
// Close cleans up resources
|
|
Close() error
|
|
|
|
// CreateKey creates a new key set with the given name
|
|
CreateKey(ctx context.Context, name string, opts CreateKeyOptions) (*HDKeySet, error)
|
|
|
|
// LoadKey loads a key set by name
|
|
LoadKey(ctx context.Context, name, password string) (*HDKeySet, error)
|
|
|
|
// SaveKey saves a key set
|
|
SaveKey(ctx context.Context, keySet *HDKeySet, password string) error
|
|
|
|
// DeleteKey removes a key
|
|
DeleteKey(ctx context.Context, name string) error
|
|
|
|
// ListKeys returns all available keys
|
|
ListKeys(ctx context.Context) ([]KeyInfo, error)
|
|
|
|
// Lock locks a key (clears from memory)
|
|
Lock(ctx context.Context, name string) error
|
|
|
|
// Unlock unlocks a key for use
|
|
Unlock(ctx context.Context, name, password string) error
|
|
|
|
// IsLocked checks if a key is locked
|
|
IsLocked(name string) bool
|
|
|
|
// Sign signs data with the specified key
|
|
Sign(ctx context.Context, name string, request SignRequest) (*SignResponse, error)
|
|
}
|
|
|
|
// CreateKeyOptions contains options for key creation
|
|
type CreateKeyOptions struct {
|
|
// Mnemonic is an optional existing mnemonic phrase
|
|
Mnemonic string
|
|
|
|
// Password for encryption (software backend)
|
|
Password string
|
|
|
|
// UseBiometrics enables TouchID/FaceID on macOS
|
|
UseBiometrics bool
|
|
|
|
// YubikeySlot specifies the PIV slot for Yubikey
|
|
YubikeySlot int
|
|
|
|
// ImportOnly indicates we're importing, not generating
|
|
ImportOnly bool
|
|
}
|
|
|
|
// backendRegistry holds all registered backends
|
|
var (
|
|
backendMu sync.RWMutex
|
|
backends = make(map[BackendType]KeyBackend)
|
|
defaultBackend BackendType
|
|
activeBackends = make(map[BackendType]KeyBackend)
|
|
)
|
|
|
|
// RegisterBackend registers a key backend
|
|
func RegisterBackend(b KeyBackend) {
|
|
backendMu.Lock()
|
|
defer backendMu.Unlock()
|
|
backends[b.Type()] = b
|
|
}
|
|
|
|
// GetBackend returns a backend by type
|
|
func GetBackend(t BackendType) (KeyBackend, error) {
|
|
backendMu.RLock()
|
|
defer backendMu.RUnlock()
|
|
|
|
b, ok := backends[t]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %s", ErrBackendNotFound, t)
|
|
}
|
|
|
|
if !b.Available() {
|
|
return nil, fmt.Errorf("%w: %s", ErrBackendNotSupported, t)
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// GetDefaultBackend returns the default backend for the current platform
|
|
func GetDefaultBackend() (KeyBackend, error) {
|
|
backendMu.RLock()
|
|
defer backendMu.RUnlock()
|
|
|
|
if defaultBackend != "" {
|
|
if b, ok := backends[defaultBackend]; ok && b.Available() {
|
|
return b, nil
|
|
}
|
|
}
|
|
|
|
// Platform-specific defaults
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
// Prefer Keychain on macOS
|
|
if b, ok := backends[BackendKeychain]; ok && b.Available() {
|
|
return b, nil
|
|
}
|
|
case "linux":
|
|
// Prefer Secret Service on Linux
|
|
if b, ok := backends[BackendSecretService]; ok && b.Available() {
|
|
return b, nil
|
|
}
|
|
}
|
|
|
|
// Fall back to software backend
|
|
if b, ok := backends[BackendSoftware]; ok {
|
|
return b, nil
|
|
}
|
|
|
|
return nil, ErrBackendNotFound
|
|
}
|
|
|
|
// SetDefaultBackend sets the default backend type
|
|
func SetDefaultBackend(t BackendType) error {
|
|
backendMu.Lock()
|
|
defer backendMu.Unlock()
|
|
|
|
if _, ok := backends[t]; !ok {
|
|
return fmt.Errorf("%w: %s", ErrBackendNotFound, t)
|
|
}
|
|
|
|
defaultBackend = t
|
|
return nil
|
|
}
|
|
|
|
// ListAvailableBackends returns all available backends
|
|
func ListAvailableBackends() []KeyBackend {
|
|
backendMu.RLock()
|
|
defer backendMu.RUnlock()
|
|
|
|
var available []KeyBackend
|
|
for _, b := range backends {
|
|
if b.Available() {
|
|
available = append(available, b)
|
|
}
|
|
}
|
|
return available
|
|
}
|
|
|
|
// BackendConfig holds configuration for backend initialization
|
|
type BackendConfig struct {
|
|
// DataDir is the base directory for key storage
|
|
DataDir string
|
|
|
|
// WalletConnectProjectID for WalletConnect backend
|
|
WalletConnectProjectID string
|
|
|
|
// ZymbitDevicePath for Zymbit HSM
|
|
ZymbitDevicePath string
|
|
|
|
// YubikeyPIN for Yubikey operations
|
|
YubikeyPIN string
|
|
}
|
|
|
|
// InitializeBackends initializes all available backends
|
|
func InitializeBackends(ctx context.Context, config BackendConfig) error {
|
|
backendMu.Lock()
|
|
defer backendMu.Unlock()
|
|
|
|
for t, b := range backends {
|
|
if b.Available() {
|
|
if err := b.Initialize(ctx); err != nil {
|
|
// Log warning but don't fail - some backends may be optional
|
|
fmt.Printf("Warning: failed to initialize %s backend: %v\n", t, err)
|
|
continue
|
|
}
|
|
activeBackends[t] = b
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CloseBackends closes all active backends
|
|
func CloseBackends() {
|
|
backendMu.Lock()
|
|
defer backendMu.Unlock()
|
|
|
|
for _, b := range activeBackends {
|
|
_ = b.Close()
|
|
}
|
|
activeBackends = make(map[BackendType]KeyBackend)
|
|
}
|
|
|
|
// SessionTimeout is the default session timeout for unlocked keys.
|
|
// This is exported for use in CLI commands to display the timeout value.
|
|
// The actual timeout is configurable via KEY_SESSION_TIMEOUT env var.
|
|
var SessionTimeout = DefaultSessionTimeout
|
|
|
|
// LockKey locks a key using the default backend
|
|
func LockKey(name string) error {
|
|
backend, err := GetDefaultBackend()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return backend.Lock(context.Background(), name)
|
|
}
|
|
|
|
// LockAllKeys locks all keys across all active backends
|
|
func LockAllKeys() {
|
|
backendMu.RLock()
|
|
defer backendMu.RUnlock()
|
|
|
|
for _, b := range activeBackends {
|
|
keys, err := b.ListKeys(context.Background())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, k := range keys {
|
|
_ = b.Lock(context.Background(), k.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// UnlockKey unlocks a key using the default backend
|
|
func UnlockKey(name, password string) error {
|
|
backend, err := GetDefaultBackend()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return backend.Unlock(context.Background(), name, password)
|
|
}
|
|
|
|
// IsKeyLocked checks if a key is locked using the default backend
|
|
func IsKeyLocked(name string) bool {
|
|
backend, err := GetDefaultBackend()
|
|
if err != nil {
|
|
return true
|
|
}
|
|
return backend.IsLocked(name)
|
|
}
|
|
|
|
// GetPasswordFromEnv returns the password from the KEY_PASSWORD environment variable
|
|
func GetPasswordFromEnv() string {
|
|
return os.Getenv(EnvKeyPassword)
|
|
}
|