mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-05-12 21:20:42 +01:00
lib: add mnemonic tests
This commit is contained in:
parent
16ff7b06db
commit
cb79e3421d
7 changed files with 169 additions and 28 deletions
|
@ -28,6 +28,7 @@ androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "
|
||||||
androidx-ui = { module = "androidx.compose.ui:ui" }
|
androidx-ui = { module = "androidx.compose.ui:ui" }
|
||||||
androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
|
androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
|
||||||
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
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" }
|
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-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" }
|
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
||||||
|
|
|
@ -105,6 +105,7 @@ dependencies {
|
||||||
implementation(libs.androidx.lifecycle.service)
|
implementation(libs.androidx.lifecycle.service)
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
|
||||||
|
testImplementation(libs.kotlin.junit)
|
||||||
testImplementation(testLibs.junit)
|
testImplementation(testLibs.junit)
|
||||||
testImplementation(testLibs.mockk)
|
testImplementation(testLibs.mockk)
|
||||||
testImplementation(testLibs.truth)
|
testImplementation(testLibs.truth)
|
||||||
|
|
|
@ -3,41 +3,65 @@ package im.molly.monero.mnemonics
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.molly.monero.parseHex
|
import im.molly.monero.parseHex
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class MoneroMnemonicTest {
|
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(
|
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",
|
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",
|
language = "en",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun validateKnownMnemonics() {
|
fun testKnownMnemonics() {
|
||||||
testVector.forEach {
|
testCases.forEach {
|
||||||
validateMnemonicGeneration(it)
|
validateMnemonicGeneration(it)
|
||||||
validateEntropyRecovery(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) {
|
private fun validateMnemonicGeneration(testCase: TestCase) {
|
||||||
val mnemonicCode = MoneroMnemonic.generateMnemonic(testCase.entropy.parseHex())
|
val mnemonicCode =
|
||||||
|
MoneroMnemonic.generateMnemonic(testCase.entropy, Locale(testCase.language))
|
||||||
assertMnemonicCode(mnemonicCode, testCase)
|
assertMnemonicCode(mnemonicCode, testCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateEntropyRecovery(testCase: TestCase) {
|
private fun validateEntropyRecovery(testCase: TestCase) {
|
||||||
val mnemonicCode = MoneroMnemonic.recoverEntropy(testCase.words)
|
val mnemonicCode = MoneroMnemonic.recoverEntropy(testCase.words)
|
||||||
assertThat(mnemonicCode).isNotNull()
|
assertMnemonicCode(mnemonicCode, testCase)
|
||||||
assertMnemonicCode(mnemonicCode!!, testCase)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertMnemonicCode(mnemonicCode: MnemonicCode, testCase: TestCase) {
|
private fun assertMnemonicCode(mnemonicCode: MnemonicCode?, testCase: TestCase) {
|
||||||
with(mnemonicCode) {
|
assertThat(mnemonicCode).isNotNull()
|
||||||
assertThat(entropy).isEqualTo(testCase.entropy.parseHex())
|
with(mnemonicCode!!) {
|
||||||
|
assertThat(entropy).isEqualTo(testCase.entropy)
|
||||||
assertThat(String(words)).isEqualTo(testCase.words)
|
assertThat(String(words)).isEqualTo(testCase.words)
|
||||||
assertThat(locale.language).isEqualTo(testCase.language)
|
assertThat(locale.language).isEqualTo(testCase.language)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,15 @@ class MnemonicCode private constructor(
|
||||||
val locale: Locale,
|
val locale: Locale,
|
||||||
) : Destroyable, Closeable, Iterable<CharArray> {
|
) : Destroyable, Closeable, Iterable<CharArray> {
|
||||||
|
|
||||||
constructor(entropy: ByteArray, words: CharBuffer, locale: Locale) : this(
|
constructor(entropy: ByteArray, words: CharBuffer, locale: Locale = Locale.ENGLISH) : this(
|
||||||
entropy.clone(),
|
entropy.clone(),
|
||||||
words.array().copyOfRange(words.position(), words.remaining()),
|
words.array().copyOfRange(words.position(), words.remaining()),
|
||||||
locale,
|
locale,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal val isNonZero
|
||||||
|
get() = !MessageDigest.isEqual(_entropy, ByteArray(_entropy.size))
|
||||||
|
|
||||||
val entropy: ByteArray
|
val entropy: ByteArray
|
||||||
get() = checkNotDestroyed { _entropy.clone() }
|
get() = checkNotDestroyed { _entropy.clone() }
|
||||||
|
|
||||||
|
@ -66,9 +69,9 @@ class MnemonicCode private constructor(
|
||||||
protected fun finalize() = destroy()
|
protected fun finalize() = destroy()
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean =
|
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 {
|
private inline fun <T> checkNotDestroyed(block: () -> T): T {
|
||||||
check(!destroyed) { "MnemonicCode has already been destroyed" }
|
check(!destroyed) { "MnemonicCode has already been destroyed" }
|
||||||
|
|
|
@ -7,7 +7,6 @@ import java.nio.CharBuffer
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
||||||
object MoneroMnemonic {
|
object MoneroMnemonic {
|
||||||
init {
|
init {
|
||||||
NativeLoader.loadMnemonicsLibrary()
|
NativeLoader.loadMnemonicsLibrary()
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package im.molly.monero
|
package im.molly.monero
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import org.junit.Assert.assertThrows
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
class SecretKeyTest {
|
class SecretKeyTest {
|
||||||
|
|
||||||
|
@ -14,20 +14,29 @@ class SecretKeyTest {
|
||||||
if (size == 32) {
|
if (size == 32) {
|
||||||
assertThat(SecretKey(secret).bytes).hasLength(size)
|
assertThat(SecretKey(secret).bytes).hasLength(size)
|
||||||
} else {
|
} 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
|
@Test
|
||||||
fun `secret keys cannot be zero`() {
|
fun `secret keys cannot be zero`() {
|
||||||
assertThrows(RuntimeException::class.java) { SecretKey(ByteArray(32)).bytes }
|
assertFailsWith<IllegalStateException> { SecretKey(ByteArray(32)).bytes }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when key is destroyed secret is zeroed`() {
|
fun `when key is destroyed secret is zeroed`() {
|
||||||
val secretBytes = Random.nextBytes(32)
|
val secretBytes = Random.nextBytes(32)
|
||||||
|
|
||||||
val key = SecretKey(secretBytes)
|
val key = SecretKey(secretBytes)
|
||||||
|
|
||||||
assertThat(key.destroyed).isFalse()
|
assertThat(key.destroyed).isFalse()
|
||||||
|
@ -37,20 +46,20 @@ class SecretKeyTest {
|
||||||
|
|
||||||
assertThat(key.destroyed).isTrue()
|
assertThat(key.destroyed).isTrue()
|
||||||
assertThat(key.isNonZero).isFalse()
|
assertThat(key.isNonZero).isFalse()
|
||||||
assertThrows(RuntimeException::class.java) { key.bytes }
|
assertFailsWith<IllegalStateException> { key.bytes }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `two keys with same secret are the same`() {
|
fun `two keys with same secret are equal`() {
|
||||||
val secret = Random.nextBytes(32)
|
val secret = Random.nextBytes(32)
|
||||||
|
|
||||||
val key = SecretKey(secret)
|
val key = SecretKey(secret)
|
||||||
val sameKey = SecretKey(secret)
|
val sameKey = SecretKey(secret)
|
||||||
val anotherKey = randomSecretKey()
|
val differentKey = randomSecretKey()
|
||||||
|
|
||||||
assertThat(key).isEqualTo(sameKey)
|
assertThat(key).isEqualTo(sameKey)
|
||||||
assertThat(sameKey).isNotEqualTo(anotherKey)
|
assertThat(sameKey).isNotEqualTo(differentKey)
|
||||||
assertThat(anotherKey).isNotEqualTo(key)
|
assertThat(differentKey).isNotEqualTo(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -64,7 +73,6 @@ class SecretKeyTest {
|
||||||
@Test
|
@Test
|
||||||
fun `keys are not equal to their destroyed versions`() {
|
fun `keys are not equal to their destroyed versions`() {
|
||||||
val secret = Random.nextBytes(32)
|
val secret = Random.nextBytes(32)
|
||||||
|
|
||||||
val key = SecretKey(secret)
|
val key = SecretKey(secret)
|
||||||
val destroyed = SecretKey(secret).also { it.destroy() }
|
val destroyed = SecretKey(secret).also { it.destroy() }
|
||||||
|
|
||||||
|
@ -73,9 +81,9 @@ class SecretKeyTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `destroyed keys are equal`() {
|
fun `destroyed keys are equal`() {
|
||||||
val destroyed = randomSecretKey().also { it.destroy() }
|
val destroyed1 = randomSecretKey().also { it.destroy() }
|
||||||
val anotherDestroyed = randomSecretKey().also { it.destroy() }
|
val destroyed2 = randomSecretKey().also { it.destroy() }
|
||||||
|
|
||||||
assertThat(destroyed).isEqualTo(anotherDestroyed)
|
assertThat(destroyed1).isEqualTo(destroyed2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue