decryptAsync method

Future<Uint8List> decryptAsync(
  1. Uint8List recipientSecretKey,
  2. PqEnvelope envelope, {
  3. Uint8List? recipientKexSecretKey,
  4. String? recipientKeyId,
  5. PqForgeAeadEngine? engine,
  6. Uint8List? aad,
  7. Uint8List? signerPublicKey,
})

Decrypts a one-shot envelope on engine, auto-detecting hybrid envelopes (hybridKex marker), a non-default AEAD suite (aeadSuite marker — the engine is rebuilt on the same provider to match), and multi-recipient envelopes (recipients[] entries).

Hybrid envelopes require recipientKexSecretKey (the raw 32-byte X25519 secret matching the public key the sender encrypted to); a missing key fails with a descriptive PqForgeException before any AEAD work — unless the envelope carries recipients[] entries, in which case those are still tried. A supplied kex key is ignored for non-hybrid envelopes.

On a multi-recipient envelope the primary derivation is tried first and the wrap entries second; recipientKeyId (this key's id) routes straight to the matching entry and skips a doomed primary attempt when the envelope names a different primary.

Implementation

Future<Uint8List> decryptAsync(
  Uint8List recipientSecretKey,
  PqEnvelope envelope, {
  Uint8List? recipientKexSecretKey,
  String? recipientKeyId,
  PqForgeAeadEngine? engine,
  Uint8List? aad,
  Uint8List? signerPublicKey,
}) async {
  final requested = engine ?? _defaultEngine();
  PqFipsMode.requireApprovedSuite(requested.cipherSuite);
  final recordedSuite = PqAeadSuite.of(envelope.metadata);
  PqFipsMode.requireApprovedSuite(recordedSuite);
  final aead = requested.cipherSuite == recordedSuite
      ? requested
      : aeadEngineForProvider(requested.provider, cipherSuite: recordedSuite);
  verifyEnvelopeForOpen(envelope, aad: aad, signerPublicKey: signerPublicKey);

  final sharedSecret = PqKemPrimitives.decapsulate(
    envelope.kemAlgorithm,
    recipientSecretKey,
    envelope.kemCiphertext,
  );
  final hasEntries = PqMultiRecipient.hasEntries(envelope.metadata);

  // Primary derivation. On a multi-recipient envelope a failure here (e.g.
  // a hybrid primary but no kex key — we may simply not BE the primary) is
  // deferred so the wrap entries still get their chance.
  Uint8List? primaryDemKey;
  PqForgeException? primaryFailure;
  try {
    if (PqHybridKemDem.isHybrid(envelope.metadata)) {
      if (recipientKexSecretKey == null) {
        throw const PqForgeException(
          'This envelope uses hybrid ML-KEM + X25519 encryption; the '
          'recipient X25519 secret key is required to decrypt it',
        );
      }
      primaryDemKey = await PqHybridKemDem.demKeyForOpen(
        profile: envelope.profile,
        kemSharedSecret: sharedSecret,
        kemCiphertext: envelope.kemCiphertext,
        metadata: envelope.metadata,
        recipientKexSecretKey: recipientKexSecretKey,
      );
    } else {
      primaryDemKey = PqForge.deriveDemKey(
        envelope.profile,
        sharedSecret,
        envelope.kemCiphertext,
      );
    }
  } on PqForgeException catch (failure) {
    if (!hasEntries) rethrow;
    primaryFailure = failure;
  }

  Future<Uint8List> open(Uint8List demKey) => aead.open(
    key: demKey,
    nonce: envelope.nonce,
    cipherTextWithTag: envelope.payload,
    aad: aad ?? envelope.kemCiphertext,
  );

  if (!hasEntries) return open(primaryDemKey!);

  // recipients[] present: trial-open, primary first unless the metadata
  // names a different primary key id than ours.
  final namedPrimary = envelope.metadata[PqMultiRecipient.primaryKeyIdKey];
  final preferEntries =
      recipientKeyId != null &&
      namedPrimary is String &&
      namedPrimary != recipientKeyId;
  Future<Uint8List?> tryPrimary() async {
    if (primaryDemKey == null) return null;
    try {
      return await open(primaryDemKey);
    } on PqForgeAuthTagException {
      return null;
    }
  }

  Future<Uint8List?> tryEntries() async {
    final demKey = await PqMultiRecipient.unwrapDemKey(
      profile: envelope.profile,
      metadata: envelope.metadata,
      recipientSecretKey: recipientSecretKey,
      recipientKexSecretKey: recipientKexSecretKey,
      recipientKeyId: recipientKeyId,
    );
    return demKey == null ? null : open(demKey);
  }

  final plaintext = preferEntries
      ? (await tryEntries() ?? await tryPrimary())
      : (await tryPrimary() ?? await tryEntries());
  if (plaintext != null) return plaintext;
  throw PqForgeException(
    'This multi-recipient envelope is not addressed to the supplied key: '
    'the primary derivation '
    '${primaryFailure == null ? 'failed authentication' : 'was unavailable (${primaryFailure.message})'} '
    'and no recipients[] entry unwrapped with it',
  );
}