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
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<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
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<String>): WordsLanguageAndIndices? {
private fun getWordsLanguageAndIndices(words: List<String>): Pair<Int, List<ULong>>? {
for (i in languages.indices) {
val indices = mutableListOf<ULong>()
@ -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<ULong>,
val languageIndex: Int,
val wordListCount: ULong,
)
/**
* 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 " +
"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 " +

View file

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