Browse Source

Improvements and simplification. Tests now run about 4X faster.

Replaced PBKDF2 implementation with Java standard also Kotlinized @ebfull's Rust implementation of computeSentence.
master
Kevin Gorham 4 years ago
parent
commit
ada0c1d2a9
No known key found for this signature in database GPG Key ID: CCA55602DF49FC38
  1. 2
      README.md
  2. 132
      lib/src/main/java/cash/z/ecc/android/bip39/Mnemonics.kt
  3. 127
      lib/src/main/java/cash/z/ecc/android/bip39/Pbkdf2Sha256.kt

2
README.md

@ -147,8 +147,6 @@ val mnemonicCode = MnemonicCode(WordCount.COUNT_24, languageCode = Locale.ENGLIS
* [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

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

@ -1,18 +1,29 @@
package cash.z.ecc.android.bip39
import cash.z.ecc.android.bip39.Mnemonics.DEFAULT_PASSPHRASE
import cash.z.ecc.android.bip39.Mnemonics.INTERATION_COUNT
import cash.z.ecc.android.bip39.Mnemonics.KEY_SIZE
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.Mnemonics.PBE_ALGORITHM
import cash.z.ecc.android.bip39.Mnemonics.WordCount
import java.io.Closeable
import java.nio.CharBuffer
import java.nio.charset.Charset
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.*
import kotlin.experimental.and
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import kotlin.experimental.or
/**
* Encompasses all mnemonic functionality, which helps keep everything concise and in one place.
*/
object Mnemonics {
const val PBE_ALGORITHM = "PBKDF2WithHmacSHA512"
const val DEFAULT_PASSPHRASE = "mnemonic"
const val INTERATION_COUNT = 2048
const val KEY_SIZE = 512
internal val secureRandom = SecureRandom()
internal var cachedList = WordList()
@ -40,12 +51,12 @@ object Mnemonics {
constructor(
entropy: ByteArray,
languageCode: String = Locale.ENGLISH.language
) : this(fromEntropy(entropy), languageCode)
) : this(computeSentence(entropy), languageCode)
constructor(
wordCount: WordCount,
languageCode: String = Locale.ENGLISH.language
) : this(fromEntropy(wordCount.toEntropy()), languageCode)
) : this(computeSentence(wordCount.toEntropy()), languageCode)
override fun close() = clear()
@ -163,7 +174,7 @@ object Mnemonics {
// Take the digest of the entropy.
val hash: ByteArray = entropy.toSha256()
val hashBits = hash.toBitArray()
val hashBits = hash.toBits()
// Check all the checksum bits.
for (i in 0 until checksumLengthBits)
@ -189,63 +200,44 @@ object Mnemonics {
*
* @see WordCount.toEntropy
*/
private fun fromEntropy(
private fun computeSentence(
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
// initialize state
var index = 0
var bitsProcessed = 0
var words = getCachedWords(languageCode)
// inner function that updates the index and copies a word after every 11 bits
// Note: the excess bits of the checksum are intentionally ignored, per BIP-39
fun processBit(bit: Boolean, chars: ArrayList<Char>) {
// update the index
index = index shl 1
if (bit) index = index or 1
// if we're at a word boundary
if ((++bitsProcessed).rem(11) == 0) {
// copy over the word and restart the index
words[index].forEach { chars.add(it) }
chars.add(' ')
index = 0
}
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]
// Compute the first byte of the checksum by SHA256(entropy)
val checksum = entropy.toSha256()[0]
return (entropy + checksum).toBits().let { bits ->
// initial size of max char count, to minimize array copies (size * 3/32 * 8)
ArrayList<Char>(entropy.size * 3/4).also { chars ->
bits.forEach { processBit(it, chars) }
// trim final space to avoid the need to track the number of words completed
chars.removeAt(chars.lastIndex)
}.let { result ->
// returning the result as a charArray creates a copy so clear the original
// so that it doesn't sit in memory until garbage collection
result.toCharArray().also { result.clear() }
}
if (cursor < result.size) result[cursor++] = wordSeparator
}
words.clear()
return result
}
}
}
@ -313,20 +305,27 @@ object Mnemonics {
* 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 passphrase an optional password to protect the phrase. Defaults to an empty string. This
* gets added to the salt. Note: it is expected that the passphrase has been normalized via a call
* to something like `Normalizer.normalize(passphrase, Normalizer.Form.NFKD)` but this only becomes
* important when additional language support is added.
* @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(
// expect: UTF-8 normalized with NFKD
passphrase: CharArray = charArrayOf(),
validate: Boolean = true
): ByteArray {
if (validate) validate()
val salt = "mnemonic".toCharArray() + passphrase
return Pbkdf2Sha256.derive(chars, salt, 2048, 64)
return (DEFAULT_PASSPHRASE.toCharArray() + passphrase).toBytes().let { salt ->
PBEKeySpec(chars, salt, INTERATION_COUNT, KEY_SIZE).let { pbeKeySpec ->
SecretKeyFactory.getInstance(PBE_ALGORITHM).generateSecret(pbeKeySpec).encoded.also {
pbeKeySpec.clearPassword()
}
}
}
}
fun WordCount.toEntropy(): ByteArray = ByteArray(bitLength / 8).apply {
@ -340,16 +339,11 @@ fun WordCount.toEntropy(): ByteArray = ByteArray(bitLength / 8).apply {
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()
private fun ByteArray.toBits(): List<Boolean> {
return flatMap { b -> (7 downTo 0).map { (b.toInt() and (1 shl it)) != 0 } }
}
bits[i * 8 + j] = tmp2 != zero
}
}
return bits
private fun CharArray.toBytes(): ByteArray {
val byteBuffer = CharBuffer.wrap(this).let { Charset.forName("UTF-8").encode(it) }
return byteBuffer.array().copyOfRange(byteBuffer.position(), byteBuffer.limit())
}

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

@ -1,127 +0,0 @@
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()
}
}
Loading…
Cancel
Save