mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 09:52:09 +01:00
Add ability to change aes password (#2254)
* Add ability to change aes password * Add docs * Rename, add logs + tests
This commit is contained in:
parent
3effd6601f
commit
c29b787ab5
@ -57,6 +57,9 @@ object Picklers {
|
||||
implicit val instantPickler: ReadWriter[Instant] =
|
||||
readwriter[Long].bimap(_.getEpochSecond, Instant.ofEpochSecond)
|
||||
|
||||
implicit val aesPasswordPickler: ReadWriter[AesPassword] =
|
||||
readwriter[String].bimap(_.toStringSensitive, AesPassword.fromString)
|
||||
|
||||
implicit val sha256DigestBEPickler: ReadWriter[Sha256DigestBE] =
|
||||
readwriter[String].bimap(_.hex, Sha256DigestBE.fromHex)
|
||||
|
||||
|
@ -17,6 +17,7 @@ import org.bitcoins.core.psbt.PSBT
|
||||
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
|
||||
import org.bitcoins.core.wallet.utxo.AddressLabelTag
|
||||
import org.bitcoins.crypto.{
|
||||
AesPassword,
|
||||
SchnorrDigitalSignature,
|
||||
SchnorrNonce,
|
||||
Sha256DigestBE
|
||||
@ -99,6 +100,12 @@ object CliReaders {
|
||||
str => Instant.ofEpochSecond(str.toLong)
|
||||
}
|
||||
|
||||
implicit val aesPasswordReads: Read[AesPassword] = new Read[AesPassword] {
|
||||
override def arity: Int = 1
|
||||
|
||||
override def reads: String => AesPassword = AesPassword.fromString
|
||||
}
|
||||
|
||||
implicit val bitcoinAddressReads: Read[BitcoinAddress] =
|
||||
new Read[BitcoinAddress] {
|
||||
val arity: Int = 1
|
||||
|
@ -21,7 +21,11 @@ import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
|
||||
import org.bitcoins.core.psbt.PSBT
|
||||
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
|
||||
import org.bitcoins.core.wallet.utxo.AddressLabelTag
|
||||
import org.bitcoins.crypto.{SchnorrDigitalSignature, Sha256DigestBE}
|
||||
import org.bitcoins.crypto.{
|
||||
AesPassword,
|
||||
SchnorrDigitalSignature,
|
||||
Sha256DigestBE
|
||||
}
|
||||
import scopt.OParser
|
||||
import ujson._
|
||||
import upickle.{default => up}
|
||||
@ -892,6 +896,44 @@ object ConsoleCli {
|
||||
case other => other
|
||||
}))
|
||||
),
|
||||
cmd("keymanagerpassphrasechange")
|
||||
.action((_, conf) =>
|
||||
conf.copy(command = KeyManagerPassphraseChange(null, null)))
|
||||
.text("Changes the wallet passphrase")
|
||||
.children(
|
||||
arg[AesPassword]("oldpassphrase")
|
||||
.text("The current passphrase")
|
||||
.required()
|
||||
.action((oldPass, conf) =>
|
||||
conf.copy(command = conf.command match {
|
||||
case wpc: KeyManagerPassphraseChange =>
|
||||
wpc.copy(oldPassword = oldPass)
|
||||
case other => other
|
||||
})),
|
||||
arg[AesPassword]("newpassphrase")
|
||||
.text("The new passphrase")
|
||||
.required()
|
||||
.action((newPass, conf) =>
|
||||
conf.copy(command = conf.command match {
|
||||
case wpc: KeyManagerPassphraseChange =>
|
||||
wpc.copy(newPassword = newPass)
|
||||
case other => other
|
||||
}))
|
||||
),
|
||||
cmd("keymanagerpassphraseset")
|
||||
.action((_, conf) => conf.copy(command = KeyManagerPassphraseSet(null)))
|
||||
.text("Encrypts the wallet with the given passphrase")
|
||||
.children(
|
||||
arg[AesPassword]("passphrase")
|
||||
.text("The passphrase to encrypt the wallet with")
|
||||
.required()
|
||||
.action((pass, conf) =>
|
||||
conf.copy(command = conf.command match {
|
||||
case wps: KeyManagerPassphraseSet =>
|
||||
wps.copy(password = pass)
|
||||
case other => other
|
||||
}))
|
||||
),
|
||||
note(sys.props("line.separator") + "=== Network ==="),
|
||||
cmd("getpeers")
|
||||
.action((_, conf) => conf.copy(command = GetPeers))
|
||||
@ -1474,6 +1516,14 @@ object ConsoleCli {
|
||||
up.writeJs(satoshisPerVirtualByte)))
|
||||
case SignPSBT(psbt) =>
|
||||
RequestParam("signpsbt", Seq(up.writeJs(psbt)))
|
||||
|
||||
case KeyManagerPassphraseChange(oldPassword, newPassword) =>
|
||||
RequestParam("keymanagerpassphrasechange",
|
||||
Seq(up.writeJs(oldPassword), up.writeJs(newPassword)))
|
||||
|
||||
case KeyManagerPassphraseSet(password) =>
|
||||
RequestParam("keymanagerpassphraseset", Seq(up.writeJs(password)))
|
||||
|
||||
// height
|
||||
case GetBlockCount => RequestParam("getblockcount")
|
||||
// filter count
|
||||
@ -1623,10 +1673,12 @@ object ConsoleCli {
|
||||
(getKey("result"), getKey("error")) match {
|
||||
case (Some(result), None) =>
|
||||
Success(jsValueToString(result))
|
||||
case (None, None) =>
|
||||
Success("")
|
||||
case (None, Some(err)) =>
|
||||
val msg = jsValueToString(err)
|
||||
error(msg)
|
||||
case (None, None) | (Some(_), Some(_)) =>
|
||||
case (Some(_), Some(_)) =>
|
||||
error(s"Got unexpected response: $rawBody")
|
||||
}
|
||||
}.flatten
|
||||
@ -1806,6 +1858,12 @@ object CliCommand {
|
||||
case class GetUnconfirmedBalance(isSats: Boolean) extends CliCommand
|
||||
case class GetAddressInfo(address: BitcoinAddress) extends CliCommand
|
||||
|
||||
case class KeyManagerPassphraseChange(
|
||||
oldPassword: AesPassword,
|
||||
newPassword: AesPassword)
|
||||
extends CliCommand
|
||||
case class KeyManagerPassphraseSet(password: AesPassword) extends CliCommand
|
||||
|
||||
// Node
|
||||
case object GetPeers extends CliCommand
|
||||
case object Stop extends CliCommand
|
||||
|
@ -6,12 +6,16 @@ import akka.http.scaladsl.server._
|
||||
import org.bitcoins.core.number._
|
||||
import org.bitcoins.core.protocol.tlv._
|
||||
import org.bitcoins.dlc.oracle._
|
||||
import org.bitcoins.dlc.oracle.config.DLCOracleAppConfig
|
||||
import org.bitcoins.keymanager.WalletStorage
|
||||
import org.bitcoins.server._
|
||||
import ujson._
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
case class OracleRoutes(oracle: DLCOracle)(implicit system: ActorSystem)
|
||||
case class OracleRoutes(oracle: DLCOracle)(implicit
|
||||
system: ActorSystem,
|
||||
conf: DLCOracleAppConfig)
|
||||
extends ServerRoute {
|
||||
import system.dispatcher
|
||||
|
||||
@ -241,5 +245,33 @@ case class OracleRoutes(oracle: DLCOracle)(implicit system: ActorSystem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case ServerCommand("keymanagerpassphrasechange", arr) =>
|
||||
KeyManagerPassphraseChange.fromJsArr(arr) match {
|
||||
case Failure(err) =>
|
||||
reject(ValidationRejection("failure", Some(err)))
|
||||
case Success(KeyManagerPassphraseChange(oldPassword, newPassword)) =>
|
||||
complete {
|
||||
val path = conf.seedPath
|
||||
WalletStorage.changeAesPassword(path,
|
||||
Some(oldPassword),
|
||||
Some(newPassword))
|
||||
|
||||
Server.httpSuccess(ujson.Null)
|
||||
}
|
||||
}
|
||||
|
||||
case ServerCommand("keymanagerpassphraseset", arr) =>
|
||||
KeyManagerPassphraseSet.fromJsArr(arr) match {
|
||||
case Failure(err) =>
|
||||
reject(ValidationRejection("failure", Some(err)))
|
||||
case Success(KeyManagerPassphraseSet(password)) =>
|
||||
complete {
|
||||
val path = conf.seedPath
|
||||
WalletStorage.changeAesPassword(path, None, Some(password))
|
||||
|
||||
Server.httpSuccess(ujson.Null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ import org.bitcoins.crypto.{
|
||||
Sha256Hash160Digest
|
||||
}
|
||||
import org.bitcoins.node.Node
|
||||
import org.bitcoins.server.BitcoinSAppConfig.implicitToWalletConf
|
||||
import org.bitcoins.testkit.BitcoinSTestAppConfig
|
||||
import org.bitcoins.wallet.MockWalletApi
|
||||
import org.scalamock.scalatest.MockFactory
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
@ -45,6 +47,9 @@ import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
|
||||
|
||||
implicit val conf: BitcoinSAppConfig =
|
||||
BitcoinSTestAppConfig.getSpvTestConfig()
|
||||
|
||||
// the genesis address
|
||||
val testAddressStr = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
|
||||
val testAddress = BitcoinAddress.fromString(testAddressStr)
|
||||
|
@ -270,6 +270,8 @@ class BitcoinSServerMain(override val args: Array[String])
|
||||
system: ActorSystem,
|
||||
conf: BitcoinSAppConfig): Future[Http.ServerBinding] = {
|
||||
implicit val nodeConf: NodeAppConfig = conf.nodeConf
|
||||
implicit val walletConf: WalletAppConfig = conf.walletConf
|
||||
|
||||
val walletRoutes = WalletRoutes(wallet)
|
||||
val nodeRoutes = NodeRoutes(nodeApi)
|
||||
val chainRoutes = ChainRoutes(chainApi)
|
||||
|
@ -12,6 +12,7 @@ import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
|
||||
import org.bitcoins.core.psbt.PSBT
|
||||
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
|
||||
import org.bitcoins.core.wallet.utxo.AddressLabelTag
|
||||
import org.bitcoins.crypto.AesPassword
|
||||
import ujson._
|
||||
import upickle.default._
|
||||
|
||||
@ -209,6 +210,55 @@ object SendRawTransaction extends ServerJsonModels {
|
||||
}
|
||||
}
|
||||
|
||||
case class KeyManagerPassphraseChange(
|
||||
oldPassword: AesPassword,
|
||||
newPassword: AesPassword)
|
||||
|
||||
object KeyManagerPassphraseChange extends ServerJsonModels {
|
||||
|
||||
def fromJsArr(jsArr: ujson.Arr): Try[KeyManagerPassphraseChange] = {
|
||||
jsArr.arr.toList match {
|
||||
case oldPassJs :: newPassJs :: Nil =>
|
||||
Try {
|
||||
val oldPass = AesPassword.fromString(oldPassJs.str)
|
||||
val newPass = AesPassword.fromString(newPassJs.str)
|
||||
|
||||
KeyManagerPassphraseChange(oldPass, newPass)
|
||||
}
|
||||
case Nil =>
|
||||
Failure(
|
||||
new IllegalArgumentException(
|
||||
"Missing old password and new password arguments"))
|
||||
case other =>
|
||||
Failure(
|
||||
new IllegalArgumentException(
|
||||
s"Bad number of arguments: ${other.length}. Expected: 2"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class KeyManagerPassphraseSet(password: AesPassword)
|
||||
|
||||
object KeyManagerPassphraseSet extends ServerJsonModels {
|
||||
|
||||
def fromJsArr(jsArr: ujson.Arr): Try[KeyManagerPassphraseSet] = {
|
||||
jsArr.arr.toList match {
|
||||
case passJs :: Nil =>
|
||||
Try {
|
||||
val pass = AesPassword.fromString(passJs.str)
|
||||
|
||||
KeyManagerPassphraseSet(pass)
|
||||
}
|
||||
case Nil =>
|
||||
Failure(new IllegalArgumentException("Missing password argument"))
|
||||
case other =>
|
||||
Failure(
|
||||
new IllegalArgumentException(
|
||||
s"Bad number of arguments: ${other.length}. Expected: 1"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class CombinePSBTs(psbts: Seq[PSBT])
|
||||
|
||||
object CombinePSBTs extends ServerJsonModels {
|
||||
|
@ -10,12 +10,16 @@ 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.wallet.config.WalletAppConfig
|
||||
import ujson._
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
case class WalletRoutes(wallet: AnyHDWalletApi)(implicit system: ActorSystem)
|
||||
case class WalletRoutes(wallet: AnyHDWalletApi)(implicit
|
||||
system: ActorSystem,
|
||||
walletConf: WalletAppConfig)
|
||||
extends ServerRoute {
|
||||
import system.dispatcher
|
||||
|
||||
@ -426,5 +430,32 @@ case class WalletRoutes(wallet: AnyHDWalletApi)(implicit system: ActorSystem)
|
||||
}
|
||||
}
|
||||
|
||||
case ServerCommand("keymanagerpassphrasechange", arr) =>
|
||||
KeyManagerPassphraseChange.fromJsArr(arr) match {
|
||||
case Failure(err) =>
|
||||
reject(ValidationRejection("failure", Some(err)))
|
||||
case Success(KeyManagerPassphraseChange(oldPassword, newPassword)) =>
|
||||
complete {
|
||||
val path = walletConf.seedPath
|
||||
WalletStorage.changeAesPassword(path,
|
||||
Some(oldPassword),
|
||||
Some(newPassword))
|
||||
|
||||
Server.httpSuccess(ujson.Null)
|
||||
}
|
||||
}
|
||||
|
||||
case ServerCommand("keymanagerpassphraseset", arr) =>
|
||||
KeyManagerPassphraseSet.fromJsArr(arr) match {
|
||||
case Failure(err) =>
|
||||
reject(ValidationRejection("failure", Some(err)))
|
||||
case Success(KeyManagerPassphraseSet(password)) =>
|
||||
complete {
|
||||
val path = walletConf.seedPath
|
||||
WalletStorage.changeAesPassword(path, None, Some(password))
|
||||
|
||||
Server.httpSuccess(ujson.Null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -126,9 +126,7 @@ final case class AesPassword private (private val value: String)
|
||||
key
|
||||
}
|
||||
|
||||
override def toStringSensitive: String = {
|
||||
ByteVector.encodeUtf8(value).toString
|
||||
}
|
||||
override def toStringSensitive: String = value
|
||||
}
|
||||
|
||||
object AesPassword extends StringFactory[AesPassword] {
|
||||
|
@ -187,7 +187,11 @@ 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
|
||||
- `oldpassphrase` - The current passphrase
|
||||
- `newpassphrase` - The new passphrase
|
||||
- `walletpassphraseset` `passphrase` - Encrypts the wallet with the given passphrase
|
||||
- `passphrase` - The passphrase to encrypt the wallet with
|
||||
|
||||
#### Network
|
||||
- `getpeers` - List the connected peers
|
||||
|
@ -89,6 +89,131 @@ class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach {
|
||||
}
|
||||
}
|
||||
|
||||
it must "change the password of an encrypted mnemonic" in {
|
||||
walletConf: WalletAppConfig =>
|
||||
assert(!walletConf.seedExists())
|
||||
|
||||
val writtenMnemonic = getAndWriteMnemonic(walletConf)
|
||||
|
||||
assert(walletConf.seedExists())
|
||||
val seedPath = getSeedPath(walletConf)
|
||||
|
||||
WalletStorage.changeAesPassword(seedPath = seedPath,
|
||||
oldPasswordOpt = passphrase,
|
||||
newPasswordOpt = badPassphrase)
|
||||
|
||||
val read =
|
||||
WalletStorage.decryptMnemonicFromDisk(seedPath, badPassphrase)
|
||||
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 "change the password of an unencrypted mnemonic" 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)
|
||||
|
||||
assert(walletConf.seedExists())
|
||||
|
||||
WalletStorage.changeAesPassword(seedPath = seedPath,
|
||||
oldPasswordOpt = None,
|
||||
newPasswordOpt = badPassphrase)
|
||||
|
||||
val read =
|
||||
WalletStorage.decryptMnemonicFromDisk(seedPath, badPassphrase)
|
||||
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 "remove the password from an encrypted mnemonic" in {
|
||||
walletConf: WalletAppConfig =>
|
||||
assert(!walletConf.seedExists())
|
||||
|
||||
val writtenMnemonic = getAndWriteMnemonic(walletConf)
|
||||
|
||||
assert(walletConf.seedExists())
|
||||
val seedPath = getSeedPath(walletConf)
|
||||
|
||||
WalletStorage.changeAesPassword(seedPath = seedPath,
|
||||
oldPasswordOpt = passphrase,
|
||||
newPasswordOpt = None)
|
||||
|
||||
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 change the aes password when given the wrong password" in {
|
||||
walletConf: WalletAppConfig =>
|
||||
assert(!walletConf.seedExists())
|
||||
|
||||
getAndWriteMnemonic(walletConf)
|
||||
|
||||
assert(walletConf.seedExists())
|
||||
val seedPath = getSeedPath(walletConf)
|
||||
|
||||
assertThrows[RuntimeException](
|
||||
WalletStorage.changeAesPassword(seedPath = seedPath,
|
||||
oldPasswordOpt = badPassphrase,
|
||||
newPasswordOpt = badPassphrase))
|
||||
}
|
||||
|
||||
it must "fail to change the aes password when given no password" in {
|
||||
walletConf: WalletAppConfig =>
|
||||
assert(!walletConf.seedExists())
|
||||
|
||||
getAndWriteMnemonic(walletConf)
|
||||
|
||||
assert(walletConf.seedExists())
|
||||
val seedPath = getSeedPath(walletConf)
|
||||
|
||||
assertThrows[RuntimeException](
|
||||
WalletStorage.changeAesPassword(seedPath = seedPath,
|
||||
oldPasswordOpt = None,
|
||||
newPasswordOpt = badPassphrase))
|
||||
}
|
||||
|
||||
it must "fail to set the aes password when given an oldPassword" 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)
|
||||
|
||||
assert(walletConf.seedExists())
|
||||
|
||||
assertThrows[RuntimeException](
|
||||
WalletStorage.changeAesPassword(seedPath = seedPath,
|
||||
oldPasswordOpt = passphrase,
|
||||
newPasswordOpt = badPassphrase))
|
||||
}
|
||||
|
||||
it must "read an encrypted mnemonic without a creation time" in {
|
||||
walletConf =>
|
||||
val badJson =
|
||||
|
@ -304,6 +304,39 @@ object WalletStorage {
|
||||
}
|
||||
}
|
||||
|
||||
def changeAesPassword(
|
||||
seedPath: Path,
|
||||
oldPasswordOpt: Option[AesPassword],
|
||||
newPasswordOpt: Option[AesPassword]): MnemonicState = {
|
||||
logger.info("Changing encryption password for seed")
|
||||
decryptMnemonicFromDisk(seedPath, oldPasswordOpt) match {
|
||||
case Left(err) => sys.error(err.toString)
|
||||
case Right(decrypted) =>
|
||||
val fileName = seedPath.getFileName.toString
|
||||
logger.info("Creating backup file...")
|
||||
val backup = seedPath.getParent.resolve(fileName + ".backup")
|
||||
Files.move(seedPath, backup)
|
||||
|
||||
val toWrite = newPasswordOpt match {
|
||||
case Some(pass) =>
|
||||
decrypted.encrypt(pass)
|
||||
case None =>
|
||||
decrypted
|
||||
}
|
||||
|
||||
Try(writeMnemonicToDisk(seedPath, toWrite)) match {
|
||||
case Failure(exception) =>
|
||||
logger.error(
|
||||
s"Failed to write new seed, backup of previous seed file can be found at $backup")
|
||||
throw exception
|
||||
case Success(_) =>
|
||||
logger.info("Successfully wrote to disk, deleting backup file")
|
||||
Files.delete(backup)
|
||||
toWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getPrivateKeyFromDisk(
|
||||
seedPath: Path,
|
||||
privKeyVersion: ExtKeyPrivVersion,
|
||||
|
Loading…
Reference in New Issue
Block a user