Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate Common and JVM-only code #158

Merged
merged 9 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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()
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
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
* about the source of words. Right now, words are kept in memory since only english is supported
* 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)
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package cash.z.ecc.android.common

internal expect interface Closeable {
fun close()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package cash.z.ecc.android.crypto

internal expect class FallbackProvider()
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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 [email protected]
* Modified to for Kotlin - Kevin Gorham [email protected]
*/
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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package cash.z.ecc.android.crypto

internal expect class SecretKeyCommon {
val encoded: ByteArray
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package cash.z.ecc.android.random

expect class SecureRandom() {
fun nextBytes(bytes: ByteArray)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -118,7 +118,7 @@ class MnemonicsTest : BehaviorSpec({
)
) { _, entropy, mnemonic ->
val code = MnemonicCode(entropy.fromHex())
String(code.chars) shouldBe mnemonic
code.chars.concatToString() shouldBe mnemonic
}
}
}
Expand All @@ -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
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package cash.z.ecc.android.common

internal actual interface Closeable : java.io.Closeable
Original file line number Diff line number Diff line change
Expand Up @@ -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" +
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import kotlin.math.ceil
* Modified to use SHA-512 - Ken Sedgwick [email protected]
* Modified to for Kotlin - Kevin Gorham [email protected]
*/
object Pbkdf2Sha512 {
internal actual object Pbkdf2Sha512 {

/**
* Generate a derived key from the given parameters.
Expand All @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this filename right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah the class matches the filename: ReadmeExamplesTestJvm
I moved that test out because use is only available on jvm

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