-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathBip39.scala
79 lines (71 loc) · 2.77 KB
/
Bip39.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//> using dep "io.github.nremond::pbkdf2-scala:0.7.0"
package bips
import ecc.*
import Secp256k1.*
import scodec.bits.*
import scala.util.chaining.*
/** see https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
*/
object Bip39:
lazy val words: List[String] =
scala.io.Source.fromFile("resources/bip39_english_wordlist.txt").getLines().toList
/** BIP39 entropy encoding
* convert entropy to a mnemonic sentence
*
* @param entropy
* a bytevector of byte length multiple of 4
* @return
* a list of mnemonic words which encode the input entropy
*/
def toMnemonics( entropy: ByteVector ): List[String] =
require(entropy.length % 4 == 0, "entropy must have byte length multiple of 4")
(entropy.bits ++ (entropy.sha256).bits.take(entropy.length / 4))
.grouped(11).map(_.toInt(signed = false))
.map(index => words(index)).toList
/** BIP39 mnemonic decoding
* convert a mnemonic sentence to the underlying entropy
*
* @param mnemonics
* @return the recovered entropy as a bytevector
*/
def toEntropy( mnemonics: List[String]): ByteVector =
require(mnemonics.nonEmpty, "mnemonic code cannot be empty")
require(mnemonics.length % 3 == 0, s"invalid mnemonic word count ${mnemonics.length}, it must be a multiple of 3")
mnemonics.foreach(word => require(words.contains(word),s"invalid mnemonic word: $word"))
val wordMap = words.zipWithIndex.toMap
val indexes = mnemonics.map(word => wordMap(word))
val bits = indexes.map(ByteVector.fromInt(_)).map(_.bits.takeRight(11)).foldLeft(BitVector.empty)(_ ++ _)
val bitlength = (bits.length * 32) / 33
val (databits, checksumbits) = bits.splitAt(bitlength)
val data = databits.bytes
val check = data.sha256.bits.take(data.length / 4)
require( check == checksumbits, "invalid checksum")
data // return the original data
/** BIP39 seed derivation
*
* @param mnemonics
* mnemonic words
* @param passphrase
* passphrase
* @return
* a BIP32-compatible seed derived from the mnemonic words and passphrase
*/
def toSeed(mnemonics: Seq[String], passphrase: String): ByteVector =
pbkdf2Sha512(
ByteVector.view(mnemonics.mkString(" ").getBytes("UTF-8")),
ByteVector.view(("mnemonic" + passphrase).getBytes("UTF-8")),
2048,
64
)
private def pbkdf2Sha512(
password: ByteVector,
salt: ByteVector,
iterations: Int,
keyLen: Int
): ByteVector = io.github.nremond.PBKDF2(
password = password.toArray,
salt = salt.toArray,
iterations = iterations,
dkLength = keyLen,
cryptoAlgo = "HmacSHA512"
).pipe(ByteVector(_))