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:
Severiano Jaramillo 2024-04-11 19:49:51 -07:00
parent 4829994d52
commit 7dab5227dc
10 changed files with 3560 additions and 2 deletions

View file

@ -7,21 +7,24 @@ androidx-activityCompose = "1.8.2"
compose = "1.6.4" compose = "1.6.4"
compose-plugin = "1.6.1" compose-plugin = "1.6.1"
coroutines = "1.8.0" coroutines = "1.8.0"
cryptography = "0.3.0"
kotlin = "1.9.23" kotlin = "1.9.23"
ktor = "2.3.9" ktor = "2.3.9"
multiplatform-settings = "1.1.1" multiplatform-settings = "1.1.1"
voyager = "1.0.0" voyager = "1.0.0"
[libraries] [libraries]
androidx-activity-compose = { group = "androidx.activity", name ="activity-compose", version.ref = "androidx-activityCompose" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activityCompose" }
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" } compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" }
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose" } 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-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" } 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-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-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" } ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" }
multiplatform-settings = { group="com.russhwolf", name = "multiplatform-settings-no-arg", version.ref = "multiplatform-settings" } multiplatform-settings = { group = "com.russhwolf", name = "multiplatform-settings-no-arg", version.ref = "multiplatform-settings" }
voyager-navigator = { group = "cafe.adriel.voyager", name = "voyager-navigator", version.ref = "voyager" } voyager-navigator = { group = "cafe.adriel.voyager", name = "voyager-navigator", version.ref = "voyager" }
voyager-screenmodel = { group = "cafe.adriel.voyager", name = "voyager-screenmodel", version.ref = "voyager" } voyager-screenmodel = { group = "cafe.adriel.voyager", name = "voyager-screenmodel", version.ref = "voyager" }

View file

@ -19,5 +19,6 @@ dependencyResolutionManagement {
} }
include(":composeApp") include(":composeApp")
include(":shared:crypto")
include(":shared:preferences") include(":shared:preferences")
include(":shared:stargate") include(":shared:stargate")

View 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
}
}

View file

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

View file

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

View file

@ -23,6 +23,8 @@ kotlin {
implementation(libs.ktor.client.cio) implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
implementation(libs.ktor.client.websockets) implementation(libs.ktor.client.websockets)
implementation(libs.cryptography.bigint)
} }
} }
} }

View file

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

View file

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