Extract identity-token handling in a separate HTTPSession layer
Dadoum

Dadoum commited on 2026-09-28 03:59:45
Showing 6 changed files, with 116 additions and 115 deletions.

... ...
@@ -2,6 +2,7 @@ mod account;
2 2
 mod anisette;
3 3
 mod app;
4 4
 mod heartbeat;
5
+mod identity;
5 6
 mod secondary_actions;
6 7
 mod url_switch;
7 8
 
... ...
@@ -29,6 +30,7 @@ use log::trace;
29 30
 use plist::{Dictionary, Value};
30 31
 use plist_macros::{array, dict};
31 32
 pub use heartbeat::*;
33
+pub use identity::*;
32 34
 use reqwest::{Certificate, Method};
33 35
 use sha2::{Digest, Sha256};
34 36
 use srp::groups::G2048;
... ...
@@ -6,8 +6,6 @@ use aes::Aes256;
6 6
 use aes::cipher::consts::U16;
7 7
 use aes_gcm::aead::{Aead, Payload};
8 8
 use aes_gcm::{AesGcm, KeyInit};
9
-use base64::Engine;
10
-use base64::prelude::BASE64_STANDARD;
11 9
 use hmac::{Hmac, Mac};
12 10
 use log::warn;
13 11
 use plist::{Dictionary, Value};
... ...
@@ -29,7 +27,7 @@ impl AsRef<str> for AltDsid {
29 27
 
30 28
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31 29
 #[serde(transparent)]
32
-pub struct IdmsToken(String);
30
+pub struct IdmsToken(pub(super) String);
33 31
 
34 32
 #[derive(Debug, Clone, Serialize, Deserialize)]
35 33
 #[serde(transparent)]
... ...
@@ -91,26 +89,6 @@ pub enum AppTokenRequestError {
91 89
     InvalidResponse(Dictionary),
92 90
 }
93 91
 
94
-#[derive(Debug, Error)]
95
-pub enum ValidateCodeError {
96
-    #[error("The provided code is not valid.")]
97
-    InvalidCode,
98
-    #[error("Could not validate the code: {0}")]
99
-    Apple(#[from] AppleError),
100
-    #[error("Cannot generate device authentication data: {0}")]
101
-    Anisette(#[from] ADIError),
102
-    #[error("Network error: {0}")]
103
-    Network(#[from] reqwest::Error),
104
-    #[error("Invalid URL bag")]
105
-    InvalidURLBag,
106
-    #[error("Unexpected validation HTTP status: {0}")]
107
-    UnexpectedStatus(reqwest::StatusCode),
108
-    #[error("Cannot parse server response: {0}")]
109
-    Parsing(#[from] plist::Error),
110
-    #[error("Invalid validation response: {0:?}")]
111
-    InvalidResponse(Dictionary),
112
-}
113
-
114 92
 #[derive(Clone)]
115 93
 pub struct AccountHTTPSession<'lt, 'adi> {
116 94
     pub http_session: AnisetteHTTPSession<'lt, 'adi>,
... ...
@@ -140,20 +118,6 @@ impl<'lt, 'adi> AccountHTTPSession<'lt, 'adi> {
140 118
             .header("X-Apple-I-Identity-Id", &self.alt_dsid.0))
141 119
     }
142 120
 
143
-    /// Adds Anisette and `X-Apple-Identity-Token` for secondary authentication actions.
144
-    /// The identity header encodes this account's alternate DSID and its IDMS token.
145
-    pub fn identity_request_builder(
146
-        &self,
147
-        method: Method,
148
-        url: &str,
149
-        idms_token: &IdmsToken,
150
-    ) -> ADIResult<RequestBuilder> {
151
-        Ok(self.anisette_request_builder(method, url)?.header(
152
-            "X-Apple-Identity-Token",
153
-            BASE64_STANDARD.encode(format!("{}:{}", self.alt_dsid.0, idms_token.0)),
154
-        ))
155
-    }
156
-
157 121
     pub async fn get_app_token<T: AppToken>(
158 122
         &self,
159 123
         idms_token: &IdmsToken,
... ...
@@ -291,49 +255,4 @@ impl<'lt, 'adi> AccountHTTPSession<'lt, 'adi> {
291 255
             })
292 256
             .ok_or_else(|| AppTokenRequestError::InvalidResponse(tokens.0))
293 257
     }
294
-
295
-    pub async fn validate_code(
296
-        &self,
297
-        idms_token: &IdmsToken,
298
-        validation_code: &str,
299
-    ) -> Result<(), ValidateCodeError> {
300
-        let validate_code_url = self
301
-            .http_session
302
-            .url_bag()
303
-            .get("validateCode")
304
-            .and_then(Value::as_string)
305
-            .ok_or(ValidateCodeError::InvalidURLBag)?;
306
-
307
-        // AuthKitWin uses POST only when attaching a piggyback verification body.
308
-        let response = self
309
-            .identity_request_builder(Method::GET, validate_code_url, idms_token)?
310
-            .header("security-code", validation_code)
311
-            .send()
312
-            .await?
313
-            .error_for_status()?;
314
-
315
-        if response.status() != reqwest::StatusCode::OK {
316
-            return Err(ValidateCodeError::UnexpectedStatus(response.status()));
317
-        }
318
-
319
-        let response_plist: Dictionary = plist::from_bytes(&response.bytes().await?)?;
320
-        let code = response_plist
321
-            .get("ec")
322
-            .and_then(Value::as_signed_integer)
323
-            .ok_or_else(|| ValidateCodeError::InvalidResponse(response_plist.clone()))?;
324
-
325
-        match code {
326
-            0 => Ok(()),
327
-            -21669 => Err(ValidateCodeError::InvalidCode),
328
-            code => Err(AppleError {
329
-                code,
330
-                message: response_plist
331
-                    .get("em")
332
-                    .and_then(Value::as_string)
333
-                    .unwrap_or("Unknown validation error")
334
-                    .to_owned(),
335
-            }
336
-            .into()),
337
-        }
338
-    }
339 258
 }
... ...
@@ -1,4 +1,4 @@
1
-use super::account::{AccountHTTPSession, IdmsToken};
1
+use super::account::AccountHTTPSession;
2 2
 use adi::proxy::ADIResult;
3 3
 use plist::Dictionary;
4 4
 use reqwest::{Method, RequestBuilder};
... ...
@@ -58,16 +58,6 @@ impl<'lt, 'adi, T: AppToken> AppHTTPSession<'lt, 'adi, T> {
58 58
         self.http_session.account_request_builder(method, url)
59 59
     }
60 60
 
61
-    pub fn identity_request_builder(
62
-        &self,
63
-        method: Method,
64
-        url: &str,
65
-        idms_token: &IdmsToken,
66
-    ) -> ADIResult<RequestBuilder> {
67
-        self.http_session
68
-            .identity_request_builder(method, url, idms_token)
69
-    }
70
-
71 61
     pub fn app_request_builder(&self, method: Method, url: &str) -> ADIResult<RequestBuilder> {
72 62
         Ok(self
73 63
             .account_request_builder(method, url)?
... ...
@@ -1,4 +1,4 @@
1
-use super::account::{AccountHTTPSession, IdmsToken};
1
+use super::account::AccountHTTPSession;
2 2
 use super::app::{AppToken, Token};
3 3
 use crate::http_session::{AppleError, parse_status};
4 4
 use crate::plist_request::plist_to_body;
... ...
@@ -145,16 +145,6 @@ impl<'lt, 'adi> HeartbeatHTTPSession<'lt, 'adi> {
145 145
         self.http_session.account_request_builder(method, url)
146 146
     }
147 147
 
148
-    pub fn identity_request_builder(
149
-        &self,
150
-        method: Method,
151
-        url: &str,
152
-        idms_token: &IdmsToken,
153
-    ) -> ADIResult<RequestBuilder> {
154
-        self.http_session
155
-            .identity_request_builder(method, url, idms_token)
156
-    }
157
-
158 148
     pub fn heartbeat_request_builder(
159 149
         &self,
160 150
         method: Method,
... ...
@@ -0,0 +1,110 @@
1
+use super::account::{AccountHTTPSession, IdmsToken};
2
+use crate::http_session::AppleError;
3
+use adi::proxy::{ADIError, ADIResult};
4
+use base64::Engine;
5
+use base64::prelude::BASE64_STANDARD;
6
+use plist::{Dictionary, Value};
7
+use reqwest::{Method, RequestBuilder};
8
+use thiserror::Error;
9
+
10
+#[derive(Debug, Error)]
11
+pub enum ValidateCodeError {
12
+    #[error("The provided code is not valid.")]
13
+    InvalidCode,
14
+    #[error("Could not validate the code: {0}")]
15
+    Apple(#[from] AppleError),
16
+    #[error("Cannot generate device authentication data: {0}")]
17
+    Anisette(#[from] ADIError),
18
+    #[error("Network error: {0}")]
19
+    Network(#[from] reqwest::Error),
20
+    #[error("Invalid URL bag")]
21
+    InvalidURLBag,
22
+    #[error("Unexpected validation HTTP status: {0}")]
23
+    UnexpectedStatus(reqwest::StatusCode),
24
+    #[error("Cannot parse server response: {0}")]
25
+    Parsing(#[from] plist::Error),
26
+    #[error("Invalid validation response: {0:?}")]
27
+    InvalidResponse(Dictionary),
28
+}
29
+
30
+/// An account session with an IDMS credential for secondary authentication actions.
31
+#[derive(Clone)]
32
+pub struct IdentityHTTPSession<'lt, 'adi> {
33
+    pub http_session: AccountHTTPSession<'lt, 'adi>,
34
+    pub idms_token: IdmsToken,
35
+}
36
+
37
+impl<'lt, 'adi> IdentityHTTPSession<'lt, 'adi> {
38
+    pub fn new(http_session: AccountHTTPSession<'lt, 'adi>, idms_token: IdmsToken) -> Self {
39
+        Self {
40
+            http_session,
41
+            idms_token,
42
+        }
43
+    }
44
+
45
+    pub fn simple_request_builder(&self, method: Method, url: &str) -> RequestBuilder {
46
+        self.http_session.simple_request_builder(method, url)
47
+    }
48
+
49
+    pub fn anisette_request_builder(&self, method: Method, url: &str) -> ADIResult<RequestBuilder> {
50
+        self.http_session.anisette_request_builder(method, url)
51
+    }
52
+
53
+    pub fn account_request_builder(&self, method: Method, url: &str) -> ADIResult<RequestBuilder> {
54
+        self.http_session.account_request_builder(method, url)
55
+    }
56
+
57
+    /// Adds Anisette and `X-Apple-Identity-Token` for secondary authentication actions.
58
+    /// The identity header encodes this account's alternate DSID and its IDMS token.
59
+    pub fn identity_request_builder(&self, method: Method, url: &str) -> ADIResult<RequestBuilder> {
60
+        Ok(self.anisette_request_builder(method, url)?.header(
61
+            "X-Apple-Identity-Token",
62
+            BASE64_STANDARD.encode(format!(
63
+                "{}:{}",
64
+                self.http_session.alt_dsid.0, self.idms_token.0
65
+            )),
66
+        ))
67
+    }
68
+
69
+    pub async fn validate_code(&self, validation_code: &str) -> Result<(), ValidateCodeError> {
70
+        let validate_code_url = self
71
+            .http_session
72
+            .http_session
73
+            .url_bag()
74
+            .get("validateCode")
75
+            .and_then(Value::as_string)
76
+            .ok_or(ValidateCodeError::InvalidURLBag)?;
77
+
78
+        // AuthKitWin uses POST only when attaching a piggyback verification body.
79
+        let response = self
80
+            .identity_request_builder(Method::GET, validate_code_url)?
81
+            .header("security-code", validation_code)
82
+            .send()
83
+            .await?
84
+            .error_for_status()?;
85
+
86
+        if response.status() != reqwest::StatusCode::OK {
87
+            return Err(ValidateCodeError::UnexpectedStatus(response.status()));
88
+        }
89
+
90
+        let response_plist: Dictionary = plist::from_bytes(&response.bytes().await?)?;
91
+        let code = response_plist
92
+            .get("ec")
93
+            .and_then(Value::as_signed_integer)
94
+            .ok_or_else(|| ValidateCodeError::InvalidResponse(response_plist.clone()))?;
95
+
96
+        match code {
97
+            0 => Ok(()),
98
+            -21669 => Err(ValidateCodeError::InvalidCode),
99
+            code => Err(AppleError {
100
+                code,
101
+                message: response_plist
102
+                    .get("em")
103
+                    .and_then(Value::as_string)
104
+                    .unwrap_or("Unknown validation error")
105
+                    .to_owned(),
106
+            }
107
+            .into()),
108
+        }
109
+    }
110
+}
... ...
@@ -1,7 +1,7 @@
1 1
 use adi::proxy::{ADIError, ADIResult};
2 2
 use grandslam::bundle_information::BundleInformation;
3 3
 use grandslam::plist_request::plist_to_body;
4
-use grandslam::{AccountHTTPSession, AppHTTPSession, AppToken, IdmsToken, Token};
4
+use grandslam::{AccountHTTPSession, AppHTTPSession, AppToken, Token};
5 5
 use plist::{Dictionary, Value};
6 6
 use plist_macros::{array, dict};
7 7
 use reqwest::{Method, RequestBuilder};
... ...
@@ -235,16 +235,6 @@ impl<'a, 'b> XcodeSession<'a, 'b> {
235 235
         self.http_session.account_request_builder(method, url)
236 236
     }
237 237
 
238
-    pub fn identity_request_builder(
239
-        &self,
240
-        method: Method,
241
-        url: &str,
242
-        idms_token: &IdmsToken,
243
-    ) -> ADIResult<RequestBuilder> {
244
-        self.http_session
245
-            .identity_request_builder(method, url, idms_token)
246
-    }
247
-
248 238
     pub fn app_request_builder(&self, method: Method, url: &str) -> ADIResult<RequestBuilder> {
249 239
         self.http_session.app_request_builder(method, url)
250 240
     }
251 241