Files
node/service/keystore/keystore.go
T
Hanzo AI 6de839a515 codec: rip github.com/luxfi/codec from leaf-misc (Wave 1D)
Wave 1D of the luxfi/codec final rip: leaf-misc tier under node/.
This batch removes the direct codec/wrappers, codec/linearcodec, and
codec.Manager imports from p2p network protocol, IP/port encoding,
indexer/keystore on-disk storage, warp socket IPC, x/archivedb +
merkledb internals, and the primary-network wallet UTXO loader.

Migration shape:

(a) codec/wrappers.Packer + length constants
    → node/utils/wrappers (same shape, already in tree as the local
      canonical home for binary IO helpers). 14 files: indexer/,
      network/, utils/ips/, utils/metric/, warp/, x/.

(b) codec.Manager + linearcodec for on-disk container storage
    → hand-rolled big-endian binary marshal/unmarshal. Hard cut;
      no codec-version prefix; payload size bounded explicitly:
      - indexer/codec.go: marshalContainer/unmarshalContainer
      - service/keystore/codec.go: marshalHash/unmarshalHash and
        marshalUser/unmarshalUser, 16 MiB blob cap retained.

(c) codec.Manager for cross-chain UTXO parsing (wallet/network/primary)
    → utxo.ParseUTXO via the ZAP wire dispatcher already registered
      by node/vms/components/lux. Drops the per-chain codec.Manager
      slot from FetchState; AddAllUTXOs no longer takes a codec.
      Underscore-import of vms/components/lux ensures the dispatcher
      is wired even for callers that don't already pull platformvm.

No snow/snowman/snowball/avalanche/avm/subnet residue introduced.
2026-06-05 11:57:14 -07:00

381 lines
9.3 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package keystore
import (
"errors"
"fmt"
"net/http"
"sync"
"github.com/gorilla/rpc/v2"
"github.com/luxfi/database"
"github.com/luxfi/database/encdb"
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/utils/json"
"github.com/luxfi/node/utils/password"
"github.com/luxfi/vm/chains/atomic"
)
const (
// maxUserLen is the maximum allowed length of a username
maxUserLen = 1024
)
var (
errEmptyUsername = errors.New("empty username")
errUserMaxLength = fmt.Errorf("username exceeds maximum length of %d chars", maxUserLen)
errUserAlreadyExists = errors.New("user already exists")
errIncorrectPassword = errors.New("incorrect password")
errNonexistentUser = errors.New("user doesn't exist")
usersPrefix = []byte("users")
bcsPrefix = []byte("bcs")
_ Keystore = (*keystore)(nil)
)
type Keystore interface {
// Create the API endpoint for this keystore.
CreateHandler() (http.Handler, error)
// NewBlockchainKeyStore returns this keystore limiting the functionality to
// a single blockchain database.
NewBlockchainKeyStore(blockchainID ids.ID) BlockchainKeystore
// Get a database that is able to read and write unencrypted values from the
// underlying database.
GetDatabase(bID ids.ID, username, password string) (*encdb.Database, error)
// Get the underlying database that is able to read and write encrypted
// values. This Database will not perform any encrypting or decrypting of
// values and is not recommended to be used when implementing a VM.
GetRawDatabase(bID ids.ID, username, password string) (database.Database, error)
// CreateUser attempts to register this username and password as a new user
// of the keystore.
CreateUser(username, pw string) error
// DeleteUser attempts to remove the provided username and all of its data
// from the keystore.
DeleteUser(username, pw string) error
// ListUsers returns all the users that currently exist in this keystore.
ListUsers() ([]string, error)
// ImportUser imports a serialized encoding of a user's information complete
// with encrypted database values. The password is integrity checked.
ImportUser(username, pw string, user []byte) error
// ExportUser exports a serialized encoding of a user's information complete
// with encrypted database values.
ExportUser(username, pw string) ([]byte, error)
// Get the password that is used by [username]. If [username] doesn't exist,
// no error is returned and a nil password hash is returned.
getPassword(username string) (*password.Hash, error)
}
type kvPair struct {
Key []byte `serialize:"true"`
Value []byte `serialize:"true"`
}
// user describes the full content of a user
type user struct {
password.Hash `serialize:"true"`
Data []kvPair `serialize:"true"`
}
type keystore struct {
lock sync.Mutex
log log.Logger
// Key: username
// Value: The hash of that user's password
usernameToPassword map[string]*password.Hash
// Used to persist users and their data
userDB database.Database
bcDB database.Database
}
func New(log log.Logger, db database.Database) Keystore {
return &keystore{
log: log,
usernameToPassword: make(map[string]*password.Hash),
userDB: prefixdb.New(usersPrefix, db),
bcDB: prefixdb.New(bcsPrefix, db),
}
}
func (ks *keystore) CreateHandler() (http.Handler, error) {
newServer := rpc.NewServer()
codec := json.NewCodec()
newServer.RegisterCodec(codec, "application/json")
newServer.RegisterCodec(codec, "application/json;charset=UTF-8")
if err := newServer.RegisterService(&service{ks: ks}, "keystore"); err != nil {
return nil, err
}
return newServer, nil
}
func (ks *keystore) NewBlockchainKeyStore(blockchainID ids.ID) BlockchainKeystore {
return &blockchainKeystore{
blockchainID: blockchainID,
ks: ks,
}
}
func (ks *keystore) GetDatabase(bID ids.ID, username, password string) (*encdb.Database, error) {
bcDB, err := ks.GetRawDatabase(bID, username, password)
if err != nil {
return nil, err
}
return encdb.New([]byte(password), bcDB)
}
func (ks *keystore) GetRawDatabase(bID ids.ID, username, pw string) (database.Database, error) {
if username == "" {
return nil, errEmptyUsername
}
ks.lock.Lock()
defer ks.lock.Unlock()
passwordHash, err := ks.getPassword(username)
if err != nil {
return nil, err
}
if passwordHash == nil || !passwordHash.Check(pw) {
return nil, fmt.Errorf("%w: user %q", errIncorrectPassword, username)
}
userDB := prefixdb.New([]byte(username), ks.bcDB)
bcDB := prefixdb.NewNested(bID[:], userDB)
return bcDB, nil
}
func (ks *keystore) CreateUser(username, pw string) error {
if username == "" {
return errEmptyUsername
}
if len(username) > maxUserLen {
return errUserMaxLength
}
ks.lock.Lock()
defer ks.lock.Unlock()
passwordHash, err := ks.getPassword(username)
if err != nil {
return err
}
if passwordHash != nil {
return fmt.Errorf("%w: %s", errUserAlreadyExists, username)
}
if err := password.IsValid(pw, password.OK); err != nil {
return err
}
passwordHash = &password.Hash{}
if err := passwordHash.Set(pw); err != nil {
return err
}
passwordBytes, err := marshalHash(passwordHash)
if err != nil {
return err
}
if err := ks.userDB.Put([]byte(username), passwordBytes); err != nil {
return err
}
ks.usernameToPassword[username] = passwordHash
return nil
}
func (ks *keystore) DeleteUser(username, pw string) error {
if username == "" {
return errEmptyUsername
}
if len(username) > maxUserLen {
return errUserMaxLength
}
ks.lock.Lock()
defer ks.lock.Unlock()
// check if user exists and valid user.
passwordHash, err := ks.getPassword(username)
switch {
case err != nil:
return err
case passwordHash == nil:
return fmt.Errorf("%w: %s", errNonexistentUser, username)
case !passwordHash.Check(pw):
return fmt.Errorf("%w: user %q", errIncorrectPassword, username)
}
userNameBytes := []byte(username)
userBatch := ks.userDB.NewBatch()
if err := userBatch.Delete(userNameBytes); err != nil {
return err
}
userDataDB := prefixdb.New(userNameBytes, ks.bcDB)
dataBatch := userDataDB.NewBatch()
it := userDataDB.NewIterator()
defer it.Release()
for it.Next() {
if err := dataBatch.Delete(it.Key()); err != nil {
return err
}
}
if err := it.Error(); err != nil {
return err
}
if err := atomic.WriteAll(dataBatch, userBatch); err != nil {
return err
}
// delete from users map.
delete(ks.usernameToPassword, username)
return nil
}
func (ks *keystore) ListUsers() ([]string, error) {
users := []string{}
ks.lock.Lock()
defer ks.lock.Unlock()
it := ks.userDB.NewIterator()
defer it.Release()
for it.Next() {
users = append(users, string(it.Key()))
}
return users, it.Error()
}
func (ks *keystore) ImportUser(username, pw string, userBytes []byte) error {
if username == "" {
return errEmptyUsername
}
if len(username) > maxUserLen {
return errUserMaxLength
}
ks.lock.Lock()
defer ks.lock.Unlock()
passwordHash, err := ks.getPassword(username)
if err != nil {
return err
}
if passwordHash != nil {
return fmt.Errorf("%w: %s", errUserAlreadyExists, username)
}
userData := user{}
if err := unmarshalUser(userBytes, &userData); err != nil {
return err
}
if !userData.Hash.Check(pw) {
return fmt.Errorf("%w: user %q", errIncorrectPassword, username)
}
usrBytes, err := marshalHash(&userData.Hash)
if err != nil {
return err
}
userBatch := ks.userDB.NewBatch()
if err := userBatch.Put([]byte(username), usrBytes); err != nil {
return err
}
userDataDB := prefixdb.New([]byte(username), ks.bcDB)
dataBatch := userDataDB.NewBatch()
for _, kvp := range userData.Data {
if err := dataBatch.Put(kvp.Key, kvp.Value); err != nil {
return fmt.Errorf("error on database put: %w", err)
}
}
if err := atomic.WriteAll(dataBatch, userBatch); err != nil {
return err
}
ks.usernameToPassword[username] = &userData.Hash
return nil
}
func (ks *keystore) ExportUser(username, pw string) ([]byte, error) {
if username == "" {
return nil, errEmptyUsername
}
if len(username) > maxUserLen {
return nil, errUserMaxLength
}
ks.lock.Lock()
defer ks.lock.Unlock()
passwordHash, err := ks.getPassword(username)
if err != nil {
return nil, err
}
if passwordHash == nil || !passwordHash.Check(pw) {
return nil, fmt.Errorf("%w: user %q", errIncorrectPassword, username)
}
userDB := prefixdb.New([]byte(username), ks.bcDB)
userData := user{Hash: *passwordHash}
it := userDB.NewIterator()
defer it.Release()
for it.Next() {
userData.Data = append(userData.Data, kvPair{
Key: it.Key(),
Value: it.Value(),
})
}
if err := it.Error(); err != nil {
return nil, err
}
// Return the byte representation of the user
return marshalUser(&userData)
}
func (ks *keystore) getPassword(username string) (*password.Hash, error) {
// If the user is already in memory, return it
passwordHash, exists := ks.usernameToPassword[username]
if exists {
return passwordHash, nil
}
// The user is not in memory; try the database
userBytes, err := ks.userDB.Get([]byte(username))
if err == database.ErrNotFound {
// The user doesn't exist
return nil, nil
}
if err != nil {
// An unexpected database error occurred
return nil, err
}
passwordHash = &password.Hash{}
return passwordHash, unmarshalHash(userBytes, passwordHash)
}