Introduce CRC-32 validation functionality.
- Created a new shared module 'crypto' that will contain crytography specific functionality. - Added a class to validate data using the CRC-32 algorithm. We had to create a class to do this from scratch because there is no readily made library available for Kotlin Multiplatform yet. - Configured unit tests in the crypto module to confirm that the CRC-32 functionality is correctly implemented. - Introduced a Mnemonics class that contains logic to convert mnemonics to a valid Dero key and viceversa. This functionality is not complete yet. - Introduced Mnemonics support for two languages for now, English and Spanish. Adding support for more languages is a matter of adding a new Mnemonics[Language] instance. We can do that later when necessary.
This commit is contained in:
parent
4829994d52
commit
7dab5227dc
10 changed files with 3560 additions and 2 deletions
|
@ -7,6 +7,7 @@ androidx-activityCompose = "1.8.2"
|
|||
compose = "1.6.4"
|
||||
compose-plugin = "1.6.1"
|
||||
coroutines = "1.8.0"
|
||||
cryptography = "0.3.0"
|
||||
kotlin = "1.9.23"
|
||||
ktor = "2.3.9"
|
||||
multiplatform-settings = "1.1.1"
|
||||
|
@ -18,6 +19,8 @@ compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", versi
|
|||
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose" }
|
||||
coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||
coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "coroutines" }
|
||||
cryptography-bigint = { group = "dev.whyoleg.cryptography", name = "cryptography-bigint", version.ref = "cryptography" }
|
||||
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
|
||||
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
|
||||
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" }
|
||||
|
|
|
@ -19,5 +19,6 @@ dependencyResolutionManagement {
|
|||
}
|
||||
|
||||
include(":composeApp")
|
||||
include(":shared:crypto")
|
||||
include(":shared:preferences")
|
||||
include(":shared:stargate")
|
||||
|
|
44
shared/crypto/build.gradle.kts
Normal file
44
shared/crypto/build.gradle.kts
Normal file
|
@ -0,0 +1,44 @@
|
|||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidLibrary)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
androidTarget {
|
||||
compilations.all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jvm()
|
||||
|
||||
iosX64()
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.cryptography.bigint)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.agorise.shared.crypto"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package net.agorise.shared.crypto.crc
|
||||
|
||||
/**
|
||||
* 0xEDB88320u is the reversed representation of the IEEE CRC-32 polynomial: 0x04C11DB7
|
||||
*/
|
||||
class Crc32(private val polynomial: UInt = 0xEDB88320u) {
|
||||
internal val lookupTable: List<UInt> = populateLookupTable(polynomial)
|
||||
|
||||
fun crc32(bytes: ByteArray): UInt {
|
||||
var crc = INITIAL_CRC32
|
||||
for (byte in bytes) {
|
||||
val index = ((crc.toInt() xor byte.toInt()) and 0xFF)
|
||||
crc = (crc shr 8) xor lookupTable[index]
|
||||
}
|
||||
return crc.inv()
|
||||
}
|
||||
|
||||
private fun populateLookupTable(polynomial: UInt): List<UInt> {
|
||||
return (0 until 256).map { index ->
|
||||
(0 until 8).fold(index.toUInt()) { crc, _ ->
|
||||
if (crc and 1u != 0u) {
|
||||
(crc shr 1) xor polynomial
|
||||
} else {
|
||||
crc shr 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INITIAL_CRC32 = 0xFFFFFFFFu
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package net.agorise.shared.crypto.crc
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class Crc32Test {
|
||||
@Test
|
||||
fun `given default crc32 - when init is called - then correct lookup table is generated`() {
|
||||
val crc32 = Crc32()
|
||||
|
||||
// Just confirm some of the values in the lookup table
|
||||
assertEquals(0x00000000u, crc32.lookupTable[0])
|
||||
assertEquals(0x77073096u, crc32.lookupTable[1])
|
||||
assertEquals(0x5edef90eu, crc32.lookupTable[120])
|
||||
assertEquals(0x2d02ef8du, crc32.lookupTable[255])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a string - when crc32 is called - then result is correct`() {
|
||||
val stringToResultMap = mapOf(
|
||||
"a" to 0xe8b7be43u,
|
||||
"abc" to 0x352441c2u,
|
||||
"Kee rocks!!" to 0x60039468u,
|
||||
)
|
||||
|
||||
val crc32 = Crc32()
|
||||
|
||||
for ((string, expectedResult) in stringToResultMap) {
|
||||
val bytes = string.encodeToByteArray()
|
||||
|
||||
val actualResult = crc32.crc32(bytes)
|
||||
|
||||
assertEquals(expectedResult, actualResult)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@ kotlin {
|
|||
implementation(libs.ktor.client.cio)
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.websockets)
|
||||
|
||||
implementation(libs.cryptography.bigint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
package net.agorise.shared.stargate.mnemonics
|
||||
|
||||
import dev.whyoleg.cryptography.bigint.BigInt
|
||||
import dev.whyoleg.cryptography.bigint.toBigInt
|
||||
|
||||
/**
|
||||
* Provides functionality to create and validate mnemonics.
|
||||
*
|
||||
* Ported from https://github.com/deroproject/derohe/blob/main/walletapi/mnemonics/mnemonics.go
|
||||
*/
|
||||
class Mnemonics {
|
||||
private val languages: List<MnemonicsLanguage> = listOf(
|
||||
mnemonicsEnglish,
|
||||
mnemonicsSpanish,
|
||||
)
|
||||
|
||||
init {
|
||||
// Validate that the list of languages have the correct number of words
|
||||
for (language in languages) {
|
||||
if (language.words.size != WORDS_SIZE) {
|
||||
println("${language.nameEnglish} language has ${language.words.size} words, but should have $WORDS_SIZE")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of languages that are supported to create/validate a seed
|
||||
*/
|
||||
fun getSupportedLanguages(): List<String> {
|
||||
val supportedLanguages = mutableListOf<String>()
|
||||
for (language in languages) {
|
||||
supportedLanguages.add(language.name)
|
||||
}
|
||||
|
||||
return supportedLanguages
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
// 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)
|
||||
?: return Result.failure(Exception("Seed not found in any language"))
|
||||
|
||||
val languageName = languages[wordsLanguageAndIndices.languageIndex].name
|
||||
|
||||
|
||||
val key = byteArrayOf()
|
||||
|
||||
return Result.success("".toBigInt()) // TODO Remove hardcoded value
|
||||
}
|
||||
|
||||
/**
|
||||
* 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? {
|
||||
for (i in languages.indices) {
|
||||
val indices = mutableListOf<ULong>()
|
||||
|
||||
// Using map of words to find the words faster
|
||||
val languageWordsMap = languages[i].words.withIndex().associate { (index, word) ->
|
||||
word to index
|
||||
}
|
||||
|
||||
var foundAllWords = true
|
||||
|
||||
// Loop through user supplied words
|
||||
for (j in words.indices) {
|
||||
val index = languageWordsMap[words[j]]
|
||||
if (index != null) {
|
||||
indices.add(index.toULong())
|
||||
} else {
|
||||
foundAllWords = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (foundAllWords) {
|
||||
val wordsListCount = languages[i].words.size.toULong()
|
||||
return WordsLanguageAndIndices(indices, i, wordsListCount)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private data class WordsLanguageAndIndices(
|
||||
val indices: List<ULong>,
|
||||
val languageIndex: Int,
|
||||
val wordListCount: ULong,
|
||||
)
|
||||
|
||||
/**
|
||||
* Verifies that the
|
||||
*/
|
||||
// private fun verifyChecksum(words: Array<String>, prefixLen: Int): Boolean {
|
||||
// val seedLength = SEED_LENGTH
|
||||
//
|
||||
// if (words.size != seedLength + 1) {
|
||||
// return false // Checksum word is not present, we cannot verify
|
||||
// }
|
||||
//
|
||||
// val (checksumIndex, err) = calculateChecksumIndex(words.sliceArray(0 until words.size - 1), prefixLen)
|
||||
// if (err != null) {
|
||||
// return false
|
||||
// }
|
||||
// val calculatedChecksumWord = words[checksumIndex]
|
||||
// val checksumWord = words[seedLength]
|
||||
//
|
||||
// return calculatedChecksumWord == checksumWord
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Calculates a checksum (using CRC algorithm) on first 24 words.
|
||||
// */
|
||||
// @OptIn(ExperimentalUnsignedTypes::class)
|
||||
// private fun calculateChecksumIndex(words: Array<String>, prefixLen: Int): Result<UInt> {
|
||||
// val trimmedRunes = mutableListOf<Char>()
|
||||
//
|
||||
// if (words.size != SEED_LENGTH) {
|
||||
// return Result.failure(Exception("Words not equal to seed length"))
|
||||
// }
|
||||
//
|
||||
// for (word in words) {
|
||||
// val trimmedWord = if (word.length > prefixLen) {
|
||||
// word.substring(0, prefixLen)
|
||||
// } else {
|
||||
// word
|
||||
// }
|
||||
// trimmedRunes.addAll(trimmedWord.toCharArray().toList())
|
||||
// }
|
||||
//
|
||||
// val checksum = CRC32().apply {
|
||||
// update(trimmedRunes.toCharArray().concatToString().toByteArray(Charsets.UTF_8))
|
||||
// }.value
|
||||
//
|
||||
// return Result.success(checksum % SEED_LENGTH.toUInt())
|
||||
// }
|
||||
|
||||
companion object {
|
||||
// Each language should have exactly this number of words
|
||||
private const val WORDS_SIZE = 1626
|
||||
|
||||
// Checksum seeds are 24 + 1 = 25 words long
|
||||
private const val SEED_LENGTH = 24
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,16 @@
|
|||
package net.agorise.shared.stargate.mnemonics
|
||||
|
||||
/**
|
||||
* Represents a valid language for mnemonics.
|
||||
*
|
||||
* @param name The name of the language
|
||||
* @param nameEnglish The name of the language in english
|
||||
* @param uniquePrefixLength Number of utf8 chars (not bytes) to use for checksum
|
||||
* @param words 1626 valid words for mnemonics
|
||||
*/
|
||||
data class MnemonicsLanguage(
|
||||
val name: String,
|
||||
val nameEnglish: String,
|
||||
val uniquePrefixLength: Int,
|
||||
val words: List<String>,
|
||||
)
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue