sendEnvelope method
Encrypts payload to to's key package and stores it for delivery,
addressed through appNamespace.
Both this client's and the recipient's enrollments must be authorized
for appNamespace — the atServer enforces this on write (here) and on
read/sync (recipient side) respectively.
Throws StateError if to advertises no key with a mutually-supported
algorithm.
Implementation
Future<void> sendEnvelope(
KeyPackage to,
String appNamespace,
Map<String, dynamic> payload,
) async {
final PackageKey? recipientKey = to.bestKeyFor(SecretSharingAlgos.keyAlgos);
if (recipientKey == null) {
throw StateError(
'Key package ${to.enrollmentId}/${to.apkamId} advertises no key '
'with a supported algorithm (supported: '
'${SecretSharingAlgos.keyAlgos})');
}
// X-Wing HPKE: pqSeal encapsulates to the recipient's published key and
// wraps the payload (AEAD over an HKDF key schedule) into one envelope —
// nothing secret travels except that sealed envelope.
final Uint8List sealed = await pqSeal(
XWingPureDartAlgo.instance,
base64Decode(recipientKey.pub),
Uint8List.fromList(utf8.encode(jsonEncode(payload))),
info: _sealInfo,
);
final envelope = SecretEnvelope(
fromKpid: kpid,
fromEnrollmentId: enrollmentId,
toKpid: recipientKey.kid,
suite: SecretSharingAlgos.xWingHpke,
kid: recipientKey.kid,
sealed: base64Encode(sealed),
);
// pqSeal's AEAD authenticates the payload; the APKAM signature over the
// whole envelope additionally authenticates the SENDER (receivers still
// verify before decrypting).
final String signedJson = await wrapAndSignAndJsonEncode(envelope.toJson());
final atKey = AtKey()
..key = '${Uuid().v4()}.${recipientKey.kid}.$envelopeKeyMarker'
..namespace = appNamespace
..sharedBy = atClient.getCurrentAtSign()
..metadata.ttl = envelopeTtl.inMilliseconds;
// shouldEncrypt=false: the value is already end-to-end encrypted to the
// recipient; self-key encryption would only obscure that the payload is
// our own ciphertext. The value is raw JSON (never whole-value base64) so
// that pre-fix readers' legacy decrypt fallback also returns it untouched.
await atClient.put(
atKey,
signedJson,
putRequestOptions: PutRequestOptions()..shouldEncrypt = false,
);
logger.info('Stored secret envelope $atKey for kpid ${recipientKey.kid}');
if (sendWakeUpNotification) {
await _sendWakeUp(atKey, appNamespace);
}
}