core_crypto/proteus/
prekey.rs1use core_crypto_keystore::{Transaction, entities::ProteusPrekey, traits::FetchFromDatabase as _};
2use proteus_wasm::keys::PreKeyBundle;
3
4use super::ProteusCentral;
5use crate::{KeystoreError, ProteusError, Result};
6
7impl ProteusCentral {
8 pub(crate) async fn new_prekey(&self, id: u16, transaction: &Transaction) -> Result<Vec<u8>> {
11 use proteus_wasm::keys::{PreKey, PreKeyId};
12
13 let prekey_id = PreKeyId::new(id);
14 let prekey = PreKey::new(prekey_id);
15 let keystore_prekey = core_crypto_keystore::entities::ProteusPrekey::from_raw(
16 id,
17 prekey.serialise().map_err(ProteusError::wrap("serialising prekey"))?,
18 );
19 let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &prekey);
20 let bundle = bundle
21 .serialise()
22 .map_err(ProteusError::wrap("serialising prekey bundle"))?;
23 transaction
24 .save(keystore_prekey)
25 .await
26 .map_err(KeystoreError::wrap("saving keystore prekey"))?;
27 Ok(bundle)
28 }
29
30 pub(crate) async fn new_prekey_auto(&self, transaction: &Transaction) -> Result<(u16, Vec<u8>)> {
34 let id = core_crypto_keystore::entities::ProteusPrekey::get_free_id(transaction)
35 .await
36 .map_err(KeystoreError::wrap("getting proteus prekey by id"))?;
37 Ok((id, self.new_prekey(id, transaction).await?))
38 }
39
40 pub fn last_resort_prekey_id() -> u16 {
42 proteus_wasm::keys::MAX_PREKEY_ID.value()
43 }
44
45 pub(crate) async fn last_resort_prekey(&self, transaction: &Transaction) -> Result<Vec<u8>> {
48 let last_resort = if let Some(last_resort) = transaction
49 .get::<core_crypto_keystore::entities::ProteusPrekey>(&Self::last_resort_prekey_id())
50 .await
51 .map_err(KeystoreError::wrap("finding proteus prekey"))?
52 {
53 proteus_wasm::keys::PreKey::deserialise(&last_resort.prekey)
54 .map_err(ProteusError::wrap("deserialising proteus prekey"))?
55 } else {
56 let last_resort = proteus_wasm::keys::PreKey::last_resort();
57 let prekey = last_resort
58 .serialise()
59 .map_err(ProteusError::wrap("serializing last resort prekey"))?;
60
61 transaction
62 .save(ProteusPrekey::from_raw(Self::last_resort_prekey_id(), prekey))
63 .await
64 .map_err(KeystoreError::wrap("storing proteus last resort prekey"))?;
65
66 last_resort
67 };
68
69 let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &last_resort);
70 let bundle = bundle
71 .serialise()
72 .map_err(ProteusError::wrap("serialising prekey bundle"))?;
73
74 Ok(bundle)
75 }
76
77 pub fn fingerprint_prekeybundle(prekey: &[u8]) -> Result<String> {
82 let prekey = PreKeyBundle::deserialise(prekey).map_err(ProteusError::wrap("deserialising prekey bundle"))?;
83 Ok(prekey.identity_key.fingerprint())
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use core_crypto_keystore::DatabaseKey;
90
91 use super::*;
92 use crate::test_utils::{proteus_utils::*, *};
93
94 #[macro_rules_attribute::apply(smol_macros::test)]
95 async fn can_produce_proteus_consumed_prekeys() {
96 #[cfg(not(target_os = "unknown"))]
97 let (path, db_file) = tmp_db_file();
98 #[cfg(target_os = "unknown")]
99 let (path, _) = tmp_db_file();
100
101 let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
102
103 let key = DatabaseKey::generate();
104 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
105 let tx = keystore.new_transaction().await.unwrap();
106 let mut alice = ProteusCentral::try_new(&tx).await.unwrap();
107
108 let mut bob = CryptoboxLike::init();
109
110 let alice_prekey_bundle_ser = alice.new_prekey(1, &tx).await.unwrap();
111
112 bob.init_session_from_prekey_bundle(&session_id, &alice_prekey_bundle_ser);
113 let message = b"Hello world!";
114 let encrypted = bob.encrypt(&session_id, message);
115
116 let (_, decrypted) = alice.session_from_message(&tx, &session_id, &encrypted).await.unwrap();
117
118 assert_eq!(message, decrypted.as_slice());
119
120 let encrypted = alice.encrypt(&tx, &session_id, message).await.unwrap();
121 let decrypted = bob.decrypt(&session_id, &encrypted).await;
122
123 assert_eq!(message, decrypted.as_slice());
124 tx.commit().await.unwrap();
125 #[cfg(not(target_os = "unknown"))]
126 drop(db_file);
127 }
128
129 #[macro_rules_attribute::apply(smol_macros::test)]
135 async fn auto_prekeys_fill_holes() {
136 use core_crypto_keystore::entities::ProteusPrekey;
137 const GAP_AMOUNT: usize = 5;
138 const EXTRA_AMOUNT: u16 = 10;
140 const ID_TEST_RANGE: std::ops::RangeInclusive<u16> = 1..=30;
141
142 async fn claim(alice: &ProteusCentral, tx: &Transaction, count: usize) -> Vec<u16> {
144 let mut ids = Vec::with_capacity(count);
145 for _ in 0..count {
146 let (pk_id, pkb) = alice.new_prekey_auto(tx).await.unwrap();
147 let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
148 assert_eq!(prekey.prekey_id.value(), pk_id, "the bundle must carry the assigned id");
149 ids.push(pk_id);
150 }
151 ids
152 }
153
154 fn pick_gap_ids(rng: &mut impl rand::Rng, count: usize) -> Vec<u16> {
156 let mut ids = Vec::with_capacity(count);
157 while ids.len() < count {
158 let id = rng.gen_range(ID_TEST_RANGE);
159 if !ids.contains(&id) {
160 ids.push(id);
161 }
162 }
163 ids.sort();
164 ids
165 }
166
167 fn ascending(ids: &[u16]) -> Vec<u16> {
168 let mut ids = ids.to_owned();
169 ids.sort();
170 ids
171 }
172
173 #[cfg(not(target_os = "unknown"))]
174 let (path, db_file) = tmp_db_file();
175 #[cfg(target_os = "unknown")]
176 let (path, _) = tmp_db_file();
177
178 let key = DatabaseKey::generate();
179 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
180 let tx = keystore.new_transaction().await.unwrap();
181 let alice = ProteusCentral::try_new(&tx).await.unwrap();
182
183 let claimed = claim(&alice, &tx, ID_TEST_RANGE.count()).await;
185 assert_eq!(claimed, ID_TEST_RANGE.collect::<Vec<_>>());
186
187 let mut rng = rand::thread_rng();
188
189 let gap_ids = pick_gap_ids(&mut rng, GAP_AMOUNT);
191 for gap_id in &gap_ids {
192 tx.remove::<ProteusPrekey>(gap_id).await.unwrap();
193 }
194 let claimed = claim(&alice, &tx, GAP_AMOUNT).await;
195 assert_eq!(
196 ascending(&claimed),
197 gap_ids,
198 "every deleted id must be reassigned exactly once"
199 );
200
201 let gap_ids = pick_gap_ids(&mut rng, GAP_AMOUNT);
204 for gap_id in &gap_ids {
205 tx.remove::<ProteusPrekey>(gap_id).await.unwrap();
206 }
207 let claimed = claim(&alice, &tx, GAP_AMOUNT + EXTRA_AMOUNT as usize).await;
208 let (filled, extended) = claimed.split_at(GAP_AMOUNT);
209 assert_eq!(
210 ascending(filled),
211 gap_ids,
212 "holes must all be filled before the id space is extended"
213 );
214 let high_water_mark = *ID_TEST_RANGE.end();
215 assert_eq!(
216 extended,
217 (high_water_mark + 1..=high_water_mark + EXTRA_AMOUNT).collect::<Vec<_>>(),
218 "once no holes remain, ids extend the id space in ascending order"
219 );
220
221 tx.commit().await.unwrap();
222 #[cfg(not(target_os = "unknown"))]
223 drop(db_file);
224 }
225
226 #[macro_rules_attribute::apply(smol_macros::test)]
229 async fn last_resort_prekey_does_not_exhaust_id_space() {
230 #[cfg(not(target_os = "unknown"))]
231 let (path, db_file) = tmp_db_file();
232 #[cfg(target_os = "unknown")]
233 let (path, _) = tmp_db_file();
234
235 let key = DatabaseKey::generate();
236 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
237
238 let tx = keystore.new_transaction().await.unwrap();
240 let alice = ProteusCentral::try_new(&tx).await.unwrap();
241 alice.last_resort_prekey(&tx).await.unwrap();
242 let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
243 assert_eq!(
244 pk_id, 1,
245 "an uncommitted last resort prekey must not block auto prekeys"
246 );
247 tx.commit().await.unwrap();
248
249 let tx = keystore.new_transaction().await.unwrap();
251 let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
252 assert_eq!(pk_id, 2, "a persisted last resort prekey must not block auto prekeys");
253 tx.commit().await.unwrap();
254
255 #[cfg(not(target_os = "unknown"))]
256 drop(db_file);
257 }
258}