This creates a subtype BIP39KeyManager and moves all existing KeyMana… (#988)

* This creates a subtype BIP39KeyManager and moves all existing KeyManager functionality to that subtype. We leave a empty 'KeyManager' trait for now

* Fix website docs
This commit is contained in:
Chris Stewart 2019-12-27 10:09:04 -06:00 committed by GitHub
parent 31a233f0c5
commit c3fb7c9a67
15 changed files with 206 additions and 46 deletions

View File

@ -6,6 +6,7 @@ import java.nio.file.Files
import akka.actor.ActorSystem
import org.bitcoins.chain.config.ChainAppConfig
import org.bitcoins.core.api.ChainQueryApi
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.keymanager.{KeyManager, KeyManagerInitializeError}
import org.bitcoins.node.config.NodeAppConfig
import org.bitcoins.node.models.Peer
@ -99,14 +100,14 @@ object Main extends App {
val locked = LockedWallet(nodeApi, chainQueryApi)
// TODO change me when we implement proper password handling
locked.unlock(KeyManager.badPassphrase) match {
locked.unlock(BIP39KeyManager.badPassphrase) match {
case Right(wallet) => Future.successful(wallet)
case Left(kmError) => error(kmError)
}
} else {
logger.info(s"Initializing key manager")
val keyManagerE: Either[KeyManagerInitializeError, KeyManager] =
KeyManager.initialize(walletConf.kmParams)
val keyManagerE: Either[KeyManagerInitializeError, BIP39KeyManager] =
BIP39KeyManager.initialize(walletConf.kmParams)
val keyManager = keyManagerE match {
case Right(keyManager) => keyManager

View File

@ -111,7 +111,8 @@ val syncF: Future[ChainApi] = configF.flatMap { _ =>
//initialize our key manager, where we store our keys
import org.bitcoins.keymanager._
val keyManager = KeyManager.initialize(walletConfig.kmParams).getOrElse {
import org.bitcoins.keymanager.bip39._
val keyManager = BIP39KeyManager.initialize(walletConfig.kmParams).getOrElse {
throw new RuntimeException(s"Failed to initalize key manager")
}

View File

@ -8,7 +8,11 @@ title: Key Manager
The key manager module's goal is to encapusulate all private key interactions with the [wallet](../applications/wallet.md) project.
As of this writing, the wallet just delegates storage of the encrypted mnemonic seed to the key manager project. Over the long run, we want to make it so that the wallet project needs to communicate with the key-manager to access private keys.
As of this writing, there is only one type of `KeyManager` - [`BIP39KeyManager`](../../key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala).
The [`BIP39KeyManager`](../../key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala) stores a [`MnemonicCode`](../../core/src/main/scala/org/bitcoins/core/crypto/MnemonicCode.scala) on disk which can be decrypted and used as a hot wallet.
Over the long run, we want to make it so that the wallet project needs to communicate with the key-manager to access private keys.
This means that ALL SIGNING should be done inside of the key-manager, and private keys should not leave the key manager.
@ -58,6 +62,8 @@ import org.bitcoins.core.hd._
import org.bitcoins.keymanager._
import org.bitcoins.keymanager.bip39._
import java.nio.file._
//this will create a temp directory with the prefix 'key-manager-example` that will
@ -72,7 +78,7 @@ val network = RegTest
val kmParams = KeyManagerParams(seedPath, purpose, network)
val km = KeyManager.initializeWithMnemonic(mnemonic, kmParams)
val km = BIP39KeyManager.initializeWithMnemonic(mnemonic, kmParams)
val rootXPub = km.right.get.getRootXPub

View File

@ -1,11 +1,12 @@
package org.bitcoins.keymanager
package org.bitcoins.keymanager.bip39
import org.bitcoins.core.config.MainNet
import org.bitcoins.core.crypto.{DoubleSha256DigestBE, MnemonicCode}
import org.bitcoins.core.hd._
import org.bitcoins.keymanager._
import scodec.bits.BitVector
class KeyManagerTest extends KeyManagerUnitTest {
class BIP39KeyManagerTest extends KeyManagerUnitTest {
val purpose = HDPurposes.Legacy
//this is taken from 'trezor-addresses.json' which give us test cases that conform with trezor
val mnemonicStr ="stage boring net gather radar radio arrest eye ask risk girl country"
@ -45,11 +46,11 @@ class KeyManagerTest extends KeyManagerUnitTest {
it must "initialize a key manager to the same xpub if we call constructor directly or use CreateKeyManagerApi" in {
val kmParams = buildParams()
val direct = KeyManager(mnemonic, kmParams)
val direct = BIP39KeyManager(mnemonic, kmParams)
val directXpub = direct.getRootXPub
val api = KeyManager.initializeWithEntropy(mnemonic.toEntropy, kmParams).right.get
val api = BIP39KeyManager.initializeWithEntropy(mnemonic.toEntropy, kmParams).right.get
val apiXpub = api.getRootXPub
@ -62,7 +63,7 @@ class KeyManagerTest extends KeyManagerUnitTest {
it must "return a mnemonic not found if we have not initialized the key manager" in {
val kmParams = buildParams()
val kmE = KeyManager.fromParams(kmParams, KeyManager.badPassphrase)
val kmE = BIP39KeyManager.fromParams(kmParams, BIP39KeyManager.badPassphrase)
assert(kmE == Left(ReadMnemonicError.NotFoundError))
}
@ -79,7 +80,7 @@ class KeyManagerTest extends KeyManagerUnitTest {
val badEntropy = BitVector.empty
val init = KeyManager.initializeWithEntropy(badEntropy, buildParams())
val init = BIP39KeyManager.initializeWithEntropy(badEntropy, buildParams())
assert(init == Left(InitializeKeyManagerError.BadEntropy))
}

View File

@ -1,13 +1,14 @@
package org.bitcoins.keymanager
package org.bitcoins.keymanager.bip39
import org.bitcoins.core.crypto.AesPassword
import org.bitcoins.keymanager.{KeyManagerTestUtil, KeyManagerUnitTest, KeyManagerUnlockError}
class LockedKeyManagerTest extends KeyManagerUnitTest {
class BIP39LockedKeyManagerTest extends KeyManagerUnitTest {
it must "be able to read a locked mnemonic from disk" in {
val km = withInitializedKeyManager()
val unlockedKm = LockedKeyManager.unlock(KeyManagerTestUtil.badPassphrase, km.kmParams) match {
val unlockedKm = BIP39LockedKeyManager.unlock(KeyManagerTestUtil.badPassphrase, km.kmParams) match {
case Right(km) => km
case Left(err) => fail(s"Failed to unlock key manager ${err}")
}
@ -19,7 +20,7 @@ class LockedKeyManagerTest extends KeyManagerUnitTest {
it must "fail to read bad json in the seed file" in {
val km = withInitializedKeyManager()
val badPassword = AesPassword.fromString("other bad password").get
LockedKeyManager.unlock(passphrase = badPassword, kmParams = km.kmParams) match {
BIP39LockedKeyManager.unlock(passphrase = badPassword, kmParams = km.kmParams) match {
case Left(KeyManagerUnlockError.BadPassword) => succeed
case result @ (Left(_) | Right(_)) =>
fail(s"Expected to fail test with ${KeyManagerUnlockError.BadPassword} got ${result}")
@ -33,7 +34,7 @@ class LockedKeyManagerTest extends KeyManagerUnitTest {
val badPath = km.kmParams.copy(seedPath = badSeedPath)
val badPassword = AesPassword.fromString("other bad password").get
LockedKeyManager.unlock(badPassword, badPath) match {
BIP39LockedKeyManager.unlock(badPassword, badPath) match {
case Left(KeyManagerUnlockError.MnemonicNotFound) => succeed
case result @ (Left(_) | Right(_)) =>
fail(s"Expected to fail test with ${KeyManagerUnlockError.MnemonicNotFound} got ${result}")

View File

@ -16,14 +16,13 @@ import scodec.bits.BitVector
* can write it down. They should also be prompted
* to confirm at least parts of the code.
*/
trait KeyManagerCreateApi {
trait KeyManagerCreateApi[T <: KeyManager] {
/**
* $initialize
*/
final def initialize(kmParams: KeyManagerParams): Either[
KeyManagerInitializeError,
KeyManager] =
final def initialize(
kmParams: KeyManagerParams): Either[KeyManagerInitializeError, T] =
initializeWithEntropy(entropy = MnemonicCode.getEntropy256Bits, kmParams)
/**
@ -31,7 +30,7 @@ trait KeyManagerCreateApi {
*/
def initializeWithEntropy(
entropy: BitVector,
kmParams: KeyManagerParams): Either[KeyManagerInitializeError, KeyManager]
kmParams: KeyManagerParams): Either[KeyManagerInitializeError, T]
/**
* Helper method to initialize a [[KeyManagerCreate$ KeyManager]] with a [[MnemonicCode MnemonicCode]]
@ -42,9 +41,7 @@ trait KeyManagerCreateApi {
*/
final def initializeWithMnemonic(
mnemonicCode: MnemonicCode,
kmParams: KeyManagerParams): Either[
KeyManagerInitializeError,
KeyManager] = {
kmParams: KeyManagerParams): Either[KeyManagerInitializeError, T] = {
val entropy = mnemonicCode.toEntropy
initializeWithEntropy(entropy = entropy, kmParams)
}

View File

@ -0,0 +1,145 @@
package org.bitcoins.keymanager.bip39
import java.nio.file.Files
import org.bitcoins.core.compat.{CompatEither, CompatLeft, CompatRight}
import org.bitcoins.core.crypto._
import org.bitcoins.core.hd.{HDAccount, HDPath}
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.keymanager.util.HDUtil
import org.bitcoins.keymanager._
import scodec.bits.BitVector
import scala.util.{Failure, Success, Try}
/**
* This is a key manager implementation meant to represent an in memory
* BIP39 key manager
*
* @param mnemonic the mnemonic seed used for this wallet
* @param kmParams the parameters used to generate the right keychain
* @see https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
*/
case class BIP39KeyManager(
private val mnemonic: MnemonicCode,
kmParams: KeyManagerParams)
extends KeyManager {
private val seed = BIP39Seed.fromMnemonic(mnemonic = mnemonic,
password = BIP39Seed.EMPTY_PASSWORD)
private val privVersion: ExtKeyPrivVersion =
HDUtil.getXprivVersion(kmParams.purpose, kmParams.network)
private val rootExtPrivKey = seed.toExtPrivateKey(privVersion)
/** Converts a non-sensitive DB representation of a UTXO into
* a signable (and sensitive) real-world UTXO
*/
def toSign(privKeyPath: HDPath): Sign = {
val xpriv =
rootExtPrivKey.deriveChildPrivKey(privKeyPath)
xpriv
}
def deriveXPub(account: HDAccount): Try[ExtPublicKey] = {
rootExtPrivKey.deriveChildPubKey(account)
}
/** Returns the root [[ExtPublicKey]] */
def getRootXPub: ExtPublicKey = {
rootExtPrivKey.extPublicKey
}
}
object BIP39KeyManager
extends KeyManagerCreateApi[BIP39KeyManager]
with BitcoinSLogger {
val badPassphrase = AesPassword.fromString("bad-password").get
/** Initializes the mnemonic seed and saves it to file */
override def initializeWithEntropy(
entropy: BitVector,
kmParams: KeyManagerParams): Either[
KeyManagerInitializeError,
BIP39KeyManager] = {
val seedPath = kmParams.seedPath
logger.info(s"Initializing wallet with seedPath=${seedPath}")
if (Files.notExists(seedPath)) {
logger.info(
s"Seed path parent directory does not exist, creating ${seedPath.getParent}")
Files.createDirectories(seedPath.getParent)
}
val mnemonicT = Try(MnemonicCode.fromEntropy(entropy))
val mnemonicE: CompatEither[KeyManagerInitializeError, MnemonicCode] =
mnemonicT match {
case Success(mnemonic) =>
logger.info(s"Created mnemonic from entropy")
CompatEither(Right(mnemonic))
case Failure(err) =>
logger.error(s"Could not create mnemonic from entropy! $err")
CompatEither(Left(InitializeKeyManagerError.BadEntropy))
}
val encryptedMnemonicE: CompatEither[
KeyManagerInitializeError,
EncryptedMnemonic] =
mnemonicE.map { EncryptedMnemonicHelper.encrypt(_, badPassphrase) }
val writeToDiskE: CompatEither[KeyManagerInitializeError, KeyManager] =
for {
mnemonic <- mnemonicE
encrypted <- encryptedMnemonicE
_ = {
val mnemonicPath =
WalletStorage.writeMnemonicToDisk(seedPath, encrypted)
logger.info(s"Saved encrypted wallet mnemonic to $mnemonicPath")
}
} yield BIP39KeyManager(mnemonic = mnemonic, kmParams = kmParams)
//verify we can unlock it for a sanity check
val unlocked = BIP39LockedKeyManager.unlock(badPassphrase, kmParams)
val biasedFinalE: CompatEither[KeyManagerInitializeError, BIP39KeyManager] =
for {
kmBeforeWrite <- writeToDiskE
invariant <- unlocked match {
case Right(unlockedKeyManager) =>
require(kmBeforeWrite == unlockedKeyManager,
s"We could not read the key manager we just wrote!")
CompatRight(unlockedKeyManager)
case Left(err) =>
CompatLeft(InitializeKeyManagerError.FailedToReadWrittenSeed(err))
}
} yield {
invariant
}
biasedFinalE match {
case CompatRight(initSuccess) =>
logger.info(s"Successfully initialized wallet")
Right(initSuccess)
case CompatLeft(err) =>
logger.error(s"Failed to initialize key manager with err=${err}")
Left(err)
}
}
/** Reads the key manager from disk and decrypts it with the given password */
def fromParams(
kmParams: KeyManagerParams,
password: AesPassword): Either[ReadMnemonicError, BIP39KeyManager] = {
val mnemonicCodeE =
WalletStorage.decryptMnemonicFromDisk(kmParams.seedPath, password)
mnemonicCodeE match {
case Right(mnemonic) => Right(new BIP39KeyManager(mnemonic, kmParams))
case Left(v) => Left(v)
}
}
}

View File

@ -1,4 +1,4 @@
package org.bitcoins.keymanager
package org.bitcoins.keymanager.bip39
import org.bitcoins.core.crypto.AesPassword
import org.bitcoins.core.util.BitcoinSLogger
@ -6,9 +6,10 @@ import org.bitcoins.keymanager.ReadMnemonicError.{
DecryptionError,
JsonParsingError
}
import org.bitcoins.keymanager._
/** Represents a */
object LockedKeyManager extends BitcoinSLogger {
object BIP39LockedKeyManager extends BitcoinSLogger {
/**
* Unlock the wallet by decrypting the [[EncryptedMnemonic]] seed
@ -16,15 +17,15 @@ object LockedKeyManager extends BitcoinSLogger {
* @param kmParams parameters needed to create the key manager
*
* */
def unlock(
passphrase: AesPassword,
kmParams: KeyManagerParams): Either[KeyManagerUnlockError, KeyManager] = {
def unlock(passphrase: AesPassword, kmParams: KeyManagerParams): Either[
KeyManagerUnlockError,
BIP39KeyManager] = {
logger.debug(s"Trying to unlock wallet with seedPath=${kmParams.seedPath}")
val resultE =
WalletStorage.decryptMnemonicFromDisk(kmParams.seedPath, passphrase)
resultE match {
case Right(mnemonicCode) =>
Right(new KeyManager(mnemonicCode, kmParams))
Right(new BIP39KeyManager(mnemonicCode, kmParams))
case Left(result) =>
result match {
case DecryptionError =>

View File

@ -3,6 +3,7 @@ package org.bitcoins.keymanager
import java.nio.file.Path
import org.bitcoins.core.crypto.AesPassword
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.testkit.BitcoinSTestAppConfig
object KeyManagerTestUtil {
@ -14,5 +15,5 @@ object KeyManagerTestUtil {
.resolve(WalletStorage.ENCRYPTED_SEED_FILE_NAME)
}
val badPassphrase: AesPassword = KeyManager.badPassphrase
val badPassphrase: AesPassword = BIP39KeyManager.badPassphrase
}

View File

@ -3,6 +3,7 @@ package org.bitcoins.keymanager
import org.bitcoins.core.config.Networks
import org.bitcoins.core.crypto.MnemonicCode
import org.bitcoins.core.hd.HDPurposes
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.scalacheck.Gen
import scodec.bits.BitVector
@ -18,8 +19,8 @@ trait KeyManagerUnitTest extends BitcoinSUnitTest {
def withInitializedKeyManager(
kmParams: KeyManagerParams = createKeyManagerParams(),
entropy: BitVector = MnemonicCode.getEntropy256Bits): KeyManager = {
val kmResult = KeyManager.initializeWithEntropy(
entropy: BitVector = MnemonicCode.getEntropy256Bits): BIP39KeyManager = {
val kmResult = BIP39KeyManager.initializeWithEntropy(
entropy = entropy,
kmParams = kmParams
)

View File

@ -10,6 +10,7 @@ import org.bitcoins.core.protocol.BlockStamp
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.db.AppConfig
import org.bitcoins.keymanager.KeyManager
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.rpc.client.common.{BitcoindRpcClient, BitcoindVersion}
import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.server.BitcoinSAppConfig._
@ -192,8 +193,8 @@ object BitcoinSWalletTest extends WalletLogger {
bitcoind: BitcoindRpcClient)
private def createNewKeyManager()(
implicit config: BitcoinSAppConfig): KeyManager = {
val keyManagerE = KeyManager.initialize(config.walletConf.kmParams)
implicit config: BitcoinSAppConfig): BIP39KeyManager = {
val keyManagerE = BIP39KeyManager.initialize(config.walletConf.kmParams)
keyManagerE match {
case Right(keyManager) => keyManager
case Left(err) =>
@ -208,7 +209,7 @@ object BitcoinSWalletTest extends WalletLogger {
* or account type.
*/
private def createNewWallet(
keyManager: KeyManager,
keyManager: BIP39KeyManager,
extraConfig: Option[Config],
nodeApi: NodeApi,
chainQueryApi: ChainQueryApi)(

View File

@ -7,6 +7,7 @@ import org.bitcoins.core.hd.HDChainType.{Change, External}
import org.bitcoins.core.hd._
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.keymanager.{KeyManager, KeyManagerParams}
import org.bitcoins.rpc.serializers.JsonSerializers._
import org.bitcoins.testkit.BitcoinSTestAppConfig
@ -135,7 +136,7 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture {
}
private def getWallet(config: WalletAppConfig)(implicit ec: ExecutionContext): Future[Wallet] = {
val kmE = KeyManager.initializeWithEntropy(mnemonic.toEntropy, config.kmParams)
val kmE = BIP39KeyManager.initializeWithEntropy(mnemonic.toEntropy, config.kmParams)
kmE match {
case Left(err) => Future.failed(new RuntimeException(s"Failed to initialize km with err=${err}"))
case Right(km) =>

View File

@ -10,6 +10,7 @@ import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.wallet.builder.BitcoinTxBuilder
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.BitcoinUTXOSpendingInfo
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.keymanager.util.HDUtil
import org.bitcoins.keymanager.{KeyManager, KeyManagerParams}
import org.bitcoins.wallet.api._
@ -141,7 +142,7 @@ sealed abstract class Wallet extends LockedWallet with UnlockedWalletApi {
object Wallet extends WalletLogger {
private case class WalletImpl(
override val keyManager: KeyManager,
override val keyManager: BIP39KeyManager,
override val nodeApi: NodeApi,
override val chainQueryApi: ChainQueryApi
)(
@ -150,7 +151,7 @@ object Wallet extends WalletLogger {
) extends Wallet
def apply(
keyManager: KeyManager,
keyManager: BIP39KeyManager,
nodeApi: NodeApi,
chainQueryApi: ChainQueryApi)(
implicit config: WalletAppConfig,
@ -159,7 +160,7 @@ object Wallet extends WalletLogger {
}
/** Creates the level 0 account for the given HD purpose */
private def createRootAccount(wallet: Wallet, keyManager: KeyManager)(
private def createRootAccount(wallet: Wallet, keyManager: BIP39KeyManager)(
implicit walletAppConfig: WalletAppConfig,
ec: ExecutionContext): Future[AccountDb] = {
val coinType = HDUtil.getCoinType(keyManager.kmParams.network)
@ -193,7 +194,8 @@ object Wallet extends WalletLogger {
//we need to create key manager params for each purpose
//and then initialize a key manager to derive the correct xpub
val kmParams = wallet.keyManager.kmParams.copy(purpose = purpose)
val kmE = KeyManager.fromParams(kmParams, KeyManager.badPassphrase)
val kmE =
BIP39KeyManager.fromParams(kmParams, BIP39KeyManager.badPassphrase)
kmE match {
case Right(km) => createRootAccount(wallet = wallet, keyManager = km)
case Left(err) =>

View File

@ -16,6 +16,7 @@ import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.keymanager._
import org.bitcoins.keymanager.bip39.{BIP39KeyManager, BIP39LockedKeyManager}
import org.bitcoins.wallet.Wallet
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.wallet.models.{AccountDb, AddressDb, SpendingInfoDb}
@ -187,7 +188,7 @@ trait LockedWalletApi extends WalletApi {
val kmParams = walletConfig.kmParams
val unlockedKeyManagerE =
LockedKeyManager.unlock(passphrase, kmParams)
BIP39LockedKeyManager.unlock(passphrase, kmParams)
unlockedKeyManagerE match {
case Right(km) =>
val w = Wallet(keyManager = km,
@ -211,7 +212,7 @@ trait LockedWalletApi extends WalletApi {
trait UnlockedWalletApi extends LockedWalletApi {
def keyManager: KeyManager
def keyManager: BIP39KeyManager
/**
* Locks the wallet. After this operation is called,

View File

@ -12,7 +12,7 @@ import org.bitcoins.core.protocol.transaction.{
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.wallet.utxo.{BitcoinUTXOSpendingInfo, ConditionalPath}
import org.bitcoins.db.{DbRowAutoInc, TableAutoInc}
import org.bitcoins.keymanager.{KeyManager}
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import slick.jdbc.SQLiteProfile.api._
import slick.lifted.ProvenShape
@ -128,7 +128,7 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
*/
def toUTXOSpendingInfo(
account: AccountDb,
keyManager: KeyManager,
keyManager: BIP39KeyManager,
network: NetworkParameters): BitcoinUTXOSpendingInfo = {
val sign: Sign = keyManager.toSign(privKeyPath = privKeyPath)