bitwarden_user_crypto_management/
v2_migration_permission.rs1use bitwarden_core::key_management::V2EncryptedMigrationsGracePeriodStart;
8use chrono::{TimeDelta, Utc};
9use serde::{Deserialize, Serialize};
10#[cfg(feature = "wasm")]
11use tsify::Tsify;
12#[cfg(feature = "wasm")]
13use wasm_bindgen::prelude::*;
14
15use crate::UserCryptoManagementClient;
16
17const GRACE_PERIOD: TimeDelta = TimeDelta::weeks(2);
19
20#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
22#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
23#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
24pub enum MigrationPermission {
25 Wait,
27 Migrate,
29}
30
31#[cfg_attr(feature = "wasm", wasm_bindgen)]
32#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
33impl UserCryptoManagementClient {
34 pub async fn request_permission_to_migrate_to_v2(&self) -> MigrationPermission {
51 let state_bridge = self.client.km_state_bridge();
52
53 match state_bridge
54 .get_v2_encrypted_migrations_grace_period_start()
55 .await
56 {
57 None => {
60 state_bridge
61 .set_v2_encrypted_migrations_grace_period_start(
62 &V2EncryptedMigrationsGracePeriodStart(Utc::now()),
63 )
64 .await;
65 MigrationPermission::Wait
66 }
67 Some(start) if Utc::now().signed_duration_since(start.0) < GRACE_PERIOD => {
68 MigrationPermission::Wait
69 }
70 Some(_) => MigrationPermission::Migrate,
71 }
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use bitwarden_core::{Client, key_management::state_bridge::test_support::InMemoryStateBridge};
78 use chrono::DateTime;
79
80 use super::*;
81 use crate::UserCryptoManagementClientExt;
82
83 fn client_with_bridge() -> Client {
85 let client = Client::new(None);
86 client
87 .km_state_bridge()
88 .register_bridge(Box::new(InMemoryStateBridge::default()));
89 client
90 }
91
92 async fn client_with_anchor(offset: TimeDelta) -> Client {
95 let client = client_with_bridge();
96 client
97 .km_state_bridge()
98 .set_v2_encrypted_migrations_grace_period_start(&V2EncryptedMigrationsGracePeriodStart(
99 Utc::now() + offset,
100 ))
101 .await;
102 client
103 }
104
105 async fn stored_anchor(client: &Client) -> Option<DateTime<Utc>> {
106 client
107 .km_state_bridge()
108 .get_v2_encrypted_migrations_grace_period_start()
109 .await
110 .map(|start| start.0)
111 }
112
113 #[tokio::test]
114 async fn test_unset_anchor_starts_the_window_and_reports_wait() {
115 let client = client_with_bridge();
116
117 assert_eq!(
118 client
119 .user_crypto_management()
120 .request_permission_to_migrate_to_v2()
121 .await,
122 MigrationPermission::Wait
123 );
124
125 let anchor = stored_anchor(&client).await.expect("the window was armed");
126 assert!(Utc::now().signed_duration_since(anchor) < TimeDelta::seconds(5));
127 }
128
129 #[tokio::test]
130 async fn test_second_call_does_not_move_the_anchor() {
131 let client = client_with_bridge();
132 let user_crypto_management = client.user_crypto_management();
133
134 assert_eq!(
135 user_crypto_management
136 .request_permission_to_migrate_to_v2()
137 .await,
138 MigrationPermission::Wait
139 );
140 let armed = stored_anchor(&client).await.expect("the window was armed");
141
142 assert_eq!(
143 user_crypto_management
144 .request_permission_to_migrate_to_v2()
145 .await,
146 MigrationPermission::Wait
147 );
148 assert_eq!(stored_anchor(&client).await, Some(armed));
149 }
150
151 #[tokio::test]
152 async fn test_one_week_old_anchor_waits() {
153 let client = client_with_anchor(-TimeDelta::weeks(1)).await;
154
155 assert_eq!(
156 client
157 .user_crypto_management()
158 .request_permission_to_migrate_to_v2()
159 .await,
160 MigrationPermission::Wait
161 );
162 }
163
164 #[tokio::test]
165 async fn test_three_week_old_anchor_migrates() {
166 let client = client_with_anchor(-TimeDelta::weeks(3)).await;
167
168 assert_eq!(
169 client
170 .user_crypto_management()
171 .request_permission_to_migrate_to_v2()
172 .await,
173 MigrationPermission::Migrate
174 );
175 }
176
177 #[tokio::test]
178 async fn test_exactly_two_weeks_migrates() {
179 let client = client_with_anchor(-GRACE_PERIOD).await;
182
183 assert_eq!(
184 client
185 .user_crypto_management()
186 .request_permission_to_migrate_to_v2()
187 .await,
188 MigrationPermission::Migrate
189 );
190 }
191
192 #[tokio::test]
193 async fn test_future_anchor_waits_and_is_untouched() {
194 let client = client_with_anchor(TimeDelta::weeks(1)).await;
195 let anchor = stored_anchor(&client).await;
196
197 assert_eq!(
198 client
199 .user_crypto_management()
200 .request_permission_to_migrate_to_v2()
201 .await,
202 MigrationPermission::Wait
203 );
204 assert_eq!(stored_anchor(&client).await, anchor);
205 }
206
207 #[tokio::test]
208 #[should_panic(expected = "StateBridge not registered")]
209 async fn test_without_a_state_bridge_panics() {
210 let client = Client::new(None);
211
212 client
213 .user_crypto_management()
214 .request_permission_to_migrate_to_v2()
215 .await;
216 }
217}