encodeEncrypted<T> static method
Spec-compliant counterpart to load / _fetchDecryptAndParse.
Wraps content in an EncodedContent envelope using codec, serializes
the envelope to protobuf bytes, then AES-256-GCM encrypts those bytes with
a fresh random secret/salt/nonce. The output is the exact wire shape
produced by xmtp-android RemoteAttachmentCodec.encodeEncrypted<T>,
xmtp-ios RemoteAttachment.encodeEncrypted, and xmtp-js
RemoteAttachmentCodec.encodeEncrypted<T> — interoperable with every
spec-compliant XMTP client.
Pre-1.0.5 we encrypted the raw inner payload directly; that decrypted
into bare bytes, not into a protobuf, and native SDKs threw on parse
(their EncodedContent.parseFrom / EncodedContent(serializedData:) /
proto.EncodedContent.decode paths have no fallback). Callers should
always use this helper — _encrypt is private on purpose.
filename populates the optional RemoteAttachmentContent.filename
envelope parameter that travels alongside the URL — purely informational
for receivers that want to display a name before downloading the payload.
The authoritative filename lives inside the encrypted envelope.
Implementation
static Future<EncryptedEncodedContent> encodeEncrypted<T>(
T content,
XMTPCodec codec, {
String? filename,
}) async {
final encoded = await codec.encode(content);
final innerContent = encoded['content'];
final Uint8List innerBytes;
if (innerContent is Uint8List) {
innerBytes = innerContent;
} else if (innerContent is List<int>) {
innerBytes = Uint8List.fromList(innerContent);
} else {
throw FormatException(
'codec.encode returned non-bytes "content": ${innerContent.runtimeType}',
);
}
final envelope = EncodedContent()
..type = (ContentTypeId()
..authorityId = codec.authorityId
..typeId = codec.typeId
..versionMajor = codec.versionMajor
..versionMinor = codec.versionMinor)
..content = innerBytes;
final innerParams = encoded['parameters'];
if (innerParams is Map) {
innerParams.forEach((k, v) {
envelope.parameters[k.toString()] = v.toString();
});
}
final serialized = Uint8List.fromList(envelope.writeToBuffer());
final random = Random.secure();
final secret = Uint8List(32);
final salt = Uint8List(32);
final nonce = Uint8List(12);
for (var i = 0; i < 32; i++) {
secret[i] = random.nextInt(256);
salt[i] = random.nextInt(256);
}
for (var i = 0; i < 12; i++) {
nonce[i] = random.nextInt(256);
}
final ciphertext = await _encrypt(
secret: secret,
salt: salt,
nonce: nonce,
plaintext: serialized,
);
final digestBytes = SHA256Digest().process(ciphertext);
final contentDigest =
digestBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
return EncryptedEncodedContent(
contentDigest: contentDigest,
secret: secret,
salt: salt,
nonce: nonce,
payload: ciphertext,
contentLength: ciphertext.length,
filename: filename,
);
}