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
|
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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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 " +
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue