|
| 1 | +use std::fmt::Write as _; |
1 | 2 | use std::io::{self, Write}; |
| 3 | +use std::time::{SystemTime, UNIX_EPOCH}; |
2 | 4 |
|
3 | 5 | use anyhow::{Context, Result}; |
| 6 | +use rand::Rng; |
4 | 7 | use reqwest::header::HeaderMap; |
| 8 | +use uuid::Uuid; |
5 | 9 |
|
6 | 10 | use super::endpoints::Endpoints; |
7 | 11 | use super::session::Session; |
8 | | -use super::srp::get_auth_headers; |
| 12 | +use super::srp::{get_auth_headers, APPLE_WIDGET_KEY}; |
9 | 13 | use crate::auth::error::AuthError; |
10 | 14 | use crate::auth::responses::AccountLoginResponse; |
11 | 15 |
|
12 | 16 | const TWO_FA_CODE_LENGTH: usize = 6; |
13 | 17 |
|
| 18 | +/// Trigger a push notification to trusted devices for 2FA code entry. |
| 19 | +/// |
| 20 | +/// Apple requires a POST to `/auth/bridge/step/0` to initiate the push |
| 21 | +/// notification flow. Without this, some accounts receive a "website login" |
| 22 | +/// email instead of a 2FA code on their trusted devices. |
| 23 | +/// |
| 24 | +/// See: icloud-photos-downloader/icloud_photos_downloader#1327 |
| 25 | +pub async fn trigger_push_notification( |
| 26 | + session: &mut Session, |
| 27 | + endpoints: &Endpoints, |
| 28 | + client_id: &str, |
| 29 | + domain: &str, |
| 30 | +) -> Result<()> { |
| 31 | + let timestamp_ms = SystemTime::now() |
| 32 | + .duration_since(UNIX_EPOCH) |
| 33 | + .context("System clock before UNIX epoch")? |
| 34 | + .as_millis(); |
| 35 | + let session_uuid = format!("{}-{}", Uuid::new_v4(), timestamp_ms); |
| 36 | + |
| 37 | + let mut ptkn_bytes = [0u8; 64]; |
| 38 | + rand::rng().fill_bytes(&mut ptkn_bytes); |
| 39 | + let mut ptkn = String::with_capacity(128); |
| 40 | + for b in &ptkn_bytes { |
| 41 | + write!(ptkn, "{b:02x}").expect("writing to String cannot fail"); |
| 42 | + } |
| 43 | + |
| 44 | + let data = serde_json::json!({ |
| 45 | + "sessionUUID": session_uuid, |
| 46 | + "ptkn": ptkn, |
| 47 | + }); |
| 48 | + |
| 49 | + let overrides: [(&str, &str); 4] = [ |
| 50 | + ("Accept", "application/json, text/plain, */*"), |
| 51 | + ("Content-type", "application/json; charset=utf-8"), |
| 52 | + ("X-Apple-App-Id", APPLE_WIDGET_KEY), |
| 53 | + ("X-Apple-Domain-Id", "3"), |
| 54 | + ]; |
| 55 | + let headers = get_auth_headers(domain, client_id, &session.session_data, Some(&overrides))?; |
| 56 | + |
| 57 | + let url = format!("{}/bridge/step/0", endpoints.auth); |
| 58 | + tracing::debug!(url = %url, "Triggering push notification to trusted devices"); |
| 59 | + |
| 60 | + let response = session |
| 61 | + .post(&url, Some(data.to_string()), Some(headers)) |
| 62 | + .await?; |
| 63 | + |
| 64 | + let status = response.status(); |
| 65 | + if !status.is_success() { |
| 66 | + let text = response.text().await.unwrap_or_default(); |
| 67 | + tracing::warn!( |
| 68 | + status = %status, |
| 69 | + body = %text, |
| 70 | + "Bridge step failed, continuing with standard 2FA flow" |
| 71 | + ); |
| 72 | + } |
| 73 | + Ok(()) |
| 74 | +} |
| 75 | + |
14 | 76 | /// Check whether a string is a valid 6-digit 2FA code. |
15 | 77 | fn is_valid_2fa_code(code: &str) -> bool { |
16 | 78 | code.len() == TWO_FA_CODE_LENGTH && code.chars().all(|c| c.is_ascii_digit()) |
|
0 commit comments