diff --git a/README.md b/README.md index a5ae31d..a0c71fd 100644 --- a/README.md +++ b/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 diff --git a/lib/src/main/java/cash/z/ecc/android/bip39/Mnemonics.kt b/lib/src/main/java/cash/z/ecc/android/bip39/Mnemonics.kt index c4a7e04..af17e1f 100644 --- a/lib/src/main/java/cash/z/ecc/android/bip39/Mnemonics.kt +++ b/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 { + // initialize state + var index = 0 + var bitsProcessed = 0 + var words = getCachedWords(languageCode) - /* 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() - 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 + // 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) { + // 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(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() - - bits[i * 8 + j] = tmp2 != zero - } - } - return bits +private fun ByteArray.toBits(): List { + return flatMap { b -> (7 downTo 0).map { (b.toInt() and (1 shl it)) != 0 } } +} + +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()) } diff --git a/lib/src/main/java/cash/z/ecc/android/bip39/Pbkdf2Sha256.kt b/lib/src/main/java/cash/z/ecc/android/bip39/Pbkdf2Sha256.kt deleted file mode 100644 index 52b535f..0000000 --- a/lib/src/main/java/cash/z/ecc/android/bip39/Pbkdf2Sha256.kt +++ /dev/null @@ -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/

- * 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() - } -}