From f70176d47c42ca6b542a158af4a7c28609793ccd Mon Sep 17 00:00:00 2001 From: Severiano Jaramillo Date: Fri, 12 Apr 2024 19:46:59 -0700 Subject: [PATCH] Add getKeyAndLanguageFromWords() method to Mnemonics class. - The getKeyAndLanguageFromWords() method receives a list of words and validates that they correspond to an actual Dero seed, and obtains the corresponding Key and language. - Added tests to verify that the getKeyAndLanguageFromWords() method works as expected in different scenarios. --- .../shared/stargate/mnemonics/Mnemonics.kt | 48 +++++++++++----- .../mnemonics/MnemonicsDataProvider.kt | 4 ++ .../stargate/mnemonics/MnemonicsTest.kt | 56 +++++++++++++++++++ 3 files changed, 93 insertions(+), 15 deletions(-) diff --git a/shared/stargate/src/commonMain/kotlin/net/agorise/shared/stargate/mnemonics/Mnemonics.kt b/shared/stargate/src/commonMain/kotlin/net/agorise/shared/stargate/mnemonics/Mnemonics.kt index 8e09182..107771e 100644 --- a/shared/stargate/src/commonMain/kotlin/net/agorise/shared/stargate/mnemonics/Mnemonics.kt +++ b/shared/stargate/src/commonMain/kotlin/net/agorise/shared/stargate/mnemonics/Mnemonics.kt @@ -1,7 +1,7 @@ package net.agorise.shared.stargate.mnemonics import dev.whyoleg.cryptography.bigint.BigInt -import dev.whyoleg.cryptography.bigint.toBigInt +import dev.whyoleg.cryptography.bigint.decodeToBigInt import io.ktor.utils.io.core.toByteArray import net.agorise.shared.crypto.crc.Crc32 @@ -40,28 +40,53 @@ class Mnemonics { /** * Given a list of words, returns the associated key if the words are valid or an exception otherwise */ - fun getKeyFromWords(words: List): Result { + fun getKeyAndLanguageFromWords(words: List): Result> { // The list must contain SEED_LENGTH + 1 words. The SEED_LENGTH + 1 word is the checksum if (words.size != (SEED_LENGTH + 1)) { return Result.failure(Exception("Invalid seed")) } - val wordsLanguageAndIndices = getWordsLanguageAndIndices(words) + val (languageIndex, indices) = getWordsLanguageAndIndices(words) ?: return Result.failure(Exception("Seed not found in any language")) - val languageName = languages[wordsLanguageAndIndices.languageIndex].name + val prefixLen = languages[languageIndex].uniquePrefixLength + if (verifyChecksum(words, prefixLen).not()) { + return Result.failure(Exception("Seed checksum verification failed")) + } + // Map 3 words to 4 bytes each, so 24 words = 32 bytes + val key = ByteArray(32) + val wordsSize = WORDS_SIZE.toULong() + for (i in 0 until SEED_LENGTH / 3) { + val w1 = indices[i * 3] + val w2 = indices[i * 3 + 1] + val w3 = indices[i * 3 + 2] - val key = byteArrayOf() + val value = w1 + wordsSize * ((wordsSize - w1 + w2) % wordsSize) + + wordsSize * wordsSize * ((wordsSize - w2 + w3) % wordsSize) - return Result.success("".toBigInt()) // TODO Remove hardcoded value + // Sanity check, this can never occur + if (value % wordsSize != w1) { + return Result.failure(Exception("Word list error")) + } + + val value32bit = value.toUInt() + key[i * 4 + 0] = (value32bit and 0xFFu).toByte() + key[i * 4 + 1] = ((value32bit shr 8) and 0xFFu).toByte() + key[i * 4 + 2] = ((value32bit shr 16) and 0xFFu).toByte() + key[i * 4 + 3] = ((value32bit shr 24) and 0xFFu).toByte() + } + + val languageName = languages[languageIndex].name + + return Result.success(Pair(key.decodeToBigInt(), languageName)) } /** * Returns the words language and the indices where each word was found. All words must be * from the same language. */ - private fun getWordsLanguageAndIndices(words: List): WordsLanguageAndIndices? { + private fun getWordsLanguageAndIndices(words: List): Pair>? { for (i in languages.indices) { val indices = mutableListOf() @@ -84,20 +109,13 @@ class Mnemonics { } if (foundAllWords) { - val wordsListCount = languages[i].words.size.toULong() - return WordsLanguageAndIndices(indices, i, wordsListCount) + return Pair(i, indices) } } return null } - private data class WordsLanguageAndIndices( - val indices: List, - val languageIndex: Int, - val wordListCount: ULong, - ) - /** * Obtains that the checksum index and verifies it corresponds to the checksum word. */ diff --git a/shared/stargate/src/commonTest/kotlin/net/agorise/shared/stargate/mnemonics/MnemonicsDataProvider.kt b/shared/stargate/src/commonTest/kotlin/net/agorise/shared/stargate/mnemonics/MnemonicsDataProvider.kt index 7993895..1074eb5 100644 --- a/shared/stargate/src/commonTest/kotlin/net/agorise/shared/stargate/mnemonics/MnemonicsDataProvider.kt +++ b/shared/stargate/src/commonTest/kotlin/net/agorise/shared/stargate/mnemonics/MnemonicsDataProvider.kt @@ -6,6 +6,10 @@ object MnemonicsDataProvider { ("sequence atlas unveil summon pebbles tuesday beer rudely snake rockets different " + "fuselage woven tagged bested dented vegan hover rapid fawns obvious muppet " + "randomly seasons summon").split(" ") + val invalidWordsSeed = + ("sequence atlas unveil summon pebbles tuesday beer rudely snake rockets different " + + "fuselage woven tagged bested dented vegan hover rapid fawns obvious muppet " + + "randomly seasons paella").split(" ") val validEnglishSeed = ("sequence atlas unveil summon pebbles tuesday beer rudely snake rockets different " + "fuselage woven tagged bested dented vegan hover rapid fawns obvious muppet " + diff --git a/shared/stargate/src/commonTest/kotlin/net/agorise/shared/stargate/mnemonics/MnemonicsTest.kt b/shared/stargate/src/commonTest/kotlin/net/agorise/shared/stargate/mnemonics/MnemonicsTest.kt index dfeb817..0670c0c 100644 --- a/shared/stargate/src/commonTest/kotlin/net/agorise/shared/stargate/mnemonics/MnemonicsTest.kt +++ b/shared/stargate/src/commonTest/kotlin/net/agorise/shared/stargate/mnemonics/MnemonicsTest.kt @@ -1,5 +1,6 @@ package net.agorise.shared.stargate.mnemonics +import dev.whyoleg.cryptography.bigint.toHexString import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -74,4 +75,59 @@ class MnemonicsTest { assertTrue(result) } + + @Test + fun `given an invalid size seed - when getKeyAndLanguageFromWords is called - then result is failure`() { + val invalidSizeSeed = MnemonicsDataProvider.invalidSizeSeed + + val result = mnemonics.getKeyAndLanguageFromWords(invalidSizeSeed) + + assertTrue(result.isFailure) + } + + @Test + fun `given an invalid words seed - when getKeyAndLanguageFromWords is called - then result is failure`() { + val invalidWordsSeed = MnemonicsDataProvider.invalidWordsSeed + + val result = mnemonics.getKeyAndLanguageFromWords(invalidWordsSeed) + + assertTrue(result.isFailure) + } + + @Test + fun `given an invalid checksum seed - when getKeyAndLanguageFromWords is called - then result is failure`() { + val invalidChecksumSeed = MnemonicsDataProvider.invalidChecksumSeed + + val result = mnemonics.getKeyAndLanguageFromWords(invalidChecksumSeed) + + assertTrue(result.isFailure) + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun `given a valid english seed - when getKeyAndLanguageFromWords is called - then result is correct`() { + val expectedKey = "b0ef6bd527b9b23b9ceef70dc8b4cd1ee83ca14541964e764ad23f5151204f0f" + val expectedLanguage = "English" + val validEnglishSeed = MnemonicsDataProvider.validEnglishSeed + + val result = mnemonics.getKeyAndLanguageFromWords(validEnglishSeed) + val (actualKey, actualLanguage) = result.getOrThrow() + + assertEquals(expectedKey, actualKey.toHexString()) + assertEquals(expectedLanguage, actualLanguage) + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun `given a valid spanish seed - when getKeyAndLanguageFromWords is called - then result is correct`() { + val expectedKey = "4f1101c4cc6adc6e6a63acde4e71fd76dc4471fa54769866d5e80a0a3d53d00c" + val expectedLanguage = "EspaƱol" + val validSpanishSeed = MnemonicsDataProvider.validSpanishSeed + + val result = mnemonics.getKeyAndLanguageFromWords(validSpanishSeed) + val (actualKey, actualLanguage) = result.getOrThrow() + + assertEquals(expectedKey, actualKey.toHexString()) + assertEquals(expectedLanguage, actualLanguage) + } }