zeroize 0.2.0
zeroize: ^0.2.0 copied to clipboard
DSE-resistant secret memory zeroing, multi-pass overwrite patterns, constant-time primitives, RAII scopes, secure containers, isolate-safe transfer. Pure Dart, no FFI, meta-only.
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
Unreleased #
0.2.0 — 2026-10-08 #
Added #
Isolate transfer
SecretTransfer— a sendable handle for secret bytes that must cross an isolate boundary.SecretBytescannot be sent: it holds aFinalizertoken and a non-sendable buffer, soIsolate.runandSendPortreject it. Previously the only portable option was a bareUint8Listwiped by hand on both sides.SecretTransfer.fromBytes(Uint8List)— copies into the transferable buffer and zeroes the source immediately.materializeSecret()— moves into a newSecretBytes, zeroing the materialized buffer in afinally.materializeBytes()— moves into a bareUint8Listfor APIs that require one (PointyCastle,package:cryptography). Caller owns and must wipe it.moveInto(SecretBytes)— moves directly into an existing container; throwsZeroizeContractErrorif the target is too small, and wipes the transferred buffer either way.length,isConsumed.
SecretBytes.intoTransfer()— moves this container's bytes into a transfer and zeroes/disposes the source, so exactly one live copy exists afterwards.
Streaming and container append
SecretBuffer.fromStream(Stream<List<int>>, {initialCapacity, pattern})— accumulates a stream chunk by chunk. Peak secret memory is one internal allocation plus the chunk currently being appended; the stream is never materialised in full a second time. Empty chunks are skipped. The returned buffer is not sealed — the caller still owns it.SecretBuffer.fromStreamdisposes the partial buffer if the stream emits an error before rethrowing, so a stream that fails mid-flight does not leave partially accumulated key material live in the heap.SecretBuffer.addSecretBytes(SecretBytes)— appends another container's bytes directly into the buffer, allocating no intermediate list. Bytes are read throughSecretBytes.use, so no reference to the source's backing buffer outlives the copy. The source is not consumed or disposed, and because the bytes are copied, mutating the source afterwards does not change what was appended.
Leak tracking covers SecretBuffer
ZeroizeConfig's debug tracker was wired into SecretBytes only. A
SecretBuffer that was allocated and dropped without being disposed or sealed
was invisible to debugAssertNoLeaks(), which reported 0 and passed.
SecretBufferis now tracked. It stops counting when eitherseal()ordispose()runs — both wipe the backing allocation, and the counter tracks live secret material, not live objects.ZeroizeConfig.liveSecretBytesCountandliveSecretBufferCountexpose the per-type figures.ZeroizeConfig.liveSecretCountis now the sum of both. The name is unchanged; the scope widened. Code assertingliveSecretCount == 0will now also catch a leakedSecretBuffer— which is the point, but is a behaviour change for anyone relying on the old SecretBytes-only meaning.debugAssertNoLeaks()now names which container type leaked, instead of reporting a bare total.- This makes
SecretBuffer.fromStream's wipe-on-stream-error path observable, and it is now covered by a regression test that fails if thatdispose()is removed.
Counters remain inside assert() closures, so this is free in release/AOT.
Notes #
SecretTransferis not a memory-erasure guarantee. The buffer insideTransferableTypedDatais runtime-managed and cannot be overwritten from Dart; the GC may still copy anyUint8Listbefore it is zeroed; and pure Dart has nomlock. These are the same limits as the rest of the package and are stated in the class documentation.- Single-consumption is per isolate copy, not global. Isolates do not share object state, so sending one transfer to two isolates yields two consumable copies. Send a transfer to exactly one receiver.
- Zero dependencies added.
package:metaremains the only runtime dependency and nodart:ffiordart:iowas introduced.
0.1.0 — 2026-09-23 #
Added #
Core Zeroing Engine
secureZero(Uint8List)— multi-pass secure zeroing withZeroizePatternselectionsecureZeroIntList(List<int>)— zeroing for NTT polynomial coefficient arrayssecureZeroRange(Uint8List, offset, count)— sub-region zeroing viaUint8List.sublistViewZeroizePatternenum —zero,ones,twoPass(default),dod,pseudoRandom,gutmann7
DSE Guard
_dseOpaqueSink— mutable package-level variable that defeats Dead Store EliminationdseOpaqueRead(List<int>, idx)—@pragma('vm:never-inline')read after overwrite passdseOpaqueReadFold(List<int>, idx)— read + 32-bit rotate-left mixing between passesdseObserve(int)— forces an intermediate value to be observable- All DSE-guard values kept within 32-bit Smi range to avoid Mint/BigInt promotion
Containers
-
SecretBytes— primary secret container- Factories:
fromList,fromUint8List,ofLength,generate - Access:
use(fn),mutate(fn)(never exposes backing buffer directly) - Operations:
xorWith,fill,subrange,concat - CT comparison:
timingSafeEquals,timingSafeEqualsBytes Finalizer<_FinalizerToken>for GC-triggered backstop zeroingZeroizeConfig.trackAllocate/trackDisposefor debug leak detection
- Factories:
-
SecretIntList—List<int>container for NTT polynomial arrays- Factories:
ofLength,fromList - Access:
[],[]=,use(fn),mutate(fn) Finalizerfor GC-triggered backstop zeroing
- Factories:
-
SecretBuffer— incremental secret accumulatoraddByte,addBytes,addList,addFill- Internal growth zeroes the old
Uint8Listallocation before release seal()atomically transfers toSecretBytesand disposes buffer
-
SecretBox<T>— generic opaque wrapper with caller-supplied zero callback -
PasswordInput— DartString-aware password handlertoUtf8SecretBytes()— UTF-8 byte representation (zeroable)toCodePointsBytes()— big-endian 32-bit code-point encoding (zeroable)- Documents the byte-collection alternative via
SecretBuffer
Lifecycle Management
Zeroizablemixin —zeroize(),useAndZeroize(fn),useAndZeroizeAsync(fn)ZeroizeScope— RAII scope with LIFO disposal ordertrack<T extends Zeroizable>(item)— registers for zeroingrun(fn)/runAsync(fn)— factory runners with guaranteed disposal
withZeroizedBytes(Uint8List, fn)/withZeroizedBytesAsync— scoped raw buffer zeroingwithZeroized(List<Zeroizable>, fn)/withZeroizedAsync— LIFO multi-secret guard
Constant-Time Primitives
- Byte comparison:
ctCompareBytes,ctEquals,ctCompareIntLists,ctEqualsIntLists - Tag verification:
ctVerifyTag— with length-mismatch dummy scan - Integer selection:
ctSelect,ctSelectByte - Buffer ops:
ctConditionalCopy,ctConditionalSwap - Predicates:
ctLessThan,ctGreaterThan,ctLessOrEqual,ctGreaterOrEqual,ctIntEquals,ctIsZero,ctIsNonZero - Arithmetic:
ctAbs,ctClamp - Modular:
ctReduceOnce,ctLiftToPositive,ctConditionalAdd,ctConditionalSub
Extensions
Uint8ListZeroize—.secureZeroize(),.xorWith(),.isAllZero,.toHexString()ListIntZeroize—.secureZeroize()
Configuration & Annotations
ZeroizeConfig—defaultPattern,setDefaultPattern,liveSecretCount,totalAllocated,debugAssertNoLeaks()@sensitive/@constantTimeannotationsZeroizeDisposedError/ZeroizeContractError— typed error hierarchy
Utilities
PseudoRng— Xorshift32 PRNG for overwrite pattern diversity (non-cryptographic)- 60+ unit tests across all components
Documentation
doc/ARCHITECTURE.md— layer-by-layer design rationaledoc/SECURITY_MODEL.md— threat model, mitigations, and known limitationsdoc/FEATURES.md— complete feature cataloguedoc/ROADMAP.md— planned versions and future directionsdoc/API_GUIDE.md— usage guide with realistic examplesdoc/INTEGRATION.md— integration patterns forpqcrypto,pqforge,pqtransportdoc/CONTRIBUTING.md— contribution process and safety rules