Skip to content

[BUG] 2FA SMS never sent: trusted phone list is never fetched, and the phone id silently falls back to a hardcoded "1" #2

Description

@nfrhnh

Summary

Signing in to an Apple ID that requires 2FA, the code is never sent. On accounts
whose trusted phone number is not id 1, sendPhone2FACodeRequest always PUTs
phoneNumber.id = 1, Apple rejects it, and no SMS/voice code arrives. There is no
way for the user to fix this from the UI.

The underlying reason the wrong id is used at all is that SideSign never obtains
the trusted phone number list: parseTrustedPhoneNumbers() always returns [],
so the UI passes an empty id down, and the hardcoded fallback takes over.

Environment

  • SideStore 0.7.0-20260915.1450+9ee1899a (nightly), iOS 27.0, iPhone 16,1
  • Also reproduces on the combined LiveContainer+SideStore package, SideStore 0.7.0-20260911.484+ff25922e
  • SideSign is pinned at df2b8e4257454f0c7629276d409d6e9d7953fdf6 in the build above; main is byte-identical to that revision today, so this is still present

Steps to reproduce

  1. Sign in with an Apple ID whose account has a trusted phone number whose id is not 1
  2. 2FA is requested; choose Text Message (SMS) (or Phone Call)
  3. No code is ever sent

Root cause

1. The phone list is never fetched, so it is always empty

Sources/DeveloperPortal/Authentication.swift:454parseTrustedPhoneNumbers reads only from the
top level of whatever dictionary it is handed:

let list = (dict?["trustedPhoneNumbers"] as? [[String: any Sendable]])
        ?? (dict?["phoneNumbers"] as? [[String: any Sendable]])
        ?? []

and the only inputs are the GrandSlam signin response and its Status sub-dictionary
(Authentication.swift:582-585):

var phoneNumbers = parseTrustedPhoneNumbers(from: completeResponseDictionary)
if phoneNumbers.isEmpty, let statusDictionary {
    phoneNumbers = parseTrustedPhoneNumbers(from: statusDictionary)
}

SideSign never issues the request that actually returns this list. Its whole GSA URL
surface is /grandslam/GsService2[/validate|/lookup], /auth/verify/phone[/put|/securitycode]
and /auth/verify/trusteddevice — there is no auth-options endpoint anywhere in the
shipped binary.

Every other implementation gets trustedPhoneNumbers from that endpoint, as a dedicated call:

Project Request
isideload (this project's own desktop sibling) GET https://gsa.apple.com/authsrc/auth/apple_account.rs, get_trusted_numbers()
fastlane GET https://idmsa.apple.com/appleauth/auth
Xcodes AuthOptionsResponse.trustedPhoneNumbers

(For reference, in at least one Apple response shape the list is nested —
phoneNumberVerification.trustedPhoneNumbers — e.g. the 412 body in
icloud-photos-downloader/icloud_photos_downloader#1026, so a top-level-only read
would miss it even if the response were available.)

2. The empty list becomes a silent hardcoded "1"

Authentication.swift:687-692:

let sanitizedPhoneID: String = {
    if let id = requestedPhoneID?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty {
        return id
    }
    return "1"      // <-- always taken, because the list is always empty
}()

That value is what goes into the PUT body (Authentication.swift:694-703), so the app
asks Apple to text a number id that does not belong to the account.

The same literal appears again at Authentication.swift:758 and is used to validate the
code afterwards.

The UI cannot compensate: SideStore/Handlers/SignInFlowHandler.swift:130 builds the
request with activeID: phoneNumbers.first?.id ?? "", and :374 falls back to that empty
string, so an empty id is passed down by construction.

Evidence that the fallback is the path being taken

Patching the two "1" literals in the shipped arm64 binary to the account's real id
(changing only the immediate of the two movz instructions that materialise the Swift
small string, 1 byte each) makes sign-in succeed on the same account, same device, same
network, with no other change. So the id sent really is the hardcoded one, i.e.
phoneNumbers really is empty.

Suggested fix

  1. Fetch the trusted phone list from the auth-options endpoint, the way isideload's
    get_trusted_numbers() does, before prompting for a delivery method — and treat the
    list as the source of truth for the id.
  2. Remove the silent ?? "1" / return "1" defaults. If no phone id is available, that
    should surface as an error to the user (or fall back to the trusted-device flow), not
    as a request sent to a guessed id.

Workarounds users currently have

  • Choose "Apple Devices" instead of SMS, which goes through
    /auth/verify/trusteddevice and needs no phone id at all.
  • Binary-patch the two "1" literals to their real id.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions