feat: Waku v2 bridge

Issue #12610
This commit is contained in:
Michal Iskierko
2023-11-12 13:29:38 +01:00
parent 56e7bd01ca
commit 6d31343205
6716 changed files with 1982502 additions and 5891 deletions

View File

@@ -0,0 +1,760 @@
package account
import (
"context"
"crypto/ecdsa"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/google/uuid"
gethkeystore "github.com/ethereum/go-ethereum/accounts/keystore"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/account/generator"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/keystore"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/extkeys"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
)
// errors
var (
ErrAddressToAccountMappingFailure = errors.New("cannot retrieve a valid account for a given address")
ErrAccountToKeyMappingFailure = errors.New("cannot retrieve a valid key for a given account")
ErrNoAccountSelected = errors.New("no account has been selected, please login")
ErrInvalidMasterKeyCreated = errors.New("can not create master extended key")
ErrOnboardingNotStarted = errors.New("onboarding must be started before choosing an account")
ErrOnboardingAccountNotFound = errors.New("cannot find onboarding account with the given id")
ErrAccountKeyStoreMissing = errors.New("account key store is not set")
ErrInvalidPersonalSignAccount = errors.New("invalid account as only the selected one can generate a signature")
)
type ErrCannotLocateKeyFile struct {
Msg string
}
func (e ErrCannotLocateKeyFile) Error() string {
return e.Msg
}
var zeroAddress = types.Address{}
type SignParams struct {
Data interface{} `json:"data"`
Address string `json:"account"`
Password string `json:"password,omitempty"`
}
func (sp *SignParams) Validate(checkPassword bool) error {
if len(sp.Address) != 2*types.AddressLength+2 {
return errors.New("address has to be provided")
}
if sp.Data == "" {
return errors.New("data has to be provided")
}
if checkPassword && sp.Password == "" {
return errors.New("password has to be provided")
}
return nil
}
type RecoverParams struct {
Message string `json:"message"`
Signature string `json:"signature"`
}
// Manager represents account manager interface
type Manager interface {
GetVerifiedWalletAccount(db *accounts.Database, address, password string) (*SelectedExtKey, error)
Sign(rpcParams SignParams, verifiedAccount *SelectedExtKey) (result types.HexBytes, err error)
CanRecover(rpcParams RecoverParams, revealedAddress types.Address) (bool, error)
DeleteAccount(address types.Address) error
}
// DefaultManager represents default account manager implementation
type DefaultManager struct {
mu sync.RWMutex
rpcClient *rpc.Client
rpcTimeout time.Duration
Keydir string
keystore types.KeyStore
accountsGenerator *generator.Generator
onboarding *Onboarding
selectedChatAccount *SelectedExtKey // account that was processed during the last call to SelectAccount()
mainAccountAddress types.Address
watchAddresses []types.Address
}
// GetKeystore is only used in tests
func (m *DefaultManager) GetKeystore() types.KeyStore {
m.mu.RLock()
defer m.mu.RUnlock()
return m.keystore
}
// AccountsGenerator returns accountsGenerator.
func (m *DefaultManager) AccountsGenerator() *generator.Generator {
return m.accountsGenerator
}
// CreateAccount creates an internal geth account
// BIP44-compatible keys are generated: CKD#1 is stored as account key, CKD#2 stored as sub-account root
// Public key of CKD#1 is returned, with CKD#2 securely encoded into account key file (to be used for
// sub-account derivations)
func (m *DefaultManager) CreateAccount(password string) (generator.GeneratedAccountInfo, Info, string, error) {
var mkInfo generator.GeneratedAccountInfo
info := Info{}
// generate mnemonic phrase
mn := extkeys.NewMnemonic()
mnemonic, err := mn.MnemonicPhrase(extkeys.EntropyStrength128, extkeys.EnglishLanguage)
if err != nil {
return mkInfo, info, "", fmt.Errorf("can not create mnemonic seed: %v", err)
}
// Generate extended master key (see BIP32)
// We call extkeys.NewMaster with a seed generated with the 12 mnemonic words
// but without using the optional password as an extra entropy as described in BIP39.
// Future ideas/iterations in Status can add an an advanced options
// for expert users, to be able to add a passphrase to the generation of the seed.
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, ""))
if err != nil {
return mkInfo, info, "", fmt.Errorf("can not create master extended key: %v", err)
}
acc := generator.NewAccount(nil, extKey)
mkInfo = acc.ToGeneratedAccountInfo("", mnemonic)
// import created key into account keystore
info.WalletAddress, info.WalletPubKey, err = m.importExtendedKey(extkeys.KeyPurposeWallet, extKey, password)
if err != nil {
return mkInfo, info, "", err
}
info.ChatAddress = info.WalletAddress
info.ChatPubKey = info.WalletPubKey
return mkInfo, info, mnemonic, nil
}
// RecoverAccount re-creates master key using given details.
// Once master key is re-generated, it is inserted into keystore (if not already there).
func (m *DefaultManager) RecoverAccount(password, mnemonic string) (Info, error) {
info := Info{}
// re-create extended key (see BIP32)
mn := extkeys.NewMnemonic()
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, ""))
if err != nil {
return info, ErrInvalidMasterKeyCreated
}
// import re-created key into account keystore
info.WalletAddress, info.WalletPubKey, err = m.importExtendedKey(extkeys.KeyPurposeWallet, extKey, password)
if err != nil {
return info, err
}
info.ChatAddress = info.WalletAddress
info.ChatPubKey = info.WalletPubKey
return info, nil
}
// VerifyAccountPassword tries to decrypt a given account key file, with a provided password.
// If no error is returned, then account is considered verified.
func (m *DefaultManager) VerifyAccountPassword(keyStoreDir, address, password string) (*types.Key, error) {
var err error
var foundKeyFile []byte
addressObj := types.BytesToAddress(types.FromHex(address))
checkAccountKey := func(path string, fileInfo os.FileInfo) error {
if len(foundKeyFile) > 0 || fileInfo.IsDir() {
return nil
}
rawKeyFile, e := ioutil.ReadFile(path)
if e != nil {
return fmt.Errorf("invalid account key file: %v", e)
}
var accountKey struct {
Address string `json:"address"`
}
if e := json.Unmarshal(rawKeyFile, &accountKey); e != nil {
return fmt.Errorf("failed to read key file: %s", e)
}
if types.HexToAddress("0x"+accountKey.Address).Hex() == addressObj.Hex() {
foundKeyFile = rawKeyFile
}
return nil
}
// locate key within key store directory (address should be within the file)
err = filepath.Walk(keyStoreDir, func(path string, fileInfo os.FileInfo, err error) error {
if err != nil {
return err
}
return checkAccountKey(path, fileInfo)
})
if err != nil {
return nil, fmt.Errorf("cannot traverse key store folder: %v", err)
}
if len(foundKeyFile) == 0 {
return nil, &ErrCannotLocateKeyFile{fmt.Sprintf("cannot locate account for address: %s", addressObj.Hex())}
}
key, err := keystore.DecryptKey(foundKeyFile, password)
if err != nil {
return nil, err
}
// avoid swap attack
if key.Address != addressObj {
return nil, fmt.Errorf("account mismatch: have %s, want %s", key.Address.Hex(), addressObj.Hex())
}
return key, nil
}
// SelectAccount selects current account, by verifying that address has corresponding account which can be decrypted
// using provided password. Once verification is done, all previous identities are removed).
func (m *DefaultManager) SelectAccount(loginParams LoginParams) error {
m.mu.Lock()
defer m.mu.Unlock()
m.accountsGenerator.Reset()
selectedChatAccount, err := m.unlockExtendedKey(loginParams.ChatAddress.String(), loginParams.Password)
if err != nil {
return err
}
m.watchAddresses = loginParams.WatchAddresses
m.mainAccountAddress = loginParams.MainAccount
m.selectedChatAccount = selectedChatAccount
return nil
}
func (m *DefaultManager) SetAccountAddresses(main types.Address, secondary ...types.Address) {
m.watchAddresses = []types.Address{main}
m.watchAddresses = append(m.watchAddresses, secondary...)
m.mainAccountAddress = main
}
// SetChatAccount initializes selectedChatAccount with privKey
func (m *DefaultManager) SetChatAccount(privKey *ecdsa.PrivateKey) error {
m.mu.Lock()
defer m.mu.Unlock()
address := crypto.PubkeyToAddress(privKey.PublicKey)
id, err := uuid.NewRandom()
if err != nil {
return err
}
key := &types.Key{
ID: id,
Address: address,
PrivateKey: privKey,
}
m.selectedChatAccount = &SelectedExtKey{
Address: address,
AccountKey: key,
}
return nil
}
// MainAccountAddress returns main account address set during login
func (m *DefaultManager) MainAccountAddress() (types.Address, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.mainAccountAddress == zeroAddress {
return zeroAddress, ErrNoAccountSelected
}
return m.mainAccountAddress, nil
}
// WatchAddresses returns currently selected watch addresses.
func (m *DefaultManager) WatchAddresses() []types.Address {
m.mu.RLock()
defer m.mu.RUnlock()
return m.watchAddresses
}
// SelectedChatAccount returns currently selected chat account
func (m *DefaultManager) SelectedChatAccount() (*SelectedExtKey, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.selectedChatAccount == nil {
return nil, ErrNoAccountSelected
}
return m.selectedChatAccount, nil
}
// Logout clears selected accounts.
func (m *DefaultManager) Logout() {
m.mu.Lock()
defer m.mu.Unlock()
m.accountsGenerator.Reset()
m.mainAccountAddress = zeroAddress
m.watchAddresses = nil
m.selectedChatAccount = nil
}
// ImportAccount imports the account specified with privateKey.
func (m *DefaultManager) ImportAccount(privateKey *ecdsa.PrivateKey, password string) (types.Address, error) {
if m.keystore == nil {
return types.Address{}, ErrAccountKeyStoreMissing
}
account, err := m.keystore.ImportECDSA(privateKey, password)
return account.Address, err
}
// ImportSingleExtendedKey imports an extended key setting it in both the PrivateKey and ExtendedKey fields
// of the Key struct.
// ImportExtendedKey is used in older version of Status where PrivateKey is set to be the BIP44 key at index 0,
// and ExtendedKey is the extended key of the BIP44 key at index 1.
func (m *DefaultManager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
if m.keystore == nil {
return "", "", ErrAccountKeyStoreMissing
}
// imports extended key, create key file (if necessary)
account, err := m.keystore.ImportSingleExtendedKey(extKey, password)
if err != nil {
return "", "", err
}
address = account.Address.Hex()
// obtain public key to return
account, key, err := m.keystore.AccountDecryptedKey(account, password)
if err != nil {
return address, "", err
}
pubKey = types.EncodeHex(crypto.FromECDSAPub(&key.PrivateKey.PublicKey))
return
}
// importExtendedKey processes incoming extended key, extracts required info and creates corresponding account key.
// Once account key is formed, that key is put (if not already) into keystore i.e. key is *encoded* into key file.
func (m *DefaultManager) importExtendedKey(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
if m.keystore == nil {
return "", "", ErrAccountKeyStoreMissing
}
// imports extended key, create key file (if necessary)
account, err := m.keystore.ImportExtendedKeyForPurpose(keyPurpose, extKey, password)
if err != nil {
return "", "", err
}
address = account.Address.Hex()
// obtain public key to return
account, key, err := m.keystore.AccountDecryptedKey(account, password)
if err != nil {
return address, "", err
}
pubKey = types.EncodeHex(crypto.FromECDSAPub(&key.PrivateKey.PublicKey))
return
}
// Accounts returns list of addresses for selected account, including
// subaccounts.
func (m *DefaultManager) Accounts() ([]types.Address, error) {
m.mu.RLock()
defer m.mu.RUnlock()
addresses := make([]types.Address, 0)
if m.mainAccountAddress != zeroAddress {
addresses = append(addresses, m.mainAccountAddress)
}
return addresses, nil
}
// StartOnboarding starts the onboarding process generating accountsCount accounts and returns a slice of OnboardingAccount.
func (m *DefaultManager) StartOnboarding(accountsCount, mnemonicPhraseLength int) ([]*OnboardingAccount, error) {
m.mu.Lock()
defer m.mu.Unlock()
onboarding, err := NewOnboarding(accountsCount, mnemonicPhraseLength)
if err != nil {
return nil, err
}
m.onboarding = onboarding
return m.onboarding.Accounts(), nil
}
// RemoveOnboarding reset the current onboarding struct setting it to nil and deleting the accounts from memory.
func (m *DefaultManager) RemoveOnboarding() {
m.mu.Lock()
defer m.mu.Unlock()
m.onboarding = nil
}
// ImportOnboardingAccount imports the account specified by id and encrypts it with password.
func (m *DefaultManager) ImportOnboardingAccount(id string, password string) (Info, string, error) {
var info Info
m.mu.Lock()
defer m.mu.Unlock()
if m.onboarding == nil {
return info, "", ErrOnboardingNotStarted
}
acc, err := m.onboarding.Account(id)
if err != nil {
return info, "", err
}
info, err = m.RecoverAccount(password, acc.mnemonic)
if err != nil {
return info, "", err
}
m.onboarding = nil
return info, acc.mnemonic, nil
}
// AddressToDecryptedAccount tries to load decrypted key for a given account.
// The running node, has a keystore directory which is loaded on start. Key file
// for a given address is expected to be in that directory prior to node start.
func (m *DefaultManager) AddressToDecryptedAccount(address, password string) (types.Account, *types.Key, error) {
if m.keystore == nil {
return types.Account{}, nil, ErrAccountKeyStoreMissing
}
account, err := ParseAccountString(address)
if err != nil {
return types.Account{}, nil, ErrAddressToAccountMappingFailure
}
account, key, err := m.keystore.AccountDecryptedKey(account, password)
if err != nil {
err = fmt.Errorf("%s: %s", ErrAccountToKeyMappingFailure, err)
}
return account, key, err
}
func (m *DefaultManager) unlockExtendedKey(address, password string) (*SelectedExtKey, error) {
account, accountKey, err := m.AddressToDecryptedAccount(address, password)
if err != nil {
return nil, err
}
selectedExtendedKey := &SelectedExtKey{
Address: account.Address,
AccountKey: accountKey,
}
return selectedExtendedKey, nil
}
func (m *DefaultManager) MigrateKeyStoreDir(oldDir, newDir string, addresses []string) error {
paths := []string{}
addressesMap := map[string]struct{}{}
for _, address := range addresses {
addressesMap[address] = struct{}{}
}
checkFile := func(path string, fileInfo os.FileInfo) error {
if fileInfo.IsDir() || filepath.Dir(path) != oldDir {
return nil
}
rawKeyFile, err := ioutil.ReadFile(path)
if err != nil {
return fmt.Errorf("invalid account key file: %v", err)
}
var accountKey struct {
Address string `json:"address"`
}
if err := json.Unmarshal(rawKeyFile, &accountKey); err != nil {
return fmt.Errorf("failed to read key file: %s", err)
}
address := types.HexToAddress("0x" + accountKey.Address).Hex()
if _, ok := addressesMap[address]; ok {
paths = append(paths, path)
}
return nil
}
err := filepath.Walk(oldDir, func(path string, fileInfo os.FileInfo, err error) error {
if err != nil {
return err
}
return checkFile(path, fileInfo)
})
if err != nil {
return fmt.Errorf("cannot traverse key store folder: %v", err)
}
for _, path := range paths {
_, fileName := filepath.Split(path)
newPath := filepath.Join(newDir, fileName)
err := os.Rename(path, newPath)
if err != nil {
return err
}
}
return nil
}
func (m *DefaultManager) ReEncryptKey(rawKey []byte, pass string, newPass string) (reEncryptedKey []byte, e error) {
cryptoJSON, e := keystore.RawKeyToCryptoJSON(rawKey)
if e != nil {
return reEncryptedKey, fmt.Errorf("convert to crypto json error: %v", e)
}
decryptedKey, e := keystore.DecryptKey(rawKey, pass)
if e != nil {
return reEncryptedKey, fmt.Errorf("decryption error: %v", e)
}
if cryptoJSON.KDFParams["n"] == nil || cryptoJSON.KDFParams["p"] == nil {
return reEncryptedKey, fmt.Errorf("Unable to determine `n` or `p`: %v", e)
}
n := int(cryptoJSON.KDFParams["n"].(float64))
p := int(cryptoJSON.KDFParams["p"].(float64))
gethKey := gethkeystore.Key{
Id: decryptedKey.ID,
Address: gethcommon.Address(decryptedKey.Address),
PrivateKey: decryptedKey.PrivateKey,
ExtendedKey: decryptedKey.ExtendedKey,
SubAccountIndex: decryptedKey.SubAccountIndex,
}
return gethkeystore.EncryptKey(&gethKey, newPass, n, p)
}
func (m *DefaultManager) ReEncryptKeyStoreDir(keyDirPath, oldPass, newPass string) error {
rencryptFileAtPath := func(tempKeyDirPath, path string, fileInfo os.FileInfo) error {
if fileInfo.IsDir() {
return nil
}
rawKeyFile, e := ioutil.ReadFile(path)
if e != nil {
return fmt.Errorf("invalid account key file: %v", e)
}
reEncryptedKey, e := m.ReEncryptKey(rawKeyFile, oldPass, newPass)
if e != nil {
return fmt.Errorf("unable to re-encrypt key file: %v, path: %s, name: %s", e, path, fileInfo.Name())
}
tempWritePath := filepath.Join(tempKeyDirPath, fileInfo.Name())
e = ioutil.WriteFile(tempWritePath, reEncryptedKey, fileInfo.Mode().Perm())
if e != nil {
return fmt.Errorf("unable write key file: %v", e)
}
return nil
}
keyDirPath = strings.TrimSuffix(keyDirPath, "/")
keyDirPath = strings.TrimSuffix(keyDirPath, "\\")
keyParent, keyDirName := filepath.Split(keyDirPath)
// backupKeyDirName used to store existing keys before final write
backupKeyDirName := keyDirName + "-backup"
// tempKeyDirName used to put re-encrypted keys
tempKeyDirName := keyDirName + "-re-encrypted"
backupKeyDirPath := filepath.Join(keyParent, backupKeyDirName)
tempKeyDirPath := filepath.Join(keyParent, tempKeyDirName)
// create temp key dir
err := os.MkdirAll(tempKeyDirPath, os.ModePerm)
if err != nil {
return fmt.Errorf("mkdirall error: %v, tempKeyDirPath: %s", err, tempKeyDirPath)
}
err = filepath.Walk(keyDirPath, func(path string, fileInfo os.FileInfo, err error) error {
if err != nil {
os.RemoveAll(tempKeyDirPath)
return fmt.Errorf("walk callback error: %v", err)
}
return rencryptFileAtPath(tempKeyDirPath, path, fileInfo)
})
if err != nil {
os.RemoveAll(tempKeyDirPath)
return fmt.Errorf("walk error: %v", err)
}
// move existing keys
err = os.Rename(keyDirPath, backupKeyDirPath)
if err != nil {
os.RemoveAll(tempKeyDirPath)
return fmt.Errorf("unable to rename keyDirPath to backupKeyDirPath: %v", err)
}
// move tempKeyDirPath to keyDirPath
err = os.Rename(tempKeyDirPath, keyDirPath)
if err != nil {
// if this happens, then the app is probably bricked, because the keystore won't exist anymore
// try to restore from backup
_ = os.Rename(backupKeyDirPath, keyDirPath)
return fmt.Errorf("unable to rename tempKeyDirPath to keyDirPath: %v", err)
}
// remove temp and backup folders and their contents
err = os.RemoveAll(tempKeyDirPath)
if err != nil {
// the re-encryption is complete so we don't throw
log.Error("unable to delete tempKeyDirPath, manual cleanup required")
}
err = os.RemoveAll(backupKeyDirPath)
if err != nil {
// the re-encryption is complete so we don't throw
log.Error("unable to delete backupKeyDirPath, manual cleanup required")
}
return nil
}
func (m *DefaultManager) DeleteAccount(address types.Address) error {
return m.keystore.Delete(types.Account{Address: address})
}
func (m *DefaultManager) GetVerifiedWalletAccount(db *accounts.Database, address, password string) (*SelectedExtKey, error) {
exists, err := db.AddressExists(types.HexToAddress(address))
if err != nil {
return nil, err
}
if !exists {
return nil, errors.New("account doesn't exist")
}
key, err := m.VerifyAccountPassword(m.Keydir, address, password)
if _, ok := err.(*ErrCannotLocateKeyFile); ok {
key, err = m.generatePartialAccountKey(db, address, password)
if err != nil {
return nil, err
}
}
if err != nil {
return nil, err
}
return &SelectedExtKey{
Address: key.Address,
AccountKey: key,
}, nil
}
func (m *DefaultManager) generatePartialAccountKey(db *accounts.Database, address string, password string) (*types.Key, error) {
dbPath, err := db.GetPath(types.HexToAddress(address))
path := "m/" + dbPath[strings.LastIndex(dbPath, "/")+1:]
if err != nil {
return nil, err
}
rootAddress, err := db.GetWalletRootAddress()
if err != nil {
return nil, err
}
info, err := m.AccountsGenerator().LoadAccount(rootAddress.Hex(), password)
if err != nil {
return nil, err
}
masterID := info.ID
accInfosMap, err := m.AccountsGenerator().StoreDerivedAccounts(masterID, password, []string{path})
if err != nil {
return nil, err
}
_, key, err := m.AddressToDecryptedAccount(accInfosMap[path].Address, password)
if err != nil {
return nil, err
}
return key, nil
}
func (m *DefaultManager) Recover(rpcParams RecoverParams) (addr types.Address, err error) {
ctx, cancel := context.WithTimeout(context.Background(), m.rpcTimeout)
defer cancel()
var gethAddr gethcommon.Address
err = m.rpcClient.CallContextIgnoringLocalHandlers(
ctx,
&gethAddr,
m.rpcClient.UpstreamChainID,
params.PersonalRecoverMethodName,
rpcParams.Message, rpcParams.Signature)
addr = types.Address(gethAddr)
return
}
func (m *DefaultManager) CanRecover(rpcParams RecoverParams, revealedAddress types.Address) (bool, error) {
recovered, err := m.Recover(rpcParams)
if err != nil {
return false, err
}
return recovered == revealedAddress, nil
}
func (m *DefaultManager) Sign(rpcParams SignParams, verifiedAccount *SelectedExtKey) (result types.HexBytes, err error) {
if !strings.EqualFold(rpcParams.Address, verifiedAccount.Address.Hex()) {
err = ErrInvalidPersonalSignAccount
return
}
ctx, cancel := context.WithTimeout(context.Background(), m.rpcTimeout)
defer cancel()
var gethResult hexutil.Bytes
err = m.rpcClient.CallContextIgnoringLocalHandlers(
ctx,
&gethResult,
m.rpcClient.UpstreamChainID,
params.PersonalSignMethodName,
rpcParams.Data, rpcParams.Address, rpcParams.Password)
result = types.HexBytes(gethResult)
return
}

View File

@@ -0,0 +1,51 @@
package account
import (
"time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/status-im/status-go/account/generator"
"github.com/status-im/status-go/rpc"
)
// GethManager represents account manager interface.
type GethManager struct {
*DefaultManager
gethAccManager *accounts.Manager
}
// NewGethManager returns new node account manager.
func NewGethManager() *GethManager {
m := &GethManager{}
m.DefaultManager = &DefaultManager{accountsGenerator: generator.New(m)}
return m
}
func (m *GethManager) SetRPCClient(rpcClient *rpc.Client, rpcTimeout time.Duration) {
m.DefaultManager.rpcClient = rpcClient
m.DefaultManager.rpcTimeout = rpcTimeout
}
// InitKeystore sets key manager and key store.
func (m *GethManager) InitKeystore(keydir string) error {
m.mu.Lock()
defer m.mu.Unlock()
var err error
m.gethAccManager, err = makeAccountManager(keydir)
if err != nil {
return err
}
m.keystore, err = makeKeyStore(m.gethAccManager)
m.Keydir = keydir
return err
}
func (m *GethManager) GetManager() *accounts.Manager {
m.mu.RLock()
defer m.mu.RUnlock()
return m.gethAccManager
}

View File

@@ -0,0 +1,23 @@
package account
import (
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
)
func CreateAddress() (address, pubKey, privKey string, err error) {
key, err := crypto.GenerateKey()
if err != nil {
return "", "", "", err
}
privKeyBytes := crypto.FromECDSA(key)
pubKeyBytes := crypto.FromECDSAPub(&key.PublicKey)
addressBytes := crypto.PubkeyToAddress(key.PublicKey)
privKey = types.EncodeHex(privKeyBytes)
pubKey = types.EncodeHex(pubKeyBytes)
address = addressBytes.Hex()
return
}

View File

@@ -0,0 +1,43 @@
# Account Generator
The Account Generator is used to generate, import, derive child keys, and store accounts.
It is instantiated in the `account.Manager` struct and it's accessible from the `lib` and `mobile`
package through functions with the `MultiAccount` prefix:
* MultiAccountGenerate
* MultiAccountGenerateAndDeriveAddresses
* MultiAccountImportMnemonic
* MultiAccountDeriveAddresses
* MultiAccountStoreDerivedAccounts
* MultiAccountImportPrivateKey
* MultiAccountStoreAccount
* MultiAccountLoadAccount
* MultiAccountReset
Using `Generate` and `ImportMnemonic`, a master key is loaded in memory and a random temporarily id is returned.
Bare in mind these accounts are not saved. They are in memory until `StoreAccount` or `StoreDerivedAccounts` are called.
Calling `Reset` or restarting the application will remove everything from memory.
Logging-in and Logging-out will do the same.
Since `Generate` and `ImportMnemonic` create extended keys, we can use those keys to derive new child keys.
`MultiAccountDeriveAddresses(id, paths)` returns a list of addresses/pubKey, one for each path.
This can be used to check balances on those addresses and show them to the user.
Once the user is happy with some specific derivation paths, we can store them using `StoreDerivedAccounts(id, passwordToEncryptKey, paths)`.
`StoreDerivedAccounts` returns an address/pubKey for each path. The address can be use in the future to load them in memory again.
Calling `StoreDerivedAccounts` will encrypt and store the keys, each one in a keystore json file, and remove all the keys from memory.
Since they are derived from an extended key, they are extended keys too, so they can be used in the future to derive more child keys.
`StoreAccount` stores the key identified by its ID, so in case the key comes from `Generate` or `ImportPrivateKey`, it will store the master key.
In general we want to avoid saving master keys, so we should only use `StoreDerivedAccounts` for extended keys, and `StoreAccount` for normal keys.
Calling `Load(address, password)` will unlock the key specified by addresses using password, and load it in memory.
`Load` returns a new id that can be used again with DeriveAddresses, `StoreAccount`, and `StoreDerivedAccounts`.
`ImportPrivateKey` imports a raw private key specified by its hex form.
It's not an extended key, so it can't be used to derive child addresses.
You can call `DeriveAddresses` to derive the address/pubKey of a normal key passing an empty string as derivation path.
`StoreAccount` will save the key without deriving a child key.

View File

@@ -0,0 +1,171 @@
package generator
import (
"crypto/ecdsa"
"crypto/sha256"
"encoding/json"
"time"
accountJson "github.com/status-im/status-go/account/json"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/extkeys"
"github.com/status-im/status-go/multiaccounts"
)
type Account struct {
privateKey *ecdsa.PrivateKey
extendedKey *extkeys.ExtendedKey
}
func NewAccount(privateKey *ecdsa.PrivateKey, extKey *extkeys.ExtendedKey) Account {
if privateKey == nil {
privateKey = extKey.ToECDSA()
}
return Account{
privateKey: privateKey,
extendedKey: extKey,
}
}
func (a *Account) ToAccountInfo() AccountInfo {
privateKeyHex := types.EncodeHex(crypto.FromECDSA(a.privateKey))
publicKeyHex := types.EncodeHex(crypto.FromECDSAPub(&a.privateKey.PublicKey))
addressHex := crypto.PubkeyToAddress(a.privateKey.PublicKey).Hex()
return AccountInfo{
PrivateKey: privateKeyHex,
PublicKey: publicKeyHex,
Address: addressHex,
}
}
func (a *Account) ToIdentifiedAccountInfo(id string) IdentifiedAccountInfo {
info := a.ToAccountInfo()
keyUID := sha256.Sum256(crypto.FromECDSAPub(&a.privateKey.PublicKey))
keyUIDHex := types.EncodeHex(keyUID[:])
return IdentifiedAccountInfo{
AccountInfo: info,
ID: id,
KeyUID: keyUIDHex,
}
}
func (a *Account) ToGeneratedAccountInfo(id string, mnemonic string) GeneratedAccountInfo {
idInfo := a.ToIdentifiedAccountInfo(id)
return GeneratedAccountInfo{
IdentifiedAccountInfo: idInfo,
Mnemonic: mnemonic,
}
}
// AccountInfo contains a PublicKey and an Address of an account.
type AccountInfo struct {
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey"`
Address string `json:"address"`
}
func (a AccountInfo) MarshalJSON() ([]byte, error) {
type Alias AccountInfo
ext, err := accountJson.ExtendStructWithPubKeyData(a.PublicKey, Alias(a))
if err != nil {
return nil, err
}
return json.Marshal(ext)
}
// IdentifiedAccountInfo contains AccountInfo and the ID of an account.
type IdentifiedAccountInfo struct {
AccountInfo
ID string `json:"id"`
// KeyUID is calculated as sha256 of the master public key and used for key
// identification. This is the only information available about the master
// key stored on a keycard before the card is paired.
// KeyUID name is chosen over KeyID in order to make it consistent with
// the name already used in Status and Keycard codebases.
KeyUID string `json:"keyUid"`
}
func (i IdentifiedAccountInfo) MarshalJSON() ([]byte, error) {
accountInfoJSON, err := i.AccountInfo.MarshalJSON()
if err != nil {
return nil, err
}
type info struct {
ID string `json:"id"`
KeyUID string `json:"keyUid"`
}
infoJSON, err := json.Marshal(info{
ID: i.ID,
KeyUID: i.KeyUID,
})
if err != nil {
return nil, err
}
infoJSON[0] = ','
return append(accountInfoJSON[:len(accountInfoJSON)-1], infoJSON...), nil
}
func (i *IdentifiedAccountInfo) ToMultiAccount() *multiaccounts.Account {
return &multiaccounts.Account{
Timestamp: time.Now().Unix(),
KeyUID: i.KeyUID,
}
}
// GeneratedAccountInfo contains IdentifiedAccountInfo and the mnemonic of an account.
type GeneratedAccountInfo struct {
IdentifiedAccountInfo
Mnemonic string `json:"mnemonic"`
}
func (g GeneratedAccountInfo) MarshalJSON() ([]byte, error) {
accountInfoJSON, err := g.IdentifiedAccountInfo.MarshalJSON()
if err != nil {
return nil, err
}
type info struct {
Mnemonic string `json:"mnemonic"`
}
infoJSON, err := json.Marshal(info{
Mnemonic: g.Mnemonic,
})
if err != nil {
return nil, err
}
infoJSON[0] = ','
return append(accountInfoJSON[:len(accountInfoJSON)-1], infoJSON...), nil
}
func (g GeneratedAccountInfo) toGeneratedAndDerived(derived map[string]AccountInfo) GeneratedAndDerivedAccountInfo {
return GeneratedAndDerivedAccountInfo{
GeneratedAccountInfo: g,
Derived: derived,
}
}
// GeneratedAndDerivedAccountInfo contains GeneratedAccountInfo and derived AccountInfo mapped by derivation path.
type GeneratedAndDerivedAccountInfo struct {
GeneratedAccountInfo
Derived map[string]AccountInfo `json:"derived"`
}
func (g GeneratedAndDerivedAccountInfo) MarshalJSON() ([]byte, error) {
accountInfoJSON, err := g.GeneratedAccountInfo.MarshalJSON()
if err != nil {
return nil, err
}
type info struct {
Derived map[string]AccountInfo `json:"derived"`
}
infoJSON, err := json.Marshal(info{
Derived: g.Derived,
})
if err != nil {
return nil, err
}
infoJSON[0] = ','
return append(accountInfoJSON[:len(accountInfoJSON)-1], infoJSON...), nil
}

View File

@@ -0,0 +1,352 @@
package generator
import (
"crypto/ecdsa"
"errors"
"fmt"
"strings"
"sync"
"github.com/pborman/uuid"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/keystore"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/extkeys"
)
var (
// ErrAccountNotFoundByID is returned when the selected account doesn't exist in memory.
ErrAccountNotFoundByID = errors.New("account not found")
// ErrAccountCannotDeriveChildKeys is returned when trying to derive child accounts from a normal key.
ErrAccountCannotDeriveChildKeys = errors.New("selected account cannot derive child keys")
// ErrAccountManagerNotSet is returned when the account mananger instance is not set.
ErrAccountManagerNotSet = errors.New("account manager not set")
)
type AccountManager interface {
AddressToDecryptedAccount(address, password string) (types.Account, *types.Key, error)
ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error)
ImportAccount(privateKey *ecdsa.PrivateKey, password string) (types.Address, error)
}
type Generator struct {
am AccountManager
accounts map[string]*Account
sync.Mutex
}
func New(am AccountManager) *Generator {
return &Generator{
am: am,
accounts: make(map[string]*Account),
}
}
func (g *Generator) Generate(mnemonicPhraseLength int, n int, bip39Passphrase string) ([]GeneratedAccountInfo, error) {
entropyStrength, err := MnemonicPhraseLengthToEntropyStrength(mnemonicPhraseLength)
if err != nil {
return nil, err
}
infos := make([]GeneratedAccountInfo, 0)
for i := 0; i < n; i++ {
mnemonic := extkeys.NewMnemonic()
mnemonicPhrase, err := mnemonic.MnemonicPhrase(entropyStrength, extkeys.EnglishLanguage)
if err != nil {
return nil, fmt.Errorf("can not create mnemonic seed: %v", err)
}
info, err := g.ImportMnemonic(mnemonicPhrase, bip39Passphrase)
if err != nil {
return nil, err
}
infos = append(infos, info)
}
return infos, err
}
func (g *Generator) CreateAccountFromPrivateKey(privateKeyHex string) (IdentifiedAccountInfo, error) {
privateKeyHex = strings.TrimPrefix(privateKeyHex, "0x")
privateKey, err := crypto.HexToECDSA(privateKeyHex)
if err != nil {
return IdentifiedAccountInfo{}, err
}
acc := &Account{
privateKey: privateKey,
}
return acc.ToIdentifiedAccountInfo(""), nil
}
func (g *Generator) ImportPrivateKey(privateKeyHex string) (IdentifiedAccountInfo, error) {
privateKeyHex = strings.TrimPrefix(privateKeyHex, "0x")
privateKey, err := crypto.HexToECDSA(privateKeyHex)
if err != nil {
return IdentifiedAccountInfo{}, err
}
acc := &Account{
privateKey: privateKey,
}
id := g.addAccount(acc)
return acc.ToIdentifiedAccountInfo(id), nil
}
func (g *Generator) ImportJSONKey(json string, password string) (IdentifiedAccountInfo, error) {
key, err := keystore.DecryptKey([]byte(json), password)
if err != nil {
return IdentifiedAccountInfo{}, err
}
acc := &Account{
privateKey: key.PrivateKey,
}
id := g.addAccount(acc)
return acc.ToIdentifiedAccountInfo(id), nil
}
func (g *Generator) CreateAccountFromMnemonicAndDeriveAccountsForPaths(mnemonicPhrase string, bip39Passphrase string, paths []string) (GeneratedAndDerivedAccountInfo, error) {
mnemonic := extkeys.NewMnemonic()
masterExtendedKey, err := extkeys.NewMaster(mnemonic.MnemonicSeed(mnemonicPhrase, bip39Passphrase))
if err != nil {
return GeneratedAndDerivedAccountInfo{}, fmt.Errorf("can not create master extended key: %v", err)
}
acc := &Account{
privateKey: masterExtendedKey.ToECDSA(),
extendedKey: masterExtendedKey,
}
derivedAccountsInfo := make(map[string]AccountInfo)
if len(paths) > 0 {
derivedAccounts, err := g.deriveChildAccounts(acc, paths)
if err != nil {
return GeneratedAndDerivedAccountInfo{}, err
}
for pathString, childAccount := range derivedAccounts {
derivedAccountsInfo[pathString] = childAccount.ToAccountInfo()
}
}
accInfo := acc.ToGeneratedAccountInfo("", mnemonicPhrase)
return accInfo.toGeneratedAndDerived(derivedAccountsInfo), nil
}
func (g *Generator) ImportMnemonic(mnemonicPhrase string, bip39Passphrase string) (GeneratedAccountInfo, error) {
mnemonic := extkeys.NewMnemonic()
masterExtendedKey, err := extkeys.NewMaster(mnemonic.MnemonicSeed(mnemonicPhrase, bip39Passphrase))
if err != nil {
return GeneratedAccountInfo{}, fmt.Errorf("can not create master extended key: %v", err)
}
acc := &Account{
privateKey: masterExtendedKey.ToECDSA(),
extendedKey: masterExtendedKey,
}
id := g.addAccount(acc)
return acc.ToGeneratedAccountInfo(id, mnemonicPhrase), nil
}
func (g *Generator) GenerateAndDeriveAddresses(mnemonicPhraseLength int, n int, bip39Passphrase string, pathStrings []string) ([]GeneratedAndDerivedAccountInfo, error) {
masterAccounts, err := g.Generate(mnemonicPhraseLength, n, bip39Passphrase)
if err != nil {
return nil, err
}
accs := make([]GeneratedAndDerivedAccountInfo, n)
for i := 0; i < len(masterAccounts); i++ {
acc := masterAccounts[i]
derived, err := g.DeriveAddresses(acc.ID, pathStrings)
if err != nil {
return nil, err
}
accs[i] = acc.toGeneratedAndDerived(derived)
}
return accs, nil
}
func (g *Generator) DeriveAddresses(accountID string, pathStrings []string) (map[string]AccountInfo, error) {
acc, err := g.findAccount(accountID)
if err != nil {
return nil, err
}
pathAccounts, err := g.deriveChildAccounts(acc, pathStrings)
if err != nil {
return nil, err
}
pathAccountsInfo := make(map[string]AccountInfo)
for pathString, childAccount := range pathAccounts {
pathAccountsInfo[pathString] = childAccount.ToAccountInfo()
}
return pathAccountsInfo, nil
}
func (g *Generator) StoreAccount(accountID string, password string) (AccountInfo, error) {
if g.am == nil {
return AccountInfo{}, ErrAccountManagerNotSet
}
acc, err := g.findAccount(accountID)
if err != nil {
return AccountInfo{}, err
}
return g.store(acc, password)
}
func (g *Generator) StoreDerivedAccounts(accountID string, password string, pathStrings []string) (map[string]AccountInfo, error) {
if g.am == nil {
return nil, ErrAccountManagerNotSet
}
acc, err := g.findAccount(accountID)
if err != nil {
return nil, err
}
pathAccounts, err := g.deriveChildAccounts(acc, pathStrings)
if err != nil {
return nil, err
}
pathAccountsInfo := make(map[string]AccountInfo)
for pathString, childAccount := range pathAccounts {
info, err := g.store(childAccount, password)
if err != nil {
return nil, err
}
pathAccountsInfo[pathString] = info
}
return pathAccountsInfo, nil
}
func (g *Generator) LoadAccount(address string, password string) (IdentifiedAccountInfo, error) {
if g.am == nil {
return IdentifiedAccountInfo{}, ErrAccountManagerNotSet
}
_, key, err := g.am.AddressToDecryptedAccount(address, password)
if err != nil {
return IdentifiedAccountInfo{}, err
}
if err := ValidateKeystoreExtendedKey(key); err != nil {
return IdentifiedAccountInfo{}, err
}
acc := &Account{
privateKey: key.PrivateKey,
extendedKey: key.ExtendedKey,
}
id := g.addAccount(acc)
return acc.ToIdentifiedAccountInfo(id), nil
}
func (g *Generator) deriveChildAccounts(acc *Account, pathStrings []string) (map[string]*Account, error) {
pathAccounts := make(map[string]*Account)
for _, pathString := range pathStrings {
childAccount, err := g.deriveChildAccount(acc, pathString)
if err != nil {
return pathAccounts, err
}
pathAccounts[pathString] = childAccount
}
return pathAccounts, nil
}
func (g *Generator) deriveChildAccount(acc *Account, pathString string) (*Account, error) {
_, path, err := decodePath(pathString)
if err != nil {
return nil, err
}
if acc.extendedKey.IsZeroed() && len(path) == 0 {
return acc, nil
}
if acc.extendedKey.IsZeroed() {
return nil, ErrAccountCannotDeriveChildKeys
}
childExtendedKey, err := acc.extendedKey.Derive(path)
if err != nil {
return nil, err
}
return &Account{
privateKey: childExtendedKey.ToECDSA(),
extendedKey: childExtendedKey,
}, nil
}
func (g *Generator) store(acc *Account, password string) (AccountInfo, error) {
if acc.extendedKey != nil {
if _, _, err := g.am.ImportSingleExtendedKey(acc.extendedKey, password); err != nil {
return AccountInfo{}, err
}
} else {
if _, err := g.am.ImportAccount(acc.privateKey, password); err != nil {
return AccountInfo{}, err
}
}
return acc.ToAccountInfo(), nil
}
func (g *Generator) addAccount(acc *Account) string {
g.Lock()
defer g.Unlock()
id := uuid.NewRandom().String()
g.accounts[id] = acc
return id
}
// Reset resets the accounts map removing all the accounts from memory.
func (g *Generator) Reset() {
g.Lock()
defer g.Unlock()
g.accounts = make(map[string]*Account)
}
func (g *Generator) findAccount(accountID string) (*Account, error) {
g.Lock()
defer g.Unlock()
acc, ok := g.accounts[accountID]
if !ok {
return nil, ErrAccountNotFoundByID
}
return acc, nil
}

View File

@@ -0,0 +1,226 @@
package generator
import (
"fmt"
"io"
"strconv"
"strings"
)
type startingPoint int
const (
tokenMaster = 0x6D // char m
tokenSeparator = 0x2F // char /
tokenHardened = 0x27 // char '
tokenDot = 0x2E // char .
hardenedStart = 0x80000000 // 2^31
)
const (
startingPointMaster startingPoint = iota + 1
startingPointCurrent
startingPointParent
)
type parseFunc = func() error
type pathDecoder struct {
s string
r *strings.Reader
f parseFunc
pos int
path []uint32
start startingPoint
currentToken string
currentTokenHardened bool
}
func newPathDecoder(path string) (*pathDecoder, error) {
d := &pathDecoder{
s: path,
r: strings.NewReader(path),
}
if err := d.reset(); err != nil {
return nil, err
}
return d, nil
}
func (d *pathDecoder) reset() error {
_, err := d.r.Seek(0, io.SeekStart)
if err != nil {
return err
}
d.pos = 0
d.start = startingPointCurrent
d.f = d.parseStart
d.path = make([]uint32, 0)
d.resetCurrentToken()
return nil
}
func (d *pathDecoder) resetCurrentToken() {
d.currentToken = ""
d.currentTokenHardened = false
}
func (d *pathDecoder) parse() (startingPoint, []uint32, error) {
for {
err := d.f()
if err != nil {
if err == io.EOF {
err = nil
} else {
err = fmt.Errorf("error parsing derivation path %s; at position %d, %s", d.s, d.pos, err.Error())
}
return d.start, d.path, err
}
}
}
func (d *pathDecoder) readByte() (byte, error) {
b, err := d.r.ReadByte()
if err != nil {
return b, err
}
d.pos++
return b, nil
}
func (d *pathDecoder) unreadByte() error {
err := d.r.UnreadByte()
if err != nil {
return err
}
d.pos--
return nil
}
func (d *pathDecoder) parseStart() error {
b, err := d.readByte()
if err != nil {
return err
}
if b == tokenMaster {
d.start = startingPointMaster
d.f = d.parseSeparator
return nil
}
if b == tokenDot {
b2, err := d.readByte()
if err != nil {
return err
}
if b2 == tokenDot {
d.f = d.parseSeparator
d.start = startingPointParent
return nil
}
d.f = d.parseSeparator
d.start = startingPointCurrent
return d.unreadByte()
}
d.f = d.parseSegment
return d.unreadByte()
}
func (d *pathDecoder) saveSegment() error {
if len(d.currentToken) > 0 {
i, err := strconv.ParseUint(d.currentToken, 10, 32)
if err != nil {
return err
}
if i >= hardenedStart {
d.pos -= len(d.currentToken) - 1
return fmt.Errorf("index must be lower than 2^31, got %d", i)
}
if d.currentTokenHardened {
i += hardenedStart
}
d.path = append(d.path, uint32(i))
}
d.f = d.parseSegment
d.resetCurrentToken()
return nil
}
func (d *pathDecoder) parseSeparator() error {
b, err := d.readByte()
if err != nil {
return err
}
if b == tokenSeparator {
return d.saveSegment()
}
return fmt.Errorf("expected %s, got %s", string(rune(tokenSeparator)), string(rune(b)))
}
func (d *pathDecoder) parseSegment() error {
b, err := d.readByte()
if err == io.EOF {
if len(d.currentToken) == 0 {
return fmt.Errorf("expected number, got EOF")
}
if newErr := d.saveSegment(); newErr != nil {
return newErr
}
return err
}
if err != nil {
return err
}
if len(d.currentToken) > 0 && b == tokenSeparator {
return d.saveSegment()
}
if len(d.currentToken) > 0 && b == tokenHardened {
d.currentTokenHardened = true
d.f = d.parseSeparator
return nil
}
if b < 0x30 || b > 0x39 {
return fmt.Errorf("expected number, got %s", string(b))
}
d.currentToken = fmt.Sprintf("%s%s", d.currentToken, string(b))
return nil
}
func decodePath(str string) (startingPoint, []uint32, error) {
d, err := newPathDecoder(str)
if err != nil {
return 0, nil, err
}
return d.parse()
}

View File

@@ -0,0 +1,46 @@
package generator
import (
"bytes"
"errors"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/extkeys"
)
var (
// ErrInvalidKeystoreExtendedKey is returned when the decrypted keystore file
// contains some old Status keys.
// The old version used to store the BIP44 account at index 0 as PrivateKey,
// and the BIP44 account at index 1 as ExtendedKey.
// The current version stores the same key as PrivateKey and ExtendedKey.
ErrInvalidKeystoreExtendedKey = errors.New("PrivateKey and ExtendedKey are different")
ErrInvalidMnemonicPhraseLength = errors.New("invalid mnemonic phrase length; valid lengths are 12, 15, 18, 21, and 24")
)
// ValidateKeystoreExtendedKey validates the keystore keys, checking that
// ExtendedKey is the extended key of PrivateKey.
func ValidateKeystoreExtendedKey(key *types.Key) error {
if key.ExtendedKey.IsZeroed() {
return nil
}
if !bytes.Equal(crypto.FromECDSA(key.PrivateKey), crypto.FromECDSA(key.ExtendedKey.ToECDSA())) {
return ErrInvalidKeystoreExtendedKey
}
return nil
}
// MnemonicPhraseLengthToEntropyStrength returns the entropy strength for a given mnemonic length
func MnemonicPhraseLengthToEntropyStrength(length int) (extkeys.EntropyStrength, error) {
if length < 12 || length > 24 || length%3 != 0 {
return 0, ErrInvalidMnemonicPhraseLength
}
bitsLength := length * 11
checksumLength := bitsLength % 32
return extkeys.EntropyStrength(bitsLength - checksumLength), nil
}

View File

@@ -0,0 +1,63 @@
package json
import (
"reflect"
"github.com/status-im/status-go/api/multiformat"
"github.com/status-im/status-go/protocol/identity/emojihash"
)
type PublicKeyData struct {
CompressedKey string `json:"compressedKey"`
EmojiHash []string `json:"emojiHash"`
}
func getPubKeyData(publicKey string) (*PublicKeyData, error) {
compressedKey, err := multiformat.SerializeLegacyKey(publicKey)
if err != nil {
return nil, err
}
emojiHash, err := emojihash.GenerateFor(publicKey)
if err != nil {
return nil, err
}
return &PublicKeyData{compressedKey, emojiHash}, nil
}
func ExtendStructWithPubKeyData(publicKey string, item any) (any, error) {
// If the public key is empty, do not attempt to extend the incoming item
if publicKey == "" {
return item, nil
}
pkd, err := getPubKeyData(publicKey)
if err != nil {
return nil, err
}
// Create a struct with 2 embedded substruct fields in order to circumvent
// "embedded field type cannot be a (pointer to a) type parameter"
// compiler error that arises if we were to use a generic function instead
typ := reflect.StructOf([]reflect.StructField{
{
Name: "Item",
Anonymous: true,
Type: reflect.TypeOf(item),
},
{
Name: "Pkd",
Anonymous: true,
Type: reflect.TypeOf(pkd),
},
})
v := reflect.New(typ).Elem()
v.Field(0).Set(reflect.ValueOf(item))
v.Field(1).Set(reflect.ValueOf(pkd))
s := v.Addr().Interface()
return s, nil
}

View File

@@ -0,0 +1,41 @@
package account
import (
"os"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
gethbridge "github.com/status-im/status-go/eth-node/bridge/geth"
"github.com/status-im/status-go/eth-node/types"
)
// makeAccountManager creates ethereum accounts.Manager with single disk backend and lightweight kdf.
// If keydir is empty new temporary directory with go-ethereum-keystore will be intialized.
func makeAccountManager(keydir string) (manager *accounts.Manager, err error) {
if keydir == "" {
// There is no datadir.
keydir, err = os.MkdirTemp("", "go-ethereum-keystore")
}
if err != nil {
return nil, err
}
if err := os.MkdirAll(keydir, 0700); err != nil {
return nil, err
}
config := accounts.Config{InsecureUnlockAllowed: false}
return accounts.NewManager(&config, keystore.NewKeyStore(keydir, keystore.LightScryptN, keystore.LightScryptP)), nil
}
func makeKeyStore(manager *accounts.Manager) (types.KeyStore, error) {
backends := manager.Backends(keystore.KeyStoreType)
if len(backends) == 0 {
return nil, ErrAccountKeyStoreMissing
}
keyStore, ok := backends[0].(*keystore.KeyStore)
if !ok {
return nil, ErrAccountKeyStoreMissing
}
return gethbridge.WrapKeyStore(keyStore), nil
}

View File

@@ -0,0 +1,114 @@
package account
import (
"fmt"
"github.com/pborman/uuid"
"github.com/status-im/status-go/account/generator"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/extkeys"
)
// OnboardingAccount is returned during onboarding and contains its ID and the mnemonic to re-generate the same account Info keys.
type OnboardingAccount struct {
ID string `json:"id"`
mnemonic string
Info Info `json:"info"`
}
// Onboarding is a struct contains a slice of OnboardingAccount.
type Onboarding struct {
accounts map[string]*OnboardingAccount
}
// NewOnboarding returns a new onboarding struct generating n accounts.
func NewOnboarding(n, mnemonicPhraseLength int) (*Onboarding, error) {
onboarding := &Onboarding{
accounts: make(map[string]*OnboardingAccount),
}
for i := 0; i < n; i++ {
account, err := onboarding.generateAccount(mnemonicPhraseLength)
if err != nil {
return nil, err
}
onboarding.accounts[account.ID] = account
}
return onboarding, nil
}
// Accounts return the list of OnboardingAccount generated.
func (o *Onboarding) Accounts() []*OnboardingAccount {
accounts := make([]*OnboardingAccount, 0)
for _, a := range o.accounts {
accounts = append(accounts, a)
}
return accounts
}
// Account returns an OnboardingAccount by id.
func (o *Onboarding) Account(id string) (*OnboardingAccount, error) {
account, ok := o.accounts[id]
if !ok {
return nil, ErrOnboardingAccountNotFound
}
return account, nil
}
func (o *Onboarding) generateAccount(mnemonicPhraseLength int) (*OnboardingAccount, error) {
entropyStrength, err := generator.MnemonicPhraseLengthToEntropyStrength(mnemonicPhraseLength)
if err != nil {
return nil, err
}
mnemonic := extkeys.NewMnemonic()
mnemonicPhrase, err := mnemonic.MnemonicPhrase(entropyStrength, extkeys.EnglishLanguage)
if err != nil {
return nil, fmt.Errorf("can not create mnemonic seed: %v", err)
}
masterExtendedKey, err := extkeys.NewMaster(mnemonic.MnemonicSeed(mnemonicPhrase, ""))
if err != nil {
return nil, fmt.Errorf("can not create master extended key: %v", err)
}
walletAddress, walletPubKey, err := o.deriveAccount(masterExtendedKey, extkeys.KeyPurposeWallet, 0)
if err != nil {
return nil, err
}
info := Info{
WalletAddress: walletAddress,
WalletPubKey: walletPubKey,
ChatAddress: walletAddress,
ChatPubKey: walletPubKey,
}
uuid := uuid.NewRandom().String()
account := &OnboardingAccount{
ID: uuid,
mnemonic: mnemonicPhrase,
Info: info,
}
return account, nil
}
func (o *Onboarding) deriveAccount(masterExtendedKey *extkeys.ExtendedKey, purpose extkeys.KeyPurpose, index uint32) (string, string, error) {
extendedKey, err := masterExtendedKey.ChildForPurpose(purpose, index)
if err != nil {
return "", "", err
}
privateKeyECDSA := extendedKey.ToECDSA()
address := crypto.PubkeyToAddress(privateKeyECDSA.PublicKey)
publicKeyHex := types.EncodeHex(crypto.FromECDSAPub(&privateKeyECDSA.PublicKey))
return address.Hex(), publicKeyHex, nil
}

130
vendor/github.com/status-im/status-go/account/utils.go generated vendored Normal file
View File

@@ -0,0 +1,130 @@
package account
import (
"encoding/json"
"errors"
"fmt"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/extkeys"
"github.com/status-im/status-go/multiaccounts"
)
// errors
var (
ErrInvalidAccountAddressOrKey = errors.New("cannot parse address or key to valid account address")
ErrInvalidMnemonicPhraseLength = errors.New("invalid mnemonic phrase length; valid lengths are 12, 15, 18, 21, and 24")
)
type LoginParams struct {
ChatAddress types.Address `json:"chatAddress"`
Password string `json:"password"`
MainAccount types.Address `json:"mainAccount"`
WatchAddresses []types.Address `json:"watchAddresses"`
MultiAccount *multiaccounts.Account `json:"multiAccount"`
}
type ErrZeroAddress struct {
field string
}
func (e *ErrZeroAddress) Error() string {
return fmt.Sprintf("%s contains an empty address", e.field)
}
func newErrZeroAddress(field string) *ErrZeroAddress {
return &ErrZeroAddress{
field: field,
}
}
func ParseLoginParams(paramsJSON string) (LoginParams, error) {
var (
params LoginParams
zeroAddress types.Address
)
if err := json.Unmarshal([]byte(paramsJSON), &params); err != nil {
return params, err
}
if params.ChatAddress == zeroAddress {
return params, newErrZeroAddress("ChatAddress")
}
if params.MainAccount == zeroAddress {
return params, newErrZeroAddress("MainAccount")
}
for _, address := range params.WatchAddresses {
if address == zeroAddress {
return params, newErrZeroAddress("WatchAddresses")
}
}
return params, nil
}
// Info contains wallet and chat addresses and public keys of an account.
type Info struct {
WalletAddress string
WalletPubKey string
ChatAddress string
ChatPubKey string
}
// SelectedExtKey is a container for the selected (logged in) external account.
type SelectedExtKey struct {
Address types.Address
AccountKey *types.Key
SubAccounts []types.Account
}
// Hex dumps address of a given extended key as hex string.
func (k *SelectedExtKey) Hex() string {
if k == nil {
return "0x0"
}
return k.Address.Hex()
}
// ParseAccountString parses hex encoded string and returns is as types.Account.
func ParseAccountString(account string) (types.Account, error) {
// valid address, convert to account
if types.IsHexAddress(account) {
return types.Account{Address: types.HexToAddress(account)}, nil
}
return types.Account{}, ErrInvalidAccountAddressOrKey
}
// FromAddress converts account address from string to types.Address.
// The function is useful to format "From" field of send transaction struct.
func FromAddress(accountAddress string) types.Address {
from, err := ParseAccountString(accountAddress)
if err != nil {
return types.Address{}
}
return from.Address
}
// ToAddress converts account address from string to *common.Address.
// The function is useful to format "To" field of send transaction struct.
func ToAddress(accountAddress string) *types.Address {
to, err := ParseAccountString(accountAddress)
if err != nil {
return nil
}
return &to.Address
}
func GetRandomMnemonic() (string, error) {
// generate mnemonic phrase
mn := extkeys.NewMnemonic()
mnemonic, err := mn.MnemonicPhrase(extkeys.EntropyStrength128, extkeys.EnglishLanguage)
if err != nil {
return "", fmt.Errorf("can not create mnemonic seed: %v", err)
}
return mnemonic, nil
}