flutter_file_hash 0.0.4 copy "flutter_file_hash: ^0.0.4" to clipboard
flutter_file_hash: ^0.0.4 copied to clipboard

Native Flutter file and string hashing powered by Zig, with SHA, MD5, HMAC, BLAKE3, XXH3, cancellation, mmap, and Android content:// support.

flutter_file_hash #

pub version pub downloads

Native streaming hashes for Flutter files, strings, HMAC, XXH3, and BLAKE3, powered by a shared Zig core.

Use it when your app needs to verify large downloads, fingerprint media, deduplicate local files or cached uploads, generate fast cache keys, compare local content, or authenticate data with HMAC or keyed BLAKE3.

Hash large files without loading them into Dart memory. Native code streams files in chunks, keeps heavy work off the UI isolate where possible, and returns a lowercase hex digest. The hashing core is zig-files-hash, exposed through its C ABI and called from Flutter through FFI/native assets.

Status #

This package targets Flutter native platforms through one shared Zig core and Flutter native assets. Web is not part of the first native package scope.

The public API is intentionally small: fileHash for files and URI strings, stringHash for small in-memory strings or base64 payloads.

Highlights #

  • Streams file data from disk instead of loading the whole file into Dart memory.
  • Uses one shared Zig hash engine across supported native platforms.
  • Safe for concurrent calls: each operation owns its native hash state.
  • Defaults to SHA-256 when you do not pass an algorithm.
  • Supports cooperative cancellation for long-running file hashes.
  • Supports local files, file:// URIs, and Android content:// URIs.
  • Provides opt-in mmap I/O for stable regular local files.
  • Returns lowercase hex strings for every algorithm.
  • Includes SHA variants, HMAC algorithms, XXH3-64, BLAKE3, and keyed BLAKE3.

Platform Status #

Platform Status Engine
Android Native zig; content:// through Android opener
iOS Native zig
macOS Native zig
Linux Native zig
Windows Native zig
Web Not planned yet Would need a separate WASM/browser path

Installation #

Use the package from pub.dev:

dependencies:
  flutter_file_hash: ^0.0.4

For local development, use a path dependency:

dependencies:
  flutter_file_hash:
    path: ../flutter-file-hash

Local development and release verification steps are documented in CONTRIBUTING.md.

Quick Start #

import 'package:flutter_file_hash/flutter_file_hash.dart';

final digest = await fileHash('/path/to/video.mp4');
// SHA-256 lowercase hex by default

final xxh3 = await fileHash(
  '/path/to/video.mp4',
  algorithm: HashAlgorithm.xxh3_64,
);

final localMmap = await fileHash(
  '/path/to/stable-large-file.bin',
  useMmap: true,
);

final textDigest = stringHash('hello world');

For large payloads, prefer fileHash. stringHash is intended for small strings or small base64 payloads already in Dart memory.

Cancel Long Hashes #

import 'package:flutter_file_hash/flutter_file_hash.dart';

final controller = HashCancellationController();

final future = fileHash(
  '/path/to/large-file.bin',
  algorithm: HashAlgorithm.sha256,
  cancellationToken: controller.token,
);

controller.cancel();

try {
  await future;
} on FlutterFileHashCancelledException {
  // Hash was cancelled.
}

Cancellation is cooperative. Large file hashes stop at chunk boundaries. Small stringHash calls may finish before cancellation is observed.

HMAC And Keyed BLAKE3 #

HMAC is selected by the algorithm:

final hmac = await fileHash(
  '/path/to/upload.bin',
  algorithm: HashAlgorithm.hmacSha256,
  hashOptions: const HashOptions(
    key: 'super-secret',
    keyEncoding: KeyEncoding.utf8,
  ),
);

Keyed BLAKE3 uses the regular BLAKE3 algorithm with a key. The decoded key must be 32 bytes:

final keyed = await fileHash(
  '/path/to/upload.bin',
  algorithm: HashAlgorithm.blake3,
  hashOptions: const HashOptions(
    key: '3031323334353637383961626364656630313233343536373839616263646566',
    keyEncoding: KeyEncoding.hex,
  ),
);

keyEncoding can be utf8, hex, or base64. The default is utf8. For BLAKE3, hashOptions.key is optional: omit it for regular BLAKE3, pass it only when you want keyed BLAKE3.

HMAC algorithms require hashOptions.key. If you intentionally need HMAC with an empty key, pass key: '' explicitly; omitting key is rejected.

Seeded XXH3 #

XXH3-64 supports an optional unsigned 64-bit seed. Omit it for regular unseeded XXH3. A seed is not a secret and does not make XXH3 cryptographic. It selects a reproducible XXH3 namespace: the same bytes, algorithm, and seed produce the same digest on your backend, CLI tools, and Flutter app.

The most direct use case is server-side verification. For example, your backend can publish a download manifest with both the expected XXH3 digest and the seed that was used to compute it:

const manifestSeed = '12345678901234567890';
const manifestXxh3 = '4b5e0a417dfa7ed2';

final actual = await fileHash(
  '/path/to/downloaded/video.mp4',
  algorithm: HashAlgorithm.xxh3_64,
  hashOptions: const HashOptions(seed: manifestSeed),
);

if (actual != manifestXxh3) {
  throw StateError('Downloaded file failed checksum verification');
}

Pass large u64 seeds from a backend as strings, either decimal or 0x hex. Use a string or BigInt for values above the signed 64-bit range.

For app-owned namespaces, you can derive a stable seed from a readable label:

final mediaCacheSeed = xxh3SeedFromLabel('media-cache-v1');

final cacheKey = await fileHash(
  '/path/to/media.bin',
  algorithm: HashAlgorithm.xxh3_64,
  hashOptions: HashOptions(seed: mediaCacheSeed),
);

hashOptions.seed accepts an int, a BigInt, a decimal string, or a 0x hex string. Values are normalized before native hashing, so 12345, BigInt.from(12345), and '0x3039' all use the same seed.

xxh3SeedFromLabel(label) derives a deterministic seed from a UTF-8 label using FNV-1a 64-bit. The helper returns a canonical 0x hex seed, for example 0x091677a156a7756e. Use it when the app controls the namespace, such as media-cache-v1 or upload-dedupe-v2. If a backend or CLI must reproduce the same hashes, share either the derived seed value or the exact label derivation rule. Use HMAC or keyed BLAKE3 when authenticity matters.

API #

fileHash(path, ...) #

Hashes a file by streaming it from native code.

final mac = await fileHash(
  '/path/to/upload.bin',
  algorithm: HashAlgorithm.hmacSha256,
  hashOptions: const HashOptions(
    key: 'upload-signing-secret',
    keyEncoding: KeyEncoding.utf8,
  ),
  cancellationToken: controller.token,
);

path can be:

  • a local filesystem path;
  • a file:// URI string;
  • an Android content:// URI string, for example from the system document picker.

If algorithm is omitted, SHA-256 is used. Use hashOptions.key only with HMAC algorithms or keyed BLAKE3; regular hashes reject keys.

useMmap defaults to false. Set it only for a stable, non-empty regular local file after benchmarking your workload. It is an I/O optimization: the digest does not change. The option applies to the path fast path only, so it is ignored for Android content:// descriptors. Do not enable it while another process can truncate or modify the file: a mapped file may fault, including with SIGBUS on POSIX.

stringHash(input, ...) #

Hashes a small Dart string or base64 payload.

final digest = stringHash(
  'hello world',
  algorithm: HashAlgorithm.sha256,
  encoding: HashInputEncoding.utf8,
);

final fromBase64 = stringHash(
  base64Payload,
  algorithm: HashAlgorithm.blake3,
  encoding: HashInputEncoding.base64,
);

If algorithm is omitted, SHA-256 is used. If encoding is omitted, utf8 is used.

Request Types #

Future<String> fileHash(
  String path, {
  HashAlgorithm algorithm = HashAlgorithm.sha256,
  HashOptions? hashOptions,
  bool useMmap = false,
  HashCancellationToken? cancellationToken,
});

String stringHash(
  String input, {
  HashAlgorithm algorithm = HashAlgorithm.sha256,
  HashInputEncoding encoding = HashInputEncoding.utf8,
  HashOptions? hashOptions,
  HashCancellationToken? cancellationToken,
});

final class HashOptions {
  const HashOptions({
    this.key,
    this.keyEncoding = KeyEncoding.utf8,
    this.seed,
  });

  final String? key;
  final KeyEncoding keyEncoding;
  final Object? seed; // int, BigInt, decimal string, or 0x hex string
}

Algorithms #

Algorithm Use case Notes
SHA-256 Default general-purpose cryptographic hash Good default for integrity checks
SHA-384, SHA-512 Stronger SHA-2 variants Larger output, usually slower
SHA-224, SHA-512/224, SHA-512/256 SHA-2 compatibility variants Useful for protocols requiring these exact digests
MD5, SHA-1 Legacy compatibility Do not use for new security-sensitive designs
HMAC-SHA-256 Shared-secret authentication Good default HMAC choice
HMAC-SHA-224/384/512, HMAC-MD5, HMAC-SHA-1 Protocol compatibility Prefer SHA-256+ for new designs
XXH3-64 Fast non-cryptographic checksum Supports optional seed; not authentication
BLAKE3 Modern high-performance hash Also supports keyed mode with a 32-byte key

XXH3-128 is intentionally not exposed because it is not present in zig-files-hash C ABI v4.

Output Lengths #

Algorithm Output length
MD5, HMAC-MD5 16 bytes, 32 hex chars
SHA-1, HMAC-SHA-1 20 bytes, 40 hex chars
SHA-224, HMAC-SHA-224, SHA-512/224 28 bytes, 56 hex chars
SHA-256, HMAC-SHA-256, SHA-512/256, BLAKE3 32 bytes, 64 hex chars
SHA-384, HMAC-SHA-384 48 bytes, 96 hex chars
SHA-512, HMAC-SHA-512 64 bytes, 128 hex chars
XXH3-64 8 bytes, 16 hex chars

Error Handling #

Errors are thrown as FlutterFileHashException with a stable code.

Common error codes:

Code Meaning
cancelled Operation was cancelled
invalid_argument Invalid algorithm/options combination
invalid_key Key cannot be decoded or has the wrong length
file_not_found File or URI cannot be opened
unsupported_algorithm Algorithm is not available
access_denied Platform denied access to the file or URI
invalid_path Path cannot be interpreted by the native layer
io_error File I/O failed
hash_failed Native hashing failed unexpectedly

Key rules:

  • HMAC algorithms require hashOptions.key; pass key: '' explicitly for an empty HMAC key.
  • BLAKE3 uses keyed mode only when hashOptions.key is provided.
  • BLAKE3 keyed mode requires a 32-byte key after decoding.
  • Other algorithms reject hashOptions.key.
  • hashOptions.seed is only valid for XXH3-64.

Android URI Streams #

Plain filesystem paths use one Dart FFI call into zfh_context_file_hash; Zig opens and streams the file internally.

Android content:// inputs are the exception to the plain FFI path. They cannot be opened with dart:io File because the bytes come from ContentResolver, not from a normal filesystem path. The Android plugin opens a descriptor and passes it to Zig in one native call:

ContentResolver.openFileDescriptor(uri, "r")
  -> JNI
  -> zfh_fd_hash
    -> Zig 64 KiB read loop

Zig reads from the descriptor's current position and never closes it; Kotlin owns the ParcelFileDescriptor lifetime. If a provider exposes only a stream, the plugin keeps the existing ContentResolver.openInputStream + streaming hasher fallback. Neither path copies content:// data into a temporary file.

In practice, common Flutter pickers may still copy selected provider files into cache before returning them, but custom pickers can pass a content:// URI directly and avoid that extra copy.

FFI/native-assets Template Usage #

flutter_file_hash uses Flutter's modern plugin_ffi/native-assets path for the native hash core. The public Dart API talks to one Zig native core through the C ABI; platform code is used only where the operating system does not expose a regular file path to Dart.

macOS release builds can print a native-assets packaging warning like:

Code asset "package:flutter_file_hash/src/zig_files_hash_bindings.dart" has
different framework names for different architectures. Picking
"zig_files_hash_c_api.framework" and ignoring "zig_files_hash_c_api1.framework".

This comes from Flutter's macOS code_assets packaging path when it combines multiple architectures into one app bundle. The generated app bundle still contains the expected universal native framework, so the warning can be ignored.

The strategy is to keep plugin_ffi/native-assets for the Zig core and adopt upstream Flutter fixes when macOS native-assets packaging improves. Android platform code remains limited to opening provider URIs, as described in Android URI Streams.

Performance #

Use physical devices and Release builds for performance claims. Debug, simulator, emulator, and VM runs are useful for smoke checks, but they do not represent production throughput.

Current manual measurements live in doc/benchmarks.md.

Practical guidance:

  • Use SHA-256 for general integrity checks.
  • Use XXH3-64 for fast non-security checksums.
  • Use HMAC-SHA-256 for shared-secret authentication.
  • Use keyed BLAKE3 when you need BLAKE3 with a fixed 32-byte secret key.
  • Avoid MD5 and SHA-1 for new security-sensitive designs.

Native Implementation #

Package users do not need a local Zig toolchain; release artifacts include Zig prebuilts.

Contributing #

Contributions are welcome.

License #

MIT. See LICENSE for details.

Built on Flutter's plugin_ffi/native-assets template.

1
likes
160
points
329
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Native Flutter file and string hashing powered by Zig, with SHA, MD5, HMAC, BLAKE3, XXH3, cancellation, mmap, and Android content:// support.

Repository (GitHub)
View/report issues
Contributing

License

MIT (license)

Dependencies

code_assets, ffi, flutter, hooks

More

Packages that depend on flutter_file_hash

Packages that implement flutter_file_hash