lib: add mnemonic tests

This commit is contained in:
Oscar Mira 2025-01-30 20:41:08 +01:00
parent 16ff7b06db
commit cb79e3421d
No known key found for this signature in database
GPG Key ID: B371B98C5DC32237
7 changed files with 169 additions and 28 deletions

View File

@ -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" }

View File

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

View File

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

View File

@ -12,12 +12,15 @@ class MnemonicCode private constructor(
val locale: Locale,
) : Destroyable, Closeable, Iterable<CharArray> {
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 <T> checkNotDestroyed(block: () -> T): T {
check(!destroyed) { "MnemonicCode has already been destroyed" }

View File

@ -7,7 +7,6 @@ import java.nio.CharBuffer
import java.nio.charset.StandardCharsets
import java.util.Locale
object MoneroMnemonic {
init {
NativeLoader.loadMnemonicsLibrary()

View File

@ -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<IllegalArgumentException> { 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<IllegalStateException> { 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<IllegalStateException> { 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)
}
}

View File

@ -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<IllegalStateException> { mnemonic.words }
assertFailsWith<IllegalStateException> { 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<NoSuchElementException> { 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)
}
}