From 198b0b8daeb60730647e28958b04dd8387575552 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 16 Feb 2021 17:01:15 -0800 Subject: [PATCH] waddrmgr: store watch-only accounts under new account type Watch-only accounts are usually backed by an external signer as they do not contain any private key information. Some external signers require a root key fingerprint for identification and signing purposes. In order to guarantee compatibility with external signers, we need to persist the root key fingerprint within the database. Before this change, watch-only accounts used the default account database structure. In this commit, we introduce a new account type to store different information for watch-only accounts only. This isn't a breaking change as watch-only accounts have yet to be supported by the primary user of the wallet (lnd). With this new account type, we can avoid the empty private key fields, which are irrelevant to watch-only accounts, and we can store the root key fingerprint. --- waddrmgr/db.go | 289 +++++++++++++++++++++++++++++++++---- waddrmgr/manager.go | 4 +- waddrmgr/scoped_manager.go | 183 ++++++++++++++--------- 3 files changed, 377 insertions(+), 99 deletions(-) diff --git a/waddrmgr/db.go b/waddrmgr/db.go index 19167ea..1d8f6cf 100644 --- a/waddrmgr/db.go +++ b/waddrmgr/db.go @@ -6,6 +6,7 @@ package waddrmgr import ( + "bytes" "crypto/sha256" "encoding/binary" "errors" @@ -83,6 +84,12 @@ const ( // database. This is an account that re-uses the key derivation schema // of BIP0044-like accounts. accountDefault accountType = 0 // not iota as they need to be stable + + // accountWatchOnly is the account type used for storing watch-only + // accounts within the database. This is an account that re-uses the key + // derivation schema of BIP0044-like accounts and does not store private + // keys. + accountWatchOnly accountType = 1 ) // dbAccountRow houses information stored about an account in the database. @@ -102,6 +109,18 @@ type dbDefaultAccountRow struct { name string } +// dbWatchOnlyAccountRow houses additional information stored about a watch-only +// account in the databse. +type dbWatchOnlyAccountRow struct { + dbAccountRow + pubKeyEncrypted []byte + masterKeyFingerprint uint32 + nextExternalIndex uint32 + nextInternalIndex uint32 + name string + addrSchema *ScopeAddrSchema +} + // dbAddressRow houses common information stored about an address in the // database. type dbAddressRow struct { @@ -809,6 +828,159 @@ func serializeDefaultAccountRow(encryptedPubKey, encryptedPrivKey []byte, return rawData } +// deserializeWatchOnlyAccountRow deserializes the raw data from the passed +// account row as a watch-only account. +func deserializeWatchOnlyAccountRow(accountID []byte, + row *dbAccountRow) (*dbWatchOnlyAccountRow, error) { + + // The serialized BIP0044 watch-only account raw data format is: + // + // + // + // 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes master key + // fingerprint + 4 bytes next external index + 4 bytes next internal + // index + 4 bytes name len + name + 1 byte addr schema exists + 2 bytes + // addr schema (if exists) + + // Given the above, the length of the entry must be at a minimum + // the constant value sizes. + if len(row.rawData) < 21 { + str := fmt.Sprintf("malformed serialized watch-only account "+ + "for key %x", accountID) + return nil, managerError(ErrDatabase, str, nil) + } + + retRow := dbWatchOnlyAccountRow{ + dbAccountRow: *row, + } + r := bytes.NewReader(row.rawData) + + var pubLen uint32 + err := binary.Read(r, binary.LittleEndian, &pubLen) + if err != nil { + return nil, err + } + retRow.pubKeyEncrypted = make([]byte, pubLen) + err = binary.Read(r, binary.LittleEndian, &retRow.pubKeyEncrypted) + if err != nil { + return nil, err + } + + err = binary.Read(r, binary.LittleEndian, &retRow.masterKeyFingerprint) + if err != nil { + return nil, err + } + + err = binary.Read(r, binary.LittleEndian, &retRow.nextExternalIndex) + if err != nil { + return nil, err + } + err = binary.Read(r, binary.LittleEndian, &retRow.nextInternalIndex) + if err != nil { + return nil, err + } + + var nameLen uint32 + err = binary.Read(r, binary.LittleEndian, &nameLen) + if err != nil { + return nil, err + } + name := make([]byte, nameLen) + err = binary.Read(r, binary.LittleEndian, &name) + if err != nil { + return nil, err + } + retRow.name = string(name) + + var addrSchemaExists bool + err = binary.Read(r, binary.LittleEndian, &addrSchemaExists) + if err != nil { + return nil, err + } + if addrSchemaExists { + var addrSchemaBytes [2]byte + err = binary.Read(r, binary.LittleEndian, &addrSchemaBytes) + if err != nil { + return nil, err + } + retRow.addrSchema = scopeSchemaFromBytes(addrSchemaBytes[:]) + } + + return &retRow, nil +} + +// serializeWatchOnlyAccountRow returns the serialization of the raw data field +// for a watch-only account. +func serializeWatchOnlyAccountRow(encryptedPubKey []byte, masterKeyFingerprint, + nextExternalIndex, nextInternalIndex uint32, name string, + addrSchema *ScopeAddrSchema) ([]byte, error) { + + // The serialized BIP0044 account raw data format is: + // + // + // + // 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes master key + // fingerprint + 4 bytes next external index + 4 bytes next internal + // index + 4 bytes name len + name + 1 byte addr schema exists + 2 bytes + // addr schema (if exists) + pubLen := uint32(len(encryptedPubKey)) + nameLen := uint32(len(name)) + + addrSchemaExists := addrSchema != nil + var addrSchemaBytes []byte + if addrSchemaExists { + addrSchemaBytes = scopeSchemaToBytes(addrSchema) + } + + bufLen := 21 + pubLen + nameLen + uint32(len(addrSchemaBytes)) + buf := bytes.NewBuffer(make([]byte, 0, bufLen)) + + err := binary.Write(buf, binary.LittleEndian, pubLen) + if err != nil { + return nil, err + } + err = binary.Write(buf, binary.LittleEndian, encryptedPubKey) + if err != nil { + return nil, err + } + + err = binary.Write(buf, binary.LittleEndian, masterKeyFingerprint) + if err != nil { + return nil, err + } + + err = binary.Write(buf, binary.LittleEndian, nextExternalIndex) + if err != nil { + return nil, err + } + err = binary.Write(buf, binary.LittleEndian, nextInternalIndex) + if err != nil { + return nil, err + } + + err = binary.Write(buf, binary.LittleEndian, nameLen) + if err != nil { + return nil, err + } + err = binary.Write(buf, binary.LittleEndian, []byte(name)) + if err != nil { + return nil, err + } + + err = binary.Write(buf, binary.LittleEndian, addrSchemaExists) + if err != nil { + return nil, err + } + if addrSchemaExists { + err = binary.Write(buf, binary.LittleEndian, addrSchemaBytes) + if err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} + // forEachKeyScope calls the given function for each known manager scope // within the set of scopes known by the root manager. func forEachKeyScope(ns walletdb.ReadBucket, fn func(KeyScope) error) error { @@ -947,6 +1119,8 @@ func fetchAccountInfo(ns walletdb.ReadBucket, scope *KeyScope, switch row.acctType { case accountDefault: return deserializeDefaultAccountRow(accountID, row) + case accountWatchOnly: + return deserializeWatchOnlyAccountRow(accountID, row) } str := fmt.Sprintf("unsupported account type '%d'", row.acctType) @@ -1087,8 +1261,9 @@ func putAccountRow(ns walletdb.ReadWriteBucket, scope *KeyScope, return nil } -// putAccountInfo stores the provided account information to the database. -func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, +// putDefaultAccountInfo stores the provided default account information to the +// database. +func putDefaultAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, account uint32, encryptedPubKey, encryptedPrivKey []byte, nextExternalIndex, nextInternalIndex uint32, name string) error { @@ -1103,7 +1278,38 @@ func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, acctType: accountDefault, rawData: rawData, } - if err := putAccountRow(ns, scope, account, &acctRow); err != nil { + return putAccountInfo(ns, scope, account, &acctRow, name) +} + +// putWatchOnlyAccountInfo stores the provided watch-only account information to +// the database. +func putWatchOnlyAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, + account uint32, encryptedPubKey []byte, masterKeyFingerprint, + nextExternalIndex, nextInternalIndex uint32, name string, + addrSchema *ScopeAddrSchema) error { + + rawData, err := serializeWatchOnlyAccountRow( + encryptedPubKey, masterKeyFingerprint, nextExternalIndex, + nextInternalIndex, name, addrSchema, + ) + if err != nil { + return err + } + + // TODO(roasbeef): pass scope bucket directly?? + + acctRow := dbAccountRow{ + acctType: accountWatchOnly, + rawData: rawData, + } + return putAccountInfo(ns, scope, account, &acctRow, name) +} + +// putAccountInfo stores the provided account information to the database. +func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, + account uint32, acctRow *dbAccountRow, name string) error { + + if err := putAccountRow(ns, scope, account, acctRow); err != nil { return err } @@ -1113,11 +1319,7 @@ func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, } // Update account name index. - if err := putAccountNameIndex(ns, scope, account, name); err != nil { - return err - } - - return nil + return putAccountNameIndex(ns, scope, account, name) } // putLastAccount stores the provided metadata - last account - to the @@ -1479,32 +1681,64 @@ func putChainedAddress(ns walletdb.ReadWriteBucket, scope *KeyScope, if err != nil { return err } - arow, err := deserializeDefaultAccountRow(accountID, row) - if err != nil { - return err + + switch row.acctType { + case accountDefault: + arow, err := deserializeDefaultAccountRow(accountID, row) + if err != nil { + return err + } + + // Increment the appropriate next index depending on whether the + // branch is internal or external. + nextExternalIndex := arow.nextExternalIndex + nextInternalIndex := arow.nextInternalIndex + if branch == InternalBranch { + nextInternalIndex = index + 1 + } else { + nextExternalIndex = index + 1 + } + + // Reserialize the account with the updated index and store it. + row.rawData = serializeDefaultAccountRow( + arow.pubKeyEncrypted, arow.privKeyEncrypted, + nextExternalIndex, nextInternalIndex, arow.name, + ) + + case accountWatchOnly: + arow, err := deserializeWatchOnlyAccountRow(accountID, row) + if err != nil { + return err + } + + // Increment the appropriate next index depending on whether the + // branch is internal or external. + nextExternalIndex := arow.nextExternalIndex + nextInternalIndex := arow.nextInternalIndex + if branch == InternalBranch { + nextInternalIndex = index + 1 + } else { + nextExternalIndex = index + 1 + } + + // Reserialize the account with the updated index and store it. + row.rawData, err = serializeWatchOnlyAccountRow( + arow.pubKeyEncrypted, arow.masterKeyFingerprint, + nextExternalIndex, nextInternalIndex, arow.name, + arow.addrSchema, + ) + if err != nil { + return err + } } - // Increment the appropriate next index depending on whether the branch - // is internal or external. - nextExternalIndex := arow.nextExternalIndex - nextInternalIndex := arow.nextInternalIndex - if branch == InternalBranch { - nextInternalIndex = index + 1 - } else { - nextExternalIndex = index + 1 - } - - // Reserialize the account with the updated index and store it. - row.rawData = serializeDefaultAccountRow( - arow.pubKeyEncrypted, arow.privKeyEncrypted, nextExternalIndex, - nextInternalIndex, arow.name, - ) err = bucket.Put(accountID, serializeAccountRow(row)) if err != nil { str := fmt.Sprintf("failed to update next index for "+ "address %x, account %d", addressID, account) return managerError(ErrDatabase, str, err) } + return nil } @@ -1741,6 +1975,9 @@ func deletePrivateKeys(ns walletdb.ReadWriteBucket) error { str := "failed to delete account private key" return managerError(ErrDatabase, str, err) } + + // Watch-only accounts don't contain any private keys. + case accountWatchOnly: } return nil diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index e3aa97f..aa2710e 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -1678,7 +1678,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket, } // Save the information for the default account to the database. - err = putAccountInfo( + err = putDefaultAccountInfo( ns, &scope, DefaultAccountNum, acctPubEnc, acctPrivEnc, 0, 0, defaultAccountName, ) @@ -1686,7 +1686,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket, return err } - return putAccountInfo( + return putDefaultAccountInfo( ns, &scope, ImportedAddrAccount, nil, nil, 0, 0, ImportedAddrAccountName, ) diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 4ffe192..09dddab 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -309,71 +309,95 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, return nil, maybeConvertDbError(err) } - // Ensure the account type is a default account. - row, ok := rowInterface.(*dbDefaultAccountRow) - if !ok { - str := fmt.Sprintf("unsupported account type %T", row) - return nil, managerError(ErrDatabase, str, nil) - } + decryptKey := func(cryptoKey EncryptorDecryptor, + encryptedKey []byte) (*hdkeychain.ExtendedKey, error) { - // Use the crypto public key to decrypt the account public extended - // key. - serializedKeyPub, err := s.rootManager.cryptoKeyPub.Decrypt(row.pubKeyEncrypted) - if err != nil { - str := fmt.Sprintf("failed to decrypt public key for account %d", - account) - return nil, managerError(ErrCrypto, str, err) - } - acctKeyPub, err := hdkeychain.NewKeyFromString(string(serializedKeyPub)) - if err != nil { - str := fmt.Sprintf("failed to create extended public key for "+ - "account %d", account) - return nil, managerError(ErrKeyChain, str, err) - } - - // Create the new account info with the known information. The rest of - // the fields are filled out below. - acctInfo := &accountInfo{ - acctName: row.name, - acctKeyEncrypted: row.privKeyEncrypted, - acctKeyPub: acctKeyPub, - nextExternalIndex: row.nextExternalIndex, - nextInternalIndex: row.nextInternalIndex, - } - - watchOnly := s.rootManager.watchOnly() || len(acctInfo.acctKeyEncrypted) == 0 - private := !s.rootManager.isLocked() && !watchOnly - if private { - // Use the crypto private key to decrypt the account private - // extended keys. - decrypted, err := s.rootManager.cryptoKeyPriv.Decrypt(acctInfo.acctKeyEncrypted) + serializedKey, err := cryptoKey.Decrypt(encryptedKey) if err != nil { - str := fmt.Sprintf("failed to decrypt private key for "+ + return nil, err + } + return hdkeychain.NewKeyFromString(string(serializedKey)) + } + + // The wallet will only contain private keys for default accounts if the + // wallet's not set up as watch-only and it's been unlocked. + watchOnly := s.rootManager.watchOnly() + hasPrivateKey := !s.rootManager.isLocked() && !watchOnly + + // Create the new account info with the known information. The rest of + // the fields are filled out below. + var acctInfo *accountInfo + switch row := rowInterface.(type) { + case *dbDefaultAccountRow: + acctInfo = &accountInfo{ + acctName: row.name, + acctKeyEncrypted: row.privKeyEncrypted, + nextExternalIndex: row.nextExternalIndex, + nextInternalIndex: row.nextInternalIndex, + } + + // Use the crypto public key to decrypt the account public + // extended key. + acctInfo.acctKeyPub, err = decryptKey( + s.rootManager.cryptoKeyPub, row.pubKeyEncrypted, + ) + if err != nil { + str := fmt.Sprintf("failed to decrypt public key for "+ "account %d", account) return nil, managerError(ErrCrypto, str, err) } - acctKeyPriv, err := hdkeychain.NewKeyFromString(string(decrypted)) - if err != nil { - str := fmt.Sprintf("failed to create extended private "+ - "key for account %d", account) - return nil, managerError(ErrKeyChain, str, err) + if hasPrivateKey { + // Use the crypto private key to decrypt the account + // private extended keys. + acctInfo.acctKeyPriv, err = decryptKey( + s.rootManager.cryptoKeyPriv, row.privKeyEncrypted, + ) + if err != nil { + str := fmt.Sprintf("failed to decrypt private "+ + "key for account %d", account) + return nil, managerError(ErrCrypto, str, err) + } } - acctInfo.acctKeyPriv = acctKeyPriv + + case *dbWatchOnlyAccountRow: + acctInfo = &accountInfo{ + acctName: row.name, + nextExternalIndex: row.nextExternalIndex, + nextInternalIndex: row.nextInternalIndex, + } + + // Use the crypto public key to decrypt the account public + // extended key. + acctInfo.acctKeyPub, err = decryptKey( + s.rootManager.cryptoKeyPub, row.pubKeyEncrypted, + ) + if err != nil { + str := fmt.Sprintf("failed to decrypt public key for "+ + "account %d", account) + return nil, managerError(ErrCrypto, str, err) + } + + watchOnly = true + hasPrivateKey = false + + default: + str := fmt.Sprintf("unsupported account type %T", row) + return nil, managerError(ErrDatabase, str, nil) } // Derive and cache the managed address for the last external address. - branch, index := ExternalBranch, row.nextExternalIndex + branch, index := ExternalBranch, acctInfo.nextExternalIndex if index > 0 { index-- } lastExtAddrPath := DerivationPath{ InternalAccount: account, - Account: acctKeyPub.ChildIndex(), + Account: acctInfo.acctKeyPub.ChildIndex(), Branch: branch, Index: index, } - lastExtKey, err := s.deriveKey(acctInfo, branch, index, private) + lastExtKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey) if err != nil { return nil, err } @@ -384,17 +408,17 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, acctInfo.lastExternalAddr = lastExtAddr // Derive and cache the managed address for the last internal address. - branch, index = InternalBranch, row.nextInternalIndex + branch, index = InternalBranch, acctInfo.nextInternalIndex if index > 0 { index-- } lastIntAddrPath := DerivationPath{ InternalAccount: account, - Account: acctKeyPub.ChildIndex(), + Account: acctInfo.acctKeyPub.ChildIndex(), Branch: branch, Index: index, } - lastIntKey, err := s.deriveKey(acctInfo, branch, index, private) + lastIntKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey) if err != nil { return nil, err } @@ -1371,7 +1395,7 @@ func (s *ScopedKeyManager) newAccount(ns walletdb.ReadWriteBucket, // We have the encrypted account extended keys, so save them to the // database - err = putAccountInfo( + err = putDefaultAccountInfo( ns, &s.scope, account, acctPubEnc, acctPrivEnc, 0, 0, name, ) if err != nil { @@ -1435,8 +1459,9 @@ func (s *ScopedKeyManager) newAccountWatchingOnly(ns walletdb.ReadWriteBucket, a // We have the encrypted account extended keys, so save them to the // database - err = putAccountInfo( - ns, &s.scope, account, acctPubEnc, nil, 0, 0, name, + // TODO: set master key fingerprint and addr schema. + err = putWatchOnlyAccountInfo( + ns, &s.scope, account, acctPubEnc, 0, 0, 0, name, nil, ) if err != nil { return err @@ -1478,29 +1503,45 @@ func (s *ScopedKeyManager) RenameAccount(ns walletdb.ReadWriteBucket, return err } - // Ensure the account type is a default account. - row, ok := rowInterface.(*dbDefaultAccountRow) - if !ok { - str := fmt.Sprintf("unsupported account type %T", row) - err = managerError(ErrDatabase, str, nil) - } - // Remove the old name key from the account id index. if err = deleteAccountIDIndex(ns, &s.scope, account); err != nil { return err } - // Remove the old name key from the account name index. - if err = deleteAccountNameIndex(ns, &s.scope, row.name); err != nil { - return err - } - err = putAccountInfo( - ns, &s.scope, account, row.pubKeyEncrypted, - row.privKeyEncrypted, row.nextExternalIndex, - row.nextInternalIndex, name, - ) - if err != nil { - return err + switch row := rowInterface.(type) { + case *dbDefaultAccountRow: + // Remove the old name key from the account name index. + if err = deleteAccountNameIndex(ns, &s.scope, row.name); err != nil { + return err + } + + err = putDefaultAccountInfo( + ns, &s.scope, account, row.pubKeyEncrypted, + row.privKeyEncrypted, row.nextExternalIndex, + row.nextInternalIndex, name, + ) + if err != nil { + return err + } + + case *dbWatchOnlyAccountRow: + // Remove the old name key from the account name index. + if err = deleteAccountNameIndex(ns, &s.scope, row.name); err != nil { + return err + } + + err = putWatchOnlyAccountInfo( + ns, &s.scope, account, row.pubKeyEncrypted, + row.masterKeyFingerprint, row.nextExternalIndex, + row.nextInternalIndex, name, row.addrSchema, + ) + if err != nil { + return err + } + + default: + str := fmt.Sprintf("unsupported account type %T", row) + return managerError(ErrDatabase, str, nil) } // Update in-memory account info with new name if cached and the db