hashiverse_lib/client/meta_post/
meta_post_crypto.rs1use crate::client::key_locker::key_locker::KeyLocker;
17use crate::client::meta_post::meta_post::MetaPostPrivateV1;
18use crate::tools::types::Salt;
19use crate::tools::{compression, encryption, json};
20
21const META_POST_ENCRYPTION_CONTEXT: &[u8] = b"hashiverse-meta-post-encryption";
22
23pub async fn derive_meta_post_encryption_key(key_locker: &dyn KeyLocker, salt: &Salt) -> anyhow::Result<Vec<u8>> {
30 let mut message = Vec::with_capacity(META_POST_ENCRYPTION_CONTEXT.len() + salt.as_ref().len());
31 message.extend_from_slice(META_POST_ENCRYPTION_CONTEXT);
32 message.extend_from_slice(salt.as_ref());
33
34 let signature = key_locker.sign(&message).await?;
35 let key_hash = blake3::hash(signature.as_ref());
36 Ok(key_hash.as_bytes().to_vec())
37}
38
39pub async fn encrypt_private_section(key_locker: &dyn KeyLocker, salt: &Salt, private: &MetaPostPrivateV1) -> anyhow::Result<String> {
41 let symmetric_key = derive_meta_post_encryption_key(key_locker, salt).await?;
42 let plaintext = json::struct_to_bytes(private)?;
43 let compressed = compression::compress_for_size(&plaintext)?.to_bytes();
44 let encrypted = encryption::encrypt_strong(&compressed, &vec![symmetric_key])?;
45 Ok(hex::encode(encrypted))
46}
47
48pub async fn decrypt_private_section(key_locker: &dyn KeyLocker, salt: &Salt, encrypted_hex: &str) -> anyhow::Result<MetaPostPrivateV1> {
50 let symmetric_key = derive_meta_post_encryption_key(key_locker, salt).await?;
51 let encrypted = hex::decode(encrypted_hex)?;
52 let compressed = encryption::decrypt(&encrypted, &symmetric_key)?;
53 let plaintext = compression::decompress(&compressed)?.to_bytes();
54 let private: MetaPostPrivateV1 = json::bytes_to_struct(&plaintext)?;
55 Ok(private)
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::client::key_locker::mem_key_locker::MemKeyLockerManager;
62 use crate::client::key_locker::key_locker::KeyLockerManager;
63 use crate::client::meta_post::meta_post::VersionedField;
64 use crate::tools::time::TimeMillis;
65
66 fn t(millis: i64) -> TimeMillis { TimeMillis(millis) }
67
68 #[tokio::test]
69 async fn encrypt_decrypt_private_section_roundtrip() -> anyhow::Result<()> {
70 let key_locker_manager = MemKeyLockerManager::new().await?;
71 let key_locker = key_locker_manager.create("test_phrase".to_string()).await?;
72 let salt = Salt::random();
73
74 let mut private = MetaPostPrivateV1::empty();
75 private.followed_client_ids.insert("client_abc".to_string(), VersionedField::new(true, t(100)));
76 private.followed_hashtags.insert("rust".to_string(), VersionedField::new(true, t(200)));
77 private.content_thresholds.insert(101, VersionedField::new(100, t(300)));
78 private.skip_warnings_for_followed = VersionedField::new(true, t(400));
79
80 let encrypted_hex = encrypt_private_section(key_locker.as_ref(), &salt, &private).await?;
81 let decrypted = decrypt_private_section(key_locker.as_ref(), &salt, &encrypted_hex).await?;
82
83 assert_eq!(private, decrypted);
84 Ok(())
85 }
86
87 #[tokio::test]
88 async fn different_salt_produces_different_key() -> anyhow::Result<()> {
89 let key_locker_manager = MemKeyLockerManager::new().await?;
90 let key_locker = key_locker_manager.create("test_phrase".to_string()).await?;
91
92 let salt_a = Salt::random();
93 let salt_b = Salt::random();
94
95 let key_a = derive_meta_post_encryption_key(key_locker.as_ref(), &salt_a).await?;
96 let key_b = derive_meta_post_encryption_key(key_locker.as_ref(), &salt_b).await?;
97
98 assert_ne!(key_a, key_b);
99 Ok(())
100 }
101
102 #[tokio::test]
103 async fn same_salt_produces_same_key() -> anyhow::Result<()> {
104 let key_locker_manager = MemKeyLockerManager::new().await?;
105 let key_locker = key_locker_manager.create("test_phrase".to_string()).await?;
106
107 let salt = Salt::random();
108
109 let key_1 = derive_meta_post_encryption_key(key_locker.as_ref(), &salt).await?;
110 let key_2 = derive_meta_post_encryption_key(key_locker.as_ref(), &salt).await?;
111
112 assert_eq!(key_1, key_2);
113 Ok(())
114 }
115
116 #[tokio::test]
117 async fn different_user_cannot_decrypt() -> anyhow::Result<()> {
118 let key_locker_manager = MemKeyLockerManager::new().await?;
119 let key_locker_a = key_locker_manager.create("client_a_phrase".to_string()).await?;
120 let key_locker_b = key_locker_manager.create("client_b_phrase".to_string()).await?;
121
122 let salt = Salt::random();
123 let private = MetaPostPrivateV1::empty();
124
125 let encrypted_hex = encrypt_private_section(key_locker_a.as_ref(), &salt, &private).await?;
126 let decrypt_result = decrypt_private_section(key_locker_b.as_ref(), &salt, &encrypted_hex).await;
127
128 assert!(decrypt_result.is_err(), "Different user should not be able to decrypt");
129 Ok(())
130 }
131
132 #[tokio::test]
133 async fn encrypt_decrypt_with_tombstones() -> anyhow::Result<()> {
134 let key_locker_manager = MemKeyLockerManager::new().await?;
135 let key_locker = key_locker_manager.create("test_phrase".to_string()).await?;
136 let salt = Salt::random();
137
138 let mut private = MetaPostPrivateV1::empty();
139 private.followed_client_ids.insert("deleted_client".to_string(), VersionedField::tombstone(t(999)));
140 private.skip_warnings_for_followed = VersionedField::tombstone(t(888));
141
142 let encrypted_hex = encrypt_private_section(key_locker.as_ref(), &salt, &private).await?;
143 let decrypted = decrypt_private_section(key_locker.as_ref(), &salt, &encrypted_hex).await?;
144
145 assert_eq!(private, decrypted);
146 assert!(decrypted.followed_client_ids["deleted_client"].is_tombstone());
147 assert!(decrypted.skip_warnings_for_followed.is_tombstone());
148 Ok(())
149 }
150}