mirror of
https://github.com/guggero/chantools
synced 2024-11-01 15:40:11 +00:00
331 lines
9.4 KiB
Go
331 lines
9.4 KiB
Go
package lnd
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcwallet/snacl"
|
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
|
"github.com/btcsuite/btcwallet/wallet"
|
|
"github.com/btcsuite/btcwallet/walletdb"
|
|
"github.com/lightningnetwork/lnd/aezeed"
|
|
"github.com/lightningnetwork/lnd/lncfg"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"go.etcd.io/bbolt"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
const (
|
|
MnemonicEnvName = "AEZEED_MNEMONIC"
|
|
PassphraseEnvName = "AEZEED_PASSPHRASE"
|
|
PasswordEnvName = "WALLET_PASSWORD"
|
|
)
|
|
|
|
var (
|
|
numberDotsRegex = regexp.MustCompile(`[\d.\-\n\r\t]*`)
|
|
multipleSpaces = regexp.MustCompile(" [ ]+")
|
|
|
|
openCallbacks = &waddrmgr.OpenCallbacks{
|
|
ObtainSeed: noConsole,
|
|
ObtainPrivatePass: noConsole,
|
|
}
|
|
|
|
// Namespace from github.com/btcsuite/btcwallet/wallet/wallet.go.
|
|
WaddrmgrNamespaceKey = []byte("waddrmgr")
|
|
|
|
// Bucket names from github.com/btcsuite/btcwallet/waddrmgr/db.go.
|
|
mainBucketName = []byte("main")
|
|
masterPrivKeyName = []byte("mpriv")
|
|
cryptoPrivKeyName = []byte("cpriv")
|
|
masterHDPrivName = []byte("mhdpriv")
|
|
)
|
|
|
|
func noConsole() ([]byte, error) {
|
|
return nil, fmt.Errorf("wallet db requires console access")
|
|
}
|
|
|
|
// ReadAezeed reads an aezeed from the console or the environment variable.
|
|
func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
|
|
error) {
|
|
|
|
// To automate things with chantools, we also offer reading the seed
|
|
// from environment variables.
|
|
mnemonicStr := strings.TrimSpace(os.Getenv(MnemonicEnvName))
|
|
|
|
// If nothing is set in the environment, read the seed from the
|
|
// terminal.
|
|
if mnemonicStr == "" {
|
|
var err error
|
|
// We'll now prompt the user to enter in their 24-word mnemonic.
|
|
fmt.Printf("Input your 24-word mnemonic separated by spaces: ")
|
|
reader := bufio.NewReader(os.Stdin)
|
|
mnemonicStr, err = reader.ReadString('\n')
|
|
if err != nil {
|
|
return nil, time.Unix(0, 0), err
|
|
}
|
|
}
|
|
|
|
// We'll trim off extra spaces, and ensure the mnemonic is all
|
|
// lower case.
|
|
mnemonicStr = strings.TrimSpace(mnemonicStr)
|
|
mnemonicStr = strings.ToLower(mnemonicStr)
|
|
|
|
// To allow the tool to also accept the copy/pasted version of the
|
|
// backup text (which contains numbers and dots and multiple spaces),
|
|
// we do some more cleanup with regex.
|
|
mnemonicStr = numberDotsRegex.ReplaceAllString(mnemonicStr, "")
|
|
mnemonicStr = multipleSpaces.ReplaceAllString(mnemonicStr, " ")
|
|
mnemonicStr = strings.TrimSpace(mnemonicStr)
|
|
|
|
cipherSeedMnemonic := strings.Split(mnemonicStr, " ")
|
|
|
|
fmt.Println()
|
|
|
|
if len(cipherSeedMnemonic) != 24 {
|
|
return nil, time.Unix(0, 0), fmt.Errorf("wrong cipher seed "+
|
|
"mnemonic length: got %v words, expecting %v words",
|
|
len(cipherSeedMnemonic), 24)
|
|
}
|
|
|
|
passphraseBytes, err := ReadPassphrase("doesn't have")
|
|
if err != nil {
|
|
return nil, time.Unix(0, 0), err
|
|
}
|
|
|
|
var mnemonic aezeed.Mnemonic
|
|
copy(mnemonic[:], cipherSeedMnemonic)
|
|
|
|
// If we're unable to map it back into the ciphertext, then either the
|
|
// mnemonic is wrong, or the passphrase is wrong.
|
|
cipherSeed, err := mnemonic.ToCipherSeed(passphraseBytes)
|
|
if err != nil {
|
|
return nil, time.Unix(0, 0), fmt.Errorf("failed to decrypt "+
|
|
"seed with passphrase: %w", err)
|
|
}
|
|
rootKey, err := hdkeychain.NewMaster(cipherSeed.Entropy[:], params)
|
|
if err != nil {
|
|
return nil, time.Unix(0, 0), fmt.Errorf("failed to derive " +
|
|
"master extended key")
|
|
}
|
|
return rootKey, cipherSeed.BirthdayTime(), nil
|
|
}
|
|
|
|
// ReadPassphrase reads a cipher seed passphrase from the console or the
|
|
// environment variable.
|
|
func ReadPassphrase(verb string) ([]byte, error) {
|
|
// Additionally, the user may have a passphrase, that will also need to
|
|
// be provided so the daemon can properly decipher the cipher seed.
|
|
// Try the environment variable first.
|
|
passphrase := strings.TrimSpace(os.Getenv(PassphraseEnvName))
|
|
|
|
// Because we cannot differentiate between an empty and a non-existent
|
|
// environment variable, we need a special character that indicates that
|
|
// no passphrase should be used. We use a single dash (-) for that as
|
|
// that would be too short for a passphrase anyway.
|
|
var passphraseBytes []byte
|
|
switch {
|
|
// The user indicated in the environment variable that no passphrase
|
|
// should be used. We don't set any value.
|
|
case passphrase == "-":
|
|
|
|
// The environment variable didn't contain anything, we'll read the
|
|
// passphrase from the terminal.
|
|
case passphrase == "":
|
|
fmt.Printf("Input your cipher seed passphrase (press enter "+
|
|
"if your seed %s a passphrase): ", verb)
|
|
var err error
|
|
passphraseBytes, err = terminal.ReadPassword(
|
|
int(syscall.Stdin), //nolint
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fmt.Println()
|
|
|
|
// There was a password in the environment, just convert it to bytes.
|
|
default:
|
|
passphraseBytes = []byte(passphrase)
|
|
}
|
|
|
|
return passphraseBytes, nil
|
|
}
|
|
|
|
// PasswordFromConsole reads a password from the console or stdin.
|
|
func PasswordFromConsole(userQuery string) ([]byte, error) {
|
|
// Read from terminal (if there is one).
|
|
if terminal.IsTerminal(int(syscall.Stdin)) { //nolint
|
|
fmt.Print(userQuery)
|
|
pw, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fmt.Println()
|
|
return pw, nil
|
|
}
|
|
|
|
// Read from stdin as a fallback.
|
|
reader := bufio.NewReader(os.Stdin)
|
|
pw, err := reader.ReadBytes('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pw, nil
|
|
}
|
|
|
|
// OpenWallet opens a lnd compatible wallet and returns it, along with the
|
|
// private wallet password.
|
|
func OpenWallet(walletDbPath string,
|
|
chainParams *chaincfg.Params) (*wallet.Wallet, []byte, func() error,
|
|
error) {
|
|
|
|
var (
|
|
publicWalletPw = lnwallet.DefaultPublicPassphrase
|
|
privateWalletPw = lnwallet.DefaultPrivatePassphrase
|
|
err error
|
|
)
|
|
|
|
// To automate things with chantools, we also offer reading the wallet
|
|
// password from environment variables.
|
|
pw := []byte(strings.TrimSpace(os.Getenv(PasswordEnvName)))
|
|
|
|
// Because we cannot differentiate between an empty and a non-existent
|
|
// environment variable, we need a special character that indicates that
|
|
// no password should be used. We use a single dash (-) for that as that
|
|
// would be too short for an explicit password anyway.
|
|
switch {
|
|
// The user indicated in the environment variable that no passphrase
|
|
// should be used. We don't set any value.
|
|
case string(pw) == "-":
|
|
|
|
// The environment variable didn't contain anything, we'll read the
|
|
// passphrase from the terminal.
|
|
case len(pw) == 0:
|
|
pw, err = PasswordFromConsole("Input wallet password: ")
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
if len(pw) > 0 {
|
|
publicWalletPw = pw
|
|
privateWalletPw = pw
|
|
}
|
|
|
|
// There was a password in the environment, just use it directly.
|
|
default:
|
|
publicWalletPw = pw
|
|
privateWalletPw = pw
|
|
}
|
|
|
|
// Try to load and open the wallet.
|
|
db, err := walletdb.Open(
|
|
"bdb", lncfg.CleanAndExpandPath(walletDbPath), false,
|
|
DefaultOpenTimeout,
|
|
)
|
|
if errors.Is(err, bbolt.ErrTimeout) {
|
|
return nil, nil, nil, fmt.Errorf("error opening wallet " +
|
|
"database, make sure lnd is not running and holding " +
|
|
"the exclusive lock on the wallet")
|
|
}
|
|
if err != nil {
|
|
return nil, nil, nil, fmt.Errorf("error opening wallet "+
|
|
"database: %w", err)
|
|
}
|
|
|
|
w, err := wallet.Open(db, publicWalletPw, openCallbacks, chainParams, 0)
|
|
if err != nil {
|
|
_ = db.Close()
|
|
return nil, nil, nil, fmt.Errorf("error opening wallet %w", err)
|
|
}
|
|
|
|
// Start and unlock the wallet.
|
|
w.Start()
|
|
err = w.Unlock(privateWalletPw, nil)
|
|
if err != nil {
|
|
w.Stop()
|
|
_ = db.Close()
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
cleanup := func() error {
|
|
w.Stop()
|
|
if err := db.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return w, privateWalletPw, cleanup, nil
|
|
}
|
|
|
|
// DecryptWalletRootKey decrypts a lnd compatible wallet's root key.
|
|
func DecryptWalletRootKey(db walletdb.DB,
|
|
privatePassphrase []byte) ([]byte, error) {
|
|
|
|
// Step 1: Load the encryption parameters and encrypted keys from the
|
|
// database.
|
|
var masterKeyPrivParams []byte
|
|
var cryptoKeyPrivEnc []byte
|
|
var masterHDPrivEnc []byte
|
|
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
|
|
ns := tx.ReadBucket(WaddrmgrNamespaceKey)
|
|
if ns == nil {
|
|
return fmt.Errorf("namespace '%s' does not exist",
|
|
WaddrmgrNamespaceKey)
|
|
}
|
|
|
|
mainBucket := ns.NestedReadBucket(mainBucketName)
|
|
if mainBucket == nil {
|
|
return fmt.Errorf("bucket '%s' does not exist",
|
|
mainBucketName)
|
|
}
|
|
|
|
val := mainBucket.Get(masterPrivKeyName)
|
|
if val != nil {
|
|
masterKeyPrivParams = make([]byte, len(val))
|
|
copy(masterKeyPrivParams, val)
|
|
}
|
|
val = mainBucket.Get(cryptoPrivKeyName)
|
|
if val != nil {
|
|
cryptoKeyPrivEnc = make([]byte, len(val))
|
|
copy(cryptoKeyPrivEnc, val)
|
|
}
|
|
val = mainBucket.Get(masterHDPrivName)
|
|
if val != nil {
|
|
masterHDPrivEnc = make([]byte, len(val))
|
|
copy(masterHDPrivEnc, val)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Step 2: Unmarshal the master private key parameters and derive
|
|
// key from passphrase.
|
|
var masterKeyPriv snacl.SecretKey
|
|
if err := masterKeyPriv.Unmarshal(masterKeyPrivParams); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := masterKeyPriv.DeriveKey(&privatePassphrase); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Step 3: Decrypt the keys in the correct order.
|
|
cryptoKeyPriv := &snacl.CryptoKey{}
|
|
cryptoKeyPrivBytes, err := masterKeyPriv.Decrypt(cryptoKeyPrivEnc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copy(cryptoKeyPriv[:], cryptoKeyPrivBytes)
|
|
return cryptoKeyPriv.Decrypt(masterHDPrivEnc)
|
|
}
|