Appleの認証処理をPythonとRustで再現できるか試してみた

に公開

Appleの認証における全体像

Appleのサービスでは、Apple IDとパスワードだけでなく、多要素認証とSRPを使って本人確認をセキュアに実施しています。

SRPを使う目的は、盗聴防止でパスワードをネットワーク上に送らず認証するためであり、Appleにもパスワードを保存しない形になっています。また、多要素認証を行うことによって、他人によるなりすましを防ぐことができます。

必要な構成要素

srp_client: パスワードを使った安全な認証鍵

通信の流れは、以下のようになっています。

クライアントとサーバーで共通鍵を計算する
↓
サーバーに「私はこの共通鍵を知ってるよ」と証明
↓
サーバーも共通鍵を持っていて、お互いに確認し合う
↓
正しく一致すれば認証成功

rustだと、srpクレートを用いて下記のように実装されます。

  • クライアントのインスタンスを立ち上げる
    • ハッシュ関数にSHA-256を使用
    • 2048bitの素数とジェネレータを指定
  • 32バイト(256bit)のランダムな秘密鍵aを生成する
  • クライアント公開鍵 A = g^a mod Nを計算する
    • 数学的には Diffie-Hellman 鍵交換の "公開鍵生成" に相当
let srp_client = SrpClient::<Sha256>::new(&G_2048);
let a: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
let a_pub = srp_client.compute_public_ephemeral(&a);

omnisette: Appleが求めるAnisette情報を用意する

Appleでは、ログインの際に「Anisette Header」と呼ばれる特殊な識別情報が必要になります。

  • 簡潔に言うと、OSやデバイスに関する秘密の情報(iOSバージョン、シリアル番号っぽい情報など)
  • omnisetteを用いて、ヘッダー情報を自作アプリや非公式APIから模倣する
  • これにより、Appleデバイスからのアクセスであると主張する

代表的なヘッダーとしては、下記があります。

# 署名付きの機器情報(バイナリ plist → Base64X-Apple-I-MD: eyJkZXZpY2VJVSA4EBE6IjAwOTAw...

# X-Apple-I-MD に対する Appleの証明書で署名された署名データ
X-Apple-I-MD-M: MIPCdgCCAk+gLwINPgIQ...

# デバイスのUDID
X-Mme-Device-Id: 00001234-000352947A43001E

# デバイスのシリアル番号
X-Apple-I-SRL-NO: B10PL0ABC123

aos_kit: SRPとAnisetteを組み合わせる役割

  • aos_kitは、Appleの認証システム全体(AOS = Apple Online Services)にアクセスするためのラッパー
  • 内部でsrp clientomnisetteを使い、最終的にログイン用のトークンやセッションを得る機能を提供

aos_kitの役割を可視化すると、下記のようになります。

Apple IDとパスワード → srp clientで安全なやりとり
                           ↓
                       Appleが認証
                           ↓
     Appleから返ってきたトークンを使ってセッション維持

全体の流れ

[1] ユーザー入力:Apple ID & パスワード
       ↓
[2] srp client:SRPプロトコルを用いてハンドシェイク
       ↓
[3] omnisette:Appleが要求する特殊なヘッダーを生成
       ↓
[4] aos_kit:Apple認証APIとやりとり、セッション取得
       ↓
[5] 成功すれば、Find My iPhone などのAPI呼び出しが可能に

実装例

Pythonの場合

下記のgistを参考にしました。

https://gist.github.com/JJTech0130/049716196f5f1751b8944d93e73d3452?permalink_comment_id=5380654

下記のように簡単に認証ができます。

response, spd = gsa_authenticate("<Apple ID>", "<PASSWORD>")

関数全体の流れとしては、上から順番に下記のようになっています。この全体の認証フローは、rustで実装する場合でも同じです。

  • gsa_authenticated_request
    • Appleの GSA API (GsService2) にリクエストを送る関数。
    • SRPハンドシェイクに必要なデータ (s, B, c, M2, spd など) を取得するために呼ばれる。
  • encrypt_password
    • Appleの SRP で用いるパスワードをハッシュ化し、PBKDF2 により暗号化されたパスワードを生成。
  • decrypt_cbc
    • spd(Signed Payload Data)をセッションキーを用いてAES-CBCで復号。
  • plist.loads
    • Appleのレスポンス形式である plist(XML形式)を Python の辞書へ変換。
  • usr.verify_session
    • サーバーから返された M2 を検証して、SRP セッションが正当なものか確認。

Rustの場合

Rustの実装では、下記のリポジトリを使用しました。

https://github.com/SideStore/apple-private-apis

実際の実装方法は下記のリポジトリにまとまっています。README.mdのUsageを参考にしてください。

https://github.com/bamboo-nova/rust-airtags

メインのコードはこちらになります。環境変数を読み込んで、apple-private-apisで提供されてる機能にApple IDとパスワードを渡すだけで実現できました。

use icloud_auth::AppleAccount;
use omnisette::AnisetteConfiguration;
use anyhow::Context;
use dotenvy::dotenv;
use std::env;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 0. 環境変数の読み込み
    dotenv().ok();
    let apple_id = env::var("APPLE_ID")
        .context("環境変数 APPLE_ID が設定されていません")?;
    let apple_pw = env::var("APPLE_APP_PASSWORD")
        .context("環境変数 APPLE_APP_PASSWORD が設定されていません")?;


    // 1. 端末ローカルの Anisette 情報を自動生成
    let anisette_cfg = AnisetteConfiguration::new();
    let mut acc = AppleAccount::new(anisette_cfg).await?;

    // 2. メール & パスワードで SRP ログイン
    match acc.login_email_pass(&apple_id, &apple_pw).await? {
        icloud_auth::LoginState::LoggedIn => {
            println!("ログイン完了。GsIdmsToken = {:?}", acc.get_pet());
        }
        next_state => {
            println!("追加認証が必要: {:?}", next_state);
        }
    }

    Ok(())
}

まとめ

今回はApple ID認証をコードから実現できるかどうかpythonおよびrustで検証してみました。

本当はAirTagの位置情報をPCから直接取り出そうと思っていたんですが、plistフォーマットのファイルがSonoma 14.4+ / iOS 17.5+ 以降からAES-GCM化されているため復号して取り出すのが難しく、そちらは断念しました。将来的にはチャレンジしてみたいと思います(一応、VenturaであればItems.data を plutil 変換することで抽出できるみたいです)。

Discussion