Files
cli/pkg/key/backend_software.go
zeekay 65f575285e cli: rename Corona → Corona (production R-LWE threshold);
LSAG ring-signature key → RingSig (descriptive, not Corona-lemur)

Two distinct concepts that shared the "Corona" name in CLI code are
now disambiguated:

  1. Production R-LWE threshold signature library — was "Corona",
     now Corona. Affects consensus / threshold-sign code paths,
     genesis precompile entry, threshold-scheme name, NTT references.

  2. LSAG ring-signature key derivation (independent feature, named
     after the lemur not the threshold sig) — now `RingSig`.
     Affects NewCoronaKeyBytes → NewRingSigKeyBytes,
     ToCoronaPublicKey → ToRingSigPublicKey, the keyset field
     `CoronaPrivateKey/PublicKey` → `RingSigPrivateKey/PublicKey`,
     JSON tag `corona_private_key` → `ringsig_private_key`, and
     the user-facing prose around the `lux key ring` command.

The `rt` subcommand is wired to github.com/luxfi/corona/cli (the
production library), via go.mod bump to luxfi/corona v0.2.1.

No "Corona" identifier remains in production CLI code or docs.
2026-05-12 09:07:44 -07:00

657 lines
16 KiB
Go

// Copyright (C) 2022-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package key
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"github.com/luxfi/crypto/secp256k1"
"golang.org/x/crypto/argon2"
)
// SoftwareBackend implements encrypted file-based key storage
type SoftwareBackend struct {
dataDir string
sessions map[string]*keySession
sessionMu sync.RWMutex
sessionTimeout time.Duration // Configurable session timeout
}
type keySession struct {
name string
key []byte
unlockedAt time.Time
expiresAt time.Time
mlocked bool // Whether the key memory is locked
}
// Argon2id parameters (OWASP recommended for password hashing)
const (
argon2Time = 3 // iterations
argon2Memory = 64 * 1024 // 64 MB
argon2Threads = 4
argon2KeyLen = 32
// DefaultSessionTimeout is the inactivity timeout for unlocked keys.
// After this duration without access, the key is automatically locked.
// Can be overridden via KEY_SESSION_TIMEOUT environment variable.
DefaultSessionTimeout = 30 * time.Second
)
// NewSoftwareBackend creates a new software-based key backend
func NewSoftwareBackend() *SoftwareBackend {
return &SoftwareBackend{
sessions: make(map[string]*keySession),
sessionTimeout: GetSessionTimeout(),
}
}
// GetSessionTimeout returns the configured session timeout.
// Checks KEY_SESSION_TIMEOUT environment variable first,
// otherwise returns DefaultSessionTimeout (30 seconds).
func GetSessionTimeout() time.Duration {
if envTimeout := os.Getenv(EnvKeySessionTimeout); envTimeout != "" {
if d, err := time.ParseDuration(envTimeout); err == nil && d > 0 {
return d
}
}
return DefaultSessionTimeout
}
// SetSessionTimeout sets the session timeout for this backend.
// The timeout resets on each key access (sliding window).
func (b *SoftwareBackend) SetSessionTimeout(d time.Duration) {
b.sessionMu.Lock()
defer b.sessionMu.Unlock()
if d > 0 {
b.sessionTimeout = d
}
}
func (*SoftwareBackend) Type() BackendType {
return BackendSoftware
}
func (*SoftwareBackend) Name() string {
return "Encrypted File Storage"
}
func (*SoftwareBackend) Available() bool {
return true // Always available
}
func (*SoftwareBackend) RequiresPassword() bool {
return true
}
func (*SoftwareBackend) RequiresHardware() bool {
return false
}
func (*SoftwareBackend) SupportsRemoteSigning() bool {
return false
}
func (b *SoftwareBackend) Initialize(ctx context.Context) error {
if b.dataDir == "" {
keysDir, err := GetKeysDir()
if err != nil {
return err
}
b.dataDir = keysDir
}
return os.MkdirAll(b.dataDir, 0o700)
}
func (b *SoftwareBackend) Close() error {
b.sessionMu.Lock()
defer b.sessionMu.Unlock()
// Securely clear all session keys
for _, s := range b.sessions {
clearSession(s)
}
b.sessions = make(map[string]*keySession)
return nil
}
func (b *SoftwareBackend) CreateKey(ctx context.Context, name string, opts CreateKeyOptions) (*HDKeySet, error) {
keyDir := filepath.Join(b.dataDir, name)
// Check if key already exists
if _, err := os.Stat(keyDir); err == nil {
return nil, ErrKeyExists
}
// Generate or import mnemonic
var mnemonic string
if opts.Mnemonic != "" {
if !ValidateMnemonic(opts.Mnemonic) {
return nil, errors.New("invalid mnemonic phrase")
}
mnemonic = opts.Mnemonic
} else {
var err error
mnemonic, err = GenerateMnemonic()
if err != nil {
return nil, fmt.Errorf("failed to generate mnemonic: %w", err)
}
}
// Derive all keys from mnemonic
keySet, err := DeriveAllKeys(name, mnemonic)
if err != nil {
return nil, fmt.Errorf("failed to derive keys: %w", err)
}
// Save encrypted
if err := b.SaveKey(ctx, keySet, opts.Password); err != nil {
return nil, err
}
return keySet, nil
}
func (b *SoftwareBackend) LoadKey(ctx context.Context, name, password string) (*HDKeySet, error) {
// Check for active session
if session := b.getSession(name); session != nil {
return b.loadWithKey(name, session.key)
}
// Need password
if password == "" {
password = os.Getenv(EnvKeyPassword)
if password == "" {
return nil, ErrKeyLocked
}
}
keyDir := filepath.Join(b.dataDir, name)
encPath := filepath.Join(keyDir, "keystore.enc")
data, err := os.ReadFile(encPath) //nolint:gosec // G304: Reading from user's key directory
if err != nil {
if os.IsNotExist(err) {
return nil, ErrKeyNotFound
}
return nil, fmt.Errorf("failed to read keystore: %w", err)
}
var store encryptedStore
if err := json.Unmarshal(data, &store); err != nil {
return nil, fmt.Errorf("failed to parse keystore: %w", err)
}
// Derive encryption key
encKey := argon2.IDKey([]byte(password), store.Salt, argon2Time, argon2Memory, argon2Threads, argon2KeyLen)
// Decrypt
plaintext, err := decryptAESGCM(encKey, store.Nonce, store.Data)
if err != nil {
// Zero key on failure
for i := range encKey {
encKey[i] = 0
}
return nil, ErrInvalidPassword
}
// Store session
b.setSession(name, encKey)
return parseKeySetJSON(plaintext)
}
func (b *SoftwareBackend) SaveKey(ctx context.Context, keySet *HDKeySet, password string) error {
if password == "" {
return ErrNoPassword
}
keyDir := filepath.Join(b.dataDir, keySet.Name)
if err := os.MkdirAll(keyDir, 0o700); err != nil {
return fmt.Errorf("failed to create key directory: %w", err)
}
// Generate salt
salt := make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
return fmt.Errorf("failed to generate salt: %w", err)
}
// Derive encryption key
encKey := argon2.IDKey([]byte(password), salt, argon2Time, argon2Memory, argon2Threads, argon2KeyLen)
defer func() {
for i := range encKey {
encKey[i] = 0
}
}()
// Serialize key set
plaintext, err := serializeKeySet(keySet)
if err != nil {
return fmt.Errorf("failed to serialize keys: %w", err)
}
defer func() {
for i := range plaintext {
plaintext[i] = 0
}
}()
// Encrypt
nonce, ciphertext, err := encryptAESGCM(encKey, plaintext)
if err != nil {
return fmt.Errorf("failed to encrypt: %w", err)
}
store := encryptedStore{
Version: 1,
Salt: salt,
Nonce: nonce,
Data: ciphertext,
CreatedAt: time.Now().Unix(),
}
storeData, err := json.Marshal(store)
if err != nil {
return fmt.Errorf("failed to marshal store: %w", err)
}
// Write encrypted keystore
encPath := filepath.Join(keyDir, "keystore.enc")
if err := os.WriteFile(encPath, storeData, 0o600); err != nil {
return fmt.Errorf("failed to write keystore: %w", err)
}
// Write public info (viewable without unlock)
pubInfo := map[string]interface{}{
"name": keySet.Name,
"ec_address": keySet.ECAddress,
"node_id": keySet.NodeID,
"created_at": time.Now().Format(time.RFC3339),
"backend": string(BackendSoftware),
}
pubData, _ := json.MarshalIndent(pubInfo, "", " ")
_ = os.WriteFile(filepath.Join(keyDir, "info.json"), pubData, 0o644) //nolint:gosec // G306: Public info file needs to be readable
return nil
}
func (b *SoftwareBackend) DeleteKey(ctx context.Context, name string) error {
keyDir := filepath.Join(b.dataDir, name)
// Remove session
b.sessionMu.Lock()
if s, ok := b.sessions[name]; ok {
clearSession(s)
delete(b.sessions, name)
}
b.sessionMu.Unlock()
return os.RemoveAll(keyDir)
}
func (b *SoftwareBackend) ListKeys(ctx context.Context) ([]KeyInfo, error) {
entries, err := os.ReadDir(b.dataDir)
if err != nil {
if os.IsNotExist(err) {
return []KeyInfo{}, nil
}
return nil, err
}
keys := make([]KeyInfo, 0, len(entries))
for _, entry := range entries {
if !entry.IsDir() {
continue
}
name := entry.Name()
keyDir := filepath.Join(b.dataDir, name)
info := KeyInfo{
Name: name,
Encrypted: true,
Locked: b.IsLocked(name),
}
// Read public info
pubPath := filepath.Join(keyDir, "info.json")
if data, err := os.ReadFile(pubPath); err == nil { //nolint:gosec // G304: Reading from user's key directory
var pubInfo struct {
ECAddress string `json:"ec_address"`
NodeID string `json:"node_id"`
CreatedAt string `json:"created_at"`
}
if json.Unmarshal(data, &pubInfo) == nil {
info.Address = pubInfo.ECAddress
info.NodeID = pubInfo.NodeID
if t, err := time.Parse(time.RFC3339, pubInfo.CreatedAt); err == nil {
info.CreatedAt = t
}
}
}
keys = append(keys, info)
}
return keys, nil
}
func (b *SoftwareBackend) Lock(ctx context.Context, name string) error {
b.sessionMu.Lock()
defer b.sessionMu.Unlock()
if s, ok := b.sessions[name]; ok {
clearSession(s)
delete(b.sessions, name)
}
return nil
}
func (b *SoftwareBackend) Unlock(ctx context.Context, name, password string) error {
_, err := b.LoadKey(ctx, name, password)
return err
}
func (b *SoftwareBackend) IsLocked(name string) bool {
return b.getSession(name) == nil
}
func (b *SoftwareBackend) Sign(ctx context.Context, name string, request SignRequest) (*SignResponse, error) {
keySet, err := b.LoadKey(ctx, name, "")
if err != nil {
return nil, err
}
// Sign based on key type needed
privKey, err := secp256k1.ToPrivateKey(keySet.ECPrivateKey)
if err != nil {
return nil, fmt.Errorf("failed to load private key: %w", err)
}
sig, err := privKey.Sign(request.DataHash[:])
if err != nil {
return nil, fmt.Errorf("failed to sign: %w", err)
}
return &SignResponse{
Signature: sig,
PublicKey: privKey.PublicKey().Bytes(),
Address: keySet.ECAddress,
}, nil
}
// Helper methods
func (b *SoftwareBackend) getSession(name string) *keySession {
b.sessionMu.Lock()
defer b.sessionMu.Unlock()
s, ok := b.sessions[name]
if !ok {
return nil
}
if time.Now().After(s.expiresAt) {
// Session expired - clear it securely
clearSession(s)
delete(b.sessions, name)
return nil
}
// Extend on access (sliding window)
s.expiresAt = time.Now().Add(b.sessionTimeout)
return s
}
func (b *SoftwareBackend) setSession(name string, key []byte) {
b.sessionMu.Lock()
defer b.sessionMu.Unlock()
// Clear existing session if present
if existing, ok := b.sessions[name]; ok {
clearSession(existing)
}
// Attempt to lock the key memory to prevent swapping
mlocked := false
if err := mlock(key); err == nil {
mlocked = true
}
b.sessions[name] = &keySession{
name: name,
key: key,
unlockedAt: time.Now(),
expiresAt: time.Now().Add(b.sessionTimeout),
mlocked: mlocked,
}
}
// clearSession securely clears a session, zeroing the key and unlocking memory.
func clearSession(s *keySession) {
if s == nil {
return
}
// Unlock memory before zeroing
if s.mlocked {
_ = munlock(s.key)
}
// Zero out the key
for i := range s.key {
s.key[i] = 0
}
}
func (b *SoftwareBackend) loadWithKey(name string, encKey []byte) (*HDKeySet, error) {
keyDir := filepath.Join(b.dataDir, name)
encPath := filepath.Join(keyDir, "keystore.enc")
data, err := os.ReadFile(encPath) //nolint:gosec // G304: Reading from user's key directory
if err != nil {
return nil, fmt.Errorf("failed to read keystore: %w", err)
}
var store encryptedStore
if err := json.Unmarshal(data, &store); err != nil {
return nil, fmt.Errorf("failed to parse keystore: %w", err)
}
plaintext, err := decryptAESGCM(encKey, store.Nonce, store.Data)
if err != nil {
return nil, ErrInvalidPassword
}
return parseKeySetJSON(plaintext)
}
// Encryption helpers
type encryptedStore struct {
Version int `json:"version"`
Salt []byte `json:"salt"`
Nonce []byte `json:"nonce"`
Data []byte `json:"data"`
CreatedAt int64 `json:"created_at"`
}
func encryptAESGCM(key, plaintext []byte) (nonce, ciphertext []byte, err error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, err
}
nonce = make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, nil, err
}
ciphertext = gcm.Seal(nil, nonce, plaintext, nil)
return nonce, ciphertext, nil
}
func decryptAESGCM(key, nonce, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
return gcm.Open(nil, nonce, ciphertext, nil)
}
func serializeKeySet(ks *HDKeySet) ([]byte, error) {
data := struct {
Name string `json:"name"`
ECPrivateKey string `json:"ec_private_key"`
ECPublicKey string `json:"ec_public_key"`
ECAddress string `json:"ec_address"`
BLSPrivateKey string `json:"bls_private_key"`
BLSPublicKey string `json:"bls_public_key"`
BLSPoP string `json:"bls_pop"`
RingSigPrivateKey string `json:"ringsig_private_key"`
RingSigPublicKey string `json:"ringsig_public_key"`
MLDSAPrivateKey string `json:"mldsa_private_key"`
MLDSAPublicKey string `json:"mldsa_public_key"`
StakingKeyPEM string `json:"staking_key_pem"`
StakingCertPEM string `json:"staking_cert_pem"`
NodeID string `json:"node_id"`
}{
Name: ks.Name,
ECPrivateKey: hex.EncodeToString(ks.ECPrivateKey),
ECPublicKey: hex.EncodeToString(ks.ECPublicKey),
ECAddress: ks.ECAddress,
BLSPrivateKey: hex.EncodeToString(ks.BLSPrivateKey),
BLSPublicKey: hex.EncodeToString(ks.BLSPublicKey),
BLSPoP: hex.EncodeToString(ks.BLSPoP),
RingSigPrivateKey: hex.EncodeToString(ks.RingSigPrivateKey),
RingSigPublicKey: hex.EncodeToString(ks.RingSigPublicKey),
MLDSAPrivateKey: hex.EncodeToString(ks.MLDSAPrivateKey),
MLDSAPublicKey: hex.EncodeToString(ks.MLDSAPublicKey),
StakingKeyPEM: string(ks.StakingKeyPEM),
StakingCertPEM: string(ks.StakingCertPEM),
NodeID: ks.NodeID,
}
return json.Marshal(data)
}
func parseKeySetJSON(data []byte) (*HDKeySet, error) {
defer func() {
for i := range data {
data[i] = 0
}
}()
var raw struct {
Name string `json:"name"`
ECPrivateKey string `json:"ec_private_key"`
ECPublicKey string `json:"ec_public_key"`
ECAddress string `json:"ec_address"`
BLSPrivateKey string `json:"bls_private_key"`
BLSPublicKey string `json:"bls_public_key"`
BLSPoP string `json:"bls_pop"`
RingSigPrivateKey string `json:"ringsig_private_key"`
RingSigPublicKey string `json:"ringsig_public_key"`
MLDSAPrivateKey string `json:"mldsa_private_key"`
MLDSAPublicKey string `json:"mldsa_public_key"`
StakingKeyPEM string `json:"staking_key_pem"`
StakingCertPEM string `json:"staking_cert_pem"`
NodeID string `json:"node_id"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
ks := &HDKeySet{
Name: raw.Name,
ECAddress: raw.ECAddress,
StakingKeyPEM: []byte(raw.StakingKeyPEM),
StakingCertPEM: []byte(raw.StakingCertPEM),
NodeID: raw.NodeID,
}
var err error
ks.ECPrivateKey, err = hex.DecodeString(raw.ECPrivateKey)
if err != nil {
return nil, fmt.Errorf("decode ec private key: %w", err)
}
ks.ECPublicKey, err = hex.DecodeString(raw.ECPublicKey)
if err != nil {
return nil, fmt.Errorf("decode ec public key: %w", err)
}
ks.BLSPrivateKey, err = hex.DecodeString(raw.BLSPrivateKey)
if err != nil {
return nil, fmt.Errorf("decode bls private key: %w", err)
}
ks.BLSPublicKey, err = hex.DecodeString(raw.BLSPublicKey)
if err != nil {
return nil, fmt.Errorf("decode bls public key: %w", err)
}
ks.BLSPoP, err = hex.DecodeString(raw.BLSPoP)
if err != nil {
return nil, fmt.Errorf("decode bls pop: %w", err)
}
ks.RingSigPrivateKey, err = hex.DecodeString(raw.RingSigPrivateKey)
if err != nil {
return nil, fmt.Errorf("decode corona private key: %w", err)
}
ks.RingSigPublicKey, err = hex.DecodeString(raw.RingSigPublicKey)
if err != nil {
return nil, fmt.Errorf("decode corona public key: %w", err)
}
ks.MLDSAPrivateKey, err = hex.DecodeString(raw.MLDSAPrivateKey)
if err != nil {
return nil, fmt.Errorf("decode mldsa private key: %w", err)
}
ks.MLDSAPublicKey, err = hex.DecodeString(raw.MLDSAPublicKey)
if err != nil {
return nil, fmt.Errorf("decode mldsa public key: %w", err)
}
return ks, nil
}
// GetKeyChecksum returns a checksum for key verification
func (b *SoftwareBackend) GetKeyChecksum(name string) (string, error) {
session := b.getSession(name)
if session == nil {
return "", ErrKeyLocked
}
ks, err := b.loadWithKey(name, session.key)
if err != nil {
return "", err
}
h := sha256.New()
h.Write(ks.ECPrivateKey)
h.Write(ks.BLSPrivateKey)
return hex.EncodeToString(h.Sum(nil)[:8]), nil
}
func init() {
RegisterBackend(NewSoftwareBackend())
}