Skip to content

Commit

Permalink
feat: support NEW srp auth in ipsw download dev cmd
Browse files Browse the repository at this point in the history
  • Loading branch information
blacktop committed Oct 20, 2024
1 parent 2aacaf6 commit e785638
Show file tree
Hide file tree
Showing 3 changed files with 895 additions and 2 deletions.
154 changes: 152 additions & 2 deletions internal/download/dev_portal.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ package download
import (
"bytes"
"context"
"crypto"
"crypto/sha1"
"crypto/tls"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
Expand All @@ -30,6 +32,7 @@ import (
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/PuerkitoBio/goquery"
"github.com/apex/log"
"github.com/blacktop/ipsw/internal/srp"
"github.com/blacktop/ipsw/internal/utils"
"github.com/pkg/errors"
)
Expand All @@ -53,7 +56,10 @@ const (
listDownloadsActionURL = "https://developer.apple.com/services-account/QH65B2/downloadws/listDownloads.action"
adcDownloadURL = "https://developerservices2.apple.com/services/download?path="

loginURL = "https://idmsa.apple.com/appleauth/auth/signin"
authURL = "https://idmsa.apple.com/appleauth/auth"
loginURL = authURL + "/signin"
initURL = loginURL + "/init"
completeURL = loginURL + "/complete?isRememberMeEnabled=false"
trustURL = "https://idmsa.apple.com/appleauth/auth/2sv/trust"
itcServiceKey = "https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com"

Expand Down Expand Up @@ -174,8 +180,13 @@ type authService struct {
type auth struct {
AccountName string `json:"accountName,omitempty"`
Password string `json:"password,omitempty"`
RememberMe bool `json:"rememberMe,omitempty"`
RememberMe bool `json:"rememberMe"`
TrustTokens []string `json:"trust_tokens,omitempty"`
A string `json:"a,omitempty"`
Protocols []string `json:"protocols,omitempty"`
M1 string `json:"m1,omitempty"`
C string `json:"c,omitempty"`
M2 string `json:"m2,omitempty"`
}

type trustedPhoneNumber struct {
Expand Down Expand Up @@ -561,6 +572,138 @@ func (dp *DevPortal) getHashcachHeaders() error {
return nil
}

type srpInitResponse struct {
Iteration int `json:"iteration,omitempty"`
Salt string `json:"salt,omitempty"`
Protocol string `json:"protocol,omitempty"`
B string `json:"b,omitempty"`
C string `json:"c,omitempty"`
}

type srpCompleteResponse struct {
AccountName string `json:"accountName,omitempty"`
RememberMe bool `json:"rememberMe,omitempty"`
Skip2Fa bool `json:"skip2FA,omitempty"`
Pause2Fa bool `json:"pause2FA,omitempty"`
DomainAutoFill bool `json:"domainAutoFill,omitempty"`
M1 string `json:"m1,omitempty"`
C string `json:"c,omitempty"`
M2 string `json:"m2,omitempty"`
ParseRememberMeTokens bool `json:"parseRememberMeTokens,omitempty"`
ServiceErrors []serviceError `json:"serviceErrors,omitempty"`
}

func (dp *DevPortal) generateSRP(username, password string) (*http.Response, error) {
s, err := srp.NewWithHash(crypto.SHA256, 2048)
if err != nil {
return nil, err
}

buf := new(bytes.Buffer)

json.NewEncoder(buf).Encode(&auth{
AccountName: username,
A: base64.StdEncoding.EncodeToString(s.A.Bytes()),
Protocols: []string{"s2k", "s2k_fo"},
})

req, err := http.NewRequest("POST", initURL, buf)
if err != nil {
return nil, fmt.Errorf("failed to create http POST request: %v", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Requested-With", "XMLHttpRequest")
req.Header.Set("X-Apple-Widget-Key", dp.config.WidgetKey)
req.Header.Set(hashcashHeader, dp.config.HashCash)
req.Header.Add("User-Agent", userAgent)
req.Header.Set("Accept", "application/json, text/javascript")

initResponse, err := dp.Client.Do(req)
if err != nil {
return nil, err
}
defer initResponse.Body.Close()

body, err := io.ReadAll(initResponse.Body)
if err != nil {
return nil, err
}

log.Debugf("SRP INIT: (%d):\n%s\n", initResponse.StatusCode, string(body))

var srpInit srpInitResponse
if err := json.Unmarshal(body, &srpInit); err != nil {
return nil, fmt.Errorf("failed to deserialize response body JSON: %v", err)
}

saltBytes, err := base64.StdEncoding.DecodeString(srpInit.Salt)
if err != nil {
return nil, err
}

bBytes, err := base64.StdEncoding.DecodeString(srpInit.B)
if err != nil {
return nil, err
}

cli, err := s.NewClient([]byte(username), []byte(password), saltBytes, srpInit.Iteration)
if err != nil {
return nil, err
}

m1, m2, err := cli.Generate(saltBytes, bBytes)
if err != nil {
return nil, err
}

buf.Reset()

json.NewEncoder(buf).Encode(&auth{
AccountName: username,
C: srpInit.C,
M1: base64.StdEncoding.EncodeToString(m1),
M2: base64.StdEncoding.EncodeToString(m2),
RememberMe: false,
})

req, err = http.NewRequest("POST", completeURL, buf)
if err != nil {
return nil, fmt.Errorf("failed to create http POST request: %v", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Requested-With", "XMLHttpRequest")
req.Header.Set("X-Apple-Widget-Key", dp.config.WidgetKey)
req.Header.Set(hashcashHeader, dp.config.HashCash)
req.Header.Add("User-Agent", userAgent)
req.Header.Set("Accept", "application/json, text/javascript")

completeResponse, err := dp.Client.Do(req)
if err != nil {
return nil, err
}
defer completeResponse.Body.Close()

body, err = io.ReadAll(completeResponse.Body)
if err != nil {
return nil, err
}

log.Debugf("SRP COMPLETE: (%d):\n%s\n", completeResponse.StatusCode, string(body))

var srpComp srpCompleteResponse
if err := json.Unmarshal(body, &srpComp); err != nil {
return nil, fmt.Errorf("failed to deserialize response body JSON: %v", err)
}

if len(srpComp.ServiceErrors) > 0 {
return nil, fmt.Errorf("failed to complete SRP: %s", srpComp.ServiceErrors[0].Message)
}

return completeResponse, nil
}

func (dp *DevPortal) signIn(username, password string) error {

if err := dp.getHashcachHeaders(); err != nil {
Expand Down Expand Up @@ -600,6 +743,13 @@ func (dp *DevPortal) signIn(username, password string) error {

log.Debugf("POST Login: (%d):\n%s\n", response.StatusCode, string(body))

if response.StatusCode == 503 { // try NEW SRP login
response, err = dp.generateSRP(username, password)
if err != nil {
return err
}
}

if response.StatusCode == 409 {
dp.xAppleIDAccountCountry = response.Header.Get("X-Apple-Id-Account-Country")
dp.config.SessionID = response.Header.Get("X-Apple-Id-Session-Id")
Expand Down
92 changes: 92 additions & 0 deletions internal/srp/prime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// prime.go - Generate safe primes
//
// Copyright 2013-2017 Sudhi Herle <sudhi.herle-at-gmail-dot-com>
// License: MIT

package srp

/* UPDATED TO WORK WITH APPLE'S SRP IMPLEMENTATION by blacktop */

import (
"crypto/rand"
"math/big"
)

// safePrime generates a safe prime; i.e., a prime 'p' such that 2p+1 is also prime.
func safePrime(bits int) (*big.Int, error) {

a := new(big.Int)
for {
p, err := rand.Prime(rand.Reader, bits)
if err != nil {
return nil, err
}

// 2p+1
a = a.Lsh(p, 1)
a = a.Add(a, one)
if a.ProbablyPrime(20) {
return a, nil
}
}

// never reached
return nil, nil
}

// Return true if g is a generator for safe prime p
//
// From Cryptography Theory & Practive, Stinson and Paterson (Th. 6.8 pp 196):
//
// If p > 2 is a prime and g is in Zp*, then
// g is a primitive element modulo p iff g ^ (p-1)/q != 1 (mod p)
// for all primes q such that q divides (p-1).
//
// "Primitive Element" and "Generator" are the same thing in Number Theory.
//
// Code below added as a result of bug pointed out by Dharmalingam G. (May 2019)
func isGenerator(g, p *big.Int) bool {
p1 := big.NewInt(0).Sub(p, one)
q := big.NewInt(0).Rsh(p1, 1) // q = p-1/2 = ((p-1) >> 1)

// p is a safe prime. i.e., it is of the form 2q+1 where q is prime.
//
// => p-1 = 2q, where q is a prime.
//
// All factors of p-1 are: {2, q, 2q}
//
// So, our check really comes down to:
// 1) g ^ (p-1/2q) != 1 mod p
// => g ^ (2q/2q) != 1 mod p
// => g != 1 mod p
// Trivial case. We ignore this.
//
// 2) g ^ (p-1/2) != 1 mod p
// => g ^ (2q/2) != 1 mod p
// => g ^ q != 1 mod p
//
// 3) g ^ (p-1/q) != 1 mod p
// => g ^ (2q/q) != 1 mod p
// => g ^ 2 != 1 mod p
//

// g ^ 2 mod p
if !ok(g, big.NewInt(0).Lsh(one, 1), p) {
return false
}

// g ^ q mod p
if !ok(g, q, p) {
return false
}

return true
}

func ok(g, x *big.Int, p *big.Int) bool {
z := big.NewInt(0).Exp(g, x, p)
// the expmod should NOT be 1
return z.Cmp(one) != 0
}

// vim: noexpandtab:sw=8:ts=8:tw=92:
Loading

0 comments on commit e785638

Please sign in to comment.