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
This commit is contained in:
Ben Carman 2020-11-06 07:00:18 -06:00 committed by GitHub
parent c8a0a0931a
commit 47a38f88db
28 changed files with 586 additions and 215 deletions

View file

@ -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 {

View file

@ -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)
}

View file

@ -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)
}
}
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

@ -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")
}

View file

@ -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))
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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)

View file

@ -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
}

View file

@ -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) =>

View file

@ -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(

View file

@ -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

View file

@ -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,

View file

@ -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 => {

View file

@ -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

View file

@ -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)
}

View file

@ -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 {

View file

@ -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)

View file

@ -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
}
}
}

View file

@ -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 {

View file

@ -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 {