No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-17 11:05:08 +02:00
conformance add search ws 2026-07-14 13:50:43 +02:00
gradle/wrapper Add JitPack publishing config, Gradle wrapper, and install docs 2026-06-08 12:13:28 +02:00
src fixed networking 2026-07-17 11:05:08 +02:00
.gitignore Add JitPack publishing config, Gradle wrapper, and install docs 2026-06-08 12:13:28 +02:00
build.gradle Add JitPack publishing config, Gradle wrapper, and install docs 2026-06-08 12:13:28 +02:00
gradlew Add JitPack publishing config, Gradle wrapper, and install docs 2026-06-08 12:13:28 +02:00
gradlew.bat Add JitPack publishing config, Gradle wrapper, and install docs 2026-06-08 12:13:28 +02:00
jitpack.yml Add JitPack publishing config, Gradle wrapper, and install docs 2026-06-08 12:13:28 +02:00
LICENSE update license 2026-06-11 07:31:42 +02:00
README.md fixed networking 2026-07-17 11:05:08 +02:00
settings.gradle init 2026-06-08 10:39:42 +02:00

unimo-sdk-java

Release

Java/Android port of the TypeScript E2EE client SDK (sdk/ts). Post-quantum (ML-DSA-87 + ML-KEM-1024), cSHAKE256-derived identities, AES-256-GCM content encryption — wire-compatible with the gateway and the TS SDK.

Status

Phase Scope State
0 Scaffold + cross-language conformance harness done
1 crypto + shared (byte-exact interop foundation) done
2 Identity: Members, Validators, register/login, deriveKeys unlock, ApiClient (OkHttp) done — live-verified vs gateway (register→login→unlock)
3 VaultController: manifest CAS PUT/GET, member add/remove + key rotation, reauth done — live-verified (addMember+rotation, new-member login, reauth, removeMember)
4 Tasker: chunked storage upload/download + CAS done — live-verified (single + multi-chunk 8.4 MiB round-trips, CAS bump)
5 Collections/KV + ReactiveValue observable (dependency-free) done — live-verified (create/persist/reload + survives key rotation)
6a Billing + Invites (manager-area read/write) done — live-verified (create→claim→finalize→login; billing catalog/state)
6b WebSocket Connection (vault:event push) + minimal Account done — live-verified (real blob_put push received; Account upload/download)
7 WebSocket Search (typing suggestions + full web search) + Connection request/response done — live-verified (suggest round-trip + error-frame path; full results need gateway search providers)

Feature-complete vs the TypeScript SDK (the only TS gaps are also stubs there: VFS/LIST/CRDTLIST collection types, and the Tasker reactive task-queue/progress UI convenience). Search is a Java-first addition — the TS SDK has no search client yet; it speaks the gateway's WS search domain directly.

Live coverage: 33/33 in IntegrationRunner (register/login/unlock · member add/remove + key rotation · reauth · single + multi-chunk encrypted storage + CAS · KV collection create/reload/rotate · billing catalog/state · full invite create→claim→finalize→login · WebSocket vault:event push · search suggest round-trip · Account wrapper; the full search-results checks self-skip loudly when the gateway lacks working search providers; set UNIMO_SEARCH_LIVE=1 to FAIL instead). Offline cross-language conformance: 45/45.

UI binding: ReactiveValue<T> is a neutral, dependency-free observable (get/set/update/subscribe/onChange). Adapt at the UI edge — MutableLiveData (rv.subscribe(ld::postValue)) for Views/Java, or MutableStateFlow (rv.subscribe { flow.value = it }, read via collectAsState()) for Compose. The core stays Android-free and pure-JVM-testable.

Dependencies (two)

  • org.bouncycastle:bcprov-jdk18on:1.81 — ML-DSA-87, ML-KEM-1024, cSHAKE256. Used via low-level APIs (no JCA provider registration) to avoid clashing with Android's bundled com.android.org.bouncycastle.
  • com.squareup.okhttp3:okhttp:4.12.0 — HTTP + WebSocket (Phase 2+).

Everything else is core JVM: AES-GCM/SHA-256 (javax.crypto/java.security), Base64/Hex (BouncyCastle encoders), JSON (hand-rolled shared/Json — canonical writer + parser), CompletableFuture for the async network surface.

Why not native PQC?

javax.crypto.KEM (JDK 21) is only a generic API; ML-KEM/ML-DSA implementations are JDK 24+ (SunJCE) and Android 17 (Keystore) only. Neither works here: cSHAKE256 has no native implementation in any JDK/Android provider, and the SDK requires seed-derived, exportable keypairs (recovery devices re-derive keys from a seed) — the opposite of Android Keystore's non-exportable, hardware-bound keys. BouncyCastle is the only fit.

Layout (mirrors sdk/ts/src_ts)

src/main/java/com/unimo/sdk/
├── crypto/   CryptoPQ (ML-DSA/ML-KEM), AEAD (AES-GCM), CryptoUtils (sha256, cSHAKE256,
│             derivations), SeedRandom (deterministic keygen), CodedException
├── shared/   Consts, Helpers (hex/base64/concat/u32be/chunkId), Json (canonical + parse)
└── client/   (Phase 2+)
conformance/  ConformanceRunner + vectors.json (golden oracle from the TS SDK)

Install (JitPack)

Published from this Codeberg repo via JitPack. Add the JitPack repository, then the dependency — BouncyCastle and OkHttp come in transitively.

Gradle

repositories {
    mavenCentral()
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation 'org.codeberg.thinking_tools:unimo-sdk-java:v0.0.1'
}

Maven

<repositories>
  <repository>
    <id>jitpack.io</id>
    <url>https://jitpack.io</url>
  </repository>
</repositories>

<dependency>
  <groupId>org.codeberg.thinking_tools</groupId>
  <artifactId>unimo-sdk-java</artifactId>
  <version>v0.0.1</version>
</dependency>

Any pushed tag, main-SNAPSHOT (latest commit), or a commit hash works as the version.

Search rides the same authenticated WebSocket as vault:event push (there is no HTTP search route). search:typing → suggestions (free); search:query → full results (counted against the vault's search quota).

Account account = new Account(serviceUrl, vault, true); // keepAlive=true opens the WS
Search search = account.search();

List<String> suggestions = search.suggest("post-quantum").get();

Search.Response res = search.search("post-quantum cryptography",
    early -> System.out.println("suggestions: " + early)).get();
for (Search.Result r : res.results) {
  System.out.println(r.title + " — " + r.url);
}

Failures reject the future with a CodedException: the gateway's error code (or WS_ERROR) for server-side errors, WS_TIMEOUT after 20s without a reply, CONNECTION_CLOSED when the socket is down or drops mid-request, BAD_FRAME when a search:results reply is malformed (missing data), EMPTY_QUERY (thrown synchronously) for blank queries. A throwing onSuggestions callback rejects the future with a HANDLER_ERROR CodedException wrapping it — keep the early-frame handler cheap and non-throwing.

Connection recovery (Android wiring — required)

The SDK sends no pings and runs no reconnect timers: keepalive is the gateway's job (the server pings; OkHttp answers pongs natively, zero code), and recovery is event-driven. Feed OS connectivity events into the connection — without this wiring a dropped connection stays down, because OkHttp cannot detect a silent network loss on its own:

ConnectivityManager cm =
    (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
Connection conn = account.connection();
cm.registerDefaultNetworkCallback(new ConnectivityManager.NetworkCallback() {
  @Override public void onAvailable(Network network) { conn.networkAvailable(); }
  @Override public void onCapabilitiesChanged(Network network, NetworkCapabilities caps) {
    // onAvailable can fire before the network is validated (DNS not yet working) — this
    // second trigger retries once validation lands. networkAvailable() is idempotent.
    if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) conn.networkAvailable();
  }
  @Override public void onLost(Network network) { conn.networkLost(); }
});

Requires ACCESS_NETWORK_STATE. Also call conn.networkAvailable() from onResume() — it covers drops that happen with no network change (e.g. a gateway restart while the app sits in the background). networkLost() kills the transport at once instead of waiting minutes for TCP timeouts; networkAvailable() reconnects immediately, or cancels a half-dead attempt and retries once. Auth-expiry recovery (401 upgrade rejection / 1008 close → reauth → reopen) stays automatic and needs no wiring.

Build & test

# Gradle (consumer path)
gradle test

# No-Gradle path (what CI/dev can run with just a JDK): fetch the 4 compile jars into libs/
M=https://repo1.maven.org/maven2
curl -sLo libs/bcprov-jdk18on-1.81.jar  $M/org/bouncycastle/bcprov-jdk18on/1.81/bcprov-jdk18on-1.81.jar
curl -sLo libs/okhttp-4.12.0.jar        $M/com/squareup/okhttp3/okhttp/4.12.0/okhttp-4.12.0.jar
curl -sLo libs/okio-jvm-3.6.0.jar       $M/com/squareup/okio/okio-jvm/3.6.0/okio-jvm-3.6.0.jar
curl -sLo libs/kotlin-stdlib-1.9.10.jar $M/org/jetbrains/kotlin/kotlin-stdlib/1.9.10/kotlin-stdlib-1.9.10.jar
CP=$(printf 'libs/%s:' bcprov-jdk18on-1.81.jar okhttp-4.12.0.jar okio-jvm-3.6.0.jar kotlin-stdlib-1.9.10.jar)
javac -cp "$CP" -d out $(find src/main/java -name '*.java') conformance/ConformanceRunner.java conformance/IntegrationRunner.java
java -cp "out:$CP" conformance.ConformanceRunner conformance/vectors.json

# Live end-to-end test (needs the `bun run dev:local` gateway up on :3000):
java -cp "out:$CP" conformance.IntegrationRunner http://localhost:3000

Conformance workflow

conformance/vectors.json is generated from the real noble/WebCrypto crypto:

bun run sdk/ts/tools/gen-conformance.ts   # regenerate after any TS crypto change

The Java port must reproduce every vector byte-for-byte (keygen-from-seed public keys, cSHAKE outputs, canonical JSON, AEAD wire bytes, cross-language sign/verify + decapsulate).

Interop contract (verified)

  • ML-DSA-87: FIPS-204 pure, empty context (0x00‖0x00‖M), hedged signing. Sign the raw 32-byte SHA-256 digest of the canonical JSON; payloadHash = base64(digest).
  • ML-KEM-1024: FIPS-203; 64-byte seed = d‖z.
  • cSHAKE256: N="", S=personalization (SP 800-185).
  • AEAD: AES-256-GCM, [12B IV][ciphertext][16B tag].
  • Canonical JSON: recursive key sort + compact JSON.stringify.