mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-22 22:36:34 +01:00
Update wallet project to work new AES changes
This commit is contained in:
parent
d58f3dc715
commit
f93b8cadbe
7 changed files with 58 additions and 69 deletions
|
@ -18,8 +18,10 @@ import scala.io.Source
|
||||||
*/
|
*/
|
||||||
sealed abstract class MnemonicCode {
|
sealed abstract class MnemonicCode {
|
||||||
require(
|
require(
|
||||||
MnemonicCode.VALID_LENGTHS.contains(words.length),
|
MnemonicCode.VALID_LENGTHS.contains(words.length), {
|
||||||
s"Number of words must be one of the following: ${MnemonicCode.VALID_LENGTHS.mkString(", ")} "
|
val validLengths = MnemonicCode.VALID_LENGTHS.mkString(", ")
|
||||||
|
s"Number of words must be one of the following: $validLengths, got: ${words.length} "
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
require({
|
require({
|
||||||
|
|
|
@ -159,11 +159,6 @@ abstract class DbCommonsColumnMappers {
|
||||||
MappedColumnType
|
MappedColumnType
|
||||||
.base[ScriptType, String](_.toString, ScriptType.fromStringExn)
|
.base[ScriptType, String](_.toString, ScriptType.fromStringExn)
|
||||||
|
|
||||||
implicit val aesSaltMapper: BaseColumnType[AesSalt] =
|
|
||||||
MappedColumnType.base[AesSalt, String](
|
|
||||||
_.value.toHex,
|
|
||||||
hex => AesSalt(ByteVector.fromValidHex(hex)))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object DbCommonsColumnMappers extends DbCommonsColumnMappers
|
object DbCommonsColumnMappers extends DbCommonsColumnMappers
|
||||||
|
|
|
@ -10,17 +10,14 @@ class EncryptedMnemonicTest extends BitcoinSUnitTest {
|
||||||
behavior of "EncryptedMnemonic"
|
behavior of "EncryptedMnemonic"
|
||||||
|
|
||||||
it must "fail to decrypt with a bad password" in {
|
it must "fail to decrypt with a bad password" in {
|
||||||
val password = AesPassword("good")
|
val password = AesPassword.fromNonEmptyString("good")
|
||||||
val badPassword = AesPassword("bad")
|
val badPassword = AesPassword.fromNonEmptyString("bad")
|
||||||
|
|
||||||
def getMnemonic(): MnemonicCode =
|
def getMnemonic(): MnemonicCode =
|
||||||
CryptoGenerators.mnemonicCode.sample.getOrElse(getMnemonic())
|
CryptoGenerators.mnemonicCode.sample.getOrElse(getMnemonic())
|
||||||
|
|
||||||
val mnemonic = getMnemonic()
|
val mnemonic = getMnemonic()
|
||||||
val encrypted = EncryptedMnemonicHelper.encrypt(mnemonic, password) match {
|
val encrypted = EncryptedMnemonicHelper.encrypt(mnemonic, password)
|
||||||
case Success(value) => value
|
|
||||||
case Failure(exception) => fail(exception)
|
|
||||||
}
|
|
||||||
|
|
||||||
val decrypted = encrypted.toMnemonic(badPassword)
|
val decrypted = encrypted.toMnemonic(badPassword)
|
||||||
|
|
||||||
|
@ -31,10 +28,7 @@ class EncryptedMnemonicTest extends BitcoinSUnitTest {
|
||||||
it must "have encryption/decryption symmetry" in {
|
it must "have encryption/decryption symmetry" in {
|
||||||
forAll(CryptoGenerators.mnemonicCode, CryptoGenerators.aesPassword) {
|
forAll(CryptoGenerators.mnemonicCode, CryptoGenerators.aesPassword) {
|
||||||
(code, password) =>
|
(code, password) =>
|
||||||
val encrypted = EncryptedMnemonicHelper.encrypt(code, password) match {
|
val encrypted = EncryptedMnemonicHelper.encrypt(code, password)
|
||||||
case Success(e) => e
|
|
||||||
case Failure(exception) => fail(exception)
|
|
||||||
}
|
|
||||||
val decrypted = encrypted.toMnemonic(password) match {
|
val decrypted = encrypted.toMnemonic(password) match {
|
||||||
case Success(clear) => clear
|
case Success(clear) => clear
|
||||||
case Failure(exc) => fail(exc)
|
case Failure(exc) => fail(exc)
|
||||||
|
|
|
@ -150,7 +150,7 @@ class WalletUnitTest extends BitcoinSWalletTest {
|
||||||
|
|
||||||
it should "fail to unlock the wallet with a bad password" in {
|
it should "fail to unlock the wallet with a bad password" in {
|
||||||
wallet: UnlockedWalletApi =>
|
wallet: UnlockedWalletApi =>
|
||||||
val badpassphrase = AesPassword("bad")
|
val badpassphrase = AesPassword.fromNonEmptyString("bad")
|
||||||
val locked = wallet.lock()
|
val locked = wallet.lock()
|
||||||
wallet.unlock(badpassphrase) match {
|
wallet.unlock(badpassphrase) match {
|
||||||
case MnemonicNotFound => fail(MnemonicNotFound)
|
case MnemonicNotFound => fail(MnemonicNotFound)
|
||||||
|
|
|
@ -2,40 +2,50 @@ package org.bitcoins.wallet
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
import org.bitcoins.core.crypto.{
|
import org.bitcoins.core.crypto.{AesCrypt, AesEncryptedData, MnemonicCode}
|
||||||
AesCrypt,
|
|
||||||
AesEncryptedData,
|
|
||||||
AesPassword,
|
|
||||||
MnemonicCode
|
|
||||||
}
|
|
||||||
import scodec.bits.ByteVector
|
import scodec.bits.ByteVector
|
||||||
|
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
|
import org.bitcoins.core.crypto.AesSalt
|
||||||
|
import org.bitcoins.core.crypto.AesPassword
|
||||||
|
import org.bitcoins.core.util.BitcoinSLogger
|
||||||
|
import scala.util.Success
|
||||||
|
import scala.util.Failure
|
||||||
|
|
||||||
case class EncryptedMnemonic(value: AesEncryptedData) {
|
case class EncryptedMnemonic(value: AesEncryptedData, salt: AesSalt)
|
||||||
|
extends BitcoinSLogger {
|
||||||
import org.bitcoins.core.util.EitherUtil.EitherOps._
|
import org.bitcoins.core.util.EitherUtil.EitherOps._
|
||||||
|
|
||||||
def toMnemonic(passphrase: AesPassword): Try[MnemonicCode] = {
|
def toMnemonic(password: AesPassword): Try[MnemonicCode] = {
|
||||||
AesCrypt.decrypt(value, passphrase).toTry.map { decrypted =>
|
val key = password.toKey(salt)
|
||||||
val wordsStr = new String(decrypted.toArray, StandardCharsets.UTF_8)
|
AesCrypt.decrypt(value, key).toTry.flatMap { decrypted =>
|
||||||
val wordsVec = wordsStr.split(" ").toVector
|
decrypted.decodeUtf8 match {
|
||||||
MnemonicCode.fromWords(wordsVec)
|
case Left(_) =>
|
||||||
|
// when failing to decode this to a UTF-8 string
|
||||||
|
// we assume it's because of a bad password
|
||||||
|
Failure(ReadMnemonicError.DecryptionError)
|
||||||
|
|
||||||
|
case Right(wordsStr) =>
|
||||||
|
val wordsVec = wordsStr.split(" ").toVector
|
||||||
|
Success(MnemonicCode.fromWords(wordsVec))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object EncryptedMnemonicHelper {
|
object EncryptedMnemonicHelper {
|
||||||
import org.bitcoins.core.util.EitherUtil.EitherOps._
|
|
||||||
|
|
||||||
def encrypt(
|
def encrypt(
|
||||||
mnemonicCode: MnemonicCode,
|
mnemonicCode: MnemonicCode,
|
||||||
passphrase: AesPassword): Try[EncryptedMnemonic] = {
|
password: AesPassword): EncryptedMnemonic = {
|
||||||
val wordsStr = mnemonicCode.words.mkString(" ")
|
val wordsStr = mnemonicCode.words.mkString(" ")
|
||||||
val Right(clearText) = ByteVector.encodeUtf8(wordsStr)
|
val Right(clearText) = ByteVector.encodeUtf8(wordsStr)
|
||||||
|
|
||||||
AesCrypt
|
val (key, salt) = password.toKey
|
||||||
.encrypt(clearText, passphrase)
|
|
||||||
.toTry
|
val encryted = AesCrypt
|
||||||
.map(EncryptedMnemonic(_))
|
.encrypt(clearText, key)
|
||||||
|
|
||||||
|
EncryptedMnemonic(encryted, salt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,12 +28,7 @@ sealed abstract class Wallet
|
||||||
*/
|
*/
|
||||||
override def lock(): LockedWalletApi = {
|
override def lock(): LockedWalletApi = {
|
||||||
logger.debug(s"Locking wallet")
|
logger.debug(s"Locking wallet")
|
||||||
val encryptedT = EncryptedMnemonicHelper.encrypt(mnemonicCode, passphrase)
|
val encrypted = EncryptedMnemonicHelper.encrypt(mnemonicCode, passphrase)
|
||||||
val encrypted = encryptedT match {
|
|
||||||
case Failure(exception) =>
|
|
||||||
throw new RuntimeException(s"Could not encrypt mnemonic: $exception")
|
|
||||||
case Success(value) => value
|
|
||||||
}
|
|
||||||
|
|
||||||
WalletStorage.writeMnemonicToDisk(encrypted)
|
WalletStorage.writeMnemonicToDisk(encrypted)
|
||||||
logger.debug("Locked wallet")
|
logger.debug("Locked wallet")
|
||||||
|
@ -120,7 +115,7 @@ object Wallet extends CreateWalletApi with BitcoinSLogger {
|
||||||
WalletImpl(mnemonicCode)
|
WalletImpl(mnemonicCode)
|
||||||
|
|
||||||
// todo figure out how to handle password part of wallet
|
// todo figure out how to handle password part of wallet
|
||||||
val badPassphrase = AesPassword("changeMe")
|
val badPassphrase = AesPassword.fromNonEmptyString("changeMe")
|
||||||
|
|
||||||
// todo fix signature
|
// todo fix signature
|
||||||
override def initializeWithEntropy(entropy: BitVector)(
|
override def initializeWithEntropy(entropy: BitVector)(
|
||||||
|
@ -143,22 +138,7 @@ object Wallet extends CreateWalletApi with BitcoinSLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
val encryptedMnemonicE: Either[InitializeWalletError, EncryptedMnemonic] =
|
val encryptedMnemonicE: Either[InitializeWalletError, EncryptedMnemonic] =
|
||||||
mnemonicE.flatMap { mnemonic =>
|
mnemonicE.map { EncryptedMnemonicHelper.encrypt(_, badPassphrase) }
|
||||||
val encryptedT = EncryptedMnemonicHelper
|
|
||||||
.encrypt(mnemonic, badPassphrase)
|
|
||||||
|
|
||||||
val encryptedE: Either[Throwable, EncryptedMnemonic] =
|
|
||||||
encryptedT match {
|
|
||||||
case Failure(exception) => Left(exception)
|
|
||||||
case Success(value) => Right(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptedE.left
|
|
||||||
.map { err =>
|
|
||||||
logger.error(s"Encryption error when encrypting mnemonic: $err")
|
|
||||||
InitializeWalletError.EncryptionError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val biasedFinalEither: Either[InitializeWalletError, Future[WalletImpl]] =
|
val biasedFinalEither: Either[InitializeWalletError, Future[WalletImpl]] =
|
||||||
for {
|
for {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import java.nio.file.Paths
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
import org.bitcoins.wallet.config.WalletAppConfig
|
import org.bitcoins.wallet.config.WalletAppConfig
|
||||||
|
import org.bitcoins.core.crypto.AesIV
|
||||||
|
|
||||||
// what do we do if seed exists? error if they aren't equal?
|
// what do we do if seed exists? error if they aren't equal?
|
||||||
object WalletStorage extends BitcoinSLogger {
|
object WalletStorage extends BitcoinSLogger {
|
||||||
|
@ -41,9 +42,9 @@ object WalletStorage extends BitcoinSLogger {
|
||||||
val jsObject = {
|
val jsObject = {
|
||||||
import MnemonicJsonKeys._
|
import MnemonicJsonKeys._
|
||||||
ujson.Obj(
|
ujson.Obj(
|
||||||
IV -> encrypted.iv.toHex,
|
IV -> encrypted.iv.hex,
|
||||||
CIPHER_TEXT -> encrypted.cipherText.toHex,
|
CIPHER_TEXT -> encrypted.cipherText.toHex,
|
||||||
SALT -> encrypted.salt.value.toHex
|
SALT -> mnemonic.salt.bytes.toHex
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,6 +136,7 @@ object WalletStorage extends BitcoinSLogger {
|
||||||
val readJsonTupleEither: Either[
|
val readJsonTupleEither: Either[
|
||||||
ReadMnemonicError,
|
ReadMnemonicError,
|
||||||
(String, String, String)] = jsonE.flatMap { json =>
|
(String, String, String)] = jsonE.flatMap { json =>
|
||||||
|
logger.trace(s"Read encrypted mnemonic JSON: $json")
|
||||||
Try {
|
Try {
|
||||||
val ivString = json(IV).str
|
val ivString = json(IV).str
|
||||||
val cipherTextString = json(CIPHER_TEXT).str
|
val cipherTextString = json(CIPHER_TEXT).str
|
||||||
|
@ -153,13 +155,12 @@ object WalletStorage extends BitcoinSLogger {
|
||||||
readJsonTupleEither.flatMap {
|
readJsonTupleEither.flatMap {
|
||||||
case (rawIv, rawCipherText, rawSalt) =>
|
case (rawIv, rawCipherText, rawSalt) =>
|
||||||
val encryptedOpt = for {
|
val encryptedOpt = for {
|
||||||
iv <- ByteVector.fromHex(rawIv)
|
iv <- ByteVector.fromHex(rawIv).map(AesIV.fromValidBytes(_))
|
||||||
cipherText <- ByteVector.fromHex(rawCipherText)
|
cipherText <- ByteVector.fromHex(rawCipherText)
|
||||||
rawSalt <- ByteVector.fromHex(rawSalt)
|
salt <- ByteVector.fromHex(rawSalt).map(AesSalt(_))
|
||||||
salt = AesSalt(rawSalt)
|
|
||||||
} yield {
|
} yield {
|
||||||
logger.debug(s"Parsed contents of $path into an EncryptedMnemonic")
|
logger.debug(s"Parsed contents of $path into an EncryptedMnemonic")
|
||||||
EncryptedMnemonic(AesEncryptedData(cipherText, iv, salt))
|
EncryptedMnemonic(AesEncryptedData(cipherText, iv), salt)
|
||||||
}
|
}
|
||||||
encryptedOpt
|
encryptedOpt
|
||||||
.map(Right(_))
|
.map(Right(_))
|
||||||
|
@ -208,7 +209,8 @@ sealed trait ReadMnemonicResult
|
||||||
case class ReadMnemonicSuccess(mnemonic: MnemonicCode)
|
case class ReadMnemonicSuccess(mnemonic: MnemonicCode)
|
||||||
extends ReadMnemonicResult
|
extends ReadMnemonicResult
|
||||||
|
|
||||||
sealed trait ReadMnemonicError extends ReadMnemonicResult
|
sealed trait ReadMnemonicError extends ReadMnemonicResult { self: Error =>
|
||||||
|
}
|
||||||
|
|
||||||
object ReadMnemonicError {
|
object ReadMnemonicError {
|
||||||
|
|
||||||
|
@ -216,14 +218,20 @@ object ReadMnemonicError {
|
||||||
* Something went wrong while decrypting the mnemonic.
|
* Something went wrong while decrypting the mnemonic.
|
||||||
* Most likely the passphrase was bad
|
* Most likely the passphrase was bad
|
||||||
*/
|
*/
|
||||||
case object DecryptionError extends ReadMnemonicError
|
case object DecryptionError
|
||||||
|
extends Error(s"Could not decrypt mnemonic!")
|
||||||
|
with ReadMnemonicError
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Something went wrong while parsing the encrypted
|
* Something went wrong while parsing the encrypted
|
||||||
* mnemonic into valid JSON
|
* mnemonic into valid JSON
|
||||||
*/
|
*/
|
||||||
case class JsonParsingError(message: String) extends ReadMnemonicError
|
case class JsonParsingError(message: String)
|
||||||
|
extends Error(s"Error when parsing JSON: $message")
|
||||||
|
with ReadMnemonicError
|
||||||
|
|
||||||
/** The encrypted mnemonic was not found on disk */
|
/** The encrypted mnemonic was not found on disk */
|
||||||
case object NotFoundError extends ReadMnemonicError
|
case object NotFoundError
|
||||||
|
extends Error("Could not find mnemonic!")
|
||||||
|
with ReadMnemonicError
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue