From cb79e3421d7def58ff654f70fc6d55b979863989 Mon Sep 17 00:00:00 2001 From: Oscar Mira Date: Thu, 30 Jan 2025 20:41:08 +0100 Subject: [PATCH] lib: add mnemonic tests --- gradle/libs.versions.toml | 1 + lib/android/build.gradle.kts | 1 + .../monero/mnemonics/MoneroMnemonicTest.kt | 46 ++++++-- .../im/molly/monero/mnemonics/MnemonicCode.kt | 9 +- .../molly/monero/mnemonics/MoneroMnemonic.kt | 1 - .../kotlin/im/molly/monero/SecretKeyTest.kt | 34 +++--- .../molly/monero/mnemonic/MnemonicCodeTest.kt | 105 ++++++++++++++++++ 7 files changed, 169 insertions(+), 28 deletions(-) create mode 100644 lib/android/src/test/kotlin/im/molly/monero/mnemonic/MnemonicCodeTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2c3ea9c..df0a4d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,7 @@ androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = " androidx-ui = { module = "androidx.compose.ui:ui" } androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +kotlin-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines"} kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } diff --git a/lib/android/build.gradle.kts b/lib/android/build.gradle.kts index 31b3678..f808679 100644 --- a/lib/android/build.gradle.kts +++ b/lib/android/build.gradle.kts @@ -105,6 +105,7 @@ dependencies { implementation(libs.androidx.lifecycle.service) implementation(libs.kotlinx.coroutines.android) + testImplementation(libs.kotlin.junit) testImplementation(testLibs.junit) testImplementation(testLibs.mockk) testImplementation(testLibs.truth) diff --git a/lib/android/src/androidTest/kotlin/im/molly/monero/mnemonics/MoneroMnemonicTest.kt b/lib/android/src/androidTest/kotlin/im/molly/monero/mnemonics/MoneroMnemonicTest.kt index 2bc86c3..3d3a60d 100644 --- a/lib/android/src/androidTest/kotlin/im/molly/monero/mnemonics/MoneroMnemonicTest.kt +++ b/lib/android/src/androidTest/kotlin/im/molly/monero/mnemonics/MoneroMnemonicTest.kt @@ -3,41 +3,65 @@ package im.molly.monero.mnemonics import com.google.common.truth.Truth.assertThat import im.molly.monero.parseHex import org.junit.Test +import java.util.Locale class MoneroMnemonicTest { - data class TestCase(val entropy: String, val words: String, val language: String) + data class TestCase(val key: String, val words: String, val language: String) { + val entropy = key.parseHex() + } - private val testVector = listOf( + private val testCases = listOf( TestCase( - entropy = "3b094ca7218f175e91fa2402b4ae239a2fe8262792a3e718533a1a357a1e4109", + key = "3b094ca7218f175e91fa2402b4ae239a2fe8262792a3e718533a1a357a1e4109", words = "tavern judge beyond bifocals deepest mural onward dummy eagle diode gained vacation rally cause firm idled jerseys moat vigilant upload bobsled jobs cunning doing jobs", language = "en", ), ) @Test - fun validateKnownMnemonics() { - testVector.forEach { + fun testKnownMnemonics() { + testCases.forEach { validateMnemonicGeneration(it) validateEntropyRecovery(it) } } + @Test(expected = IllegalArgumentException::class) + fun testEmptyEntropy() { + MoneroMnemonic.generateMnemonic(ByteArray(0)) + } + + @Test(expected = IllegalArgumentException::class) + fun testInvalidEntropy() { + MoneroMnemonic.generateMnemonic(ByteArray(2)) + } + + @Test(expected = IllegalArgumentException::class) + fun testEmptyWords() { + MoneroMnemonic.recoverEntropy("") + } + + @Test(expected = IllegalArgumentException::class) + fun testInvalidLanguage() { + MoneroMnemonic.generateMnemonic(ByteArray(32), Locale("ZZ")) + } + private fun validateMnemonicGeneration(testCase: TestCase) { - val mnemonicCode = MoneroMnemonic.generateMnemonic(testCase.entropy.parseHex()) + val mnemonicCode = + MoneroMnemonic.generateMnemonic(testCase.entropy, Locale(testCase.language)) assertMnemonicCode(mnemonicCode, testCase) } private fun validateEntropyRecovery(testCase: TestCase) { val mnemonicCode = MoneroMnemonic.recoverEntropy(testCase.words) - assertThat(mnemonicCode).isNotNull() - assertMnemonicCode(mnemonicCode!!, testCase) + assertMnemonicCode(mnemonicCode, testCase) } - private fun assertMnemonicCode(mnemonicCode: MnemonicCode, testCase: TestCase) { - with(mnemonicCode) { - assertThat(entropy).isEqualTo(testCase.entropy.parseHex()) + private fun assertMnemonicCode(mnemonicCode: MnemonicCode?, testCase: TestCase) { + assertThat(mnemonicCode).isNotNull() + with(mnemonicCode!!) { + assertThat(entropy).isEqualTo(testCase.entropy) assertThat(String(words)).isEqualTo(testCase.words) assertThat(locale.language).isEqualTo(testCase.language) } diff --git a/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MnemonicCode.kt b/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MnemonicCode.kt index 0a9e9c6..15c2d2f 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MnemonicCode.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MnemonicCode.kt @@ -12,12 +12,15 @@ class MnemonicCode private constructor( val locale: Locale, ) : Destroyable, Closeable, Iterable { - constructor(entropy: ByteArray, words: CharBuffer, locale: Locale) : this( + constructor(entropy: ByteArray, words: CharBuffer, locale: Locale = Locale.ENGLISH) : this( entropy.clone(), words.array().copyOfRange(words.position(), words.remaining()), locale, ) + internal val isNonZero + get() = !MessageDigest.isEqual(_entropy, ByteArray(_entropy.size)) + val entropy: ByteArray get() = checkNotDestroyed { _entropy.clone() } @@ -66,9 +69,9 @@ class MnemonicCode private constructor( protected fun finalize() = destroy() override fun equals(other: Any?): Boolean = - this === other || (other is MnemonicCode && MessageDigest.isEqual(entropy, other.entropy)) + this === other || (other is MnemonicCode && MessageDigest.isEqual(_entropy, other._entropy)) - override fun hashCode(): Int = entropy.contentHashCode() + override fun hashCode(): Int = _entropy.contentHashCode() private inline fun checkNotDestroyed(block: () -> T): T { check(!destroyed) { "MnemonicCode has already been destroyed" } diff --git a/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MoneroMnemonic.kt b/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MoneroMnemonic.kt index 09569bf..268ce71 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MoneroMnemonic.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MoneroMnemonic.kt @@ -7,7 +7,6 @@ import java.nio.CharBuffer import java.nio.charset.StandardCharsets import java.util.Locale - object MoneroMnemonic { init { NativeLoader.loadMnemonicsLibrary() diff --git a/lib/android/src/test/kotlin/im/molly/monero/SecretKeyTest.kt b/lib/android/src/test/kotlin/im/molly/monero/SecretKeyTest.kt index db46b72..913f502 100644 --- a/lib/android/src/test/kotlin/im/molly/monero/SecretKeyTest.kt +++ b/lib/android/src/test/kotlin/im/molly/monero/SecretKeyTest.kt @@ -1,9 +1,9 @@ package im.molly.monero import com.google.common.truth.Truth.assertThat -import org.junit.Assert.assertThrows import org.junit.Test import kotlin.random.Random +import kotlin.test.assertFailsWith class SecretKeyTest { @@ -14,20 +14,29 @@ class SecretKeyTest { if (size == 32) { assertThat(SecretKey(secret).bytes).hasLength(size) } else { - assertThrows(RuntimeException::class.java) { SecretKey(secret) } + assertFailsWith { SecretKey(secret) } } } } + @Test + fun `secret key copies buffer`() { + val secretBytes = Random.nextBytes(32) + val key = SecretKey(secretBytes) + + assertThat(key.bytes).isEqualTo(secretBytes) + secretBytes.fill(0) + assertThat(key.bytes).isNotEqualTo(secretBytes) + } + @Test fun `secret keys cannot be zero`() { - assertThrows(RuntimeException::class.java) { SecretKey(ByteArray(32)).bytes } + assertFailsWith { SecretKey(ByteArray(32)).bytes } } @Test fun `when key is destroyed secret is zeroed`() { val secretBytes = Random.nextBytes(32) - val key = SecretKey(secretBytes) assertThat(key.destroyed).isFalse() @@ -37,20 +46,20 @@ class SecretKeyTest { assertThat(key.destroyed).isTrue() assertThat(key.isNonZero).isFalse() - assertThrows(RuntimeException::class.java) { key.bytes } + assertFailsWith { key.bytes } } @Test - fun `two keys with same secret are the same`() { + fun `two keys with same secret are equal`() { val secret = Random.nextBytes(32) val key = SecretKey(secret) val sameKey = SecretKey(secret) - val anotherKey = randomSecretKey() + val differentKey = randomSecretKey() assertThat(key).isEqualTo(sameKey) - assertThat(sameKey).isNotEqualTo(anotherKey) - assertThat(anotherKey).isNotEqualTo(key) + assertThat(sameKey).isNotEqualTo(differentKey) + assertThat(differentKey).isNotEqualTo(key) } @Test @@ -64,7 +73,6 @@ class SecretKeyTest { @Test fun `keys are not equal to their destroyed versions`() { val secret = Random.nextBytes(32) - val key = SecretKey(secret) val destroyed = SecretKey(secret).also { it.destroy() } @@ -73,9 +81,9 @@ class SecretKeyTest { @Test fun `destroyed keys are equal`() { - val destroyed = randomSecretKey().also { it.destroy() } - val anotherDestroyed = randomSecretKey().also { it.destroy() } + val destroyed1 = randomSecretKey().also { it.destroy() } + val destroyed2 = randomSecretKey().also { it.destroy() } - assertThat(destroyed).isEqualTo(anotherDestroyed) + assertThat(destroyed1).isEqualTo(destroyed2) } } diff --git a/lib/android/src/test/kotlin/im/molly/monero/mnemonic/MnemonicCodeTest.kt b/lib/android/src/test/kotlin/im/molly/monero/mnemonic/MnemonicCodeTest.kt new file mode 100644 index 0000000..1a623a0 --- /dev/null +++ b/lib/android/src/test/kotlin/im/molly/monero/mnemonic/MnemonicCodeTest.kt @@ -0,0 +1,105 @@ +package im.molly.monero.mnemonic + +import com.google.common.truth.Truth.assertThat +import im.molly.monero.mnemonics.MnemonicCode +import org.junit.Test +import java.nio.CharBuffer +import java.util.Locale +import kotlin.random.Random +import kotlin.test.assertFailsWith + +class MnemonicCodeTest { + + private fun randomEntropy(size: Int = 32): ByteArray = Random.nextBytes(size) + + private fun charBufferOf(str: String): CharBuffer = CharBuffer.wrap(str.toCharArray()) + + @Test + fun `mnemonic copies entropy and words`() { + val entropy = randomEntropy() + val words = charBufferOf("arbre soleil maison") + val locale = Locale.FRANCE + + val mnemonic = MnemonicCode(entropy, words, locale) + + assertThat(mnemonic.entropy).isEqualTo(entropy) + assertThat(mnemonic.words).isEqualTo(words.array()) + assertThat(mnemonic.locale).isEqualTo(locale) + + entropy.fill(0) + words.put("modified".toCharArray()) + + assertThat(mnemonic.entropy).isNotEqualTo(entropy) + assertThat(mnemonic.words).isNotEqualTo(words.array()) + } + + @Test + fun `destroyed mnemonic code zeroes entropy and words`() { + val entropy = randomEntropy() + val words = charBufferOf("test mnemonic") + + val mnemonic = MnemonicCode(entropy, words) + + mnemonic.destroy() + + assertThat(mnemonic.destroyed).isTrue() + assertThat(mnemonic.isNonZero).isFalse() + assertFailsWith { mnemonic.words } + assertFailsWith { mnemonic.entropy } + } + + @Test + fun `two mnemonics with same entropy are equal`() { + val entropy = randomEntropy() + val words = charBufferOf("test mnemonic") + val locale = Locale.ENGLISH + + val mnemonic = MnemonicCode(entropy, words, locale) + val sameMnemonic = MnemonicCode(entropy, words, locale) + val differentMnemonic = MnemonicCode(randomEntropy(), words, locale) + + assertThat(mnemonic).isEqualTo(sameMnemonic) + assertThat(differentMnemonic).isNotEqualTo(mnemonic) + } + + @Test + fun `iterator correctly iterates words`() { + val words = charBufferOf("word1 word2 word3") + val mnemonic = MnemonicCode(randomEntropy(), words) + + val iteratedWords = mnemonic.map { String(it) } + + assertThat(iteratedWords).containsExactly("word1", "word2", "word3").inOrder() + } + + @Test + fun `calling next on iterator without checking hasNext throws exception`() { + val words = charBufferOf("test mnemonic") + val mnemonic = MnemonicCode(randomEntropy(), words) + val iterator = mnemonic.iterator() + + iterator.next() + iterator.next() + + assertFailsWith { iterator.next() } + } + + @Test + fun `mnemonics are not equal to their destroyed versions`() { + val entropy = randomEntropy() + val words = charBufferOf("test mnemonic") + + val mnemonic = MnemonicCode(entropy, words) + val destroyed = MnemonicCode(entropy, words).also { it.destroy() } + + assertThat(mnemonic).isNotEqualTo(destroyed) + } + + @Test + fun `destroyed mnemonics are equal`() { + val destroyed1 = MnemonicCode(randomEntropy(), charBufferOf("word1")).also { it.destroy() } + val destroyed2 = MnemonicCode(randomEntropy(), charBufferOf("word2")).also { it.destroy() } + + assertThat(destroyed1).isEqualTo(destroyed2) + } +}