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 = "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"
|
||||||
|
@ -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" }
|
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" }
|
||||||
|
|
|
@ -19,5 +19,6 @@ dependencyResolutionManagement {
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":composeApp")
|
include(":composeApp")
|
||||||
|
include(":shared:crypto")
|
||||||
include(":shared:preferences")
|
include(":shared:preferences")
|
||||||
include(":shared:stargate")
|
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.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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