Import Seed cli commands (#2376)

* Import Seed cli commands

* Respond to review
This commit is contained in:
Ben Carman 2020-12-21 06:53:20 -06:00 committed by GitHub
parent 4d9c9415d9
commit 559bebbdbe
7 changed files with 274 additions and 10 deletions

View file

@ -2,7 +2,12 @@ package org.bitcoins.commons.serializers
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
import org.bitcoins.core.api.wallet.CoinSelectionAlgo
import org.bitcoins.core.crypto.ExtPublicKey
import org.bitcoins.core.crypto.{
ExtKey,
ExtPrivateKey,
ExtPublicKey,
MnemonicCode
}
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.dlc.DLCMessage._
@ -507,4 +512,12 @@ object Picklers {
)
}
}
implicit val mnemonicCodePickler: ReadWriter[MnemonicCode] =
readwriter[String].bimap(
_.words.mkString(" "),
str => MnemonicCode.fromWords(str.split(' ').toVector))
implicit val extPrivateKeyPickler: ReadWriter[ExtPrivateKey] =
readwriter[String].bimap(ExtKey.toString, ExtPrivateKey.fromString)
}

View file

@ -3,11 +3,11 @@ package org.bitcoins.cli
import java.io.File
import java.nio.file.Path
import java.time.{Instant, ZoneId, ZonedDateTime}
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
import org.bitcoins.core.protocol.dlc.DLCMessage._
import org.bitcoins.core.api.wallet.CoinSelectionAlgo
import org.bitcoins.core.config.{NetworkParameters, Networks}
import org.bitcoins.core.crypto.{ExtPrivateKey, MnemonicCode}
import org.bitcoins.core.currency._
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BlockStamp.BlockTime
@ -317,4 +317,21 @@ object CliReaders {
override def reads: String => LnMessage[DLCSignTLV] =
LnMessageFactory(DLCSignTLV).fromHex
}
implicit val extPrivKeyReads: Read[ExtPrivateKey] = new Read[ExtPrivateKey] {
override def arity: Int = 1
override def reads: String => ExtPrivateKey = ExtPrivateKey.fromString
}
implicit val mnemonicCodeReads: Read[MnemonicCode] = new Read[MnemonicCode] {
override def arity: Int = 1
override def reads: String => MnemonicCode =
str => {
val words = str.split(' ')
MnemonicCode.fromWords(words.toVector)
}
}
}

View file

@ -10,6 +10,7 @@ import org.bitcoins.core.protocol.dlc.DLCMessage._
import org.bitcoins.commons.serializers.Picklers._
import org.bitcoins.core.api.wallet.CoinSelectionAlgo
import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.core.crypto.{ExtPrivateKey, MnemonicCode}
import org.bitcoins.core.currency._
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.tlv._
@ -757,6 +758,68 @@ object ConsoleCli {
case other => other
}))
),
cmd("importseed")
.action((_, conf) => conf.copy(command = ImportSeed("", null, None)))
.text("Imports a mnemonic seed as a new seed file")
.children(
arg[String]("walletname")
.text("Name to associate with this seed")
.required()
.action((walletName, conf) =>
conf.copy(command = conf.command match {
case is: ImportSeed =>
is.copy(walletName = walletName)
case other => other
})),
arg[MnemonicCode]("words")
.text("Mnemonic seed words, space separated")
.required()
.action((mnemonic, conf) =>
conf.copy(command = conf.command match {
case is: ImportSeed =>
is.copy(mnemonic = mnemonic)
case other => other
})),
arg[AesPassword]("passphrase")
.text("Passphrase to encrypt the seed with")
.action((password, conf) =>
conf.copy(command = conf.command match {
case is: ImportSeed =>
is.copy(passwordOpt = Some(password))
case other => other
}))
),
cmd("importxprv")
.action((_, conf) => conf.copy(command = ImportXprv("", null, None)))
.text("Imports a xprv as a new seed file")
.children(
arg[String]("walletname")
.text("What name to associate with this seed")
.required()
.action((walletName, conf) =>
conf.copy(command = conf.command match {
case ix: ImportXprv =>
ix.copy(walletName = walletName)
case other => other
})),
arg[ExtPrivateKey]("xprv")
.text("base58 encoded extended private key")
.required()
.action((xprv, conf) =>
conf.copy(command = conf.command match {
case ix: ImportXprv =>
ix.copy(xprv = xprv)
case other => other
})),
arg[AesPassword]("passphrase")
.text("Passphrase to encrypt this seed with")
.action((password, conf) =>
conf.copy(command = conf.command match {
case ix: ImportXprv =>
ix.copy(passwordOpt = Some(password))
case other => other
}))
),
cmd("keymanagerpassphrasechange")
.action((_, conf) =>
conf.copy(command = KeyManagerPassphraseChange(null, null)))
@ -1370,6 +1433,18 @@ object ConsoleCli {
case KeyManagerPassphraseSet(password) =>
RequestParam("keymanagerpassphraseset", Seq(up.writeJs(password)))
case ImportSeed(walletName, mnemonic, passwordOpt) =>
RequestParam("importseed",
Seq(up.writeJs(walletName),
up.writeJs(mnemonic),
up.writeJs(passwordOpt)))
case ImportXprv(walletName, xprv, passwordOpt) =>
RequestParam("importxprv",
Seq(up.writeJs(walletName),
up.writeJs(xprv),
up.writeJs(passwordOpt)))
// height
case GetBlockCount => RequestParam("getblockcount")
// filter count
@ -1678,6 +1753,18 @@ object CliCommand {
extends CliCommand
case class KeyManagerPassphraseSet(password: AesPassword) extends CliCommand
case class ImportSeed(
walletName: String,
mnemonic: MnemonicCode,
passwordOpt: Option[AesPassword])
extends CliCommand
case class ImportXprv(
walletName: String,
xprv: ExtPrivateKey,
passwordOpt: Option[AesPassword])
extends CliCommand
// Node
case object GetPeers extends CliCommand
case object Stop extends CliCommand

View file

@ -3,6 +3,7 @@ package org.bitcoins.server
import java.time.Instant
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
import org.bitcoins.core.api.wallet.CoinSelectionAlgo
import org.bitcoins.core.crypto._
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.protocol.BlockStamp.BlockHeight
import org.bitcoins.core.protocol.tlv._
@ -258,6 +259,91 @@ object KeyManagerPassphraseSet extends ServerJsonModels {
}
}
case class ImportSeed(
walletName: String,
mnemonic: MnemonicCode,
passwordOpt: Option[AesPassword])
object ImportSeed extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[ImportSeed] = {
jsArr.arr.toList match {
case walletNameJs :: mnemonicJs :: passJs :: Nil =>
Try {
val walletName = walletNameJs.str
val mnemonicWords = mnemonicJs match {
case Str(str) => str.split(' ').toVector
case Arr(arr) => arr.map(_.str).toVector
case Null | False | True | Num(_) | Obj(_) =>
throw new IllegalArgumentException(
"mnemonic must be a string or array of strings")
}
val mnemonic = MnemonicCode.fromWords(mnemonicWords)
val pass = passJs match {
case Str(str) =>
Some(AesPassword.fromString(str))
case Null =>
None
case Arr(_) | False | True | Num(_) | Obj(_) =>
throw new IllegalArgumentException(
"password must be a string or null")
}
ImportSeed(walletName, mnemonic, pass)
}
case Nil =>
Failure(
new IllegalArgumentException(
"Missing walletName, mnemonic, and password argument"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 3"))
}
}
}
case class ImportXprv(
walletName: String,
xprv: ExtPrivateKey,
passwordOpt: Option[AesPassword])
object ImportXprv extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[ImportXprv] = {
jsArr.arr.toList match {
case walletNameJs :: xprvJs :: passJs :: Nil =>
Try {
val walletName = walletNameJs.str
val xprv = ExtPrivateKey.fromString(xprvJs.str)
val pass = passJs match {
case Str(str) =>
Some(AesPassword.fromString(str))
case Null =>
None
case Arr(_) | False | True | Bool(_) | Num(_) | Obj(_) =>
throw new IllegalArgumentException(
"password must be a string or null")
}
ImportXprv(walletName, xprv, pass)
}
case Nil =>
Failure(
new IllegalArgumentException(
"Missing walletName, xprv, and password argument"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 3"))
}
}
}
case class CombinePSBTs(psbts: Seq[PSBT])
object CombinePSBTs extends ServerJsonModels {

View file

@ -10,10 +10,12 @@ import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.utxo.{AddressLabelTagType, TxoState}
import org.bitcoins.crypto.NetworkElement
import org.bitcoins.keymanager.WalletStorage
import org.bitcoins.keymanager._
import org.bitcoins.keymanager.config.KeyManagerAppConfig
import org.bitcoins.wallet.config.WalletAppConfig
import ujson._
import java.time.Instant
import scala.concurrent.Future
import scala.util.{Failure, Success}
@ -23,6 +25,8 @@ case class WalletRoutes(wallet: AnyHDWalletApi)(implicit
extends ServerRoute {
import system.dispatcher
implicit val kmConf: KeyManagerAppConfig = walletConf.kmConf
private def spendingInfoDbToJson(spendingInfoDb: SpendingInfoDb): Value = {
Obj(
"outpoint" -> Str(spendingInfoDb.outPoint.hex),
@ -469,6 +473,54 @@ case class WalletRoutes(wallet: AnyHDWalletApi)(implicit
val path = walletConf.seedPath
WalletStorage.changeAesPassword(path, None, Some(password))
Server.httpSuccess(ujson.Null)
}
}
case ServerCommand("importseed", arr) =>
ImportSeed.fromJsArr(arr) match {
case Failure(err) =>
reject(ValidationRejection("failure", Some(err)))
case Success(ImportSeed(walletName, mnemonic, passwordOpt)) =>
complete {
val seedPath = kmConf.seedFolder.resolve(
s"$walletName-${WalletStorage.ENCRYPTED_SEED_FILE_NAME}")
val creationTime = Instant.ofEpochSecond(WalletStorage.GENESIS_TIME)
val mnemonicState = passwordOpt match {
case Some(pass) =>
DecryptedMnemonic(mnemonic, creationTime).encrypt(pass)
case None =>
DecryptedMnemonic(mnemonic, creationTime)
}
WalletStorage.writeSeedToDisk(seedPath, mnemonicState)
Server.httpSuccess(ujson.Null)
}
}
case ServerCommand("importxprv", arr) =>
ImportXprv.fromJsArr(arr) match {
case Failure(err) =>
reject(ValidationRejection("failure", Some(err)))
case Success(ImportXprv(walletName, xprv, passwordOpt)) =>
complete {
val seedPath = kmConf.seedFolder.resolve(
s"$walletName-${WalletStorage.ENCRYPTED_SEED_FILE_NAME}")
val creationTime = Instant.ofEpochSecond(WalletStorage.GENESIS_TIME)
val mnemonicState = passwordOpt match {
case Some(pass) =>
DecryptedExtPrivKey(xprv, creationTime).encrypt(pass)
case None =>
DecryptedExtPrivKey(xprv, creationTime)
}
WalletStorage.writeSeedToDisk(seedPath, mnemonicState)
Server.httpSuccess(ujson.Null)
}
}

View file

@ -189,10 +189,18 @@ For more information on how to use our built in `cli` to interact with the serve
- `lockunspent` `unlock` `transactions` - Temporarily lock (unlock=false) or unlock (unlock=true) specified transaction outputs.
- `unlock` - Whether to unlock (true) or lock (false) the specified transactions
- `transactions` - The transaction outpoints to unlock/lock
- `walletpassphrasechange` `oldpassphrase` `newpassphrase` - Changes the wallet passphrase
- `importseed` `walletname` `words` `passphrase` - Imports a mnemonic seed as a new seed file
- `walletname` - Name to associate with this seed
- `words` - Mnemonic seed words, space separated
- `passphrase` - Passphrase to encrypt this seed with
- `importxprv` `walletname` `xprv` `passphrase` - Imports a mnemonic seed as a new seed file
- `walletname` - Name to associate with this seed
- `xprv` - base58 encoded extended private key
- `passphrase` - Passphrase to encrypt this seed with
- `keymanagerpassphrasechange` `oldpassphrase` `newpassphrase` - Changes the wallet passphrase
- `oldpassphrase` - The current passphrase
- `newpassphrase` - The new passphrase
- `walletpassphraseset` `passphrase` - Encrypts the wallet with the given passphrase
- `keymanagerpassphraseset` `passphrase` - Encrypts the wallet with the given passphrase
- `passphrase` - The passphrase to encrypt the wallet with
#### Network

View file

@ -33,6 +33,9 @@ case class KeyManagerAppConfig(
config.getStringOrNone(s"bitcoin-s.wallet.walletName")
}
lazy val seedFolder: Path = baseDatadir
.resolve(WalletStorage.SEED_FOLDER_NAME)
/** The path to our encrypted mnemonic seed */
lazy val seedPath: Path = {
val prefix = walletNameOpt match {
@ -40,17 +43,15 @@ case class KeyManagerAppConfig(
s"$walletName-"
case None => ""
}
baseDatadir
.resolve(WalletStorage.SEED_FOLDER_NAME)
.resolve(s"$prefix${WalletStorage.ENCRYPTED_SEED_FILE_NAME}")
seedFolder.resolve(s"$prefix${WalletStorage.ENCRYPTED_SEED_FILE_NAME}")
}
override def start(): Future[Unit] = {
val oldDefaultFile =
baseDatadir.resolve(WalletStorage.ENCRYPTED_SEED_FILE_NAME)
val newDefaultFile = baseDatadir
.resolve(WalletStorage.SEED_FOLDER_NAME)
val newDefaultFile = seedFolder
.resolve(WalletStorage.ENCRYPTED_SEED_FILE_NAME)
if (!Files.exists(newDefaultFile) && Files.exists(oldDefaultFile)) {