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.
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.
package:cryptography. HKDF is RFC 5869.
@noble/post-quantum (audited pure-JS);
the native backend's depends on liboqs. sk_pqc trusts those libraries — it does not re-implement them.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.
Pub.dev publishing is pending — for now, depend on it directly from GitHub. The API is the same either way.
# 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
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.
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."
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.
| Element | Layout | Bytes |
|---|---|---|
| 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 secret | HKDF-SHA256( X25519_ss ‖ MLKEM768_ss ) | 32 |
Selected by conditional import: if (dart.library.ffi) … else if (dart.library.js_interop) …
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.
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.
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.
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.
The combiner is verified against RFC 5869 §A.1 known answers, plus hand-computed values, salt/info domain-separation, and wrong-length rejection.
A keypair/encapsulation from the web lib (noble) decapsulates under the native lib (liboqs) and vice-versa — both decapsulate the shared interop vector identically.
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.
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.