Skip to content

Commit

Permalink
Implement appstore auth logic
Browse files Browse the repository at this point in the history
  • Loading branch information
majd committed Dec 5, 2022
1 parent b73de7c commit 4633cf1
Show file tree
Hide file tree
Showing 10 changed files with 663 additions and 0 deletions.
7 changes: 7 additions & 0 deletions pkg/appstore/account.go
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
package appstore

type Account struct {
Email string `json:"email,omitempty"`
PasswordToken string `json:"passwordToken,omitempty"`
DirectoryServicesID string `json:"directoryServicesIdentifier,omitempty"`
Name string `json:"name,omitempty"`
}
37 changes: 37 additions & 0 deletions pkg/appstore/appstore.go
Original file line number Diff line number Diff line change
@@ -1 +1,38 @@
package appstore

import (
"github.com/majd/ipatool/pkg/http"
"github.com/majd/ipatool/pkg/keychain"
"github.com/majd/ipatool/pkg/util"
"io"
"os"
)

type AppStore interface {
Login(email, password, authCode string) error
Info() error
Revoke() error
}

type appstore struct {
keychain keychain.Keychain
loginClient http.Client[LoginResult]
ioReader io.Reader
machine util.Machine
}

type Args struct {
Keychain keychain.Keychain
CookieJar http.CookieJar
}

func NewAppStore(args *Args) AppStore {
return &appstore{
keychain: args.Keychain,
loginClient: http.NewClient[LoginResult](&http.Args{
CookieJar: args.CookieJar,
}),
ioReader: os.Stdin,
machine: util.NewMachine(),
}
}
27 changes: 27 additions & 0 deletions pkg/appstore/appstore_info.go
Original file line number Diff line number Diff line change
@@ -1 +1,28 @@
package appstore

import (
"encoding/json"
"github.com/majd/ipatool/pkg/log"
"github.com/pkg/errors"
)

func (a *appstore) Info() error {
data, err := a.keychain.Get("account")
if err != nil {
return errors.Wrap(err, "account was not found")
}

var account Account
err = json.Unmarshal(data, &account)
if err != nil {
return errors.Wrap(err, "failed to unmarshall account data")
}

log.Info().
Str("name", account.Name).
Str("email", account.Email).
Bool("succes", true).
Send()

return nil
}
76 changes: 76 additions & 0 deletions pkg/appstore/appstore_info_test.go
Original file line number Diff line number Diff line change
@@ -1 +1,77 @@
package appstore

import (
"github.com/golang/mock/gomock"
"github.com/majd/ipatool/pkg/keychain"
"github.com/majd/ipatool/pkg/log"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)

var _ = Describe("AppStore (Info)", Ordered, func() {
var (
ctrl *gomock.Controller
appstore AppStore
mockKeychain *keychain.MockKeychain
)

BeforeAll(func() {
log.Logger = log.Output(log.NewWriter()).Level(zerolog.Disabled)
})

BeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())
mockKeychain = keychain.NewMockKeychain(ctrl)
appstore = NewAppStore(&Args{
Keychain: mockKeychain,
})
})

AfterEach(func() {
ctrl.Finish()
})

When("keychain returns error", func() {
var testErr = errors.New("test error")

BeforeEach(func() {
mockKeychain.EXPECT().
Get("account").
Return([]byte{}, testErr)
})

It("returns wrapped error", func() {
err := appstore.Info()
Expect(err).To(MatchError(ContainSubstring(testErr.Error())))
Expect(err).To(MatchError(ContainSubstring("account was not found")))
})
})

When("keychain returns invalid data", func() {
BeforeEach(func() {
mockKeychain.EXPECT().
Get("account").
Return([]byte("..."), nil)
})

It("fails to unmarshall JSON data", func() {
err := appstore.Info()
Expect(err).To(MatchError(ContainSubstring("failed to unmarshall account data")))
})
})

When("keychain returns valid data", func() {
BeforeEach(func() {
mockKeychain.EXPECT().
Get("account").
Return([]byte("{}"), nil)
})

It("returns nil", func() {
err := appstore.Info()
Expect(err).ToNot(HaveOccurred())
})
})
})
160 changes: 160 additions & 0 deletions pkg/appstore/appstore_login.go
Original file line number Diff line number Diff line change
@@ -1 +1,161 @@
package appstore

import (
"bufio"
"encoding/json"
"fmt"
"github.com/majd/ipatool/pkg/http"
"github.com/majd/ipatool/pkg/log"
"github.com/majd/ipatool/pkg/util"
"github.com/pkg/errors"
"strings"
)

type LoginAddressResult struct {
FirstName string `plist:"firstName,omitempty"`
LastName string `plist:"lastName,omitempty"`
}

type LoginAccountResult struct {
Email string `plist:"appleId,omitempty"`
Address LoginAddressResult `plist:"address,omitempty"`
}

type LoginResult struct {
FailureType string `plist:"failureType,omitempty"`
CustomerMessage string `plist:"customerMessage,omitempty"`
Account LoginAccountResult `plist:"accountInfo,omitempty"`
DirectoryServicesID string `plist:"dsPersonId,omitempty"`
PasswordToken string `plist:"passwordToken,omitempty"`
}

func (a *appstore) Login(email, password, authCode string) error {
macAddr, err := a.machine.MacAddress()
if err != nil {
return errors.Wrap(err, "failed to read MAC address")
}

guid := strings.ReplaceAll(strings.ToUpper(macAddr), ":", "")
log.Debug().
Str("mac", macAddr).
Str("guid", guid).
Send()

return a.login(email, password, authCode, guid, 0)
}

func (a *appstore) login(email, password, authCode, guid string, attempt int) error {
log.Debug().
Int("attempt", attempt).
Str("password", password).
Str("email", email).
Str("authCode", util.IfEmpty(authCode, "<nil>")).
Msg("sending login request")

request := a.loginRequest(email, password, authCode, guid)
res, err := a.loginClient.Send(request)
if err != nil {
return errors.Wrap(err, "request failed")
}

if attempt == 0 && res.Data.FailureType == FailureTypeInvalidCredentials {
return a.login(email, password, authCode, guid, 1)
}

if res.Data.FailureType != "" && res.Data.CustomerMessage != "" {
log.Debug().
Interface("response", res).
Send()
return errors.New(res.Data.CustomerMessage)
}

if res.Data.FailureType != "" {
log.Debug().
Interface("response", res).
Send()
return errors.New("unknown error occurred")
}

if res.Data.FailureType == "" && authCode == "" && res.Data.CustomerMessage == CustomerMessageBadLogin {
log.Warn().Msg("enter 2FA code:")
authCode, err = a.promptForAuthCode()
if err != nil {
return errors.Wrap(err, "failed to prompt for 2FA code")
}

return a.login(email, password, authCode, guid, 0)
}

addr := res.Data.Account.Address
name := strings.Join([]string{addr.FirstName, addr.LastName}, " ")

data, err := json.Marshal(Account{
Name: name,
Email: res.Data.Account.Email,
PasswordToken: res.Data.PasswordToken,
DirectoryServicesID: res.Data.DirectoryServicesID,
})
if err != nil {
return errors.Wrap(err, "failed to marshall account data")
}

err = a.keychain.Set("account", data)
if err != nil {
return errors.Wrap(err, "failed to save account data in keychain")
}

log.Info().
Str("name", name).
Str("email", res.Data.Account.Email).
Bool("success", true).
Send()

return nil
}

func (a *appstore) loginRequest(email, password, authCode, guid string) http.Request {
attempt := "4"
if authCode != "" {
attempt = "2"
}

return http.Request{
Method: http.MethodPOST,
URL: a.authDomain(authCode, guid),
Headers: map[string]string{
"User-Agent": "Configurator/2.15 (Macintosh; OS X 11.0.0; 16G29) AppleWebKit/2603.3.8",
"Content-Type": "application/x-www-form-urlencoded",
},
Payload: &http.XMLPayload{
Content: map[string]interface{}{
"appleId": email,
"attempt": attempt,
"createSession": "true",
"guid": guid,
"password": fmt.Sprintf("%s%s", password, authCode),
"rmp": "0",
"why": "signIn",
},
},
}
}

func (a *appstore) promptForAuthCode() (string, error) {
reader := bufio.NewReader(a.ioReader)
authCode, err := reader.ReadString('\n')
if err != nil {
return "", errors.Wrap(err, "failed to read string from stdin")
}

return strings.Trim(authCode, "\n"), nil
}

func (*appstore) authDomain(authCode, guid string) string {
prefix := PriavteAppStoreAPIDomainPrefixWithoutAuthCode
if authCode != "" {
prefix = PriavteAppStoreAPIDomainPrefixWithAuthCode
}

return fmt.Sprintf(
"https://%s-%s%s?guid=%s", prefix, PrivateAppStoreAPIDomain, PrivateAppStoreAPIPathAuthenticate, guid)
}
Loading

0 comments on commit 4633cf1

Please sign in to comment.