Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b67bfc6
feat(mnemonic): generate checked-in BIP-39 English wordlist
bmc08gt Sep 3, 2026
0552497
chore(mnemonic): convert build.gradle.kts to the KMP module shape
bmc08gt Sep 3, 2026
8282961
refactor(mnemonic): move MnemonicException to commonMain
bmc08gt Sep 3, 2026
53c2e95
refactor(mnemonic): move DerivePath to commonMain
bmc08gt Sep 3, 2026
8ad02ea
refactor(mnemonic): relocate DerivePath.relationship(domain:) to andr…
bmc08gt Sep 3, 2026
e2a49c9
refactor(mnemonic): move Domain.kt to androidMain
bmc08gt Sep 3, 2026
839231d
refactor(mnemonic): port MnemonicCode to commonMain as an object
bmc08gt Sep 3, 2026
7897f09
refactor(mnemonic): port Derive to commonMain, return raw key bytes
bmc08gt Sep 3, 2026
d878dff
refactor(mnemonic): move MnemonicPhrase to androidMain, adapt to Deri…
bmc08gt Sep 3, 2026
422a992
refactor(mnemonic): simplify MnemonicCache.init() to a no-op
bmc08gt Sep 3, 2026
789d917
refactor(mnemonic): move DerivedKey to androidMain
bmc08gt Sep 3, 2026
dd54606
refactor(mnemonic): move DerivePathTest to commonTest
bmc08gt Sep 3, 2026
369470a
refactor(mnemonic): move MnemonicPhraseTest to androidHostTest
bmc08gt Sep 3, 2026
c7510fd
test(mnemonic): add SLIP-10 canonical vector gate test
bmc08gt Sep 3, 2026
ef41ccf
refactor(mnemonic): move the instrumented SLIP-10 gate to androidDevi…
bmc08gt Sep 3, 2026
f97a833
chore(mnemonic): drop the dead Android wordlist resource
bmc08gt Sep 3, 2026
ecbd274
feat(shared-core): export :libs:encryption:mnemonic through SharedCore
bmc08gt Sep 3, 2026
30c87a4
chore(mnemonic): exclude androidHostTest from Linux CI (JNI ed25519 d…
bmc08gt Sep 3, 2026
f0ca2e7
ci(shared-core): run the mnemonic module's iOS simulator tests
bmc08gt Sep 3, 2026
c15b0cc
feat(shared-core): add SharedDerivation Swift facade
bmc08gt Sep 3, 2026
75cd233
test(shared-core): add SharedDerivation SLIP-10 cross-platform gate
bmc08gt Sep 3, 2026
4f34b7a
feat(shared-core): add macOS Kotlin/Native targets
bmc08gt Sep 3, 2026
7723dd2
fix(login): use MnemonicCode.getWordList() instead of the private field
bmc08gt Sep 3, 2026
955357e
Merge branch 'code/cash' into feat/shared-core-mnemonic-derivation
bmc08gt Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/shared-core-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ jobs:
:libs:encryption:base58:iosSimulatorArm64Test \
:libs:encryption:ed25519:iosSimulatorArm64Test \
:libs:encryption:hmac:iosSimulatorArm64Test \
:libs:encryption:mnemonic:iosSimulatorArm64Test \
:libs:encryption:sha256:iosSimulatorArm64Test \
:libs:encryption:sha512:iosSimulatorArm64Test

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ internal class SeedInputViewModel @Inject constructor(
if (isLoading || isSuccess) return

val userWordList = wordsString.lowercase(Locale.CANADA).split(" ")
val wordCount = getValidCount(userWordList, mnemonicCode.wordList)
val wordCount = getValidCount(userWordList, mnemonicCode.getWordList())
uiFlow.update {
it.copy(
wordsString = wordsString,
Expand Down
5 changes: 4 additions & 1 deletion kmp/shared-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ kotlin {
minSdk = 29
}

val appleTargets = listOf(iosArm64(), iosSimulatorArm64(), iosX64())
val appleTargets = listOf(iosArm64(), iosSimulatorArm64(), iosX64(), macosArm64(), macosX64())
appleTargets.forEach {
it.binaries.framework {
baseName = "SharedCore"
Expand All @@ -36,6 +36,7 @@ kotlin {
export(project(":libs:encryption:sha512"))
export(project(":libs:encryption:hmac"))
export(project(":libs:encryption:ed25519"))
export(project(":libs:encryption:mnemonic"))
}
}

Expand All @@ -48,6 +49,7 @@ kotlin {
api(project(":libs:encryption:sha512"))
api(project(":libs:encryption:hmac"))
api(project(":libs:encryption:ed25519"))
api(project(":libs:encryption:mnemonic"))
}
}
}
Expand All @@ -65,5 +67,6 @@ kmmbridge {
// with a custom file it's `spm/Package.swift` that decides, so keep the two in step.
spm(spmDirectory = spmPackageDir, useCustomPackageFile = true, swiftToolVersion = "5.9") {
iOS { v("15") }
macOS { v("14") }
}
}
3 changes: 2 additions & 1 deletion kmp/shared-core/spm/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ let binaryTarget: Target = sharedCoreLocalRoot.map {
let package = Package(
name: packageName,
platforms: [
.iOS(.v15)
.iOS(.v15),
.macOS(.v14),
],
products: [
// The only product on purpose. Callers get Swift types; the Kotlin framework's
Expand Down
18 changes: 18 additions & 0 deletions kmp/shared-core/spm/Sources/SharedCoreKit/Derivation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation
import SharedCore

/// BIP-39 seed derivation + SLIP-0010 ed25519 key derivation, backed by the shared Kotlin
/// implementation in `:libs:encryption:mnemonic`.
public enum SharedDerivation {

/// Derives a 64-byte PBKDF2-SHA512 seed from a BIP-39 mnemonic phrase.
public static func seed(mnemonic: [String], passphrase: String = "") -> Data {
Data(MnemonicCode.shared.toSeed(words: mnemonic, passphrase: passphrase))
}

/// Derives the raw 32-byte private key by walking `hardenedIndexes` (each already offset by
/// 0x80000000) from `seed` per SLIP-0010.
public static func derivedKey(seed: Data, hardenedIndexes: [Int64]) -> Data {
Data(Derive.shared.derivedKey(seed: seed.kotlinByteArray, hardenedIndexes: hardenedIndexes.kotlinLongArray))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import SharedCore

extension Array where Element == Int64 {
/// Converts this array to a `KotlinLongArray` for calling into shared Kotlin.
var kotlinLongArray: KotlinLongArray {
let array = KotlinLongArray(size: Int32(count))
for (index, value) in enumerated() {
array.set(index: Int32(index), value: value)
}
return array
}
}
39 changes: 39 additions & 0 deletions kmp/shared-core/spm/Tests/SharedCoreKitTests/DerivationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Testing
import Foundation
@testable import SharedCoreKit

@Suite("SharedDerivation")
struct DerivationTests {

struct Vector: Decodable {
let name, mnemonic, passphrase, path, derivedKey, publicKey: String
}
struct Fixture: Decodable { let vectors: [Vector] }

@Test("derivation matches the canonical SLIP-10 vectors")
func derivationMatchesCanonicalVectors() throws {
let fixture = try Fixtures.load("slip10", as: Fixture.self)
#expect(!fixture.vectors.isEmpty)

for v in fixture.vectors {
let words = v.mnemonic.split(separator: " ").map(String.init)
let seed = SharedDerivation.seed(mnemonic: words, passphrase: v.passphrase)

let hardenedIndexes = try v.path
.split(separator: "/")
.dropFirst() // leading "m"
.map { component -> Int64 in
let hardened = component.hasSuffix("'")
let digits = hardened ? String(component.dropLast()) : String(component)
let value = try #require(Int64(digits))
return 0x8000_0000 + value
}

let derivedKey = SharedDerivation.derivedKey(seed: seed, hardenedIndexes: hardenedIndexes)
#expect(derivedKey.hexString == v.derivedKey, "derivedKey mismatch for \(v.name)")

let keyPair = SharedEd25519.keyPair(seed: derivedKey)
#expect(keyPair.publicKey.hexString == v.publicKey, "publicKey mismatch for \(v.name)")
}
}
}
2 changes: 2 additions & 0 deletions libs/codes/kikcode/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ kotlin {
iosArm64()
iosSimulatorArm64()
iosX64()
macosArm64()
macosX64()

sourceSets {
commonMain {
Expand Down
2 changes: 2 additions & 0 deletions libs/encryption/base58/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ kotlin {
iosArm64()
iosSimulatorArm64()
iosX64()
macosArm64()
macosX64()

sourceSets {
commonMain {
Expand Down
4 changes: 4 additions & 0 deletions libs/encryption/ed25519/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ val appleTargetDefs = listOf(
AppleTarget("iosArm64", "iphoneos", "arm64"),
AppleTarget("iosSimulatorArm64", "iphonesimulator", "arm64"),
AppleTarget("iosX64", "iphonesimulator", "x86_64"),
AppleTarget("macosArm64", "macosx", "arm64"),
AppleTarget("macosX64", "macosx", "x86_64"),
)

appleTargetDefs.forEach { target ->
Expand Down Expand Up @@ -81,6 +83,8 @@ kotlin {
iosArm64()
iosSimulatorArm64()
iosX64()
macosArm64()
macosX64()

// ── Cinterop + linker wiring for each Apple target ────────────────────────
targets.withType<KotlinNativeTarget>().configureEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import kotlinx.cinterop.toCValues
import kotlinx.cinterop.UByteVar

/**
* iOS actual: calls the vendored orlp/ed25519 C library directly via Kotlin/Native cinterop.
* Apple actual (iOS + macOS): calls the vendored orlp/ed25519 C library directly via Kotlin/Native cinterop.
*
* Memory safety:
* - All byte arrays are pinned or copied into C-managed memory inside [memScoped].
Expand Down
2 changes: 2 additions & 0 deletions libs/encryption/hmac/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ kotlin {
iosArm64()
iosSimulatorArm64()
iosX64()
macosArm64()
macosX64()

sourceSets {
commonMain {
Expand Down
70 changes: 52 additions & 18 deletions libs/encryption/mnemonic/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,26 +1,60 @@
plugins {
alias(libs.plugins.flipcash.android.library)
alias(libs.plugins.flipcash.android.ed25519.shadow)
kotlin("multiplatform")
id("com.android.kotlin.multiplatform.library")
alias(libs.plugins.flipcash.kmp.test.fixtures)
}

android {
namespace = "${Gradle.codeNamespace}.encryption.mnemonic"
// Compiles `src/commonTest/resources` into a generated `TestFixtures.kt` on `commonTest` --
// see the `flipcash.kmp.test.fixtures` convention plugin.
testFixtures {
packageName = "com.getcode.crypt"
}

dependencies {
implementation(project(":libs:encryption:base58"))
implementation(project(":libs:encryption:ed25519"))
implementation(project(":libs:encryption:hmac"))
implementation(project(":libs:encryption:sha256"))
implementation(project(":libs:encryption:sha512"))
implementation(project(":libs:encryption:utils"))
implementation(libs.grpc.okhttp)
implementation(libs.grpc.kotlin)
implementation(libs.androidx.core)
kotlin {
android {
namespace = "com.getcode.encryption.mnemonic"
compileSdk = 37
minSdk = 29
withHostTest {}
withDeviceTest {}
}

testImplementation(kotlin("test"))
iosArm64()
iosSimulatorArm64()
iosX64()
macosArm64()
macosX64()

// Cross-platform derivation test-vector gate (instrumented: wordlist res/raw + JNI ed25519).
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.test.runner)
sourceSets {
commonMain {
dependencies {
implementation(project(":libs:encryption:ed25519"))
implementation(project(":libs:encryption:hmac"))
implementation(project(":libs:encryption:sha256"))
implementation(project(":libs:encryption:sha512"))
implementation(project(":libs:encryption:utils"))
}
}
commonTest {
dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.serialization.json)
}
}
androidMain {
dependencies {
// Legacy vendored Base58 codec (fromEntropyB58/getBase58EncodedEntropy).
implementation(project(":libs:encryption:base58"))
// Domain.kt uses android.net.Uri.
implementation(libs.androidx.core)
}
}
getByName("androidDeviceTest") {
dependencies {
// Cross-platform derivation test-vector gate (instrumented: JNI ed25519 pipeline).
implementation(libs.androidx.junit)
implementation(libs.androidx.test.runner)
}
}
}
}
66 changes: 66 additions & 0 deletions libs/encryption/mnemonic/src/androidDeviceTest/assets/slip10.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"algorithm": "bip39+slip10-ed25519",
"note": "BIP39 seed -> SLIP-0010 ed25519 (all indices force-hardened) -> ed25519 keypair. Apps must reproduce publicKey/address for each mnemonic+path.",
"vectors": [
{
"name": "abandon-x11-about m/44'/501'/0'/0'",
"mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"passphrase": "",
"path": "m/44'/501'/0'/0'",
"seedBip39": "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4",
"derivedKey": "37df573b3ac4ad5b522e064e25b63ea16bcbe79d449e81a0268d1047948bb445",
"publicKey": "f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7",
"address": "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk"
},
{
"name": "abandon-x11-about m/44'/501'/0'/0'/7665'/0",
"mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"passphrase": "",
"path": "m/44'/501'/0'/0'/7665'/0",
"seedBip39": "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4",
"derivedKey": "1f317a98c39b1459a28fbc5d7e4ed9c9799dc69d194157ec7b9d645c4ed3a474",
"publicKey": "051d88bbb9c70cc1045090c3c01675620b060123f2d1f1700d8ef1f5dadcc5d8",
"address": "LyACZbC6QzPP3ixxyBjuCC1sKWuAi1bZU1xgTuFVpRD"
},
{
"name": "abandon-x11-about m/44'/501'/0'/0'/2335'/5",
"mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"passphrase": "",
"path": "m/44'/501'/0'/0'/2335'/5",
"seedBip39": "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4",
"derivedKey": "4b83957e1b5ca6b16e9f39fdd38c88a6a96c38e61f9b4cecb7fda43e5e249764",
"publicKey": "7bc1de0ce3459814c74dae0529df060261d896dea23264e298eb90d1d236c123",
"address": "9L6c3GSoNLLXbXAoxCgHR1fkhbX96jNaLqmQnsRYUCo4"
},
{
"name": "legal-winner m/44'/501'/0'/0'",
"mnemonic": "legal winner thank year wave sausage worth useful legal winner thank yellow",
"passphrase": "",
"path": "m/44'/501'/0'/0'",
"seedBip39": "878386efb78845b3355bd15ea4d39ef97d179cb712b77d5c12b6be415fffeffe5f377ba02bf3f8544ab800b955e51fbff09828f682052a20faa6addbbddfb096",
"derivedKey": "6987bdb06aa8a243a3019f41489ffa8e609c953a885a748d1849a8df760aa479",
"publicKey": "999d46fb3d1256f7049c8ed09314d7268612e8a91b800e91934463848305c98c",
"address": "BLeUXTx9thHGT7VJUtF9vHEmfMDgW1nnKZ9UVer2CoLX"
},
{
"name": "legal-winner m/44'/501'/0'/0'/7665'/0",
"mnemonic": "legal winner thank year wave sausage worth useful legal winner thank yellow",
"passphrase": "",
"path": "m/44'/501'/0'/0'/7665'/0",
"seedBip39": "878386efb78845b3355bd15ea4d39ef97d179cb712b77d5c12b6be415fffeffe5f377ba02bf3f8544ab800b955e51fbff09828f682052a20faa6addbbddfb096",
"derivedKey": "82c2531aae3c058eb076618c696c3d496c3249b066dd528cb83adc4f4bbe8294",
"publicKey": "9ed602ebfe2399e961e3a08535a50fd4391fdefc0341b1c790e0c4947ccd16a0",
"address": "Bh2gxt6UiDWjWkmsfK9qp46kRrjTPgvXn7DhvaPTxUr3"
},
{
"name": "legal-winner m/44'/501'/0'/0'/2335'/5",
"mnemonic": "legal winner thank year wave sausage worth useful legal winner thank yellow",
"passphrase": "",
"path": "m/44'/501'/0'/0'/2335'/5",
"seedBip39": "878386efb78845b3355bd15ea4d39ef97d179cb712b77d5c12b6be415fffeffe5f377ba02bf3f8544ab800b955e51fbff09828f682052a20faa6addbbddfb096",
"derivedKey": "4eb3bb8632aa83691f51a4cff44021ff785df069432007bb023e858b6416f6ec",
"publicKey": "b5310f3190a4a39d5146f17072a767fb5c566593e4f2417d293d11fcd3b246f7",
"address": "DCJBY1iQroEzsNi2BMwu3zNmnB27rCV6iJHHJDAqTTmg"
}
]
}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.getcode.crypt

import com.getcode.model.Domain

fun DerivePath.Companion.relationship(domain: Domain): DerivePath {
return DerivePath.newInstance("m/44'/501'/0'/0'/0'/0", password = domain.relationshipHost)!!
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ data class DerivedKey(val path: DerivePath, val keyPair: Ed25519.KeyPair) {
return result
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.getcode.crypt

import android.content.Context

object MnemonicCache {
val cachedCode: MnemonicCode = MnemonicCode

fun init(context: Context) {
// MnemonicCode no longer needs Android resources -- the wordlist is a compile-time
// constant (see Bip39EnglishWordList). Kept so MnemonicCacheInitializer's startup
// call still compiles.
}

val cache = mutableMapOf<Pair<List<String>, String>, ByteArray>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class MnemonicPhrase(val kind: Kind, val words: List<String>) {

mnemonicCode.check(words)

return Derive.path(mnemonicSeed, path)
return Ed25519.createKeyPair(Derive.derivedKey(mnemonicSeed, path).encodeBase64())
}

val wordString: String
Expand Down Expand Up @@ -74,4 +74,4 @@ class MnemonicPhrase(val kind: Kind, val words: List<String>) {
return newInstance(words)!!
}
}
}
}
Loading
Loading