encodeEncrypted<T> static method

Future<EncryptedEncodedContent> encodeEncrypted<T>(
  1. T content,
  2. XMTPCodec codec, {
  3. String? filename,
})

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,
  );
}