Skip to content

feat(shared-core): expose the crypto primitives through SharedCoreKit - #1390

Merged
bmc08gt merged 5 commits into
code/cashfrom
feat/shared-core-crypto-facade
Sep 1, 2026
Merged

feat(shared-core): expose the crypto primitives through SharedCoreKit#1390
bmc08gt merged 5 commits into
code/cashfrom
feat/shared-core-crypto-facade

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

The primitives have been KMP and exported from the framework for a while, but the Swift facade only
covered kikcode, so iOS could not reach any of them — FlipcashCore still carries its own Base58,
CommonCrypto hashes, and CodeCurves wrapper. This adds facades for Base58, SHA-256/512, HMAC,
PBKDF2 and Ed25519, plus the KotlinByteArray -> Data direction the return values need.

Base58 and Ed25519 assert against the canonical fixtures in code/test-vectors/; the hashes anchor
on RFC 4231 case 2 and the BIP-39 all-abandon seed vector, so the tests say "correct" rather than
"unchanged". 17 tests in 5 suites.

Three things came out of building this that are worth reading past the diff:

The Kotlin marker object shadowed the Swift module. SharedCore.Base58.shared.encode(...) does
not compile: Kotlin exported an object named SharedCore into a framework named SharedCore, and
the type wins Swift name lookup over the module. Renamed the object to SharedCoreBuild. Any facade
whose natural Swift name collides with the Kotlin type it wraps needs that qualification, so this
had to be fixed rather than worked around.

There was no one-shot SHA-512 in the shared Kotlin. The only one was ByteArray.sha512() in
libs/encryption/utils androidMain, a JVM MessageDigest extension that cannot cross to iOS.
Added Sha512 to libs/encryption/sha512/commonMain over the same kotlincrypto backend
Sha256Hash uses. Pointing the Android extension at it would remove the second implementation;
left out of this PR on purpose.

Ed25519Kmp's documented key layout was wrong. Both doc comments said
privateKey = 64 bytes (seed || public key, orlp convention). It is the clamped SHA-512 expansion
of the seed, and the seed is not recoverable from it. Found by asserting the documented layout and
watching it fail while every RFC 8032 vector passed. Corrected the comments, and a test now pins the
real layout — the FlipcashCore swap depends on it, since anything reading the first 32 bytes back as
a seed gets the wrong bytes.

Nothing in this repo's CI ran either suite: CI is Ubuntu, so it covered only the JVM half of
commonTest and never the Kotlin/Native actuals, and publish-shared-core.yml assembles the
XCFramework without testing the package against it. .github/workflows/shared-core-tests.yml adds
the macOS lane — the six modules' iosSimulatorArm64Test, then the facade suite via xcodebuild test against an XCFramework assembled from the checkout. It is a separate workflow because path
filters attach to the trigger rather than the job, and CI's pull_request trigger is unfiltered;
this one runs only on kmp/shared-core/**, libs/encryption/**, libs/codes/kikcode/** and the
version catalog.

The forward copy already existed; the facades that return bytes need the
reverse. Kotlin bytes are signed, so the tests pin the high-bit range that a
plain numeric conversion would trap on.
FlipcashCore has to call these through Swift types before its native copies
can go, so each gets a facade over the exported Kotlin. Three findings from
standing them up:

- The Kotlin marker object was called SharedCore, which shadows the Swift
  module called SharedCore, so no facade could qualify a Kotlin type whose
  name it reused. Renamed to SharedCoreBuild.
- There was no one-shot SHA-512 in the shared Kotlin at all — Android's was a
  JVM MessageDigest extension in androidMain, which cannot cross. Added
  Sha512 alongside Sha256Hash, over the same kotlincrypto backend.
- Ed25519Kmp's doc comment described the private key as seed || publicKey. It
  is the clamped SHA-512 expansion of the seed; the seed is not recoverable
  from it. The test pins the real layout so a caller reading the old comment
  finds out here rather than in a wallet.

Base58 and ed25519 assert against the canonical cross-platform vectors;
the hashes anchor on RFC 4231 and the BIP-39 seed vector rather than on
whatever the Kotlin happens to return.
@bmc08gt bmc08gt self-assigned this Sep 1, 2026
@github-actions github-actions Bot added area: crypto Solana, keys, encryption, signing type: feature New functionality labels Sep 1, 2026
Nothing ran either suite. The `CI` workflow is Ubuntu, so it covers only the
JVM side of commonTest — the Kotlin/Native actuals, including ed25519's
cinterop over vendored C, went untested — and `publish-shared-core.yml`
assembles the XCFramework without running the SwiftPM package against it.

A separate workflow rather than a job in `CI`, because path filters attach to
the trigger, not the job: `CI` has an unfiltered `pull_request` trigger, so a
job added there would take a macOS runner on every PR. The filter here is the
set of directories that can change the answer.

The facade suite runs against the XCFramework assembled from this checkout via
FLIPCASH_SHARED_CORE_LOCAL, not against the last published release, so a facade
written for an unreleased Kotlin change is covered before the release rather
than after it. The package is iOS-only and the framework has no host slice, so
it goes through `xcodebuild test` on a simulator destination picked from
whatever the runner image has.
@bmc08gt
bmc08gt requested a review from jeffyanta as a code owner September 1, 2026 20:51
`src/iosTest/TestResources.ios.kt` did not compile — `stringWithContentsOfFile`
needs `@OptIn(ExperimentalForeignApi::class)`, and nothing had ever built that
source set, so the error sat there unnoticed. Fixing the opt-in would only have
moved the failure to runtime: the loader reads `NSBundle.mainBundle`, and a
Kotlin/Native test binary ships no resource bundle, so `pathForResource` returns
null for `ed25519.json`.

`flipcash.kmp.test.fixtures` already solves this — it compiles
`src/commonTest/resources` into a generated `TestFixtures.kt` on `commonTest`,
which every target can read with no platform code. base58 and kikcode use it;
ed25519 kept the hand-rolled expect/actual it predates. Applying it here deletes
all four `TestResources` files, the dead instrumented-test actual included: the
module declares only `withHostTest`, so nothing compiled that one either.

`Ed25519VectorTest` now passes 4/4 on `iosSimulatorArm64Test` as well as on the
JVM. Found by the macOS workflow in the previous commit, on its first run.
`xcodebuild test -quiet` prints nothing when it passes, so a run that executed
no tests — a scheme that stopped including the test target, a filter that
matched nothing — is indistinguishable from a run that passed all of them. That
is the one failure mode a gate cannot have.

Writes a result bundle and reads `totalTestCount` out of it, so the log carries
the number and a zero fails the job. Verified against a real bundle: `17 tests:
17 passed, 0 failed, 0 skipped`.
@bmc08gt
bmc08gt merged commit c5d1d32 into code/cash Sep 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: crypto Solana, keys, encryption, signing type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant