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
- Sign in with an Apple ID whose account has a trusted phone number whose
id is not 1
- 2FA is requested; choose Text Message (SMS) (or Phone Call)
- No code is ever sent
Root cause
1. The phone list is never fetched, so it is always empty
Sources/DeveloperPortal/Authentication.swift:454 — parseTrustedPhoneNumbers 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/auth — src/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
- 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.
- 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.
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,sendPhone2FACodeRequestalways PUTsphoneNumber.id = 1, Apple rejects it, and no SMS/voice code arrives. There is noway 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
0.7.0-20260915.1450+9ee1899a(nightly), iOS 27.0, iPhone 16,10.7.0-20260911.484+ff25922edf2b8e4257454f0c7629276d409d6e9d7953fdf6in the build above;mainis byte-identical to that revision today, so this is still presentSteps to reproduce
idis not1Root cause
1. The phone list is never fetched, so it is always empty
Sources/DeveloperPortal/Authentication.swift:454—parseTrustedPhoneNumbersreads only from thetop level of whatever dictionary it is handed:
and the only inputs are the GrandSlam signin response and its
Statussub-dictionary(
Authentication.swift:582-585):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 theshipped binary.
Every other implementation gets
trustedPhoneNumbersfrom that endpoint, as a dedicated call:GET https://gsa.apple.com/auth—src/auth/apple_account.rs,get_trusted_numbers()GET https://idmsa.apple.com/appleauth/authAuthOptionsResponse.trustedPhoneNumbers(For reference, in at least one Apple response shape the list is nested —
phoneNumberVerification.trustedPhoneNumbers— e.g. the 412 body inicloud-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:That value is what goes into the PUT body (
Authentication.swift:694-703), so the appasks Apple to text a number id that does not belong to the account.
The same literal appears again at
Authentication.swift:758and is used to validate thecode afterwards.
The UI cannot compensate:
SideStore/Handlers/SignInFlowHandler.swift:130builds therequest with
activeID: phoneNumbers.first?.id ?? "", and:374falls back to that emptystring, 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
movzinstructions that materialise the Swiftsmall 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.
phoneNumbersreally is empty.Suggested fix
isideload'sget_trusted_numbers()does, before prompting for a delivery method — and treat thelist as the source of truth for the id.
?? "1"/return "1"defaults. If no phone id is available, thatshould 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
/auth/verify/trusteddeviceand needs no phone id at all."1"literals to their real id.