Files
ethdo/util/account.go
2025-01-28 19:32:01 +00:00

329 lines
8.7 KiB
Go

// Copyright © 2020 - 2025 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/pkg/errors"
"github.com/wealdtech/go-ecodec"
util "github.com/wealdtech/go-eth2-util"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// ParseAccount parses input to obtain an account.
func ParseAccount(ctx context.Context,
accountStr string,
supplementary []string,
unlock bool,
) (
e2wtypes.Account,
error,
) {
switch {
case accountStr == "":
return nil, errors.New("no account specified")
case strings.HasPrefix(accountStr, "0x"):
// A key.
return parseAccountFromKey(ctx, accountStr, unlock)
case strings.HasPrefix(accountStr, "{"):
// This could be a keystore.
return parseAccountFromKeystore(ctx, accountStr, supplementary, unlock)
case strings.Contains(accountStr, "/"):
// An account specifier.
account, err := parseAccountFromSpecifier(ctx, accountStr, supplementary, unlock)
if err != nil {
// It is possible that this is actually a path to a keystore, so try that instead.
if _, statErr := os.Stat(accountStr); statErr == nil {
account, err = parseAccountFromKeystorePath(ctx, accountStr, supplementary, unlock)
}
}
if err != nil {
if strings.Count(accountStr, " ") > 7 {
// It is also possible that this is a mnemonic with "/" in the additional word, so try that as well.
account, err = parseAccountFromMnemonic(ctx, accountStr, supplementary, unlock)
}
}
if err != nil {
return nil, err
}
return account, nil
case strings.Contains(accountStr, " "):
// A mnemonic.
return parseAccountFromMnemonic(ctx, accountStr, supplementary, unlock)
default:
// This could be the path to a keystore.
if _, err := os.Stat(accountStr); err != nil {
return nil, fmt.Errorf("unknown account specifier %s", accountStr)
}
account, err := parseAccountFromKeystorePath(ctx, accountStr, supplementary, unlock)
if err != nil {
return nil, err
}
return account, nil
}
}
func parseAccountFromKey(ctx context.Context,
accountStr string,
unlock bool,
) (
e2wtypes.Account,
error,
) {
var account e2wtypes.Account
var err error
// A key. Could be public or private.
data, err := hex.DecodeString(strings.TrimPrefix(accountStr, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to parse account key")
}
switch len(data) {
case 48:
// Public key.
account, err = newScratchAccountFromPubKey(data)
if err != nil {
return nil, errors.Wrap(err, "failed to create account from public key")
}
if unlock {
return nil, errors.New("cannot unlock an account specified by its public key")
}
case 32:
// Private key.
account, err = newScratchAccountFromPrivKey(data)
if err != nil {
return nil, errors.Wrap(err, "failed to create account from private key")
}
if unlock {
_, err = UnlockAccount(ctx, account, nil)
if err != nil {
return nil, err
}
}
default:
return nil, fmt.Errorf("key of length %d neither public nor private key", len(data))
}
return account, nil
}
func parseAccountFromSpecifier(ctx context.Context,
accountStr string,
supplementary []string,
unlock bool,
) (
e2wtypes.Account,
error,
) {
var account e2wtypes.Account
var err error
_, account, err = WalletAndAccountFromPath(ctx, accountStr)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain account")
}
if unlock {
// Supplementary will be the unlock passphrase(s).
_, err = UnlockAccount(ctx, account, supplementary)
if err != nil {
return nil, err
}
}
return account, nil
}
func parseAccountFromMnemonic(ctx context.Context,
accountStr string,
supplementary []string,
unlock bool,
) (
e2wtypes.Account,
error,
) {
var account e2wtypes.Account
var err error
if len(supplementary) == 0 {
return nil, errors.New("missing derivation path")
}
account, err = accountFromMnemonicAndPath(accountStr, supplementary[0])
if err != nil {
return nil, err
}
if unlock {
err = account.(e2wtypes.AccountLocker).Unlock(ctx, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to unlock account")
}
}
return account, nil
}
func parseAccountFromKeystore(ctx context.Context,
accountStr string,
supplementary []string,
unlock bool,
) (
e2wtypes.Account,
error,
) {
var account e2wtypes.Account
var err error
// Need to import the keystore in to a temporary wallet to fetch the private key.
store := scratch.New()
encryptor := keystorev4.New()
// Need to add a couple of fields to the keystore to make it compliant.
var keystore map[string]any
if err := json.Unmarshal([]byte(accountStr), &keystore); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal keystore")
}
keystore["name"] = "Import"
keystore["encryptor"] = "keystore"
keystoreData, err := json.Marshal(keystore)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal keystore")
}
walletData := fmt.Sprintf(`{"wallet":{"name":"Import","type":"non-deterministic","uuid":"e1526407-1dc7-4f3f-9d05-ab696f40707c","version":1},"accounts":[%s]}`, keystoreData)
encryptedData, err := ecodec.Encrypt([]byte(walletData), []byte(`password`))
if err != nil {
return nil, err
}
wallet, err := nd.Import(ctx, encryptedData, []byte(`password`), store, encryptor)
if err != nil {
return nil, errors.Wrap(err, "failed to import account")
}
account = <-wallet.Accounts(ctx)
if unlock {
if locker, isLocker := account.(e2wtypes.AccountLocker); isLocker {
unlocked := false
for _, passphrase := range supplementary {
if err = locker.Unlock(ctx, []byte(passphrase)); err == nil {
unlocked = true
break
}
}
if !unlocked {
return nil, errors.New("failed to unlock account")
}
}
}
return account, nil
}
func parseAccountFromKeystorePath(ctx context.Context,
accountStr string,
supplementary []string,
unlock bool,
) (
e2wtypes.Account,
error,
) {
data, err := os.ReadFile(accountStr)
if err != nil {
return nil, errors.Wrap(err, "failed to read keystore file")
}
return parseAccountFromKeystore(ctx, string(data), supplementary, unlock)
}
func accountFromMnemonicAndPath(mnemonic string, path string) (e2wtypes.Account, error) {
seed, err := SeedFromMnemonic(mnemonic)
if err != nil {
return nil, err
}
// Ensure the path is valid.
match := hdPathRegex.MatchString(path)
if !match {
return nil, errors.New("path does not match expected format m/…")
}
// Derive private key from seed and path.
key, err := util.PrivateKeyFromSeedAndPath(seed, path)
if err != nil {
return nil, errors.Wrap(err, "failed to generate key")
}
// Create a scratch account given the private key.
account, err := newScratchAccountFromPrivKey(key.Marshal())
if err != nil {
return nil, errors.Wrap(err, "failed to generate scratch account")
}
return account, nil
}
// UnlockAccount attempts to unlock an account. It returns true if the account was already unlocked.
func UnlockAccount(ctx context.Context, account e2wtypes.Account, passphrases []string) (bool, error) {
locker, isAccountLocker := account.(e2wtypes.AccountLocker)
if !isAccountLocker {
// This account doesn't support unlocking; return okay.
return true, nil
}
alreadyUnlocked, err := locker.IsUnlocked(ctx)
if err != nil {
return false, errors.Wrap(err, "unable to ascertain if account is unlocked")
}
if alreadyUnlocked {
return true, nil
}
// Not already unlocked; attempt to unlock it.
for _, passphrase := range passphrases {
err = locker.Unlock(ctx, []byte(passphrase))
if err == nil {
// Unlocked.
return false, nil
}
}
// Also attempt to unlock without any passphrase.
err = locker.Unlock(ctx, nil)
if err == nil {
// Unlocked.
return false, nil
}
// Failed to unlock it.
return false, errors.New("failed to unlock account")
}
// LockAccount attempts to lock an account.
func LockAccount(ctx context.Context, account e2wtypes.Account) error {
locker, isAccountLocker := account.(e2wtypes.AccountLocker)
if !isAccountLocker {
// This account doesn't support locking; return okay.
return nil
}
return locker.Lock(ctx)
}