use thiserror::Error;
pub const PRO_STANDARD_CHARACTER_LIMIT: usize = 2000;
pub const PRO_HIGHER_CHARACTER_LIMIT: usize = 10000;
pub const PRO_STANDARD_PINNED_CONVERSATION_LIMIT: usize = 5;
pub const COMMUNITY_OR_1O1_MSG_PADDING: usize = 160;
pub const PADDING_TERMINATING_BYTE: u8 = 0x80;
pub const GENERATE_PROOF_HASH_PERSONALISATION: &str = "ProGenerateProof";
pub const BUILD_PROOF_HASH_PERSONALISATION: &str = "ProProof________";
pub const ADD_PRO_PAYMENT_HASH_PERSONALISATION: &str = "ProAddPayment___";
pub const SET_PAYMENT_REFUND_REQUESTED_HASH_PERSONALISATION: &str = "ProSetRefundReq_";
pub const GET_PRO_DETAILS_HASH_PERSONALISATION: &str = "ProGetProDetReq_";
pub const PRO_BACKEND_BLAKE2B_PERSONALISATION: &str = "SeshProBackend__";
pub struct ProtocolStrings {
pub build_variant_apk: &'static str,
pub build_variant_fdroid: &'static str,
pub build_variant_huawei: &'static str,
pub build_variant_ipa: &'static str,
pub url_donations: &'static str,
pub url_donations_app: &'static str,
pub url_download: &'static str,
pub url_faq: &'static str,
pub url_feedback: &'static str,
pub url_network: &'static str,
pub url_privacy_policy: &'static str,
pub url_pro_access_not_found: &'static str,
pub url_pro_faq: &'static str,
pub url_pro_page: &'static str,
pub url_pro_privacy_policy: &'static str,
pub url_pro_roadmap: &'static str,
pub url_pro_support: &'static str,
pub url_pro_terms_of_service: &'static str,
pub url_pro_upgrade: &'static str,
pub url_staking: &'static str,
pub url_support: &'static str,
pub url_survey: &'static str,
pub url_terms_of_service: &'static str,
pub url_token: &'static str,
pub url_translate: &'static str,
}
pub static PROTOCOL_STRINGS: ProtocolStrings = ProtocolStrings {
build_variant_apk: "APK",
build_variant_fdroid: "F-Droid Store",
build_variant_huawei: "Huawei App Gallery",
build_variant_ipa: "IPA",
url_donations: "https://getsession.org/donate",
url_donations_app: "https://getsession.org/donate#app",
url_download: "https://getsession.org/download",
url_faq: "https://getsession.org/faq",
url_feedback: "https://getsession.org/feedback",
url_network: "https://docs.getsession.org/session-network",
url_privacy_policy: "https://getsession.org/privacy-policy",
url_pro_access_not_found:
"https://sessionapp.zendesk.com/hc/sections/4416517450649-Support",
url_pro_faq: "https://getsession.org/pro#faq",
url_pro_page: "https://getsession.org/pro",
url_pro_privacy_policy: "https://getsession.org/pro-privacy",
url_pro_roadmap: "https://getsession.org/pro#roadmap",
url_pro_support: "https://getsession.org/pro-support",
url_pro_terms_of_service: "https://getsession.org/pro-terms",
url_pro_upgrade: "https://getsession.org/pro#upgrade",
url_staking: "https://docs.getsession.org/session-network/staking",
url_support: "https://getsession.org/support",
url_survey: "https://getsession.org/survey",
url_terms_of_service: "https://getsession.org/terms-of-service",
url_token: "https://token.getsession.org",
url_translate: "https://getsession.org/translate",
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ProStatus {
InvalidProBackendSig = 1,
InvalidUserSig = 2,
Valid = 3,
Expired = 4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ProProofVersion {
V0 = 0,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProProof {
pub version: u8,
pub gen_index_hash: [u8; 32],
pub rotating_pubkey: [u8; 32],
pub expiry_unix_ts_ms: u64,
pub sig: [u8; 64],
}
#[derive(Debug, Error)]
pub enum ProProofError {
#[error("Invalid verify_pubkey: must be 32 byte Ed25519 public key (was: {0})")]
InvalidPubkeySize(usize),
#[error("Invalid signature: must be 64 bytes (was: {0})")]
InvalidSignatureSize(usize),
#[error("Crypto error: {0}")]
Crypto(#[from] crate::crypto::types::CryptoError),
}
impl ProProof {
pub fn hash(&self) -> [u8; 32] {
proof_hash_internal(
self.version,
&self.gen_index_hash,
&self.rotating_pubkey,
self.expiry_unix_ts_ms,
)
}
pub fn verify_signature(&self, verify_pubkey: &[u8]) -> Result<bool, ProProofError> {
if verify_pubkey.len() != 32 {
return Err(ProProofError::InvalidPubkeySize(verify_pubkey.len()));
}
let hash_to_sign = self.hash();
let result = crate::crypto::ed25519::verify(&self.sig, verify_pubkey, &hash_to_sign)?;
Ok(result)
}
pub fn verify_message(&self, sig: &[u8], msg: &[u8]) -> Result<bool, ProProofError> {
if sig.len() != 64 {
return Err(ProProofError::InvalidSignatureSize(sig.len()));
}
let result = crate::crypto::ed25519::verify(sig, &self.rotating_pubkey, msg)?;
Ok(result)
}
pub fn is_active(&self, unix_ts_ms: u64) -> bool {
unix_ts_ms <= self.expiry_unix_ts_ms
}
pub fn status(
&self,
verify_pubkey: &[u8],
unix_ts_ms: u64,
signed_msg: Option<ProSignedMessage<'_>>,
) -> Result<ProStatus, ProProofError> {
if !self.verify_signature(verify_pubkey)? {
return Ok(ProStatus::InvalidProBackendSig);
}
if let Some(sm) = signed_msg
&& !self.verify_message(sm.sig, sm.msg)? {
return Ok(ProStatus::InvalidUserSig);
}
if !self.is_active(unix_ts_ms) {
return Ok(ProStatus::Expired);
}
Ok(ProStatus::Valid)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ProSignedMessage<'a> {
pub sig: &'a [u8],
pub msg: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u64)]
pub enum ProProfileFeature {
ProBadge = 0,
AnimatedAvatar = 1,
}
pub const PRO_PROFILE_FEATURES_COUNT: usize = 2;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ProProfileBitset {
pub data: u64,
}
impl ProProfileBitset {
pub fn set(&mut self, feature: ProProfileFeature) {
self.data |= 1u64 << (feature as u64);
}
pub fn unset(&mut self, feature: ProProfileFeature) {
self.data &= !(1u64 << (feature as u64));
}
pub fn is_set(&self, feature: ProProfileFeature) -> bool {
(self.data & (1u64 << (feature as u64))) != 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u64)]
pub enum ProMessageFeature {
CharacterLimit10K = 0,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ProMessageBitset {
pub data: u64,
}
impl ProMessageBitset {
pub fn set(&mut self, feature: ProMessageFeature) {
self.data |= 1u64 << (feature as u64);
}
pub fn unset(&mut self, feature: ProMessageFeature) {
self.data &= !(1u64 << (feature as u64));
}
pub fn is_set(&self, feature: ProMessageFeature) -> bool {
(self.data & (1u64 << (feature as u64))) != 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ProFeaturesForMsgStatus {
Success = 0,
UtfDecodingError = 1,
ExceedsCharacterLimit = 2,
}
#[derive(Debug, Clone)]
pub struct ProFeaturesForMsg {
pub status: ProFeaturesForMsgStatus,
pub error: Option<String>,
pub bitset: ProMessageBitset,
pub codepoint_count: usize,
}
pub fn pro_features_for_utf8(text: &str) -> ProFeaturesForMsg {
let codepoint_count = text.chars().count();
if codepoint_count > PRO_HIGHER_CHARACTER_LIMIT {
return ProFeaturesForMsg {
status: ProFeaturesForMsgStatus::ExceedsCharacterLimit,
error: Some("Message exceeds the maximum character limit allowed".into()),
bitset: ProMessageBitset::default(),
codepoint_count,
};
}
let mut bitset = ProMessageBitset::default();
if codepoint_count > PRO_STANDARD_CHARACTER_LIMIT {
bitset.set(ProMessageFeature::CharacterLimit10K);
}
ProFeaturesForMsg {
status: ProFeaturesForMsgStatus::Success,
error: None,
bitset,
codepoint_count,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum DestinationType {
SyncOr1o1 = 0,
Group = 1,
CommunityInbox = 2,
Community = 3,
}
pub mod envelope_flags {
pub const SOURCE: u32 = 1 << 0;
pub const SOURCE_DEVICE: u32 = 1 << 1;
pub const SERVER_TIMESTAMP: u32 = 1 << 2;
pub const PRO_SIG: u32 = 1 << 3;
pub const TIMESTAMP: u32 = 1 << 4;
}
pub fn pad_message(payload: &[u8]) -> Vec<u8> {
let padded_content_size = payload.len() + 1; let bytes_for_padding =
COMMUNITY_OR_1O1_MSG_PADDING - (padded_content_size % COMMUNITY_OR_1O1_MSG_PADDING);
let total_size = padded_content_size + bytes_for_padding;
debug_assert!(total_size.is_multiple_of(COMMUNITY_OR_1O1_MSG_PADDING));
let mut result = vec![0u8; total_size];
result[..payload.len()].copy_from_slice(payload);
result[payload.len()] = PADDING_TERMINATING_BYTE;
result
}
pub fn unpad_message(payload: &[u8]) -> &[u8] {
let mut size = payload.len();
while size > 0 {
let ch = payload[size - 1];
if ch != 0 && ch != PADDING_TERMINATING_BYTE {
return payload;
}
size -= 1;
if ch == PADDING_TERMINATING_BYTE {
break;
}
}
&payload[..size]
}
fn proof_hash_internal(
version: u8,
gen_index_hash: &[u8; 32],
rotating_pubkey: &[u8; 32],
expiry_unix_ts_ms: u64,
) -> [u8; 32] {
use blake2b_simd::Params;
let mut params = Params::new();
params.hash_length(32);
debug_assert!(BUILD_PROOF_HASH_PERSONALISATION.len() == 16);
let mut personal = [0u8; 16];
personal.copy_from_slice(BUILD_PROOF_HASH_PERSONALISATION.as_bytes());
params.personal(&personal);
let mut state = params.to_state();
state.update(&[version]);
state.update(gen_index_hash);
state.update(rotating_pubkey);
state.update(&expiry_unix_ts_ms.to_le_bytes());
let hash = state.finalize();
let mut result = [0u8; 32];
result.copy_from_slice(hash.as_bytes());
result
}
pub fn make_blake2b32_hasher(personalisation: &str) -> blake2b_simd::State {
debug_assert!(personalisation.len() == 16);
let mut personal = [0u8; 16];
let len = personalisation.len().min(16);
personal[..len].copy_from_slice(&personalisation.as_bytes()[..len]);
let mut params = blake2b_simd::Params::new();
params.hash_length(32);
params.personal(&personal);
params.to_state()
}
pub const GROUP_MESSAGE_PADDING: usize = 256;
pub const SESSION_ID_PREFIX_STANDARD: u8 = 0x05;
pub const SESSION_ID_PREFIX_GROUP: u8 = 0x03;
pub fn pro_features_for_utf16(utf16: &[u16]) -> ProFeaturesForMsg {
let decoded: Result<String, _> = char::decode_utf16(utf16.iter().copied())
.map(|r| r.map(|c| c.to_string()))
.collect::<Result<Vec<_>, _>>()
.map(|v| v.concat());
let (codepoint_count, decode_err) = match decoded {
Ok(s) => (s.chars().count(), None),
Err(e) => (0usize, Some(e.to_string())),
};
if let Some(err) = decode_err {
return ProFeaturesForMsg {
status: ProFeaturesForMsgStatus::UtfDecodingError,
error: Some(err),
bitset: ProMessageBitset::default(),
codepoint_count: 0,
};
}
if codepoint_count > PRO_HIGHER_CHARACTER_LIMIT {
return ProFeaturesForMsg {
status: ProFeaturesForMsgStatus::ExceedsCharacterLimit,
error: Some("Message exceeds the maximum character limit allowed".into()),
bitset: ProMessageBitset::default(),
codepoint_count,
};
}
let mut bitset = ProMessageBitset::default();
if codepoint_count > PRO_STANDARD_CHARACTER_LIMIT {
bitset.set(ProMessageFeature::CharacterLimit10K);
}
ProFeaturesForMsg {
status: ProFeaturesForMsgStatus::Success,
error: None,
bitset,
codepoint_count,
}
}
#[derive(Debug, Clone)]
pub struct Destination {
pub dest_type: Option<DestinationType>,
pub pro_rotating_ed25519_privkey: Vec<u8>,
pub sent_timestamp_ms: u64,
pub recipient_pubkey: [u8; 33],
pub community_inbox_server_pubkey: [u8; 32],
pub group_ed25519_pubkey: [u8; 33],
pub group_enc_key: Vec<u8>,
}
impl Default for Destination {
fn default() -> Self {
Self {
dest_type: None,
pro_rotating_ed25519_privkey: Vec::new(),
sent_timestamp_ms: 0,
recipient_pubkey: [0u8; 33],
community_inbox_server_pubkey: [0u8; 32],
group_ed25519_pubkey: [0u8; 33],
group_enc_key: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Envelope {
pub flags: u32,
pub timestamp: u64,
pub source: [u8; 33],
pub source_device: u32,
pub server_timestamp: u64,
pub pro_sig: [u8; 64],
}
impl Default for Envelope {
fn default() -> Self {
Self {
flags: 0,
timestamp: 0,
source: [0u8; 33],
source_device: 0,
server_timestamp: 0,
pro_sig: [0u8; 64],
}
}
}
#[derive(Debug, Clone)]
pub struct DecodedPro {
pub status: ProStatus,
pub proof: ProProof,
pub msg_bitset: ProMessageBitset,
pub profile_bitset: ProProfileBitset,
}
#[derive(Debug, Clone)]
pub struct DecodedEnvelope {
pub envelope: Envelope,
pub content_plaintext: Vec<u8>,
pub sender_ed25519_pubkey: [u8; 32],
pub sender_x25519_pubkey: [u8; 32],
pub pro: Option<DecodedPro>,
}
#[derive(Debug, Clone)]
pub struct DecodedCommunityMessage {
pub envelope: Option<Envelope>,
pub content_plaintext: Vec<u8>,
pub pro_sig: Option<[u8; 64]>,
pub pro: Option<DecodedPro>,
}
#[derive(Debug, Clone, Default)]
pub struct DecodeEnvelopeKey<'a> {
pub group_ed25519_pubkey: Option<&'a [u8]>,
pub decrypt_keys: &'a [&'a [u8]],
}
#[derive(Debug, Error)]
pub enum EnvelopeError {
#[error("Missing destination type")]
MissingDestinationType,
#[error("Invalid recipient pubkey: expected 33 bytes (0x05 prefix)")]
InvalidRecipientPubkey,
#[error("Invalid group pubkey: expected 33 bytes (0x03 prefix)")]
InvalidGroupPubkey,
#[error("Invalid group encryption key: expected 32 or 64 bytes")]
InvalidGroupEncKey,
#[error("Encrypting for a legacy group (0x05 prefix) is not supported")]
LegacyGroupUnsupported,
#[error("Invalid Session Pro rotating ed25519 privkey: expected 32 or 64 bytes")]
InvalidProKeySize,
#[error("Crypto error: {0}")]
Crypto(#[from] crate::crypto::types::CryptoError),
#[error("Ed25519 error: {0}")]
Ed25519(String),
#[error("Protobuf encode error: {0}")]
ProtobufEncode(String),
#[error("Protobuf decode error: {0}")]
ProtobufDecode(String),
#[error("Decryption failed after trying {0} key(s)")]
DecryptionFailed(usize),
#[error("Envelope missing required content field")]
MissingContent,
#[error("Malformed envelope: {0}")]
MalformedEnvelope(String),
#[error("Malformed content: {0}")]
MalformedContent(String),
#[error("Malformed Pro proof: {0}")]
MalformedProProof(String),
}
fn expand_ed25519_sk(key: &[u8]) -> Result<[u8; 64], EnvelopeError> {
match key.len() {
32 => {
let (_, sk) = crate::crypto::ed25519::ed25519_key_pair_from_seed(key)?;
Ok(sk)
}
64 => {
let mut out = [0u8; 64];
out.copy_from_slice(key);
Ok(out)
}
_ => Err(EnvelopeError::InvalidProKeySize),
}
}
fn detached_sign(sk_64: &[u8; 64], msg: &[u8]) -> Result<[u8; 64], EnvelopeError> {
use ed25519_dalek::{Signer, SigningKey};
let mut seed = [0u8; 32];
seed.copy_from_slice(&sk_64[..32]);
let signing_key = SigningKey::from_bytes(&seed);
let sig = signing_key.sign(msg);
Ok(sig.to_bytes())
}
fn throwaway_sign(msg: &[u8]) -> [u8; 64] {
use ed25519_dalek::{Signer, SigningKey};
use rand::RngExt;
let seed: [u8; 32] = rand::rng().random();
let sk = SigningKey::from_bytes(&seed);
sk.sign(msg).to_bytes()
}
pub fn encode_for_destination(
plaintext: &[u8],
ed25519_privkey: &[u8],
dest: &Destination,
) -> Result<Vec<u8>, EnvelopeError> {
use prost::Message as _;
use crate::proto::session_protos::{envelope, Content, Envelope as PbEnvelope};
use crate::proto::web_socket_protos::{
web_socket_message, WebSocketMessage as PbWsMsg, WebSocketRequestMessage as PbWsReq,
};
let dest_type = dest
.dest_type
.ok_or(EnvelopeError::MissingDestinationType)?;
let pro_rotating_sk: Option<[u8; 64]> = if !dest.pro_rotating_ed25519_privkey.is_empty() {
Some(expand_ed25519_sk(&dest.pro_rotating_ed25519_privkey)?)
} else {
None
};
match dest_type {
DestinationType::SyncOr1o1 | DestinationType::Group => {
let is_group = matches!(dest_type, DestinationType::Group);
let is_1o1 = !is_group;
if is_1o1 && dest.recipient_pubkey[0] != SESSION_ID_PREFIX_STANDARD {
return Err(EnvelopeError::InvalidRecipientPubkey);
}
if is_group {
if dest.group_ed25519_pubkey[0] == SESSION_ID_PREFIX_STANDARD {
return Err(EnvelopeError::LegacyGroupUnsupported);
}
if dest.group_ed25519_pubkey[0] != SESSION_ID_PREFIX_GROUP {
return Err(EnvelopeError::InvalidGroupPubkey);
}
if dest.group_enc_key.len() != 32 && dest.group_enc_key.len() != 64 {
return Err(EnvelopeError::InvalidGroupEncKey);
}
}
let envelope_content: Vec<u8> = if is_1o1 {
let padded = pad_message(plaintext);
crate::crypto::session_encrypt::encrypt_for_recipient(
ed25519_privkey,
&dest.recipient_pubkey,
&padded,
)?
} else {
plaintext.to_vec()
};
let pro_sig = match pro_rotating_sk {
Some(sk) => detached_sign(&sk, &envelope_content)?,
None => throwaway_sign(&envelope_content),
};
let pb_envelope = PbEnvelope {
r#type: if is_1o1 {
envelope::Type::SessionMessage as i32
} else {
envelope::Type::ClosedGroupMessage as i32
},
source: None,
source_device: Some(1),
timestamp: dest.sent_timestamp_ms,
content: Some(envelope_content),
server_timestamp: None,
pro_sig: Some(pro_sig.to_vec()),
};
let envelope_bytes = pb_envelope.encode_to_vec();
if is_group {
let group_pubkey_32 = &dest.group_ed25519_pubkey[1..];
let ciphertext = crate::crypto::session_encrypt::encrypt_for_group(
ed25519_privkey,
group_pubkey_32,
&dest.group_enc_key,
&envelope_bytes,
true,
256,
)?;
Ok(ciphertext)
} else {
let ws_req = PbWsReq {
verb: Some(String::new()),
path: Some(String::new()),
body: Some(envelope_bytes),
headers: Vec::new(),
request_id: Some(0),
};
let ws_msg = PbWsMsg {
r#type: Some(web_socket_message::Type::Request as i32),
request: Some(ws_req),
response: None,
};
Ok(ws_msg.encode_to_vec())
}
}
DestinationType::Community | DestinationType::CommunityInbox => {
let is_inbox = matches!(dest_type, DestinationType::CommunityInbox);
let content_bytes: Vec<u8> = if let Some(sk) = pro_rotating_sk.as_ref() {
let mut content = Content::decode(plaintext)
.map_err(|e| EnvelopeError::MalformedContent(e.to_string()))?;
if content.pro_sig_for_community_message_only.is_some() {
return Err(EnvelopeError::MalformedContent(
"Pro signature for community message must not be pre-set".into(),
));
}
let padded_once = pad_message(plaintext);
let pro_sig = detached_sign(sk, &padded_once)?;
content.pro_sig_for_community_message_only = Some(pro_sig.to_vec());
let reserialised = content.encode_to_vec();
pad_message(&reserialised)
} else {
pad_message(plaintext)
};
if is_inbox {
let ciphertext = crate::crypto::session_encrypt::encrypt_for_blinded_recipient(
ed25519_privkey,
&dest.community_inbox_server_pubkey,
&dest.recipient_pubkey,
&content_bytes,
)?;
Ok(ciphertext)
} else {
Ok(content_bytes)
}
}
}
}
pub fn encode_for_1o1(
plaintext: &[u8],
ed25519_privkey: &[u8],
sent_timestamp_ms: u64,
recipient_pubkey: &[u8; 33],
pro_rotating_ed25519_privkey: Option<&[u8]>,
) -> Result<Vec<u8>, EnvelopeError> {
let dest = Destination {
dest_type: Some(DestinationType::SyncOr1o1),
pro_rotating_ed25519_privkey: pro_rotating_ed25519_privkey
.map(|s| s.to_vec())
.unwrap_or_default(),
sent_timestamp_ms,
recipient_pubkey: *recipient_pubkey,
..Default::default()
};
encode_for_destination(plaintext, ed25519_privkey, &dest)
}
pub fn encode_for_community_inbox(
plaintext: &[u8],
ed25519_privkey: &[u8],
sent_timestamp_ms: u64,
recipient_pubkey: &[u8; 33],
community_server_pubkey: &[u8; 32],
pro_rotating_ed25519_privkey: Option<&[u8]>,
) -> Result<Vec<u8>, EnvelopeError> {
let dest = Destination {
dest_type: Some(DestinationType::CommunityInbox),
pro_rotating_ed25519_privkey: pro_rotating_ed25519_privkey
.map(|s| s.to_vec())
.unwrap_or_default(),
sent_timestamp_ms,
recipient_pubkey: *recipient_pubkey,
community_inbox_server_pubkey: *community_server_pubkey,
..Default::default()
};
encode_for_destination(plaintext, ed25519_privkey, &dest)
}
pub fn encode_for_community(
plaintext: &[u8],
pro_rotating_ed25519_privkey: Option<&[u8]>,
) -> Result<Vec<u8>, EnvelopeError> {
let dest = Destination {
dest_type: Some(DestinationType::Community),
pro_rotating_ed25519_privkey: pro_rotating_ed25519_privkey
.map(|s| s.to_vec())
.unwrap_or_default(),
..Default::default()
};
encode_for_destination(plaintext, &[], &dest)
}
pub fn encode_for_group(
plaintext: &[u8],
ed25519_privkey: &[u8],
sent_timestamp_ms: u64,
group_ed25519_pubkey: &[u8; 33],
group_enc_key: &[u8],
pro_rotating_ed25519_privkey: Option<&[u8]>,
) -> Result<Vec<u8>, EnvelopeError> {
let dest = Destination {
dest_type: Some(DestinationType::Group),
pro_rotating_ed25519_privkey: pro_rotating_ed25519_privkey
.map(|s| s.to_vec())
.unwrap_or_default(),
sent_timestamp_ms,
group_ed25519_pubkey: *group_ed25519_pubkey,
group_enc_key: group_enc_key.to_vec(),
..Default::default()
};
encode_for_destination(plaintext, ed25519_privkey, &dest)
}
pub fn decode_envelope(
keys: &DecodeEnvelopeKey<'_>,
envelope_payload: &[u8],
pro_backend_pubkey: &[u8; 32],
) -> Result<DecodedEnvelope, EnvelopeError> {
use prost::Message as _;
use crate::proto::session_protos::{Content, Envelope as PbEnvelope};
use crate::proto::web_socket_protos::WebSocketMessage as PbWsMsg;
let mut result_envelope = Envelope::default();
let mut sender_ed25519_pubkey = [0u8; 32];
let mut sender_x25519_pubkey = [0u8; 32];
let content_plaintext_vec: Vec<u8>;
let pb_envelope: PbEnvelope = if let Some(group_pubkey) = keys.group_ed25519_pubkey {
let decrypt = crate::crypto::session_encrypt::decrypt_group_message(
keys.decrypt_keys,
group_pubkey,
envelope_payload,
)?;
if decrypt.session_id.len() != 66 || !decrypt.session_id.starts_with("05") {
return Err(EnvelopeError::MalformedEnvelope(format!(
"Invalid sender session ID extracted from group envelope: {}",
decrypt.session_id
)));
}
let sender_x_bytes = hex::decode(&decrypt.session_id[2..]).map_err(|e| {
EnvelopeError::MalformedEnvelope(format!("Invalid sender session ID hex: {e}"))
})?;
if sender_x_bytes.len() != 32 {
return Err(EnvelopeError::MalformedEnvelope(
"Sender x25519 pubkey has wrong length".into(),
));
}
sender_x25519_pubkey.copy_from_slice(&sender_x_bytes);
PbEnvelope::decode(decrypt.data.as_slice())
.map_err(|e| EnvelopeError::ProtobufDecode(e.to_string()))?
} else {
let ws = PbWsMsg::decode(envelope_payload)
.map_err(|e| EnvelopeError::ProtobufDecode(e.to_string()))?;
let request = ws
.request
.ok_or_else(|| EnvelopeError::MalformedEnvelope("missing WS request".into()))?;
let body = request
.body
.ok_or_else(|| EnvelopeError::MalformedEnvelope("missing WS request body".into()))?;
PbEnvelope::decode(body.as_slice())
.map_err(|e| EnvelopeError::ProtobufDecode(e.to_string()))?
};
result_envelope.timestamp = pb_envelope.timestamp;
result_envelope.flags |= envelope_flags::TIMESTAMP;
if let Some(source) = pb_envelope.source.as_ref() {
if !source.is_empty() {
if source.len() != 64 {
return Err(EnvelopeError::MalformedEnvelope(format!(
"Envelope source has unexpected size: {}b",
source.len()
)));
}
let src_bytes = hex::decode(source).map_err(|e| {
EnvelopeError::MalformedEnvelope(format!("Invalid source hex: {e}"))
})?;
if src_bytes.len() == 32 {
result_envelope.source[0] = SESSION_ID_PREFIX_STANDARD;
result_envelope.source[1..].copy_from_slice(&src_bytes);
result_envelope.flags |= envelope_flags::SOURCE;
}
}
}
if let Some(sd) = pb_envelope.source_device {
result_envelope.source_device = sd;
result_envelope.flags |= envelope_flags::SOURCE_DEVICE;
}
if let Some(st) = pb_envelope.server_timestamp {
result_envelope.server_timestamp = st;
result_envelope.flags |= envelope_flags::SERVER_TIMESTAMP;
}
let content = pb_envelope
.content
.as_ref()
.ok_or(EnvelopeError::MissingContent)?;
if keys.group_ed25519_pubkey.is_some() {
content_plaintext_vec = content.clone();
} else {
let mut success = false;
let mut plaintext = Vec::new();
let mut sender_pk = Vec::new();
let mut last_err: Option<EnvelopeError> = None;
for sk in keys.decrypt_keys {
match crate::crypto::session_encrypt::decrypt_incoming(sk, content) {
Ok((pt, spk)) => {
plaintext = pt;
sender_pk = spk;
success = true;
break;
}
Err(e) => last_err = Some(e.into()),
}
}
if !success {
return Err(last_err
.unwrap_or(EnvelopeError::DecryptionFailed(keys.decrypt_keys.len())));
}
let unpadded = unpad_message(&plaintext);
content_plaintext_vec = unpadded.to_vec();
if sender_pk.len() != 32 {
return Err(EnvelopeError::MalformedEnvelope(
"sender ed25519 pubkey has wrong length".into(),
));
}
sender_ed25519_pubkey.copy_from_slice(&sender_pk);
sender_x25519_pubkey =
crate::crypto::curve25519::to_curve25519_pubkey(&sender_ed25519_pubkey)?;
}
let pb_content = Content::decode(content_plaintext_vec.as_slice())
.map_err(|e| EnvelopeError::ProtobufDecode(e.to_string()))?;
let mut pro_out: Option<DecodedPro> = None;
if let Some(ps) = pb_envelope.pro_sig.as_ref() {
if ps.len() != 64 {
return Err(EnvelopeError::MalformedEnvelope(
"pro signature has wrong size".into(),
));
}
result_envelope.pro_sig.copy_from_slice(ps);
if let Some(pro_msg) = pb_content.pro_message.as_ref() {
let sig_timestamp = pb_content.sig_timestamp.ok_or_else(|| {
EnvelopeError::MalformedContent(
"Content missing sigTimestamp; Pro proof expiry is unverifiable".into(),
)
})?;
result_envelope.flags |= envelope_flags::PRO_SIG;
let proto_proof = pro_msg
.proof
.as_ref()
.ok_or_else(|| EnvelopeError::MalformedProProof("pro_message missing proof".into()))?;
let version = proto_proof
.version
.ok_or_else(|| EnvelopeError::MalformedProProof("proof missing version".into()))?;
if version != ProProofVersion::V0 as u32 {
return Err(EnvelopeError::MalformedProProof(format!(
"unexpected proof version {version}"
)));
}
let gen_index_hash_bytes = proto_proof
.gen_index_hash
.as_ref()
.ok_or_else(|| EnvelopeError::MalformedProProof("missing genIndexHash".into()))?;
if gen_index_hash_bytes.len() != 32 {
return Err(EnvelopeError::MalformedProProof(
"genIndexHash wrong size".into(),
));
}
let rotating_pk_bytes = proto_proof
.rotating_public_key
.as_ref()
.ok_or_else(|| EnvelopeError::MalformedProProof("missing rotatingPublicKey".into()))?;
if rotating_pk_bytes.len() != 32 {
return Err(EnvelopeError::MalformedProProof(
"rotatingPublicKey wrong size".into(),
));
}
let expiry = proto_proof
.expiry_unix_ts
.ok_or_else(|| EnvelopeError::MalformedProProof("missing expiryUnixTs".into()))?;
let sig_bytes = proto_proof
.sig
.as_ref()
.ok_or_else(|| EnvelopeError::MalformedProProof("missing sig".into()))?;
if sig_bytes.len() != 64 {
return Err(EnvelopeError::MalformedProProof("sig wrong size".into()));
}
let mut proof = ProProof {
version: version as u8,
gen_index_hash: [0u8; 32],
rotating_pubkey: [0u8; 32],
expiry_unix_ts_ms: expiry,
sig: [0u8; 64],
};
proof.gen_index_hash.copy_from_slice(gen_index_hash_bytes);
proof.rotating_pubkey.copy_from_slice(rotating_pk_bytes);
proof.sig.copy_from_slice(sig_bytes);
let msg_bitset = ProMessageBitset {
data: pro_msg.msg_bitset.unwrap_or(0),
};
let profile_bitset = ProProfileBitset {
data: pro_msg.profile_bitset.unwrap_or(0),
};
let signed_msg = ProSignedMessage {
sig: ps,
msg: content,
};
let status = proof
.status(pro_backend_pubkey, sig_timestamp, Some(signed_msg))
.map_err(|e| EnvelopeError::Ed25519(e.to_string()))?;
pro_out = Some(DecodedPro {
status,
proof,
msg_bitset,
profile_bitset,
});
}
}
Ok(DecodedEnvelope {
envelope: result_envelope,
content_plaintext: content_plaintext_vec,
sender_ed25519_pubkey,
sender_x25519_pubkey,
pro: pro_out,
})
}
pub fn decode_for_community(
content_or_envelope_payload: &[u8],
unix_ts_ms: u64,
pro_backend_pubkey: &[u8; 32],
) -> Result<DecodedCommunityMessage, EnvelopeError> {
use prost::Message as _;
use crate::proto::session_protos::{Content, Envelope as PbEnvelope};
let mut result = DecodedCommunityMessage {
envelope: None,
content_plaintext: Vec::new(),
pro_sig: None,
pro: None,
};
let mut pro_sig_from_envelope: Option<Vec<u8>> = None;
let parsed_envelope = PbEnvelope::decode(content_or_envelope_payload)
.ok()
.filter(|e| e.content.is_some());
if let Some(pb_envelope) = parsed_envelope {
let mut envelope = Envelope::default();
if let Some(source) = pb_envelope.source.as_ref()
&& !source.is_empty()
{
if source.len() != 64 {
return Err(EnvelopeError::MalformedEnvelope(format!(
"source has unexpected size: {}b",
source.len()
)));
}
let src_bytes = hex::decode(source).map_err(|e| {
EnvelopeError::MalformedEnvelope(format!("Invalid source hex: {e}"))
})?;
if src_bytes.len() == 32 {
envelope.source[0] = SESSION_ID_PREFIX_STANDARD;
envelope.source[1..].copy_from_slice(&src_bytes);
envelope.flags |= envelope_flags::SOURCE;
}
}
envelope.timestamp = pb_envelope.timestamp;
envelope.flags |= envelope_flags::TIMESTAMP;
if let Some(sd) = pb_envelope.source_device {
envelope.source_device = sd;
envelope.flags |= envelope_flags::SOURCE_DEVICE;
}
if let Some(st) = pb_envelope.server_timestamp {
envelope.server_timestamp = st;
envelope.flags |= envelope_flags::SERVER_TIMESTAMP;
}
if let Some(ps) = pb_envelope.pro_sig.as_ref() {
envelope.flags |= envelope_flags::PRO_SIG;
pro_sig_from_envelope = Some(ps.clone());
}
if let Some(content) = pb_envelope.content {
result.content_plaintext = content;
}
result.envelope = Some(envelope);
} else {
result.content_plaintext = content_or_envelope_payload.to_vec();
}
let unpadded = unpad_message(&result.content_plaintext);
let pb_content = Content::decode(unpadded)
.map_err(|e| EnvelopeError::ProtobufDecode(e.to_string()))?;
let mut effective_pro_sig: Option<Vec<u8>> = pro_sig_from_envelope.clone();
if let Some(content_sig) = pb_content.pro_sig_for_community_message_only.as_ref() {
if effective_pro_sig.is_some() {
return Err(EnvelopeError::MalformedContent(
"Envelope and content both provided a pro signature".into(),
));
}
effective_pro_sig = Some(content_sig.clone());
}
if let Some(ps) = effective_pro_sig.as_ref() {
if ps.len() != 64 {
return Err(EnvelopeError::MalformedEnvelope(
"pro signature wrong size".into(),
));
}
let mut pro_sig_arr = [0u8; 64];
pro_sig_arr.copy_from_slice(ps);
result.pro_sig = Some(pro_sig_arr);
if let Some(env) = result.envelope.as_mut() {
env.pro_sig.copy_from_slice(ps);
}
if let Some(pro_msg) = pb_content.pro_message.as_ref() {
let proto_proof = pro_msg
.proof
.as_ref()
.ok_or_else(|| EnvelopeError::MalformedProProof("pro_message missing proof".into()))?;
let version = proto_proof
.version
.ok_or_else(|| EnvelopeError::MalformedProProof("proof missing version".into()))?;
if version != ProProofVersion::V0 as u32 {
return Err(EnvelopeError::MalformedProProof(format!(
"unexpected proof version {version}"
)));
}
let gen_index_hash_bytes = proto_proof
.gen_index_hash
.as_ref()
.ok_or_else(|| EnvelopeError::MalformedProProof("missing genIndexHash".into()))?;
let rotating_pk_bytes = proto_proof
.rotating_public_key
.as_ref()
.ok_or_else(|| EnvelopeError::MalformedProProof("missing rotatingPublicKey".into()))?;
let expiry = proto_proof
.expiry_unix_ts
.ok_or_else(|| EnvelopeError::MalformedProProof("missing expiryUnixTs".into()))?;
let sig_bytes = proto_proof
.sig
.as_ref()
.ok_or_else(|| EnvelopeError::MalformedProProof("missing sig".into()))?;
if gen_index_hash_bytes.len() != 32
|| rotating_pk_bytes.len() != 32
|| sig_bytes.len() != 64
{
return Err(EnvelopeError::MalformedProProof("field wrong size".into()));
}
let mut proof = ProProof {
version: version as u8,
gen_index_hash: [0u8; 32],
rotating_pubkey: [0u8; 32],
expiry_unix_ts_ms: expiry,
sig: [0u8; 64],
};
proof.gen_index_hash.copy_from_slice(gen_index_hash_bytes);
proof.rotating_pubkey.copy_from_slice(rotating_pk_bytes);
proof.sig.copy_from_slice(sig_bytes);
let msg_bitset = ProMessageBitset {
data: pro_msg.msg_bitset.unwrap_or(0),
};
let profile_bitset = ProProfileBitset {
data: pro_msg.profile_bitset.unwrap_or(0),
};
let status = if result.envelope.is_some() {
proof
.status(
pro_backend_pubkey,
unix_ts_ms,
Some(ProSignedMessage {
sig: ps,
msg: &result.content_plaintext,
}),
)
.map_err(|e| EnvelopeError::Ed25519(e.to_string()))?
} else {
let mut content_without_sig = pb_content.clone();
content_without_sig.pro_sig_for_community_message_only = None;
let serialised = content_without_sig.encode_to_vec();
let padded = pad_message(&serialised);
proof
.status(
pro_backend_pubkey,
unix_ts_ms,
Some(ProSignedMessage {
sig: ps,
msg: &padded,
}),
)
.map_err(|e| EnvelopeError::Ed25519(e.to_string()))?
};
result.pro = Some(DecodedPro {
status,
proof,
msg_bitset,
profile_bitset,
});
}
}
result.content_plaintext = unpadded.to_vec();
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_decode_1o1_roundtrip() {
use prost::Message as _;
use crate::crypto::{curve25519, ed25519};
use crate::proto::session_protos::{Content, DataMessage};
let (_sender_ed_pk, sender_ed_sk) = ed25519::ed25519_key_pair();
let (recipient_ed_pk, recipient_ed_sk) = ed25519::ed25519_key_pair();
let recipient_x = curve25519::to_curve25519_pubkey(&recipient_ed_pk).unwrap();
let mut recipient_session_id = [0u8; 33];
recipient_session_id[0] = SESSION_ID_PREFIX_STANDARD;
recipient_session_id[1..].copy_from_slice(&recipient_x);
let body_text = "hello alpin!".to_string();
let ts_ms: u64 = 1_700_000_000_000;
let content = Content {
data_message: Some(DataMessage {
body: Some(body_text.clone()),
timestamp: Some(ts_ms),
..Default::default()
}),
sig_timestamp: Some(ts_ms),
..Default::default()
};
let plaintext = content.encode_to_vec();
let wire = encode_for_1o1(
&plaintext,
&sender_ed_sk,
ts_ms,
&recipient_session_id,
None,
)
.expect("encode_for_1o1 should succeed");
assert!(!wire.is_empty());
let keys = DecodeEnvelopeKey {
group_ed25519_pubkey: None,
decrypt_keys: &[&recipient_ed_sk],
};
let pro_backend_pk = [0u8; 32]; let decoded = decode_envelope(&keys, &wire, &pro_backend_pk)
.expect("decode_envelope should succeed");
assert_eq!(decoded.envelope.timestamp, ts_ms);
assert!(decoded.envelope.flags & envelope_flags::TIMESTAMP != 0);
let parsed_content = Content::decode(decoded.content_plaintext.as_slice())
.expect("content should parse");
let dm = parsed_content.data_message.expect("data_message present");
assert_eq!(dm.body.as_deref(), Some(body_text.as_str()));
assert!(decoded.pro.is_none());
}
#[test]
fn test_encode_decode_for_community_roundtrip() {
use prost::Message as _;
use crate::proto::session_protos::{Content, DataMessage};
let content = Content {
data_message: Some(DataMessage {
body: Some("hi community".into()),
timestamp: Some(1),
..Default::default()
}),
..Default::default()
};
let plaintext = content.encode_to_vec();
let wire = encode_for_community(&plaintext, None).expect("encode ok");
let pro_backend_pk = [0u8; 32];
let decoded = decode_for_community(&wire, 0, &pro_backend_pk).expect("decode ok");
let parsed = Content::decode(decoded.content_plaintext.as_slice()).expect("parse");
assert_eq!(
parsed.data_message.and_then(|d| d.body).as_deref(),
Some("hi community")
);
assert!(decoded.pro.is_none());
assert!(decoded.pro_sig.is_none());
}
#[test]
fn test_pro_features_for_utf16_short() {
let text: Vec<u16> = "hello".encode_utf16().collect();
let result = pro_features_for_utf16(&text);
assert_eq!(result.status, ProFeaturesForMsgStatus::Success);
assert_eq!(result.codepoint_count, 5);
assert_eq!(result.bitset.data, 0);
}
#[test]
fn test_constants() {
assert_eq!(PRO_STANDARD_CHARACTER_LIMIT, 2000);
assert_eq!(PRO_HIGHER_CHARACTER_LIMIT, 10000);
assert_eq!(PRO_STANDARD_PINNED_CONVERSATION_LIMIT, 5);
assert_eq!(COMMUNITY_OR_1O1_MSG_PADDING, 160);
}
#[test]
fn test_personalisation_lengths() {
assert_eq!(GENERATE_PROOF_HASH_PERSONALISATION.len(), 16);
assert_eq!(BUILD_PROOF_HASH_PERSONALISATION.len(), 16);
assert_eq!(ADD_PRO_PAYMENT_HASH_PERSONALISATION.len(), 16);
assert_eq!(SET_PAYMENT_REFUND_REQUESTED_HASH_PERSONALISATION.len(), 16);
assert_eq!(GET_PRO_DETAILS_HASH_PERSONALISATION.len(), 16);
}
#[test]
fn test_protocol_strings() {
assert_eq!(PROTOCOL_STRINGS.build_variant_apk, "APK");
assert_eq!(PROTOCOL_STRINGS.build_variant_fdroid, "F-Droid Store");
assert_eq!(PROTOCOL_STRINGS.build_variant_huawei, "Huawei App Gallery");
assert_eq!(PROTOCOL_STRINGS.build_variant_ipa, "IPA");
assert_eq!(
PROTOCOL_STRINGS.url_donations,
"https://getsession.org/donate"
);
assert_eq!(
PROTOCOL_STRINGS.url_donations_app,
"https://getsession.org/donate#app"
);
assert_eq!(
PROTOCOL_STRINGS.url_download,
"https://getsession.org/download"
);
assert_eq!(PROTOCOL_STRINGS.url_faq, "https://getsession.org/faq");
assert_eq!(
PROTOCOL_STRINGS.url_feedback,
"https://getsession.org/feedback"
);
assert_eq!(
PROTOCOL_STRINGS.url_network,
"https://docs.getsession.org/session-network"
);
assert_eq!(
PROTOCOL_STRINGS.url_privacy_policy,
"https://getsession.org/privacy-policy"
);
assert_eq!(
PROTOCOL_STRINGS.url_pro_access_not_found,
"https://sessionapp.zendesk.com/hc/sections/4416517450649-Support"
);
assert_eq!(
PROTOCOL_STRINGS.url_pro_faq,
"https://getsession.org/pro#faq"
);
assert_eq!(
PROTOCOL_STRINGS.url_pro_page,
"https://getsession.org/pro"
);
assert_eq!(
PROTOCOL_STRINGS.url_pro_privacy_policy,
"https://getsession.org/pro-privacy"
);
assert_eq!(
PROTOCOL_STRINGS.url_pro_roadmap,
"https://getsession.org/pro#roadmap"
);
assert_eq!(
PROTOCOL_STRINGS.url_pro_support,
"https://getsession.org/pro-support"
);
assert_eq!(
PROTOCOL_STRINGS.url_pro_terms_of_service,
"https://getsession.org/pro-terms"
);
assert_eq!(
PROTOCOL_STRINGS.url_pro_upgrade,
"https://getsession.org/pro#upgrade"
);
assert_eq!(
PROTOCOL_STRINGS.url_staking,
"https://docs.getsession.org/session-network/staking"
);
assert_eq!(
PROTOCOL_STRINGS.url_support,
"https://getsession.org/support"
);
assert_eq!(
PROTOCOL_STRINGS.url_survey,
"https://getsession.org/survey"
);
assert_eq!(
PROTOCOL_STRINGS.url_terms_of_service,
"https://getsession.org/terms-of-service"
);
assert_eq!(PROTOCOL_STRINGS.url_token, "https://token.getsession.org");
assert_eq!(
PROTOCOL_STRINGS.url_translate,
"https://getsession.org/translate"
);
}
#[test]
fn test_pro_status_values() {
assert_eq!(ProStatus::InvalidProBackendSig as u8, 1);
assert_eq!(ProStatus::InvalidUserSig as u8, 2);
assert_eq!(ProStatus::Valid as u8, 3);
assert_eq!(ProStatus::Expired as u8, 4);
}
#[test]
fn test_pro_profile_bitset() {
let mut bitset = ProProfileBitset::default();
assert!(!bitset.is_set(ProProfileFeature::ProBadge));
assert!(!bitset.is_set(ProProfileFeature::AnimatedAvatar));
bitset.set(ProProfileFeature::ProBadge);
assert!(bitset.is_set(ProProfileFeature::ProBadge));
assert!(!bitset.is_set(ProProfileFeature::AnimatedAvatar));
assert_eq!(bitset.data, 1);
bitset.set(ProProfileFeature::AnimatedAvatar);
assert!(bitset.is_set(ProProfileFeature::ProBadge));
assert!(bitset.is_set(ProProfileFeature::AnimatedAvatar));
assert_eq!(bitset.data, 3);
bitset.unset(ProProfileFeature::ProBadge);
assert!(!bitset.is_set(ProProfileFeature::ProBadge));
assert!(bitset.is_set(ProProfileFeature::AnimatedAvatar));
assert_eq!(bitset.data, 2);
}
#[test]
fn test_pro_message_bitset() {
let mut bitset = ProMessageBitset::default();
assert!(!bitset.is_set(ProMessageFeature::CharacterLimit10K));
bitset.set(ProMessageFeature::CharacterLimit10K);
assert!(bitset.is_set(ProMessageFeature::CharacterLimit10K));
assert_eq!(bitset.data, 1);
bitset.unset(ProMessageFeature::CharacterLimit10K);
assert!(!bitset.is_set(ProMessageFeature::CharacterLimit10K));
assert_eq!(bitset.data, 0);
}
#[test]
fn test_pro_features_for_utf8_short_message() {
let msg = "Hello, world!";
let result = pro_features_for_utf8(msg);
assert_eq!(result.status, ProFeaturesForMsgStatus::Success);
assert!(result.error.is_none());
assert!(!result.bitset.is_set(ProMessageFeature::CharacterLimit10K));
assert_eq!(result.codepoint_count, 13);
}
#[test]
fn test_pro_features_for_utf8_at_standard_limit() {
let msg: String = "a".repeat(PRO_STANDARD_CHARACTER_LIMIT);
let result = pro_features_for_utf8(&msg);
assert_eq!(result.status, ProFeaturesForMsgStatus::Success);
assert!(!result.bitset.is_set(ProMessageFeature::CharacterLimit10K));
assert_eq!(result.codepoint_count, PRO_STANDARD_CHARACTER_LIMIT);
}
#[test]
fn test_pro_features_for_utf8_above_standard_limit() {
let msg: String = "a".repeat(PRO_STANDARD_CHARACTER_LIMIT + 1);
let result = pro_features_for_utf8(&msg);
assert_eq!(result.status, ProFeaturesForMsgStatus::Success);
assert!(result.bitset.is_set(ProMessageFeature::CharacterLimit10K));
assert_eq!(result.codepoint_count, PRO_STANDARD_CHARACTER_LIMIT + 1);
}
#[test]
fn test_pro_features_for_utf8_at_pro_limit() {
let msg: String = "a".repeat(PRO_HIGHER_CHARACTER_LIMIT);
let result = pro_features_for_utf8(&msg);
assert_eq!(result.status, ProFeaturesForMsgStatus::Success);
assert!(result.bitset.is_set(ProMessageFeature::CharacterLimit10K));
assert_eq!(result.codepoint_count, PRO_HIGHER_CHARACTER_LIMIT);
}
#[test]
fn test_pro_features_for_utf8_above_pro_limit() {
let msg: String = "a".repeat(PRO_HIGHER_CHARACTER_LIMIT + 1);
let result = pro_features_for_utf8(&msg);
assert_eq!(result.status, ProFeaturesForMsgStatus::ExceedsCharacterLimit);
assert!(result.error.is_some());
}
#[test]
fn test_pro_features_for_utf8_multibyte() {
let msg = "\u{1F600}\u{1F601}\u{1F602}"; let result = pro_features_for_utf8(msg);
assert_eq!(result.status, ProFeaturesForMsgStatus::Success);
assert_eq!(result.codepoint_count, 3);
}
#[test]
fn test_pad_message_alignment() {
let payload = b"hello";
let padded = pad_message(payload);
assert_eq!(padded.len() % COMMUNITY_OR_1O1_MSG_PADDING, 0);
assert!(padded.len() >= payload.len() + 1);
assert_eq!(&padded[..payload.len()], payload);
assert_eq!(padded[payload.len()], PADDING_TERMINATING_BYTE);
for &b in &padded[payload.len() + 1..] {
assert_eq!(b, 0);
}
}
#[test]
fn test_pad_message_exact_boundary() {
let payload = vec![0xAA; COMMUNITY_OR_1O1_MSG_PADDING - 1];
let padded = pad_message(&payload);
assert_eq!(padded.len() % COMMUNITY_OR_1O1_MSG_PADDING, 0);
assert_eq!(padded.len(), COMMUNITY_OR_1O1_MSG_PADDING * 2);
}
#[test]
fn test_unpad_message() {
let original = b"hello world";
let padded = pad_message(original);
let unpadded = unpad_message(&padded);
assert_eq!(unpadded, original);
}
#[test]
fn test_unpad_message_empty() {
let unpadded = unpad_message(&[]);
assert_eq!(unpadded, &[] as &[u8]);
}
#[test]
fn test_proof_hash_deterministic() {
let proof = ProProof {
version: 0,
gen_index_hash: [1u8; 32],
rotating_pubkey: [2u8; 32],
expiry_unix_ts_ms: 1000000,
sig: [0u8; 64],
};
let h1 = proof.hash();
let h2 = proof.hash();
assert_eq!(h1, h2);
assert_ne!(h1, [0u8; 32]);
}
#[test]
fn test_proof_hash_different_inputs() {
let proof1 = ProProof {
version: 0,
gen_index_hash: [1u8; 32],
rotating_pubkey: [2u8; 32],
expiry_unix_ts_ms: 1000000,
sig: [0u8; 64],
};
let proof2 = ProProof {
version: 1,
gen_index_hash: [1u8; 32],
rotating_pubkey: [2u8; 32],
expiry_unix_ts_ms: 1000000,
sig: [0u8; 64],
};
assert_ne!(proof1.hash(), proof2.hash());
}
#[test]
fn test_proof_is_active() {
let proof = ProProof {
version: 0,
gen_index_hash: [0u8; 32],
rotating_pubkey: [0u8; 32],
expiry_unix_ts_ms: 5000,
sig: [0u8; 64],
};
assert!(proof.is_active(4999));
assert!(proof.is_active(5000));
assert!(!proof.is_active(5001));
}
#[test]
fn test_proof_verify_signature_invalid_pubkey_size() {
let proof = ProProof {
version: 0,
gen_index_hash: [0u8; 32],
rotating_pubkey: [0u8; 32],
expiry_unix_ts_ms: 5000,
sig: [0u8; 64],
};
let bad_key = [0u8; 16];
let result = proof.verify_signature(&bad_key);
assert!(result.is_err());
}
#[test]
fn test_proof_verify_message_invalid_sig_size() {
let proof = ProProof {
version: 0,
gen_index_hash: [0u8; 32],
rotating_pubkey: [0u8; 32],
expiry_unix_ts_ms: 5000,
sig: [0u8; 64],
};
let bad_sig = [0u8; 32];
let result = proof.verify_message(&bad_sig, b"hello");
assert!(result.is_err());
}
#[test]
fn test_proof_sign_and_verify() {
let (backend_pk, backend_sk) = crate::crypto::ed25519::ed25519_key_pair();
let (rotating_pk, rotating_sk) = crate::crypto::ed25519::ed25519_key_pair();
let mut proof = ProProof {
version: 0,
gen_index_hash: [0xAA; 32],
rotating_pubkey: rotating_pk,
expiry_unix_ts_ms: u64::MAX, sig: [0u8; 64],
};
let hash = proof.hash();
let sig = crate::crypto::ed25519::sign(&backend_sk, &hash).unwrap();
proof.sig = sig;
assert!(proof.verify_signature(&backend_pk).unwrap());
let (wrong_pk, _) = crate::crypto::ed25519::ed25519_key_pair();
assert!(!proof.verify_signature(&wrong_pk).unwrap());
let msg = b"test message";
let msg_sig = crate::crypto::ed25519::sign(&rotating_sk, msg).unwrap();
assert!(proof.verify_message(&msg_sig, msg).unwrap());
let status = proof.status(&backend_pk, 0, None).unwrap();
assert_eq!(status, ProStatus::Valid);
let status = proof
.status(
&backend_pk,
0,
Some(ProSignedMessage {
sig: &msg_sig,
msg,
}),
)
.unwrap();
assert_eq!(status, ProStatus::Valid);
}
#[test]
fn test_proof_status_expired() {
let (backend_pk, backend_sk) = crate::crypto::ed25519::ed25519_key_pair();
let mut proof = ProProof {
version: 0,
gen_index_hash: [0xBB; 32],
rotating_pubkey: [0u8; 32],
expiry_unix_ts_ms: 1000,
sig: [0u8; 64],
};
let hash = proof.hash();
proof.sig = crate::crypto::ed25519::sign(&backend_sk, &hash).unwrap();
let status = proof.status(&backend_pk, 1001, None).unwrap();
assert_eq!(status, ProStatus::Expired);
}
#[test]
fn test_destination_type_values() {
assert_eq!(DestinationType::SyncOr1o1 as u8, 0);
assert_eq!(DestinationType::Group as u8, 1);
assert_eq!(DestinationType::CommunityInbox as u8, 2);
assert_eq!(DestinationType::Community as u8, 3);
}
#[test]
fn test_envelope_flags() {
assert_eq!(envelope_flags::SOURCE, 1);
assert_eq!(envelope_flags::SOURCE_DEVICE, 2);
assert_eq!(envelope_flags::SERVER_TIMESTAMP, 4);
assert_eq!(envelope_flags::PRO_SIG, 8);
assert_eq!(envelope_flags::TIMESTAMP, 16);
}
#[test]
fn test_make_blake2b32_hasher() {
let mut hasher = make_blake2b32_hasher(BUILD_PROOF_HASH_PERSONALISATION);
hasher.update(b"test data");
let result = hasher.finalize();
assert_eq!(result.as_bytes().len(), 32);
}
}