diff --git a/pkg/connector/login.go b/pkg/connector/login.go index d91164ac..539e2fed 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -47,15 +47,29 @@ type TwitterLogin struct { tc *TwitterConnector isMigration bool // True if upgrading from main branch (had cookies but no encryption keys) needsPINSetup bool - - client *twittermeow.Client - profile twittermeow.CurrentUserProfile + useCookieLogin bool + + client *twittermeow.Client + webLogin *twittermeow.WebLoginSession + webLoginPassword string + webLoginChallenge *twittermeow.WebLoginChallenge + webLoginMethods []twittermeow.WebLoginAuthMethod + profile twittermeow.CurrentUserProfile } var ( - LoginStepIDCookies = "fi.mau.twitter.login.enter_cookies" - LoginStepJuiceboxPIN = "fi.mau.twitter.login.juicebox_pin" - LoginStepIDComplete = "fi.mau.twitter.login.complete" + LoginFlowIDPassword = "password" + LoginFlowIDCookies = "cookies" + LoginStepIDCredentials = "fi.mau.twitter.login.enter_credentials" + LoginStepIDVerification = "fi.mau.twitter.login.enter_verification" + LoginStepIDAuthMethod = "fi.mau.twitter.login.select_auth_method" + LoginStepIDCookies = "fi.mau.twitter.login.enter_cookies" + LoginStepJuiceboxPIN = "fi.mau.twitter.login.juicebox_pin" + LoginStepIDComplete = "fi.mau.twitter.login.complete" + loginFieldIdentifier = "identifier" + loginFieldPassword = "password" + loginFieldVerificationCode = "verification_code" + loginFieldAuthMethod = "auth_method" ) var _ bridgev2.LoginProcessCookies = (*TwitterLogin)(nil) @@ -104,26 +118,47 @@ var ( Err: "Couldn't read your X account ID. Please try again.", StatusCode: http.StatusInternalServerError, } + ErrMissingLoginInput = bridgev2.RespError{ + ErrCode: "FI.MAU.TWITTER.MISSING_LOGIN_INPUT", + Err: "Missing required login input.", + StatusCode: http.StatusBadRequest, + } + ErrWebLoginFailed = bridgev2.RespError{ + ErrCode: "FI.MAU.TWITTER.LOGIN_FAILED", + Err: "X login failed.", + StatusCode: http.StatusBadGateway, + } ) func (tc *TwitterConnector) GetLoginFlows() []bridgev2.LoginFlow { return []bridgev2.LoginFlow{ + { + Name: "Username/password", + Description: "Log in with your X username, email, or phone number and password", + ID: LoginFlowIDPassword, + }, { Name: "Cookies", Description: "Log in with your X account using your cookies", - ID: "cookies", + ID: LoginFlowIDCookies, }, } } func (tc *TwitterConnector) CreateLogin(_ context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) { - if flowID != "cookies" { + if flowID == "" { + flowID = LoginFlowIDPassword + } + if flowID != LoginFlowIDPassword && flowID != LoginFlowIDCookies { return nil, bridgev2.ErrInvalidLoginFlowID } - return &TwitterLogin{User: user, tc: tc}, nil + return &TwitterLogin{User: user, tc: tc, useCookieLogin: flowID == LoginFlowIDCookies}, nil } func (t *TwitterLogin) Start(_ context.Context) (*bridgev2.LoginStep, error) { + if !t.useCookieLogin { + return makeCredentialsStep(""), nil + } return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeCookies, StepID: LoginStepIDCookies, @@ -153,6 +188,119 @@ func (t *TwitterLogin) Start(_ context.Context) (*bridgev2.LoginStep, error) { func (t *TwitterLogin) Cancel() {} +func makeCredentialsStep(errorLine string) *bridgev2.LoginStep { + instructions := "Enter your X username, email, or phone number and password." + if errorLine != "" { + instructions = fmt.Sprintf("%s\n\n%s", errorLine, instructions) + } + return &bridgev2.LoginStep{ + Type: bridgev2.LoginStepTypeUserInput, + StepID: LoginStepIDCredentials, + Instructions: instructions, + UserInputParams: &bridgev2.LoginUserInputParams{ + Fields: []bridgev2.LoginInputDataField{ + { + Type: bridgev2.LoginInputFieldTypeUsername, + ID: loginFieldIdentifier, + Name: "Username, email, or phone", + Description: "The identifier you use to sign in to X.", + }, + { + Type: bridgev2.LoginInputFieldTypePassword, + ID: loginFieldPassword, + Name: "Password", + }, + }, + }, + } +} + +func makeVerificationStep(challenge *twittermeow.WebLoginChallenge, errorLine string) *bridgev2.LoginStep { + instructions := "X needs additional verification for this login." + fieldName := "Verification" + fieldType := bridgev2.LoginInputFieldTypeToken + if challenge != nil { + if challenge.Description != "" { + instructions = challenge.Description + } else if challenge.Hint != "" { + instructions = challenge.Hint + } + switch challenge.InputKind { + case twittermeow.WebLoginChallengeInputKindPhoneNumber: + fieldName = "Phone number" + fieldType = bridgev2.LoginInputFieldTypePhoneNumber + if instructions == "" { + instructions = "Enter the phone number associated with your X account." + } + case twittermeow.WebLoginChallengeInputKindCode: + fieldName = "Verification code" + fieldType = bridgev2.LoginInputFieldType2FACode + if instructions == "" { + instructions = "Enter the verification code from X." + } + default: + if challenge.IsTwoFactor { + fieldName = "Verification code" + fieldType = bridgev2.LoginInputFieldType2FACode + if instructions == "" { + instructions = "Enter the verification code from X." + } + } + } + } + if errorLine != "" { + instructions = fmt.Sprintf("%s\n\n%s", errorLine, instructions) + } + return &bridgev2.LoginStep{ + Type: bridgev2.LoginStepTypeUserInput, + StepID: LoginStepIDVerification, + Instructions: instructions, + UserInputParams: &bridgev2.LoginUserInputParams{ + Fields: []bridgev2.LoginInputDataField{ + { + Type: fieldType, + ID: loginFieldVerificationCode, + Name: fieldName, + }, + }, + }, + } +} + +func makeAuthMethodStep(methods []twittermeow.WebLoginAuthMethod, errorLine string) *bridgev2.LoginStep { + instructions := "Choose how to verify this X login." + if errorLine != "" { + instructions = fmt.Sprintf("%s\n\n%s", errorLine, instructions) + } + options := make([]string, 0, len(methods)) + for _, method := range methods { + if !method.Supported { + continue + } + if method.Name == "" { + options = append(options, method.ID) + } else { + options = append(options, method.Name) + } + } + return &bridgev2.LoginStep{ + Type: bridgev2.LoginStepTypeUserInput, + StepID: LoginStepIDAuthMethod, + Instructions: instructions, + UserInputParams: &bridgev2.LoginUserInputParams{ + Fields: []bridgev2.LoginInputDataField{ + { + Type: bridgev2.LoginInputFieldTypeSelect, + ID: loginFieldAuthMethod, + Name: "Verification method", + Description: "Choose the method X should use for this login.", + Options: options, + }, + }, + }, + } +} + func makePINStep(errorLine string, isSetup bool) *bridgev2.LoginStep { instructions := passcodeBodyRecover fieldName := "Passcode" @@ -395,9 +543,7 @@ func (t *TwitterLogin) bootstrapJuiceboxPIN(ctx context.Context, pin string) (*K juiceboxLogger := t.User.Log.With().Str("component", "juicebox").Logger() juiceboxLogger.Debug(). - Str("juicebox_config", juiceboxConfigJSON). Int("juicebox_config_len", len(juiceboxConfigJSON)). - Any("auth_tokens", authTokens). Int("auth_tokens_count", len(authTokens)). Int("max_guess_count", addResp.Data.UserAddPublicKey.TokenMap.MaxGuessCount). Msg("Juicebox bootstrap parameters") @@ -437,9 +583,7 @@ func (t *TwitterLogin) recoverJuiceboxPIN( juiceboxLogger := t.User.Log.With().Str("component", "juicebox").Logger() juiceboxLogger.Debug(). - Str("juicebox_config", juiceboxConfigJSON). Int("juicebox_config_len", len(juiceboxConfigJSON)). - Any("auth_tokens", authTokens). Int("auth_tokens_count", len(authTokens)). Msg("Juicebox recovery parameters") @@ -462,6 +606,259 @@ func (t *TwitterLogin) recoverJuiceboxPIN( } func (t *TwitterLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { + if _, ok := input["pin"]; ok { + return t.submitPINInput(ctx, input) + } + if _, ok := input[loginFieldAuthMethod]; ok { + return t.submitWebAuthMethodInput(ctx, input) + } + if _, ok := input[loginFieldVerificationCode]; ok || t.webLoginChallenge != nil { + return t.submitWebVerificationInput(ctx, input) + } + if _, ok := input[loginFieldIdentifier]; ok || input[loginFieldPassword] != "" { + return t.submitCredentialsInput(ctx, input) + } + return nil, ErrMissingLoginInput +} + +func (t *TwitterLogin) submitCredentialsInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { + identifier := strings.TrimSpace(input[loginFieldIdentifier]) + password := input[loginFieldPassword] + if identifier == "" || password == "" { + return nil, ErrMissingLoginInput + } + + client := twittermeow.NewClient(twitCookies.NewCookies(nil), nil, t.User.Log.With().Str("component", "login_twitter_client").Logger()) + t.webLogin = twittermeow.NewWebLoginSession(client) + t.webLoginPassword = password + t.webLoginChallenge = nil + t.webLoginMethods = nil + + result, err := t.webLogin.Start(ctx) + if err != nil { + return nil, webLoginFailureError(err) + } + if result.Status != twittermeow.WebLoginStatusNeedsIdentifier { + return t.handleWebLoginResult(ctx, result) + } + + result, err = t.webLogin.SubmitCredentials(ctx, identifier, password) + if err != nil { + return handleWebLoginCredentialsError(err) + } + return t.handleWebLoginResult(ctx, result) +} + +func (t *TwitterLogin) submitWebAuthMethodInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { + if t.webLogin == nil { + t.webLoginMethods = nil + return makeCredentialsStep("The X login session expired. Enter your X login details again."), nil + } + methodID := strings.TrimSpace(input[loginFieldAuthMethod]) + if methodID == "" { + return nil, ErrMissingLoginInput + } + result, err := t.webLogin.SubmitAuthMethod(ctx, methodID) + if err != nil { + return nil, webLoginFailureError(err) + } + return t.handleWebLoginResult(ctx, result) +} + +func findWebLoginAuthMethod(methods []twittermeow.WebLoginAuthMethod, selected string) (twittermeow.WebLoginAuthMethod, bool) { + selected = normalizeLoginChoice(selected) + for _, method := range methods { + if normalizeLoginChoice(method.ID) == selected || normalizeLoginChoice(method.Name) == selected { + return method, true + } + } + return twittermeow.WebLoginAuthMethod{}, false +} + +func normalizeLoginChoice(value string) string { + value = strings.ToLower(strings.TrimSpace(value)) + value = strings.ReplaceAll(value, " ", "") + value = strings.ReplaceAll(value, "_", "") + value = strings.ReplaceAll(value, "-", "") + value = strings.ReplaceAll(value, ".", "") + return value +} + +func (t *TwitterLogin) submitWebVerificationInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { + if t.webLogin == nil { + t.webLoginChallenge = nil + t.webLoginPassword = "" + t.webLoginMethods = nil + return makeCredentialsStep("The X login session expired. Enter your X login details again."), nil + } + text := strings.TrimSpace(input[loginFieldVerificationCode]) + if text == "" { + return nil, ErrMissingLoginInput + } + result, err := t.webLogin.SubmitText(ctx, text) + if err != nil { + return handleWebLoginVerificationError(t.webLoginChallenge, err) + } + if result.Status == twittermeow.WebLoginStatusNeedsPassword && t.webLoginPassword != "" { + result, err = t.webLogin.SubmitPassword(ctx, t.webLoginPassword) + if err != nil { + return handleWebLoginCredentialsError(err) + } + } + return t.handleWebLoginResult(ctx, result) +} + +func (t *TwitterLogin) handleWebLoginResult(ctx context.Context, result *twittermeow.WebLoginResult) (*bridgev2.LoginStep, error) { + if result == nil { + return makeCredentialsStep("X did not return a login step. Try again."), nil + } + switch result.Status { + case twittermeow.WebLoginStatusComplete: + return t.completeWebLogin(ctx) + case twittermeow.WebLoginStatusNeedsAuthMethod: + if len(result.AuthMethods) == 0 { + return makeCredentialsStep("X returned a verification method chooser without any methods. Try again."), nil + } + t.webLoginChallenge = nil + t.webLoginMethods = result.AuthMethods + return makeAuthMethodStep(result.AuthMethods, ""), nil + case twittermeow.WebLoginStatusNeedsText: + t.webLoginChallenge = result.Challenge + t.webLoginMethods = nil + return makeVerificationStep(result.Challenge, ""), nil + case twittermeow.WebLoginStatusNeedsPassword: + if t.webLogin != nil && t.webLoginPassword != "" { + next, err := t.webLogin.SubmitPassword(ctx, t.webLoginPassword) + if err != nil { + return handleWebLoginCredentialsError(err) + } + if next != nil && next.Status != twittermeow.WebLoginStatusNeedsPassword { + return t.handleWebLoginResult(ctx, next) + } + } + return makeCredentialsStep("X still needs your password. Enter your login details again."), nil + case twittermeow.WebLoginStatusNeedsIdentifier: + return makeCredentialsStep("X still needs your username, email, or phone. Enter your login details again."), nil + default: + t.User.Log.Warn(). + Str("subtask_id", result.CurrentSubtaskID). + Str("status", string(result.Status)). + Msg("X returned unsupported login subtask") + return makeCredentialsStep(webLoginUnsupportedInstructions(result)), nil + } +} + +func webLoginUnsupportedInstructions(result *twittermeow.WebLoginResult) string { + if result != nil && result.Challenge != nil { + description := strings.TrimSpace(result.Challenge.Description) + if description != "" { + return description + } + } + return "X returned a login challenge this bridge does not support yet." +} + +func handleWebLoginCredentialsError(err error) (*bridgev2.LoginStep, error) { + if isWebLoginCredentialsInputError(err) { + return makeCredentialsStep(webLoginErrorInstructions(err)), nil + } + return nil, webLoginFailureError(err) +} + +func handleWebLoginVerificationError(challenge *twittermeow.WebLoginChallenge, err error) (*bridgev2.LoginStep, error) { + if isWebLoginVerificationInputError(err) { + return makeVerificationStep(challenge, webLoginErrorInstructions(err)), nil + } + return nil, webLoginFailureError(err) +} + +func isWebLoginCredentialsInputError(err error) bool { + var webErr *twittermeow.WebLoginError + if !errors.As(err, &webErr) { + return false + } + msg := strings.ToLower(strings.TrimSpace(webErr.Message)) + if webErr.Code != 32 { + return false + } + return strings.Contains(msg, "wrong password") || + strings.Contains(msg, "incorrect password") || + strings.Contains(msg, "invalid password") || + strings.Contains(msg, "password you entered") || + strings.Contains(msg, "password is incorrect") || + strings.Contains(msg, "username and password") && strings.Contains(msg, "did not match") || + strings.Contains(msg, "invalid username or password") || + strings.Contains(msg, "invalid credentials") || + strings.Contains(msg, "missing_account") || + strings.Contains(msg, "not registered") +} + +func isWebLoginVerificationInputError(err error) bool { + var webErr *twittermeow.WebLoginError + if !errors.As(err, &webErr) { + return false + } + msg := strings.ToLower(strings.TrimSpace(webErr.Message)) + return strings.Contains(msg, "wrong code") || + strings.Contains(msg, "incorrect code") || + strings.Contains(msg, "invalid code") || + strings.Contains(msg, "code is incorrect") || + strings.Contains(msg, "verification code") && strings.Contains(msg, "incorrect") || + strings.Contains(msg, "authentication code") && strings.Contains(msg, "incorrect") +} + +func webLoginFailureError(err error) error { + if err == nil { + return ErrWebLoginFailed + } + return ErrWebLoginFailed.WithMessage(webLoginErrorInstructions(err)) +} + +func (t *TwitterLogin) completeWebLogin(ctx context.Context) (*bridgev2.LoginStep, error) { + if t.webLogin == nil || t.webLogin.Client() == nil { + return makeCredentialsStep("The X login session expired. Enter your X login details again."), nil + } + client := t.webLogin.Client() + profile, err := client.LoadMessagesPage(ctx) + if err != nil { + return nil, fmt.Errorf("failed to load authenticated X messages page after login: %w", err) + } + t.client = client + t.profile = profile + t.persistClientCookiesAndUserID() + t.refreshPINSetupState(ctx, "Failed to determine PIN setup state after native login, using recovery prompt") + t.webLoginPassword = "" + t.webLoginChallenge = nil + t.webLoginMethods = nil + + return makePINStep("", t.needsPINSetup), nil +} + +func webLoginErrorInstructions(err error) string { + if err == nil { + return "X rejected this login. Please check the details and try again." + } + var webErr *twittermeow.WebLoginError + if errors.As(err, &webErr) { + return webErr.UserMessage() + } + if errors.Is(err, twittermeow.ErrWebLoginUnexpectedSubtask) { + return "X returned a login challenge this bridge does not support yet." + } + if errors.Is(err, twittermeow.ErrWebLoginUnsupportedAuthMethod) { + return "That X verification method is not available for this login." + } + if errors.Is(err, twittermeow.ErrWebLoginMissingAuthMethodState) { + return "The X verification method selection expired. Enter your X login details again." + } + msg := strings.TrimSpace(err.Error()) + if msg == "" { + return "X rejected this login. Please check the details and try again." + } + return fmt.Sprintf("X login failed: %s", msg) +} + +func (t *TwitterLogin) submitPINInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { pin, err := parsePINInput(input) if err != nil { return nil, err diff --git a/pkg/connector/login_live_probe_test.go b/pkg/connector/login_live_probe_test.go new file mode 100644 index 00000000..6b0423db --- /dev/null +++ b/pkg/connector/login_live_probe_test.go @@ -0,0 +1,196 @@ +//go:build liveprobe + +package connector + +import ( + "context" + "errors" + "os" + "strings" + "testing" + "time" + + "github.com/rs/zerolog" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/database" + "maunium.net/go/mautrix/id" + + "go.mau.fi/mautrix-twitter/pkg/twittermeow" + twitCookies "go.mau.fi/mautrix-twitter/pkg/twittermeow/cookies" +) + +func TestLiveNativeLoginFlowProbe(t *testing.T) { + identifier := strings.TrimSpace(os.Getenv("TWITTER_LIVE_IDENTIFIER")) + password := os.Getenv("TWITTER_LIVE_PASSWORD") + if identifier == "" || password == "" { + t.Skip("TWITTER_LIVE_IDENTIFIER and TWITTER_LIVE_PASSWORD are required") + } + + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + + tc := &TwitterConnector{} + user := &bridgev2.User{ + User: &database.User{ + MXID: id.UserID("@highest:beeper.com"), + }, + Log: zerolog.Nop(), + } + process, err := tc.CreateLogin(ctx, user, LoginFlowIDPassword) + if err != nil { + t.Fatalf("CreateLogin() failed: %v", err) + } + defer process.Cancel() + + first, err := process.Start(ctx) + if err != nil { + t.Fatalf("Start() failed: %v", err) + } + if first == nil { + t.Fatal("Start() returned nil step") + } + t.Logf("first step: type=%s id=%s", first.Type, first.StepID) + if first.Type != bridgev2.LoginStepTypeUserInput || first.StepID != LoginStepIDCredentials { + t.Fatalf("unexpected first step: type=%s id=%s", first.Type, first.StepID) + } + + next, err := process.(bridgev2.LoginProcessUserInput).SubmitUserInput(ctx, map[string]string{ + loginFieldIdentifier: identifier, + loginFieldPassword: password, + }) + if err != nil { + t.Fatalf("SubmitUserInput(credentials) failed: %v", err) + } + if next == nil { + t.Fatal("SubmitUserInput(credentials) returned nil step") + } + t.Logf("next step: type=%s id=%s instructions=%q", next.Type, next.StepID, next.Instructions) + + switch next.StepID { + case LoginStepJuiceboxPIN, LoginStepIDVerification, LoginStepIDComplete: + verificationCode := strings.TrimSpace(os.Getenv("TWITTER_LIVE_VERIFICATION_CODE")) + if next.StepID == LoginStepIDVerification { + if verificationCode == "" { + return + } + next, err = process.(bridgev2.LoginProcessUserInput).SubmitUserInput(ctx, map[string]string{ + loginFieldVerificationCode: verificationCode, + }) + if err != nil { + t.Fatalf("SubmitUserInput(verification) failed: %v", err) + } + if next == nil { + t.Fatal("SubmitUserInput(verification) returned nil step") + } + t.Logf("after verification step: type=%s id=%s instructions=%q", next.Type, next.StepID, next.Instructions) + } + if verificationCode != "" && next.StepID == LoginStepIDVerification { + t.Fatalf("verification code was provided but flow stayed on verification: %s", next.Instructions) + } + if next.StepID != LoginStepJuiceboxPIN { + return + } + case LoginStepIDCredentials: + if strings.Contains(next.Instructions, "Wait a bit") || + strings.Contains(next.Instructions, "cannot log") || + strings.Contains(next.Instructions, "could not log") || + strings.Contains(next.Instructions, "returned a login challenge") { + t.Fatalf("native credential submission returned retry/error step: %s", next.Instructions) + } + t.Fatalf("native credential submission did not advance past credentials: %s", next.Instructions) + default: + t.Fatalf("unexpected next step after credentials: type=%s id=%s instructions=%q", next.Type, next.StepID, next.Instructions) + } + + pin := strings.TrimSpace(os.Getenv("TWITTER_LIVE_PIN")) + if pin == "" { + return + } + if next.StepID != LoginStepJuiceboxPIN { + t.Fatalf("PIN was provided but flow did not reach PIN step: type=%s id=%s instructions=%q", next.Type, next.StepID, next.Instructions) + } + final, err := process.(bridgev2.LoginProcessUserInput).SubmitUserInput(ctx, map[string]string{ + "pin": pin, + }) + if err != nil { + t.Fatalf("SubmitUserInput(pin) failed: %v", err) + } + if final == nil { + t.Fatal("SubmitUserInput(pin) returned nil step") + } + t.Logf("final step: type=%s id=%s", final.Type, final.StepID) + if final.Type != bridgev2.LoginStepTypeComplete || final.StepID != LoginStepIDComplete { + t.Fatalf("unexpected final step after pin: type=%s id=%s instructions=%q", final.Type, final.StepID, final.Instructions) + } +} + +func TestLiveNativeLoginStageProbe(t *testing.T) { + identifier := strings.TrimSpace(os.Getenv("TWITTER_LIVE_IDENTIFIER")) + password := os.Getenv("TWITTER_LIVE_PASSWORD") + if identifier == "" || password == "" { + t.Skip("TWITTER_LIVE_IDENTIFIER and TWITTER_LIVE_PASSWORD are required") + } + + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + + client := twittermeow.NewClient(twitCookies.NewCookies(nil), nil, zerolog.Nop()) + webLogin := twittermeow.NewWebLoginSession(client) + + result, err := webLogin.Start(ctx) + logStage(t, "start", result, err) + if err != nil { + t.Fatalf("Start() failed: %v", err) + } + + result, err = webLogin.SubmitCredentials(ctx, identifier, password) + logStage(t, "credentials", result, err) + if err != nil { + t.Fatalf("SubmitCredentials() failed: %v", err) + } + switch result.Status { + case twittermeow.WebLoginStatusComplete, twittermeow.WebLoginStatusNeedsText, twittermeow.WebLoginStatusNeedsPassword: + default: + t.Fatalf("SubmitCredentials() returned status %s, want complete, password, or text challenge", result.Status) + } +} + +func TestLiveIdentifierOnlyProbe(t *testing.T) { + identifier := strings.TrimSpace(os.Getenv("TWITTER_IDENTIFIER_PROBE")) + if identifier == "" { + t.Skip("TWITTER_IDENTIFIER_PROBE is required") + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + client := twittermeow.NewClient(twitCookies.NewCookies(nil), nil, zerolog.Nop()) + webLogin := twittermeow.NewWebLoginSession(client) + + result, err := webLogin.Start(ctx) + logStage(t, "start", result, err) + if err != nil { + t.Fatalf("Start() failed: %v", err) + } + + result, err = webLogin.SubmitIdentifier(ctx, identifier) + logStage(t, "identifier", result, err) + if err == nil && result.Status != twittermeow.WebLoginStatusNeedsPassword { + t.Fatalf("SubmitIdentifier() status = %s, want password step or a login error", result.Status) + } +} + +func logStage(t *testing.T, stage string, result *twittermeow.WebLoginResult, err error) { + t.Helper() + if result != nil { + t.Logf("%s result: status=%s subtask=%s", stage, result.Status, result.CurrentSubtaskID) + } + if err != nil { + var webErr *twittermeow.WebLoginError + if errors.As(err, &webErr) { + t.Logf("%s error: code=%d message=%q", stage, webErr.Code, webErr.Message) + return + } + t.Logf("%s error: %T", stage, err) + } +} diff --git a/pkg/connector/login_test.go b/pkg/connector/login_test.go new file mode 100644 index 00000000..5282a38f --- /dev/null +++ b/pkg/connector/login_test.go @@ -0,0 +1,142 @@ +package connector + +import ( + "context" + "errors" + "strings" + "testing" + + "maunium.net/go/mautrix/bridgev2" + + "go.mau.fi/mautrix-twitter/pkg/twittermeow" +) + +func TestSubmitUserInputRejectsMissingRequiredCredentialFields(t *testing.T) { + login := &TwitterLogin{} + tests := []map[string]string{ + {}, + {loginFieldIdentifier: "alice"}, + {loginFieldPassword: "secret"}, + {loginFieldIdentifier: " ", loginFieldPassword: "secret"}, + {loginFieldIdentifier: "alice", loginFieldPassword: ""}, + } + + for _, input := range tests { + step, err := login.SubmitUserInput(context.Background(), input) + if step != nil { + t.Fatalf("SubmitUserInput(%#v) step = %#v, want nil", input, step) + } + if !errors.Is(err, ErrMissingLoginInput) { + t.Fatalf("SubmitUserInput(%#v) error = %v, want ErrMissingLoginInput", input, err) + } + } +} + +func TestHandleWebLoginCredentialsErrorRetriesOnlyCredentialErrors(t *testing.T) { + step, err := handleWebLoginCredentialsError(&twittermeow.WebLoginError{ + Code: 32, + Message: "Wrong password", + }) + if err != nil { + t.Fatalf("handleWebLoginCredentialsError(wrong password) error = %v", err) + } + if step == nil || step.StepID != LoginStepIDCredentials { + t.Fatalf("handleWebLoginCredentialsError(wrong password) step = %#v, want credentials step", step) + } + + step, err = handleWebLoginCredentialsError(&twittermeow.WebLoginError{ + Code: 399, + Message: "We've temporarily limited your login. Please try again later.", + }) + if step != nil { + t.Fatalf("handleWebLoginCredentialsError(temporary limit) step = %#v, want nil", step) + } + var respErr bridgev2.RespError + if !errors.As(err, &respErr) || respErr.ErrCode != ErrWebLoginFailed.ErrCode { + t.Fatalf("handleWebLoginCredentialsError(temporary limit) error = %#v, want ErrWebLoginFailed response", err) + } +} + +func TestMakeAuthMethodStepUsesNativeSelect(t *testing.T) { + methods := []twittermeow.WebLoginAuthMethod{ + {ID: "Totp", Name: "Authenticator App", Supported: true}, + {ID: "Sms", Name: "Text Message", Supported: false}, + {ID: "BackupCode", Name: "Backup Code", Supported: true}, + {ID: "U2fSecurityKey", Name: "Security Key PC", Supported: false}, + } + step := makeAuthMethodStep(methods, "") + + if step.Type != bridgev2.LoginStepTypeUserInput { + t.Fatalf("Type = %s, want user input", step.Type) + } + if step.StepID != LoginStepIDAuthMethod { + t.Fatalf("StepID = %s, want %s", step.StepID, LoginStepIDAuthMethod) + } + if step.UserInputParams == nil || len(step.UserInputParams.Fields) != 1 { + t.Fatalf("UserInputParams = %#v, want one field", step.UserInputParams) + } + field := step.UserInputParams.Fields[0] + if field.Type != bridgev2.LoginInputFieldTypeSelect { + t.Fatalf("field.Type = %s, want select", field.Type) + } + if field.ID != loginFieldAuthMethod { + t.Fatalf("field.ID = %s, want %s", field.ID, loginFieldAuthMethod) + } + if strings.Join(field.Options, ",") != "Authenticator App,Backup Code" { + t.Fatalf("field.Options = %#v", field.Options) + } + if strings.Contains(step.Instructions, "not supported") { + t.Fatalf("Instructions = %q, want no unsupported caveat", step.Instructions) + } +} + +func TestWebLoginUnsupportedInstructionsUsesChallengeDescription(t *testing.T) { + result := &twittermeow.WebLoginResult{ + Status: twittermeow.WebLoginStatusUnsupported, + Challenge: &twittermeow.WebLoginChallenge{ + Description: "Text message verification is coming soon.", + }, + } + + if got := webLoginUnsupportedInstructions(result); got != "Text message verification is coming soon." { + t.Fatalf("webLoginUnsupportedInstructions() = %q", got) + } +} + +func TestFindWebLoginAuthMethodMatchesNameOrID(t *testing.T) { + methods := []twittermeow.WebLoginAuthMethod{ + {ID: "Totp", Name: "Authenticator App", Supported: true}, + {ID: "Sms", Name: "Text Message", Supported: true}, + {ID: "BackupCode", Name: "Backup Code", Supported: true}, + } + if method, ok := findWebLoginAuthMethod(methods, "Authenticator App"); !ok || method.ID != "Totp" { + t.Fatalf("find by label = %#v %t, want Totp", method, ok) + } + if method, ok := findWebLoginAuthMethod(methods, "backup_code"); !ok || method.ID != "BackupCode" { + t.Fatalf("find by normalized ID = %#v %t, want BackupCode", method, ok) + } + if method, ok := findWebLoginAuthMethod(methods, "text_message"); !ok || method.ID != "Sms" { + t.Fatalf("find by normalized ID = %#v %t, want Sms", method, ok) + } +} + +func TestMakeVerificationStepUsesPhoneNumberInput(t *testing.T) { + step := makeVerificationStep(&twittermeow.WebLoginChallenge{ + Description: "Enter the phone number associated with your X account.", + InputKind: twittermeow.WebLoginChallengeInputKindPhoneNumber, + }, "") + + if step.UserInputParams == nil || len(step.UserInputParams.Fields) != 1 { + t.Fatalf("UserInputParams = %#v, want one field", step.UserInputParams) + } + field := step.UserInputParams.Fields[0] + if field.Type != bridgev2.LoginInputFieldTypePhoneNumber { + t.Fatalf("field.Type = %s, want phone_number", field.Type) + } + if field.Name != "Phone number" { + t.Fatalf("field.Name = %q, want Phone number", field.Name) + } + if !strings.Contains(step.Instructions, "phone number") { + t.Fatalf("Instructions = %q, want phone number prompt", step.Instructions) + } +} diff --git a/pkg/twittermeow/castle_token.go b/pkg/twittermeow/castle_token.go new file mode 100644 index 00000000..c09830bc --- /dev/null +++ b/pkg/twittermeow/castle_token.go @@ -0,0 +1,662 @@ +package twittermeow + +import ( + "crypto/rand" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "fmt" + "math" + "net/url" + "time" +) + +const castlePublicKey = "pk_AvRa79bHyJSYSQHnRpcVtzyxetSvFerx" + +const ( + castleValueB2H = 3 + castleValueSerializedByteArray = 4 + castleValueB2HWithChecks = 5 + castleValueB2HRounded = 6 + castleValueJustAppend = 7 + castleValueEmpty = -1 +) + +func addCastleTokenToForm(form url.Values) error { + if form.Get("$castle_token") != "" { + return nil + } + token, err := createCastleRequestToken() + if err != nil { + return err + } + form.Set("$castle_token", token) + return nil +} + +func createCastleRequestToken() (string, error) { + initDelta, err := cryptoRandInt(2*60*1000, 30*60*1000+1) + if err != nil { + return "", err + } + initTime := time.Now().UnixMilli() - int64(initDelta) + tokenUUID, err := randomBytes(16) + if err != nil { + return "", err + } + tokenUUIDHex := hex.EncodeToString(tokenUUID) + + fpOne, err := castleFPOne(initTime) + if err != nil { + return "", err + } + fpTwo, err := castleFPTwo(initTime) + if err != nil { + return "", err + } + fpThree, err := castleFPThree(initTime) + if err != nil { + return "", err + } + eventLog, err := castleEventLog() + if err != nil { + return "", err + } + eventValues, err := castleFPEventValues() + if err != nil { + return "", err + } + + fpData := append(append(append(append(fpOne, fpTwo...), fpThree...), eventLog...), eventValues...) + fpData = append(fpData, 0xff) + fpDataKey := castleEncodeTimestampEncrypted(time.Now().UnixMilli()) + encryptedFPData := deriveKeyAndXORBytes(hex.EncodeToString(fpDataKey), 4, hex.EncodeToString(fpDataKey)[3], fpData) + encryptedFPDataTwo := deriveKeyAndXORBytes(tokenUUIDHex, 8, tokenUUIDHex[9], append(fpDataKey, encryptedFPData...)) + + header, err := castleHeader(tokenUUIDHex, initTime) + if err != nil { + return "", err + } + fingerprint := append(header, encryptedFPDataTwo...) + encryptedFP := xxteaEncrypt(fingerprint, []uint32{1164413191, 3891440048, 185273099, 2746598870}) + encryptedFP = append([]byte{0x0b, byte(len(encryptedFP) - len(fingerprint))}, encryptedFP...) + encryptedFP = append(encryptedFP, byte((len(encryptedFP)*2)&0xff)) + + randomByte, err := cryptoRandInt(0, 256) + if err != nil { + return "", err + } + final := append([]byte{byte(randomByte)}, xorBytes(encryptedFP, []byte{byte(randomByte)})...) + return base64.RawURLEncoding.EncodeToString(final), nil +} + +func castleHeader(uuidHex string, initTime int64) ([]byte, error) { + uuidBytes, err := hex.DecodeString(uuidHex) + if err != nil { + return nil, err + } + out := castleEncodeTimestampEncrypted(initTime) + version := uint16((3 << 13) | (1 << 11) | (6 << 6)) + out = binary.BigEndian.AppendUint16(out, version) + out = append(out, []byte(castlePublicKey)...) + out = append(out, uuidBytes...) + return out, nil +} + +func castleEncodeTimestampEncrypted(timestampMillis int64) []byte { + seconds := int64(math.Floor(float64(timestampMillis)/1e3 - 1535e6)) + if seconds < 0 { + seconds = 0 + } else if seconds > 268435455 { + seconds = 268435455 + } + timeBytes := []byte{byte(seconds >> 24), byte(seconds >> 16), byte(seconds >> 8), byte(seconds)} + msBytes := []byte{byte((timestampMillis % 1000) >> 8), byte(timestampMillis % 1000)} + key, _ := cryptoRandInt(0, 16) + return append(xorAndAppendNibbleKey(timeBytes, byte(key)), xorAndAppendNibbleKey(msBytes, byte(key))...) +} + +func xorAndAppendNibbleKey(buf []byte, key byte) []byte { + nibbles := make([]byte, 0, len(buf)*2) + for _, b := range buf { + nibbles = append(nibbles, b>>4, b&0x0f) + } + outNibbles := make([]byte, 0, len(nibbles)) + for _, nibble := range nibbles[1:] { + outNibbles = append(outNibbles, nibble^key) + } + outNibbles = append(outNibbles, key) + out := make([]byte, len(outNibbles)/2) + for i := range out { + out[i] = outNibbles[i*2]<<4 | outNibbles[i*2+1] + } + return out +} + +func processCastleFPValue(index, valueType int, val []byte, intVal int, initTime int64) ([]byte, error) { + out := []byte{byte(((31 & index) << 3) | (7 & valueType))} + switch valueType { + case castleValueB2HRounded, castleValueB2H: + out = append(out, byte(intVal)) + case castleValueB2HWithChecks: + if intVal <= 127 { + out = append(out, byte(intVal)) + } else { + out = binary.BigEndian.AppendUint16(out, uint16((1<<15)|(32767&intVal))) + } + case castleValueSerializedByteArray: + encrypted := xxteaEncrypt(val, []uint32{uint32(index), uint32(initTime), 16373134, 643144773, 1762804430, 1186572681, 1164413191}) + out = append(out, byte(len(encrypted))) + out = append(out, encrypted...) + case castleValueJustAppend: + out = append(out, val...) + case castleValueEmpty: + case 1, 2: + default: + return nil, fmt.Errorf("unsupported castle fp value type %d", valueType) + } + return out, nil +} + +func castleValuesToBytes(first, second int) []byte { + r := uint16(32767 & first) + e := uint16(65535 & second) + if r == e { + return []byte{byte((32768 | int(r)) >> 8), byte(32768 | int(r))} + } + out := make([]byte, 4) + binary.BigEndian.PutUint16(out[:2], r) + binary.BigEndian.PutUint16(out[2:], e) + return out +} + +func castleEncodeBits(bits []int, bitSize int) []byte { + numBytes := bitSize / 8 + out := make([]byte, numBytes) + for _, bit := range bits { + byteIndex := (numBytes - 1) - (bit / 8) + bitPosition := bit % 8 + if byteIndex >= 0 && byteIndex < numBytes { + out[byteIndex] |= 1 << bitPosition + } + } + return out +} + +func castleBoolArrayToBinary(values []bool, size int) int { + e := values + if size > 0 && len(values) > size { + e = values[:size] + } + out := 0 + for i := len(e) - 1; i >= 0; i-- { + if e[i] { + out |= 1 << (len(e) - i - 1) + } + } + if size > 0 && len(e) < size { + out <<= size - len(e) + } + return out +} + +func castleFPOne(initTime int64) ([]byte, error) { + timezone := jetfuelTimezone() + values := [][]byte{} + addInt := func(index, valueType, value int) error { + v, err := processCastleFPValue(index, valueType, nil, value, initTime) + values = append(values, v) + return err + } + addBytes := func(index, valueType int, value []byte) error { + v, err := processCastleFPValue(index, valueType, value, 0, initTime) + values = append(values, v) + return err + } + if err := addInt(0, castleValueB2H, 1); err != nil { + return nil, err + } + if err := addInt(1, castleValueB2H, 0); err != nil { + return nil, err + } + if err := addBytes(2, castleValueSerializedByteArray, []byte("en-US")); err != nil { + return nil, err + } + if err := addInt(3, castleValueB2HRounded, 80); err != nil { + return nil, err + } + screenDims := append(castleValuesToBytes(1920, 1920), castleValuesToBytes(1080, 1032)...) + if err := addBytes(4, castleValueJustAppend, screenDims); err != nil { + return nil, err + } + if err := addInt(5, castleValueB2HWithChecks, 24); err != nil { + return nil, err + } + if err := addInt(6, castleValueB2HWithChecks, 24); err != nil { + return nil, err + } + if err := addInt(7, castleValueB2HRounded, 10); err != nil { + return nil, err + } + if err := addBytes(8, castleValueJustAppend, castleTimezoneDiff(timezone)); err != nil { + return nil, err + } + if err := addBytes(9, castleValueJustAppend, []byte{0x02, 0x7d, 0x5f, 0xc9, 0xa7}); err != nil { + return nil, err + } + if err := addBytes(10, castleValueJustAppend, []byte{0x05, 0x72, 0x93, 0x02, 0x08}); err != nil { + return nil, err + } + if err := addBytes(11, castleValueJustAppend, append([]byte{12}, castleEncodeBits([]int{0, 1, 2, 3, 4, 5, 6}, 16)...)); err != nil { + return nil, err + } + ua := xxteaEncrypt([]byte(UserAgent), []uint32{12, uint32(initTime), 16373134, 643144773, 1762804430, 1186572681, 1164413191}) + if err := addBytes(12, castleValueJustAppend, append([]byte{1, byte(len(ua))}, ua...)); err != nil { + return nil, err + } + if err := addBytes(13, castleValueSerializedByteArray, []byte("54b4b5cf")); err != nil { + return nil, err + } + if err := addBytes(14, castleValueJustAppend, append([]byte{3}, castleEncodeBits([]int{0, 1, 2}, 8)...)); err != nil { + return nil, err + } + if err := addInt(17, castleValueB2H, 0); err != nil { + return nil, err + } + if err := addBytes(18, castleValueSerializedByteArray, []byte("c6749e76")); err != nil { + return nil, err + } + gpu := "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 (0x00002504) Direct3D11 vs_5_0 ps_5_0, D3D11)" + if err := addBytes(19, castleValueSerializedByteArray, []byte(gpu)); err != nil { + return nil, err + } + if err := addBytes(20, castleValueSerializedByteArray, []byte(castleEpochLocaleString(timezone))); err != nil { + return nil, err + } + if err := addBytes(21, castleValueJustAppend, append([]byte{8}, castleEncodeBits(nil, 8)...)); err != nil { + return nil, err + } + if err := addInt(22, castleValueB2HWithChecks, 33); err != nil { + return nil, err + } + if err := addInt(24, castleValueB2HWithChecks, 12549); err != nil { + return nil, err + } + if err := addInt(25, castleValueB2H, 0); err != nil { + return nil, err + } + if err := addInt(26, castleValueB2H, 1); err != nil { + return nil, err + } + if err := addInt(27, castleValueB2HWithChecks, 4644); err != nil { + return nil, err + } + if err := addBytes(28, castleValueJustAppend, []byte{0}); err != nil { + return nil, err + } + if err := addInt(29, castleValueB2H, 3); err != nil { + return nil, err + } + if err := addBytes(30, castleValueJustAppend, []byte{0x5d, 0xc5, 0xab, 0xb5, 0x88}); err != nil { + return nil, err + } + if err := addBytes(31, castleValueJustAppend, []byte{0xa2, 0x6a}); err != nil { + return nil, err + } + out := []byte{byte(31 & len(values))} + for _, value := range values { + out = append(out, value...) + } + return out, nil +} + +func castleTimezoneDiff(timezone string) []byte { + switch timezone { + case "America/Chicago": + return []byte{20, 4} + case "America/Los_Angeles": + return []byte{28, 4} + case "America/New_York": + return []byte{16, 4} + case "America/Denver": + return []byte{24, 4} + case "America/Anchorage": + return []byte{32, 4} + case "Pacific/Honolulu": + return []byte{40, 0} + default: + _, offset := time.Now().Zone() + return []byte{byte((-offset / 60) / 15), 0} + } +} + +func castleEpochLocaleString(timezone string) string { + switch timezone { + case "America/Chicago": + return "12/31/1969, 6:00:00 PM" + case "America/Los_Angeles": + return "12/31/1969, 4:00:00 PM" + case "America/New_York": + return "12/31/1969, 7:00:00 PM" + case "America/Denver": + return "12/31/1969, 5:00:00 PM" + case "America/Anchorage": + return "12/31/1969, 3:00:00 PM" + case "Pacific/Honolulu": + return "12/31/1969, 2:00:00 PM" + default: + return "1/1/1970, 12:00:00 AM" + } +} + +func castleFPTwo(initTime int64) ([]byte, error) { + timezone := jetfuelTimezone() + values := [][]byte{} + addInt := func(index, valueType, value int) error { + v, err := processCastleFPValue(index, valueType, nil, value, initTime) + values = append(values, v) + return err + } + addBytes := func(index, valueType int, value []byte) error { + v, err := processCastleFPValue(index, valueType, value, 0, initTime) + values = append(values, v) + return err + } + if err := addInt(0, castleValueB2H, 0); err != nil { + return nil, err + } + if enum, ok := map[string]int{ + "America/New_York": 0, + "America/Sao_Paulo": 1, + "America/Chicago": 2, + "America/Los_Angeles": 3, + "America/Mexico_City": 4, + "Asia/Shanghai": 5, + }[timezone]; ok { + if err := addInt(1, castleValueB2H, enum); err != nil { + return nil, err + } + } else if err := addBytes(1, castleValueSerializedByteArray, []byte(timezone)); err != nil { + return nil, err + } + if err := addBytes(2, castleValueSerializedByteArray, []byte("en-US,en")); err != nil { + return nil, err + } + if err := addInt(6, castleValueB2HWithChecks, 0); err != nil { + return nil, err + } + if err := addBytes(10, castleValueJustAppend, append([]byte{4}, castleEncodeBits([]int{1, 2, 3}, 8)...)); err != nil { + return nil, err + } + if err := addInt(12, castleValueB2HWithChecks, 80); err != nil { + return nil, err + } + if err := addBytes(13, castleValueJustAppend, []byte{9, 0, 0}); err != nil { + return nil, err + } + if err := addBytes(17, castleValueJustAppend, append([]byte{0x0d}, castleEncodeBits([]int{1, 5, 8, 9, 10}, 16)...)); err != nil { + return nil, err + } + if err := addInt(18, 1, 0); err != nil { + return nil, err + } + if err := addBytes(21, castleValueJustAppend, []byte{0, 0, 0, 0}); err != nil { + return nil, err + } + if err := addBytes(22, castleValueSerializedByteArray, []byte("en-US")); err != nil { + return nil, err + } + if err := addBytes(23, castleValueJustAppend, append([]byte{2}, castleEncodeBits([]int{0}, 8)...)); err != nil { + return nil, err + } + heightDiff, err := cryptoRandInt(10, 31) + if err != nil { + return nil, err + } + if err := addBytes(24, castleValueJustAppend, []byte{0, 0, 0, byte(heightDiff)}); err != nil { + return nil, err + } + out := []byte{byte((7&4)<<5 | (31 & len(values)))} + for _, value := range values { + out = append(out, value...) + } + return out, nil +} + +func castleFPThree(initTime int64) ([]byte, error) { + minute := time.UnixMilli(initTime).UTC().Minute() + first, err := processCastleFPValue(3, castleValueB2HWithChecks, nil, 1, initTime) + if err != nil { + return nil, err + } + second, err := processCastleFPValue(4, castleValueB2HWithChecks, nil, minute, initTime) + if err != nil { + return nil, err + } + return append([]byte{byte((7 << 5) | 2)}, append(first, second...)...), nil +} + +func castleEventLog() ([]byte, error) { + simpleEvents := []int{21, 18, 25, 26, 27} + targetEvents := []int{0, 6, 5} + allEvents := append(simpleEvents, targetEvents...) + count, err := cryptoRandInt(30, 71) + if err != nil { + return nil, err + } + events := []byte{} + for range count { + idx, err := cryptoRandInt(0, len(allEvents)) + if err != nil { + return nil, err + } + eventID := allEvents[idx] + if eventID == 0 || eventID == 6 || eventID == 5 { + events = append(events, byte(eventID|128), 63) + } else { + events = append(events, byte(eventID)) + } + } + payload := append([]byte{0}, byte(count>>8), byte(count), 0) + payload = payload[:3] + payload = append(payload, events...) + return append([]byte{byte(len(payload) >> 8), byte(len(payload))}, payload...), nil +} + +func castleFPEventValues() ([]byte, error) { + bits := make([]bool, 15) + for _, bit := range []int{2, 3, 5, 6, 9, 11, 12} { + bits[bit] = true + } + binaryNum := castleBoolArrayToBinary(bits, 16) + encodedNum := (6 << 20) | (2 << 16) | (65535 & binaryNum) + out := []byte{byte(encodedNum >> 16), byte(encodedNum >> 8), byte(encodedNum)} + floatValues := []float64{ + randFloat(40, 50), -1, randFloat(70, 80), -1, randFloat(60, 70), -1, + 0, 0, randFloat(60, 80), randFloat(5, 10), randFloat(30, 40), randFloat(2, 5), + -1, -1, -1, -1, -1, -1, -1, -1, + randFloat(150, 180), randFloat(3, 6), randFloat(150, 180), randFloat(3, 6), + randFloat(0, 2), randFloat(0, 2), 0, 0, -1, -1, -1, -1, + 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, + randFloat(0, 4), randFloat(0, 3), randFloat(25, 50), randFloat(25, 50), + randFloat(25, 50), randFloat(25, 30), randFloat(0, 2), randFloat(0, 1), + randFloat(0, 1), 1, 0, + } + for _, value := range floatValues { + if value == -1 { + out = append(out, 0) + } else { + out = append(out, byte(castleEncodeEventFloat(value))) + } + } + mouseMove, err := cryptoRandInt(100, 200) + if err != nil { + return nil, err + } + keyUp, err := cryptoRandInt(1, 5) + if err != nil { + return nil, err + } + click, err := cryptoRandInt(1, 5) + if err != nil { + return nil, err + } + keyDown, err := cryptoRandInt(0, 5) + if err != nil { + return nil, err + } + wheel, err := cryptoRandInt(0, 5) + if err != nil { + return nil, err + } + unk1, err := cryptoRandInt(0, 11) + if err != nil { + return nil, err + } + out = append(out, byte(mouseMove), byte(keyUp), byte(click), 0, byte(keyDown), 0, 0, 0, byte(wheel), byte(unk1), 0, 11) + return out, nil +} + +func castleEncodeEventFloat(value float64) int { + n := math.Max(value, 0) + if n <= 15 { + return 64 | castleCustomFloatEncode(2, 4, n+1) + } + return 128 | castleCustomFloatEncode(4, 3, n-14) +} + +func castleCustomFloatEncode(expBits, manBits int, n float64) int { + if n == 0 { + return 0 + } + exponent := 0 + if n < 0 { + n = -n + } + for 2 <= n { + n /= 2 + exponent++ + } + for n < 1 { + n *= 2 + exponent-- + } + maxExponent := (1 << expBits) - 1 + if exponent > maxExponent { + exponent = maxExponent + } + fractional := n - math.Floor(n) + mantissaBits := 0 + position := 1 + for fractional > 0 && position <= manBits { + fractional *= 2 + bit := int(math.Floor(fractional)) + mantissaBits |= bit << (manBits - position) + fractional -= float64(bit) + position++ + } + return exponent<= '0' && ch <= '9': + return ch - '0' + case ch >= 'a' && ch <= 'f': + return ch - 'a' + 10 + case ch >= 'A' && ch <= 'F': + return ch - 'A' + 10 + default: + return 0 + } +} + +func xorBytes(data, key []byte) []byte { + out := make([]byte, len(data)) + for i, b := range data { + out[i] = b ^ key[i%len(key)] + } + return out +} + +func xxteaEncrypt(data []byte, key []uint32) []byte { + blocks := bytesToXXTEABlocks(data) + u := len(blocks) - 1 + if u < 1 { + return data + } + sum := uint32(0) + o := blocks[u] + rounds := 6 + 52/(u+1) + for rounds > 0 { + rounds-- + sum += 0x9e3779b9 + e := (sum >> 2) & 3 + for c := 0; c < u; c++ { + a := blocks[c+1] + blocks[c] += (((o >> 5) ^ (a << 2)) + ((a >> 3) ^ (o << 4))) ^ ((sum ^ a) + (key[(c&3)^int(e)] ^ o)) + o = blocks[c] + } + a := blocks[0] + blocks[u] += (((o >> 5) ^ (a << 2)) + ((a >> 3) ^ (o << 4))) ^ ((sum ^ a) + (key[(u&3)^int(e)] ^ o)) + o = blocks[u] + } + out := make([]byte, len(blocks)*4) + for i, block := range blocks { + binary.LittleEndian.PutUint32(out[i*4:], block) + } + return out +} + +func bytesToXXTEABlocks(data []byte) []uint32 { + count := (len(data) + 3) / 4 + padded := make([]byte, count*4) + copy(padded, data) + blocks := make([]uint32, count) + for i := range blocks { + blocks[i] = binary.LittleEndian.Uint32(padded[i*4:]) + } + return blocks +} + +func randFloat(min, max float64) float64 { + n, err := randomUint64() + if err != nil { + return min + } + return min + (float64(n>>11) / (1 << 53) * (max - min)) +} + +func cryptoRandInt(minInclusive, maxExclusive int) (int, error) { + if maxExclusive <= minInclusive { + return minInclusive, nil + } + n, err := randomUint64() + if err != nil { + return 0, err + } + return minInclusive + int(n%uint64(maxExclusive-minInclusive)), nil +} + +func randomUint64() (uint64, error) { + var buf [8]byte + if _, err := rand.Read(buf[:]); err != nil { + return 0, err + } + return binary.BigEndian.Uint64(buf[:]), nil +} + +func randomBytes(length int) ([]byte, error) { + out := make([]byte, length) + _, err := rand.Read(out) + return out, err +} diff --git a/pkg/twittermeow/castle_token_test.go b/pkg/twittermeow/castle_token_test.go new file mode 100644 index 00000000..dc6488a9 --- /dev/null +++ b/pkg/twittermeow/castle_token_test.go @@ -0,0 +1,21 @@ +package twittermeow + +import "testing" + +func TestCastleRequestTokenShape(t *testing.T) { + token, err := createCastleRequestToken() + if err != nil { + t.Fatalf("createCastleRequestToken() error = %v", err) + } + if !looksLikeCastleToken(token) { + t.Fatalf("createCastleRequestToken() returned suspicious token length=%d", len(token)) + } + t.Logf("Castle request token length=%d", len(token)) + second, err := createCastleRequestToken() + if err != nil { + t.Fatalf("second createCastleRequestToken() error = %v", err) + } + if token == second { + t.Fatal("createCastleRequestToken() returned identical tokens") + } +} diff --git a/pkg/twittermeow/castle_token_test_helpers_test.go b/pkg/twittermeow/castle_token_test_helpers_test.go new file mode 100644 index 00000000..def25cff --- /dev/null +++ b/pkg/twittermeow/castle_token_test_helpers_test.go @@ -0,0 +1,7 @@ +package twittermeow + +import "strings" + +func looksLikeCastleToken(token string) bool { + return len(token) > 500 && !strings.Contains(token, "=") +} diff --git a/pkg/twittermeow/castle_web_token.go b/pkg/twittermeow/castle_web_token.go new file mode 100644 index 00000000..51583c81 --- /dev/null +++ b/pkg/twittermeow/castle_web_token.go @@ -0,0 +1,5329 @@ +package twittermeow + +import ( + "bytes" + "compress/flate" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "math" + "math/bits" + "strconv" + "strings" + "time" + "unicode/utf16" +) + +const ( + currentCastleTokenPrefix = "IBYIll|" + currentCastleChecksumLabel = "Nqkju" + currentCastlePayloadSlots = 494 + + currentCastleSlot6ComponentCount = 47 + currentCastleSlot6LengthMask = 0x7fff + currentCastleSlot6LengthContinue = 0x8000 + currentCastleSlot6LengthShiftBits = 15 + + currentCastleAcceptedAutomationBitfield = 0x1353003b + currentCastleDefaultLanguage = "en-US" + currentCastleDefaultLanguages = "en-US,en" + currentCastleDefaultOrientation = "landscape-primary" + currentCastleDefaultPlatform = "Win32" + currentCastleDefaultHost = "x.com" + currentCastleDefaultWebGLVendor = "Google Inc." + currentCastleDefaultWebGLRenderer = "ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Ti (0x00001B06) Direct3D11 vs_5_0 ps_5_0, D3D11)" + currentCastleDefaultFeatureBits = "1111111111111111111111111111111111111000000000" + currentCastleDefaultAudioProbe = "20030107" + currentCastleDefaultPrecisionProbe = "1000" + currentCastleDefaultNumericProbe = "43530826428" + currentCastleDefaultWASMProbeHex = "000002c3c600000789c60007" + currentCastleSlot6TimestampSkewMillis = 16 + currentCastleDefaultHash5 = "39a01d2b" + currentCastleDefaultHash7 = "bd4be70d" + currentCastleDefaultHash10 = "89dd3552" + currentCastleDefaultHash11 = "8fc90f7d" + currentCastleDefaultHash16 = "5d55117f" + currentCastleDefaultHash18 = "42d68d60" + currentCastleDefaultHash23 = "34b454a7" + currentCastleDefaultHash39 = "db77b610" +) + +var currentCastleAcceptedNumericSlots = []int{ + 2, 20, 26, 27, 28, 31, 42, 44, 47, 49, 53, 56, 64, 67, 68, 71, 72, 73, + 87, 90, 98, 102, 103, 104, 106, 108, 109, 115, 116, 121, 123, 130, 133, + 147, 149, 150, 152, 154, 156, 159, 168, 174, 176, 177, 180, 182, 183, 184, + 188, 194, 196, 198, 203, 205, 206, 207, 209, 212, 220, 221, 223, 224, 228, + 235, 237, 240, 245, 246, 247, 256, 257, 258, 260, 261, 266, 267, 274, 277, + 279, 281, 288, 289, 292, 293, 297, 298, 304, 308, 311, 312, 316, 318, 321, + 326, 329, 330, 334, 339, 341, 344, 348, 349, 354, 356, 359, 366, 369, 374, + 375, 376, 378, 383, 386, 388, 390, 393, 395, 399, 406, 421, 422, 425, 428, + 429, 430, +} + +var currentCastleAcceptedObjectSlots = []int{ + 41, 46, 77, 99, 100, 128, 173, 214, 290, 342, 352, 416, 424, +} + +const currentCastleAcceptedBooleanSlot = 273 + +func createCurrentCastleWrappedToken(payloadJSON string, timestampMillis int64) (string, error) { + if payloadJSON == "" { + return "", fmt.Errorf("current Castle payload is empty") + } + timestamp := strconv.FormatInt(timestampMillis, 10) + payloadWithChecksum := insertCurrentCastleChecksum(payloadJSON) + encodedTimestamp := encodeCurrentCastleTimestamp(timestamp) + xoredPayload := xorCurrentCastleString(payloadWithChecksum, timestamp) + wire := string([]byte{byte(len(encodedTimestamp))}) + encodedTimestamp + xoredPayload + compressed, err := deflateCurrentCastleWireString(wire) + if err != nil { + return "", err + } + return currentCastleTokenPrefix + base64.StdEncoding.EncodeToString(compressed), nil +} + +func createCurrentCastleWrappedPayloadToken(payload []any, timestampMillis int64) (string, error) { + payloadJSON, err := marshalCurrentCastlePayload(payload) + if err != nil { + return "", err + } + return createCurrentCastleWrappedToken(payloadJSON, timestampMillis) +} + +// newCurrentCastlePayloadScaffold returns a null-free payload skeleton. Unknown +// slots remain empty until the corresponding active X Castle slot is ported. +func newCurrentCastlePayloadScaffold() []any { + payload := make([]any, currentCastlePayloadSlots) + for i := range payload { + payload[i] = "" + } + for _, slot := range currentCastleAcceptedNumericSlots { + payload[slot] = 0 + } + for _, slot := range currentCastleAcceptedObjectSlots { + payload[slot] = map[string]any{} + } + payload[currentCastleAcceptedBooleanSlot] = false + return payload +} + +type currentCastlePayloadInput struct { + AutomationBitfield uint32 + Slot6Components []string + NumericValues map[int]uint32 + LowerFloatValues map[int]float64 + ArrayValues map[int][]float64 + ObjectValues map[int]map[string]any + PackedStringValues map[int][]string + UnitPackedStrings map[int][]string + StringValues map[int]string + HighTimingValues map[int]float64 +} + +type currentCastleSlot6Fingerprint struct { + TimestampMillis int64 + Timezone string + WebGLRenderer string + WebGLVendor string + Orientation string + UserAgent string + Language string + Languages string + Platform string + Host string + PublicKey string + ClientUUID string + FeatureBits string + PrecisionProbe string + AudioProbe string + ProbeDateString string + NumericProbe string + WASMProbeHex string + + Hash5 string + Hash7 string + Hash10 string + Hash11 string + Hash16 string + Hash18 string + Hash23 string + Hash39 string +} + +func createCurrentCastleRequestToken(clientUUID string) (string, error) { + timestampMillis := time.Now().UnixMilli() + components, err := buildCurrentCastleSlot6Components(defaultCurrentCastleSlot6Fingerprint(timestampMillis, clientUUID)) + if err != nil { + return "", err + } + payload, err := buildCurrentCastlePayload(currentCastlePayloadInput{ + AutomationBitfield: currentCastleAcceptedAutomationBitfield, + Slot6Components: components, + }) + if err != nil { + return "", err + } + return createCurrentCastleWrappedPayloadToken(payload, timestampMillis) +} + +func defaultCurrentCastleSlot6Fingerprint(timestampMillis int64, clientUUID string) currentCastleSlot6Fingerprint { + if timestampMillis <= 0 { + timestampMillis = time.Now().UnixMilli() + } + timezone := jetfuelTimezone() + renderer := currentCastleDefaultWebGLRenderer + return currentCastleSlot6Fingerprint{ + TimestampMillis: timestampMillis, + Timezone: timezone, + WebGLRenderer: renderer, + WebGLVendor: currentCastleDefaultWebGLVendor, + Orientation: currentCastleDefaultOrientation, + UserAgent: UserAgent, + Language: currentCastleDefaultLanguage, + Languages: currentCastleDefaultLanguages, + Platform: currentCastleDefaultPlatform, + Host: currentCastleDefaultHost, + PublicKey: castlePublicKey, + ClientUUID: clientUUID, + FeatureBits: currentCastleDefaultFeatureBits, + PrecisionProbe: currentCastleDefaultPrecisionProbe, + AudioProbe: currentCastleDefaultAudioProbe, + ProbeDateString: currentCastleSlot6ProbeDateString(timezone), + NumericProbe: currentCastleDefaultNumericProbe, + WASMProbeHex: currentCastleDefaultWASMProbeHex, + Hash5: currentCastleDefaultHash5, + Hash7: currentCastleDefaultHash7, + Hash10: currentCastleDefaultHash10, + Hash11: currentCastleDefaultHash11, + Hash16: currentCastleDefaultHash16, + Hash18: currentCastleDefaultHash18, + Hash23: currentCastleDefaultHash23, + Hash39: currentCastleDefaultHash39, + } +} + +func buildCurrentCastleSlot6Components(fp currentCastleSlot6Fingerprint) ([]string, error) { + return encodeCurrentCastleSlot6ComponentValues(buildCurrentCastleSlot6RawValues(fp)) +} + +func buildCurrentCastleSlot6RawValues(fp currentCastleSlot6Fingerprint) []string { + fp = normalizeCurrentCastleSlot6Fingerprint(fp) + values := make([]string, currentCastleSlot6ComponentCount) + values[0] = "toString" + values[1] = "TypeError: Cyclic __proto__ value" + values[2] = fp.Timezone + values[3] = fp.WebGLRenderer + values[5] = fp.Hash5 + values[6] = fp.Orientation + values[7] = fp.Hash7 + values[8] = "RangeError" + values[9] = "[]" + values[10] = fp.Hash10 + values[11] = fp.Hash11 + values[12] = "probably" + values[13] = "maybe" + values[14] = fp.FeatureBits + values[15] = "Cannot read properties of undefined (reading 'b')" + values[16] = fp.Hash16 + values[18] = fp.Hash18 + values[19] = fp.ClientUUID + values[20] = fp.PrecisionProbe + values[21] = fp.WebGLVendor + values[22] = fp.WebGLRenderer + values[23] = fp.Hash23 + values[24] = fp.AudioProbe + values[25] = "{}" + values[27] = fp.PublicKey + values[28] = "Illegal invocation" + values[29] = fp.UserAgent + values[30] = "r:1" + values[31] = fp.Language + values[33] = strconv.FormatInt(currentCastleSlot6ComponentTimestampMillis(fp.TimestampMillis), 10) + values[34] = fp.Language + values[35] = "Maximum call stack size exceeded" + values[36] = fp.Platform + values[37] = fp.ProbeDateString + values[38] = "r:1" + values[39] = fp.Hash39 + values[41] = fp.Host + values[43] = fp.WASMProbeHex + values[44] = fp.Languages + values[45] = "probably" + values[46] = fp.NumericProbe + return values +} + +func normalizeCurrentCastleSlot6Fingerprint(fp currentCastleSlot6Fingerprint) currentCastleSlot6Fingerprint { + if fp.TimestampMillis <= 0 { + fp.TimestampMillis = time.Now().UnixMilli() + } + if fp.Timezone == "" { + fp.Timezone = jetfuelTimezone() + } + if fp.WebGLRenderer == "" { + fp.WebGLRenderer = currentCastleDefaultWebGLRenderer + } + if fp.WebGLVendor == "" { + fp.WebGLVendor = currentCastleDefaultWebGLVendor + } + if fp.Orientation == "" { + fp.Orientation = currentCastleDefaultOrientation + } + if fp.UserAgent == "" { + fp.UserAgent = UserAgent + } + if fp.Language == "" { + fp.Language = currentCastleDefaultLanguage + } + if fp.Languages == "" { + fp.Languages = currentCastleDefaultLanguages + } + if fp.Platform == "" { + fp.Platform = currentCastleDefaultPlatform + } + if fp.Host == "" { + fp.Host = currentCastleDefaultHost + } + if fp.PublicKey == "" { + fp.PublicKey = castlePublicKey + } + if fp.FeatureBits == "" { + fp.FeatureBits = currentCastleDefaultFeatureBits + } + if fp.PrecisionProbe == "" { + fp.PrecisionProbe = currentCastleDefaultPrecisionProbe + } + if fp.AudioProbe == "" { + fp.AudioProbe = currentCastleDefaultAudioProbe + } + if fp.ProbeDateString == "" { + fp.ProbeDateString = currentCastleSlot6ProbeDateString(fp.Timezone) + } + if fp.NumericProbe == "" { + fp.NumericProbe = currentCastleDefaultNumericProbe + } + if fp.WASMProbeHex == "" { + fp.WASMProbeHex = currentCastleDefaultWASMProbeHex + } + if fp.Hash5 == "" { + fp.Hash5 = currentCastleDefaultHash5 + } + if fp.Hash7 == "" { + fp.Hash7 = currentCastleDefaultHash7 + } + if fp.Hash10 == "" { + fp.Hash10 = currentCastleDefaultHash10 + } + if fp.Hash11 == "" { + fp.Hash11 = currentCastleDefaultHash11 + } + if fp.Hash16 == "" { + fp.Hash16 = currentCastleDefaultHash16 + } + if fp.Hash18 == "" { + fp.Hash18 = currentCastleDefaultHash18 + } + if fp.Hash23 == "" { + fp.Hash23 = currentCastleDefaultHash23 + } + if fp.Hash39 == "" { + fp.Hash39 = currentCastleDefaultHash39 + } + return fp +} + +func currentCastleSlot6ComponentTimestampMillis(timestampMillis int64) int64 { + if timestampMillis <= currentCastleSlot6TimestampSkewMillis { + return timestampMillis + } + return timestampMillis - currentCastleSlot6TimestampSkewMillis +} + +func currentCastleSlot6ProbeDateString(timezone string) string { + switch timezone { + case "America/Chicago": + return "3/3/1970, 6:00:00 PM" + case "America/Los_Angeles": + return "3/3/1970, 4:00:00 PM" + case "America/New_York": + return "3/3/1970, 7:00:00 PM" + case "America/Denver": + return "3/3/1970, 5:00:00 PM" + case "America/Anchorage": + return "3/3/1970, 3:00:00 PM" + case "Pacific/Honolulu": + return "3/3/1970, 2:00:00 PM" + default: + return "3/4/1970, 12:00:00 AM" + } +} + +func buildCurrentCastlePayload(input currentCastlePayloadInput) ([]any, error) { + payload := newCurrentCastlePayloadScaffold() + if err := populateCurrentCastleAutomationSlot(payload, input.AutomationBitfield); err != nil { + return nil, err + } + if err := populateCurrentCastleNumericSlots(payload, input.NumericValues); err != nil { + return nil, err + } + if err := populateCurrentCastleLowerFloatSlots(payload, input.LowerFloatValues); err != nil { + return nil, err + } + if err := populateCurrentCastleArraySlots(payload, input.ArrayValues); err != nil { + return nil, err + } + if err := populateCurrentCastlePackedStringSlots(payload, input.PackedStringValues); err != nil { + return nil, err + } + if err := populateCurrentCastleUnitPackedStringSlots(payload, input.UnitPackedStrings); err != nil { + return nil, err + } + if err := populateCurrentCastleStringSlots(payload, input.StringValues); err != nil { + return nil, err + } + if err := populateCurrentCastleObjectSlots(payload, input.ObjectValues); err != nil { + return nil, err + } + slot6, err := encodeCurrentCastleSlot6(input.Slot6Components) + if err != nil { + return nil, err + } + payload[6] = slot6 + if err = populateCurrentCastleHighTimingSlots(payload, input.HighTimingValues); err != nil { + return nil, err + } + populateCurrentCastleObservedStringSlots(payload, input) + if _, err = marshalCurrentCastlePayload(payload); err != nil { + return nil, err + } + return payload, nil +} + +func populateCurrentCastleAutomationSlot(payload []any, rawBitfield uint32) error { + if len(payload) <= 2 { + return fmt.Errorf("current Castle payload has %d slots, want at least 3", len(payload)) + } + payload[2] = currentCastleTB(rawBitfield) + return nil +} + +var currentCastleNumericSlotEncoders = map[int]func(uint32) uint32{ + 20: encodeCurrentCastleNumericSlot20, + 26: currentCastleNumericAffineEncoder(currentCastleIndexR7(0x8dadb484), currentCastleIndexR7(0xa02eff37), 0x6ba90087), + 27: currentCastleNumericAffineEncoder(0xf4730f04, 0x5aa87861, 0xe1ed70a8), + 28: currentCastleNumericAffineEncoder(0x0d877048, 0x49b0e02d, currentCastleIndexR7(0x058e30f8)), + 31: currentCastleNumericAffineEncoder(0xc3b6e1f8, 0xf35393df, currentCastleIndexTG(0x0fb221a7)), + 42: currentCastleNumericAffineEncoder(0xeaf85686, 0xf6253667, currentCastleIndexR7(0x2a606d93)), + 44: encodeCurrentCastleNumericIdentity, + 47: encodeCurrentCastleNumericIdentity, + 49: currentCastleNumericAffineEncoder(0x14ba9b74, 0x6381aa33, 0xc8fde421), + 53: currentCastleNumericAffineEncoder(0xf967d101, 0x6627399f, 0x4badc824), + 56: currentCastleNumericAffineEncoder(0x0fdaf2c8, 0xd32ce769, 0x35531149), + 64: currentCastleNumericAffineEncoder(0x40a5541c, currentCastleIndexR7(0x187ef987), currentCastleIndexTG(0xca19a6f1)), + 67: currentCastleNumericAffineEncoder(0x5e57a336, 0x4cc0a093, 0x90ebbe3b), + 68: currentCastleNumericAffineEncoder(0x93d2c136, currentCastleIndexTG(0xfeae7a21), currentCastleIndexR7(0xcc851a80)), + 71: currentCastleNumericAffineEncoder(currentCastleIndexTG(0xe5c05ac6), currentCastleIndexTG(0xd9dea0bd), 0x2ba0704d), + 72: encodeCurrentCastleNumericIdentity, + 73: currentCastleNumericAffineEncoder(0xcac4cfca, 0x19c3286b, 0x1540a806), + 87: currentCastleNumericAffineEncoder(0xb438dadb, 0xbd6a0d4f, currentCastleIndexTG(0x4e27a0eb)), + 90: encodeCurrentCastleNumericIdentity, + 98: currentCastleNumericAffineEncoder(0x5f9d021c, currentCastleIndexR7(0x2171a35d), currentCastleIndexR7(0x8016951b)), + 102: currentCastleNumericAffineEncoder(0xe4cc9400, currentCastleIndexR7(0x1d8d20c9), 0xfbf84330), + 103: encodeCurrentCastleNumericIdentity, + 104: encodeCurrentCastleNumericIdentity, + 106: currentCastleNumericAffineEncoder(currentCastleIndexR7(0x95b45d4c), 0xfcc3ad03, 0x67847013), + 108: encodeCurrentCastleNumericIdentity, + 109: encodeCurrentCastleNumericIdentity, + 115: currentCastleNumericAffineEncoder(0xe33fe88d, 0x5aca0647, 0x58d3320e), + 116: currentCastleNumericAffineEncoder(0xd726d1f7, currentCastleIndexR7(0x2416a9d1), currentCastleIndexTG(0x2fca6a65)), + 121: currentCastleNumericAffineEncoder(0x78e89baf, currentCastleIndexR7(0x6f76f97e), 0x1639fdd5), + 123: currentCastleNumericAffineEncoder(0x0472b08c, currentCastleIndexR7(0x64137da7), currentCastleIndexR7(0x5cf504e4)), + 130: encodeCurrentCastleNumericIdentity, + 133: currentCastleNumericAffineEncoder(0x4aa2215c, 0x0f3c0647, 0x9cb82d4f), + 147: currentCastleNumericAffineEncoder(0x92f56e95, 0x5feb3bd7, 0x0308f549), + 149: currentCastleNumericAffineEncoder(0x2a8c6716, 0xb94477db, 0x496e2ec4), + 150: currentCastleNumericAffineEncoder(0x7e49a49f, 0xc276ed9b, 0x1ad0a021), + 152: currentCastleNumericAffineEncoder(0xc60062fb, 0xf6dd85bd, 0xf1d97dda), + 154: encodeCurrentCastleNumericSlot154, + 156: encodeCurrentCastleNumericIdentity, + 159: encodeCurrentCastleNumericIdentity, + 168: currentCastleNumericAffineEncoder(0xc99ace12, currentCastleIndexR7(0x30387440), currentCastleIndexR7(0x8e8ca6a7)), + 174: encodeCurrentCastleNumericIdentity, + 176: encodeCurrentCastleNumericIdentity, + 177: encodeCurrentCastleNumericIdentity, + 180: encodeCurrentCastleNumericIdentity, + 182: currentCastleTB, + 183: encodeCurrentCastleNumericIdentity, + 184: encodeCurrentCastleNumericIdentity, + 188: encodeCurrentCastleNumericIdentity, + 194: currentCastleNumericAffineEncoder(0xa3e41d03, 0xd468aef7, 0x4fc32d5a), + 196: currentCastleNumericAffineEncoder(0x338ca7e4, 0x004c6c1b, 0xcf397719), + 198: currentCastleNumericAffineEncoder(currentCastleIndexTG(0x3a6b8b4a), currentCastleIndexR7(0x15997926), 0x109ad9c4), + 203: currentCastleNumericAffineEncoder(currentCastleIndexTG(0xbeff5831), currentCastleIndexR7(0x1bb9f08f), currentCastleIndexR7(0xf15b90bc)), + 205: currentCastleNumericAffineEncoder(0xafbd8da7, 0xdf5818a9, 0x532b20e8), + 206: currentCastleNumericAffineEncoder(0xe27d001f, 0xc7a3e3bf, 0x094f1534), + 207: currentCastleNumericAffineEncoder(currentCastleIndexTG(0x553a4119), currentCastleIndexR7(0xcf68a2a6), 0x7342df93), + 209: currentCastleNumericAffineEncoder(0xc970ba2f, currentCastleIndexR7(0x95562fdc), 0x396c0bfc), + 212: currentCastleNumericAffineEncoder(0xa84e4ee0, currentCastleIndexTG(0x8e122ec3), 0x19f6e853), + 220: encodeCurrentCastleNumericIdentity, + 221: currentCastleNumericAffineEncoder(0xaf9fb005, currentCastleIndexR7(0xd6326bbb), 0x0cd83feb), + 223: currentCastleNumericAffineEncoder(0x17dbb40b, currentCastleIndexTG(0xb13dfc57), currentCastleIndexR7(0x8ae57b7f)), + 224: currentCastleNumericAffineEncoder(currentCastleIndexTG(0xe9ad1a81), 0x4c0f0ccd, 0x7651692c), + 228: currentCastleNumericAffineEncoder(0x081ff5ca, currentCastleIndexR7(0x0b50b5e1), 0xddfdc282), + 235: encodeCurrentCastleNumericIdentity, + 237: encodeCurrentCastleNumericIdentity, + 240: currentCastleNumericAffineEncoder(0x15608129, currentCastleIndexR7(0x7f143c5a), 0x31aaf010), + 245: encodeCurrentCastleNumericIdentity, + 246: currentCastleNumericAffineEncoder(0x2da919dc, 0x24b26a51, currentCastleIndexTG(0x8db5b1b5)), + 247: currentCastleNumericAffineEncoder(0xf1398f6f, 0x031a5829, 0xa39c022e), + 256: encodeCurrentCastleNumericSlot256, + 257: currentCastleNumericAffineEncoder(0x63d3c42d, currentCastleIndexTG(0xa499760d), 0xb61b007f), + 258: currentCastleNumericAffineEncoder(currentCastleIndexR7(0xddb05bd9), currentCastleIndexTG(0xda4f6e1b), currentCastleIndexR7(0x44f87d7f)), + 260: currentCastleNumericAffineEncoder(0x5c2d1334, 0x82f4555d, 0xfc40a036), + 261: encodeCurrentCastleNumericIdentity, + 266: currentCastleNumericAffineEncoder(currentCastleIndexR7(0x6c2338c7), currentCastleIndexTG(0x5e089af3), 0x47add5ce), + 267: currentCastleNumericAffineEncoder(currentCastleIndexTG(0x82a9dbff), 0x7e034531, 0xc32531d9), + 274: encodeCurrentCastleNumericIdentity, + 277: currentCastleNumericAffineEncoder(0x87fda9a5, currentCastleIndexTG(0x061ad74d), 0x97429312), + 279: encodeCurrentCastleNumericIdentity, + 281: encodeCurrentCastleNumericIdentity, + 289: currentCastleTB, + 288: currentCastleNumericAffineEncoder(0x7539f159, 0xdd1ef321, currentCastleIndexR7(0x0de88265)), + 292: encodeCurrentCastleNumericIdentity, + 293: currentCastleNumericAffineEncoder(0xb2bb9131, 0x3ddd3435, 0x808c247f), + 297: currentCastleNumericAffineEncoder(0x6c4cf908, 0xabee7b25, 0x00f0dead), + 298: encodeCurrentCastleNumericIdentity, + 304: encodeCurrentCastleNumericIdentity, + 308: encodeCurrentCastleNumericIdentity, + 311: currentCastleNumericAffineEncoder(0xab4e6a90, 0xb2896847, 0xbddd98cc), + 312: currentCastleNumericAffineEncoder(currentCastleIndexTG(0x7d8c8409), 0xb2c0779f, 0xdf29574a), + 316: currentCastleIndexEUnderscore, + 318: currentCastleNumericAffineEncoder(0xb70bef86, 0xa653b485, currentCastleIndexTG(0x1921c4cc)), + 321: currentCastleNumericAffineEncoder(0x022eeaef, 0x42e2bfb5, 0x25d5ac75), + 326: encodeCurrentCastleNumericIdentity, + 329: currentCastleNumericAffineEncoder(0xf7a020e8, 0xfb500465, 0x8f5dd1f8), + 330: encodeCurrentCastleNumericIdentity, + 334: currentCastleNumericAffineEncoder(0x073bb038, currentCastleIndexR7(0x5e87765b), currentCastleIndexTG(0xc7d82a02)), + 339: currentCastleNumericAffineEncoder(0x31a8b928, 0x74c029f1, 0x39a0b04b), + 341: encodeCurrentCastleNumericSlot341, + 344: encodeCurrentCastleNumericIdentity, + 348: encodeCurrentCastleNumericSlot348, + 349: currentCastleNumericAffineEncoder(0x29789f9c, 0x81d1cfb9, 0x5fafa5dd), + 354: currentCastleNumericAffineEncoder(0xa5d12073, 0x5a141b6f, 0x5c04da70), + 356: currentCastleNumericAffineEncoder(currentCastleIndexTG(0xefdfa846), 0x7479d8cd, 0xb5e1823b), + 359: currentCastleNumericAffineEncoder(0x5bca9e62, 0x7115f42f, currentCastleIndexTG(0xd01dd90c)), + 366: currentCastleNumericAffineEncoder(currentCastleIndexTG(0xd819e362), 0x1865a9bb, 0x4a8e00a2), + 369: currentCastleNumericAffineEncoder(currentCastleIndexTG(0x6252cdfb), 0x77cf48f5, 0x16577410), + 374: currentCastleNumericAffineEncoder(0x5ad18bb4, currentCastleIndexR7(0x35f9af04), 0xa86efa9e), + 375: currentCastleNumericAffineEncoder(0x52aa8eda, currentCastleIndexTG(0x79d65887), 0xf14f9c27), + 376: currentCastleNumericAffineEncoder(0x9118a572, 0xc1804b39, currentCastleIndexR7(0xd0d41fd7)), + 378: encodeCurrentCastleNumericIdentity, + 383: encodeCurrentCastleNumericIdentity, + 386: currentCastleNumericAffineEncoder(currentCastleIndexTG(0x8994cbfe), currentCastleIndexR7(0xa78374d8), 0xaef88e50), + 388: currentCastleNumericAffineEncoder(0x5f1db9d0, 0x48572a5d, 0x58b3854a), + 390: currentCastleNumericAffineEncoder(0x3d4f1683, 0x97d2840b, 0x754b73a7), + 393: currentCastleNumericAffineEncoder(0x569dbc0a, 0xa03ef08d, 0x9efe8725), + 395: currentCastleNumericAffineEncoder(0xd1f0afd2, 0x456a2621, 0x8313e4bc), + 399: currentCastleNumericAffineEncoder(0x2d54860a, 0x68fac53f, currentCastleIndexR7(0x060755c2)), + 406: currentCastleNumericAffineEncoder(0x7837ed09, 0xdc5b6193, 0xf2dbed27), + 421: encodeCurrentCastleNumericIdentity, + 422: currentCastleNumericAffineEncoder(currentCastleIndexTG(0x7f83d97a), 0xa935b369, currentCastleIndexR7(0x45368801)), + 425: currentCastleNumericAffineEncoder(0x74c852e2, 0x23ff47e7, 0x29394da4), + 428: encodeCurrentCastleNumericSlot428, + 429: encodeCurrentCastleNumericSlot429, + 430: currentCastleNumericAffineEncoder(0x47d64b14, currentCastleIndexTG(0x98f93ed5), 0x464a9053), +} + +func defaultCurrentCastleNumericSlotValues() map[int]uint32 { + return map[int]uint32{ + // Browser-like defaults: the media query does not match, navigator.webdriver + // is false, and the T4/T6 counter probes fall back to zero on errors. + 20: 0, + 26: 0, + 27: 0, + 28: 0, + // navigator.permissions exists in normal Chrome and is wrapped through tB. + 31: currentCastleTB(1), + // The input-type support map is initialized false and only read by the + // active bundle. tel/text/password/email all wrap false through e_. + 42: currentCastleIndexEUnderscore(0), + // Pending formula ports below use final numeric values observed from + // the current visible Chrome begin_login token on this Windows machine. + 44: 0x0637b4dd, + 47: 0x5b4fa7aa, + // EO.length from the current Chrome begin_login token on this machine. + 49: 253, + 53: 0, + 56: 0, + 64: currentCastleIndexEUnderscore(0), + // The active Gi-gated slot-67 branch falls back to the e_ wrapper's false value. + 67: currentCastleIndexEUnderscore(0), + 68: 0, + 71: currentCastleIndexEUnderscore(0), + 72: 0xe5371923, + 73: 0, + 87: currentCastleIndexEUnderscore(0), + 90: 0x443a1261, + 98: 0, + // EX[1] from the current Chrome begin_login token on this machine. + 102: 64, + 103: 0x975b0b86, + 104: 0xddc55158, + // navigator.credentials exists in normal Chrome and is wrapped through tB. + 106: currentCastleTB(1), + 108: 0x810588a6, + 109: 0x4901e987, + 115: 0, + // pX() returns window.outerWidth - window.innerWidth. Keep the default + // neutral and allow live Chrome-captured deltas to override it. + 116: 0, + // Ho(r) returns 0 in the no-event/default path. + 121: 0, + // Hv() is false unless the window/visualViewport dimensions match the + // full screen, then wraps through the e_ transform. + 123: currentCastleIndexEUnderscore(0), + 130: 0x605ff24d, + // navigator.javaEnabled() is present but false in normal Chrome. + 133: currentCastleTB(0), + 147: 0, + 149: 0, + 150: 0, + 152: 0, + 154: 0, + 156: 0x96efd7bf, + // HM() is a window flag probe; the browser-like missing flag branch is false. + 159: 21203, + 168: 0, + 174: 0x6ae4238a, + 176: 0x4fa64045, + 177: 0x834bc08c, + 180: 0x49eaa0e7, + // Ta/QC/TH all default false, so only the inverted TH bit is set. + 182: 8, + 183: 0x8894a25b, + 184: 0x1851c3d2, + 188: 0xaad37e98, + 194: 0, + 196: 0, + // The Gi-gated screen/window branch is disabled in this Go port, so + // slot 198 uses the tB(false) wrapper default. + 198: currentCastleTB(0), + 203: 0, + 205: 0, + 206: 0, + 207: 0, + 209: 0, + // Slot 212 is window.outerHeight - window.innerHeight. + 212: 0, + 220: 0x960e16fd, + 221: 0, + 223: 0, + 224: currentCastleIndexEUnderscore(0), + // The Gi-gated storage branch is false in the current Go port, so slot + // 228 takes f0(false), which is the e_ wrapper's false value. + 228: currentCastleIndexEUnderscore(0), + 235: 0x0350a000, + 237: 0x522c3f07, + 240: 0, + 245: 0x5abc844a, + 246: 0, + 247: currentCastleIndexEUnderscore(0), + 256: 0, + // iR(false) returns tB(false) before slot 257's affine mix. + 257: currentCastleTB(0), + 258: 0, + 260: 0, + 261: 0xa9ca285e, + 266: 0, + // No-event/default callbacks for the QT/Qu event probes. + 267: 0, + 274: 0x330f0003, + 277: 0, + 279: 0xe1895c5b, + 281: 0x4877c85d, + 288: 0, + // fD(false) returns tB(false), then slot 289 runs that through tB again. + 289: currentCastleTB(0), + 292: 0x521f133b, + 293: 0, + // No-event defaults for GP/G_/G$ event-array probes. + 297: 0, + 298: 0xa4d06c96, + 304: 0x78571199, + 308: 0xe48da40e, + 311: 0, + 312: 0, + // window.external is present in Chrome but its native toString() does not + // contain the Sequentum marker; QN wraps false through tB, then slot 316 + // applies the e_ index transform inline. + 316: currentCastleTB(0), + // Qy/QU return the e_ wrapper's false value in the no-event path. + 318: currentCastleIndexEUnderscore(0), + 321: currentCastleIndexEUnderscore(0), + 326: 0x1350806a, + 329: 0, + 330: 0xdd2ae869, + // FM.hidden is false in the static input-type support map. + 334: currentCastleTB(0), + 339: 0, + // p9() is another missing window flag probe; the default branch is false. + 341: 0, + 344: 0x58c2cc46, + 348: 0, + 349: 0, + 354: 0, + 356: currentCastleTB(0), + // Normal Chrome reports navigator.webdriver as false. + 359: currentCastleTB(0), + // Hn() returns 0 unless the active aV() probe throws a named error. + 366: 0, + // navigator.pdfViewerEnabled exists and is true in normal Chrome, so the + // active predicate `pdfViewerEnabled in navigator && false === value` is false. + 369: currentCastleIndexEUnderscore(0), + 374: 0, + // QD() reads navigator.webdriver and wraps the normal false value through tB. + 375: currentCastleTB(0), + // CanvasRenderingContext2D.prototype.getImageData is native in Chrome, so + // the active anti-tamper predicate is false and wraps through e_. + 376: currentCastleIndexEUnderscore(0), + 378: 0x3979e7ca, + 383: 0x82c994ec, + 386: 0, + 388: 0, + 390: 0, + 393: 0, + 395: 0, + // Qf()/io() current Chrome default from the visible begin_login path. + 399: 2770, + 406: 0, + 421: 0x749316dd, + // screen.availHeight from the current Chrome profile on this machine. + 422: 1152, + 425: 0, + // The active Castle closure shadows tp with an array before the slot-428 + // assignment, so the bundle takes the typeof-not-function sentinel. + 428: 0xdae88a03, + // Hu() falls back to e_(false) when the canvas copy probe is unavailable. + 429: currentCastleIndexEUnderscore(0), + // Ha returns the tB(false) wrapper when no event/mn array is present. + 430: currentCastleTB(0), + } +} + +func populateCurrentCastleNumericSlots(payload []any, values map[int]uint32) error { + if len(payload) != currentCastlePayloadSlots { + return fmt.Errorf("current Castle payload has %d slots, want %d", len(payload), currentCastlePayloadSlots) + } + defaults := defaultCurrentCastleNumericSlotValues() + finalDefaults := defaultCurrentCastleNumericSlotFinalValues() + for slot, encoder := range currentCastleNumericSlotEncoders { + if _, hasOverride := values[slot]; !hasOverride { + if finalValue, ok := finalDefaults[slot]; ok { + payload[slot] = finalValue + continue + } + } + value, ok := defaults[slot] + if override, hasOverride := values[slot]; hasOverride { + value = override + ok = true + } + if !ok { + return fmt.Errorf("current Castle numeric slot %d has no default", slot) + } + payload[slot] = encoder(value) + } + return nil +} + +func defaultCurrentCastleNumericSlotFinalValues() map[int]uint32 { + return map[int]uint32{ + // Current Chrome begin_login numeric payload defaults observed from the + // native Windows profile. Slot 2 is populated by the automation encoder. + 20: 1607583818, + 26: 3643181490, + 27: 1696677852, + 28: 609078933, + 31: 1676013394, + 42: 3525692521, + 44: 104314077, + 47: 1560894515, + 49: 4152372946, + 53: 2655580655, + 56: 3433073095, + 64: 1726107147, + 67: 3800479293, + 68: 3971848222, + 71: 1354671007, + 72: 3845593379, + 73: 3755269648, + 87: 255423802, + 90: 1144656481, + 98: 1125315884, + 102: 3724615792, + 103: 3143232709, + 104: 3720696152, + 106: 1535610710, + 108: 2164623526, + 109: 2608958949, + 115: 4005085634, + 116: 697758972, + 121: 2010197273, + 123: 1687974328, + 130: 1616900685, + 133: 2388381063, + 147: 4144616434, + 149: 1675400182, + 150: 1577344424, + 152: 4164810793, + 154: 4047298945, + 156: 2532300735, + 159: 21203, + 168: 1297108559, + 174: 1793336202, + 176: 1336295493, + 177: 2202779788, + 180: 1240113383, + 182: 1686, + 183: 2291442267, + 184: 408011730, + 188: 2865987224, + 194: 3796548060, + 196: 2406903333, + 198: 2949066494, + 203: 2202744921, + 205: 2658569404, + 206: 261761365, + 207: 2341113330, + 209: 4275254847, + 212: 469580049, + 220: 2517505789, + 221: 3617915702, + 223: 4066925288, + 224: 1663298151, + 228: 2333063244, + 235: 55615488, + 237: 1378631431, + 240: 2095554485, + 245: 1522304074, + 246: 4049567695, + 247: 1912840949, + 256: 1319576667, + 257: 2968100594, + 258: 1031170005, + 260: 3845770389, + 261: 2848598110, + 266: 2747539663, + 267: 724571498, + 274: 856621059, + 277: 3017337424, + 279: 3783875675, + 281: 450570634, + 288: 2851106031, + 289: 119280, + 292: 1377768251, + 293: 682616773, + 297: 2166863061, + 298: 2765122710, + 304: 2018972057, + 308: 3834487822, + 311: 1812913062, + 312: 1106886207, + 316: 62586880, + 318: 269607942, + 321: 2331875440, + 326: 324042858, + 329: 1748866603, + 330: 3710576745, + 334: 1588140542, + 339: 1910411492, + 341: 63143, + 344: 1489161286, + 348: 1412005389, + 349: 2996412825, + 354: 1192889677, + 356: 1432694549, + 359: 533304594, + 366: 3605025046, + 369: 2077348073, + 374: 3870528754, + 375: 3354097035, + 376: 1590110334, + 378: 964290506, + 383: 2194248940, + 386: 693294050, + 388: 1338730888, + 390: 649623368, + 393: 331217063, + 395: 3022502606, + 399: 825292510, + 406: 2109928018, + 421: 1955796701, + 422: 1587800882, + 425: 2383857042, + 428: 3942703131, + 429: 1915333822, + 430: 3276067091, + } +} + +var currentCastleObjectSlotEncoders = map[int]func([]any) (map[string]any, error){ + 41: encodeCurrentCastleObjectSlot41, + 77: encodeCurrentCastleObjectSlot77, + 99: encodeCurrentCastleObjectSlot99, + 100: encodeCurrentCastleObjectSlot100, + 46: encodeCurrentCastleObjectSlot46, + 128: encodeCurrentCastleObjectSlot128, + 173: encodeCurrentCastleObjectSlot173, + 214: encodeCurrentCastleObjectSlot214, + 290: encodeCurrentCastleObjectSlot290, + 342: encodeCurrentCastleObjectSlot342, + 352: encodeCurrentCastleObjectSlot352, + 416: encodeCurrentCastleObjectSlot416, + 424: encodeCurrentCastleObjectSlot424, +} + +var currentCastleObjectSlotOrder = []int{ + 41, + 77, + 99, + 100, + 46, + 128, + 173, + 214, + 290, + 342, + 352, + 416, + 424, +} + +func populateCurrentCastleObjectSlots(payload []any, values map[int]map[string]any) error { + if len(payload) != currentCastlePayloadSlots { + return fmt.Errorf("current Castle payload has %d slots, want %d", len(payload), currentCastlePayloadSlots) + } + finalDefaults := defaultCurrentCastleObjectSlotFinalValues() + for _, slot := range currentCastleObjectSlotOrder { + encoder := currentCastleObjectSlotEncoders[slot] + if override, ok := values[slot]; ok { + payload[slot] = override + continue + } + if finalValue, ok := finalDefaults[slot]; ok { + payload[slot] = finalValue + continue + } + encoded, err := encoder(payload) + if err != nil { + return fmt.Errorf("current Castle object slot %d: %w", slot, err) + } + payload[slot] = encoded + } + return nil +} + +func defaultCurrentCastleObjectSlotFinalValues() map[int]map[string]any { + return map[int]map[string]any{ + // Current Chrome begin_login object payload defaults observed from the + // native Windows profile. + 41: { + "V": "Ls9BGR3QrImUhLGh8DdWMQ==", + "wh": "", + "y": "", + "G": "", + "Hrc": map[string]any{ + "j": "", + }, + }, + 46: { + "mx": "", + "h": "", + "r": "", + "AI": "", + }, + 77: { + "AFS": "JcW/1uh5mugs0/qG3eXd5Q==", + "tu": "", + "Xd": "", + "BgV": "", + }, + 99: { + "n": "", + "cOw": "", + }, + 100: { + "rj": "5e4bQb/W", + "KwJ": map[string]any{ + "T": "CzejbOYagxFvwg==", + }, + "L": map[string]any{ + "T": "", + }, + "E": "", + }, + 128: { + "ekC": "jM4d0BtBVjEbQeXusaE=", + "fCH": "8Dc=", + }, + 173: { + "t": "gY/mGnrO", + "JZX": "rImUhLGh8DdWMQ==", + "ga": map[string]any{ + "sE": "", + "OkL": "", + "hD": "", + }, + "wPg": "", + "FqX": "", + }, + 214: { + "h": "LRTQyw==", + "yhj": "S+hTpr/W8DdL6PA3", + "XI": "", + }, + 290: { + "d": "LRRvwgs3+WmjbOYaCzcMuQ==", + }, + 342: { + "kFx": "CzfQy+YaCzdac9DLLRSQHQ==", + }, + 352: { + "XYw": "gY/mGnrO0MsLN5Ad9+ejbA==", + }, + 416: { + "y": "LRR6zlpzo2xac/lpkB335w==", + "OiJ": "", + "HRd": map[string]any{ + "j": "", + "hCP": "", + }, + }, + 424: { + "kr": "jM4d0BtBVjE=", + "vj": map[string]any{ + "GXV": "G0Hl7rGh8Dc=", + }, + "RK": "", + "wXd": map[string]any{ + "RtN": map[string]any{ + "o": "", + "ApQ": "", + "G": "", + }, + }, + }, + } +} + +func encodeCurrentCastleObjectSlot41(payload []any) (map[string]any, error) { + units, err := currentCastleTQHashUnitsForSlot(payload, 40) + if err != nil { + return nil, err + } + return map[string]any{ + "V": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 0, 9)), + "wh": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 9, 15)), + "y": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 15, 16)), + "G": encodeCurrentCastleUnits(currentCastleSliceUnits(units, 16, 21)), + "Hrc": map[string]any{ + "j": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 21, len(units))), + }, + }, nil +} + +func encodeCurrentCastleObjectSlot290(payload []any) (map[string]any, error) { + return encodeCurrentCastleTQTTObjectSlot(payload, 289, "d") +} + +func encodeCurrentCastleObjectSlot352(payload []any) (map[string]any, error) { + return encodeCurrentCastleTQTTObjectSlot(payload, 351, "XYw") +} + +func encodeCurrentCastleObjectSlot46(_ []any) (map[string]any, error) { + return encodeCurrentCastleObjectSlot46FromMatches(nil), nil +} + +func encodeCurrentCastleObjectSlot46FromMatches(matches []string) map[string]any { + source := "" + if len(matches) > 0 { + source = currentCastleSlot6HashHex(strings.Join(matches, ",")) + } + encoded := encodeCurrentCastleLR(source) + units := jsUTF16Units(encoded) + return map[string]any{ + "mx": encodeCurrentCastleUnits(currentCastleSliceUnits(units, 0, 8)), + "h": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 8, 14)), + "r": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 14, 16)), + "AI": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 16, len(units))), + } +} + +func encodeCurrentCastleObjectSlot342(payload []any) (map[string]any, error) { + return encodeCurrentCastleTITTObjectSlot(payload, 341, "kFx") +} + +func encodeCurrentCastleObjectSlot77(_ []any) (map[string]any, error) { + return encodeCurrentCastleObjectSlot77FromSource("[]"), nil +} + +func encodeCurrentCastleObjectSlot77FromSource(source string) map[string]any { + token := encodeCurrentCastleUnits(jsUTF16Units(source)) + units := jsUTF16Units(token) + return map[string]any{ + "AFS": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 0, 10)), + "tu": encodeCurrentCastleUnits(currentCastleSliceUnits(units, 10, 16)), + "Xd": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 16, 24)), + "BgV": encodeCurrentCastleUnits(currentCastleSliceUnits(units, 24, len(units))), + } +} + +func encodeCurrentCastleObjectSlot99(_ []any) (map[string]any, error) { + return encodeCurrentCastleObjectSlot99FromCanPlayType(""), nil +} + +func encodeCurrentCastleObjectSlot99FromCanPlayType(canPlayType string) map[string]any { + return encodeCurrentCastleObjectSlot99FromSource(encodeCurrentCastleRawPackedUnits(jsUTF16Units(canPlayType))) +} + +func encodeCurrentCastleObjectSlot99FromSource(source string) map[string]any { + units := jsUTF16Units(source) + return map[string]any{ + "n": encodeCurrentCastleUnits(currentCastleSliceUnits(units, 0, 9)), + "cOw": encodeCurrentCastleUnits(currentCastleSliceUnits(units, 9, len(units))), + } +} + +func encodeCurrentCastleObjectSlot100(payload []any) (map[string]any, error) { + if 99 >= len(payload) { + return nil, fmt.Errorf("source slot 99 outside payload length %d", len(payload)) + } + slot99, ok := payload[99].(map[string]any) + if !ok { + return nil, fmt.Errorf("source slot 99 has type %T, want map[string]any", payload[99]) + } + return encodeCurrentCastleObjectSlot100FromSlot99(slot99) +} + +func encodeCurrentCastleObjectSlot100FromSlot99(slot99 map[string]any) (map[string]any, error) { + slotJSON, err := currentCastleJSONStringifySlot99(slot99) + if err != nil { + return nil, err + } + units := jsUTF16Units(currentCastleTQHash(slotJSON)) + return map[string]any{ + "rj": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 0, 1)), + "KwJ": map[string]any{ + "T": encodeCurrentCastleUnits(currentCastleSliceUnits(units, 1, 10)), + }, + "L": map[string]any{ + "T": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 10, 14)), + }, + "E": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 14, len(units))), + }, nil +} + +func encodeCurrentCastleObjectSlot128(payload []any) (map[string]any, error) { + return encodeCurrentCastleTIToObjectSlot(payload, 127, map[string]currentCastleObjectFieldSpan{ + "ekC": {start: 0, end: 7}, + "fCH": {start: 7, end: 8}, + }) +} + +func encodeCurrentCastleObjectSlot214(payload []any) (map[string]any, error) { + return encodeCurrentCastleTIObjectSlot214(payload) +} + +func encodeCurrentCastleObjectSlot173(payload []any) (map[string]any, error) { + units, err := currentCastleTQHashUnitsForSlot(payload, 172) + if err != nil { + return nil, err + } + nested := currentCastleSliceUnits(units, 12, 17) + return map[string]any{ + "t": encodeCurrentCastleUnits(currentCastleSliceUnits(units, 0, 3)), + "JZX": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 3, 12)), + "ga": map[string]any{ + "sE": encodeCurrentCastleUnits(currentCastleSliceUnits(nested, 0, 6)), + "OkL": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(nested, 6, 16)), + "hD": encodeCurrentCastleUnits(currentCastleSliceUnits(nested, 16, len(nested))), + }, + "wPg": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 17, 25)), + "FqX": encodeCurrentCastleUnits(currentCastleSliceUnits(units, 25, len(units))), + }, nil +} + +func encodeCurrentCastleObjectSlot416(payload []any) (map[string]any, error) { + units, err := currentCastleTIHashUnitsForSlot(payload, 415) + if err != nil { + return nil, err + } + tail := currentCastleSliceUnits(units, 14, len(units)) + return map[string]any{ + "y": encodeCurrentCastleUnits(currentCastleSliceUnits(units, 0, 9)), + "OiJ": encodeCurrentCastleUnits(currentCastleSliceUnits(units, 9, 14)), + "HRd": map[string]any{ + "j": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(tail, 0, 6)), + "hCP": encodeCurrentCastleUnits(currentCastleSliceUnits(tail, 6, len(tail))), + }, + }, nil +} + +func encodeCurrentCastleObjectSlot424(payload []any) (map[string]any, error) { + units, err := currentCastleTIHashUnitsForSlot(payload, 423) + if err != nil { + return nil, err + } + value := currentCastleSliceUnits(units, 4, 11) + tail := currentCastleSliceUnits(units, 13, len(units)) + return map[string]any{ + "kr": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(units, 0, 4)), + "vj": map[string]any{ + "GXV": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(value, 0, len(value))), + }, + "RK": encodeCurrentCastleUnits(currentCastleSliceUnits(units, 11, 13)), + "wXd": map[string]any{ + "RtN": encodeCurrentCastleRawPackedUnits(currentCastleSliceUnits(tail, 0, len(tail))), + }, + }, nil +} + +type currentCastleObjectFieldSpan struct { + start int + end int +} + +func currentCastleTIHashUnitsForSlot(payload []any, sourceSlot int) ([]uint16, error) { + return currentCastleHashUnitsForSlot(payload, sourceSlot, currentCastleTIHash) +} + +func currentCastleTQHashUnitsForSlot(payload []any, sourceSlot int) ([]uint16, error) { + return currentCastleHashUnitsForSlot(payload, sourceSlot, currentCastleTQHash) +} + +func currentCastleHashUnitsForSlot(payload []any, sourceSlot int, hash func(string) string) ([]uint16, error) { + if sourceSlot < 0 || sourceSlot >= len(payload) { + return nil, fmt.Errorf("source slot %d outside payload length %d", sourceSlot, len(payload)) + } + slotJSON, err := currentCastleJSONStringify(payload[sourceSlot]) + if err != nil { + return nil, err + } + return jsUTF16Units(hash(slotJSON)), nil +} + +func currentCastleSliceUnits(units []uint16, start, end int) []uint16 { + if start < 0 { + start = 0 + } + if end < start { + end = start + } + if start > len(units) { + start = len(units) + } + if end > len(units) { + end = len(units) + } + return units[start:end] +} + +func encodeCurrentCastleTIToObjectSlot(payload []any, sourceSlot int, fields map[string]currentCastleObjectFieldSpan) (map[string]any, error) { + units, err := currentCastleTIHashUnitsForSlot(payload, sourceSlot) + if err != nil { + return nil, err + } + out := make(map[string]any, len(fields)) + for field, span := range fields { + if span.start < 0 || span.end < span.start || span.end > len(units) { + return nil, fmt.Errorf("field %s span %d:%d outside hash length %d", field, span.start, span.end, len(units)) + } + out[field] = encodeCurrentCastleRawPackedUnits(units[span.start:span.end]) + } + return out, nil +} + +func encodeCurrentCastleTIObjectSlot214(payload []any) (map[string]any, error) { + if 213 >= len(payload) { + return nil, fmt.Errorf("source slot 213 outside payload length %d", len(payload)) + } + slotJSON, err := currentCastleJSONStringify(payload[213]) + if err != nil { + return nil, err + } + hash := currentCastleTIHash(slotJSON) + units := jsUTF16Units(hash) + return map[string]any{ + "h": encodeCurrentCastleUnits(units[:min(len(units), 2)]), + "yhj": encodeCurrentCastleRawPackedUnits(units[min(len(units), 2):min(len(units), 10)]), + "XI": encodeCurrentCastleRawPackedUnits(units[min(len(units), 10):]), + }, nil +} + +func encodeCurrentCastleTQTTObjectSlot(payload []any, sourceSlot int, field string) (map[string]any, error) { + if sourceSlot < 0 || sourceSlot >= len(payload) { + return nil, fmt.Errorf("source slot %d outside payload length %d", sourceSlot, len(payload)) + } + slotJSON, err := currentCastleJSONStringify(payload[sourceSlot]) + if err != nil { + return nil, err + } + return map[string]any{ + field: encodeCurrentCastleUnits(jsUTF16Units(currentCastleTQHash(slotJSON))), + }, nil +} + +func encodeCurrentCastleTITTObjectSlot(payload []any, sourceSlot int, field string) (map[string]any, error) { + if sourceSlot < 0 || sourceSlot >= len(payload) { + return nil, fmt.Errorf("source slot %d outside payload length %d", sourceSlot, len(payload)) + } + slotJSON, err := currentCastleJSONStringify(payload[sourceSlot]) + if err != nil { + return nil, err + } + return map[string]any{ + field: encodeCurrentCastleUnits(jsUTF16Units(currentCastleTIHash(slotJSON))), + }, nil +} + +func currentCastleJSONStringify(value any) (string, error) { + data, err := json.Marshal(value) + if err != nil { + return "", err + } + return string(data), nil +} + +func currentCastleJSONStringifySlot99(slot99 map[string]any) (string, error) { + n, ok := slot99["n"].(string) + if !ok { + return "", fmt.Errorf("slot 99 field n has type %T, want string", slot99["n"]) + } + cOw, ok := slot99["cOw"].(string) + if !ok { + return "", fmt.Errorf("slot 99 field cOw has type %T, want string", slot99["cOw"]) + } + nJSON, err := json.Marshal(n) + if err != nil { + return "", err + } + cOwJSON, err := json.Marshal(cOw) + if err != nil { + return "", err + } + return `{"n":` + string(nJSON) + `,"cOw":` + string(cOwJSON) + `}`, nil +} + +var currentCastleLowerFloatSlotEncoders = map[int]func(float64) string{ + 1: encodeCurrentCastleLowerFloatSlot1, + 3: encodeCurrentCastleLowerFloatSlot3, + 4: encodeCurrentCastleLowerFloatSlot4, + 5: encodeCurrentCastleLowerFloatSlot5, + 9: encodeCurrentCastleLowerFloatSlot9, + 10: encodeCurrentCastleLowerFloatSlot10, + 12: encodeCurrentCastleLowerFloatSlot12, + 13: encodeCurrentCastleLowerFloatSlot13, + 14: encodeCurrentCastleLowerFloatSlot14, + 15: encodeCurrentCastleLowerFloatSlot15, + 16: encodeCurrentCastleLowerFloatSlot16, + 17: encodeCurrentCastleLowerFloatSlot17, + 18: encodeCurrentCastleLowerFloatSlot18, + 19: encodeCurrentCastleLowerFloatSlot19, + 21: encodeCurrentCastleLowerFloatSlot21, + 22: encodeCurrentCastleLowerFloatSlot22, + 23: encodeCurrentCastleLowerFloatSlot23, + 24: encodeCurrentCastleLowerFloatSlot24, + 29: encodeCurrentCastleLowerFloatSlot29, + 30: encodeCurrentCastleLowerFloatSlot30, + 32: encodeCurrentCastleLowerFloatSlot32, + 33: encodeCurrentCastleLowerFloatSlot33, + 34: encodeCurrentCastleLowerFloatSlot34, + 35: encodeCurrentCastleLowerFloatSlot35, + 36: encodeCurrentCastleLowerFloatSlot36, + 37: encodeCurrentCastleLowerFloatSlot37, + 38: encodeCurrentCastleLowerFloatSlot38, + 39: encodeCurrentCastleLowerFloatSlot39, + 43: encodeCurrentCastleLowerFloatSlot43, + 45: encodeCurrentCastleLowerFloatSlot45, + 48: encodeCurrentCastleLowerFloatSlot48, + 50: encodeCurrentCastleLowerFloatSlot50, + 51: encodeCurrentCastleLowerFloatSlot51, + 52: encodeCurrentCastleLowerFloatSlot52, + 54: encodeCurrentCastleLowerFloatSlot54, + 55: encodeCurrentCastleLowerFloatSlot55, + 57: encodeCurrentCastleLowerFloatSlot57, + 58: encodeCurrentCastleLowerFloatSlot58, + 59: encodeCurrentCastleLowerFloatSlot59, + 61: encodeCurrentCastleLowerFloatSlot61, + 62: encodeCurrentCastleLowerFloatSlot62, + 63: encodeCurrentCastleLowerFloatSlot63, + 65: encodeCurrentCastleLowerFloatSlot65, + 66: encodeCurrentCastleLowerFloatSlot66, + 69: encodeCurrentCastleLowerFloatSlot69, + 70: encodeCurrentCastleLowerFloatSlot70, + 74: encodeCurrentCastleLowerFloatSlot74, + 76: encodeCurrentCastleLowerFloatSlot76, + 80: encodeCurrentCastleLowerFloatSlot80, + 81: encodeCurrentCastleLowerFloatSlot81, + 82: encodeCurrentCastleLowerFloatSlot82, + 84: encodeCurrentCastleLowerFloatSlot84, + 85: encodeCurrentCastleLowerFloatSlot85, + 86: encodeCurrentCastleLowerFloatSlot86, + 88: encodeCurrentCastleLowerFloatSlot88, + 91: encodeCurrentCastleLowerFloatSlot91, + 92: encodeCurrentCastleLowerFloatSlot92, + 93: encodeCurrentCastleLowerFloatSlot93, + 94: encodeCurrentCastleLowerFloatSlot94, + 95: encodeCurrentCastleLowerFloatSlot95, + 96: encodeCurrentCastleLowerFloatSlot96, + 97: encodeCurrentCastleLowerFloatSlot97, + 101: encodeCurrentCastleLowerFloatSlot101, + 105: encodeCurrentCastleLowerFloatSlot105, + 107: encodeCurrentCastleLowerFloatSlot107, + 110: encodeCurrentCastleLowerFloatSlot110, + 111: encodeCurrentCastleLowerFloatSlot111, + 112: encodeCurrentCastleLowerFloatSlot112, + 114: encodeCurrentCastleLowerFloatSlot114, + 117: encodeCurrentCastleLowerFloatSlot117, + 118: encodeCurrentCastleLowerFloatSlot118, + 122: encodeCurrentCastleLowerFloatSlot122, + 124: encodeCurrentCastleLowerFloatSlot124, + 125: encodeCurrentCastleLowerFloatSlot125, + 126: encodeCurrentCastleLowerFloatSlot126, + 129: encodeCurrentCastleLowerFloatSlot129, + 131: encodeCurrentCastleLowerFloatSlot131, + 132: encodeCurrentCastleLowerFloatSlot132, + 134: encodeCurrentCastleLowerFloatSlot134, + 135: encodeCurrentCastleLowerFloatSlot135, + 136: encodeCurrentCastleLowerFloatSlot136, + 138: encodeCurrentCastleLowerFloatSlot138, + 139: encodeCurrentCastleLowerFloatSlot139, + 140: encodeCurrentCastleLowerFloatSlot140, + 141: encodeCurrentCastleLowerFloatSlot141, + 142: encodeCurrentCastleLowerFloatSlot142, + 143: encodeCurrentCastleLowerFloatSlot143, + 144: encodeCurrentCastleLowerFloatSlot144, + 145: encodeCurrentCastleLowerFloatSlot145, + 146: encodeCurrentCastleLowerFloatSlot146, + 148: encodeCurrentCastleLowerFloatSlot148, + 151: encodeCurrentCastleLowerFloatSlot151, + 153: encodeCurrentCastleLowerFloatSlot153, + 155: encodeCurrentCastleLowerFloatSlot155, + 157: encodeCurrentCastleLowerFloatSlot157, + 158: encodeCurrentCastleLowerFloatSlot158, + 160: encodeCurrentCastleLowerFloatSlot160, + 161: encodeCurrentCastleLowerFloatSlot161, + 162: encodeCurrentCastleLowerFloatSlot162, + 163: encodeCurrentCastleLowerFloatSlot163, + 164: encodeCurrentCastleLowerFloatSlot164, + 165: encodeCurrentCastleLowerFloatSlot165, + 166: encodeCurrentCastleLowerFloatSlot166, + 167: encodeCurrentCastleLowerFloatSlot167, + 169: encodeCurrentCastleLowerFloatSlot169, + 170: encodeCurrentCastleLowerFloatSlot170, + 171: encodeCurrentCastleLowerFloatSlot171, + 178: encodeCurrentCastleLowerFloatSlot178, + 179: encodeCurrentCastleLowerFloatSlot179, + 181: encodeCurrentCastleLowerFloatSlot181, + 185: encodeCurrentCastleLowerFloatSlot185, + 186: encodeCurrentCastleLowerFloatSlot186, + 187: encodeCurrentCastleLowerFloatSlot187, + 189: encodeCurrentCastleLowerFloatSlot189, + 190: encodeCurrentCastleLowerFloatSlot190, + 191: encodeCurrentCastleLowerFloatSlot191, + 192: encodeCurrentCastleLowerFloatSlot192, + 193: encodeCurrentCastleLowerFloatSlot193, + 195: encodeCurrentCastleLowerFloatSlot195, + 197: encodeCurrentCastleLowerFloatSlot197, + 199: encodeCurrentCastleLowerFloatSlot199, + 200: encodeCurrentCastleLowerFloatSlot200, + 201: encodeCurrentCastleLowerFloatSlot201, + 202: encodeCurrentCastleLowerFloatSlot202, + 208: encodeCurrentCastleLowerFloatSlot208, + 210: encodeCurrentCastleLowerFloatSlot210, + 211: encodeCurrentCastleLowerFloatSlot211, + 215: encodeCurrentCastleLowerFloatSlot215, + 216: encodeCurrentCastleLowerFloatSlot216, + 218: encodeCurrentCastleLowerFloatSlot218, + 219: encodeCurrentCastleLowerFloatSlot219, + 222: encodeCurrentCastleLowerFloatSlot222, + 225: encodeCurrentCastleLowerFloatSlot225, + 226: encodeCurrentCastleLowerFloatSlot226, + 227: encodeCurrentCastleLowerFloatSlot227, + 229: encodeCurrentCastleLowerFloatSlot229, + 230: encodeCurrentCastleLowerFloatSlot230, + 232: encodeCurrentCastleLowerFloatSlot232, + 233: encodeCurrentCastleLowerFloatSlot233, + 234: encodeCurrentCastleLowerFloatSlot234, + 236: encodeCurrentCastleLowerFloatSlot236, + 238: encodeCurrentCastleLowerFloatSlot238, + 241: encodeCurrentCastleLowerFloatSlot241, + 242: encodeCurrentCastleLowerFloatSlot242, + 243: encodeCurrentCastleLowerFloatSlot243, + 244: encodeCurrentCastleLowerFloatSlot244, + 248: encodeCurrentCastleLowerFloatSlot248, + 249: encodeCurrentCastleLowerFloatSlot249, + 250: encodeCurrentCastleLowerFloatSlot250, + 251: encodeCurrentCastleLowerFloatSlot251, + 252: encodeCurrentCastleLowerFloatSlot252, + 253: encodeCurrentCastleLowerFloatSlot253, + 255: encodeCurrentCastleLowerFloatSlot255, + 259: encodeCurrentCastleLowerFloatSlot259, + 262: encodeCurrentCastleLowerFloatSlot262, + 263: encodeCurrentCastleLowerFloatSlot263, + 264: encodeCurrentCastleLowerFloatSlot264, + 265: encodeCurrentCastleLowerFloatSlot265, + 268: encodeCurrentCastleLowerFloatSlot268, + 269: encodeCurrentCastleLowerFloatSlot269, + 270: encodeCurrentCastleLowerFloatSlot270, + 271: encodeCurrentCastleLowerFloatSlot271, + 272: encodeCurrentCastleLowerFloatSlot272, + 275: encodeCurrentCastleLowerFloatSlot275, + 276: encodeCurrentCastleLowerFloatSlot276, + 278: encodeCurrentCastleLowerFloatSlot278, + 280: encodeCurrentCastleLowerFloatSlot280, + 282: encodeCurrentCastleLowerFloatSlot282, + 283: encodeCurrentCastleLowerFloatSlot283, + 284: encodeCurrentCastleLowerFloatSlot284, + 285: encodeCurrentCastleLowerFloatSlot285, + 286: encodeCurrentCastleLowerFloatSlot286, + 287: encodeCurrentCastleLowerFloatSlot287, + 291: encodeCurrentCastleLowerFloatSlot291, + 294: encodeCurrentCastleLowerFloatSlot294, + 295: encodeCurrentCastleLowerFloatSlot295, + 296: encodeCurrentCastleLowerFloatSlot296, + 299: encodeCurrentCastleLowerFloatSlot299, + 300: encodeCurrentCastleLowerFloatSlot300, + 301: encodeCurrentCastleLowerFloatSlot301, + 303: encodeCurrentCastleLowerFloatSlot303, + 305: encodeCurrentCastleLowerFloatSlot305, + 306: encodeCurrentCastleLowerFloatSlot306, + 307: encodeCurrentCastleLowerFloatSlot307, + 309: encodeCurrentCastleLowerFloatSlot309, + 310: encodeCurrentCastleLowerFloatSlot310, + 313: encodeCurrentCastleLowerFloatSlot313, + 314: encodeCurrentCastleLowerFloatSlot314, + 315: encodeCurrentCastleLowerFloatSlot315, + 317: encodeCurrentCastleLowerFloatSlot317, + 319: encodeCurrentCastleLowerFloatSlot319, + 320: encodeCurrentCastleLowerFloatSlot320, + 322: encodeCurrentCastleLowerFloatSlot322, + 323: encodeCurrentCastleLowerFloatSlot323, + 324: encodeCurrentCastleLowerFloatSlot324, + 325: encodeCurrentCastleLowerFloatSlot325, + 327: encodeCurrentCastleLowerFloatSlot327, + 331: encodeCurrentCastleLowerFloatSlot331, + 332: encodeCurrentCastleLowerFloatSlot332, + 333: encodeCurrentCastleLowerFloatSlot333, + 335: encodeCurrentCastleLowerFloatSlot335, + 336: encodeCurrentCastleLowerFloatSlot336, + 337: encodeCurrentCastleLowerFloatSlot337, + 338: encodeCurrentCastleLowerFloatSlot338, + 340: encodeCurrentCastleLowerFloatSlot340, + 343: encodeCurrentCastleLowerFloatSlot343, + 345: encodeCurrentCastleLowerFloatSlot345, + 346: encodeCurrentCastleLowerFloatSlot346, + 347: encodeCurrentCastleLowerFloatSlot347, + 353: encodeCurrentCastleLowerFloatSlot353, + 355: encodeCurrentCastleLowerFloatSlot355, + 357: encodeCurrentCastleLowerFloatSlot357, + 358: encodeCurrentCastleLowerFloatSlot358, + 360: encodeCurrentCastleLowerFloatSlot360, + 361: encodeCurrentCastleLowerFloatSlot361, + 362: encodeCurrentCastleLowerFloatSlot362, + 363: encodeCurrentCastleLowerFloatSlot363, + 364: encodeCurrentCastleLowerFloatSlot364, + 365: encodeCurrentCastleLowerFloatSlot365, + 367: encodeCurrentCastleLowerFloatSlot367, + 368: encodeCurrentCastleLowerFloatSlot368, + 370: encodeCurrentCastleLowerFloatSlot370, + 371: encodeCurrentCastleLowerFloatSlot371, + 372: encodeCurrentCastleLowerFloatSlot372, + 373: encodeCurrentCastleLowerFloatSlot373, + 377: encodeCurrentCastleLowerFloatSlot377, + 379: encodeCurrentCastleLowerFloatSlot379, + 380: encodeCurrentCastleLowerFloatSlot380, + 381: encodeCurrentCastleLowerFloatSlot381, + 382: encodeCurrentCastleLowerFloatSlot382, + 384: encodeCurrentCastleLowerFloatSlot384, + 385: encodeCurrentCastleLowerFloatSlot385, + 387: encodeCurrentCastleLowerFloatSlot387, + 389: encodeCurrentCastleLowerFloatSlot389, + 391: encodeCurrentCastleLowerFloatSlot391, + 392: encodeCurrentCastleLowerFloatSlot392, + 396: encodeCurrentCastleLowerFloatSlot396, + 397: encodeCurrentCastleLowerFloatSlot397, + 398: encodeCurrentCastleLowerFloatSlot398, + 401: encodeCurrentCastleLowerFloatSlot401, + 402: encodeCurrentCastleLowerFloatSlot402, + 403: encodeCurrentCastleLowerFloatSlot403, + 404: encodeCurrentCastleLowerFloatSlot404, + 405: encodeCurrentCastleLowerFloatSlot405, + 407: encodeCurrentCastleLowerFloatSlot407, + 408: encodeCurrentCastleLowerFloatSlot408, + 409: encodeCurrentCastleLowerFloatSlot409, + 410: encodeCurrentCastleLowerFloatSlot410, + 411: encodeCurrentCastleLowerFloatSlot411, + 412: encodeCurrentCastleLowerFloatSlot412, + 413: encodeCurrentCastleLowerFloatSlot413, + 414: encodeCurrentCastleLowerFloatSlot414, + 417: encodeCurrentCastleLowerFloatSlot417, + 418: encodeCurrentCastleLowerFloatSlot418, + 419: encodeCurrentCastleLowerFloatSlot419, + 420: encodeCurrentCastleLowerFloatSlot420, + 426: encodeCurrentCastleLowerFloatSlot426, + 427: encodeCurrentCastleLowerFloatSlot427, +} + +func populateCurrentCastleLowerFloatSlots(payload []any, values map[int]float64) error { + if len(payload) < currentCastlePayloadSlots { + return fmt.Errorf("current Castle payload has %d slots, want at least %d", len(payload), currentCastlePayloadSlots) + } + for slot, encoder := range currentCastleLowerFloatSlotEncoders { + payload[slot] = encoder(values[slot]) + } + return nil +} + +var currentCastleArraySlotEncoders = map[int]func([]float64) string{ + 254: encodeCurrentCastleArraySlot254, + 302: encodeCurrentCastleArraySlot302, +} + +func defaultCurrentCastleArraySlotValues() map[int][]float64 { + return map[int][]float64{ + // The visible Chrome begin_login token encodes sparse one-hot samples. + 254: currentCastleFloat64Samples(256, 199), + 302: currentCastleFloat64Samples(16, 13), + } +} + +func currentCastleFloat64Samples(count int, oneIndexes ...int) []float64 { + if count <= 0 { + return nil + } + values := make([]float64, count) + for _, index := range oneIndexes { + if index >= 0 && index < len(values) { + values[index] = 1 + } + } + return values +} + +func populateCurrentCastleArraySlots(payload []any, values map[int][]float64) error { + if len(payload) < currentCastlePayloadSlots { + return fmt.Errorf("current Castle payload has %d slots, want at least %d", len(payload), currentCastlePayloadSlots) + } + defaults := defaultCurrentCastleArraySlotValues() + for slot, encoder := range currentCastleArraySlotEncoders { + slotValues, ok := values[slot] + if !ok { + slotValues = defaults[slot] + } + payload[slot] = encoder(slotValues) + } + return nil +} + +var currentCastlePackedStringSlotEncoders = map[int]func([]string) string{ + 0: encodeCurrentCastlePackedStringComponents, + 7: encodeCurrentCastlePackedStringComponents, + 119: encodeCurrentCastlePackedStringComponents, +} + +func defaultCurrentCastlePackedStringSlotValues() map[int][]string { + return map[int][]string{ + 0: defaultCurrentCastleSlot0PackedComponents(), + 7: defaultCurrentCastleSlot7PackedComponents(), + 119: defaultCurrentCastleWebGPUPackedComponents(), + } +} + +func defaultCurrentCastleSlot0PackedComponents() []string { + return []string{ + // Current Chrome begin_login defaults from the native Windows profile. + "pLJf4l/i", + "hoom", + "vbq6wVWT2EHZILqMkw==", + "", + "nLpq61raGg==", + "P0JHPD48RUZARTw/R0Q=", + } +} + +func defaultCurrentCastleSlot7PackedComponents() []string { + return []string{ + // Current Chrome begin_login defaults from the native Windows profile. + "CJk=", + "JEbZeg==", + "m3s=", + "sJ5lbMPD9N7IWWe3L7ZsGYue1nK3+Vu35GdZZyq3tmwZUTsqt19RO6y3VCcnw1i2WH2abPveyLIWWbJRty+az1uwIwO3w2w6WLdGWAI6nqy3Yu8tnlBY3uQ7HFlnWWdZZ7fS9OH0LWzeyLIWWbJR", + "i+vAAJAdJIbAaMBvZ2dPnsmaY5KQZ+HJwF7AnMmQ09VnJMBowK3WJeRq5J/iUJ/krSVcwHFe68AAkB0khsBowGOSkGfh1VnhwF7AnMmQ09VnJMBowK3WJeRq5J/iUJ/krSVcwHFe68AAkB0khsBowERnFpU9qyCQHSSGwF7AnMmQ09VnJMBowFDW5GrkauRqwHER", + "ltZunm6e", + "YYsEQpkB5Q==", + "2gykDtI=", + "kHDTk+PKP+HT69NcxMQ8qa59eMXjxHau0+nTx67jqJLEP9Pr05qxItPG6XDTk+PKP+HT69N4xePEdpI+dtPp08eu46iSxD/T69OasSLTxulw05Pjyj/h0+vTX8Rx0uqws+PKP+HT6dPHruOoksQ/0+vTI7HTxuY=", + "8/u7", + "Fy9X/g/+J08fJ/4XVz8=", + } +} + +func defaultCurrentCastleWebGPUPackedComponents() []string { + return []string{ + // Current Chrome begin_login defaults from the native Windows profile. + "ACiTIpOL", + "Lga4aAbykN6kkA==", + "mY47m47x", + } +} + +func populateCurrentCastlePackedStringSlots(payload []any, values map[int][]string) error { + if len(payload) < currentCastlePayloadSlots { + return fmt.Errorf("current Castle payload has %d slots, want at least %d", len(payload), currentCastlePayloadSlots) + } + defaults := defaultCurrentCastlePackedStringSlotValues() + for slot, encoder := range currentCastlePackedStringSlotEncoders { + slotValues, ok := values[slot] + if !ok { + slotValues = defaults[slot] + } + payload[slot] = encoder(slotValues) + } + return nil +} + +var currentCastleUnitPackedStringSlotEncoders = map[int]func([]string) string{ + 8: encodeCurrentCastleUnitPackedStringComponents, +} + +func defaultCurrentCastleUnitPackedStringSlotValues() map[int][]string { + return map[int][]string{ + 8: defaultCurrentCastleSlot8PackedComponents(), + } +} + +func defaultCurrentCastleSlot8PackedComponents() []string { + return []string{ + // Current Chrome begin_login defaults from the native Windows profile. + "a14=", + "pD/f1v/ISA==", + } +} + +func populateCurrentCastleUnitPackedStringSlots(payload []any, values map[int][]string) error { + if len(payload) < currentCastlePayloadSlots { + return fmt.Errorf("current Castle payload has %d slots, want at least %d", len(payload), currentCastlePayloadSlots) + } + defaults := defaultCurrentCastleUnitPackedStringSlotValues() + for slot, encoder := range currentCastleUnitPackedStringSlotEncoders { + slotValues, ok := values[slot] + if !ok { + slotValues = defaults[slot] + } + payload[slot] = encoder(slotValues) + } + return nil +} + +var currentCastleStringSlotEncoders = map[int]func(string) string{ + 11: encodeCurrentCastleRawString, + 40: encodeCurrentCastleDoubleUTF16Scramble, + 89: encodeCurrentCastleUTF16Scramble, + 127: encodeCurrentCastleUTF16Scramble, + 172: encodeCurrentCastleUTF16Scramble, + 175: encodeCurrentCastleUTF16Scramble, + 213: encodeCurrentCastleRawString, + 217: encodeCurrentCastleDoubleUTF16Scramble, + 231: encodeCurrentCastleUTF16Scramble, + 351: encodeCurrentCastleUTF16Scramble, + 400: encodeCurrentCastleDoubleUTF16Scramble, + 415: encodeCurrentCastleDoubleUTF16Scramble, + 423: encodeCurrentCastleDoubleUTF16Scramble, +} + +func defaultCurrentCastleStringSlotValues() map[int]string { + return map[int]string{ + 11: "isz", + 40: "0", + 89: "0.5", + 127: "0", + 172: "0.95", + 175: "0.207", + 213: "GraL", + 217: "0.217", + 231: "0.486", + 351: "0.279", + 400: "0.4", + 415: "0.415", + 423: "0.423", + } +} + +func populateCurrentCastleStringSlots(payload []any, values map[int]string) error { + if len(payload) < currentCastlePayloadSlots { + return fmt.Errorf("current Castle payload has %d slots, want at least %d", len(payload), currentCastlePayloadSlots) + } + defaults := defaultCurrentCastleStringSlotValues() + finalDefaults := defaultCurrentCastleStringSlotEncodedValues() + for slot, encoder := range currentCastleStringSlotEncoders { + if _, hasOverride := values[slot]; !hasOverride { + if encoded, ok := finalDefaults[slot]; ok { + payload[slot] = encoded + continue + } + } + slotValue, ok := values[slot] + if !ok { + slotValue = defaults[slot] + } + payload[slot] = encoder(slotValue) + } + return nil +} + +func defaultCurrentCastleStringSlotEncodedValues() map[int]string { + const encodedZero = "NoAtnP9LeAQ=" + return map[int]string{ + // Current Chrome begin_login defaults from the native Windows profile. + 11: "isz", + 40: encodedZero, + 89: encodedZero, + 127: encodedZero, + 172: encodedZero, + 175: encodedZero, + 213: "GraL", + 217: encodedZero, + 231: encodedZero, + 351: encodedZero, + 400: encodedZero, + 415: encodedZero, + 423: encodedZero, + } +} + +func currentCastleLengthPrefixUnits(length uint32) []uint16 { + var out []uint16 + for length >= currentCastleSlot6LengthContinue { + out = append(out, uint16(length¤tCastleSlot6LengthMask|currentCastleSlot6LengthContinue)) + length >>= currentCastleSlot6LengthShiftBits + } + return append(out, uint16(length¤tCastleSlot6LengthMask)) +} + +func currentCastleSlot6LengthPrefixUnits(componentLengths []int) ([]uint16, error) { + if len(componentLengths) != currentCastleSlot6ComponentCount { + return nil, fmt.Errorf("current Castle slot 6 has %d component lengths, want %d", len(componentLengths), currentCastleSlot6ComponentCount) + } + out := currentCastleLengthPrefixUnits(currentCastleSlot6ComponentCount) + for i, length := range componentLengths { + if length < 0 { + return nil, fmt.Errorf("current Castle slot 6 component %d has negative length %d", i, length) + } + out = append(out, currentCastleLengthPrefixUnits(uint32(length))...) + } + return out, nil +} + +func marshalCurrentCastlePayload(payload []any) (string, error) { + if len(payload) != currentCastlePayloadSlots { + return "", fmt.Errorf("current Castle payload has %d slots, want %d", len(payload), currentCastlePayloadSlots) + } + for i, value := range payload { + if value == nil { + return "", fmt.Errorf("current Castle payload slot %d is nil", i) + } + } + data, err := json.Marshal(payload) + if err != nil { + return "", err + } + return string(data), nil +} + +func insertCurrentCastleChecksum(payloadJSON string) string { + units := jsUTF16Units(payloadJSON) + checksum := currentCastleChecksum(payloadJSON) + insertAt := int(math.Floor(0.76 * float64(len(units)))) + label := currentCastleChecksumLabel + checksum + withChecksum := make([]uint16, 0, len(units)+len(label)) + withChecksum = append(withChecksum, units[:insertAt]...) + withChecksum = append(withChecksum, jsUTF16Units(label)...) + withChecksum = append(withChecksum, units[insertAt:]...) + return string(utf16.Decode(withChecksum)) +} + +func currentCastleChecksum(value string) string { + state := uint32(len(jsUTF16Units(value))) + sum := uint32(0) + for index, ch := range jsUTF16Units(value) { + state = bitswap32(state) + state = bitswap32(state) + state = state + currentCastleRotate16Into32(ch, 7, 25) + state = bitswap32(state) + sum += uint32(ch) * 0x9e3779b1 + if index&3 == 3 { + state = state + currentCastleRotate16Into32(uint16(state), 4, 28) + } + } + state ^= sum + state = (state << 21) | (state >> 11) + state ^= state >> 16 + state *= 0x85ebca6b + state ^= state >> 13 + state *= 0xc2b2ae35 + state ^= state >> 16 + var out [4]byte + out[0] = byte(state >> 24) + out[1] = byte(state >> 16) + out[2] = byte(state >> 8) + out[3] = byte(state) + return hex.EncodeToString(out[:]) +} + +func currentCastleTQHash(value string) string { + units := jsUTF16Units(value) + state := uint32(0xc1960396) ^ uint32(len(units)) + sum := uint32(0) + state ^= 0 * 0x38da94b1 + state += ((state & 0xffff) << 5) | ((state & 0xffff) >> 27) + state ^= ((state & 0xffff) << 24) | ((state & 0xffff) >> 8) + state = (state^(state>>11))*0x88a4e807 + 0x89f085ba + for index, ch := range units { + raw := uint32(ch) + state ^= raw + state = (state ^ raw) * 0x69d69459 + state ^= 0xa87dabc4 + state ^= raw + state ^= ((raw & 0xffff) << 14) | ((raw & 0xffff) >> 18) + state ^= 0x83274064 + state += ((raw & 0xffff) << 14) | ((raw & 0xffff) >> 18) + state ^= (sum << 13) | (sum >> 19) + state += raw * 0x858ff2cb + state += ((raw & 0xffff) << 12) | ((raw & 0xffff) >> 20) + state = (state ^ raw) * 0x9dfcf351 + state = bits.RotateLeft32(state, 17) + state = bitswap32(state) + sum += raw * 0x9e3779b1 + if index&3 == 3 { + state += (state ^ (state >> 11)) * 0xd1a79fef + state += ((state & 0xffff) << 8) | ((state & 0xffff) >> 24) + state = (state ^ (state >> 11)) * 0xa3cfce21 + state ^= state >> 5 + } + } + state ^= sum + state ^= sum + state = bitswap32(state) + state += ((sum & 0xffff) << 15) | ((sum & 0xffff) >> 17) + state = bits.RotateLeft32(state, 15) + state ^= state >> 16 + state *= 0x85ebca6b + state ^= state >> 13 + state *= 0xc2b2ae35 + state ^= state >> 16 + return fmt.Sprintf("%08x", state) +} + +func currentCastleTIHash(value string) string { + units := jsUTF16Units(value) + state := uint32(0xdd3b1081) ^ uint32(len(units)) + sum := uint32(0) + state ^= 0 * 0x1ab5d0cb + state = (state ^ (state >> 11)) * 0xfcc4435f + state += (state ^ (state >> 11)) * 0x7d8bc77f + for index, ch := range units { + raw := uint32(ch) + state ^= state >> 8 + state ^= state >> 16 + state += ((raw & 0xffff) << 1) | ((raw & 0xffff) >> 31) + state ^= ((raw & 0xffff) << 5) | ((raw & 0xffff) >> 27) + state ^= raw + state += ((raw & 0xffff) << 5) | ((raw & 0xffff) >> 27) + state += ((raw & 0xffff) << 14) | ((raw & 0xffff) >> 18) + state = bits.RotateLeft32(state, 20) + state = (state ^ raw) * 0x98f0350b + state += raw * 0xd9853933 + sum += raw * 0x9e3779b1 + if index&3 == 3 { + state ^= uint32(len(units)) * 0x6963131f + state = (state^(state>>11))*0xe0acd687 + 0x75c53c2d + state = bits.RotateLeft32(state, 12) + } + } + state ^= sum + state = (state ^ sum) * 0xb7fb31df + state ^= ((sum & 0xffff) << 12) | ((sum & 0xffff) >> 20) + state ^= (sum << 19) | (sum >> 13) + state ^= state >> 16 + state *= 0x85ebca6b + state ^= state >> 13 + state *= 0xc2b2ae35 + state ^= state >> 16 + return fmt.Sprintf("%08x", state) +} + +func encodeCurrentCastleTimestamp(timestamp string) string { + return encodeCurrentCastleUnits(jsUTF16Units(timestamp)) +} + +func encodeCurrentCastleUnits(units []uint16) string { + out := make([]byte, 0, len(units)*2) + for _, ch := range units { + n := (uint32(ch) - 54655) & 0xffff + n = (46488 ^ n) & 0xffff + n = (uint32(uint16(n))*54213 + 385) & 0xffff + n = ((n >> 12) | (n << 4)) & 0xffff + n = (60834 + n) & 0xffff + n = ((n >> 11) | (n << 5)) & 0xffff + out = append(out, byte(n>>8), byte(n)) + } + return base64.StdEncoding.EncodeToString(out) +} + +func encodeCurrentCastleRawPackedUnits(units []uint16) string { + out := make([]byte, 0, len(units)*2) + for _, ch := range units { + n := (uint32(ch)*25467 + 59068) & 0xffff + n = (n ^ 17750) & 0xffff + n = currentCastleRotL16(n, 14) + n = currentCastleRotL16(n, 14) + out = append(out, byte(n>>8), byte(n)) + } + return base64.StdEncoding.EncodeToString(out) +} + +func encodeCurrentCastleUTF16Scramble(value string) string { + out := make([]byte, 0, len(jsUTF16Units(value))*2) + for _, ch := range jsUTF16Units(value) { + n := (uint32(ch) ^ 27722) & 0xffff + n = (n - 40955) & 0xffff + n = ((n << 15) | (n >> 1)) & 0xffff + n = (uint32(uint16(n))*49687 + 51498) & 0xffff + n = (n ^ 36024) & 0xffff + out = append(out, byte(n>>8), byte(n)) + } + return base64.StdEncoding.EncodeToString(out) +} + +func encodeCurrentCastleDoubleUTF16Scramble(value string) string { + return encodeCurrentCastleUTF16Scramble(encodeCurrentCastleUTF16Scramble(value)) +} + +func encodeCurrentCastleRawString(value string) string { + return value +} + +func encodeCurrentCastleLR(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b ^= 8 + b = currentCastleRotL8(b, 6) + b = byte(uint16(b) * 207) + b += 218 + return b + }) +} + +func currentCastleLengthPrefixedStringComponentUnits(values []string) []uint16 { + componentUnits := make([][]uint16, len(values)) + componentLengths := make([]int, len(values)) + totalComponentUnits := 0 + for i, value := range values { + units := jsUTF16Units(value) + componentUnits[i] = units + componentLengths[i] = len(units) + totalComponentUnits += len(units) + } + rawUnits := make([]uint16, 0, len(values)+1+totalComponentUnits) + rawUnits = append(rawUnits, currentCastleLengthPrefixUnits(uint32(len(values)))...) + for _, length := range componentLengths { + rawUnits = append(rawUnits, currentCastleLengthPrefixUnits(uint32(length))...) + } + for _, units := range componentUnits { + rawUnits = append(rawUnits, units...) + } + return rawUnits +} + +func encodeCurrentCastlePackedStringComponents(values []string) string { + rawUnits := currentCastleLengthPrefixedStringComponentUnits(values) + return encodeCurrentCastleRawPackedUnits(rawUnits) +} + +func encodeCurrentCastleUnitPackedStringComponents(values []string) string { + return encodeCurrentCastleUnits(currentCastleLengthPrefixedStringComponentUnits(values)) +} + +func currentCastleSlot6RawUnits(components []string) ([]uint16, error) { + if len(components) != currentCastleSlot6ComponentCount { + return nil, fmt.Errorf("current Castle slot 6 has %d components, want %d", len(components), currentCastleSlot6ComponentCount) + } + componentUnits := make([][]uint16, len(components)) + componentLengths := make([]int, len(components)) + totalComponentUnits := 0 + for i, component := range components { + units := jsUTF16Units(component) + componentUnits[i] = units + componentLengths[i] = len(units) + totalComponentUnits += len(units) + } + prefixUnits, err := currentCastleSlot6LengthPrefixUnits(componentLengths) + if err != nil { + return nil, err + } + out := make([]uint16, 0, len(prefixUnits)+totalComponentUnits) + out = append(out, prefixUnits...) + for _, units := range componentUnits { + out = append(out, units...) + } + return out, nil +} + +func encodeCurrentCastleSlot6ComponentValues(values []string) ([]string, error) { + if len(values) != currentCastleSlot6ComponentCount { + return nil, fmt.Errorf("current Castle slot 6 has %d raw component values, want %d", len(values), currentCastleSlot6ComponentCount) + } + encoders := [...]func(string) string{ + encodeCurrentCastleSlot6Component0, + encodeCurrentCastleSlot6Component1, + encodeCurrentCastleSlot6Component2, + encodeCurrentCastleSlot6UZ, + encodeCurrentCastleSlot6TF, + encodeCurrentCastleSlot6Component5Fallback, + encodeCurrentCastleSlot6TU, + encodeCurrentCastleSlot6IH, + encodeCurrentCastleSlot6FJ, + encodeCurrentCastleSlot6IB, + encodeCurrentCastleSlot6AH, + encodeCurrentCastleSlot6IT, + encodeCurrentCastleSlot6AG, + encodeCurrentCastleSlot6AD, + encodeCurrentCastleSlot6T7, + encodeCurrentCastleSlot6ID, + encodeCurrentCastleSlot6FU, + encodeCurrentCastleSlot6Component17, + encodeCurrentCastleSlot6EJ, + encodeCurrentCastleSlot6Component19, + encodeCurrentCastleSlot6L9, + encodeCurrentCastleSlot6TX, + encodeCurrentCastleSlot6LI, + encodeCurrentCastleSlot6FL, + encodeCurrentCastleSlot6IO, + encodeCurrentCastleSlot6F3, + encodeCurrentCastleSlot6AJ, + encodeCurrentCastleSlot6Component27, + encodeCurrentCastleSlot6TM, + encodeCurrentCastleSlot6TUpperF, + encodeCurrentCastleSlot6FS, + encodeCurrentCastleSlot6Component31, + encodeCurrentCastleSlot6FF, + encodeCurrentCastleSlot6AU, + encodeCurrentCastleSlot6AQ, + encodeCurrentCastleSlot6LW, + encodeCurrentCastleSlot6TUpperM, + encodeCurrentCastleSlot6TH, + encodeCurrentCastleSlot6IS, + encodeCurrentCastleSlot6A8, + encodeCurrentCastleSlot6LUpperM, + encodeCurrentCastleSlot6EA, + encodeCurrentCastleSlot6E3, + encodeCurrentCastleSlot6T2, + encodeCurrentCastleSlot6TUpperX, + encodeCurrentCastleSlot6L5, + encodeCurrentCastleSlot6Component46, + } + components := make([]string, currentCastleSlot6ComponentCount) + for i, value := range values { + components[i] = encoders[i](value) + } + return components, nil +} + +func encodeCurrentCastleSlot6(components []string) (string, error) { + units, err := currentCastleSlot6RawUnits(components) + if err != nil { + return "", err + } + return encodeCurrentCastleUnits(units), nil +} + +func encodeCurrentCastleTransformedUTF8(value string, transform func(byte) byte) string { + input := []byte(value) + output := make([]byte, len(input)) + for i, b := range input { + output[i] = transform(b) + } + return base64.StdEncoding.EncodeToString(output) +} + +func currentCastleRotL8(value byte, shift uint) byte { + return value<>(8-shift) +} + +func currentCastleRotL16(value uint32, shift uint) uint32 { + return ((value << shift) | (value >> (16 - shift))) & 0xffff +} + +func encodeCurrentCastleSlot6TU(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 113) + b ^= 246 + b += 41 + return currentCastleRotL8(b, 2) + }) +} + +func encodeCurrentCastleSlot6TF(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 187) + b = byte(uint16(b) * 195) + b ^= 22 + b ^= 64 + return b + }) +} + +func encodeCurrentCastleSlot6Component0(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 57 + b += 39 + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleSlot6Component1(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 3) + b = byte(uint16(b) * 99) + b = byte(uint16(b) * 229) + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleSlot6Component2(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 231 + b ^= 237 + b = byte(uint16(b) * 213) + return b + }) +} + +func encodeCurrentCastleSlot6Component5Fallback(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 239 + b ^= 202 + b += 206 + return b + }) +} + +func encodeCurrentCastleSlot6UZ(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b ^= 114 + b = currentCastleRotL8(b, 2) + b = byte(uint16(b) * 193) + b ^= 47 + return b + }) +} + +func encodeCurrentCastleSlot6IH(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 205) + b = byte(uint16(b) * 29) + b += 8 + return b + }) +} + +func encodeCurrentCastleSlot6FJ(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b ^= 188 + return currentCastleRotL8(b, 5) + }) +} + +func encodeCurrentCastleSlot6IB(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 51) + b += 205 + b ^= 155 + b ^= 189 + return b + }) +} + +func encodeCurrentCastleSlot6AH(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b = currentCastleRotL8(b, 7) + return byte(uint16(b) * 63) + }) +} + +func encodeCurrentCastleSlot6IT(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 43) + b ^= 227 + b = currentCastleRotL8(b, 4) + return byte(uint16(b) * 179) + }) +} + +func encodeCurrentCastleSlot6AG(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b ^= 230 + b ^= 224 + b += 233 + return byte(uint16(b) * 33) + }) +} + +func encodeCurrentCastleSlot6AD(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b ^= 3 + b = currentCastleRotL8(b, 4) + return byte(uint16(b) * 107) + }) +} + +func encodeCurrentCastleSlot6T7(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b ^= 85 + b = byte(uint16(b) * 33) + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleSlot6ID(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 85 + b = currentCastleRotL8(b, 7) + b = currentCastleRotL8(b, 4) + return currentCastleRotL8(b, 2) + }) +} + +func encodeCurrentCastleSlot6FU(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 63) + b = currentCastleRotL8(b, 4) + b = currentCastleRotL8(b, 2) + b += 45 + return b + }) +} + +func encodeCurrentCastleSlot6EJ(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b ^= 203 + b = currentCastleRotL8(b, 3) + b = currentCastleRotL8(b, 6) + b ^= 191 + return b + }) +} + +func encodeCurrentCastleSlot6L9(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b += 90 + b += 98 + b ^= 190 + return b + }) +} + +func encodeCurrentCastleSlot6TX(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 244 + b += 253 + b ^= 234 + return b + }) +} + +func encodeCurrentCastleSlot6LI(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b = currentCastleRotL8(b, 2) + b += 38 + return currentCastleRotL8(b, 5) + }) +} + +func encodeCurrentCastleSlot6FL(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b = currentCastleRotL8(b, 2) + b += 230 + b ^= 145 + return b + }) +} + +func encodeCurrentCastleSlot6IO(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 249) + b += 215 + b ^= 29 + return b + }) +} + +func encodeCurrentCastleSlot6F3(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 99) + b = currentCastleRotL8(b, 3) + b ^= 18 + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleSlot6AJ(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 173) + b += 38 + b += 59 + return currentCastleRotL8(b, 2) + }) +} + +func encodeCurrentCastleSlot6Component17(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b ^= 252 + b += 214 + b += 43 + return b + }) +} + +func encodeCurrentCastleSlot6Component19(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 133) + b += 122 + b += 123 + return b + }) +} + +func encodeCurrentCastleSlot6Component27(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b = currentCastleRotL8(b, 6) + return currentCastleRotL8(b, 7) + }) +} + +func encodeCurrentCastleSlot6TM(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b = currentCastleRotL8(b, 4) + b ^= 154 + return b + }) +} + +func encodeCurrentCastleSlot6TUpperF(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 30 + b ^= 145 + b += 207 + return b + }) +} + +func encodeCurrentCastleSlot6FS(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 109) + b ^= 216 + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleSlot6Component31(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b = currentCastleRotL8(b, 3) + b += 241 + return b + }) +} + +func encodeCurrentCastleSlot6FF(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 73) + b ^= 129 + b = currentCastleRotL8(b, 7) + b += 228 + return b + }) +} + +func encodeCurrentCastleSlot6AU(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 111) + b = currentCastleRotL8(b, 3) + b ^= 200 + return currentCastleRotL8(b, 4) + }) +} + +func encodeCurrentCastleSlot6AQ(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 29) + b = currentCastleRotL8(b, 2) + b ^= 143 + return b + }) +} + +func encodeCurrentCastleSlot6LW(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 27 + b += 255 + return byte(uint16(b) * 185) + }) +} + +func encodeCurrentCastleSlot6TUpperM(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 137) + b += 63 + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleSlot6TH(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 69 + b += 178 + b += 241 + return currentCastleRotL8(b, 4) + }) +} + +func encodeCurrentCastleSlot6IS(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 229 + b ^= 42 + return currentCastleRotL8(b, 4) + }) +} + +func encodeCurrentCastleSlot6A8(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 196 + b ^= 10 + return currentCastleRotL8(b, 4) + }) +} + +func encodeCurrentCastleSlot6LUpperM(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 107) + b = byte(uint16(b) * 23) + b = byte(uint16(b) * 221) + b += 218 + return b + }) +} + +func encodeCurrentCastleSlot6EA(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b ^= 190 + b += 177 + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleSlot6E3(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 179) + b ^= 115 + b += 11 + return b + }) +} + +func encodeCurrentCastleSlot6T2(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b ^= 9 + b = byte(uint16(b) * 139) + return byte(uint16(b) * 111) + }) +} + +func encodeCurrentCastleSlot6TUpperX(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b = currentCastleRotL8(b, 7) + b ^= 13 + return b + }) +} + +func encodeCurrentCastleSlot6L5(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 235 + b = currentCastleRotL8(b, 1) + return byte(uint16(b) * 41) + }) +} + +func encodeCurrentCastleSlot6Component46(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b ^= 151 + b ^= 95 + b += 189 + b += 23 + return b + }) +} + +func currentCastleSlot6Hash(value string) uint32 { + units := jsUTF16Units(value) + hash := uint32(0) + index := 0 + remaining := len(units) & 3 + blockEnd := len(units) - remaining + blockMul := currentCastleIndexTG(0xea02e370) + for index < blockEnd { + k := uint32(units[index]&0xff) | + uint32(units[index+1]&0xff)<<8 | + uint32(units[index+2]&0xff)<<16 | + uint32(units[index+3]&0xff)<<24 + index += 4 + k *= 0xcc9e2d51 + k = bits.RotateLeft32(k, 15) + k *= blockMul + hash ^= k + hash = bits.RotateLeft32(hash, 13) + hashTimesFive := hash * 5 + hash = (hashTimesFive & 0xffff) + + currentCastleIndexTG(0x10bd0000) + + ((((hashTimesFive >> 16) + currentCastleIndexTG(8196848)) & 0xffff) << 16) + } + k := uint32(0) + switch remaining { + case 3: + k ^= uint32(units[index+2]&0xff) << 16 + fallthrough + case 2: + k ^= uint32(units[index+1]&0xff) << 8 + fallthrough + case 1: + k ^= uint32(units[index] & 0xff) + k *= 0xcc9e2d51 + k = bits.RotateLeft32(k, 15) + k *= blockMul + hash ^= k + } + hash ^= uint32(len(units)) + hash ^= hash >> 16 + hash *= 0x85ebca6b + hash ^= hash >> 13 + hash *= 0xc2b2ae35 + hash ^= hash >> 16 + return hash +} + +func currentCastleSlot6HashHex(value string) string { + return strconv.FormatUint(uint64(currentCastleSlot6Hash(value)), 16) +} + +func encodeCurrentCastleFloat64WithTransform(value float64, transform func(byte) byte) string { + var raw [8]byte + binary.BigEndian.PutUint64(raw[:], math.Float64bits(value)) + for i, b := range raw { + raw[i] = transform(b) + } + return base64.StdEncoding.EncodeToString(raw[:]) +} + +func encodeCurrentCastleFloat64(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + return byte((uint16(b)*65)&0xff) ^ 0xc6 + }) +} + +func encodeCurrentCastleFloat64ArrayWithTransform(values []float64, transform func(byte) byte) string { + raw := make([]byte, len(values)*8) + for i, value := range values { + binary.BigEndian.PutUint64(raw[i*8:], math.Float64bits(value)) + } + for i, b := range raw { + raw[i] = transform(b) + } + return base64.StdEncoding.EncodeToString(raw) +} + +func encodeCurrentCastleArraySlot254(values []float64) string { + return encodeCurrentCastleFloat64ArrayWithTransform(values, func(b byte) byte { + b ^= 32 + b = byte(uint16(b) * 185) + b = byte(uint16(b) * 109) + b += 195 + return b + }) +} + +func encodeCurrentCastleArraySlot302(values []float64) string { + return encodeCurrentCastleFloat64ArrayWithTransform(values, func(b byte) byte { + b = byte(uint16(b) * 167) + b = currentCastleRotL8(b, 4) + b ^= 192 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot1(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 155 + b += 129 + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot3(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b += 233 + b += 224 + b ^= 167 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot4(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 45 + b = currentCastleRotL8(b, 2) + b ^= 7 + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleLowerFloatSlot5(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 32 + b = currentCastleRotL8(b, 6) + b += 89 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot9(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 123) + b = byte(uint16(b) * 91) + b += 89 + return byte(uint16(b) * 29) + }) +} + +func encodeCurrentCastleLowerFloatSlot10(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 228 + b = byte(uint16(b) * 79) + b ^= 50 + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleLowerFloatSlot12(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 129) + b = currentCastleRotL8(b, 5) + return byte(uint16(b) * 251) + }) +} + +func encodeCurrentCastleLowerFloatSlot13(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 1) + b ^= 153 + b = currentCastleRotL8(b, 3) + return byte(uint16(b) * 25) + }) +} + +func encodeCurrentCastleLowerFloatSlot14(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 43) + b = byte(uint16(b) * 107) + b ^= 253 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot15(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b = currentCastleRotL8(b, 7) + b ^= 30 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot16(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 166 + b = byte(uint16(b) * 235) + b ^= 26 + return currentCastleRotL8(b, 7) + }) +} + +func encodeCurrentCastleLowerFloatSlot17(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 182 + b += 207 + return byte(uint16(b) * 175) + }) +} + +func encodeCurrentCastleLowerFloatSlot18(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 3) + b = currentCastleRotL8(b, 7) + return currentCastleRotL8(b, 7) + }) +} + +func encodeCurrentCastleLowerFloatSlot19(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 187) + b = currentCastleRotL8(b, 6) + return byte(uint16(b) * 161) + }) +} + +func encodeCurrentCastleLowerFloatSlot21(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 42 + b += 51 + return currentCastleRotL8(b, 4) + }) +} + +func encodeCurrentCastleLowerFloatSlot22(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 192 + b = currentCastleRotL8(b, 6) + return currentCastleRotL8(b, 7) + }) +} + +func encodeCurrentCastleLowerFloatSlot23(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b += 29 + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot24(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 185) + b = currentCastleRotL8(b, 3) + b += 243 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot29(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 3) + b = currentCastleRotL8(b, 3) + b ^= 51 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot30(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 1) + b = currentCastleRotL8(b, 4) + b ^= 172 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot32(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 3) + b += 213 + b ^= 45 + b += 225 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot33(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b = currentCastleRotL8(b, 7) + b = currentCastleRotL8(b, 1) + b += 48 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot34(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 113) + b ^= 57 + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleLowerFloatSlot35(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 243) + b += 173 + return byte(uint16(b) * 111) + }) +} + +func encodeCurrentCastleLowerFloatSlot36(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 3) + b += 152 + return byte(uint16(b) * 41) + }) +} + +func encodeCurrentCastleLowerFloatSlot37(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 87 + b ^= 113 + b = byte(uint16(b) * 137) + b += 216 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot38(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b = currentCastleRotL8(b, 5) + b += 51 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot39(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 115) + b = currentCastleRotL8(b, 6) + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot43(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b = byte(uint16(b) * 21) + b ^= 171 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot45(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 203 + b ^= 29 + b += 175 + return byte(uint16(b) * 111) + }) +} + +func encodeCurrentCastleLowerFloatSlot48(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 251) + b = byte(uint16(b) * 97) + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot50(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 250 + b ^= 171 + return byte(uint16(b) * 137) + }) +} + +func encodeCurrentCastleLowerFloatSlot51(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 84 + b = byte(uint16(b) * 59) + b = currentCastleRotL8(b, 3) + b ^= 251 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot52(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 123) + b = currentCastleRotL8(b, 3) + return currentCastleRotL8(b, 5) + }) +} + +func encodeCurrentCastleLowerFloatSlot54(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 106 + b ^= 142 + b = byte(uint16(b) * 207) + return currentCastleRotL8(b, 4) + }) +} + +func encodeCurrentCastleLowerFloatSlot55(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b = byte(uint16(b) * 11) + return currentCastleRotL8(b, 4) + }) +} + +func encodeCurrentCastleLowerFloatSlot57(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 72 + b = currentCastleRotL8(b, 1) + return byte(uint16(b) * 163) + }) +} + +func encodeCurrentCastleLowerFloatSlot58(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 63) + b = currentCastleRotL8(b, 3) + b ^= 92 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot59(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 14 + b += 79 + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot61(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 243 + b = byte(uint16(b) * 39) + return currentCastleRotL8(b, 4) + }) +} + +func encodeCurrentCastleLowerFloatSlot62(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 221 + b += 214 + return byte(uint16(b) * 47) + }) +} + +func encodeCurrentCastleLowerFloatSlot63(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 178 + b ^= 223 + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot65(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 109) + b = byte(uint16(b) * 17) + b ^= 210 + b ^= 81 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot66(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 229 + b += 213 + return byte(uint16(b) * 27) + }) +} + +func encodeCurrentCastleLowerFloatSlot69(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 11) + b += 250 + b = byte(uint16(b) * 85) + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleLowerFloatSlot70(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b += 144 + b ^= 112 + b ^= 41 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot74(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 177) + b += 195 + b ^= 92 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot76(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b ^= 230 + b += 167 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot80(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 48 + b = currentCastleRotL8(b, 2) + b += 80 + return byte(uint16(b) * 239) + }) +} + +func encodeCurrentCastleLowerFloatSlot81(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 249) + b = currentCastleRotL8(b, 6) + b ^= 178 + b ^= 139 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot82(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b = byte(uint16(b) * 59) + b ^= 115 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot84(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b = byte(uint16(b) * 75) + b ^= 239 + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleLowerFloatSlot85(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 14 + b ^= 182 + b = currentCastleRotL8(b, 3) + return byte(uint16(b) * 111) + }) +} + +func encodeCurrentCastleLowerFloatSlot86(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 29) + b = currentCastleRotL8(b, 2) + b += 143 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot88(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 215 + b ^= 73 + b = currentCastleRotL8(b, 7) + b += 115 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot91(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 203) + b += 81 + return currentCastleRotL8(b, 4) + }) +} + +func encodeCurrentCastleLowerFloatSlot92(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 69) + b ^= 38 + b += 139 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot93(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 174 + b = currentCastleRotL8(b, 6) + b = currentCastleRotL8(b, 6) + return currentCastleRotL8(b, 5) + }) +} + +func encodeCurrentCastleLowerFloatSlot94(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 222 + b ^= 114 + b ^= 81 + b += 90 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot95(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 94 + b += 229 + return byte(uint16(b) * 73) + }) +} + +func encodeCurrentCastleLowerFloatSlot96(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 1) + b = byte(uint16(b) * 15) + b ^= 28 + return byte(uint16(b) * 81) + }) +} + +func encodeCurrentCastleLowerFloatSlot97(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 123 + b += 78 + b ^= 219 + return byte(uint16(b) * 121) + }) +} + +func encodeCurrentCastleLowerFloatSlot101(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 76 + b = byte(uint16(b) * 109) + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleLowerFloatSlot105(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 229) + b = currentCastleRotL8(b, 5) + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleLowerFloatSlot107(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 233) + b = byte(uint16(b) * 97) + b ^= 58 + b ^= 22 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot110(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 33 + b += 241 + return byte(uint16(b) * 69) + }) +} + +func encodeCurrentCastleLowerFloatSlot111(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 227) + b += 224 + return currentCastleRotL8(b, 2) + }) +} + +func encodeCurrentCastleLowerFloatSlot112(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 63) + b = byte(uint16(b) * 85) + b = byte(uint16(b) * 3) + return byte(uint16(b) * 233) + }) +} + +func encodeCurrentCastleLowerFloatSlot114(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 141) + b = currentCastleRotL8(b, 6) + b ^= 105 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot117(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 69) + b = currentCastleRotL8(b, 4) + b ^= 3 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot118(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 209 + b += 207 + b ^= 221 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot122(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 4 + b += 22 + return currentCastleRotL8(b, 5) + }) +} + +func encodeCurrentCastleLowerFloatSlot124(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b = byte(uint16(b) * 47) + b ^= 32 + b ^= 40 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot125(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 1) + b += 141 + b += 49 + b ^= 148 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot126(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 149) + b ^= 55 + b = byte(uint16(b) * 25) + return byte(uint16(b) * 115) + }) +} + +func encodeCurrentCastleLowerFloatSlot129(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 175 + b += 72 + return byte(uint16(b) * 225) + }) +} + +func encodeCurrentCastleLowerFloatSlot131(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 3) + b ^= 116 + b = currentCastleRotL8(b, 3) + b ^= 159 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot132(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 89) + b += 253 + b ^= 166 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot134(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 111) + b = byte(uint16(b) * 209) + b ^= 197 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot135(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 247 + b = byte(uint16(b) * 57) + return currentCastleRotL8(b, 5) + }) +} + +func encodeCurrentCastleLowerFloatSlot136(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 205 + b = currentCastleRotL8(b, 5) + b += 74 + b += 130 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot138(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 151 + b ^= 160 + b = byte(uint16(b) * 83) + b ^= 182 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot139(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 165) + b = byte(uint16(b) * 203) + b ^= 56 + b += 87 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot140(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 99) + b = byte(uint16(b) * 239) + b ^= 72 + return byte(uint16(b) * 87) + }) +} + +func encodeCurrentCastleLowerFloatSlot141(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b = byte(uint16(b) * 195) + b ^= 149 + b ^= 197 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot142(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 87 + b = currentCastleRotL8(b, 4) + b = currentCastleRotL8(b, 4) + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleLowerFloatSlot143(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b = byte(uint16(b) * 125) + b ^= 31 + b += 42 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot144(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b = byte(uint16(b) * 75) + b += 83 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot145(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 42 + b = currentCastleRotL8(b, 2) + b = currentCastleRotL8(b, 5) + b += 146 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot146(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 207) + b ^= 207 + return byte(uint16(b) * 193) + }) +} + +func encodeCurrentCastleLowerFloatSlot148(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 1) + b += 78 + b ^= 38 + return byte(uint16(b) * 167) + }) +} + +func encodeCurrentCastleLowerFloatSlot151(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 211) + b = currentCastleRotL8(b, 5) + b ^= 234 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot153(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 7) + b ^= 79 + b = currentCastleRotL8(b, 1) + b ^= 230 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot155(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 103) + b = currentCastleRotL8(b, 3) + b ^= 82 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot157(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b ^= 144 + b ^= 38 + b += 99 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot158(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 57 + b = byte(uint16(b) * 205) + b += 48 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot160(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 12 + b = currentCastleRotL8(b, 4) + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleLowerFloatSlot161(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 145 + b = currentCastleRotL8(b, 1) + return currentCastleRotL8(b, 5) + }) +} + +func encodeCurrentCastleLowerFloatSlot162(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b = byte(uint16(b) * 115) + b ^= 55 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot163(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b = currentCastleRotL8(b, 5) + b = byte(uint16(b) * 81) + b ^= 106 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot164(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 1) + b += 153 + b = byte(uint16(b) * 73) + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot165(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 113) + b += 232 + b ^= 33 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot166(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b ^= 210 + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleLowerFloatSlot167(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 121) + b = byte(uint16(b) * 33) + b += 13 + b += 50 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot169(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 139) + b = currentCastleRotL8(b, 3) + b += 138 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot170(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b += 249 + b += 200 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot171(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 78 + b += 149 + return byte(uint16(b) * 205) + }) +} + +func encodeCurrentCastleLowerFloatSlot178(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 187 + b = byte(uint16(b) * 83) + b ^= 87 + return byte(uint16(b) * 145) + }) +} + +func encodeCurrentCastleLowerFloatSlot179(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 71) + b += 123 + b += 99 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot181(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b = currentCastleRotL8(b, 5) + b = byte(uint16(b) * 153) + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot185(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 41 + b = byte(uint16(b) * 207) + b = currentCastleRotL8(b, 2) + b ^= 86 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot186(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 129) + b = currentCastleRotL8(b, 6) + b = byte(uint16(b) * 147) + return currentCastleRotL8(b, 5) + }) +} + +func encodeCurrentCastleLowerFloatSlot187(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b += 53 + b ^= 133 + b += 236 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot189(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 91) + b = currentCastleRotL8(b, 6) + b += 183 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot190(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 66 + b += 41 + b ^= 35 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot191(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 41 + b = currentCastleRotL8(b, 6) + b ^= 212 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot192(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 177) + b = byte(uint16(b) * 27) + b = currentCastleRotL8(b, 3) + b ^= 3 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot193(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 203) + b = currentCastleRotL8(b, 7) + b ^= 126 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot195(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 43 + b ^= 237 + return byte(uint16(b) * 63) + }) +} + +func encodeCurrentCastleLowerFloatSlot197(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 150 + b += 6 + return byte(uint16(b) * 29) + }) +} + +func encodeCurrentCastleLowerFloatSlot199(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 101 + b = currentCastleRotL8(b, 7) + b += 117 + return currentCastleRotL8(b, 2) + }) +} + +func encodeCurrentCastleLowerFloatSlot200(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 30 + b = currentCastleRotL8(b, 2) + b = currentCastleRotL8(b, 6) + return byte(uint16(b) * 115) + }) +} + +func encodeCurrentCastleLowerFloatSlot201(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 3) + b = currentCastleRotL8(b, 1) + b ^= 212 + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleLowerFloatSlot202(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 3 + b = currentCastleRotL8(b, 2) + b ^= 180 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot208(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 162 + b ^= 221 + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot210(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 133) + b = byte(uint16(b) * 111) + return byte(uint16(b) * 59) + }) +} + +func encodeCurrentCastleLowerFloatSlot211(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 160 + b = currentCastleRotL8(b, 6) + return currentCastleRotL8(b, 7) + }) +} + +func encodeCurrentCastleLowerFloatSlot215(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b = byte(uint16(b) * 5) + b = byte(uint16(b) * 193) + b ^= 105 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot216(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 89 + b = currentCastleRotL8(b, 1) + b = byte(uint16(b) * 87) + return byte(uint16(b) * 255) + }) +} + +func encodeCurrentCastleLowerFloatSlot218(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 173 + b ^= 224 + b += 143 + return byte(uint16(b) * 5) + }) +} + +func encodeCurrentCastleLowerFloatSlot219(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 232 + b ^= 173 + b = byte(uint16(b) * 173) + b += 18 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot222(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 159) + b = byte(uint16(b) * 19) + b += 207 + b ^= 44 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot225(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 249) + b = currentCastleRotL8(b, 3) + b += 150 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot226(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 217) + b = currentCastleRotL8(b, 6) + b ^= 58 + return currentCastleRotL8(b, 4) + }) +} + +func encodeCurrentCastleLowerFloatSlot227(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b ^= 244 + b = currentCastleRotL8(b, 2) + b += 169 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot229(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b = currentCastleRotL8(b, 3) + b = byte(uint16(b) * 199) + return byte(uint16(b) * 75) + }) +} + +func encodeCurrentCastleLowerFloatSlot230(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 25 + b ^= 255 + b += 54 + return byte(uint16(b) * 161) + }) +} + +func encodeCurrentCastleLowerFloatSlot232(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 127) + b = currentCastleRotL8(b, 4) + return byte(uint16(b) * 229) + }) +} + +func encodeCurrentCastleLowerFloatSlot233(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 129) + b = currentCastleRotL8(b, 5) + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleLowerFloatSlot234(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 25 + b += 147 + b ^= 86 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot236(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 132 + b += 58 + b = byte(uint16(b) * 33) + return byte(uint16(b) * 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot238(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 17 + b = currentCastleRotL8(b, 6) + b ^= 92 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot241(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 219 + b = currentCastleRotL8(b, 7) + b += 166 + b ^= 60 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot242(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b = byte(uint16(b) * 199) + b ^= 46 + b ^= 241 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot243(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 1 + b = byte(uint16(b) * 179) + b ^= 215 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot244(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 71) + b = currentCastleRotL8(b, 3) + b ^= 50 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot248(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 93) + b = currentCastleRotL8(b, 2) + b ^= 183 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot249(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b = byte(uint16(b) * 181) + b ^= 146 + b ^= 126 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot250(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b = currentCastleRotL8(b, 5) + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot251(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 1) + b = byte(uint16(b) * 191) + b += 10 + b += 54 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot252(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 158 + b ^= 101 + b += 127 + b ^= 83 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot253(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 101) + b += 40 + return byte(uint16(b) * 239) + }) +} + +func encodeCurrentCastleLowerFloatSlot255(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 91 + b ^= 143 + b += 94 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot259(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 145 + b ^= 133 + b ^= 110 + b += 19 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot262(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b = byte(uint16(b) * 207) + return byte(uint16(b) * 25) + }) +} + +func encodeCurrentCastleLowerFloatSlot263(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 199 + b = currentCastleRotL8(b, 5) + return currentCastleRotL8(b, 7) + }) +} + +func encodeCurrentCastleLowerFloatSlot264(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 21 + b = byte(uint16(b) * 161) + b ^= 59 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot265(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 252 + b = byte(uint16(b) * 21) + return byte(uint16(b) * 191) + }) +} + +func encodeCurrentCastleLowerFloatSlot268(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 157) + b = byte(uint16(b) * 247) + b = currentCastleRotL8(b, 4) + return byte(uint16(b) * 149) + }) +} + +func encodeCurrentCastleLowerFloatSlot269(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b += 65 + b ^= 148 + b ^= 102 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot270(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b = byte(uint16(b) * 125) + b += 245 + b ^= 159 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot271(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 117 + b += 71 + b += 25 + b ^= 200 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot272(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 51 + b = currentCastleRotL8(b, 3) + b ^= 165 + b ^= 228 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot275(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b = byte(uint16(b) * 181) + b += 161 + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot276(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b = currentCastleRotL8(b, 6) + b ^= 78 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot278(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 1) + b ^= 154 + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot280(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 3) + b += 133 + b += 187 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot282(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 3) + b = currentCastleRotL8(b, 5) + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleLowerFloatSlot283(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 193 + b = currentCastleRotL8(b, 4) + b += 80 + b ^= 85 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot284(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 207) + b = byte(uint16(b) * 149) + b = currentCastleRotL8(b, 5) + return byte(uint16(b) * 39) + }) +} + +func encodeCurrentCastleLowerFloatSlot285(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 219) + b += 238 + b ^= 14 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot286(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b = currentCastleRotL8(b, 6) + b ^= 219 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot287(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 49 + b += 28 + b += 47 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot291(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b = byte(uint16(b) * 229) + b = currentCastleRotL8(b, 7) + b ^= 134 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot294(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 109) + b = byte(uint16(b) * 25) + b ^= 52 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot295(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 179) + b = currentCastleRotL8(b, 1) + return byte(uint16(b) * 191) + }) +} + +func encodeCurrentCastleLowerFloatSlot296(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 141) + b = currentCastleRotL8(b, 2) + b += 84 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot299(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 245 + b = byte(uint16(b) * 243) + return byte(uint16(b) * 109) + }) +} + +func encodeCurrentCastleLowerFloatSlot300(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 37) + b = byte(uint16(b) * 87) + b += 174 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot301(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b = byte(uint16(b) * 43) + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleLowerFloatSlot303(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b = byte(uint16(b) * 107) + b = byte(uint16(b) * 179) + b += 248 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot305(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 160 + b += 202 + b += 29 + b ^= 243 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot306(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b += 131 + b ^= 221 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot307(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 3) + b = currentCastleRotL8(b, 2) + b ^= 194 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot309(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 251 + b = byte(uint16(b) * 81) + b ^= 22 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot310(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 51) + b = currentCastleRotL8(b, 4) + b += 226 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot313(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 151) + b = byte(uint16(b) * 71) + b ^= 243 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot314(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b = currentCastleRotL8(b, 1) + b ^= 77 + b ^= 163 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot315(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b = currentCastleRotL8(b, 3) + b += 240 + b += 186 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot317(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 96 + b = currentCastleRotL8(b, 7) + b = currentCastleRotL8(b, 7) + b += 126 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot319(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 173) + b ^= 52 + b = byte(uint16(b) * 189) + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleLowerFloatSlot320(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 138 + b = byte(uint16(b) * 143) + b += 46 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot322(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 237) + b = byte(uint16(b) * 203) + b = byte(uint16(b) * 17) + b += 249 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot323(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b = byte(uint16(b) * 31) + return byte(uint16(b) * 163) + }) +} + +func encodeCurrentCastleLowerFloatSlot324(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 20 + b = currentCastleRotL8(b, 2) + b ^= 110 + b += 28 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot325(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 15 + b ^= 6 + b += 17 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot327(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b = currentCastleRotL8(b, 1) + return byte(uint16(b) * 53) + }) +} + +func encodeCurrentCastleLowerFloatSlot331(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 23) + b += 235 + b = byte(uint16(b) * 211) + b += 29 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot332(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 231 + b += 175 + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleLowerFloatSlot333(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 136 + b = byte(uint16(b) * 91) + b ^= 213 + b ^= 205 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot335(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b = currentCastleRotL8(b, 1) + b += 136 + b += 79 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot336(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 129) + b = byte(uint16(b) * 133) + b ^= 60 + return byte(uint16(b) * 229) + }) +} + +func encodeCurrentCastleLowerFloatSlot337(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 79 + b = byte(uint16(b) * 139) + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot338(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 97) + b = currentCastleRotL8(b, 6) + b = byte(uint16(b) * 139) + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleLowerFloatSlot340(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 186 + b += 193 + return currentCastleRotL8(b, 7) + }) +} + +func encodeCurrentCastleLowerFloatSlot343(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 255) + b += 96 + b ^= 170 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot345(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 158 + b ^= 96 + b = byte(uint16(b) * 161) + b ^= 179 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot346(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 80 + b = currentCastleRotL8(b, 4) + return byte(uint16(b) * 55) + }) +} + +func encodeCurrentCastleLowerFloatSlot347(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 11) + b += 91 + return byte(uint16(b) * 215) + }) +} + +func encodeCurrentCastleLowerFloatSlot353(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 159) + b += 188 + return byte(uint16(b) * 119) + }) +} + +func encodeCurrentCastleLowerFloatSlot355(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 163 + b = currentCastleRotL8(b, 5) + b ^= 215 + b ^= 189 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot357(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 31) + b = byte(uint16(b) * 153) + b ^= 78 + b += 147 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot358(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 26 + b ^= 187 + return byte(uint16(b) * 215) + }) +} + +func encodeCurrentCastleLowerFloatSlot360(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 137) + b += 245 + return byte(uint16(b) * 143) + }) +} + +func encodeCurrentCastleLowerFloatSlot361(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 160 + b = currentCastleRotL8(b, 7) + b = byte(uint16(b) * 131) + return byte(uint16(b) * 35) + }) +} + +func encodeCurrentCastleLowerFloatSlot362(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 169 + b = currentCastleRotL8(b, 6) + b = byte(uint16(b) * 27) + b ^= 151 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot363(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 107) + b = currentCastleRotL8(b, 5) + b ^= 21 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot364(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 23) + b = byte(uint16(b) * 195) + return currentCastleRotL8(b, 2) + }) +} + +func encodeCurrentCastleLowerFloatSlot365(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b ^= 74 + return byte(uint16(b) * 115) + }) +} + +func encodeCurrentCastleLowerFloatSlot367(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 83 + b = byte(uint16(b) * 31) + b = currentCastleRotL8(b, 6) + b ^= 231 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot368(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b = currentCastleRotL8(b, 4) + return byte(uint16(b) * 97) + }) +} + +func encodeCurrentCastleLowerFloatSlot370(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 173 + b ^= 250 + b = currentCastleRotL8(b, 3) + b ^= 27 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot371(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 37) + b += 133 + b ^= 43 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot372(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 1) + b = byte(uint16(b) * 37) + b ^= 55 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot373(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 244 + b += 152 + return byte(uint16(b) * 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot377(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b ^= 4 + b += 114 + b += 130 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot379(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 107) + b ^= 139 + b += 196 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot380(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 181 + b += 115 + b ^= 57 + b += 171 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot381(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 95) + b = byte(uint16(b) * 249) + return currentCastleRotL8(b, 7) + }) +} + +func encodeCurrentCastleLowerFloatSlot382(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b = byte(uint16(b) * 33) + b += 71 + b ^= 6 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot384(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 52 + b += 254 + b += 51 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot385(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 233) + b = currentCastleRotL8(b, 3) + b = currentCastleRotL8(b, 4) + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleLowerFloatSlot387(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 225) + b = byte(uint16(b) * 65) + b += 208 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot389(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b ^= 144 + return currentCastleRotL8(b, 7) + }) +} + +func encodeCurrentCastleLowerFloatSlot391(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b = currentCastleRotL8(b, 1) + b = currentCastleRotL8(b, 7) + return currentCastleRotL8(b, 2) + }) +} + +func encodeCurrentCastleLowerFloatSlot392(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 35) + b = byte(uint16(b) * 205) + b = byte(uint16(b) * 105) + b += 145 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot396(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 162 + b += 249 + b += 171 + b ^= 29 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot397(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 112 + b ^= 131 + b += 38 + b ^= 110 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot398(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 68 + b = byte(uint16(b) * 133) + b += 39 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot401(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 89) + b ^= 92 + b = currentCastleRotL8(b, 6) + return currentCastleRotL8(b, 4) + }) +} + +func encodeCurrentCastleLowerFloatSlot402(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 7) + b ^= 218 + b += 151 + b ^= 110 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot403(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 113 + b = byte(uint16(b) * 231) + b ^= 241 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot404(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 251) + b += 93 + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleLowerFloatSlot405(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 228 + b = currentCastleRotL8(b, 3) + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleLowerFloatSlot407(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 157) + b ^= 152 + b ^= 46 + return currentCastleRotL8(b, 2) + }) +} + +func encodeCurrentCastleLowerFloatSlot408(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 107) + b = currentCastleRotL8(b, 1) + b += 35 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot409(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 202 + b = currentCastleRotL8(b, 1) + return currentCastleRotL8(b, 3) + }) +} + +func encodeCurrentCastleLowerFloatSlot410(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 96 + b = currentCastleRotL8(b, 5) + b = currentCastleRotL8(b, 7) + b ^= 94 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot411(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 199 + b += 9 + b ^= 29 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot412(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b ^= 217 + b = currentCastleRotL8(b, 1) + b ^= 83 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot413(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b += 238 + b += 192 + return currentCastleRotL8(b, 7) + }) +} + +func encodeCurrentCastleLowerFloatSlot414(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 148 + b ^= 145 + b += 118 + b += 53 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot417(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b ^= 134 + b += 40 + b = byte(uint16(b) * 19) + b += 138 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot418(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b += 47 + b += 187 + return currentCastleRotL8(b, 1) + }) +} + +func encodeCurrentCastleLowerFloatSlot419(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 1) + b += 92 + b += 6 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot420(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 107) + b = byte(uint16(b) * 255) + return currentCastleRotL8(b, 5) + }) +} + +func encodeCurrentCastleLowerFloatSlot426(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b += 188 + b = byte(uint16(b) * 81) + b += 151 + return b + }) +} + +func encodeCurrentCastleLowerFloatSlot427(value float64) string { + return encodeCurrentCastleFloat64WithTransform(value, func(b byte) byte { + b = byte(uint16(b) * 135) + b = byte(uint16(b) * 195) + b += 135 + return b + }) +} + +func currentCastleIndexR7(value uint32) uint32 { + value = bits.RotateLeft32(value, -16) + value -= 762 + value = bits.RotateLeft32(value, 3) + value -= 836 + value += 144 + return value +} + +func currentCastleIndexEUnderscore(value uint32) uint32 { + value -= 144 + value += 836 + value = bits.RotateLeft32(value, -3) + value += 762 + value = bits.RotateLeft32(value, 16) + return value +} + +func currentCastleIndexTG(value uint32) uint32 { + value -= 956 + value -= 24 + value += 42 + value += 41 + value += 478 + value -= 433 + return value * 0x70586723 +} + +func currentCastleTB(value uint32) uint32 { + value = value*139 + 433 + value -= 478 + value -= 41 + value -= 42 + value += 24 + value += 956 + return value +} + +func encodeCurrentCastleNumericSlot20(value uint32) uint32 { + value = currentCastleIndexEUnderscore(value) + return (0x9f5593c9^value)*0x5d546885 + 0x312038dd +} + +func encodeCurrentCastleNumericSlot154(value uint32) uint32 { + value = currentCastleTB(value) + return (0x3cf29b3d^value)*0x5bc9f085 + 0x786d9417 +} + +func encodeCurrentCastleNumericSlot256(value uint32) uint32 { + return (0x40ad2308^value)*0x6b0c1905 + 0xbb6d942e +} + +func encodeCurrentCastleNumericSlot348(value uint32) uint32 { + return (0x00076853^value)*0x0438488b + 0x1ca480fc +} + +func currentCastleNumericAffineEncoder(xorKey, multiplier, addend uint32) func(uint32) uint32 { + return func(value uint32) uint32 { + return (xorKey^value)*multiplier + addend + } +} + +func encodeCurrentCastleNumericIdentity(value uint32) uint32 { + return value +} + +func encodeCurrentCastleNumericSlot341(value uint32) uint32 { + if value != 0 { + return currentCastleIndexR7(0x12780000) + } + return currentCastleIndexTG(8777729) +} + +func encodeCurrentCastleNumericSlot428(value uint32) uint32 { + value = currentCastleTB(value) + return (0xd57437b0^value)*0x62d22c71 + 0x0e965777 +} + +func encodeCurrentCastleNumericSlot429(value uint32) uint32 { + return (0xb280aa5f^value)*0x23ad12b7 + currentCastleIndexR7(0x95eb36d0) +} + +func currentCastleHighTimingSlotIndexes() []int { + return []int{ + 431, 432, 433, 434, 435, + int(currentCastleIndexTG(61456)), + 437, 438, + int(currentCastleIndexTG(61873)), + 440, + int(currentCastleIndexTG(62151)), + int(currentCastleIndexR7(0x0387c000)), + 443, + int(currentCastleIndexR7(0x03880000)), + 445, 446, 447, 448, 449, + int(currentCastleIndexTG(63402)), + 451, 452, 453, + int(currentCastleIndexR7(0x03894000)), + 455, 456, 457, 458, 459, + int(currentCastleIndexR7(0x038a0000)), + 461, 462, 463, + int(currentCastleIndexTG(65348)), + 465, 466, 467, 468, + int(currentCastleIndexR7(0x038b2000)), + 470, 471, + int(currentCastleIndexTG(66460)), + 473, 474, 475, 476, + int(currentCastleIndexTG(67155)), + 478, 479, 480, 481, 482, + int(currentCastleIndexTG(67989)), + int(currentCastleIndexTG(68128)), + 485, + int(currentCastleIndexR7(0x038d4000)), + int(currentCastleIndexR7(0x038d6000)), + int(currentCastleIndexR7(0x038d8000)), + int(currentCastleIndexR7(0x038da000)), + 490, 491, 492, + int(currentCastleIndexR7(0x038e2000)), + } +} + +func populateCurrentCastleHighTimingSlots(payload []any, values map[int]float64) error { + if len(payload) < currentCastlePayloadSlots { + return fmt.Errorf("current Castle payload has %d slots, want at least %d", len(payload), currentCastlePayloadSlots) + } + defaults := defaultCurrentCastleHighTimingSlotEncodedValues() + for _, slot := range currentCastleHighTimingSlotIndexes() { + if value, ok := values[slot]; ok { + payload[slot] = encodeCurrentCastleFloat64(value) + } else if encoded, ok := defaults[slot]; ok { + payload[slot] = encoded + } else { + payload[slot] = encodeCurrentCastleFloat64(0) + } + } + return nil +} + +func populateCurrentCastleObservedStringSlots(payload []any, input currentCastlePayloadInput) { + for slot, encoded := range defaultCurrentCastleObservedStringSlotEncodedValues() { + if currentCastleInputHasStringSlotOverride(input, slot) { + continue + } + payload[slot] = encoded + } +} + +func currentCastleInputHasStringSlotOverride(input currentCastlePayloadInput, slot int) bool { + if _, ok := input.LowerFloatValues[slot]; ok { + return true + } + if _, ok := input.ArrayValues[slot]; ok { + return true + } + if _, ok := input.PackedStringValues[slot]; ok { + return true + } + if _, ok := input.UnitPackedStrings[slot]; ok { + return true + } + if _, ok := input.StringValues[slot]; ok { + return true + } + if _, ok := input.HighTimingValues[slot]; ok { + return true + } + return false +} + +func defaultCurrentCastleObservedStringSlotEncodedValues() map[int]string { + return map[int]string{ + // Current Chrome begin_login string payload defaults observed from the + // native Windows profile. These cover lower browser-probe string slots, + // one compact array slot, and high-timing slots that are not stable zeroes. + 25: "NoAtnP9LeAQ=", + 34: "vXZOTk5OTk4=", + 48: "BukFAAAAAAA=", + 60: "NoAtnP9LeAQ=", + 75: "NoAtnP9LeAQ=", + 78: "3Nzc3Nzc3Nw=", + 79: "PT09PT09PT0=", + 83: "5bKenp6enp4=", + 86: "kE2Rj4+Pj48=", + 91: "ZBoVFRUVFRU=", + 97: "gfKCgoKCgoI=", + 110: "Gj062tra2to=", + 113: "NoAtnP9LeAQ=", + 120: "z8/Pz8/Pz88=", + 122: "K02P7J1heMI=", + 135: "D+3r//////8=", + 137: "NoAtnP9LeAQ=", + 148: "Jk/Y2NjY2Ng=", + 185: "VlZWVlZWVlY=", + 187: "PRScnJycnJw=", + 190: "iPkkFBQUFBs=", + 191: "1NTU1NTU1NQ=", + 204: "z8/Pz8/Pz88=", + 208: "4XnOkU1ytBo=", + 215: "DbSN7GNpaWk=", + 218: "DDxMTExMTEw=", + 222: "bh7W/YRTetY=", + 239: "NoAtnP9LeAQ=", + 242: "ThnLgCnt7EY=", + 243: "F4LS0tLS0tI=", + 249: "vDuC8vLy8rI=", + 262: "uN6u6urq6uo=", + 283: "NWYCT+EYstQ=", + 303: "tCv4+Pj4+Pg=", + 314: "/s0XIiIiIiI=", + 315: "pusz3/VxUJ0=", + 327: "jerv8g6b8dM=", + 328: "NoAtnP9LeAQ=", + 332: "daGlpaWlpaU=", + 345: "bv3T2KP8NIk=", + 350: "XFyWlpaWlpY=", + 353: "pCkEZGRkZGQ=", + 370: "qyChoaGhoaE=", + 387: "78DQ0NDQ0NA=", + 392: "cpmRkZGRkZE=", + 394: "zESL", + 396: "mIu1jgUq8l8=", + 397: "PGd3d3d3d3c=", + 407: "VJqyPvHtTOg=", + 411: "Ej0vAL/cZNE=", + 436: "OT8fXsbGxsY=", + 442: "hinGxsbGxsY=", + 443: "OT8f3MbGxsY=", + 445: "xsbGxsbGxsY=", + 447: "OT8f3MbGxsY=", + 449: "OT8f3MbGxsY=", + 456: "OaAgIIbGxsY=", + 459: "OT8f3MbGxsY=", + 460: "xsbGxsbGxsY=", + 462: "xsbGxsbGxsY=", + 466: "xsbGxsbGxsY=", + 468: "OT8fXsbGxsY=", + 476: "hpMfH17GxsY=", + 478: "OT8fXsbGxsY=", + 479: "Oc8f3MbGxsY=", + 480: "huGKCgoGxsY=", + 481: "hmt1NTWGxsY=", + 482: "hqqwICBGxsY=", + 483: "huVGxsbGxsY=", + 484: "huUPHx9GxsY=", + 485: "hqLFNTWGxsY=", + 486: "hqJPHx8GxsY=", + 487: "hlOfHx9GxsY=", + 488: "hpL/Hx9GxsY=", + 489: "OT8f3MbGxsY=", + 490: "hlM/Hx9GxsY=", + 491: "OVU1NcbGxsY=", + 492: "hhhKCsvGxsY=", + 493: "huWaCsvGxsY=", + } +} + +func defaultCurrentCastleHighTimingSlotEncodedValues() map[int]string { + return map[int]string{ + // Current Chrome begin_login high-timing defaults observed on this + // Windows profile. Other high-timing slots encode zero by default. + 442: "hoZgICDGxsY=", + 443: "OVU1NcbGxsY=", + 445: "OT8f3MbGxsY=", + 449: "OT8fXsbGxsY=", + 456: "OWU1NYbGxsY=", + 460: "OT8f3MbGxsY=", + 462: "OT8f3MbGxsY=", + 466: "OT8f3MbGxsY=", + 476: "hmQKCgrGxsY=", + 478: "OT8f3MbGxsY=", + 479: "OT8f3MbGxsY=", + 480: "hnMPHx9mxsY=", + 481: "hn8XHx9GxsY=", + 482: "hn/tNTWGxsY=", + 483: "hrZXHx9GxsY=", + 484: "hrZfHx9GxsY=", + 485: "hrbXHx9mxsY=", + 486: "hndAICBGxsY=", + 487: "hu1gICBGxsY=", + 488: "hu0VNTWGxsY=", + 489: "Oc8f3MbGxsY=", + 490: "hu1qCgoGxsY=", + 491: "Oc8f3MbGxsY=", + 492: "hiz6CgoGxsY=", + 493: "hrYuxsbGxsY=", + } +} + +func xorCurrentCastleString(value, key string) string { + valueUnits := jsUTF16Units(value) + keyUnits := jsUTF16Units(key) + if len(valueUnits) == 0 || len(keyUnits) == 0 { + return value + } + out := make([]uint16, len(valueUnits)) + for i, ch := range valueUnits { + out[i] = ch ^ keyUnits[i%len(keyUnits)] + } + return string(utf16.Decode(out)) +} + +func deflateCurrentCastleWireString(value string) ([]byte, error) { + var buf bytes.Buffer + writer, err := flate.NewWriter(&buf, flate.BestSpeed) + if err != nil { + return nil, err + } + if _, err = writer.Write([]byte(value)); err != nil { + _ = writer.Close() + return nil, err + } + if err = writer.Close(); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func currentCastleRotate16Into32(value uint16, leftShift, rightShift uint) uint32 { + return uint32(value)<>rightShift +} + +func bitswap32(value uint32) uint32 { + return value>>24 | value>>8&0xff00 | value<<8&0xff0000 | value<<24 +} + +func jsUTF16Units(value string) []uint16 { + return utf16.Encode([]rune(value)) +} diff --git a/pkg/twittermeow/client.go b/pkg/twittermeow/client.go index 54c13aad..fc2eec0f 100644 --- a/pkg/twittermeow/client.go +++ b/pkg/twittermeow/client.go @@ -3,8 +3,10 @@ package twittermeow import ( "context" "encoding/base64" + "errors" "fmt" "net/http" + "net/url" "regexp" "slices" "strconv" @@ -277,6 +279,50 @@ func (c *Client) fetchScript(ctx context.Context, url string) ([]byte, error) { return scriptRespBody, err } +func (c *Client) fetchCloudflareJSD(ctx context.Context, pageURL *url.URL, mainPageHTML string) error { + scriptURL := methods.ParseCloudflareJSDURL(mainPageHTML) + if scriptURL == "" { + return nil + } + parsedScriptURL, err := pageURL.Parse(scriptURL) + if err != nil { + return err + } + extraHeaders := map[string]string{ + "accept": "*/*", + "sec-fetch-dest": "script", + "sec-fetch-mode": "no-cors", + "sec-fetch-site": "same-origin", + } + originalCheckRedirect := c.HTTP.CheckRedirect + c.disableRedirects() + defer func() { + c.HTTP.CheckRedirect = originalCheckRedirect + }() + for i := 0; i < 4; i++ { + resp, _, err := c.MakeRequest(ctx, parsedScriptURL.String(), http.MethodGet, c.buildHeaders(HeaderOpts{ + Extra: extraHeaders, + Referer: pageURL.String(), + WithCookies: true, + }), nil, types.ContentTypeNone) + if resp != nil { + c.cookies.UpdateFromResponse(resp) + } + if !errors.Is(err, ErrRedirectAttempted) { + return err + } + location := resp.Header.Get("Location") + if location == "" { + return err + } + parsedScriptURL, err = parsedScriptURL.Parse(location) + if err != nil { + return err + } + } + return ErrMaxRetriesReached +} + func (c *Client) fetchAndParseMainScript(ctx context.Context, scriptURL string) string { scriptRespBody, err := c.fetchScript(ctx, scriptURL) if err != nil { @@ -381,6 +427,15 @@ func (c *Client) parseMainPageHTML(ctx context.Context, mainPageResp *http.Respo Msg("Found loading animations and verification token") } + for name, value := range methods.ParseDocumentCookieAssignments(mainPageHTML) { + c.cookies.Set(cookies.XCookieName(name), value) + } + if mainPageResp.Request != nil && mainPageResp.Request.URL != nil { + if err := c.fetchCloudflareJSD(ctx, mainPageResp.Request.URL, mainPageHTML); err != nil { + c.Logger.Debug().Err(err).Msg("Failed to fetch Cloudflare JSD bootstrap") + } + } + c.session.Country = country c.session.VerificationToken = verificationToken c.session.loadingAnims = loadingAnims diff --git a/pkg/twittermeow/cookies/cookies.go b/pkg/twittermeow/cookies/cookies.go index 7e195264..72b06385 100644 --- a/pkg/twittermeow/cookies/cookies.go +++ b/pkg/twittermeow/cookies/cookies.go @@ -21,6 +21,8 @@ const ( XLang XCookieName = "lang" XAtt XCookieName = "att" XPersonalizationID XCookieName = "personalization_id" + XDtabLocal XCookieName = "dtab_local" + XGuestIDAds XCookieName = "guest_id_ads" XGuestIDMarketing XCookieName = "guest_id_marketing" ) @@ -41,23 +43,33 @@ func NewCookies(store map[string]string) *Cookies { func NewCookiesFromString(cookieStr string) *Cookies { c := NewCookies(nil) - cookieStrings := strings.Split(cookieStr, ";") - fakeHeader := http.Header{} - for _, cookieStr := range cookieStrings { - trimmedCookieStr := strings.TrimSpace(cookieStr) - if trimmedCookieStr != "" { - fakeHeader.Add("Set-Cookie", trimmedCookieStr) - } - } - fakeResponse := &http.Response{Header: fakeHeader} - - for _, cookie := range fakeResponse.Cookies() { + for _, cookie := range parseCookiesFromString(cookieStr) { c.store[cookie.Name] = cookie.Value } return c } +func parseCookiesFromString(cookieStr string) []*http.Cookie { + parsedCookies, err := http.ParseCookie(cookieStr) + if err == nil { + return parsedCookies + } + setCookie, err := http.ParseSetCookie(cookieStr) + if err == nil { + return []*http.Cookie{setCookie} + } + cookieStrings := strings.Split(cookieStr, ";") + cookies := make([]*http.Cookie, 0, len(cookieStrings)) + for _, cookieStr := range cookieStrings { + cookie, err := http.ParseSetCookie(strings.TrimSpace(cookieStr)) + if err == nil { + cookies = append(cookies, cookie) + } + } + return cookies +} + func (c *Cookies) String() string { c.lock.RLock() defer c.lock.RUnlock() @@ -88,7 +100,7 @@ func (c *Cookies) UpdateFromResponse(r *http.Response) { c.lock.Lock() defer c.lock.Unlock() for _, cookie := range r.Cookies() { - if cookie.MaxAge == 0 || cookie.Expires.Before(time.Now()) { + if cookie.MaxAge < 0 || (!cookie.Expires.IsZero() && cookie.Expires.Before(time.Now())) { delete(c.store, cookie.Name) } else { //log.Println(fmt.Sprintf("updated cookie %s to value %s", cookie.Name, cookie.Value)) diff --git a/pkg/twittermeow/cookies/cookies_test.go b/pkg/twittermeow/cookies/cookies_test.go new file mode 100644 index 00000000..3a3c2a5d --- /dev/null +++ b/pkg/twittermeow/cookies/cookies_test.go @@ -0,0 +1,44 @@ +package cookies + +import ( + "net/http" + "testing" +) + +func TestNewCookiesFromStringParsesCookieHeader(t *testing.T) { + store := NewCookiesFromString("auth_token=auth-value; ct0=csrf-value") + + if got := store.Get(XAuthToken); got != "auth-value" { + t.Fatalf("auth_token = %q, want auth-value", got) + } + if got := store.Get(XCt0); got != "csrf-value" { + t.Fatalf("ct0 = %q, want csrf-value", got) + } +} + +func TestNewCookiesFromStringParsesSingleSetCookieHeader(t *testing.T) { + store := NewCookiesFromString("ct0=csrf-value; Path=/; Secure; HttpOnly") + + if got := store.Get(XCt0); got != "csrf-value" { + t.Fatalf("ct0 = %q, want csrf-value", got) + } + if got := store.Get(XCookieName("Path")); got != "" { + t.Fatalf("Path pseudo-cookie = %q, want omitted", got) + } +} + +func TestUpdateFromResponseKeepsSessionCookies(t *testing.T) { + store := NewCookies(map[string]string{"deleted": "old"}) + resp := &http.Response{Header: http.Header{}} + resp.Header.Add("Set-Cookie", "__cf_bm=session-value; Path=/; Secure; HttpOnly") + resp.Header.Add("Set-Cookie", "deleted=gone; Max-Age=0; Path=/") + + store.UpdateFromResponse(resp) + + if got := store.Get(XCookieName("__cf_bm")); got != "session-value" { + t.Fatalf("__cf_bm = %q, want session-value", got) + } + if got := store.Get(XCookieName("deleted")); got != "" { + t.Fatalf("deleted cookie still present: %q", got) + } +} diff --git a/pkg/twittermeow/data/endpoints/endpoints.go b/pkg/twittermeow/data/endpoints/endpoints.go index 87f33459..2ae1b32f 100644 --- a/pkg/twittermeow/data/endpoints/endpoints.go +++ b/pkg/twittermeow/data/endpoints/endpoints.go @@ -9,9 +9,12 @@ const ( BASE_HOST = "x.com" BASE_URL = "https://" + BASE_HOST BASE_LOGIN_URL = BASE_URL + "/login" + BASE_FLOW_LOGIN_URL = BASE_URL + "/i/flow/login" BASE_MESSAGES_URL = BASE_URL + "/messages" BASE_LOGOUT_URL = BASE_URL + "/logout" BASE_NOTIFICATION_SETTINGS_URL = BASE_URL + "/settings/push_notifications" + JETFUEL_BASE_URL = BASE_URL + "/i/jfapi" + JETFUEL_LOGIN_REFERER_URL = BASE_URL + "/i/jf/onboarding/web?mode=login" API_BASE_HOST = "api.x.com" API_BASE_URL = "https://" + API_BASE_HOST @@ -59,6 +62,17 @@ const ( JOT_CLIENT_EVENT_URL = API_BASE_URL + "/1.1/jot/client_event.json" JOT_CES_P2_URL = API_BASE_URL + "/1.1/jot/ces/p2" + VIEWER_CONTEXT_URL = API_BASE_URL + "/1.1/graphql/viewer_context.json" + + GUEST_ACTIVATE_URL = API_BASE_URL + "/1.1/guest/activate.json" + ONBOARDING_TASK_URL = API_BASE_URL + "/1.1/onboarding/task.json" + ONBOARDING_LOGIN_TASK_URL = ONBOARDING_TASK_URL + "?flow_name=login" + JETFUEL_LANDING_PATH = "/onboarding/web/landing" + JETFUEL_LOGIN_PATH = "/onboarding/web?mode=login" + JETFUEL_BEGIN_LOGIN_PATH = "/onboarding/web/actions/begin_login" + JETFUEL_LOGIN_ENTER_PASSWORD_PATH = "/onboarding/web/actions/login_enter_password" + JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH = "/onboarding/web/actions/begin_two_factor_auth" + JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH = "/onboarding/web/actions/finish_two_factor_auth" PIPELINE_EVENTS_URL = API_BASE_URL + "/live_pipeline/events" PIPELINE_UPDATE_URL = API_BASE_URL + "/1.1/live_pipeline/update_subscriptions" diff --git a/pkg/twittermeow/headers.go b/pkg/twittermeow/headers.go index 81cde678..503811ab 100644 --- a/pkg/twittermeow/headers.go +++ b/pkg/twittermeow/headers.go @@ -8,10 +8,10 @@ import ( ) const BrowserName = "Chrome" -const ChromeVersion = "141" -const UserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + ChromeVersion + ".0.0.0 Safari/537.36" -const SecCHUserAgent = `"Chromium";v="` + ChromeVersion + `", "Google Chrome";v="` + ChromeVersion + `", "Not-A.Brand";v="99"` -const OSName = "Linux" +const ChromeVersion = "149" +const UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + ChromeVersion + ".0.0.0 Safari/537.36" +const SecCHUserAgent = `"Google Chrome";v="` + ChromeVersion + `", "Chromium";v="` + ChromeVersion + `", "Not)A;Brand";v="24"` +const OSName = "Windows" const SecCHPlatform = `"` + OSName + `"` const SecCHMobile = "?0" @@ -19,7 +19,7 @@ const UDID = OSName + "/" + BrowserName var BaseHeaders = http.Header{ "Accept": []string{"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}, - "Accept-Language": []string{"en-US,en;q=0.9"}, + "Accept-Language": []string{"en"}, "User-Agent": []string{UserAgent}, "Sec-Ch-Ua": []string{SecCHUserAgent}, "Sec-Ch-Ua-Platform": []string{SecCHPlatform}, diff --git a/pkg/twittermeow/login_jetfuel.go b/pkg/twittermeow/login_jetfuel.go new file mode 100644 index 00000000..6a7cd0e2 --- /dev/null +++ b/pkg/twittermeow/login_jetfuel.go @@ -0,0 +1,1341 @@ +package twittermeow + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "os" + "regexp" + "slices" + "strings" + "time" + "unicode/utf8" + + "go.mau.fi/mautrix-twitter/pkg/twittermeow/cookies" + "go.mau.fi/mautrix-twitter/pkg/twittermeow/crypto" + "go.mau.fi/mautrix-twitter/pkg/twittermeow/data/endpoints" + "go.mau.fi/mautrix-twitter/pkg/twittermeow/data/types" +) + +const ( + webLoginBackendOCF webLoginBackend = "ocf" + webLoginBackendJetfuel webLoginBackend = "jetfuel" + + jetfuelHeaderVersion = "JP-5" +) + +var ( + jetfuelActionPathRegex = regexp.MustCompile(`/onboarding/web/actions/[A-Za-z0-9_./-]+`) + jetfuelFieldRegex = regexp.MustCompile(`^[A-Za-z_$][A-Za-z0-9_$-]{1,80}$`) + jetfuelNumericIDRegex = regexp.MustCompile(`\b[0-9]{5,30}\b`) + jetfuelUUIDRegex = regexp.MustCompile(`(?i)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) + jetfuelActionAliases = map[string]string{ + "begin_login": endpoints.JETFUEL_BEGIN_LOGIN_PATH, + "login_enter_password": endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH, + "begin_two_factor_auth": endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH, + "finish_two_factor_auth": endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH, + "two_factor_code": "/onboarding/web/actions/two_factor_code", + } +) + +type webLoginBackend string + +type jetfuelLoginState struct { + identifier string + passwordAction string + verificationAction string + verificationFields []string + twoFactorAction string + twoFactorMethods []WebLoginAuthMethod + sessionToken string + preludeDispatchID string + userID string +} + +type jetfuelLoginResponse struct { + strings []string + paths []string + fields []string + raw []byte +} + +func (wls *WebLoginSession) startJetfuel(ctx context.Context) (*WebLoginResult, error) { + if err := wls.client.loadPage(ctx, endpoints.BASE_FLOW_LOGIN_URL); err != nil { + return nil, fmt.Errorf("failed to load X login page: %w", err) + } + if _, err := wls.client.jetfuelGet(ctx, endpoints.JETFUEL_LANDING_PATH); err != nil { + wls.client.Logger.Debug().Err(err).Msg("Jetfuel login landing preflight failed") + } + body, err := wls.client.jetfuelGet(ctx, endpoints.JETFUEL_LOGIN_PATH) + if err != nil { + return nil, err + } + parsed := parseJetfuelLoginResponse(body) + if !parsed.hasPath(endpoints.JETFUEL_BEGIN_LOGIN_PATH) && !parsed.hasField("username_or_email") { + return nil, fmt.Errorf("%w: jetfuel login page did not expose a username action", ErrWebLoginUnexpectedSubtask) + } + wls.backend = webLoginBackendJetfuel + wls.jetfuel = &jetfuelLoginState{} + return &WebLoginResult{ + Status: WebLoginStatusNeedsIdentifier, + CurrentSubtaskID: "JetfuelBeginLogin", + Challenge: &WebLoginChallenge{ + SubtaskID: "JetfuelBeginLogin", + Hint: "Phone, email, or username", + }, + }, nil +} + +func (wls *WebLoginSession) submitJetfuelIdentifier(ctx context.Context, identifier string) (*WebLoginResult, error) { + identifier = strings.TrimSpace(identifier) + if identifier == "" { + return nil, fmt.Errorf("x username, email, or phone is required") + } + if err := wls.client.sendJetfuelViewerContextEvent(ctx); err != nil { + wls.client.Logger.Debug().Err(err).Msg("Jetfuel viewer-context preflight failed") + } + body, err := wls.client.jetfuelPostForm(ctx, endpoints.JETFUEL_BEGIN_LOGIN_PATH, url.Values{ + "username_or_email": {identifier}, + }) + if err != nil { + return nil, err + } + parsed := parseJetfuelLoginResponse(body) + if err := parsed.loginError(); err != nil { + return nil, err + } + wls.updateJetfuelState(parsed) + wls.jetfuel.identifier = identifier + if wls.client.IsLoggedIn() { + return &WebLoginResult{Status: WebLoginStatusComplete}, nil + } + if action := parsed.passwordAction(); action != "" { + wls.jetfuel.passwordAction = action + return &WebLoginResult{ + Status: WebLoginStatusNeedsPassword, + CurrentSubtaskID: "JetfuelPassword", + Challenge: &WebLoginChallenge{ + SubtaskID: "JetfuelPassword", + Hint: "Password", + }, + }, nil + } + if action := parsed.verificationAction(); action != "" { + wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = parsed.verificationCodeFields() + return &WebLoginResult{ + Status: WebLoginStatusNeedsText, + CurrentSubtaskID: "JetfuelVerification", + Challenge: parsed.verificationChallenge(), + }, nil + } + return nil, fmt.Errorf("%w: jetfuel identifier response did not expose a supported next action", ErrWebLoginUnexpectedSubtask) +} + +func (wls *WebLoginSession) submitJetfuelCredentials(ctx context.Context, identifier, password string) (*WebLoginResult, error) { + identifier = strings.TrimSpace(identifier) + if identifier == "" { + return nil, fmt.Errorf("x username, email, or phone is required") + } + if password == "" { + return nil, fmt.Errorf("x password is required") + } + result, err := wls.submitJetfuelIdentifier(ctx, identifier) + if err != nil { + if !isJetfuelPrePasswordParityError(err) { + return nil, err + } + wls.client.Logger.Debug().Err(err).Msg("Jetfuel sequential identifier submit failed, trying combined credentials submit") + return wls.submitJetfuelCombinedCredentials(ctx, identifier, password) + } + if result.Status != WebLoginStatusNeedsPassword { + return result, nil + } + return wls.submitJetfuelPassword(ctx, password) +} + +func isJetfuelPrePasswordParityError(err error) bool { + var webErr *WebLoginError + if !errors.As(err, &webErr) { + return false + } + text := strings.ToLower(webErr.Message) + return webErr.Code == 399 && (strings.Contains(text, "temporarily limited") || + strings.Contains(text, "official x apps") || strings.Contains(text, "use x.com")) +} + +func (wls *WebLoginSession) submitJetfuelCombinedCredentials(ctx context.Context, identifier, password string) (*WebLoginResult, error) { + body, err := wls.client.jetfuelPostForm(ctx, endpoints.JETFUEL_BEGIN_LOGIN_PATH, url.Values{ + "username_or_email": {identifier}, + "password": {password}, + }) + if err != nil { + return nil, err + } + parsed := parseJetfuelLoginResponse(body) + if err := parsed.loginError(); err != nil { + return nil, err + } + wls.updateJetfuelState(parsed) + if wls.jetfuel != nil { + wls.jetfuel.identifier = identifier + } + if wls.client.IsLoggedIn() || parsed.isComplete() { + return &WebLoginResult{Status: WebLoginStatusComplete}, nil + } + if result := wls.jetfuelAuthMethodChoiceResult(parsed); result != nil { + return result, nil + } + if action := parsed.verificationAction(); action != "" { + wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = parsed.verificationCodeFields() + return &WebLoginResult{ + Status: WebLoginStatusNeedsText, + CurrentSubtaskID: "JetfuelVerification", + Challenge: parsed.verificationChallenge(), + }, nil + } + if action := parsed.passwordAction(); action != "" { + wls.jetfuel.passwordAction = action + return &WebLoginResult{ + Status: WebLoginStatusNeedsPassword, + CurrentSubtaskID: "JetfuelPassword", + Challenge: &WebLoginChallenge{ + SubtaskID: "JetfuelPassword", + Hint: "Password", + }, + }, nil + } + return nil, fmt.Errorf("%w: jetfuel credentials response did not complete or expose a supported challenge", ErrWebLoginUnexpectedSubtask) +} + +func (wls *WebLoginSession) submitJetfuelPassword(ctx context.Context, password string) (*WebLoginResult, error) { + if password == "" { + return nil, fmt.Errorf("x password is required") + } + if wls.jetfuel == nil || wls.jetfuel.passwordAction == "" { + return nil, fmt.Errorf("%w: jetfuel password action is missing", ErrWebLoginUnexpectedSubtask) + } + form := url.Values{ + "password": {password}, + } + if wls.jetfuel.identifier != "" { + form.Set("username", wls.jetfuel.identifier) + } + if wls.jetfuel.sessionToken != "" { + form.Set("session_token", wls.jetfuel.sessionToken) + } + body, err := wls.client.jetfuelPostForm(ctx, wls.jetfuel.passwordAction, form) + if err != nil { + return nil, err + } + parsed := parseJetfuelLoginResponse(body) + if err := parsed.loginError(); err != nil { + return nil, err + } + wls.updateJetfuelState(parsed) + if wls.client.IsLoggedIn() || parsed.isComplete() { + return &WebLoginResult{Status: WebLoginStatusComplete}, nil + } + if result := wls.jetfuelAuthMethodChoiceResult(parsed); result != nil { + return result, nil + } + if action := parsed.beginTwoFactorAction(); action != "" { + return wls.submitJetfuelBeginTwoFactor(ctx, action) + } + if action := parsed.verificationAction(); action != "" { + wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = parsed.verificationCodeFields() + return &WebLoginResult{ + Status: WebLoginStatusNeedsText, + CurrentSubtaskID: "JetfuelVerification", + Challenge: parsed.verificationChallenge(), + }, nil + } + return nil, fmt.Errorf("%w: jetfuel password response did not complete or expose a supported challenge", ErrWebLoginUnexpectedSubtask) +} + +func (wls *WebLoginSession) submitJetfuelBeginTwoFactor(ctx context.Context, action string) (*WebLoginResult, error) { + if wls.jetfuel == nil { + return nil, fmt.Errorf("%w: jetfuel session state is missing", ErrWebLoginUnexpectedSubtask) + } + form := url.Values{} + if wls.jetfuel.preludeDispatchID != "" { + form.Set("prelude_dispatch_id", wls.jetfuel.preludeDispatchID) + } + if wls.jetfuel.sessionToken != "" { + form.Set("session_token", wls.jetfuel.sessionToken) + } + body, err := wls.client.jetfuelPostForm(ctx, action, form) + if err != nil { + return nil, err + } + parsed := parseJetfuelLoginResponse(body) + if err := parsed.loginError(); err != nil { + return nil, err + } + wls.updateJetfuelState(parsed) + if wls.client.IsLoggedIn() || parsed.isComplete() { + return &WebLoginResult{Status: WebLoginStatusComplete}, nil + } + if result := wls.jetfuelAuthMethodChoiceResult(parsed); result != nil { + return result, nil + } + if action := parsed.verificationAction(); action != "" { + wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = parsed.verificationCodeFields() + return &WebLoginResult{ + Status: WebLoginStatusNeedsText, + CurrentSubtaskID: "JetfuelVerification", + Challenge: parsed.verificationChallenge(), + }, nil + } + return nil, fmt.Errorf("%w: jetfuel two-factor prelude did not expose a verification challenge", ErrWebLoginUnexpectedSubtask) +} + +func (wls *WebLoginSession) submitJetfuelText(ctx context.Context, text string) (*WebLoginResult, error) { + text = strings.TrimSpace(text) + if text == "" { + return nil, fmt.Errorf("x verification code is required") + } + if wls.jetfuel == nil || wls.jetfuel.verificationAction == "" { + return nil, fmt.Errorf("%w: jetfuel verification action is missing", ErrWebLoginUnexpectedSubtask) + } + form := url.Values{} + for _, field := range wls.jetfuelVerificationFields() { + form.Set(field, text) + } + if wls.jetfuel.sessionToken != "" { + form.Set("session_token", wls.jetfuel.sessionToken) + } + if wls.jetfuel.preludeDispatchID != "" { + form.Set("prelude_dispatch_id", wls.jetfuel.preludeDispatchID) + } + body, err := wls.client.jetfuelPostForm(ctx, wls.jetfuel.verificationAction, form) + if err != nil { + return nil, err + } + parsed := parseJetfuelLoginResponse(body) + if err := parsed.loginError(); err != nil { + return nil, err + } + wls.updateJetfuelState(parsed) + if wls.client.IsLoggedIn() || parsed.isComplete() { + return &WebLoginResult{Status: WebLoginStatusComplete}, nil + } + if result := wls.jetfuelAuthMethodChoiceResult(parsed); result != nil { + return result, nil + } + if action := parsed.passwordAction(); action != "" { + wls.jetfuel.passwordAction = action + return &WebLoginResult{ + Status: WebLoginStatusNeedsPassword, + CurrentSubtaskID: "JetfuelPassword", + Challenge: &WebLoginChallenge{ + SubtaskID: "JetfuelPassword", + Hint: "Password", + }, + }, nil + } + if action := parsed.beginTwoFactorAction(); action != "" { + return wls.submitJetfuelBeginTwoFactor(ctx, action) + } + if action := parsed.verificationAction(); action != "" { + wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = parsed.verificationCodeFields() + return &WebLoginResult{ + Status: WebLoginStatusNeedsText, + CurrentSubtaskID: "JetfuelVerification", + Challenge: parsed.verificationChallenge(), + }, nil + } + return nil, fmt.Errorf("%w: jetfuel verification response did not complete login", ErrWebLoginUnexpectedSubtask) +} + +func (wls *WebLoginSession) updateJetfuelState(parsed jetfuelLoginResponse) { + if wls.jetfuel == nil { + wls.jetfuel = &jetfuelLoginState{} + } + if token := parsed.uuidValue("session_token"); token != "" { + wls.jetfuel.sessionToken = token + } + if id := parsed.uuidValue("prelude_dispatch_id"); id != "" { + wls.jetfuel.preludeDispatchID = id + } + if id := parsed.numericValue("user_id"); id != "" { + wls.jetfuel.userID = id + } + if action := parsed.passwordAction(); action != "" { + wls.jetfuel.passwordAction = action + } + if action := parsed.beginTwoFactorAction(); action != "" { + wls.jetfuel.twoFactorAction = action + } + if action := parsed.verificationAction(); action != "" { + wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = parsed.verificationCodeFields() + } + if methods := parsed.authMethods(); len(methods) > 0 { + wls.jetfuel.twoFactorMethods = methods + } +} + +func (wls *WebLoginSession) jetfuelVerificationFields() []string { + if wls.jetfuel != nil && len(wls.jetfuel.verificationFields) > 0 { + return wls.jetfuel.verificationFields + } + return defaultJetfuelVerificationFields() +} + +func (wls *WebLoginSession) jetfuelAuthMethodChoiceResult(parsed jetfuelLoginResponse) *WebLoginResult { + methods := parsed.authMethods() + if len(methods) == 0 && wls.jetfuel != nil { + methods = wls.jetfuel.twoFactorMethods + } + if len(methods) == 0 || !parsed.isAuthMethodChoice() { + return nil + } + if wls.jetfuel != nil { + wls.jetfuel.twoFactorMethods = methods + } + supportedMethods := supportedWebLoginAuthMethods(methods) + if len(supportedMethods) == 0 { + return &WebLoginResult{ + Status: WebLoginStatusUnsupported, + Challenge: unsupportedWebLoginAuthMethodChallenge(methods), + CurrentSubtaskID: "JetfuelTwoFactorMethod", + } + } + return &WebLoginResult{ + Status: WebLoginStatusNeedsAuthMethod, + CurrentSubtaskID: "JetfuelTwoFactorMethod", + Challenge: &WebLoginChallenge{ + SubtaskID: "JetfuelTwoFactorMethod", + Hint: "Verification method", + Description: "Choose how to verify this X login.", + IsTwoFactor: true, + }, + AuthMethods: supportedMethods, + } +} + +func unsupportedWebLoginAuthMethodChallenge(methods []WebLoginAuthMethod) *WebLoginChallenge { + description := "X returned a login challenge this bridge does not support yet." + if len(methods) == 1 && methods[0].Description != "" { + description = methods[0].Description + } + return &WebLoginChallenge{ + SubtaskID: "JetfuelTwoFactorMethod", + Hint: "Verification method", + Description: description, + IsTwoFactor: true, + } +} + +func supportedWebLoginAuthMethods(methods []WebLoginAuthMethod) []WebLoginAuthMethod { + supported := make([]WebLoginAuthMethod, 0, len(methods)) + for _, method := range methods { + if method.Supported { + supported = append(supported, method) + } + } + return supported +} + +func (wls *WebLoginSession) submitJetfuelAuthMethod(ctx context.Context, methodID string) (*WebLoginResult, error) { + if wls.jetfuel == nil || len(wls.jetfuel.twoFactorMethods) == 0 { + return nil, ErrWebLoginMissingAuthMethodState + } + method, ok := wls.jetfuel.findAuthMethod(methodID) + if !ok { + return nil, fmt.Errorf("%w: %s", ErrWebLoginUnsupportedAuthMethod, strings.TrimSpace(methodID)) + } + if !method.Supported { + return nil, fmt.Errorf("%w: %s", ErrWebLoginUnsupportedAuthMethod, method.Name) + } + action := wls.jetfuel.twoFactorAction + if action == "" { + action = endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH + } + wls.client.Logger.Debug(). + Str("method_id", method.ID). + Str("method_submit_id", method.SubmitID). + Str("method_kind", string(method.Kind)). + Int("method_index", method.Index). + Str("action", action). + Msg("Submitting Jetfuel auth method") + body, err := wls.client.jetfuelPostForm(ctx, action, wls.jetfuel.authMethodForm(method)) + if err != nil { + return nil, err + } + parsed := parseJetfuelLoginResponse(body) + if err := parsed.loginError(); err != nil { + return nil, err + } + wls.updateJetfuelState(parsed) + if wls.client.IsLoggedIn() || parsed.isComplete() { + return &WebLoginResult{Status: WebLoginStatusComplete}, nil + } + if action := parsed.verificationActionForMethod(method); action != "" { + wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = parsed.verificationCodeFields() + return &WebLoginResult{ + Status: WebLoginStatusNeedsText, + CurrentSubtaskID: "JetfuelVerification", + Challenge: parsed.verificationChallengeForMethod(method), + }, nil + } + if result := wls.jetfuelAuthMethodChoiceResult(parsed); result != nil { + return result, nil + } + wls.client.Logger.Debug(). + Str("method_id", method.ID). + Str("method_submit_id", method.SubmitID). + Str("method_kind", string(method.Kind)). + Strs("paths", parsed.paths). + Strs("fields", parsed.fields). + Bool("has_verification_challenge", parsed.hasVerificationChallengeForMethod(method)). + Msg("Jetfuel auth method response did not expose a supported verification challenge") + return nil, fmt.Errorf("%w: jetfuel auth method response did not expose a verification challenge", ErrWebLoginUnexpectedSubtask) +} + +func (jls *jetfuelLoginState) findAuthMethod(methodID string) (WebLoginAuthMethod, bool) { + methodID = normalizeJetfuelMethodID(methodID) + for _, method := range jls.twoFactorMethods { + if normalizeJetfuelMethodID(method.ID) == methodID || + normalizeJetfuelMethodID(method.SubmitID) == methodID || + normalizeJetfuelMethodID(method.Name) == methodID { + return method, true + } + } + return WebLoginAuthMethod{}, false +} + +func (jls *jetfuelLoginState) authMethodForm(method WebLoginAuthMethod) url.Values { + methodID := method.ID + if method.SubmitID != "" { + methodID = method.SubmitID + } + form := url.Values{ + "two_factor_auth_method_type": {methodID}, + "_selected_method_idx": {fmt.Sprintf("%d", method.Index)}, + } + if jls.userID != "" { + form.Set("user_id", jls.userID) + } + if jls.sessionToken != "" { + form.Set("session_token", jls.sessionToken) + } + return form +} + +func (c *Client) jetfuelGet(ctx context.Context, path string) ([]byte, error) { + return c.jetfuelRequest(ctx, path, http.MethodGet, nil) +} + +func (c *Client) jetfuelPostForm(ctx context.Context, path string, form url.Values) ([]byte, error) { + if err := c.addJetfuelCastleTokenToForm(form); err != nil { + c.Logger.Trace().Err(err).Msg("Failed to create Castle request token") + } + return c.jetfuelRequest(ctx, path, http.MethodPost, []byte(form.Encode())) +} + +func (c *Client) addJetfuelCastleTokenToForm(form url.Values) error { + if form.Get("$castle_token") != "" { + return nil + } + token, err := createCurrentCastleRequestToken(c.session.ClientUUID) + if err == nil { + form.Set("$castle_token", token) + return nil + } + if fallbackErr := addCastleTokenToForm(form); fallbackErr != nil { + return fmt.Errorf("current Castle: %w; fallback Castle: %w", err, fallbackErr) + } + c.Logger.Trace().Err(err).Msg("Fell back to legacy Castle request token") + return nil +} + +func (c *Client) jetfuelRequest(ctx context.Context, path, method string, body []byte) ([]byte, error) { + fullURL := endpoints.JETFUEL_BASE_URL + ensureLeadingSlash(path) + txID, err := crypto.SignTransaction(c.session.AnimationToken, c.session.VerificationToken, fullURL, method) + if err != nil { + c.Logger.Trace().Err(err).Msg("Failed to create X Jetfuel client transaction ID") + txID = "e:" + } + extra := map[string]string{ + "accept": "*/*", + "origin": endpoints.BASE_URL, + "priority": "u=1, i", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "timezone": jetfuelTimezone(), + "x-client-transaction-id": txID, + "x-jf-client-theme": "light", + "x-jf-v": jetfuelHeaderVersion, + "x-twitter-active-user": "yes", + } + c.addJetfuelBrowserParityHeaders(extra) + if csrfToken := c.cookies.Get(cookies.XCt0); csrfToken != "" { + extra["x-csrf-token"] = csrfToken + } + headers := c.buildHeaders(HeaderOpts{ + WithNonAuthBearer: true, + WithCookies: true, + WithXGuestToken: true, + Referer: endpoints.BASE_URL + "/", + Extra: extra, + }) + headers.Del("x-twitter-client-language") + contentType := types.ContentTypeNone + if method == http.MethodPost { + contentType = types.ContentTypeForm + } + resp, respBody, err := c.makeRequestDirect(ctx, fullURL, method, headers, body, contentType) + if resp != nil { + c.cookies.UpdateFromResponse(resp) + } + if err != nil { + return respBody, err + } + return respBody, nil +} + +type jetfuelViewerContextEvent struct { + Category string `json:"_category_"` + FormatVersion int `json:"format_version"` + TriggeredOn int64 `json:"triggered_on"` + Items []any `json:"items"` + EventNamespace jetfuelViewerEventNamespace `json:"event_namespace"` + ClientEventSequenceStartTimestamp int64 `json:"client_event_sequence_start_timestamp"` + ClientEventSequenceNumber int `json:"client_event_sequence_number"` + ClientAppID string `json:"client_app_id"` +} + +type jetfuelViewerEventNamespace struct { + Page string `json:"page"` + Action string `json:"action"` + Element string `json:"element"` + Client string `json:"client"` +} + +func (c *Client) sendJetfuelViewerContextEvent(ctx context.Context) error { + now := time.Now().UnixMilli() + event := jetfuelViewerContextEvent{ + Category: "client_event", + FormatVersion: 2, + TriggeredOn: now, + Items: []any{}, + EventNamespace: jetfuelViewerEventNamespace{Page: "front", Action: "click", Element: "continue", Client: "m5"}, + ClientEventSequenceStartTimestamp: now, + ClientEventSequenceNumber: 1, + ClientAppID: "3033300", + } + logPayload, err := json.Marshal([]jetfuelViewerContextEvent{event}) + if err != nil { + return err + } + form := url.Values{ + "debug": {"true"}, + "log": {string(logPayload)}, + } + txID, err := crypto.SignTransaction(c.session.AnimationToken, c.session.VerificationToken, endpoints.VIEWER_CONTEXT_URL, http.MethodPost) + if err != nil { + c.Logger.Trace().Err(err).Msg("Failed to create X viewer-context client transaction ID") + txID = "e:" + } + headers := c.buildHeaders(HeaderOpts{ + WithNonAuthBearer: true, + WithCookies: true, + WithXGuestToken: true, + WithXTwitterHeaders: true, + Origin: endpoints.BASE_URL, + Referer: endpoints.BASE_URL + "/", + Extra: map[string]string{ + "accept": "*/*", + "priority": "u=1, i", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "x-client-transaction-id": txID, + }, + }) + resp, _, err := c.makeRequestDirect(ctx, endpoints.VIEWER_CONTEXT_URL, http.MethodPost, headers, []byte(form.Encode()), types.ContentTypeForm) + if resp != nil { + c.cookies.UpdateFromResponse(resp) + } + return err +} + +func jetfuelTimezone() string { + if timezone := strings.TrimSpace(os.Getenv("TWITTER_JETFUEL_TIMEZONE")); timezone != "" { + return timezone + } + if local := time.Local.String(); strings.Contains(local, "/") { + return local + } + _, offset := time.Now().Zone() + switch offset { + case -10 * 60 * 60: + return "Pacific/Honolulu" + case -9 * 60 * 60: + return "America/Anchorage" + case -8 * 60 * 60: + return "America/Los_Angeles" + case -7 * 60 * 60: + return "America/Denver" + case -6 * 60 * 60, -5 * 60 * 60: + return "America/Chicago" + case -4 * 60 * 60: + return "America/New_York" + default: + return "UTC" + } +} + +func (c *Client) addJetfuelBrowserParityHeaders(extra map[string]string) { + addTFEGuestCookieHeader(extra, "x-tfe-guest-cookie-id", c.cookies.Get(cookies.XGuestID)) + addTFEGuestCookieHeader(extra, "x-tfe-guest-cookie-id-ads", c.cookies.Get(cookies.XGuestIDAds)) + addTFEGuestCookieHeader(extra, "x-tfe-guest-cookie-id-marketing", c.cookies.Get(cookies.XGuestIDMarketing)) + if dtab := decodeDtabLocal(c.cookies.Get(cookies.XDtabLocal)); dtab != "" { + extra["dtab-local"] = dtab + } +} + +func addTFEGuestCookieHeader(extra map[string]string, headerName, cookieValue string) { + if value := decodeTFEGuestCookie(cookieValue); value != "" { + extra[headerName] = value + } +} + +func decodeTFEGuestCookie(cookieValue string) string { + value, err := url.QueryUnescape(cookieValue) + if err != nil { + value = cookieValue + } + return strings.TrimPrefix(value, "v1:") +} + +func decodeDtabLocal(cookieValue string) string { + if cookieValue == "" { + return "" + } + value, err := url.QueryUnescape(cookieValue) + if err != nil { + value = cookieValue + } + if strings.HasPrefix(value, "/") && isPrintableASCII(value) { + return value + } + decoded, err := base64.RawURLEncoding.DecodeString(value) + if err != nil { + decoded, err = base64.URLEncoding.DecodeString(value) + } + if err == nil && strings.HasPrefix(string(decoded), "/") && isPrintableASCII(string(decoded)) { + return string(decoded) + } + if isPrintableASCII(value) { + return value + } + return "" +} + +func isPrintableASCII(value string) bool { + for _, r := range value { + if r < 0x20 || r > 0x7e { + return false + } + } + return value != "" +} + +func ensureLeadingSlash(path string) string { + if strings.HasPrefix(path, "/") { + return path + } + return "/" + path +} + +func parseJetfuelLoginResponse(body []byte) jetfuelLoginResponse { + strs := extractJetfuelStrings(body) + paths := make([]string, 0) + fields := make([]string, 0) + for _, str := range strs { + for _, path := range jetfuelActionPathRegex.FindAllString(str, -1) { + paths = appendJetfuelPath(paths, path) + } + if path := canonicalJetfuelActionPath(str); path != "" { + paths = appendJetfuelPath(paths, path) + } + if jetfuelFieldRegex.MatchString(str) && !slices.Contains(fields, str) { + fields = append(fields, str) + } + } + return jetfuelLoginResponse{strings: strs, paths: paths, fields: fields, raw: body} +} + +func canonicalJetfuelActionPath(value string) string { + value = strings.TrimSpace(value) + if strings.HasPrefix(value, "/onboarding/web/actions/") { + return value + } + return jetfuelActionAliases[value] +} + +func appendJetfuelPath(paths []string, path string) []string { + if path == "" || slices.Contains(paths, path) { + return paths + } + return append(paths, path) +} + +func extractJetfuelStrings(body []byte) []string { + var out []string + seen := make(map[string]struct{}) + start := -1 + for i := 0; i < len(body); { + r, size := utf8.DecodeRune(body[i:]) + if r == utf8.RuneError && size == 1 { + if start >= 0 { + addJetfuelString(body[start:i], seen, &out) + start = -1 + } + i++ + continue + } + if isJetfuelStringRune(r) { + if start < 0 { + start = i + } + } else if start >= 0 { + addJetfuelString(body[start:i], seen, &out) + start = -1 + } + i += size + } + if start >= 0 { + addJetfuelString(body[start:], seen, &out) + } + return out +} + +func isJetfuelStringRune(r rune) bool { + return r == '\n' || r == '\r' || r == '\t' || r >= 0x20 && r != utf8.RuneError +} + +func addJetfuelString(raw []byte, seen map[string]struct{}, out *[]string) { + str := strings.TrimSpace(string(bytes.Trim(raw, "\x00"))) + if len(str) < 3 { + return + } + if _, ok := seen[str]; ok { + return + } + seen[str] = struct{}{} + *out = append(*out, str) +} + +func (jfr jetfuelLoginResponse) text() string { + return strings.ToLower(strings.Join(jfr.strings, "\n")) +} + +func (jfr jetfuelLoginResponse) hasPath(path string) bool { + return slices.Contains(jfr.paths, path) +} + +func (jfr jetfuelLoginResponse) hasField(field string) bool { + return slices.Contains(jfr.fields, field) +} + +func (jfr jetfuelLoginResponse) passwordAction() string { + for _, path := range jfr.paths { + lower := strings.ToLower(path) + if strings.Contains(lower, "password") { + return path + } + } + if jfr.hasField("password") || strings.Contains(jfr.text(), "password") { + return endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH + } + return "" +} + +func (jfr jetfuelLoginResponse) beginTwoFactorAction() string { + for _, path := range jfr.paths { + if strings.Contains(strings.ToLower(path), "begin_two_factor_auth") { + return path + } + } + return "" +} + +func (jfr jetfuelLoginResponse) verificationAction() string { + for _, path := range jfr.paths { + lower := strings.ToLower(path) + if strings.Contains(lower, "begin_two_factor_auth") { + continue + } + if strings.Contains(lower, "two_factor") || strings.Contains(lower, "2fa") || + strings.Contains(lower, "challenge") || strings.Contains(lower, "verification") { + return path + } + } + return "" +} + +func (jfr jetfuelLoginResponse) verificationActionForMethod(method WebLoginAuthMethod) string { + if action := jfr.verificationAction(); action != "" { + return action + } + if action := jfr.methodVerificationAction(method); action != "" { + return action + } + if jfr.hasVerificationChallengeForMethod(method) { + return endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH + } + return "" +} + +func (jfr jetfuelLoginResponse) methodVerificationAction(method WebLoginAuthMethod) string { + if method.Kind != WebLoginAuthMethodKindSMS { + return "" + } + isPhoneNumberChallenge := jfr.isPhoneNumberChallenge() + for _, path := range jfr.paths { + lower := strings.ToLower(path) + if strings.Contains(lower, "begin_two_factor_auth") || strings.Contains(lower, "resend") { + continue + } + if isPhoneNumberChallenge && + (strings.Contains(lower, "phone") || strings.Contains(lower, "sms") || + strings.Contains(lower, "text") || strings.Contains(lower, "send")) { + return path + } + if !isPhoneNumberChallenge && (strings.Contains(lower, "code") || strings.Contains(lower, "pin")) { + return path + } + } + return "" +} + +func (jfr jetfuelLoginResponse) hasVerificationChallengeForMethod(method WebLoginAuthMethod) bool { + text := jfr.text() + if strings.Contains(text, "verification code") || + strings.Contains(text, "authentication code") || + strings.Contains(text, "two-factor code") || + strings.Contains(text, "two factor code") || + strings.Contains(text, "backup code") { + return true + } + if method.Kind == WebLoginAuthMethodKindSMS && + (strings.Contains(text, "text message") || strings.Contains(text, "sms") || + isJetfuelPhoneNumberChallengeText(text) || jfr.hasExplicitPhoneNumberField()) { + return true + } + return jfr.hasExplicitVerificationField() +} + +func (jfr jetfuelLoginResponse) hasExplicitPhoneNumberField() bool { + for _, field := range jfr.fields { + lower := strings.ToLower(field) + if lower == "phone_number" || lower == "phone" || lower == "loginacid" || lower == "login_acid" || + strings.Contains(lower, "phone") || strings.Contains(lower, "acid") { + return true + } + } + return false +} + +func (jfr jetfuelLoginResponse) hasExplicitVerificationField() bool { + for _, field := range jfr.fields { + lower := strings.ToLower(field) + if lower == "session_token" || lower == "prelude_dispatch_id" || + strings.Contains(lower, "csrf") || strings.Contains(lower, "oauth") || + strings.Contains(lower, "castle") { + continue + } + if lower == "challenge_response" || lower == "verification_code" || + lower == "two_factor_code" || lower == "backup_code" || + lower == "code" || lower == "response" || lower == "token" || + strings.Contains(lower, "otp") || strings.Contains(lower, "phone") || + strings.Contains(lower, "acid") { + return true + } + } + return false +} + +func (jfr jetfuelLoginResponse) verificationCodeFields() []string { + if jfr.isPhoneNumberChallenge() { + if fields := jfr.phoneNumberVerificationFields(); len(fields) > 0 { + return fields + } + } + fields := make([]string, 0, 8) + text := jfr.text() + preferred := []string{ + "challenge_response", + "verification_code", + "two_factor_code", + "code", + "token", + } + if strings.Contains(text, "backup code") { + preferred = append([]string{"backup_code"}, preferred...) + } + for _, field := range preferred { + if jfr.hasField(field) { + fields = appendJetfuelVerificationField(fields, field) + } + } + for _, field := range jfr.fields { + lower := strings.ToLower(field) + if field != lower { + continue + } + if lower == "session_token" || lower == "prelude_dispatch_id" || + strings.Contains(lower, "csrf") || strings.Contains(lower, "oauth") || + strings.Contains(lower, "castle") { + continue + } + if strings.Contains(lower, "code") || strings.Contains(lower, "otp") || + strings.Contains(lower, "challenge") || strings.Contains(lower, "response") || + strings.Contains(lower, "token") { + fields = appendJetfuelVerificationField(fields, field) + } + } + if len(fields) == 0 { + return defaultJetfuelVerificationFields() + } + return fields +} + +func (jfr jetfuelLoginResponse) phoneNumberVerificationFields() []string { + fields := make([]string, 0, 4) + for _, field := range []string{"phone_number", "phone", "LoginAcid", "login_acid"} { + if jfr.hasField(field) { + fields = appendJetfuelVerificationField(fields, field) + } + } + if len(fields) > 0 { + return fields + } + for _, field := range jfr.fields { + lower := strings.ToLower(field) + if lower == "session_token" || lower == "prelude_dispatch_id" || + strings.Contains(lower, "csrf") || strings.Contains(lower, "oauth") || + strings.Contains(lower, "castle") { + continue + } + if strings.Contains(lower, "phone") || strings.Contains(lower, "acid") { + fields = appendJetfuelVerificationField(fields, field) + } + } + if len(fields) > 0 { + return fields + } + for _, field := range []string{"challenge_response", "response"} { + if jfr.hasField(field) { + fields = appendJetfuelVerificationField(fields, field) + } + } + return fields +} + +func appendJetfuelVerificationField(fields []string, field string) []string { + field = strings.TrimSpace(field) + if field == "" || slices.Contains(fields, field) { + return fields + } + return append(fields, field) +} + +func defaultJetfuelVerificationFields() []string { + return []string{"challenge_response", "verification_code", "two_factor_code", "backup_code", "code"} +} + +func (jfr jetfuelLoginResponse) uuidValue(field string) string { + field = strings.ToLower(field) + for i, str := range jfr.strings { + if !strings.Contains(strings.ToLower(str), field) { + continue + } + if uuid := firstJetfuelUUID(str); uuid != "" { + return uuid + } + for next := i + 1; next < len(jfr.strings) && next <= i+6; next++ { + if uuid := firstJetfuelUUID(jfr.strings[next]); uuid != "" { + return uuid + } + } + } + return "" +} + +func (jfr jetfuelLoginResponse) numericValue(field string) string { + field = strings.ToLower(field) + for i, str := range jfr.strings { + if !strings.Contains(strings.ToLower(str), field) { + continue + } + if id := firstJetfuelNumericID(str); id != "" { + return id + } + for next := i + 1; next < len(jfr.strings) && next <= i+6; next++ { + if id := firstJetfuelNumericID(jfr.strings[next]); id != "" { + return id + } + } + } + return "" +} + +func firstJetfuelUUID(value string) string { + return jetfuelUUIDRegex.FindString(value) +} + +func firstJetfuelNumericID(value string) string { + return jetfuelNumericIDRegex.FindString(value) +} + +func (jfr jetfuelLoginResponse) isAuthMethodChoice() bool { + text := jfr.text() + if strings.Contains(text, "select a method") || + strings.Contains(text, "choose the method") || + strings.Contains(text, "two_factor_method") || + strings.Contains(text, "two factor method") { + return true + } + if strings.Contains(text, "verification code") || strings.Contains(text, "authentication code") || + strings.Contains(text, "backup code") || strings.Contains(text, "two-factor code") || + strings.Contains(text, "two factor code") { + return false + } + return jfr.beginTwoFactorAction() != "" && jfr.hasAuthMethodToken() +} + +func (jfr jetfuelLoginResponse) hasAuthMethodToken() bool { + return slices.ContainsFunc(jfr.strings, func(str string) bool { + _, ok := classifyJetfuelAuthMethod(str) + return ok + }) +} + +func (jfr jetfuelLoginResponse) authMethods() []WebLoginAuthMethod { + if !jfr.isAuthMethodChoice() { + return nil + } + methods := make([]WebLoginAuthMethod, 0, 3) + methodIndex := 0 + for _, str := range jfr.strings { + method, ok := classifyJetfuelAuthMethod(str) + if !ok { + continue + } + if updated := updateJetfuelAuthMethod(methods, method); updated { + continue + } + method.Index = methodIndex + methodIndex++ + methods = append(methods, method) + } + return methods +} + +func classifyJetfuelAuthMethod(value string) (WebLoginAuthMethod, bool) { + normalized := normalizeJetfuelMethodID(value) + switch normalized { + case "totp", "authenticatorapp", "authenticationapp": + return WebLoginAuthMethod{ + ID: "Totp", + Name: "Authenticator App", + Description: "Use the code from your authentication app.", + Kind: WebLoginAuthMethodKindCode, + Supported: true, + }, true + case "backupcode": + return WebLoginAuthMethod{ + ID: "BackupCode", + Name: "Backup Code", + Description: "Use a backup code from your X account settings.", + Kind: WebLoginAuthMethodKindBackupCode, + Supported: true, + }, true + case "sms", "smscode", "smsverification", "smsauth", "smsauthentication", "text", "textmessage", "textmessagecode", "textmessageauth", "textmessageauthentication", "phone", "phonecode", "phonenumber", "phoneverification", "phoneauth", "phoneauthentication", "mobile", "mobilephone": + return WebLoginAuthMethod{ + ID: "Sms", + SubmitID: jetfuelAuthMethodSubmitID("Sms", value), + Name: "Text Message", + Description: "Text message verification is coming soon.", + Kind: WebLoginAuthMethodKindSMS, + Supported: false, + }, true + case "u2fsecuritykey", "securitykey", "securitykeypc", "passkey": + return WebLoginAuthMethod{ + ID: "U2fSecurityKey", + Name: "Security Key PC", + Description: "Requires passkey/WebAuthn client support.", + Kind: WebLoginAuthMethodKindUnknown, + Supported: false, + }, true + default: + return WebLoginAuthMethod{}, false + } +} + +func jetfuelAuthMethodSubmitID(canonicalID, raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" || normalizeJetfuelMethodID(raw) == normalizeJetfuelMethodID(canonicalID) { + return "" + } + if strings.ContainsAny(raw, " \t\r\n") { + return "" + } + return raw +} + +func updateJetfuelAuthMethod(methods []WebLoginAuthMethod, method WebLoginAuthMethod) bool { + normalized := normalizeJetfuelMethodID(method.ID) + for i := range methods { + if normalizeJetfuelMethodID(methods[i].ID) != normalized { + continue + } + if methods[i].SubmitID == "" && method.SubmitID != "" { + method.Index = methods[i].Index + methods[i] = method + } + return true + } + return false +} + +func normalizeJetfuelMethodID(value string) string { + value = strings.ToLower(strings.TrimSpace(value)) + value = strings.ReplaceAll(value, " ", "") + value = strings.ReplaceAll(value, "_", "") + value = strings.ReplaceAll(value, "-", "") + value = strings.ReplaceAll(value, ".", "") + return value +} + +func (jfr jetfuelLoginResponse) verificationChallenge() *WebLoginChallenge { + text := jfr.text() + isPhoneNumber := jfr.isPhoneNumberChallenge() + hint := "Verification code" + inputKind := WebLoginChallengeInputKindCode + if isPhoneNumber { + hint = "Phone number" + inputKind = WebLoginChallengeInputKindPhoneNumber + } + return &WebLoginChallenge{ + SubtaskID: "JetfuelVerification", + Hint: hint, + Description: jetfuelChallengeDescription(text), + IsTwoFactor: !isPhoneNumber && (strings.Contains(text, "two-factor") || strings.Contains(text, "two factor") || + strings.Contains(text, "authentication code") || strings.Contains(text, "verification code") || + strings.Contains(text, "backup code") || strings.Contains(text, "totp")), + InputKind: inputKind, + } +} + +func (jfr jetfuelLoginResponse) verificationChallengeForMethod(method WebLoginAuthMethod) *WebLoginChallenge { + challenge := jfr.verificationChallenge() + switch method.Kind { + case WebLoginAuthMethodKindCode: + challenge.Description = "Enter the code from your authentication app." + case WebLoginAuthMethodKindBackupCode: + challenge.Hint = "Backup code" + challenge.Description = "Enter a backup code from X." + case WebLoginAuthMethodKindSMS: + if challenge.InputKind == WebLoginChallengeInputKindPhoneNumber { + challenge.Description = "Enter the phone number associated with your X account." + } else { + challenge.Description = "Enter the code sent to your phone number." + } + } + challenge.IsTwoFactor = challenge.InputKind != WebLoginChallengeInputKindPhoneNumber + return challenge +} + +func jetfuelChallengeDescription(text string) string { + switch { + case isJetfuelPhoneNumberChallengeText(text): + return "Enter the phone number associated with your X account." + case strings.Contains(text, "backup code"): + return "Enter the code from your authentication app." + case strings.Contains(text, "text message") || strings.Contains(text, "phone") || strings.Contains(text, "sms"): + return "Enter the code sent to your phone number." + case strings.Contains(text, "authentication code"): + return "Enter the authentication code from X." + case strings.Contains(text, "verification code"): + return "Enter the verification code from X." + default: + return "X needs additional verification for this login." + } +} + +func (jfr jetfuelLoginResponse) isPhoneNumberChallenge() bool { + text := jfr.text() + if strings.Contains(text, "verification code") || strings.Contains(text, "authentication code") || + strings.Contains(text, "backup code") || strings.Contains(text, "two-factor code") || + strings.Contains(text, "two factor code") || strings.Contains(text, "text message") || + strings.Contains(text, "sms") { + return false + } + if isJetfuelPhoneNumberChallengeText(text) { + return true + } + return jfr.hasField("phone_number") || jfr.hasField("phone") || + jfr.hasField("LoginAcid") || jfr.hasField("login_acid") +} + +func isJetfuelPhoneNumberChallengeText(text string) bool { + if !strings.Contains(text, "phone") { + return false + } + return strings.Contains(text, "enter") || strings.Contains(text, "confirm") || + strings.Contains(text, "verify") || strings.Contains(text, "provide") || + strings.Contains(text, "associated") +} + +func (jfr jetfuelLoginResponse) isComplete() bool { + text := jfr.text() + return strings.Contains(text, "/home") || strings.Contains(text, "open_account") +} + +func (jfr jetfuelLoginResponse) loginError() error { + text := jfr.text() + switch { + case strings.Contains(text, "official x apps") || strings.Contains(text, "use x.com"): + return &WebLoginError{Code: 399, Message: "Please use X.com or official X apps to proceed with log in/sign up."} + case strings.Contains(text, "temporarily limited") || strings.Contains(text, "try again later"): + return &WebLoginError{Code: 399, Message: "We've temporarily limited your login. Please try again later."} + case strings.Contains(text, "too many attempts") || strings.Contains(text, "try again in a few minutes"): + return &WebLoginError{Code: 399, Message: "Too many attempts. Try again in a few minutes."} + case strings.Contains(text, "missing_account") || strings.Contains(text, "not registered"): + return &WebLoginError{Code: 32, Message: "This email or username is not registered yet."} + case isJetfuelBadCredentialsText(text): + return &WebLoginError{Code: 32, Message: "Wrong password"} + case strings.Contains(text, "could not log you in") || strings.Contains(text, "couldn't log you in"): + return &WebLoginError{Code: 399, Message: "Could not log you in now. Please try again later."} + default: + return nil + } +} + +func isJetfuelBadCredentialsText(text string) bool { + return strings.Contains(text, "wrong password") || + strings.Contains(text, "incorrect password") || + strings.Contains(text, "invalid password") || + strings.Contains(text, "password you entered") || + strings.Contains(text, "password is incorrect") || + strings.Contains(text, "username and password") && strings.Contains(text, "did not match") || + strings.Contains(text, "invalid username or password") || + strings.Contains(text, "invalid credentials") +} diff --git a/pkg/twittermeow/login_jetfuel_live_probe_test.go b/pkg/twittermeow/login_jetfuel_live_probe_test.go new file mode 100644 index 00000000..ea9ba9a2 --- /dev/null +++ b/pkg/twittermeow/login_jetfuel_live_probe_test.go @@ -0,0 +1,568 @@ +//go:build liveprobe + +package twittermeow + +import ( + "context" + "errors" + "net/http" + "net/url" + "os" + "regexp" + "strings" + "testing" + "time" + + "github.com/rs/zerolog" + + "go.mau.fi/mautrix-twitter/pkg/twittermeow/cookies" + "go.mau.fi/mautrix-twitter/pkg/twittermeow/data/endpoints" + "go.mau.fi/mautrix-twitter/pkg/twittermeow/data/types" + "go.mau.fi/mautrix-twitter/pkg/twittermeow/methods" +) + +func TestLiveJetfuelLoginLandingProbe(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + if err := client.loadPage(ctx, jetfuelProbePageURL()); err != nil { + t.Fatalf("loadPage() failed: %v", err) + } + if os.Getenv("TWITTER_DNT_PROBE") == "1" { + client.cookies.Set(cookies.XCookieName("dnt"), "1") + t.Logf("using injected dnt cookie: true") + } + if cuid := strings.TrimSpace(os.Getenv("TWITTER_CUID_PROBE")); cuid != "" { + client.cookies.Set(cookies.XCookieName("__cuid"), cuid) + t.Logf("using injected __cuid cookie: true") + } + t.Logf("transaction token state after login page: verification=%t animation=%t", + client.session.VerificationToken != "", + client.session.AnimationToken != "", + ) + t.Logf("cookie presence after login page: cf_bm=%t cuid=%t dnt=%t guest_id=%t guest_id_ads=%t guest_id_marketing=%t personalization_id=%t gt=%t ct0=%t att=%t", + !client.cookies.IsCookieEmpty(cookies.XCookieName("__cf_bm")), + !client.cookies.IsCookieEmpty(cookies.XCookieName("__cuid")), + !client.cookies.IsCookieEmpty(cookies.XCookieName("dnt")), + !client.cookies.IsCookieEmpty(cookies.XGuestID), + !client.cookies.IsCookieEmpty(cookies.XGuestIDAds), + !client.cookies.IsCookieEmpty(cookies.XGuestIDMarketing), + !client.cookies.IsCookieEmpty(cookies.XPersonalizationID), + !client.cookies.IsCookieEmpty(cookies.XGuestToken), + !client.cookies.IsCookieEmpty(cookies.XCt0), + !client.cookies.IsCookieEmpty(cookies.XAtt), + ) + if _, err := client.jetfuelGet(ctx, endpoints.JETFUEL_LANDING_PATH); err != nil { + t.Logf("landing preflight failed: %v", err) + } + t.Logf("cookie presence after Jetfuel landing: cf_bm=%t cuid=%t dnt=%t guest_id=%t guest_id_ads=%t guest_id_marketing=%t personalization_id=%t gt=%t ct0=%t att=%t", + !client.cookies.IsCookieEmpty(cookies.XCookieName("__cf_bm")), + !client.cookies.IsCookieEmpty(cookies.XCookieName("__cuid")), + !client.cookies.IsCookieEmpty(cookies.XCookieName("dnt")), + !client.cookies.IsCookieEmpty(cookies.XGuestID), + !client.cookies.IsCookieEmpty(cookies.XGuestIDAds), + !client.cookies.IsCookieEmpty(cookies.XGuestIDMarketing), + !client.cookies.IsCookieEmpty(cookies.XPersonalizationID), + !client.cookies.IsCookieEmpty(cookies.XGuestToken), + !client.cookies.IsCookieEmpty(cookies.XCt0), + !client.cookies.IsCookieEmpty(cookies.XAtt), + ) + + body, err := client.jetfuelGet(ctx, endpoints.JETFUEL_LOGIN_PATH) + if err != nil { + t.Fatalf("jetfuelGet() failed: %v", err) + } + parsed := parseJetfuelLoginResponse(body) + t.Logf("Jetfuel landing strings=%d paths=%v fields=%v", len(parsed.strings), parsed.paths, parsed.fields) + if !parsed.hasPath(endpoints.JETFUEL_BEGIN_LOGIN_PATH) && !parsed.hasField("username_or_email") { + t.Fatalf("Jetfuel landing did not expose begin_login or username_or_email") + } +} + +func TestLiveJetfuelIdentifierMetadataProbe(t *testing.T) { + identifier := strings.TrimSpace(os.Getenv("TWITTER_IDENTIFIER_PROBE")) + if identifier == "" { + t.Skip("TWITTER_IDENTIFIER_PROBE is required") + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + if err := client.loadPage(ctx, jetfuelProbePageURL()); err != nil { + t.Fatalf("loadPage() failed: %v", err) + } + if os.Getenv("TWITTER_DNT_PROBE") == "1" { + client.cookies.Set(cookies.XCookieName("dnt"), "1") + t.Logf("using injected dnt cookie: true") + } + if cuid := strings.TrimSpace(os.Getenv("TWITTER_CUID_PROBE")); cuid != "" { + client.cookies.Set(cookies.XCookieName("__cuid"), cuid) + t.Logf("using injected __cuid cookie: true") + } + t.Logf("transaction token state after login page: verification=%t animation=%t", + client.session.VerificationToken != "", + client.session.AnimationToken != "", + ) + if _, err := client.jetfuelGet(ctx, endpoints.JETFUEL_LANDING_PATH); err != nil { + t.Logf("landing preflight failed: %v", err) + } + if _, err := client.jetfuelGet(ctx, endpoints.JETFUEL_LOGIN_PATH); err != nil { + t.Fatalf("login graph GET failed: %v", err) + } + + form := url.Values{ + "username_or_email": {identifier}, + } + if password := os.Getenv("TWITTER_PASSWORD_PROBE"); password != "" { + form.Set("password", password) + t.Logf("using injected password field: true") + } + if os.Getenv("TWITTER_CURRENT_CASTLE_TOKEN_PROBE") == "1" { + castleToken, err := createCurrentCastleRequestToken("091a1f32-3826-4bad-9250-aa14e3c0a2b2") + if err != nil { + t.Fatalf("createCurrentCastleRequestToken() failed: %v", err) + } + form.Set("$castle_token", castleToken) + t.Logf("using generated current Castle token: true") + } + if castleToken := strings.TrimSpace(os.Getenv("TWITTER_CASTLE_TOKEN_PROBE")); castleToken != "" { + form.Set("$castle_token", castleToken) + t.Logf("using injected castle token: true") + } + body, err := client.jetfuelPostForm(ctx, endpoints.JETFUEL_BEGIN_LOGIN_PATH, form) + if err != nil { + t.Fatalf("begin_login POST failed: %v", err) + } + parsed := parseJetfuelLoginResponse(body) + t.Logf("begin_login strings=%d paths=%v fields=%v", len(parsed.strings), parsed.paths, redactJetfuelDebugList(parsed.fields, identifier)) + for _, line := range filteredJetfuelDebugStrings(parsed, identifier) { + t.Logf("begin_login string: %s", line) + } +} + +func TestLiveJetfuelVerificationResponseProbe(t *testing.T) { + identifier := strings.TrimSpace(os.Getenv("TWITTER_LIVE_IDENTIFIER")) + password := os.Getenv("TWITTER_LIVE_PASSWORD") + code := strings.TrimSpace(os.Getenv("TWITTER_LIVE_VERIFICATION_CODE")) + if identifier == "" || password == "" || code == "" { + t.Skip("TWITTER_LIVE_IDENTIFIER, TWITTER_LIVE_PASSWORD, and TWITTER_LIVE_VERIFICATION_CODE are required") + } + + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + session := NewWebLoginSession(client) + result, err := session.Start(ctx) + logWebLoginStage(t, "start", result, err) + if err != nil { + t.Fatalf("Start() failed: %v", err) + } + result, err = session.SubmitCredentials(ctx, identifier, password) + logWebLoginStage(t, "credentials", result, err) + if err != nil { + t.Fatalf("SubmitCredentials() failed: %v", err) + } + if result.Status == WebLoginStatusNeedsPassword { + result, err = session.SubmitPassword(ctx, password) + logWebLoginStage(t, "password", result, err) + if err != nil { + t.Fatalf("SubmitPassword() failed: %v", err) + } + } + if result.Status != WebLoginStatusNeedsText { + t.Fatalf("SubmitCredentials() status = %s, want %s", result.Status, WebLoginStatusNeedsText) + } + t.Logf("verification action present=%t fields=%v", session.jetfuel != nil && session.jetfuel.verificationAction != "", session.jetfuelVerificationFields()) + + form := url.Values{} + for _, field := range session.jetfuelVerificationFields() { + form.Set(field, code) + } + if session.jetfuel.sessionToken != "" { + form.Set("session_token", session.jetfuel.sessionToken) + } + if session.jetfuel.preludeDispatchID != "" { + form.Set("prelude_dispatch_id", session.jetfuel.preludeDispatchID) + } + body, err := client.jetfuelPostForm(ctx, session.jetfuel.verificationAction, form) + if err != nil { + t.Fatalf("verification POST failed: %v", err) + } + parsed := parseJetfuelLoginResponse(body) + t.Logf("verification response: logged_in=%t complete=%t strings=%d paths=%v fields=%v password_action=%q begin_2fa_action=%q verification_action=%q", + client.IsLoggedIn(), + parsed.isComplete(), + len(parsed.strings), + redactJetfuelDebugList(parsed.paths, identifier, password, code), + redactJetfuelDebugList(parsed.fields, identifier, password, code), + parsed.passwordAction(), + parsed.beginTwoFactorAction(), + parsed.verificationAction(), + ) + if err := parsed.loginError(); err != nil { + var webErr *WebLoginError + if errors.As(err, &webErr) { + t.Logf("verification response login error: code=%d message=%q", webErr.Code, redactJetfuelDebugString(webErr.Message, identifier, password, code)) + } else { + t.Logf("verification response login error: %T", err) + } + } + for _, line := range filteredJetfuelDebugStrings(parsed, identifier, password, code) { + t.Logf("verification response string: %s", line) + } +} + +func TestLiveJetfuelPasswordResponseProbe(t *testing.T) { + identifier := strings.TrimSpace(os.Getenv("TWITTER_LIVE_IDENTIFIER")) + password := os.Getenv("TWITTER_LIVE_PASSWORD") + if identifier == "" || password == "" { + t.Skip("TWITTER_LIVE_IDENTIFIER and TWITTER_LIVE_PASSWORD are required") + } + + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + session := NewWebLoginSession(client) + result, err := session.Start(ctx) + logWebLoginStage(t, "start", result, err) + if err != nil { + t.Fatalf("Start() failed: %v", err) + } + result, err = session.SubmitIdentifier(ctx, identifier) + logWebLoginStage(t, "identifier", result, err) + if err != nil { + t.Fatalf("SubmitIdentifier() failed: %v", err) + } + if result.Status != WebLoginStatusNeedsPassword { + t.Fatalf("SubmitIdentifier() status = %s, want %s", result.Status, WebLoginStatusNeedsPassword) + } + + form := url.Values{"password": {password}} + if session.jetfuel.identifier != "" { + form.Set("username", session.jetfuel.identifier) + } + if session.jetfuel.sessionToken != "" { + form.Set("session_token", session.jetfuel.sessionToken) + } + body, err := client.jetfuelPostForm(ctx, session.jetfuel.passwordAction, form) + if err != nil { + t.Fatalf("password POST failed: %v", err) + } + parsed := parseJetfuelLoginResponse(body) + t.Logf("password response: logged_in=%t complete=%t strings=%d paths=%v fields=%v password_action=%q begin_2fa_action=%q verification_action=%q", + client.IsLoggedIn(), + parsed.isComplete(), + len(parsed.strings), + redactJetfuelDebugList(parsed.paths, identifier, password), + redactJetfuelDebugList(parsed.fields, identifier, password), + parsed.passwordAction(), + parsed.beginTwoFactorAction(), + parsed.verificationAction(), + ) + if err := parsed.loginError(); err != nil { + var webErr *WebLoginError + if errors.As(err, &webErr) { + t.Logf("password response login error: code=%d message=%q", webErr.Code, redactJetfuelDebugString(webErr.Message, identifier, password)) + } else { + t.Logf("password response login error: %T", err) + } + } + for _, line := range filteredJetfuelDebugStrings(parsed, identifier, password) { + t.Logf("password response string: %s", line) + } +} + +func TestLiveJetfuelAuthMethodSelectionProbe(t *testing.T) { + identifier := strings.TrimSpace(os.Getenv("TWITTER_LIVE_IDENTIFIER")) + password := os.Getenv("TWITTER_LIVE_PASSWORD") + methodID := strings.TrimSpace(os.Getenv("TWITTER_LIVE_AUTH_METHOD")) + if identifier == "" || password == "" || methodID == "" { + t.Skip("TWITTER_LIVE_IDENTIFIER, TWITTER_LIVE_PASSWORD, and TWITTER_LIVE_AUTH_METHOD are required") + } + + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + session := NewWebLoginSession(client) + result, err := session.Start(ctx) + logWebLoginStage(t, "start", result, err) + if err != nil { + t.Fatalf("Start() failed: %v", err) + } + result, err = session.SubmitCredentials(ctx, identifier, password) + logWebLoginStage(t, "credentials", result, err) + if err != nil { + t.Fatalf("SubmitCredentials() failed: %v", err) + } + if result.Status != WebLoginStatusNeedsAuthMethod { + t.Fatalf("SubmitCredentials() status = %s, want %s", result.Status, WebLoginStatusNeedsAuthMethod) + } + + method, ok := session.jetfuel.findAuthMethod(methodID) + if !ok { + t.Fatalf("auth method %q not found in %#v", methodID, result.AuthMethods) + } + action := session.jetfuel.twoFactorAction + if action == "" { + action = endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH + } + body, err := client.jetfuelPostForm(ctx, action, session.jetfuel.authMethodForm(method)) + if err != nil { + t.Fatalf("auth method POST failed: %v", err) + } + parsed := parseJetfuelLoginResponse(body) + t.Logf("auth method response: method=%s kind=%s supported=%t logged_in=%t complete=%t strings=%d paths=%v fields=%v begin_2fa_action=%q verification_action=%q", + method.ID, + method.Kind, + method.Supported, + client.IsLoggedIn(), + parsed.isComplete(), + len(parsed.strings), + redactJetfuelDebugList(parsed.paths, identifier, password), + redactJetfuelDebugList(parsed.fields, identifier, password), + parsed.beginTwoFactorAction(), + parsed.verificationAction(), + ) + if err := parsed.loginError(); err != nil { + var webErr *WebLoginError + if errors.As(err, &webErr) { + t.Logf("auth method response login error: code=%d message=%q", webErr.Code, redactJetfuelDebugString(webErr.Message, identifier, password)) + } else { + t.Logf("auth method response login error: %T", err) + } + } + for _, line := range filteredJetfuelDebugStrings(parsed, identifier, password) { + t.Logf("auth method response string: %s", line) + } +} + +func jetfuelProbePageURL() string { + if os.Getenv("TWITTER_ROOT_PAGE_PROBE") == "1" { + return endpoints.BASE_URL + "/" + } + return endpoints.BASE_FLOW_LOGIN_URL +} + +func TestLiveCloudflareJSDProbe(t *testing.T) { + if os.Getenv("TWITTER_CLOUDFLARE_JSD_PROBE") != "1" { + t.Skip("TWITTER_CLOUDFLARE_JSD_PROBE=1 is required") + } + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + extraHeaders := map[string]string{ + "upgrade-insecure-requests": "1", + "sec-fetch-site": "none", + "sec-fetch-user": "?1", + "sec-fetch-dest": "document", + } + resp, body, err := client.MakeRequest(ctx, endpoints.BASE_FLOW_LOGIN_URL, http.MethodGet, client.buildHeaders(HeaderOpts{Extra: extraHeaders, WithCookies: true}), nil, types.ContentTypeNone) + if resp != nil { + client.cookies.UpdateFromResponse(resp) + } + if err != nil { + t.Fatalf("login page request failed: %v", err) + } + pageURL := resp.Request.URL + scriptURL := methods.ParseCloudflareJSDURL(string(body)) + t.Logf("cloudflare jsd script present=%t", scriptURL != "") + if scriptURL == "" { + return + } + parsedScriptURL, err := pageURL.Parse(scriptURL) + if err != nil { + t.Fatalf("parse script URL failed: %v", err) + } + originalCheckRedirect := client.HTTP.CheckRedirect + client.disableRedirects() + defer func() { client.HTTP.CheckRedirect = originalCheckRedirect }() + for i := 0; i < 4; i++ { + resp, _, err = client.MakeRequest(ctx, parsedScriptURL.String(), http.MethodGet, client.buildHeaders(HeaderOpts{ + Extra: map[string]string{ + "accept": "*/*", + "sec-fetch-dest": "script", + "sec-fetch-mode": "no-cors", + "sec-fetch-site": "same-origin", + }, + Referer: pageURL.String(), + WithCookies: true, + }), nil, types.ContentTypeNone) + names := setCookieNames(resp) + if resp != nil { + client.cookies.UpdateFromResponse(resp) + t.Logf("jsd hop=%d status=%d set_cookie_names=%v cf_bm_present=%t", i, resp.StatusCode, names, !client.cookies.IsCookieEmpty(cookies.XCookieName("__cf_bm"))) + } + if !errors.Is(err, ErrRedirectAttempted) { + if err != nil { + t.Fatalf("jsd request failed: %v", err) + } + return + } + location := resp.Header.Get("Location") + if location == "" { + t.Fatalf("redirect without location") + } + parsedScriptURL, err = parsedScriptURL.Parse(location) + if err != nil { + t.Fatalf("parse redirect URL failed: %v", err) + } + } +} + +func setCookieNames(resp *http.Response) []string { + if resp == nil { + return nil + } + names := make([]string, 0) + for _, cookie := range resp.Cookies() { + names = append(names, cookie.Name) + } + return names +} + +func TestLiveJetfuelFakeCredentialsProbe(t *testing.T) { + if os.Getenv("TWITTER_FAKE_CREDENTIALS_PROBE") != "1" { + t.Skip("TWITTER_FAKE_CREDENTIALS_PROBE=1 is required") + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + session := NewWebLoginSession(client) + result, err := session.Start(ctx) + if err != nil { + t.Fatalf("Start() failed: %v", err) + } + if result.Status != WebLoginStatusNeedsIdentifier { + t.Fatalf("Start() status = %s, want %s", result.Status, WebLoginStatusNeedsIdentifier) + } + result, err = session.SubmitCredentials(ctx, "codex_probe_20260625_noacct", "not-a-real-password") + if err == nil { + t.Fatalf("SubmitCredentials() returned result %#v, want missing-account error", result) + } + var webErr *WebLoginError + if !errors.As(err, &webErr) { + t.Fatalf("SubmitCredentials() error = %T, want *WebLoginError: %v", err, err) + } + if webErr.Code != 32 { + t.Fatalf("WebLoginError.Code = %d, want missing-account/credential code 32 (%s)", webErr.Code, webErr.Message) + } + if strings.Contains(strings.ToLower(webErr.Message), "temporarily limited") { + t.Fatalf("SubmitCredentials() hit temporary-limit branch: %s", webErr.Message) + } +} + +func TestLiveOCFFakeCredentialsProbe(t *testing.T) { + if os.Getenv("TWITTER_OCF_FAKE_CREDENTIALS_PROBE") != "1" { + t.Skip("TWITTER_OCF_FAKE_CREDENTIALS_PROBE=1 is required") + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + session := NewWebLoginSession(client) + result, err := session.startOCF(ctx) + if err != nil { + t.Fatalf("startOCF() failed: %v", err) + } + if result.Status != WebLoginStatusNeedsIdentifier { + t.Fatalf("startOCF() status = %s, want %s", result.Status, WebLoginStatusNeedsIdentifier) + } + result, err = session.SubmitCredentials(ctx, "codex_probe_20260625_noacct", "not-a-real-password") + if err == nil { + t.Fatalf("SubmitCredentials() returned result %#v, want credential/missing-account error", result) + } + var webErr *WebLoginError + if !errors.As(err, &webErr) { + t.Fatalf("SubmitCredentials() error = %T, want *WebLoginError: %v", err, err) + } + if strings.Contains(strings.ToLower(webErr.Message), "temporarily limited") { + t.Fatalf("OCF SubmitCredentials() hit temporary-limit branch: %s", webErr.Message) + } + t.Logf("OCF fake credentials error: code=%d message=%q", webErr.Code, webErr.Message) +} + +func filteredJetfuelDebugStrings(parsed jetfuelLoginResponse, secrets ...string) []string { + var out []string + needles := []string{ + "password", + "login", + "verification", + "challenge", + "two", + "factor", + "error", + "message", + "limited", + "temporarily", + "castle", + "action", + "flow", + "next", + } + for _, str := range parsed.strings { + lower := strings.ToLower(str) + matched := false + for _, needle := range needles { + if strings.Contains(lower, needle) { + matched = true + break + } + } + if !matched { + continue + } + redacted := redactJetfuelDebugString(str, secrets...) + if len(redacted) > 300 { + redacted = redacted[:300] + "..." + } + out = append(out, redacted) + if len(out) >= 30 { + break + } + } + return out +} + +func redactJetfuelDebugList(values []string, secrets ...string) []string { + out := make([]string, len(values)) + for i, value := range values { + out[i] = redactJetfuelDebugString(value, secrets...) + } + return out +} + +func redactJetfuelDebugString(value string, secrets ...string) string { + for _, secret := range secrets { + if secret != "" { + value = strings.ReplaceAll(value, secret, "") + } + } + value = uuidDebugRegex.ReplaceAllString(value, "") + return value +} + +func logWebLoginStage(t *testing.T, stage string, result *WebLoginResult, err error) { + t.Helper() + if result != nil { + t.Logf("%s result: status=%s subtask=%s", stage, result.Status, result.CurrentSubtaskID) + } + if err != nil { + var webErr *WebLoginError + if errors.As(err, &webErr) { + t.Logf("%s error: code=%d message=%q", stage, webErr.Code, webErr.Message) + return + } + t.Logf("%s error: %T", stage, err) + } +} + +var uuidDebugRegex = regexp.MustCompile(`[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`) diff --git a/pkg/twittermeow/login_web.go b/pkg/twittermeow/login_web.go new file mode 100644 index 00000000..93707e87 --- /dev/null +++ b/pkg/twittermeow/login_web.go @@ -0,0 +1,715 @@ +package twittermeow + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "go.mau.fi/mautrix-twitter/pkg/twittermeow/cookies" + "go.mau.fi/mautrix-twitter/pkg/twittermeow/crypto" + "go.mau.fi/mautrix-twitter/pkg/twittermeow/data/endpoints" + "go.mau.fi/mautrix-twitter/pkg/twittermeow/data/types" +) + +const ( + webLoginSubtaskJSInstrumentation = "LoginJsInstrumentationSubtask" + webLoginSubtaskIdentifier = "LoginEnterUserIdentifierSSO" + webLoginSubtaskTwoFactor = "LoginTwoFactorAuthChallenge" + + webLoginLinkNext = "next_link" +) + +var ( + ErrWebLoginUnexpectedSubtask = errors.New("unexpected X login subtask") + ErrWebLoginMissingFlowToken = errors.New("x login response did not include a flow token") + ErrWebLoginMissingGuestToken = errors.New("x guest activation response did not include a guest token") + ErrWebLoginUnsupportedAuthMethod = errors.New("unsupported X login verification method") + ErrWebLoginMissingAuthMethodState = errors.New("x login verification method state is missing") +) + +type WebLoginStatus string + +const ( + WebLoginStatusNeedsIdentifier WebLoginStatus = "needs_identifier" + WebLoginStatusNeedsPassword WebLoginStatus = "needs_password" + WebLoginStatusNeedsText WebLoginStatus = "needs_text" + WebLoginStatusNeedsAuthMethod WebLoginStatus = "needs_auth_method" + WebLoginStatusComplete WebLoginStatus = "complete" + WebLoginStatusUnsupported WebLoginStatus = "unsupported" +) + +type WebLoginAuthMethodKind string + +const ( + WebLoginAuthMethodKindCode WebLoginAuthMethodKind = "code" + WebLoginAuthMethodKindBackupCode WebLoginAuthMethodKind = "backup_code" + WebLoginAuthMethodKindSMS WebLoginAuthMethodKind = "sms" + WebLoginAuthMethodKindUnknown WebLoginAuthMethodKind = "unknown" +) + +type WebLoginChallengeInputKind string + +const ( + WebLoginChallengeInputKindCode WebLoginChallengeInputKind = "code" + WebLoginChallengeInputKindPhoneNumber WebLoginChallengeInputKind = "phone_number" +) + +type WebLoginError struct { + Code int + Message string +} + +func (wle *WebLoginError) Error() string { + if wle == nil { + return "" + } + if wle.Code != 0 { + return fmt.Sprintf("X login failed (%d): %s", wle.Code, wle.Message) + } + return fmt.Sprintf("X login failed: %s", wle.Message) +} + +func (wle *WebLoginError) UserMessage() string { + if wle == nil { + return "X rejected this login. Please check the details and try again." + } + switch wle.Code { + case 32, 64, 89, 99: + return "X rejected the username or password. Check the details and try again." + case 88, 226, 326, 399: + return "X rejected this login attempt. Wait a bit, then try again." + default: + msg := strings.TrimSpace(wle.Message) + if msg == "" { + return "X rejected this login. Please check the details and try again." + } + return fmt.Sprintf("X rejected this login: %s", msg) + } +} + +type WebLoginChallenge struct { + SubtaskID string + Hint string + Description string + IsTwoFactor bool + InputKind WebLoginChallengeInputKind +} + +type WebLoginAuthMethod struct { + ID string + SubmitID string + Name string + Description string + Kind WebLoginAuthMethodKind + Supported bool + Index int +} + +type WebLoginResult struct { + Status WebLoginStatus + Challenge *WebLoginChallenge + AuthMethods []WebLoginAuthMethod + CurrentSubtaskID string +} + +type WebLoginSession struct { + client *Client + flowToken string + subtasks []onboardingSubtask + backend webLoginBackend + jetfuel *jetfuelLoginState +} + +func NewWebLoginSession(client *Client) *WebLoginSession { + return &WebLoginSession{client: client} +} + +func (wls *WebLoginSession) Client() *Client { + return wls.client +} + +func (wls *WebLoginSession) Start(ctx context.Context) (*WebLoginResult, error) { + if result, err := wls.startJetfuel(ctx); err == nil { + return result, nil + } else { + wls.client.Logger.Warn().Err(err).Msg("Jetfuel login start failed, falling back to OCF login") + } + return wls.startOCF(ctx) +} + +func (wls *WebLoginSession) startOCF(ctx context.Context) (*WebLoginResult, error) { + wls.backend = webLoginBackendOCF + if err := wls.client.loadPage(ctx, endpoints.BASE_FLOW_LOGIN_URL); err != nil { + return nil, fmt.Errorf("failed to load x login page: %w", err) + } + if err := wls.client.activateGuest(ctx); err != nil { + return nil, err + } + + startPayload := newWebLoginStartPayload(wls.client.session.Country) + resp, err := wls.client.sendOnboardingTask(ctx, endpoints.ONBOARDING_LOGIN_TASK_URL, startPayload) + if err != nil { + return nil, err + } + if err := wls.update(resp); err != nil { + return nil, err + } + return wls.advanceJSInstrumentation(ctx) +} + +func newWebLoginStartPayload(countryCode string) onboardingTaskRequest { + return onboardingTaskRequest{ + InputFlowData: &onboardingInputFlowData{ + FlowContext: onboardingFlowContext{ + DebugOverrides: map[string]any{}, + StartLocation: map[string]string{"location": "manual_link"}, + }, + CountryCode: countryCode, + }, + SubtaskVersions: webLoginSubtaskVersions(), + } +} + +func (wls *WebLoginSession) SubmitIdentifier(ctx context.Context, identifier string) (*WebLoginResult, error) { + if wls.backend == webLoginBackendJetfuel { + return wls.submitJetfuelIdentifier(ctx, identifier) + } + identifier = strings.TrimSpace(identifier) + if identifier == "" { + return nil, fmt.Errorf("x username, email, or phone is required") + } + st := wls.currentSubtask() + if st == nil || st.SettingsList == nil { + return nil, fmt.Errorf("%w: expected identifier settings_list, got %s", ErrWebLoginUnexpectedSubtask, subtaskName(st)) + } + link := st.SettingsList.nextLinkID() + payload := onboardingTaskRequest{ + FlowToken: wls.flowToken, + SubtaskInputs: []onboardingSubtaskInput{{ + SubtaskID: st.SubtaskID, + SettingsList: &settingsListInput{ + SettingResponses: []settingResponseInput{{ + Key: "user_identifier", + ResponseData: map[string]resultInput{ + "text_data": {Result: identifier}, + }, + }}, + Link: link, + }, + }}, + } + resp, err := wls.client.sendOnboardingTask(ctx, endpoints.ONBOARDING_TASK_URL, payload) + if err != nil { + return nil, err + } + if err := wls.update(resp); err != nil { + return nil, err + } + return wls.advanceJSInstrumentation(ctx) +} + +func (wls *WebLoginSession) SubmitCredentials(ctx context.Context, identifier, password string) (*WebLoginResult, error) { + if wls.backend == webLoginBackendJetfuel { + return wls.submitJetfuelCredentials(ctx, identifier, password) + } + result, err := wls.SubmitIdentifier(ctx, identifier) + if err != nil { + return nil, err + } + if result.Status != WebLoginStatusNeedsPassword { + return result, nil + } + return wls.SubmitPassword(ctx, password) +} + +func (wls *WebLoginSession) SubmitPassword(ctx context.Context, password string) (*WebLoginResult, error) { + if wls.backend == webLoginBackendJetfuel { + return wls.submitJetfuelPassword(ctx, password) + } + if password == "" { + return nil, fmt.Errorf("x password is required") + } + st := wls.currentSubtask() + if st == nil || st.EnterPassword == nil { + return nil, fmt.Errorf("%w: expected enter_password, got %s", ErrWebLoginUnexpectedSubtask, subtaskName(st)) + } + link := st.EnterPassword.nextLinkID() + payload := onboardingTaskRequest{ + FlowToken: wls.flowToken, + SubtaskInputs: []onboardingSubtaskInput{{ + SubtaskID: st.SubtaskID, + EnterPassword: &enterPasswordInput{ + Password: password, + Link: link, + }, + }}, + } + resp, err := wls.client.sendOnboardingTask(ctx, endpoints.ONBOARDING_TASK_URL, payload) + if err != nil { + return nil, err + } + if err := wls.update(resp); err != nil { + return nil, err + } + return wls.advanceJSInstrumentation(ctx) +} + +func (wls *WebLoginSession) SubmitAuthMethod(ctx context.Context, methodID string) (*WebLoginResult, error) { + if wls.backend == webLoginBackendJetfuel { + return wls.submitJetfuelAuthMethod(ctx, methodID) + } + return nil, fmt.Errorf("%w: auth method selection is unsupported for %s login", ErrWebLoginUnexpectedSubtask, wls.backend) +} + +func (wls *WebLoginSession) SubmitText(ctx context.Context, text string) (*WebLoginResult, error) { + if wls.backend == webLoginBackendJetfuel { + return wls.submitJetfuelText(ctx, text) + } + text = strings.TrimSpace(text) + if text == "" { + return nil, fmt.Errorf("x verification code is required") + } + st := wls.currentSubtask() + if st == nil || st.EnterText == nil { + return nil, fmt.Errorf("%w: expected enter_text, got %s", ErrWebLoginUnexpectedSubtask, subtaskName(st)) + } + link := st.EnterText.nextLinkID() + payload := onboardingTaskRequest{ + FlowToken: wls.flowToken, + SubtaskInputs: []onboardingSubtaskInput{{ + SubtaskID: st.SubtaskID, + EnterText: &enterTextInput{ + Text: text, + Link: link, + }, + }}, + } + resp, err := wls.client.sendOnboardingTask(ctx, endpoints.ONBOARDING_TASK_URL, payload) + if err != nil { + return nil, err + } + if err := wls.update(resp); err != nil { + return nil, err + } + return wls.advanceJSInstrumentation(ctx) +} + +func (wls *WebLoginSession) advanceJSInstrumentation(ctx context.Context) (*WebLoginResult, error) { + for range 3 { + st := wls.currentSubtask() + if st == nil || st.JSInstrumentation == nil { + return wls.result(), nil + } + metrics := "{}" + payload := onboardingTaskRequest{ + FlowToken: wls.flowToken, + SubtaskInputs: []onboardingSubtaskInput{{ + SubtaskID: st.SubtaskID, + JSInstrumentation: &jsInstrumentationInput{ + Response: metrics, + Link: webLoginLinkNext, + }, + }}, + } + resp, err := wls.client.sendOnboardingTask(ctx, endpoints.ONBOARDING_TASK_URL, payload) + if err != nil { + return nil, err + } + if err := wls.update(resp); err != nil { + return nil, err + } + } + return nil, fmt.Errorf("%w: JS instrumentation loop did not settle", ErrWebLoginUnexpectedSubtask) +} + +func (wls *WebLoginSession) update(resp *onboardingTaskResponse) error { + if resp == nil { + return fmt.Errorf("x login response was empty") + } + if resp.FlowToken == "" && !wls.client.IsLoggedIn() { + return ErrWebLoginMissingFlowToken + } + if resp.FlowToken != "" { + wls.flowToken = resp.FlowToken + } + wls.subtasks = resp.Subtasks + return nil +} + +func (wls *WebLoginSession) result() *WebLoginResult { + if wls.client != nil && wls.client.IsLoggedIn() { + return &WebLoginResult{Status: WebLoginStatusComplete} + } + st := wls.currentSubtask() + if st == nil { + return &WebLoginResult{Status: WebLoginStatusUnsupported} + } + result := &WebLoginResult{CurrentSubtaskID: st.SubtaskID} + switch { + case st.JSInstrumentation != nil: + result.Status = WebLoginStatusUnsupported + case st.SettingsList != nil: + result.Status = WebLoginStatusNeedsIdentifier + result.Challenge = &WebLoginChallenge{ + SubtaskID: st.SubtaskID, + Hint: st.SettingsList.identifierHint(), + Description: richTextText(st.SettingsList.DetailText), + } + case st.EnterPassword != nil: + result.Status = WebLoginStatusNeedsPassword + result.Challenge = &WebLoginChallenge{ + SubtaskID: st.SubtaskID, + Hint: st.EnterPassword.hint(), + Description: richTextText(st.EnterPassword.SecondaryText), + } + case st.EnterText != nil: + result.Status = WebLoginStatusNeedsText + result.Challenge = &WebLoginChallenge{ + SubtaskID: st.SubtaskID, + Hint: st.EnterText.hint(), + Description: richTextText(st.EnterText.DetailText), + IsTwoFactor: st.SubtaskID == webLoginSubtaskTwoFactor, + } + case st.OpenAccount != nil || st.OpenHomeTimeline != nil || st.EndFlow != nil: + result.Status = WebLoginStatusComplete + default: + result.Status = WebLoginStatusUnsupported + } + return result +} + +func (wls *WebLoginSession) currentSubtask() *onboardingSubtask { + for i := range wls.subtasks { + st := &wls.subtasks[i] + if st.JSInstrumentation != nil || st.SettingsList != nil || st.EnterPassword != nil || + st.EnterText != nil || st.OpenAccount != nil || st.OpenHomeTimeline != nil || st.EndFlow != nil { + return st + } + } + if len(wls.subtasks) == 0 { + return nil + } + return &wls.subtasks[0] +} + +func subtaskName(st *onboardingSubtask) string { + if st == nil { + return "" + } + if st.SubtaskID == "" { + return "" + } + return st.SubtaskID +} + +func (c *Client) activateGuest(ctx context.Context) error { + resp, respBody, err := c.MakeRequest(ctx, endpoints.GUEST_ACTIVATE_URL, http.MethodPost, c.buildHeaders(HeaderOpts{ + WithNonAuthBearer: true, + WithCookies: true, + WithXTwitterHeaders: true, + Origin: endpoints.BASE_URL, + Referer: endpoints.BASE_FLOW_LOGIN_URL, + }), []byte(`{}`), types.ContentTypeJSON) + if resp != nil { + c.cookies.UpdateFromResponse(resp) + } + if err != nil { + return fmt.Errorf("failed to activate X guest session: %w", err) + } + var guest struct { + GuestToken string `json:"guest_token"` + } + if err = json.Unmarshal(respBody, &guest); err != nil { + return fmt.Errorf("failed to parse X guest activation response: %w", err) + } + if guest.GuestToken == "" { + return ErrWebLoginMissingGuestToken + } + c.cookies.Set(cookies.XGuestToken, guest.GuestToken) + return nil +} + +func (c *Client) sendOnboardingTask(ctx context.Context, url string, payload onboardingTaskRequest) (*onboardingTaskResponse, error) { + body, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to encode X login task: %w", err) + } + txID, err := crypto.SignTransaction(c.session.AnimationToken, c.session.VerificationToken, url, http.MethodPost) + if err != nil { + c.Logger.Trace().Err(err).Msg("Failed to create X login client transaction ID") + txID = "e:" + } + resp, respBody, err := c.makeRequestDirect(ctx, url, http.MethodPost, c.buildHeaders(HeaderOpts{ + WithNonAuthBearer: true, + WithCookies: true, + WithXTwitterHeaders: true, + WithXGuestToken: true, + Origin: endpoints.BASE_URL, + Referer: endpoints.BASE_FLOW_LOGIN_URL, + Extra: map[string]string{ + "x-client-transaction-id": txID, + "accept": "*/*", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + }, + }), body, types.ContentTypeJSON) + if resp != nil { + c.cookies.UpdateFromResponse(resp) + } + taskResp := &onboardingTaskResponse{} + if len(respBody) > 0 { + if unmarshalErr := json.Unmarshal(respBody, taskResp); unmarshalErr != nil && err == nil { + return nil, fmt.Errorf("failed to parse X login task response: %w", unmarshalErr) + } + } + if len(taskResp.Errors) > 0 { + return taskResp, &WebLoginError{ + Code: taskResp.Errors[0].Code, + Message: taskResp.Errors[0].Message, + } + } + if err != nil { + return taskResp, err + } + return taskResp, nil +} + +type onboardingTaskRequest struct { + FlowToken string `json:"flow_token,omitempty"` + InputFlowData *onboardingInputFlowData `json:"input_flow_data,omitempty"` + SubtaskVersions map[string]int `json:"subtask_versions,omitempty"` + SubtaskInputs []onboardingSubtaskInput `json:"subtask_inputs,omitempty"` +} + +type onboardingInputFlowData struct { + FlowContext onboardingFlowContext `json:"flow_context"` + CountryCode string `json:"country_code,omitempty"` +} + +type onboardingFlowContext struct { + DebugOverrides map[string]any `json:"debug_overrides"` + StartLocation map[string]string `json:"start_location"` +} + +type onboardingSubtaskInput struct { + SubtaskID string `json:"subtask_id"` + JSInstrumentation *jsInstrumentationInput `json:"js_instrumentation,omitempty"` + SettingsList *settingsListInput `json:"settings_list,omitempty"` + EnterPassword *enterPasswordInput `json:"enter_password,omitempty"` + EnterText *enterTextInput `json:"enter_text,omitempty"` +} + +type jsInstrumentationInput struct { + Response string `json:"response"` + Link string `json:"link"` +} + +type settingsListInput struct { + SettingResponses []settingResponseInput `json:"setting_responses"` + Link string `json:"link"` + CastleToken string `json:"castle_token,omitempty"` +} + +type settingResponseInput struct { + Key string `json:"key"` + ResponseData map[string]resultInput `json:"response_data"` +} + +type resultInput struct { + Result any `json:"result"` +} + +type enterPasswordInput struct { + Password string `json:"password"` + Link string `json:"link"` + CastleToken string `json:"castle_token,omitempty"` +} + +type enterTextInput struct { + Text string `json:"text"` + Link string `json:"link"` + CastleToken string `json:"castle_token,omitempty"` +} + +type onboardingTaskResponse struct { + FlowToken string `json:"flow_token"` + Subtasks []onboardingSubtask `json:"subtasks"` + Errors []TwitterError `json:"errors"` +} + +type onboardingSubtask struct { + SubtaskID string `json:"subtask_id"` + JSInstrumentation *jsInstrumentationSubtask `json:"js_instrumentation,omitempty"` + SettingsList *settingsListSubtask `json:"settings_list,omitempty"` + EnterPassword *enterPasswordSubtask `json:"enter_password,omitempty"` + EnterText *enterTextSubtask `json:"enter_text,omitempty"` + OpenAccount *struct{} `json:"open_account,omitempty"` + OpenHomeTimeline *struct{} `json:"open_home_timeline,omitempty"` + EndFlow *struct{} `json:"end_flow,omitempty"` +} + +type jsInstrumentationSubtask struct { + URL string `json:"url"` +} + +type settingsListSubtask struct { + Settings []settingsListSetting `json:"settings"` + NextLink *navigationLink `json:"next_link,omitempty"` + DetailText *richText `json:"detail_text,omitempty"` +} + +type settingsListSetting struct { + ValueIdentifier string `json:"value_identifier"` + ValueType string `json:"value_type"` + ValueData *settingValueData `json:"value_data,omitempty"` +} + +type settingValueData struct { + TextField *textFieldData `json:"text_field,omitempty"` + Button *buttonData `json:"button,omitempty"` +} + +type textFieldData struct { + HintText string `json:"hint_text"` +} + +type buttonData struct { + NavigationLink *navigationLink `json:"navigation_link,omitempty"` +} + +type enterPasswordSubtask struct { + Hint string `json:"hint"` + PasswordField *textFieldData `json:"password_field,omitempty"` + NextLink *navigationLink `json:"next_link,omitempty"` + SecondaryText *richText `json:"secondary_text,omitempty"` +} + +type enterTextSubtask struct { + HintText string `json:"hint_text"` + DetailText *richText `json:"detail_text,omitempty"` + NextLink *navigationLink `json:"next_link,omitempty"` +} + +type navigationLink struct { + LinkID string `json:"link_id"` +} + +type richText struct { + Text string `json:"text"` +} + +func (sls *settingsListSubtask) nextLinkID() string { + if sls == nil { + return webLoginLinkNext + } + if sls.NextLink != nil && sls.NextLink.LinkID != "" { + return sls.NextLink.LinkID + } + for _, setting := range sls.Settings { + if setting.ValueIdentifier == "next_button" && setting.ValueData != nil && + setting.ValueData.Button != nil && setting.ValueData.Button.NavigationLink != nil && + setting.ValueData.Button.NavigationLink.LinkID != "" { + return setting.ValueData.Button.NavigationLink.LinkID + } + } + return webLoginLinkNext +} + +func (sls *settingsListSubtask) identifierHint() string { + if sls == nil { + return "" + } + for _, setting := range sls.Settings { + if setting.ValueIdentifier == "user_identifier" && setting.ValueData != nil && setting.ValueData.TextField != nil { + return setting.ValueData.TextField.HintText + } + } + return "" +} + +func (eps *enterPasswordSubtask) nextLinkID() string { + if eps != nil && eps.NextLink != nil && eps.NextLink.LinkID != "" { + return eps.NextLink.LinkID + } + return webLoginLinkNext +} + +func (eps *enterPasswordSubtask) hint() string { + if eps == nil { + return "" + } + if eps.PasswordField != nil && eps.PasswordField.HintText != "" { + return eps.PasswordField.HintText + } + return eps.Hint +} + +func (ets *enterTextSubtask) nextLinkID() string { + if ets != nil && ets.NextLink != nil && ets.NextLink.LinkID != "" { + return ets.NextLink.LinkID + } + return webLoginLinkNext +} + +func (ets *enterTextSubtask) hint() string { + if ets == nil { + return "" + } + return ets.HintText +} + +func richTextText(rt *richText) string { + if rt == nil { + return "" + } + return strings.TrimSpace(rt.Text) +} + +func webLoginSubtaskVersions() map[string]int { + return map[string]int{ + "action_list": 2, + "alert_dialog": 1, + "app_download_cta": 1, + "check_logged_in_account": 1, + "choice_selection": 3, + "contacts_live_sync_permission_prompt": 0, + "cta": 7, + "email_verification": 2, + "end_flow": 1, + "enter_date": 1, + "enter_email": 2, + "enter_password": 5, + "enter_phone": 2, + "enter_recaptcha": 1, + "enter_text": 5, + "enter_username": 2, + "generic_urt": 3, + "in_app_notification": 1, + "interest_picker": 3, + "js_instrumentation": 1, + "menu_dialog": 1, + "notifications_permission_prompt": 2, + "open_account": 2, + "open_home_timeline": 1, + "open_link": 1, + "phone_verification": 4, + "privacy_options": 1, + "select_avatar": 4, + "select_banner": 2, + "settings_list": 7, + "show_code": 1, + "sign_up": 2, + "sign_up_review": 4, + "tweet_selection_urt": 1, + "update_users": 1, + "upload_media": 1, + "user_recommendations_list": 4, + "user_recommendations_urt": 1, + "wait_spinner": 3, + "web_modal": 1, + } +} diff --git a/pkg/twittermeow/login_web_test.go b/pkg/twittermeow/login_web_test.go new file mode 100644 index 00000000..90878115 --- /dev/null +++ b/pkg/twittermeow/login_web_test.go @@ -0,0 +1,782 @@ +package twittermeow + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/rs/zerolog" + + "go.mau.fi/mautrix-twitter/pkg/twittermeow/cookies" + "go.mau.fi/mautrix-twitter/pkg/twittermeow/data/endpoints" +) + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (rtf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return rtf(req) +} + +func TestSettingsListIdentifierPayloadShape(t *testing.T) { + payload := onboardingTaskRequest{ + FlowToken: "flow-token", + SubtaskInputs: []onboardingSubtaskInput{{ + SubtaskID: webLoginSubtaskIdentifier, + SettingsList: &settingsListInput{ + SettingResponses: []settingResponseInput{{ + Key: "user_identifier", + ResponseData: map[string]resultInput{ + "text_data": {Result: "example"}, + }, + }}, + Link: webLoginLinkNext, + }, + }}, + } + body, err := json.Marshal(payload) + if err != nil { + t.Fatalf("Marshal() error = %v", err) + } + + var got map[string]any + if err = json.Unmarshal(body, &got); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + inputs := got["subtask_inputs"].([]any) + settingsList := inputs[0].(map[string]any)["settings_list"].(map[string]any) + responses := settingsList["setting_responses"].([]any) + firstResponse := responses[0].(map[string]any) + responseData := firstResponse["response_data"].(map[string]any) + textData := responseData["text_data"].(map[string]any) + + if firstResponse["key"] != "user_identifier" { + t.Fatalf("setting response key = %v, want user_identifier", firstResponse["key"]) + } + if textData["result"] != "example" { + t.Fatalf("text_data.result = %v, want example", textData["result"]) + } + if settingsList["link"] != webLoginLinkNext { + t.Fatalf("settings_list.link = %v, want %s", settingsList["link"], webLoginLinkNext) + } + if _, ok := settingsList["castle_token"]; ok { + t.Fatalf("castle_token should be omitted when empty") + } +} + +func TestWebLoginStartPayloadShape(t *testing.T) { + payload := newWebLoginStartPayload("US") + body, err := json.Marshal(payload) + if err != nil { + t.Fatalf("Marshal() error = %v", err) + } + + var got map[string]any + if err = json.Unmarshal(body, &got); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + inputFlowData := got["input_flow_data"].(map[string]any) + flowContext := inputFlowData["flow_context"].(map[string]any) + startLocation := flowContext["start_location"].(map[string]any) + subtaskVersions := got["subtask_versions"].(map[string]any) + + if inputFlowData["country_code"] != "US" { + t.Fatalf("country_code = %v, want US", inputFlowData["country_code"]) + } + if startLocation["location"] != "manual_link" { + t.Fatalf("start_location.location = %v, want manual_link", startLocation["location"]) + } + if subtaskVersions["settings_list"] != float64(7) { + t.Fatalf("settings_list version = %v, want 7", subtaskVersions["settings_list"]) + } +} + +func TestWebLoginResultClassifiesTwoFactor(t *testing.T) { + session := &WebLoginSession{ + subtasks: []onboardingSubtask{{ + SubtaskID: webLoginSubtaskTwoFactor, + EnterText: &enterTextSubtask{ + HintText: "Enter your code", + NextLink: &navigationLink{LinkID: webLoginLinkNext}, + }, + }}, + } + result := session.result() + if result.Status != WebLoginStatusNeedsText { + t.Fatalf("Status = %s, want %s", result.Status, WebLoginStatusNeedsText) + } + if result.Challenge == nil || !result.Challenge.IsTwoFactor { + t.Fatalf("Challenge = %#v, want two-factor challenge", result.Challenge) + } + if result.Challenge.Hint != "Enter your code" { + t.Fatalf("Challenge.Hint = %q", result.Challenge.Hint) + } +} + +func TestParseJetfuelLoginResponseFindsActionsAndFields(t *testing.T) { + body := []byte{ + 0x03, 0x00, 0xff, + } + body = append(body, []byte("username_or_email\x00/onboarding/web/actions/begin_login\x00password\x00/onboarding/web/actions/login_enter_password\x00session_token\x0012345678-1234-1234-1234-123456789abc")...) + + parsed := parseJetfuelLoginResponse(body) + if !parsed.hasField("username_or_email") { + t.Fatalf("username_or_email field not found in %#v", parsed.fields) + } + if !parsed.hasPath("/onboarding/web/actions/begin_login") { + t.Fatalf("begin_login path not found in %#v", parsed.paths) + } + if action := parsed.passwordAction(); action != endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH { + t.Fatalf("passwordAction() = %q", action) + } + if token := parsed.uuidValue("session_token"); token != "12345678-1234-1234-1234-123456789abc" { + t.Fatalf("uuidValue(session_token) = %q", token) + } +} + +func TestJetfuelLoginResponseSeparatesTwoFactorPreludeFromCodeAction(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("prelude_dispatch_id\x00aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\x00/onboarding/web/actions/begin_two_factor_auth\x00/onboarding/web/actions/two_factor_code")) + + if action := parsed.beginTwoFactorAction(); action != endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH { + t.Fatalf("beginTwoFactorAction() = %q", action) + } + if action := parsed.verificationAction(); action != "/onboarding/web/actions/two_factor_code" { + t.Fatalf("verificationAction() = %q", action) + } + if id := parsed.uuidValue("prelude_dispatch_id"); id != "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" { + t.Fatalf("uuidValue(prelude_dispatch_id) = %q", id) + } +} + +func TestJetfuelLoginResponseExpandsBareTwoFactorActions(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("prelude_dispatch_id\x00aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\x00begin_two_factor_auth\x00finish_two_factor_auth\x00challenge_response")) + + if action := parsed.beginTwoFactorAction(); action != endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH { + t.Fatalf("beginTwoFactorAction() = %q", action) + } + if action := parsed.verificationAction(); action != "/onboarding/web/actions/finish_two_factor_auth" { + t.Fatalf("verificationAction() = %q", action) + } +} + +func TestJetfuelLoginResponseFindsSMSPhoneAction(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte( + "Confirm the phone number associated with your X account.\x00phone_number\x00/onboarding/web/actions/send_sms_code\x00session_token", + )) + + method := WebLoginAuthMethod{Kind: WebLoginAuthMethodKindSMS} + if action := parsed.verificationActionForMethod(method); action != "/onboarding/web/actions/send_sms_code" { + t.Fatalf("verificationActionForMethod(SMS) = %q, want send_sms_code action", action) + } +} + +func TestJetfuelLoginResponseFindsSMSCodeAction(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte( + "We sent a text message with a verification code to your phone.\x00challenge_response\x00/onboarding/web/actions/enter_sms_pin\x00session_token", + )) + + method := WebLoginAuthMethod{Kind: WebLoginAuthMethodKindSMS} + if action := parsed.verificationActionForMethod(method); action != "/onboarding/web/actions/enter_sms_pin" { + t.Fatalf("verificationActionForMethod(SMS) = %q, want enter_sms_pin action", action) + } +} + +func TestJetfuelLoginResponseFindsTwoFactorCodeFields(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("authentication code\x00session_token\x00challenge_response\x00verification_code\x00two_factor_code\x00prelude_dispatch_id")) + fields := parsed.verificationCodeFields() + want := []string{"challenge_response", "verification_code", "two_factor_code"} + if strings.Join(fields, ",") != strings.Join(want, ",") { + t.Fatalf("verificationCodeFields() = %#v, want %#v", fields, want) + } +} + +func TestJetfuelLoginResponseFindsBackupCodeFields(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("Enter a backup code from X.\x00backup_code\x00challenge_response\x00session_token\x00castle_token")) + fields := parsed.verificationCodeFields() + want := []string{"backup_code", "challenge_response"} + if strings.Join(fields, ",") != strings.Join(want, ",") { + t.Fatalf("verificationCodeFields() = %#v, want %#v", fields, want) + } +} + +func TestJetfuelLoginResponseFindsPhoneNumberFields(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("Enter the phone number associated with your X account.\x00phone_number\x00challenge_response\x00session_token")) + fields := parsed.verificationCodeFields() + want := []string{"phone_number"} + if strings.Join(fields, ",") != strings.Join(want, ",") { + t.Fatalf("verificationCodeFields() = %#v, want %#v", fields, want) + } +} + +func TestJetfuelLoginResponseBuildsPhoneNumberChallenge(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("Confirm the phone number associated with your X account.\x00phone_number\x00session_token")) + challenge := parsed.verificationChallenge() + if challenge.Description != "Enter the phone number associated with your X account." { + t.Fatalf("verificationChallenge().Description = %q", challenge.Description) + } + if challenge.Hint != "Phone number" { + t.Fatalf("verificationChallenge().Hint = %q", challenge.Hint) + } + if challenge.InputKind != WebLoginChallengeInputKindPhoneNumber { + t.Fatalf("verificationChallenge().InputKind = %q, want phone_number", challenge.InputKind) + } + if challenge.IsTwoFactor { + t.Fatal("verificationChallenge().IsTwoFactor = true, want false for phone number input") + } +} + +func TestJetfuelLoginResponseKeepsSMSCodeAsTwoFactorCode(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("We sent a text message with a verification code to your phone.\x00challenge_response\x00session_token")) + challenge := parsed.verificationChallenge() + if challenge.InputKind != WebLoginChallengeInputKindCode { + t.Fatalf("verificationChallenge().InputKind = %q, want code", challenge.InputKind) + } + if challenge.Description != "Enter the code sent to your phone number." { + t.Fatalf("verificationChallenge().Description = %q", challenge.Description) + } + if !challenge.IsTwoFactor { + t.Fatal("verificationChallenge().IsTwoFactor = false, want true") + } +} + +func TestJetfuelLoginResponseUsesAuthenticatorAppCopyForBackupCodePrompt(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("Enter a backup code from X.\x00challenge_response\x00Totp\x00BackupCode\x00session_token")) + challenge := parsed.verificationChallenge() + if challenge.Description != "Enter the code from your authentication app." { + t.Fatalf("verificationChallenge().Description = %q", challenge.Description) + } + if !challenge.IsTwoFactor { + t.Fatal("verificationChallenge().IsTwoFactor = false, want true") + } +} + +func TestJetfuelLoginResponseDoesNotTreatChallengeModesAsFields(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("Enter a backup code from X.\x00challenge_response\x00Totp\x00BackupCode\x00session_token")) + fields := parsed.verificationCodeFields() + want := []string{"challenge_response"} + if strings.Join(fields, ",") != strings.Join(want, ",") { + t.Fatalf("verificationCodeFields() = %#v, want %#v", fields, want) + } +} + +func TestJetfuelLoginResponseFindsAuthMethodChoice(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte( + "Select a method to authenticate\x00Choose the method you prefer to use for 2-step verification.\x00two_factor_method\x00" + + "Totp\x00Sms\x00BackupCode\x00U2fSecurityKey\x00user_id\x001127993589949243392\x00session_token\x0012345678-1234-1234-1234-123456789abc\x00begin_two_factor_auth", + )) + methods := parsed.authMethods() + if len(methods) != 4 { + t.Fatalf("authMethods() length = %d, want 4: %#v", len(methods), methods) + } + wantIDs := []string{"Totp", "Sms", "BackupCode", "U2fSecurityKey"} + for i, want := range wantIDs { + if methods[i].ID != want || methods[i].Index != i { + t.Fatalf("method[%d] = %#v, want ID %s index %d", i, methods[i], want, i) + } + } + if !methods[0].Supported || methods[1].Supported || !methods[2].Supported { + t.Fatalf("only authenticator app and backup code should be supported: %#v", methods) + } + if methods[1].Kind != WebLoginAuthMethodKindSMS { + t.Fatalf("sms method = %#v, want SMS method", methods[1]) + } + if methods[3].Supported || methods[3].Kind != WebLoginAuthMethodKindUnknown { + t.Fatalf("security key method = %#v, want known unsupported method", methods[3]) + } + supported := supportedWebLoginAuthMethods(methods) + if len(supported) != 2 { + t.Fatalf("supportedWebLoginAuthMethods() length = %d, want 2: %#v", len(supported), supported) + } + if got := parsed.numericValue("user_id"); got != "1127993589949243392" { + t.Fatalf("numericValue(user_id) = %q", got) + } + if action := parsed.beginTwoFactorAction(); action != endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH { + t.Fatalf("beginTwoFactorAction() = %q", action) + } +} + +func TestJetfuelLoginResponseFindsBareAuthMethodChoice(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte( + "Totp\x00Text\x00BackupCode\x00U2fSecurityKey\x00user_id\x001127993589949243392\x00begin_two_factor_auth", + )) + session := &WebLoginSession{jetfuel: &jetfuelLoginState{}} + session.updateJetfuelState(parsed) + + result := session.jetfuelAuthMethodChoiceResult(parsed) + if result == nil { + t.Fatal("jetfuelAuthMethodChoiceResult() returned nil") + } + if result.Status != WebLoginStatusNeedsAuthMethod { + t.Fatalf("Status = %s, want %s", result.Status, WebLoginStatusNeedsAuthMethod) + } + if len(result.AuthMethods) != 2 { + t.Fatalf("AuthMethods length = %d, want 2 supported methods: %#v", len(result.AuthMethods), result.AuthMethods) + } + wantIDs := []string{"Totp", "BackupCode"} + for i, want := range wantIDs { + if result.AuthMethods[i].ID != want { + t.Fatalf("AuthMethods[%d].ID = %q, want %q", i, result.AuthMethods[i].ID, want) + } + } + if session.jetfuel.twoFactorAction != endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH { + t.Fatalf("twoFactorAction = %q, want begin_two_factor_auth", session.jetfuel.twoFactorAction) + } +} + +func TestJetfuelLoginResponsePrefersSMSSubmitToken(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte( + "Select a method to authenticate\x00two_factor_method\x00Text Message\x00Totp\x00Text\x00BackupCode\x00begin_two_factor_auth", + )) + methods := parsed.authMethods() + if len(methods) != 3 { + t.Fatalf("authMethods() length = %d, want 3: %#v", len(methods), methods) + } + if methods[0].ID != "Sms" { + t.Fatalf("authMethods()[0].ID = %q, want Sms", methods[0].ID) + } + if methods[0].SubmitID != "Text" { + t.Fatalf("authMethods()[0].SubmitID = %q, want Text", methods[0].SubmitID) + } + if methods[0].Index != 0 { + t.Fatalf("authMethods()[0].Index = %d, want 0", methods[0].Index) + } +} + +func TestJetfuelLoginResponseDoesNotTreatCodePromptAsAuthMethodChoice(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("Enter your two factor code\x00Use your authenticator app to generate the code.\x00challenge_response\x00Totp\x00BackupCode\x00session_token")) + if methods := parsed.authMethods(); len(methods) != 0 { + t.Fatalf("authMethods() = %#v, want none for code prompt", methods) + } +} + +func TestJetfuelLoginResponseClassifiesPhoneAuthMethodAliases(t *testing.T) { + tests := []struct { + raw string + submitID string + }{ + {raw: "Sms"}, + {raw: "SMS"}, + {raw: "Text", submitID: "Text"}, + {raw: "Text message"}, + {raw: "Phone number"}, + {raw: "TextMessage", submitID: "TextMessage"}, + {raw: "PhoneNumber", submitID: "PhoneNumber"}, + } + for _, test := range tests { + method, ok := classifyJetfuelAuthMethod(test.raw) + if !ok { + t.Fatalf("classifyJetfuelAuthMethod(%q) returned false", test.raw) + } + if method.ID != "Sms" || method.Kind != WebLoginAuthMethodKindSMS || method.Supported { + t.Fatalf("classifyJetfuelAuthMethod(%q) = %#v, want coming-soon SMS method", test.raw, method) + } + if method.SubmitID != test.submitID { + t.Fatalf("classifyJetfuelAuthMethod(%q).SubmitID = %q, want %q", test.raw, method.SubmitID, test.submitID) + } + } +} + +func TestJetfuelAuthMethodFormShape(t *testing.T) { + state := &jetfuelLoginState{ + sessionToken: "session-token", + preludeDispatchID: "prelude-id", + userID: "1127993589949243392", + } + form := state.authMethodForm(WebLoginAuthMethod{ID: "Totp", Index: 0}) + + if got := form.Get("two_factor_auth_method_type"); got != "Totp" { + t.Fatalf("two_factor_auth_method_type = %q", got) + } + if got := form.Get("_selected_method_idx"); got != "0" { + t.Fatalf("_selected_method_idx = %q", got) + } + if got := form.Get("user_id"); got != "1127993589949243392" { + t.Fatalf("user_id = %q", got) + } + if got := form.Get("session_token"); got != "session-token" { + t.Fatalf("session_token = %q", got) + } + if got := form.Get("prelude_dispatch_id"); got != "" { + t.Fatalf("prelude_dispatch_id = %q, want omitted", got) + } +} + +func TestJetfuelAuthMethodFormUsesSubmitID(t *testing.T) { + state := &jetfuelLoginState{} + form := state.authMethodForm(WebLoginAuthMethod{ID: "Sms", SubmitID: "TextMessage", Index: 1}) + + if got := form.Get("two_factor_auth_method_type"); got != "TextMessage" { + t.Fatalf("two_factor_auth_method_type = %q, want TextMessage", got) + } + if got := form.Get("_selected_method_idx"); got != "1" { + t.Fatalf("_selected_method_idx = %q, want 1", got) + } +} + +func TestSubmitJetfuelAuthMethodPrefersVerificationChallenge(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.HTTP = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if req.URL.Path != "/i/jfapi"+endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH { + t.Fatalf("request path = %s", req.URL.Path) + } + body, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("ReadAll(request body) error = %v", err) + } + form := string(body) + if !strings.Contains(form, "two_factor_auth_method_type=Totp") { + t.Fatalf("request body = %q, want Totp method", form) + } + responseBody := "Select a method to authenticate\x00two_factor_method\x00Totp\x00BackupCode\x00U2fSecurityKey\x00" + + "Enter the code from your authentication app.\x00challenge_response\x00finish_two_factor_auth\x00session_token" + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(responseBody)), + }, nil + })} + session := NewWebLoginSession(client) + session.backend = webLoginBackendJetfuel + session.jetfuel = &jetfuelLoginState{ + sessionToken: "session-token", + twoFactorAction: endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH, + twoFactorMethods: []WebLoginAuthMethod{ + {ID: "Totp", Name: "Authenticator App", Kind: WebLoginAuthMethodKindCode, Supported: true}, + {ID: "BackupCode", Name: "Backup Code", Kind: WebLoginAuthMethodKindBackupCode, Supported: true, Index: 1}, + {ID: "U2fSecurityKey", Name: "Security Key PC", Kind: WebLoginAuthMethodKindUnknown, Supported: false, Index: 2}, + }, + } + + result, err := session.SubmitAuthMethod(context.Background(), "Authenticator App") + if err != nil { + t.Fatalf("SubmitAuthMethod() error = %v", err) + } + if result.Status != WebLoginStatusNeedsText { + t.Fatalf("SubmitAuthMethod() status = %s, want %s", result.Status, WebLoginStatusNeedsText) + } + if result.Challenge == nil || result.Challenge.Description != "Enter the code from your authentication app." { + t.Fatalf("Challenge = %#v, want authenticator app code prompt", result.Challenge) + } + if session.jetfuel.verificationAction != endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH { + t.Fatalf("verificationAction = %q", session.jetfuel.verificationAction) + } +} + +func TestSubmitJetfuelSMSAuthMethodReturnsPhoneChallenge(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.HTTP = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if req.URL.Path != "/i/jfapi"+endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH { + t.Fatalf("request path = %s", req.URL.Path) + } + body, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("ReadAll(request body) error = %v", err) + } + form, err := url.ParseQuery(string(body)) + if err != nil { + t.Fatalf("ParseQuery(request body) error = %v", err) + } + if got := form.Get("two_factor_auth_method_type"); got != "Sms" { + t.Fatalf("two_factor_auth_method_type = %q, want Sms", got) + } + if got := form.Get("_selected_method_idx"); got != "1" { + t.Fatalf("_selected_method_idx = %q, want 1", got) + } + responseBody := "We sent a text message with a verification code to your phone.\x00challenge_response\x00finish_two_factor_auth\x00session_token" + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(responseBody)), + }, nil + })} + session := NewWebLoginSession(client) + session.backend = webLoginBackendJetfuel + session.jetfuel = &jetfuelLoginState{ + sessionToken: "session-token", + twoFactorAction: endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH, + twoFactorMethods: []WebLoginAuthMethod{ + {ID: "Totp", Name: "Authenticator App", Kind: WebLoginAuthMethodKindCode, Supported: true}, + {ID: "Sms", Name: "Text Message", Kind: WebLoginAuthMethodKindSMS, Supported: true, Index: 1}, + {ID: "BackupCode", Name: "Backup Code", Kind: WebLoginAuthMethodKindBackupCode, Supported: true, Index: 2}, + }, + } + + result, err := session.SubmitAuthMethod(context.Background(), "Text Message") + if err != nil { + t.Fatalf("SubmitAuthMethod() error = %v", err) + } + if result.Status != WebLoginStatusNeedsText { + t.Fatalf("SubmitAuthMethod() status = %s, want %s", result.Status, WebLoginStatusNeedsText) + } + if result.Challenge == nil || result.Challenge.Description != "Enter the code sent to your phone number." { + t.Fatalf("Challenge = %#v, want phone code prompt", result.Challenge) + } + if session.jetfuel.verificationAction != endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH { + t.Fatalf("verificationAction = %q", session.jetfuel.verificationAction) + } +} + +func TestSubmitJetfuelSMSAuthMethodDefaultsActionForPhoneChallenge(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.HTTP = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if req.URL.Path != "/i/jfapi"+endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH { + t.Fatalf("request path = %s", req.URL.Path) + } + body, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("ReadAll(request body) error = %v", err) + } + form, err := url.ParseQuery(string(body)) + if err != nil { + t.Fatalf("ParseQuery(request body) error = %v", err) + } + if got := form.Get("two_factor_auth_method_type"); got != "Text" { + t.Fatalf("two_factor_auth_method_type = %q, want Text", got) + } + responseBody := "Confirm the phone number associated with your X account.\x00phone_number\x00session_token" + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(responseBody)), + }, nil + })} + session := NewWebLoginSession(client) + session.backend = webLoginBackendJetfuel + session.jetfuel = &jetfuelLoginState{ + sessionToken: "session-token", + twoFactorAction: endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH, + twoFactorMethods: []WebLoginAuthMethod{ + {ID: "Totp", Name: "Authenticator App", Kind: WebLoginAuthMethodKindCode, Supported: true}, + {ID: "Sms", SubmitID: "Text", Name: "Text Message", Kind: WebLoginAuthMethodKindSMS, Supported: true, Index: 1}, + {ID: "BackupCode", Name: "Backup Code", Kind: WebLoginAuthMethodKindBackupCode, Supported: true, Index: 2}, + }, + } + + result, err := session.SubmitAuthMethod(context.Background(), "Text Message") + if err != nil { + t.Fatalf("SubmitAuthMethod() error = %v", err) + } + if result.Status != WebLoginStatusNeedsText { + t.Fatalf("SubmitAuthMethod() status = %s, want %s", result.Status, WebLoginStatusNeedsText) + } + if result.Challenge == nil || result.Challenge.InputKind != WebLoginChallengeInputKindPhoneNumber { + t.Fatalf("Challenge = %#v, want phone-number prompt", result.Challenge) + } + if session.jetfuel.verificationAction != endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH { + t.Fatalf("verificationAction = %q", session.jetfuel.verificationAction) + } +} + +func TestSubmitJetfuelPhoneNumberVerificationPostsPhoneField(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.HTTP = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if req.URL.Path != "/i/jfapi"+endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH { + t.Fatalf("request path = %s", req.URL.Path) + } + body, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("ReadAll(request body) error = %v", err) + } + form, err := url.ParseQuery(string(body)) + if err != nil { + t.Fatalf("ParseQuery(request body) error = %v", err) + } + if got := form.Get("phone_number"); got != "+15551234567" { + t.Fatalf("phone_number = %q, want test phone number", got) + } + if got := form.Get("challenge_response"); got != "" { + t.Fatalf("challenge_response = %q, want omitted when phone_number is known", got) + } + if got := form.Get("verification_code"); got != "" { + t.Fatalf("verification_code = %q, want omitted when phone_number is known", got) + } + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader("/home")), + }, nil + })} + session := NewWebLoginSession(client) + session.backend = webLoginBackendJetfuel + session.jetfuel = &jetfuelLoginState{ + verificationAction: endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH, + verificationFields: []string{"phone_number"}, + } + + result, err := session.SubmitText(context.Background(), "+15551234567") + if err != nil { + t.Fatalf("SubmitText() error = %v", err) + } + if result.Status != WebLoginStatusComplete { + t.Fatalf("SubmitText() status = %s, want %s", result.Status, WebLoginStatusComplete) + } +} + +func TestJetfuelAuthMethodChoiceWithOnlyUnsupportedMethod(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("Select a method to authenticate\x00two_factor_method\x00U2fSecurityKey\x00begin_two_factor_auth")) + session := &WebLoginSession{jetfuel: &jetfuelLoginState{}} + + result := session.jetfuelAuthMethodChoiceResult(parsed) + if result == nil { + t.Fatal("jetfuelAuthMethodChoiceResult() returned nil") + } + if result.Status != WebLoginStatusUnsupported { + t.Fatalf("Status = %s, want %s", result.Status, WebLoginStatusUnsupported) + } + if len(session.jetfuel.twoFactorMethods) != 1 || session.jetfuel.twoFactorMethods[0].Supported { + t.Fatalf("twoFactorMethods = %#v, want one unsupported method", session.jetfuel.twoFactorMethods) + } +} + +func TestJetfuelAuthMethodChoiceWithOnlySMSIsComingSoon(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("Select a method to authenticate\x00two_factor_method\x00Text\x00begin_two_factor_auth")) + session := &WebLoginSession{jetfuel: &jetfuelLoginState{}} + + result := session.jetfuelAuthMethodChoiceResult(parsed) + if result == nil { + t.Fatal("jetfuelAuthMethodChoiceResult() returned nil") + } + if result.Status != WebLoginStatusUnsupported { + t.Fatalf("Status = %s, want %s", result.Status, WebLoginStatusUnsupported) + } + if len(session.jetfuel.twoFactorMethods) != 1 { + t.Fatalf("twoFactorMethods length = %d, want 1: %#v", len(session.jetfuel.twoFactorMethods), session.jetfuel.twoFactorMethods) + } + method := session.jetfuel.twoFactorMethods[0] + if method.ID != "Sms" || method.Supported || method.Description != "Text message verification is coming soon." { + t.Fatalf("SMS method = %#v, want coming-soon unsupported method", method) + } + if result.Challenge == nil || result.Challenge.Description != "Text message verification is coming soon." { + t.Fatalf("Challenge = %#v, want coming-soon description", result.Challenge) + } +} + +func TestSubmitJetfuelAuthMethodRejectsUnsupportedMethod(t *testing.T) { + session := &WebLoginSession{ + backend: webLoginBackendJetfuel, + jetfuel: &jetfuelLoginState{ + twoFactorMethods: []WebLoginAuthMethod{{ + ID: "U2fSecurityKey", + Name: "Security Key PC", + Supported: false, + }}, + }, + } + + _, err := session.SubmitAuthMethod(context.Background(), "U2fSecurityKey") + if !errors.Is(err, ErrWebLoginUnsupportedAuthMethod) { + t.Fatalf("SubmitAuthMethod() error = %v, want ErrWebLoginUnsupportedAuthMethod", err) + } +} + +func TestJetfuelLoginResponseClassifiesTemporaryLimit(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("We've temporarily limited your login. Please try again later.")) + err := parsed.loginError() + if err == nil { + t.Fatal("loginError() returned nil") + } + webErr, ok := err.(*WebLoginError) + if !ok { + t.Fatalf("loginError() = %T, want *WebLoginError", err) + } + if webErr.Code != 399 { + t.Fatalf("WebLoginError.Code = %d, want 399", webErr.Code) + } +} + +func TestJetfuelLoginResponseClassifiesTooManyAttempts(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("errors.Too many attempts. Try again in a few minutes.\x00message.Too many attempts. Try again in a few minutes.")) + err := parsed.loginError() + if err == nil { + t.Fatal("loginError() returned nil") + } + webErr, ok := err.(*WebLoginError) + if !ok { + t.Fatalf("loginError() = %T, want *WebLoginError", err) + } + if webErr.Code != 399 { + t.Fatalf("WebLoginError.Code = %d, want 399", webErr.Code) + } +} + +func TestJetfuelLoginResponseClassifiesOfficialClientError(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("Please use X.com or official X apps to proceed with log in/sign up.")) + err := parsed.loginError() + if err == nil { + t.Fatal("loginError() returned nil") + } + webErr, ok := err.(*WebLoginError) + if !ok { + t.Fatalf("loginError() = %T, want *WebLoginError", err) + } + if webErr.Code != 399 { + t.Fatalf("WebLoginError.Code = %d, want 399", webErr.Code) + } + if !strings.Contains(webErr.Message, "official X apps") { + t.Fatalf("WebLoginError.Message = %q", webErr.Message) + } +} + +func TestJetfuelLoginResponseClassifiesMissingAccount(t *testing.T) { + parsed := parseJetfuelLoginResponse([]byte("missing_account This email or username is not registered yet")) + err := parsed.loginError() + if err == nil { + t.Fatal("loginError() returned nil") + } + webErr, ok := err.(*WebLoginError) + if !ok { + t.Fatalf("loginError() = %T, want *WebLoginError", err) + } + if webErr.Code != 32 { + t.Fatalf("WebLoginError.Code = %d, want 32", webErr.Code) + } +} + +func TestJetfuelLoginResponseClassifiesBadCredentials(t *testing.T) { + tests := []string{ + "Wrong password", + "The password you entered is incorrect.", + "The username and password you entered did not match our records.", + "Invalid username or password", + "Invalid credentials", + } + for _, body := range tests { + parsed := parseJetfuelLoginResponse([]byte(body)) + err := parsed.loginError() + if err == nil { + t.Fatalf("loginError(%q) returned nil", body) + } + webErr, ok := err.(*WebLoginError) + if !ok { + t.Fatalf("loginError(%q) = %T, want *WebLoginError", body, err) + } + if webErr.Code != 32 { + t.Fatalf("WebLoginError.Code for %q = %d, want 32", body, webErr.Code) + } + } +} + +func TestJetfuelBrowserParityHeaderDecoding(t *testing.T) { + if got := decodeTFEGuestCookie("v1%3A12345"); got != "12345" { + t.Fatalf("decodeTFEGuestCookie() = %q, want 12345", got) + } + path := "/svc/route" + if got := decodeDtabLocal(base64.RawURLEncoding.EncodeToString([]byte(path))); got != path { + t.Fatalf("decodeDtabLocal() = %q, want %s", got, path) + } + if got := decodeDtabLocal("%2Fsvc%2Froute"); got != path { + t.Fatalf("decodeDtabLocal(encoded path) = %q, want %s", got, path) + } +} + +func TestJetfuelTimezoneEnvOverride(t *testing.T) { + t.Setenv("TWITTER_JETFUEL_TIMEZONE", "Europe/Paris") + if got := jetfuelTimezone(); got != "Europe/Paris" { + t.Fatalf("jetfuelTimezone() = %q, want Europe/Paris", got) + } +} diff --git a/pkg/twittermeow/methods/html.go b/pkg/twittermeow/methods/html.go index d6e88b2a..05bb89a0 100644 --- a/pkg/twittermeow/methods/html.go +++ b/pkg/twittermeow/methods/html.go @@ -1,7 +1,9 @@ package methods import ( + "net/http" "regexp" + "strconv" "go.mau.fi/mautrix-twitter/pkg/twittermeow/data/payload" ) @@ -11,6 +13,8 @@ var ( migrateFormDataRegex = regexp.MustCompile(`]* action="([^"]+)"[^>]*>[\s\S]*?]* name="tok" value="([^"]+)"[^>]*>[\s\S]*?]* name="data" value="([^"]+)"[^>]*>`) mainScriptURLRegex = regexp.MustCompile(`https:\/\/(?:[A-Za-z0-9.-]+)\/responsive-web\/client-web\/main\.[0-9A-Za-z]+\.js`) bearerTokenRegex = regexp.MustCompile(`Bearer\s[A-Za-z0-9%]{16,}`) + documentCookieRegex = regexp.MustCompile(`document\.cookie\s*=\s*("(?:\\.|[^"\\])*")`) + cloudflareJSDRegex = regexp.MustCompile(`]+src=["']([^"']*/cdn-cgi/challenge-platform/[^"']*api\.js[^"']*)["']`) guestTokenRegex = regexp.MustCompile(`gt=([0-9]+)`) verificationTokenRegex = regexp.MustCompile(`meta name="twitter-site-verification" content="([^"]+)"`) countryCodeRegex = regexp.MustCompile(`"country":\s*"([A-Z]{2})"`) @@ -59,6 +63,36 @@ func ParseGuestToken(html string) string { return match[1] } +func ParseDocumentCookieAssignments(html string) map[string]string { + matches := documentCookieRegex.FindAllStringSubmatch(html, -1) + if len(matches) == 0 { + return nil + } + out := make(map[string]string) + for _, match := range matches { + if len(match) < 2 { + continue + } + cookie, err := strconv.Unquote(match[1]) + if err != nil || cookie == "" { + continue + } + parsedCookie, err := http.ParseSetCookie(cookie) + if err == nil { + out[parsedCookie.Name] = parsedCookie.Value + } + } + return out +} + +func ParseCloudflareJSDURL(html string) string { + match := cloudflareJSDRegex.FindStringSubmatch(html) + if len(match) < 2 { + return "" + } + return match[1] +} + func ParseVerificationToken(html string) string { match := verificationTokenRegex.FindStringSubmatch(html) if len(match) < 1 { diff --git a/pkg/twittermeow/methods/html_test.go b/pkg/twittermeow/methods/html_test.go index 8385d34a..02bbbcf6 100644 --- a/pkg/twittermeow/methods/html_test.go +++ b/pkg/twittermeow/methods/html_test.go @@ -7,6 +7,24 @@ import ( "time" ) +func TestParseDocumentCookieAssignments(t *testing.T) { + html := `` + + cookies := ParseDocumentCookieAssignments(html) + if got := cookies["ct0"]; got != "csrf-value" { + t.Fatalf("ct0 = %q, want csrf-value", got) + } + if got := cookies["guest_id"]; got != "guest-value" { + t.Fatalf("guest_id = %q, want guest-value", got) + } + if got := cookies["Path"]; got != "" { + t.Fatalf("Path pseudo-cookie = %q, want omitted", got) + } +} + func TestParseOndemandSURLFromScript(t *testing.T) { client := &http.Client{Timeout: 20 * time.Second} req, err := http.NewRequest(http.MethodGet, "https://x.com/", nil)