decryptAsync method
- Uint8List recipientSecretKey,
- PqEnvelope envelope, {
- Uint8List? recipientKexSecretKey,
- String? recipientKeyId,
- PqForgeAeadEngine? engine,
- Uint8List? aad,
- 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',
);
}