Added module for generating mnemonic phrases and seeds.

The underlying library is not perfect but it will do for now. Closes #7. Closes #8.
This commit is contained in:
Kevin Gorham 2019-12-14 21:38:16 -05:00
parent 9b2e7b318b
commit 1a64eb3fbd
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
7 changed files with 213 additions and 1 deletions

1
mnemonic/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

13
mnemonic/build.gradle Normal file
View File

@ -0,0 +1,13 @@
import cash.z.ecc.android.Deps
apply plugin: 'kotlin'
dependencies {
implementation Deps.Kotlin.STDLIB
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'io.github.novacrypto:BIP39:2019.01.27'
implementation 'io.github.novacrypto:securestring:2019.01.27'
testImplementation Deps.Test.JUNIT
}

View File

@ -0,0 +1,18 @@
package cash.z.ecc.kotlin.mnemonic
import java.util.*
/**
* Clears out the given char array in memory, for security purposes.
*/
fun CharArray.clear() {
Arrays.fill(this, '0')
}
/**
* Clears out the given byte array in memory, for security purposes.
*/
fun ByteArray.clear() {
Arrays.fill(this, 0.toByte())
}

View File

@ -0,0 +1,22 @@
package cash.z.ecc.kotlin.mnemonic
/**
* Generic interface to separate the underlying implementation used by this module and the code that
* interacts with it.
*/
interface MnemonicProvider {
/**
* Generate a random 24-word mnemonic phrase
*/
fun nextMnemonic(): CharArray
/**
* Generate a random 24-word mnemonic phrase, represented as a list of words.
*/
fun nextMnemonicList(): List<CharArray>
/**
* Generate a 64-byte seed from the 24-word mnemonic phrase
*/
fun toSeed(mnemonic: CharArray): ByteArray
}

View File

@ -0,0 +1,55 @@
package cash.z.ecc.kotlin.mnemonic
import io.github.novacrypto.bip39.MnemonicGenerator
import io.github.novacrypto.bip39.SeedCalculator
import io.github.novacrypto.bip39.Words
import io.github.novacrypto.bip39.wordlists.English
import java.security.SecureRandom
class Mnemonics : MnemonicProvider {
override fun nextMnemonic(): CharArray {
// TODO: either find another library that allows for doing this without strings or modify this code to leverage SecureCharBuffer (which doesn't work well with SeedCalculator.calculateSeed, which expects a string so for that reason, we just use Strings here)
return StringBuilder().let { builder ->
ByteArray(Words.TWENTY_FOUR.byteLength()).also {
SecureRandom().nextBytes(it)
MnemonicGenerator(English.INSTANCE).createMnemonic(it) { c ->
builder.append(c)
}
}
builder.toString().toCharArray()
}
}
override fun nextMnemonicList(): List<CharArray> {
return WordListBuilder().let { builder ->
ByteArray(Words.TWENTY_FOUR.byteLength()).also {
SecureRandom().nextBytes(it)
MnemonicGenerator(English.INSTANCE).createMnemonic(it) { c ->
builder.append(c)
}
}
builder.wordList
}
}
override fun toSeed(mnemonic: CharArray): ByteArray {
// TODO: either find another library that allows for doing this without strings or modify this code to leverage SecureCharBuffer (which doesn't work well with SeedCalculator.calculateSeed, which expects a string so for that reason, we just use Strings here)
return SeedCalculator().calculateSeed(mnemonic.toString(), "")
}
class WordListBuilder {
val wordList = mutableListOf<CharArray>()
fun append(c: CharSequence) {
if (c[0] != English.INSTANCE.space) addWord(c)
}
private fun addWord(c: CharSequence) {
c.length.let { size ->
val word = CharArray(size)
repeat(size) {
word[it] = c[it]
}
wordList.add(word)
}
}
}
}

View File

@ -0,0 +1,103 @@
package cash.z.ecc.android.util
import cash.z.ecc.kotlin.mnemonic.MnemonicProvider
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import io.github.novacrypto.SecureCharBuffer
import io.github.novacrypto.bip39.MnemonicGenerator
import io.github.novacrypto.bip39.SeedCalculator
import io.github.novacrypto.bip39.Words
import io.github.novacrypto.bip39.wordlists.English
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.security.SecureRandom
class MnemonicTest {
lateinit var mnemonics: MnemonicProvider
@Before
fun start() {
mnemonics = Mnemonics()
}
@Test
fun testSeed_fromMnemonic() {
val seed = mnemonics.run {
toSeed(nextMnemonic())
}
assertEquals(64, seed.size)
}
@Test
fun testMnemonic_create() {
val words = String(mnemonics.nextMnemonic()).split(' ')
assertEquals(24, words.size)
validate(words)
}
@Test
fun testMnemonic_createList() {
val words = mnemonics.nextMnemonicList()
assertEquals(24, words.size)
validate(words.map { String(it) })
}
private fun validate(words: List<String>) {
// return or crash!
words.forEach { word ->
var i = 0
while (true) {
if (English.INSTANCE.getWord(i++) == word) {
println(word)
break
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Sample code for working with SecureCharBuffer
// (but the underlying implementation isn't compatible with SeedCalculator.calculateSeed)
////////////////////////////////////////////////////////////////////////////////////////////////
@Test
fun testMneumonicFromSeed_secure() {
SecureCharBuffer().use { secure ->
val entropy = ByteArray(Words.TWENTY_FOUR.byteLength()).also {
SecureRandom().nextBytes(it)
MnemonicGenerator(English.INSTANCE).createMnemonic(it, secure::append)
}
val words = secure.toWords()
assertEquals(24, words.size)
words.forEach { word ->
// verify no spaces
assertTrue(word.all { it != ' ' })
}
val mnemonic = secure.toStringAble().toString()
val seed = SeedCalculator().calculateSeed(mnemonic, "")
assertEquals(64, seed.size)
}
}
}
private fun CharSequence.toWords(): List<CharSequence> {
return mutableListOf<CharSequence>().let { result ->
var index = 0
repeat(length) {
if (this[it] == ' ') {
result.add(subSequence(index, it))
index = it + 1
}
}
result.add(subSequence(index, length))
result
}
}

View File

@ -1,2 +1,2 @@
rootProject.name='Zcash Wallet'
include ':app', ':qrecycler', ':feedback'
include ':app', ':qrecycler', ':feedback', ':mnemonic'