1use crate::client::key_locker::key_locker::KeyLocker;
22use crate::tools::buckets::{BucketLocation, BucketType};
23use crate::tools::client_id::ClientId;
24use crate::tools::config;
25use crate::tools::time::TimeMillis;
26use crate::tools::types::{Hash, Id, PQCommitmentBytes, Signature, VerificationKey, VerificationKeyBytes, HASH_BYTES, ID_BYTES, SIGNATURE_BYTES};
27use crate::tools::{compression, encryption, hashing, json, signing};
28use crate::{anyhow_assert_eq, anyhow_assert_ge};
29use bytes::{Buf, BufMut, Bytes, BytesMut};
30use serde::{Deserialize, Serialize};
31use std::fmt::Debug;
32use std::sync::Arc;
33
34#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
35pub struct EncodedPostHeaderSignatureDirectV1 {}
36#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
37pub struct EncodedPostHeaderSignatureEphemeralV1 {
38 }
42
43#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
44pub struct EncodedPostHeaderSignatureDelegationV1 {
45 }
47
48#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
49pub struct EncodedPostHeaderSignatureMechanismV1 {
50 direct: Option<EncodedPostHeaderSignatureDirectV1>,
51 ephemeral: Option<EncodedPostHeaderSignatureEphemeralV1>,
52 delegation: Option<EncodedPostHeaderSignatureDelegationV1>,
53}
54
55#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
56pub struct EncodedPostHeaderV1 {
57 pub verification_key_bytes: VerificationKeyBytes,
58 pub pq_commitment_bytes: PQCommitmentBytes,
59 pub time_millis: TimeMillis,
60 pub post_length: usize,
61 pub linked_base_ids: Vec<Id>,
62 pub signature_mechanism: EncodedPostHeaderSignatureMechanismV1,
63}
64
65impl EncodedPostHeaderV1 {
66 pub fn client_id(&self) -> anyhow::Result<ClientId> {
67 ClientId::new(self.verification_key_bytes, self.pq_commitment_bytes)
68 }
69}
70
71#[derive(Debug, PartialEq, Clone)]
84pub struct EncodedPostV1 {
85 pub post_id: Id, pub signature: Signature,
87 pub header: EncodedPostHeaderV1,
88 pub post: String,
89}
90
91impl EncodedPostV1 {
92 pub fn bytes_without_body(bytes: Bytes) -> anyhow::Result<Bytes> {
96 let fixed_prefix = ID_BYTES + SIGNATURE_BYTES + 1 + HASH_BYTES * 2;
97 anyhow_assert_ge!(bytes.len(), fixed_prefix + 4 + 4, "Bytes too short for header");
98 let header_encrypted_length = u32::from_be_bytes(bytes[fixed_prefix..fixed_prefix + 4].try_into()?) as usize;
99 let length_without_body = fixed_prefix + 4 + 4 + header_encrypted_length;
100 anyhow_assert_ge!(bytes.len(), length_without_body, "Bytes too short for encrypted header");
101 Ok(bytes.slice(..length_without_body))
102 }
103
104 pub fn new(client_id: &ClientId, timestamp: TimeMillis, linked_base_ids: Vec<Id>, post: &str) -> Self {
105 let post = post.to_string();
106 let post_length = post.len();
107
108 Self {
109 post_id: Id::zero(),
110 signature: Signature::zero(),
111 header: EncodedPostHeaderV1 {
112 verification_key_bytes: client_id.verification_key_bytes,
113 pq_commitment_bytes: client_id.pq_commitment_bytes,
114 time_millis: timestamp,
115 post_length,
116 linked_base_ids,
117 signature_mechanism: EncodedPostHeaderSignatureMechanismV1 {
118 direct: None,
119 ephemeral: None,
120 delegation: None,
121 },
122 },
123
124 post,
125 }
126 }
127
128 pub async fn encode_to_bytes_direct(&mut self, key_locker: &Arc<dyn KeyLocker>) -> anyhow::Result<EncodedPostBytesV1> {
129 self.header.signature_mechanism.direct = Some(EncodedPostHeaderSignatureDirectV1 {});
130
131 let mut passwords = Vec::new();
132 {
133 let client_id = ClientId::id_from_parts(&self.header.verification_key_bytes, &self.header.pq_commitment_bytes)?;
134 passwords.push(client_id.as_bytes().to_vec());
135 let mut linked_base_ids = self.header.linked_base_ids.iter().map(|id| id.as_bytes().to_vec()).collect();
136 passwords.append(&mut linked_base_ids);
137 }
138
139 let header = json::struct_to_bytes(&self.header)?;
140 let header_compressed = compression::compress_for_size(&header)?.to_bytes();
141 let header_encrypted = encryption::encrypt_weak(&header_compressed, &passwords)?;
142 let header_encrypted_hash = hashing::hash(&header_encrypted);
143
144 let post_compressed = compression::compress_for_size(self.post.as_bytes())?.to_bytes();
145 let post_encrypted = encryption::encrypt_weak(&post_compressed, &passwords)?;
146 let post_encrypted_hash = hashing::hash(&post_encrypted);
147
148 let hash = hashing::hash_multiple(&[header_encrypted_hash.as_ref(), post_encrypted_hash.as_ref()]);
149 let signature = key_locker.sign(hash.as_ref()).await?;
150 let post_id = Id::from_hash(hashing::hash(signature.as_ref()))?;
151
152 self.signature = signature;
153 self.post_id = post_id;
154
155 let mut bytes = BytesMut::new();
156 bytes.put_slice(post_id.as_ref());
157 bytes.put_slice(signature.as_ref());
158 bytes.put_u8(1u8); bytes.put_slice(header_encrypted_hash.as_ref());
160 bytes.put_slice(post_encrypted_hash.as_ref());
161 bytes.put_u32(header_encrypted.len() as u32);
162 bytes.put_u32(post_encrypted.len() as u32);
163 bytes.put_slice(header_encrypted.as_ref());
164 bytes.put_slice(post_encrypted.as_ref());
165
166 let bytes = bytes.freeze();
167
168 Ok(EncodedPostBytesV1 {
169 length_without_body: bytes.len() - post_encrypted.len(),
170 bytes,
171 })
172 }
173
174 pub fn decode_signature_from_bytes(bytes: &[u8]) -> anyhow::Result<Signature> {
175 anyhow::ensure!(bytes.len() >= SIGNATURE_BYTES, "decode_signature_from_bytes: need {} bytes, got {}", SIGNATURE_BYTES, bytes.len());
176 Signature::from_slice(&bytes[0..SIGNATURE_BYTES])
177 }
178
179 pub fn decode_from_bytes(mut bytes: Bytes, password_base_id: &Id, expect_body: bool, decode_body: bool) -> anyhow::Result<Self> {
180 let password = password_base_id.as_ref();
181
182 anyhow_assert_ge!(bytes.remaining(), ID_BYTES, "Missing post_id");
183 let post_id = Id::from_slice(&bytes.split_to(ID_BYTES))?;
184
185 anyhow_assert_ge!(bytes.remaining(), SIGNATURE_BYTES, "Missing signature");
186 let signature = Signature::from_slice(&bytes.split_to(SIGNATURE_BYTES))?;
187
188 anyhow_assert_ge!(bytes.remaining(), 1, "Missing version");
189 let version = bytes.get_u8();
190 if 1 != version {
191 anyhow::bail!("Invalid buffer: unknown version");
192 }
193
194 anyhow_assert_ge!(bytes.remaining(), HASH_BYTES, "Missing encrypted hashes");
195 let header_encrypted_hash = Hash::from_slice(&bytes.split_to(HASH_BYTES))?;
196 anyhow_assert_ge!(bytes.remaining(), HASH_BYTES, "Missing encrypted hashes");
197 let post_encrypted_hash = Hash::from_slice(&bytes.split_to(HASH_BYTES))?;
198
199 anyhow_assert_ge!(bytes.remaining(), size_of::<u32>(), "Missing encrypted lengths");
200 let header_encrypted_length = bytes.get_u32() as usize;
201 anyhow_assert_ge!(bytes.remaining(), size_of::<u32>(), "Missing encrypted lengths");
202 let post_encrypted_length = bytes.get_u32() as usize;
203
204 anyhow_assert_ge!(bytes.remaining(), header_encrypted_length, "Missing encrypted header");
205 let header_encrypted = bytes.split_to(header_encrypted_length);
206
207 let post_encrypted = match expect_body {
208 true => {
209 anyhow_assert_ge!(bytes.remaining(), post_encrypted_length, "Missing encrypted post");
210 bytes.split_to(post_encrypted_length)
211 }
212 false => Bytes::new(),
213 };
214
215 anyhow_assert_eq!(bytes.remaining(), 0, "Unexpected remaining data");
216
217 let header_compressed = encryption::decrypt(&header_encrypted, password)?;
220 let header_bytes = compression::decompress(&header_compressed)?.to_bytes();
222 let header = json::bytes_to_struct::<EncodedPostHeaderV1>(&header_bytes)?;
224
225 anyhow_assert_eq!(header_encrypted_hash, hashing::hash(&header_encrypted));
227 if expect_body {
228 anyhow_assert_eq!(post_encrypted_hash, hashing::hash(&post_encrypted));
229 }
230
231 {
233 let hash = hashing::hash_multiple(&[header_encrypted_hash.as_ref(), post_encrypted_hash.as_ref()]);
234
235 if header.signature_mechanism.direct.is_some() {
237 let verification_key = VerificationKey::from_bytes(&header.verification_key_bytes)?;
238 signing::verify(&verification_key, &signature, hash.as_ref())?;
239 }
240 else if header.signature_mechanism.ephemeral.is_some() {
242 anyhow::bail!("Signature verification ephemeral not implemented")
243 }
244 else if header.signature_mechanism.delegation.is_some() {
246 anyhow::bail!("Signature verification delegation not implemented")
247 }
248 else {
250 anyhow::bail!("No signature verification mechanisms")
251 }
252 }
253
254 {
256 anyhow_assert_eq!(post_id, Id::from_hash(hashing::hash(signature.as_ref()))?, "post_id is not the has of signature");
257 }
258
259 let post = match expect_body && decode_body {
261 true => {
262 let post_compressed = encryption::decrypt(&post_encrypted, password)?;
264 let post_bytes = compression::decompress(&post_compressed)?.to_bytes();
266 let post = std::str::from_utf8(&post_bytes)?;
268 if post.len() != header.post_length {
269 anyhow::bail!("Post length mismatch: header.post_length={}, actual={}", header.post_length, post.len())
270 }
271 post.to_string()
272 }
273 false => String::new(),
274 };
275
276 Ok(Self { post_id, signature, header, post })
278 }
279
280 pub fn check_admissible_to_bucket(&self, bucket_location: &BucketLocation, reference_time_millis: TimeMillis) -> anyhow::Result<()> {
289 let post_bucket_time_millis = BucketLocation::round_down_to_bucket_start(self.header.time_millis, bucket_location.duration);
290 if post_bucket_time_millis != bucket_location.bucket_time_millis {
291 anyhow::bail!("post {} timestamp {} does not fall in bucket starting {}", self.post_id, self.header.time_millis, bucket_location.bucket_time_millis);
292 }
293
294 if !self.header.linked_base_ids.contains(&bucket_location.base_id) {
295 anyhow::bail!("post {} is not linked to base_id {}", self.post_id, bucket_location.base_id);
296 }
297
298 if bucket_location.bucket_type == BucketType::User && self.header.client_id()?.id != bucket_location.base_id {
299 anyhow::bail!("post {} in User bucket {} was not authored by that user", self.post_id, bucket_location.base_id);
300 }
301
302 if self.header.time_millis > reference_time_millis + config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD {
303 anyhow::bail!("post {} timestamp {} is too far after reference time {}", self.post_id, self.header.time_millis, reference_time_millis);
304 }
305
306 Ok(())
307 }
308}
309
310pub struct EncodedPostBytesV1 {
311 length_without_body: usize,
312 bytes: Bytes,
313}
314
315impl EncodedPostBytesV1 {
316 pub fn bytes_without_body(&self) -> &[u8] {
317 &self.bytes[..self.length_without_body]
318 }
319 pub fn bytes(&self) -> &[u8] {
320 self.bytes.as_ref()
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::client::key_locker::key_locker::{KeyLocker, KeyLockerManager};
328 use crate::client::key_locker::mem_key_locker::MemKeyLockerManager;
329 use crate::tools::time::{MILLIS_IN_MONTH, MILLIS_IN_SECOND, MILLIS_IN_YEAR};
330 use crate::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
331
332 #[tokio::test]
333 async fn test_post_v1_verification() -> anyhow::Result<()> {
334 let key_locker_manager = MemKeyLockerManager::new().await?;
335 let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("this is a random keyphrase".to_string()).await?;
336 let time_provider = RealTimeProvider;
337 let client_id = key_locker.client_id();
338 let timestamp = time_provider.current_time_millis();
339 let linked_base_ids = vec![client_id.id, Id::random(), Id::random(), Id::random()];
340
341 let password1 = linked_base_ids[0];
342 let password2 = linked_base_ids[1];
343
344 let mut encoded_post = EncodedPostV1::new(client_id, timestamp, linked_base_ids, "this is a test post");
345 let bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
346
347 {
349 {
350 let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &password1, true, true)?;
351 assert_eq!(encoded_post, decoded_post);
352 }
353
354 {
355 let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &password2, true, true)?;
356 assert_eq!(encoded_post, decoded_post);
357 }
358 }
359
360 {
362 let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &password2, true, false)?;
363 assert_eq!("", decoded_post.post);
364 }
365
366 {
368 let wrong_password = Id::random();
369 let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &wrong_password, true, true);
370 if decoded_post_attempt.is_ok() {
371 anyhow::bail!("Decoding with wrong password should fail")
372 }
373 }
374
375 {
377 let mut tampered_bytes = Vec::from(bytes.bytes());
378 tampered_bytes[100] ^= 0xff;
379 let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&tampered_bytes), &password1, true, true);
380 if decoded_post_attempt.is_ok() {
381 anyhow::bail!("Decoding tampered bytes should fail")
382 }
383 }
384
385 Ok(())
386 }
387
388 #[tokio::test]
389 async fn test_header_only_verification() -> anyhow::Result<()> {
390 let key_locker_manager = MemKeyLockerManager::new().await?;
391 let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("this is a random keyphrase".to_string()).await?;
392 let time_provider = RealTimeProvider;
393 let client_id = key_locker.client_id();
394 let timestamp = time_provider.current_time_millis();
395 let linked_base_ids = vec![client_id.id, Id::random(), Id::random(), Id::random()];
396
397 let password1 = linked_base_ids[0];
398 let password2 = linked_base_ids[1];
399
400 let mut encoded_post = EncodedPostV1::new(client_id, timestamp, linked_base_ids, "this is a test post");
401 let bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
402
403 {
404 let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes_without_body()), &password1, false, false)?;
405 assert_eq!(encoded_post.signature, decoded_post.signature);
406 assert_eq!(encoded_post.header, decoded_post.header);
407 assert_eq!(String::new(), decoded_post.post);
408 }
409
410 {
411 let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes_without_body()), &password2, false, false)?;
412 assert_eq!(encoded_post.signature, decoded_post.signature);
413 assert_eq!(encoded_post.header, decoded_post.header);
414 assert_eq!(String::new(), decoded_post.post);
415 }
416
417 {
419 let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &password2, false, false);
420 if decoded_post_attempt.is_ok() {
421 anyhow::bail!("Decoding with wrong too many bytes should fail")
422 }
423 }
424
425 {
426 let wrong_password = Id::random();
427 let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes_without_body()), &wrong_password, false, false);
428 if decoded_post_attempt.is_ok() {
429 anyhow::bail!("Decoding with wrong password should fail")
430 }
431 }
432
433 {
434 let mut tampered_bytes = Vec::from(bytes.bytes_without_body());
435 tampered_bytes[100] ^= 0xff;
436 let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&tampered_bytes), &password1, false, false);
437 if decoded_post_attempt.is_ok() {
438 anyhow::bail!("Decoding tampered bytes should fail")
439 }
440 }
441
442 Ok(())
443 }
444
445 #[tokio::test]
446 async fn test_post_with_no_linked_base_ids() -> anyhow::Result<()> {
447 let key_locker_manager = MemKeyLockerManager::new().await?;
448 let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("this is a random keyphrase".to_string()).await?;
449 let time_provider = RealTimeProvider;
450 let client_id = key_locker.client_id();
451 let timestamp = time_provider.current_time_millis();
452
453 let password = client_id.id;
455 let mut encoded_post = EncodedPostV1::new(client_id, timestamp, vec![], "post with no linked ids");
456 let bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
457
458 let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &password, true, true)?;
460 assert_eq!(encoded_post, decoded_post);
461
462 let wrong_password = Id::random();
464 let attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &wrong_password, true, true);
465 if attempt.is_ok() {
466 anyhow::bail!("Decoding with wrong password should fail");
467 }
468
469 Ok(())
470 }
471
472 #[tokio::test]
473 async fn test_bytes_without_body_round_trip() -> anyhow::Result<()> {
474 let key_locker_manager = MemKeyLockerManager::new().await?;
475 let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("test keyphrase for bytes_without_body".to_string()).await?;
476 let time_provider = RealTimeProvider;
477 let client_id = key_locker.client_id();
478 let timestamp = time_provider.current_time_millis();
479 let linked_base_ids = vec![client_id.id, Id::random(), Id::random()];
480
481 let password = linked_base_ids[0];
482
483 let mut encoded_post = EncodedPostV1::new(client_id, timestamp, linked_base_ids, "test post for bytes_without_body");
484 let bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
485
486 let full_bytes = Bytes::copy_from_slice(bytes.bytes());
488 let header_bytes_static = EncodedPostV1::bytes_without_body(full_bytes.clone())?;
489 let header_bytes_original = Bytes::copy_from_slice(bytes.bytes_without_body());
490 assert_eq!(header_bytes_static, header_bytes_original);
491
492 let decoded_header_only = EncodedPostV1::decode_from_bytes(header_bytes_static.clone(), &password, false, false)?;
494 assert_eq!(encoded_post.header, decoded_header_only.header);
495 assert_eq!(encoded_post.signature, decoded_header_only.signature);
496 assert_eq!(encoded_post.post_id, decoded_header_only.post_id);
497 assert_eq!("", decoded_header_only.post);
498
499 let hex_encoded = hex::encode(&header_bytes_static);
501 let hex_decoded = Bytes::from(hex::decode(&hex_encoded)?);
502 let decoded_from_hex = EncodedPostV1::decode_from_bytes(hex_decoded, &password, false, false)?;
503 assert_eq!(encoded_post.header, decoded_from_hex.header);
504
505 Ok(())
506 }
507
508 #[test]
511 fn test_decode_signature_from_bytes_empty() {
512 assert!(EncodedPostV1::decode_signature_from_bytes(&[]).is_err());
513 }
514
515 #[test]
516 fn test_decode_signature_from_bytes_too_short() {
517 assert!(EncodedPostV1::decode_signature_from_bytes(&[0u8; SIGNATURE_BYTES - 1]).is_err());
518 }
519
520 #[test]
521 fn test_decode_signature_from_bytes_exact_length() {
522 assert!(EncodedPostV1::decode_signature_from_bytes(&[0u8; SIGNATURE_BYTES]).is_ok());
524 }
525
526 #[test]
529 fn test_decode_from_bytes_empty() {
530 let password = Id::random();
531 assert!(EncodedPostV1::decode_from_bytes(Bytes::new(), &password, true, true).is_err());
532 }
533
534 #[test]
535 fn test_decode_from_bytes_too_short_for_post_id() {
536 let password = Id::random();
537 let bytes = Bytes::from_static(&[0u8; ID_BYTES - 1]);
538 assert!(EncodedPostV1::decode_from_bytes(bytes, &password, true, true).is_err());
539 }
540
541 #[test]
542 fn test_decode_from_bytes_garbage() {
543 let password = Id::random();
544 let bytes = Bytes::from_static(&[0xff; 256]);
545 assert!(EncodedPostV1::decode_from_bytes(bytes, &password, true, true).is_err());
546 }
547
548 #[test]
551 fn test_bytes_without_body_empty() {
552 assert!(EncodedPostV1::bytes_without_body(Bytes::new()).is_err());
553 }
554
555 #[test]
556 fn test_bytes_without_body_too_short() {
557 let bytes = Bytes::from_static(&[0u8; 10]);
558 assert!(EncodedPostV1::bytes_without_body(bytes).is_err());
559 }
560
561 async fn make_decoded_post(timestamp: TimeMillis, linked_base_ids_extra: Vec<Id>) -> anyhow::Result<(ClientId, EncodedPostV1)> {
565 let key_locker_manager = MemKeyLockerManager::new().await?;
566 let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("admissibility keyphrase".to_string()).await?;
567 let client_id = key_locker.client_id().clone();
568 let mut linked_base_ids = vec![client_id.id];
569 linked_base_ids.extend(linked_base_ids_extra);
570 let mut encoded_post = EncodedPostV1::new(&client_id, timestamp, linked_base_ids, "admissibility test post");
571 let encoded_post_bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
572 let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(encoded_post_bytes.bytes()), &client_id.id, true, false)?;
573 Ok((client_id, decoded_post))
574 }
575
576 #[tokio::test]
577 async fn test_check_admissible_to_bucket_accepts_matching_post() -> anyhow::Result<()> {
578 let timestamp = RealTimeProvider.current_time_millis();
579 let hashtag_id = Id::random();
580 let (client_id, decoded_post) = make_decoded_post(timestamp, vec![hashtag_id]).await?;
581
582 let user_bucket_location = BucketLocation::new(BucketType::User, client_id.id, MILLIS_IN_MONTH, timestamp)?;
583 decoded_post.check_admissible_to_bucket(&user_bucket_location, timestamp)?;
584
585 let hashtag_bucket_location = BucketLocation::new(BucketType::Hashtag, hashtag_id, MILLIS_IN_MONTH, timestamp)?;
586 decoded_post.check_admissible_to_bucket(&hashtag_bucket_location, timestamp)?;
587 Ok(())
588 }
589
590 #[tokio::test]
591 async fn test_check_admissible_to_bucket_rejects_post_from_other_bucket() -> anyhow::Result<()> {
592 let timestamp = RealTimeProvider.current_time_millis();
593 let (client_id, decoded_post) = make_decoded_post(timestamp, vec![]).await?;
594
595 let earlier_bucket_location = BucketLocation::new(BucketType::User, client_id.id, MILLIS_IN_MONTH, timestamp - MILLIS_IN_MONTH)?;
597 assert!(decoded_post.check_admissible_to_bucket(&earlier_bucket_location, timestamp).is_err());
598 let later_bucket_location = BucketLocation::new(BucketType::User, client_id.id, MILLIS_IN_MONTH, timestamp + MILLIS_IN_MONTH)?;
599 assert!(decoded_post.check_admissible_to_bucket(&later_bucket_location, timestamp + MILLIS_IN_MONTH).is_err());
600 Ok(())
601 }
602
603 #[tokio::test]
604 async fn test_check_admissible_to_bucket_rejects_unlinked_base_id() -> anyhow::Result<()> {
605 let timestamp = RealTimeProvider.current_time_millis();
606 let (_client_id, decoded_post) = make_decoded_post(timestamp, vec![]).await?;
607
608 let hashtag_bucket_location = BucketLocation::new(BucketType::Hashtag, Id::random(), MILLIS_IN_MONTH, timestamp)?;
609 assert!(decoded_post.check_admissible_to_bucket(&hashtag_bucket_location, timestamp).is_err());
610 Ok(())
611 }
612
613 #[tokio::test]
614 async fn test_check_admissible_to_bucket_rejects_foreign_author_in_user_bucket() -> anyhow::Result<()> {
615 let timestamp = RealTimeProvider.current_time_millis();
616 let victim_id = Id::random();
617 let (_client_id, decoded_post) = make_decoded_post(timestamp, vec![victim_id]).await?;
619
620 let victim_user_bucket_location = BucketLocation::new(BucketType::User, victim_id, MILLIS_IN_MONTH, timestamp)?;
621 assert!(decoded_post.check_admissible_to_bucket(&victim_user_bucket_location, timestamp).is_err());
622
623 let victim_mention_bucket_location = BucketLocation::new(BucketType::Mention, victim_id, MILLIS_IN_MONTH, timestamp)?;
625 decoded_post.check_admissible_to_bucket(&victim_mention_bucket_location, timestamp)?;
626 Ok(())
627 }
628
629 #[tokio::test]
630 async fn test_check_admissible_to_bucket_rejects_post_newer_than_reference() -> anyhow::Result<()> {
631 let timestamp = RealTimeProvider.current_time_millis();
632 let (client_id, decoded_post) = make_decoded_post(timestamp, vec![]).await?;
633 let user_bucket_location = BucketLocation::new(BucketType::User, client_id.id, MILLIS_IN_YEAR, timestamp)?;
634
635 decoded_post.check_admissible_to_bucket(&user_bucket_location, timestamp - config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD)?;
637
638 let too_early_reference_time_millis = timestamp - config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD - MILLIS_IN_SECOND;
640 assert!(decoded_post.check_admissible_to_bucket(&user_bucket_location, too_early_reference_time_millis).is_err());
641 Ok(())
642 }
643
644 #[cfg(not(target_arch = "wasm32"))]
645 mod bolero_fuzz {
646 use crate::protocol::posting::encoded_post::EncodedPostV1;
647 use crate::tools::types::Id;
648 use bytes::Bytes;
649
650 #[test]
651 fn fuzz_decode_from_bytes() {
652 bolero::check!().for_each(|data: &[u8]| {
653 let password = Id::zero();
654 let _ = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(data), &password, true, true);
655 });
656 }
657
658 #[test]
659 fn fuzz_decode_signature_from_bytes() {
660 bolero::check!().for_each(|data: &[u8]| {
661 let _ = EncodedPostV1::decode_signature_from_bytes(data);
662 });
663 }
664
665 #[test]
666 fn fuzz_bytes_without_body() {
667 bolero::check!().for_each(|data: &[u8]| {
668 let _ = EncodedPostV1::bytes_without_body(Bytes::copy_from_slice(data));
669 });
670 }
671 }
672}