encryptFile method
Future<PqStreamingStats>
encryptFile({
- required Uint8List recipientPublicKey,
- required File input,
- required File output,
- required PqForgeProfile profile,
- Uint8List? recipientKexPublicKey,
- List<
PqRecipientSpec> additionalRecipients = const [], - String? recipientKeyId,
- Uint8List? aad,
- Map<
String, Object?> metadata = const {}, - Uint8List? signerSecretKey,
- PqSignatureAlgorithm? signatureAlgorithm,
- String? signerKeyId,
- int frameSize = PqStreamingEnvelope.defaultFrameSize,
Streams input into a .pqfs container at output.
When signerSecretKey is supplied the header (never the payload) is signed
with preHash:true, so signing is O(1) in file size. When
recipientKexPublicKey (a raw 32-byte X25519 public key) is supplied the
container is hybrid: the DEM key combines the ML-KEM shared secret
with an ephemeral X25519 exchange (see PqHybridKemDem). Each
additionalRecipients entry wraps the same DEM key to one more key
(see PqMultiRecipient) — the payload is still sealed exactly once. On
failure the partial output is removed.
Implementation
Future<PqStreamingStats> encryptFile({
required Uint8List recipientPublicKey,
required File input,
required File output,
required PqForgeProfile profile,
Uint8List? recipientKexPublicKey,
List<PqRecipientSpec> additionalRecipients = const [],
String? recipientKeyId,
Uint8List? aad,
Map<String, Object?> metadata = const {},
Uint8List? signerSecretKey,
PqSignatureAlgorithm? signatureAlgorithm,
String? signerKeyId,
int frameSize = PqStreamingEnvelope.defaultFrameSize,
}) async {
final context = await _prepareEncrypt(
recipientPublicKey: recipientPublicKey,
recipientKexPublicKey: recipientKexPublicKey,
additionalRecipients: additionalRecipients,
recipientKeyId: recipientKeyId,
profile: profile,
aad: aad,
metadata: metadata,
signerSecretKey: signerSecretKey,
signatureAlgorithm: signatureAlgorithm,
signerKeyId: signerKeyId,
frameSize: frameSize,
);
final source = await input.open();
try {
return await _writeContainer(context, output, (writer) async {
final total = await source.length();
if (total == 0) {
// An empty input still gets one (empty) final frame so the reader
// sees a terminator rather than a truncated stream.
await writer.add(Uint8List(0), isFinal: true);
return;
}
// Double-buffered read-ahead (R9): the next frame is read from the
// source while the current one is sealed and written. Input and
// output are separate file handles, so the disk read genuinely
// overlaps the AEAD + write; peak memory grows by exactly one frame.
var current = Uint8List(frameSize);
var next = Uint8List(frameSize);
var read = await source.readInto(current);
var currentLen = read;
while (currentLen > 0) {
final isFinal = read >= total;
final pending = isFinal ? null : source.readInto(next);
try {
await writer.add(
Uint8List.sublistView(current, 0, currentLen),
isFinal: isFinal,
);
} on Object {
// Never leave the prefetch dangling: an unawaited failing future
// would surface as an unhandled async error during cleanup.
await _drainQuietly(pending);
rethrow;
}
if (pending == null) return;
final n = await pending;
if (n <= 0) break; // input shrank; the reader detects truncation
read += n;
currentLen = n;
final spare = current;
current = next;
next = spare;
}
});
} finally {
await source.close();
}
}