1use serde_derive::*;
33use std::collections::{HashMap, HashSet};
34
35use super::{
36 config::Config,
37 oauth::{AccessTokenInfo, RefreshToken},
38 profile::Profile,
39 CachedResponse, Result,
40};
41use crate::{DeviceCapability, LocalDevice, ScopedKey};
42
43pub(crate) type PersistedState = StateV2;
46
47pub(crate) fn state_from_json(data: &str) -> Result<PersistedState> {
50 let stored_state: PersistedStateTagged = serde_json::from_str(data)?;
51 upgrade_state(stored_state)
52}
53
54pub(crate) fn state_to_json(state: &PersistedState) -> Result<String> {
57 let state = PersistedStateTagged::V2(state.clone());
58 serde_json::to_string(&state).map_err(Into::into)
59}
60
61fn upgrade_state(in_state: PersistedStateTagged) -> Result<PersistedState> {
62 match in_state {
63 PersistedStateTagged::V2(state) => Ok(state),
64 }
65}
66
67#[derive(Serialize, Deserialize)]
71#[serde(tag = "schema_version")]
72#[allow(clippy::large_enum_variant)]
73enum PersistedStateTagged {
74 V2(StateV2),
75}
76
77#[derive(Clone, Serialize, Deserialize)]
91pub(crate) struct StateV2 {
92 pub(crate) config: Config,
93 pub(crate) current_device_id: Option<String>,
94 pub(crate) refresh_token: Option<RefreshToken>,
95 pub(crate) scoped_keys: HashMap<String, ScopedKey>,
96 pub(crate) last_handled_command: Option<u64>,
97 #[serde(default)]
101 pub(crate) commands_data: HashMap<String, String>,
102 #[serde(default)]
103 pub(crate) device_capabilities: HashSet<DeviceCapability>,
104 #[serde(default)]
105 pub(crate) access_token_cache: HashMap<String, AccessTokenInfo>,
106 pub(crate) session_token: Option<String>, pub(crate) last_seen_profile: Option<CachedResponse<Profile>>,
108 #[serde(default)]
110 pub(crate) server_local_device_info: Option<LocalDevice>,
111 #[serde(default)]
112 pub(crate) logged_out_from_auth_issues: bool,
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_invalid_schema_version() {
121 let state_v1_json = "{\"schema_version\":\"V1\",\"client_id\":\"98adfa37698f255b\",\"redirect_uri\":\"https://lockbox.firefox.com/fxa/ios-redirect.html\",\"config\":{\"content_url\":\"https://accounts.firefox.com\",\"auth_url\":\"https://api.accounts.firefox.com/\",\"oauth_url\":\"https://oauth.accounts.firefox.com/\",\"profile_url\":\"https://profile.accounts.firefox.com/\",\"token_server_endpoint_url\":\"https://token.services.mozilla.com/1.0/sync/1.5\",\"authorization_endpoint\":\"https://accounts.firefox.com/authorization\",\"issuer\":\"https://accounts.firefox.com\",\"jwks_uri\":\"https://oauth.accounts.firefox.com/v1/jwks\",\"token_endpoint\":\"https://oauth.accounts.firefox.com/v1/token\",\"userinfo_endpoint\":\"https://profile.accounts.firefox.com/v1/profile\"},\"oauth_cache\":{\"https://identity.mozilla.com/apps/oldsync https://identity.mozilla.com/apps/lockbox profile\":{\"access_token\":\"bef37ec0340783356bcac67a86c4efa23a56f2ddd0c7a6251d19988bab7bdc99\",\"keys\":\"{\\\"https://identity.mozilla.com/apps/oldsync\\\":{\\\"kty\\\":\\\"oct\\\",\\\"scope\\\":\\\"https://identity.mozilla.com/apps/oldsync\\\",\\\"k\\\":\\\"kMtwpVC0ZaYFJymPza8rXK_0CgCp3KMwRStwGfBRBDtL6hXRDVJgQFaoOQ2dimw0Bko5WVv2gNTy7RX5zFYZHg\\\",\\\"kid\\\":\\\"1542236016429-Ox1FbJfFfwTe5t-xq4v2hQ\\\"},\\\"https://identity.mozilla.com/apps/lockbox\\\":{\\\"kty\\\":\\\"oct\\\",\\\"scope\\\":\\\"https://identity.mozilla.com/apps/lockbox\\\",\\\"k\\\":\\\"Qk4K4xF2PgQ6XvBXW8X7B7AWwWgW2bHQov9NHNd4v-k\\\",\\\"kid\\\":\\\"1231014287-KDVj0DFaO3wGpPJD8oPwVg\\\"}}\",\"refresh_token\":\"bed5532f4fea7e39c5c4f609f53603ee7518fd1c103cc4034da3618f786ed188\",\"expires_at\":1543474657,\"scopes\":[\"https://identity.mozilla.com/apps/oldsync\",\"https://identity.mozilla.com/apps/lockbox\",\"profile\"]}}}";
122 if state_from_json(state_v1_json).is_ok() {
123 panic!("Invalid schema passed the conversion from json")
124 }
125 }
126
127 #[test]
128 fn test_v2_ignores_unknown_fields_introduced_by_future_changes_to_the_schema() {
129 let state_v2_json = "{\"schema_version\":\"V2\",\"config\":{\"client_id\":\"98adfa37698f255b\",\"redirect_uri\":\"https://lockbox.firefox.com/fxa/ios-redirect.html\",\"content_url\":\"https://accounts.firefox.com\",\"remote_config\":{\"auth_url\":\"https://api.accounts.firefox.com/\",\"oauth_url\":\"https://oauth.accounts.firefox.com/\",\"profile_url\":\"https://profile.accounts.firefox.com/\",\"token_server_endpoint_url\":\"https://token.services.mozilla.com/1.0/sync/1.5\",\"authorization_endpoint\":\"https://accounts.firefox.com/authorization\",\"issuer\":\"https://accounts.firefox.com\",\"jwks_uri\":\"https://oauth.accounts.firefox.com/v1/jwks\",\"token_endpoint\":\"https://oauth.accounts.firefox.com/v1/token\",\"userinfo_endpoint\":\"https://profile.accounts.firefox.com/v1/profile\"}},\"refresh_token\":{\"token\":\"bed5532f4fea7e39c5c4f609f53603ee7518fd1c103cc4034da3618f786ed188\",\"scopes\":[\"https://identity.mozilla.com/apps/oldysnc\"]},\"scoped_keys\":{\"https://identity.mozilla.com/apps/oldsync\":{\"kty\":\"oct\",\"scope\":\"https://identity.mozilla.com/apps/oldsync\",\"k\":\"kMtwpVC0ZaYFJymPza8rXK_0CgCp3KMwRStwGfBRBDtL6hXRDVJgQFaoOQ2dimw0Bko5WVv2gNTy7RX5zFYZHg\",\"kid\":\"1542236016429-Ox1FbJfFfwTe5t-xq4v2hQ\"}},\"login_state\":{\"Unknown\":null},\"a_new_field\":42}";
132 let state = state_from_json(state_v2_json).unwrap();
133 let refresh_token = state.refresh_token.unwrap();
134 assert_eq!(
135 refresh_token.token,
136 "bed5532f4fea7e39c5c4f609f53603ee7518fd1c103cc4034da3618f786ed188"
137 );
138 }
139
140 #[test]
141 fn test_v2_creates_an_empty_access_token_cache_if_its_missing() {
142 let state_v2_json = "{\"schema_version\":\"V2\",\"config\":{\"client_id\":\"98adfa37698f255b\",\"redirect_uri\":\"https://lockbox.firefox.com/fxa/ios-redirect.html\",\"content_url\":\"https://accounts.firefox.com\"},\"refresh_token\":{\"token\":\"bed5532f4fea7e39c5c4f609f53603ee7518fd1c103cc4034da3618f786ed188\",\"scopes\":[\"https://identity.mozilla.com/apps/oldysnc\"]},\"scoped_keys\":{\"https://identity.mozilla.com/apps/oldsync\":{\"kty\":\"oct\",\"scope\":\"https://identity.mozilla.com/apps/oldsync\",\"k\":\"kMtwpVC0ZaYFJymPza8rXK_0CgCp3KMwRStwGfBRBDtL6hXRDVJgQFaoOQ2dimw0Bko5WVv2gNTy7RX5zFYZHg\",\"kid\":\"1542236016429-Ox1FbJfFfwTe5t-xq4v2hQ\"}},\"login_state\":{\"Unknown\":null}}";
143 let state = state_from_json(state_v2_json).unwrap();
144 let refresh_token = state.refresh_token.unwrap();
145 assert_eq!(
146 refresh_token.token,
147 "bed5532f4fea7e39c5c4f609f53603ee7518fd1c103cc4034da3618f786ed188"
148 );
149 assert_eq!(state.access_token_cache.len(), 0);
150 }
151}