🔐
🐧
🧬
🔑
SKWorld quantum-resistance initiative

sk_pqc

suite: x25519-mlkem768

Hybrid post-quantum key encapsulation for Dart & Flutter.
X25519 + ML-KEM-768 (FIPS 203), one API across web and native.

A hybrid KEM whose derived secret is secure if either the classical or the post-quantum leg holds. The same construction as TLS X25519MLKEM768 and Signal PQXDH.

Secure if either leg holds Web + native · one Dart API FIPS 203 · ML-KEM-768 tier Apache-2.0 · open

What it is

A hybrid KEM — not a replacement for classical crypto

sk_pqc is a hybrid key encapsulation mechanism. We never implement the lattice or curve primitives ourselves — every cryptographic leg binds an audited library. The only original cryptographic code is the HKDF combiner that fuses the two shared secrets.

⚠️ Honest claims — read this

  • This is not "quantum-proof," "unbreakable," or "quantum-safe." Lattice cryptography is young. The defensible words are "post-quantum" / "quantum-resistant."
  • The web backend's assurance depends on @noble/post-quantum (audited pure-JS); the native backend's depends on liboqs. sk_pqc trusts those libraries — it does not re-implement them.
  • Hybrid means belt-and-suspenders, not magic: it hedges the (real) risk that one primitive falls, but it inherits the assurance of whichever library you run.
  • The only cryptography we wrote is the HKDF-SHA256 combiner, a standard KDF construction verified against RFC 5869 known answers.
Why now

Harvest now, decrypt later

A large-scale quantum computer does not exist today — but an adversary does not need one today to threaten data sent today. They can capture encrypted traffic now and store it, waiting for a cryptographically-relevant quantum computer to decrypt it years later. Anything with a long secrecy lifetime — identity keys, archives, medical or financial records, sovereign comms — is exposed the moment it crosses a wire.

That is why migration is a now problem, not a future one. And it is why the responsible first step is hybrid: you keep the battle-tested classical curve (X25519) and add a post-quantum lattice KEM (ML-KEM-768) alongside it. If the young lattice scheme has a flaw, you are no worse than classical. If a quantum computer arrives, the post-quantum leg has you covered. You lose nothing and you hedge everything.

This is exactly the path the standards bodies took: TLS 1.3 ships X25519MLKEM768, and Signal's PQXDH uses the same hybrid shape. sk_pqc brings that construction to Dart and Flutter, so a phone app and a web app can speak it identically.

Quick start

Generate → encapsulate → decapsulate

Pub.dev publishing is pending — for now, depend on it directly from GitHub. The API is the same either way.

pubspec.yaml — depend from GitHub (pub.dev publish pending)
# Once published you'll be able to: dart pub add sk_pqc
# Until then, depend on the repo directly:
dependencies:
  sk_pqc:
    git:
      url: https://github.com/smilinTux/sk_pqc.git
example.dart — minimal usage
import 'package:sk_pqc/sk_pqc.dart';

final kem  = HybridKemImpl();                // backend auto-selected (web/native)
final keys = await kem.generateKeyPair();   // publish keys.publicKey (1216 B)

// Sender encapsulates to the recipient's public key:
final enc  = await kem.encapsulate(keys.publicKey);
// enc.ciphertext (1120 B) goes on the wire; enc.sharedSecret stays local

// Recipient recovers the same 32-byte secret from the ciphertext:
final ss   = await kem.decapsulate(enc.ciphertext, keys.privateKey);
// ss == enc.sharedSecret  → use it as an AES-256-GCM / ChaCha20 key

Errors throw SkPqcError rather than crashing. A tampered ML-KEM ciphertext does not error — ML-KEM uses implicit rejection, so decapsulation simply yields a secret that won't match.

How it works

One combiner, two legs, fixed wire format

Both legs run independently and produce a shared secret each. We concatenate those two secrets (X25519 first) and run a single HKDF-SHA256 to derive the final 32-byte key. Concatenate-then-KDF. Never XOR. Never pure-PQ.

salt defaults to empty; info defaults to sk_pqc/x25519-mlkem768/v1 (pass a context label for domain separation). X25519 acts as a KEM via ephemeral-static Diffie–Hellman (DHKEM, as in HPKE / TLS): the encapsulator ships a fresh 32-byte ephemeral public key as the X25519 "ciphertext."

Wire format — the interop contract

Every element is the byte concatenation of the X25519 part followed by the ML-KEM-768 part. These lengths are fixed and MUST NOT change.

ElementLayoutBytes
public key X25519_pub (32) ‖ MLKEM768_pub (1184) 1216
private key X25519_priv_seed (32) ‖ MLKEM768_secret (2400)2432
ciphertext X25519_ephemeral_pub (32) ‖ MLKEM768_ct (1088)1120
shared secretHKDF-SHA256( X25519_ss ‖ MLKEM768_ss ) 32

Two backends, one API

Selected by conditional import: if (dart.library.ffi) … else if (dart.library.js_interop) …

native · dart:ffi → liboqs

desktop / mobile

The ML-KEM-768 leg binds liboqs' OQS_KEM API; X25519 is package:cryptography. You provide the liboqs shared library at runtime (via SK_PQC_LIBOQS or platform default paths).

v1 proves the FFI path on Linux desktop (liboqs 0.14.0). Per-arch binaries for Android / iOS / macOS / Windows are a CI follow-up.

web · dart:js_interop → noble

browser / dart2js

The ML-KEM-768 leg binds @noble/post-quantum's ml_kem768 (audited pure-JS); X25519 is package:cryptography.

WebCrypto has no PQC API in any browser (2026), so the JS dep is exposed via globalThis.skPqc — a ready bootstrap ships at web/sk_pqc_noble_bootstrap.js.

Verified

Anchored to known answers, cross-checked across implementations

A machine-readable interop vector lives at test_vectors/hybrid_kem_x25519_mlkem768.json: given the private key and ciphertext, every conformant implementation MUST recover the same shared secret.

✅ FIPS 203 KAT

ML-KEM-768 known answer

The ML-KEM leg keypair is derived from the NIST ACVP FIPS 203 keyGen seed (d ‖ z, tcId 26), anchoring it to an official known-answer test.

✅ RFC 5869 KAT

HKDF-SHA256 combiner

The combiner is verified against RFC 5869 §A.1 known answers, plus hand-computed values, salt/info domain-separation, and wrong-length rejection.

✅ cross-backend

web ↔ native agree

A keypair/encapsulation from the web lib (noble) decapsulates under the native lib (liboqs) and vice-versa — both decapsulate the shared interop vector identically.

✅ Dart ↔ Python

cross-language interop vector

The same vector is re-derived in Python (pyca X25519 + liboqs-python + HKDF-SHA256) and asserted equal — the contract Dart and Python must both satisfy.

Status honesty: the combiner and the Linux-desktop FFI path are tested and proven; the web path compiles under dart2js and is cross-checked against liboqs. Per-arch native binaries (Android / iOS / macOS / Windows) are a CI follow-up, and signatures are out of scope.

Part of the SKWorld quantum-resistance initiative

Sovereign crypto, built in the open

sk_pqc is one piece of SKWorld — sovereign AI infrastructure built on open, auditable cryptography. Apache-2.0, no rented clouds, your hardware and your keys.