Skip to content
Open
13 changes: 8 additions & 5 deletions docs/developer/adr/0010-keyvault-encryption-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ wallet facing lock, unlock, encryption, decryption, and derivation APIs.
### Responsibilities

1. **Own lock state and key lifecycle**
Keyvault manages unlock state, key material lifetime, auto lock behavior, and
secure memory zeroing.
Keyvault manages lock state, key material lifetime, and secure memory
zeroing.

2. **Expose typed domain interfaces**
Keyvault returns domain types such as `*btcec.PrivateKey`,
Expand Down Expand Up @@ -125,6 +125,9 @@ and parameters, consistent with ADR 0001. That routing is handled inside store
implementations or keyvault adapters, not repeated throughout wallet domain
code.

Auto lock timeout scheduling is a wallet or controller lifecycle policy, not
part of `keyvault.Vault`.

### Pros

1. **Separation of concerns**
Expand All @@ -136,8 +139,8 @@ code.
blobs.

3. **Centralized lock management**
Lock state, unlock timers, cache lifetime, and secret zeroing are owned by
one component.
Lock state and secret zeroing are owned by one component, while timeout and
auto lock scheduling stay outside the vault.

4. **Extensible responsibility boundary**
Keyvault centralizes secret and key responsibilities, making it easier to
Expand All @@ -153,7 +156,7 @@ code.
tested against mock vault implementations.

7. **Per wallet isolation**
Each wallet has its own vault instance, lock state, cache, timers, and secret
Each wallet has its own vault instance, lock state, cache, and secret
material lifetime.

8. **Migration support**
Expand Down
13 changes: 6 additions & 7 deletions wallet/internal/bwtest/mock/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package mock

import (
"context"
"time"

"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/stretchr/testify/mock"
Expand Down Expand Up @@ -36,10 +35,8 @@ func (m *Vault) Decrypt(keyType waddrmgr.CryptoKeyType,
}

// Unlock forwards to the configured testify expectations.
func (m *Vault) Unlock(ctx context.Context, passphrase []byte,
timeout time.Duration) error {

args := m.Called(ctx, passphrase, timeout)
func (m *Vault) Unlock(ctx context.Context, passphrase []byte) error {
args := m.Called(ctx, passphrase)
return args.Error(0)
}

Expand All @@ -55,8 +52,10 @@ func (m *Vault) IsLocked() bool {
}

// RefreshPrivatePassphrase forwards to the configured testify expectations.
func (m *Vault) RefreshPrivatePassphrase(passphrase []byte) error {
args := m.Called(passphrase)
func (m *Vault) RefreshPrivatePassphrase(ctx context.Context,
passphrase []byte) error {

args := m.Called(ctx, passphrase)
return args.Error(0)
}

Expand Down
77 changes: 0 additions & 77 deletions wallet/internal/keyvault/db_vault.go

This file was deleted.

40 changes: 21 additions & 19 deletions wallet/internal/keyvault/keyvault.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,31 @@ package keyvault

import (
"context"
"time"
"errors"

"github.com/btcsuite/btcwallet/waddrmgr"
)

// ErrInvalidPassphrase reports that the provided vault passphrase is wrong.
var ErrInvalidPassphrase = errors.New("invalid vault passphrase")

// ErrVaultLocked reports that an operation requiring unlocked runtime state
// was attempted while the vault was locked.
var ErrVaultLocked = errors.New("vault is locked")

// Vault manages the lock lifecycle and cryptographic operations for wallet key
// material.
type Vault interface {
// Unlock unlocks the vault with the provided passphrase and applies the
// requested automatic lock timeout.
//
// A zero timeout uses the implementation's default timeout.
// A negative timeout disables automatic locking until Lock is called.
// A positive timeout schedules Lock to run after that duration.
// Unlock unlocks the vault with the provided passphrase.
//
// A successful Unlock replaces any previously scheduled lock. An invalid
// passphrase must leave the vault locked.
Unlock(ctx context.Context, passphrase []byte, timeout time.Duration) error

// Lock locks the vault, clears any pending automatic lock, and erases
// secret material from memory. Lock is idempotent.
// If the passphrase is invalid, or the unlock operation fails, the vault
// must remain locked. If Unlock is called while the vault is already
// unlocked, it must be a no-op and must not validate the provided
// passphrase.
Unlock(ctx context.Context, passphrase []byte) error

// Lock locks the vault and erases secret material from memory. Lock is
// idempotent.
Lock()

// IsLocked reports whether the vault is currently locked.
Expand All @@ -37,10 +41,8 @@ type Vault interface {
// type.
Decrypt(keyType waddrmgr.CryptoKeyType, ciphertext []byte) ([]byte, error)

// RefreshPrivatePassphrase refreshes vault owned runtime passphrase and
// crypto state after a successful private passphrase rotation.
//
// The vault must still be unlocked with the new passphrase when this method
// is called.
RefreshPrivatePassphrase(passphrase []byte) error
// RefreshPrivatePassphrase rotates persisted wallet secrets to the provided
// new private passphrase. The vault must already be unlocked when this
// method is called.
RefreshPrivatePassphrase(ctx context.Context, passphrase []byte) error

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: so this is now basically ChangePassphrase?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but I can easy change to ChangePassphrase if u think it is clearer

}
73 changes: 73 additions & 0 deletions wallet/internal/keyvault/vault.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package keyvault

import (
"errors"
"sync"

"github.com/btcsuite/btcd/btcutil/v2/hdkeychain"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/wallet/internal/db"
)

var (
// errUnexpectedState reports that the vault is in an unexpected state,
// which may indicate a programming error or data corruption. Normal
// operation should not return this error, and that's why it's unexported.
errUnexpectedState = errors.New("unexpected state")
)

// WalletVault adapts db.Store wallet secret storage to the wallet key-vault
// boundary.
type WalletVault struct {
// store is the underlying durable persistence layer for the wallets.
store db.Store

// walletID is the wallet row id that this vault is scoped to.
walletID uint32

// mtx guards concurrent access.
mtx sync.Mutex

// unlockedState holds sensitive runtime secret material that is only
// available when the vault is unlocked.
unlockedState *unlockedState
}

// unlockedState holds sensitive runtime secret material.
type unlockedState struct {
// cryptoKeyPrivate is the key used to encrypt and decrypt private material.
cryptoKeyPrivate snacl.CryptoKey

// cryptoKeyScript is the key used to encrypt and decrypt script material.
cryptoKeyScript snacl.CryptoKey

// hdRootKey is the master HD extended key for the wallet, which can derive
// all sub scopes, accounts, addresses, and keys.
hdRootKey *hdkeychain.ExtendedKey
}

// Ensure WalletVault implements keyvault.Vault.
var _ Vault = (*WalletVault)(nil)

// NewDBVault creates a key-vault bridge scoped to one wallet row.
func NewDBVault(store db.Store, walletID uint32) *WalletVault {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also update the constructor's name and callsites to NewWalletVault?

return &WalletVault{
store: store,
walletID: walletID,
}
}

// zero clears the runtime secret material held by the unlocked state.
func (s *unlockedState) zero() {
if s == nil {
return
}

s.cryptoKeyPrivate.Zero()
s.cryptoKeyScript.Zero()

if s.hdRootKey != nil {
s.hdRootKey.Zero()
s.hdRootKey = nil
}
}
Loading
Loading