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:
parent
c690d839da
commit
f70176d47c
3 changed files with 93 additions and 15 deletions
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 " +
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue