Skip to main content

tuwunel_service/users/
device.rs

1use std::{
2	net::IpAddr,
3	sync::Arc,
4	time::{Duration, SystemTime},
5};
6
7use futures::{FutureExt, Stream, StreamExt, future::join};
8use ruma::{
9	DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, UserId,
10	api::client::device::Device, events::AnyToDeviceEvent, serde::Raw,
11};
12use serde_json::json;
13use tuwunel_core::{
14	Err, Result, at, implement, trace,
15	utils::{
16		self, BoolExt, ReadyExt, random_string,
17		stream::{IterStream, TryIgnore},
18		string::to_small_string,
19		time::{
20			duration_since_epoch, timepoint_from_epoch, timepoint_from_now, timepoint_has_passed,
21		},
22	},
23};
24use tuwunel_database::{Cbor, Deserialized, Ignore, Interfix, Json, Map, Txn};
25
26/// generated device ID length
27const DEVICE_ID_LENGTH: usize = 10;
28
29/// generated user access token length
30pub const TOKEN_LENGTH: usize = 32;
31
32/// Adds a new device to a user.
33#[implement(super::Service)]
34#[tracing::instrument(level = "info", skip(self, access_token))]
35pub async fn create_device(
36	&self,
37	user_id: &UserId,
38	device_id: Option<&DeviceId>,
39	(access_token, expires_in): (Option<&str>, Option<Duration>),
40	refresh_token: Option<&str>,
41	initial_device_display_name: Option<&str>,
42	client_ip: Option<IpAddr>,
43) -> Result<OwnedDeviceId> {
44	let device_id = resolve_device_id(device_id);
45
46	if !self.exists(user_id).await {
47		return Err!(Request(InvalidParam(error!(
48			"Called create_device for non-existent user {user_id}"
49		))));
50	}
51
52	let notify = true;
53	self.put_device_metadata(user_id, notify, &Device {
54		device_id: device_id.clone(),
55		display_name: initial_device_display_name.map(Into::into),
56		last_seen_ts: Some(MilliSecondsSinceUnixEpoch::now()),
57		last_seen_ip: client_ip.map(to_small_string),
58	});
59
60	if let Some(access_token) = access_token {
61		self.set_access_token(user_id, &device_id, access_token, expires_in, refresh_token)
62			.await?;
63	}
64
65	Ok(device_id)
66}
67
68fn resolve_device_id(device_id: Option<&DeviceId>) -> OwnedDeviceId {
69	// Treat an empty device_id ("") as unspecified.
70	device_id
71		.filter(|device_id| !device_id.as_str().is_empty())
72		.map(ToOwned::to_owned)
73		.unwrap_or_else(|| OwnedDeviceId::from(random_string(DEVICE_ID_LENGTH)))
74}
75
76/// Removes a device from a user.
77#[implement(super::Service)]
78#[tracing::instrument(level = "info", skip(self))]
79pub async fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) {
80	// Remove access tokens
81	self.remove_tokens(user_id, device_id).await;
82
83	// Remove todevice events
84	let prefix = (user_id, device_id, Interfix);
85	self.db
86		.todeviceid_events
87		.keys_prefix_raw(&prefix)
88		.ignore_err()
89		.ready_for_each(|key| self.db.todeviceid_events.remove(key))
90		.await;
91
92	// Remove pushers
93	self.services
94		.pusher
95		.get_device_pushkeys(user_id, device_id)
96		.map(Vec::into_iter)
97		.map(IterStream::stream)
98		.flatten_stream()
99		.for_each(async |pushkey| {
100			self.services
101				.pusher
102				.delete_pusher(user_id, &pushkey)
103				.await;
104		})
105		.await;
106
107	// Removes the dehydrated device if the ID matches, otherwise no-op
108	self.remove_dehydrated_device(user_id, Some(device_id))
109		.await
110		.ok();
111
112	// TODO: Remove onetimekeys
113
114	// MSC2732: drop fallback keys for this device.
115	let prefix = (user_id, device_id, Interfix);
116	self.db
117		.userdeviceidalgorithm_fallback
118		.keys_prefix_raw(&prefix)
119		.ignore_err()
120		.ready_for_each(|key| self.db.userdeviceidalgorithm_fallback.remove(key))
121		.await;
122
123	// MSC3890: drop this device's local notification settings.
124	let event_type = format!("org.matrix.msc3890.local_notification_settings.{device_id}").into();
125	self.services
126		.account_data
127		.delete(None, user_id, event_type)
128		.await
129		.ok();
130
131	let userdeviceid = (user_id, device_id);
132	self.db.userdeviceid_metadata.del(userdeviceid);
133	self.db.oidcdevice_userdeviceid.del(userdeviceid);
134
135	self.mark_device_key_update(user_id).await;
136	increment(&self.db.userid_devicelistversion, user_id.as_bytes());
137}
138
139/// Returns an iterator over all device ids of this user.
140#[implement(super::Service)]
141pub fn all_device_ids<'a>(
142	&'a self,
143	user_id: &'a UserId,
144) -> impl Stream<Item = &DeviceId> + Send + 'a {
145	let prefix = (user_id, Interfix);
146	self.db
147		.userdeviceid_metadata
148		.keys_prefix(&prefix)
149		.ignore_err()
150		.map(|(_, device_id): (Ignore, &DeviceId)| device_id)
151}
152
153/// Find out which user an access or refresh token belongs to.
154#[implement(super::Service)]
155#[tracing::instrument(level = "trace", skip(self, token))]
156pub async fn find_from_token(
157	&self,
158	token: &str,
159) -> Result<(OwnedUserId, OwnedDeviceId, Option<SystemTime>)> {
160	self.db
161		.token_userdeviceid
162		.get(token)
163		.await
164		.deserialized()
165		.and_then(|(user_id, device_id, expires_at): (_, _, Option<u64>)| {
166			let expires_at = expires_at
167				.map(Duration::from_secs)
168				.map(timepoint_from_epoch)
169				.transpose()?;
170
171			Ok((user_id, device_id, expires_at))
172		})
173}
174
175#[implement(super::Service)]
176#[tracing::instrument(level = "debug", skip(self))]
177pub async fn remove_tokens(&self, user_id: &UserId, device_id: &DeviceId) {
178	let remove_access = self
179		.remove_access_token(user_id, device_id)
180		.map(Result::ok);
181
182	let remove_refresh = self
183		.remove_refresh_token(user_id, device_id)
184		.map(Result::ok);
185
186	join(remove_access, remove_refresh).await;
187}
188
189/// Replaces the access token of one device.
190#[implement(super::Service)]
191#[tracing::instrument(level = "debug", skip(self))]
192pub async fn set_access_token(
193	&self,
194	user_id: &UserId,
195	device_id: &DeviceId,
196	access_token: &str,
197	expires_in: Option<Duration>,
198	refresh_token: Option<&str>,
199) -> Result {
200	assert!(
201		access_token.len() >= TOKEN_LENGTH,
202		"Caller must supply an access_token >= {TOKEN_LENGTH} chars."
203	);
204
205	if let Some(refresh_token) = refresh_token {
206		self.set_refresh_token(user_id, device_id, refresh_token)
207			.await?;
208	}
209
210	let expires_at = expires_in
211		.map(timepoint_from_now)
212		.transpose()?
213		.map(duration_since_epoch)
214		.as_ref()
215		.map(Duration::as_secs);
216
217	let userdeviceid = (user_id, device_id);
218
219	// Fold the prior pointer token into the index for pre-index upgrades.
220	let previous = self
221		.db
222		.userdeviceid_token
223		.qry(&userdeviceid)
224		.await
225		.deserialized::<String>()
226		.ok();
227
228	let mut txn = self.services.db.txn();
229
230	if let Some(previous) = previous.as_deref() {
231		let key = (user_id, device_id, previous);
232
233		txn.put_raw(&self.db.userdeviceidtoken_index, key, []);
234	}
235
236	let key = (user_id, device_id, access_token);
237	let value = (user_id, device_id, expires_at);
238
239	txn.raw_put(&self.db.token_userdeviceid, access_token, value);
240	txn.put_raw(&self.db.userdeviceidtoken_index, key, []);
241	txn.put_raw(&self.db.userdeviceid_token, userdeviceid, access_token);
242
243	txn.execute();
244
245	Ok(())
246}
247
248/// Revoke every access token of one device, without deleting the device. Take
249/// care to not leave dangling devices if using this method.
250#[implement(super::Service)]
251pub async fn remove_access_token(&self, user_id: &UserId, device_id: &DeviceId) -> Result {
252	let prefix = (user_id, device_id, Interfix);
253	self.db
254		.userdeviceidtoken_index
255		.keys_prefix(&prefix)
256		.ignore_err()
257		.ready_for_each(|(_, _, token): (Ignore, Ignore, &str)| {
258			self.db.token_userdeviceid.remove(token);
259			self.db
260				.userdeviceidtoken_index
261				.del((user_id, device_id, token));
262		})
263		.await;
264
265	// Cover any pre-index token still recorded only in the legacy pointer.
266	let token = self
267		.db
268		.userdeviceid_token
269		.qry(&(user_id, device_id))
270		.await
271		.deserialized::<String>()
272		.ok();
273
274	let mut txn = self.services.db.txn();
275
276	if let Some(token) = token.as_deref() {
277		txn.del_raw(&self.db.token_userdeviceid, token);
278	}
279
280	txn.del(&self.db.userdeviceid_token, (user_id, device_id));
281
282	txn.execute();
283
284	Ok(())
285}
286
287/// Revoke a single access token by value, leaving the device and any other
288/// tokens it holds intact.
289#[implement(super::Service)]
290pub async fn remove_access_token_value(&self, access_token: &str) {
291	let owner = self
292		.db
293		.token_userdeviceid
294		.get(access_token)
295		.await
296		.deserialized::<(OwnedUserId, OwnedDeviceId, Option<u64>)>()
297		.ok();
298
299	let mut txn = self.services.db.txn();
300
301	if let Some((user_id, device_id, _)) = owner {
302		let user_device_token = (&*user_id, &*device_id, access_token);
303
304		txn.del(&self.db.userdeviceidtoken_index, user_device_token);
305	}
306
307	txn.del_raw(&self.db.token_userdeviceid, access_token);
308
309	txn.execute();
310}
311
312#[implement(super::Service)]
313pub fn generate_access_token(&self, expires: bool) -> (String, Option<Duration>) {
314	let access_token = random_string(TOKEN_LENGTH);
315	let expires_in = expires
316		.then_some(self.services.server.config.access_token_ttl)
317		.map(Duration::from_secs);
318
319	(access_token, expires_in)
320}
321
322/// Replaces the refresh token of one device.
323#[implement(super::Service)]
324#[tracing::instrument(level = "debug", skip(self))]
325pub async fn set_refresh_token(
326	&self,
327	user_id: &UserId,
328	device_id: &DeviceId,
329	refresh_token: &str,
330) -> Result {
331	debug_assert!(refresh_token.starts_with("refresh_"), "refresh_token missing prefix");
332
333	let config = &self.services.server.config;
334	let ttl = config.refresh_token_ttl;
335	let idle_only = config.refresh_token_idle_only;
336
337	// Absolute mode carries the prior deadline forward instead of sliding it.
338	let prior_expires_at: Option<SystemTime> = (ttl != 0 && !idle_only)
339		.then_async(|| self.find_refresh_token_expires_at(user_id, device_id))
340		.await
341		.flatten();
342
343	// Capture the outgoing token before removal so it can be retained for one
344	// generation, making a later replay detectable.
345	let spent: Option<String> = self
346		.db
347		.userdeviceid_refresh
348		.qry(&(user_id, device_id))
349		.await
350		.deserialized()
351		.ok();
352
353	// Also drops the prior spent entry.
354	self.remove_refresh_token(user_id, device_id)
355		.await
356		.ok();
357
358	let expires_at = match (ttl, prior_expires_at) {
359		| (0, _) => None,
360		| (_, Some(prior)) => Some(prior),
361		| (ttl, None) => Some(timepoint_from_now(Duration::from_secs(ttl))?),
362	};
363
364	let expires_at_secs = expires_at
365		.map(duration_since_epoch)
366		.as_ref()
367		.map(Duration::as_secs);
368
369	let userdeviceid = (user_id, device_id);
370	let value = (user_id, device_id, expires_at_secs);
371	let mut txn = self.services.db.txn();
372
373	txn.raw_put(&self.db.token_userdeviceid, refresh_token, value);
374	txn.put_raw(&self.db.userdeviceid_refresh, userdeviceid, refresh_token);
375
376	// Retain the outgoing token as the device's spent token, pointing at its
377	// successor so a double-submit can be distinguished from a replay.
378	if let Some(spent) = spent {
379		let spent_at = duration_since_epoch(SystemTime::now()).as_secs();
380		let value = (user_id, device_id, refresh_token, spent_at);
381
382		txn.raw_put(&self.db.spentrefresh_userdeviceid, &*spent, value);
383		txn.put_raw(&self.db.userdeviceid_spentrefresh, userdeviceid, &*spent);
384	}
385
386	txn.execute();
387
388	Ok(())
389}
390
391/// Look up the expiry stored alongside the current refresh token for this
392/// device, if one is recorded. Pre-rotation entries carry no expiry and
393/// return `None`.
394#[implement(super::Service)]
395async fn find_refresh_token_expires_at(
396	&self,
397	user_id: &UserId,
398	device_id: &DeviceId,
399) -> Option<SystemTime> {
400	let userdeviceid = (user_id, device_id);
401	let old_token: String = self
402		.db
403		.userdeviceid_refresh
404		.qry(&userdeviceid)
405		.await
406		.deserialized()
407		.ok()?;
408
409	let (_, _, expires_at_secs): (Ignore, Ignore, Option<u64>) = self
410		.db
411		.token_userdeviceid
412		.get(&old_token)
413		.await
414		.deserialized()
415		.ok()?;
416
417	expires_at_secs
418		.map(Duration::from_secs)
419		.map(timepoint_from_epoch)?
420		.ok()
421}
422
423/// Revoke the refresh token without deleting the device. Take care to not leave
424/// dangling devices if using this method.
425#[implement(super::Service)]
426pub async fn remove_refresh_token(&self, user_id: &UserId, device_id: &DeviceId) -> Result {
427	let userdeviceid = (user_id, device_id);
428	let refresh_token = self
429		.db
430		.userdeviceid_refresh
431		.qry(&userdeviceid)
432		.await;
433
434	let mut txn = self.services.db.txn();
435
436	if let Ok(refresh_token) = refresh_token {
437		txn.del_raw(&self.db.token_userdeviceid, &refresh_token);
438	}
439
440	txn.del(&self.db.userdeviceid_refresh, userdeviceid);
441
442	self.forget_spent_refresh_token(user_id, device_id, &mut txn)
443		.await;
444
445	txn.execute();
446
447	Ok(())
448}
449
450/// Drop the spent (previous-generation) refresh token retained for reuse
451/// detection, if any.
452#[implement(super::Service)]
453async fn forget_spent_refresh_token(
454	&self,
455	user_id: &UserId,
456	device_id: &DeviceId,
457	txn: &mut Txn,
458) {
459	let userdeviceid = (user_id, device_id);
460
461	if let Ok(spent) = self
462		.db
463		.userdeviceid_spentrefresh
464		.qry(&userdeviceid)
465		.await
466	{
467		txn.del_raw(&self.db.spentrefresh_userdeviceid, &spent);
468	}
469
470	txn.del(&self.db.userdeviceid_spentrefresh, userdeviceid);
471}
472
473/// Classification of a refresh token presented for rotation at a token
474/// endpoint.
475pub enum RefreshToken {
476	/// The device's current refresh token; rotate it.
477	Current {
478		user_id: OwnedUserId,
479		device_id: OwnedDeviceId,
480		expires_at: Option<SystemTime>,
481	},
482
483	/// A spent (already-rotated) token retained for one generation. `grace` is
484	/// set when its successor is still current and it was spent within the
485	/// configured window, marking a benign double-submit rather than a replay;
486	/// `current` is the successor for which to re-issue an access token.
487	Replayed {
488		user_id: OwnedUserId,
489		device_id: OwnedDeviceId,
490		current: String,
491		grace: bool,
492	},
493
494	/// Not a recognised refresh token.
495	Unknown,
496}
497
498/// Classify a presented refresh token for the token-endpoint rotation path.
499#[implement(super::Service)]
500pub async fn classify_refresh_token(&self, presented: &str) -> RefreshToken {
501	// The current refresh token resolves and matches the device's active
502	// pointer (an access token resolves but will not match).
503	if let Ok((user_id, device_id, expires_at)) = self.find_from_token(presented).await {
504		let current: Option<String> = self
505			.db
506			.userdeviceid_refresh
507			.qry(&(&user_id, &device_id))
508			.await
509			.deserialized()
510			.ok();
511
512		if current.as_deref() == Some(presented) {
513			return RefreshToken::Current { user_id, device_id, expires_at };
514		}
515	}
516
517	// Otherwise it may be the one retained spent token: a benign double-submit
518	// inside the grace window, or a replay to be treated as a compromise.
519	let Ok((user_id, device_id, successor, spent_at)) = self
520		.db
521		.spentrefresh_userdeviceid
522		.get(presented)
523		.await
524		.deserialized::<(OwnedUserId, OwnedDeviceId, String, u64)>()
525	else {
526		return RefreshToken::Unknown;
527	};
528
529	let current: Option<String> = self
530		.db
531		.userdeviceid_refresh
532		.qry(&(&user_id, &device_id))
533		.await
534		.deserialized()
535		.ok();
536
537	let grace_window = self
538		.services
539		.server
540		.config
541		.refresh_token_reuse_grace;
542	let elapsed = duration_since_epoch(SystemTime::now())
543		.as_secs()
544		.saturating_sub(spent_at);
545
546	let grace = grace_window != 0
547		&& elapsed <= grace_window
548		&& current.as_deref() == Some(successor.as_str());
549
550	RefreshToken::Replayed {
551		user_id,
552		device_id,
553		current: successor,
554		grace,
555	}
556}
557
558#[must_use]
559pub fn generate_refresh_token() -> String { format!("refresh_{}", random_string(TOKEN_LENGTH)) }
560
561#[implement(super::Service)]
562pub fn add_to_device_event(
563	&self,
564	sender: &UserId,
565	target_user_id: &UserId,
566	target_device_id: &DeviceId,
567	event_type: &str,
568	content: &serde_json::Value,
569) -> u64 {
570	let count = self.services.globals.next_count();
571
572	let key = (target_user_id, target_device_id, *count);
573	self.db.todeviceid_events.put(
574		key,
575		Json(json!({
576			"type": event_type,
577			"sender": sender,
578			"content": content,
579		})),
580	);
581
582	trace!(
583		%target_user_id,
584		%target_device_id,
585		count = *count,
586		%event_type,
587		%sender,
588		"to_device write",
589	);
590
591	*count
592}
593
594#[implement(super::Service)]
595pub fn get_to_device_events<'a>(
596	&'a self,
597	user_id: &'a UserId,
598	device_id: &'a DeviceId,
599	since: Option<u64>,
600	to: Option<u64>,
601) -> impl Stream<Item = (u64, Raw<AnyToDeviceEvent>)> + Send + 'a {
602	type Key<'a> = (&'a UserId, &'a DeviceId, u64);
603
604	let from = (user_id, device_id, since.map_or(0, |since| since.saturating_add(1)));
605
606	self.db
607		.todeviceid_events
608		.stream_from(&from)
609		.ignore_err()
610		.ready_take_while(move |((user_id_, device_id_, count), _): &(Key<'_>, _)| {
611			user_id == *user_id_ && device_id == *device_id_ && to.is_none_or(|to| *count <= to)
612		})
613		.map(|((_, _, count), event)| (count, event))
614}
615
616#[implement(super::Service)]
617pub async fn remove_to_device_events<Until>(
618	&self,
619	user_id: &UserId,
620	device_id: &DeviceId,
621	until: Until,
622) where
623	Until: Into<Option<u64>> + Send,
624{
625	type Key<'a> = (&'a UserId, &'a DeviceId, u64);
626
627	let until = until.into().unwrap_or(u64::MAX);
628	let from = (user_id, device_id, until);
629	self.db
630		.todeviceid_events
631		.rev_keys_from(&from)
632		.ignore_err()
633		.ready_take_while(move |(user_id_, device_id_, _): &Key<'_>| {
634			user_id == *user_id_ && device_id == *device_id_
635		})
636		.ready_for_each(|key: Key<'_>| {
637			self.db.todeviceid_events.del(key);
638		})
639		.await;
640}
641
642#[implement(super::Service)]
643pub async fn update_device_last_seen(
644	&self,
645	user_id: &UserId,
646	device_id: &DeviceId,
647	last_seen_ip: Option<IpAddr>,
648	last_seen_ts: Option<MilliSecondsSinceUnixEpoch>,
649) -> Result {
650	let mut device = self
651		.get_device_metadata(user_id, device_id)
652		.await?;
653
654	if let Some(last_seen_ip) = last_seen_ip.map(to_small_string) {
655		device.last_seen_ip.replace(last_seen_ip);
656	}
657
658	device
659		.last_seen_ts
660		.replace(last_seen_ts.unwrap_or_else(MilliSecondsSinceUnixEpoch::now));
661
662	self.put_device_metadata(user_id, false, &device);
663
664	Ok(())
665}
666
667#[implement(super::Service)]
668pub fn put_device_metadata(&self, user_id: &UserId, notify: bool, device: &Device) {
669	let key = (user_id, &device.device_id);
670	self.db
671		.userdeviceid_metadata
672		.put(key, Json(device));
673
674	if notify {
675		increment(&self.db.userid_devicelistversion, user_id.as_bytes());
676	}
677}
678
679/// Get device metadata.
680#[implement(super::Service)]
681pub async fn get_device_metadata(
682	&self,
683	user_id: &UserId,
684	device_id: &DeviceId,
685) -> Result<Device> {
686	self.db
687		.userdeviceid_metadata
688		.qry(&(user_id, device_id))
689		.await
690		.deserialized()
691		.inspect(|device: &Device| {
692			debug_assert_eq!(&device.device_id, device_id, "device_id mismatch");
693		})
694}
695
696#[implement(super::Service)]
697pub async fn device_exists(&self, user_id: &UserId, device_id: &DeviceId) -> bool {
698	self.db
699		.userdeviceid_metadata
700		.contains(&(user_id, device_id))
701		.await
702}
703
704#[implement(super::Service)]
705pub async fn is_oidc_device(&self, user_id: &UserId, device_id: &DeviceId) -> bool {
706	self.db
707		.oidcdevice_userdeviceid
708		.contains(&(user_id, device_id))
709		.await
710}
711
712/// Returns the IdP that originally authenticated this device, if known.
713/// Returns `None` for devices predating the idp_id field or non-OIDC devices.
714#[implement(super::Service)]
715pub async fn get_oidc_device_idp(
716	&self,
717	user_id: &UserId,
718	device_id: &DeviceId,
719) -> Option<String> {
720	self.db
721		.oidcdevice_userdeviceid
722		.qry(&(user_id, device_id))
723		.await
724		.deserialized::<Json<String>>()
725		.ok()
726		.map(|Json(idp)| idp)
727		.filter(|idp| !idp.is_empty())
728}
729
730#[implement(super::Service)]
731pub fn mark_oidc_device(&self, user_id: &UserId, device_id: &DeviceId, idp_id: &str) {
732	self.db
733		.oidcdevice_userdeviceid
734		.put((user_id, device_id), Json(idp_id));
735}
736
737/// Allow cross-signing key replacement without UIAA for the next 10 minutes.
738/// Returns the expiry timestamp in milliseconds.
739#[expect(clippy::must_use_candidate)]
740#[implement(super::Service)]
741pub fn allow_cross_signing_replacement(&self, user_id: &UserId) -> SystemTime {
742	let duration = Duration::from_mins(10);
743	let expires = timepoint_from_now(duration).expect("failed to create timepoint from now");
744
745	self.db
746		.oidccskeybypass_userid
747		.raw_put(user_id, Cbor(expires));
748
749	expires
750}
751
752/// Check if the user is allowed to replace cross-signing keys without UIAA.
753#[implement(super::Service)]
754pub async fn can_replace_cross_signing_keys(&self, user_id: &UserId) -> bool {
755	let Ok(expires): Result<SystemTime, _> = self
756		.db
757		.oidccskeybypass_userid
758		.get(user_id)
759		.await
760		.deserialized::<Cbor<_>>()
761		.map(at!(0))
762	else {
763		return false;
764	};
765
766	if !timepoint_has_passed(expires) {
767		return true;
768	}
769
770	self.db.oidccskeybypass_userid.remove(user_id);
771	false
772}
773
774#[implement(super::Service)]
775pub async fn get_devicelist_version(&self, user_id: &UserId) -> Result<u64> {
776	self.db
777		.userid_devicelistversion
778		.get(user_id)
779		.await
780		.deserialized()
781}
782
783#[implement(super::Service)]
784pub fn all_devices_metadata<'a>(
785	&'a self,
786	user_id: &'a UserId,
787) -> impl Stream<Item = Device> + Send + 'a {
788	let key = (user_id, Interfix);
789	self.db
790		.userdeviceid_metadata
791		.stream_prefix(&key)
792		.ignore_err()
793		.map(|(_, val): (Ignore, Device)| val)
794}
795
796//TODO: this is an ABA
797fn increment(db: &Arc<Map>, key: &[u8]) {
798	let old = db.get_blocking(key);
799	let new = utils::increment(old.ok().as_deref());
800	db.insert(key, new);
801}
802
803#[cfg(test)]
804mod tests {
805	use super::*;
806
807	#[test]
808	fn absent_device_id_is_generated() {
809		let device_id = resolve_device_id(None);
810
811		assert_eq!(device_id.as_str().len(), DEVICE_ID_LENGTH);
812	}
813
814	#[test]
815	fn empty_device_id_is_generated() {
816		let device_id = resolve_device_id(Some("".into()));
817
818		assert_eq!(device_id.as_str().len(), DEVICE_ID_LENGTH);
819	}
820
821	#[test]
822	fn provided_device_id_is_preserved() {
823		let device_id = resolve_device_id(Some("HELLOWORLD".into()));
824
825		assert_eq!(device_id.as_str(), "HELLOWORLD");
826	}
827}