use crate::client::key_locker::key_locker::KeyLocker;
use crate::tools::client_id::ClientId;
use crate::tools::time::TimeMillis;
use crate::tools::types::{Hash, Id, PQCommitmentBytes, Signature, VerificationKey, VerificationKeyBytes, HASH_BYTES, ID_BYTES, SIGNATURE_BYTES};
use crate::tools::{compression, encryption, hashing, json, signing};
use crate::{anyhow_assert_eq, anyhow_assert_ge};
use bytes::{Buf, BufMut, Bytes, BytesMut};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::sync::Arc;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct EncodedPostHeaderSignatureDirectV1 {}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct EncodedPostHeaderSignatureEphemeralV1 {
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct EncodedPostHeaderSignatureDelegationV1 {
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct EncodedPostHeaderSignatureMechanismV1 {
direct: Option<EncodedPostHeaderSignatureDirectV1>,
ephemeral: Option<EncodedPostHeaderSignatureEphemeralV1>,
delegation: Option<EncodedPostHeaderSignatureDelegationV1>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct EncodedPostHeaderV1 {
pub verification_key_bytes: VerificationKeyBytes,
pub pq_commitment_bytes: PQCommitmentBytes,
pub time_millis: TimeMillis,
pub post_length: usize,
pub linked_base_ids: Vec<Id>,
pub signature_mechanism: EncodedPostHeaderSignatureMechanismV1,
}
impl EncodedPostHeaderV1 {
pub fn client_id(&self) -> anyhow::Result<ClientId> {
ClientId::new(self.verification_key_bytes, self.pq_commitment_bytes)
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct EncodedPostV1 {
pub post_id: Id, pub signature: Signature,
pub header: EncodedPostHeaderV1,
pub post: String,
}
impl EncodedPostV1 {
pub fn bytes_without_body(bytes: Bytes) -> anyhow::Result<Bytes> {
let fixed_prefix = ID_BYTES + SIGNATURE_BYTES + 1 + HASH_BYTES * 2;
anyhow_assert_ge!(bytes.len(), fixed_prefix + 4 + 4, "Bytes too short for header");
let header_encrypted_length = u32::from_be_bytes(bytes[fixed_prefix..fixed_prefix + 4].try_into()?) as usize;
let length_without_body = fixed_prefix + 4 + 4 + header_encrypted_length;
anyhow_assert_ge!(bytes.len(), length_without_body, "Bytes too short for encrypted header");
Ok(bytes.slice(..length_without_body))
}
pub fn new(client_id: &ClientId, timestamp: TimeMillis, linked_base_ids: Vec<Id>, post: &str) -> Self {
let post = post.to_string();
let post_length = post.len();
Self {
post_id: Id::zero(),
signature: Signature::zero(),
header: EncodedPostHeaderV1 {
verification_key_bytes: client_id.verification_key_bytes,
pq_commitment_bytes: client_id.pq_commitment_bytes,
time_millis: timestamp,
post_length,
linked_base_ids,
signature_mechanism: EncodedPostHeaderSignatureMechanismV1 {
direct: None,
ephemeral: None,
delegation: None,
},
},
post,
}
}
pub async fn encode_to_bytes_direct(&mut self, key_locker: &Arc<dyn KeyLocker>) -> anyhow::Result<EncodedPostBytesV1> {
self.header.signature_mechanism.direct = Some(EncodedPostHeaderSignatureDirectV1 {});
let mut passwords = Vec::new();
{
let client_id = ClientId::id_from_parts(&self.header.verification_key_bytes, &self.header.pq_commitment_bytes)?;
passwords.push(client_id.as_bytes().to_vec());
let mut linked_base_ids = self.header.linked_base_ids.iter().map(|id| id.as_bytes().to_vec()).collect();
passwords.append(&mut linked_base_ids);
}
let header = json::struct_to_bytes(&self.header)?;
let header_compressed = compression::compress_for_size(&header)?.to_bytes();
let header_encrypted = encryption::encrypt_weak(&header_compressed, &passwords)?;
let header_encrypted_hash = hashing::hash(&header_encrypted);
let post_compressed = compression::compress_for_size(self.post.as_bytes())?.to_bytes();
let post_encrypted = encryption::encrypt_weak(&post_compressed, &passwords)?;
let post_encrypted_hash = hashing::hash(&post_encrypted);
let hash = hashing::hash_multiple(&[header_encrypted_hash.as_ref(), post_encrypted_hash.as_ref()]);
let signature = key_locker.sign(hash.as_ref()).await?;
let post_id = Id::from_hash(hashing::hash(signature.as_ref()))?;
self.signature = signature;
self.post_id = post_id;
let mut bytes = BytesMut::new();
bytes.put_slice(post_id.as_ref());
bytes.put_slice(signature.as_ref());
bytes.put_u8(1u8); bytes.put_slice(header_encrypted_hash.as_ref());
bytes.put_slice(post_encrypted_hash.as_ref());
bytes.put_u32(header_encrypted.len() as u32);
bytes.put_u32(post_encrypted.len() as u32);
bytes.put_slice(header_encrypted.as_ref());
bytes.put_slice(post_encrypted.as_ref());
let bytes = bytes.freeze();
Ok(EncodedPostBytesV1 {
length_without_body: bytes.len() - post_encrypted.len(),
bytes,
})
}
pub fn decode_signature_from_bytes(bytes: &[u8]) -> anyhow::Result<Signature> {
anyhow::ensure!(bytes.len() >= SIGNATURE_BYTES, "decode_signature_from_bytes: need {} bytes, got {}", SIGNATURE_BYTES, bytes.len());
Signature::from_slice(&bytes[0..SIGNATURE_BYTES])
}
pub fn decode_from_bytes(mut bytes: Bytes, password_base_id: &Id, expect_body: bool, decode_body: bool) -> anyhow::Result<Self> {
let password = password_base_id.as_ref();
anyhow_assert_ge!(bytes.remaining(), ID_BYTES, "Missing post_id");
let post_id = Id::from_slice(&bytes.split_to(ID_BYTES))?;
anyhow_assert_ge!(bytes.remaining(), SIGNATURE_BYTES, "Missing signature");
let signature = Signature::from_slice(&bytes.split_to(SIGNATURE_BYTES))?;
anyhow_assert_ge!(bytes.remaining(), 1, "Missing version");
let version = bytes.get_u8();
if 1 != version {
anyhow::bail!("Invalid buffer: unknown version");
}
anyhow_assert_ge!(bytes.remaining(), HASH_BYTES, "Missing encrypted hashes");
let header_encrypted_hash = Hash::from_slice(&bytes.split_to(HASH_BYTES))?;
anyhow_assert_ge!(bytes.remaining(), HASH_BYTES, "Missing encrypted hashes");
let post_encrypted_hash = Hash::from_slice(&bytes.split_to(HASH_BYTES))?;
anyhow_assert_ge!(bytes.remaining(), size_of::<u32>(), "Missing encrypted lengths");
let header_encrypted_length = bytes.get_u32() as usize;
anyhow_assert_ge!(bytes.remaining(), size_of::<u32>(), "Missing encrypted lengths");
let post_encrypted_length = bytes.get_u32() as usize;
anyhow_assert_ge!(bytes.remaining(), header_encrypted_length, "Missing encrypted header");
let header_encrypted = bytes.split_to(header_encrypted_length);
let post_encrypted = match expect_body {
true => {
anyhow_assert_ge!(bytes.remaining(), post_encrypted_length, "Missing encrypted post");
bytes.split_to(post_encrypted_length)
}
false => Bytes::new(),
};
anyhow_assert_eq!(bytes.remaining(), 0, "Unexpected remaining data");
let header_compressed = encryption::decrypt(&header_encrypted, password)?;
let header_bytes = compression::decompress(&header_compressed)?.to_bytes();
let header = json::bytes_to_struct::<EncodedPostHeaderV1>(&header_bytes)?;
anyhow_assert_eq!(header_encrypted_hash, hashing::hash(&header_encrypted));
if expect_body {
anyhow_assert_eq!(post_encrypted_hash, hashing::hash(&post_encrypted));
}
{
let hash = hashing::hash_multiple(&[header_encrypted_hash.as_ref(), post_encrypted_hash.as_ref()]);
if header.signature_mechanism.direct.is_some() {
let verification_key = VerificationKey::from_bytes(&header.verification_key_bytes)?;
signing::verify(&verification_key, &signature, hash.as_ref())?;
}
else if header.signature_mechanism.ephemeral.is_some() {
anyhow::bail!("Signature verification ephemeral not implemented")
}
else if header.signature_mechanism.delegation.is_some() {
anyhow::bail!("Signature verification delegation not implemented")
}
else {
anyhow::bail!("No signature verification mechanisms")
}
}
{
anyhow_assert_eq!(post_id, Id::from_hash(hashing::hash(signature.as_ref()))?, "post_id is not the has of signature");
}
let post = match expect_body && decode_body {
true => {
let post_compressed = encryption::decrypt(&post_encrypted, password)?;
let post_bytes = compression::decompress(&post_compressed)?.to_bytes();
let post = std::str::from_utf8(&post_bytes)?;
if post.len() != header.post_length {
anyhow::bail!("Post length mismatch: header.post_length={}, actual={}", header.post_length, post.len())
}
post.to_string()
}
false => String::new(),
};
Ok(Self { post_id, signature, header, post })
}
}
pub struct EncodedPostBytesV1 {
length_without_body: usize,
bytes: Bytes,
}
impl EncodedPostBytesV1 {
pub fn bytes_without_body(&self) -> &[u8] {
&self.bytes[..self.length_without_body]
}
pub fn bytes(&self) -> &[u8] {
self.bytes.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::key_locker::key_locker::{KeyLocker, KeyLockerManager};
use crate::client::key_locker::mem_key_locker::MemKeyLockerManager;
use crate::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
#[tokio::test]
async fn test_post_v1_verification() -> anyhow::Result<()> {
let key_locker_manager = MemKeyLockerManager::new().await?;
let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("this is a random keyphrase".to_string()).await?;
let time_provider = RealTimeProvider;
let client_id = key_locker.client_id();
let timestamp = time_provider.current_time_millis();
let linked_base_ids = vec![client_id.id, Id::random(), Id::random(), Id::random()];
let password1 = linked_base_ids[0];
let password2 = linked_base_ids[1];
let mut encoded_post = EncodedPostV1::new(client_id, timestamp, linked_base_ids, "this is a test post");
let bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
{
{
let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &password1, true, true)?;
assert_eq!(encoded_post, decoded_post);
}
{
let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &password2, true, true)?;
assert_eq!(encoded_post, decoded_post);
}
}
{
let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &password2, true, false)?;
assert_eq!("", decoded_post.post);
}
{
let wrong_password = Id::random();
let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &wrong_password, true, true);
if decoded_post_attempt.is_ok() {
anyhow::bail!("Decoding with wrong password should fail")
}
}
{
let mut tampered_bytes = Vec::from(bytes.bytes());
tampered_bytes[100] ^= 0xff;
let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&tampered_bytes), &password1, true, true);
if decoded_post_attempt.is_ok() {
anyhow::bail!("Decoding tampered bytes should fail")
}
}
Ok(())
}
#[tokio::test]
async fn test_header_only_verification() -> anyhow::Result<()> {
let key_locker_manager = MemKeyLockerManager::new().await?;
let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("this is a random keyphrase".to_string()).await?;
let time_provider = RealTimeProvider;
let client_id = key_locker.client_id();
let timestamp = time_provider.current_time_millis();
let linked_base_ids = vec![client_id.id, Id::random(), Id::random(), Id::random()];
let password1 = linked_base_ids[0];
let password2 = linked_base_ids[1];
let mut encoded_post = EncodedPostV1::new(client_id, timestamp, linked_base_ids, "this is a test post");
let bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
{
let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes_without_body()), &password1, false, false)?;
assert_eq!(encoded_post.signature, decoded_post.signature);
assert_eq!(encoded_post.header, decoded_post.header);
assert_eq!(String::new(), decoded_post.post);
}
{
let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes_without_body()), &password2, false, false)?;
assert_eq!(encoded_post.signature, decoded_post.signature);
assert_eq!(encoded_post.header, decoded_post.header);
assert_eq!(String::new(), decoded_post.post);
}
{
let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &password2, false, false);
if decoded_post_attempt.is_ok() {
anyhow::bail!("Decoding with wrong too many bytes should fail")
}
}
{
let wrong_password = Id::random();
let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes_without_body()), &wrong_password, false, false);
if decoded_post_attempt.is_ok() {
anyhow::bail!("Decoding with wrong password should fail")
}
}
{
let mut tampered_bytes = Vec::from(bytes.bytes_without_body());
tampered_bytes[100] ^= 0xff;
let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&tampered_bytes), &password1, false, false);
if decoded_post_attempt.is_ok() {
anyhow::bail!("Decoding tampered bytes should fail")
}
}
Ok(())
}
#[tokio::test]
async fn test_post_with_no_linked_base_ids() -> anyhow::Result<()> {
let key_locker_manager = MemKeyLockerManager::new().await?;
let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("this is a random keyphrase".to_string()).await?;
let time_provider = RealTimeProvider;
let client_id = key_locker.client_id();
let timestamp = time_provider.current_time_millis();
let password = client_id.id;
let mut encoded_post = EncodedPostV1::new(client_id, timestamp, vec![], "post with no linked ids");
let bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &password, true, true)?;
assert_eq!(encoded_post, decoded_post);
let wrong_password = Id::random();
let attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &wrong_password, true, true);
if attempt.is_ok() {
anyhow::bail!("Decoding with wrong password should fail");
}
Ok(())
}
#[tokio::test]
async fn test_bytes_without_body_round_trip() -> anyhow::Result<()> {
let key_locker_manager = MemKeyLockerManager::new().await?;
let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("test keyphrase for bytes_without_body".to_string()).await?;
let time_provider = RealTimeProvider;
let client_id = key_locker.client_id();
let timestamp = time_provider.current_time_millis();
let linked_base_ids = vec![client_id.id, Id::random(), Id::random()];
let password = linked_base_ids[0];
let mut encoded_post = EncodedPostV1::new(client_id, timestamp, linked_base_ids, "test post for bytes_without_body");
let bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
let full_bytes = Bytes::copy_from_slice(bytes.bytes());
let header_bytes_static = EncodedPostV1::bytes_without_body(full_bytes.clone())?;
let header_bytes_original = Bytes::copy_from_slice(bytes.bytes_without_body());
assert_eq!(header_bytes_static, header_bytes_original);
let decoded_header_only = EncodedPostV1::decode_from_bytes(header_bytes_static.clone(), &password, false, false)?;
assert_eq!(encoded_post.header, decoded_header_only.header);
assert_eq!(encoded_post.signature, decoded_header_only.signature);
assert_eq!(encoded_post.post_id, decoded_header_only.post_id);
assert_eq!("", decoded_header_only.post);
let hex_encoded = hex::encode(&header_bytes_static);
let hex_decoded = Bytes::from(hex::decode(&hex_encoded)?);
let decoded_from_hex = EncodedPostV1::decode_from_bytes(hex_decoded, &password, false, false)?;
assert_eq!(encoded_post.header, decoded_from_hex.header);
Ok(())
}
#[test]
fn test_decode_signature_from_bytes_empty() {
assert!(EncodedPostV1::decode_signature_from_bytes(&[]).is_err());
}
#[test]
fn test_decode_signature_from_bytes_too_short() {
assert!(EncodedPostV1::decode_signature_from_bytes(&[0u8; SIGNATURE_BYTES - 1]).is_err());
}
#[test]
fn test_decode_signature_from_bytes_exact_length() {
assert!(EncodedPostV1::decode_signature_from_bytes(&[0u8; SIGNATURE_BYTES]).is_ok());
}
#[test]
fn test_decode_from_bytes_empty() {
let password = Id::random();
assert!(EncodedPostV1::decode_from_bytes(Bytes::new(), &password, true, true).is_err());
}
#[test]
fn test_decode_from_bytes_too_short_for_post_id() {
let password = Id::random();
let bytes = Bytes::from_static(&[0u8; ID_BYTES - 1]);
assert!(EncodedPostV1::decode_from_bytes(bytes, &password, true, true).is_err());
}
#[test]
fn test_decode_from_bytes_garbage() {
let password = Id::random();
let bytes = Bytes::from_static(&[0xff; 256]);
assert!(EncodedPostV1::decode_from_bytes(bytes, &password, true, true).is_err());
}
#[test]
fn test_bytes_without_body_empty() {
assert!(EncodedPostV1::bytes_without_body(Bytes::new()).is_err());
}
#[test]
fn test_bytes_without_body_too_short() {
let bytes = Bytes::from_static(&[0u8; 10]);
assert!(EncodedPostV1::bytes_without_body(bytes).is_err());
}
#[cfg(not(target_arch = "wasm32"))]
mod bolero_fuzz {
use crate::protocol::posting::encoded_post::EncodedPostV1;
use crate::tools::types::Id;
use bytes::Bytes;
#[test]
fn fuzz_decode_from_bytes() {
bolero::check!().for_each(|data: &[u8]| {
let password = Id::zero();
let _ = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(data), &password, true, true);
});
}
#[test]
fn fuzz_decode_signature_from_bytes() {
bolero::check!().for_each(|data: &[u8]| {
let _ = EncodedPostV1::decode_signature_from_bytes(data);
});
}
#[test]
fn fuzz_bytes_without_body() {
bolero::check!().for_each(|data: &[u8]| {
let _ = EncodedPostV1::bytes_without_body(Bytes::copy_from_slice(data));
});
}
}
}