Improvements and simplification. Tests now run about 4X faster.
Replaced PBKDF2 implementation with Java standard also Kotlinized @ebfull's Rust implementation of computeSentence.
This commit is contained in:
parent
227d5e7af4
commit
ada0c1d2a9
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
// initialize state
|
||||
var index = 0
|
||||
for (j in 0..10) {
|
||||
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 (concatBits[i * 11 + j]) index = index or 0x1
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -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…
Reference in New Issue