litert_crypto 0.2.1 copy "litert_crypto: ^0.2.1" to clipboard
litert_crypto: ^0.2.1 copied to clipboard

Stop shipping your .tflite in plain sight. Encrypt LiteRT (TFLite) models at build time and decrypt them in memory only — with pluggable key providers.

litert_crypto #

Encrypt and load LiteRT (TensorFlow Lite / TFLite) models — a build-time encryption CLI plus an in-memory decryption loader with pluggable key providers.

This package does not guarantee protection. It provides encryption tooling and a key injection point (KeyProvider); the actual protection strength is determined by how you manage your keys. Read the threat model first.

A .tflite model bundled in a Flutter app can be extracted verbatim by unzipping the APK / IPA / desktop install folder. This package encrypts models at build time and decrypts them in memory only at runtime before handing them to an Interpreter. The plaintext model never touches the disk.

Usage #

flutter pub add litert_crypto

1. Generate a key and encrypt models (build time) #

dart run litert_crypto init                   # writes an annotated litert_crypto.yaml
dart run litert_crypto keygen                 # writes .secrets/model_master.key (gitignore it!)
dart run litert_crypto encrypt                # encrypts everything the config lists

init refuses to overwrite an existing config. A filled-in config looks like:

litert_crypto:
  key_file: .secrets/model_master.key
  key_parts_out: lib/model_master_key.dart   # optional, see below
  models:
    - src: models_src/yolo.tflite
      out: assets/tflite_model/yolo.tflite.enc
    - src: models_src/detector.tflite
      out: assets/tflite_model/detector.tflite.enc

encrypt reads the config named by --config, or the nearest litert_crypto.yaml at or above the working directory. Paths inside the config resolve relative to the config file. For a one-off you can skip the config and pass --key, --in, and --out directly (that path does not regenerate key_parts_out).

Keep plaintext originals (models_src/) outside your assets so they are never bundled, and register only the .enc files as Flutter assets.

Windows: the first run may ask for NASM

On Windows, the first CLI run (or flutter test) can stop with:

No CMAKE_ASM_NASM_COMPILER could be found

The crypto engine is BoringSSL, compiled from source on first use (cached after that), and on Windows that build assembles its x64 sources with NASM.

winget install nasm

If the same error persists, NASM is installed but its folder (usually %LOCALAPPDATA%\bin\NASM) never made it onto PATH — tool/setup.ps1 does the check, install, and PATH repair in one go.

key_parts_out — generated key source for EmbeddedKeyProvider

With key_parts_out set, encrypt also writes Dart source for the key it just encrypted with — the .enc files and the key the app decrypts with always match:

// GENERATED by `dart run litert_crypto encrypt` — do not edit by hand.
// key-fingerprint: 3f1a92c7
KeyProvider buildModelKeyProvider() => EmbeddedKeyProvider.fromParts([_partA, _partB]);

Under the same key, re-running encrypt rewrites the .enc files every time (fresh random IV per run) but not the generated Dart file — it records a key fingerprint and changes only with the key. Use key_parts_symbol to rename the generated function (default buildModelKeyProvider).

Key rotation and key_id

Rotating is just encrypting again with a fresh key: one encrypt run re-encrypts every model and regenerates the key parts together. Ship the new .enc files and the app in the same release

If two key generations must coexist — users on the previous release still fetch the old key from your server while new installs need the new one — bump key_id in the config when you rotate. The number is stamped into each .enc header and reaches your KeyProvider as KeyContext.keyId, which is how it can tell which generation's key to return.

2. Load at runtime #

import 'package:litert_crypto/litert_crypto.dart';

// Before: Interpreter.fromAsset('assets/tflite_model/model.tflite')
final interpreter = await EncryptedModel.fromAsset(
  'assets/tflite_model/model.tflite.enc',
  keyProvider: buildModelKeyProvider(),   // from the generated key_parts_out file
  build: Interpreter.fromBuffer,
);
// You get back exactly what your runtime returns — the inference code that
// follows stays unchanged.

This package depends on no inference runtime. You hand it the buffer constructor, so the same call works with flutter_litert, tflite_flutter, or anything else that accepts model bytes. Nothing here pins you to a runtime version, and its bugs are not yours to inherit.

The decrypted buffer is zeroed as soon as build returns, since runtimes copy the model into their own memory. If yours keeps the buffer alive instead, use EncryptedModel.decryptAsset() and manage the lifetime (and the wipe) yourself.

Cost, and where it is paid

Decryption runs on BoringSSL (via package:webcrypto), which uses the CPU's AES instructions — every ARMv8 phone has them. Measured on an AES-NI desktop: 1.2 ms/MB, a 72 MB model in 87 ms. Expect the same order of magnitude on phones.

Even fast decryption is solid CPU work, so it runs on a worker isolate by default and the calling isolate keeps rendering through it. Pass inIsolate: false to any loader entry point to keep it on the calling isolate.

Plan for a load to briefly hold a few model-sized buffers at once — the ciphertext, the plaintext, and the copy the inference runtime makes for itself. Only the runtime's copy survives the load.

The native library

App builds need no setup — BoringSSL (the crypto engine) is compiled by your normal Gradle/NDK or Xcode build. Running on a host machine instead (flutter test, or the CLI on a plain Dart VM) compiles it there, which needs cmake and a C compiler — plus NASM on Windows x64, covered in the Windows note above. Only the first run compiles; after that it is cached.

KeyProvider — the key source is your policy #

Provider Key source Strength Use case
EmbeddedKeyProvider Embedded in the app (XOR part-combining helper) Low — the key ships with the app Minimum defense — only stops unzip extraction
CallbackKeyProvider Your callback (license file, custom storage, ...) Up to you App-specific policies such as license binding
RemoteKeyProvider Your fetch callback, with retries, single-flight and optional caching Up to your server's gate Keeping the key out of the binary entirely
FallbackKeyProvider Tries providers in order Combinations like cache → server
// Example: pull the key from a signed license file.
final provider = CallbackKeyProvider((context) async {
  final license = await License.loadAndVerify();
  return license.modelKey;
});

Where the key lives — what actually decides the strength #

Encrypting the model is the easy half. What decides how much protection you actually get is whether the key ships with the app:

  • The key is in the build (EmbeddedKeyProvider, native-code derivation, ...) — whoever holds the app holds the key. Hiding it raises the extraction effort, but this tier only stops casual extraction from the distributed artifact; a debugger reads the key out of memory no matter how it was hidden.
  • The key does not ship (license file, server delivery, device-bound secure storage) — the qualitative jump: someone holding only your app has nothing to decrypt with. Access is gated by your issuing process, your server's checks, or the OS.

Moving up does not touch the encrypted assets or the load call — only the keyProvider argument changes. The full effort ladder and per-option trade-offs: docs/key-management.

Fetching the key from a server #

The transport is yours — this package has no HTTP dependency. Bring package:http, dio, or a platform channel; RemoteKeyProvider adds the retry, single-flight and caching plumbing around it.

final provider = RemoteKeyProvider(
  fetch: (ctx) async {
    final res = await http.get(
      Uri.parse('https://keys.example.com/model-key'
          '?keyId=${ctx.keyId}&label=${ctx.label}'),
      headers: {'Authorization': 'Bearer ${await session.token()}'},
    );
    if (res.statusCode == 403) {
      throw const KeyUnavailableException('not entitled'); // permanent: no retry
    }
    if (res.statusCode != 200) throw StateError('HTTP ${res.statusCode}');
    return decodeKeyBytes(res.bodyBytes); // JSON {"key": base64} / base64 / raw
  },
  cache: InMemoryKeyCache(ttl: const Duration(hours: 12)),
);

The callback receives keyId and label, so one service can serve several models and key generations. Throw KeyUnavailableException for permanent failures (rejected auth, unknown key) — anything else counts as transient and is retried.

A server moves the secret out of your binary; it does not decide who deserves it. That gate is your server's job — attestation on mobile, a user credential elsewhere. See docs/key-management.

KeyCache is an interface, not a plugin: InMemoryKeyCache keeps a fetched key for the life of the process and never touches disk. To survive restarts, implement KeyCache on top of flutter_secure_storage or your own channel — the package carries no platform-channel plugins of its own, so it works anywhere your inference runtime does.

Threat model #

Attack Defended?
Extracting the model by unzipping the bundle (APK / install folder) ✅ Yes — only ciphertext ships
Model tampering (backdoored model injection) ✅ Detected — GCM tag authenticates header + ciphertext
Copying ciphertext + app to another machine and decrypting offline Depends on your KeyProvider — Embedded ⚠️ / external key ✅
Memory dump while the app is running ❌ No — inherent limit of on-device inference; the exposure window is narrowed by zeroing keys and buffers after use
Patching / reverse engineering the decryption logic ❌ No — combine with --obfuscate

File format #

[magic "LRTC" (4B)] [version (1B)] [keyId (2B)] [labelLen (1B)] [label] [IV (12B)] [ciphertext ‖ GCM tag (16B)]

AES-256-GCM, and the entire model file is encrypted — nothing of the original .tflite survives in the clear. The header is authenticated but readable; the label defaults to the output file name, so a generic file name is all the discretion you need. Files written by 0.1.0 (format version 1) must be re-encrypted — one dart run litert_crypto encrypt is the whole migration. Field details and the reasoning behind the cipher choice: docs/design.

Roadmap #

Current status, planned work, and what was deliberately left out: ROADMAP.

0
likes
0
points
259
downloads

Publisher

verified publishercornpip.dev

Weekly Downloads

Stop shipping your .tflite in plain sight. Encrypt LiteRT (TFLite) models at build time and decrypt them in memory only — with pluggable key providers.

Repository (GitHub)
View/report issues

Topics

#litert #tflite #encryption #security

License

unknown (license)

Dependencies

args, flutter, webcrypto, yaml

More

Packages that depend on litert_crypto