From b1a601fabeb3f24fc414b1825ea5c7e258a2d52b Mon Sep 17 00:00:00 2001 From: Adel-Ayoub Date: Wed, 5 Aug 2026 01:32:06 +0100 Subject: [PATCH 1/2] docs: describe the current format restrictions --- CHANGELOG.md | 29 ++++++ CONTRIBUTING.md | 40 ++++---- README.md | 175 +++++++++++++++++--------------- RELEASE_GUIDE.md | 32 ++++-- example/lib/main.dart | 4 +- example/pubspec.lock | 4 +- ios/m_security.podspec | 2 +- lib/src/evfs/vault_service.dart | 3 +- macos/m_security.podspec | 2 +- pubspec.yaml | 6 +- rust/Cargo.lock | 2 +- rust/Cargo.toml | 2 +- 12 files changed, 179 insertions(+), 122 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f12462..ca18ef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +## [v0.3.6](https://github.com/MicroClub-USTHB/M-Security/releases/tag/v0.3.6) - 2026-07-31 + +### Added + +- `UnsafeLegacyEvfsPolicy` on `VaultService.create()` and `open()`, defaulting to `deny`. Both now fail with `unsafeLegacyFormatDenied` before touching the path; `allowUnauthenticatedV1V2` restores the previous behaviour and changes no stored bytes. +- Argon2id verification limits. The password may be at most 1024 UTF-8 bytes and the hash must sit within published parameter bounds, both checked before any memory is reserved. One verification runs at a time. +- `Argon2PolicyViolation`, `Argon2VerificationBusy`, `DisabledFormat` and `UnsafeLegacyFormatDenied` variants in `CryptoError`. +- `example/integration_test/containment_test.dart`, executed in CI by a consumer built outside the repository against the assembled publish payload. + +### Removed + +- `.mvex` export and import, and encrypted or compressed `MSSE` stream files, from the Rust source, the generated bindings and the built library's exported symbols. The six Dart methods remain as stubs so existing code compiles, each emitting one `disabledFormat` error before touching input or output. Existing files in either format are unreadable by this release and unchanged on disk. +- `createNoopEncryption()`, which aborted the host process when the testing feature was absent. + +### Changed + +- Flutter Rust Bridge pinned to exact 2.12.0 across the manifest, the crate, the CI generators and the committed bindings. A range let a fresh install resolve a runtime the bindings refuse. +- Flutter floor raised to `>=3.38.9`, which carries Dart 3.10.8. The previous `>=3.3.0` could not coexist with the `^3.10.8` Dart constraint. + +### Fixed + +- Apple pod builds no longer dump the process environment into build logs. +- The publish payload no longer carries local build output or example `Podfile.lock` files, and `rust/.gitignore` no longer hides the tracked `src/frb_generated.rs`. +- README, CONTRIBUTING and RELEASE_GUIDE describe the current surface. + +### Security + +- The v1/v2 vault format derives its keys with no per-vault salt, so two vaults under one master key repeat their nonces. Segment nonces come from the segment index and generation rather than the CSPRNG, structural metadata is unauthenticated, and log replay can restore an index pointing at ciphertext a delete already erased. Opting in accepts all of it. + ## [v0.3.5](https://github.com/MicroClub-USTHB/M-Security/releases/tag/v0.3.5) - 2026-04-10 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9ee22b..b44e2f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -269,38 +269,44 @@ Keep commits atomic, with one logical change per commit. ```bash cd rust && cargo test +cd rust && cargo test --release ``` -There are 79 unit tests covering all algorithms, including NIST and RFC test vectors (RFC 8439 for ChaCha20, RFC 5869 for HKDF). +There are 466 unit tests covering all algorithms, including NIST and RFC test vectors (RFC 8439 for ChaCha20, RFC 5869 for HKDF). Both profiles run the same set. -### Dart Integration Tests +### Host Dart tests -Integration tests require a running device or simulator. From the **project root**: +```bash +flutter test test/ tool/ +``` + +Twenty cases, no device needed. + +### Integration tests + +One file executes, against the native library built from the assembled publish payload. ```bash cd example -flutter test integration_test/aes_gcm_test.dart -flutter test integration_test/chacha20_test.dart -flutter test integration_test/hashing_test.dart -flutter test integration_test/argon2_test.dart -flutter test integration_test/hkdf_test.dart +flutter test integration_test/containment_test.dart -d macos ``` -There are 44 integration tests across 5 files covering all features. +The broader suites under `integration_test/` and `example/integration_test/` hold 98 and 120 declarations and are not wired into any runner. Adding a case to one of them does not make it run. Canonicalizing those trees is open work. ### CI Pipeline All pull requests must pass the CI pipeline (`.github/workflows/ci.yml`), which runs: -| Job | Runner | What it does | -| ----------- | --------------- | --------------------------------------------- | -| **Rust** | `ubuntu-latest` | `cargo clippy -- -D warnings` + `cargo test` | -| **Dart** | `ubuntu-latest` | FRB codegen + `build_runner` + `dart analyze` | -| **Android** | `ubuntu-latest` | Full APK build (ARM64 + ARMv7, NDK r27c) | -| **iOS** | `macos-latest` | Simulator debug build (ARM64 + ARM64-sim) | -| **Linux** | `ubuntu-latest` | Release build with GTK-3 | +| Job | Runner | What it does | +| --------------------- | --------------- | -------------------------------------------------------- | +| **Rust** | `ubuntu-latest` | `cargo clippy -- -D warnings` + `cargo test` | +| **Dart** | `ubuntu-latest` | FRB codegen + `build_runner` + `dart analyze` + host tests | +| **Packaged consumer** | `ubuntu-latest` | Assembles the publish payload, builds a consumer outside the repository against it and runs the integration file | +| **Android** | `ubuntu-latest` | Release APK (ARM64 + ARMv7, NDK r27c) | +| **Apple** | `macos-latest` | iOS simulator debug build and a macOS debug build | +| **Linux** | `ubuntu-latest` | Release build with GTK-3 | -The CI is triggered on pushes and PRs to `main` and `dev` branches. +CI triggers on pushes to `main` and `dev`, and on pull requests to those and to `staging/**`. The last three jobs are skipped when the base is a `staging/` branch, so they first run at the promotion into `dev`. Adding a case to `containment_test.dart` means raising `--min-tests` in the workflow to match. ## Submitting a Pull Request diff --git a/README.md b/README.md index c39ac5d..abb76cf 100644 --- a/README.md +++ b/README.md @@ -12,37 +12,46 @@ [![CI](https://github.com/MicroClub-USTHB/M-Security/actions/workflows/ci.yml/badge.svg)](https://github.com/MicroClub-USTHB/M-Security/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -A native Rust security SDK for Flutter, providing high-performance cryptographic services, streaming encryption with compression, an encrypted virtual file system (EVFS), and secure memory management. All operations run in Rust through [Flutter Rust Bridge](https://cjycode.com/flutter_rust_bridge/). No Dart-level crypto, no platform channels. +A native Rust security SDK for Flutter. AEAD encryption, hashing, password hashing and key derivation, plus an encrypted virtual file system this release denies by default. All operations run in Rust through [Flutter Rust Bridge](https://cjycode.com/flutter_rust_bridge/). No Dart-level crypto, no platform channels. Built and maintained by the **Dev Department** of [MicroClub](https://github.com/MicroClub-USTHB), the computer science club at USTHB (University of Science and Technology Houari Boumediene, Algiers). ## Features -| Category | Algorithm / Feature | Highlights | -| ------------------------ | ---------------------- | ----------------------------------------------------------- | -| **AEAD Encryption** | AES-256-GCM | Industry-standard, hardware-accelerated on most CPUs | -| | ChaCha20-Poly1305 | Optimized for mobile (no AES hardware needed) | -| **Streaming Encryption** | AES-256-GCM / ChaCha20 | Chunk-based processing with progress callbacks | -| **Compression** | Zstd, Brotli | Configurable levels, integrated into streaming and EVFS | -| **Hashing** | BLAKE3 | Ultra-fast, one-shot and streaming | -| | SHA-3-256 (Keccak) | NIST-standard, one-shot and streaming | -| **Password Hashing** | Argon2id | PHC winner, Mobile and Desktop presets | -| **Key Derivation** | HKDF-SHA256 | RFC 5869, extract-then-expand with domain separation | -| **Encrypted VFS (EVFS)** | `.vault` container | Named segments, WAL recovery, shadow index, secure deletion | -| **Segment Enhancements** | Metadata, rename, parallel | Per-segment key-value tags, rename without re-encryption, concurrent reads | -| **Key Management** | Rotation, export/import | Atomic re-encryption, `.mvex` portable archives | -| **Zero-Copy I/O** | mmap + DCO codec | Memory-mapped vault reads, zero-copy Rust-to-Dart transfers | +| Category | Algorithm / Feature | Highlights | +| ------------------------ | -------------------------- | ----------------------------------------------------------- | +| **AEAD Encryption** | AES-256-GCM | Industry-standard, hardware-accelerated on most CPUs | +| | ChaCha20-Poly1305 | Optimized for mobile (no AES hardware needed) | +| **Hashing** | BLAKE3 | Ultra-fast, one-shot and streaming | +| | SHA-3-256 (Keccak) | NIST-standard, one-shot and streaming | +| **File Hashing** | BLAKE3 / SHA-3-256 | Constant-memory hashing of a file path | +| **Password Hashing** | Argon2id | PHC winner, Mobile and Desktop presets, bounded verification | +| **Key Derivation** | HKDF-SHA256 | RFC 5869, extract-then-expand with domain separation | +| **Compression** | Zstd, Brotli | Configurable levels, applied per vault segment | +| **Encrypted VFS (EVFS)** | `.vault` container | Named segments, metadata, rename, parallel reads. Denied unless you opt in, see [Current format restrictions](#current-format-restrictions) | +| **Zero-Copy I/O** | mmap + DCO codec | Memory-mapped vault reads, zero-copy Rust-to-Dart transfers | **Security by design:** -- All key material lives in Rust behind opaque handles; raw keys never cross FFI -- Automatic memory zeroization on drop (`ZeroizeOnDrop`) -- Nonces generated internally via OS-level CSPRNG (`OsRng`) +- Cipher state lives in Rust behind an opaque handle. Raw key bytes still cross FFI in both directions, since `generateAes256GcmKey` and the HKDF calls return them to Dart while `createAes256Gcm`, `VaultService.create`, `open` and `rotateKey` take them from it +- Key material held in `SecretBuffer` is zeroed on drop (`ZeroizeOnDrop`). That is not a whole-process property, because expanded cipher state is not proved wiped and no test inspects freed memory +- One-shot AEAD nonces come from the OS CSPRNG (`OsRng`). EVFS segment nonces do not, see [Current format restrictions](#current-format-restrictions) - AEAD tag verification prevents silent decryption of tampered data - `panic = "abort"` in release profile, preventing undefined behavior from panics crossing FFI -- `clippy::unwrap_used = "deny"`, ensuring all operations return `Result` +- `clippy::unwrap_used = "deny"`. Fallible operations return `Result`; the infallible ones, such as one-shot hashing, return their value directly - Release builds strip all symbols except FRB entry points (LTO + ELF version script) -- `mlock()` pins mmap'd ciphertext pages to prevent swap-to-disk (unix) +- CI reads the shipped Linux `.so` with `nm -D --defined-only` and fails if any of the seven removed entries is present or any of four kept entries is missing. That library is a debug build, so the check covers the version script rather than the release profile's stripping +- `mlock()` asks the OS to keep mmap'd ciphertext pages out of swap (unix). A failure is ignored, so it is best effort rather than a guarantee + +## Current format restrictions + +For now, EVFS is denied by default and the archive and encrypted stream formats are unreachable. Existing code still compiles. + +EVFS `create` and `open` fail with `unsafeLegacyFormatDenied` before they touch the path. A caller who accepts the risk passes `UnsafeLegacyEvfsPolicy.allowUnauthenticatedV1V2` explicitly, and that opt-in does not change a single byte on disk or make an existing vault safe. The format derives its keys with no per-vault salt, so two vaults under the same master key repeat their encryption nonces. Segment nonces come from the segment index and generation rather than from the CSPRNG. Structural metadata is not authenticated. Write-ahead log replay can restore an index pointing at ciphertext that a delete already erased, so "WAL recovery" and "secure deletion" are not properties this release offers. + +`.mvex` archive export and import, and encrypted or compressed `MSSE` stream files, are gone from the native library. Six methods are kept so current code compiles, namely `VaultService.export` and `importVault`, `StreamingService.encryptFile` and `decryptFile`, and `CompressionService.compressAndEncryptFile` and `decryptAndDecompressFile`. Each fails with exactly one `disabledFormat` error, the four stream methods by emitting it rather than returning it, before reading its input or creating its output. Files you already wrote in either format are unreadable by this release and unchanged on disk. There is no converter, and this release does not ship a replacement format. + +File hashing through `StreamingService.hashFile` is unaffected. ## Installation @@ -50,7 +59,7 @@ Add to your `pubspec.yaml`: ```yaml dependencies: - m_security: ^0.3.5 + m_security: ^0.3.6 ``` Then run: @@ -144,7 +153,7 @@ final hash = await argon2IdHash(password: 'hunter2'); await argon2IdVerify(phcHash: hash, password: 'hunter2'); // Explicit preset selection -final hash = await argon2IdHash( +final desktopHash = await argon2IdHash( password: 'hunter2', preset: Argon2Preset.desktop, // 256 MiB, t=4, p=8 ); @@ -152,6 +161,10 @@ final hash = await argon2IdHash( The default preset is selected at compile time: `Argon2Preset.mobile` (64 MiB, t=3, p=4) unless built with `-DIS_DESKTOP=true`. +Verification is bounded before it reserves anything. The password must be at most 1024 UTF-8 bytes. The hash must be Argon2id version 19, with `t` from 1 to 4, `p` from 1 to 8, `m` from `8*p` KiB up to 262144 KiB, and a 16 to 64 byte output. The salt must decode to at least 8 bytes, and the PHC parser caps the encoded form at 64 characters, so 48 decoded bytes is the largest that reaches a verifier at all. + +Parameters outside those bounds return `Argon2PolicyViolation`. Input the PHC parser rejects, including an over-long salt, returns `InvalidParameter`. Both come back before any Argon2 memory is allocated. One verification runs at a time, so a call arriving during another gets `Argon2VerificationBusy` rather than waiting. + ### HKDF-SHA256 Key Derivation ```dart @@ -174,30 +187,23 @@ final derived = await MHKDF.expand(prk: prk, info: infoBytes, outputLen: 32); Output length must be between 1 and 8160 bytes (RFC 5869 limit for SHA-256: 255 \* 32). -### Streaming Encryption +### File hashing ```dart -import 'package:m_security/src/rust/api/streaming.dart'; - -// Encrypt a file in chunks with progress -final encrypted = await streamEncrypt( - plaintext: largeData, - algorithm: StreamAlgorithm.aes256Gcm, - compression: CompressionAlgorithm.zstd, - compressionLevel: 3, - onProgress: (progress) => print('${(progress * 100).toInt()}%'), -); +final hasher = await createBlake3(); // or createSha3() -// Decrypt -final decrypted = await streamDecrypt( - ciphertext: encrypted, - algorithm: StreamAlgorithm.aes256Gcm, - compression: CompressionAlgorithm.zstd, +final digest = await StreamingService.hashFile( + filePath: '/path/to/large.bin', + hasher: hasher, ); ``` +The file is read in 64 KB chunks, so memory use does not grow with its size. `StreamingService.encryptFile` and `decryptFile` are disabled, see [Current format restrictions](#current-format-restrictions). + ### Encrypted Virtual File System (EVFS) +Every snippet below needs the opt-in shown here. Without it `create` and `open` return `unsafeLegacyFormatDenied` and never touch the path. + ```dart import 'package:m_security/m_security.dart'; @@ -207,6 +213,7 @@ final handle = await VaultService.create( key: key, algorithm: 'aes-256-gcm', capacityBytes: 10 * 1024 * 1024, + unsafeLegacyPolicy: UnsafeLegacyEvfsPolicy.allowUnauthenticatedV1V2, ); // Write a segment (with optional compression and metadata) @@ -229,31 +236,16 @@ await VaultService.delete(handle: handle, name: 'secret.txt'); await VaultService.close(handle: handle); ``` -#### Key Management +#### Key rotation ```dart -// Rotate master key (re-encrypts all segments atomically) +// Rotate master key (re-encrypts all segments under the new key) final newHandle = await VaultService.rotateKey(handle: handle, newKey: newKey); // Old handle is invalidated; use newHandle from here - -// Export vault to portable encrypted archive -await VaultService.export( - handle: handle, - wrappingKey: wrappingKey, - exportPath: '/path/to/backup.mvex', -); - -// Import vault from archive (creates new vault with fresh key) -final imported = await VaultService.importVault( - archivePath: '/path/to/backup.mvex', - wrappingKey: wrappingKey, - destPath: '/path/to/restored.vault', - newMasterKey: localKey, - algorithm: 'aes-256-gcm', - capacityBytes: 10 * 1024 * 1024, -); ``` +Rotation copies to a new file and renames, but the sequence is not crash-atomic. A machine that dies mid-rotation can leave a `.rotating` file that the next `open` cleans up, and that cleanup has no power-loss evidence behind it. + #### Segment Enhancements ```dart @@ -282,7 +274,7 @@ final health = await VaultService.health(handle: handle); print('Consistent: ${health.isConsistent}'); print('Fragmentation: ${(health.fragmentationRatio * 100).toStringAsFixed(1)}%'); -// Defragment — compact segments, coalesce free space (WAL-protected) +// Defragment, compacting segments and coalescing free space final result = await VaultService.defragment(handle: handle); print('Moved ${result.segmentsMoved} segments, reclaimed ${result.bytesReclaimed} bytes'); @@ -292,17 +284,13 @@ await VaultService.resize(handle: handle, newCapacityBytes: 20 * 1024 * 1024); ### BLAKE3 & SHA-3-256 Hashing -For one-shot and streaming hashing, use the lower-level FFI API directly: - ```dart -import 'package:m_security/src/rust/api/hashing.dart'; - // One-shot hashing (32-byte output) final blake3Digest = await blake3Hash(data: inputBytes); final sha3Digest = await sha3Hash(data: inputBytes); // Streaming: process data in chunks -final hasher = createBlake3(); // or createSha3() +final hasher = await createBlake3(); // or createSha3() await hasherUpdate(handle: hasher, data: chunk1); await hasherUpdate(handle: hasher, data: chunk2); final digest = await hasherFinalize(handle: hasher); @@ -321,7 +309,7 @@ await hasherReset(handle: hasher); - **Opaque handles.** `CipherHandle` and `HasherHandle` are `#[frb(opaque)]`. Dart holds a pointer, never raw key bytes. - **Trait objects.** `Box` and `Box` with `Send + Sync + 'static` enable runtime algorithm selection. -- **SecretBuffer.** All key material is wrapped in `SecretBuffer` which derives `ZeroizeOnDrop`. Memory is zeroed when handles are dropped. +- **SecretBuffer.** Key material is wrapped in `SecretBuffer`, which derives `ZeroizeOnDrop`, so its buffer is zeroed when the handle drops. Cipher state expanded from that key is not covered by the same guarantee. - **No panics across FFI.** `panic = "abort"` in release profile. All FFI functions return `Result`. - **Format headers.** Encrypted data includes a `MSEC` magic header with version and algorithm identifiers for forward compatibility. @@ -360,7 +348,7 @@ argon2id_hash_with_salt(password, salt, preset) -> Result (PHC) argon2id_verify(phc_hash, password) -> Result<()> ``` -Presets: `Mobile` (64 MiB, t=3, p=4) | `Desktop` (256 MiB, t=4, p=8) +Two presets exist, `Mobile` (64 MiB, t=3, p=4) and `Desktop` (256 MiB, t=4, p=8). `argon2id_verify` enforces the limits described under [Argon2id Password Hashing](#argon2id-password-hashing). ### Key Derivation (HKDF-SHA256) @@ -372,37 +360,53 @@ hkdf_expand(prk, info, output_len) -> Result> ## Platform Support -| Platform | Target | Status | -| -------- | -------------------------------------------------- | --------- | -| Android | `aarch64-linux-android`, `armv7-linux-androideabi` | CI-tested | -| iOS | `aarch64-apple-ios`, `aarch64-apple-ios-sim` | CI-tested | -| macOS | `aarch64-apple-darwin`, `x86_64-apple-darwin` | Supported | -| Linux | `x86_64-unknown-linux-gnu` | CI-tested | -| Windows | `x86_64-pc-windows-msvc` | Supported | +The targets below are configured. What CI does with each of them varies, so the table says which, and the platform builds all wait for the release promotion rather than running on every change. + +| Platform | Configured target | What CI builds | +| -------- | -------------------------------------------------- | ---------------------------------------------------------------- | +| Android | `aarch64-linux-android`, `armv7-linux-androideabi` | A release APK, at the promotion | +| iOS | `aarch64-apple-ios-sim`, `aarch64-apple-ios` | A debug simulator build, at the promotion. The device target is never built | +| macOS | `aarch64-apple-darwin`, `x86_64-apple-darwin` | A debug build on the runner's own architecture, at the promotion. The other one is never built | +| Linux | `x86_64-unknown-linux-gnu` | A release build at the promotion, and on every change a debug library that a clean consumer runs the tests against | + +Ubuntu x86_64 is the only target with both a build and a runtime gate, and that gate runs against a debug library. No release-profile artifact is executed anywhere, on any platform. ## Testing -**Rust unit tests** (331 tests including EVFS streaming and defrag): +**Rust unit tests**, 466 in each profile. ```bash cd rust && cargo test +cd rust && cargo test --release ``` -**Dart integration tests** (76 tests across all features, requires a running device/simulator): +**Host Dart tests**, 20 cases. + +```bash +flutter test test/ tool/ +``` + +**Containment integration tests**, 20 cases against the built native library. ```bash cd example -flutter test integration_test/ +flutter test integration_test/containment_test.dart -d macos ``` +That resolves the package through the checkout. CI runs the same file on Linux from a consumer assembled outside this repository, depending only on the publish payload, which is the packaged path. + +The broader suites under `integration_test/` and `example/integration_test/` hold 98 and 120 declarations. Nothing executes them. `containment_test.dart` is one of the 120 and is the only file in either tree that runs anywhere; reconciling the rest is later work. + ## Tech Stack | Component | Version | -| ------------------- | ------- | -| Rust | stable | -| Flutter Rust Bridge | 2.11.1 | -| Dart SDK | ^3.10.8 | -| Flutter SDK | >=3.3.0 | +| ------------------- | -------- | +| Rust | stable | +| Flutter Rust Bridge | 2.12.0 | +| Dart SDK | ^3.10.8 | +| Flutter SDK | >=3.38.9 | + +The bridge constraint is exact, not a range. The runtime compares the version stamped into the committed bindings against its own and refuses a mismatch, so a range would ship a package that installs and cannot start. **Rust crates:** `aes-gcm` 0.10, `chacha20poly1305` 0.10, `blake3` 1.8, `sha3` 0.10, `argon2` 0.5, `hkdf` 0.12, `zstd` 0.13, `brotli` 7.0, `zeroize` 1.8, `memmap2` 0.9 @@ -410,13 +414,16 @@ flutter test integration_test/ | Feature | Description | Status | | ---------------------------------------- | ----------------------------------------------------------------------------------- | ------- | -| **Streaming encryption** | Process large files in chunks with progress callbacks | v0.3.0 | -| **Compression pipeline** | Zstd/Brotli compression integrated into streaming and EVFS | v0.3.0 | -| **Encrypted Virtual File System (EVFS)** | `.vault` container with named segments, WAL recovery, shadow index, secure deletion | v0.3.0 | +| **Compression pipeline** | Zstd/Brotli compression with configurable levels | v0.3.0 | +| **Encrypted Virtual File System (EVFS)** | `.vault` container with named segments and a shadow index | v0.3.0 | | **EVFS v2: Defrag & resize** | Online defragmentation, vault resizing, health diagnostics | v0.3.1 | | **EVFS v2: Streaming I/O** | Constant-memory streaming reads/writes, per-chunk AEAD, progress callbacks | v0.3.2 | | **Zero-copy FFI optimization** | mmap vault reads, DCO codec, release profile hardening, symbol stripping | v0.3.3 | -| **EVFS v2: Key management** | Key rotation, vault export/import (`.mvex` archives), Dart wrappers | v0.3.4 | +| **EVFS v2: Key rotation** | Master key rotation with Dart wrappers | v0.3.4 | +| **Streaming encryption** | Chunked file encryption, shipped in v0.3.0 and withdrawn in v0.3.6. Returns over an authenticated stream format | Withdrawn | +| **`.mvex` portable archives** | Vault export and import, shipped in v0.3.4 and withdrawn in v0.3.6. Returns over an authenticated archive format | Withdrawn | +| **Crash-atomic vault recovery** | Write-ahead log ordering that cannot restore an index pointing at erased ciphertext | Planned | +| **Secure deletion** | Erasure the container format can actually guarantee | Planned | | **Stealth storage** | Ephemeral secrets in Rust-managed memory with derived-path obfuscation | Planned | | **Hardware key wrap** | Master key in Secure Enclave (iOS) / KeyStore (Android) with biometric unlock | Planned | diff --git a/RELEASE_GUIDE.md b/RELEASE_GUIDE.md index 57c2212..ac7ce1e 100644 --- a/RELEASE_GUIDE.md +++ b/RELEASE_GUIDE.md @@ -90,12 +90,21 @@ cd .. flutter pub get flutter_rust_bridge_codegen generate dart run build_runner build --delete-conflicting-outputs -dart analyze lib/ integration_test/ +dart analyze lib/ integration_test/ test/ tool/ example/ +flutter test test/ tool/ -# Run integration tests (requires a device/simulator) +# Run the integration file that actually executes (requires a device/simulator) cd example -flutter test integration_test/ +flutter test integration_test/containment_test.dart -d macos cd .. + +# Assemble the publish payload and check it from outside the checkout. +# Do this rather than running the dry run against the repository. The payload +# is what ships, and only this proves no untracked local file influences it. +dart run tool/publication.dart --out /tmp/payload +dart run tool/packaged_consumer.dart --payload /tmp/payload --out /tmp/consumer \ + --device macos --min-tests 20 --report /tmp/payload.consumer.json +(cd /tmp/payload && dart pub publish --dry-run) ``` ### 5. Open a Pull Request to `main` @@ -210,21 +219,26 @@ git checkout -b hotfix/vX.Y.Z main ## Checklist -Use this checklist when preparing a release: +Use this checklist when preparing a release. The eight version locations move together, and missing one ships a package whose parts disagree. - [ ] Version updated in `pubspec.yaml` - [ ] Version updated in `rust/Cargo.toml` +- [ ] Version updated in the `m_security` root entry of `rust/Cargo.lock` (`cargo update -p m_security`) - [ ] Version updated in `ios/m_security.podspec` - [ ] Version updated in `macos/m_security.podspec` -- [ ] CHANGELOG.md updated with release date -- [ ] All Rust tests pass (`cargo test`) -- [ ] Clippy clean (`cargo clippy -- -D warnings`) +- [ ] Version updated in the README installation snippet +- [ ] Version updated in `example/lib/main.dart` (both the app title and the AppBar) +- [ ] Version updated in `example/pubspec.lock` (`cd example && flutter pub get`) +- [ ] CHANGELOG.md has a heading naming this version, otherwise pub warns and the payload assembler fails +- [ ] All Rust tests pass in both profiles (`cargo test`, `cargo test --release`) +- [ ] Clippy clean (`cargo clippy --all-targets -- -D warnings`) - [ ] FRB codegen runs cleanly (`flutter_rust_bridge_codegen generate`) - [ ] Dart analysis clean (`dart analyze`) -- [ ] Integration tests pass +- [ ] Host Dart tests pass (`flutter test test/ tool/`) +- [ ] The packaged consumer reports a nonzero executed count and a clean symbol scan +- [ ] `dart pub publish --dry-run` succeeds from the assembled payload, not from the checkout - [ ] CI pipeline passes on the PR - [ ] PR merged to `main` -- [ ] Dry-run publish passes (`dart pub publish --dry-run`) - [ ] Git tag created and pushed - [ ] GitHub Release created - [ ] Published to pub.dev (`dart pub publish`) diff --git a/example/lib/main.dart b/example/lib/main.dart index 73e888a..f642353 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -18,7 +18,7 @@ class ExampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'M-Security v0.3.5', + title: 'M-Security v0.3.6', theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true), home: const DemoHome(), ); @@ -38,7 +38,7 @@ class _DemoHomeState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('M-Security v0.3.4')), + appBar: AppBar(title: const Text('M-Security v0.3.6')), body: IndexedStack( index: _tab, children: const [ diff --git a/example/pubspec.lock b/example/pubspec.lock index a4efc7c..6b5086d 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -176,7 +176,7 @@ packages: path: ".." relative: true source: path - version: "0.3.5" + version: "0.3.6" matcher: dependency: transitive description: @@ -328,4 +328,4 @@ packages: version: "3.1.0" sdks: dart: ">=3.10.8 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.38.9" diff --git a/ios/m_security.podspec b/ios/m_security.podspec index fc81b5b..5fdd8ae 100644 --- a/ios/m_security.podspec +++ b/ios/m_security.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'm_security' - s.version = '0.3.5' + s.version = '0.3.6' s.summary = 'A high-performance cryptographic SDK for Flutter powered by native Rust via FFI.' s.description = <<-DESC A high-performance cryptographic SDK for Flutter powered by native Rust via FFI. diff --git a/lib/src/evfs/vault_service.dart b/lib/src/evfs/vault_service.dart index a800232..9ce148d 100644 --- a/lib/src/evfs/vault_service.dart +++ b/lib/src/evfs/vault_service.dart @@ -274,7 +274,8 @@ class VaultService { /// Defragment the vault: compact segments, coalesce free space. /// - /// Each segment move is WAL-protected for crash safety. + /// Each segment move is journalled, but the journal is the same one described + /// on this class, so a crash part way through is not a recoverable state. /// Returns a [DefragResult] with move count and bytes reclaimed. static Future defragment({ required rust_types.VaultHandle handle, diff --git a/macos/m_security.podspec b/macos/m_security.podspec index 4205372..a80059b 100644 --- a/macos/m_security.podspec +++ b/macos/m_security.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'm_security' - s.version = '0.3.5' + s.version = '0.3.6' s.summary = 'A high-performance cryptographic SDK for Flutter powered by native Rust via FFI.' s.description = <<-DESC A high-performance cryptographic SDK for Flutter powered by native Rust via FFI. diff --git a/pubspec.yaml b/pubspec.yaml index 1b2f10c..845fdb2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ name: m_security description: >- A native Rust cryptographic SDK for Flutter via FFI. AES-256-GCM, ChaCha20-Poly1305, - BLAKE3, SHA-3, Argon2id, HKDF, streaming encryption, and encrypted virtual file system. -version: 0.3.5 + BLAKE3, SHA-3, Argon2id, HKDF, file hashing, and an encrypted virtual file system. +version: 0.3.6 homepage: https://github.com/MicroClub-USTHB/M-Security repository: https://github.com/MicroClub-USTHB/M-Security issue_tracker: https://github.com/MicroClub-USTHB/M-Security/issues @@ -15,7 +15,7 @@ topics: environment: sdk: ^3.10.8 - flutter: '>=3.3.0' + flutter: '>=3.38.9' dependencies: collection: ^1.18.0 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 05f8c97..8d5be54 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -762,7 +762,7 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "m_security" -version = "0.3.5" +version = "0.3.6" dependencies = [ "aes-gcm", "argon2", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 5807331..fe91b64 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "m_security" -version = "0.3.5" +version = "0.3.6" edition = "2021" [lib] From fe81a46171c699b5c05e77a8bb0a7abf80d384ed Mon Sep 17 00:00:00 2001 From: Adel-Ayoub Date: Wed, 5 Aug 2026 01:32:06 +0100 Subject: [PATCH 2/2] build(ci): print which files leave the worktree dirty --- .github/workflows/ci.yml | 2 ++ tool/publication.dart | 33 +++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e95de04..e15bdd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,8 @@ jobs: - run: flutter pub get - name: Assemble the publish payload run: dart run tool/publication.dart --out "$RUNNER_TEMP/payload" + # --min-tests is the number of cases containment_test.dart declares, so it + # has to move with that file. It is a floor, not an assertion of equality. - name: Run the containment subset from a clean consumer run: > xvfb-run -a dart run tool/packaged_consumer.dart diff --git a/tool/publication.dart b/tool/publication.dart index 9823e44..71c4d26 100644 --- a/tool/publication.dart +++ b/tool/publication.dart @@ -32,6 +32,11 @@ const String _usage = 'usage: dart run tool/publication.dart --out '; /// constraint is deliberately a single version: the committed bindings are /// generated by one exact bridge release and refuse to initialise against any /// other, so pub's advice to widen it would ship a package that cannot start. +/// +/// That second entry matches on the dependency name, not its version, so a +/// bridge bump keeps it tolerated. It goes stale only if pub rewords the +/// complaint or the dependency is renamed, and either way the run fails rather +/// than waving something through. const List _toleratedComplaints = [ 'modified in git', '"flutter_rust_bridge" should allow more than one version', @@ -107,6 +112,24 @@ Future main(List args) async { final archiveDigest = sha256.convert(archive.readAsBytesSync()).toString(); await _verifyArchive(archive, entries); + // A clean CI checkout has reported a dirty worktree while pub reported no + // modified file, so something exists on the runner that pub cannot see. + // Either an untracked path, or a tracked one `.pubignore` keeps out of the + // payload, since pub's own check only looks at files it would publish. + // Naming it beats inferring it. + // + // NOTE: on a public repository the job log is world-readable, so this prints + // paths somewhere more visible than the retained report. It is a path list + // from a CI checkout, which holds nothing private, but do not widen it to + // file contents. + final porcelain = await _capture('git', ['status', '--porcelain'], repoRoot); + if (porcelain.isNotEmpty) { + stderr.writeln('publication: worktree is not clean:'); + for (final line in porcelain.split('\n')) { + stderr.writeln(' $line'); + } + } + final report = { 'revision': await _capture('git', ['rev-parse', 'HEAD'], repoRoot), 'flutter': (await _capture( @@ -117,12 +140,10 @@ Future main(List args) async { 'dart': await _capture('dart', ['--version'], repoRoot), 'rustc': await _capture('rustc', ['--version'], repoRoot), // The revision above names a commit; these two say how far the bytes that - // were hashed have drifted from it. - 'worktree_clean': (await _capture( - 'git', - ['status', '--porcelain'], - repoRoot, - )).isEmpty, + // were hashed have drifted from it. The paths themselves stay on stderr so + // this stays a fixed-shape record rather than growing a list whose length + // depends on the machine. + 'worktree_clean': porcelain.isEmpty, 'file_count': entries.length, 'uncommitted_count': uncommitted.length, 'total_bytes': entries.fold(0, (sum, e) => sum + e.size),