OQS for Dart

pub package License

Dart FFI bindings for liboqs, providing post-quantum KEM and signature primitives.

Version Compatibility

oqs package liboqs
4.x 0.16.x
3.x 0.15.x
2.x 0.14.x (legacy)

4.0.0 is a breaking release aligned to liboqs 0.16.0 (a major bump to force a re-read of the FrodoKEM naming change below before upgrading.

3.0.0 is a breaking release aligned to liboqs 0.15.0.

Install

dependencies:
  oqs: ^3.3.1

Native Library Setup

You need a native liboqs library for your platform.

Download pre-built binaries from:

Supported platforms:

  • Linux: x86_64, ARM64 (aarch64)
  • macOS: x86_64 (Intel), ARM64 (Apple Silicon)
  • Windows: x64
  • iOS: XCFramework (device + simulator)
  • Android: armeabi-v7a, arm64-v8a, x86, x86_64

Using the all-platforms archive:

# Extract the combined archive
tar -xzf liboqs-0.16.0-all-platforms.tar.gz

# Use in your Dart code
LibOQSLoader.customPaths = LibraryPaths.fromReleaseArchive('/path/to/liboqs-0.16.0');
LibOQS.init();

The all-platforms archive keeps every platform/architecture in its own subfolder:

liboqs-0.16.0/
  linux/x86_64/liboqs.so
  linux/aarch64/liboqs.so
  macos/x86_64/liboqs.dylib
  macos/arm64/liboqs.dylib
  windows/x86_64/oqs.dll
  android/arm64-v8a/liboqs.so    # + armeabi-v7a, x86, x86_64
  ios/liboqs.xcframework/

Android/iOS: the archive only stages the files. You still might need to copy the Android .so into android/app/src/main/jniLibs/<abi>/ and add the iOS .xcframework to your Xcode project's embedded frameworks.

Option 2: Build from source

git clone https://github.com/open-quantum-safe/liboqs.git
cd liboqs
mkdir build && cd build
cmake -GNinja -DCMAKE_INSTALL_PREFIX=/usr/local ..
ninja
ninja install

Optional explicit paths

import 'package:oqs/oqs.dart';

LibOQSLoader.customPaths = LibraryPaths(
  windows: r'C:\libs\oqs.dll',
  linuxX64: '/usr/local/lib/liboqs.so',
  linuxArm64: '/usr/local/lib/liboqs.so',  // For ARM64 systems
  macOS: '/opt/homebrew/lib/liboqs.dylib',
);

Library Loading Guide

LibOQSLoader.loadLibrary() uses fallback strategies in this exact order:

  1. explicitPath argument
  2. LibOQSLoader.customPaths (LibraryPaths)
  3. Deprecated LibOQSLoader.customPath
  4. Environment variable (LIBOQS_PATH, or envVarName)
  5. releaseArchiveRoot — extracted liboqs-binaries v2.0.0+ combined archive (recommended)
  6. Deprecated binaryRoot — legacy layout, kept for compatibility only
  7. Package-relative paths
  8. System loader/default name (liboqs.so, oqs.dll, liboqs.dylib)
  9. Legacy default paths (bin/<platform>/...)

If all fail, LibraryLoadException includes all attempted strategies.

Auto Path Selection (Package-relative)

PackageRelativeStrategy checks:

  • ./bin/<library-file>
  • ./lib/<library-file>
  • ./lib/native/<library-file>
  • ./native/<library-file>
  • ./blobs/<library-file>
  • Android extras:
    • ./lib/arm64-v8a/liboqs.so
    • ./lib/armeabi-v7a/liboqs.so
    • ./lib/x86_64/liboqs.so
    • ./lib/x86/liboqs.so

Platform Notes

  • Linux: Automatically detects x86_64 vs ARM64 (aarch64) architecture
  • iOS: Uses DynamicLibrary.process() (XCFramework/static linking), not DynamicLibrary.open(...)
  • Android: ABI-specific selection supported through LibraryPaths.currentPlatformPath
  • macOS/Windows: System resolution works when library is installed in standard paths

Use explicit, deterministic config for production:

final lib = LibOQSLoader.loadLibrary(
  explicitPath: '/opt/liboqs/lib/liboqs.so',
);

Or per-platform config:

LibOQSLoader.customPaths = LibraryPaths(
  windows: r'C:\oqs\oqs.dll',
  linuxX64: '/usr/local/lib/liboqs.so',
  linuxArm64: '/usr/local/lib/liboqs.so',
  macOS: '/opt/homebrew/lib/liboqs.dylib',
  androidArm64: '/data/local/tmp/liboqs.so',
);

Or extracted release root:

final lib = LibOQSLoader.loadLibrary(releaseArchiveRoot: '/opt/liboqs-0.16.0');

Cache Behavior

  • Loader caches resolved DynamicLibrary by default.
  • Update paths at runtime: set LibOQSLoader.customPaths = ... (this clears cache).
  • Manual reset: LibOQSLoader.clearCache().

Debug Checklist

  1. Verify LibOQS.getVersion() returns non-empty string.
  2. Print LibOQS.getSupportedKEMAlgorithms() to confirm expected build features.
  3. If loading fails, inspect thrown LibraryLoadException strategy list and fix the earliest intended path.

Quick Start

import 'dart:typed_data';
import 'package:oqs/oqs.dart';

void main() {
  LibOQS.init();

  final kems = LibOQS.getSupportedKEMAlgorithms();
  if (kems.isEmpty) {
    throw StateError('No enabled KEM algorithms in loaded liboqs');
  }

  final kem = KEM.create(kems.first)!;
  final kp = kem.generateKeyPair();
  final enc = kem.encapsulate(kp.publicKey);
  final dec = kem.decapsulate(enc.ciphertext, kp.secretKey);

  print(dec.length == enc.sharedSecret.length); // true

  kem.dispose();
  LibOQS.cleanup();
}

API Notes

  • Prefer runtime algorithm discovery:
    • LibOQS.getSupportedKEMAlgorithms()
    • LibOQS.getSupportedSignatureAlgorithms()
  • Do not hard-code key/signature lengths. Use:
    • kem.publicKeyLength, kem.secretKeyLength, kem.ciphertextLength
    • sig.publicKeyLength, sig.secretKeyLength, sig.maxSignatureLength
  • Deterministic keypair generation is algorithm-dependent:
    • kem.supportsDeterministicGeneration
    • kem.seedLength

Signature Example

import 'dart:convert';
import 'dart:typed_data';
import 'package:oqs/oqs.dart';

void main() {
  final sigAlgs = LibOQS.getSupportedSignatureAlgorithms();
  if (sigAlgs.isEmpty) {
    throw StateError('No enabled signature algorithms');
  }

  final sig = Signature.create(sigAlgs.first);
  final kp = sig.generateKeyPair();

  final msg = Uint8List.fromList(utf8.encode('hello pqc'));
  final s = sig.sign(msg, kp.secretKey);
  final ok = sig.verify(msg, s, kp.publicKey);

  print(ok); // true
  sig.dispose();
}

Migration to 3.x (liboqs 0.15.0)

  1. Replace fixed algorithm assumptions (Kyber*, Dilithium*) with runtime discovery.
  2. Remove hard-coded size assertions and read lengths from each algorithm instance.
  3. Re-run tests against every target platform binary you ship.

Migration to 4.x (liboqs 0.16.0)

  1. Read the FrodoKEM section below before upgrading if you use FrodoKEM.
  2. SPHINCS+ was removed upstream. If you called Signature.create('SPHINCS+-...') or similar, getSupportedSignatureAlgorithms() will no longer list it and create()/isSupported() will report it as unavailable. Migrate to ML-DSA or Falcon.
  3. No Dart API changes are required otherwise

⚠️ FrodoKEM naming change (important, not just cosmetic)

Starting with liboqs 0.16.0, the same algorithm names now mean something different:

  • FrodoKEM-640-AES, FrodoKEM-976-AES, FrodoKEM-1344-AES (and -SHAKE variants) used to be the ephemeral (one-shot) variant in liboqs 0.15.x.
  • In 0.16.0, those exact names now point to the new salted FrodoKEM, intended for keypairs that will encapsulate many ciphertexts.
  • The old ephemeral behavior is still available, but under new names: eFrodoKEM-640-AES, eFrodoKEM-976-AES, eFrodoKEM-1344-AES, etc.

Common Problems

Library not found

Set LibOQSLoader.customPaths or install liboqs to standard system paths.

Algorithm not available

Enabled algorithms depend on how your liboqs binary was built. Check:

print(LibOQS.getSupportedKEMAlgorithms());
print(LibOQS.getSupportedSignatureAlgorithms());

Security Notes

  • Use NIST-standardized algorithms (ML-KEM-*, ML-DSA-*) for production.
  • Dispose algorithm objects (kem.dispose(), sig.dispose()) when done.
  • Call dispose() on key pairs and results (e.g., kp.dispose()) to wipe sensitive data from the Dart heap (best-effort).
  • Keep liboqs binaries updated and track security advisories.
  • Do not share mutable crypto object state across isolates/threads.

Examples

See the example/ directory for end-to-end usage samples.

Libraries

oqs