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:
parent
9b2e7b318b
commit
1a64eb3fbd
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
rootProject.name='Zcash Wallet'
|
||||
include ':app', ':qrecycler', ':feedback'
|
||||
include ':app', ':qrecycler', ':feedback', ':mnemonic'
|
||||
|
|
Loading…
Reference in New Issue