core_crypto/proteus/
prekey.rs1use core_crypto_keystore::{
2 CryptoKeystoreError, Transaction,
3 entities::ProteusPrekey,
4 traits::{Entity, FetchFromDatabase as _},
5};
6use proteus_wasm::keys::PreKeyBundle;
7
8use super::ProteusCentral;
9use crate::{KeystoreError, ProteusError, Result};
10
11impl ProteusCentral {
12 pub(crate) async fn new_prekey(&self, id: u16, transaction: &Transaction) -> Result<Vec<u8>> {
18 use proteus_wasm::keys::{PreKey, PreKeyId};
19
20 if transaction
27 .get::<ProteusPrekey>(&id)
28 .await
29 .map_err(KeystoreError::wrap("checking whether a proteus prekey id is free"))?
30 .is_some()
31 {
32 return Err(
33 KeystoreError::wrap("saving keystore prekey")(CryptoKeystoreError::AlreadyExists(
34 <ProteusPrekey as Entity>::TABLE_NAME,
35 ))
36 .into(),
37 );
38 }
39
40 let prekey_id = PreKeyId::new(id);
41 let prekey = PreKey::new(prekey_id);
42 let keystore_prekey = core_crypto_keystore::entities::ProteusPrekey::from_raw(
43 id,
44 prekey.serialise().map_err(ProteusError::wrap("serialising prekey"))?,
45 );
46 let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &prekey);
47 let bundle = bundle
48 .serialise()
49 .map_err(ProteusError::wrap("serialising prekey bundle"))?;
50 transaction
51 .save(keystore_prekey)
52 .await
53 .map_err(KeystoreError::wrap("saving keystore prekey"))?;
54 Ok(bundle)
55 }
56
57 pub(crate) async fn new_prekey_auto(&self, transaction: &Transaction) -> Result<(u16, Vec<u8>)> {
61 let id = core_crypto_keystore::entities::ProteusPrekey::get_free_id(transaction)
62 .await
63 .map_err(KeystoreError::wrap("getting proteus prekey by id"))?;
64 Ok((id, self.new_prekey(id, transaction).await?))
65 }
66
67 pub fn last_resort_prekey_id() -> u16 {
69 proteus_wasm::keys::MAX_PREKEY_ID.value()
70 }
71
72 pub(crate) async fn last_resort_prekey(&self, transaction: &Transaction) -> Result<Vec<u8>> {
75 let last_resort = if let Some(last_resort) = transaction
76 .get::<core_crypto_keystore::entities::ProteusPrekey>(&Self::last_resort_prekey_id())
77 .await
78 .map_err(KeystoreError::wrap("finding proteus prekey"))?
79 {
80 proteus_wasm::keys::PreKey::deserialise(&last_resort.prekey)
81 .map_err(ProteusError::wrap("deserialising proteus prekey"))?
82 } else {
83 let last_resort = proteus_wasm::keys::PreKey::last_resort();
84 let prekey = last_resort
85 .serialise()
86 .map_err(ProteusError::wrap("serializing last resort prekey"))?;
87
88 transaction
89 .save(ProteusPrekey::from_raw(Self::last_resort_prekey_id(), prekey))
90 .await
91 .map_err(KeystoreError::wrap("storing proteus last resort prekey"))?;
92
93 last_resort
94 };
95
96 let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &last_resort);
97 let bundle = bundle
98 .serialise()
99 .map_err(ProteusError::wrap("serialising prekey bundle"))?;
100
101 Ok(bundle)
102 }
103
104 pub fn fingerprint_prekeybundle(prekey: &[u8]) -> Result<String> {
109 let prekey = PreKeyBundle::deserialise(prekey).map_err(ProteusError::wrap("deserialising prekey bundle"))?;
110 Ok(prekey.identity_key.fingerprint())
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use core_crypto_keystore::DatabaseKey;
117
118 use super::*;
119 use crate::test_utils::{proteus_utils::*, *};
120
121 #[macro_rules_attribute::apply(smol_macros::test)]
122 async fn can_produce_proteus_consumed_prekeys() {
123 #[cfg(not(target_os = "unknown"))]
124 let (path, db_file) = tmp_db_file();
125 #[cfg(target_os = "unknown")]
126 let (path, _) = tmp_db_file();
127
128 let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
129
130 let key = DatabaseKey::generate();
131 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
132 let tx = keystore.new_transaction().await.unwrap();
133 let mut alice = ProteusCentral::try_new(&tx).await.unwrap();
134
135 let mut bob = CryptoboxLike::init();
136
137 let alice_prekey_bundle_ser = alice.new_prekey(1, &tx).await.unwrap();
138
139 bob.init_session_from_prekey_bundle(&session_id, &alice_prekey_bundle_ser);
140 let message = b"Hello world!";
141 let encrypted = bob.encrypt(&session_id, message);
142
143 let (_, decrypted) = alice.session_from_message(&tx, &session_id, &encrypted).await.unwrap();
144
145 assert_eq!(message, decrypted.as_slice());
146
147 let encrypted = alice.encrypt(&tx, &session_id, message).await.unwrap();
148 let decrypted = bob.decrypt(&session_id, &encrypted).await;
149
150 assert_eq!(message, decrypted.as_slice());
151 tx.commit().await.unwrap();
152 #[cfg(not(target_os = "unknown"))]
153 drop(db_file);
154 }
155
156 #[macro_rules_attribute::apply(smol_macros::test)]
162 async fn auto_prekeys_fill_holes() {
163 use core_crypto_keystore::entities::ProteusPrekey;
164 const GAP_AMOUNT: usize = 5;
165 const EXTRA_AMOUNT: u16 = 10;
167 const ID_TEST_RANGE: std::ops::RangeInclusive<u16> = 1..=30;
168
169 async fn claim(alice: &ProteusCentral, tx: &Transaction, count: usize) -> Vec<u16> {
171 let mut ids = Vec::with_capacity(count);
172 for _ in 0..count {
173 let (pk_id, pkb) = alice.new_prekey_auto(tx).await.unwrap();
174 let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
175 assert_eq!(prekey.prekey_id.value(), pk_id, "the bundle must carry the assigned id");
176 ids.push(pk_id);
177 }
178 ids
179 }
180
181 fn pick_gap_ids(rng: &mut impl rand::Rng, count: usize) -> Vec<u16> {
183 let mut ids = Vec::with_capacity(count);
184 while ids.len() < count {
185 let id = rng.gen_range(ID_TEST_RANGE);
186 if !ids.contains(&id) {
187 ids.push(id);
188 }
189 }
190 ids.sort();
191 ids
192 }
193
194 fn ascending(ids: &[u16]) -> Vec<u16> {
195 let mut ids = ids.to_owned();
196 ids.sort();
197 ids
198 }
199
200 #[cfg(not(target_os = "unknown"))]
201 let (path, db_file) = tmp_db_file();
202 #[cfg(target_os = "unknown")]
203 let (path, _) = tmp_db_file();
204
205 let key = DatabaseKey::generate();
206 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
207 let tx = keystore.new_transaction().await.unwrap();
208 let alice = ProteusCentral::try_new(&tx).await.unwrap();
209
210 let claimed = claim(&alice, &tx, ID_TEST_RANGE.count()).await;
212 assert_eq!(claimed, ID_TEST_RANGE.collect::<Vec<_>>());
213
214 let mut rng = rand::thread_rng();
215
216 let gap_ids = pick_gap_ids(&mut rng, GAP_AMOUNT);
218 for gap_id in &gap_ids {
219 tx.remove::<ProteusPrekey>(gap_id).await.unwrap();
220 }
221 let claimed = claim(&alice, &tx, GAP_AMOUNT).await;
222 assert_eq!(
223 ascending(&claimed),
224 gap_ids,
225 "every deleted id must be reassigned exactly once"
226 );
227
228 let gap_ids = pick_gap_ids(&mut rng, GAP_AMOUNT);
231 for gap_id in &gap_ids {
232 tx.remove::<ProteusPrekey>(gap_id).await.unwrap();
233 }
234 let claimed = claim(&alice, &tx, GAP_AMOUNT + EXTRA_AMOUNT as usize).await;
235 let (filled, extended) = claimed.split_at(GAP_AMOUNT);
236 assert_eq!(
237 ascending(filled),
238 gap_ids,
239 "holes must all be filled before the id space is extended"
240 );
241 let high_water_mark = *ID_TEST_RANGE.end();
242 assert_eq!(
243 extended,
244 (high_water_mark + 1..=high_water_mark + EXTRA_AMOUNT).collect::<Vec<_>>(),
245 "once no holes remain, ids extend the id space in ascending order"
246 );
247
248 tx.commit().await.unwrap();
249 #[cfg(not(target_os = "unknown"))]
250 drop(db_file);
251 }
252
253 #[macro_rules_attribute::apply(smol_macros::test)]
256 async fn last_resort_prekey_does_not_exhaust_id_space() {
257 #[cfg(not(target_os = "unknown"))]
258 let (path, db_file) = tmp_db_file();
259 #[cfg(target_os = "unknown")]
260 let (path, _) = tmp_db_file();
261
262 let key = DatabaseKey::generate();
263 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
264
265 let tx = keystore.new_transaction().await.unwrap();
267 let alice = ProteusCentral::try_new(&tx).await.unwrap();
268 alice.last_resort_prekey(&tx).await.unwrap();
269 let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
270 assert_eq!(
271 pk_id, 1,
272 "an uncommitted last resort prekey must not block auto prekeys"
273 );
274 tx.commit().await.unwrap();
275
276 let tx = keystore.new_transaction().await.unwrap();
278 let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
279 assert_eq!(pk_id, 2, "a persisted last resort prekey must not block auto prekeys");
280 tx.commit().await.unwrap();
281
282 #[cfg(not(target_os = "unknown"))]
283 drop(db_file);
284 }
285
286 #[macro_rules_attribute::apply(smol_macros::test)]
293 async fn cannot_reuse_a_prekey_id() {
294 #[cfg(not(target_os = "unknown"))]
295 let (path, db_file) = tmp_db_file();
296 #[cfg(target_os = "unknown")]
297 let (path, _) = tmp_db_file();
298
299 let key = DatabaseKey::generate();
300 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
301 let tx = keystore.new_transaction().await.unwrap();
302 let alice = ProteusCentral::try_new(&tx).await.unwrap();
303
304 let bundle = alice.new_prekey(1, &tx).await.unwrap();
305
306 assert!(
309 alice.new_prekey(1, &tx).await.is_err(),
310 "an id claimed earlier in this transaction must not be reassigned"
311 );
312 tx.commit().await.unwrap();
313
314 let tx = keystore.new_transaction().await.unwrap();
316 assert!(
317 alice.new_prekey(1, &tx).await.is_err(),
318 "a persisted id must not be reassigned"
319 );
320
321 let stored = tx.get::<ProteusPrekey>(&1).await.unwrap().unwrap();
323 let stored = proteus_wasm::keys::PreKey::deserialise(&stored.prekey).unwrap();
324 let bundle = PreKeyBundle::deserialise(&bundle).unwrap();
325 assert_eq!(
326 stored.key_pair.public_key, bundle.public_key,
327 "the rejected claims must have left the original prekey in place"
328 );
329
330 let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
332 assert_eq!(pk_id, 2, "a rejected claim must not poison the transaction");
333 tx.commit().await.unwrap();
334
335 #[cfg(not(target_os = "unknown"))]
336 drop(db_file);
337 }
338}