mirror of
https://github.com/luxfi/sdk.git
synced 2026-10-30 15:19:23 +00:00
Lux Partners Limited is not the entity behind this work. Every place the old name stood — copyright lines, site footers, brand config, legal and compliance text — now names the company that actually holds it. lux.fund is the exception and is not renamed to match: that site belongs to Lux Assets Management LLC, a different company, and it says so. Two kinds of line were left for a person rather than rewritten. A dated news item recording that Lux Partners Limited was established in the Isle of Man is a historical fact, and renaming it would falsify the record. And a sentence asserting that the company is domiciled in the Isle of Man is a claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle of Man company, so swapping the name there would assert something untrue. Co-authored-by: Hanzo Dev <[email protected]>
514 lines
14 KiB
Go
514 lines
14 KiB
Go
// Copyright (C) 2020-2025, Lux Industries Inc All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package key provides cryptographic key generation and management.
|
|
package key
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/luxfi/crypto/bls"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/sdk/crypto"
|
|
)
|
|
|
|
// Key represents a cryptographic key with metadata
|
|
type Key struct {
|
|
ID ids.ID `json:"id"`
|
|
Type string `json:"type"` // "ed25519", "bls", "secp256k1"
|
|
PrivateKey crypto.PrivateKey `json:"-"`
|
|
PublicKey crypto.PublicKey `json:"publicKey"`
|
|
Address ids.ShortID `json:"address"`
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// generateAddress generates an address from a public key
|
|
func generateAddress(pubKey crypto.PublicKey) ids.ShortID {
|
|
addr := ids.ShortID{}
|
|
copy(addr[:], pubKey[:20])
|
|
return addr
|
|
}
|
|
|
|
// Keychain manages a collection of private keys for signing
|
|
type Keychain struct {
|
|
keys map[ids.ShortID]crypto.PrivateKey
|
|
}
|
|
|
|
// NewKeychain creates a new keychain
|
|
func NewKeychain() *Keychain {
|
|
return &Keychain{
|
|
keys: make(map[ids.ShortID]crypto.PrivateKey),
|
|
}
|
|
}
|
|
|
|
// Add adds a private key to the keychain
|
|
func (kc *Keychain) Add(privateKey crypto.PrivateKey) error {
|
|
address := generateAddress(privateKey.PublicKey())
|
|
if _, exists := kc.keys[address]; exists {
|
|
return fmt.Errorf("key already exists for address %s", address)
|
|
}
|
|
kc.keys[address] = privateKey
|
|
return nil
|
|
}
|
|
|
|
// Sign signs a message with the key for the given address
|
|
func (kc *Keychain) Sign(message []byte, address ids.ShortID) (crypto.Signature, error) {
|
|
privateKey, exists := kc.keys[address]
|
|
if !exists {
|
|
return crypto.EmptySignature, fmt.Errorf("no key for address %s", address)
|
|
}
|
|
return crypto.Sign(message, privateKey), nil
|
|
}
|
|
|
|
// Get retrieves a private key by address
|
|
func (kc *Keychain) Get(address ids.ShortID) (crypto.PrivateKey, error) {
|
|
privateKey, exists := kc.keys[address]
|
|
if !exists {
|
|
return crypto.EmptyPrivateKey, fmt.Errorf("no key for address %s", address)
|
|
}
|
|
return privateKey, nil
|
|
}
|
|
|
|
// Addresses returns all addresses in the keychain
|
|
func (kc *Keychain) Addresses() []ids.ShortID {
|
|
addresses := make([]ids.ShortID, 0, len(kc.keys))
|
|
for addr := range kc.keys {
|
|
addresses = append(addresses, addr)
|
|
}
|
|
return addresses
|
|
}
|
|
|
|
// GenerateMnemonic generates a mnemonic phrase
|
|
func GenerateMnemonic(bitSize int) ([]string, error) {
|
|
// Simple mock implementation for testing
|
|
// In production, use a proper BIP39 implementation
|
|
words := []string{
|
|
"abandon", "ability", "able", "about", "above", "absent",
|
|
"absorb", "abstract", "absurd", "abuse", "access", "accident",
|
|
}
|
|
|
|
switch bitSize {
|
|
case 128:
|
|
return words[:12], nil
|
|
case 256:
|
|
return append(words, words...)[:24], nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported bit size: %d", bitSize)
|
|
}
|
|
}
|
|
|
|
// DeriveKey derives a key from a mnemonic at the given index
|
|
func DeriveKey(mnemonic []string, index uint32) (crypto.PrivateKey, error) {
|
|
// Simple mock implementation for testing
|
|
// In production, use proper BIP32/BIP44 derivation
|
|
|
|
// Generate a deterministic key based on mnemonic and index
|
|
seed := fmt.Sprintf("%v-%d", mnemonic, index)
|
|
|
|
// Create a deterministic private key (for testing only!)
|
|
// Use a simple hash to ensure valid key
|
|
h := [64]byte{}
|
|
copy(h[:], []byte(seed))
|
|
|
|
// Ensure it's different for different indices
|
|
h[0] = byte(index)
|
|
h[1] = byte(index >> 8)
|
|
h[2] = byte(index >> 16)
|
|
h[3] = byte(index >> 24)
|
|
|
|
var privKey crypto.PrivateKey
|
|
copy(privKey[:], h[:crypto.PrivateKeyLen])
|
|
|
|
return privKey, nil
|
|
}
|
|
|
|
// Manager handles key generation, storage, and retrieval
|
|
type Manager struct {
|
|
keyDir string
|
|
keys map[ids.ID]*Key
|
|
}
|
|
|
|
// NewManager creates a new key manager
|
|
func NewManager(keyDir string) (*Manager, error) {
|
|
if err := os.MkdirAll(keyDir, 0700); err != nil {
|
|
return nil, fmt.Errorf("failed to create key directory: %w", err)
|
|
}
|
|
|
|
m := &Manager{
|
|
keyDir: keyDir,
|
|
keys: make(map[ids.ID]*Key),
|
|
}
|
|
|
|
// Load existing keys
|
|
if err := m.loadKeys(); err != nil {
|
|
return nil, fmt.Errorf("failed to load keys: %w", err)
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// GenerateEd25519 generates a new Ed25519 key
|
|
func (m *Manager) GenerateEd25519() (*Key, error) {
|
|
privateKey, err := crypto.GeneratePrivateKey()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate private key: %w", err)
|
|
}
|
|
|
|
key := &Key{
|
|
ID: ids.GenerateTestID(),
|
|
Type: "ed25519",
|
|
PrivateKey: privateKey,
|
|
PublicKey: privateKey.PublicKey(),
|
|
Address: generateAddress(privateKey.PublicKey()),
|
|
Metadata: make(map[string]string),
|
|
}
|
|
|
|
m.keys[key.ID] = key
|
|
return key, nil
|
|
}
|
|
|
|
// GenerateBLS generates a new BLS key
|
|
func (m *Manager) GenerateBLS() (*Key, *bls.SecretKey, error) {
|
|
blsKey, err := bls.NewSecretKey()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to generate BLS key: %w", err)
|
|
}
|
|
|
|
// Create a wrapper key
|
|
pubKey := bls.PublicFromSecretKey(blsKey)
|
|
pubKeyBytes := bls.PublicKeyToUncompressedBytes(pubKey)
|
|
|
|
// Generate ID from public key
|
|
keyID := ids.ID{}
|
|
copy(keyID[:], pubKeyBytes)
|
|
|
|
key := &Key{
|
|
ID: keyID,
|
|
Type: "bls",
|
|
Metadata: make(map[string]string),
|
|
}
|
|
|
|
// Store BLS public key info in metadata
|
|
key.Metadata["blsPublicKey"] = hex.EncodeToString(pubKeyBytes)
|
|
|
|
m.keys[key.ID] = key
|
|
return key, blsKey, nil
|
|
}
|
|
|
|
// GenerateKey generates a new key of the specified type
|
|
func (m *Manager) GenerateKey(keyType string) (*Key, error) {
|
|
switch keyType {
|
|
case "ed25519":
|
|
return m.GenerateEd25519()
|
|
case "bls":
|
|
key, blsKey, err := m.GenerateBLS()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Generate address from BLS public key
|
|
pubKey := bls.PublicFromSecretKey(blsKey)
|
|
pubKeyBytes := bls.PublicKeyToUncompressedBytes(pubKey)
|
|
key.Address = generateAddress(crypto.PublicKey(pubKeyBytes[:crypto.PublicKeyLen]))
|
|
return key, err
|
|
default:
|
|
return nil, fmt.Errorf("unsupported key type: %s", keyType)
|
|
}
|
|
}
|
|
|
|
// ImportPrivateKey imports an existing private key
|
|
func (m *Manager) ImportPrivateKey(privateKey crypto.PrivateKey) (*Key, error) {
|
|
key := &Key{
|
|
ID: ids.GenerateTestID(),
|
|
Type: "ed25519",
|
|
PrivateKey: privateKey,
|
|
PublicKey: privateKey.PublicKey(),
|
|
Address: generateAddress(privateKey.PublicKey()),
|
|
Metadata: make(map[string]string),
|
|
}
|
|
|
|
m.keys[key.ID] = key
|
|
return key, nil
|
|
}
|
|
|
|
// ImportKey imports a key with the given private key and type
|
|
func (m *Manager) ImportKey(privateKey crypto.PrivateKey, keyType string) (*Key, error) {
|
|
if keyType != "ed25519" {
|
|
return nil, fmt.Errorf("unsupported key type for import: %s", keyType)
|
|
}
|
|
return m.ImportPrivateKey(privateKey)
|
|
}
|
|
|
|
// GetKey retrieves a key by ID
|
|
func (m *Manager) GetKey(keyID ids.ID) (*Key, error) {
|
|
return m.Get(keyID)
|
|
}
|
|
|
|
// SaveKey saves a key to disk
|
|
func (m *Manager) SaveKey(key *Key) error {
|
|
m.keys[key.ID] = key
|
|
return m.Save(key.ID)
|
|
}
|
|
|
|
// DeleteKey removes a key from the manager
|
|
func (m *Manager) DeleteKey(keyID ids.ID) error {
|
|
return m.Delete(keyID)
|
|
}
|
|
|
|
// ListKeys returns all keys
|
|
func (m *Manager) ListKeys() []*Key {
|
|
return m.List()
|
|
}
|
|
|
|
// ExportKey exports a key's private key as hex string
|
|
func (m *Manager) ExportKey(keyID ids.ID) (string, error) {
|
|
key, err := m.Get(keyID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if key.Type != "ed25519" || key.PrivateKey == crypto.EmptyPrivateKey {
|
|
return "", fmt.Errorf("key does not have exportable private key")
|
|
}
|
|
|
|
return hex.EncodeToString(key.PrivateKey[:]), nil
|
|
}
|
|
|
|
// Get retrieves a key by ID
|
|
func (m *Manager) Get(keyID ids.ID) (*Key, error) {
|
|
key, exists := m.keys[keyID]
|
|
if !exists {
|
|
return nil, errors.New("key not found")
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
// GetByAddress retrieves a key by address
|
|
func (m *Manager) GetByAddress(address ids.ShortID) (*Key, error) {
|
|
for _, key := range m.keys {
|
|
if key.Address == address {
|
|
return key, nil
|
|
}
|
|
}
|
|
return nil, errors.New("key not found for address")
|
|
}
|
|
|
|
// List returns all keys
|
|
func (m *Manager) List() []*Key {
|
|
keys := make([]*Key, 0, len(m.keys))
|
|
for _, key := range m.keys {
|
|
keys = append(keys, key)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// Delete removes a key
|
|
func (m *Manager) Delete(keyID ids.ID) error {
|
|
if _, exists := m.keys[keyID]; !exists {
|
|
return errors.New("key not found")
|
|
}
|
|
|
|
delete(m.keys, keyID)
|
|
|
|
// Delete from disk
|
|
keyFile := filepath.Join(m.keyDir, fmt.Sprintf("%s.key", keyID))
|
|
if err := os.Remove(keyFile); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to delete key file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Save persists a key to disk
|
|
func (m *Manager) Save(keyID ids.ID) error {
|
|
key, exists := m.keys[keyID]
|
|
if !exists {
|
|
return errors.New("key not found")
|
|
}
|
|
|
|
keyFile := filepath.Join(m.keyDir, fmt.Sprintf("%s.key", keyID))
|
|
|
|
// Serialize key (excluding private key for security)
|
|
data, err := json.MarshalIndent(key, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal key: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(keyFile, data, 0600); err != nil {
|
|
return fmt.Errorf("failed to write key file: %w", err)
|
|
}
|
|
|
|
// Save private key separately (encrypted in production)
|
|
privKeyFile := filepath.Join(m.keyDir, fmt.Sprintf("%s.priv", keyID))
|
|
if key.Type == "ed25519" && key.PrivateKey != crypto.EmptyPrivateKey {
|
|
privKeyData := key.PrivateKey[:]
|
|
if err := os.WriteFile(privKeyFile, privKeyData, 0600); err != nil {
|
|
return fmt.Errorf("failed to write private key file: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SaveAll persists all keys to disk
|
|
func (m *Manager) SaveAll() error {
|
|
for keyID := range m.keys {
|
|
if err := m.Save(keyID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// loadKeys loads all keys from disk
|
|
func (m *Manager) loadKeys() error {
|
|
entries, err := os.ReadDir(m.keyDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil // No keys yet
|
|
}
|
|
return err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if filepath.Ext(entry.Name()) != ".key" {
|
|
continue
|
|
}
|
|
|
|
keyFile := filepath.Join(m.keyDir, entry.Name())
|
|
data, err := os.ReadFile(keyFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read key file %s: %w", entry.Name(), err)
|
|
}
|
|
|
|
var key Key
|
|
if err := json.Unmarshal(data, &key); err != nil {
|
|
return fmt.Errorf("failed to unmarshal key %s: %w", entry.Name(), err)
|
|
}
|
|
|
|
// Try to load private key
|
|
privKeyFile := filepath.Join(m.keyDir, fmt.Sprintf("%s.priv", key.ID))
|
|
if privKeyData, err := os.ReadFile(privKeyFile); err == nil && key.Type == "ed25519" {
|
|
// Reconstruct private key (simplified - in production this would be encrypted)
|
|
if len(privKeyData) == crypto.PrivateKeyLen {
|
|
key.PrivateKey = crypto.PrivateKey(privKeyData)
|
|
} else {
|
|
// Skip this key, it has invalid private key data
|
|
continue
|
|
}
|
|
}
|
|
|
|
m.keys[key.ID] = &key
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ExportKey exports a key in various formats
|
|
func ExportKey(key *Key, format string, writer io.Writer) error {
|
|
switch format {
|
|
case "json":
|
|
encoder := json.NewEncoder(writer)
|
|
encoder.SetIndent("", " ")
|
|
return encoder.Encode(key)
|
|
case "hex":
|
|
if key.Type != "ed25519" || key.PrivateKey == crypto.EmptyPrivateKey {
|
|
return errors.New("no private key to export")
|
|
}
|
|
_, err := writer.Write([]byte(hex.EncodeToString(key.PrivateKey[:])))
|
|
return err
|
|
default:
|
|
return fmt.Errorf("unsupported export format: %s", format)
|
|
}
|
|
}
|
|
|
|
// LoadSoft loads a soft key from a file path
|
|
func LoadSoft(networkID uint32, keyPath string) (*Key, error) {
|
|
keyBytes, err := os.ReadFile(keyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read key file: %w", err)
|
|
}
|
|
|
|
// Try to parse as hex-encoded private key (64 chars)
|
|
keyStr := strings.TrimSpace(string(keyBytes))
|
|
|
|
// Strip prefix if present (like "PrivateKey-...")
|
|
// TrimPrefix returns the original string if prefix is not found
|
|
const privKeyPrefix = "PrivateKey-"
|
|
keyStr = strings.TrimPrefix(keyStr, privKeyPrefix)
|
|
// TODO: decode CB58 format if needed
|
|
|
|
// Try hex decoding
|
|
privateKeyBytes, err := hex.DecodeString(keyStr)
|
|
if err == nil && len(privateKeyBytes) == 32 {
|
|
// This is a valid 32-byte private key
|
|
var privateKey crypto.PrivateKey
|
|
copy(privateKey[:], privateKeyBytes)
|
|
|
|
// For compatibility, pad to 64 bytes if needed (seed + public key format)
|
|
if len(privateKey) == 32 {
|
|
// Generate public key from private key seed
|
|
publicKey := privateKey.PublicKey()
|
|
fullKey := append(privateKeyBytes, publicKey[:]...)
|
|
copy(privateKey[:], fullKey[:crypto.PrivateKeyLen])
|
|
}
|
|
|
|
return &Key{
|
|
Type: "ed25519",
|
|
PrivateKey: privateKey,
|
|
PublicKey: privateKey.PublicKey(),
|
|
Address: generateAddress(privateKey.PublicKey()),
|
|
}, nil
|
|
}
|
|
|
|
// Try JSON format
|
|
var keyData struct {
|
|
PrivateKey string `json:"privateKey"`
|
|
Type string `json:"type"`
|
|
}
|
|
if err := json.Unmarshal(keyBytes, &keyData); err == nil {
|
|
privateKeyBytes, err := hex.DecodeString(keyData.PrivateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode private key from JSON: %w", err)
|
|
}
|
|
|
|
var privateKey crypto.PrivateKey
|
|
if len(privateKeyBytes) == 32 {
|
|
// Seed only, need to expand
|
|
copy(privateKey[:32], privateKeyBytes)
|
|
publicKey := privateKey.PublicKey()
|
|
copy(privateKey[32:], publicKey[:])
|
|
} else if len(privateKeyBytes) == crypto.PrivateKeyLen {
|
|
copy(privateKey[:], privateKeyBytes)
|
|
} else {
|
|
return nil, fmt.Errorf("invalid private key length: %d", len(privateKeyBytes))
|
|
}
|
|
|
|
return &Key{
|
|
Type: keyData.Type,
|
|
PrivateKey: privateKey,
|
|
PublicKey: privateKey.PublicKey(),
|
|
Address: generateAddress(privateKey.PublicKey()),
|
|
}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("unable to parse key file")
|
|
}
|
|
|
|
// C returns the Ethereum-style address (0x prefixed hex string)
|
|
func (k *Key) C() string {
|
|
// Convert the address to hex format
|
|
return fmt.Sprintf("0x%x", k.Address[:])
|
|
}
|
|
|
|
// PrivKeyHex returns the private key as a hex string
|
|
func (k *Key) PrivKeyHex() string {
|
|
return hex.EncodeToString(k.PrivateKey[:32]) // Only return the seed part
|
|
}
|