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.
This commit is contained in:
Severiano Jaramillo 2024-04-12 19:46:59 -07:00
parent c690d839da
commit f70176d47c
3 changed files with 93 additions and 15 deletions

View file

@ -1,7 +1,7 @@
package net.agorise.shared.stargate.mnemonics package net.agorise.shared.stargate.mnemonics
import dev.whyoleg.cryptography.bigint.BigInt 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 io.ktor.utils.io.core.toByteArray
import net.agorise.shared.crypto.crc.Crc32 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 * Given a list of words, returns the associated key if the words are valid or an exception otherwise
*/ */
fun getKeyFromWords(words: List<String>): Result<BigInt> { fun getKeyAndLanguageFromWords(words: List<String>): Result<Pair<BigInt, String>> {
// The list must contain SEED_LENGTH + 1 words. The SEED_LENGTH + 1 word is the checksum // The list must contain SEED_LENGTH + 1 words. The SEED_LENGTH + 1 word is the checksum
if (words.size != (SEED_LENGTH + 1)) { if (words.size != (SEED_LENGTH + 1)) {
return Result.failure(Exception("Invalid seed")) 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")) ?: 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 * Returns the words language and the indices where each word was found. All words must be
* from the same language. * from the same language.
*/ */
private fun getWordsLanguageAndIndices(words: List<String>): WordsLanguageAndIndices? { private fun getWordsLanguageAndIndices(words: List<String>): Pair<Int, List<ULong>>? {
for (i in languages.indices) { for (i in languages.indices) {
val indices = mutableListOf<ULong>() val indices = mutableListOf<ULong>()
@ -84,20 +109,13 @@ class Mnemonics {
} }
if (foundAllWords) { if (foundAllWords) {
val wordsListCount = languages[i].words.size.toULong() return Pair(i, indices)
return WordsLanguageAndIndices(indices, i, wordsListCount)
} }
} }
return null return null
} }
private data class WordsLanguageAndIndices(
val indices: List<ULong>,
val languageIndex: Int,
val wordListCount: ULong,
)
/** /**
* Obtains that the checksum index and verifies it corresponds to the checksum word. * Obtains that the checksum index and verifies it corresponds to the checksum word.
*/ */

View file

@ -6,6 +6,10 @@ object MnemonicsDataProvider {
("sequence atlas unveil summon pebbles tuesday beer rudely snake rockets different " + ("sequence atlas unveil summon pebbles tuesday beer rudely snake rockets different " +
"fuselage woven tagged bested dented vegan hover rapid fawns obvious muppet " + "fuselage woven tagged bested dented vegan hover rapid fawns obvious muppet " +
"randomly seasons summon").split(" ") "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 = val validEnglishSeed =
("sequence atlas unveil summon pebbles tuesday beer rudely snake rockets different " + ("sequence atlas unveil summon pebbles tuesday beer rudely snake rockets different " +
"fuselage woven tagged bested dented vegan hover rapid fawns obvious muppet " + "fuselage woven tagged bested dented vegan hover rapid fawns obvious muppet " +

View file

@ -1,5 +1,6 @@
package net.agorise.shared.stargate.mnemonics package net.agorise.shared.stargate.mnemonics
import dev.whyoleg.cryptography.bigint.toHexString
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
@ -74,4 +75,59 @@ class MnemonicsTest {
assertTrue(result) 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)
}
} }