diff --git a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/bip39/Mnemonics.kt b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/bip39/Mnemonics.kt similarity index 92% rename from bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/bip39/Mnemonics.kt rename to bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/bip39/Mnemonics.kt index f98121b..91561cc 100644 --- a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/bip39/Mnemonics.kt +++ b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/bip39/Mnemonics.kt @@ -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 { 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 = flatMap { it.toBits() } private fun Byte.toBits(): List = (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() } diff --git a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/bip39/WordList.kt b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/bip39/WordList.kt similarity index 99% rename from bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/bip39/WordList.kt rename to bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/bip39/WordList.kt index 721efc2..e54486e 100644 --- a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/bip39/WordList.kt +++ b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/bip39/WordList.kt @@ -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. diff --git a/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/common/Closeable.kt b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/common/Closeable.kt new file mode 100644 index 0000000..fd46294 --- /dev/null +++ b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/common/Closeable.kt @@ -0,0 +1,5 @@ +package cash.z.ecc.android.common + +internal expect interface Closeable { + fun close() +} diff --git a/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/FallbackProvider.kt b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/FallbackProvider.kt new file mode 100644 index 0000000..9ad4942 --- /dev/null +++ b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/FallbackProvider.kt @@ -0,0 +1,3 @@ +package cash.z.ecc.android.crypto + +internal expect class FallbackProvider() diff --git a/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/PBEKeySpecCommon.kt b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/PBEKeySpecCommon.kt new file mode 100644 index 0000000..1cb56b2 --- /dev/null +++ b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/PBEKeySpecCommon.kt @@ -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() +} diff --git a/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/Pbkdf2Sha512.kt b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/Pbkdf2Sha512.kt new file mode 100644 index 0000000..feb9b43 --- /dev/null +++ b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/Pbkdf2Sha512.kt @@ -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/

+ * 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 +} diff --git a/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/SecretKeyCommon.kt b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/SecretKeyCommon.kt new file mode 100644 index 0000000..ee435b6 --- /dev/null +++ b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/SecretKeyCommon.kt @@ -0,0 +1,5 @@ +package cash.z.ecc.android.crypto + +internal expect class SecretKeyCommon { + val encoded: ByteArray +} diff --git a/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/SecretKeyFactoryCommon.kt b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/SecretKeyFactoryCommon.kt new file mode 100644 index 0000000..ede0181 --- /dev/null +++ b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/crypto/SecretKeyFactoryCommon.kt @@ -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 + } +} diff --git a/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/random/SecureRandom.kt b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/random/SecureRandom.kt new file mode 100644 index 0000000..f8be32e --- /dev/null +++ b/bip39-lib/src/commonMain/kotlin/cash/z/ecc/android/random/SecureRandom.kt @@ -0,0 +1,5 @@ +package cash.z.ecc.android.random + +expect class SecureRandom() { + fun nextBytes(bytes: ByteArray) +} diff --git a/bip39-lib/src/commonTest/kotlin/cash/z/ecc/android/bip39/MnemonicsTest.kt b/bip39-lib/src/commonTest/kotlin/cash/z/ecc/android/bip39/MnemonicsTest.kt index 8aaeae2..62227d9 100644 --- a/bip39-lib/src/commonTest/kotlin/cash/z/ecc/android/bip39/MnemonicsTest.kt +++ b/bip39-lib/src/commonTest/kotlin/cash/z/ecc/android/bip39/MnemonicsTest.kt @@ -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 } } } diff --git a/bip39-lib/src/commonTest/kotlin/cash/z/ecc/android/bip39/ReadmeExamplesTest.kt b/bip39-lib/src/commonTest/kotlin/cash/z/ecc/android/bip39/ReadmeExamplesTest.kt index 52d625f..86258c2 100644 --- a/bip39-lib/src/commonTest/kotlin/cash/z/ecc/android/bip39/ReadmeExamplesTest.kt +++ b/bip39-lib/src/commonTest/kotlin/cash/z/ecc/android/bip39/ReadmeExamplesTest.kt @@ -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 - } - } }) diff --git a/bip39-lib/src/commonTest/kotlin/cash/z/ecc/android/crypto/Pbkdf2Sha512Test.kt b/bip39-lib/src/commonTest/kotlin/cash/z/ecc/android/crypto/Pbkdf2Sha512Test.kt index d128902..3699b83 100644 --- a/bip39-lib/src/commonTest/kotlin/cash/z/ecc/android/crypto/Pbkdf2Sha512Test.kt +++ b/bip39-lib/src/commonTest/kotlin/cash/z/ecc/android/crypto/Pbkdf2Sha512Test.kt @@ -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 } } diff --git a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/bip39/ToSha256.kt b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/bip39/ToSha256.kt new file mode 100644 index 0000000..63223ce --- /dev/null +++ b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/bip39/ToSha256.kt @@ -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) diff --git a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/common/Closeable.kt b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/common/Closeable.kt new file mode 100644 index 0000000..2de774c --- /dev/null +++ b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/common/Closeable.kt @@ -0,0 +1,3 @@ +package cash.z.ecc.android.common + +internal actual interface Closeable : java.io.Closeable diff --git a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/FallbackProvider.kt b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/FallbackProvider.kt index 286351b..b7cfe06 100644 --- a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/FallbackProvider.kt +++ b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/FallbackProvider.kt @@ -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" + diff --git a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/PBEKeySpecCommon.kt b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/PBEKeySpecCommon.kt new file mode 100644 index 0000000..29a33bd --- /dev/null +++ b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/PBEKeySpecCommon.kt @@ -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() + } +} diff --git a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/Pbkdf2Sha512.kt b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/Pbkdf2Sha512.kt index 0ce0a87..8e108dd 100644 --- a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/Pbkdf2Sha512.kt +++ b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/Pbkdf2Sha512.kt @@ -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 -> diff --git a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/SecretKeyCommon.kt b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/SecretKeyCommon.kt new file mode 100644 index 0000000..3a59cb0 --- /dev/null +++ b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/SecretKeyCommon.kt @@ -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 +} diff --git a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/SecretKeyFactoryCommon.kt b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/SecretKeyFactoryCommon.kt new file mode 100644 index 0000000..0d07aeb --- /dev/null +++ b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/crypto/SecretKeyFactoryCommon.kt @@ -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)) + } +} diff --git a/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/random/SecureRandom.kt b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/random/SecureRandom.kt new file mode 100644 index 0000000..1a4162d --- /dev/null +++ b/bip39-lib/src/jvmMain/kotlin/cash/z/ecc/android/random/SecureRandom.kt @@ -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) + } +} diff --git a/bip39-lib/src/jvmTest/kotlin/cash/z/ecc/android/bip39/ReadmeExamplesTestJvm.kt b/bip39-lib/src/jvmTest/kotlin/cash/z/ecc/android/bip39/ReadmeExamplesTestJvm.kt new file mode 100644 index 0000000..cd4762f --- /dev/null +++ b/bip39-lib/src/jvmTest/kotlin/cash/z/ecc/android/bip39/ReadmeExamplesTestJvm.kt @@ -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 + } + } +})