From 47a38f88db470266a1a0485f5b9190431efbb36c Mon Sep 17 00:00:00 2001 From: Ben Carman Date: Fri, 6 Nov 2020 07:00:18 -0600 Subject: [PATCH] Make aesPassword option for wallet config (#2217) * Make aesPassword option for wallet config * Add to docs * Make AesPassword optional * Small touchups * Fix for oracle server * Fix docs * Increase code coverage --- .../oracle/server/OracleServerMain.scala | 5 +- .../api/keymanager/KeyManagerCreateApi.scala | 12 +- .../org/bitcoins/core/crypto/BIP39Seed.scala | 14 + .../org/bitcoins/dlc/oracle/DLCOracle.scala | 11 +- .../dlc/oracle/DLCOracleAppConfig.scala | 19 +- docs/chain/chain-query-api.md | 5 +- docs/config/configuration.md | 3 + docs/key-manager/key-manager.md | 6 +- docs/node/node-api.md | 6 +- docs/wallet/wallet-callbacks.md | 5 +- docs/wallet/wallet.md | 3 +- .../keymanager/WalletStorageTest.scala | 301 ++++++++++++++---- .../bip39/BIP39KeyManagerApiTest.scala | 47 ++- .../bip39/BIP39LockedKeyManagerApiTest.scala | 12 +- .../keymanager/EncryptedMnemonic.scala | 12 +- .../bitcoins/keymanager/WalletStorage.scala | 204 +++++++++--- .../keymanager/bip39/BIP39KeyManager.scala | 48 ++- .../bip39/BIP39LockedKeyManager.scala | 4 +- .../testkit/BitcoinSTestAppConfig.scala | 14 +- .../fixtures/DLCOracleDAOFixture.scala | 3 +- .../testkit/fixtures/DLCOracleFixture.scala | 3 +- .../keymanager/KeyManagerApiUnitTest.scala | 3 + .../keymanager/KeyManagerTestUtil.scala | 6 +- .../testkit/wallet/BitcoinSWalletTest.scala | 3 +- .../bitcoins/wallet/TrezorAddressTest.scala | 2 + .../org/bitcoins/wallet/WalletUnitTest.scala | 22 +- .../scala/org/bitcoins/wallet/Wallet.scala | 11 +- .../wallet/config/WalletAppConfig.scala | 17 +- 28 files changed, 586 insertions(+), 215 deletions(-) diff --git a/app/oracle-server/src/main/scala/org/bitcoins/oracle/server/OracleServerMain.scala b/app/oracle-server/src/main/scala/org/bitcoins/oracle/server/OracleServerMain.scala index d9b6576c11..a1395780ff 100644 --- a/app/oracle-server/src/main/scala/org/bitcoins/oracle/server/OracleServerMain.scala +++ b/app/oracle-server/src/main/scala/org/bitcoins/oracle/server/OracleServerMain.scala @@ -1,8 +1,6 @@ package org.bitcoins.oracle.server -import org.bitcoins.crypto.AesPassword import org.bitcoins.dlc.oracle.DLCOracleAppConfig -import org.bitcoins.keymanager.bip39.BIP39KeyManager import org.bitcoins.server.{BitcoinSRunner, Server} import scala.concurrent.Future @@ -19,10 +17,9 @@ class OracleServerMain(override val args: Array[String]) // TODO need to prompt user for these val bip39PasswordOpt: Option[String] = None - val aesPassword: AesPassword = BIP39KeyManager.badPassphrase for { _ <- conf.start() - oracle <- conf.initialize(aesPassword, bip39PasswordOpt) + oracle <- conf.initialize(bip39PasswordOpt) routes = Seq(OracleRoutes(oracle)) server = rpcPortOpt match { diff --git a/core/src/main/scala/org/bitcoins/core/api/keymanager/KeyManagerCreateApi.scala b/core/src/main/scala/org/bitcoins/core/api/keymanager/KeyManagerCreateApi.scala index 7189c1f1d7..64f28704ba 100644 --- a/core/src/main/scala/org/bitcoins/core/api/keymanager/KeyManagerCreateApi.scala +++ b/core/src/main/scala/org/bitcoins/core/api/keymanager/KeyManagerCreateApi.scala @@ -5,6 +5,7 @@ import org.bitcoins.core.wallet.keymanagement.{ KeyManagerInitializeError, KeyManagerParams } +import org.bitcoins.crypto.AesPassword import scodec.bits.BitVector trait KeyManagerCreateApi @@ -29,9 +30,11 @@ trait BIP39KeyManagerCreateApi[T <: BIP39KeyManagerApi] * $initialize */ final def initialize( + aesPasswordOpt: Option[AesPassword], kmParams: KeyManagerParams, bip39PasswordOpt: Option[String]): Either[KeyManagerInitializeError, T] = - initializeWithEntropy(entropy = MnemonicCode.getEntropy256Bits, + initializeWithEntropy(aesPasswordOpt = aesPasswordOpt, + entropy = MnemonicCode.getEntropy256Bits, bip39PasswordOpt = bip39PasswordOpt, kmParams = kmParams) @@ -39,23 +42,26 @@ trait BIP39KeyManagerCreateApi[T <: BIP39KeyManagerApi] * $initializeWithEnt */ def initializeWithEntropy( + aesPasswordOpt: Option[AesPassword], entropy: BitVector, bip39PasswordOpt: Option[String], kmParams: KeyManagerParams): Either[KeyManagerInitializeError, T] /** - * Helper method to initialize a [[KeyManagerCreate$ KeyManager]] with a [[MnemonicCode MnemonicCode]] + * Helper method to initialize a [[KeyManagerApi KeyManager]] with a [[MnemonicCode MnemonicCode]] * * @param mnemonicCode * @param kmParams * @return */ final def initializeWithMnemonic( + aesPasswordOpt: Option[AesPassword], mnemonicCode: MnemonicCode, bip39PasswordOpt: Option[String], kmParams: KeyManagerParams): Either[KeyManagerInitializeError, T] = { val entropy = mnemonicCode.toEntropy - initializeWithEntropy(entropy = entropy, + initializeWithEntropy(aesPasswordOpt = aesPasswordOpt, + entropy = entropy, bip39PasswordOpt = bip39PasswordOpt, kmParams = kmParams) } diff --git a/core/src/main/scala/org/bitcoins/core/crypto/BIP39Seed.scala b/core/src/main/scala/org/bitcoins/core/crypto/BIP39Seed.scala index 54c7bd016e..51f59c0fe6 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/BIP39Seed.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/BIP39Seed.scala @@ -58,4 +58,18 @@ object BIP39Seed extends Factory[BIP39Seed] { BIP39Seed.fromBytes(ByteVector(encodedBytes)) } + /** + * Generates a [[https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki BIP32]] + * seed from a mnemonic code. An optional password can be supplied. + */ + def fromMnemonic( + mnemonic: MnemonicCode, + passwordOpt: Option[String]): BIP39Seed = { + passwordOpt match { + case Some(pass) => + fromMnemonic(mnemonic, pass) + case None => + fromMnemonic(mnemonic) + } + } } diff --git a/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracle.scala b/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracle.scala index 3696f3158b..06417bf5ce 100644 --- a/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracle.scala +++ b/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracle.scala @@ -220,19 +220,22 @@ object DLCOracle { def apply( mnemonicCode: MnemonicCode, - password: AesPassword, + passwordOpt: Option[AesPassword], bip39PasswordOpt: Option[String] = None)(implicit conf: DLCOracleAppConfig): DLCOracle = { val decryptedMnemonic = DecryptedMnemonic(mnemonicCode, TimeUtil.now) - val encrypted = decryptedMnemonic.encrypt(password) + val toWrite = passwordOpt match { + case Some(password) => decryptedMnemonic.encrypt(password) + case None => decryptedMnemonic + } if (!conf.seedExists()) { - WalletStorage.writeMnemonicToDisk(conf.seedPath, encrypted) + WalletStorage.writeMnemonicToDisk(conf.seedPath, toWrite) } val key = WalletStorage.getPrivateKeyFromDisk(conf.seedPath, SegWitMainNetPriv, - password, + passwordOpt, bip39PasswordOpt) DLCOracle(key) } diff --git a/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracleAppConfig.scala b/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracleAppConfig.scala index 4b00d51109..09374ba09a 100644 --- a/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracleAppConfig.scala +++ b/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracleAppConfig.scala @@ -72,6 +72,11 @@ case class DLCOracleAppConfig( } } + lazy val aesPasswordOpt: Option[AesPassword] = { + val passOpt = config.getStringOrNone("bitcoin-s.oracle.aesPassword") + passOpt.flatMap(AesPassword.fromStringOpt) + } + /** Checks if our oracle as a mnemonic seed associated with it */ def seedExists(): Boolean = { WalletStorage.seedExists(seedPath) @@ -90,21 +95,23 @@ case class DLCOracleAppConfig( start().map(_ => oracle) } - def initialize( - password: AesPassword, - bip39PasswordOpt: Option[String] = None): Future[DLCOracle] = { + def initialize(bip39PasswordOpt: Option[String] = None): Future[DLCOracle] = { if (!seedExists()) { val entropy = MnemonicCode.getEntropy256Bits val mnemonicCode = MnemonicCode.fromEntropy(entropy) val decryptedMnemonic = DecryptedMnemonic(mnemonicCode, TimeUtil.now) - val encrypted = decryptedMnemonic.encrypt(password) - WalletStorage.writeMnemonicToDisk(seedPath, encrypted) + val toWrite = aesPasswordOpt match { + case Some(password) => decryptedMnemonic.encrypt(password) + case None => decryptedMnemonic + } + + WalletStorage.writeMnemonicToDisk(seedPath, toWrite) } val key = WalletStorage.getPrivateKeyFromDisk(seedPath, SegWitMainNetPriv, - password, + aesPasswordOpt, bip39PasswordOpt) val oracle = DLCOracle(key)(this) initialize(oracle) diff --git a/docs/chain/chain-query-api.md b/docs/chain/chain-query-api.md index 702e2606ef..1e637d3319 100644 --- a/docs/chain/chain-query-api.md +++ b/docs/chain/chain-query-api.md @@ -8,7 +8,7 @@ import akka.actor.ActorSystem import org.bitcoins.core.api._ import org.bitcoins.core.api.chain.ChainQueryApi import org.bitcoins.core.api.chain.ChainQueryApi.FilterResponse -import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE} +import org.bitcoins.crypto._ import org.bitcoins.core.gcs.{FilterType, GolombFilter} import org.bitcoins.core.protocol.BlockStamp import org.bitcoins.core.protocol.blockchain.Block @@ -83,7 +83,8 @@ val bitcoind = BitcoindV19RpcClient(BitcoindInstance.fromConfigFile()) val nodeApi = BitcoinSWalletTest.MockNodeApi // Create our key manager -val keyManagerE = BIP39KeyManager.initialize(kmParams = walletConf.kmParams, +val keyManagerE = BIP39KeyManager.initialize(aesPasswordOpt = Some(AesPassword.fromString("password")), + kmParams = walletConf.kmParams, bip39PasswordOpt = None) val keyManager = keyManagerE match { diff --git a/docs/config/configuration.md b/docs/config/configuration.md index 8fff0d8830..af47aa5c60 100644 --- a/docs/config/configuration.md +++ b/docs/config/configuration.md @@ -141,6 +141,9 @@ bitcoin-s { # You can optionally set a BIP 39 password # bip39password = "changeMe" + # Password that your seed is encrypted in + aespassword = changeMe + defaultAccountType = legacy # legacy, segwit, nested-segwit bloomFalsePositiveRate = 0.0001 # percentage diff --git a/docs/key-manager/key-manager.md b/docs/key-manager/key-manager.md index 782ec2ec41..9337a5006d 100644 --- a/docs/key-manager/key-manager.md +++ b/docs/key-manager/key-manager.md @@ -66,6 +66,8 @@ Now we can construct a native segwit key manager for the regtest network! ```scala mdoc:invisible import java.time.Instant +import org.bitcoins.crypto._ + import org.bitcoins.core.crypto._ import org.bitcoins.core.config._ @@ -96,7 +98,9 @@ val network = RegTest val kmParams = KeyManagerParams(seedPath, purpose, network) -val km = BIP39KeyManager.initializeWithMnemonic(mnemonic, None, kmParams) +val aesPasswordOpt = Some(AesPassword.fromString("password")) + +val km = BIP39KeyManager.initializeWithMnemonic(aesPasswordOpt, mnemonic, None, kmParams) val rootXPub = km.right.get.getRootXPub diff --git a/docs/node/node-api.md b/docs/node/node-api.md index 63ff4aaeb7..afdf2b76a2 100644 --- a/docs/node/node-api.md +++ b/docs/node/node-api.md @@ -7,7 +7,7 @@ title: Node API import akka.actor.ActorSystem import org.bitcoins.core.api._ import org.bitcoins.core.api.node._ -import org.bitcoins.crypto.DoubleSha256Digest +import org.bitcoins.crypto._ import org.bitcoins.core.protocol.blockchain.Block import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.core.wallet.fee._ @@ -60,9 +60,11 @@ implicit val walletConf: WalletAppConfig = // and a ChainApi val bitcoind = BitcoindV19RpcClient(BitcoindInstance.fromConfigFile()) val chainApi = BitcoinSWalletTest.MockChainQueryApi +val aesPasswordOpt = Some(AesPassword.fromString("password")) // Create our key manager -val keyManagerE = BIP39KeyManager.initialize(kmParams = walletConf.kmParams, +val keyManagerE = BIP39KeyManager.initialize(aesPasswordOpt = aesPasswordOpt, + kmParams = walletConf.kmParams, bip39PasswordOpt = None) val keyManager = keyManagerE match { diff --git a/docs/wallet/wallet-callbacks.md b/docs/wallet/wallet-callbacks.md index 8cbbc16fd6..b0a9e2f8d9 100644 --- a/docs/wallet/wallet-callbacks.md +++ b/docs/wallet/wallet-callbacks.md @@ -24,6 +24,7 @@ Here is an example of constructing a wallet and registering a callback, so you c ```scala mdoc:invisible import akka.actor.ActorSystem import org.bitcoins.core.api._ +import org.bitcoins.crypto._ import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.core.wallet.fee._ import org.bitcoins.feeprovider._ @@ -48,9 +49,11 @@ implicit val walletConf: WalletAppConfig = // let's use a helper method to get a v19 bitcoind // and a ChainApi val bitcoind = BitcoindV19RpcClient(BitcoindInstance.fromConfigFile()) +val aesPasswordOpt = Some(AesPassword.fromString("password")) // Create our key manager - val keyManagerE = BIP39KeyManager.initialize(kmParams = walletConf.kmParams, + val keyManagerE = BIP39KeyManager.initialize(aesPasswordOpt = aesPasswordOpt, + kmParams = walletConf.kmParams, bip39PasswordOpt = None) val keyManager = keyManagerE match { diff --git a/docs/wallet/wallet.md b/docs/wallet/wallet.md index daae7d97bb..8e7c3434ed 100644 --- a/docs/wallet/wallet.md +++ b/docs/wallet/wallet.md @@ -132,10 +132,11 @@ val syncF: Future[ChainApi] = configF.flatMap { _ => } //initialize our key manager, where we store our keys +val aesPasswordOpt = Some(AesPassword.fromString("password")) //you can add a password here if you want //val bip39PasswordOpt = Some("my-password-here") val bip39PasswordOpt = None -val keyManager = BIP39KeyManager.initialize(walletConfig.kmParams, bip39PasswordOpt).getOrElse { +val keyManager = BIP39KeyManager.initialize(aesPasswordOpt, walletConfig.kmParams, bip39PasswordOpt).getOrElse { throw new RuntimeException(s"Failed to initalize key manager") } diff --git a/key-manager-test/src/test/scala/org/bitcoins/keymanager/WalletStorageTest.scala b/key-manager-test/src/test/scala/org/bitcoins/keymanager/WalletStorageTest.scala index e56023deaa..1446a2b912 100644 --- a/key-manager-test/src/test/scala/org/bitcoins/keymanager/WalletStorageTest.scala +++ b/key-manager-test/src/test/scala/org/bitcoins/keymanager/WalletStorageTest.scala @@ -1,23 +1,18 @@ package org.bitcoins.keymanager import java.nio.file.{Files, Path} -import java.util.NoSuchElementException import org.bitcoins.core.crypto.BIP39Seed import org.bitcoins.core.crypto.ExtKeyVersion.SegWitMainNetPriv import org.bitcoins.core.util.TimeUtil import org.bitcoins.crypto.AesPassword -import org.bitcoins.keymanager.ReadMnemonicError.{ - DecryptionError, - JsonParsingError -} +import org.bitcoins.keymanager.ReadMnemonicError._ import org.bitcoins.keymanager.bip39.BIP39KeyManager import org.bitcoins.testkit.Implicits._ import org.bitcoins.testkit.core.gen.CryptoGenerators import org.bitcoins.testkit.wallet.BitcoinSWalletTest import org.bitcoins.wallet.config.WalletAppConfig import org.scalatest.{BeforeAndAfterEach, FutureOutcome} -import ujson.Value.InvalidData class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach { @@ -32,21 +27,24 @@ class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach { behavior of "WalletStorage" - val passphrase = AesPassword.fromNonEmptyString("this_is_secret") - val badPassphrase = AesPassword.fromNonEmptyString("this_is_also_secret") + val passphrase: Some[AesPassword] = Some( + AesPassword.fromNonEmptyString("this_is_secret")) + + val badPassphrase: Some[AesPassword] = Some( + AesPassword.fromNonEmptyString("this_is_also_secret")) def getAndWriteMnemonic(walletConf: WalletAppConfig): DecryptedMnemonic = { val mnemonicCode = CryptoGenerators.mnemonicCode.sampleSome val decryptedMnemonic = DecryptedMnemonic(mnemonicCode, TimeUtil.now) val encrypted = - EncryptedMnemonicHelper.encrypt(decryptedMnemonic, passphrase) + EncryptedMnemonicHelper.encrypt(decryptedMnemonic, passphrase.get) val seedPath = getSeedPath(walletConf) val _ = WalletStorage.writeMnemonicToDisk(seedPath, encrypted) decryptedMnemonic } - it must "write and read a mnemonic to disk" in { + it must "write and read an encrypted mnemonic to disk" in { walletConf: WalletAppConfig => assert(!walletConf.seedExists()) @@ -68,31 +66,77 @@ class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach { } } - it must "read a mnemonic without a creation time" in { walletConf => - val badJson = - """ - | { - | "iv":"d2aeeda5ab83d43bb0b8fe6416b12009", - | "cipherText": "003ad9acd6c3559911d7e2446dc329c869266844fda949d69fce591205ab7a32ddb0aa614b1be5963ecc5b784bb0c1454d5d757b71584d5d990ecadc3d4414b87df50ffc46a54c912f258d5ab094bbeb49f92ef02ab60c92a52b3f205ce91943dc6c21b15bfbc635c17b049a8eec4b0a341c48ea163d5384ebbd69c79ff175823e8fbb0849e5a223e243c81c7f7c5bca62a11b7396", - | "salt":"db3a6d3c88f430bf44f4a834d85255ad6b52c187c05e95fac3b427b094298028" - | } - """.stripMargin - val seedPath = getSeedPath(walletConf) - Files.write(seedPath, badJson.getBytes()) + it must "write and read an unencrypted mnemonic to disk" in { + walletConf: WalletAppConfig => + assert(!walletConf.seedExists()) + val mnemonicCode = CryptoGenerators.mnemonicCode.sampleSome + val writtenMnemonic = DecryptedMnemonic(mnemonicCode, TimeUtil.now) + val seedPath = getSeedPath(walletConf) + WalletStorage.writeMnemonicToDisk(seedPath, writtenMnemonic) - val read = - WalletStorage.decryptMnemonicFromDisk(seedPath, - BIP39KeyManager.badPassphrase) - - read match { - case Right(readMnemonic) => - assert( - readMnemonic.creationTime.getEpochSecond == WalletStorage.FIRST_BITCOIN_S_WALLET_TIME) - case Left(err) => fail(err.toString) - } + // should have been written by now + assert(walletConf.seedExists()) + val read = + WalletStorage.decryptMnemonicFromDisk(seedPath, None) + read match { + case Right(readMnemonic) => + assert(writtenMnemonic.mnemonicCode == readMnemonic.mnemonicCode) + // Need to compare using getEpochSecond because when reading an epoch second + // it will not include the milliseconds that writtenMnemonic will have + assert( + writtenMnemonic.creationTime.getEpochSecond == readMnemonic.creationTime.getEpochSecond) + case Left(err) => fail(err.toString) + } } - it must "fail to read a mnemonic with improperly formatted creation time" in { + it must "read an encrypted mnemonic without a creation time" in { + walletConf => + val badJson = + """ + | { + | "iv":"d2aeeda5ab83d43bb0b8fe6416b12009", + | "cipherText": "003ad9acd6c3559911d7e2446dc329c869266844fda949d69fce591205ab7a32ddb0aa614b1be5963ecc5b784bb0c1454d5d757b71584d5d990ecadc3d4414b87df50ffc46a54c912f258d5ab094bbeb49f92ef02ab60c92a52b3f205ce91943dc6c21b15bfbc635c17b049a8eec4b0a341c48ea163d5384ebbd69c79ff175823e8fbb0849e5a223e243c81c7f7c5bca62a11b7396", + | "salt":"db3a6d3c88f430bf44f4a834d85255ad6b52c187c05e95fac3b427b094298028" + | } + """.stripMargin + val seedPath = getSeedPath(walletConf) + Files.write(seedPath, badJson.getBytes()) + + val read = + WalletStorage.decryptMnemonicFromDisk( + seedPath, + Some(BIP39KeyManager.badPassphrase)) + + read match { + case Right(readMnemonic) => + assert( + readMnemonic.creationTime.getEpochSecond == WalletStorage.FIRST_BITCOIN_S_WALLET_TIME) + case Left(err) => fail(err.toString) + } + } + + it must "read an unencrypted mnemonic without a creation time" in { + walletConf => + val badJson = + """ + | { + | "mnemonicSeed":["stage","boring","net","gather","radar","radio","arrest","eye","ask","risk","girl","country"] + | } + """.stripMargin + val seedPath = getSeedPath(walletConf) + Files.write(seedPath, badJson.getBytes()) + + val read = WalletStorage.decryptMnemonicFromDisk(seedPath, None) + + read match { + case Right(readMnemonic) => + assert( + readMnemonic.creationTime.getEpochSecond == WalletStorage.FIRST_BITCOIN_S_WALLET_TIME) + case Left(err) => fail(err.toString) + } + } + + it must "fail to read an encrypted mnemonic with improperly formatted creation time" in { walletConf => val badJson = """ @@ -106,47 +150,93 @@ class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach { val seedPath = getSeedPath(walletConf) Files.write(seedPath, badJson.getBytes()) - assertThrows[InvalidData] { - WalletStorage.decryptMnemonicFromDisk(seedPath, - BIP39KeyManager.badPassphrase) + val read = + WalletStorage.decryptMnemonicFromDisk(seedPath, passphrase) + + read match { + case Left(JsonParsingError(_)) => succeed + case res @ (Left(_) | Right(_)) => fail(res.toString) } } - it must "fail to read a mnemonic with bad password" in { walletConf => - val _ = getAndWriteMnemonic(walletConf) - val seedPath = getSeedPath(walletConf) - val read = WalletStorage.decryptMnemonicFromDisk(seedPath, badPassphrase) - - read match { - case Right(_) => - fail("Wrote and read with different passwords") - case Left(DecryptionError) => succeed - case Left(err) => fail(err.toString) - } - } - - it must "fail to read a mnemonic that has bad JSON in it" in { walletConf => - val badJson = - """ - | { - | "iv":"ba7722683dad8067df8d069ee04530cc", - | "cipherText":, - | "salt":"2b7e7d718139518070a87fbbda03ea33cdcda83b555020e9344774e6e7d08af2" - | } + it must "fail to read an unencrypted mnemonic with improperly formatted creation time" in { + walletConf => + val badJson = + """ + | { + | "mnemonicSeed":["stage","boring","net","gather","radar","radio","arrest","eye","ask","risk","girl","country"], + | "creationTime":"not a number" + | } """.stripMargin - val seedPath = getSeedPath(walletConf) - Files.write(seedPath, badJson.getBytes()) + val seedPath = getSeedPath(walletConf) + Files.write(seedPath, badJson.getBytes()) - val read = - WalletStorage.decryptMnemonicFromDisk(seedPath, passphrase) + val read = + WalletStorage.decryptMnemonicFromDisk(seedPath, None) - read match { - case Left(JsonParsingError(_)) => succeed - case res @ (Left(_) | Right(_)) => fail(res.toString()) - } + read match { + case Left(JsonParsingError(_)) => succeed + case res @ (Left(_) | Right(_)) => fail(res.toString) + } } - it must "fail to read a mnemonic that has missing a JSON field" in { + it must "fail to read an encrypted mnemonic with bad aes password" in { + walletConf => + val _ = getAndWriteMnemonic(walletConf) + val seedPath = getSeedPath(walletConf) + val read = WalletStorage.decryptMnemonicFromDisk(seedPath, badPassphrase) + + read match { + case Right(_) => + fail("Wrote and read with different passwords") + case Left(DecryptionError) => succeed + case Left(err) => fail(err.toString) + } + } + + it must "fail to read an encrypted mnemonic that has bad JSON in it" in { + walletConf => + val badJson = + """ + | { + | "iv":"ba7722683dad8067df8d069ee04530cc", + | "cipherText":, + | "salt":"2b7e7d718139518070a87fbbda03ea33cdcda83b555020e9344774e6e7d08af2" + | } + """.stripMargin + val seedPath = getSeedPath(walletConf) + Files.write(seedPath, badJson.getBytes()) + + val read = + WalletStorage.decryptMnemonicFromDisk(seedPath, passphrase) + + read match { + case Left(JsonParsingError(_)) => succeed + case res @ (Left(_) | Right(_)) => fail(res.toString) + } + } + + it must "fail to read an unencrypted mnemonic that has bad JSON in it" in { + walletConf => + val badJson = + """ + | { + | "mnemonicSeed":, + | } + """.stripMargin + val seedPath = getSeedPath(walletConf) + Files.write(seedPath, badJson.getBytes()) + + val read = + WalletStorage.decryptMnemonicFromDisk(seedPath, None) + + read match { + case Left(JsonParsingError(_)) => succeed + case res @ (Left(_) | Right(_)) => fail(res.toString) + } + } + + it must "fail to read an encrypted mnemonic that has missing a JSON field" in { walletConf => val badJson = """ @@ -158,12 +248,35 @@ class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach { val seedPath = getSeedPath(walletConf) Files.write(seedPath, badJson.getBytes()) - assertThrows[NoSuchElementException] { + val read = WalletStorage.decryptMnemonicFromDisk(seedPath, passphrase) + + read match { + case Left(JsonParsingError(_)) => succeed + case res @ (Left(_) | Right(_)) => fail(res.toString) } } - it must "fail to read a mnemonic not in hex" in { walletConf => + it must "fail to read an unencrypted mnemonic that has missing a JSON field" in { + walletConf => + val badJson = + """ + | { + | "creationTime":1601917137 + | } + """.stripMargin + val seedPath = getSeedPath(walletConf) + Files.write(seedPath, badJson.getBytes()) + + val read = WalletStorage.decryptMnemonicFromDisk(seedPath, None) + + read match { + case Left(JsonParsingError(_)) => succeed + case res @ (Left(_) | Right(_)) => fail(res.toString) + } + } + + it must "fail to read an encrypted mnemonic not in hex" in { walletConf => val badJson = """ | { @@ -184,6 +297,19 @@ class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach { } } + it must "fail to read an unencrypted seed that doesn't exist" in { + walletConf => + require(!walletConf.seedExists()) + val seedPath = getSeedPath(walletConf) + val read = + WalletStorage.decryptMnemonicFromDisk(seedPath, None) + + read match { + case Left(NotFoundError) => succeed + case res @ (Left(_) | Right(_)) => fail(res.toString) + } + } + it must "throw an exception if we attempt to overwrite an existing seed" in { walletConf => assert(!walletConf.seedExists()) @@ -199,7 +325,7 @@ class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach { } } - it must "write and read an ExtPrivateKey from disk" in { + it must "write and read an encrypted ExtPrivateKey from disk" in { walletConf: WalletAppConfig => assert(!walletConf.seedExists()) @@ -224,4 +350,43 @@ class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach { assert(read == expected) } + + it must "write and read an unencrypted ExtPrivateKey from disk" in { + walletConf: WalletAppConfig => + assert(!walletConf.seedExists()) + val mnemonicCode = CryptoGenerators.mnemonicCode.sampleSome + val writtenMnemonic = DecryptedMnemonic(mnemonicCode, TimeUtil.now) + val seedPath = getSeedPath(walletConf) + WalletStorage.writeMnemonicToDisk(seedPath, writtenMnemonic) + + val password = getBIP39PasswordOpt().getOrElse(BIP39Seed.EMPTY_PASSWORD) + val keyVersion = SegWitMainNetPriv + + val expected = BIP39Seed + .fromMnemonic(mnemonic = writtenMnemonic.mnemonicCode, + password = password) + .toExtPrivateKey(keyVersion) + .toHardened + + // should have been written by now + assert(walletConf.seedExists()) + val read = + WalletStorage.getPrivateKeyFromDisk(seedPath, + keyVersion, + None, + Some(password)) + + assert(read == expected) + } + + it must "fail to read unencrypted ExtPrivateKey from disk that doesn't exist" in { + walletConf: WalletAppConfig => + assert(!walletConf.seedExists()) + val seedPath = getSeedPath(walletConf) + val keyVersion = SegWitMainNetPriv + + assertThrows[RuntimeException]( + WalletStorage.getPrivateKeyFromDisk(seedPath, keyVersion, None, None)) + + } } diff --git a/key-manager-test/src/test/scala/org/bitcoins/keymanager/bip39/BIP39KeyManagerApiTest.scala b/key-manager-test/src/test/scala/org/bitcoins/keymanager/bip39/BIP39KeyManagerApiTest.scala index f06c9cca2b..ee8c7eacdf 100644 --- a/key-manager-test/src/test/scala/org/bitcoins/keymanager/bip39/BIP39KeyManagerApiTest.scala +++ b/key-manager-test/src/test/scala/org/bitcoins/keymanager/bip39/BIP39KeyManagerApiTest.scala @@ -46,21 +46,23 @@ class BIP39KeyManagerApiTest extends KeyManagerApiUnitTest { it must "initialize the key manager" in { val entropy = MnemonicCode.getEntropy256Bits - val keyManager = withInitializedKeyManager(entropy = entropy) + val aesPasswordOpt = KeyManagerTestUtil.aesPasswordOpt + val keyManager = + withInitializedKeyManager(aesPasswordOpt = aesPasswordOpt, + entropy = entropy) val seedPath = keyManager.kmParams.seedPath //verify we wrote the seed assert(WalletStorage.seedExists(seedPath), "KeyManager did not write the seed to disk!") val decryptedE = - WalletStorage.decryptMnemonicFromDisk(seedPath, - KeyManagerTestUtil.badPassphrase) + WalletStorage.decryptMnemonicFromDisk(seedPath, aesPasswordOpt) val mnemonic = decryptedE match { case Right(m) => m case Left(err) => fail( - s"Failed to read mnemonic that was written by key manager with err=${err}") + s"Failed to read mnemonic that was written by key manager with err=$err") } assert(mnemonic.mnemonicCode.toEntropy == entropy, @@ -86,7 +88,8 @@ class BIP39KeyManagerApiTest extends KeyManagerApiUnitTest { val directXpub = direct.getRootXPub val api = BIP39KeyManager - .initializeWithEntropy(entropy = mnemonic.toEntropy, + .initializeWithEntropy(aesPasswordOpt = KeyManagerTestUtil.aesPasswordOpt, + entropy = mnemonic.toEntropy, bip39PasswordOpt = None, kmParams = kmParams) .getOrElse(fail()) @@ -109,7 +112,10 @@ class BIP39KeyManagerApiTest extends KeyManagerApiUnitTest { val directXpub = direct.getRootXPub val api = BIP39KeyManager - .initializeWithEntropy(mnemonic.toEntropy, Some(bip39Pw), kmParams) + .initializeWithEntropy(aesPasswordOpt = KeyManagerTestUtil.aesPasswordOpt, + mnemonic.toEntropy, + Some(bip39Pw), + kmParams) .getOrElse(fail()) val apiXpub = api.getRootXPub @@ -135,7 +141,7 @@ class BIP39KeyManagerApiTest extends KeyManagerApiUnitTest { val directXpub = direct.getRootXPub val api = BIP39KeyManager - .fromParams(kmParams, password, Some(bip39Pw)) + .fromParams(kmParams, Some(password), Some(bip39Pw)) .getOrElse(fail()) val apiXpub = api.getRootXPub @@ -156,7 +162,11 @@ class BIP39KeyManagerApiTest extends KeyManagerApiUnitTest { val directXpub = direct.getRootXPub val api = BIP39KeyManager - .initializeWithMnemonic(mnemonic, Some(bip39Pw), kmParams) + .initializeWithMnemonic(aesPasswordOpt = + KeyManagerTestUtil.aesPasswordOpt, + mnemonic, + Some(bip39Pw), + kmParams) .getOrElse(fail()) val apiXpub = api.getRootXPub @@ -190,8 +200,8 @@ class BIP39KeyManagerApiTest extends KeyManagerApiUnitTest { it must "return a mnemonic not found if we have not initialized the key manager" in { val kmParams = buildParams() val kmE = BIP39KeyManager.fromParams(kmParams = kmParams, - password = - BIP39KeyManager.badPassphrase, + passwordOpt = + Some(BIP39KeyManager.badPassphrase), bip39PasswordOpt = None) assert(kmE == Left(ReadMnemonicError.NotFoundError)) @@ -209,6 +219,7 @@ class BIP39KeyManagerApiTest extends KeyManagerApiUnitTest { val badEntropy = BitVector.empty val init = BIP39KeyManager.initializeWithEntropy( + aesPasswordOpt = KeyManagerTestUtil.aesPasswordOpt, entropy = badEntropy, bip39PasswordOpt = KeyManagerTestUtil.bip39PasswordOpt, kmParams = buildParams()) @@ -218,11 +229,13 @@ class BIP39KeyManagerApiTest extends KeyManagerApiUnitTest { it must "read an existing seed from disk if we call initialize and one already exists" in { val seedPath = KeyManagerTestUtil.tmpSeedPath + val aesPasswordOpt = KeyManagerTestUtil.aesPasswordOpt val kmParams = keymanagement.KeyManagerParams(seedPath, HDPurposes.SegWit, RegTest) val entropy = MnemonicCode.getEntropy256Bits val passwordOpt = Some(KeyManagerTestUtil.bip39Password) - val keyManager = withInitializedKeyManager(kmParams = kmParams, + val keyManager = withInitializedKeyManager(aesPasswordOpt = aesPasswordOpt, + kmParams = kmParams, entropy = entropy, bip39PasswordOpt = passwordOpt) @@ -233,7 +246,9 @@ class BIP39KeyManagerApiTest extends KeyManagerApiUnitTest { //now let's try to initialize again, our xpub should be exactly the same val keyManager2E = - BIP39KeyManager.initialize(kmParams, bip39PasswordOpt = passwordOpt) + BIP39KeyManager.initialize(aesPasswordOpt = aesPasswordOpt, + kmParams, + bip39PasswordOpt = passwordOpt) keyManager2E match { case Left(_) => fail(s"Must have been able to intiialize the key manager for test") @@ -245,11 +260,13 @@ class BIP39KeyManagerApiTest extends KeyManagerApiUnitTest { it must "fail read an existing seed from disk if it is malformed" in { val seedPath = KeyManagerTestUtil.tmpSeedPath + val aesPasswordOpt = KeyManagerTestUtil.aesPasswordOpt val kmParams = keymanagement.KeyManagerParams(seedPath, HDPurposes.SegWit, RegTest) val entropy = MnemonicCode.getEntropy256Bits val passwordOpt = Some(KeyManagerTestUtil.bip39Password) - val keyManager = withInitializedKeyManager(kmParams = kmParams, + val keyManager = withInitializedKeyManager(aesPasswordOpt = aesPasswordOpt, + kmParams = kmParams, entropy = entropy, bip39PasswordOpt = passwordOpt) @@ -261,7 +278,9 @@ class BIP39KeyManagerApiTest extends KeyManagerApiUnitTest { //now let's try to initialize again, it should fail with a JsonParsingError val keyManager2E = - BIP39KeyManager.initialize(kmParams, bip39PasswordOpt = passwordOpt) + BIP39KeyManager.initialize(aesPasswordOpt, + kmParams, + bip39PasswordOpt = passwordOpt) keyManager2E match { case Left(InitializeKeyManagerError.FailedToReadWrittenSeed(unlockErr)) => unlockErr match { diff --git a/key-manager-test/src/test/scala/org/bitcoins/keymanager/bip39/BIP39LockedKeyManagerApiTest.scala b/key-manager-test/src/test/scala/org/bitcoins/keymanager/bip39/BIP39LockedKeyManagerApiTest.scala index 7fc08787ef..8d592a9c50 100644 --- a/key-manager-test/src/test/scala/org/bitcoins/keymanager/bip39/BIP39LockedKeyManagerApiTest.scala +++ b/key-manager-test/src/test/scala/org/bitcoins/keymanager/bip39/BIP39LockedKeyManagerApiTest.scala @@ -13,10 +13,12 @@ class BIP39LockedKeyManagerApiTest extends KeyManagerApiUnitTest { it must "be able to read a locked mnemonic from disk" in { val bip39PwOpt = KeyManagerTestUtil.bip39PasswordOpt - val km = withInitializedKeyManager(bip39PasswordOpt = bip39PwOpt) + val aesPasswordOpt = KeyManagerTestUtil.aesPasswordOpt + val km = withInitializedKeyManager(aesPasswordOpt = aesPasswordOpt, + bip39PasswordOpt = bip39PwOpt) val unlockedE = - BIP39LockedKeyManager.unlock(KeyManagerTestUtil.badPassphrase, + BIP39LockedKeyManager.unlock(aesPasswordOpt, bip39PasswordOpt = bip39PwOpt, km.kmParams) @@ -31,8 +33,8 @@ class BIP39LockedKeyManagerApiTest extends KeyManagerApiUnitTest { it must "fail to read bad json in the seed file" in { val km = withInitializedKeyManager() - val badPassword = AesPassword.fromString("other bad password") - val unlockedE = BIP39LockedKeyManager.unlock(passphrase = badPassword, + val badPassword = Some(AesPassword.fromString("other bad password")) + val unlockedE = BIP39LockedKeyManager.unlock(passphraseOpt = badPassword, bip39PasswordOpt = None, kmParams = km.kmParams) @@ -49,7 +51,7 @@ class BIP39LockedKeyManagerApiTest extends KeyManagerApiUnitTest { val km = withInitializedKeyManager() val badPath = km.kmParams.copy(seedPath = badSeedPath) - val badPassword = AesPassword.fromString("other bad password") + val badPassword = Some(AesPassword.fromString("other bad password")) val unlockedE = BIP39LockedKeyManager.unlock(badPassword, None, badPath) unlockedE match { diff --git a/key-manager/src/main/scala/org/bitcoins/keymanager/EncryptedMnemonic.scala b/key-manager/src/main/scala/org/bitcoins/keymanager/EncryptedMnemonic.scala index 301d582064..69a57d0b9e 100644 --- a/key-manager/src/main/scala/org/bitcoins/keymanager/EncryptedMnemonic.scala +++ b/key-manager/src/main/scala/org/bitcoins/keymanager/EncryptedMnemonic.scala @@ -9,9 +9,12 @@ import scodec.bits.ByteVector import scala.util.{Failure, Success, Try} -case class DecryptedMnemonic( - mnemonicCode: MnemonicCode, - creationTime: Instant) { +sealed trait MnemonicState { + def creationTime: Instant +} + +case class DecryptedMnemonic(mnemonicCode: MnemonicCode, creationTime: Instant) + extends MnemonicState { def encrypt(password: AesPassword): EncryptedMnemonic = EncryptedMnemonicHelper.encrypt(this, password) @@ -20,7 +23,8 @@ case class DecryptedMnemonic( case class EncryptedMnemonic( value: AesEncryptedData, salt: AesSalt, - creationTime: Instant) { + creationTime: Instant) + extends MnemonicState { def toMnemonic(password: AesPassword): Try[MnemonicCode] = { val key = password.toKey(salt) diff --git a/key-manager/src/main/scala/org/bitcoins/keymanager/WalletStorage.scala b/key-manager/src/main/scala/org/bitcoins/keymanager/WalletStorage.scala index f1ce2b8c02..910a6f89a8 100644 --- a/key-manager/src/main/scala/org/bitcoins/keymanager/WalletStorage.scala +++ b/key-manager/src/main/scala/org/bitcoins/keymanager/WalletStorage.scala @@ -9,6 +9,7 @@ import org.bitcoins.core.crypto._ import org.bitcoins.crypto.{AesEncryptedData, AesIV, AesPassword, AesSalt} import org.slf4j.LoggerFactory import scodec.bits.ByteVector +import ujson.{Obj, Value} import scala.util.{Failure, Success, Try} @@ -35,6 +36,23 @@ object WalletStorage { val CIPHER_TEXT = "cipherText" val SALT = "salt" val CREATION_TIME = "creationTime" + val MNEMONIC_SEED = "mnemonicSeed" + } + + /** + * Writes the mnemonic to disk. + * If we encounter a file in the place we're about + * to write to, we move it to a backup location + * with the current epoch timestamp as part of + * the file name. + */ + def writeMnemonicToDisk(seedPath: Path, mnemonic: MnemonicState): Path = { + mnemonic match { + case decryptedMnemonic: DecryptedMnemonic => + writeMnemonicToDisk(seedPath, decryptedMnemonic) + case encryptedMnemonic: EncryptedMnemonic => + writeMnemonicToDisk(seedPath, encryptedMnemonic) + } } /** @@ -57,33 +75,64 @@ object WalletStorage { ) } + writeMnemonicJsonToDisk(seedPath, jsObject) + } + + /** + * Writes the unencrypted mnemonic to disk. + * If we encounter a file in the place we're about + * to write to, we move it to a backup location + * with the current epoch timestamp as part of + * the file name. + */ + def writeMnemonicToDisk(seedPath: Path, mnemonic: DecryptedMnemonic): Path = { + val mnemonicCode = mnemonic.mnemonicCode + val jsObject = { + import MnemonicJsonKeys._ + ujson.Obj( + MNEMONIC_SEED -> mnemonicCode.toVector, + CREATION_TIME -> ujson.Num( + mnemonic.creationTime.getEpochSecond.toDouble) + ) + } + + writeMnemonicJsonToDisk(seedPath, jsObject) + } + + private def writeMnemonicJsonToDisk(seedPath: Path, jsObject: Obj): Path = { logger.info(s"Writing mnemonic to $seedPath") val writtenJs = ujson.write(jsObject) - def writeJsToDisk() = { + def writeJsToDisk(): Path = { val writtenPath = Files.write(seedPath, writtenJs.getBytes()) logger.trace(s"Wrote encrypted mnemonic to $seedPath") writtenPath } - //check to see if a mnemonic exists already... - val foundMnemonicOpt: Option[EncryptedMnemonic] = - readEncryptedMnemonicFromDisk(seedPath) match { - case CompatLeft(_) => - None - case CompatRight(mnemonic) => Some(mnemonic) - } + // Check to see if a mnemonic exists already + val hasMnemonic: Boolean = Files.isRegularFile(seedPath) - foundMnemonicOpt match { - case None => - logger.trace(s"$seedPath does not exist") - writeJsToDisk() - case Some(_) => - logger.info(s"$seedPath already exists") - throw new RuntimeException( - s"Attempting to overwrite an existing mnemonic seed, this is dangerous!") + if (hasMnemonic) { + logger.info(s"$seedPath already exists") + throw new RuntimeException( + s"Attempting to overwrite an existing mnemonic seed, this is dangerous! path: $seedPath") + } else { + logger.trace(s"$seedPath does not exist") + writeJsToDisk() + } + } + + private def parseCreationTime(json: Value): Long = { + Try(json(MnemonicJsonKeys.CREATION_TIME).num.toLong) match { + case Success(value) => + value + case Failure(_: NoSuchElementException) => + // If no CREATION_TIME is set, we set date to start of bitcoin-s wallet project + // default is Block 555,990 block time on 2018-12-28 + FIRST_BITCOIN_S_WALLET_TIME + case Failure(exception) => throw exception } } @@ -122,23 +171,16 @@ object WalletStorage { ReadMnemonicError, (String, String, String, Long)] = jsonE.flatMap { json => logger.trace(s"Read encrypted mnemonic JSON: $json") - val creationTimeNum = Try(json(CREATION_TIME).num.toLong) match { - case Success(value) => - value - case Failure(err) if err.isInstanceOf[NoSuchElementException] => - // If no CREATION_TIME is set, we set date to start of bitcoin-s wallet project - // default is Block 555,990 block time on 2018-12-28 - FIRST_BITCOIN_S_WALLET_TIME - case Failure(exception) => throw exception - } Try { + val creationTimeNum = parseCreationTime(json) val ivString = json(IV).str val cipherTextString = json(CIPHER_TEXT).str val rawSaltString = json(SALT).str (ivString, cipherTextString, rawSaltString, creationTimeNum) } match { - case Success(value) => CompatRight(value) - case Failure(exception) => throw exception + case Success(value) => CompatRight(value) + case Failure(exception) => + CompatLeft(JsonParsingError(exception.getMessage)) } } @@ -166,28 +208,94 @@ object WalletStorage { encryptedEither } + /** Reads the raw unencrypted mnemonic from disk */ + private def readUnencryptedMnemonicFromDisk( + seedPath: Path): CompatEither[ReadMnemonicError, DecryptedMnemonic] = { + + val jsonE: CompatEither[ReadMnemonicError, ujson.Value] = { + if (Files.isRegularFile(seedPath)) { + val rawJson = Files.readAllLines(seedPath).asScala.mkString("\n") + logger.debug(s"Read raw mnemonic from $seedPath") + + Try(ujson.read(rawJson)) match { + case Failure(ujson.ParseException(clue, _, _, _)) => + CompatLeft(ReadMnemonicError.JsonParsingError(clue)) + case Failure(exception) => throw exception + case Success(value) => + logger.debug(s"Parsed $seedPath into valid json") + CompatRight(value) + } + } else { + logger.error(s"Mnemonic not found at $seedPath") + CompatLeft(ReadMnemonicError.NotFoundError) + } + } + + import MnemonicJsonKeys._ + import ReadMnemonicError._ + + val readJsonTupleEither: CompatEither[ + ReadMnemonicError, + (Vector[String], Long)] = jsonE.flatMap { json => + logger.trace(s"Read mnemonic JSON: Masked(json)") + Try { + val creationTimeNum = parseCreationTime(json) + val words = json(MNEMONIC_SEED).arr.toVector.map(_.str) + (words, creationTimeNum) + } match { + case Success(value) => CompatRight(value) + case Failure(exception) => + CompatLeft(JsonParsingError(exception.getMessage)) + } + } + + readJsonTupleEither.flatMap { + case (words, rawCreationTime) => + val decryptedMnemonicT = for { + mnemonicCodeT <- Try(MnemonicCode.fromWords(words)) + } yield { + logger.debug(s"Parsed contents of $seedPath into a DecryptedMnemonic") + DecryptedMnemonic(mnemonicCodeT, + Instant.ofEpochSecond(rawCreationTime)) + } + + val toRight: Try[CompatRight[ReadMnemonicError, DecryptedMnemonic]] = + decryptedMnemonicT + .map(CompatRight(_)) + + toRight.getOrElse( + CompatLeft(JsonParsingError("JSON contents was correctly formatted"))) + } + } + /** * Reads the wallet mnemonic from disk and tries to parse and * decrypt it */ def decryptMnemonicFromDisk( seedPath: Path, - passphrase: AesPassword): Either[ReadMnemonicError, DecryptedMnemonic] = { - - val encryptedEither = readEncryptedMnemonicFromDisk(seedPath) - + passphraseOpt: Option[AesPassword]): Either[ + ReadMnemonicError, + DecryptedMnemonic] = { val decryptedEither: CompatEither[ReadMnemonicError, DecryptedMnemonic] = - encryptedEither.flatMap { encrypted => - encrypted.toMnemonic(passphrase) match { - case Failure(exc) => - logger.error(s"Error when decrypting $encrypted: $exc") - CompatLeft(ReadMnemonicError.DecryptionError) - case Success(mnemonic) => - logger.debug(s"Decrypted $encrypted successfully") - val decryptedMnemonic = - DecryptedMnemonic(mnemonic, encrypted.creationTime) - CompatRight(decryptedMnemonic) - } + passphraseOpt match { + case Some(passphrase) => + val encryptedEither = readEncryptedMnemonicFromDisk(seedPath) + + encryptedEither.flatMap { encrypted => + encrypted.toMnemonic(passphrase) match { + case Failure(exc) => + logger.error(s"Error when decrypting $encrypted: $exc") + CompatLeft(ReadMnemonicError.DecryptionError) + case Success(mnemonic) => + logger.debug(s"Decrypted $encrypted successfully") + val decryptedMnemonic = + DecryptedMnemonic(mnemonic, encrypted.creationTime) + CompatRight(decryptedMnemonic) + } + } + case None => + readUnencryptedMnemonicFromDisk(seedPath) } decryptedEither match { @@ -199,19 +307,13 @@ object WalletStorage { def getPrivateKeyFromDisk( seedPath: Path, privKeyVersion: ExtKeyPrivVersion, - passphrase: AesPassword, + passphraseOpt: Option[AesPassword], bip39PasswordOpt: Option[String]): ExtPrivateKeyHardened = { - val mnemonicCode = decryptMnemonicFromDisk(seedPath, passphrase) match { + val mnemonicCode = decryptMnemonicFromDisk(seedPath, passphraseOpt) match { case Left(error) => sys.error(error.toString) case Right(mnemonic) => mnemonic.mnemonicCode } - val seed = bip39PasswordOpt match { - case Some(pw) => - BIP39Seed.fromMnemonic(mnemonic = mnemonicCode, password = pw) - case None => - BIP39Seed.fromMnemonic(mnemonic = mnemonicCode, - password = BIP39Seed.EMPTY_PASSWORD) - } + val seed = BIP39Seed.fromMnemonic(mnemonicCode, bip39PasswordOpt) seed.toExtPrivateKey(privKeyVersion).toHardened } diff --git a/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala b/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala index cd7a581bab..85ce72fa72 100644 --- a/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala +++ b/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala @@ -40,13 +40,7 @@ case class BIP39KeyManager( extends BIP39KeyManagerApi with KeyManagerLogger { - private val seed = bip39PasswordOpt match { - case Some(pw) => - BIP39Seed.fromMnemonic(mnemonic = mnemonic, password = pw) - case None => - BIP39Seed.fromMnemonic(mnemonic = mnemonic, - password = BIP39Seed.EMPTY_PASSWORD) - } + private val seed = BIP39Seed.fromMnemonic(mnemonic, bip39PasswordOpt) override def equals(other: Any): Boolean = other match { @@ -87,17 +81,18 @@ case class BIP39KeyManager( object BIP39KeyManager extends BIP39KeyManagerCreateApi[BIP39KeyManager] with BitcoinSLogger { - val badPassphrase = AesPassword.fromString("changeMe") + val badPassphrase: AesPassword = AesPassword.fromString("changeMe") /** Initializes the mnemonic seed and saves it to file */ override def initializeWithEntropy( + aesPasswordOpt: Option[AesPassword], entropy: BitVector, bip39PasswordOpt: Option[String], kmParams: KeyManagerParams): Either[ KeyManagerInitializeError, BIP39KeyManager] = { val seedPath = kmParams.seedPath - logger.info(s"Initializing wallet with seedPath=${seedPath}") + logger.info(s"Initializing wallet with seedPath=$seedPath") val time = TimeUtil.now @@ -118,24 +113,27 @@ object BIP39KeyManager CompatEither(Left(InitializeKeyManagerError.BadEntropy)) } - val encryptedMnemonicE: CompatEither[ + val writableMnemonicE: CompatEither[ KeyManagerInitializeError, - EncryptedMnemonic] = + MnemonicState] = mnemonicE.map { mnemonic => - EncryptedMnemonicHelper.encrypt(DecryptedMnemonic(mnemonic, time), - badPassphrase) + val decryptedMnemonic = DecryptedMnemonic(mnemonic, time) + aesPasswordOpt match { + case Some(aesPassword) => + EncryptedMnemonicHelper.encrypt(decryptedMnemonic, aesPassword) + case None => + decryptedMnemonic + } } for { mnemonic <- mnemonicE - encrypted <- encryptedMnemonicE - _ = { - val mnemonicPath = - WalletStorage.writeMnemonicToDisk(seedPath, encrypted) - logger.info(s"Saved encrypted wallet mnemonic to $mnemonicPath") - } - + writable <- writableMnemonicE } yield { + val mnemonicPath = + WalletStorage.writeMnemonicToDisk(seedPath, writable) + logger.info(s"Saved wallet mnemonic to $mnemonicPath") + BIP39KeyManager(mnemonic = mnemonic, kmParams = kmParams, bip39PasswordOpt = bip39PasswordOpt, @@ -146,7 +144,7 @@ object BIP39KeyManager s"Seed file already exists, attempting to initialize form existing seed file=$seedPath.") WalletStorage.decryptMnemonicFromDisk(kmParams.seedPath, - badPassphrase) match { + aesPasswordOpt) match { case Right(mnemonic) => CompatRight( BIP39KeyManager(mnemonic = mnemonic.mnemonicCode, @@ -161,7 +159,7 @@ object BIP39KeyManager } //verify we can unlock it for a sanity check - val unlocked = BIP39LockedKeyManager.unlock(passphrase = badPassphrase, + val unlocked = BIP39LockedKeyManager.unlock(passphraseOpt = aesPasswordOpt, bip39PasswordOpt = bip39PasswordOpt, kmParams = kmParams) @@ -189,7 +187,7 @@ object BIP39KeyManager logger.info(s"Successfully initialized wallet") Right(initSuccess) case CompatLeft(err) => - logger.error(s"Failed to initialize key manager with err=${err}") + logger.error(s"Failed to initialize key manager with err=$err") Left(err) } } @@ -197,12 +195,12 @@ object BIP39KeyManager /** Reads the key manager from disk and decrypts it with the given password */ def fromParams( kmParams: KeyManagerParams, - password: AesPassword, + passwordOpt: Option[AesPassword], bip39PasswordOpt: Option[String]): Either[ ReadMnemonicError, BIP39KeyManager] = { val mnemonicCodeE = - WalletStorage.decryptMnemonicFromDisk(kmParams.seedPath, password) + WalletStorage.decryptMnemonicFromDisk(kmParams.seedPath, passwordOpt) mnemonicCodeE match { case Right(mnemonic) => diff --git a/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39LockedKeyManager.scala b/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39LockedKeyManager.scala index 0491b66389..2077d747ac 100644 --- a/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39LockedKeyManager.scala +++ b/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39LockedKeyManager.scala @@ -21,14 +21,14 @@ object BIP39LockedKeyManager extends BitcoinSLogger { * @param kmParams parameters needed to create the key manager */ def unlock( - passphrase: AesPassword, + passphraseOpt: Option[AesPassword], bip39PasswordOpt: Option[String], kmParams: KeyManagerParams): Either[ KeyManagerUnlockError, BIP39KeyManager] = { logger.debug(s"Trying to unlock wallet with seedPath=${kmParams.seedPath}") val resultE = - WalletStorage.decryptMnemonicFromDisk(kmParams.seedPath, passphrase) + WalletStorage.decryptMnemonicFromDisk(kmParams.seedPath, passphraseOpt) resultE match { case Right(mnemonic) => Right( diff --git a/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala b/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala index f53ff96f4a..bf00c58a8f 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/BitcoinSTestAppConfig.scala @@ -5,6 +5,7 @@ import java.nio.file._ import com.typesafe.config._ import org.bitcoins.dlc.oracle.DLCOracleAppConfig import org.bitcoins.server.BitcoinSAppConfig +import org.bitcoins.testkit.keymanager.KeyManagerTestUtil import org.bitcoins.testkit.util.FileUtil import scala.concurrent.ExecutionContext @@ -88,9 +89,20 @@ object BitcoinSTestAppConfig { def getDLCOracleWithEmbeddedDbTestConfig( pgUrl: () => Option[String], config: Config*)(implicit ec: ExecutionContext): DLCOracleAppConfig = { + val overrideConf = KeyManagerTestUtil.aesPasswordOpt match { + case Some(value) => + ConfigFactory.parseString { + s""" + |bitcoin-s.oracle.aesPassword = $value + """.stripMargin + } + case None => + ConfigFactory.empty() + } + DLCOracleAppConfig( tmpDir(), - configWithEmbeddedDb(project = None, pgUrl) +: config: _*) + overrideConf +: configWithEmbeddedDb(project = None, pgUrl) +: config: _*) } sealed trait ProjectType diff --git a/testkit/src/main/scala/org/bitcoins/testkit/fixtures/DLCOracleDAOFixture.scala b/testkit/src/main/scala/org/bitcoins/testkit/fixtures/DLCOracleDAOFixture.scala index 5ade539549..8ee384e7ec 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/fixtures/DLCOracleDAOFixture.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/fixtures/DLCOracleDAOFixture.scala @@ -1,6 +1,5 @@ package org.bitcoins.testkit.fixtures -import org.bitcoins.crypto.AesPassword import org.bitcoins.dlc.oracle.DLCOracleAppConfig import org.bitcoins.dlc.oracle.storage._ import org.bitcoins.testkit.keymanager.KeyManagerTestUtil.bip39PasswordOpt @@ -25,7 +24,7 @@ trait DLCOracleDAOFixture extends BitcoinSFixture with EmbeddedPg { makeFixture( build = () => { config - .initialize(AesPassword.fromString("Ben was here"), bip39PasswordOpt) + .initialize(bip39PasswordOpt) .map(oracle => DLCOracleDAOs(oracle.rValueDAO, oracle.eventDAO, diff --git a/testkit/src/main/scala/org/bitcoins/testkit/fixtures/DLCOracleFixture.scala b/testkit/src/main/scala/org/bitcoins/testkit/fixtures/DLCOracleFixture.scala index 70629af19b..8df71621a6 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/fixtures/DLCOracleFixture.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/fixtures/DLCOracleFixture.scala @@ -1,6 +1,5 @@ package org.bitcoins.testkit.fixtures -import org.bitcoins.crypto.AesPassword import org.bitcoins.dlc.oracle.DLCOracle import org.bitcoins.testkit.keymanager.KeyManagerTestUtil.bip39PasswordOpt import org.bitcoins.testkit.util.FileUtil @@ -17,7 +16,7 @@ trait DLCOracleFixture extends BitcoinSFixture with EmbeddedPg { val builder: () => Future[DLCOracle] = () => { val conf = BitcoinSTestAppConfig.getDLCOracleWithEmbeddedDbTestConfig(pgUrl) - conf.initialize(AesPassword.fromString("Ben was here"), bip39PasswordOpt) + conf.initialize(bip39PasswordOpt) } val destroy: DLCOracle => Future[Unit] = dlcOracle => { diff --git a/testkit/src/main/scala/org/bitcoins/testkit/keymanager/KeyManagerApiUnitTest.scala b/testkit/src/main/scala/org/bitcoins/testkit/keymanager/KeyManagerApiUnitTest.scala index f0e5627836..ea995270b0 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/keymanager/KeyManagerApiUnitTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/keymanager/KeyManagerApiUnitTest.scala @@ -2,6 +2,7 @@ package org.bitcoins.testkit.keymanager import org.bitcoins.core.crypto.MnemonicCode import org.bitcoins.core.wallet.keymanagement.KeyManagerParams +import org.bitcoins.crypto.AesPassword import org.bitcoins.keymanager.bip39.BIP39KeyManager import org.bitcoins.testkit.util.BitcoinSUnitTest import scodec.bits.BitVector @@ -9,11 +10,13 @@ import scodec.bits.BitVector trait KeyManagerApiUnitTest extends BitcoinSUnitTest { def withInitializedKeyManager( + aesPasswordOpt: Option[AesPassword] = KeyManagerTestUtil.aesPasswordOpt, kmParams: KeyManagerParams = KeyManagerTestUtil.createKeyManagerParams(), entropy: BitVector = MnemonicCode.getEntropy256Bits, bip39PasswordOpt: Option[String] = KeyManagerTestUtil.bip39PasswordOpt): BIP39KeyManager = { val kmResult = BIP39KeyManager.initializeWithEntropy( + aesPasswordOpt = aesPasswordOpt, entropy = entropy, bip39PasswordOpt = bip39PasswordOpt, kmParams = kmParams diff --git a/testkit/src/main/scala/org/bitcoins/testkit/keymanager/KeyManagerTestUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/keymanager/KeyManagerTestUtil.scala index e7d891831f..3bd0f3b06b 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/keymanager/KeyManagerTestUtil.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/keymanager/KeyManagerTestUtil.scala @@ -6,8 +6,8 @@ import org.bitcoins.core.config.Networks import org.bitcoins.core.hd.HDPurposes import org.bitcoins.core.wallet.keymanagement.KeyManagerParams import org.bitcoins.crypto.AesPassword -import org.bitcoins.keymanager.bip39.BIP39KeyManager import org.bitcoins.keymanager.WalletStorage +import org.bitcoins.keymanager.bip39.BIP39KeyManager import org.bitcoins.testkit.BitcoinSTestAppConfig import org.bitcoins.testkit.core.gen.CryptoGenerators import org.scalacheck.Gen @@ -49,5 +49,7 @@ object KeyManagerTestUtil { else attempt } - val badPassphrase: AesPassword = BIP39KeyManager.badPassphrase + def aesPasswordOpt: Option[AesPassword] = CryptoGenerators.aesPassword.sample + + val badPassphrase: Some[AesPassword] = Some(BIP39KeyManager.badPassphrase) } diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala index 795ae8d7ee..ad8f970743 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala @@ -364,7 +364,8 @@ object BitcoinSWalletTest extends WalletLogger { private def createNewKeyManager( bip39PasswordOpt: Option[String] = KeyManagerTestUtil.bip39PasswordOpt)( implicit config: WalletAppConfig): BIP39KeyManager = { - val keyManagerE = BIP39KeyManager.initialize(kmParams = config.kmParams, + val keyManagerE = BIP39KeyManager.initialize(config.aesPasswordOpt, + kmParams = config.kmParams, bip39PasswordOpt = bip39PasswordOpt) keyManagerE match { diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala index a37220ebef..591ae0c766 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala @@ -13,6 +13,7 @@ import org.bitcoins.feeprovider.ConstantFeeRateProvider import org.bitcoins.keymanager.bip39.BIP39KeyManager import org.bitcoins.testkit.BitcoinSTestAppConfig import org.bitcoins.testkit.fixtures.EmptyFixture +import org.bitcoins.testkit.keymanager.KeyManagerTestUtil import org.bitcoins.testkit.wallet.BitcoinSWalletTest import org.bitcoins.testkit.wallet.BitcoinSWalletTest.{ MockChainQueryApi, @@ -147,6 +148,7 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture { ec: ExecutionContext): Future[Wallet] = { val bip39PasswordOpt = None val kmE = BIP39KeyManager.initializeWithEntropy( + aesPasswordOpt = config.aesPasswordOpt, entropy = mnemonic.toEntropy, bip39PasswordOpt = bip39PasswordOpt, kmParams = config.kmParams) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala index 6c8a8db35e..e09b1bd7e1 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala @@ -134,18 +134,28 @@ class WalletUnitTest extends BitcoinSWalletTest { } yield res } - it should "fail to unlock the wallet with a bad password" in { + it should "fail to unlock the wallet with a bad aes password" in { wallet: Wallet => - val badpassphrase = AesPassword.fromNonEmptyString("bad") + val badPassphrase = Some(AesPassword.fromNonEmptyString("bad")) - val errorType = wallet.unlock(badpassphrase, None) match { + val errorType = wallet.unlock(badPassphrase, None) match { case Right(_) => fail("Unlocked wallet with bad password!") case Left(err) => err } errorType match { - case KeyManagerUnlockError.MnemonicNotFound => fail(MnemonicNotFound) - case KeyManagerUnlockError.BadPassword => succeed - case KeyManagerUnlockError.JsonParsingError(message) => fail(message) + case KeyManagerUnlockError.MnemonicNotFound => fail(MnemonicNotFound) + case KeyManagerUnlockError.BadPassword => + // If wallet is unencrypted then we shouldn't get a bad password error + wallet.walletConfig.aesPasswordOpt match { + case Some(_) => succeed + case None => fail() + } + case KeyManagerUnlockError.JsonParsingError(message) => + // If wallet is encrypted then we shouldn't get a json parsing error + wallet.walletConfig.aesPasswordOpt match { + case Some(_) => fail(message) + case None => succeed + } } } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala index 7e334a1ccb..f38b7caa94 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala @@ -188,13 +188,15 @@ abstract class Wallet } } - def unlock(passphrase: AesPassword, bip39PasswordOpt: Option[String]): Either[ + def unlock( + passphraseOpt: Option[AesPassword], + bip39PasswordOpt: Option[String]): Either[ KeyManagerUnlockError, Wallet] = { val kmParams = walletConfig.kmParams val unlockedKeyManagerE = - BIP39LockedKeyManager.unlock(passphrase = passphrase, + BIP39LockedKeyManager.unlock(passphraseOpt = passphraseOpt, bip39PasswordOpt = bip39PasswordOpt, kmParams = kmParams) unlockedKeyManagerE match { @@ -732,10 +734,11 @@ object Wallet extends WalletLogger { def initialize(wallet: Wallet, bip39PasswordOpt: Option[String])(implicit walletAppConfig: WalletAppConfig, ec: ExecutionContext): Future[Wallet] = { + val passwordOpt = walletAppConfig.aesPasswordOpt // We want to make sure all level 0 accounts are created, // so the user can change the default account kind later // and still have their wallet work - def createAccountFutures = + def createAccountFutures: Future[Vector[Future[AccountDb]]] = for { _ <- walletAppConfig.start() accounts = HDPurposes.singleSigPurposes.map { purpose => @@ -744,7 +747,7 @@ object Wallet extends WalletLogger { val kmParams = wallet.keyManager.kmParams.copy(purpose = purpose) val kmE = { BIP39KeyManager.fromParams(kmParams = kmParams, - password = BIP39KeyManager.badPassphrase, + passwordOpt = passwordOpt, bip39PasswordOpt = bip39PasswordOpt) } kmE match { diff --git a/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala b/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala index d172a6362a..eea3a64d57 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala @@ -13,6 +13,7 @@ import org.bitcoins.core.wallet.keymanagement.{ KeyManagerInitializeError, KeyManagerParams } +import org.bitcoins.crypto.AesPassword import org.bitcoins.db.DatabaseDriver.{PostgreSQL, SQLite} import org.bitcoins.db._ import org.bitcoins.keymanager.WalletStorage @@ -108,6 +109,11 @@ case class WalletAppConfig( config.getStringOrNone("bitcoin-s.wallet.bip39password") } + lazy val aesPasswordOpt: Option[AesPassword] = { + val passOpt = config.getStringOrNone("bitcoin-s.wallet.aesPassword") + passOpt.flatMap(AesPassword.fromStringOpt) + } + override def start(): Future[Unit] = { for { _ <- super.start() @@ -218,11 +224,14 @@ object WalletAppConfig walletConf: WalletAppConfig, ec: ExecutionContext): Future[Wallet] = { walletConf.hasWallet().flatMap { walletExists => + val aesPasswordOpt = walletConf.aesPasswordOpt + val bip39PasswordOpt = walletConf.bip39PasswordOpt + if (walletExists) { logger.info(s"Using pre-existing wallet") // TODO change me when we implement proper password handling - BIP39LockedKeyManager.unlock(BIP39KeyManager.badPassphrase, - walletConf.bip39PasswordOpt, + BIP39LockedKeyManager.unlock(aesPasswordOpt, + bip39PasswordOpt, walletConf.kmParams) match { case Right(km) => val wallet = @@ -233,9 +242,9 @@ object WalletAppConfig } } else { logger.info(s"Initializing key manager") - val bip39PasswordOpt = walletConf.bip39PasswordOpt val keyManagerE: Either[KeyManagerInitializeError, BIP39KeyManager] = - BIP39KeyManager.initialize(kmParams = walletConf.kmParams, + BIP39KeyManager.initialize(aesPasswordOpt = aesPasswordOpt, + kmParams = walletConf.kmParams, bip39PasswordOpt = bip39PasswordOpt) val keyManager = keyManagerE match {