From a89e85ea79219118c83895627a7736b7dba5c314 Mon Sep 17 00:00:00 2001 From: doylet Date: Mon, 15 Dec 2025 17:00:55 +1100 Subject: [PATCH] Add blinded community session encrypt functions --- include/session/session_protocol.h | 34 ++++ include/session/session_protocol.hpp | 19 +++ src/session_protocol.cpp | 201 ++++++++++++++++++---- tests/test_session_protocol.cpp | 241 ++++++++++++++++++++++----- 4 files changed, 417 insertions(+), 78 deletions(-) diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index 34efb16f..8e1061ac 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -139,8 +139,16 @@ typedef enum SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS { // See session::Pro typedef enum SESSION_PROTOCOL_DESTINATION_TYPE { // See session::DestinationType SESSION_PROTOCOL_DESTINATION_TYPE_SYNC_OR_1O1, SESSION_PROTOCOL_DESTINATION_TYPE_GROUP, + + // Old-style community messages that is content encrypted using the blinding protocol or + // plaintext content respectively (inbox vs non-inbox). SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY_INBOX, SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY, + + // New-style community messages that are sent as envelopes, encrypted using the blinding + // protocol or plaintext content respectively (inbox vs non-inbox). + SESSION_PROTOCOL_DESTINATION_TYPE_ENVELOPE_COMMUNITY_INBOX, + SESSION_PROTOCOL_DESTINATION_TYPE_ENVELOPE_COMMUNITY, } SESSION_PROTOCOL_DESTINATION_TYPE; typedef struct session_protocol_destination session_protocol_destination; @@ -709,6 +717,32 @@ session_protocol_encoded_for_destination session_protocol_encode_for_destination LIBSESSION_EXPORT void session_protocol_encode_for_destination_free( session_protocol_encoded_for_destination* encrypt); +/// API: session_protocol/decode_for_community_inbox +/// +/// Given a blinded encrypted content or envelope payload extract the content and any associated pro +/// metadata if there was any in the message. +/// +/// This function is the same as calling `decrypt_from_blinded_recipient` to decrypt the payload and +/// then decode with `decode_for_community`. See those functions for more information. On failure +/// this function throws as per `decrypt_from_blinded_recipient` +LIBSESSION_EXPORT session_protocol_decoded_community_message +session_protocol_decode_for_community_inbox( + const unsigned char* ed25519_privkey, + size_t ed25519_privkey_len, + const unsigned char* community_pubkey, + size_t community_pubkey_len, + const unsigned char* sender_id, + size_t sender_id_len, + const unsigned char* recipient_id, + size_t recipient_id_len, + const unsigned char* ciphertext, + size_t ciphertext_len, + uint64_t unix_ts_ms, + OPTIONAL const void* pro_backend_pubkey, + OPTIONAL size_t pro_backend_pubkey_len, + OPTIONAL char* error, + size_t error_len); + /// API: session_protocol/session_protocol_decode_envelope /// /// Given an envelope payload (i.e.: protobuf encoded stream of `WebsocketRequestMessage` which diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 2ef515d5..bd5dcc18 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -213,6 +213,8 @@ enum class DestinationType { Group = SESSION_PROTOCOL_DESTINATION_TYPE_GROUP, CommunityInbox = SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY_INBOX, Community = SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY, + EnvelopeCommunityInbox = SESSION_PROTOCOL_DESTINATION_TYPE_ENVELOPE_COMMUNITY_INBOX, + EnvelopeCommunity = SESSION_PROTOCOL_DESTINATION_TYPE_ENVELOPE_COMMUNITY, }; struct Destination { @@ -606,6 +608,23 @@ DecodedEnvelope decode_envelope( std::span envelope_payload, const array_uc32& pro_backend_pubkey); +/// API: session_protocol/decode_for_community_inbox +/// +/// Given a blinded encrypted content or envelope payload extract the plaintext to the content and +/// any associated pro metadata if there was any in the message. +/// +/// This function is the same as calling `decrypt_from_blinded_recipient` to decrypt the payload and +/// then decode with `decode_for_community`. See those functions for more information. On failure +/// this function throws as per `decrypt_from_blinded_recipient` +DecodedCommunityMessage decode_for_community_inbox( + std::span ed25519_privkey, + std::span community_pubkey, + std::span sender_id, + std::span recipient_id, + std::span ciphertext, + std::chrono::sys_time unix_ts, + const array_uc32& pro_backend_pubkey); + /// API: session_protocol/decode_for_community /// /// Given an unencrypted content or envelope payload extract the plaintext to the content and any diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index aeabdee8..4d8bca23 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -445,6 +445,8 @@ static EncryptedForDestinationInternal encode_for_destination_internal( bool is_1o1 = dest_type == DestinationType::SyncOr1o1; bool is_community_inbox = dest_type == DestinationType::CommunityInbox; bool is_community = dest_type == DestinationType::Community; + bool is_community_envelope = dest_type == DestinationType::EnvelopeCommunityInbox || + dest_type == DestinationType::EnvelopeCommunityInbox; if (!is_community) { assert(ed25519_privkey.size() == crypto_sign_ed25519_SECRETKEYBYTES || ed25519_privkey.size() == crypto_sign_ed25519_SEEDBYTES); @@ -474,8 +476,12 @@ static EncryptedForDestinationInternal encode_for_destination_internal( EncryptedForDestinationInternal result = {}; switch (dest_type) { - case DestinationType::Group: /*FALLTHRU*/ + case DestinationType::EnvelopeCommunity: /*FALLTHRU*/ + case DestinationType::EnvelopeCommunityInbox: /*FALLTHRU*/ + case DestinationType::Group: /*FALLTHRU*/ case DestinationType::SyncOr1o1: { + assert(is_group || is_1o1 || is_community_envelope); + if (is_group && dest_group_ed25519_pubkey[0] != static_cast(SessionIDPrefix::group)) { // Legacy groups which have a 05 prefixed key @@ -484,14 +490,23 @@ static EncryptedForDestinationInternal encode_for_destination_internal( "no longer supported"}; } - // For Sync or 1o1 mesasges, we need to pad the contents to 160 bytes, see: - // https://github.com/session-foundation/session-desktop/blob/a04e62427034a6b6fee39dcff7dbabf0d0131b13/ts/session/crypto/BufferPadding.ts#L49 std::vector tmp_content_buffer; - if (is_1o1) { // Encrypt the padded output + if (is_1o1) { + // For Sync or 1o1 mesasges, we need to pad the contents to 160 bytes, see: + // https://github.com/session-foundation/session-desktop/blob/a04e62427034a6b6fee39dcff7dbabf0d0131b13/ts/session/crypto/BufferPadding.ts#L49 std::vector padded_payload = pad_message(content); + + // Encrypt the padded output tmp_content_buffer = encrypt_for_recipient_deterministic( ed25519_privkey, dest_recipient_pubkey, padded_payload); content = tmp_content_buffer; + } else if (dest_type == DestinationType::EnvelopeCommunityInbox) { + // For community messages we only pad the content if it's an inbox message (e.g. DMs + // require encryption in a community, so we pad to avoid leakage of metadata of the + // kind of message being sent being inferred from the size of the payload). + assert(is_community_envelope); + tmp_content_buffer = pad_message(content); + content = tmp_content_buffer; } // Create envelope @@ -499,9 +514,11 @@ static EncryptedForDestinationInternal encode_for_destination_internal( // https://github.com/session-foundation/session-ios/blob/82deef869d0f7389b799295817f42ad14f8a1316/SessionMessagingKit/Utilities/MessageWrapper.swift#L57 SessionProtos::Envelope envelope = {}; envelope.set_type( - is_1o1 ? SessionProtos::Envelope_Type_SESSION_MESSAGE - : SessionProtos::Envelope_Type_CLOSED_GROUP_MESSAGE); - envelope.set_sourcedevice(1); + (is_1o1 || is_community_envelope) + ? SessionProtos::Envelope_Type_SESSION_MESSAGE + : SessionProtos::Envelope_Type_CLOSED_GROUP_MESSAGE); + if (!is_community_envelope) + envelope.set_sourcedevice(1); envelope.set_timestamp(dest_sent_timestamp_ms.count()); envelope.set_content(content.data(), content.size()); @@ -540,7 +557,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( } else { result.ciphertext_cpp = std::move(ciphertext); } - } else { + } else if (is_1o1) { // 1o1, Wrap in websocket message WebSocketProtos::WebSocketMessage msg = {}; msg.set_type(WebSocketProtos::WebSocketMessage_Type::WebSocketMessage_Type_REQUEST); @@ -564,6 +581,35 @@ static EncryptedForDestinationInternal encode_for_destination_internal( result.ciphertext_cpp.data(), result.ciphertext_cpp.size()); } assert(serialized); + } else { + assert(is_community_envelope); + if (dest_type == DestinationType::EnvelopeCommunityInbox) { + std::string bytes = envelope.SerializeAsString(); + std::vector ciphertext = encrypt_for_blinded_recipient( + ed25519_privkey, + dest_community_inbox_server_pubkey, + dest_recipient_pubkey, // recipient blinded pubkey + to_span(bytes)); + + if (use_malloc == UseMalloc::Yes) { + result.ciphertext_c = + span_u8_copy_or_throw(ciphertext.data(), ciphertext.size()); + } else { + result.ciphertext_cpp = std::move(ciphertext); + } + } else { + [[maybe_unused]] bool serialized = false; + if (use_malloc == UseMalloc::Yes) { + result.ciphertext_c = span_u8_alloc_or_throw(envelope.ByteSizeLong()); + serialized = envelope.SerializeToArray( + result.ciphertext_c.data, result.ciphertext_c.size); + } else { + result.ciphertext_cpp.resize(envelope.ByteSizeLong()); + envelope.SerializeToArray( + result.ciphertext_cpp.data(), result.ciphertext_cpp.size()); + } + assert(serialized); + } } } break; @@ -573,19 +619,19 @@ static EncryptedForDestinationInternal encode_for_destination_internal( std::vector tmp_content_buffer; // Sign the message with the Session Pro key if given and then pad the message (both - // community message types require it) + // community message types require it for old-style content messages) // https://github.com/session-foundation/session-ios/blob/82deef869d0f7389b799295817f42ad14f8a1316/SessionMessagingKit/Sending%20%26%20Receiving/MessageSender.swift#L398 if (dest_pro_rotating_ed25519_privkey.size()) { // Key should be verified by the time we hit this branch assert(dest_pro_rotating_ed25519_privkey.size() == crypto_sign_ed25519_SECRETKEYBYTES); - // TODO: Sub-optimal, but we parse the content again to make sure it's valid. Sign - // the blob then, fill in the signature in-place as part of the transitioning of - // open groups messages to envelopes. As part of that, libsession is going to take - // responsibility of constructing community messages so that eventually all - // platforms switch over to envelopes and we can change the implementation across - // all platforms in one swoop and remove this. + // TODO: Sub-optimal, but we parse the content again to make sure it's valid. + // Sign the blob then, fill in the signature in-place as part of the + // transitioning of open groups messages to envelopes. As part of that, + // libsession is going to take responsibility of constructing community messages + // so that eventually all platforms switch over to envelopes and we can change + // the implementation across all platforms in one swoop and remove this. // // Parse the content blob SessionProtos::Content content_w_sig = {}; @@ -608,9 +654,9 @@ static EncryptedForDestinationInternal encode_for_destination_internal( dest_pro_rotating_ed25519_privkey.data()) == 0; assert(was_signed); - // Now assign the community specific pro signature field, reserialize it and we have - // to, yes, pad it again. This is all temporary wasted work whilst transitioning - // open groups. + // Now assign the community specific pro signature field, reserialize it and we + // have to, yes, pad it again. This is all temporary wasted work whilst + // transitioning open groups. content_w_sig.set_prosigforcommunitymessageonly(pro_sig.data(), pro_sig.size()); tmp_content_buffer.resize(content_w_sig.ByteSizeLong()); bool serialized = content_w_sig.SerializeToArray( @@ -624,10 +670,6 @@ static EncryptedForDestinationInternal encode_for_destination_internal( content = tmp_content_buffer; } - // TODO: We don't need to actually pad the community message since that's unencrypted, - // there's no need to make the message sizes uniform but we need it for backwards - // compat. We can remove this eventually, first step is to unify the clients. - if (is_community_inbox) { std::vector ciphertext = encrypt_for_blinded_recipient( ed25519_privkey, @@ -868,6 +910,8 @@ DecodedEnvelope decode_envelope( static_assert(sizeof(result.envelope.pro_sig) == crypto_sign_ed25519_BYTES); std::memcpy(result.envelope.pro_sig.data(), pro_sig.data(), pro_sig.size()); + // We only care about verifying the pro-signature if the content has pro data populated, + // otherwise we assume that it's a dummy signature for preventing metadata leakage. if (content.has_promessage()) { if (!content.sigtimestamp()) throw std::runtime_error{fmt::format( @@ -1007,9 +1051,10 @@ DecodedCommunityMessage decode_for_community( std::span unpadded_content = unpad_message(result.content_plaintext); SessionProtos::Content content = {}; if (!content.ParseFromArray(unpadded_content.data(), unpadded_content.size())) - throw std::runtime_error{ - "Decoding community message failed, could not interpret blob as content or " - "envelope"}; + throw std::runtime_error{fmt::format( + "Decoding community message failed, could not interpret blob as {}: {}b", + result.envelope ? "envelope" : "content", + unpadded_content.size())}; // Extract the pro signature from content if it was present if (content.has_prosigforcommunitymessageonly()) { @@ -1120,6 +1165,21 @@ DecodedCommunityMessage decode_for_community( return result; } +DecodedCommunityMessage decode_for_community_inbox( + std::span ed25519_privkey, + std::span community_pubkey, + std::span sender_id, + std::span recipient_id, + std::span ciphertext, + std::chrono::sys_time unix_ts, + const array_uc32& pro_backend_pubkey) { + auto [decrypted_blob, session_id] = decrypt_from_blinded_recipient( + ed25519_privkey, community_pubkey, sender_id, recipient_id, ciphertext); + DecodedCommunityMessage result = + decode_for_community(decrypted_blob, unix_ts, pro_backend_pubkey); + return result; +} + void make_blake2b32_hasher( crypto_generichash_blake2b_state* hasher, std::string_view personalization) { assert(personalization.data() == nullptr || @@ -1458,6 +1518,85 @@ LIBSESSION_C_API void session_protocol_encode_for_destination_free( } } +static session_protocol_decoded_community_message +session_protocol_decoded_community_message_from_cpp(const DecodedCommunityMessage& decoded) { + session_protocol_decoded_community_message result = {}; + result.has_envelope = decoded.envelope.has_value(); + if (result.has_envelope) + result.envelope = envelope_from_cpp(*decoded.envelope); + result.content_plaintext = session::span_u8_copy_or_throw( + decoded.content_plaintext.data(), decoded.content_plaintext.size()); + result.has_pro = decoded.pro.has_value(); + if (decoded.pro_sig) + std::memcpy(result.pro_sig.data, decoded.pro_sig->data(), decoded.pro_sig->max_size()); + if (decoded.pro) + result.pro = decoded_pro_from_cpp(*decoded.pro); + result.success = true; + return result; +} + +LIBSESSION_C_API session_protocol_decoded_community_message +session_protocol_decode_for_community_inbox( + const unsigned char* ed25519_privkey, + size_t ed25519_privkey_len, + const unsigned char* community_pubkey, + size_t community_pubkey_len, + const unsigned char* sender_id, + size_t sender_id_len, + const unsigned char* recipient_id, + size_t recipient_id_len, + const unsigned char* ciphertext, + size_t ciphertext_len, + uint64_t unix_ts_ms, + OPTIONAL const void* pro_backend_pubkey, + OPTIONAL size_t pro_backend_pubkey_len, + OPTIONAL char* error, + size_t error_len) { + std::span ed25519_privkey_span = {ed25519_privkey, ed25519_privkey_len}; + std::span community_pubkey_span = {community_pubkey, community_pubkey_len}; + std::span sender_id_span = {sender_id, sender_id_len}; + std::span recipient_id_span = {recipient_id, recipient_id_len}; + std::span ciphertext_span = {ciphertext, ciphertext_len}; + auto unix_ts = + std::chrono::sys_time(std::chrono::milliseconds(unix_ts_ms)); + + session_protocol_decoded_community_message result = {}; + array_uc32_from_ptr_result pro_backend_pubkey_cpp = + array_uc32_from_ptr(pro_backend_pubkey, pro_backend_pubkey_len); + if (!pro_backend_pubkey_cpp.success) { + result.error_len_incl_null_terminator = snprintf_clamped( + error, + error_len, + "Invalid pro_backend_pubkey: Key was " + "set but was not 32 bytes, was: %zu", + pro_backend_pubkey_len) + + 1; + return result; + } + + try { + DecodedCommunityMessage decoded = decode_for_community_inbox( + ed25519_privkey_span, + community_pubkey_span, + sender_id_span, + recipient_id_span, + ciphertext_span, + unix_ts, + pro_backend_pubkey_cpp.data); + result = session_protocol_decoded_community_message_from_cpp(decoded); + } catch (const std::exception& e) { + std::string error_cpp = e.what(); + result.error_len_incl_null_terminator = snprintf_clamped( + error, + error_len, + "%.*s", + static_cast(error_cpp.size()), + error_cpp.data()) + + 1; + } + return result; +} + LIBSESSION_C_API session_protocol_decoded_envelope session_protocol_decode_envelope( const session_protocol_decode_envelope_keys* keys, @@ -1597,17 +1736,7 @@ session_protocol_decoded_community_message session_protocol_decode_for_community try { DecodedCommunityMessage decoded = decode_for_community( content_or_envelope_payload_span, unix_ts, pro_backend_pubkey_cpp.data); - result.has_envelope = decoded.envelope.has_value(); - if (result.has_envelope) - result.envelope = envelope_from_cpp(*decoded.envelope); - result.content_plaintext = session::span_u8_copy_or_throw( - decoded.content_plaintext.data(), decoded.content_plaintext.size()); - result.has_pro = decoded.pro.has_value(); - if (decoded.pro_sig) - std::memcpy(result.pro_sig.data, decoded.pro_sig->data(), decoded.pro_sig->max_size()); - if (decoded.pro) - result.pro = decoded_pro_from_cpp(*decoded.pro); - result.success = true; + result = session_protocol_decoded_community_message_from_cpp(decoded); } catch (const std::exception& e) { std::string error_cpp = e.what(); result.success = false; diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 8b154c5f..4e65f11d 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -109,7 +109,7 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Do tests that require no setup - SECTION("Ensure get pro fetaures detects large message") { + SECTION("Ensure get pro features detects large message") { // Try a message below the size threshold { auto msg = std::string(SESSION_PROTOCOL_PRO_STANDARD_CHARACTER_LIMIT, 'a'); @@ -847,36 +847,37 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { REQUIRE(decoded.pro.status == SESSION_PROTOCOL_PRO_STATUS_VALID); } - SECTION("Encode/decode for community inbox (content message)") { - const auto community_seed = - "0123456789abcdef0123456789abcdeff00baadeadb33f000000000000000000"_hexbytes; - array_uc64 community_sk = {}; - array_uc32 community_pk = {}; - crypto_sign_ed25519_seed_keypair( - community_pk.data(), community_sk.data(), community_seed.data()); - - bytes32 session_blind15_sk0 = {}; - bytes33 session_blind15_pk0 = {}; - session_blind15_pk0.data[0] = 0x15; - session_blind15_key_pair( - keys.ed_sk0.data(), - community_pk.data(), - session_blind15_pk0.data + 1, - session_blind15_sk0.data); - - bytes32 session_blind15_sk1 = {}; - bytes33 session_blind15_pk1 = {}; - session_blind15_pk1.data[0] = 0x15; - session_blind15_key_pair( - keys.ed_sk1.data(), - community_pk.data(), - session_blind15_pk1.data + 1, - session_blind15_sk1.data); - - bytes33 recipient_pubkey = session_blind15_pk1; - bytes32 community_pubkey = {}; - std::memcpy(community_pubkey.data, community_pk.data(), community_pk.size()); + // NOTE: Setup some keys for community inbox testing + const auto community_seed = + "0123456789abcdef0123456789abcdeff00baadeadb33f000000000000000000"_hexbytes; + array_uc64 community_sk = {}; + array_uc32 community_pk = {}; + crypto_sign_ed25519_seed_keypair( + community_pk.data(), community_sk.data(), community_seed.data()); + + bytes32 session_blind15_sk0 = {}; + bytes33 session_blind15_pk0 = {}; + session_blind15_pk0.data[0] = 0x15; + session_blind15_key_pair( + keys.ed_sk0.data(), + community_pk.data(), + session_blind15_pk0.data + 1, + session_blind15_sk0.data); + + bytes32 session_blind15_sk1 = {}; + bytes33 session_blind15_pk1 = {}; + session_blind15_pk1.data[0] = 0x15; + session_blind15_key_pair( + keys.ed_sk1.data(), + community_pk.data(), + session_blind15_pk1.data + 1, + session_blind15_sk1.data); + + bytes33 recipient_pubkey = session_blind15_pk1; + bytes32 community_pubkey = {}; + std::memcpy(community_pubkey.data, community_pk.data(), community_pk.size()); + SECTION("Encode/decode for community inbox (content message)") { session_protocol_encoded_for_destination encoded = session_protocol_encode_for_community_inbox( protobuf_content.plaintext.data(), @@ -892,22 +893,178 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { sizeof(error)); scope_exit encoded_free{[&]() { session_protocol_encode_for_destination_free(&encoded); }}; - auto [decrypted_cipher, sender_id] = session::decrypt_from_blinded_recipient( - keys.ed_sk1, - community_pk, - {session_blind15_pk0.data, sizeof(session_blind15_pk0.data)}, - {session_blind15_pk1.data, sizeof(session_blind15_pk1.data)}, - {encoded.ciphertext.data, encoded.ciphertext.size}); + session_protocol_decoded_community_message decoded = + session_protocol_decode_for_community_inbox( + keys.ed_sk1.data(), + keys.ed_sk1.size(), + community_pk.data(), + community_pk.size(), + /*sender*/ session_blind15_pk0.data, + sizeof(session_blind15_pk0.data), + /*recipient*/ session_blind15_pk1.data, + sizeof(session_blind15_pk1.data), + encoded.ciphertext.data, + encoded.ciphertext.size, + timestamp_ms.time_since_epoch().count(), + pro_backend_ed_pk.data(), + pro_backend_ed_pk.size(), + error, + sizeof(error)); + INFO(error); + REQUIRE(decoded.error_len_incl_null_terminator == 0); + REQUIRE(decoded.success); + scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; + REQUIRE(!decoded.has_pro); + } - session_protocol_decoded_community_message decoded = session_protocol_decode_for_community( - decrypted_cipher.data(), - decrypted_cipher.size(), - timestamp_ms.time_since_epoch().count(), - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + SECTION("Encode/decode for community inbox (content message+pro)") { + session_protocol_encoded_for_destination encoded = + session_protocol_encode_for_community_inbox( + protobuf_content.plaintext.data(), + protobuf_content.plaintext.size(), + keys.ed_sk0.data(), + keys.ed_sk0.size(), + timestamp_ms.time_since_epoch().count(), + &recipient_pubkey, + &community_pubkey, + user_pro_ed_sk.data(), + user_pro_ed_sk.size(), + error, + sizeof(error)); + scope_exit encoded_free{[&]() { session_protocol_encode_for_destination_free(&encoded); }}; + + session_protocol_decoded_community_message decoded = + session_protocol_decode_for_community_inbox( + keys.ed_sk1.data(), + keys.ed_sk1.size(), + community_pk.data(), + community_pk.size(), + /*sender*/ session_blind15_pk0.data, + sizeof(session_blind15_pk0.data), + /*recipient*/ session_blind15_pk1.data, + sizeof(session_blind15_pk1.data), + encoded.ciphertext.data, + encoded.ciphertext.size, + timestamp_ms.time_since_epoch().count(), + pro_backend_ed_pk.data(), + pro_backend_ed_pk.size(), + error, + sizeof(error)); + INFO(error); + REQUIRE(decoded.error_len_incl_null_terminator == 0); + REQUIRE(decoded.success); + scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; + REQUIRE(decoded.has_pro); + REQUIRE(decoded.pro.status == SESSION_PROTOCOL_PRO_STATUS_VALID); + } + + SECTION("Encode/decode for community inbox (envelope)") { + // TODO: For these tests we call directly into the encode for destination function. The + // public helper-functions that wrap over this lower level function construct content + // community messages. This is the default behaviour before we transition to envelopes for + // community messages. + // + // To test envelope construction for communities we bypass that function as we need to set + // the type to Envelope community inbox. + session_protocol_destination dest = {}; + dest.type = SESSION_PROTOCOL_DESTINATION_TYPE_ENVELOPE_COMMUNITY_INBOX; + dest.sent_timestamp_ms = timestamp_ms.time_since_epoch().count(); + dest.recipient_pubkey = session_blind15_pk1; + dest.community_inbox_server_pubkey = community_pubkey; + + // Build content without pro attached + std::string plaintext; + { + SessionProtos::Content content = {}; + content.set_sigtimestamp(timestamp_ms.time_since_epoch().count()); + + SessionProtos::DataMessage* data = content.mutable_datamessage(); + data->set_body(std::string(data_body)); + plaintext = content.SerializeAsString(); + REQUIRE(plaintext.size() > data_body.size()); + } + + memset(error, 0, sizeof(error)); + session_protocol_encoded_for_destination encoded = session_protocol_encode_for_destination( + plaintext.data(), + plaintext.size(), + keys.ed_sk0.data(), + keys.ed_sk0.size(), + &dest, error, sizeof(error)); + scope_exit encoded_free{[&]() { session_protocol_encode_for_destination_free(&encoded); }}; + REQUIRE(encoded.success); + + memset(error, 0, sizeof(error)); + session_protocol_decoded_community_message decoded = + session_protocol_decode_for_community_inbox( + keys.ed_sk1.data(), + keys.ed_sk1.size(), + community_pk.data(), + community_pk.size(), + /*sender*/ session_blind15_pk0.data, + sizeof(session_blind15_pk0.data), + /*recipient*/ session_blind15_pk1.data, + sizeof(session_blind15_pk1.data), + encoded.ciphertext.data, + encoded.ciphertext.size, + timestamp_ms.time_since_epoch().count(), + nullptr, + 0, + error, + sizeof(error)); + INFO("ERROR: " << error << ", cipher was: " << encoded.ciphertext.size << "b"); + REQUIRE(decoded.error_len_incl_null_terminator == 0); + REQUIRE(decoded.success); scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; REQUIRE(!decoded.has_pro); } + + SECTION("Encode/decode for community inbox (envelope+pro)") { + session_protocol_destination dest = {}; + dest.type = SESSION_PROTOCOL_DESTINATION_TYPE_ENVELOPE_COMMUNITY_INBOX; + dest.pro_rotating_ed25519_privkey = user_pro_ed_sk.data(); + dest.pro_rotating_ed25519_privkey_len = user_pro_ed_sk.size(); + dest.sent_timestamp_ms = timestamp_ms.time_since_epoch().count(); + dest.recipient_pubkey = session_blind15_pk1; + dest.community_inbox_server_pubkey = community_pubkey; + + memset(error, 0, sizeof(error)); + session_protocol_encoded_for_destination encoded = session_protocol_encode_for_destination( + protobuf_content.plaintext.data(), + protobuf_content.plaintext.size(), + keys.ed_sk0.data(), + keys.ed_sk0.size(), + &dest, + error, + sizeof(error)); + scope_exit encoded_free{[&]() { session_protocol_encode_for_destination_free(&encoded); }}; + REQUIRE(encoded.success); + + memset(error, 0, sizeof(error)); + session_protocol_decoded_community_message decoded = + session_protocol_decode_for_community_inbox( + keys.ed_sk1.data(), + keys.ed_sk1.size(), + community_pk.data(), + community_pk.size(), + /*sender*/ session_blind15_pk0.data, + sizeof(session_blind15_pk0.data), + /*recipient*/ session_blind15_pk1.data, + sizeof(session_blind15_pk1.data), + encoded.ciphertext.data, + encoded.ciphertext.size, + timestamp_ms.time_since_epoch().count(), + pro_backend_ed_pk.data(), + pro_backend_ed_pk.size(), + error, + sizeof(error)); + INFO("ERROR: " << error << ", cipher was: " << encoded.ciphertext.size << "b"); + REQUIRE(decoded.error_len_incl_null_terminator == 0); + REQUIRE(decoded.success); + scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; + REQUIRE(decoded.has_pro); + REQUIRE(decoded.pro.status == SESSION_PROTOCOL_PRO_STATUS_VALID); + } }