Separate Common and JVM-only code

Also make classes in crypto package internal
This commit is contained in:
Luca Spinazzola 2023-02-10 07:35:32 -05:00 committed by GitHub
parent eb741fdc8f
commit cf397e3292
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 181 additions and 45 deletions

View File

@ -6,15 +6,11 @@ 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 cash.z.ecc.android.common.Closeable
import cash.z.ecc.android.crypto.FallbackProvider
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 javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import cash.z.ecc.android.crypto.PBEKeySpecCommon
import cash.z.ecc.android.crypto.SecretKeyFactoryCommon
import cash.z.ecc.android.random.SecureRandom
import kotlin.experimental.or
/**
@ -25,6 +21,7 @@ object Mnemonics {
const val DEFAULT_PASSPHRASE = "mnemonic"
const val INTERATION_COUNT = 2048
const val KEY_SIZE = 512
const val DEFAULT_LANGUAGE_CODE = "en"
internal val secureRandom = SecureRandom()
internal var cachedList = WordList()
@ -40,22 +37,22 @@ object Mnemonics {
// Inner Classes
//
class MnemonicCode(val chars: CharArray, val languageCode: String = Locale.ENGLISH.language) :
class MnemonicCode(val chars: CharArray, val languageCode: String = DEFAULT_LANGUAGE_CODE) :
Closeable, Iterable<String> {
constructor(
phrase: String,
languageCode: String = Locale.ENGLISH.language
languageCode: String = DEFAULT_LANGUAGE_CODE
) : this(phrase.toCharArray(), languageCode)
constructor(
entropy: ByteArray,
languageCode: String = Locale.ENGLISH.language
languageCode: String = DEFAULT_LANGUAGE_CODE
) : this(computeSentence(entropy), languageCode)
constructor(
wordCount: WordCount,
languageCode: String = Locale.ENGLISH.language
languageCode: String = DEFAULT_LANGUAGE_CODE
) : this(computeSentence(wordCount.toEntropy()), languageCode)
override fun close() = clear()
@ -88,7 +85,7 @@ object Mnemonics {
override fun next(): String {
val nextSpaceIndex = nextSpaceIndex()
val word = String(chars, cursor, nextSpaceIndex - cursor)
val word = chars.concatToString(cursor, cursor + (nextSpaceIndex - cursor))
cursor = nextSpaceIndex + 1
return word
}
@ -215,7 +212,7 @@ object Mnemonics {
*/
private fun computeSentence(
entropy: ByteArray,
languageCode: String = Locale.ENGLISH.language
languageCode: String = DEFAULT_LANGUAGE_CODE
): CharArray {
// initialize state
var index = 0
@ -336,11 +333,11 @@ fun MnemonicCode.toSeed(
// such as when it was just generated from new/correct entropy (common case for new seeds)
if (validate) validate()
return (DEFAULT_PASSPHRASE.toCharArray() + passphrase).toBytes().let { salt ->
PBEKeySpec(chars, salt, INTERATION_COUNT, KEY_SIZE).let { pbeKeySpec ->
PBEKeySpecCommon(chars, salt, INTERATION_COUNT, KEY_SIZE).let { pbeKeySpec ->
runCatching {
SecretKeyFactory.getInstance(PBE_ALGORITHM)
SecretKeyFactoryCommon.getInstance(PBE_ALGORITHM)
}.getOrElse {
SecretKeyFactory.getInstance(PBE_ALGORITHM, FallbackProvider())
SecretKeyFactoryCommon.getInstance(PBE_ALGORITHM, FallbackProvider())
}.let { keyFactory ->
keyFactory.generateSecret(pbeKeySpec).encoded.also {
pbeKeySpec.clearPassword()
@ -358,13 +355,12 @@ fun WordCount.toEntropy(): ByteArray = ByteArray(bitLength / 8).apply {
// Private Extensions
//
private fun ByteArray?.toSha256() = MessageDigest.getInstance("SHA-256").digest(this)
internal expect fun ByteArray.toSha256(): ByteArray
private fun ByteArray.toBits(): List<Boolean> = flatMap { it.toBits() }
private fun Byte.toBits(): List<Boolean> = (7 downTo 0).map { (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())
return map { it.code.toByte() }.toByteArray()
}

View File

@ -1,7 +1,6 @@
package cash.z.ecc.android.bip39
import java.lang.UnsupportedOperationException
import java.util.*
import cash.z.ecc.android.bip39.Mnemonics.DEFAULT_LANGUAGE_CODE
/**
* A Cached list of words. This serves as an abstraction, allowing collaborators to be agnostic
@ -9,8 +8,7 @@ import java.util.*
* but, eventually, they will come from the file system and library users should not have to change
* any code.
*/
class WordList internal constructor(val languageCode: String) {
constructor(locale: Locale = Locale.ENGLISH) : this(locale.language)
class WordList internal constructor(val languageCode: String = DEFAULT_LANGUAGE_CODE) {
init {
validate(languageCode)
@ -31,7 +29,7 @@ class WordList internal constructor(val languageCode: String) {
* Returns true when the given language code (like "en") is supported. Currently, only
* English is supported but this will change in future versions.
*/
fun isSupported(languageCode: String): Boolean = languageCode == Locale.ENGLISH.language
fun isSupported(languageCode: String): Boolean = languageCode == DEFAULT_LANGUAGE_CODE
/**
* Throws an error when the given language code is not supported.

View File

@ -0,0 +1,5 @@
package cash.z.ecc.android.common
internal expect interface Closeable {
fun close()
}

View File

@ -0,0 +1,3 @@
package cash.z.ecc.android.crypto
internal expect class FallbackProvider()

View File

@ -0,0 +1,11 @@
package cash.z.ecc.android.crypto
internal expect class PBEKeySpecCommon(password: CharArray?, salt: ByteArray?, iterationCount: Int, keyLength: Int) {
var password: CharArray?
var salt: ByteArray?
var iterationCount: Int
var keyLength: Int
fun clearPassword()
}

View File

@ -0,0 +1,29 @@
package cash.z.ecc.android.crypto
/**
*
* 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
* Modified to for Kotlin - Kevin Gorham anothergmale@gmail.com
*/
internal expect object Pbkdf2Sha512 {
/**
* Generate a derived key from the given parameters.
*
* @param p the password
* @param s the salt
* @param c the iteration count
* @param dkLen the key length in bits
*/
fun derive(p: CharArray, s: ByteArray, c: Int, dkLen: Int): ByteArray
}

View File

@ -0,0 +1,5 @@
package cash.z.ecc.android.crypto
internal expect class SecretKeyCommon {
val encoded: ByteArray
}

View File

@ -0,0 +1,10 @@
package cash.z.ecc.android.crypto
internal expect class SecretKeyFactoryCommon {
fun generateSecret(pbeKeySpec: PBEKeySpecCommon): SecretKeyCommon
companion object {
fun getInstance(algorithm: String): SecretKeyFactoryCommon
fun getInstance(algorithm: String, provider: FallbackProvider): SecretKeyFactoryCommon
}
}

View File

@ -0,0 +1,5 @@
package cash.z.ecc.android.random
expect class SecureRandom() {
fun nextBytes(bytes: ByteArray)
}

View File

@ -70,12 +70,12 @@ class MnemonicsTest : BehaviorSpec({
Mnemonics.WordCount.values().forEach { wordCount ->
When("a mnemonic phrase is created using the ${wordCount.name} enum value") {
MnemonicCode(wordCount).let { phrase ->
String(phrase.chars).asClue { phraseString ->
phrase.chars.concatToString().asClue { phraseString ->
Then("it has ${wordCount.count - 1} spaces") {
phrase.chars.count { it == ' ' } shouldBe wordCount.count - 1
}
And("when that is converted to a list of CharArrays") {
phrase.words.map { String(it) }.asClue { words ->
phrase.words.map { it.concatToString() }.asClue { words ->
Then("It has ${wordCount.count} elements") {
words.size shouldBe wordCount.count
}
@ -118,7 +118,7 @@ class MnemonicsTest : BehaviorSpec({
)
) { _, entropy, mnemonic ->
val code = MnemonicCode(entropy.fromHex())
String(code.chars) shouldBe mnemonic
code.chars.concatToString() shouldBe mnemonic
}
}
}
@ -131,7 +131,7 @@ class MnemonicsTest : BehaviorSpec({
englishTestData.forEach {
val entropy = it[0].fromHex()
val mnemonic = it[1]
String(MnemonicCode(entropy).chars) shouldBe mnemonic
MnemonicCode(entropy).chars.concatToString() shouldBe mnemonic
}
}
}

View File

@ -67,9 +67,9 @@ class ReadmeExamplesTest : ShouldSpec({
var seed: ByteArray
charArrayOf('z', 'c', 'a', 's', 'h').let { passphrase ->
seed = MnemonicCode(validPhrase).toSeed(passphrase)
String(passphrase) shouldBe "zcash"
passphrase.concatToString() shouldBe "zcash"
passphrase.fill('0')
String(passphrase) shouldBe "00000"
passphrase.concatToString() shouldBe "00000"
}
seed.size shouldBe 64
}
@ -93,14 +93,4 @@ class ReadmeExamplesTest : ShouldSpec({
count shouldBe 24
}
}
context("Example: auto-clear") {
should("clear the mnemonic when done") {
val mnemonicCode = MnemonicCode(WordCount.COUNT_24)
mnemonicCode.use {
mnemonicCode.wordCount shouldBe 24
}
// content gets automatically cleared after use!
mnemonicCode.wordCount shouldBe 0
}
}
})

View File

@ -44,7 +44,7 @@ class Pbkdf2Sha512Test : BehaviorSpec({
row("passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqKIzwOtMnlxQLttpE57Un4u12D2YD7oOPpiEvCDYvntXEe4NNPLCnGGeJArbYDEu6xDoCfWH6kbuV6awi04U", "saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcGMwbjzQKMSaf63IJemkURWoqHusIeVB8Il91NjiCGQacPUu9qTFaShLbKG0Yj4RCMV56WPj7E14EMpbxy6P", 113, 520, "e4c2be8f5cad779f90f54bec52888d6a1684f55d5145103515981217cc6609a039a86a41b3d22bae22f9a6687a605ae5c9e9dc411d83ba892f69af608b37fb89e8"),
row("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", 127, 520, "bb344a5712d07c4c49dfb9f77e44c5b4c29406c78c84214b07defb36a7898ae7a96c6cfeaf8d753b4bde382c4e48f247a90c17df79726228e2fed11c40b98e2648")
) { password: String, salt: String, count: Int, length: Int, expected: String ->
val result = Pbkdf2Sha512.derive(password.toCharArray(), salt.toByteArray(), count, length)
val result = Pbkdf2Sha512.derive(password.toCharArray(), salt.encodeToByteArray(), count, length)
result.toHex() shouldBe expected
}
}

View File

@ -0,0 +1,5 @@
package cash.z.ecc.android.bip39
import java.security.MessageDigest
internal actual fun ByteArray.toSha256(): ByteArray = MessageDigest.getInstance("SHA-256").digest(this)

View File

@ -0,0 +1,3 @@
package cash.z.ecc.android.common
internal actual interface Closeable : java.io.Closeable

View File

@ -16,7 +16,7 @@ import javax.crypto.spec.SecretKeySpec
// Constructor was deprecated in Java 9, but for compatibility with Android (Java 8, effectively) the old constructor
// must continue to be used.
@Suppress("DEPRECATION")
class FallbackProvider : Provider(
internal actual class FallbackProvider : Provider(
"FallbackProvider",
1.0,
"Provides a bridge to a default implementation of the PBKDF2WithHmacSHA512 algorithm" +

View File

@ -0,0 +1,24 @@
package cash.z.ecc.android.crypto
import javax.crypto.spec.PBEKeySpec
internal actual class PBEKeySpecCommon actual constructor(password: CharArray?, salt: ByteArray?, iterationCount: Int, keyLength: Int) {
val wrappedPbeKeySpec = PBEKeySpec(password, salt, iterationCount, keyLength)
actual var password: CharArray? = null
get() = wrappedPbeKeySpec.password
private set
actual var salt: ByteArray? = wrappedPbeKeySpec.salt
get() = wrappedPbeKeySpec.salt
private set
actual var iterationCount: Int = wrappedPbeKeySpec.iterationCount
get() = wrappedPbeKeySpec.iterationCount
private set
actual var keyLength: Int = wrappedPbeKeySpec.keyLength
get() = wrappedPbeKeySpec.keyLength
private set
actual fun clearPassword() {
wrappedPbeKeySpec.clearPassword()
}
}

View File

@ -21,7 +21,7 @@ import kotlin.math.ceil
* Modified to use SHA-512 - Ken Sedgwick ken@bonsai.com
* Modified to for Kotlin - Kevin Gorham anothergmale@gmail.com
*/
object Pbkdf2Sha512 {
internal actual object Pbkdf2Sha512 {
/**
* Generate a derived key from the given parameters.
@ -31,7 +31,7 @@ object Pbkdf2Sha512 {
* @param c the iteration count
* @param dkLen the key length in bits
*/
fun derive(p: CharArray, s: ByteArray, c: Int, dkLen: Int): ByteArray {
actual fun derive(p: CharArray, s: ByteArray, c: Int, dkLen: Int): ByteArray {
ByteArrayOutputStream().use { baos ->
val dkLenBytes = dkLen / 8
val pBytes = p.foldIndexed(ByteArray(p.size)) { i, acc, c ->

View File

@ -0,0 +1,8 @@
package cash.z.ecc.android.crypto
import javax.crypto.SecretKey
internal actual class SecretKeyCommon(generatedSecret: SecretKey) {
actual val encoded = generatedSecret.encoded
}

View File

@ -0,0 +1,15 @@
package cash.z.ecc.android.crypto
internal actual class SecretKeyFactoryCommon(private val jvmSecretKeyFactory: javax.crypto.SecretKeyFactory) {
actual fun generateSecret(pbeKeySpec: PBEKeySpecCommon): SecretKeyCommon =
SecretKeyCommon(jvmSecretKeyFactory.generateSecret(pbeKeySpec.wrappedPbeKeySpec))
actual companion object {
actual fun getInstance(algorithm: String): SecretKeyFactoryCommon =
SecretKeyFactoryCommon(javax.crypto.SecretKeyFactory.getInstance(algorithm))
actual fun getInstance(algorithm: String, provider: FallbackProvider): SecretKeyFactoryCommon =
SecretKeyFactoryCommon(javax.crypto.SecretKeyFactory.getInstance(algorithm))
}
}

View File

@ -0,0 +1,9 @@
package cash.z.ecc.android.random
actual class SecureRandom {
private val jvmSecureRandom = java.security.SecureRandom()
actual fun nextBytes(bytes: ByteArray) {
jvmSecureRandom.nextBytes(bytes)
}
}

View File

@ -0,0 +1,20 @@
package cash.z.ecc.android.bip39
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.Mnemonics.WordCount
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
class ReadmeExamplesTestJvm : ShouldSpec({
context("Example: auto-clear") {
should("clear the mnemonic when done") {
val mnemonicCode = MnemonicCode(WordCount.COUNT_24)
mnemonicCode.use {
mnemonicCode.wordCount shouldBe 24
}
// content gets automatically cleared after use!
mnemonicCode.wordCount shouldBe 0
}
}
})