encryptStream method

Future<PqStreamingStats> encryptStream({
  1. required Uint8List recipientPublicKey,
  2. required Stream<List<int>> source,
  3. required File output,
  4. required PqForgeProfile profile,
  5. Uint8List? recipientKexPublicKey,
  6. List<PqRecipientSpec> additionalRecipients = const [],
  7. String? recipientKeyId,
  8. Uint8List? aad,
  9. Map<String, Object?> metadata = const {},
  10. Uint8List? signerSecretKey,
  11. PqSignatureAlgorithm? signatureAlgorithm,
  12. String? signerKeyId,
  13. int frameSize = PqStreamingEnvelope.defaultFrameSize,
})

Streams arbitrary plaintext source bytes into a .pqfs container at output — same container, framing, and signing as encryptFile, but the input never has to exist as a file. This is what lets pack seal a whole folder without ever spooling plaintext to disk.

The total length need not be known up front: a one-frame lookahead decides which frame is final. Peak memory ≈ 2 × frameSize.

Implementation

Future<PqStreamingStats> encryptStream({
  required Uint8List recipientPublicKey,
  required Stream<List<int>> source,
  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,
  );
  return _writeContainer(context, output, (writer) async {
    // (No read-ahead here: the source is already an async stream, so the
    // producer naturally runs ahead while a frame is awaited.)
    final frame = Uint8List(frameSize);
    var filled = 0;
    Uint8List? readyFrame; // full frame held until finality is known
    await for (final chunk in source) {
      var offset = 0;
      while (offset < chunk.length) {
        final n = (frameSize - filled) < (chunk.length - offset)
            ? frameSize - filled
            : chunk.length - offset;
        frame.setRange(filled, filled + n, chunk, offset);
        filled += n;
        offset += n;
        if (filled == frameSize) {
          if (readyFrame != null) {
            await writer.add(readyFrame, isFinal: false);
          }
          readyFrame = Uint8List.fromList(frame);
          filled = 0;
        }
      }
    }
    if (readyFrame != null) {
      if (filled == 0) {
        await writer.add(readyFrame, isFinal: true);
      } else {
        await writer.add(readyFrame, isFinal: false);
        await writer.add(
          Uint8List.sublistView(frame, 0, filled),
          isFinal: true,
        );
      }
    } else {
      // Covers both a short (<1 frame) stream and a fully empty one.
      await writer.add(
        Uint8List.sublistView(frame, 0, filled),
        isFinal: true,
      );
    }
  });
}