Browse Source

Implementation is functionally complete.

All tests now pass. The next step is to publish the library artifacts and then setup CI.
master
Kevin Gorham 4 years ago
parent
commit
3998dd61ae
No known key found for this signature in database GPG Key ID: CCA55602DF49FC38
  1. 151
      README.md
  2. 13
      buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt
  3. 14
      lib/build.gradle
  4. 336
      lib/src/main/java/cash/z/ecc/android/bip39/Mnemonics.kt
  5. 127
      lib/src/main/java/cash/z/ecc/android/bip39/Pbkdf2Sha256.kt
  6. 2106
      lib/src/main/java/cash/z/ecc/android/bip39/WordList.kt
  7. 139
      lib/src/test/java/cash/z/ecc/android/bip39/MnemonicsTest.kt
  8. 108
      lib/src/test/java/cash/z/ecc/android/bip39/ReadmeExamplesTest.kt
  9. 148
      lib/src/test/resources/data/BIP-0039-test-values.json

151
README.md

@ -1,2 +1,151 @@
# android-bip39
A concise implementation of BIP-0039 in Kotlin for Android.
[![license](https://img.shields.io/github/license/zcash/android-bip39.svg?maxAge=2592000&style=plastic)](https://github.com/zcash/android-bip39/blob/master/LICENSE)
[![CircleCI](https://img.shields.io/circleci/build/github/zcash/android-bip39/master?style=plastic)](https://circleci.com/gh/zcash/android-bip39/tree/master)
[![@gmale](https://img.shields.io/badge/contact-android@z.cash-5AA9E7.svg?style=plastic)](https://github.com/gmale)
![Bintray](https://img.shields.io/bintray/v/ecc-mobile/android-bip39/android-bip39?color=success&style=plastic)
[![Keybase ZEC](https://img.shields.io/keybase/zec/kevinecc?logoColor=red&style=social)](https://keybase.io/kevinecc)
## Introduction
A concise implementation of [BIP-0039](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) in Kotlin for Android.
### Motivation
* There are not many bip-39 implementations for android
* Most that do exist are not Kotlin
* or they are not idiomatic (because they are direct Java ports to Kotlin)
* or they have restrictive licenses
* No other implementation uses [CharArrays](https://stackoverflow.com/a/8881376/178433), from the ground up, for [added security](https://docs.oracle.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html#PBEEx) and lower chances of [accidentally logging](https://stackoverflow.com/a/8885343/178433) sensitive info.
Consequently, this library strives to use both [idiomatic Kotlin](https://kotlinlang.org/docs/reference/idioms.html) and `CharArrays` whenever possible. It also aims to be concise and thoroughly tested. As a pure kotlin library, it probably also works outside of Android but that is not an explicit goal.
Plus, it uses a permissive MIT license and no dependencies beyond Kotlin's stdlib!
## Getting Started
### Gradle
Add dependencies:
```groovy
dependencies {
implementation 'cash.z.ecc.android:android-bip39:1.0.0-beta02'
}
repository {
jcenter()
}
```
***
## Usage
This library prefers `CharArrays` over `Strings` for [added security](https://stackoverflow.com/a/8881376/178433).
Note: If strings or lists are desired, it is very easy (but not recommended) to convert to/from a CharArray via `String(charArray)` or `String(charArray).split(' ')`.
* Create new 24-word mnemonic phrase
```kotlin
val mnemonicCode: MnemonicCode = MnemonicCode(WordCount.COUNT_24)
// assert: mnemonicCode.wordCount == 24, mnemonicCode.languageCode == "en"
```
* Generate seed
```kotlin
val seed: ByteArray = mnemonicCode.toSeed()
```
* Generate seed from existing mnemonic
```kotlin
val preExistingPhraseString = "scheme spot photo card baby mountain device kick cradle pact join borrow"
val preExistingPhraseChars = validPhraseString.toCharArray()
// from CharArray
seed = MnemonicCode(preExistingPhraseChars).toSeed()
// from String
seed = MnemonicCode(preExistingPhraseString).toSeed()
```
* Generate seed with passphrase
```kotlin
// normal way
val passphrase = "bitcoin".toCharArray()
mnemonicCode.toSeed(passphrase)
// more private way (erase at the end)
charArrayOf('z', 'c', 'a', 's', 'h').let { passphrase ->
mnemonicCode.toSeed(passphrase)
passphrase.fill('0') // erased!
}
```
* Generate raw entropy for a corresponding word count
```kotlin
val entropy: ByteArray = WordCount.COUNT_18.toEntropy()
// this can be used to directly generate a mnemonic:
val mnemonicCode = MnemonicCode(entropy)
// note: that gives the same result as calling:
MnemonicCode(WordCount.COUNT_18)
```
* Validate pre-existing or user-provided mnemonic
(NOTE: mnemonics generated by the library "from scratch" are valid, by definition)
```kotlin
// throws a typed exception when invalid:
// ChecksumException - when checksum fails, usually meaning words are swapped
// WordCountException(count) - invalid number of words
// InvalidWordException(word) - contains a word not found on the list
mnemonicCode.validate()
```
* Iterate over words
```kotlin
// mnemonicCodes are iterable
for (word in mnemonicCode) {
println(word)
}
mnemonicCode.forEach { word ->
println(word)
}
```
* Clean up!
```kotlin
mnemonicCode.clear() // code words are deleted and no longer available for attacker
```
#### Advanced Usage
These generated codes are compatible with kotlin's [scoped resource usage](https://kotlinlang.org/docs/tutorials/kotlin-for-py/scoped-resource-usage.html)
* Leverage `use` to automatically clean-up after use
```kotlin
MnemonicCode(WordCount.COUNT_24).use {
// Do something with the words (wordCount == 24)
}
// memory has been cleared at this point (wordCount == 0)
```
* Generate original entropy that was used to create the mnemonic
(or throw exception if the mnemonic is invalid).
* Note: Calling this function only succeeds when the entropy is valid so it also can be used, indirectly, for validation. In fact, currently, it is called as part of the `MnemonicCode::validate()` function.
```kotlin
val entropy: ByteArray = MnemonicCode(preExistingPhraseString).toEntropy()
```
* Mnemonics generated by the library do not need to be validated while creating the corresponding seed. That step can be skipped for a little added speed and security (because validation generates strings on the heap--which might get improved in a future release).
```kotlin
seed = MnemonicCode(WordCount.COUNT_24).toSeed(validate = false)
```
* Other languages are not yet supported but the API for them is in place. It accepts any `ISO 639-1` language code. For now, using it with anything other than "en" will result in an `UnsupportedOperationException`.
```kotlin
// results in exception, for now
val mnemonicCode = MnemonicCode(WordCount.COUNT_24, languageCode = Locale.GERMAN.language)
// english is the only language that doesn't crash
val mnemonicCode = MnemonicCode(WordCount.COUNT_24, languageCode = Locale.ENGLISH.language)
```
## Test Results
![Screenshot from 2020-06-04 04-05-37](https://user-images.githubusercontent.com/1699841/83732898-ba1c5180-a61a-11ea-92a5-16397a1660e7.png)
## Credits
* [zcash/ebfull](https://github.com/ebfull) - zcash core dev and BIP-0039 co-author who inspired me to create this library
* [bitcoinj](https://github.com/bitcoinj/bitcoinj/blob/master/core/src/main/java/org/bitcoinj/crypto/MnemonicCode.java) - Java implementation from which much of this code was adapted
* [Trezor](https://github.com/trezor/python-mnemonic/blob/master/vectors.json) - for their OG [test data set](https://github.com/trezor/python-mnemonic/blob/master/vectors.json) that has excellent edge cases
* [Cole Barnes](http://cryptofreek.org/2012/11/29/pbkdf2-pure-java-implementation/) - whose PBKDF2SHA512 Java implementation is floating around _everywhere_ online
* [Ken Sedgwick](https://github.com/ksedgwic) - who adapted Cole Barnes' work to use SHA-512
## License
MIT

13
buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt

@ -6,13 +6,18 @@ object Deps {
const val kotlinVersion = "1.3.72"
object Kotlin : Version(kotlinVersion) {
val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version"
val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version"
}
object Kotest : Version("4.0.5") {
val RUNNER = "io.kotest:kotest-runner-junit5-jvm:$version"
val ASSERTIONS = "io.kotest:kotest-assertions-core-jvm:$version"
val PROPERTY = "io.kotest:kotest-property-jvm:$version"
val RUNNER = "io.kotest:kotest-runner-junit5-jvm:$version"
val ASSERTIONS = "io.kotest:kotest-assertions-core-jvm:$version"
val PROPERTY = "io.kotest:kotest-property-jvm:$version"
}
object Square : Version("1.9.2") {
val MOSHI = "com.squareup.moshi:moshi:$version"
val MOSHI_KOTLIN = "com.squareup.moshi:moshi-kotlin:$version"
}
}

14
lib/build.gradle

@ -6,8 +6,18 @@ plugins {
}
group = "cash.z.ecc.android"
version = "1.0.0-alpha01"
version = "1.0.0-beta01"
tasks {
compileKotlin {
kotlinOptions { jvmTarget = 1.8 }
sourceCompatibility = 1.8
}
compileTestKotlin {
kotlinOptions { jvmTarget = 1.8 }
sourceCompatibility = 1.8
}
}
dependencies {
implementation Deps.Kotlin.STDLIB
@ -15,6 +25,8 @@ dependencies {
testImplementation Deps.Kotest.RUNNER
testImplementation Deps.Kotest.ASSERTIONS
testImplementation Deps.Kotest.PROPERTY
testImplementation Deps.Square.MOSHI
testImplementation Deps.Square.MOSHI_KOTLIN
}
test {
useJUnitPlatform()

336
lib/src/main/java/cash/z/ecc/android/bip39/Mnemonics.kt

@ -1,20 +1,274 @@
package cash.z.ecc.android.bip39
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.Mnemonics.WordCount
import java.io.Closeable
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.*
import kotlin.experimental.and
import kotlin.experimental.or
class Mnemonics {
private val secureRandom = SecureRandom()
/**
* Encompasses all mnemonic functionality, which helps keep everything concise and in one place.
*/
object Mnemonics {
fun createEntropy(words: WordCount): ByteArray = ByteArray(words.bitLength).apply {
secureRandom.nextBytes(this)
internal val secureRandom = SecureRandom()
internal var cachedList = WordList()
fun getCachedWords(languageCode: String): List<String> {
if (cachedList.languageCode != languageCode) {
cachedList = WordList(languageCode)
}
return cachedList.words
}
//
// Inner Classes
//
class MnemonicCode(val chars: CharArray, val languageCode: String = Locale.ENGLISH.language) :
Closeable, Iterable<String> {
constructor(
phrase: String,
languageCode: String = Locale.ENGLISH.language
) : this(phrase.toCharArray(), languageCode)
constructor(
entropy: ByteArray,
languageCode: String = Locale.ENGLISH.language
) : this(fromEntropy(entropy), languageCode)
constructor(
wordCount: WordCount,
languageCode: String = Locale.ENGLISH.language
) : this(fromEntropy(wordCount.toEntropy()), languageCode)
override fun close() = clear()
val wordCount get() = chars.count { it == ' ' }.let { if (it == 0) it else it + 1 }
val words: List<CharArray> get() {
val wordList = mutableListOf<CharArray>()
var cursor = 0
repeat(chars.size) { i ->
val isSpace = chars[i] == ' '
if (isSpace || i == (chars.size - 1)) {
val wordSize = i - cursor + if (isSpace) 0 else 1
wordList.add(CharArray(wordSize).apply {
repeat(wordSize) {
this[it] = chars[cursor + it]
}
})
cursor = i + 1
}
}
return wordList
}
fun clear() = chars.fill(0.toChar())
fun isEmpty() = chars.isEmpty()
override fun iterator(): Iterator<String> = object : Iterator<String> {
var cursor: Int = 0
override fun hasNext() = cursor < chars.size - 1
override fun next(): String {
var nextSpaceIndex = nextSpaceIndex()
val word = String(chars, cursor, nextSpaceIndex - cursor)
cursor = nextSpaceIndex + 1
return word
}
private fun nextSpaceIndex(): Int {
var i = cursor
while (i < chars.size - 1) {
if (chars[i].isWhitespace()) return i else i++
}
return chars.size
}
}
fun validate() {
// verify: word count is supported
wordCount.let { wordCount ->
if (WordCount.values().none { it.count == wordCount }) {
throw WordCountException(wordCount)
}
}
// verify: all words are on the list
var sublist = getCachedWords(languageCode)
var nextLetter = 0
chars.forEachIndexed { i, c ->
// filter down, by character, ensuring that there are always matching words.
// per BIP39, we could stop checking each word after 4 chars but we check them all,
// for completeness
if (c == ' ') {
sublist = getCachedWords(languageCode)
nextLetter = 0
} else {
sublist = sublist.filter { it.length > nextLetter && it[nextLetter] == c }
if (sublist.isEmpty()) throw InvalidWordException(i)
nextLetter++
}
}
// verify: checksum (this function contains a checksum validation)
toEntropy()
}
/**
* Convert this mnemonic word list to its original entropy value.
*/
fun toEntropy(): ByteArray {
wordCount.let { wordCount ->
if (wordCount % 3 > 0) throw WordCountException(wordCount)
}
if (isEmpty()) throw RuntimeException("Word list is empty.")
// Look up all the words in the list and construct the
// concatenation of the original entropy and the checksum.
//
val concatLenBits = wordCount * 11
val concatBits = BooleanArray(concatLenBits)
var wordindex = 0
// TODO: iterate by characters instead of by words, for a little added security
forEach { word ->
// Find the words index in the wordlist.
val ndx = getCachedWords(languageCode).binarySearch(word)
if (ndx < 0) throw InvalidWordException(word)
// Set the next 11 bits to the value of the index.
for (ii in 0..10) concatBits[wordindex * 11 + ii] =
ndx and (1 shl 10 - ii) != 0
++wordindex
}
val checksumLengthBits = concatLenBits / 33
val entropyLengthBits = concatLenBits - checksumLengthBits
// Extract original entropy as bytes.
val entropy = ByteArray(entropyLengthBits / 8)
for (ii in entropy.indices)
for (jj in 0..7)
if (concatBits[ii * 8 + jj]) {
entropy[ii] = entropy[ii] or (1 shl 7 - jj).toByte()
}
// Take the digest of the entropy.
val hash: ByteArray = entropy.toSha256()
val hashBits = hash.toBitArray()
// Check all the checksum bits.
for (i in 0 until checksumLengthBits)
if (concatBits[entropyLengthBits + i] != hashBits[i]) throw ChecksumException
return entropy
}
companion object {
/**
* Utility function to create a mnemonic code as a character array from the given
* entropy. Typically, new mnemonic codes are created starting with a WordCount
* instance, rather than entropy, to ensure that the entropy has been created correctly.
* This function is more useful during testing, when you want to validate that known
* input produces known output.
*
* @param entropy the entropy to use for creating the mnemonic code. Typically, this
* value is created via WordCount.toEntropy.
* @param languageCode the language code to use. Typically, `en`.
*
* @return an array of characters for the mnemonic code that corresponds to the given
* entropy.
*
* @see WordCount.toEntropy
*/
private fun fromEntropy(
entropy: ByteArray,
languageCode: String = Locale.ENGLISH.language
): CharArray {
/* From SPEC:
First, an initial entropy of ENT bits is generated.
A checksum is generated by taking the first ENT / 32 bits of its SHA256 hash.
This checksum is appended to the end of the initial entropy.
---
Next, these concatenated bits are split into groups of 11 bits,
each encoding a number from 0-2047, serving as an index into a wordlist.
---
Finally, we convert these numbers into words and use the joined words as a mnemonic
sentence.
*/
val hash: ByteArray = entropy.toSha256()
val hashBits = hash.toBitArray()
val entropyBits = entropy.toBitArray()
val checksumLengthBits = entropyBits.size / 32
// We append these bits to the end of the initial entropy.
val concatBits = BooleanArray(entropyBits.size + checksumLengthBits)
System.arraycopy(entropyBits, 0, concatBits, 0, entropyBits.size)
System.arraycopy(hashBits, 0, concatBits, entropyBits.size, checksumLengthBits)
// Next we take these concatenated bits and split them into
// groups of 11 bits. Each group encodes number from 0-2047
// which is a position in a wordlist. We convert numbers into
// words and use joined words as mnemonic sentence.
val words = ArrayList<String>()
val nwords = concatBits.size / 11
for (i in 0 until nwords) {
var index = 0
for (j in 0..10) {
index = index shl 1
if (concatBits[i * 11 + j]) index = index or 0x1
}
words += Mnemonics.getCachedWords(languageCode)[index]
}
// for added security, convert to one array, without adding string objects to the heap
var result = CharArray(words.sumBy { it.length } + words.size - 1)
var cursor = 0
// TODO: use the right separator for japanese, once that language is supported by this lib
var wordSeparator = ' '
words.forEach { word ->
repeat(word.length) { i ->
result[cursor++] = word[i]
}
if (cursor < result.size) result[cursor++] = wordSeparator
}
words.clear()
return result
}
}
}
/**
* The supported word counts that can be used for creating entropy.
*
* @param count the number of words in the resulting mnemonic
*/
enum class WordCount(val count: Int) {
COUNT_12(12), COUNT_15(15), COUNT_18(18), COUNT_21(21), COUNT_24(24);
/**
* The bit length of the entropy necessary to create a mnemonic with the given word count.
*/
val bitLength = count / 3 * 32
companion object {
/**
* Convert a count into an instance of [WordCount].
*/
fun valueOf(count: Int): WordCount? {
values().forEach {
if (it.count == count) return it
@ -24,16 +278,78 @@ class Mnemonics {
}
}
companion object {
fun toSeed(mnemonic: CharArray): ByteArray {
return ByteArray(256)
}
//
// Typed Exceptions
//
object ChecksumException :
RuntimeException(
"Error: The checksum failed. Verify that none of the words have been transposed."
)
class WordCountException(count: Int) :
RuntimeException("Error: $count is an invalid word count.")
class InvalidWordException : RuntimeException {
constructor(index: Int) : super("Error: invalid word encountered at index $index.")
constructor(word: String) : super("Error: <$word> was not found in the word list.")
}
}
//
// Extensions
// Public Extensions
//
fun CharArray.toSeed(): ByteArray = Mnemonics.toSeed(this)
/**
* Given a mnemonic, create a seed per BIP-0039.
*
* Per the proposal, "A user may decide to protect their mnemonic with a passphrase. If a
* passphrase is not present, an empty string "" is used instead. To create a binary seed from
* the mnemonic, we use the PBKDF2 function with a mnemonic sentence (in UTF-8 NFKD) used as the
* password and the string "mnemonic" + passphrase (again in UTF-8 NFKD) used as the salt. The
* iteration count is set to 2048 and HMAC-SHA512 is used as the pseudo-random function. The
* length of the derived key is 512 bits (= 64 bytes).
*
* @param mnemonic the mnemonic to convert into a seed
* @param passphrase an optional password to protect the phrase. Defaults to an empty string.
* This gets added to the salt.
* @param validate true to validate the mnemonic before attempting to generate the seed. This
* can add a bit of extra time to the calculation and is mainly only necessary when the seed is
* provided by user input. Meaning, in most cases, this can be false but we default to `true` to
* be "safe by default."
*/
fun MnemonicCode.toSeed(
passphrase: CharArray = charArrayOf(),
validate: Boolean = true
): ByteArray {
if (validate) validate()
val salt = "mnemonic".toCharArray() + passphrase
return Pbkdf2Sha256.derive(chars, salt, 2048, 64)
}
fun WordCount.toEntropy(): ByteArray = ByteArray(bitLength / 8).apply {
Mnemonics.secureRandom.nextBytes(this)
}
//
// Private Extensions
//
private fun ByteArray?.toSha256() = MessageDigest.getInstance("SHA-256").digest(this)
private fun ByteArray.toBitArray(): BooleanArray {
val bits = BooleanArray(size * 8)
val zero = 0.toByte()
repeat(size) { i ->
repeat(8) { j ->
val tmp1 = 1 shl (7 - j)
val tmp2 = this[i] and tmp1.toByte()
bits[i * 8 + j] = tmp2 != zero
}
}
return bits
}

127
lib/src/main/java/cash/z/ecc/android/bip39/Pbkdf2Sha256.kt

@ -0,0 +1,127 @@
package cash.z.ecc.android.bip39
/*
* Copyright (c) 2012 Cole Barnes [cryptofreek{at}gmail{dot}com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.xor
/**
*
* This is a clean-room implementation of PBKDF2 using RFC 2898 as a reference.
*
*
* RFC 2898: http://tools.ietf.org/html/rfc2898#section-5.2
*
*
* This code passes all RFC 6070 test vectors: http://tools.ietf.org/html/rfc6070
*
*
* http://cryptofreek.org/2012/11/29/pbkdf2-pure-java-implementation/<br></br>
* Modified to use SHA-512 - Ken Sedgwick ken@bonsai.com
*/
object Pbkdf2Sha256 {
fun derive(P: CharArray, S: CharArray, c: Int, dkLen: Int): ByteArray {
val baos = ByteArrayOutputStream()
try {
val hLen = 20
if (dkLen > (Math.pow(2.0, 32.0) - 1) * hLen) {
throw IllegalArgumentException("derived key too long")
} else {
val l = Math.ceil(dkLen.toDouble() / hLen.toDouble()).toInt()
// int r = dkLen - (l-1)*hLen;
for (i in 1..l) {
val T = F(P, S, c, i)
baos.write(T!!)
}
}
} catch (e: Exception) {
throw RuntimeException(e)
}
val baDerived = ByteArray(dkLen)
System.arraycopy(baos.toByteArray(), 0, baDerived, 0, baDerived.size)
return baDerived
}
@Throws(Exception::class)
private fun F(P: CharArray, S: CharArray, c: Int, i: Int): ByteArray? {
var U_LAST: ByteArray? = null
var U_XOR: ByteArray? = null
val pBytes = ByteArray(P.size).apply {
P.forEachIndexed { i, c -> this[i] = c.toByte() }
}
val sBytes = ByteArray(S.size).apply {
S.forEachIndexed { i, c -> this[i] = c.toByte() }
}
val key = SecretKeySpec(pBytes, "HmacSHA512")
val mac = Mac.getInstance(key.algorithm)
mac.init(key)
for (j in 0 until c) {
if (j == 0) {
val baS = sBytes
val baI = INT(i)
val baU = ByteArray(baS.size + baI.size)
System.arraycopy(baS, 0, baU, 0, baS.size)
System.arraycopy(baI, 0, baU, baS.size, baI.size)
U_XOR = mac.doFinal(baU)
U_LAST = U_XOR
mac.reset()
} else {
val baU = mac.doFinal(U_LAST)
mac.reset()
for (k in U_XOR!!.indices) {
U_XOR[k] = (U_XOR[k].xor(baU[k]))
}
U_LAST = baU
}
}
return U_XOR
}
private fun INT(i: Int): ByteArray {
val bb = ByteBuffer.allocate(4)
bb.order(ByteOrder.BIG_ENDIAN)
bb.putInt(i)
return bb.array()
}
}

2106
lib/src/main/java/cash/z/ecc/android/bip39/WordList.kt

File diff suppressed because it is too large

139
lib/src/test/java/cash/z/ecc/android/bip39/MnemonicsTest.kt

@ -1,18 +1,29 @@
package cash.z.ecc.android.bip39
import io.kotest.assertions.fail
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import io.kotest.assertions.asClue
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.data.forAll
import io.kotest.data.row
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import okio.Okio
import java.io.File
import java.util.*
class MnemonicsTest : BehaviorSpec({
val validPhrase = "void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold"
val lang = Locale.ENGLISH.language
Given("a valid, known mnemonic phrase") {
val mnemonic =
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
When("it is converted into a seed") {
val result = mnemonic.toCharArray().toSeed()
val result = MnemonicCode(validPhrase).toSeed()
Then("it should not be null") {
result shouldNotBe null
}
@ -28,14 +39,13 @@ class MnemonicsTest : BehaviorSpec({
hex.length shouldBe 128
}
Then("it should equal the expected value") {
hex shouldBe "f550d5399659396587a59b6ad446eb89da7741ebb1e42f87c22451d20ece8bb1e09ccb3c19f967f37fbf435367bc295c692c0ce000c52f5b991f1ca91169565e"
hex shouldBe "b873212f885ccffbf4692afcb84bc2e55886de2dfa07d90f5c3c239abc31c0a6ce047e30fd8bf6a281e71389aa82d73df74c7bbfb3b06b4639a5cee775cccd3c"
}
}
}
}
Given("a request for entropy") {
val m = Mnemonics()
When("each supported word count is requested") {
Then("ensure each result has the correct bit length") {
forAll(
@ -48,18 +58,23 @@ class MnemonicsTest : BehaviorSpec({
val wordCount = Mnemonics.WordCount.valueOf(count)
wordCount shouldNotBe null
bitLength shouldBe wordCount!!.bitLength
val entropy = m.createEntropy(wordCount)
entropy.size shouldBe bitLength
wordCount.toEntropy().let { entropy ->
entropy.size * 8 shouldBe bitLength
}
}
}
}
}
Given("a supported word length") {
Mnemonics.WordCount.values().forEach {
When("a mnemonic phrase is created using the ${it.name} enum value") {
Then("it has ${it.count - 1} spaces") {
fail("not implemented")
Given("a supported word count") {
Mnemonics.WordCount.values().forEach { wordCount ->
When("a mnemonic phrase is created using the ${wordCount.name} enum value") {
Then("it has ${wordCount.count - 1} spaces") {
MnemonicCode(wordCount).let { phrase ->
withClue(String(phrase.chars)) {
phrase.chars.count { it == ' ' } shouldBe wordCount.count - 1
}
}
}
}
}
@ -68,29 +83,84 @@ class MnemonicsTest : BehaviorSpec({
Given("predefined entropy as hex") {
When("it is converted to a mnemonic phrase") {
Then("it should match the expected phrase") {
val m = Mnemonics()
forAll(
row(24, "b893a6b0da8fc9b73d709bda939e818a677aa376c266949378300b65a34b8e52", "review outdoor promote relax wish swear volume beach surround ostrich parrot below jeans faculty swallow error nest orange army bitter focus place deer fat"),
row(18, "d5bcbf62dea1a07ab1abb0144b299300137168a7939f3071f112b557", "stick tourist suffer run borrow diary shop invite begin flock gospel ability damage reform oxygen initial corn moon dwarf height image"),
row(15, "e06ce21369dc09eb2bda66510a76f65ab3f947cce90fcb10", "there grow luggage squirrel scene void quarter error extra father rural rely display physical crisp capable slam lumber"),
row(12, "0b01c3c0b0590faf45fc171da17cfb22", "arch asthma usual gaze movie stumble blood load buffalo armor disagree earth")
) { count, entropy, mnemonic ->
fail("not implemented")
val code = MnemonicCode(entropy.fromHex())
String(code.chars) shouldBe mnemonic
}
}
}
}
// TODO: use test values from the original BIP : https://github.com/trezor/python-mnemonic/blob/master/vectors.json
// uses test values from the original BIP : https://github.com/trezor/python-mnemonic/blob/master/vectors.json
Given("The original BIP-0039 test data set") {
When("each provided entropy is converted to a mnemonic phrase") {
Then("each result matches the corresponding test phrase") {
fail("not implemented")
val testData: TestDataSet? = loadTestData()
testData shouldNotBe null
When("each provided entropy is converted to a mnemonic phrase [entropy -> mnemonic]") {
Then("each result matches the corresponding test mnemonic phrase") {
testData!!.values.forEach {
val entropy = it[0].fromHex()
val mnemonic = it[1]
String(MnemonicCode(entropy).chars) shouldBe mnemonic
}
}
}
When("each provided mnemonic phrase is converted into a seed") {
When("each provided mnemonic phrase is reverted to entropy [mnemonic -> entropy]") {
Then("each result matches the corresponding test entropy") {
testData!!.values.forEach {
val entropy = it[0]
val mnemonic = it[1]
MnemonicCode(mnemonic).toEntropy().toHex() shouldBe entropy
}
}
}
When("each provided mnemonic phrase is converted into a seed [mnemonic -> seed]") {
Then("each result matches the corresponding test seed") {
fail("not implemented")
testData!!.values.forEach {
val mnemonic = it[1].toCharArray()
val seed = it[2]
val passphrase = "TREZOR".toCharArray()
val language = Locale.ENGLISH.language
MnemonicCode(mnemonic, language).toSeed(passphrase).toHex() shouldBe seed
}
}
}
}
Given("an invalid mnemonic") {
When("it was created by swapping two words in a valid mnemonic") {
// swapped "trend" and "flight"
val mnemonicPhrase = validPhrase.swap(4, 5)
Then("it fails with a checksum error") {
mnemonicPhrase.asClue {
shouldThrow<Mnemonics.ChecksumException> {
MnemonicCode(mnemonicPhrase).validate()
}
}
}
}
When("it contains an invalid word") {
val mnemonicPhrase = validPhrase.split(' ').let { words ->
validPhrase.replace(words[23], "convincee")
}
Then("it fails with a word validation error") {
mnemonicPhrase.asClue {
shouldThrow<Mnemonics.InvalidWordException> {
MnemonicCode(mnemonicPhrase).validate()
}
}
}
}
When("it contains an unsupported number of words") {
val mnemonicPhrase = "$validPhrase still"
Then("it fails with a word count error") {
shouldThrow<Mnemonics.WordCountException> {
MnemonicCode(mnemonicPhrase).validate()
}
}
}
}
@ -101,6 +171,11 @@ class MnemonicsTest : BehaviorSpec({
// Test Utilities
//
@JsonClass(generateAdapter = true)
data class TestDataSet (
@Json(name = "english") val values: List<List<String>>
)
fun ByteArray.toHex(): String {
val sb = StringBuilder(size * 2)
for (b in this)
@ -119,3 +194,25 @@ fun String.fromHex(): ByteArray {
}
return data
}
fun String.swap(srcWord: Int, destWord: Int = srcWord + 1): String {
if (srcWord >= destWord) throw IllegalArgumentException("srcWord must be less than destWord")
if (destWord > count { it == ' '}) throw IllegalArgumentException("there aren't that many words")
return split(' ').let { words ->
words.reduceIndexed { i, result, word ->
val next = when (i) {
srcWord -> words[destWord]
destWord -> words[srcWord]
else -> word
}
if (srcWord == 0 && i == 1) "${words[destWord]} $next" else "$result $next"
}
}
}
fun loadTestData(): TestDataSet? =
Okio.buffer(Okio.source(File("src/test/resources/data/BIP-0039-test-values.json")))
.use { dataFile ->
Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
.adapter(TestDataSet::class.java).fromJson(dataFile)
}

108
lib/src/test/java/cash/z/ecc/android/bip39/ReadmeExamplesTest.kt

@ -0,0 +1,108 @@
package cash.z.ecc.android.bip39
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.Mnemonics.WordCount
import io.kotest.assertions.asClue
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
class ReadmeExamplesTest : ShouldSpec({
val validPhrase =
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
val validPhraseChars = validPhrase.toCharArray()
context("Example: Create 24-word mnemonic phrase") {
val mnemonicCode = MnemonicCode(WordCount.COUNT_24)
should("result in a valid 24-word phrase") {
mnemonicCode.wordCount shouldBe 24
}
should("result in a valid phrase"){
shouldNotThrowAny {
mnemonicCode.validate()
}
}
}
context("Example: Generate seed") {
val mnemonicCode = MnemonicCode(WordCount.COUNT_24)
should("result in a valid 24-word phrase") {
mnemonicCode.toSeed()
mnemonicCode.wordCount shouldBe 24
}
should("result in a valid phrase"){
shouldNotThrowAny {
mnemonicCode.validate()
}
}
}
context("Example: Generate seed from existing mnemonic chars") {
val mnemonicCode = MnemonicCode(validPhrase.toCharArray())
should("result in a valid 24-word phrase") {
mnemonicCode.toSeed()
mnemonicCode.wordCount shouldBe 24
}
should("result in a valid phrase"){
shouldNotThrowAny {
mnemonicCode.validate()
}
}
}
context("Example: Generate seed from existing mnemonic string") {
val mnemonicCode = MnemonicCode(validPhrase)
should("result in a valid 24-word phrase") {
mnemonicCode.toSeed()
mnemonicCode.wordCount shouldBe 24
}
should("result in a valid phrase"){
shouldNotThrowAny {
mnemonicCode.validate()
}
}
}
context("Example: Generate seed with passphrase") {
val passphrase = "bitcoin".toCharArray()
should("'normal way' results in a 64 byte seed") {
val seed = MnemonicCode(validPhrase).toSeed(passphrase)
seed.size shouldBe 64
}
should("'private way' results in a 64 byte seed") {
var seed: ByteArray
charArrayOf('z', 'c', 'a', 's', 'h').let { passphrase ->
seed = MnemonicCode(validPhrase).toSeed(passphrase)
String(passphrase) shouldBe "zcash"
passphrase.fill('0')
String(passphrase) shouldBe "00000"
}
seed.size shouldBe 64
}
}
context("Example: Iterate over mnemonic codes") {
val mnemonicCode = MnemonicCode(validPhrase)
should("work in a for loop") {
var count = 0
for (word in mnemonicCode) {
count++
validPhrase shouldContain word
}
count shouldBe 24
}
should("work with forEach"){
var count = 0
mnemonicCode.forEach { word ->
count++
validPhrase shouldContain word
}
count shouldBe 24
}
}
context("Example: auto-clear") {
should("clear the mnemonic when done") {
val mnemonicCode = MnemonicCode(WordCount.COUNT_24)
mnemonicCode.use {
mnemonicCode.wordCount shouldBe 24
}
// content gets automatically cleared after use!
mnemonicCode.wordCount shouldBe 0
}
}
})

148
lib/src/test/resources/data/BIP-0039-test-values.json

@ -0,0 +1,148 @@
{
"english": [
[
"00000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04",
"xprv9s21ZrQH143K3h3fDYiay8mocZ3afhfULfb5GX8kCBdno77K4HiA15Tg23wpbeF1pLfs1c5SPmYHrEpTuuRhxMwvKDwqdKiGJS9XFKzUsAF"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank yellow",
"2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607",
"xprv9s21ZrQH143K2gA81bYFHqU68xz1cX2APaSq5tt6MFSLeXnCKV1RVUJt9FWNTbrrryem4ZckN8k4Ls1H6nwdvDTvnV7zEXs2HgPezuVccsq"
],
[
"80808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
"d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8",
"xprv9s21ZrQH143K2shfP28KM3nr5Ap1SXjz8gc2rAqqMEynmjt6o1qboCDpxckqXavCwdnYds6yBHZGKHv7ef2eTXy461PXUjBFQg6PrwY4Gzq"
],
[
"ffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
"ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069",
"xprv9s21ZrQH143K2V4oox4M8Zmhi2Fjx5XK4Lf7GKRvPSgydU3mjZuKGCTg7UPiBUD7ydVPvSLtg9hjp7MQTYsW67rZHAXeccqYqrsx8LcXnyd"
],
[
"000000000000000000000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
"035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa",
"xprv9s21ZrQH143K3mEDrypcZ2usWqFgzKB6jBBx9B6GfC7fu26X6hPRzVjzkqkPvDqp6g5eypdk6cyhGnBngbjeHTe4LsuLG1cCmKJka5SMkmU"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will",
"f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd",
"xprv9s21ZrQH143K3Lv9MZLj16np5GzLe7tDKQfVusBni7toqJGcnKRtHSxUwbKUyUWiwpK55g1DUSsw76TF1T93VT4gz4wt5RM23pkaQLnvBh7"
],
[
"808080808080808080808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always",
"107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65",
"xprv9s21ZrQH143K3VPCbxbUtpkh9pRG371UCLDz3BjceqP1jz7XZsQ5EnNkYAEkfeZp62cDNj13ZTEVG1TEro9sZ9grfRmcYWLBhCocViKEJae"
],
[
"ffffffffffffffffffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
"0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528",
"xprv9s21ZrQH143K36Ao5jHRVhFGDbLP6FCx8BEEmpru77ef3bmA928BxsqvVM27WnvvyfWywiFN8K6yToqMaGYfzS6Db1EHAXT5TuyCLBXUfdm"
],
[
"0000000000000000000000000000000000000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
"bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8",
"xprv9s21ZrQH143K32qBagUJAMU2LsHg3ka7jqMcV98Y7gVeVyNStwYS3U7yVVoDZ4btbRNf4h6ibWpY22iRmXq35qgLs79f312g2kj5539ebPM"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
"bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87",
"xprv9s21ZrQH143K3Y1sd2XVu9wtqxJRvybCfAetjUrMMco6r3v9qZTBeXiBZkS8JxWbcGJZyio8TrZtm6pkbzG8SYt1sxwNLh3Wx7to5pgiVFU"
],
[
"8080808080808080808080808080808080808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
"c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f",
"xprv9s21ZrQH143K3CSnQNYC3MqAAqHwxeTLhDbhF43A4ss4ciWNmCY9zQGvAKUSqVUf2vPHBTSE1rB2pg4avopqSiLVzXEU8KziNnVPauTqLRo"
],
[
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
"dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad",
"xprv9s21ZrQH143K2WFF16X85T2QCpndrGwx6GueB72Zf3AHwHJaknRXNF37ZmDrtHrrLSHvbuRejXcnYxoZKvRquTPyp2JiNG3XcjQyzSEgqCB"
],
[
"9e885d952ad362caeb4efe34a8e91bd2",
"ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic",
"274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028",
"xprv9s21ZrQH143K2oZ9stBYpoaZ2ktHj7jLz7iMqpgg1En8kKFTXJHsjxry1JbKH19YrDTicVwKPehFKTbmaxgVEc5TpHdS1aYhB2s9aFJBeJH"
],
[
"6610b25967cdcca9d59875f5cb50b0ea75433311869e930b",
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog",
"628c3827a8823298ee685db84f55caa34b5cc195a778e52d45f59bcf75aba68e4d7590e101dc414bc1bbd5737666fbbef35d1f1903953b66624f910feef245ac",
"xprv9s21ZrQH143K3uT8eQowUjsxrmsA9YUuQQK1RLqFufzybxD6DH6gPY7NjJ5G3EPHjsWDrs9iivSbmvjc9DQJbJGatfa9pv4MZ3wjr8qWPAK"
],
[
"68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c",
"hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length",
"64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440",
"xprv9s21ZrQH143K2XTAhys3pMNcGn261Fi5Ta2Pw8PwaVPhg3D8DWkzWQwjTJfskj8ofb81i9NP2cUNKxwjueJHHMQAnxtivTA75uUFqPFeWzk"
],
[
"c0ba5a8e914111210f2bd131f3d5e08d",
"scheme spot photo card baby mountain device kick cradle pact join borrow",
"ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612",
"xprv9s21ZrQH143K3FperxDp8vFsFycKCRcJGAFmcV7umQmcnMZaLtZRt13QJDsoS5F6oYT6BB4sS6zmTmyQAEkJKxJ7yByDNtRe5asP2jFGhT6"
],
[
"6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3",
"horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave",
"fd579828af3da1d32544ce4db5c73d53fc8acc4ddb1e3b251a31179cdb71e853c56d2fcb11aed39898ce6c34b10b5382772db8796e52837b54468aeb312cfc3d",
"xprv9s21ZrQH143K3R1SfVZZLtVbXEB9ryVxmVtVMsMwmEyEvgXN6Q84LKkLRmf4ST6QrLeBm3jQsb9gx1uo23TS7vo3vAkZGZz71uuLCcywUkt"
],
[
"9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863",
"panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside",
"72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d",
"xprv9s21ZrQH143K2WNnKmssvZYM96VAr47iHUQUTUyUXH3sAGNjhJANddnhw3i3y3pBbRAVk5M5qUGFr4rHbEWwXgX4qrvrceifCYQJbbFDems"
],
[
"23db8160a31d3e0dca3688ed941adbf3",
"cat swing flag economy stadium alone churn speed unique patch report train",
"deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5",
"xprv9s21ZrQH143K4G28omGMogEoYgDQuigBo8AFHAGDaJdqQ99QKMQ5J6fYTMfANTJy6xBmhvsNZ1CJzRZ64PWbnTFUn6CDV2FxoMDLXdk95DQ"
],
[
"8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0",
"light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access",
"4cbdff1ca2db800fd61cae72a57475fdc6bab03e441fd63f96dabd1f183ef5b782925f00105f318309a7e9c3ea6967c7801e46c8a58082674c860a37b93eda02",
"xprv9s21ZrQH143K3wtsvY8L2aZyxkiWULZH4vyQE5XkHTXkmx8gHo6RUEfH3Jyr6NwkJhvano7Xb2o6UqFKWHVo5scE31SGDCAUsgVhiUuUDyh"
],
[
"066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad",
"all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform",
"26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d",
"xprv9s21ZrQH143K3rEfqSM4QZRVmiMuSWY9wugscmaCjYja3SbUD3KPEB1a7QXJoajyR2T1SiXU7rFVRXMV9XdYVSZe7JoUXdP4SRHTxsT1nzm"
],
[
"f30f8c1da665478f49b001d94c5fc452",
"vessel ladder alter error federal sibling chat ability sun glass valve picture",
"2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f",
"xprv9s21ZrQH143K2QWV9Wn8Vvs6jbqfF1YbTCdURQW9dLFKDovpKaKrqS3SEWsXCu6ZNky9PSAENg6c9AQYHcg4PjopRGGKmdD313ZHszymnps"
],
[
"c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05",
"scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump",
"7b4a10be9d98e6cba265566db7f136718e1398c71cb581e1b2f464cac1ceedf4f3e274dc270003c670ad8d02c4558b2f8e39edea2775c9e232c7cb798b069e88",
"xprv9s21ZrQH143K4aERa2bq7559eMCCEs2QmmqVjUuzfy5eAeDX4mqZffkYwpzGQRE2YEEeLVRoH4CSHxianrFaVnMN2RYaPUZJhJx8S5j6puX"
],
[
"f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f",
"void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold",
"01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998",
"xprv9s21ZrQH143K39rnQJknpH1WEPFJrzmAqqasiDcVrNuk926oizzJDDQkdiTvNPr2FYDYzWgiMiC63YmfPAa2oPyNB23r2g7d1yiK6WpqaQS"
]
]
}
Loading…
Cancel
Save