From f9a66f22e63032790a037343b6ecd642c0448383 Mon Sep 17 00:00:00 2001 From: highesttt Date: Fri, 26 Jun 2026 10:59:11 -0500 Subject: [PATCH 01/20] feat: basic login via matrix login workflow 2fa not supported yet --- .gitignore | 7 + pkg/connector/login.go | 229 +- pkg/twittermeow/castle_token.go | 668 +++ pkg/twittermeow/castle_web_token.go | 5562 +++++++++++++++++++ pkg/twittermeow/client.go | 57 +- pkg/twittermeow/cookies/cookies.go | 4 +- pkg/twittermeow/data/endpoints/endpoints.go | 13 + pkg/twittermeow/headers.go | 10 +- pkg/twittermeow/login_jetfuel.go | 732 +++ pkg/twittermeow/login_web.go | 678 +++ pkg/twittermeow/methods/html.go | 36 + 11 files changed, 7977 insertions(+), 19 deletions(-) create mode 100644 pkg/twittermeow/castle_token.go create mode 100644 pkg/twittermeow/castle_web_token.go create mode 100644 pkg/twittermeow/login_jetfuel.go create mode 100644 pkg/twittermeow/login_web.go diff --git a/.gitignore b/.gitignore index 1de87ca7..74830d9b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,10 @@ logs/ !.pre-commit-config.yaml /start /pkg/mautrix + +/*.dll.a +/*.dll +/mautrix-twitter.exe +/data +actions*.txt +grind*.md \ No newline at end of file diff --git a/pkg/connector/login.go b/pkg/connector/login.go index d91164ac..fe710119 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -47,15 +47,26 @@ type TwitterLogin struct { tc *TwitterConnector isMigration bool // True if upgrading from main branch (had cookies but no encryption keys) needsPINSetup bool + useCookieLogin bool - client *twittermeow.Client - profile twittermeow.CurrentUserProfile + client *twittermeow.Client + webLogin *twittermeow.WebLoginSession + webLoginPassword string + webLoginChallenge *twittermeow.WebLoginChallenge + 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" + 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" ) var _ bridgev2.LoginProcessCookies = (*TwitterLogin)(nil) @@ -108,22 +119,33 @@ var ( func (tc *TwitterConnector) GetLoginFlows() []bridgev2.LoginFlow { return []bridgev2.LoginFlow{ + { + Name: "X login", + 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 +175,70 @@ 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 + } + 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 makePINStep(errorLine string, isSetup bool) *bridgev2.LoginStep { instructions := passcodeBodyRecover fieldName := "Passcode" @@ -395,9 +481,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 +521,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 +544,129 @@ 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[loginFieldVerificationCode]; ok || t.webLoginChallenge != nil { + return t.submitWebVerificationInput(ctx, input) + } + if _, ok := input[loginFieldIdentifier]; ok || input[loginFieldPassword] != "" { + return t.submitCredentialsInput(ctx, input) + } + return makeCredentialsStep("Enter your X username, email, or phone number and password."), nil +} + +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 makeCredentialsStep("Enter both your X identifier and password."), nil + } + + 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 + + result, err := t.webLogin.Start(ctx) + if err != nil { + return makeCredentialsStep(webLoginErrorInstructions(err)), nil + } + if result.Status != twittermeow.WebLoginStatusNeedsIdentifier { + return t.handleWebLoginResult(ctx, result) + } + + result, err = t.webLogin.SubmitCredentials(ctx, identifier, password) + if err != nil { + return makeCredentialsStep(webLoginErrorInstructions(err)), nil + } + return t.handleWebLoginResult(ctx, result) +} + +func (t *TwitterLogin) submitWebVerificationInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { + if t.webLogin == nil { + t.webLoginChallenge = nil + t.webLoginPassword = "" + return makeCredentialsStep("The X login session expired. Enter your X login details again."), nil + } + code := strings.TrimSpace(input[loginFieldVerificationCode]) + if code == "" { + return makeVerificationStep(t.webLoginChallenge, "Enter the X verification code."), nil + } + result, err := t.webLogin.SubmitText(ctx, code) + if err != nil { + return makeVerificationStep(t.webLoginChallenge, webLoginErrorInstructions(err)), nil + } + if result.Status == twittermeow.WebLoginStatusNeedsPassword && t.webLoginPassword != "" { + result, err = t.webLogin.SubmitPassword(ctx, t.webLoginPassword) + if err != nil { + return makeCredentialsStep(webLoginErrorInstructions(err)), nil + } + } + 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.WebLoginStatusNeedsText: + t.webLoginChallenge = result.Challenge + return makeVerificationStep(result.Challenge, ""), nil + case twittermeow.WebLoginStatusNeedsPassword: + 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("X returned a login challenge this bridge does not support yet."), nil + } +} + +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 makeCredentialsStep("X accepted the login, but the bridge could not load the authenticated messages page. Try again."), nil + } + 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 + + 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." + } + 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/twittermeow/castle_token.go b/pkg/twittermeow/castle_token.go new file mode 100644 index 00000000..7f7aa3ba --- /dev/null +++ b/pkg/twittermeow/castle_token.go @@ -0,0 +1,668 @@ +package twittermeow + +import ( + "crypto/rand" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "fmt" + "math" + "net/url" + "strings" + "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) | ((3 & 1) << 11) | ((31 & 6) << 6) | (63 & 0)) + 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((7&0)<<5 | (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&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] + a := blocks[0] + 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 +} + +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..f5e0b8d8 --- /dev/null +++ b/pkg/twittermeow/castle_web_token.go @@ -0,0 +1,5562 @@ +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 encodeCurrentCastleSlot0Primary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 1 + b = byte(uint16(b) * 7) + b += 52 + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleSlot0Secondary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 163) + b = currentCastleRotL8(b, 2) + b = currentCastleRotL8(b, 3) + return currentCastleRotL8(b, 7) + }) +} + +func encodeCurrentCastleSlot0Tertiary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 185) + b = currentCastleRotL8(b, 5) + b += 212 + return b + }) +} + +func encodeCurrentCastleSlot0Quaternary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 223) + b = currentCastleRotL8(b, 5) + b ^= 159 + return b + }) +} + +func encodeCurrentCastleSlot0Quinary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b += 220 + b = currentCastleRotL8(b, 2) + b = byte(uint16(b) * 31) + return b + }) +} + +func encodeCurrentCastleSlot0Senary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 249 + b += 85 + b += 192 + return b + }) +} + +func encodeCurrentCastleSlot7CanvasPrimary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 133) + b += 242 + b = byte(uint16(b) * 215) + b = byte(uint16(b) * 105) + return b + }) +} + +func encodeCurrentCastleSlot7WorkerTiming(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 83 + b ^= 38 + b = byte(uint16(b) * 105) + b = byte(uint16(b) * 211) + return b + }) +} + +func encodeCurrentCastleSlot7MediaRatio(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b ^= 207 + b += 239 + return b + }) +} + +func encodeCurrentCastleSlot7PluginState(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 135) + b ^= 37 + b += 242 + return b + }) +} + +func encodeCurrentCastleSlot7FrameRatio(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 238 + b ^= 80 + b = byte(uint16(b) * 67) + return b + }) +} + +func encodeCurrentCastleSlot7WorkerCanvas(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b = currentCastleRotL8(b, 5) + b ^= 31 + return b + }) +} + +func encodeCurrentCastleSlot7WorkerFeature(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 105 + b ^= 187 + b = byte(uint16(b) * 211) + return b + }) +} + +func encodeCurrentCastleSlot7NavigatorProbe(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 119) + b = byte(uint16(b) * 227) + b = currentCastleRotL8(b, 5) + b += 96 + return b + }) +} + +func encodeCurrentCastleSlot7ViewportRatio(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 55) + b = byte(uint16(b) * 129) + b ^= 157 + return b + }) +} + +func encodeCurrentCastleSlot7WorkerSignal(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 70 + b = currentCastleRotL8(b, 5) + b += 151 + b ^= 157 + return b + }) +} + +func encodeCurrentCastleSlot7PointerRatio(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b += 202 + b += 162 + b = currentCastleRotL8(b, 5) + return b + }) +} + +func encodeCurrentCastleSlot8Primary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b = byte(uint16(b) * 45) + b ^= 87 + return b + }) +} + +func encodeCurrentCastleSlot8Secondary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b += 154 + b = byte(uint16(b) * 201) + return b + }) +} + +func encodeCurrentCastleWebGPUVendor(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 79) + b = byte(uint16(b) * 211) + b ^= 118 + return b + }) +} + +func encodeCurrentCastleWebGPULimits(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b = currentCastleRotL8(b, 5) + b += 72 + b = byte(uint16(b) * 187) + return b + }) +} + +func encodeCurrentCastleWebGPUArchitecture(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 59) + b = currentCastleRotL8(b, 1) + b ^= 56 + return b + }) +} + +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 encodeCurrentCastleSlot6TZ(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 223) + b = currentCastleRotL8(b, 5) + b ^= 159 + return b + }) +} + +func encodeCurrentCastleSlot6TP(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 133) + b += 242 + b = byte(uint16(b) * 215) + return byte(uint16(b) * 105) + }) +} + +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 invertCurrentCastleTB(value uint32) uint32 { + value -= 956 + value -= 24 + value += 42 + value += 41 + value += 478 + value -= 433 + return value * 1884841763 +} + +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..6a44c8f0 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" @@ -69,7 +71,7 @@ type Client struct { func NewClient(cookies *cookies.Cookies, store crypto.KeyStore, logger zerolog.Logger) *Client { cli := Client{ HTTP: &http.Client{ - Transport: req.NewClient().ImpersonateChrome().DisableHTTP3().GetTransport(), + Transport: req.NewClient().ImpersonateChrome().SetTLSFingerprintChrome().DisableHTTP3().GetTransport(), Timeout: 60 * time.Second, }, Logger: logger, @@ -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..5c13da48 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" ) @@ -88,7 +90,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/data/endpoints/endpoints.go b/pkg/twittermeow/data/endpoints/endpoints.go index 87f33459..8fb1d181 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,16 @@ 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" 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..ece12307 --- /dev/null +++ b/pkg/twittermeow/login_jetfuel.go @@ -0,0 +1,732 @@ +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}$`) + jetfuelUUIDRegex = regexp.MustCompile(`(?i)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) +) + +type webLoginBackend string + +type jetfuelLoginState struct { + identifier string + passwordAction string + verificationAction string + sessionToken string + preludeDispatchID string +} + +type jetfuelLoginResponse struct { + strings []string + paths []string + fields []string +} + +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 + 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 action := parsed.verificationAction(); action != "" { + wls.jetfuel.verificationAction = action + 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 action := parsed.beginTwoFactorAction(); action != "" { + return wls.submitJetfuelBeginTwoFactor(ctx, action) + } + if action := parsed.verificationAction(); action != "" { + wls.jetfuel.verificationAction = action + 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 action := parsed.verificationAction(); action != "" { + wls.jetfuel.verificationAction = action + 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{ + "challenge_response": {text}, + "verification_code": {text}, + "code": {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 + } + if wls.client.IsLoggedIn() || parsed.isComplete() { + return &WebLoginResult{Status: WebLoginStatusComplete}, 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 action := parsed.passwordAction(); action != "" { + wls.jetfuel.passwordAction = action + } + if action := parsed.verificationAction(); action != "" { + wls.jetfuel.verificationAction = action + } +} + +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) { + if !slices.Contains(paths, path) { + paths = append(paths, path) + } + } + if jetfuelFieldRegex.MatchString(str) && !slices.Contains(fields, str) { + fields = append(fields, str) + } + } + return jetfuelLoginResponse{strings: strs, paths: paths, fields: fields} +} + +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) 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 firstJetfuelUUID(value string) string { + return jetfuelUUIDRegex.FindString(value) +} + +func (jfr jetfuelLoginResponse) verificationChallenge() *WebLoginChallenge { + text := jfr.text() + return &WebLoginChallenge{ + SubtaskID: "JetfuelVerification", + Hint: "Verification code", + Description: jetfuelChallengeDescription(text), + IsTwoFactor: strings.Contains(text, "two-factor") || strings.Contains(text, "two factor") || + strings.Contains(text, "authentication code") || strings.Contains(text, "verification code"), + } +} + +func jetfuelChallengeDescription(text string) string { + switch { + case strings.Contains(text, "backup code"): + return "Enter a backup code from X." + 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) 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, "missing_account") || strings.Contains(text, "not registered"): + return &WebLoginError{Code: 32, Message: "This email or username is not registered yet."} + case strings.Contains(text, "wrong password") || strings.Contains(text, "incorrect password"): + 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 + } +} diff --git a/pkg/twittermeow/login_web.go b/pkg/twittermeow/login_web.go new file mode 100644 index 00000000..551007c3 --- /dev/null +++ b/pkg/twittermeow/login_web.go @@ -0,0 +1,678 @@ +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") +) + +type WebLoginStatus string + +const ( + WebLoginStatusNeedsIdentifier WebLoginStatus = "needs_identifier" + WebLoginStatusNeedsPassword WebLoginStatus = "needs_password" + WebLoginStatusNeedsText WebLoginStatus = "needs_text" + WebLoginStatusComplete WebLoginStatus = "complete" + WebLoginStatusUnsupported WebLoginStatus = "unsupported" +) + +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 +} + +type WebLoginResult struct { + Status WebLoginStatus + Challenge *WebLoginChallenge + 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) 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, + "security_key": 3, + "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/methods/html.go b/pkg/twittermeow/methods/html.go index d6e88b2a..db333724 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,38 @@ 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 + } + header := http.Header{} + for _, match := range matches { + if len(match) < 2 { + continue + } + cookie, err := strconv.Unquote(match[1]) + if err != nil || cookie == "" { + continue + } + header.Add("Set-Cookie", cookie) + } + resp := &http.Response{Header: header} + out := make(map[string]string) + for _, cookie := range resp.Cookies() { + out[cookie.Name] = cookie.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 { From dc2ac574ce9b4ececbf1430a289b040b89dc2f19 Mon Sep 17 00:00:00 2001 From: highesttt Date: Fri, 26 Jun 2026 15:28:15 -0500 Subject: [PATCH 02/20] feat: 2fa codes support --- pkg/connector/login.go | 9 +++ pkg/twittermeow/login_jetfuel.go | 132 ++++++++++++++++++++++++++++--- 2 files changed, 132 insertions(+), 9 deletions(-) diff --git a/pkg/connector/login.go b/pkg/connector/login.go index fe710119..a2f9e30a 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -617,6 +617,15 @@ func (t *TwitterLogin) handleWebLoginResult(ctx context.Context, result *twitter t.webLoginChallenge = result.Challenge 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 makeCredentialsStep(webLoginErrorInstructions(err)), nil + } + 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 diff --git a/pkg/twittermeow/login_jetfuel.go b/pkg/twittermeow/login_jetfuel.go index ece12307..6023c18b 100644 --- a/pkg/twittermeow/login_jetfuel.go +++ b/pkg/twittermeow/login_jetfuel.go @@ -33,6 +33,13 @@ var ( jetfuelActionPathRegex = regexp.MustCompile(`/onboarding/web/actions/[A-Za-z0-9_./-]+`) jetfuelFieldRegex = regexp.MustCompile(`^[A-Za-z_$][A-Za-z0-9_$-]{1,80}$`) 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": "/onboarding/web/actions/finish_two_factor_auth", + "two_factor_code": "/onboarding/web/actions/two_factor_code", + } ) type webLoginBackend string @@ -41,6 +48,7 @@ type jetfuelLoginState struct { identifier string passwordAction string verificationAction string + verificationFields []string sessionToken string preludeDispatchID string } @@ -114,6 +122,7 @@ func (wls *WebLoginSession) submitJetfuelIdentifier(ctx context.Context, identif } if action := parsed.verificationAction(); action != "" { wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = parsed.verificationCodeFields() return &WebLoginResult{ Status: WebLoginStatusNeedsText, CurrentSubtaskID: "JetfuelVerification", @@ -176,6 +185,7 @@ func (wls *WebLoginSession) submitJetfuelCombinedCredentials(ctx context.Context } if action := parsed.verificationAction(); action != "" { wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = parsed.verificationCodeFields() return &WebLoginResult{ Status: WebLoginStatusNeedsText, CurrentSubtaskID: "JetfuelVerification", @@ -229,6 +239,7 @@ func (wls *WebLoginSession) submitJetfuelPassword(ctx context.Context, password } if action := parsed.verificationAction(); action != "" { wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = parsed.verificationCodeFields() return &WebLoginResult{ Status: WebLoginStatusNeedsText, CurrentSubtaskID: "JetfuelVerification", @@ -263,6 +274,7 @@ func (wls *WebLoginSession) submitJetfuelBeginTwoFactor(ctx context.Context, act } if action := parsed.verificationAction(); action != "" { wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = parsed.verificationCodeFields() return &WebLoginResult{ Status: WebLoginStatusNeedsText, CurrentSubtaskID: "JetfuelVerification", @@ -280,10 +292,9 @@ func (wls *WebLoginSession) submitJetfuelText(ctx context.Context, text string) if wls.jetfuel == nil || wls.jetfuel.verificationAction == "" { return nil, fmt.Errorf("%w: Jetfuel verification action is missing", ErrWebLoginUnexpectedSubtask) } - form := url.Values{ - "challenge_response": {text}, - "verification_code": {text}, - "code": {text}, + form := url.Values{} + for _, field := range wls.jetfuelVerificationFields() { + form.Set(field, text) } if wls.jetfuel.sessionToken != "" { form.Set("session_token", wls.jetfuel.sessionToken) @@ -299,9 +310,33 @@ func (wls *WebLoginSession) submitJetfuelText(ctx context.Context, text string) 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.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) } @@ -320,9 +355,17 @@ func (wls *WebLoginSession) updateJetfuelState(parsed jetfuelLoginResponse) { } if action := parsed.verificationAction(); action != "" { wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = parsed.verificationCodeFields() } } +func (wls *WebLoginSession) jetfuelVerificationFields() []string { + if wls.jetfuel != nil && len(wls.jetfuel.verificationFields) > 0 { + return wls.jetfuel.verificationFields + } + return defaultJetfuelVerificationFields() +} + func (c *Client) jetfuelGet(ctx context.Context, path string) ([]byte, error) { return c.jetfuelRequest(ctx, path, http.MethodGet, nil) } @@ -557,9 +600,10 @@ func parseJetfuelLoginResponse(body []byte) jetfuelLoginResponse { fields := make([]string, 0) for _, str := range strs { for _, path := range jetfuelActionPathRegex.FindAllString(str, -1) { - if !slices.Contains(paths, path) { - paths = append(paths, path) - } + 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) @@ -568,6 +612,21 @@ func parseJetfuelLoginResponse(body []byte) jetfuelLoginResponse { return jetfuelLoginResponse{strings: strs, paths: paths, fields: fields} } +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{}) @@ -662,6 +721,58 @@ func (jfr jetfuelLoginResponse) verificationAction() string { return "" } +func (jfr jetfuelLoginResponse) verificationCodeFields() []string { + 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 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 { @@ -691,14 +802,15 @@ func (jfr jetfuelLoginResponse) verificationChallenge() *WebLoginChallenge { Hint: "Verification code", Description: jetfuelChallengeDescription(text), IsTwoFactor: strings.Contains(text, "two-factor") || strings.Contains(text, "two factor") || - strings.Contains(text, "authentication code") || strings.Contains(text, "verification code"), + strings.Contains(text, "authentication code") || strings.Contains(text, "verification code") || + strings.Contains(text, "backup code") || strings.Contains(text, "totp"), } } func jetfuelChallengeDescription(text string) string { switch { case strings.Contains(text, "backup code"): - return "Enter a backup code from X." + return "Enter the code from your authentication app." case strings.Contains(text, "authentication code"): return "Enter the authentication code from X." case strings.Contains(text, "verification code"): @@ -720,6 +832,8 @@ func (jfr jetfuelLoginResponse) loginError() error { 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 strings.Contains(text, "wrong password") || strings.Contains(text, "incorrect password"): From 46009d44b91e02dea22f483b90f48bfeea4d3c49 Mon Sep 17 00:00:00 2001 From: highesttt Date: Fri, 26 Jun 2026 16:26:51 -0500 Subject: [PATCH 03/20] fix: ci issues --- pkg/connector/login_live_probe_test.go | 196 + pkg/twittermeow/castle_token.go | 16 +- pkg/twittermeow/castle_token_test.go | 21 + .../castle_token_test_helpers_test.go | 7 + pkg/twittermeow/castle_web_token.go | 233 -- pkg/twittermeow/castle_web_token_test.go | 3432 +++++++++++++++++ .../castle_web_token_test_helpers_test.go | 234 ++ pkg/twittermeow/cookies/cookies_test.go | 22 + pkg/twittermeow/login_jetfuel.go | 28 +- .../login_jetfuel_live_probe_test.go | 503 +++ pkg/twittermeow/login_web.go | 14 +- pkg/twittermeow/login_web_test.go | 272 ++ 12 files changed, 4713 insertions(+), 265 deletions(-) create mode 100644 pkg/connector/login_live_probe_test.go create mode 100644 pkg/twittermeow/castle_token_test.go create mode 100644 pkg/twittermeow/castle_token_test_helpers_test.go create mode 100644 pkg/twittermeow/castle_web_token_test.go create mode 100644 pkg/twittermeow/castle_web_token_test_helpers_test.go create mode 100644 pkg/twittermeow/cookies/cookies_test.go create mode 100644 pkg/twittermeow/login_jetfuel_live_probe_test.go create mode 100644 pkg/twittermeow/login_web_test.go 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/twittermeow/castle_token.go b/pkg/twittermeow/castle_token.go index 7f7aa3ba..c09830bc 100644 --- a/pkg/twittermeow/castle_token.go +++ b/pkg/twittermeow/castle_token.go @@ -8,7 +8,6 @@ import ( "fmt" "math" "net/url" - "strings" "time" ) @@ -97,7 +96,7 @@ func castleHeader(uuidHex string, initTime int64) ([]byte, error) { return nil, err } out := castleEncodeTimestampEncrypted(initTime) - version := uint16((3 << 13) | ((3 & 1) << 11) | ((31 & 6) << 6) | (63 & 0)) + version := uint16((3 << 13) | (1 << 11) | (6 << 6)) out = binary.BigEndian.AppendUint16(out, version) out = append(out, []byte(castlePublicKey)...) out = append(out, uuidBytes...) @@ -304,7 +303,7 @@ func castleFPOne(initTime int64) ([]byte, error) { if err := addBytes(31, castleValueJustAppend, []byte{0xa2, 0x6a}); err != nil { return nil, err } - out := []byte{byte((7&0)<<5 | (31 & len(values)))} + out := []byte{byte(31 & len(values))} for _, value := range values { out = append(out, value...) } @@ -434,7 +433,7 @@ func castleFPThree(initTime int64) ([]byte, error) { if err != nil { return nil, err } - return append([]byte{byte((7&7)<<5 | 2)}, append(first, second...)...), nil + return append([]byte{byte((7 << 5) | 2)}, append(first, second...)...), nil } func castleEventLog() ([]byte, error) { @@ -597,18 +596,17 @@ func xxteaEncrypt(data []byte, key []uint32) []byte { } sum := uint32(0) o := blocks[u] - a := blocks[0] 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] + 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] + a := blocks[0] blocks[u] += (((o >> 5) ^ (a << 2)) + ((a >> 3) ^ (o << 4))) ^ ((sum ^ a) + (key[(u&3)^int(e)] ^ o)) o = blocks[u] } @@ -662,7 +660,3 @@ func randomBytes(length int) ([]byte, error) { _, err := rand.Read(out) return out, err } - -func looksLikeCastleToken(token string) bool { - return len(token) > 500 && !strings.Contains(token, "=") -} 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 index f5e0b8d8..51583c81 100644 --- a/pkg/twittermeow/castle_web_token.go +++ b/pkg/twittermeow/castle_web_token.go @@ -2056,211 +2056,6 @@ func encodeCurrentCastleUnitPackedStringComponents(values []string) string { return encodeCurrentCastleUnits(currentCastleLengthPrefixedStringComponentUnits(values)) } -func encodeCurrentCastleSlot0Primary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b += 1 - b = byte(uint16(b) * 7) - b += 52 - return currentCastleRotL8(b, 6) - }) -} - -func encodeCurrentCastleSlot0Secondary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 163) - b = currentCastleRotL8(b, 2) - b = currentCastleRotL8(b, 3) - return currentCastleRotL8(b, 7) - }) -} - -func encodeCurrentCastleSlot0Tertiary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 185) - b = currentCastleRotL8(b, 5) - b += 212 - return b - }) -} - -func encodeCurrentCastleSlot0Quaternary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 223) - b = currentCastleRotL8(b, 5) - b ^= 159 - return b - }) -} - -func encodeCurrentCastleSlot0Quinary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 2) - b += 220 - b = currentCastleRotL8(b, 2) - b = byte(uint16(b) * 31) - return b - }) -} - -func encodeCurrentCastleSlot0Senary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b += 249 - b += 85 - b += 192 - return b - }) -} - -func encodeCurrentCastleSlot7CanvasPrimary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 133) - b += 242 - b = byte(uint16(b) * 215) - b = byte(uint16(b) * 105) - return b - }) -} - -func encodeCurrentCastleSlot7WorkerTiming(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b += 83 - b ^= 38 - b = byte(uint16(b) * 105) - b = byte(uint16(b) * 211) - return b - }) -} - -func encodeCurrentCastleSlot7MediaRatio(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 4) - b ^= 207 - b += 239 - return b - }) -} - -func encodeCurrentCastleSlot7PluginState(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 135) - b ^= 37 - b += 242 - return b - }) -} - -func encodeCurrentCastleSlot7FrameRatio(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b += 238 - b ^= 80 - b = byte(uint16(b) * 67) - return b - }) -} - -func encodeCurrentCastleSlot7WorkerCanvas(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 6) - b = currentCastleRotL8(b, 5) - b ^= 31 - return b - }) -} - -func encodeCurrentCastleSlot7WorkerFeature(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b += 105 - b ^= 187 - b = byte(uint16(b) * 211) - return b - }) -} - -func encodeCurrentCastleSlot7NavigatorProbe(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 119) - b = byte(uint16(b) * 227) - b = currentCastleRotL8(b, 5) - b += 96 - return b - }) -} - -func encodeCurrentCastleSlot7ViewportRatio(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 55) - b = byte(uint16(b) * 129) - b ^= 157 - return b - }) -} - -func encodeCurrentCastleSlot7WorkerSignal(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b += 70 - b = currentCastleRotL8(b, 5) - b += 151 - b ^= 157 - return b - }) -} - -func encodeCurrentCastleSlot7PointerRatio(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 6) - b += 202 - b += 162 - b = currentCastleRotL8(b, 5) - return b - }) -} - -func encodeCurrentCastleSlot8Primary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 5) - b = byte(uint16(b) * 45) - b ^= 87 - return b - }) -} - -func encodeCurrentCastleSlot8Secondary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 5) - b += 154 - b = byte(uint16(b) * 201) - return b - }) -} - -func encodeCurrentCastleWebGPUVendor(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 79) - b = byte(uint16(b) * 211) - b ^= 118 - return b - }) -} - -func encodeCurrentCastleWebGPULimits(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 4) - b = currentCastleRotL8(b, 5) - b += 72 - b = byte(uint16(b) * 187) - return b - }) -} - -func encodeCurrentCastleWebGPUArchitecture(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 59) - b = currentCastleRotL8(b, 1) - b ^= 56 - return b - }) -} - 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) @@ -2790,24 +2585,6 @@ func encodeCurrentCastleSlot6Component46(value string) string { }) } -func encodeCurrentCastleSlot6TZ(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 223) - b = currentCastleRotL8(b, 5) - b ^= 159 - return b - }) -} - -func encodeCurrentCastleSlot6TP(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 133) - b += 242 - b = byte(uint16(b) * 215) - return byte(uint16(b) * 105) - }) -} - func currentCastleSlot6Hash(value string) uint32 { units := jsUTF16Units(value) hash := uint32(0) @@ -5261,16 +5038,6 @@ func currentCastleTB(value uint32) uint32 { return value } -func invertCurrentCastleTB(value uint32) uint32 { - value -= 956 - value -= 24 - value += 42 - value += 41 - value += 478 - value -= 433 - return value * 1884841763 -} - func encodeCurrentCastleNumericSlot20(value uint32) uint32 { value = currentCastleIndexEUnderscore(value) return (0x9f5593c9^value)*0x5d546885 + 0x312038dd diff --git a/pkg/twittermeow/castle_web_token_test.go b/pkg/twittermeow/castle_web_token_test.go new file mode 100644 index 00000000..5136d72e --- /dev/null +++ b/pkg/twittermeow/castle_web_token_test.go @@ -0,0 +1,3432 @@ +package twittermeow + +import ( + "bytes" + "compress/flate" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "os" + "regexp" + "sort" + "strconv" + "strings" + "testing" + "unicode/utf16" +) + +func TestCurrentCastleWrappedTokenRoundTrip(t *testing.T) { + const payload = `[null,"abc",123,true,{"k":"v"}]` + const timestampMillis = int64(1760000000123) + + token, err := createCurrentCastleWrappedToken(payload, timestampMillis) + if err != nil { + t.Fatalf("createCurrentCastleWrappedToken() error = %v", err) + } + if !strings.HasPrefix(token, currentCastleTokenPrefix) { + t.Fatalf("token missing current Castle prefix: %q", token[:min(len(token), len(currentCastleTokenPrefix))]) + } + wire, err := decodeCurrentCastleWrappedTokenForTest(token) + if err != nil { + t.Fatalf("decodeCurrentCastleWrappedTokenForTest() error = %v", err) + } + if len(wire) < 2 { + t.Fatalf("decoded wire too short: %d", len(wire)) + } + encodedTimestampLen := int(wire[0]) + if len(wire) <= 1+encodedTimestampLen { + t.Fatalf("decoded wire missing payload: wire_len=%d encoded_timestamp_len=%d", len(wire), encodedTimestampLen) + } + timestamp := strconv.FormatInt(timestampMillis, 10) + expectedTimestamp := encodeCurrentCastleTimestamp(timestamp) + if got := wire[1 : 1+encodedTimestampLen]; got != expectedTimestamp { + t.Fatalf("encoded timestamp = %q, want %q", got, expectedTimestamp) + } + gotPayload := xorCurrentCastleString(wire[1+encodedTimestampLen:], timestamp) + if wantPayload := insertCurrentCastleChecksum(payload); gotPayload != wantPayload { + t.Fatalf("decoded payload = %q, want %q", gotPayload, wantPayload) + } + if !strings.Contains(gotPayload, currentCastleChecksumLabel) { + t.Fatalf("decoded payload missing checksum label %q: %q", currentCastleChecksumLabel, gotPayload) + } +} + +func TestCurrentCastleKnownTransforms(t *testing.T) { + const payload = `[1,2,3]` + const timestamp = `1760000000123` + + if got, want := currentCastleChecksum(payload), "9cdea041"; got != want { + t.Fatalf("currentCastleChecksum() = %q, want %q", got, want) + } + tqCases := map[string]string{ + "": "969740e5", + "abc": "4a84ecf2", + "119280": "f394e296", + "3695375093": "a3e047f9", + } + for input, want := range tqCases { + if got := currentCastleTQHash(input); got != want { + t.Fatalf("currentCastleTQHash(%q) = %q, want %q", input, got, want) + } + } + tiCases := map[string]string{ + "": "83b623b0", + "abc": "2debeae9", + `"GraL"`: "f0b537b7", + `"override"`: "37d6d39f", + } + for input, want := range tiCases { + if got := currentCastleTIHash(input); got != want { + t.Fatalf("currentCastleTIHash(%q) = %q, want %q", input, got, want) + } + } + if got, want := encodeCurrentCastleTimestamp(timestamp), "WnP35wy50MvQy9DL0MvQy9DL0Mtac+Yab8I="; got != want { + t.Fatalf("encodeCurrentCastleTimestamp() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleUnits([]uint16{0x002f, 0x8000, 0xffff}), "RyQxU6ar"; got != want { + t.Fatalf("encodeCurrentCastleUnits() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleRawPackedUnits(jsUTF16Units("b537b7")), "S+hTpr/W8DdL6PA3"; got != want { + t.Fatalf("encodeCurrentCastleRawPackedUnits() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleLR("abc"), "oGAg"; got != want { + t.Fatalf("encodeCurrentCastleLR() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleFloat64(0), "xsbGxsbGxsY="; got != want { + t.Fatalf("encodeCurrentCastleFloat64(0) = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleFloat64(1), "OTbGxsbGxsY="; got != want { + t.Fatalf("encodeCurrentCastleFloat64(1) = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleFloat64(123.456), "hhjbKVyZ+PE="; got != want { + t.Fatalf("encodeCurrentCastleFloat64(123.456) = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6TU("abc"), "QXe5"; got != want { + t.Fatalf("encodeCurrentCastleSlot6TU() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6TF("abc"), "hxTl"; got != want { + t.Fatalf("encodeCurrentCastleSlot6TF() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6Component0("abc"), "cLDw"; got != want { + t.Fatalf("encodeCurrentCastleSlot6Component0() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6Component1("abc"), "Sjsq"; got != want { + t.Fatalf("encodeCurrentCastleSlot6Component1() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6Component1("X Castle"), "vB8NSisamgo="; got != want { + t.Fatalf("encodeCurrentCastleSlot6Component1() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6Component2("abc"), "SXTz"; got != want { + t.Fatalf("encodeCurrentCastleSlot6Component2() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6Component5Fallback("abc"), "aGlm"; got != want { + t.Fatalf("encodeCurrentCastleSlot6Component5Fallback() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6UZ("abc"), "Y29r"; got != want { + t.Fatalf("encodeCurrentCastleSlot6UZ() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6UZ("X Castle"), "hybrYys3V3M="; got != want { + t.Fatalf("encodeCurrentCastleSlot6UZ() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6IH("abc"), "odoT"; got != want { + t.Fatalf("encodeCurrentCastleSlot6IH() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6IH("X Castle"), "oCjzoaPcFIU="; got != want { + t.Fatalf("encodeCurrentCastleSlot6IH() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6FJ("abc"), "Eh4a"; got != want { + t.Fatalf("encodeCurrentCastleSlot6FJ() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6FJ("X Castle"), "9heaElpGJgI="; got != want { + t.Fatalf("encodeCurrentCastleSlot6FJ() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6IB("abc"), "BnWg"; got != want { + t.Fatalf("encodeCurrentCastleSlot6IB() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6IB("X Castle"), "cwsABpDPd8o="; got != want { + t.Fatalf("encodeCurrentCastleSlot6IB() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6AH("abc"), "1LSU"; got != want { + t.Fatalf("encodeCurrentCastleSlot6AH() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6AH("X Castle"), "tfyY1BLys1Q="; got != want { + t.Fatalf("encodeCurrentCastleSlot6AH() = %q, want %q", got, want) + } + indexR7Cases := map[uint32]uint32{ + 0x03718000: 264, + 0x0372e000: 275, + 0x03730000: 276, + 0x0373c000: 282, + 0x0376c000: 306, + 0x0377e000: 315, + 0x03788000: 320, + 0x037a2000: 333, + 0x03816000: 391, + 0x0383a000: 409, + 0x0385e000: 427, + 0x0387c000: 442, + 0x03880000: 444, + 0x03894000: 454, + 0x038a0000: 460, + 0x038b2000: 469, + 0x038d4000: 486, + 0x038d6000: 487, + 0x038d8000: 488, + 0x038da000: 489, + 0x038e2000: 493, + } + for input, want := range indexR7Cases { + if got := currentCastleIndexR7(input); got != want { + t.Fatalf("currentCastleIndexR7(%#x) = %d, want %d", input, got, want) + } + } + indexTGCases := map[uint32]uint32{ + 38521: 271, + 46027: 325, + 47278: 334, + 51170: 362, + 51865: 367, + 52560: 372, + 52699: 373, + 53811: 381, + 57842: 410, + 58120: 412, + 58259: 413, + 59232: 420, + 61456: 436, + 61873: 439, + 62151: 441, + 63402: 450, + 65348: 464, + 66460: 472, + 67155: 477, + 67989: 483, + 68128: 484, + } + for input, want := range indexTGCases { + if got := currentCastleIndexTG(input); got != want { + t.Fatalf("currentCastleIndexTG(%d) = %d, want %d", input, got, want) + } + } + tbCases := map[uint32]uint32{ + 0: 852, + 1: 991, + 0x12345678: 3798660732, + 0xffffffff: 713, + } + for input, want := range tbCases { + got := currentCastleTB(input) + if got != want { + t.Fatalf("currentCastleTB(%#x) = %d, want %d", input, got, want) + } + if roundTrip := invertCurrentCastleTB(got); roundTrip != input { + t.Fatalf("invertCurrentCastleTB(currentCastleTB(%#x)) = %#x, want %#x", input, roundTrip, input) + } + } + if got, want := invertCurrentCastleTB(2115052381), uint32(0x1353003b); got != want { + t.Fatalf("invertCurrentCastleTB(accepted slot 2) = %#x, want %#x", got, want) + } + highSlots := currentCastleHighTimingSlotIndexes() + if len(highSlots) != 63 { + t.Fatalf("currentCastleHighTimingSlotIndexes() len = %d, want 63", len(highSlots)) + } + sortedHighSlots := append([]int(nil), highSlots...) + sort.Ints(sortedHighSlots) + for i, got := range sortedHighSlots { + if want := 431 + i; got != want { + t.Fatalf("currentCastleHighTimingSlotIndexes()[sorted %d] = %d, want %d", i, got, want) + } + } + if got, want := insertCurrentCastleChecksum(payload), `[1,2,Nqkju9cdea0413]`; got != want { + t.Fatalf("insertCurrentCastleChecksum() = %q, want %q", got, want) + } +} + +func TestCurrentCastleLowerFloatTransforms(t *testing.T) { + cases := []struct { + name string + fn func(float64) string + zero string + one string + some string + }{ + {name: "slot1", fn: encodeCurrentCastleLowerFloatSlot1, zero: "4ODg4ODg4OA=", one: "KWfg4ODg4OA=", some: "4jI+qRAsNWs="}, + {name: "slot3", fn: encodeCurrentCastleLowerFloatSlot3, zero: "bm5ubm5ubm4=", one: "G39ubm5ubm4=", some: "agkBHM1lE+c="}, + {name: "slot4", fn: encodeCurrentCastleLowerFloatSlot4, zero: "7Ozs7Ozs7Ow=", one: "0xzs7Ozs7Ow=", some: "rLIxw/ZzUps="}, + {name: "slot5", fn: encodeCurrentCastleLowerFloatSlot5, zero: "YWFhYWFhYWE=", one: "II1hYWFhYWE=", some: "cfjYHOdIAC4="}, + {name: "slot9", fn: encodeCurrentCastleLowerFloatSlot9, zero: "FRUVFRUVFRU=", one: "YMUVFRUVFRU=", some: "VQuWEPdA6/g="}, + {name: "slot10", fn: encodeCurrentCastleLowerFloatSlot10, zero: "m5ubm5ubm5s=", one: "6Yebm5ubm5s=", some: "qxVp5RTxPds="}, + {name: "slot12", fn: encodeCurrentCastleLowerFloatSlot12, zero: "AAAAAAAAAAA=", one: "LWoAAAAAAAA=", some: "2AmpN7GRzQo="}, + {name: "slot13", fn: encodeCurrentCastleLowerFloatSlot13, zero: "7Ozs7Ozs7Ow=", one: "Jwvs7Ozs7Ow=", some: "iAGpDqUtz0M="}, + {name: "slot14", fn: encodeCurrentCastleLowerFloatSlot14, zero: "/f39/f39/f0=", one: "uo39/f39/f0=", some: "vZMISrdaM0I="}, + {name: "slot15", fn: encodeCurrentCastleLowerFloatSlot15, zero: "Hh4eHh4eHh4=", one: "YP8eHh4eHh4=", some: "nqKlQCohY/A="}, + {name: "slot16", fn: encodeCurrentCastleLowerFloatSlot16, zero: "PDw8PDw8PDw=", one: "llQ8PDw8PDw=", some: "HFutzi2ma8I="}, + {name: "slot17", fn: encodeCurrentCastleLowerFloatSlot17, zero: "6+vr6+vr6+s=", one: "/Pvr6+vr6+s=", some: "qy3+DLGczUQ="}, + {name: "slot18", fn: encodeCurrentCastleLowerFloatSlot18, zero: "AAAAAAAAAAA=", one: "fuEAAAAAAAA=", some: "gLy7XjQ/fe4="}, + {name: "slot19", fn: encodeCurrentCastleLowerFloatSlot19, zero: "AAAAAAAAAAA=", one: "4ZQAAAAAAAA=", some: "MOq7dR/p8ls="}, + {name: "slot21", fn: encodeCurrentCastleLowerFloatSlot21, zero: "1dXV1dXV1dU=", one: "hNDV1dXV1dU=", some: "2XqigzaOfAk="}, + {name: "slot22", fn: encodeCurrentCastleLowerFloatSlot22, zero: "GBgYGBgYGBg=", one: "/wYYGBgYGBg=", some: "ENOj/Vvrz/Y="}, + {name: "slot23", fn: encodeCurrentCastleLowerFloatSlot23, zero: "6Ojo6Ojo6Og=", one: "Z8ro6Ojo6Og=", some: "aaWkRx0gZtc="}, + {name: "slot24", fn: encodeCurrentCastleLowerFloatSlot24, zero: "8/Pz8/Pz8/M=", one: "L3bz8/Pz8/M=", some: "9WqgskkyZfI="}, + {name: "slot29", fn: encodeCurrentCastleLowerFloatSlot29, zero: "MzMzMzMzMzM=", one: "/A8zMzMzMzM=", some: "I6RE+LXUnO4="}, + {name: "slot30", fn: encodeCurrentCastleLowerFloatSlot30, zero: "rKysrKysrKw=", one: "S7KsrKysrKw=", some: "pGcXSe9fe0I="}, + {name: "slot32", fn: encodeCurrentCastleLowerFloatSlot32, zero: "2dnZ2dnZ2dk=", one: "xFLZ2dnZ2dk=", some: "28vPRGndyJ4="}, + {name: "slot33", fn: encodeCurrentCastleLowerFloatSlot33, zero: "MDAwMDAwMDA=", one: "/2wwMDAwMDA=", some: "QMen+7YX3w0="}, + {name: "slot34", fn: encodeCurrentCastleLowerFloatSlot34, zero: "Tk5OTk5OTk4=", one: "vXJOTk5OTk4=", some: "XtEtodCF+a8="}, + {name: "slot35", fn: encodeCurrentCastleLowerFloatSlot35, zero: "AwMDAwMDAwM=", one: "5jMDAwMDAwM=", some: "QylMFnXGCT4="}, + {name: "slot36", fn: encodeCurrentCastleLowerFloatSlot36, zero: "WFhYWFhYWFg=", one: "OfdYWFhYWFg=", some: "qhp2uai0lUs="}, + {name: "slot37", fn: encodeCurrentCastleLowerFloatSlot37, zero: "Li4uLi4uLi4=", one: "OV4uLi4uLi4=", some: "bhArqfTZMDE="}, + {name: "slot38", fn: encodeCurrentCastleLowerFloatSlot38, zero: "MzMzMzMzMzM=", one: "LLozMzMzMzM=", some: "NSUhrAMvKO4="}, + {name: "slot39", fn: encodeCurrentCastleLowerFloatSlot39, zero: "AAAAAAAAAAA=", one: "mqEAAAAAAAA=", some: "gXSOOl3atOo="}, + {name: "slot43", fn: encodeCurrentCastleLowerFloatSlot43, zero: "q6urq6urq6s=", one: "RJCrq6urq6s=", some: "/2KKcZ7G7Gg="}, + {name: "slot45", fn: encodeCurrentCastleLowerFloatSlot45, zero: "q6urq6urq6s=", one: "2turq6urq6s=", some: "a21cCml6DRI="}, + {name: "slot48", fn: encodeCurrentCastleLowerFloatSlot48, zero: "AAAAAAAAAAA=", one: "LYIAAAAAAAA=", some: "Bld6r/UuUGw="}, + {name: "slot50", fn: encodeCurrentCastleLowerFloatSlot50, zero: "WVlZWVlZWVk=", one: "IslZWVlZWVk=", some: "mQtckjfCK6o="}, + {name: "slot51", fn: encodeCurrentCastleLowerFloatSlot51, zero: "GRkZGRkZGRk=", one: "9J4ZGRkZGRk=", some: "G8uhcinzyrU="}, + {name: "slot52", fn: encodeCurrentCastleLowerFloatSlot52, zero: "AAAAAAAAAAA=", one: "RVAAAAAAAAA=", some: "wCovlX5lSi0="}, + {name: "slot54", fn: encodeCurrentCastleLowerFloatSlot54, zero: "xcXFxcXFxcU=", one: "mMbFxcXFxcU=", some: "wal4mWGSoxw="}, + {name: "slot55", fn: encodeCurrentCastleLowerFloatSlot55, zero: "AAAAAAAAAAA=", one: "3qQAAAAAAAA=", some: "hZuQfR4X06M="}, + {name: "slot57", fn: encodeCurrentCastleLowerFloatSlot57, zero: "sLCwsLCwsLA=", one: "jVCwsLCwsLA=", some: "0wceiszNpN0="}, + {name: "slot58", fn: encodeCurrentCastleLowerFloatSlot58, zero: "XFxcXFxcXFw=", one: "UNxcXFxcXFw=", some: "Wk1H0G9VShY="}, + {name: "slot59", fn: encodeCurrentCastleLowerFloatSlot59, zero: "6urq6urq6uo=", one: "BGrq6urq6uo=", some: "7PwRgxsH/0Y="}, + {name: "slot61", fn: encodeCurrentCastleLowerFloatSlot61, zero: "UFBQUFBQUFA=", one: "6VlQUFBQUFA=", some: "XHUL4r/jf2I="}, + {name: "slot62", fn: encodeCurrentCastleLowerFloatSlot62, zero: "3d3d3d3d3d0=", one: "yI3d3d3d3d0=", some: "HVdKuNNod4A="}, + {name: "slot63", fn: encodeCurrentCastleLowerFloatSlot63, zero: "a2tra2tra2s=", one: "cetra2tra2s=", some: "aX6C8Zh0fbc="}, + {name: "slot65", fn: encodeCurrentCastleLowerFloatSlot65, zero: "g4ODg4ODg4M=", one: "gLODg4ODg4M=", some: "w+UqsLFgxdg="}, + {name: "slot66", fn: encodeCurrentCastleLowerFloatSlot66, zero: "np6enp6enp4=", one: "da6enp6enp4=", some: "3jBfxVxVEN0="}, + {name: "slot69", fn: encodeCurrentCastleLowerFloatSlot69, zero: "gICAgICAgIA=", one: "xqSAgICAgIA=", some: "sBVL6j7uPeg="}, + {name: "slot70", fn: encodeCurrentCastleLowerFloatSlot70, zero: "ycnJycnJyck=", one: "dlHJycnJyck=", some: "6eYnfsQGthI="}, + {name: "slot74", fn: encodeCurrentCastleLowerFloatSlot74, zero: "n5+fn5+fn58=", one: "Du+fn5+fn58=", some: "X53MHuHufVY="}, + {name: "slot76", fn: encodeCurrentCastleLowerFloatSlot76, zero: "jY2NjY2NjY0=", one: "IEWNjY2NjY0=", some: "bXCvGJLQYAQ="}, + {name: "slot80", fn: encodeCurrentCastleLowerFloatSlot80, zero: "8PDw8PDw8PA=", one: "IzDw8PDw8PA=", some: "39Y8Y/eBRTI="}, + {name: "slot81", fn: encodeCurrentCastleLowerFloatSlot81, zero: "OTk5OTk5OTk=", one: "6CU5OTk5OTk=", some: "KaJE1KvQitY="}, + {name: "slot82", fn: encodeCurrentCastleLowerFloatSlot82, zero: "c3Nzc3Nzc3M=", one: "Tplzc3Nzc3M=", some: "q7pqtAJy/qk="}, + {name: "slot84", fn: encodeCurrentCastleLowerFloatSlot84, zero: "+/v7+/v7+/s=", one: "t6L7+/v7+/s=", some: "8D4UQjEHjYw="}, + {name: "slot85", fn: encodeCurrentCastleLowerFloatSlot85, zero: "a2tra2tra2s=", one: "BJ5ra2tra2s=", some: "SdmlhBu30KI="}, + {name: "slot88", fn: encodeCurrentCastleLowerFloatSlot88, zero: "wsLCwsLCwsI=", one: "Q6rCwsLCwsI=", some: "4tMUS7Xzg2c="}, + {name: "slot91", fn: encodeCurrentCastleLowerFloatSlot91, zero: "FRUVFRUVFRU=", one: "ZBoVFRUVFRU=", some: "Eb0Jaf5mv+o="}, + {name: "slot93", fn: encodeCurrentCastleLowerFloatSlot93, zero: "XV1dXV1dXV0=", one: "I7xdXV1dXV0=", some: "3eHmA2liILM="}, + {name: "slot94", fn: encodeCurrentCastleLowerFloatSlot94, zero: "V1dXV1dXV1c=", one: "mEdXV1dXV1c=", some: "l3nyiDW4GdA="}, + {name: "slot95", fn: encodeCurrentCastleLowerFloatSlot95, zero: "GxsbGxsbGxs=", one: "9usbGxsbGxs=", some: "202ohrFWLf4="}, + {name: "slot96", fn: encodeCurrentCastleLowerFloatSlot96, zero: "3Nzc3Nzc3Nw=", one: "3iPc3Nzc3Nw=", some: "XJi5/hC9/04="}, + {name: "slot97", fn: encodeCurrentCastleLowerFloatSlot97, zero: "goKCgoKCgoI=", one: "gfKCgoKCgoI=", some: "wmg3MdQhiPk="}, + {name: "slot101", fn: encodeCurrentCastleLowerFloatSlot101, zero: "uLi4uLi4uLg=", one: "Xhm4uLi4uLg=", some: "OcTqvtwehA4="}, + {name: "slot105", fn: encodeCurrentCastleLowerFloatSlot105, zero: "AAAAAAAAAAA=", one: "1iwAAAAAAAA=", some: "EIVswpDOvdw="}, + {name: "slot107", fn: encodeCurrentCastleLowerFloatSlot107, zero: "LCwsLCwsLCw=", one: "21wsLCwsLCw=", some: "bOIpS0Z7AsM="}, + {name: "slot110", fn: encodeCurrentCastleLowerFloatSlot110, zero: "2tra2tra2to=", one: "1Yra2tra2to=", some: "GjBrhdy1EO0="}, + {name: "slot111", fn: encodeCurrentCastleLowerFloatSlot111, zero: "g4ODg4ODg4M=", one: "9sKDg4ODg4M=", some: "guhfNrt3aZU="}, + {name: "slot112", fn: encodeCurrentCastleLowerFloatSlot112, zero: "AAAAAAAAAAA=", one: "l3AAAAAAAAA=", some: "QA7lByr3bo8="}, + {name: "slot114", fn: encodeCurrentCastleLowerFloatSlot114, zero: "aWlpaWlpaWk=", one: "hWVpaWlpaWk=", some: "edgHkf2NwIs="}, + {name: "slot117", fn: encodeCurrentCastleLowerFloatSlot117, zero: "AwMDAwMDAwM=", one: "vAgDAwMDAwM=", some: "B2YauSO+YDI="}, + {name: "slot118", fn: encodeCurrentCastleLowerFloatSlot118, zero: "fX19fX19fX0=", one: "Ak19fX19fX0=", some: "PSOgEmfig8o="}, + {name: "slot122", fn: encodeCurrentCastleLowerFloatSlot122, zero: "Q0NDQ0NDQ0M=", one: "K0FDQ0NDQ0M=", some: "Sw/+KYY3GzI="}, + {name: "slot124", fn: encodeCurrentCastleLowerFloatSlot124, zero: "CAgICAgICAg=", one: "lckICAgICAg=", some: "tAObZoe/LdE="}, + {name: "slot125", fn: encodeCurrentCastleLowerFloatSlot125, zero: "KioqKioqKio=", one: "qAsqKioqKio=", some: "qu7tiGZprzg="}, + {name: "slot126", fn: encodeCurrentCastleLowerFloatSlot126, zero: "ra2tra2tra0=", one: "9B2tra2tra0=", some: "bbuS5NdUG7w="}, + {name: "slot129", fn: encodeCurrentCastleLowerFloatSlot129, zero: "FxcXFxcXFxc=", one: "dgcXFxcXFxc=", some: "V7VUZvHWFa4="}, + {name: "slot131", fn: encodeCurrentCastleLowerFloatSlot131, zero: "PDw8PDw8PDw=", one: "8wA8PDw8PDw=", some: "LKtL97rbk+E="}, + {name: "slot132", fn: encodeCurrentCastleLowerFloatSlot132, zero: "W1tbW1tbW1s=", one: "QstbW1tbW1s=", some: "mw108qHirfo="}, + {name: "slot134", fn: encodeCurrentCastleLowerFloatSlot134, zero: "xcXFxcXFxcU=", one: "5NXFxcXFxcU=", some: "BaeG9OMExyw="}, + {name: "slot136", fn: encodeCurrentCastleLowerFloatSlot136, zero: "hYWFhYWFhYU=", one: "TYOFhYWFhYU=", some: "bTEha8hZPVQ="}, + {name: "slot138", fn: encodeCurrentCastleLowerFloatSlot138, zero: "Y2NjY2NjY2M=", one: "9BNjY2NjY2M=", some: "IzkKpDUU2dw="}, + {name: "slot139", fn: encodeCurrentCastleLowerFloatSlot139, zero: "j4+Pj4+Pj48=", one: "KP+Pj4+Pj48=", some: "TyH6mEUIASA="}, + {name: "slot140", fn: encodeCurrentCastleLowerFloatSlot140, zero: "eHh4eHh4eHg=", one: "rch4eHh4eHg=", some: "uIKHfZZNIiU="}, + {name: "slot141", fn: encodeCurrentCastleLowerFloatSlot141, zero: "UFBQUFBQUFA=", one: "TThQUFBQUFA=", some: "MJ0aVbf9DSE="}, + {name: "slot142", fn: encodeCurrentCastleLowerFloatSlot142, zero: "rq6urq6urq4=", one: "LY6urq6urq4=", some: "L2toDeLtKp0="}, + {name: "slot143", fn: encodeCurrentCastleLowerFloatSlot143, zero: "SUlJSUlJSUk=", one: "/uNJSUlJSUk=", some: "ISp6+NLiDlM="}, + {name: "slot144", fn: encodeCurrentCastleLowerFloatSlot144, zero: "U1NTU1NTU1M=", one: "6HtTU1NTU1M=", some: "sxgNkCL4KBw="}, + {name: "slot145", fn: encodeCurrentCastleLowerFloatSlot145, zero: "p6enp6enp6c=", one: "Rp+np6enp6c=", some: "x9YVPrR2BmI="}, + {name: "slot146", fn: encodeCurrentCastleLowerFloatSlot146, zero: "Dw8PDw8PDw8=", one: "vh8PDw8PDw8=", some: "T418ToneLXY="}, + {name: "slot148", fn: encodeCurrentCastleLowerFloatSlot148, zero: "2NjY2NjY2Ng=", one: "pt/Y2NjY2Ng=", some: "WLSpBvyNm/Y="}, + {name: "slot151", fn: encodeCurrentCastleLowerFloatSlot151, zero: "6urq6urq6uo=", one: "V/Dq6urq6uo=", some: "8qUOXSdLuUg="}, + {name: "slot153", fn: encodeCurrentCastleLowerFloatSlot153, zero: "eHh4eHh4eHg=", one: "C1l4eHh4eHg=", some: "+V1u6hXKHPo="}, + {name: "slot155", fn: encodeCurrentCastleLowerFloatSlot155, zero: "UlJSUlJSUlI=", one: "mNZSUlJSUlI=", some: "VMQNHeGdwV0="}, + {name: "slot157", fn: encodeCurrentCastleLowerFloatSlot157, zero: "GRkZGRkZGRk=", one: "tAsZGRkZGRk=", some: "IeBwtlioxLs="}, + {name: "slot158", fn: encodeCurrentCastleLowerFloatSlot158, zero: "1dXV1dXV1dU=", one: "/iXV1dXV1dU=", some: "FavEzjceS6Y="}, + {name: "slot160", fn: encodeCurrentCastleLowerFloatSlot160, zero: "MDAwMDAwMDA=", one: "LfMwMDAwMDA=", some: "Mamn7JiuKw4="}, + {name: "slot162", fn: encodeCurrentCastleLowerFloatSlot162, zero: "Nzc3Nzc3Nzc=", one: "ysM3Nzc3Nzc=", some: "B+JCBgXyqnA="}, + {name: "slot163", fn: encodeCurrentCastleLowerFloatSlot163, zero: "ampqampqamo=", one: "tFtqampqamo=", some: "6hZB1B6F5yQ="}, + {name: "slot164", fn: encodeCurrentCastleLowerFloatSlot164, zero: "DQ0NDQ0NDQ0=", one: "fFYNDQ0NDQ0=", some: "Cemne6vEMvs="}, + {name: "slot165", fn: encodeCurrentCastleLowerFloatSlot165, zero: "ycnJycnJyck=", one: "lvnJycnJyck=", some: "CUdUhkM2504="}, + {name: "slot166", fn: encodeCurrentCastleLowerFloatSlot166, zero: "tLS0tLS0tLQ=", one: "SHe0tLS0tLQ=", some: "tc3DCNzKTmk="}, + {name: "slot167", fn: encodeCurrentCastleLowerFloatSlot167, zero: "Pz8/Pz8/Pz8=", one: "5q8/Pz8/Pz8=", some: "f21UVslGzV4="}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.fn(0); got != tc.zero { + t.Fatalf("%s(0) = %q, want %q", tc.name, got, tc.zero) + } + if got := tc.fn(1); got != tc.one { + t.Fatalf("%s(1) = %q, want %q", tc.name, got, tc.one) + } + if got := tc.fn(123.456); got != tc.some { + t.Fatalf("%s(123.456) = %q, want %q", tc.name, got, tc.some) + } + }) + } +} + +func TestCurrentCastleSlot6Hash(t *testing.T) { + cases := []struct { + input string + hash uint32 + hex string + }{ + {input: "", hash: 0, hex: "0"}, + {input: "abc", hash: 1712826425, hex: "6617a839"}, + {input: "X Castle", hash: 2975336394, hex: "b15807ca"}, + {input: "function test() { return 1; }", hash: 1625178382, hex: "60de410e"}, + } + for _, tc := range cases { + t.Run(tc.input, func(t *testing.T) { + if got := currentCastleSlot6Hash(tc.input); got != tc.hash { + t.Fatalf("currentCastleSlot6Hash() = %d/%#x, want %d/%#x", got, got, tc.hash, tc.hash) + } + if got := currentCastleSlot6HashHex(tc.input); got != tc.hex { + t.Fatalf("currentCastleSlot6HashHex() = %q, want %q", got, tc.hex) + } + }) + } + if got, want := encodeCurrentCastleSlot6Component5Fallback(currentCastleSlot6HashHex("abc")), "vb24umi7trA="; got != want { + t.Fatalf("encoded component-5 hash = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6AH(currentCastleSlot6HashHex("abc")), "urpamtS5Gpk="; got != want { + t.Fatalf("encoded aH hash = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6EJ(currentCastleSlot6HashHex("abc")), "RERKRupYTlo="; got != want { + t.Fatalf("encoded ej hash = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot6A8(currentCastleSlot6HashHex("abc")), "Dw//H/Jv338="; got != want { + t.Fatalf("encoded a8 hash = %q, want %q", got, want) + } +} + +func TestCurrentCastleSlot6AdditionalByteTransforms(t *testing.T) { + cases := []struct { + name string + got string + want string + }{ + {name: "iT abc", got: encodeCurrentCastleSlot6IT("abc"), want: "fjss"}, + {name: "iT x castle", got: encodeCurrentCastleSlot6IT("X Castle"), want: "dihefhEbtHM="}, + {name: "aG abc", got: encodeCurrentCastleSlot6AG("abc"), want: "UO0O"}, + {name: "aG x castle", got: encodeCurrentCastleSlot6AG("X Castle"), want: "J+/uUB67s8w="}, + {name: "aD abc", got: encodeCurrentCastleSlot6AD("abc"), want: "4jKC"}, + {name: "aD x castle", got: encodeCurrentCastleSlot6AD("X Castle"), want: "p+as4u290qI="}, + {name: "t7 abc", got: encodeCurrentCastleSlot6T7("abc"), want: "LcW9"}, + {name: "t7 x castle", got: encodeCurrentCastleSlot6T7("X Castle"), want: "a0W1LblQVgw="}, + {name: "id abc", got: encodeCurrentCastleSlot6ID("abc"), want: "1vYX"}, + {name: "id x castle", got: encodeCurrentCastleSlot6ID("X Castle"), want: "ta4T1hk5OFc="}, + {name: "fu abc", got: encodeCurrentCastleSlot6FU("abc"), want: "JLSE"}, + {name: "fu x castle", got: encodeCurrentCastleSlot6FU("X Castle"), want: "V2WMJIBQUiM="}, + {name: "ej abc", got: encodeCurrentCastleSlot6EJ("abc"), want: "6uzu"}, + {name: "ej x castle", got: encodeCurrentCastleSlot6EJ("X Castle"), want: "mGiu6s7A8OI="}, + {name: "l9 abc", got: encodeCurrentCastleSlot6L9("abc"), want: "bFxM"}, + {name: "l9 x castle", got: encodeCurrentCastleSlot6L9("X Castle"), want: "/wBObE29PKw="}, + {name: "tx abc", got: encodeCurrentCastleSlot6TX("abc"), want: "uLm+"}, + {name: "tx x castle", got: encodeCurrentCastleSlot6TX("X Castle"), want: "o/veuI6Pt7w="}, + {name: "li abc", got: encodeCurrentCastleSlot6LI("abc"), want: "h4mL"}, + {name: "li x castle", got: encodeCurrentCastleSlot6LI("X Castle"), want: "dQVLh6utnY8="}, + {name: "fL abc", got: encodeCurrentCastleSlot6FL("abc"), want: "B4YG"}, + {name: "fL x castle", got: encodeCurrentCastleSlot6FL("X Castle"), want: "g2cWBw6xjQk="}, + {name: "iO abc", got: encodeCurrentCastleSlot6IO("abc"), want: "LTQ/"}, + {name: "iO x castle", got: encodeCurrentCastleSlot6IO("X Castle"), want: "cuofLa+2/gk="}, + {name: "f3 abc", got: encodeCurrentCastleSlot6F3("abc"), want: "HEqw"}, + {name: "f3 x castle", got: encodeCurrentCastleSlot6F3("X Castle"), want: "pCK6HLPpaNQ="}, + {name: "aj abc", got: encodeCurrentCastleSlot6AJ("abc"), want: "u24h"}, + {name: "aj x castle", got: encodeCurrentCastleSlot6AJ("X Castle"), want: "ZwSiu2AXdYo="}, + {name: "component17 abc", got: encodeCurrentCastleSlot6Component17("abc"), want: "np+g"}, + {name: "component17 x castle", got: encodeCurrentCastleSlot6Component17("X Castle"), want: "pd3AnpCJkZo="}, + {name: "component19 abc", got: encodeCurrentCastleSlot6Component19("abc"), want: "Wt9k"}, + {name: "component19 x castle", got: encodeCurrentCastleSlot6Component19("X Castle"), want: "rZXEWrQ5EW4="}, + {name: "component27 abc", got: encodeCurrentCastleSlot6Component27("abc"), want: "hYmN"}, + {name: "component27 x castle", got: encodeCurrentCastleSlot6Component27("X Castle"), want: "YYANhc3RsZU="}, + {name: "tm abc", got: encodeCurrentCastleSlot6TM("abc"), want: "kYmB"}, + {name: "tm x castle", got: encodeCurrentCastleSlot6TM("X Castle"), want: "WJuAkQE5+bE="}, + {name: "tF abc", got: encodeCurrentCastleSlot6TUpperF("abc"), want: "veDf"}, + {name: "tF x castle", got: encodeCurrentCastleSlot6TUpperF("X Castle"), want: "tn6/vc/S6uE="}, + {name: "fs abc", got: encodeCurrentCastleSlot6FS("abc"), want: "ZZj/"}, + {name: "fs x castle", got: encodeCurrentCastleSlot6FS("X Castle"), want: "KB7XZcsvCXY="}, + {name: "component31 abc", got: encodeCurrentCastleSlot6Component31("abc"), want: "s7W3"}, + {name: "component31 x castle", got: encodeCurrentCastleSlot6Component31("X Castle"), want: "oTF3s9fZybs="}, + {name: "fF abc", got: encodeCurrentCastleSlot6FF("abc"), want: "+J1B"}, + {name: "fF x castle", got: encodeCurrentCastleSlot6FF("X Castle"), want: "sLQx+Amuigo="}, + {name: "aU abc", got: encodeCurrentCastleSlot6AU("abc"), want: "C7N6"}, + {name: "aU x castle", got: encodeCurrentCastleSlot6AU("X Castle"), want: "mPwKC2Kq5mk="}, + {name: "aQ abc", got: encodeCurrentCastleSlot6AQ("abc"), want: "eOdT"}, + {name: "aQ x castle", got: encodeCurrentCastleSlot6AQ("X Castle"), want: "bA3ReJMff0o="}, + {name: "lW abc", got: encodeCurrentCastleSlot6LW("abc"), want: "45xV"}, + {name: "lW x castle", got: encodeCurrentCastleSlot6LW("X Castle"), want: "Yuo14+We1sc="}, + {name: "tM abc", got: encodeCurrentCastleSlot6TUpperM("abc"), want: "UGN0"}, + {name: "tM x castle", got: encodeCurrentCastleSlot6TUpperM("X Castle"), want: "rr40UJWmFpg="}, + {name: "th abc", got: encodeCurrentCastleSlot6TH("abc"), want: "lKS0"}, + {name: "th x castle", got: encodeCurrentCastleSlot6TH("X Castle"), want: "BICylLXFRdQ="}, + {name: "is abc", got: encodeCurrentCastleSlot6IS("abc"), want: "xtYm"}, + {name: "is x castle", got: encodeCurrentCastleSlot6IS("X Castle"), want: "cfIgxic3twY="}, + {name: "a8 abc", got: encodeCurrentCastleSlot6A8("abc"), want: "8sLS"}, + {name: "a8 x castle", got: encodeCurrentCastleSlot6A8("X Castle"), want: "Ye7Q8tMjozI="}, + {name: "lM abc", got: encodeCurrentCastleSlot6LUpperM("abc"), want: "w0zV"}, + {name: "lM x castle", got: encodeCurrentCastleSlot6LUpperM("X Castle"), want: "8vq1w2Xupuc="}, + {name: "eA abc", got: encodeCurrentCastleSlot6EA("abc"), want: "IRsd"}, + {name: "eA x castle", got: encodeCurrentCastleSlot6EA("X Castle"), want: "L55dIfz2Bxk="}, + {name: "e3 abc", got: encodeCurrentCastleSlot6E3("abc"), want: "qwBV"}, + {name: "e3 x castle", got: encodeCurrentCastleSlot6E3("X Castle"), want: "Bh61qyV6Avc="}, + {name: "t2 abc", got: encodeCurrentCastleSlot6T2("abc"), want: "CNeS"}, + {name: "t2 x castle", got: encodeCurrentCastleSlot6T2("X Castle"), want: "1Q3yCOKxORw="}, + {name: "tX abc", got: encodeCurrentCastleSlot6TUpperX("abc"), want: "Bh4W"}, + {name: "tX x castle", got: encodeCurrentCastleSlot6TUpperX("X Castle"), want: "zwwXBpaubiY="}, + {name: "l5 abc", got: encodeCurrentCastleSlot6L5("abc"), want: "WKr8"}, + {name: "l5 x castle", got: encodeCurrentCastleSlot6L5("X Castle"), want: "doa8WBxu3qA="}, + {name: "component46 abc", got: encodeCurrentCastleSlot6Component46("abc"), want: "fX5/"}, + {name: "component46 x castle", got: encodeCurrentCastleSlot6Component46("X Castle"), want: "ZLxffY+QeIE="}, + {name: "tz abc", got: encodeCurrentCastleSlot6TZ("abc"), want: "cFQ4"}, + {name: "tz x castle", got: encodeCurrentCastleSlot6TZ("X Castle"), want: "ioM0cDoeHeA="}, + {name: "tP abc", got: encodeCurrentCastleSlot6TP("abc"), want: "+WTP"}, + {name: "tP x castle", got: encodeCurrentCastleSlot6TP("X Castle"), want: "Ns5v+X/qkqU="}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.got != tc.want { + t.Fatalf("got %q, want %q", tc.got, tc.want) + } + }) + } +} + +func TestPopulateCurrentCastleHighTimingSlots(t *testing.T) { + payload := make([]any, currentCastlePayloadSlots) + if err := populateCurrentCastleHighTimingSlots(payload, map[int]float64{ + 431: 0, + 480: 123.456, + 493: 1, + }); err != nil { + t.Fatalf("populateCurrentCastleHighTimingSlots() error = %v", err) + } + for slot := 431; slot <= 493; slot++ { + if payload[slot] == nil { + t.Fatalf("payload[%d] is nil", slot) + } + } + if got, want := payload[431], "xsbGxsbGxsY="; got != want { + t.Fatalf("payload[431] = %q, want %q", got, want) + } + if got, want := payload[442], "hoZgICDGxsY="; got != want { + t.Fatalf("payload[442] = %q, want %q", got, want) + } + if got, want := payload[480], "hhjbKVyZ+PE="; got != want { + t.Fatalf("payload[480] = %q, want %q", got, want) + } + if got, want := payload[493], "OTbGxsbGxsY="; got != want { + t.Fatalf("payload[493] = %q, want %q", got, want) + } + if err := populateCurrentCastleHighTimingSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { + t.Fatalf("populateCurrentCastleHighTimingSlots() with short payload succeeded") + } +} + +func TestPopulateCurrentCastleLowerFloatSlots(t *testing.T) { + payload := newCurrentCastlePayloadScaffold() + if err := populateCurrentCastleLowerFloatSlots(payload, map[int]float64{ + 1: 1, + 3: 123.456, + 19: 1, + }); err != nil { + t.Fatalf("populateCurrentCastleLowerFloatSlots() error = %v", err) + } + if got, want := payload[1], "KWfg4ODg4OA="; got != want { + t.Fatalf("payload[1] = %q, want %q", got, want) + } + if got, want := payload[3], "agkBHM1lE+c="; got != want { + t.Fatalf("payload[3] = %q, want %q", got, want) + } + if got, want := payload[4], "7Ozs7Ozs7Ow="; got != want { + t.Fatalf("payload[4] = %q, want %q", got, want) + } + if got, want := payload[19], "4ZQAAAAAAAA="; got != want { + t.Fatalf("payload[19] = %q, want %q", got, want) + } + if got, want := payload[21], "1dXV1dXV1dU="; got != want { + t.Fatalf("payload[21] = %q, want %q", got, want) + } + if got, want := payload[39], "AAAAAAAAAAA="; got != want { + t.Fatalf("payload[39] = %q, want %q", got, want) + } + if got, want := payload[43], "q6urq6urq6s="; got != want { + t.Fatalf("payload[43] = %q, want %q", got, want) + } + if got, want := payload[63], "a2tra2tra2s="; got != want { + t.Fatalf("payload[63] = %q, want %q", got, want) + } + if got, want := payload[65], "g4ODg4ODg4M="; got != want { + t.Fatalf("payload[65] = %q, want %q", got, want) + } + if got, want := payload[107], "LCwsLCwsLCw="; got != want { + t.Fatalf("payload[107] = %q, want %q", got, want) + } + if got, want := payload[167], "Pz8/Pz8/Pz8="; got != want { + t.Fatalf("payload[167] = %q, want %q", got, want) + } + if got, want := payload[169], "ioqKioqKioo="; got != want { + t.Fatalf("payload[169] = %q, want %q", got, want) + } + if got, want := payload[226], "o6Ojo6Ojo6M="; got != want { + t.Fatalf("payload[226] = %q, want %q", got, want) + } + if got, want := payload[255], "MjIyMjIyMjI="; got != want { + t.Fatalf("payload[255] = %q, want %q", got, want) + } + if got, want := payload[294], "NDQ0NDQ0NDQ="; got != want { + t.Fatalf("payload[294] = %q, want %q", got, want) + } + if got, want := payload[291], "hoaGhoaGhoY="; got != want { + t.Fatalf("payload[291] = %q, want %q", got, want) + } + if got, want := payload[324], "WlpaWlpaWlo="; got != want { + t.Fatalf("payload[324] = %q, want %q", got, want) + } + if got, want := payload[325], "GhoaGhoaGho="; got != want { + t.Fatalf("payload[325] = %q, want %q", got, want) + } + if got, want := payload[373], "pKSkpKSkpKQ="; got != want { + t.Fatalf("payload[373] = %q, want %q", got, want) + } + if got, want := payload[407], "2tra2tra2to="; got != want { + t.Fatalf("payload[407] = %q, want %q", got, want) + } + if got, want := payload[427], "h4eHh4eHh4c="; got != want { + t.Fatalf("payload[427] = %q, want %q", got, want) + } + for slot := range currentCastleLowerFloatSlotEncoders { + if _, ok := payload[slot].(string); !ok || payload[slot] == "" { + t.Fatalf("payload[%d] was not populated with a non-empty string: %#v", slot, payload[slot]) + } + } + if err := populateCurrentCastleLowerFloatSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { + t.Fatalf("populateCurrentCastleLowerFloatSlots() with short payload succeeded") + } +} + +func TestCurrentCastleNumericSlotEncoders(t *testing.T) { + if got, want := currentCastleIndexEUnderscore(0), uint32(0x03508000); got != want { + t.Fatalf("currentCastleIndexEUnderscore(0) = %#x, want %#x", got, want) + } + if got, want := currentCastleIndexR7(currentCastleIndexEUnderscore(0)), uint32(0); got != want { + t.Fatalf("currentCastleIndexR7(currentCastleIndexEUnderscore(0)) = %#x, want %#x", got, want) + } + + simpleCases := []struct { + name string + fn func(uint32) uint32 + zero uint32 + one uint32 + }{ + {name: "slot20", fn: encodeCurrentCastleNumericSlot20, zero: 0xd2c1284a, one: 0x5fd1c84a}, + {name: "slot154", fn: encodeCurrentCastleNumericSlot154, zero: 0x8eca32a4, one: 0xf13ce181}, + {name: "slot159", fn: encodeCurrentCastleNumericIdentity, zero: 0, one: 1}, + {name: "slot182", fn: currentCastleTB, zero: 0x00000354, one: 0x000003df}, + {name: "slot316", fn: currentCastleIndexEUnderscore, zero: 0x03508000, one: 0x0350a000}, + {name: "slot256", fn: encodeCurrentCastleNumericSlot256, zero: 0xe39b0b56, one: 0x4ea7245b}, + {name: "slot348", fn: encodeCurrentCastleNumericSlot348, zero: 0x54297e0d, one: 0x4ff13582}, + } + for _, tc := range simpleCases { + if got := tc.fn(0); got != tc.zero { + t.Fatalf("%s(0) = %#x, want %#x", tc.name, got, tc.zero) + } + if got := tc.fn(1); got != tc.one { + t.Fatalf("%s(1) = %#x, want %#x", tc.name, got, tc.one) + } + } + affineCases := []struct { + slot int + zero uint32 + one uint32 + }{ + {slot: 26, zero: 0x60bd09b2, one: 0x670022bf}, + {slot: 27, zero: 0xb330012c, one: 0x0dd8798d}, + {slot: 28, zero: 0x244dce95, one: 0x6dfeaec2}, + {slot: 31, zero: 0xb78dc961, one: 0xaae15d40}, + {slot: 42, zero: 0x9e224c69, one: 0x944782d0}, + {slot: 49, zero: 0x06d9e43d, one: 0x6a5b8e70}, + {slot: 53, zero: 0x8dd8d0c3, one: 0x27b19724}, + {slot: 56, zero: 0xd9931d51, one: 0xacc004ba}, + {slot: 64, zero: 0xc538ce0b, one: 0x9171777e}, + {slot: 67, zero: 0xf3c0363d, one: 0x4080d6d0}, + {slot: 68, zero: 0xecbd981e, one: 0x76dc5125}, + {slot: 71, zero: 0x677c239f, one: 0xce58e7fa}, + {slot: 73, zero: 0x96d91174, one: 0xb09c39df}, + {slot: 87, zero: 0xaa10f53a, one: 0xeca6e7eb}, + {slot: 98, zero: 0x95bd552c, one: 0xb0a64635}, + {slot: 102, zero: 0x4bcca730, one: 0x52157915}, + {slot: 106, zero: 0xb5f76f6d, one: 0xb2bb1c70}, + {slot: 115, zero: 0xf041ff29, one: 0x9577f8e2}, + {slot: 116, zero: 0x8c84e89a, one: 0x3dfbe269}, + {slot: 121, zero: 0xdbab50b2, one: 0x0fb7ef7f}, + {slot: 123, zero: 0x89d7f1b8, one: 0x7712f7cf}, + {slot: 133, zero: 0x520795d3, one: 0x61439c1a}, + {slot: 147, zero: 0xcd6c2b6c, one: 0x6d80ef95}, + {slot: 149, zero: 0x3b4d9896, one: 0xf4921071}, + {slot: 150, zero: 0x1e187f66, one: 0x5ba191cb}, + {slot: 152, zero: 0xf83df829, one: 0x0160726c}, + {slot: 168, zero: 0x4d504e4f, one: 0xef51b58e}, + {slot: 194, zero: 0xc1cb353f, one: 0xed628648}, + {slot: 196, zero: 0x6d935c25, one: 0x6ddfc840}, + {slot: 198, zero: 0x1a205ab2, one: 0xe350ecf9}, + {slot: 203, zero: 0x45c65c75, one: 0xc14d992a}, + {slot: 205, zero: 0x46fd4c27, one: 0x67a5337e}, + {slot: 206, zero: 0x086aa955, one: 0x40c6c596}, + {slot: 207, zero: 0x2496e7c2, one: 0x0f608701}, + {slot: 209, zero: 0xfed3363f, one: 0x7feea612}, + {slot: 212, zero: 0x2c3ea5b3, one: 0x665c3ee0}, + {slot: 221, zero: 0xfd5c8336, one: 0x9f7dec27}, + {slot: 223, zero: 0xdeb6d52a, one: 0xcb0594c1}, + {slot: 224, zero: 0xa9cd6a67, one: 0x5dbe5d9a}, + {slot: 228, zero: 0xa7e0384c, one: 0x56e8784d}, + {slot: 240, zero: 0xf4661fb5, one: 0x11924198}, + {slot: 246, zero: 0x3c8624df, one: 0x61388f30}, + {slot: 247, zero: 0xed6822f5, one: 0xea4dcacc}, + {slot: 257, zero: 0xac01bfae, one: 0x0e46a163}, + {slot: 258, zero: 0x3d7667d5, one: 0xceb0120a}, + {slot: 260, zero: 0x2590de1a, one: 0xa8853377}, + {slot: 266, zero: 0xa3c420cf, one: 0xa4ee6d12}, + {slot: 267, zero: 0x2b30156a, one: 0xad2cd039}, + {slot: 277, zero: 0xb939ef29, one: 0x4aadc51e}, + {slot: 288, zero: 0x4d25ec39, one: 0x7006f918}, + {slot: 293, zero: 0x9f2c27a4, one: 0x614ef36f}, + {slot: 297, zero: 0x8127b4d5, one: 0x2d162ffa}, + {slot: 311, zero: 0x42f7a6bc, one: 0xf5810f03}, + {slot: 312, zero: 0xca4c0beb, one: 0x178b944c}, + {slot: 318, zero: 0x0fc46406, one: 0xb618188b}, + {slot: 321, zero: 0xb7481870, one: 0x746558bb}, + {slot: 329, zero: 0x828e6d80, one: 0x7dde71e5}, + {slot: 334, zero: 0x79a7bbd2, one: 0x2c829589}, + {slot: 339, zero: 0xfd1e66f3, one: 0x71de90e4}, + {slot: 349, zero: 0xcb462199, one: 0x4d17f152}, + {slot: 354, zero: 0x471a0d4d, one: 0xed05f1de}, + {slot: 356, zero: 0xbdfe52d9, one: 0x32782ba6}, + {slot: 359, zero: 0xa583bc26, one: 0x1699b055}, + {slot: 366, zero: 0xa9ac1690, one: 0xc211c04b}, + {slot: 369, zero: 0xc33c50e9, one: 0x4b6d07f4}, + {slot: 374, zero: 0xe6b394f2, one: 0x5ed52a3b}, + {slot: 375, zero: 0xcf565231, one: 0x4d737d2a}, + {slot: 376, zero: 0x30b3a87e, one: 0xf233f3b7}, + {slot: 386, zero: 0xd1ee53e2, one: 0x78b37579}, + {slot: 388, zero: 0x6cac25da, one: 0xb5035037}, + {slot: 390, zero: 0x9bc2f748, one: 0x03f0733d}, + {slot: 393, zero: 0xfe9478a7, one: 0x9ed36934}, + {slot: 395, zero: 0xb427bace, one: 0xf991e0ef}, + {slot: 399, zero: 0xd6c6c42c, one: 0x3fc1896b}, + {slot: 406, zero: 0x29fc7252, one: 0x4da110bf}, + {slot: 422, zero: 0xecfc92b2, one: 0x9632461b}, + {slot: 425, zero: 0x8e16c592, one: 0xb2160d79}, + {slot: 430, zero: 0x2b90120f, one: 0x40ec1bb2}, + } + for _, tc := range affineCases { + fn, ok := currentCastleNumericSlotEncoders[tc.slot] + if !ok { + t.Fatalf("missing numeric slot encoder for slot %d", tc.slot) + } + if got := fn(0); got != tc.zero { + t.Fatalf("numeric slot %d raw 0 = %#x, want %#x", tc.slot, got, tc.zero) + } + if got := fn(1); got != tc.one { + t.Fatalf("numeric slot %d raw 1 = %#x, want %#x", tc.slot, got, tc.one) + } + } + if got, want := encodeCurrentCastleNumericSlot20(0xdae88a03), uint32(0x15bbadc1); got != want { + t.Fatalf("encodeCurrentCastleNumericSlot20(sentinel) = %#x, want %#x", got, want) + } + if got, want := encodeCurrentCastleNumericSlot154(0xdae88a03), uint32(0xbd4808ff); got != want { + t.Fatalf("encodeCurrentCastleNumericSlot154(sentinel) = %#x, want %#x", got, want) + } + if got, want := currentCastleTB(8), uint32(0x000007ac); got != want { + t.Fatalf("currentCastleTB(8) = %#x, want %#x", got, want) + } + if got, want := currentCastleNumericSlotEncoders[257](currentCastleTB(0)), uint32(0xb0e99ef2); got != want { + t.Fatalf("numeric slot 257 false default = %#x, want %#x", got, want) + } + if got, want := currentCastleNumericSlotEncoders[257](currentCastleTB(1)), uint32(0x3e5af065); got != want { + t.Fatalf("numeric slot 257 true-like default = %#x, want %#x", got, want) + } + if got, want := currentCastleNumericSlotEncoders[289](currentCastleTB(0)), uint32(0x0001d1f0); got != want { + t.Fatalf("numeric slot 289 false default = %#x, want %#x", got, want) + } + if got, want := currentCastleNumericSlotEncoders[289](currentCastleTB(1)), uint32(0x00021d69); got != want { + t.Fatalf("numeric slot 289 true-like default = %#x, want %#x", got, want) + } + if got, want := currentCastleNumericSlotEncoders[152](0xdae88a03), uint32(0xb8ee54f2); got != want { + t.Fatalf("numeric slot 152 sentinel = %#x, want %#x", got, want) + } + if got, want := currentCastleNumericSlotEncoders[258](0xdae88a03), uint32(0x5c577ba0); got != want { + t.Fatalf("numeric slot 258 sentinel = %#x, want %#x", got, want) + } + sentinelCases := []struct { + slot int + want uint32 + }{ + {slot: 26, want: 0xb8d522a5}, + {slot: 28, want: 0xd800b11c}, + {slot: 31, want: 0x20b7bafe}, + {slot: 42, want: 0xff1f0002}, + {slot: 49, want: 0x34e764d6}, + {slot: 64, want: 0xe3b2c864}, + {slot: 67, want: 0xe83187aa}, + {slot: 68, want: 0x572da517}, + {slot: 71, want: 0x08140144}, + {slot: 87, want: 0x938e374d}, + {slot: 98, want: 0x813ade47}, + {slot: 102, want: 0x919f8edf}, + {slot: 106, want: 0x5338546a}, + {slot: 116, want: 0xde374007}, + {slot: 123, want: 0xa65269fd}, + {slot: 133, want: 0x1cb5eea8}, + {slot: 168, want: 0xfdcef110}, + {slot: 198, want: 0xa79b0e6b}, + {slot: 203, want: 0x884f8094}, + {slot: 207, want: 0x1032cf7f}, + {slot: 209, want: 0xd57543b8}, + {slot: 212, want: 0xa8aa2f3a}, + {slot: 221, want: 0xc1423045}, + {slot: 223, want: 0x510aadef}, + {slot: 224, want: 0x945b9200}, + {slot: 228, want: 0xf62f824b}, + {slot: 240, want: 0xf5609fd2}, + {slot: 246, want: 0x6a5ffdd2}, + {slot: 247, want: 0x52eb007a}, + {slot: 257, want: 0x3f3e4bf9}, + {slot: 266, want: 0xb4e6c28c}, + {slot: 277, want: 0xe1cc5734}, + {slot: 289, want: 0xdc42f2f5}, + {slot: 312, want: 0x4f85ef0e}, + {slot: 316, want: 0x1490fb5d}, + {slot: 318, want: 0x2858fd81}, + {slot: 321, want: 0xd4c04751}, + {slot: 356, want: 0x4f2df80c}, + {slot: 366, want: 0xdfa64ed5}, + {slot: 374, want: 0x6746facd}, + {slot: 375, want: 0xa8e6ed38}, + {slot: 376, want: 0x2e9f1745}, + {slot: 386, want: 0x6185984b}, + {slot: 399, want: 0xc363f8ed}, + {slot: 422, want: 0xaf6c7949}, + {slot: 430, want: 0xe14d50f8}, + } + for _, tc := range sentinelCases { + if got := currentCastleNumericSlotEncoders[tc.slot](0xdae88a03); got != tc.want { + t.Fatalf("numeric slot %d sentinel = %#x, want %#x", tc.slot, got, tc.want) + } + } + + slot341Cases := []struct { + name string + raw uint32 + want uint32 + }{ + {name: "falseFlag", raw: 0, want: 0x0000f6a7}, + {name: "trueFlag", raw: 1, want: 0x0000793c}, + } + for _, tc := range slot341Cases { + if got := encodeCurrentCastleNumericSlot341(tc.raw); got != tc.want { + t.Fatalf("encodeCurrentCastleNumericSlot341(%s) = %#x, want %#x", tc.name, got, tc.want) + } + } + + slot428Cases := []struct { + name string + raw uint32 + want uint32 + }{ + {name: "false", raw: 0, want: 0xeb00e01b}, + {name: "true", raw: 1, want: 0xc0f29076}, + {name: "notFunction", raw: 0xdae88a03, want: 0xc64546ec}, + {name: "undefinedTProtect", raw: 0x7b862786, want: 0xfc5c96bd}, + } + for _, tc := range slot428Cases { + if got := encodeCurrentCastleNumericSlot428(tc.raw); got != tc.want { + t.Fatalf("encodeCurrentCastleNumericSlot428(%s) = %#x, want %#x", tc.name, got, tc.want) + } + } + + slot429Cases := []struct { + name string + raw uint32 + want uint32 + }{ + {name: "huFalseFallback", raw: currentCastleIndexEUnderscore(0), want: 0x14808cbe}, + {name: "huTrueFallback", raw: currentCastleIndexEUnderscore(1), want: 0x7229acbe}, + {name: "zero", raw: 0, want: 0x7bac0cbe}, + {name: "one", raw: 1, want: 0x57fefa07}, + } + for _, tc := range slot429Cases { + if got := encodeCurrentCastleNumericSlot429(tc.raw); got != tc.want { + t.Fatalf("encodeCurrentCastleNumericSlot429(%s) = %#x, want %#x", tc.name, got, tc.want) + } + } +} + +func TestPopulateCurrentCastleNumericSlots(t *testing.T) { + payload := newCurrentCastlePayloadScaffold() + if err := populateCurrentCastleNumericSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleNumericSlots() error = %v", err) + } + for slot, want := range defaultCurrentCastleNumericSlotFinalValues() { + if got := payload[slot]; got != want { + t.Fatalf("payload[%d] = %#v, want %#v", slot, got, want) + } + } + + if err := populateCurrentCastleNumericSlots(payload, map[int]uint32{ + 20: 1, + 26: 1, + 27: 1, + 28: 1, + 31: currentCastleTB(0), + 42: currentCastleIndexEUnderscore(1), + 49: 1, + 64: currentCastleIndexEUnderscore(1), + 67: currentCastleIndexEUnderscore(1), + 68: 1, + 71: currentCastleIndexEUnderscore(1), + 87: currentCastleIndexEUnderscore(1), + 98: 1, + 102: 1, + 106: currentCastleTB(0), + 116: 1, + 121: 1, + 123: currentCastleIndexEUnderscore(1), + 133: currentCastleTB(1), + 152: 1, + 154: 1, + 159: 1, + 168: 1, + 182: 1, + 198: currentCastleTB(1), + 203: 1, + 207: 1, + 209: 1, + 212: 1, + 221: 1, + 223: 1, + 224: currentCastleIndexEUnderscore(1), + 228: currentCastleIndexEUnderscore(1), + 240: 1, + 246: 1, + 247: currentCastleIndexEUnderscore(1), + 256: 1, + 257: currentCastleTB(1), + 258: 1, + 266: 1, + 267: 1, + 277: 1, + 288: 1, + 289: currentCastleTB(1), + 297: 1, + 312: 1, + 316: currentCastleTB(1), + 318: currentCastleIndexEUnderscore(1), + 321: currentCastleIndexEUnderscore(1), + 334: currentCastleTB(1), + 341: 1, + 348: 1, + 356: currentCastleTB(1), + 359: currentCastleTB(1), + 366: 1, + 369: currentCastleIndexEUnderscore(1), + 374: 1, + 375: currentCastleTB(1), + 376: currentCastleIndexEUnderscore(1), + 386: 1, + 399: 1, + 406: 1, + 422: 1, + 425: 1, + 428: 1, + 429: 1, + 430: currentCastleTB(1), + }); err != nil { + t.Fatalf("populateCurrentCastleNumericSlots() with override error = %v", err) + } + if got, want := payload[428], uint32(0xc0f29076); got != want { + t.Fatalf("override payload[428] = %#v, want %#v", got, want) + } + if got, want := payload[429], uint32(0x57fefa07); got != want { + t.Fatalf("override payload[429] = %#v, want %#v", got, want) + } + if got, want := payload[20], uint32(0x5fd1c84a); got != want { + t.Fatalf("override payload[20] = %#v, want %#v", got, want) + } + if got, want := payload[26], uint32(0x670022bf); got != want { + t.Fatalf("override payload[26] = %#v, want %#v", got, want) + } + if got, want := payload[28], uint32(0x6dfeaec2); got != want { + t.Fatalf("override payload[28] = %#v, want %#v", got, want) + } + if got, want := payload[31], uint32(0xce51c22d); got != want { + t.Fatalf("override payload[31] = %#v, want %#v", got, want) + } + if got, want := payload[42], uint32(0x78f2ac69); got != want { + t.Fatalf("override payload[42] = %#v, want %#v", got, want) + } + if got, want := payload[49], uint32(0x6a5b8e70); got != want { + t.Fatalf("override payload[49] = %#v, want %#v", got, want) + } + if got, want := payload[64], uint32(0x7c10ae0b); got != want { + t.Fatalf("override payload[64] = %#v, want %#v", got, want) + } + if got, want := payload[67], uint32(0xce74563d); got != want { + t.Fatalf("override payload[67] = %#v, want %#v", got, want) + } + if got, want := payload[68], uint32(0x76dc5125); got != want { + t.Fatalf("override payload[68] = %#v, want %#v", got, want) + } + if got, want := payload[71], uint32(0xe94a039f); got != want { + t.Fatalf("override payload[71] = %#v, want %#v", got, want) + } + if got, want := payload[87], uint32(0x50e3553a); got != want { + t.Fatalf("override payload[87] = %#v, want %#v", got, want) + } + if got, want := payload[98], uint32(0xb0a64635); got != want { + t.Fatalf("override payload[98] = %#v, want %#v", got, want) + } + if got, want := payload[102], uint32(0x52157915); got != want { + t.Fatalf("override payload[102] = %#v, want %#v", got, want) + } + if got, want := payload[106], uint32(0xdc9222f1); got != want { + t.Fatalf("override payload[106] = %#v, want %#v", got, want) + } + if got, want := payload[116], uint32(0x3dfbe269); got != want { + t.Fatalf("override payload[116] = %#v, want %#v", got, want) + } + if got, want := payload[123], uint32(0x03d991b8); got != want { + t.Fatalf("override payload[123] = %#v, want %#v", got, want) + } + if got, want := payload[133], uint32(0xe032d1a4); got != want { + t.Fatalf("override payload[133] = %#v, want %#v", got, want) + } + if got, want := payload[154], uint32(0xf13ce181); got != want { + t.Fatalf("override payload[154] = %#v, want %#v", got, want) + } + if got, want := payload[212], uint32(0x665c3ee0); got != want { + t.Fatalf("override payload[212] = %#v, want %#v", got, want) + } + if got, want := payload[224], uint32(0x44bd8a67); got != want { + t.Fatalf("override payload[224] = %#v, want %#v", got, want) + } + if got, want := payload[228], uint32(0x830f984c); got != want { + t.Fatalf("override payload[228] = %#v, want %#v", got, want) + } + if got, want := payload[121], uint32(0x0fb7ef7f); got != want { + t.Fatalf("override payload[121] = %#v, want %#v", got, want) + } + if got, want := payload[256], uint32(0x4ea7245b); got != want { + t.Fatalf("override payload[256] = %#v, want %#v", got, want) + } + if got, want := payload[257], uint32(0x3e5af065); got != want { + t.Fatalf("override payload[257] = %#v, want %#v", got, want) + } + if got, want := payload[247], uint32(0xbd08c2f5); got != want { + t.Fatalf("override payload[247] = %#v, want %#v", got, want) + } + if got, want := payload[267], uint32(0xad2cd039); got != want { + t.Fatalf("override payload[267] = %#v, want %#v", got, want) + } + if got, want := payload[288], uint32(0x7006f918); got != want { + t.Fatalf("override payload[288] = %#v, want %#v", got, want) + } + if got, want := payload[289], uint32(0x00021d69); got != want { + t.Fatalf("override payload[289] = %#v, want %#v", got, want) + } + if got, want := payload[348], uint32(0x4ff13582); got != want { + t.Fatalf("override payload[348] = %#v, want %#v", got, want) + } + if got, want := payload[27], uint32(0x0dd8798d); got != want { + t.Fatalf("override payload[27] = %#v, want %#v", got, want) + } + if got, want := payload[152], uint32(0x0160726c); got != want { + t.Fatalf("override payload[152] = %#v, want %#v", got, want) + } + if got, want := payload[159], uint32(1); got != want { + t.Fatalf("override payload[159] = %#v, want %#v", got, want) + } + if got, want := payload[182], uint32(0x000003df); got != want { + t.Fatalf("override payload[182] = %#v, want %#v", got, want) + } + if got, want := payload[168], uint32(0xef51b58e); got != want { + t.Fatalf("override payload[168] = %#v, want %#v", got, want) + } + if got, want := payload[198], uint32(0x97d20b6f); got != want { + t.Fatalf("override payload[198] = %#v, want %#v", got, want) + } + if got, want := payload[203], uint32(0xc14d992a); got != want { + t.Fatalf("override payload[203] = %#v, want %#v", got, want) + } + if got, want := payload[258], uint32(0xceb0120a); got != want { + t.Fatalf("override payload[258] = %#v, want %#v", got, want) + } + if got, want := payload[341], uint32(0x0000793c); got != want { + t.Fatalf("override payload[341] = %#v, want %#v", got, want) + } + if got, want := payload[318], uint32(0x99814406); got != want { + t.Fatalf("override payload[318] = %#v, want %#v", got, want) + } + if got, want := payload[321], uint32(0x3306f870); got != want { + t.Fatalf("override payload[321] = %#v, want %#v", got, want) + } + if got, want := payload[334], uint32(0x4dcfb4eb); got != want { + t.Fatalf("override payload[334] = %#v, want %#v", got, want) + } + if got, want := payload[356], uint32(0xc1a68330); got != want { + t.Fatalf("override payload[356] = %#v, want %#v", got, want) + } + if got, want := payload[359], uint32(0xc25d59db); got != want { + t.Fatalf("override payload[359] = %#v, want %#v", got, want) + } + if got, want := payload[369], uint32(0x92b330e9); got != want { + t.Fatalf("override payload[369] = %#v, want %#v", got, want) + } + if got, want := payload[374], uint32(0x5ed52a3b); got != want { + t.Fatalf("override payload[374] = %#v, want %#v", got, want) + } + if got, want := payload[386], uint32(0x78b37579); got != want { + t.Fatalf("override payload[386] = %#v, want %#v", got, want) + } + if got, want := payload[406], uint32(0x4da110bf); got != want { + t.Fatalf("override payload[406] = %#v, want %#v", got, want) + } + if got, want := payload[399], uint32(0x3fc1896b); got != want { + t.Fatalf("override payload[399] = %#v, want %#v", got, want) + } + if got, want := payload[422], uint32(0x9632461b); got != want { + t.Fatalf("override payload[422] = %#v, want %#v", got, want) + } + if got, want := payload[297], uint32(0x2d162ffa); got != want { + t.Fatalf("override payload[297] = %#v, want %#v", got, want) + } + if got, want := payload[316], uint32(0x03cc6000); got != want { + t.Fatalf("override payload[316] = %#v, want %#v", got, want) + } + if got, want := payload[425], uint32(0xb2160d79); got != want { + t.Fatalf("override payload[425] = %#v, want %#v", got, want) + } + if got, want := payload[366], uint32(0xc211c04b); got != want { + t.Fatalf("override payload[366] = %#v, want %#v", got, want) + } + if got, want := payload[375], uint32(0xc3e69804); got != want { + t.Fatalf("override payload[375] = %#v, want %#v", got, want) + } + if got, want := payload[376], uint32(0x5560087e); got != want { + t.Fatalf("override payload[376] = %#v, want %#v", got, want) + } + if got, want := payload[430], uint32(0x5c3e0c94); got != want { + t.Fatalf("override payload[430] = %#v, want %#v", got, want) + } + if err := populateCurrentCastleNumericSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { + t.Fatalf("populateCurrentCastleNumericSlots() with short payload succeeded") + } +} + +func TestPopulateCurrentCastleObjectSlots(t *testing.T) { + payload := newCurrentCastlePayloadScaffold() + if err := populateCurrentCastleNumericSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleNumericSlots() error = %v", err) + } + if err := populateCurrentCastleStringSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleStringSlots() error = %v", err) + } + if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleObjectSlots() error = %v", err) + } + slot41, ok := payload[41].(map[string]any) + if !ok { + t.Fatalf("payload[41] type = %T, want map[string]any", payload[41]) + } + if got, want := slot41["V"], "Ls9BGR3QrImUhLGh8DdWMQ=="; got != want { + t.Fatalf("payload[41].V = %q, want %q", got, want) + } + if got, want := slot41["wh"], ""; got != want { + t.Fatalf("payload[41].wh = %q, want %q", got, want) + } + if got, want := slot41["y"], ""; got != want { + t.Fatalf("payload[41].y = %q, want %q", got, want) + } + if got, want := slot41["G"], ""; got != want { + t.Fatalf("payload[41].G = %q, want %q", got, want) + } + slot41Hrc, ok := slot41["Hrc"].(map[string]any) + if !ok { + t.Fatalf("payload[41].Hrc type = %T, want map[string]any", slot41["Hrc"]) + } + if got, want := slot41Hrc["j"], ""; got != want { + t.Fatalf("payload[41].Hrc.j = %q, want %q", got, want) + } + slot77, ok := payload[77].(map[string]any) + if !ok { + t.Fatalf("payload[77] type = %T, want map[string]any", payload[77]) + } + if got, want := slot77["AFS"], "JcW/1uh5mugs0/qG3eXd5Q=="; got != want { + t.Fatalf("payload[77].AFS = %q, want %q", got, want) + } + if got, want := slot77["tu"], ""; got != want { + t.Fatalf("payload[77].tu = %q, want %q", got, want) + } + if got, want := slot77["Xd"], ""; got != want { + t.Fatalf("payload[77].Xd = %q, want %q", got, want) + } + if got, want := slot77["BgV"], ""; got != want { + t.Fatalf("payload[77].BgV = %q, want %q", got, want) + } + slot77OverrideSource := encodeCurrentCastleObjectSlot77FromSource("override") + if got, want := slot77OverrideSource["AFS"], "LNP2Gx3Q2X7QdIp/Bza9OsmqjM4="; got != want { + t.Fatalf("override-source payload[77].AFS = %q, want %q", got, want) + } + if got, want := slot77OverrideSource["tu"], "WnPIiEckrrMC9K6z"; got != want { + t.Fatalf("override-source payload[77].tu = %q, want %q", got, want) + } + if got, want := slot77OverrideSource["Xd"], "84ixof8NxS9L6Bba3eXd5Q=="; got != want { + t.Fatalf("override-source payload[77].Xd = %q, want %q", got, want) + } + if got, want := slot77OverrideSource["BgV"], ""; got != want { + t.Fatalf("override-source payload[77].BgV = %q, want %q", got, want) + } + slot99, ok := payload[99].(map[string]any) + if !ok { + t.Fatalf("payload[99] type = %T, want map[string]any", payload[99]) + } + if got, want := slot99["n"], ""; got != want { + t.Fatalf("payload[99].n = %q, want %q", got, want) + } + if got, want := slot99["cOw"], ""; got != want { + t.Fatalf("payload[99].cOw = %q, want %q", got, want) + } + slot99Probably := encodeCurrentCastleObjectSlot99FromCanPlayType("probably") + if got, want := slot99Probably["n"], "xwZQrlpzYPkEdsCAOFtie66z"; got != want { + t.Fatalf("probably payload[99].n = %q, want %q", got, want) + } + if got, want := slot99Probably["cOw"], "vx4WQxnF0Mtie4ybxwZ6zgR2vx56zgy5O90z1TPV"; got != want { + t.Fatalf("probably payload[99].cOw = %q, want %q", got, want) + } + slot99OverrideSource := encodeCurrentCastleObjectSlot99FromSource("override") + if got, want := slot99OverrideSource["n"], "xwaMm6NsZf1l/SuSGcWjbA=="; got != want { + t.Fatalf("override-source payload[99].n = %q, want %q", got, want) + } + if got, want := slot99OverrideSource["cOw"], ""; got != want { + t.Fatalf("override-source payload[99].cOw = %q, want %q", got, want) + } + slot100, ok := payload[100].(map[string]any) + if !ok { + t.Fatalf("payload[100] type = %T, want map[string]any", payload[100]) + } + if got, want := slot100["rj"], "5e4bQb/W"; got != want { + t.Fatalf("payload[100].rj = %q, want %q", got, want) + } + slot100KwJ, ok := slot100["KwJ"].(map[string]any) + if !ok { + t.Fatalf("payload[100].KwJ type = %T, want map[string]any", slot100["KwJ"]) + } + if got, want := slot100KwJ["T"], "CzejbOYagxFvwg=="; got != want { + t.Fatalf("payload[100].KwJ.T = %q, want %q", got, want) + } + slot100L, ok := slot100["L"].(map[string]any) + if !ok { + t.Fatalf("payload[100].L type = %T, want map[string]any", slot100["L"]) + } + if got, want := slot100L["T"], ""; got != want { + t.Fatalf("payload[100].L.T = %q, want %q", got, want) + } + if got, want := slot100["E"], ""; got != want { + t.Fatalf("payload[100].E = %q, want %q", got, want) + } + slot100Probably, err := encodeCurrentCastleObjectSlot100FromSlot99(slot99Probably) + if err != nil { + t.Fatalf("encodeCurrentCastleObjectSlot100FromSlot99(probably) error = %v", err) + } + if got, want := slot100Probably["rj"], "Ls8="; got != want { + t.Fatalf("probably payload[100].rj = %q, want %q", got, want) + } + slot100ProbablyKwJ := slot100Probably["KwJ"].(map[string]any) + if got, want := slot100ProbablyKwJ["T"], "LRR6zgy5es7352/CgxE="; got != want { + t.Fatalf("probably payload[100].KwJ.T = %q, want %q", got, want) + } + slot100OverrideSource, err := encodeCurrentCastleObjectSlot100FromSlot99(slot99OverrideSource) + if err != nil { + t.Fatalf("encodeCurrentCastleObjectSlot100FromSlot99(override source) error = %v", err) + } + if got, want := slot100OverrideSource["rj"], "U6Y="; got != want { + t.Fatalf("override-source payload[100].rj = %q, want %q", got, want) + } + slot100OverrideSourceKwJ := slot100OverrideSource["KwJ"].(map[string]any) + if got, want := slot100OverrideSourceKwJ["T"], "5hrmGi0UkB0LN/fnkB0="; got != want { + t.Fatalf("override-source payload[100].KwJ.T = %q, want %q", got, want) + } + slot46, ok := payload[46].(map[string]any) + if !ok { + t.Fatalf("payload[46] type = %T, want map[string]any", payload[46]) + } + if got, want := slot46["mx"], ""; got != want { + t.Fatalf("payload[46].mx = %q, want %q", got, want) + } + if got, want := slot46["h"], ""; got != want { + t.Fatalf("payload[46].h = %q, want %q", got, want) + } + if got, want := slot46["r"], ""; got != want { + t.Fatalf("payload[46].r = %q, want %q", got, want) + } + if got, want := slot46["AI"], ""; got != want { + t.Fatalf("payload[46].AI = %q, want %q", got, want) + } + slot46Matches := encodeCurrentCastleObjectSlot46FromMatches([]string{"1", "2"}) + if got, want := slot46Matches["mx"], "iZfmGsWEiBWjbBTBAvSMmw=="; got != want { + t.Fatalf("matches payload[46].mx = %q, want %q", got, want) + } + if got, want := slot46Matches["h"], "k5JfO+Xu3eU="; got != want { + t.Fatalf("matches payload[46].h = %q, want %q", got, want) + } + if got, want := slot46Matches["r"], ""; got != want { + t.Fatalf("matches payload[46].r = %q, want %q", got, want) + } + if got, want := slot46Matches["AI"], ""; got != want { + t.Fatalf("matches payload[46].AI = %q, want %q", got, want) + } + slot128, ok := payload[128].(map[string]any) + if !ok { + t.Fatalf("payload[128] type = %T, want map[string]any", payload[128]) + } + if got, want := slot128["ekC"], "jM4d0BtBVjEbQeXusaE="; got != want { + t.Fatalf("payload[128].ekC = %q, want %q", got, want) + } + if got, want := slot128["fCH"], "8Dc="; got != want { + t.Fatalf("payload[128].fCH = %q, want %q", got, want) + } + slot173, ok := payload[173].(map[string]any) + if !ok { + t.Fatalf("payload[173] type = %T, want map[string]any", payload[173]) + } + if got, want := slot173["t"], "gY/mGnrO"; got != want { + t.Fatalf("payload[173].t = %q, want %q", got, want) + } + if got, want := slot173["JZX"], "rImUhLGh8DdWMQ=="; got != want { + t.Fatalf("payload[173].JZX = %q, want %q", got, want) + } + slot173Ga, ok := slot173["ga"].(map[string]any) + if !ok { + t.Fatalf("payload[173].ga type = %T, want map[string]any", slot173["ga"]) + } + if got, want := slot173Ga["sE"], ""; got != want { + t.Fatalf("payload[173].ga.sE = %q, want %q", got, want) + } + if got, want := slot173Ga["OkL"], ""; got != want { + t.Fatalf("payload[173].ga.OkL = %q, want %q", got, want) + } + if got, want := slot173Ga["hD"], ""; got != want { + t.Fatalf("payload[173].ga.hD = %q, want %q", got, want) + } + if got, want := slot173["wPg"], ""; got != want { + t.Fatalf("payload[173].wPg = %q, want %q", got, want) + } + if got, want := slot173["FqX"], ""; got != want { + t.Fatalf("payload[173].FqX = %q, want %q", got, want) + } + slot214, ok := payload[214].(map[string]any) + if !ok { + t.Fatalf("payload[214] type = %T, want map[string]any", payload[214]) + } + if got, want := slot214["h"], "LRTQyw=="; got != want { + t.Fatalf("payload[214].h = %q, want %q", got, want) + } + if got, want := slot214["yhj"], "S+hTpr/W8DdL6PA3"; got != want { + t.Fatalf("payload[214].yhj = %q, want %q", got, want) + } + if got, want := slot214["XI"], ""; got != want { + t.Fatalf("payload[214].XI = %q, want %q", got, want) + } + slot290, ok := payload[290].(map[string]any) + if !ok { + t.Fatalf("payload[290] type = %T, want map[string]any", payload[290]) + } + if got, want := slot290["d"], "LRRvwgs3+WmjbOYaCzcMuQ=="; got != want { + t.Fatalf("payload[290].d = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleObjectSlot290ValueForTest(payload), "LRRvwgs3+WmjbOYaCzcMuQ=="; got != want { + t.Fatalf("encodeCurrentCastleObjectSlot290ValueForTest() = %q, want %q", got, want) + } + slot342, ok := payload[342].(map[string]any) + if !ok { + t.Fatalf("payload[342] type = %T, want map[string]any", payload[342]) + } + if got, want := slot342["kFx"], "CzfQy+YaCzdac9DLLRSQHQ=="; got != want { + t.Fatalf("payload[342].kFx = %q, want %q", got, want) + } + slot352, ok := payload[352].(map[string]any) + if !ok { + t.Fatalf("payload[352] type = %T, want map[string]any", payload[352]) + } + if got, want := slot352["XYw"], "gY/mGnrO0MsLN5Ad9+ejbA=="; got != want { + t.Fatalf("payload[352].XYw = %q, want %q", got, want) + } + slot416, ok := payload[416].(map[string]any) + if !ok { + t.Fatalf("payload[416] type = %T, want map[string]any", payload[416]) + } + if got, want := slot416["y"], "LRR6zlpzo2xac/lpkB335w=="; got != want { + t.Fatalf("payload[416].y = %q, want %q", got, want) + } + if got, want := slot416["OiJ"], ""; got != want { + t.Fatalf("payload[416].OiJ = %q, want %q", got, want) + } + slot416HRd, ok := slot416["HRd"].(map[string]any) + if !ok { + t.Fatalf("payload[416].HRd type = %T, want map[string]any", slot416["HRd"]) + } + if got, want := slot416HRd["j"], ""; got != want { + t.Fatalf("payload[416].HRd.j = %q, want %q", got, want) + } + if got, want := slot416HRd["hCP"], ""; got != want { + t.Fatalf("payload[416].HRd.hCP = %q, want %q", got, want) + } + slot424, ok := payload[424].(map[string]any) + if !ok { + t.Fatalf("payload[424] type = %T, want map[string]any", payload[424]) + } + if got, want := slot424["kr"], "jM4d0BtBVjE="; got != want { + t.Fatalf("payload[424].kr = %q, want %q", got, want) + } + slot424Vj, ok := slot424["vj"].(map[string]any) + if !ok { + t.Fatalf("payload[424].vj type = %T, want map[string]any", slot424["vj"]) + } + if got, want := slot424Vj["GXV"], "G0Hl7rGh8Dc="; got != want { + t.Fatalf("payload[424].vj.GXV = %q, want %q", got, want) + } + if got, want := slot424["RK"], ""; got != want { + t.Fatalf("payload[424].RK = %q, want %q", got, want) + } + slot424Wxd, ok := slot424["wXd"].(map[string]any) + if !ok { + t.Fatalf("payload[424].wXd type = %T, want map[string]any", slot424["wXd"]) + } + slot424RtN, ok := slot424Wxd["RtN"].(map[string]any) + if !ok { + t.Fatalf("payload[424].wXd.RtN type = %T, want map[string]any", slot424Wxd["RtN"]) + } + if got, want := slot424RtN["o"], ""; got != want { + t.Fatalf("payload[424].wXd.RtN.o = %q, want %q", got, want) + } + if got, want := slot424RtN["ApQ"], ""; got != want { + t.Fatalf("payload[424].wXd.RtN.ApQ = %q, want %q", got, want) + } + if got, want := slot424RtN["G"], ""; got != want { + t.Fatalf("payload[424].wXd.RtN.G = %q, want %q", got, want) + } + if err := populateCurrentCastleNumericSlots(payload, map[int]uint32{289: 0xdae88a03}); err != nil { + t.Fatalf("populateCurrentCastleNumericSlots() sentinel override error = %v", err) + } + if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleObjectSlots() after override error = %v", err) + } + slot290 = payload[290].(map[string]any) + if got, want := slot290["d"], "LRRvwgs3+WmjbOYaCzcMuQ=="; got != want { + t.Fatalf("sentinel payload[290].d = %q, want %q", got, want) + } + if err := populateCurrentCastleNumericSlots(payload, map[int]uint32{341: 1}); err != nil { + t.Fatalf("populateCurrentCastleNumericSlots() slot 341 override error = %v", err) + } + if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleObjectSlots() after slot 341 override error = %v", err) + } + slot342 = payload[342].(map[string]any) + if got, want := slot342["kFx"], "CzfQy+YaCzdac9DLLRSQHQ=="; got != want { + t.Fatalf("override payload[342].kFx = %q, want %q", got, want) + } + if err := populateCurrentCastleStringSlots(payload, map[int]string{351: "override"}); err != nil { + t.Fatalf("populateCurrentCastleStringSlots() slot 351 override error = %v", err) + } + if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleObjectSlots() after string override error = %v", err) + } + slot352 = payload[352].(map[string]any) + if got, want := slot352["XYw"], "gY/mGnrO0MsLN5Ad9+ejbA=="; got != want { + t.Fatalf("override payload[352].XYw = %q, want %q", got, want) + } + if err := populateCurrentCastleStringSlots(payload, map[int]string{213: "override"}); err != nil { + t.Fatalf("populateCurrentCastleStringSlots() slot 213 override error = %v", err) + } + if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleObjectSlots() after slot 213 override error = %v", err) + } + slot214 = payload[214].(map[string]any) + if got, want := slot214["h"], "LRTQyw=="; got != want { + t.Fatalf("override payload[214].h = %q, want %q", got, want) + } + if got, want := slot214["yhj"], "S+hTpr/W8DdL6PA3"; got != want { + t.Fatalf("override payload[214].yhj = %q, want %q", got, want) + } + if got, want := slot214["XI"], ""; got != want { + t.Fatalf("override payload[214].XI = %q, want %q", got, want) + } + if err := populateCurrentCastleStringSlots(payload, map[int]string{127: "override"}); err != nil { + t.Fatalf("populateCurrentCastleStringSlots() slot 127 override error = %v", err) + } + if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleObjectSlots() after slot 127 override error = %v", err) + } + slot128 = payload[128].(map[string]any) + if got, want := slot128["ekC"], "jM4d0BtBVjEbQeXusaE="; got != want { + t.Fatalf("override payload[128].ekC = %q, want %q", got, want) + } + if got, want := slot128["fCH"], "8Dc="; got != want { + t.Fatalf("override payload[128].fCH = %q, want %q", got, want) + } + if err := populateCurrentCastleStringSlots(payload, map[int]string{172: "override"}); err != nil { + t.Fatalf("populateCurrentCastleStringSlots() slot 172 override error = %v", err) + } + if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleObjectSlots() after slot 172 override error = %v", err) + } + slot173 = payload[173].(map[string]any) + if got, want := slot173["t"], "gY/mGnrO"; got != want { + t.Fatalf("override payload[173].t = %q, want %q", got, want) + } + if got, want := slot173["JZX"], "rImUhLGh8DdWMQ=="; got != want { + t.Fatalf("override payload[173].JZX = %q, want %q", got, want) + } + slot173Ga = slot173["ga"].(map[string]any) + if got, want := slot173Ga["sE"], ""; got != want { + t.Fatalf("override payload[173].ga.sE = %q, want %q", got, want) + } + if got, want := slot173Ga["OkL"], ""; got != want { + t.Fatalf("override payload[173].ga.OkL = %q, want %q", got, want) + } + if got, want := slot173Ga["hD"], ""; got != want { + t.Fatalf("override payload[173].ga.hD = %q, want %q", got, want) + } + if got, want := slot173["wPg"], ""; got != want { + t.Fatalf("override payload[173].wPg = %q, want %q", got, want) + } + if got, want := slot173["FqX"], ""; got != want { + t.Fatalf("override payload[173].FqX = %q, want %q", got, want) + } + if err := populateCurrentCastleStringSlots(payload, map[int]string{415: "override"}); err != nil { + t.Fatalf("populateCurrentCastleStringSlots() slot 415 override error = %v", err) + } + if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleObjectSlots() after slot 415 override error = %v", err) + } + slot416 = payload[416].(map[string]any) + if got, want := slot416["y"], "LRR6zlpzo2xac/lpkB335w=="; got != want { + t.Fatalf("override payload[416].y = %q, want %q", got, want) + } + if got, want := slot416["OiJ"], ""; got != want { + t.Fatalf("override payload[416].OiJ = %q, want %q", got, want) + } + slot416HRd = slot416["HRd"].(map[string]any) + if got, want := slot416HRd["j"], ""; got != want { + t.Fatalf("override payload[416].HRd.j = %q, want %q", got, want) + } + if got, want := slot416HRd["hCP"], ""; got != want { + t.Fatalf("override payload[416].HRd.hCP = %q, want %q", got, want) + } + if err := populateCurrentCastleStringSlots(payload, map[int]string{423: "override"}); err != nil { + t.Fatalf("populateCurrentCastleStringSlots() slot 423 override error = %v", err) + } + if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleObjectSlots() after slot 423 override error = %v", err) + } + slot424 = payload[424].(map[string]any) + if got, want := slot424["kr"], "jM4d0BtBVjE="; got != want { + t.Fatalf("override payload[424].kr = %q, want %q", got, want) + } + slot424Vj = slot424["vj"].(map[string]any) + if got, want := slot424Vj["GXV"], "G0Hl7rGh8Dc="; got != want { + t.Fatalf("override payload[424].vj.GXV = %q, want %q", got, want) + } + if got, want := slot424["RK"], ""; got != want { + t.Fatalf("override payload[424].RK = %q, want %q", got, want) + } + slot424Wxd = slot424["wXd"].(map[string]any) + slot424RtN = slot424Wxd["RtN"].(map[string]any) + if got, want := slot424RtN["o"], ""; got != want { + t.Fatalf("override payload[424].wXd.RtN.o = %q, want %q", got, want) + } + if got, want := slot424RtN["ApQ"], ""; got != want { + t.Fatalf("override payload[424].wXd.RtN.ApQ = %q, want %q", got, want) + } + if got, want := slot424RtN["G"], ""; got != want { + t.Fatalf("override payload[424].wXd.RtN.G = %q, want %q", got, want) + } + if err := populateCurrentCastleStringSlots(payload, map[int]string{40: "override"}); err != nil { + t.Fatalf("populateCurrentCastleStringSlots() slot 40 override error = %v", err) + } + if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleObjectSlots() after slot 40 override error = %v", err) + } + slot41 = payload[41].(map[string]any) + if got, want := slot41["V"], "Ls9BGR3QrImUhLGh8DdWMQ=="; got != want { + t.Fatalf("override payload[41].V = %q, want %q", got, want) + } + if got, want := slot41["wh"], ""; got != want { + t.Fatalf("override payload[41].wh = %q, want %q", got, want) + } + if got, want := slot41["y"], ""; got != want { + t.Fatalf("override payload[41].y = %q, want %q", got, want) + } + if got, want := slot41["G"], ""; got != want { + t.Fatalf("override payload[41].G = %q, want %q", got, want) + } + slot41Hrc = slot41["Hrc"].(map[string]any) + if got, want := slot41Hrc["j"], ""; got != want { + t.Fatalf("override payload[41].Hrc.j = %q, want %q", got, want) + } + if err := populateCurrentCastleObjectSlots(payload, map[int]map[string]any{ + 41: {"V": "override41"}, + 77: {"AFS": "override77"}, + 99: {"n": "override99"}, + 100: {"rj": "override100"}, + 46: {"mx": "override46"}, + 128: {"ekC": "override128"}, + 173: {"t": "override173"}, + 214: {"h": "override214"}, + 290: {"d": "override"}, + 342: {"kFx": "override342"}, + 352: {"XYw": "override352"}, + 416: {"y": "override416"}, + 424: {"kr": "override424"}, + }); err != nil { + t.Fatalf("populateCurrentCastleObjectSlots() override error = %v", err) + } + slot41 = payload[41].(map[string]any) + if got, want := slot41["V"], "override41"; got != want { + t.Fatalf("override payload[41].V = %q, want %q", got, want) + } + slot77 = payload[77].(map[string]any) + if got, want := slot77["AFS"], "override77"; got != want { + t.Fatalf("override payload[77].AFS = %q, want %q", got, want) + } + slot99 = payload[99].(map[string]any) + if got, want := slot99["n"], "override99"; got != want { + t.Fatalf("override payload[99].n = %q, want %q", got, want) + } + slot100 = payload[100].(map[string]any) + if got, want := slot100["rj"], "override100"; got != want { + t.Fatalf("override payload[100].rj = %q, want %q", got, want) + } + slot46 = payload[46].(map[string]any) + if got, want := slot46["mx"], "override46"; got != want { + t.Fatalf("override payload[46].mx = %q, want %q", got, want) + } + slot128 = payload[128].(map[string]any) + if got, want := slot128["ekC"], "override128"; got != want { + t.Fatalf("override payload[128].ekC = %q, want %q", got, want) + } + slot173 = payload[173].(map[string]any) + if got, want := slot173["t"], "override173"; got != want { + t.Fatalf("override payload[173].t = %q, want %q", got, want) + } + slot214 = payload[214].(map[string]any) + if got, want := slot214["h"], "override214"; got != want { + t.Fatalf("override payload[214].h = %q, want %q", got, want) + } + slot290 = payload[290].(map[string]any) + if got, want := slot290["d"], "override"; got != want { + t.Fatalf("override payload[290].d = %q, want %q", got, want) + } + slot342 = payload[342].(map[string]any) + if got, want := slot342["kFx"], "override342"; got != want { + t.Fatalf("override payload[342].kFx = %q, want %q", got, want) + } + slot352 = payload[352].(map[string]any) + if got, want := slot352["XYw"], "override352"; got != want { + t.Fatalf("override payload[352].XYw = %q, want %q", got, want) + } + slot416 = payload[416].(map[string]any) + if got, want := slot416["y"], "override416"; got != want { + t.Fatalf("override payload[416].y = %q, want %q", got, want) + } + slot424 = payload[424].(map[string]any) + if got, want := slot424["kr"], "override424"; got != want { + t.Fatalf("override payload[424].kr = %q, want %q", got, want) + } + if err := populateCurrentCastleObjectSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { + t.Fatalf("populateCurrentCastleObjectSlots() with short payload succeeded") + } +} + +func encodeCurrentCastleObjectSlot290ValueForTest(payload []any) string { + encoded, err := encodeCurrentCastleObjectSlot290(payload) + if err != nil { + return "ERROR: " + err.Error() + } + value, _ := encoded["d"].(string) + return value +} + +func TestCurrentCastleLowerFloatEncoderSixthBatch(t *testing.T) { + cases := []struct { + slot int + want string + }{ + {slot: 169, want: "kNqJtno023Y="}, + {slot: 170, want: "wjo4fSk/u54="}, + {slot: 171, want: "Bw3Aapka7RI="}, + {slot: 178, want: "lugV26QrSCM="}, + {slot: 179, want: "nvAp5xT3kN8="}, + {slot: 181, want: "I+6oFcmOm/g="}, + {slot: 185, want: "yfI99uK0cdc="}, + {slot: 186, want: "Bra+Pl6or+g="}, + {slot: 187, want: "vM2SNbNt/WE="}, + {slot: 189, want: "51GaJEYYWQo="}, + {slot: 191, want: "jjVVwgTmLfw="}, + {slot: 193, want: "Hjvh3DH0K9A="}, + {slot: 195, want: "+pxbCViZ/HE="}, + {slot: 197, want: "7FYto4qzNis="}, + {slot: 199, want: "HVla/tDdGI8="}, + {slot: 200, want: "OsCZA8zz4Cs="}, + {slot: 201, want: "NExCiV1Lz+g="}, + {slot: 202, want: "uTE3fMA+s10="}, + {slot: 208, want: "+e4VYAvk7SY="}, + {slot: 210, want: "QI6lR6o37s8="}, + {slot: 211, want: "HN+v+Vfny+I="}, + {slot: 216, want: "Aj7xzHYVp7w="}, + {slot: 219, want: "c1HKtEVksdw="}, + {slot: 222, want: "IznkXo0O2TY="}, + {slot: 225, want: "mAlFU+jTDJM="}, + {slot: 226, want: "ohn2/Iu8m9w="}, + } + for _, tc := range cases { + t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { + encoder := currentCastleLowerFloatSlotEncoders[tc.slot] + if encoder == nil { + t.Fatalf("missing encoder for slot %d", tc.slot) + } + if got := encoder(123.456); got != tc.want { + t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) + } + }) + } +} + +func TestCurrentCastleLowerFloatEncoderSeventhBatch(t *testing.T) { + cases := []struct { + slot int + want string + }{ + {slot: 227, want: "gN+3yhvT4U0="}, + {slot: 229, want: "oCOWa+lDkz8="}, + {slot: 230, want: "XH6fDUKdHsU="}, + {slot: 232, want: "vJLiyWbWNNA="}, + {slot: 233, want: "EJdX64bHr/0="}, + {slot: 234, want: "ulzfjZAdPHU="}, + {slot: 236, want: "OtTxp4j39H8="}, + {slot: 238, want: "CIfnTJZwr34="}, + {slot: 241, want: "D34+l5zfTvM="}, + {slot: 242, want: "5xKC3Mo6/t0="}, + {slot: 243, want: "pLrtRzY3Wj8="}, + {slot: 244, want: "NKJoeoP6pzo="}, + {slot: 248, want: "ti+S+364r1s="}, + {slot: 250, want: "BOXd8qH563c="}, + {slot: 251, want: "wITFYgxBg9I="}, + {slot: 252, want: "aUvOdA+E67w="}, + {slot: 253, want: "GOIXHfbtAjU="}, + {slot: 255, want: "cpQVY1jT9Ls="}, + {slot: 259, want: "TReYPlPut/Y="}, + {slot: 262, want: "uJ0tM2U1MSI="}, + {slot: 263, want: "eJmhjt2Flws="}, + {slot: 265, want: "lDYL8aIhFtk="}, + {slot: 268, want: "/ErazXMOdDo="}, + {slot: 269, want: "sEhKD1tNyew="}, + {slot: 270, want: "7ZWPXiLkmEE="}, + {slot: 272, want: "2s3BUivXzhQ="}, + {slot: 278, want: "0DEJJnUtP6M="}, + {slot: 280, want: "QjIuuRA8Nfs="}, + {slot: 284, want: "qIPjHok0HyM="}, + {slot: 285, want: "oFbzLSL9drU="}, + {slot: 286, want: "m4UG9MFEZaw="}, + {slot: 287, want: "vLo3aXb52pE="}, + {slot: 294, want: "dKJFf/ZPQoc="}, + } + for _, tc := range cases { + t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { + encoder := currentCastleLowerFloatSlotEncoders[tc.slot] + if encoder == nil { + t.Fatalf("missing encoder for slot %d", tc.slot) + } + if got := encoder(123.456); got != tc.want { + t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) + } + }) + } +} + +func TestCurrentCastleLowerFloatEncoderEighthBatch(t *testing.T) { + cases := []struct { + slot int + want string + }{ + {slot: 295, want: "P0sxhaQmCxY="}, + {slot: 296, want: "VW86452i7oI="}, + {slot: 299, want: "I32YVhlG3W4="}, + {slot: 301, want: "GHm+V8txfVo="}, + {slot: 303, want: "ycEfdODWEmU="}, + {slot: 305, want: "NBaXhVLV9k0="}, + {slot: 306, want: "VpPjtRurh6w="}, + {slot: 307, want: "ygl5J4ExFSw="}, + {slot: 309, want: "vT9OXLOsnwQ="}, + {slot: 310, want: "7o1St8S8jz0="}, + {slot: 313, want: "s23OvClMDWQ="}, + {slot: 315, want: "qyMhZhIopIc="}, + {slot: 317, want: "hg3tURx9NUM="}, + {slot: 319, want: "SdTyth3WFUY="}, + {slot: 320, want: "BJrHWZ7pOoE="}, + {slot: 322, want: "udt86p96e6I="}, + {slot: 323, want: "9BEpqt3Vf9s="}, + {slot: 324, want: "W2NlnnJc4P8="}, + {slot: 325, want: "Wnz7SUC53JE="}, + } + for _, tc := range cases { + t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { + encoder := currentCastleLowerFloatSlotEncoders[tc.slot] + if encoder == nil { + t.Fatalf("missing encoder for slot %d", tc.slot) + } + if got := encoder(123.456); got != tc.want { + t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) + } + }) + } +} + +func TestCurrentCastleLowerFloatEncoderSkippedMidBatch(t *testing.T) { + cases := []struct { + slot int + want string + }{ + {slot: 264, want: "TmgJ/7QPiDc="}, + {slot: 271, want: "XUPAcgeC46o="}, + {slot: 275, want: "CuY/I5bnZsY="}, + {slot: 276, want: "Tzc58iYwtJM="}, + {slot: 282, want: "gLy7XjQ/fe4="}, + {slot: 283, want: "NRRsClgDEoY="}, + {slot: 291, want: "rg8/TelXQ14="}, + } + for _, tc := range cases { + t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { + encoder := currentCastleLowerFloatSlotEncoders[tc.slot] + if encoder == nil { + t.Fatalf("missing encoder for slot %d", tc.slot) + } + if got := encoder(123.456); got != tc.want { + t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) + } + }) + } +} + +func TestCurrentCastleLowerFloatEncoderNinthBatch(t *testing.T) { + cases := []struct { + slot int + want string + }{ + {slot: 327, want: "ahpGDRAsubc="}, + {slot: 331, want: "DsRPybD5pLE="}, + {slot: 332, want: "tT3ccSxNFUM="}, + {slot: 333, want: "ANr/FY7F+r0="}, + {slot: 335, want: "525Ool2+hrQ="}, + {slot: 336, want: "7FKBU/bjcks="}, + {slot: 337, want: "LX8nUxjReBQ="}, + {slot: 338, want: "YVuK46XKa+8="}, + {slot: 340, want: "3ewsVcoNnHk="}, + {slot: 343, want: "iqgpm+xrCEM="}, + {slot: 345, want: "za9IfmvODyY="}, + {slot: 346, want: "70YeEaqSIKQ="}, + {slot: 347, want: "rdMWoJ9Qs8g="}, + {slot: 353, want: "pPKJKw4bUrM="}, + {slot: 355, want: "Fkp6MN0iRik="}, + {slot: 357, want: "IW9YGosqDyI="}, + {slot: 358, want: "98XUPhmuJUY="}, + {slot: 360, want: "m21mpJG0DZw="}, + {slot: 361, want: "8JfuP6W3x4M="}, + {slot: 362, want: "STiYbHNIUNo="}, + {slot: 363, want: "DVz+oc67WKI="}, + {slot: 364, want: "AVtHrQpu2k8="}, + {slot: 365, want: "Cp3VqJFpU2c="}, + {slot: 367, want: "lDzTSCt0JHo="}, + {slot: 368, want: "YdkXPGi+ur0="}, + {slot: 370, want: "o5SYKvKul+0="}, + {slot: 371, want: "7jBde2yr0JM="}, + {slot: 372, want: "txswobMsJlE="}, + {slot: 373, want: "5MZDWZIJplE="}, + } + for _, tc := range cases { + t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { + encoder := currentCastleLowerFloatSlotEncoders[tc.slot] + if encoder == nil { + t.Fatalf("missing encoder for slot %d", tc.slot) + } + if got := encoder(123.456); got != tc.want { + t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) + } + }) + } +} + +func TestCurrentCastleLowerFloatEncoderTenthBatch(t *testing.T) { + cases := []struct { + slot int + want string + }{ + {slot: 377, want: "GB/eh/2/T7M="}, + {slot: 379, want: "D4WY8hnCpfo="}, + {slot: 380, want: "/BKN38ZP8rc="}, + {slot: 381, want: "YGn19Dv8OfA="}, + {slot: 382, want: "UbiYdIsI0MI="}, + {slot: 384, want: "pZsaTF/cu3Q="}, + {slot: 385, want: "QI4lx6q37k8="}, + {slot: 387, want: "EO5N3ypPTic="}, + {slot: 389, want: "SrqmMZi0vfM="}, + {slot: 391, want: "gLy7XjQ/fe4="}, + {slot: 396, want: "m7k+aH34GaA="}, + {slot: 397, want: "Nx2aLEHcveQ="}, + {slot: 398, want: "O6mkvv3uCaY="}, + {slot: 401, want: "cMsmLFlsSQw="}, + {slot: 402, want: "/+KligDCcpY="}, + {slot: 403, want: "RjiTkZyBmKk="}, + {slot: 404, want: "R+EDnPaQ6YI="}, + {slot: 405, want: "SquTvO+3pTk="}, + {slot: 407, want: "20D8lRHUwDU="}, + } + for _, tc := range cases { + t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { + encoder := currentCastleLowerFloatSlotEncoders[tc.slot] + if encoder == nil { + t.Fatalf("missing encoder for slot %d", tc.slot) + } + if got := encoder(123.456); got != tc.want { + t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) + } + }) + } +} + +func TestCurrentCastleLowerFloatEncoderEleventhBatch(t *testing.T) { + cases := []struct { + slot int + want string + }{ + {slot: 408, want: "pLfhbuAN954="}, + {slot: 409, want: "qElxXg1VR9s="}, + {slot: 410, want: "VLWNpvmhvyM="}, + {slot: 411, want: "DTOw4vdyk1o="}, + {slot: 412, want: "8HeXK2YHTz0="}, + {slot: 413, want: "15OSNQsWVMU="}, + {slot: 414, want: "8AaD1cpFZh0="}, + {slot: 417, want: "NIpDDRZdqmU="}, + {slot: 418, want: "VJCPMggTUcI="}, + {slot: 419, want: "4h4dwJah31A="}, + {slot: 420, want: "CNY0a0Rx0mg="}, + {slot: 426, want: "ZFy6j/vxLQA="}, + {slot: 427, want: "x71ooinSnYo="}, + } + for _, tc := range cases { + t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { + encoder := currentCastleLowerFloatSlotEncoders[tc.slot] + if encoder == nil { + t.Fatalf("missing encoder for slot %d", tc.slot) + } + if got := encoder(123.456); got != tc.want { + t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) + } + }) + } +} + +func TestCurrentCastleLowerFloatEncoderScatteredBatch(t *testing.T) { + cases := []struct { + slot int + want string + }{ + {slot: 86, want: "kCmz3FqbqXw="}, + {slot: 92, want: "8ftCGK+Im8A="}, + {slot: 135, want: "572GzjnMqc8="}, + {slot: 161, want: "dPubMOoM0wI="}, + {slot: 190, want: "iOpruaYpCsE="}, + {slot: 192, want: "BVX/KPGqVOg="}, + {slot: 215, want: "fVB4U4z0vvo="}, + {slot: 218, want: "DCqbtX7liu0="}, + {slot: 249, want: "vC/Pa1K/V60="}, + {slot: 300, want: "bqiVq5z7yAM="}, + {slot: 314, want: "/nmZJWgJQTM="}, + {slot: 392, want: "UXMUgjcSEzo="}, + } + for _, tc := range cases { + t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { + encoder := currentCastleLowerFloatSlotEncoders[tc.slot] + if encoder == nil { + t.Fatalf("missing encoder for slot %d", tc.slot) + } + if got := encoder(123.456); got != tc.want { + t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) + } + }) + } +} + +func TestCurrentCastleArraySlotEncoders(t *testing.T) { + sample := []float64{0, 1, 123.456} + cases := []struct { + name string + slot int + values []float64 + want string + }{ + {name: "slot254 sample", slot: 254, values: sample, want: "Y2NjY2NjY2Oe02NjY2NjY6O5dE5lvlm2"}, + {name: "slot302 sample", slot: 302, values: sample, want: "wMDAwMDAwMBRycDAwMDAwMzlclqvW+/a"}, + {name: "slot254 empty", slot: 254, values: nil, want: ""}, + {name: "slot302 empty", slot: 302, values: nil, want: ""}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + encoder := currentCastleArraySlotEncoders[tc.slot] + if encoder == nil { + t.Fatalf("missing array encoder for slot %d", tc.slot) + } + if got := encoder(tc.values); got != tc.want { + t.Fatalf("slot %d array sample = %q, want %q", tc.slot, got, tc.want) + } + }) + } + + payload := newCurrentCastlePayloadScaffold() + if err := populateCurrentCastleArraySlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleArraySlots() error = %v", err) + } + if got, want := payload[254], encodeCurrentCastleArraySlot254(currentCastleFloat64Samples(256, 199)); got != want { + t.Fatalf("payload[254] = %q, want %q", got, want) + } + if got, want := payload[302], encodeCurrentCastleArraySlot302(currentCastleFloat64Samples(16, 13)); got != want { + t.Fatalf("payload[302] = %q, want %q", got, want) + } + if err := populateCurrentCastleArraySlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { + t.Fatalf("populateCurrentCastleArraySlots() with short payload succeeded") + } +} + +func TestCurrentCastleStringSlotEncoders(t *testing.T) { + cases := []struct { + name string + value string + want string + }{ + {name: "empty", value: "", want: ""}, + {name: "ascii", value: "abc", want: "qeqtnO+D"}, + {name: "number", value: "123.456", want: "9FL4BDprU243uXGgdFI="}, + {name: "timezone", value: "America/Chicago", want: "j1otnCU4vWSQTu+DqeqtVc1zVmeQTu+Dqepr0VO1"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := encodeCurrentCastleUTF16Scramble(tc.value); got != tc.want { + t.Fatalf("encodeCurrentCastleUTF16Scramble(%q) = %q, want %q", tc.value, got, tc.want) + } + }) + } + if got, want := encodeCurrentCastleDoubleUTF16Scramble("123.456"), "/DYPWrElN7lzDMlBf0u9ZBRw+AQ3uTprNoBGn0lBa9Hr0Q9adz54BA=="; got != want { + t.Fatalf("encodeCurrentCastleDoubleUTF16Scramble() = %q, want %q", got, want) + } + + payload := newCurrentCastlePayloadScaffold() + if err := populateCurrentCastleStringSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleStringSlots() error = %v", err) + } + expectedDefaults := map[int]string{ + 11: "isz", + 40: "NoAtnP9LeAQ=", + 89: "NoAtnP9LeAQ=", + 127: "NoAtnP9LeAQ=", + 172: "NoAtnP9LeAQ=", + 175: "NoAtnP9LeAQ=", + 213: "GraL", + 217: "NoAtnP9LeAQ=", + 231: "NoAtnP9LeAQ=", + 351: "NoAtnP9LeAQ=", + 400: "NoAtnP9LeAQ=", + 415: "NoAtnP9LeAQ=", + 423: "NoAtnP9LeAQ=", + } + for slot, want := range expectedDefaults { + if got := payload[slot]; got != want { + t.Fatalf("payload[%d] = %q, want %q", slot, got, want) + } + } + if err := populateCurrentCastleStringSlots(payload, map[int]string{89: "123.456"}); err != nil { + t.Fatalf("populateCurrentCastleStringSlots() with override error = %v", err) + } + if got, want := payload[89], "9FL4BDprU243uXGgdFI="; got != want { + t.Fatalf("payload[89] override = %q, want %q", got, want) + } + if err := populateCurrentCastleStringSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { + t.Fatalf("populateCurrentCastleStringSlots() with short payload succeeded") + } +} + +func TestCurrentCastlePackedStringSlotEncoders(t *testing.T) { + const slot0MissingProbe = "\u216c\u2166\u216c\u2129\u2097\u598c" + slot0Cases := []struct { + name string + fn func(string) string + want string + }{ + {name: "primary", fn: encodeCurrentCastleSlot0Primary, want: "Wrf7WrdxWrf7WvW2WnIXn3HD"}, + {name: "secondary", fn: encodeCurrentCastleSlot0Secondary, want: "bvpIbvorbvpIbsC5bmxS/CtC"}, + {name: "tertiary", fn: encodeCurrentCastleSlot0Tertiary, want: "HnddHneyHnddHmD4HjK3g7JZ"}, + {name: "quaternary", fn: encodeCurrentCastleSlot0Quaternary, want: "ROQFROTMROQFRAB5RFiu8MwB"}, + {name: "quinary", fn: encodeCurrentCastleSlot0Quinary, want: "A5UGA5VHA5UGA6U2A8UY00fI"}, + {name: "senary", fn: encodeCurrentCastleSlot0Senary, want: "8JO68JO08JO68JK38JCl87Sa"}, + } + for _, tc := range slot0Cases { + t.Run("slot0 "+tc.name, func(t *testing.T) { + if got := tc.fn(slot0MissingProbe); got != tc.want { + t.Fatalf("slot 0 %s encoder = %q, want %q", tc.name, got, tc.want) + } + }) + } + if got, want := encodeCurrentCastleSlot0Primary("abc"), "uHo8"; got != want { + t.Fatalf("encodeCurrentCastleSlot0Primary(sample) = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot0Secondary("abc"), "PGaQ"; got != want { + t.Fatalf("encodeCurrentCastleSlot0Secondary(sample) = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot0Quaternary("abc"), "cFQ4"; got != want { + t.Fatalf("encodeCurrentCastleSlot0Quaternary(sample) = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot0Quinary("abc"), "Gwv7"; got != want { + t.Fatalf("encodeCurrentCastleSlot0Quinary(sample) = %q, want %q", got, want) + } + + slot7Cases := []struct { + name string + fn func(string) string + want string + }{ + {name: "canvas primary", fn: encodeCurrentCastleSlot7CanvasPrimary, want: "K6zp"}, + {name: "worker timing", fn: encodeCurrentCastleSlot7WorkerTiming, want: "JNn2"}, + {name: "media ratio", fn: encodeCurrentCastleSlot7MediaRatio, want: "p1vL"}, + {name: "plugin state", fn: encodeCurrentCastleSlot7PluginState, want: "1qXk"}, + {name: "frame ratio", fn: encodeCurrentCastleSlot7FrameRatio, want: "32it"}, + {name: "worker canvas", fn: encodeCurrentCastleSlot7WorkerCanvas, want: "pM6W"}, + {name: "worker feature", fn: encodeCurrentCastleSlot7WorkerFeature, want: "Acgz"}, + {name: "navigator probe", fn: encodeCurrentCastleSlot7NavigatorProbe, want: "2qQO"}, + {name: "viewport ratio", fn: encodeCurrentCastleSlot7ViewportRatio, want: "jOua"}, + {name: "worker signal", fn: encodeCurrentCastleSlot7WorkerSignal, want: "0zoY"}, + {name: "pointer ratio", fn: encodeCurrentCastleSlot7PointerRatio, want: "KV8X"}, + } + for _, tc := range slot7Cases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.fn("w:1"); got != tc.want { + t.Fatalf("slot 7 %s encoder = %q, want %q", tc.name, got, tc.want) + } + }) + } + if got, want := encodeCurrentCastleSlot7CanvasPrimary("abc"), "+WTP"; got != want { + t.Fatalf("encodeCurrentCastleSlot7CanvasPrimary(sample) = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot7WorkerTiming("abc"), "RtEw"; got != want { + t.Fatalf("encodeCurrentCastleSlot7WorkerTiming(sample) = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot7WorkerSignal("abc"), "FjFR"; got != want { + t.Fatalf("encodeCurrentCastleSlot7WorkerSignal(sample) = %q, want %q", got, want) + } + + if got, want := encodeCurrentCastleWebGPUVendor("g:1"), "3eT7"; got != want { + t.Fatalf("encodeCurrentCastleWebGPUVendor() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleWebGPULimits("g:1"), "ElQu"; got != want { + t.Fatalf("encodeCurrentCastleWebGPULimits() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleWebGPUArchitecture("g:1"), "Q4Su"; got != want { + t.Fatalf("encodeCurrentCastleWebGPUArchitecture() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastlePackedStringComponents(nil), "qj4="; got != want { + t.Fatalf("encodeCurrentCastlePackedStringComponents(nil) = %q, want %q", got, want) + } + if got, want := encodeCurrentCastlePackedStringComponents([]string{"a", "bb"}), "To4Q9k6OHdBL6Evo"; got != want { + t.Fatalf("encodeCurrentCastlePackedStringComponents(sample) = %q, want %q", got, want) + } + if got, want := encodeCurrentCastlePackedStringComponents([]string{"", "", ""}), "tUeqPqo+qj4="; got != want { + t.Fatalf("encodeCurrentCastlePackedStringComponents(empty3) = %q, want %q", got, want) + } + + payload := newCurrentCastlePayloadScaffold() + if err := populateCurrentCastlePackedStringSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastlePackedStringSlots() error = %v", err) + } + if got, want := payload[0].(string), 192; len(got) != want { + t.Fatalf("payload[0] len = %d, want %d", len(got), want) + } + if got, want := payload[7].(string), 1560; len(got) != want { + t.Fatalf("payload[7] len = %d, want %d", len(got), want) + } + if got, want := payload[119].(string), 96; len(got) != want { + t.Fatalf("payload[119] len = %d, want %d", len(got), want) + } + if err := populateCurrentCastlePackedStringSlots(payload, map[int][]string{0: {"a", "bb"}, 7: {"a", "bb"}, 119: {"a", "bb"}}); err != nil { + t.Fatalf("populateCurrentCastlePackedStringSlots() with override error = %v", err) + } + if got, want := payload[0], "To4Q9k6OHdBL6Evo"; got != want { + t.Fatalf("payload[0] override = %q, want %q", got, want) + } + if got, want := payload[7], "To4Q9k6OHdBL6Evo"; got != want { + t.Fatalf("payload[7] override = %q, want %q", got, want) + } + if got, want := payload[119], "To4Q9k6OHdBL6Evo"; got != want { + t.Fatalf("payload[119] override = %q, want %q", got, want) + } + if err := populateCurrentCastlePackedStringSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { + t.Fatalf("populateCurrentCastlePackedStringSlots() with short payload succeeded") + } +} + +func TestCurrentCastleUnitPackedStringSlotEncoders(t *testing.T) { + if got, want := encodeCurrentCastleSlot8Primary("s:1"), "ASz5"; got != want { + t.Fatalf("encodeCurrentCastleSlot8Primary() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot8Secondary("s:1"), "SKnA"; got != want { + t.Fatalf("encodeCurrentCastleSlot8Secondary() = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot8Primary("abc"), "6wur"; got != want { + t.Fatalf("encodeCurrentCastleSlot8Primary(sample) = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleSlot8Secondary("abc"), "dpa2"; got != want { + t.Fatalf("encodeCurrentCastleSlot8Secondary(sample) = %q, want %q", got, want) + } + if got, want := encodeCurrentCastleUnitPackedStringComponents([]string{"a", "bb"}), "RaK7+kWies4EdgR2"; got != want { + t.Fatalf("encodeCurrentCastleUnitPackedStringComponents(sample) = %q, want %q", got, want) + } + + payload := newCurrentCastlePayloadScaffold() + if err := populateCurrentCastleUnitPackedStringSlots(payload, nil); err != nil { + t.Fatalf("populateCurrentCastleUnitPackedStringSlots() error = %v", err) + } + if got, want := payload[8].(string), 52; len(got) != want { + t.Fatalf("payload[8] len = %d, want %d", len(got), want) + } + if err := populateCurrentCastleUnitPackedStringSlots(payload, map[int][]string{8: {"a", "bb"}}); err != nil { + t.Fatalf("populateCurrentCastleUnitPackedStringSlots() with override error = %v", err) + } + if got, want := payload[8], "RaK7+kWies4EdgR2"; got != want { + t.Fatalf("payload[8] override = %q, want %q", got, want) + } + if err := populateCurrentCastleUnitPackedStringSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { + t.Fatalf("populateCurrentCastleUnitPackedStringSlots() with short payload succeeded") + } +} + +func TestCurrentCastlePayloadScaffold(t *testing.T) { + payload := newCurrentCastlePayloadScaffold() + if len(payload) != currentCastlePayloadSlots { + t.Fatalf("newCurrentCastlePayloadScaffold() len = %d, want %d", len(payload), currentCastlePayloadSlots) + } + for slot, value := range payload { + if value == nil { + t.Fatalf("payload[%d] is nil", slot) + } + } + if err := populateCurrentCastleAutomationSlot(payload, 0x1353003b); err != nil { + t.Fatalf("populateCurrentCastleAutomationSlot() error = %v", err) + } + if err := populateCurrentCastleHighTimingSlots(payload, map[int]float64{ + 431: 0, + 480: 123.456, + 493: 1, + }); err != nil { + t.Fatalf("populateCurrentCastleHighTimingSlots() error = %v", err) + } + payloadJSON, err := marshalCurrentCastlePayload(payload) + if err != nil { + t.Fatalf("marshalCurrentCastlePayload() error = %v", err) + } + if strings.Contains(payloadJSON, "null") { + t.Fatalf("payload JSON contains null: %s", payloadJSON) + } + var decoded []any + if err = json.Unmarshal([]byte(payloadJSON), &decoded); err != nil { + t.Fatalf("payload JSON did not unmarshal: %v", err) + } + if len(decoded) != currentCastlePayloadSlots { + t.Fatalf("decoded payload len = %d, want %d", len(decoded), currentCastlePayloadSlots) + } + if got, want := uint32(decoded[2].(float64)), uint32(2115052381); got != want { + t.Fatalf("decoded payload[2] = %d, want %d", got, want) + } + if got, want := decoded[431], "xsbGxsbGxsY="; got != want { + t.Fatalf("decoded payload[431] = %q, want %q", got, want) + } + if got, want := decoded[480], "hhjbKVyZ+PE="; got != want { + t.Fatalf("decoded payload[480] = %q, want %q", got, want) + } + token, err := createCurrentCastleWrappedPayloadToken(payload, 1760000000123) + if err != nil { + t.Fatalf("createCurrentCastleWrappedPayloadToken() error = %v", err) + } + wire, err := decodeCurrentCastleWrappedTokenForTest(token) + if err != nil { + t.Fatalf("decodeCurrentCastleWrappedTokenForTest() error = %v", err) + } + encodedTimestampLen := int(wire[0]) + decodedPayload := xorCurrentCastleString(wire[1+encodedTimestampLen:], "1760000000123") + if strings.Contains(decodedPayload, "null") { + t.Fatalf("wrapped payload contains null: %s", decodedPayload) + } + if _, err = marshalCurrentCastlePayload(payload[:currentCastlePayloadSlots-1]); err == nil { + t.Fatalf("marshalCurrentCastlePayload() with short payload succeeded") + } + payload[7] = nil + if _, err = marshalCurrentCastlePayload(payload); err == nil { + t.Fatalf("marshalCurrentCastlePayload() with nil slot succeeded") + } + if err = populateCurrentCastleAutomationSlot(make([]any, 2), 0); err == nil { + t.Fatalf("populateCurrentCastleAutomationSlot() with short payload succeeded") + } +} + +func TestCurrentCastlePayloadScaffoldAcceptedTypes(t *testing.T) { + payload := newCurrentCastlePayloadScaffold() + typeCounts := map[string]int{} + numericIndexes := make([]int, 0) + objectIndexes := make([]int, 0) + booleanIndexes := make([]int, 0) + for index, value := range payload { + switch value.(type) { + case int, int64, uint32, float64: + typeCounts["number"]++ + numericIndexes = append(numericIndexes, index) + case map[string]any: + typeCounts["object"]++ + objectIndexes = append(objectIndexes, index) + case bool: + typeCounts["boolean"]++ + booleanIndexes = append(booleanIndexes, index) + case string: + typeCounts["string"]++ + default: + t.Fatalf("payload[%d] has unexpected type %T", index, value) + } + } + if got, want := typeCounts["string"], 355; got != want { + t.Fatalf("string slot count = %d, want %d", got, want) + } + if got, want := typeCounts["number"], 125; got != want { + t.Fatalf("number slot count = %d, want %d", got, want) + } + if got, want := typeCounts["object"], 13; got != want { + t.Fatalf("object slot count = %d, want %d", got, want) + } + if got, want := typeCounts["boolean"], 1; got != want { + t.Fatalf("boolean slot count = %d, want %d", got, want) + } + if !sameInts(numericIndexes, currentCastleAcceptedNumericSlots) { + t.Fatalf("numeric slots = %#v, want %#v", numericIndexes, currentCastleAcceptedNumericSlots) + } + if !sameInts(objectIndexes, currentCastleAcceptedObjectSlots) { + t.Fatalf("object slots = %#v, want %#v", objectIndexes, currentCastleAcceptedObjectSlots) + } + if !sameInts(booleanIndexes, []int{currentCastleAcceptedBooleanSlot}) { + t.Fatalf("boolean slots = %#v, want [%d]", booleanIndexes, currentCastleAcceptedBooleanSlot) + } + data, err := json.Marshal(payload) + if err != nil { + t.Fatalf("json.Marshal(payload) error = %v", err) + } + if strings.Contains(string(data), "null") { + t.Fatalf("payload JSON contains null: %s", string(data)) + } + var decoded []any + if err = json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("json.Unmarshal(payload) error = %v", err) + } + decodedCounts := map[string]int{} + for _, value := range decoded { + switch value.(type) { + case float64: + decodedCounts["number"]++ + case map[string]any: + decodedCounts["object"]++ + case bool: + decodedCounts["boolean"]++ + case string: + decodedCounts["string"]++ + default: + t.Fatalf("decoded payload has unexpected type %T", value) + } + } + for key, want := range typeCounts { + if got := decodedCounts[key]; got != want { + t.Fatalf("decoded %s count = %d, want %d", key, got, want) + } + } +} + +func TestBuildCurrentCastlePayload(t *testing.T) { + components := make([]string, currentCastleSlot6ComponentCount) + components[0] = encodeCurrentCastleSlot6Component0("ua") + components[1] = encodeCurrentCastleSlot6Component1("probe") + components[2] = encodeCurrentCastleSlot6Component2("en-US") + components[6] = encodeCurrentCastleSlot6TU("window") + components[10] = encodeCurrentCastleSlot6AH(currentCastleSlot6HashHex("function test() { return 1; }")) + components[18] = encodeCurrentCastleSlot6EJ(currentCastleSlot6HashHex("canvas")) + components[39] = encodeCurrentCastleSlot6A8(currentCastleSlot6HashHex("document")) + expectedSlot6, err := encodeCurrentCastleSlot6(components) + if err != nil { + t.Fatalf("encodeCurrentCastleSlot6() error = %v", err) + } + payload, err := buildCurrentCastlePayload(currentCastlePayloadInput{ + AutomationBitfield: 0x1353003b, + Slot6Components: components, + NumericValues: map[int]uint32{ + 20: 1, + 26: 1, + 27: 1, + 68: 1, + 98: 1, + 152: 1, + 154: 1, + 159: 1, + 182: 1, + 203: 1, + 256: 1, + 257: currentCastleTB(1), + 258: 1, + 289: currentCastleTB(1), + 341: 1, + 348: 1, + 374: 1, + 386: 1, + 406: 1, + 428: 1, + 429: 1, + }, + LowerFloatValues: map[int]float64{ + 1: 1, + 3: 123.456, + }, + HighTimingValues: map[int]float64{ + 431: 0, + 480: 123.456, + 493: 1, + }, + }) + if err != nil { + t.Fatalf("buildCurrentCastlePayload() error = %v", err) + } + if len(payload) != currentCastlePayloadSlots { + t.Fatalf("buildCurrentCastlePayload() len = %d, want %d", len(payload), currentCastlePayloadSlots) + } + if got, want := payload[2], uint32(2115052381); got != want { + t.Fatalf("payload[2] = %v, want %v", got, want) + } + if got, want := payload[20], uint32(0x5fd1c84a); got != want { + t.Fatalf("payload[20] = %v, want %v", got, want) + } + if got, want := payload[26], uint32(0x670022bf); got != want { + t.Fatalf("payload[26] = %v, want %v", got, want) + } + if got, want := payload[68], uint32(0x76dc5125); got != want { + t.Fatalf("payload[68] = %v, want %v", got, want) + } + if got, want := payload[98], uint32(0xb0a64635); got != want { + t.Fatalf("payload[98] = %v, want %v", got, want) + } + if got, want := payload[154], uint32(0xf13ce181); got != want { + t.Fatalf("payload[154] = %v, want %v", got, want) + } + if got, want := payload[77].(map[string]any)["AFS"], "JcW/1uh5mugs0/qG3eXd5Q=="; got != want { + t.Fatalf("payload[77].AFS = %v, want %v", got, want) + } + if got, want := payload[99].(map[string]any)["n"], ""; got != want { + t.Fatalf("payload[99].n = %v, want %v", got, want) + } + if got, want := payload[99].(map[string]any)["cOw"], ""; got != want { + t.Fatalf("payload[99].cOw = %v, want %v", got, want) + } + if got, want := payload[100].(map[string]any)["rj"], "5e4bQb/W"; got != want { + t.Fatalf("payload[100].rj = %v, want %v", got, want) + } + if got, want := payload[100].(map[string]any)["KwJ"].(map[string]any)["T"], "CzejbOYagxFvwg=="; got != want { + t.Fatalf("payload[100].KwJ.T = %v, want %v", got, want) + } + if got, want := payload[46].(map[string]any)["mx"], ""; got != want { + t.Fatalf("payload[46].mx = %v, want %v", got, want) + } + if got, want := payload[128].(map[string]any)["ekC"], "jM4d0BtBVjEbQeXusaE="; got != want { + t.Fatalf("payload[128].ekC = %v, want %v", got, want) + } + if got, want := payload[128].(map[string]any)["fCH"], "8Dc="; got != want { + t.Fatalf("payload[128].fCH = %v, want %v", got, want) + } + if got, want := payload[173].(map[string]any)["t"], "gY/mGnrO"; got != want { + t.Fatalf("payload[173].t = %v, want %v", got, want) + } + if got, want := payload[173].(map[string]any)["JZX"], "rImUhLGh8DdWMQ=="; got != want { + t.Fatalf("payload[173].JZX = %v, want %v", got, want) + } + if got, want := payload[256], uint32(0x4ea7245b); got != want { + t.Fatalf("payload[256] = %v, want %v", got, want) + } + if got, want := payload[257], uint32(0x3e5af065); got != want { + t.Fatalf("payload[257] = %v, want %v", got, want) + } + if got, want := payload[289], uint32(0x00021d69); got != want { + t.Fatalf("payload[289] = %v, want %v", got, want) + } + if got, want := payload[214].(map[string]any)["h"], "LRTQyw=="; got != want { + t.Fatalf("payload[214].h = %v, want %v", got, want) + } + if got, want := payload[214].(map[string]any)["yhj"], "S+hTpr/W8DdL6PA3"; got != want { + t.Fatalf("payload[214].yhj = %v, want %v", got, want) + } + if got, want := payload[214].(map[string]any)["XI"], ""; got != want { + t.Fatalf("payload[214].XI = %v, want %v", got, want) + } + if got, want := payload[290].(map[string]any)["d"], "LRRvwgs3+WmjbOYaCzcMuQ=="; got != want { + t.Fatalf("payload[290].d = %v, want %v", got, want) + } + if got, want := payload[348], uint32(0x4ff13582); got != want { + t.Fatalf("payload[348] = %v, want %v", got, want) + } + if got, want := payload[352].(map[string]any)["XYw"], "gY/mGnrO0MsLN5Ad9+ejbA=="; got != want { + t.Fatalf("payload[352].XYw = %v, want %v", got, want) + } + if got, want := payload[27], uint32(0x0dd8798d); got != want { + t.Fatalf("payload[27] = %v, want %v", got, want) + } + if got, want := payload[152], uint32(0x0160726c); got != want { + t.Fatalf("payload[152] = %v, want %v", got, want) + } + if got, want := payload[159], uint32(1); got != want { + t.Fatalf("payload[159] = %v, want %v", got, want) + } + if got, want := payload[182], uint32(0x000003df); got != want { + t.Fatalf("payload[182] = %v, want %v", got, want) + } + if got, want := payload[203], uint32(0xc14d992a); got != want { + t.Fatalf("payload[203] = %v, want %v", got, want) + } + if got, want := payload[258], uint32(0xceb0120a); got != want { + t.Fatalf("payload[258] = %v, want %v", got, want) + } + if got, want := payload[341], uint32(0x0000793c); got != want { + t.Fatalf("payload[341] = %v, want %v", got, want) + } + if got, want := payload[342].(map[string]any)["kFx"], "CzfQy+YaCzdac9DLLRSQHQ=="; got != want { + t.Fatalf("payload[342].kFx = %v, want %v", got, want) + } + if got, want := payload[374], uint32(0x5ed52a3b); got != want { + t.Fatalf("payload[374] = %v, want %v", got, want) + } + if got, want := payload[386], uint32(0x78b37579); got != want { + t.Fatalf("payload[386] = %v, want %v", got, want) + } + if got, want := payload[406], uint32(0x4da110bf); got != want { + t.Fatalf("payload[406] = %v, want %v", got, want) + } + if got, want := payload[416].(map[string]any)["y"], "LRR6zlpzo2xac/lpkB335w=="; got != want { + t.Fatalf("payload[416].y = %v, want %v", got, want) + } + if got, want := payload[424].(map[string]any)["kr"], "jM4d0BtBVjE="; got != want { + t.Fatalf("payload[424].kr = %v, want %v", got, want) + } + if got, want := payload[428], uint32(0xc0f29076); got != want { + t.Fatalf("payload[428] = %v, want %v", got, want) + } + if got, want := payload[429], uint32(0x57fefa07); got != want { + t.Fatalf("payload[429] = %v, want %v", got, want) + } + if got := payload[6]; got != expectedSlot6 { + t.Fatalf("payload[6] = %q, want %q", got, expectedSlot6) + } + if got, want := payload[1], "KWfg4ODg4OA="; got != want { + t.Fatalf("payload[1] = %q, want %q", got, want) + } + if got, want := payload[3], "agkBHM1lE+c="; got != want { + t.Fatalf("payload[3] = %q, want %q", got, want) + } + if got, want := payload[4], "7Ozs7Ozs7Ow="; got != want { + t.Fatalf("payload[4] = %q, want %q", got, want) + } + if got, want := payload[0].(string), 192; len(got) != want { + t.Fatalf("payload[0] len = %d, want %d", len(got), want) + } + if got, want := payload[7].(string), 1560; len(got) != want { + t.Fatalf("payload[7] len = %d, want %d", len(got), want) + } + if got, want := payload[8].(string), 52; len(got) != want { + t.Fatalf("payload[8] len = %d, want %d", len(got), want) + } + if got, want := payload[254], encodeCurrentCastleArraySlot254(currentCastleFloat64Samples(256, 199)); got != want { + t.Fatalf("payload[254] = %q, want %q", got, want) + } + if got, want := payload[302], encodeCurrentCastleArraySlot302(currentCastleFloat64Samples(16, 13)); got != want { + t.Fatalf("payload[302] = %q, want %q", got, want) + } + if got, want := payload[119].(string), 96; len(got) != want { + t.Fatalf("payload[119] len = %d, want %d", len(got), want) + } + if got, want := payload[431], "xsbGxsbGxsY="; got != want { + t.Fatalf("payload[431] = %q, want %q", got, want) + } + if got, want := payload[480], "hhjbKVyZ+PE="; got != want { + t.Fatalf("payload[480] = %q, want %q", got, want) + } + if got, want := payload[493], "OTbGxsbGxsY="; got != want { + t.Fatalf("payload[493] = %q, want %q", got, want) + } + payloadJSON, err := marshalCurrentCastlePayload(payload) + if err != nil { + t.Fatalf("marshalCurrentCastlePayload() error = %v", err) + } + if strings.Contains(payloadJSON, "null") { + t.Fatalf("payload JSON contains null: %s", payloadJSON) + } + var decoded []any + if err = json.Unmarshal([]byte(payloadJSON), &decoded); err != nil { + t.Fatalf("payload JSON did not unmarshal: %v", err) + } + if got := decoded[6]; got != expectedSlot6 { + t.Fatalf("decoded payload[6] = %q, want %q", got, expectedSlot6) + } + token, err := createCurrentCastleWrappedPayloadToken(payload, 1760000000123) + if err != nil { + t.Fatalf("createCurrentCastleWrappedPayloadToken() error = %v", err) + } + wire, err := decodeCurrentCastleWrappedTokenForTest(token) + if err != nil { + t.Fatalf("decodeCurrentCastleWrappedTokenForTest() error = %v", err) + } + encodedTimestampLen := int(wire[0]) + decodedPayload := xorCurrentCastleString(wire[1+encodedTimestampLen:], "1760000000123") + if !strings.Contains(decodedPayload, expectedSlot6) { + t.Fatalf("wrapped payload missing slot 6 value") + } + if strings.Contains(decodedPayload, "null") { + t.Fatalf("wrapped payload contains null: %s", decodedPayload) + } + if _, err = buildCurrentCastlePayload(currentCastlePayloadInput{ + AutomationBitfield: 0x1353003b, + Slot6Components: components[:len(components)-1], + }); err == nil { + t.Fatalf("buildCurrentCastlePayload() with short slot 6 components succeeded") + } +} + +func TestCurrentCastleLengthPrefixUnits(t *testing.T) { + cases := []struct { + name string + input uint32 + output []uint16 + }{ + {name: "zero", input: 0, output: []uint16{0}}, + {name: "component count", input: 47, output: []uint16{47}}, + {name: "single max", input: 0x7fff, output: []uint16{0x7fff}}, + {name: "first continuation", input: 0x8000, output: []uint16{0x8000, 0x0001}}, + {name: "continuation with payload bits", input: 0x8001, output: []uint16{0x8001, 0x0001}}, + {name: "three units", input: 0x40000000, output: []uint16{0x8000, 0x8000, 0x0001}}, + {name: "uint32 max", input: 0xffffffff, output: []uint16{0xffff, 0xffff, 0x0003}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := currentCastleLengthPrefixUnits(tc.input); !sameUint16s(got, tc.output) { + t.Fatalf("currentCastleLengthPrefixUnits(%#x) = %#v, want %#v", tc.input, got, tc.output) + } + }) + } +} + +func TestCurrentCastleSlot6LengthPrefixUnits(t *testing.T) { + lengths := make([]int, currentCastleSlot6ComponentCount) + for i := range lengths { + lengths[i] = i + } + lengths[1] = 0x8000 + units, err := currentCastleSlot6LengthPrefixUnits(lengths) + if err != nil { + t.Fatalf("currentCastleSlot6LengthPrefixUnits() error = %v", err) + } + wantPrefix := []uint16{47, 0, 0x8000, 1, 2, 3, 4} + if !sameUint16s(units[:len(wantPrefix)], wantPrefix) { + t.Fatalf("slot 6 prefix start = %#v, want %#v", units[:len(wantPrefix)], wantPrefix) + } + if _, err = currentCastleSlot6LengthPrefixUnits(lengths[:len(lengths)-1]); err == nil { + t.Fatalf("currentCastleSlot6LengthPrefixUnits() with short component list succeeded") + } + lengths[7] = -1 + if _, err = currentCastleSlot6LengthPrefixUnits(lengths); err == nil { + t.Fatalf("currentCastleSlot6LengthPrefixUnits() with negative component length succeeded") + } +} + +func TestCurrentCastleSlot6RawUnits(t *testing.T) { + components := make([]string, currentCastleSlot6ComponentCount) + components[0] = "A" + components[1] = "BC" + units, err := currentCastleSlot6RawUnits(components) + if err != nil { + t.Fatalf("currentCastleSlot6RawUnits() error = %v", err) + } + wantPrefix := []uint16{47, 1, 2, 0, 0} + if !sameUint16s(units[:len(wantPrefix)], wantPrefix) { + t.Fatalf("slot 6 raw prefix = %#v, want %#v", units[:len(wantPrefix)], wantPrefix) + } + wantSuffix := []uint16{'A', 'B', 'C'} + if !sameUint16s(units[len(units)-len(wantSuffix):], wantSuffix) { + t.Fatalf("slot 6 raw suffix = %#v, want %#v", units[len(units)-len(wantSuffix):], wantSuffix) + } + if len(units) != currentCastleSlot6ComponentCount+1+len(wantSuffix) { + t.Fatalf("slot 6 raw unit len = %d, want %d", len(units), currentCastleSlot6ComponentCount+1+len(wantSuffix)) + } + encoded, err := encodeCurrentCastleSlot6(components) + if err != nil { + t.Fatalf("encodeCurrentCastleSlot6() error = %v", err) + } + const wantEncoded = "RyS7+kWiMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTO93FhE8s" + if encoded != wantEncoded { + t.Fatalf("encodeCurrentCastleSlot6() = %q, want %q", encoded, wantEncoded) + } + if _, err = currentCastleSlot6RawUnits(components[:len(components)-1]); err == nil { + t.Fatalf("currentCastleSlot6RawUnits() with short components succeeded") + } + if _, err = encodeCurrentCastleSlot6(components[:len(components)-1]); err == nil { + t.Fatalf("encodeCurrentCastleSlot6() with short components succeeded") + } +} + +func TestEncodeCurrentCastleSlot6ComponentValues(t *testing.T) { + values := make([]string, currentCastleSlot6ComponentCount) + values[0] = "toString" + values[1] = "TypeError: Cyclic __proto__ value" + values[2] = "America/Chicago" + values[3] = "ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Ti (0x00001B06) Direct3D11 vs_5_0 ps_5_0, D3D11)" + values[5] = currentCastleSlot6HashHex("function test() { return 1; }") + values[6] = "landscape-primary" + values[8] = "RangeError" + values[12] = "probably" + values[13] = "maybe" + values[14] = "1111111111111111111111111111111111111000000000" + values[15] = "Cannot read properties of undefined (reading 'b')" + values[19] = "091a1f32-3826-4bad-9250-aa14e3c0a2b2" + values[20] = "1000" + values[21] = "Google Inc." + values[22] = values[3] + values[25] = "{}" + values[29] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36" + values[31] = "en-US" + values[34] = "en-US" + values[36] = "Win32" + values[41] = "x.com" + values[44] = "en-US,en" + values[45] = "probably" + components, err := encodeCurrentCastleSlot6ComponentValues(values) + if err != nil { + t.Fatalf("encodeCurrentCastleSlot6ComponentValues() error = %v", err) + } + if len(components) != currentCastleSlot6ComponentCount { + t.Fatalf("components len = %d, want %d", len(components), currentCastleSlot6ComponentCount) + } + cases := map[int]string{ + 0: encodeCurrentCastleSlot6Component0(values[0]), + 1: encodeCurrentCastleSlot6Component1(values[1]), + 2: encodeCurrentCastleSlot6Component2(values[2]), + 3: encodeCurrentCastleSlot6UZ(values[3]), + 5: encodeCurrentCastleSlot6Component5Fallback(values[5]), + 6: encodeCurrentCastleSlot6TU(values[6]), + 8: encodeCurrentCastleSlot6FJ(values[8]), + 12: encodeCurrentCastleSlot6AG(values[12]), + 13: encodeCurrentCastleSlot6AD(values[13]), + 14: encodeCurrentCastleSlot6T7(values[14]), + 15: encodeCurrentCastleSlot6ID(values[15]), + 19: encodeCurrentCastleSlot6Component19(values[19]), + 20: encodeCurrentCastleSlot6L9(values[20]), + 21: encodeCurrentCastleSlot6TX(values[21]), + 22: encodeCurrentCastleSlot6LI(values[22]), + 25: encodeCurrentCastleSlot6F3(values[25]), + 29: encodeCurrentCastleSlot6TUpperF(values[29]), + 31: encodeCurrentCastleSlot6Component31(values[31]), + 34: encodeCurrentCastleSlot6AQ(values[34]), + 36: encodeCurrentCastleSlot6TUpperM(values[36]), + 41: encodeCurrentCastleSlot6EA(values[41]), + 44: encodeCurrentCastleSlot6TUpperX(values[44]), + 45: encodeCurrentCastleSlot6L5(values[45]), + } + for index, want := range cases { + if got := components[index]; got != want { + t.Fatalf("components[%d] = %q, want %q", index, got, want) + } + } + slot6, err := encodeCurrentCastleSlot6(components) + if err != nil { + t.Fatalf("encodeCurrentCastleSlot6() error = %v", err) + } + units, err := currentCastleSlot6RawUnits(components) + if err != nil { + t.Fatalf("currentCastleSlot6RawUnits() error = %v", err) + } + if slot6 != encodeCurrentCastleUnits(units) { + t.Fatalf("slot 6 encoding mismatch") + } + if _, err = encodeCurrentCastleSlot6ComponentValues(values[:len(values)-1]); err == nil { + t.Fatalf("encodeCurrentCastleSlot6ComponentValues() with short values succeeded") + } +} + +func TestBuildCurrentCastleSlot6RawValues(t *testing.T) { + fp := currentCastleSlot6Fingerprint{ + TimestampMillis: 1782425262310, + Timezone: "America/Chicago", + WebGLRenderer: "ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Ti (0x00001B06) Direct3D11 vs_5_0 ps_5_0, D3D11)", + WebGLVendor: "Google Inc.", + Orientation: "landscape-primary", + UserAgent: UserAgent, + Language: "en-US", + Languages: "en-US,en", + Platform: "Win32", + Host: "x.com", + PublicKey: castlePublicKey, + ClientUUID: "091a1f32-3826-4bad-9250-aa14e3c0a2b2", + FeatureBits: currentCastleDefaultFeatureBits, + PrecisionProbe: "1000", + AudioProbe: "20030107", + ProbeDateString: "3/3/1970, 6:00:00 PM", + NumericProbe: "51833740944", + WASMProbeHex: "c0ffee", + Hash5: "39a01d2b", + Hash7: "bd4be70d", + Hash10: "45eb4280", + Hash11: "8fc90f7d", + Hash16: "5d55117f", + Hash18: "42d68d60", + Hash23: "34b454a7", + Hash39: "db77b610", + } + values := buildCurrentCastleSlot6RawValues(fp) + if len(values) != currentCastleSlot6ComponentCount { + t.Fatalf("raw values len = %d, want %d", len(values), currentCastleSlot6ComponentCount) + } + cases := map[int]string{ + 0: "toString", + 1: "TypeError: Cyclic __proto__ value", + 2: fp.Timezone, + 3: fp.WebGLRenderer, + 5: fp.Hash5, + 6: fp.Orientation, + 7: fp.Hash7, + 8: "RangeError", + 9: "[]", + 10: fp.Hash10, + 11: fp.Hash11, + 12: "probably", + 13: "maybe", + 14: fp.FeatureBits, + 15: "Cannot read properties of undefined (reading 'b')", + 16: fp.Hash16, + 18: fp.Hash18, + 19: fp.ClientUUID, + 20: fp.PrecisionProbe, + 21: fp.WebGLVendor, + 22: fp.WebGLRenderer, + 23: fp.Hash23, + 24: fp.AudioProbe, + 25: "{}", + 27: fp.PublicKey, + 28: "Illegal invocation", + 29: fp.UserAgent, + 30: "r:1", + 31: fp.Language, + 33: strconv.FormatInt(currentCastleSlot6ComponentTimestampMillis(fp.TimestampMillis), 10), + 34: fp.Language, + 35: "Maximum call stack size exceeded", + 36: fp.Platform, + 37: fp.ProbeDateString, + 38: "r:1", + 39: fp.Hash39, + 41: fp.Host, + 43: fp.WASMProbeHex, + 44: fp.Languages, + 45: "probably", + 46: fp.NumericProbe, + } + for index, want := range cases { + if got := values[index]; got != want { + t.Fatalf("values[%d] = %q, want %q", index, got, want) + } + } + for _, index := range []int{4, 17, 26, 32, 40, 42} { + if values[index] != "" { + t.Fatalf("values[%d] = %q, want empty", index, values[index]) + } + } + + components, err := buildCurrentCastleSlot6Components(fp) + if err != nil { + t.Fatalf("buildCurrentCastleSlot6Components() error = %v", err) + } + if got, want := components[2], encodeCurrentCastleSlot6Component2(fp.Timezone); got != want { + t.Fatalf("components[2] = %q, want %q", got, want) + } + if got, want := components[19], encodeCurrentCastleSlot6Component19(fp.ClientUUID); got != want { + t.Fatalf("components[19] = %q, want %q", got, want) + } + if got, want := components[27], encodeCurrentCastleSlot6Component27(fp.PublicKey); got != want { + t.Fatalf("components[27] = %q, want %q", got, want) + } +} + +func TestDefaultCurrentCastleSlot6Fingerprint(t *testing.T) { + t.Setenv("TWITTER_JETFUEL_TIMEZONE", "America/Chicago") + const timestampMillis = int64(1782425262310) + const clientUUID = "091a1f32-3826-4bad-9250-aa14e3c0a2b2" + + fp := defaultCurrentCastleSlot6Fingerprint(timestampMillis, clientUUID) + values := buildCurrentCastleSlot6RawValues(fp) + if got, want := values[2], "America/Chicago"; got != want { + t.Fatalf("values[2] = %q, want %q", got, want) + } + if got, want := values[19], clientUUID; got != want { + t.Fatalf("values[19] = %q, want %q", got, want) + } + if got, want := values[21], currentCastleDefaultWebGLVendor; got != want { + t.Fatalf("values[21] = %q, want %q", got, want) + } + if got, want := values[22], currentCastleDefaultWebGLRenderer; got != want { + t.Fatalf("values[22] = %q, want %q", got, want) + } + if got, want := values[27], castlePublicKey; got != want { + t.Fatalf("values[27] = %q, want %q", got, want) + } + if got, want := values[29], UserAgent; got != want { + t.Fatalf("values[29] = %q, want %q", got, want) + } + if got, want := values[33], strconv.FormatInt(currentCastleSlot6ComponentTimestampMillis(timestampMillis), 10); got != want { + t.Fatalf("values[33] = %q, want %q", got, want) + } + if got, want := values[37], "3/3/1970, 6:00:00 PM"; got != want { + t.Fatalf("values[37] = %q, want %q", got, want) + } + exactDefaults := map[int]string{ + 5: currentCastleDefaultHash5, + 7: currentCastleDefaultHash7, + 10: currentCastleDefaultHash10, + 11: currentCastleDefaultHash11, + 16: currentCastleDefaultHash16, + 18: currentCastleDefaultHash18, + 23: currentCastleDefaultHash23, + 39: currentCastleDefaultHash39, + 43: currentCastleDefaultWASMProbeHex, + 46: currentCastleDefaultNumericProbe, + } + for index, want := range exactDefaults { + if got := values[index]; got != want { + t.Fatalf("values[%d] = %q, want %q", index, got, want) + } + } + components, err := buildCurrentCastleSlot6Components(fp) + if err != nil { + t.Fatalf("buildCurrentCastleSlot6Components() error = %v", err) + } + payload, err := buildCurrentCastlePayload(currentCastlePayloadInput{ + AutomationBitfield: currentCastleAcceptedAutomationBitfield, + Slot6Components: components, + }) + if err != nil { + t.Fatalf("buildCurrentCastlePayload() error = %v", err) + } + if got, want := payload[2], uint32(2115052381); got != want { + t.Fatalf("payload[2] = %v, want %v", got, want) + } + if payload[6] == "" { + t.Fatalf("payload[6] is empty") + } + token, err := createCurrentCastleWrappedPayloadToken(payload, timestampMillis) + if err != nil { + t.Fatalf("createCurrentCastleWrappedPayloadToken() error = %v", err) + } + if !strings.HasPrefix(token, currentCastleTokenPrefix) { + t.Fatalf("token missing current Castle prefix") + } +} + +func TestCreateCurrentCastleRequestToken(t *testing.T) { + t.Setenv("TWITTER_JETFUEL_TIMEZONE", "America/Chicago") + token, err := createCurrentCastleRequestToken("091a1f32-3826-4bad-9250-aa14e3c0a2b2") + if err != nil { + t.Fatalf("createCurrentCastleRequestToken() error = %v", err) + } + if !strings.HasPrefix(token, currentCastleTokenPrefix) { + t.Fatalf("token missing current Castle prefix") + } + t.Logf("current Castle token length: %d", len(token)) + if len(token) < 1000 { + t.Fatalf("token length = %d, want a current-format request token", len(token)) + } + if _, err = decodeCurrentCastleWrappedTokenForTest(token); err != nil { + t.Fatalf("decodeCurrentCastleWrappedTokenForTest() error = %v", err) + } +} + +func TestCompareChromeCurrentCastleTokenProbe(t *testing.T) { + token := strings.TrimSpace(os.Getenv("TWITTER_CASTLE_TOKEN_DIFF_PROBE")) + if token == "" { + t.Skip("TWITTER_CASTLE_TOKEN_DIFF_PROBE is required") + } + chromePayload, timestampMillis := decodeCurrentCastlePayloadFromTokenForTest(t, token) + components, err := buildCurrentCastleSlot6Components(defaultCurrentCastleSlot6Fingerprint(timestampMillis, "091a1f32-3826-4bad-9250-aa14e3c0a2b2")) + if err != nil { + t.Fatalf("buildCurrentCastleSlot6Components() error = %v", err) + } + goPayload, err := buildCurrentCastlePayload(currentCastlePayloadInput{ + AutomationBitfield: currentCastleAcceptedAutomationBitfield, + Slot6Components: components, + }) + if err != nil { + t.Fatalf("buildCurrentCastlePayload() error = %v", err) + } + if len(chromePayload) != len(goPayload) { + t.Fatalf("payload lengths: chrome=%d go=%d", len(chromePayload), len(goPayload)) + } + typeCounts := map[string]int{} + mismatches := make([]string, 0) + for i := range chromePayload { + typeCounts[fmt.Sprintf("chrome_%s", currentCastleJSONTypeForTest(chromePayload[i]))]++ + typeCounts[fmt.Sprintf("go_%s", currentCastleJSONTypeForTest(goPayload[i]))]++ + chromeJSON := currentCastleJSONSummaryForTest(t, chromePayload[i]) + goJSON := currentCastleJSONSummaryForTest(t, goPayload[i]) + if chromeJSON != goJSON { + mismatches = append(mismatches, fmt.Sprintf( + "%d:%s/%d/%s -> %s/%d/%s", + i, + currentCastleJSONTypeForTest(chromePayload[i]), + len(chromeJSON), + currentCastleShortHashForTest(chromeJSON), + currentCastleJSONTypeForTest(goPayload[i]), + len(goJSON), + currentCastleShortHashForTest(goJSON), + )) + } + } + sort.Strings(mismatches) + t.Logf("decoded timestamp millis: %d", timestampMillis) + t.Logf("type counts: %v", typeCounts) + t.Logf("mismatch count: %d", len(mismatches)) + limit := min(len(mismatches), 120) + for _, mismatch := range mismatches[:limit] { + t.Logf("mismatch %s", mismatch) + } + if len(mismatches) > limit { + t.Logf("mismatch output truncated: %d more", len(mismatches)-limit) + } + if slots := strings.TrimSpace(os.Getenv("TWITTER_CASTLE_TOKEN_DIFF_VALUE_SLOTS")); slots != "" { + for _, part := range strings.Split(slots, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + slot, err := strconv.Atoi(part) + if err != nil { + t.Fatalf("invalid slot %q in TWITTER_CASTLE_TOKEN_DIFF_VALUE_SLOTS: %v", part, err) + } + if slot < 0 || slot >= len(chromePayload) { + t.Fatalf("slot %d outside payload length %d", slot, len(chromePayload)) + } + chromeString, chromeOK := chromePayload[slot].(string) + goString, goOK := goPayload[slot].(string) + if !chromeOK || !goOK { + t.Logf("value %d chrome_type=%s go_type=%s", slot, currentCastleJSONTypeForTest(chromePayload[slot]), currentCastleJSONTypeForTest(goPayload[slot])) + continue + } + t.Logf("value %d chrome=%q go=%q", slot, chromeString, goString) + } + } + if slots := strings.TrimSpace(os.Getenv("TWITTER_CASTLE_TOKEN_DIFF_ARRAY_SLOTS")); slots != "" { + for _, part := range strings.Split(slots, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + slot, err := strconv.Atoi(part) + if err != nil { + t.Fatalf("invalid slot %q in TWITTER_CASTLE_TOKEN_DIFF_ARRAY_SLOTS: %v", part, err) + } + chromeString, ok := chromePayload[slot].(string) + if !ok { + t.Logf("array %d chrome_type=%s", slot, currentCastleJSONTypeForTest(chromePayload[slot])) + continue + } + values := decodeCurrentCastleFloat64ArraySlotForTest(t, slot, chromeString) + nonZero := 0 + firstNonZero := make([]string, 0, 12) + for i, value := range values { + if value == 0 { + continue + } + nonZero++ + if len(firstNonZero) < 12 { + firstNonZero = append(firstNonZero, fmt.Sprintf("%d:%g", i, value)) + } + } + preview := make([]float64, min(16, len(values))) + copy(preview, values[:len(preview)]) + t.Logf("array %d len=%d nonzero=%d first=%v first_nonzero=%v", slot, len(values), nonZero, preview, firstNonZero) + } + } + if os.Getenv("TWITTER_CASTLE_TOKEN_DIFF_SLOT6") != "" { + chromeSlot6, chromeOK := chromePayload[6].(string) + goSlot6, goOK := goPayload[6].(string) + if !chromeOK || !goOK { + t.Logf("slot6 chrome_type=%s go_type=%s", currentCastleJSONTypeForTest(chromePayload[6]), currentCastleJSONTypeForTest(goPayload[6])) + } else { + chromeComponents := decodeCurrentCastleSlot6ComponentsForTest(t, chromeSlot6) + goComponents := decodeCurrentCastleSlot6ComponentsForTest(t, goSlot6) + componentMismatches := make([]string, 0) + for i := range chromeComponents { + chromeJSON := currentCastleJSONSummaryForTest(t, chromeComponents[i]) + goJSON := currentCastleJSONSummaryForTest(t, goComponents[i]) + if chromeJSON != goJSON { + componentMismatches = append(componentMismatches, fmt.Sprintf( + "%d:%d/%s -> %d/%s", + i, + len(chromeComponents[i]), + currentCastleShortHashForTest(chromeJSON), + len(goComponents[i]), + currentCastleShortHashForTest(goJSON), + )) + } + } + t.Logf("slot6 component mismatch count: %d", len(componentMismatches)) + for _, mismatch := range componentMismatches { + t.Logf("slot6 component %s", mismatch) + } + if os.Getenv("TWITTER_CASTLE_TOKEN_DIFF_SLOT6_RAW") != "" { + for i := range chromeComponents { + chromeRaw := decodeCurrentCastleSlot6EncodedComponentForTest(t, i, chromeComponents[i]) + goRaw := decodeCurrentCastleSlot6EncodedComponentForTest(t, i, goComponents[i]) + if chromeRaw != goRaw { + t.Logf("slot6 raw component %d: chrome=%q go=%q", i, chromeRaw, goRaw) + } + } + } + } + } +} + +func decodeCurrentCastlePayloadFromTokenForTest(t *testing.T, token string) ([]any, int64) { + t.Helper() + wire, err := decodeCurrentCastleWrappedTokenForTest(token) + if err != nil { + t.Fatalf("decodeCurrentCastleWrappedTokenForTest() error = %v", err) + } + if len(wire) < 2 { + t.Fatalf("decoded wire too short: %d", len(wire)) + } + encodedTimestampLen := int(wire[0]) + if len(wire) <= 1+encodedTimestampLen { + t.Fatalf("decoded wire missing payload: wire_len=%d encoded_timestamp_len=%d", len(wire), encodedTimestampLen) + } + timestamp := decodeCurrentCastleTimestampForTest(t, wire[1:1+encodedTimestampLen]) + timestampMillis, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + t.Fatalf("decoded timestamp %q parse error: %v", timestamp, err) + } + payloadWithChecksum := xorCurrentCastleString(wire[1+encodedTimestampLen:], timestamp) + payloadJSON := stripCurrentCastleChecksumForTest(t, payloadWithChecksum) + var payload []any + if err = json.Unmarshal([]byte(payloadJSON), &payload); err != nil { + t.Fatalf("json.Unmarshal(decoded payload) error = %v", err) + } + return payload, timestampMillis +} + +func decodeCurrentCastleTimestampForTest(t *testing.T, encoded string) string { + t.Helper() + return string(utf16.Decode(decodeCurrentCastleUnitsForTest(t, encoded))) +} + +func decodeCurrentCastleUnitsForTest(t *testing.T, encoded string) []uint16 { + t.Helper() + raw, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + t.Fatalf("base64 Castle units decode error = %v", err) + } + if len(raw)%2 != 0 { + t.Fatalf("encoded Castle units decoded to odd byte length %d", len(raw)) + } + units := make([]uint16, 0, len(raw)/2) + for i := 0; i < len(raw); i += 2 { + n := uint32(raw[i])<<8 | uint32(raw[i+1]) + n = currentCastleRotR16ForTest(n, 5) + n = (n - 60834) & 0xffff + n = currentCastleRotR16ForTest(n, 4) + n = ((n - 385) * 13069) & 0xffff + n = (46488 ^ n) & 0xffff + n = (n + 54655) & 0xffff + units = append(units, uint16(n)) + } + return units +} + +func currentCastleRotR16ForTest(value uint32, shift uint) uint32 { + return ((value >> shift) | (value << (16 - shift))) & 0xffff +} + +func stripCurrentCastleChecksumForTest(t *testing.T, payload string) string { + t.Helper() + re := regexp.MustCompile(regexp.QuoteMeta(currentCastleChecksumLabel) + `[0-9a-f]{8}`) + loc := re.FindStringIndex(payload) + if loc == nil { + t.Fatalf("decoded payload missing checksum marker") + } + return payload[:loc[0]] + payload[loc[1]:] +} + +func currentCastleJSONSummaryForTest(t *testing.T, value any) string { + t.Helper() + data, err := json.Marshal(value) + if err != nil { + t.Fatalf("json.Marshal(summary) error = %v", err) + } + return string(data) +} + +func currentCastleJSONTypeForTest(value any) string { + switch value.(type) { + case string: + return "string" + case float64: + return "number" + case bool: + return "bool" + case []any: + return "array" + case map[string]any: + return "object" + case nil: + return "null" + default: + return fmt.Sprintf("%T", value) + } +} + +func currentCastleShortHashForTest(value string) string { + sum := sha256.Sum256([]byte(value)) + return fmt.Sprintf("%x", sum[:4]) +} + +func decodeCurrentCastleFloat64ArraySlotForTest(t *testing.T, slot int, encoded string) []float64 { + t.Helper() + raw, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + t.Fatalf("base64 slot %d decode error = %v", slot, err) + } + if len(raw)%8 != 0 { + t.Fatalf("slot %d decoded to byte length %d, not divisible by 8", slot, len(raw)) + } + for i, b := range raw { + switch slot { + case 254: + raw[i] = decodeCurrentCastleArraySlot254ByteForTest(b) + case 302: + raw[i] = decodeCurrentCastleArraySlot302ByteForTest(b) + default: + t.Fatalf("unsupported array slot %d", slot) + } + } + values := make([]float64, len(raw)/8) + for i := range values { + values[i] = math.Float64frombits(binary.BigEndian.Uint64(raw[i*8:])) + } + return values +} + +func decodeCurrentCastleArraySlot254ByteForTest(b byte) byte { + n := uint32(b) + n = (n - 195) & 0xff + n = (n * 13) & 0xff // modular inverse of 185*109 mod 256 + n ^= 32 + return byte(n) +} + +func decodeCurrentCastleArraySlot302ByteForTest(b byte) byte { + n := uint32(b) ^ 192 + n = currentCastleRotR8ForTest(n, 4) + n = (n * 23) & 0xff // modular inverse of 167 mod 256 + return byte(n) +} + +func currentCastleRotR8ForTest(value uint32, shift uint) uint32 { + return ((value >> shift) | (value << (8 - shift))) & 0xff +} + +func decodeCurrentCastleSlot6ComponentsForTest(t *testing.T, encoded string) []string { + t.Helper() + units := decodeCurrentCastleUnitsForTest(t, encoded) + index := 0 + count := readCurrentCastleLengthPrefixForTest(t, units, &index) + if count != currentCastleSlot6ComponentCount { + t.Fatalf("slot 6 component count = %d, want %d", count, currentCastleSlot6ComponentCount) + } + lengths := make([]int, count) + for i := range lengths { + lengths[i] = readCurrentCastleLengthPrefixForTest(t, units, &index) + } + components := make([]string, count) + for i, length := range lengths { + if index+length > len(units) { + t.Fatalf("slot 6 component %d length %d overflows decoded unit len %d at %d", i, length, len(units), index) + } + components[i] = string(utf16.Decode(units[index : index+length])) + index += length + } + if index != len(units) { + t.Fatalf("slot 6 decoded %d units but consumed %d", len(units), index) + } + return components +} + +func readCurrentCastleLengthPrefixForTest(t *testing.T, units []uint16, index *int) int { + t.Helper() + length := 0 + shift := 0 + for { + if *index >= len(units) { + t.Fatalf("slot 6 length prefix overflow") + } + unit := units[*index] + *index = *index + 1 + length |= int(unit¤tCastleSlot6LengthMask) << shift + if unit¤tCastleSlot6LengthContinue == 0 { + return length + } + shift += currentCastleSlot6LengthShiftBits + } +} + +func decodeCurrentCastleSlot6EncodedComponentForTest(t *testing.T, index int, encoded string) string { + t.Helper() + if index < 0 || index >= currentCastleSlot6ComponentCount { + t.Fatalf("slot 6 component index %d outside range", index) + } + 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, + } + raw, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + t.Fatalf("base64 slot 6 component %d decode error: %v", index, err) + } + inverse := map[byte]byte{} + for b := 0; b < 256; b++ { + probe, err := base64.StdEncoding.DecodeString(encoders[index](string([]byte{byte(b)}))) + if err != nil { + t.Fatalf("base64 slot 6 component %d inverse probe decode error: %v", index, err) + } + if len(probe) != 1 { + t.Fatalf("slot 6 component %d inverse probe len = %d, want 1", index, len(probe)) + } + inverse[probe[0]] = byte(b) + } + out := make([]byte, len(raw)) + for i, b := range raw { + value, ok := inverse[b] + if !ok { + t.Fatalf("slot 6 component %d byte %d has no inverse for %#x", index, i, b) + } + out[i] = value + } + return string(out) +} + +func decodeCurrentCastleWrappedTokenForTest(token string) (string, error) { + if !strings.HasPrefix(token, currentCastleTokenPrefix) { + return "", errCurrentCastleTestMissingPrefix + } + raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(token, currentCastleTokenPrefix)) + if err != nil { + return "", err + } + reader := flate.NewReader(bytes.NewReader(raw)) + defer reader.Close() + out, err := io.ReadAll(reader) + if err != nil { + return "", err + } + return string(out), nil +} + +func sameUint16s(a, b []uint16) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func sameInts(a, b []int) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +var errCurrentCastleTestMissingPrefix = errors.New("current Castle token missing prefix") diff --git a/pkg/twittermeow/castle_web_token_test_helpers_test.go b/pkg/twittermeow/castle_web_token_test_helpers_test.go new file mode 100644 index 00000000..c3086492 --- /dev/null +++ b/pkg/twittermeow/castle_web_token_test_helpers_test.go @@ -0,0 +1,234 @@ +package twittermeow + +func encodeCurrentCastleSlot0Primary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 1 + b = byte(uint16(b) * 7) + b += 52 + return currentCastleRotL8(b, 6) + }) +} + +func encodeCurrentCastleSlot0Secondary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 163) + b = currentCastleRotL8(b, 2) + b = currentCastleRotL8(b, 3) + return currentCastleRotL8(b, 7) + }) +} + +func encodeCurrentCastleSlot0Tertiary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 185) + b = currentCastleRotL8(b, 5) + b += 212 + return b + }) +} + +func encodeCurrentCastleSlot0Quaternary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 223) + b = currentCastleRotL8(b, 5) + b ^= 159 + return b + }) +} + +func encodeCurrentCastleSlot0Quinary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 2) + b += 220 + b = currentCastleRotL8(b, 2) + b = byte(uint16(b) * 31) + return b + }) +} + +func encodeCurrentCastleSlot0Senary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 249 + b += 85 + b += 192 + return b + }) +} + +func encodeCurrentCastleSlot7CanvasPrimary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 133) + b += 242 + b = byte(uint16(b) * 215) + b = byte(uint16(b) * 105) + return b + }) +} + +func encodeCurrentCastleSlot7WorkerTiming(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 83 + b ^= 38 + b = byte(uint16(b) * 105) + b = byte(uint16(b) * 211) + return b + }) +} + +func encodeCurrentCastleSlot7MediaRatio(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b ^= 207 + b += 239 + return b + }) +} + +func encodeCurrentCastleSlot7PluginState(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 135) + b ^= 37 + b += 242 + return b + }) +} + +func encodeCurrentCastleSlot7FrameRatio(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 238 + b ^= 80 + b = byte(uint16(b) * 67) + return b + }) +} + +func encodeCurrentCastleSlot7WorkerCanvas(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b = currentCastleRotL8(b, 5) + b ^= 31 + return b + }) +} + +func encodeCurrentCastleSlot7WorkerFeature(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 105 + b ^= 187 + b = byte(uint16(b) * 211) + return b + }) +} + +func encodeCurrentCastleSlot7NavigatorProbe(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 119) + b = byte(uint16(b) * 227) + b = currentCastleRotL8(b, 5) + b += 96 + return b + }) +} + +func encodeCurrentCastleSlot7ViewportRatio(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 55) + b = byte(uint16(b) * 129) + b ^= 157 + return b + }) +} + +func encodeCurrentCastleSlot7WorkerSignal(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b += 70 + b = currentCastleRotL8(b, 5) + b += 151 + b ^= 157 + return b + }) +} + +func encodeCurrentCastleSlot7PointerRatio(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 6) + b += 202 + b += 162 + b = currentCastleRotL8(b, 5) + return b + }) +} + +func encodeCurrentCastleSlot8Primary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b = byte(uint16(b) * 45) + b ^= 87 + return b + }) +} + +func encodeCurrentCastleSlot8Secondary(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 5) + b += 154 + b = byte(uint16(b) * 201) + return b + }) +} + +func encodeCurrentCastleWebGPUVendor(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 79) + b = byte(uint16(b) * 211) + b ^= 118 + return b + }) +} + +func encodeCurrentCastleWebGPULimits(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = currentCastleRotL8(b, 4) + b = currentCastleRotL8(b, 5) + b += 72 + b = byte(uint16(b) * 187) + return b + }) +} + +func encodeCurrentCastleWebGPUArchitecture(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 59) + b = currentCastleRotL8(b, 1) + b ^= 56 + return b + }) +} + +func encodeCurrentCastleSlot6TZ(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 223) + b = currentCastleRotL8(b, 5) + b ^= 159 + return b + }) +} + +func encodeCurrentCastleSlot6TP(value string) string { + return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { + b = byte(uint16(b) * 133) + b += 242 + b = byte(uint16(b) * 215) + return byte(uint16(b) * 105) + }) +} + +func invertCurrentCastleTB(value uint32) uint32 { + value -= 956 + value -= 24 + value += 42 + value += 41 + value += 478 + value -= 433 + return value * 1884841763 +} diff --git a/pkg/twittermeow/cookies/cookies_test.go b/pkg/twittermeow/cookies/cookies_test.go new file mode 100644 index 00000000..294a71c1 --- /dev/null +++ b/pkg/twittermeow/cookies/cookies_test.go @@ -0,0 +1,22 @@ +package cookies + +import ( + "net/http" + "testing" +) + +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/login_jetfuel.go b/pkg/twittermeow/login_jetfuel.go index 6023c18b..acc31f18 100644 --- a/pkg/twittermeow/login_jetfuel.go +++ b/pkg/twittermeow/login_jetfuel.go @@ -72,7 +72,7 @@ func (wls *WebLoginSession) startJetfuel(ctx context.Context) (*WebLoginResult, } 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) + return nil, fmt.Errorf("%w: jetfuel login page did not expose a username action", ErrWebLoginUnexpectedSubtask) } wls.backend = webLoginBackendJetfuel wls.jetfuel = &jetfuelLoginState{} @@ -89,7 +89,7 @@ func (wls *WebLoginSession) startJetfuel(ctx context.Context) (*WebLoginResult, 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") + 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") @@ -129,16 +129,16 @@ func (wls *WebLoginSession) submitJetfuelIdentifier(ctx context.Context, identif Challenge: parsed.verificationChallenge(), }, nil } - return nil, fmt.Errorf("%w: Jetfuel identifier response did not expose a supported next action", ErrWebLoginUnexpectedSubtask) + 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") + return nil, fmt.Errorf("x username, email, or phone is required") } if password == "" { - return nil, fmt.Errorf("X password is required") + return nil, fmt.Errorf("x password is required") } result, err := wls.submitJetfuelIdentifier(ctx, identifier) if err != nil { @@ -203,15 +203,15 @@ func (wls *WebLoginSession) submitJetfuelCombinedCredentials(ctx context.Context }, }, nil } - return nil, fmt.Errorf("%w: Jetfuel credentials response did not complete or expose a supported challenge", ErrWebLoginUnexpectedSubtask) + 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") + 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) + return nil, fmt.Errorf("%w: jetfuel password action is missing", ErrWebLoginUnexpectedSubtask) } form := url.Values{ "password": {password}, @@ -246,12 +246,12 @@ func (wls *WebLoginSession) submitJetfuelPassword(ctx context.Context, password Challenge: parsed.verificationChallenge(), }, nil } - return nil, fmt.Errorf("%w: Jetfuel password response did not complete or expose a supported challenge", ErrWebLoginUnexpectedSubtask) + 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) + return nil, fmt.Errorf("%w: jetfuel session state is missing", ErrWebLoginUnexpectedSubtask) } form := url.Values{} if wls.jetfuel.preludeDispatchID != "" { @@ -281,16 +281,16 @@ func (wls *WebLoginSession) submitJetfuelBeginTwoFactor(ctx context.Context, act Challenge: parsed.verificationChallenge(), }, nil } - return nil, fmt.Errorf("%w: Jetfuel two-factor prelude did not expose a verification challenge", ErrWebLoginUnexpectedSubtask) + 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") + 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) + return nil, fmt.Errorf("%w: jetfuel verification action is missing", ErrWebLoginUnexpectedSubtask) } form := url.Values{} for _, field := range wls.jetfuelVerificationFields() { @@ -337,7 +337,7 @@ func (wls *WebLoginSession) submitJetfuelText(ctx context.Context, text string) Challenge: parsed.verificationChallenge(), }, nil } - return nil, fmt.Errorf("%w: Jetfuel verification response did not complete login", ErrWebLoginUnexpectedSubtask) + return nil, fmt.Errorf("%w: jetfuel verification response did not complete login", ErrWebLoginUnexpectedSubtask) } func (wls *WebLoginSession) updateJetfuelState(parsed jetfuelLoginResponse) { 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..fc4b00e5 --- /dev/null +++ b/pkg/twittermeow/login_jetfuel_live_probe_test.go @@ -0,0 +1,503 @@ +//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 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 index 551007c3..f6e6e38e 100644 --- a/pkg/twittermeow/login_web.go +++ b/pkg/twittermeow/login_web.go @@ -24,8 +24,8 @@ const ( 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") + 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") ) type WebLoginStatus string @@ -112,7 +112,7 @@ func (wls *WebLoginSession) Start(ctx context.Context) (*WebLoginResult, error) 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) + return nil, fmt.Errorf("failed to load x login page: %w", err) } if err := wls.client.activateGuest(ctx); err != nil { return nil, err @@ -148,7 +148,7 @@ func (wls *WebLoginSession) SubmitIdentifier(ctx context.Context, identifier str } identifier = strings.TrimSpace(identifier) if identifier == "" { - return nil, fmt.Errorf("X username, email, or phone is required") + return nil, fmt.Errorf("x username, email, or phone is required") } st := wls.currentSubtask() if st == nil || st.SettingsList == nil { @@ -199,7 +199,7 @@ func (wls *WebLoginSession) SubmitPassword(ctx context.Context, password string) return wls.submitJetfuelPassword(ctx, password) } if password == "" { - return nil, fmt.Errorf("X password is required") + return nil, fmt.Errorf("x password is required") } st := wls.currentSubtask() if st == nil || st.EnterPassword == nil { @@ -232,7 +232,7 @@ func (wls *WebLoginSession) SubmitText(ctx context.Context, text string) (*WebLo } text = strings.TrimSpace(text) if text == "" { - return nil, fmt.Errorf("X verification code is required") + return nil, fmt.Errorf("x verification code is required") } st := wls.currentSubtask() if st == nil || st.EnterText == nil { @@ -289,7 +289,7 @@ func (wls *WebLoginSession) advanceJSInstrumentation(ctx context.Context) (*WebL func (wls *WebLoginSession) update(resp *onboardingTaskResponse) error { if resp == nil { - return fmt.Errorf("X login response was empty") + return fmt.Errorf("x login response was empty") } if resp.FlowToken == "" && !wls.client.IsLoggedIn() { return ErrWebLoginMissingFlowToken diff --git a/pkg/twittermeow/login_web_test.go b/pkg/twittermeow/login_web_test.go new file mode 100644 index 00000000..bc25d8b2 --- /dev/null +++ b/pkg/twittermeow/login_web_test.go @@ -0,0 +1,272 @@ +package twittermeow + +import ( + "encoding/base64" + "encoding/json" + "strings" + "testing" + + "go.mau.fi/mautrix-twitter/pkg/twittermeow/data/endpoints" +) + +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 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 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 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 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) + } +} From dad8f13c8469261e8d6d48270410bc005f83179a Mon Sep 17 00:00:00 2001 From: highesttt Date: Fri, 26 Jun 2026 22:06:55 -0500 Subject: [PATCH 04/20] style: line break at the end of gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 74830d9b..4bdd1e5b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ logs/ /mautrix-twitter.exe /data actions*.txt -grind*.md \ No newline at end of file +grind*.md From e9fc84c46e7b6b544333129a9ee5cca23b031b87 Mon Sep 17 00:00:00 2001 From: highesttt Date: Mon, 29 Jun 2026 08:45:25 -0500 Subject: [PATCH 05/20] feat: 2fa via security key --- pkg/connector/login.go | 93 ++++++ pkg/connector/login_test.go | 55 ++++ pkg/twittermeow/data/endpoints/endpoints.go | 17 +- pkg/twittermeow/login_jetfuel.go | 268 ++++++++++++++- .../login_jetfuel_live_probe_test.go | 65 ++++ pkg/twittermeow/login_web.go | 35 +- pkg/twittermeow/login_web_test.go | 121 +++++++ pkg/twittermeow/webauthn.go | 308 ++++++++++++++++++ pkg/twittermeow/webauthn_other.go | 12 + pkg/twittermeow/webauthn_windows.go | 214 ++++++++++++ 10 files changed, 1175 insertions(+), 13 deletions(-) create mode 100644 pkg/connector/login_test.go create mode 100644 pkg/twittermeow/webauthn.go create mode 100644 pkg/twittermeow/webauthn_other.go create mode 100644 pkg/twittermeow/webauthn_windows.go diff --git a/pkg/connector/login.go b/pkg/connector/login.go index a2f9e30a..bf53f025 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -53,6 +53,7 @@ type TwitterLogin struct { webLogin *twittermeow.WebLoginSession webLoginPassword string webLoginChallenge *twittermeow.WebLoginChallenge + webLoginMethods []twittermeow.WebLoginAuthMethod profile twittermeow.CurrentUserProfile } @@ -61,12 +62,14 @@ var ( 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) @@ -239,6 +242,37 @@ func makeVerificationStep(challenge *twittermeow.WebLoginChallenge, errorLine st } } +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.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" @@ -547,6 +581,9 @@ func (t *TwitterLogin) SubmitUserInput(ctx context.Context, input map[string]str 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) } @@ -567,6 +604,7 @@ func (t *TwitterLogin) submitCredentialsInput(ctx context.Context, input map[str t.webLogin = twittermeow.NewWebLoginSession(client) t.webLoginPassword = password t.webLoginChallenge = nil + t.webLoginMethods = nil result, err := t.webLogin.Start(ctx) if err != nil { @@ -583,10 +621,50 @@ func (t *TwitterLogin) submitCredentialsInput(ctx context.Context, input map[str 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 makeAuthMethodStep(t.webLoginMethods, "Choose a verification method."), nil + } + result, err := t.webLogin.SubmitAuthMethod(ctx, methodID) + if err != nil { + if errors.Is(err, twittermeow.ErrWebLoginUnsupportedAuthMethod) || + errors.Is(err, twittermeow.ErrWebLoginMissingAuthMethodState) { + return makeAuthMethodStep(t.webLoginMethods, webLoginErrorInstructions(err)), nil + } + return makeCredentialsStep(webLoginErrorInstructions(err)), nil + } + 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 } code := strings.TrimSpace(input[loginFieldVerificationCode]) @@ -613,8 +691,16 @@ func (t *TwitterLogin) handleWebLoginResult(ctx context.Context, result *twitter 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 != "" { @@ -653,6 +739,7 @@ func (t *TwitterLogin) completeWebLogin(ctx context.Context) (*bridgev2.LoginSte 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 } @@ -668,6 +755,12 @@ func webLoginErrorInstructions(err error) string { 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." diff --git a/pkg/connector/login_test.go b/pkg/connector/login_test.go new file mode 100644 index 00000000..66b8ea22 --- /dev/null +++ b/pkg/connector/login_test.go @@ -0,0 +1,55 @@ +package connector + +import ( + "strings" + "testing" + + "maunium.net/go/mautrix/bridgev2" + + "go.mau.fi/mautrix-twitter/pkg/twittermeow" +) + +func TestMakeAuthMethodStepUsesNativeSelect(t *testing.T) { + methods := []twittermeow.WebLoginAuthMethod{ + {ID: "Totp", Name: "Authenticator App", Supported: true}, + {ID: "BackupCode", Name: "Backup Code", Supported: true}, + {ID: "U2fSecurityKey", Name: "Security Key PC", Supported: true}, + } + 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,Security Key PC" { + 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 TestFindWebLoginAuthMethodMatchesNameOrID(t *testing.T) { + methods := []twittermeow.WebLoginAuthMethod{ + {ID: "Totp", Name: "Authenticator App", Supported: true}, + {ID: "U2fSecurityKey", Name: "Security Key PC", 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, "u2f_security_key"); !ok || method.ID != "U2fSecurityKey" { + t.Fatalf("find by normalized ID = %#v %t, want U2fSecurityKey", method, ok) + } +} diff --git a/pkg/twittermeow/data/endpoints/endpoints.go b/pkg/twittermeow/data/endpoints/endpoints.go index 8fb1d181..2ae1b32f 100644 --- a/pkg/twittermeow/data/endpoints/endpoints.go +++ b/pkg/twittermeow/data/endpoints/endpoints.go @@ -64,14 +64,15 @@ const ( 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" + 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/login_jetfuel.go b/pkg/twittermeow/login_jetfuel.go index acc31f18..4b945448 100644 --- a/pkg/twittermeow/login_jetfuel.go +++ b/pkg/twittermeow/login_jetfuel.go @@ -32,12 +32,13 @@ const ( 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": "/onboarding/web/actions/finish_two_factor_auth", + "finish_two_factor_auth": endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH, "two_factor_code": "/onboarding/web/actions/two_factor_code", } ) @@ -49,14 +50,18 @@ type jetfuelLoginState struct { 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) { @@ -183,6 +188,9 @@ func (wls *WebLoginSession) submitJetfuelCombinedCredentials(ctx context.Context 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() @@ -234,6 +242,9 @@ func (wls *WebLoginSession) submitJetfuelPassword(ctx context.Context, password 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) } @@ -272,6 +283,9 @@ func (wls *WebLoginSession) submitJetfuelBeginTwoFactor(ctx context.Context, act 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() @@ -314,6 +328,9 @@ func (wls *WebLoginSession) submitJetfuelText(ctx context.Context, text string) 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{ @@ -350,13 +367,22 @@ func (wls *WebLoginSession) updateJetfuelState(parsed jetfuelLoginResponse) { 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 { @@ -366,6 +392,137 @@ func (wls *WebLoginSession) jetfuelVerificationFields() []string { 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 + } + return &WebLoginResult{ + Status: WebLoginStatusNeedsAuthMethod, + CurrentSubtaskID: "JetfuelTwoFactorMethod", + Challenge: &WebLoginChallenge{ + SubtaskID: "JetfuelTwoFactorMethod", + Hint: "Verification method", + Description: "Choose how to verify this X login.", + IsTwoFactor: true, + }, + AuthMethods: methods, + } +} + +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)) + } + action := wls.jetfuel.twoFactorAction + if action == "" { + action = endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH + } + 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 method.Kind == WebLoginAuthMethodKindSecurityKey { + return wls.submitJetfuelSecurityKeyChallenge(ctx, method, parsed) + } + 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.verificationChallengeForMethod(method), + }, nil + } + return nil, fmt.Errorf("%w: jetfuel auth method response did not expose a verification challenge", ErrWebLoginUnexpectedSubtask) +} + +func (wls *WebLoginSession) submitJetfuelSecurityKeyChallenge(ctx context.Context, method WebLoginAuthMethod, parsed jetfuelLoginResponse) (*WebLoginResult, error) { + challenge, ok := parsed.webAuthnChallenge() + if !ok { + return nil, missingWebAuthnChallengeError(method.Name) + } + challengeResponse, err := createWebAuthnChallengeResponse(ctx, challenge) + if err != nil { + return nil, err + } + action := parsed.verificationAction() + if action == "" { + action = endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH + } + form := webAuthnChallengeSubmitForm(challengeResponse, parsed.verificationCodeFields(), wls.jetfuel.sessionToken, wls.jetfuel.preludeDispatchID) + body, err := wls.client.jetfuelPostForm(ctx, action, form) + if err != nil { + return nil, err + } + next := parseJetfuelLoginResponse(body) + if err := next.loginError(); err != nil { + return nil, err + } + wls.updateJetfuelState(next) + if wls.client.IsLoggedIn() || next.isComplete() { + return &WebLoginResult{Status: WebLoginStatusComplete}, nil + } + if result := wls.jetfuelAuthMethodChoiceResult(next); result != nil { + return result, nil + } + if action := next.verificationAction(); action != "" { + wls.jetfuel.verificationAction = action + wls.jetfuel.verificationFields = next.verificationCodeFields() + return &WebLoginResult{ + Status: WebLoginStatusNeedsText, + CurrentSubtaskID: "JetfuelVerification", + Challenge: next.verificationChallengeForMethod(method), + }, nil + } + return nil, fmt.Errorf("%w: jetfuel security-key response did not complete login", ErrWebLoginUnexpectedSubtask) +} + +func (jls *jetfuelLoginState) findAuthMethod(methodID string) (WebLoginAuthMethod, bool) { + methodID = normalizeJetfuelMethodID(methodID) + for _, method := range jls.twoFactorMethods { + if normalizeJetfuelMethodID(method.ID) == methodID || normalizeJetfuelMethodID(method.Name) == methodID { + return method, true + } + } + return WebLoginAuthMethod{}, false +} + +func (jls *jetfuelLoginState) authMethodForm(method WebLoginAuthMethod) url.Values { + form := url.Values{ + "two_factor_auth_method_type": {method.ID}, + "_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) } @@ -609,7 +766,7 @@ func parseJetfuelLoginResponse(body []byte) jetfuelLoginResponse { fields = append(fields, str) } } - return jetfuelLoginResponse{strings: strs, paths: paths, fields: fields} + return jetfuelLoginResponse{strings: strs, paths: paths, fields: fields, raw: body} } func canonicalJetfuelActionPath(value string) string { @@ -791,10 +948,104 @@ func (jfr jetfuelLoginResponse) uuidValue(field string) string { 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() + return strings.Contains(text, "select a method") || + strings.Contains(text, "choose the method") || + strings.Contains(text, "two_factor_method") || + strings.Contains(text, "two factor method") +} + +func (jfr jetfuelLoginResponse) authMethods() []WebLoginAuthMethod { + if !jfr.isAuthMethodChoice() { + return nil + } + methods := make([]WebLoginAuthMethod, 0, 3) + for _, str := range jfr.strings { + method, ok := classifyJetfuelAuthMethod(str) + if !ok || containsJetfuelAuthMethod(methods, method.ID) { + continue + } + method.Index = len(methods) + 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 "u2fsecuritykey", "securitykey", "securitykeypc", "passkey": + return WebLoginAuthMethod{ + ID: "U2fSecurityKey", + Name: "Security Key PC", + Description: "Use a security key or passkey.", + Kind: WebLoginAuthMethodKindSecurityKey, + Supported: true, + }, true + default: + return WebLoginAuthMethod{}, false + } +} + +func containsJetfuelAuthMethod(methods []WebLoginAuthMethod, id string) bool { + normalized := normalizeJetfuelMethodID(id) + return slices.ContainsFunc(methods, func(method WebLoginAuthMethod) bool { + return normalizeJetfuelMethodID(method.ID) == normalized + }) +} + +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() return &WebLoginChallenge{ @@ -807,6 +1058,19 @@ func (jfr jetfuelLoginResponse) verificationChallenge() *WebLoginChallenge { } } +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." + } + challenge.IsTwoFactor = true + return challenge +} + func jetfuelChallengeDescription(text string) string { switch { case strings.Contains(text, "backup code"): diff --git a/pkg/twittermeow/login_jetfuel_live_probe_test.go b/pkg/twittermeow/login_jetfuel_live_probe_test.go index fc4b00e5..ea9ba9a2 100644 --- a/pkg/twittermeow/login_jetfuel_live_probe_test.go +++ b/pkg/twittermeow/login_jetfuel_live_probe_test.go @@ -276,6 +276,71 @@ func TestLiveJetfuelPasswordResponseProbe(t *testing.T) { } } +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 + "/" diff --git a/pkg/twittermeow/login_web.go b/pkg/twittermeow/login_web.go index f6e6e38e..0006162c 100644 --- a/pkg/twittermeow/login_web.go +++ b/pkg/twittermeow/login_web.go @@ -23,9 +23,11 @@ const ( ) 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") + 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 @@ -34,10 +36,20 @@ 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" + WebLoginAuthMethodKindSecurityKey WebLoginAuthMethodKind = "security_key" + WebLoginAuthMethodKindUnknown WebLoginAuthMethodKind = "unknown" +) + type WebLoginError struct { Code int Message string @@ -78,9 +90,19 @@ type WebLoginChallenge struct { IsTwoFactor bool } +type WebLoginAuthMethod struct { + ID string + Name string + Description string + Kind WebLoginAuthMethodKind + Supported bool + Index int +} + type WebLoginResult struct { Status WebLoginStatus Challenge *WebLoginChallenge + AuthMethods []WebLoginAuthMethod CurrentSubtaskID string } @@ -226,6 +248,13 @@ func (wls *WebLoginSession) SubmitPassword(ctx context.Context, password string) 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) diff --git a/pkg/twittermeow/login_web_test.go b/pkg/twittermeow/login_web_test.go index bc25d8b2..eca4a3cb 100644 --- a/pkg/twittermeow/login_web_test.go +++ b/pkg/twittermeow/login_web_test.go @@ -188,6 +188,127 @@ func TestJetfuelLoginResponseDoesNotTreatChallengeModesAsFields(t *testing.T) { } } +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\x00BackupCode\x00U2fSecurityKey\x00user_id\x001127993589949243392\x00session_token\x0012345678-1234-1234-1234-123456789abc\x00begin_two_factor_auth", + )) + methods := parsed.authMethods() + if len(methods) != 3 { + t.Fatalf("authMethods() length = %d, want 3: %#v", len(methods), methods) + } + wantIDs := []string{"Totp", "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 { + t.Fatalf("code-based methods should be supported: %#v", methods) + } + if !methods[2].Supported || methods[2].Kind != WebLoginAuthMethodKindSecurityKey { + t.Fatalf("security key method = %#v, want supported security-key method", methods[2]) + } + 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 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 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 TestJetfuelLoginResponseFindsWebAuthnChallenge(t *testing.T) { + body := []byte(`security_key` + "\x00" + `{"action":"verification","challenge":{"publicKeyCredentialRequestOptions":{"rpId":"x.com","challenge":"abc123","allowCredentials":[{"type":"public-key","id":"AQID"}],"extensions":{"appid":"https://twitter.com"}}},"timeout":60000}` + "\x00challenge_response\x00finish_two_factor_auth") + parsed := parseJetfuelLoginResponse(body) + + challenge, ok := parsed.webAuthnChallenge() + if !ok { + t.Fatal("webAuthnChallenge() returned false") + } + if challenge.RequestOptions.RPID != "x.com" { + t.Fatalf("rpId = %q", challenge.RequestOptions.RPID) + } + if challenge.RequestOptions.Challenge != "abc123" { + t.Fatalf("challenge = %q", challenge.RequestOptions.Challenge) + } + if got := challenge.RequestOptions.AllowCredentials[0].ID; got != "AQID" { + t.Fatalf("allowCredentials[0].id = %q", got) + } + if got := challenge.RequestOptions.Extensions.AppID; got != "https://twitter.com" { + t.Fatalf("extensions.appid = %q", got) + } +} + +func TestWebAuthnChallengeResponseShape(t *testing.T) { + response, err := marshalWebAuthnChallengeResponse(&webAuthnAssertion{ + CredentialID: []byte{1, 2, 3}, + ClientDataJSON: []byte(`{"type":"webauthn.get"}`), + AuthenticatorData: []byte{4, 5, 6}, + Signature: []byte{7, 8, 9}, + }) + if err != nil { + t.Fatalf("marshalWebAuthnChallengeResponse() error = %v", err) + } + + var payload struct { + ID string `json:"id"` + Type string `json:"type"` + Response struct { + ClientDataJSON string `json:"clientDataJSON"` + AuthenticatorData string `json:"authenticatorData"` + Signature string `json:"signature"` + UserHandle string `json:"userHandle"` + } `json:"response"` + ClientExtensionResults struct { + AppID bool `json:"appid"` + } `json:"clientExtensionResults"` + } + if err := json.Unmarshal([]byte(response), &payload); err != nil { + t.Fatalf("Unmarshal(response) error = %v", err) + } + if payload.ID != "AQID" || payload.Type != "public-key" { + t.Fatalf("payload identity = %#v", payload) + } + if payload.Response.AuthenticatorData != "BAUG" || payload.Response.Signature != "BwgJ" { + t.Fatalf("payload response = %#v", payload.Response) + } + if payload.Response.UserHandle != "" || !payload.ClientExtensionResults.AppID { + t.Fatalf("payload extension/user handle = %#v", payload) + } +} + func TestJetfuelLoginResponseClassifiesTemporaryLimit(t *testing.T) { parsed := parseJetfuelLoginResponse([]byte("We've temporarily limited your login. Please try again later.")) err := parsed.loginError() diff --git a/pkg/twittermeow/webauthn.go b/pkg/twittermeow/webauthn.go new file mode 100644 index 00000000..0be06d9d --- /dev/null +++ b/pkg/twittermeow/webauthn.go @@ -0,0 +1,308 @@ +package twittermeow + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" +) + +const webAuthnOrigin = "https://x.com" + +type webAuthnChallenge struct { + RequestOptions webAuthnRequestOptions +} + +type webAuthnRequestOptions struct { + RPID string `json:"rpId"` + Challenge string `json:"challenge"` + AllowCredentials []webAuthnCredentialHint `json:"allowCredentials"` + Extensions webAuthnExtensions `json:"extensions"` +} + +type webAuthnCredentialHint struct { + Type string `json:"type"` + ID string `json:"id"` + Transports []string `json:"transports"` +} + +type webAuthnExtensions struct { + AppID string `json:"appid"` +} + +type webAuthnAssertion struct { + CredentialID []byte + ClientDataJSON []byte + AuthenticatorData []byte + Signature []byte +} + +type webAuthnClientData struct { + Type string `json:"type"` + Challenge string `json:"challenge"` + Origin string `json:"origin"` + CrossOrigin bool `json:"crossOrigin"` +} + +func (challenge webAuthnChallenge) rpID() string { + rpID := strings.TrimSpace(challenge.RequestOptions.RPID) + if rpID == "" { + return "x.com" + } + return rpID +} + +func (challenge webAuthnChallenge) clientDataJSON() ([]byte, error) { + challengeValue := strings.TrimSpace(challenge.RequestOptions.Challenge) + if challengeValue == "" { + return nil, errors.New("x security-key challenge is missing") + } + return json.Marshal(webAuthnClientData{ + Type: "webauthn.get", + Challenge: challengeValue, + Origin: webAuthnOrigin, + CrossOrigin: false, + }) +} + +func createWebAuthnChallengeResponse(ctx context.Context, challenge webAuthnChallenge) (string, error) { + clientDataJSON, err := challenge.clientDataJSON() + if err != nil { + return "", err + } + assertion, err := platformWebAuthnGetAssertion(ctx, challenge, clientDataJSON) + if err != nil { + return "", err + } + return marshalWebAuthnChallengeResponse(assertion) +} + +func marshalWebAuthnChallengeResponse(assertion *webAuthnAssertion) (string, error) { + if assertion == nil { + return "", errors.New("x security-key assertion was empty") + } + payload := struct { + ID string `json:"id"` + Type string `json:"type"` + Response struct { + ClientDataJSON string `json:"clientDataJSON"` + AuthenticatorData string `json:"authenticatorData"` + Signature string `json:"signature"` + UserHandle string `json:"userHandle"` + } `json:"response"` + ClientExtensionResults struct { + AppID bool `json:"appid"` + } `json:"clientExtensionResults"` + }{ + ID: webAuthnBase64(assertion.CredentialID), + Type: "public-key", + } + payload.Response.ClientDataJSON = webAuthnBase64(assertion.ClientDataJSON) + payload.Response.AuthenticatorData = webAuthnBase64(assertion.AuthenticatorData) + payload.Response.Signature = webAuthnBase64(assertion.Signature) + payload.Response.UserHandle = "" + payload.ClientExtensionResults.AppID = true + data, err := json.Marshal(payload) + if err != nil { + return "", err + } + return string(data), nil +} + +func webAuthnBase64(data []byte) string { + return base64.RawURLEncoding.EncodeToString(data) +} + +func webAuthnDecodeBase64(value string) ([]byte, error) { + value = strings.TrimSpace(value) + if value == "" { + return nil, nil + } + if data, err := base64.RawURLEncoding.DecodeString(value); err == nil { + return data, nil + } + return base64.URLEncoding.DecodeString(value) +} + +func (jfr jetfuelLoginResponse) webAuthnChallenge() (webAuthnChallenge, bool) { + for _, str := range jfr.strings { + if challenge, ok := findWebAuthnChallengeInText(str); ok { + return challenge, true + } + } + if len(jfr.raw) > 0 { + return findWebAuthnChallengeInText(string(jfr.raw)) + } + return webAuthnChallenge{}, false +} + +func findWebAuthnChallengeInText(text string) (webAuthnChallenge, bool) { + if !strings.Contains(text, "publicKeyCredentialRequestOptions") && !strings.Contains(text, "rpId") { + return webAuthnChallenge{}, false + } + for _, candidate := range extractJSONObjectsContaining(text, "publicKeyCredentialRequestOptions") { + if challenge, ok := parseWebAuthnChallengeCandidate(candidate); ok { + return challenge, true + } + } + for _, candidate := range extractJSONObjectsContaining(text, "rpId") { + if challenge, ok := parseWebAuthnChallengeCandidate(candidate); ok { + return challenge, true + } + } + return webAuthnChallenge{}, false +} + +func parseWebAuthnChallengeCandidate(candidate string) (webAuthnChallenge, bool) { + for _, variant := range webAuthnJSONVariants(candidate) { + if challenge, ok := parseWebAuthnChallengeJSON([]byte(variant)); ok { + return challenge, true + } + } + return webAuthnChallenge{}, false +} + +func webAuthnJSONVariants(candidate string) []string { + candidate = strings.TrimSpace(candidate) + var out []string + add := func(value string) { + value = strings.TrimSpace(value) + if value == "" { + return + } + for _, existing := range out { + if existing == value { + return + } + } + out = append(out, value) + } + add(candidate) + if unescaped, err := url.QueryUnescape(candidate); err == nil { + add(unescaped) + } + if strings.Contains(candidate, `\"`) { + add(strings.ReplaceAll(candidate, `\"`, `"`)) + } + var quoted string + if err := json.Unmarshal([]byte(candidate), "ed); err == nil { + add(quoted) + } + return out +} + +func parseWebAuthnChallengeJSON(data []byte) (webAuthnChallenge, bool) { + var envelope struct { + Challenge json.RawMessage `json:"challenge"` + PublicKeyCredentialRequestOptions *webAuthnRequestOptions `json:"publicKeyCredentialRequestOptions"` + RPID string `json:"rpId"` + } + if err := json.Unmarshal(data, &envelope); err != nil { + return webAuthnChallenge{}, false + } + if envelope.PublicKeyCredentialRequestOptions != nil && envelope.PublicKeyCredentialRequestOptions.Challenge != "" { + return webAuthnChallenge{RequestOptions: *envelope.PublicKeyCredentialRequestOptions}, true + } + if len(envelope.Challenge) > 0 { + challengeData := []byte(envelope.Challenge) + var challengeString string + if err := json.Unmarshal(envelope.Challenge, &challengeString); err == nil { + challengeData = []byte(challengeString) + } + if challenge, ok := parseWebAuthnChallengeCandidate(string(challengeData)); ok { + return challenge, true + } + } + var direct webAuthnRequestOptions + if err := json.Unmarshal(data, &direct); err == nil && direct.Challenge != "" { + return webAuthnChallenge{RequestOptions: direct}, true + } + return webAuthnChallenge{}, false +} + +func extractJSONObjectsContaining(text, needle string) []string { + var out []string + for searchStart := 0; ; { + idx := strings.Index(text[searchStart:], needle) + if idx < 0 { + return out + } + idx += searchStart + for start := idx; start >= 0; start-- { + if text[start] != '{' { + continue + } + end, ok := balancedJSONEnd(text, start) + if !ok || end <= idx { + continue + } + out = append(out, text[start:end]) + break + } + searchStart = idx + len(needle) + } +} + +func balancedJSONEnd(text string, start int) (int, bool) { + depth := 0 + inString := false + escaped := false + for i := start; i < len(text); i++ { + ch := text[i] + if inString { + if escaped { + escaped = false + continue + } + switch ch { + case '\\': + escaped = true + case '"': + inString = false + } + continue + } + switch ch { + case '"': + inString = true + case '{': + depth++ + case '}': + depth-- + if depth == 0 { + return i + 1, true + } + if depth < 0 { + return 0, false + } + } + } + return 0, false +} + +func webAuthnChallengeSubmitForm(challengeResponse string, fields []string, sessionToken, preludeDispatchID string) url.Values { + form := url.Values{} + for _, field := range fields { + lower := strings.ToLower(field) + if strings.Contains(lower, "challenge") || strings.Contains(lower, "response") || strings.Contains(lower, "webauthn") { + form.Set(field, challengeResponse) + } + } + if form.Get("challenge_response") == "" { + form.Set("challenge_response", challengeResponse) + } + if sessionToken != "" { + form.Set("session_token", sessionToken) + } + if preludeDispatchID != "" { + form.Set("prelude_dispatch_id", preludeDispatchID) + } + return form +} + +func missingWebAuthnChallengeError(methodName string) error { + return fmt.Errorf("%w: %s did not expose a security-key challenge", ErrWebLoginUnexpectedSubtask, methodName) +} diff --git a/pkg/twittermeow/webauthn_other.go b/pkg/twittermeow/webauthn_other.go new file mode 100644 index 00000000..f8f616f3 --- /dev/null +++ b/pkg/twittermeow/webauthn_other.go @@ -0,0 +1,12 @@ +//go:build !windows + +package twittermeow + +import ( + "context" + "fmt" +) + +func platformWebAuthnGetAssertion(context.Context, webAuthnChallenge, []byte) (*webAuthnAssertion, error) { + return nil, fmt.Errorf("native security-key/passkey login requires Windows WebAuthn support") +} diff --git a/pkg/twittermeow/webauthn_windows.go b/pkg/twittermeow/webauthn_windows.go new file mode 100644 index 00000000..89386659 --- /dev/null +++ b/pkg/twittermeow/webauthn_windows.go @@ -0,0 +1,214 @@ +//go:build windows + +package twittermeow + +import ( + "context" + "fmt" + "runtime" + "strings" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + windowsWebAuthnClientDataVersion = 1 + windowsWebAuthnCredentialVersion = 1 + windowsWebAuthnGetAssertionOptionsVersion = 2 + windowsWebAuthnUserVerificationDiscouraged = 3 + windowsWebAuthnDefaultAssertionTimeoutMS = 60000 + windowsWebAuthnCredentialTypePublicKey = "public-key" + windowsWebAuthnHashAlgorithmSHA256 = "SHA-256" + windowsWebAuthnCredentialAttachmentAny uint32 = 0 +) + +var ( + windowsWebAuthnDLL = windows.NewLazySystemDLL("webauthn.dll") + windowsWebAuthnGetAssertionProc = windowsWebAuthnDLL.NewProc("WebAuthNAuthenticatorGetAssertion") + windowsWebAuthnFreeAssertionProc = windowsWebAuthnDLL.NewProc("WebAuthNFreeAssertion") +) + +type windowsWebAuthnClientData struct { + Version uint32 + ClientDataJSONN uint32 + ClientDataJSON *byte + HashAlgID *uint16 +} + +type windowsWebAuthnCredential struct { + Version uint32 + IDN uint32 + ID *byte + CredentialType *uint16 +} + +type windowsWebAuthnCredentials struct { + CredentialsN uint32 + Credentials *windowsWebAuthnCredential +} + +type windowsWebAuthnExtension struct { + ExtensionIdentifier *uint16 + ExtensionN uint32 + Extension unsafe.Pointer +} + +type windowsWebAuthnExtensions struct { + ExtensionsN uint32 + Extensions *windowsWebAuthnExtension +} + +type windowsWebAuthnGetAssertionOptionsV2 struct { + Version uint32 + TimeoutMilliseconds uint32 + CredentialList windowsWebAuthnCredentials + Extensions windowsWebAuthnExtensions + AuthenticatorAttachment uint32 + UserVerificationRequirement uint32 + Flags uint32 + U2fAppID *uint16 + U2fAppIDUsed *int32 +} + +type windowsWebAuthnAssertion struct { + Version uint32 + AuthenticatorDataN uint32 + AuthenticatorData *byte + SignatureN uint32 + Signature *byte + Credential windowsWebAuthnCredential + UserIDN uint32 + UserID *byte + Extensions windowsWebAuthnExtensions + CredLargeBlobN uint32 + CredLargeBlob *byte + CredLargeBlobStatus uint32 + HmacSecret unsafe.Pointer + UsedTransport uint32 + UnsignedExtensionOutputsN uint32 + UnsignedExtensionOutputs *byte + ClientDataJSONN uint32 + ClientDataJSON *byte + AuthenticationResponseJSONN uint32 + AuthenticationResponseJSON *byte +} + +func platformWebAuthnGetAssertion(_ context.Context, challenge webAuthnChallenge, clientDataJSON []byte) (*webAuthnAssertion, error) { + rpID, err := windows.UTF16PtrFromString(challenge.rpID()) + if err != nil { + return nil, err + } + hashAlg, err := windows.UTF16PtrFromString(windowsWebAuthnHashAlgorithmSHA256) + if err != nil { + return nil, err + } + credentialType, err := windows.UTF16PtrFromString(windowsWebAuthnCredentialTypePublicKey) + if err != nil { + return nil, err + } + + credentialIDs, credentials, err := windowsWebAuthnCredentialsFromChallenge(challenge, credentialType) + if err != nil { + return nil, err + } + var credentialPtr *windowsWebAuthnCredential + if len(credentials) > 0 { + credentialPtr = &credentials[0] + } + + clientData := windowsWebAuthnClientData{ + Version: windowsWebAuthnClientDataVersion, + ClientDataJSONN: uint32(len(clientDataJSON)), + ClientDataJSON: byteSlicePtr(clientDataJSON), + HashAlgID: hashAlg, + } + opts := windowsWebAuthnGetAssertionOptionsV2{ + Version: windowsWebAuthnGetAssertionOptionsVersion, + TimeoutMilliseconds: windowsWebAuthnDefaultAssertionTimeoutMS, + CredentialList: windowsWebAuthnCredentials{CredentialsN: uint32(len(credentials)), Credentials: credentialPtr}, + AuthenticatorAttachment: windowsWebAuthnCredentialAttachmentAny, + UserVerificationRequirement: windowsWebAuthnUserVerificationDiscouraged, + } + var u2fAppIDUsed int32 + if appID := strings.TrimSpace(challenge.RequestOptions.Extensions.AppID); appID != "" { + u2fAppID, err := windows.UTF16PtrFromString(appID) + if err != nil { + return nil, err + } + opts.U2fAppID = u2fAppID + opts.U2fAppIDUsed = &u2fAppIDUsed + } + + var assertionPtr *windowsWebAuthnAssertion + result, _, _ := windowsWebAuthnGetAssertionProc.Call( + 0, + uintptr(unsafe.Pointer(rpID)), + uintptr(unsafe.Pointer(&clientData)), + uintptr(unsafe.Pointer(&opts)), + uintptr(unsafe.Pointer(&assertionPtr)), + ) + runtime.KeepAlive(clientDataJSON) + runtime.KeepAlive(credentialIDs) + runtime.KeepAlive(credentials) + runtime.KeepAlive(credentialType) + runtime.KeepAlive(opts) + if result != 0 { + return nil, fmt.Errorf("windows webauthn assertion failed: HRESULT 0x%08x", uint32(result)) + } + if assertionPtr == nil { + return nil, fmt.Errorf("windows webauthn assertion was empty") + } + defer windowsWebAuthnFreeAssertionProc.Call(uintptr(unsafe.Pointer(assertionPtr))) + + assertion := *assertionPtr + assertionClientData := copyWindowsBytes(assertion.ClientDataJSON, assertion.ClientDataJSONN) + if len(assertionClientData) == 0 { + assertionClientData = append([]byte(nil), clientDataJSON...) + } + return &webAuthnAssertion{ + CredentialID: copyWindowsBytes(assertion.Credential.ID, assertion.Credential.IDN), + ClientDataJSON: assertionClientData, + AuthenticatorData: copyWindowsBytes(assertion.AuthenticatorData, assertion.AuthenticatorDataN), + Signature: copyWindowsBytes(assertion.Signature, assertion.SignatureN), + }, nil +} + +func windowsWebAuthnCredentialsFromChallenge(challenge webAuthnChallenge, credentialType *uint16) ([][]byte, []windowsWebAuthnCredential, error) { + ids := make([][]byte, 0, len(challenge.RequestOptions.AllowCredentials)) + credentials := make([]windowsWebAuthnCredential, 0, len(challenge.RequestOptions.AllowCredentials)) + for _, allowed := range challenge.RequestOptions.AllowCredentials { + if allowed.Type != "" && allowed.Type != windowsWebAuthnCredentialTypePublicKey { + continue + } + id, err := webAuthnDecodeBase64(allowed.ID) + if err != nil { + return nil, nil, fmt.Errorf("decode X security-key credential ID: %w", err) + } + if len(id) == 0 { + continue + } + ids = append(ids, id) + credentials = append(credentials, windowsWebAuthnCredential{ + Version: windowsWebAuthnCredentialVersion, + IDN: uint32(len(id)), + ID: byteSlicePtr(id), + CredentialType: credentialType, + }) + } + return ids, credentials, nil +} + +func byteSlicePtr(data []byte) *byte { + if len(data) == 0 { + return nil + } + return &data[0] +} + +func copyWindowsBytes(ptr *byte, size uint32) []byte { + if ptr == nil || size == 0 { + return nil + } + return append([]byte(nil), unsafe.Slice(ptr, int(size))...) +} From 3724894ef05bc74119066da2386b60460a7c6560 Mon Sep 17 00:00:00 2001 From: highesttt Date: Mon, 29 Jun 2026 10:35:06 -0500 Subject: [PATCH 06/20] fix: ordering for auth 2fa + backup code --- pkg/twittermeow/login_jetfuel.go | 6 +-- pkg/twittermeow/login_web_test.go | 61 +++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/pkg/twittermeow/login_jetfuel.go b/pkg/twittermeow/login_jetfuel.go index 4b945448..9971205a 100644 --- a/pkg/twittermeow/login_jetfuel.go +++ b/pkg/twittermeow/login_jetfuel.go @@ -443,9 +443,6 @@ func (wls *WebLoginSession) submitJetfuelAuthMethod(ctx context.Context, methodI if method.Kind == WebLoginAuthMethodKindSecurityKey { return wls.submitJetfuelSecurityKeyChallenge(ctx, method, parsed) } - if result := wls.jetfuelAuthMethodChoiceResult(parsed); result != nil { - return result, nil - } if action := parsed.verificationAction(); action != "" { wls.jetfuel.verificationAction = action wls.jetfuel.verificationFields = parsed.verificationCodeFields() @@ -455,6 +452,9 @@ func (wls *WebLoginSession) submitJetfuelAuthMethod(ctx context.Context, methodI Challenge: parsed.verificationChallengeForMethod(method), }, nil } + if result := wls.jetfuelAuthMethodChoiceResult(parsed); result != nil { + return result, nil + } return nil, fmt.Errorf("%w: jetfuel auth method response did not expose a verification challenge", ErrWebLoginUnexpectedSubtask) } diff --git a/pkg/twittermeow/login_web_test.go b/pkg/twittermeow/login_web_test.go index eca4a3cb..2679aa74 100644 --- a/pkg/twittermeow/login_web_test.go +++ b/pkg/twittermeow/login_web_test.go @@ -1,14 +1,26 @@ package twittermeow import ( + "context" "encoding/base64" "encoding/json" + "io" + "net/http" "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", @@ -249,6 +261,55 @@ func TestJetfuelAuthMethodFormShape(t *testing.T) { } } +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: WebLoginAuthMethodKindSecurityKey, Supported: true, 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 TestJetfuelLoginResponseFindsWebAuthnChallenge(t *testing.T) { body := []byte(`security_key` + "\x00" + `{"action":"verification","challenge":{"publicKeyCredentialRequestOptions":{"rpId":"x.com","challenge":"abc123","allowCredentials":[{"type":"public-key","id":"AQID"}],"extensions":{"appid":"https://twitter.com"}}},"timeout":60000}` + "\x00challenge_response\x00finish_two_factor_auth") parsed := parseJetfuelLoginResponse(body) From 46f1951384a31405ca2c53632421bc8f70f10b74 Mon Sep 17 00:00:00 2001 From: highesttt Date: Wed, 1 Jul 2026 14:27:21 -0400 Subject: [PATCH 07/20] Started working on 2fa via phone, removed security key testing files --- pkg/connector/login.go | 47 ++- pkg/connector/login_test.go | 49 +++- pkg/twittermeow/login_jetfuel.go | 361 ++++++++++++++++++----- pkg/twittermeow/login_web.go | 18 +- pkg/twittermeow/login_web_test.go | 430 ++++++++++++++++++++++++---- pkg/twittermeow/webauthn.go | 308 -------------------- pkg/twittermeow/webauthn_other.go | 12 - pkg/twittermeow/webauthn_windows.go | 214 -------------- 8 files changed, 773 insertions(+), 666 deletions(-) delete mode 100644 pkg/twittermeow/webauthn.go delete mode 100644 pkg/twittermeow/webauthn_other.go delete mode 100644 pkg/twittermeow/webauthn_windows.go diff --git a/pkg/connector/login.go b/pkg/connector/login.go index bf53f025..b2073e2e 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -215,12 +215,27 @@ func makeVerificationStep(challenge *twittermeow.WebLoginChallenge, errorLine st } else if challenge.Hint != "" { instructions = challenge.Hint } - if challenge.IsTwoFactor { + 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 != "" { @@ -242,6 +257,13 @@ func makeVerificationStep(challenge *twittermeow.WebLoginChallenge, errorLine st } } +func webLoginMissingVerificationInput(challenge *twittermeow.WebLoginChallenge) string { + if challenge != nil && challenge.InputKind == twittermeow.WebLoginChallengeInputKindPhoneNumber { + return "Enter the phone number associated with your X account." + } + return "Enter the X verification code." +} + func makeAuthMethodStep(methods []twittermeow.WebLoginAuthMethod, errorLine string) *bridgev2.LoginStep { instructions := "Choose how to verify this X login." if errorLine != "" { @@ -249,6 +271,9 @@ func makeAuthMethodStep(methods []twittermeow.WebLoginAuthMethod, errorLine stri } options := make([]string, 0, len(methods)) for _, method := range methods { + if !method.Supported { + continue + } if method.Name == "" { options = append(options, method.ID) } else { @@ -667,11 +692,11 @@ func (t *TwitterLogin) submitWebVerificationInput(ctx context.Context, input map t.webLoginMethods = nil return makeCredentialsStep("The X login session expired. Enter your X login details again."), nil } - code := strings.TrimSpace(input[loginFieldVerificationCode]) - if code == "" { - return makeVerificationStep(t.webLoginChallenge, "Enter the X verification code."), nil + text := strings.TrimSpace(input[loginFieldVerificationCode]) + if text == "" { + return makeVerificationStep(t.webLoginChallenge, webLoginMissingVerificationInput(t.webLoginChallenge)), nil } - result, err := t.webLogin.SubmitText(ctx, code) + result, err := t.webLogin.SubmitText(ctx, text) if err != nil { return makeVerificationStep(t.webLoginChallenge, webLoginErrorInstructions(err)), nil } @@ -720,8 +745,18 @@ func (t *TwitterLogin) handleWebLoginResult(ctx context.Context, result *twitter Str("subtask_id", result.CurrentSubtaskID). Str("status", string(result.Status)). Msg("X returned unsupported login subtask") - return makeCredentialsStep("X returned a login challenge this bridge does not support yet."), nil + 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 (t *TwitterLogin) completeWebLogin(ctx context.Context) (*bridgev2.LoginStep, error) { diff --git a/pkg/connector/login_test.go b/pkg/connector/login_test.go index 66b8ea22..0a78f85d 100644 --- a/pkg/connector/login_test.go +++ b/pkg/connector/login_test.go @@ -12,8 +12,9 @@ import ( 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: true}, + {ID: "U2fSecurityKey", Name: "Security Key PC", Supported: false}, } step := makeAuthMethodStep(methods, "") @@ -33,7 +34,7 @@ func TestMakeAuthMethodStepUsesNativeSelect(t *testing.T) { if field.ID != loginFieldAuthMethod { t.Fatalf("field.ID = %s, want %s", field.ID, loginFieldAuthMethod) } - if strings.Join(field.Options, ",") != "Authenticator App,Backup Code,Security Key PC" { + if strings.Join(field.Options, ",") != "Authenticator App,Backup Code" { t.Fatalf("field.Options = %#v", field.Options) } if strings.Contains(step.Instructions, "not supported") { @@ -41,15 +42,53 @@ func TestMakeAuthMethodStepUsesNativeSelect(t *testing.T) { } } +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: "U2fSecurityKey", Name: "Security Key PC", 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, "u2f_security_key"); !ok || method.ID != "U2fSecurityKey" { - t.Fatalf("find by normalized ID = %#v %t, want U2fSecurityKey", 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/login_jetfuel.go b/pkg/twittermeow/login_jetfuel.go index 9971205a..6a7cd0e2 100644 --- a/pkg/twittermeow/login_jetfuel.go +++ b/pkg/twittermeow/login_jetfuel.go @@ -403,6 +403,14 @@ func (wls *WebLoginSession) jetfuelAuthMethodChoiceResult(parsed jetfuelLoginRes 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", @@ -412,8 +420,31 @@ func (wls *WebLoginSession) jetfuelAuthMethodChoiceResult(parsed jetfuelLoginRes Description: "Choose how to verify this X login.", IsTwoFactor: true, }, - AuthMethods: methods, + 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) { @@ -424,10 +455,20 @@ func (wls *WebLoginSession) submitJetfuelAuthMethod(ctx context.Context, methodI 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 @@ -440,10 +481,7 @@ func (wls *WebLoginSession) submitJetfuelAuthMethod(ctx context.Context, methodI if wls.client.IsLoggedIn() || parsed.isComplete() { return &WebLoginResult{Status: WebLoginStatusComplete}, nil } - if method.Kind == WebLoginAuthMethodKindSecurityKey { - return wls.submitJetfuelSecurityKeyChallenge(ctx, method, parsed) - } - if action := parsed.verificationAction(); action != "" { + if action := parsed.verificationActionForMethod(method); action != "" { wls.jetfuel.verificationAction = action wls.jetfuel.verificationFields = parsed.verificationCodeFields() return &WebLoginResult{ @@ -455,54 +493,23 @@ func (wls *WebLoginSession) submitJetfuelAuthMethod(ctx context.Context, methodI 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 (wls *WebLoginSession) submitJetfuelSecurityKeyChallenge(ctx context.Context, method WebLoginAuthMethod, parsed jetfuelLoginResponse) (*WebLoginResult, error) { - challenge, ok := parsed.webAuthnChallenge() - if !ok { - return nil, missingWebAuthnChallengeError(method.Name) - } - challengeResponse, err := createWebAuthnChallengeResponse(ctx, challenge) - if err != nil { - return nil, err - } - action := parsed.verificationAction() - if action == "" { - action = endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH - } - form := webAuthnChallengeSubmitForm(challengeResponse, parsed.verificationCodeFields(), wls.jetfuel.sessionToken, wls.jetfuel.preludeDispatchID) - body, err := wls.client.jetfuelPostForm(ctx, action, form) - if err != nil { - return nil, err - } - next := parseJetfuelLoginResponse(body) - if err := next.loginError(); err != nil { - return nil, err - } - wls.updateJetfuelState(next) - if wls.client.IsLoggedIn() || next.isComplete() { - return &WebLoginResult{Status: WebLoginStatusComplete}, nil - } - if result := wls.jetfuelAuthMethodChoiceResult(next); result != nil { - return result, nil - } - if action := next.verificationAction(); action != "" { - wls.jetfuel.verificationAction = action - wls.jetfuel.verificationFields = next.verificationCodeFields() - return &WebLoginResult{ - Status: WebLoginStatusNeedsText, - CurrentSubtaskID: "JetfuelVerification", - Challenge: next.verificationChallengeForMethod(method), - }, nil - } - return nil, fmt.Errorf("%w: jetfuel security-key response did not complete login", ErrWebLoginUnexpectedSubtask) -} - func (jls *jetfuelLoginState) findAuthMethod(methodID string) (WebLoginAuthMethod, bool) { methodID = normalizeJetfuelMethodID(methodID) for _, method := range jls.twoFactorMethods { - if normalizeJetfuelMethodID(method.ID) == methodID || normalizeJetfuelMethodID(method.Name) == methodID { + if normalizeJetfuelMethodID(method.ID) == methodID || + normalizeJetfuelMethodID(method.SubmitID) == methodID || + normalizeJetfuelMethodID(method.Name) == methodID { return method, true } } @@ -510,8 +517,12 @@ func (jls *jetfuelLoginState) findAuthMethod(methodID string) (WebLoginAuthMetho } 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": {method.ID}, + "two_factor_auth_method_type": {methodID}, "_selected_method_idx": {fmt.Sprintf("%d", method.Index)}, } if jls.userID != "" { @@ -878,7 +889,94 @@ func (jfr jetfuelLoginResponse) verificationAction() string { 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{ @@ -918,6 +1016,38 @@ func (jfr jetfuelLoginResponse) verificationCodeFields() []string { 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) { @@ -976,10 +1106,25 @@ func firstJetfuelNumericID(value string) string { func (jfr jetfuelLoginResponse) isAuthMethodChoice() bool { text := jfr.text() - return strings.Contains(text, "select a method") || + 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") + 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 { @@ -987,12 +1132,17 @@ func (jfr jetfuelLoginResponse) authMethods() []WebLoginAuthMethod { return nil } methods := make([]WebLoginAuthMethod, 0, 3) + methodIndex := 0 for _, str := range jfr.strings { method, ok := classifyJetfuelAuthMethod(str) - if !ok || containsJetfuelAuthMethod(methods, method.ID) { + if !ok { + continue + } + if updated := updateJetfuelAuthMethod(methods, method); updated { continue } - method.Index = len(methods) + method.Index = methodIndex + methodIndex++ methods = append(methods, method) } return methods @@ -1017,24 +1167,52 @@ func classifyJetfuelAuthMethod(value string) (WebLoginAuthMethod, bool) { 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: "Use a security key or passkey.", - Kind: WebLoginAuthMethodKindSecurityKey, - Supported: true, + Description: "Requires passkey/WebAuthn client support.", + Kind: WebLoginAuthMethodKindUnknown, + Supported: false, }, true default: return WebLoginAuthMethod{}, false } } -func containsJetfuelAuthMethod(methods []WebLoginAuthMethod, id string) bool { - normalized := normalizeJetfuelMethodID(id) - return slices.ContainsFunc(methods, func(method WebLoginAuthMethod) bool { - return normalizeJetfuelMethodID(method.ID) == normalized - }) +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 { @@ -1048,13 +1226,21 @@ func normalizeJetfuelMethodID(value string) string { 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: "Verification code", + Hint: hint, Description: jetfuelChallengeDescription(text), - IsTwoFactor: strings.Contains(text, "two-factor") || strings.Contains(text, "two factor") || + 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"), + strings.Contains(text, "backup code") || strings.Contains(text, "totp")), + InputKind: inputKind, } } @@ -1066,15 +1252,25 @@ func (jfr jetfuelLoginResponse) verificationChallengeForMethod(method WebLoginAu 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 = true + 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"): @@ -1084,6 +1280,30 @@ func jetfuelChallengeDescription(text string) string { } } +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") @@ -1100,7 +1320,7 @@ func (jfr jetfuelLoginResponse) loginError() error { 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 strings.Contains(text, "wrong password") || strings.Contains(text, "incorrect password"): + 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."} @@ -1108,3 +1328,14 @@ func (jfr jetfuelLoginResponse) loginError() error { 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_web.go b/pkg/twittermeow/login_web.go index 0006162c..93707e87 100644 --- a/pkg/twittermeow/login_web.go +++ b/pkg/twittermeow/login_web.go @@ -44,10 +44,17 @@ const ( type WebLoginAuthMethodKind string const ( - WebLoginAuthMethodKindCode WebLoginAuthMethodKind = "code" - WebLoginAuthMethodKindBackupCode WebLoginAuthMethodKind = "backup_code" - WebLoginAuthMethodKindSecurityKey WebLoginAuthMethodKind = "security_key" - WebLoginAuthMethodKindUnknown WebLoginAuthMethodKind = "unknown" + 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 { @@ -88,10 +95,12 @@ type WebLoginChallenge struct { Hint string Description string IsTwoFactor bool + InputKind WebLoginChallengeInputKind } type WebLoginAuthMethod struct { ID string + SubmitID string Name string Description string Kind WebLoginAuthMethodKind @@ -689,7 +698,6 @@ func webLoginSubtaskVersions() map[string]int { "open_link": 1, "phone_verification": 4, "privacy_options": 1, - "security_key": 3, "select_avatar": 4, "select_banner": 2, "settings_list": 7, diff --git a/pkg/twittermeow/login_web_test.go b/pkg/twittermeow/login_web_test.go index 2679aa74..90878115 100644 --- a/pkg/twittermeow/login_web_test.go +++ b/pkg/twittermeow/login_web_test.go @@ -4,8 +4,10 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "io" "net/http" + "net/url" "strings" "testing" @@ -162,6 +164,28 @@ func TestJetfuelLoginResponseExpandsBareTwoFactorActions(t *testing.T) { } } +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() @@ -180,6 +204,46 @@ func TestJetfuelLoginResponseFindsBackupCodeFields(t *testing.T) { } } +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() @@ -203,23 +267,30 @@ func TestJetfuelLoginResponseDoesNotTreatChallengeModesAsFields(t *testing.T) { 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\x00BackupCode\x00U2fSecurityKey\x00user_id\x001127993589949243392\x00session_token\x0012345678-1234-1234-1234-123456789abc\x00begin_two_factor_auth", + "Totp\x00Sms\x00BackupCode\x00U2fSecurityKey\x00user_id\x001127993589949243392\x00session_token\x0012345678-1234-1234-1234-123456789abc\x00begin_two_factor_auth", )) methods := parsed.authMethods() - if len(methods) != 3 { - t.Fatalf("authMethods() length = %d, want 3: %#v", len(methods), methods) + if len(methods) != 4 { + t.Fatalf("authMethods() length = %d, want 4: %#v", len(methods), methods) } - wantIDs := []string{"Totp", "BackupCode", "U2fSecurityKey"} + 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 { - t.Fatalf("code-based methods should be supported: %#v", methods) + 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[2].Supported || methods[2].Kind != WebLoginAuthMethodKindSecurityKey { - t.Fatalf("security key method = %#v, want supported security-key method", methods[2]) + 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) @@ -229,6 +300,53 @@ func TestJetfuelLoginResponseFindsAuthMethodChoice(t *testing.T) { } } +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 { @@ -236,6 +354,33 @@ func TestJetfuelLoginResponseDoesNotTreatCodePromptAsAuthMethodChoice(t *testing } } +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", @@ -261,6 +406,18 @@ func TestJetfuelAuthMethodFormShape(t *testing.T) { } } +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) { @@ -291,7 +448,7 @@ func TestSubmitJetfuelAuthMethodPrefersVerificationChallenge(t *testing.T) { 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: WebLoginAuthMethodKindSecurityKey, Supported: true, Index: 2}, + {ID: "U2fSecurityKey", Name: "Security Key PC", Kind: WebLoginAuthMethodKindUnknown, Supported: false, Index: 2}, }, } @@ -310,63 +467,210 @@ func TestSubmitJetfuelAuthMethodPrefersVerificationChallenge(t *testing.T) { } } -func TestJetfuelLoginResponseFindsWebAuthnChallenge(t *testing.T) { - body := []byte(`security_key` + "\x00" + `{"action":"verification","challenge":{"publicKeyCredentialRequestOptions":{"rpId":"x.com","challenge":"abc123","allowCredentials":[{"type":"public-key","id":"AQID"}],"extensions":{"appid":"https://twitter.com"}}},"timeout":60000}` + "\x00challenge_response\x00finish_two_factor_auth") - parsed := parseJetfuelLoginResponse(body) +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}, + }, + } - challenge, ok := parsed.webAuthnChallenge() - if !ok { - t.Fatal("webAuthnChallenge() returned false") + 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}, + }, } - if challenge.RequestOptions.RPID != "x.com" { - t.Fatalf("rpId = %q", challenge.RequestOptions.RPID) + + result, err := session.SubmitAuthMethod(context.Background(), "Text Message") + if err != nil { + t.Fatalf("SubmitAuthMethod() error = %v", err) } - if challenge.RequestOptions.Challenge != "abc123" { - t.Fatalf("challenge = %q", challenge.RequestOptions.Challenge) + if result.Status != WebLoginStatusNeedsText { + t.Fatalf("SubmitAuthMethod() status = %s, want %s", result.Status, WebLoginStatusNeedsText) } - if got := challenge.RequestOptions.AllowCredentials[0].ID; got != "AQID" { - t.Fatalf("allowCredentials[0].id = %q", got) + if result.Challenge == nil || result.Challenge.InputKind != WebLoginChallengeInputKindPhoneNumber { + t.Fatalf("Challenge = %#v, want phone-number prompt", result.Challenge) } - if got := challenge.RequestOptions.Extensions.AppID; got != "https://twitter.com" { - t.Fatalf("extensions.appid = %q", got) + if session.jetfuel.verificationAction != endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH { + t.Fatalf("verificationAction = %q", session.jetfuel.verificationAction) } } -func TestWebAuthnChallengeResponseShape(t *testing.T) { - response, err := marshalWebAuthnChallengeResponse(&webAuthnAssertion{ - CredentialID: []byte{1, 2, 3}, - ClientDataJSON: []byte(`{"type":"webauthn.get"}`), - AuthenticatorData: []byte{4, 5, 6}, - Signature: []byte{7, 8, 9}, - }) +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("marshalWebAuthnChallengeResponse() error = %v", err) + 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{}} - var payload struct { - ID string `json:"id"` - Type string `json:"type"` - Response struct { - ClientDataJSON string `json:"clientDataJSON"` - AuthenticatorData string `json:"authenticatorData"` - Signature string `json:"signature"` - UserHandle string `json:"userHandle"` - } `json:"response"` - ClientExtensionResults struct { - AppID bool `json:"appid"` - } `json:"clientExtensionResults"` + 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 err := json.Unmarshal([]byte(response), &payload); err != nil { - t.Fatalf("Unmarshal(response) error = %v", err) + if len(session.jetfuel.twoFactorMethods) != 1 { + t.Fatalf("twoFactorMethods length = %d, want 1: %#v", len(session.jetfuel.twoFactorMethods), session.jetfuel.twoFactorMethods) } - if payload.ID != "AQID" || payload.Type != "public-key" { - t.Fatalf("payload identity = %#v", payload) + 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 payload.Response.AuthenticatorData != "BAUG" || payload.Response.Signature != "BwgJ" { - t.Fatalf("payload response = %#v", payload.Response) + if result.Challenge == nil || result.Challenge.Description != "Text message verification is coming soon." { + t.Fatalf("Challenge = %#v, want coming-soon description", result.Challenge) } - if payload.Response.UserHandle != "" || !payload.ClientExtensionResults.AppID { - t.Fatalf("payload extension/user handle = %#v", payload) +} + +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) } } @@ -433,6 +737,30 @@ func TestJetfuelLoginResponseClassifiesMissingAccount(t *testing.T) { } } +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) diff --git a/pkg/twittermeow/webauthn.go b/pkg/twittermeow/webauthn.go deleted file mode 100644 index 0be06d9d..00000000 --- a/pkg/twittermeow/webauthn.go +++ /dev/null @@ -1,308 +0,0 @@ -package twittermeow - -import ( - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "net/url" - "strings" -) - -const webAuthnOrigin = "https://x.com" - -type webAuthnChallenge struct { - RequestOptions webAuthnRequestOptions -} - -type webAuthnRequestOptions struct { - RPID string `json:"rpId"` - Challenge string `json:"challenge"` - AllowCredentials []webAuthnCredentialHint `json:"allowCredentials"` - Extensions webAuthnExtensions `json:"extensions"` -} - -type webAuthnCredentialHint struct { - Type string `json:"type"` - ID string `json:"id"` - Transports []string `json:"transports"` -} - -type webAuthnExtensions struct { - AppID string `json:"appid"` -} - -type webAuthnAssertion struct { - CredentialID []byte - ClientDataJSON []byte - AuthenticatorData []byte - Signature []byte -} - -type webAuthnClientData struct { - Type string `json:"type"` - Challenge string `json:"challenge"` - Origin string `json:"origin"` - CrossOrigin bool `json:"crossOrigin"` -} - -func (challenge webAuthnChallenge) rpID() string { - rpID := strings.TrimSpace(challenge.RequestOptions.RPID) - if rpID == "" { - return "x.com" - } - return rpID -} - -func (challenge webAuthnChallenge) clientDataJSON() ([]byte, error) { - challengeValue := strings.TrimSpace(challenge.RequestOptions.Challenge) - if challengeValue == "" { - return nil, errors.New("x security-key challenge is missing") - } - return json.Marshal(webAuthnClientData{ - Type: "webauthn.get", - Challenge: challengeValue, - Origin: webAuthnOrigin, - CrossOrigin: false, - }) -} - -func createWebAuthnChallengeResponse(ctx context.Context, challenge webAuthnChallenge) (string, error) { - clientDataJSON, err := challenge.clientDataJSON() - if err != nil { - return "", err - } - assertion, err := platformWebAuthnGetAssertion(ctx, challenge, clientDataJSON) - if err != nil { - return "", err - } - return marshalWebAuthnChallengeResponse(assertion) -} - -func marshalWebAuthnChallengeResponse(assertion *webAuthnAssertion) (string, error) { - if assertion == nil { - return "", errors.New("x security-key assertion was empty") - } - payload := struct { - ID string `json:"id"` - Type string `json:"type"` - Response struct { - ClientDataJSON string `json:"clientDataJSON"` - AuthenticatorData string `json:"authenticatorData"` - Signature string `json:"signature"` - UserHandle string `json:"userHandle"` - } `json:"response"` - ClientExtensionResults struct { - AppID bool `json:"appid"` - } `json:"clientExtensionResults"` - }{ - ID: webAuthnBase64(assertion.CredentialID), - Type: "public-key", - } - payload.Response.ClientDataJSON = webAuthnBase64(assertion.ClientDataJSON) - payload.Response.AuthenticatorData = webAuthnBase64(assertion.AuthenticatorData) - payload.Response.Signature = webAuthnBase64(assertion.Signature) - payload.Response.UserHandle = "" - payload.ClientExtensionResults.AppID = true - data, err := json.Marshal(payload) - if err != nil { - return "", err - } - return string(data), nil -} - -func webAuthnBase64(data []byte) string { - return base64.RawURLEncoding.EncodeToString(data) -} - -func webAuthnDecodeBase64(value string) ([]byte, error) { - value = strings.TrimSpace(value) - if value == "" { - return nil, nil - } - if data, err := base64.RawURLEncoding.DecodeString(value); err == nil { - return data, nil - } - return base64.URLEncoding.DecodeString(value) -} - -func (jfr jetfuelLoginResponse) webAuthnChallenge() (webAuthnChallenge, bool) { - for _, str := range jfr.strings { - if challenge, ok := findWebAuthnChallengeInText(str); ok { - return challenge, true - } - } - if len(jfr.raw) > 0 { - return findWebAuthnChallengeInText(string(jfr.raw)) - } - return webAuthnChallenge{}, false -} - -func findWebAuthnChallengeInText(text string) (webAuthnChallenge, bool) { - if !strings.Contains(text, "publicKeyCredentialRequestOptions") && !strings.Contains(text, "rpId") { - return webAuthnChallenge{}, false - } - for _, candidate := range extractJSONObjectsContaining(text, "publicKeyCredentialRequestOptions") { - if challenge, ok := parseWebAuthnChallengeCandidate(candidate); ok { - return challenge, true - } - } - for _, candidate := range extractJSONObjectsContaining(text, "rpId") { - if challenge, ok := parseWebAuthnChallengeCandidate(candidate); ok { - return challenge, true - } - } - return webAuthnChallenge{}, false -} - -func parseWebAuthnChallengeCandidate(candidate string) (webAuthnChallenge, bool) { - for _, variant := range webAuthnJSONVariants(candidate) { - if challenge, ok := parseWebAuthnChallengeJSON([]byte(variant)); ok { - return challenge, true - } - } - return webAuthnChallenge{}, false -} - -func webAuthnJSONVariants(candidate string) []string { - candidate = strings.TrimSpace(candidate) - var out []string - add := func(value string) { - value = strings.TrimSpace(value) - if value == "" { - return - } - for _, existing := range out { - if existing == value { - return - } - } - out = append(out, value) - } - add(candidate) - if unescaped, err := url.QueryUnescape(candidate); err == nil { - add(unescaped) - } - if strings.Contains(candidate, `\"`) { - add(strings.ReplaceAll(candidate, `\"`, `"`)) - } - var quoted string - if err := json.Unmarshal([]byte(candidate), "ed); err == nil { - add(quoted) - } - return out -} - -func parseWebAuthnChallengeJSON(data []byte) (webAuthnChallenge, bool) { - var envelope struct { - Challenge json.RawMessage `json:"challenge"` - PublicKeyCredentialRequestOptions *webAuthnRequestOptions `json:"publicKeyCredentialRequestOptions"` - RPID string `json:"rpId"` - } - if err := json.Unmarshal(data, &envelope); err != nil { - return webAuthnChallenge{}, false - } - if envelope.PublicKeyCredentialRequestOptions != nil && envelope.PublicKeyCredentialRequestOptions.Challenge != "" { - return webAuthnChallenge{RequestOptions: *envelope.PublicKeyCredentialRequestOptions}, true - } - if len(envelope.Challenge) > 0 { - challengeData := []byte(envelope.Challenge) - var challengeString string - if err := json.Unmarshal(envelope.Challenge, &challengeString); err == nil { - challengeData = []byte(challengeString) - } - if challenge, ok := parseWebAuthnChallengeCandidate(string(challengeData)); ok { - return challenge, true - } - } - var direct webAuthnRequestOptions - if err := json.Unmarshal(data, &direct); err == nil && direct.Challenge != "" { - return webAuthnChallenge{RequestOptions: direct}, true - } - return webAuthnChallenge{}, false -} - -func extractJSONObjectsContaining(text, needle string) []string { - var out []string - for searchStart := 0; ; { - idx := strings.Index(text[searchStart:], needle) - if idx < 0 { - return out - } - idx += searchStart - for start := idx; start >= 0; start-- { - if text[start] != '{' { - continue - } - end, ok := balancedJSONEnd(text, start) - if !ok || end <= idx { - continue - } - out = append(out, text[start:end]) - break - } - searchStart = idx + len(needle) - } -} - -func balancedJSONEnd(text string, start int) (int, bool) { - depth := 0 - inString := false - escaped := false - for i := start; i < len(text); i++ { - ch := text[i] - if inString { - if escaped { - escaped = false - continue - } - switch ch { - case '\\': - escaped = true - case '"': - inString = false - } - continue - } - switch ch { - case '"': - inString = true - case '{': - depth++ - case '}': - depth-- - if depth == 0 { - return i + 1, true - } - if depth < 0 { - return 0, false - } - } - } - return 0, false -} - -func webAuthnChallengeSubmitForm(challengeResponse string, fields []string, sessionToken, preludeDispatchID string) url.Values { - form := url.Values{} - for _, field := range fields { - lower := strings.ToLower(field) - if strings.Contains(lower, "challenge") || strings.Contains(lower, "response") || strings.Contains(lower, "webauthn") { - form.Set(field, challengeResponse) - } - } - if form.Get("challenge_response") == "" { - form.Set("challenge_response", challengeResponse) - } - if sessionToken != "" { - form.Set("session_token", sessionToken) - } - if preludeDispatchID != "" { - form.Set("prelude_dispatch_id", preludeDispatchID) - } - return form -} - -func missingWebAuthnChallengeError(methodName string) error { - return fmt.Errorf("%w: %s did not expose a security-key challenge", ErrWebLoginUnexpectedSubtask, methodName) -} diff --git a/pkg/twittermeow/webauthn_other.go b/pkg/twittermeow/webauthn_other.go deleted file mode 100644 index f8f616f3..00000000 --- a/pkg/twittermeow/webauthn_other.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !windows - -package twittermeow - -import ( - "context" - "fmt" -) - -func platformWebAuthnGetAssertion(context.Context, webAuthnChallenge, []byte) (*webAuthnAssertion, error) { - return nil, fmt.Errorf("native security-key/passkey login requires Windows WebAuthn support") -} diff --git a/pkg/twittermeow/webauthn_windows.go b/pkg/twittermeow/webauthn_windows.go deleted file mode 100644 index 89386659..00000000 --- a/pkg/twittermeow/webauthn_windows.go +++ /dev/null @@ -1,214 +0,0 @@ -//go:build windows - -package twittermeow - -import ( - "context" - "fmt" - "runtime" - "strings" - "unsafe" - - "golang.org/x/sys/windows" -) - -const ( - windowsWebAuthnClientDataVersion = 1 - windowsWebAuthnCredentialVersion = 1 - windowsWebAuthnGetAssertionOptionsVersion = 2 - windowsWebAuthnUserVerificationDiscouraged = 3 - windowsWebAuthnDefaultAssertionTimeoutMS = 60000 - windowsWebAuthnCredentialTypePublicKey = "public-key" - windowsWebAuthnHashAlgorithmSHA256 = "SHA-256" - windowsWebAuthnCredentialAttachmentAny uint32 = 0 -) - -var ( - windowsWebAuthnDLL = windows.NewLazySystemDLL("webauthn.dll") - windowsWebAuthnGetAssertionProc = windowsWebAuthnDLL.NewProc("WebAuthNAuthenticatorGetAssertion") - windowsWebAuthnFreeAssertionProc = windowsWebAuthnDLL.NewProc("WebAuthNFreeAssertion") -) - -type windowsWebAuthnClientData struct { - Version uint32 - ClientDataJSONN uint32 - ClientDataJSON *byte - HashAlgID *uint16 -} - -type windowsWebAuthnCredential struct { - Version uint32 - IDN uint32 - ID *byte - CredentialType *uint16 -} - -type windowsWebAuthnCredentials struct { - CredentialsN uint32 - Credentials *windowsWebAuthnCredential -} - -type windowsWebAuthnExtension struct { - ExtensionIdentifier *uint16 - ExtensionN uint32 - Extension unsafe.Pointer -} - -type windowsWebAuthnExtensions struct { - ExtensionsN uint32 - Extensions *windowsWebAuthnExtension -} - -type windowsWebAuthnGetAssertionOptionsV2 struct { - Version uint32 - TimeoutMilliseconds uint32 - CredentialList windowsWebAuthnCredentials - Extensions windowsWebAuthnExtensions - AuthenticatorAttachment uint32 - UserVerificationRequirement uint32 - Flags uint32 - U2fAppID *uint16 - U2fAppIDUsed *int32 -} - -type windowsWebAuthnAssertion struct { - Version uint32 - AuthenticatorDataN uint32 - AuthenticatorData *byte - SignatureN uint32 - Signature *byte - Credential windowsWebAuthnCredential - UserIDN uint32 - UserID *byte - Extensions windowsWebAuthnExtensions - CredLargeBlobN uint32 - CredLargeBlob *byte - CredLargeBlobStatus uint32 - HmacSecret unsafe.Pointer - UsedTransport uint32 - UnsignedExtensionOutputsN uint32 - UnsignedExtensionOutputs *byte - ClientDataJSONN uint32 - ClientDataJSON *byte - AuthenticationResponseJSONN uint32 - AuthenticationResponseJSON *byte -} - -func platformWebAuthnGetAssertion(_ context.Context, challenge webAuthnChallenge, clientDataJSON []byte) (*webAuthnAssertion, error) { - rpID, err := windows.UTF16PtrFromString(challenge.rpID()) - if err != nil { - return nil, err - } - hashAlg, err := windows.UTF16PtrFromString(windowsWebAuthnHashAlgorithmSHA256) - if err != nil { - return nil, err - } - credentialType, err := windows.UTF16PtrFromString(windowsWebAuthnCredentialTypePublicKey) - if err != nil { - return nil, err - } - - credentialIDs, credentials, err := windowsWebAuthnCredentialsFromChallenge(challenge, credentialType) - if err != nil { - return nil, err - } - var credentialPtr *windowsWebAuthnCredential - if len(credentials) > 0 { - credentialPtr = &credentials[0] - } - - clientData := windowsWebAuthnClientData{ - Version: windowsWebAuthnClientDataVersion, - ClientDataJSONN: uint32(len(clientDataJSON)), - ClientDataJSON: byteSlicePtr(clientDataJSON), - HashAlgID: hashAlg, - } - opts := windowsWebAuthnGetAssertionOptionsV2{ - Version: windowsWebAuthnGetAssertionOptionsVersion, - TimeoutMilliseconds: windowsWebAuthnDefaultAssertionTimeoutMS, - CredentialList: windowsWebAuthnCredentials{CredentialsN: uint32(len(credentials)), Credentials: credentialPtr}, - AuthenticatorAttachment: windowsWebAuthnCredentialAttachmentAny, - UserVerificationRequirement: windowsWebAuthnUserVerificationDiscouraged, - } - var u2fAppIDUsed int32 - if appID := strings.TrimSpace(challenge.RequestOptions.Extensions.AppID); appID != "" { - u2fAppID, err := windows.UTF16PtrFromString(appID) - if err != nil { - return nil, err - } - opts.U2fAppID = u2fAppID - opts.U2fAppIDUsed = &u2fAppIDUsed - } - - var assertionPtr *windowsWebAuthnAssertion - result, _, _ := windowsWebAuthnGetAssertionProc.Call( - 0, - uintptr(unsafe.Pointer(rpID)), - uintptr(unsafe.Pointer(&clientData)), - uintptr(unsafe.Pointer(&opts)), - uintptr(unsafe.Pointer(&assertionPtr)), - ) - runtime.KeepAlive(clientDataJSON) - runtime.KeepAlive(credentialIDs) - runtime.KeepAlive(credentials) - runtime.KeepAlive(credentialType) - runtime.KeepAlive(opts) - if result != 0 { - return nil, fmt.Errorf("windows webauthn assertion failed: HRESULT 0x%08x", uint32(result)) - } - if assertionPtr == nil { - return nil, fmt.Errorf("windows webauthn assertion was empty") - } - defer windowsWebAuthnFreeAssertionProc.Call(uintptr(unsafe.Pointer(assertionPtr))) - - assertion := *assertionPtr - assertionClientData := copyWindowsBytes(assertion.ClientDataJSON, assertion.ClientDataJSONN) - if len(assertionClientData) == 0 { - assertionClientData = append([]byte(nil), clientDataJSON...) - } - return &webAuthnAssertion{ - CredentialID: copyWindowsBytes(assertion.Credential.ID, assertion.Credential.IDN), - ClientDataJSON: assertionClientData, - AuthenticatorData: copyWindowsBytes(assertion.AuthenticatorData, assertion.AuthenticatorDataN), - Signature: copyWindowsBytes(assertion.Signature, assertion.SignatureN), - }, nil -} - -func windowsWebAuthnCredentialsFromChallenge(challenge webAuthnChallenge, credentialType *uint16) ([][]byte, []windowsWebAuthnCredential, error) { - ids := make([][]byte, 0, len(challenge.RequestOptions.AllowCredentials)) - credentials := make([]windowsWebAuthnCredential, 0, len(challenge.RequestOptions.AllowCredentials)) - for _, allowed := range challenge.RequestOptions.AllowCredentials { - if allowed.Type != "" && allowed.Type != windowsWebAuthnCredentialTypePublicKey { - continue - } - id, err := webAuthnDecodeBase64(allowed.ID) - if err != nil { - return nil, nil, fmt.Errorf("decode X security-key credential ID: %w", err) - } - if len(id) == 0 { - continue - } - ids = append(ids, id) - credentials = append(credentials, windowsWebAuthnCredential{ - Version: windowsWebAuthnCredentialVersion, - IDN: uint32(len(id)), - ID: byteSlicePtr(id), - CredentialType: credentialType, - }) - } - return ids, credentials, nil -} - -func byteSlicePtr(data []byte) *byte { - if len(data) == 0 { - return nil - } - return &data[0] -} - -func copyWindowsBytes(ptr *byte, size uint32) []byte { - if ptr == nil || size == 0 { - return nil - } - return append([]byte(nil), unsafe.Slice(ptr, int(size))...) -} From 45684e15c83a8e76eb1616f69065c784d2dc1352 Mon Sep 17 00:00:00 2001 From: highesttt Date: Thu, 2 Jul 2026 18:44:57 -0400 Subject: [PATCH 08/20] fix: address most review comments for login flow --- .gitignore | 7 - pkg/connector/login.go | 101 +- pkg/connector/login_test.go | 48 + pkg/twittermeow/castle_web_token_test.go | 3432 ----------------- .../castle_web_token_test_helpers_test.go | 234 -- pkg/twittermeow/client.go | 2 +- pkg/twittermeow/cookies/cookies.go | 32 +- pkg/twittermeow/cookies/cookies_test.go | 22 + pkg/twittermeow/methods/html.go | 12 +- pkg/twittermeow/methods/html_test.go | 18 + 10 files changed, 193 insertions(+), 3715 deletions(-) delete mode 100644 pkg/twittermeow/castle_web_token_test.go delete mode 100644 pkg/twittermeow/castle_web_token_test_helpers_test.go diff --git a/.gitignore b/.gitignore index 4bdd1e5b..1de87ca7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,10 +8,3 @@ logs/ !.pre-commit-config.yaml /start /pkg/mautrix - -/*.dll.a -/*.dll -/mautrix-twitter.exe -/data -actions*.txt -grind*.md diff --git a/pkg/connector/login.go b/pkg/connector/login.go index b2073e2e..539e2fed 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -118,12 +118,22 @@ 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: "X login", + Name: "Username/password", Description: "Log in with your X username, email, or phone number and password", ID: LoginFlowIDPassword, }, @@ -257,13 +267,6 @@ func makeVerificationStep(challenge *twittermeow.WebLoginChallenge, errorLine st } } -func webLoginMissingVerificationInput(challenge *twittermeow.WebLoginChallenge) string { - if challenge != nil && challenge.InputKind == twittermeow.WebLoginChallengeInputKindPhoneNumber { - return "Enter the phone number associated with your X account." - } - return "Enter the X verification code." -} - func makeAuthMethodStep(methods []twittermeow.WebLoginAuthMethod, errorLine string) *bridgev2.LoginStep { instructions := "Choose how to verify this X login." if errorLine != "" { @@ -615,14 +618,14 @@ func (t *TwitterLogin) SubmitUserInput(ctx context.Context, input map[string]str if _, ok := input[loginFieldIdentifier]; ok || input[loginFieldPassword] != "" { return t.submitCredentialsInput(ctx, input) } - return makeCredentialsStep("Enter your X username, email, or phone number and password."), nil + 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 makeCredentialsStep("Enter both your X identifier and password."), nil + return nil, ErrMissingLoginInput } client := twittermeow.NewClient(twitCookies.NewCookies(nil), nil, t.User.Log.With().Str("component", "login_twitter_client").Logger()) @@ -633,7 +636,7 @@ func (t *TwitterLogin) submitCredentialsInput(ctx context.Context, input map[str result, err := t.webLogin.Start(ctx) if err != nil { - return makeCredentialsStep(webLoginErrorInstructions(err)), nil + return nil, webLoginFailureError(err) } if result.Status != twittermeow.WebLoginStatusNeedsIdentifier { return t.handleWebLoginResult(ctx, result) @@ -641,7 +644,7 @@ func (t *TwitterLogin) submitCredentialsInput(ctx context.Context, input map[str result, err = t.webLogin.SubmitCredentials(ctx, identifier, password) if err != nil { - return makeCredentialsStep(webLoginErrorInstructions(err)), nil + return handleWebLoginCredentialsError(err) } return t.handleWebLoginResult(ctx, result) } @@ -653,15 +656,11 @@ func (t *TwitterLogin) submitWebAuthMethodInput(ctx context.Context, input map[s } methodID := strings.TrimSpace(input[loginFieldAuthMethod]) if methodID == "" { - return makeAuthMethodStep(t.webLoginMethods, "Choose a verification method."), nil + return nil, ErrMissingLoginInput } result, err := t.webLogin.SubmitAuthMethod(ctx, methodID) if err != nil { - if errors.Is(err, twittermeow.ErrWebLoginUnsupportedAuthMethod) || - errors.Is(err, twittermeow.ErrWebLoginMissingAuthMethodState) { - return makeAuthMethodStep(t.webLoginMethods, webLoginErrorInstructions(err)), nil - } - return makeCredentialsStep(webLoginErrorInstructions(err)), nil + return nil, webLoginFailureError(err) } return t.handleWebLoginResult(ctx, result) } @@ -694,16 +693,16 @@ func (t *TwitterLogin) submitWebVerificationInput(ctx context.Context, input map } text := strings.TrimSpace(input[loginFieldVerificationCode]) if text == "" { - return makeVerificationStep(t.webLoginChallenge, webLoginMissingVerificationInput(t.webLoginChallenge)), nil + return nil, ErrMissingLoginInput } result, err := t.webLogin.SubmitText(ctx, text) if err != nil { - return makeVerificationStep(t.webLoginChallenge, webLoginErrorInstructions(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 makeCredentialsStep(webLoginErrorInstructions(err)), nil + return handleWebLoginCredentialsError(err) } } return t.handleWebLoginResult(ctx, result) @@ -731,7 +730,7 @@ func (t *TwitterLogin) handleWebLoginResult(ctx context.Context, result *twitter if t.webLogin != nil && t.webLoginPassword != "" { next, err := t.webLogin.SubmitPassword(ctx, t.webLoginPassword) if err != nil { - return makeCredentialsStep(webLoginErrorInstructions(err)), nil + return handleWebLoginCredentialsError(err) } if next != nil && next.Status != twittermeow.WebLoginStatusNeedsPassword { return t.handleWebLoginResult(ctx, next) @@ -759,6 +758,62 @@ func webLoginUnsupportedInstructions(result *twittermeow.WebLoginResult) string 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 @@ -766,7 +821,7 @@ func (t *TwitterLogin) completeWebLogin(ctx context.Context) (*bridgev2.LoginSte client := t.webLogin.Client() profile, err := client.LoadMessagesPage(ctx) if err != nil { - return makeCredentialsStep("X accepted the login, but the bridge could not load the authenticated messages page. Try again."), nil + return nil, fmt.Errorf("failed to load authenticated X messages page after login: %w", err) } t.client = client t.profile = profile diff --git a/pkg/connector/login_test.go b/pkg/connector/login_test.go index 0a78f85d..5282a38f 100644 --- a/pkg/connector/login_test.go +++ b/pkg/connector/login_test.go @@ -1,6 +1,8 @@ package connector import ( + "context" + "errors" "strings" "testing" @@ -9,6 +11,52 @@ import ( "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}, diff --git a/pkg/twittermeow/castle_web_token_test.go b/pkg/twittermeow/castle_web_token_test.go deleted file mode 100644 index 5136d72e..00000000 --- a/pkg/twittermeow/castle_web_token_test.go +++ /dev/null @@ -1,3432 +0,0 @@ -package twittermeow - -import ( - "bytes" - "compress/flate" - "crypto/sha256" - "encoding/base64" - "encoding/binary" - "encoding/json" - "errors" - "fmt" - "io" - "math" - "os" - "regexp" - "sort" - "strconv" - "strings" - "testing" - "unicode/utf16" -) - -func TestCurrentCastleWrappedTokenRoundTrip(t *testing.T) { - const payload = `[null,"abc",123,true,{"k":"v"}]` - const timestampMillis = int64(1760000000123) - - token, err := createCurrentCastleWrappedToken(payload, timestampMillis) - if err != nil { - t.Fatalf("createCurrentCastleWrappedToken() error = %v", err) - } - if !strings.HasPrefix(token, currentCastleTokenPrefix) { - t.Fatalf("token missing current Castle prefix: %q", token[:min(len(token), len(currentCastleTokenPrefix))]) - } - wire, err := decodeCurrentCastleWrappedTokenForTest(token) - if err != nil { - t.Fatalf("decodeCurrentCastleWrappedTokenForTest() error = %v", err) - } - if len(wire) < 2 { - t.Fatalf("decoded wire too short: %d", len(wire)) - } - encodedTimestampLen := int(wire[0]) - if len(wire) <= 1+encodedTimestampLen { - t.Fatalf("decoded wire missing payload: wire_len=%d encoded_timestamp_len=%d", len(wire), encodedTimestampLen) - } - timestamp := strconv.FormatInt(timestampMillis, 10) - expectedTimestamp := encodeCurrentCastleTimestamp(timestamp) - if got := wire[1 : 1+encodedTimestampLen]; got != expectedTimestamp { - t.Fatalf("encoded timestamp = %q, want %q", got, expectedTimestamp) - } - gotPayload := xorCurrentCastleString(wire[1+encodedTimestampLen:], timestamp) - if wantPayload := insertCurrentCastleChecksum(payload); gotPayload != wantPayload { - t.Fatalf("decoded payload = %q, want %q", gotPayload, wantPayload) - } - if !strings.Contains(gotPayload, currentCastleChecksumLabel) { - t.Fatalf("decoded payload missing checksum label %q: %q", currentCastleChecksumLabel, gotPayload) - } -} - -func TestCurrentCastleKnownTransforms(t *testing.T) { - const payload = `[1,2,3]` - const timestamp = `1760000000123` - - if got, want := currentCastleChecksum(payload), "9cdea041"; got != want { - t.Fatalf("currentCastleChecksum() = %q, want %q", got, want) - } - tqCases := map[string]string{ - "": "969740e5", - "abc": "4a84ecf2", - "119280": "f394e296", - "3695375093": "a3e047f9", - } - for input, want := range tqCases { - if got := currentCastleTQHash(input); got != want { - t.Fatalf("currentCastleTQHash(%q) = %q, want %q", input, got, want) - } - } - tiCases := map[string]string{ - "": "83b623b0", - "abc": "2debeae9", - `"GraL"`: "f0b537b7", - `"override"`: "37d6d39f", - } - for input, want := range tiCases { - if got := currentCastleTIHash(input); got != want { - t.Fatalf("currentCastleTIHash(%q) = %q, want %q", input, got, want) - } - } - if got, want := encodeCurrentCastleTimestamp(timestamp), "WnP35wy50MvQy9DL0MvQy9DL0Mtac+Yab8I="; got != want { - t.Fatalf("encodeCurrentCastleTimestamp() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleUnits([]uint16{0x002f, 0x8000, 0xffff}), "RyQxU6ar"; got != want { - t.Fatalf("encodeCurrentCastleUnits() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleRawPackedUnits(jsUTF16Units("b537b7")), "S+hTpr/W8DdL6PA3"; got != want { - t.Fatalf("encodeCurrentCastleRawPackedUnits() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleLR("abc"), "oGAg"; got != want { - t.Fatalf("encodeCurrentCastleLR() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleFloat64(0), "xsbGxsbGxsY="; got != want { - t.Fatalf("encodeCurrentCastleFloat64(0) = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleFloat64(1), "OTbGxsbGxsY="; got != want { - t.Fatalf("encodeCurrentCastleFloat64(1) = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleFloat64(123.456), "hhjbKVyZ+PE="; got != want { - t.Fatalf("encodeCurrentCastleFloat64(123.456) = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6TU("abc"), "QXe5"; got != want { - t.Fatalf("encodeCurrentCastleSlot6TU() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6TF("abc"), "hxTl"; got != want { - t.Fatalf("encodeCurrentCastleSlot6TF() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6Component0("abc"), "cLDw"; got != want { - t.Fatalf("encodeCurrentCastleSlot6Component0() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6Component1("abc"), "Sjsq"; got != want { - t.Fatalf("encodeCurrentCastleSlot6Component1() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6Component1("X Castle"), "vB8NSisamgo="; got != want { - t.Fatalf("encodeCurrentCastleSlot6Component1() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6Component2("abc"), "SXTz"; got != want { - t.Fatalf("encodeCurrentCastleSlot6Component2() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6Component5Fallback("abc"), "aGlm"; got != want { - t.Fatalf("encodeCurrentCastleSlot6Component5Fallback() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6UZ("abc"), "Y29r"; got != want { - t.Fatalf("encodeCurrentCastleSlot6UZ() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6UZ("X Castle"), "hybrYys3V3M="; got != want { - t.Fatalf("encodeCurrentCastleSlot6UZ() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6IH("abc"), "odoT"; got != want { - t.Fatalf("encodeCurrentCastleSlot6IH() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6IH("X Castle"), "oCjzoaPcFIU="; got != want { - t.Fatalf("encodeCurrentCastleSlot6IH() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6FJ("abc"), "Eh4a"; got != want { - t.Fatalf("encodeCurrentCastleSlot6FJ() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6FJ("X Castle"), "9heaElpGJgI="; got != want { - t.Fatalf("encodeCurrentCastleSlot6FJ() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6IB("abc"), "BnWg"; got != want { - t.Fatalf("encodeCurrentCastleSlot6IB() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6IB("X Castle"), "cwsABpDPd8o="; got != want { - t.Fatalf("encodeCurrentCastleSlot6IB() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6AH("abc"), "1LSU"; got != want { - t.Fatalf("encodeCurrentCastleSlot6AH() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6AH("X Castle"), "tfyY1BLys1Q="; got != want { - t.Fatalf("encodeCurrentCastleSlot6AH() = %q, want %q", got, want) - } - indexR7Cases := map[uint32]uint32{ - 0x03718000: 264, - 0x0372e000: 275, - 0x03730000: 276, - 0x0373c000: 282, - 0x0376c000: 306, - 0x0377e000: 315, - 0x03788000: 320, - 0x037a2000: 333, - 0x03816000: 391, - 0x0383a000: 409, - 0x0385e000: 427, - 0x0387c000: 442, - 0x03880000: 444, - 0x03894000: 454, - 0x038a0000: 460, - 0x038b2000: 469, - 0x038d4000: 486, - 0x038d6000: 487, - 0x038d8000: 488, - 0x038da000: 489, - 0x038e2000: 493, - } - for input, want := range indexR7Cases { - if got := currentCastleIndexR7(input); got != want { - t.Fatalf("currentCastleIndexR7(%#x) = %d, want %d", input, got, want) - } - } - indexTGCases := map[uint32]uint32{ - 38521: 271, - 46027: 325, - 47278: 334, - 51170: 362, - 51865: 367, - 52560: 372, - 52699: 373, - 53811: 381, - 57842: 410, - 58120: 412, - 58259: 413, - 59232: 420, - 61456: 436, - 61873: 439, - 62151: 441, - 63402: 450, - 65348: 464, - 66460: 472, - 67155: 477, - 67989: 483, - 68128: 484, - } - for input, want := range indexTGCases { - if got := currentCastleIndexTG(input); got != want { - t.Fatalf("currentCastleIndexTG(%d) = %d, want %d", input, got, want) - } - } - tbCases := map[uint32]uint32{ - 0: 852, - 1: 991, - 0x12345678: 3798660732, - 0xffffffff: 713, - } - for input, want := range tbCases { - got := currentCastleTB(input) - if got != want { - t.Fatalf("currentCastleTB(%#x) = %d, want %d", input, got, want) - } - if roundTrip := invertCurrentCastleTB(got); roundTrip != input { - t.Fatalf("invertCurrentCastleTB(currentCastleTB(%#x)) = %#x, want %#x", input, roundTrip, input) - } - } - if got, want := invertCurrentCastleTB(2115052381), uint32(0x1353003b); got != want { - t.Fatalf("invertCurrentCastleTB(accepted slot 2) = %#x, want %#x", got, want) - } - highSlots := currentCastleHighTimingSlotIndexes() - if len(highSlots) != 63 { - t.Fatalf("currentCastleHighTimingSlotIndexes() len = %d, want 63", len(highSlots)) - } - sortedHighSlots := append([]int(nil), highSlots...) - sort.Ints(sortedHighSlots) - for i, got := range sortedHighSlots { - if want := 431 + i; got != want { - t.Fatalf("currentCastleHighTimingSlotIndexes()[sorted %d] = %d, want %d", i, got, want) - } - } - if got, want := insertCurrentCastleChecksum(payload), `[1,2,Nqkju9cdea0413]`; got != want { - t.Fatalf("insertCurrentCastleChecksum() = %q, want %q", got, want) - } -} - -func TestCurrentCastleLowerFloatTransforms(t *testing.T) { - cases := []struct { - name string - fn func(float64) string - zero string - one string - some string - }{ - {name: "slot1", fn: encodeCurrentCastleLowerFloatSlot1, zero: "4ODg4ODg4OA=", one: "KWfg4ODg4OA=", some: "4jI+qRAsNWs="}, - {name: "slot3", fn: encodeCurrentCastleLowerFloatSlot3, zero: "bm5ubm5ubm4=", one: "G39ubm5ubm4=", some: "agkBHM1lE+c="}, - {name: "slot4", fn: encodeCurrentCastleLowerFloatSlot4, zero: "7Ozs7Ozs7Ow=", one: "0xzs7Ozs7Ow=", some: "rLIxw/ZzUps="}, - {name: "slot5", fn: encodeCurrentCastleLowerFloatSlot5, zero: "YWFhYWFhYWE=", one: "II1hYWFhYWE=", some: "cfjYHOdIAC4="}, - {name: "slot9", fn: encodeCurrentCastleLowerFloatSlot9, zero: "FRUVFRUVFRU=", one: "YMUVFRUVFRU=", some: "VQuWEPdA6/g="}, - {name: "slot10", fn: encodeCurrentCastleLowerFloatSlot10, zero: "m5ubm5ubm5s=", one: "6Yebm5ubm5s=", some: "qxVp5RTxPds="}, - {name: "slot12", fn: encodeCurrentCastleLowerFloatSlot12, zero: "AAAAAAAAAAA=", one: "LWoAAAAAAAA=", some: "2AmpN7GRzQo="}, - {name: "slot13", fn: encodeCurrentCastleLowerFloatSlot13, zero: "7Ozs7Ozs7Ow=", one: "Jwvs7Ozs7Ow=", some: "iAGpDqUtz0M="}, - {name: "slot14", fn: encodeCurrentCastleLowerFloatSlot14, zero: "/f39/f39/f0=", one: "uo39/f39/f0=", some: "vZMISrdaM0I="}, - {name: "slot15", fn: encodeCurrentCastleLowerFloatSlot15, zero: "Hh4eHh4eHh4=", one: "YP8eHh4eHh4=", some: "nqKlQCohY/A="}, - {name: "slot16", fn: encodeCurrentCastleLowerFloatSlot16, zero: "PDw8PDw8PDw=", one: "llQ8PDw8PDw=", some: "HFutzi2ma8I="}, - {name: "slot17", fn: encodeCurrentCastleLowerFloatSlot17, zero: "6+vr6+vr6+s=", one: "/Pvr6+vr6+s=", some: "qy3+DLGczUQ="}, - {name: "slot18", fn: encodeCurrentCastleLowerFloatSlot18, zero: "AAAAAAAAAAA=", one: "fuEAAAAAAAA=", some: "gLy7XjQ/fe4="}, - {name: "slot19", fn: encodeCurrentCastleLowerFloatSlot19, zero: "AAAAAAAAAAA=", one: "4ZQAAAAAAAA=", some: "MOq7dR/p8ls="}, - {name: "slot21", fn: encodeCurrentCastleLowerFloatSlot21, zero: "1dXV1dXV1dU=", one: "hNDV1dXV1dU=", some: "2XqigzaOfAk="}, - {name: "slot22", fn: encodeCurrentCastleLowerFloatSlot22, zero: "GBgYGBgYGBg=", one: "/wYYGBgYGBg=", some: "ENOj/Vvrz/Y="}, - {name: "slot23", fn: encodeCurrentCastleLowerFloatSlot23, zero: "6Ojo6Ojo6Og=", one: "Z8ro6Ojo6Og=", some: "aaWkRx0gZtc="}, - {name: "slot24", fn: encodeCurrentCastleLowerFloatSlot24, zero: "8/Pz8/Pz8/M=", one: "L3bz8/Pz8/M=", some: "9WqgskkyZfI="}, - {name: "slot29", fn: encodeCurrentCastleLowerFloatSlot29, zero: "MzMzMzMzMzM=", one: "/A8zMzMzMzM=", some: "I6RE+LXUnO4="}, - {name: "slot30", fn: encodeCurrentCastleLowerFloatSlot30, zero: "rKysrKysrKw=", one: "S7KsrKysrKw=", some: "pGcXSe9fe0I="}, - {name: "slot32", fn: encodeCurrentCastleLowerFloatSlot32, zero: "2dnZ2dnZ2dk=", one: "xFLZ2dnZ2dk=", some: "28vPRGndyJ4="}, - {name: "slot33", fn: encodeCurrentCastleLowerFloatSlot33, zero: "MDAwMDAwMDA=", one: "/2wwMDAwMDA=", some: "QMen+7YX3w0="}, - {name: "slot34", fn: encodeCurrentCastleLowerFloatSlot34, zero: "Tk5OTk5OTk4=", one: "vXJOTk5OTk4=", some: "XtEtodCF+a8="}, - {name: "slot35", fn: encodeCurrentCastleLowerFloatSlot35, zero: "AwMDAwMDAwM=", one: "5jMDAwMDAwM=", some: "QylMFnXGCT4="}, - {name: "slot36", fn: encodeCurrentCastleLowerFloatSlot36, zero: "WFhYWFhYWFg=", one: "OfdYWFhYWFg=", some: "qhp2uai0lUs="}, - {name: "slot37", fn: encodeCurrentCastleLowerFloatSlot37, zero: "Li4uLi4uLi4=", one: "OV4uLi4uLi4=", some: "bhArqfTZMDE="}, - {name: "slot38", fn: encodeCurrentCastleLowerFloatSlot38, zero: "MzMzMzMzMzM=", one: "LLozMzMzMzM=", some: "NSUhrAMvKO4="}, - {name: "slot39", fn: encodeCurrentCastleLowerFloatSlot39, zero: "AAAAAAAAAAA=", one: "mqEAAAAAAAA=", some: "gXSOOl3atOo="}, - {name: "slot43", fn: encodeCurrentCastleLowerFloatSlot43, zero: "q6urq6urq6s=", one: "RJCrq6urq6s=", some: "/2KKcZ7G7Gg="}, - {name: "slot45", fn: encodeCurrentCastleLowerFloatSlot45, zero: "q6urq6urq6s=", one: "2turq6urq6s=", some: "a21cCml6DRI="}, - {name: "slot48", fn: encodeCurrentCastleLowerFloatSlot48, zero: "AAAAAAAAAAA=", one: "LYIAAAAAAAA=", some: "Bld6r/UuUGw="}, - {name: "slot50", fn: encodeCurrentCastleLowerFloatSlot50, zero: "WVlZWVlZWVk=", one: "IslZWVlZWVk=", some: "mQtckjfCK6o="}, - {name: "slot51", fn: encodeCurrentCastleLowerFloatSlot51, zero: "GRkZGRkZGRk=", one: "9J4ZGRkZGRk=", some: "G8uhcinzyrU="}, - {name: "slot52", fn: encodeCurrentCastleLowerFloatSlot52, zero: "AAAAAAAAAAA=", one: "RVAAAAAAAAA=", some: "wCovlX5lSi0="}, - {name: "slot54", fn: encodeCurrentCastleLowerFloatSlot54, zero: "xcXFxcXFxcU=", one: "mMbFxcXFxcU=", some: "wal4mWGSoxw="}, - {name: "slot55", fn: encodeCurrentCastleLowerFloatSlot55, zero: "AAAAAAAAAAA=", one: "3qQAAAAAAAA=", some: "hZuQfR4X06M="}, - {name: "slot57", fn: encodeCurrentCastleLowerFloatSlot57, zero: "sLCwsLCwsLA=", one: "jVCwsLCwsLA=", some: "0wceiszNpN0="}, - {name: "slot58", fn: encodeCurrentCastleLowerFloatSlot58, zero: "XFxcXFxcXFw=", one: "UNxcXFxcXFw=", some: "Wk1H0G9VShY="}, - {name: "slot59", fn: encodeCurrentCastleLowerFloatSlot59, zero: "6urq6urq6uo=", one: "BGrq6urq6uo=", some: "7PwRgxsH/0Y="}, - {name: "slot61", fn: encodeCurrentCastleLowerFloatSlot61, zero: "UFBQUFBQUFA=", one: "6VlQUFBQUFA=", some: "XHUL4r/jf2I="}, - {name: "slot62", fn: encodeCurrentCastleLowerFloatSlot62, zero: "3d3d3d3d3d0=", one: "yI3d3d3d3d0=", some: "HVdKuNNod4A="}, - {name: "slot63", fn: encodeCurrentCastleLowerFloatSlot63, zero: "a2tra2tra2s=", one: "cetra2tra2s=", some: "aX6C8Zh0fbc="}, - {name: "slot65", fn: encodeCurrentCastleLowerFloatSlot65, zero: "g4ODg4ODg4M=", one: "gLODg4ODg4M=", some: "w+UqsLFgxdg="}, - {name: "slot66", fn: encodeCurrentCastleLowerFloatSlot66, zero: "np6enp6enp4=", one: "da6enp6enp4=", some: "3jBfxVxVEN0="}, - {name: "slot69", fn: encodeCurrentCastleLowerFloatSlot69, zero: "gICAgICAgIA=", one: "xqSAgICAgIA=", some: "sBVL6j7uPeg="}, - {name: "slot70", fn: encodeCurrentCastleLowerFloatSlot70, zero: "ycnJycnJyck=", one: "dlHJycnJyck=", some: "6eYnfsQGthI="}, - {name: "slot74", fn: encodeCurrentCastleLowerFloatSlot74, zero: "n5+fn5+fn58=", one: "Du+fn5+fn58=", some: "X53MHuHufVY="}, - {name: "slot76", fn: encodeCurrentCastleLowerFloatSlot76, zero: "jY2NjY2NjY0=", one: "IEWNjY2NjY0=", some: "bXCvGJLQYAQ="}, - {name: "slot80", fn: encodeCurrentCastleLowerFloatSlot80, zero: "8PDw8PDw8PA=", one: "IzDw8PDw8PA=", some: "39Y8Y/eBRTI="}, - {name: "slot81", fn: encodeCurrentCastleLowerFloatSlot81, zero: "OTk5OTk5OTk=", one: "6CU5OTk5OTk=", some: "KaJE1KvQitY="}, - {name: "slot82", fn: encodeCurrentCastleLowerFloatSlot82, zero: "c3Nzc3Nzc3M=", one: "Tplzc3Nzc3M=", some: "q7pqtAJy/qk="}, - {name: "slot84", fn: encodeCurrentCastleLowerFloatSlot84, zero: "+/v7+/v7+/s=", one: "t6L7+/v7+/s=", some: "8D4UQjEHjYw="}, - {name: "slot85", fn: encodeCurrentCastleLowerFloatSlot85, zero: "a2tra2tra2s=", one: "BJ5ra2tra2s=", some: "SdmlhBu30KI="}, - {name: "slot88", fn: encodeCurrentCastleLowerFloatSlot88, zero: "wsLCwsLCwsI=", one: "Q6rCwsLCwsI=", some: "4tMUS7Xzg2c="}, - {name: "slot91", fn: encodeCurrentCastleLowerFloatSlot91, zero: "FRUVFRUVFRU=", one: "ZBoVFRUVFRU=", some: "Eb0Jaf5mv+o="}, - {name: "slot93", fn: encodeCurrentCastleLowerFloatSlot93, zero: "XV1dXV1dXV0=", one: "I7xdXV1dXV0=", some: "3eHmA2liILM="}, - {name: "slot94", fn: encodeCurrentCastleLowerFloatSlot94, zero: "V1dXV1dXV1c=", one: "mEdXV1dXV1c=", some: "l3nyiDW4GdA="}, - {name: "slot95", fn: encodeCurrentCastleLowerFloatSlot95, zero: "GxsbGxsbGxs=", one: "9usbGxsbGxs=", some: "202ohrFWLf4="}, - {name: "slot96", fn: encodeCurrentCastleLowerFloatSlot96, zero: "3Nzc3Nzc3Nw=", one: "3iPc3Nzc3Nw=", some: "XJi5/hC9/04="}, - {name: "slot97", fn: encodeCurrentCastleLowerFloatSlot97, zero: "goKCgoKCgoI=", one: "gfKCgoKCgoI=", some: "wmg3MdQhiPk="}, - {name: "slot101", fn: encodeCurrentCastleLowerFloatSlot101, zero: "uLi4uLi4uLg=", one: "Xhm4uLi4uLg=", some: "OcTqvtwehA4="}, - {name: "slot105", fn: encodeCurrentCastleLowerFloatSlot105, zero: "AAAAAAAAAAA=", one: "1iwAAAAAAAA=", some: "EIVswpDOvdw="}, - {name: "slot107", fn: encodeCurrentCastleLowerFloatSlot107, zero: "LCwsLCwsLCw=", one: "21wsLCwsLCw=", some: "bOIpS0Z7AsM="}, - {name: "slot110", fn: encodeCurrentCastleLowerFloatSlot110, zero: "2tra2tra2to=", one: "1Yra2tra2to=", some: "GjBrhdy1EO0="}, - {name: "slot111", fn: encodeCurrentCastleLowerFloatSlot111, zero: "g4ODg4ODg4M=", one: "9sKDg4ODg4M=", some: "guhfNrt3aZU="}, - {name: "slot112", fn: encodeCurrentCastleLowerFloatSlot112, zero: "AAAAAAAAAAA=", one: "l3AAAAAAAAA=", some: "QA7lByr3bo8="}, - {name: "slot114", fn: encodeCurrentCastleLowerFloatSlot114, zero: "aWlpaWlpaWk=", one: "hWVpaWlpaWk=", some: "edgHkf2NwIs="}, - {name: "slot117", fn: encodeCurrentCastleLowerFloatSlot117, zero: "AwMDAwMDAwM=", one: "vAgDAwMDAwM=", some: "B2YauSO+YDI="}, - {name: "slot118", fn: encodeCurrentCastleLowerFloatSlot118, zero: "fX19fX19fX0=", one: "Ak19fX19fX0=", some: "PSOgEmfig8o="}, - {name: "slot122", fn: encodeCurrentCastleLowerFloatSlot122, zero: "Q0NDQ0NDQ0M=", one: "K0FDQ0NDQ0M=", some: "Sw/+KYY3GzI="}, - {name: "slot124", fn: encodeCurrentCastleLowerFloatSlot124, zero: "CAgICAgICAg=", one: "lckICAgICAg=", some: "tAObZoe/LdE="}, - {name: "slot125", fn: encodeCurrentCastleLowerFloatSlot125, zero: "KioqKioqKio=", one: "qAsqKioqKio=", some: "qu7tiGZprzg="}, - {name: "slot126", fn: encodeCurrentCastleLowerFloatSlot126, zero: "ra2tra2tra0=", one: "9B2tra2tra0=", some: "bbuS5NdUG7w="}, - {name: "slot129", fn: encodeCurrentCastleLowerFloatSlot129, zero: "FxcXFxcXFxc=", one: "dgcXFxcXFxc=", some: "V7VUZvHWFa4="}, - {name: "slot131", fn: encodeCurrentCastleLowerFloatSlot131, zero: "PDw8PDw8PDw=", one: "8wA8PDw8PDw=", some: "LKtL97rbk+E="}, - {name: "slot132", fn: encodeCurrentCastleLowerFloatSlot132, zero: "W1tbW1tbW1s=", one: "QstbW1tbW1s=", some: "mw108qHirfo="}, - {name: "slot134", fn: encodeCurrentCastleLowerFloatSlot134, zero: "xcXFxcXFxcU=", one: "5NXFxcXFxcU=", some: "BaeG9OMExyw="}, - {name: "slot136", fn: encodeCurrentCastleLowerFloatSlot136, zero: "hYWFhYWFhYU=", one: "TYOFhYWFhYU=", some: "bTEha8hZPVQ="}, - {name: "slot138", fn: encodeCurrentCastleLowerFloatSlot138, zero: "Y2NjY2NjY2M=", one: "9BNjY2NjY2M=", some: "IzkKpDUU2dw="}, - {name: "slot139", fn: encodeCurrentCastleLowerFloatSlot139, zero: "j4+Pj4+Pj48=", one: "KP+Pj4+Pj48=", some: "TyH6mEUIASA="}, - {name: "slot140", fn: encodeCurrentCastleLowerFloatSlot140, zero: "eHh4eHh4eHg=", one: "rch4eHh4eHg=", some: "uIKHfZZNIiU="}, - {name: "slot141", fn: encodeCurrentCastleLowerFloatSlot141, zero: "UFBQUFBQUFA=", one: "TThQUFBQUFA=", some: "MJ0aVbf9DSE="}, - {name: "slot142", fn: encodeCurrentCastleLowerFloatSlot142, zero: "rq6urq6urq4=", one: "LY6urq6urq4=", some: "L2toDeLtKp0="}, - {name: "slot143", fn: encodeCurrentCastleLowerFloatSlot143, zero: "SUlJSUlJSUk=", one: "/uNJSUlJSUk=", some: "ISp6+NLiDlM="}, - {name: "slot144", fn: encodeCurrentCastleLowerFloatSlot144, zero: "U1NTU1NTU1M=", one: "6HtTU1NTU1M=", some: "sxgNkCL4KBw="}, - {name: "slot145", fn: encodeCurrentCastleLowerFloatSlot145, zero: "p6enp6enp6c=", one: "Rp+np6enp6c=", some: "x9YVPrR2BmI="}, - {name: "slot146", fn: encodeCurrentCastleLowerFloatSlot146, zero: "Dw8PDw8PDw8=", one: "vh8PDw8PDw8=", some: "T418ToneLXY="}, - {name: "slot148", fn: encodeCurrentCastleLowerFloatSlot148, zero: "2NjY2NjY2Ng=", one: "pt/Y2NjY2Ng=", some: "WLSpBvyNm/Y="}, - {name: "slot151", fn: encodeCurrentCastleLowerFloatSlot151, zero: "6urq6urq6uo=", one: "V/Dq6urq6uo=", some: "8qUOXSdLuUg="}, - {name: "slot153", fn: encodeCurrentCastleLowerFloatSlot153, zero: "eHh4eHh4eHg=", one: "C1l4eHh4eHg=", some: "+V1u6hXKHPo="}, - {name: "slot155", fn: encodeCurrentCastleLowerFloatSlot155, zero: "UlJSUlJSUlI=", one: "mNZSUlJSUlI=", some: "VMQNHeGdwV0="}, - {name: "slot157", fn: encodeCurrentCastleLowerFloatSlot157, zero: "GRkZGRkZGRk=", one: "tAsZGRkZGRk=", some: "IeBwtlioxLs="}, - {name: "slot158", fn: encodeCurrentCastleLowerFloatSlot158, zero: "1dXV1dXV1dU=", one: "/iXV1dXV1dU=", some: "FavEzjceS6Y="}, - {name: "slot160", fn: encodeCurrentCastleLowerFloatSlot160, zero: "MDAwMDAwMDA=", one: "LfMwMDAwMDA=", some: "Mamn7JiuKw4="}, - {name: "slot162", fn: encodeCurrentCastleLowerFloatSlot162, zero: "Nzc3Nzc3Nzc=", one: "ysM3Nzc3Nzc=", some: "B+JCBgXyqnA="}, - {name: "slot163", fn: encodeCurrentCastleLowerFloatSlot163, zero: "ampqampqamo=", one: "tFtqampqamo=", some: "6hZB1B6F5yQ="}, - {name: "slot164", fn: encodeCurrentCastleLowerFloatSlot164, zero: "DQ0NDQ0NDQ0=", one: "fFYNDQ0NDQ0=", some: "Cemne6vEMvs="}, - {name: "slot165", fn: encodeCurrentCastleLowerFloatSlot165, zero: "ycnJycnJyck=", one: "lvnJycnJyck=", some: "CUdUhkM2504="}, - {name: "slot166", fn: encodeCurrentCastleLowerFloatSlot166, zero: "tLS0tLS0tLQ=", one: "SHe0tLS0tLQ=", some: "tc3DCNzKTmk="}, - {name: "slot167", fn: encodeCurrentCastleLowerFloatSlot167, zero: "Pz8/Pz8/Pz8=", one: "5q8/Pz8/Pz8=", some: "f21UVslGzV4="}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - if got := tc.fn(0); got != tc.zero { - t.Fatalf("%s(0) = %q, want %q", tc.name, got, tc.zero) - } - if got := tc.fn(1); got != tc.one { - t.Fatalf("%s(1) = %q, want %q", tc.name, got, tc.one) - } - if got := tc.fn(123.456); got != tc.some { - t.Fatalf("%s(123.456) = %q, want %q", tc.name, got, tc.some) - } - }) - } -} - -func TestCurrentCastleSlot6Hash(t *testing.T) { - cases := []struct { - input string - hash uint32 - hex string - }{ - {input: "", hash: 0, hex: "0"}, - {input: "abc", hash: 1712826425, hex: "6617a839"}, - {input: "X Castle", hash: 2975336394, hex: "b15807ca"}, - {input: "function test() { return 1; }", hash: 1625178382, hex: "60de410e"}, - } - for _, tc := range cases { - t.Run(tc.input, func(t *testing.T) { - if got := currentCastleSlot6Hash(tc.input); got != tc.hash { - t.Fatalf("currentCastleSlot6Hash() = %d/%#x, want %d/%#x", got, got, tc.hash, tc.hash) - } - if got := currentCastleSlot6HashHex(tc.input); got != tc.hex { - t.Fatalf("currentCastleSlot6HashHex() = %q, want %q", got, tc.hex) - } - }) - } - if got, want := encodeCurrentCastleSlot6Component5Fallback(currentCastleSlot6HashHex("abc")), "vb24umi7trA="; got != want { - t.Fatalf("encoded component-5 hash = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6AH(currentCastleSlot6HashHex("abc")), "urpamtS5Gpk="; got != want { - t.Fatalf("encoded aH hash = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6EJ(currentCastleSlot6HashHex("abc")), "RERKRupYTlo="; got != want { - t.Fatalf("encoded ej hash = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot6A8(currentCastleSlot6HashHex("abc")), "Dw//H/Jv338="; got != want { - t.Fatalf("encoded a8 hash = %q, want %q", got, want) - } -} - -func TestCurrentCastleSlot6AdditionalByteTransforms(t *testing.T) { - cases := []struct { - name string - got string - want string - }{ - {name: "iT abc", got: encodeCurrentCastleSlot6IT("abc"), want: "fjss"}, - {name: "iT x castle", got: encodeCurrentCastleSlot6IT("X Castle"), want: "dihefhEbtHM="}, - {name: "aG abc", got: encodeCurrentCastleSlot6AG("abc"), want: "UO0O"}, - {name: "aG x castle", got: encodeCurrentCastleSlot6AG("X Castle"), want: "J+/uUB67s8w="}, - {name: "aD abc", got: encodeCurrentCastleSlot6AD("abc"), want: "4jKC"}, - {name: "aD x castle", got: encodeCurrentCastleSlot6AD("X Castle"), want: "p+as4u290qI="}, - {name: "t7 abc", got: encodeCurrentCastleSlot6T7("abc"), want: "LcW9"}, - {name: "t7 x castle", got: encodeCurrentCastleSlot6T7("X Castle"), want: "a0W1LblQVgw="}, - {name: "id abc", got: encodeCurrentCastleSlot6ID("abc"), want: "1vYX"}, - {name: "id x castle", got: encodeCurrentCastleSlot6ID("X Castle"), want: "ta4T1hk5OFc="}, - {name: "fu abc", got: encodeCurrentCastleSlot6FU("abc"), want: "JLSE"}, - {name: "fu x castle", got: encodeCurrentCastleSlot6FU("X Castle"), want: "V2WMJIBQUiM="}, - {name: "ej abc", got: encodeCurrentCastleSlot6EJ("abc"), want: "6uzu"}, - {name: "ej x castle", got: encodeCurrentCastleSlot6EJ("X Castle"), want: "mGiu6s7A8OI="}, - {name: "l9 abc", got: encodeCurrentCastleSlot6L9("abc"), want: "bFxM"}, - {name: "l9 x castle", got: encodeCurrentCastleSlot6L9("X Castle"), want: "/wBObE29PKw="}, - {name: "tx abc", got: encodeCurrentCastleSlot6TX("abc"), want: "uLm+"}, - {name: "tx x castle", got: encodeCurrentCastleSlot6TX("X Castle"), want: "o/veuI6Pt7w="}, - {name: "li abc", got: encodeCurrentCastleSlot6LI("abc"), want: "h4mL"}, - {name: "li x castle", got: encodeCurrentCastleSlot6LI("X Castle"), want: "dQVLh6utnY8="}, - {name: "fL abc", got: encodeCurrentCastleSlot6FL("abc"), want: "B4YG"}, - {name: "fL x castle", got: encodeCurrentCastleSlot6FL("X Castle"), want: "g2cWBw6xjQk="}, - {name: "iO abc", got: encodeCurrentCastleSlot6IO("abc"), want: "LTQ/"}, - {name: "iO x castle", got: encodeCurrentCastleSlot6IO("X Castle"), want: "cuofLa+2/gk="}, - {name: "f3 abc", got: encodeCurrentCastleSlot6F3("abc"), want: "HEqw"}, - {name: "f3 x castle", got: encodeCurrentCastleSlot6F3("X Castle"), want: "pCK6HLPpaNQ="}, - {name: "aj abc", got: encodeCurrentCastleSlot6AJ("abc"), want: "u24h"}, - {name: "aj x castle", got: encodeCurrentCastleSlot6AJ("X Castle"), want: "ZwSiu2AXdYo="}, - {name: "component17 abc", got: encodeCurrentCastleSlot6Component17("abc"), want: "np+g"}, - {name: "component17 x castle", got: encodeCurrentCastleSlot6Component17("X Castle"), want: "pd3AnpCJkZo="}, - {name: "component19 abc", got: encodeCurrentCastleSlot6Component19("abc"), want: "Wt9k"}, - {name: "component19 x castle", got: encodeCurrentCastleSlot6Component19("X Castle"), want: "rZXEWrQ5EW4="}, - {name: "component27 abc", got: encodeCurrentCastleSlot6Component27("abc"), want: "hYmN"}, - {name: "component27 x castle", got: encodeCurrentCastleSlot6Component27("X Castle"), want: "YYANhc3RsZU="}, - {name: "tm abc", got: encodeCurrentCastleSlot6TM("abc"), want: "kYmB"}, - {name: "tm x castle", got: encodeCurrentCastleSlot6TM("X Castle"), want: "WJuAkQE5+bE="}, - {name: "tF abc", got: encodeCurrentCastleSlot6TUpperF("abc"), want: "veDf"}, - {name: "tF x castle", got: encodeCurrentCastleSlot6TUpperF("X Castle"), want: "tn6/vc/S6uE="}, - {name: "fs abc", got: encodeCurrentCastleSlot6FS("abc"), want: "ZZj/"}, - {name: "fs x castle", got: encodeCurrentCastleSlot6FS("X Castle"), want: "KB7XZcsvCXY="}, - {name: "component31 abc", got: encodeCurrentCastleSlot6Component31("abc"), want: "s7W3"}, - {name: "component31 x castle", got: encodeCurrentCastleSlot6Component31("X Castle"), want: "oTF3s9fZybs="}, - {name: "fF abc", got: encodeCurrentCastleSlot6FF("abc"), want: "+J1B"}, - {name: "fF x castle", got: encodeCurrentCastleSlot6FF("X Castle"), want: "sLQx+Amuigo="}, - {name: "aU abc", got: encodeCurrentCastleSlot6AU("abc"), want: "C7N6"}, - {name: "aU x castle", got: encodeCurrentCastleSlot6AU("X Castle"), want: "mPwKC2Kq5mk="}, - {name: "aQ abc", got: encodeCurrentCastleSlot6AQ("abc"), want: "eOdT"}, - {name: "aQ x castle", got: encodeCurrentCastleSlot6AQ("X Castle"), want: "bA3ReJMff0o="}, - {name: "lW abc", got: encodeCurrentCastleSlot6LW("abc"), want: "45xV"}, - {name: "lW x castle", got: encodeCurrentCastleSlot6LW("X Castle"), want: "Yuo14+We1sc="}, - {name: "tM abc", got: encodeCurrentCastleSlot6TUpperM("abc"), want: "UGN0"}, - {name: "tM x castle", got: encodeCurrentCastleSlot6TUpperM("X Castle"), want: "rr40UJWmFpg="}, - {name: "th abc", got: encodeCurrentCastleSlot6TH("abc"), want: "lKS0"}, - {name: "th x castle", got: encodeCurrentCastleSlot6TH("X Castle"), want: "BICylLXFRdQ="}, - {name: "is abc", got: encodeCurrentCastleSlot6IS("abc"), want: "xtYm"}, - {name: "is x castle", got: encodeCurrentCastleSlot6IS("X Castle"), want: "cfIgxic3twY="}, - {name: "a8 abc", got: encodeCurrentCastleSlot6A8("abc"), want: "8sLS"}, - {name: "a8 x castle", got: encodeCurrentCastleSlot6A8("X Castle"), want: "Ye7Q8tMjozI="}, - {name: "lM abc", got: encodeCurrentCastleSlot6LUpperM("abc"), want: "w0zV"}, - {name: "lM x castle", got: encodeCurrentCastleSlot6LUpperM("X Castle"), want: "8vq1w2Xupuc="}, - {name: "eA abc", got: encodeCurrentCastleSlot6EA("abc"), want: "IRsd"}, - {name: "eA x castle", got: encodeCurrentCastleSlot6EA("X Castle"), want: "L55dIfz2Bxk="}, - {name: "e3 abc", got: encodeCurrentCastleSlot6E3("abc"), want: "qwBV"}, - {name: "e3 x castle", got: encodeCurrentCastleSlot6E3("X Castle"), want: "Bh61qyV6Avc="}, - {name: "t2 abc", got: encodeCurrentCastleSlot6T2("abc"), want: "CNeS"}, - {name: "t2 x castle", got: encodeCurrentCastleSlot6T2("X Castle"), want: "1Q3yCOKxORw="}, - {name: "tX abc", got: encodeCurrentCastleSlot6TUpperX("abc"), want: "Bh4W"}, - {name: "tX x castle", got: encodeCurrentCastleSlot6TUpperX("X Castle"), want: "zwwXBpaubiY="}, - {name: "l5 abc", got: encodeCurrentCastleSlot6L5("abc"), want: "WKr8"}, - {name: "l5 x castle", got: encodeCurrentCastleSlot6L5("X Castle"), want: "doa8WBxu3qA="}, - {name: "component46 abc", got: encodeCurrentCastleSlot6Component46("abc"), want: "fX5/"}, - {name: "component46 x castle", got: encodeCurrentCastleSlot6Component46("X Castle"), want: "ZLxffY+QeIE="}, - {name: "tz abc", got: encodeCurrentCastleSlot6TZ("abc"), want: "cFQ4"}, - {name: "tz x castle", got: encodeCurrentCastleSlot6TZ("X Castle"), want: "ioM0cDoeHeA="}, - {name: "tP abc", got: encodeCurrentCastleSlot6TP("abc"), want: "+WTP"}, - {name: "tP x castle", got: encodeCurrentCastleSlot6TP("X Castle"), want: "Ns5v+X/qkqU="}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - if tc.got != tc.want { - t.Fatalf("got %q, want %q", tc.got, tc.want) - } - }) - } -} - -func TestPopulateCurrentCastleHighTimingSlots(t *testing.T) { - payload := make([]any, currentCastlePayloadSlots) - if err := populateCurrentCastleHighTimingSlots(payload, map[int]float64{ - 431: 0, - 480: 123.456, - 493: 1, - }); err != nil { - t.Fatalf("populateCurrentCastleHighTimingSlots() error = %v", err) - } - for slot := 431; slot <= 493; slot++ { - if payload[slot] == nil { - t.Fatalf("payload[%d] is nil", slot) - } - } - if got, want := payload[431], "xsbGxsbGxsY="; got != want { - t.Fatalf("payload[431] = %q, want %q", got, want) - } - if got, want := payload[442], "hoZgICDGxsY="; got != want { - t.Fatalf("payload[442] = %q, want %q", got, want) - } - if got, want := payload[480], "hhjbKVyZ+PE="; got != want { - t.Fatalf("payload[480] = %q, want %q", got, want) - } - if got, want := payload[493], "OTbGxsbGxsY="; got != want { - t.Fatalf("payload[493] = %q, want %q", got, want) - } - if err := populateCurrentCastleHighTimingSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { - t.Fatalf("populateCurrentCastleHighTimingSlots() with short payload succeeded") - } -} - -func TestPopulateCurrentCastleLowerFloatSlots(t *testing.T) { - payload := newCurrentCastlePayloadScaffold() - if err := populateCurrentCastleLowerFloatSlots(payload, map[int]float64{ - 1: 1, - 3: 123.456, - 19: 1, - }); err != nil { - t.Fatalf("populateCurrentCastleLowerFloatSlots() error = %v", err) - } - if got, want := payload[1], "KWfg4ODg4OA="; got != want { - t.Fatalf("payload[1] = %q, want %q", got, want) - } - if got, want := payload[3], "agkBHM1lE+c="; got != want { - t.Fatalf("payload[3] = %q, want %q", got, want) - } - if got, want := payload[4], "7Ozs7Ozs7Ow="; got != want { - t.Fatalf("payload[4] = %q, want %q", got, want) - } - if got, want := payload[19], "4ZQAAAAAAAA="; got != want { - t.Fatalf("payload[19] = %q, want %q", got, want) - } - if got, want := payload[21], "1dXV1dXV1dU="; got != want { - t.Fatalf("payload[21] = %q, want %q", got, want) - } - if got, want := payload[39], "AAAAAAAAAAA="; got != want { - t.Fatalf("payload[39] = %q, want %q", got, want) - } - if got, want := payload[43], "q6urq6urq6s="; got != want { - t.Fatalf("payload[43] = %q, want %q", got, want) - } - if got, want := payload[63], "a2tra2tra2s="; got != want { - t.Fatalf("payload[63] = %q, want %q", got, want) - } - if got, want := payload[65], "g4ODg4ODg4M="; got != want { - t.Fatalf("payload[65] = %q, want %q", got, want) - } - if got, want := payload[107], "LCwsLCwsLCw="; got != want { - t.Fatalf("payload[107] = %q, want %q", got, want) - } - if got, want := payload[167], "Pz8/Pz8/Pz8="; got != want { - t.Fatalf("payload[167] = %q, want %q", got, want) - } - if got, want := payload[169], "ioqKioqKioo="; got != want { - t.Fatalf("payload[169] = %q, want %q", got, want) - } - if got, want := payload[226], "o6Ojo6Ojo6M="; got != want { - t.Fatalf("payload[226] = %q, want %q", got, want) - } - if got, want := payload[255], "MjIyMjIyMjI="; got != want { - t.Fatalf("payload[255] = %q, want %q", got, want) - } - if got, want := payload[294], "NDQ0NDQ0NDQ="; got != want { - t.Fatalf("payload[294] = %q, want %q", got, want) - } - if got, want := payload[291], "hoaGhoaGhoY="; got != want { - t.Fatalf("payload[291] = %q, want %q", got, want) - } - if got, want := payload[324], "WlpaWlpaWlo="; got != want { - t.Fatalf("payload[324] = %q, want %q", got, want) - } - if got, want := payload[325], "GhoaGhoaGho="; got != want { - t.Fatalf("payload[325] = %q, want %q", got, want) - } - if got, want := payload[373], "pKSkpKSkpKQ="; got != want { - t.Fatalf("payload[373] = %q, want %q", got, want) - } - if got, want := payload[407], "2tra2tra2to="; got != want { - t.Fatalf("payload[407] = %q, want %q", got, want) - } - if got, want := payload[427], "h4eHh4eHh4c="; got != want { - t.Fatalf("payload[427] = %q, want %q", got, want) - } - for slot := range currentCastleLowerFloatSlotEncoders { - if _, ok := payload[slot].(string); !ok || payload[slot] == "" { - t.Fatalf("payload[%d] was not populated with a non-empty string: %#v", slot, payload[slot]) - } - } - if err := populateCurrentCastleLowerFloatSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { - t.Fatalf("populateCurrentCastleLowerFloatSlots() with short payload succeeded") - } -} - -func TestCurrentCastleNumericSlotEncoders(t *testing.T) { - if got, want := currentCastleIndexEUnderscore(0), uint32(0x03508000); got != want { - t.Fatalf("currentCastleIndexEUnderscore(0) = %#x, want %#x", got, want) - } - if got, want := currentCastleIndexR7(currentCastleIndexEUnderscore(0)), uint32(0); got != want { - t.Fatalf("currentCastleIndexR7(currentCastleIndexEUnderscore(0)) = %#x, want %#x", got, want) - } - - simpleCases := []struct { - name string - fn func(uint32) uint32 - zero uint32 - one uint32 - }{ - {name: "slot20", fn: encodeCurrentCastleNumericSlot20, zero: 0xd2c1284a, one: 0x5fd1c84a}, - {name: "slot154", fn: encodeCurrentCastleNumericSlot154, zero: 0x8eca32a4, one: 0xf13ce181}, - {name: "slot159", fn: encodeCurrentCastleNumericIdentity, zero: 0, one: 1}, - {name: "slot182", fn: currentCastleTB, zero: 0x00000354, one: 0x000003df}, - {name: "slot316", fn: currentCastleIndexEUnderscore, zero: 0x03508000, one: 0x0350a000}, - {name: "slot256", fn: encodeCurrentCastleNumericSlot256, zero: 0xe39b0b56, one: 0x4ea7245b}, - {name: "slot348", fn: encodeCurrentCastleNumericSlot348, zero: 0x54297e0d, one: 0x4ff13582}, - } - for _, tc := range simpleCases { - if got := tc.fn(0); got != tc.zero { - t.Fatalf("%s(0) = %#x, want %#x", tc.name, got, tc.zero) - } - if got := tc.fn(1); got != tc.one { - t.Fatalf("%s(1) = %#x, want %#x", tc.name, got, tc.one) - } - } - affineCases := []struct { - slot int - zero uint32 - one uint32 - }{ - {slot: 26, zero: 0x60bd09b2, one: 0x670022bf}, - {slot: 27, zero: 0xb330012c, one: 0x0dd8798d}, - {slot: 28, zero: 0x244dce95, one: 0x6dfeaec2}, - {slot: 31, zero: 0xb78dc961, one: 0xaae15d40}, - {slot: 42, zero: 0x9e224c69, one: 0x944782d0}, - {slot: 49, zero: 0x06d9e43d, one: 0x6a5b8e70}, - {slot: 53, zero: 0x8dd8d0c3, one: 0x27b19724}, - {slot: 56, zero: 0xd9931d51, one: 0xacc004ba}, - {slot: 64, zero: 0xc538ce0b, one: 0x9171777e}, - {slot: 67, zero: 0xf3c0363d, one: 0x4080d6d0}, - {slot: 68, zero: 0xecbd981e, one: 0x76dc5125}, - {slot: 71, zero: 0x677c239f, one: 0xce58e7fa}, - {slot: 73, zero: 0x96d91174, one: 0xb09c39df}, - {slot: 87, zero: 0xaa10f53a, one: 0xeca6e7eb}, - {slot: 98, zero: 0x95bd552c, one: 0xb0a64635}, - {slot: 102, zero: 0x4bcca730, one: 0x52157915}, - {slot: 106, zero: 0xb5f76f6d, one: 0xb2bb1c70}, - {slot: 115, zero: 0xf041ff29, one: 0x9577f8e2}, - {slot: 116, zero: 0x8c84e89a, one: 0x3dfbe269}, - {slot: 121, zero: 0xdbab50b2, one: 0x0fb7ef7f}, - {slot: 123, zero: 0x89d7f1b8, one: 0x7712f7cf}, - {slot: 133, zero: 0x520795d3, one: 0x61439c1a}, - {slot: 147, zero: 0xcd6c2b6c, one: 0x6d80ef95}, - {slot: 149, zero: 0x3b4d9896, one: 0xf4921071}, - {slot: 150, zero: 0x1e187f66, one: 0x5ba191cb}, - {slot: 152, zero: 0xf83df829, one: 0x0160726c}, - {slot: 168, zero: 0x4d504e4f, one: 0xef51b58e}, - {slot: 194, zero: 0xc1cb353f, one: 0xed628648}, - {slot: 196, zero: 0x6d935c25, one: 0x6ddfc840}, - {slot: 198, zero: 0x1a205ab2, one: 0xe350ecf9}, - {slot: 203, zero: 0x45c65c75, one: 0xc14d992a}, - {slot: 205, zero: 0x46fd4c27, one: 0x67a5337e}, - {slot: 206, zero: 0x086aa955, one: 0x40c6c596}, - {slot: 207, zero: 0x2496e7c2, one: 0x0f608701}, - {slot: 209, zero: 0xfed3363f, one: 0x7feea612}, - {slot: 212, zero: 0x2c3ea5b3, one: 0x665c3ee0}, - {slot: 221, zero: 0xfd5c8336, one: 0x9f7dec27}, - {slot: 223, zero: 0xdeb6d52a, one: 0xcb0594c1}, - {slot: 224, zero: 0xa9cd6a67, one: 0x5dbe5d9a}, - {slot: 228, zero: 0xa7e0384c, one: 0x56e8784d}, - {slot: 240, zero: 0xf4661fb5, one: 0x11924198}, - {slot: 246, zero: 0x3c8624df, one: 0x61388f30}, - {slot: 247, zero: 0xed6822f5, one: 0xea4dcacc}, - {slot: 257, zero: 0xac01bfae, one: 0x0e46a163}, - {slot: 258, zero: 0x3d7667d5, one: 0xceb0120a}, - {slot: 260, zero: 0x2590de1a, one: 0xa8853377}, - {slot: 266, zero: 0xa3c420cf, one: 0xa4ee6d12}, - {slot: 267, zero: 0x2b30156a, one: 0xad2cd039}, - {slot: 277, zero: 0xb939ef29, one: 0x4aadc51e}, - {slot: 288, zero: 0x4d25ec39, one: 0x7006f918}, - {slot: 293, zero: 0x9f2c27a4, one: 0x614ef36f}, - {slot: 297, zero: 0x8127b4d5, one: 0x2d162ffa}, - {slot: 311, zero: 0x42f7a6bc, one: 0xf5810f03}, - {slot: 312, zero: 0xca4c0beb, one: 0x178b944c}, - {slot: 318, zero: 0x0fc46406, one: 0xb618188b}, - {slot: 321, zero: 0xb7481870, one: 0x746558bb}, - {slot: 329, zero: 0x828e6d80, one: 0x7dde71e5}, - {slot: 334, zero: 0x79a7bbd2, one: 0x2c829589}, - {slot: 339, zero: 0xfd1e66f3, one: 0x71de90e4}, - {slot: 349, zero: 0xcb462199, one: 0x4d17f152}, - {slot: 354, zero: 0x471a0d4d, one: 0xed05f1de}, - {slot: 356, zero: 0xbdfe52d9, one: 0x32782ba6}, - {slot: 359, zero: 0xa583bc26, one: 0x1699b055}, - {slot: 366, zero: 0xa9ac1690, one: 0xc211c04b}, - {slot: 369, zero: 0xc33c50e9, one: 0x4b6d07f4}, - {slot: 374, zero: 0xe6b394f2, one: 0x5ed52a3b}, - {slot: 375, zero: 0xcf565231, one: 0x4d737d2a}, - {slot: 376, zero: 0x30b3a87e, one: 0xf233f3b7}, - {slot: 386, zero: 0xd1ee53e2, one: 0x78b37579}, - {slot: 388, zero: 0x6cac25da, one: 0xb5035037}, - {slot: 390, zero: 0x9bc2f748, one: 0x03f0733d}, - {slot: 393, zero: 0xfe9478a7, one: 0x9ed36934}, - {slot: 395, zero: 0xb427bace, one: 0xf991e0ef}, - {slot: 399, zero: 0xd6c6c42c, one: 0x3fc1896b}, - {slot: 406, zero: 0x29fc7252, one: 0x4da110bf}, - {slot: 422, zero: 0xecfc92b2, one: 0x9632461b}, - {slot: 425, zero: 0x8e16c592, one: 0xb2160d79}, - {slot: 430, zero: 0x2b90120f, one: 0x40ec1bb2}, - } - for _, tc := range affineCases { - fn, ok := currentCastleNumericSlotEncoders[tc.slot] - if !ok { - t.Fatalf("missing numeric slot encoder for slot %d", tc.slot) - } - if got := fn(0); got != tc.zero { - t.Fatalf("numeric slot %d raw 0 = %#x, want %#x", tc.slot, got, tc.zero) - } - if got := fn(1); got != tc.one { - t.Fatalf("numeric slot %d raw 1 = %#x, want %#x", tc.slot, got, tc.one) - } - } - if got, want := encodeCurrentCastleNumericSlot20(0xdae88a03), uint32(0x15bbadc1); got != want { - t.Fatalf("encodeCurrentCastleNumericSlot20(sentinel) = %#x, want %#x", got, want) - } - if got, want := encodeCurrentCastleNumericSlot154(0xdae88a03), uint32(0xbd4808ff); got != want { - t.Fatalf("encodeCurrentCastleNumericSlot154(sentinel) = %#x, want %#x", got, want) - } - if got, want := currentCastleTB(8), uint32(0x000007ac); got != want { - t.Fatalf("currentCastleTB(8) = %#x, want %#x", got, want) - } - if got, want := currentCastleNumericSlotEncoders[257](currentCastleTB(0)), uint32(0xb0e99ef2); got != want { - t.Fatalf("numeric slot 257 false default = %#x, want %#x", got, want) - } - if got, want := currentCastleNumericSlotEncoders[257](currentCastleTB(1)), uint32(0x3e5af065); got != want { - t.Fatalf("numeric slot 257 true-like default = %#x, want %#x", got, want) - } - if got, want := currentCastleNumericSlotEncoders[289](currentCastleTB(0)), uint32(0x0001d1f0); got != want { - t.Fatalf("numeric slot 289 false default = %#x, want %#x", got, want) - } - if got, want := currentCastleNumericSlotEncoders[289](currentCastleTB(1)), uint32(0x00021d69); got != want { - t.Fatalf("numeric slot 289 true-like default = %#x, want %#x", got, want) - } - if got, want := currentCastleNumericSlotEncoders[152](0xdae88a03), uint32(0xb8ee54f2); got != want { - t.Fatalf("numeric slot 152 sentinel = %#x, want %#x", got, want) - } - if got, want := currentCastleNumericSlotEncoders[258](0xdae88a03), uint32(0x5c577ba0); got != want { - t.Fatalf("numeric slot 258 sentinel = %#x, want %#x", got, want) - } - sentinelCases := []struct { - slot int - want uint32 - }{ - {slot: 26, want: 0xb8d522a5}, - {slot: 28, want: 0xd800b11c}, - {slot: 31, want: 0x20b7bafe}, - {slot: 42, want: 0xff1f0002}, - {slot: 49, want: 0x34e764d6}, - {slot: 64, want: 0xe3b2c864}, - {slot: 67, want: 0xe83187aa}, - {slot: 68, want: 0x572da517}, - {slot: 71, want: 0x08140144}, - {slot: 87, want: 0x938e374d}, - {slot: 98, want: 0x813ade47}, - {slot: 102, want: 0x919f8edf}, - {slot: 106, want: 0x5338546a}, - {slot: 116, want: 0xde374007}, - {slot: 123, want: 0xa65269fd}, - {slot: 133, want: 0x1cb5eea8}, - {slot: 168, want: 0xfdcef110}, - {slot: 198, want: 0xa79b0e6b}, - {slot: 203, want: 0x884f8094}, - {slot: 207, want: 0x1032cf7f}, - {slot: 209, want: 0xd57543b8}, - {slot: 212, want: 0xa8aa2f3a}, - {slot: 221, want: 0xc1423045}, - {slot: 223, want: 0x510aadef}, - {slot: 224, want: 0x945b9200}, - {slot: 228, want: 0xf62f824b}, - {slot: 240, want: 0xf5609fd2}, - {slot: 246, want: 0x6a5ffdd2}, - {slot: 247, want: 0x52eb007a}, - {slot: 257, want: 0x3f3e4bf9}, - {slot: 266, want: 0xb4e6c28c}, - {slot: 277, want: 0xe1cc5734}, - {slot: 289, want: 0xdc42f2f5}, - {slot: 312, want: 0x4f85ef0e}, - {slot: 316, want: 0x1490fb5d}, - {slot: 318, want: 0x2858fd81}, - {slot: 321, want: 0xd4c04751}, - {slot: 356, want: 0x4f2df80c}, - {slot: 366, want: 0xdfa64ed5}, - {slot: 374, want: 0x6746facd}, - {slot: 375, want: 0xa8e6ed38}, - {slot: 376, want: 0x2e9f1745}, - {slot: 386, want: 0x6185984b}, - {slot: 399, want: 0xc363f8ed}, - {slot: 422, want: 0xaf6c7949}, - {slot: 430, want: 0xe14d50f8}, - } - for _, tc := range sentinelCases { - if got := currentCastleNumericSlotEncoders[tc.slot](0xdae88a03); got != tc.want { - t.Fatalf("numeric slot %d sentinel = %#x, want %#x", tc.slot, got, tc.want) - } - } - - slot341Cases := []struct { - name string - raw uint32 - want uint32 - }{ - {name: "falseFlag", raw: 0, want: 0x0000f6a7}, - {name: "trueFlag", raw: 1, want: 0x0000793c}, - } - for _, tc := range slot341Cases { - if got := encodeCurrentCastleNumericSlot341(tc.raw); got != tc.want { - t.Fatalf("encodeCurrentCastleNumericSlot341(%s) = %#x, want %#x", tc.name, got, tc.want) - } - } - - slot428Cases := []struct { - name string - raw uint32 - want uint32 - }{ - {name: "false", raw: 0, want: 0xeb00e01b}, - {name: "true", raw: 1, want: 0xc0f29076}, - {name: "notFunction", raw: 0xdae88a03, want: 0xc64546ec}, - {name: "undefinedTProtect", raw: 0x7b862786, want: 0xfc5c96bd}, - } - for _, tc := range slot428Cases { - if got := encodeCurrentCastleNumericSlot428(tc.raw); got != tc.want { - t.Fatalf("encodeCurrentCastleNumericSlot428(%s) = %#x, want %#x", tc.name, got, tc.want) - } - } - - slot429Cases := []struct { - name string - raw uint32 - want uint32 - }{ - {name: "huFalseFallback", raw: currentCastleIndexEUnderscore(0), want: 0x14808cbe}, - {name: "huTrueFallback", raw: currentCastleIndexEUnderscore(1), want: 0x7229acbe}, - {name: "zero", raw: 0, want: 0x7bac0cbe}, - {name: "one", raw: 1, want: 0x57fefa07}, - } - for _, tc := range slot429Cases { - if got := encodeCurrentCastleNumericSlot429(tc.raw); got != tc.want { - t.Fatalf("encodeCurrentCastleNumericSlot429(%s) = %#x, want %#x", tc.name, got, tc.want) - } - } -} - -func TestPopulateCurrentCastleNumericSlots(t *testing.T) { - payload := newCurrentCastlePayloadScaffold() - if err := populateCurrentCastleNumericSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleNumericSlots() error = %v", err) - } - for slot, want := range defaultCurrentCastleNumericSlotFinalValues() { - if got := payload[slot]; got != want { - t.Fatalf("payload[%d] = %#v, want %#v", slot, got, want) - } - } - - if err := populateCurrentCastleNumericSlots(payload, map[int]uint32{ - 20: 1, - 26: 1, - 27: 1, - 28: 1, - 31: currentCastleTB(0), - 42: currentCastleIndexEUnderscore(1), - 49: 1, - 64: currentCastleIndexEUnderscore(1), - 67: currentCastleIndexEUnderscore(1), - 68: 1, - 71: currentCastleIndexEUnderscore(1), - 87: currentCastleIndexEUnderscore(1), - 98: 1, - 102: 1, - 106: currentCastleTB(0), - 116: 1, - 121: 1, - 123: currentCastleIndexEUnderscore(1), - 133: currentCastleTB(1), - 152: 1, - 154: 1, - 159: 1, - 168: 1, - 182: 1, - 198: currentCastleTB(1), - 203: 1, - 207: 1, - 209: 1, - 212: 1, - 221: 1, - 223: 1, - 224: currentCastleIndexEUnderscore(1), - 228: currentCastleIndexEUnderscore(1), - 240: 1, - 246: 1, - 247: currentCastleIndexEUnderscore(1), - 256: 1, - 257: currentCastleTB(1), - 258: 1, - 266: 1, - 267: 1, - 277: 1, - 288: 1, - 289: currentCastleTB(1), - 297: 1, - 312: 1, - 316: currentCastleTB(1), - 318: currentCastleIndexEUnderscore(1), - 321: currentCastleIndexEUnderscore(1), - 334: currentCastleTB(1), - 341: 1, - 348: 1, - 356: currentCastleTB(1), - 359: currentCastleTB(1), - 366: 1, - 369: currentCastleIndexEUnderscore(1), - 374: 1, - 375: currentCastleTB(1), - 376: currentCastleIndexEUnderscore(1), - 386: 1, - 399: 1, - 406: 1, - 422: 1, - 425: 1, - 428: 1, - 429: 1, - 430: currentCastleTB(1), - }); err != nil { - t.Fatalf("populateCurrentCastleNumericSlots() with override error = %v", err) - } - if got, want := payload[428], uint32(0xc0f29076); got != want { - t.Fatalf("override payload[428] = %#v, want %#v", got, want) - } - if got, want := payload[429], uint32(0x57fefa07); got != want { - t.Fatalf("override payload[429] = %#v, want %#v", got, want) - } - if got, want := payload[20], uint32(0x5fd1c84a); got != want { - t.Fatalf("override payload[20] = %#v, want %#v", got, want) - } - if got, want := payload[26], uint32(0x670022bf); got != want { - t.Fatalf("override payload[26] = %#v, want %#v", got, want) - } - if got, want := payload[28], uint32(0x6dfeaec2); got != want { - t.Fatalf("override payload[28] = %#v, want %#v", got, want) - } - if got, want := payload[31], uint32(0xce51c22d); got != want { - t.Fatalf("override payload[31] = %#v, want %#v", got, want) - } - if got, want := payload[42], uint32(0x78f2ac69); got != want { - t.Fatalf("override payload[42] = %#v, want %#v", got, want) - } - if got, want := payload[49], uint32(0x6a5b8e70); got != want { - t.Fatalf("override payload[49] = %#v, want %#v", got, want) - } - if got, want := payload[64], uint32(0x7c10ae0b); got != want { - t.Fatalf("override payload[64] = %#v, want %#v", got, want) - } - if got, want := payload[67], uint32(0xce74563d); got != want { - t.Fatalf("override payload[67] = %#v, want %#v", got, want) - } - if got, want := payload[68], uint32(0x76dc5125); got != want { - t.Fatalf("override payload[68] = %#v, want %#v", got, want) - } - if got, want := payload[71], uint32(0xe94a039f); got != want { - t.Fatalf("override payload[71] = %#v, want %#v", got, want) - } - if got, want := payload[87], uint32(0x50e3553a); got != want { - t.Fatalf("override payload[87] = %#v, want %#v", got, want) - } - if got, want := payload[98], uint32(0xb0a64635); got != want { - t.Fatalf("override payload[98] = %#v, want %#v", got, want) - } - if got, want := payload[102], uint32(0x52157915); got != want { - t.Fatalf("override payload[102] = %#v, want %#v", got, want) - } - if got, want := payload[106], uint32(0xdc9222f1); got != want { - t.Fatalf("override payload[106] = %#v, want %#v", got, want) - } - if got, want := payload[116], uint32(0x3dfbe269); got != want { - t.Fatalf("override payload[116] = %#v, want %#v", got, want) - } - if got, want := payload[123], uint32(0x03d991b8); got != want { - t.Fatalf("override payload[123] = %#v, want %#v", got, want) - } - if got, want := payload[133], uint32(0xe032d1a4); got != want { - t.Fatalf("override payload[133] = %#v, want %#v", got, want) - } - if got, want := payload[154], uint32(0xf13ce181); got != want { - t.Fatalf("override payload[154] = %#v, want %#v", got, want) - } - if got, want := payload[212], uint32(0x665c3ee0); got != want { - t.Fatalf("override payload[212] = %#v, want %#v", got, want) - } - if got, want := payload[224], uint32(0x44bd8a67); got != want { - t.Fatalf("override payload[224] = %#v, want %#v", got, want) - } - if got, want := payload[228], uint32(0x830f984c); got != want { - t.Fatalf("override payload[228] = %#v, want %#v", got, want) - } - if got, want := payload[121], uint32(0x0fb7ef7f); got != want { - t.Fatalf("override payload[121] = %#v, want %#v", got, want) - } - if got, want := payload[256], uint32(0x4ea7245b); got != want { - t.Fatalf("override payload[256] = %#v, want %#v", got, want) - } - if got, want := payload[257], uint32(0x3e5af065); got != want { - t.Fatalf("override payload[257] = %#v, want %#v", got, want) - } - if got, want := payload[247], uint32(0xbd08c2f5); got != want { - t.Fatalf("override payload[247] = %#v, want %#v", got, want) - } - if got, want := payload[267], uint32(0xad2cd039); got != want { - t.Fatalf("override payload[267] = %#v, want %#v", got, want) - } - if got, want := payload[288], uint32(0x7006f918); got != want { - t.Fatalf("override payload[288] = %#v, want %#v", got, want) - } - if got, want := payload[289], uint32(0x00021d69); got != want { - t.Fatalf("override payload[289] = %#v, want %#v", got, want) - } - if got, want := payload[348], uint32(0x4ff13582); got != want { - t.Fatalf("override payload[348] = %#v, want %#v", got, want) - } - if got, want := payload[27], uint32(0x0dd8798d); got != want { - t.Fatalf("override payload[27] = %#v, want %#v", got, want) - } - if got, want := payload[152], uint32(0x0160726c); got != want { - t.Fatalf("override payload[152] = %#v, want %#v", got, want) - } - if got, want := payload[159], uint32(1); got != want { - t.Fatalf("override payload[159] = %#v, want %#v", got, want) - } - if got, want := payload[182], uint32(0x000003df); got != want { - t.Fatalf("override payload[182] = %#v, want %#v", got, want) - } - if got, want := payload[168], uint32(0xef51b58e); got != want { - t.Fatalf("override payload[168] = %#v, want %#v", got, want) - } - if got, want := payload[198], uint32(0x97d20b6f); got != want { - t.Fatalf("override payload[198] = %#v, want %#v", got, want) - } - if got, want := payload[203], uint32(0xc14d992a); got != want { - t.Fatalf("override payload[203] = %#v, want %#v", got, want) - } - if got, want := payload[258], uint32(0xceb0120a); got != want { - t.Fatalf("override payload[258] = %#v, want %#v", got, want) - } - if got, want := payload[341], uint32(0x0000793c); got != want { - t.Fatalf("override payload[341] = %#v, want %#v", got, want) - } - if got, want := payload[318], uint32(0x99814406); got != want { - t.Fatalf("override payload[318] = %#v, want %#v", got, want) - } - if got, want := payload[321], uint32(0x3306f870); got != want { - t.Fatalf("override payload[321] = %#v, want %#v", got, want) - } - if got, want := payload[334], uint32(0x4dcfb4eb); got != want { - t.Fatalf("override payload[334] = %#v, want %#v", got, want) - } - if got, want := payload[356], uint32(0xc1a68330); got != want { - t.Fatalf("override payload[356] = %#v, want %#v", got, want) - } - if got, want := payload[359], uint32(0xc25d59db); got != want { - t.Fatalf("override payload[359] = %#v, want %#v", got, want) - } - if got, want := payload[369], uint32(0x92b330e9); got != want { - t.Fatalf("override payload[369] = %#v, want %#v", got, want) - } - if got, want := payload[374], uint32(0x5ed52a3b); got != want { - t.Fatalf("override payload[374] = %#v, want %#v", got, want) - } - if got, want := payload[386], uint32(0x78b37579); got != want { - t.Fatalf("override payload[386] = %#v, want %#v", got, want) - } - if got, want := payload[406], uint32(0x4da110bf); got != want { - t.Fatalf("override payload[406] = %#v, want %#v", got, want) - } - if got, want := payload[399], uint32(0x3fc1896b); got != want { - t.Fatalf("override payload[399] = %#v, want %#v", got, want) - } - if got, want := payload[422], uint32(0x9632461b); got != want { - t.Fatalf("override payload[422] = %#v, want %#v", got, want) - } - if got, want := payload[297], uint32(0x2d162ffa); got != want { - t.Fatalf("override payload[297] = %#v, want %#v", got, want) - } - if got, want := payload[316], uint32(0x03cc6000); got != want { - t.Fatalf("override payload[316] = %#v, want %#v", got, want) - } - if got, want := payload[425], uint32(0xb2160d79); got != want { - t.Fatalf("override payload[425] = %#v, want %#v", got, want) - } - if got, want := payload[366], uint32(0xc211c04b); got != want { - t.Fatalf("override payload[366] = %#v, want %#v", got, want) - } - if got, want := payload[375], uint32(0xc3e69804); got != want { - t.Fatalf("override payload[375] = %#v, want %#v", got, want) - } - if got, want := payload[376], uint32(0x5560087e); got != want { - t.Fatalf("override payload[376] = %#v, want %#v", got, want) - } - if got, want := payload[430], uint32(0x5c3e0c94); got != want { - t.Fatalf("override payload[430] = %#v, want %#v", got, want) - } - if err := populateCurrentCastleNumericSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { - t.Fatalf("populateCurrentCastleNumericSlots() with short payload succeeded") - } -} - -func TestPopulateCurrentCastleObjectSlots(t *testing.T) { - payload := newCurrentCastlePayloadScaffold() - if err := populateCurrentCastleNumericSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleNumericSlots() error = %v", err) - } - if err := populateCurrentCastleStringSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleStringSlots() error = %v", err) - } - if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleObjectSlots() error = %v", err) - } - slot41, ok := payload[41].(map[string]any) - if !ok { - t.Fatalf("payload[41] type = %T, want map[string]any", payload[41]) - } - if got, want := slot41["V"], "Ls9BGR3QrImUhLGh8DdWMQ=="; got != want { - t.Fatalf("payload[41].V = %q, want %q", got, want) - } - if got, want := slot41["wh"], ""; got != want { - t.Fatalf("payload[41].wh = %q, want %q", got, want) - } - if got, want := slot41["y"], ""; got != want { - t.Fatalf("payload[41].y = %q, want %q", got, want) - } - if got, want := slot41["G"], ""; got != want { - t.Fatalf("payload[41].G = %q, want %q", got, want) - } - slot41Hrc, ok := slot41["Hrc"].(map[string]any) - if !ok { - t.Fatalf("payload[41].Hrc type = %T, want map[string]any", slot41["Hrc"]) - } - if got, want := slot41Hrc["j"], ""; got != want { - t.Fatalf("payload[41].Hrc.j = %q, want %q", got, want) - } - slot77, ok := payload[77].(map[string]any) - if !ok { - t.Fatalf("payload[77] type = %T, want map[string]any", payload[77]) - } - if got, want := slot77["AFS"], "JcW/1uh5mugs0/qG3eXd5Q=="; got != want { - t.Fatalf("payload[77].AFS = %q, want %q", got, want) - } - if got, want := slot77["tu"], ""; got != want { - t.Fatalf("payload[77].tu = %q, want %q", got, want) - } - if got, want := slot77["Xd"], ""; got != want { - t.Fatalf("payload[77].Xd = %q, want %q", got, want) - } - if got, want := slot77["BgV"], ""; got != want { - t.Fatalf("payload[77].BgV = %q, want %q", got, want) - } - slot77OverrideSource := encodeCurrentCastleObjectSlot77FromSource("override") - if got, want := slot77OverrideSource["AFS"], "LNP2Gx3Q2X7QdIp/Bza9OsmqjM4="; got != want { - t.Fatalf("override-source payload[77].AFS = %q, want %q", got, want) - } - if got, want := slot77OverrideSource["tu"], "WnPIiEckrrMC9K6z"; got != want { - t.Fatalf("override-source payload[77].tu = %q, want %q", got, want) - } - if got, want := slot77OverrideSource["Xd"], "84ixof8NxS9L6Bba3eXd5Q=="; got != want { - t.Fatalf("override-source payload[77].Xd = %q, want %q", got, want) - } - if got, want := slot77OverrideSource["BgV"], ""; got != want { - t.Fatalf("override-source payload[77].BgV = %q, want %q", got, want) - } - slot99, ok := payload[99].(map[string]any) - if !ok { - t.Fatalf("payload[99] type = %T, want map[string]any", payload[99]) - } - if got, want := slot99["n"], ""; got != want { - t.Fatalf("payload[99].n = %q, want %q", got, want) - } - if got, want := slot99["cOw"], ""; got != want { - t.Fatalf("payload[99].cOw = %q, want %q", got, want) - } - slot99Probably := encodeCurrentCastleObjectSlot99FromCanPlayType("probably") - if got, want := slot99Probably["n"], "xwZQrlpzYPkEdsCAOFtie66z"; got != want { - t.Fatalf("probably payload[99].n = %q, want %q", got, want) - } - if got, want := slot99Probably["cOw"], "vx4WQxnF0Mtie4ybxwZ6zgR2vx56zgy5O90z1TPV"; got != want { - t.Fatalf("probably payload[99].cOw = %q, want %q", got, want) - } - slot99OverrideSource := encodeCurrentCastleObjectSlot99FromSource("override") - if got, want := slot99OverrideSource["n"], "xwaMm6NsZf1l/SuSGcWjbA=="; got != want { - t.Fatalf("override-source payload[99].n = %q, want %q", got, want) - } - if got, want := slot99OverrideSource["cOw"], ""; got != want { - t.Fatalf("override-source payload[99].cOw = %q, want %q", got, want) - } - slot100, ok := payload[100].(map[string]any) - if !ok { - t.Fatalf("payload[100] type = %T, want map[string]any", payload[100]) - } - if got, want := slot100["rj"], "5e4bQb/W"; got != want { - t.Fatalf("payload[100].rj = %q, want %q", got, want) - } - slot100KwJ, ok := slot100["KwJ"].(map[string]any) - if !ok { - t.Fatalf("payload[100].KwJ type = %T, want map[string]any", slot100["KwJ"]) - } - if got, want := slot100KwJ["T"], "CzejbOYagxFvwg=="; got != want { - t.Fatalf("payload[100].KwJ.T = %q, want %q", got, want) - } - slot100L, ok := slot100["L"].(map[string]any) - if !ok { - t.Fatalf("payload[100].L type = %T, want map[string]any", slot100["L"]) - } - if got, want := slot100L["T"], ""; got != want { - t.Fatalf("payload[100].L.T = %q, want %q", got, want) - } - if got, want := slot100["E"], ""; got != want { - t.Fatalf("payload[100].E = %q, want %q", got, want) - } - slot100Probably, err := encodeCurrentCastleObjectSlot100FromSlot99(slot99Probably) - if err != nil { - t.Fatalf("encodeCurrentCastleObjectSlot100FromSlot99(probably) error = %v", err) - } - if got, want := slot100Probably["rj"], "Ls8="; got != want { - t.Fatalf("probably payload[100].rj = %q, want %q", got, want) - } - slot100ProbablyKwJ := slot100Probably["KwJ"].(map[string]any) - if got, want := slot100ProbablyKwJ["T"], "LRR6zgy5es7352/CgxE="; got != want { - t.Fatalf("probably payload[100].KwJ.T = %q, want %q", got, want) - } - slot100OverrideSource, err := encodeCurrentCastleObjectSlot100FromSlot99(slot99OverrideSource) - if err != nil { - t.Fatalf("encodeCurrentCastleObjectSlot100FromSlot99(override source) error = %v", err) - } - if got, want := slot100OverrideSource["rj"], "U6Y="; got != want { - t.Fatalf("override-source payload[100].rj = %q, want %q", got, want) - } - slot100OverrideSourceKwJ := slot100OverrideSource["KwJ"].(map[string]any) - if got, want := slot100OverrideSourceKwJ["T"], "5hrmGi0UkB0LN/fnkB0="; got != want { - t.Fatalf("override-source payload[100].KwJ.T = %q, want %q", got, want) - } - slot46, ok := payload[46].(map[string]any) - if !ok { - t.Fatalf("payload[46] type = %T, want map[string]any", payload[46]) - } - if got, want := slot46["mx"], ""; got != want { - t.Fatalf("payload[46].mx = %q, want %q", got, want) - } - if got, want := slot46["h"], ""; got != want { - t.Fatalf("payload[46].h = %q, want %q", got, want) - } - if got, want := slot46["r"], ""; got != want { - t.Fatalf("payload[46].r = %q, want %q", got, want) - } - if got, want := slot46["AI"], ""; got != want { - t.Fatalf("payload[46].AI = %q, want %q", got, want) - } - slot46Matches := encodeCurrentCastleObjectSlot46FromMatches([]string{"1", "2"}) - if got, want := slot46Matches["mx"], "iZfmGsWEiBWjbBTBAvSMmw=="; got != want { - t.Fatalf("matches payload[46].mx = %q, want %q", got, want) - } - if got, want := slot46Matches["h"], "k5JfO+Xu3eU="; got != want { - t.Fatalf("matches payload[46].h = %q, want %q", got, want) - } - if got, want := slot46Matches["r"], ""; got != want { - t.Fatalf("matches payload[46].r = %q, want %q", got, want) - } - if got, want := slot46Matches["AI"], ""; got != want { - t.Fatalf("matches payload[46].AI = %q, want %q", got, want) - } - slot128, ok := payload[128].(map[string]any) - if !ok { - t.Fatalf("payload[128] type = %T, want map[string]any", payload[128]) - } - if got, want := slot128["ekC"], "jM4d0BtBVjEbQeXusaE="; got != want { - t.Fatalf("payload[128].ekC = %q, want %q", got, want) - } - if got, want := slot128["fCH"], "8Dc="; got != want { - t.Fatalf("payload[128].fCH = %q, want %q", got, want) - } - slot173, ok := payload[173].(map[string]any) - if !ok { - t.Fatalf("payload[173] type = %T, want map[string]any", payload[173]) - } - if got, want := slot173["t"], "gY/mGnrO"; got != want { - t.Fatalf("payload[173].t = %q, want %q", got, want) - } - if got, want := slot173["JZX"], "rImUhLGh8DdWMQ=="; got != want { - t.Fatalf("payload[173].JZX = %q, want %q", got, want) - } - slot173Ga, ok := slot173["ga"].(map[string]any) - if !ok { - t.Fatalf("payload[173].ga type = %T, want map[string]any", slot173["ga"]) - } - if got, want := slot173Ga["sE"], ""; got != want { - t.Fatalf("payload[173].ga.sE = %q, want %q", got, want) - } - if got, want := slot173Ga["OkL"], ""; got != want { - t.Fatalf("payload[173].ga.OkL = %q, want %q", got, want) - } - if got, want := slot173Ga["hD"], ""; got != want { - t.Fatalf("payload[173].ga.hD = %q, want %q", got, want) - } - if got, want := slot173["wPg"], ""; got != want { - t.Fatalf("payload[173].wPg = %q, want %q", got, want) - } - if got, want := slot173["FqX"], ""; got != want { - t.Fatalf("payload[173].FqX = %q, want %q", got, want) - } - slot214, ok := payload[214].(map[string]any) - if !ok { - t.Fatalf("payload[214] type = %T, want map[string]any", payload[214]) - } - if got, want := slot214["h"], "LRTQyw=="; got != want { - t.Fatalf("payload[214].h = %q, want %q", got, want) - } - if got, want := slot214["yhj"], "S+hTpr/W8DdL6PA3"; got != want { - t.Fatalf("payload[214].yhj = %q, want %q", got, want) - } - if got, want := slot214["XI"], ""; got != want { - t.Fatalf("payload[214].XI = %q, want %q", got, want) - } - slot290, ok := payload[290].(map[string]any) - if !ok { - t.Fatalf("payload[290] type = %T, want map[string]any", payload[290]) - } - if got, want := slot290["d"], "LRRvwgs3+WmjbOYaCzcMuQ=="; got != want { - t.Fatalf("payload[290].d = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleObjectSlot290ValueForTest(payload), "LRRvwgs3+WmjbOYaCzcMuQ=="; got != want { - t.Fatalf("encodeCurrentCastleObjectSlot290ValueForTest() = %q, want %q", got, want) - } - slot342, ok := payload[342].(map[string]any) - if !ok { - t.Fatalf("payload[342] type = %T, want map[string]any", payload[342]) - } - if got, want := slot342["kFx"], "CzfQy+YaCzdac9DLLRSQHQ=="; got != want { - t.Fatalf("payload[342].kFx = %q, want %q", got, want) - } - slot352, ok := payload[352].(map[string]any) - if !ok { - t.Fatalf("payload[352] type = %T, want map[string]any", payload[352]) - } - if got, want := slot352["XYw"], "gY/mGnrO0MsLN5Ad9+ejbA=="; got != want { - t.Fatalf("payload[352].XYw = %q, want %q", got, want) - } - slot416, ok := payload[416].(map[string]any) - if !ok { - t.Fatalf("payload[416] type = %T, want map[string]any", payload[416]) - } - if got, want := slot416["y"], "LRR6zlpzo2xac/lpkB335w=="; got != want { - t.Fatalf("payload[416].y = %q, want %q", got, want) - } - if got, want := slot416["OiJ"], ""; got != want { - t.Fatalf("payload[416].OiJ = %q, want %q", got, want) - } - slot416HRd, ok := slot416["HRd"].(map[string]any) - if !ok { - t.Fatalf("payload[416].HRd type = %T, want map[string]any", slot416["HRd"]) - } - if got, want := slot416HRd["j"], ""; got != want { - t.Fatalf("payload[416].HRd.j = %q, want %q", got, want) - } - if got, want := slot416HRd["hCP"], ""; got != want { - t.Fatalf("payload[416].HRd.hCP = %q, want %q", got, want) - } - slot424, ok := payload[424].(map[string]any) - if !ok { - t.Fatalf("payload[424] type = %T, want map[string]any", payload[424]) - } - if got, want := slot424["kr"], "jM4d0BtBVjE="; got != want { - t.Fatalf("payload[424].kr = %q, want %q", got, want) - } - slot424Vj, ok := slot424["vj"].(map[string]any) - if !ok { - t.Fatalf("payload[424].vj type = %T, want map[string]any", slot424["vj"]) - } - if got, want := slot424Vj["GXV"], "G0Hl7rGh8Dc="; got != want { - t.Fatalf("payload[424].vj.GXV = %q, want %q", got, want) - } - if got, want := slot424["RK"], ""; got != want { - t.Fatalf("payload[424].RK = %q, want %q", got, want) - } - slot424Wxd, ok := slot424["wXd"].(map[string]any) - if !ok { - t.Fatalf("payload[424].wXd type = %T, want map[string]any", slot424["wXd"]) - } - slot424RtN, ok := slot424Wxd["RtN"].(map[string]any) - if !ok { - t.Fatalf("payload[424].wXd.RtN type = %T, want map[string]any", slot424Wxd["RtN"]) - } - if got, want := slot424RtN["o"], ""; got != want { - t.Fatalf("payload[424].wXd.RtN.o = %q, want %q", got, want) - } - if got, want := slot424RtN["ApQ"], ""; got != want { - t.Fatalf("payload[424].wXd.RtN.ApQ = %q, want %q", got, want) - } - if got, want := slot424RtN["G"], ""; got != want { - t.Fatalf("payload[424].wXd.RtN.G = %q, want %q", got, want) - } - if err := populateCurrentCastleNumericSlots(payload, map[int]uint32{289: 0xdae88a03}); err != nil { - t.Fatalf("populateCurrentCastleNumericSlots() sentinel override error = %v", err) - } - if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleObjectSlots() after override error = %v", err) - } - slot290 = payload[290].(map[string]any) - if got, want := slot290["d"], "LRRvwgs3+WmjbOYaCzcMuQ=="; got != want { - t.Fatalf("sentinel payload[290].d = %q, want %q", got, want) - } - if err := populateCurrentCastleNumericSlots(payload, map[int]uint32{341: 1}); err != nil { - t.Fatalf("populateCurrentCastleNumericSlots() slot 341 override error = %v", err) - } - if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleObjectSlots() after slot 341 override error = %v", err) - } - slot342 = payload[342].(map[string]any) - if got, want := slot342["kFx"], "CzfQy+YaCzdac9DLLRSQHQ=="; got != want { - t.Fatalf("override payload[342].kFx = %q, want %q", got, want) - } - if err := populateCurrentCastleStringSlots(payload, map[int]string{351: "override"}); err != nil { - t.Fatalf("populateCurrentCastleStringSlots() slot 351 override error = %v", err) - } - if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleObjectSlots() after string override error = %v", err) - } - slot352 = payload[352].(map[string]any) - if got, want := slot352["XYw"], "gY/mGnrO0MsLN5Ad9+ejbA=="; got != want { - t.Fatalf("override payload[352].XYw = %q, want %q", got, want) - } - if err := populateCurrentCastleStringSlots(payload, map[int]string{213: "override"}); err != nil { - t.Fatalf("populateCurrentCastleStringSlots() slot 213 override error = %v", err) - } - if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleObjectSlots() after slot 213 override error = %v", err) - } - slot214 = payload[214].(map[string]any) - if got, want := slot214["h"], "LRTQyw=="; got != want { - t.Fatalf("override payload[214].h = %q, want %q", got, want) - } - if got, want := slot214["yhj"], "S+hTpr/W8DdL6PA3"; got != want { - t.Fatalf("override payload[214].yhj = %q, want %q", got, want) - } - if got, want := slot214["XI"], ""; got != want { - t.Fatalf("override payload[214].XI = %q, want %q", got, want) - } - if err := populateCurrentCastleStringSlots(payload, map[int]string{127: "override"}); err != nil { - t.Fatalf("populateCurrentCastleStringSlots() slot 127 override error = %v", err) - } - if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleObjectSlots() after slot 127 override error = %v", err) - } - slot128 = payload[128].(map[string]any) - if got, want := slot128["ekC"], "jM4d0BtBVjEbQeXusaE="; got != want { - t.Fatalf("override payload[128].ekC = %q, want %q", got, want) - } - if got, want := slot128["fCH"], "8Dc="; got != want { - t.Fatalf("override payload[128].fCH = %q, want %q", got, want) - } - if err := populateCurrentCastleStringSlots(payload, map[int]string{172: "override"}); err != nil { - t.Fatalf("populateCurrentCastleStringSlots() slot 172 override error = %v", err) - } - if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleObjectSlots() after slot 172 override error = %v", err) - } - slot173 = payload[173].(map[string]any) - if got, want := slot173["t"], "gY/mGnrO"; got != want { - t.Fatalf("override payload[173].t = %q, want %q", got, want) - } - if got, want := slot173["JZX"], "rImUhLGh8DdWMQ=="; got != want { - t.Fatalf("override payload[173].JZX = %q, want %q", got, want) - } - slot173Ga = slot173["ga"].(map[string]any) - if got, want := slot173Ga["sE"], ""; got != want { - t.Fatalf("override payload[173].ga.sE = %q, want %q", got, want) - } - if got, want := slot173Ga["OkL"], ""; got != want { - t.Fatalf("override payload[173].ga.OkL = %q, want %q", got, want) - } - if got, want := slot173Ga["hD"], ""; got != want { - t.Fatalf("override payload[173].ga.hD = %q, want %q", got, want) - } - if got, want := slot173["wPg"], ""; got != want { - t.Fatalf("override payload[173].wPg = %q, want %q", got, want) - } - if got, want := slot173["FqX"], ""; got != want { - t.Fatalf("override payload[173].FqX = %q, want %q", got, want) - } - if err := populateCurrentCastleStringSlots(payload, map[int]string{415: "override"}); err != nil { - t.Fatalf("populateCurrentCastleStringSlots() slot 415 override error = %v", err) - } - if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleObjectSlots() after slot 415 override error = %v", err) - } - slot416 = payload[416].(map[string]any) - if got, want := slot416["y"], "LRR6zlpzo2xac/lpkB335w=="; got != want { - t.Fatalf("override payload[416].y = %q, want %q", got, want) - } - if got, want := slot416["OiJ"], ""; got != want { - t.Fatalf("override payload[416].OiJ = %q, want %q", got, want) - } - slot416HRd = slot416["HRd"].(map[string]any) - if got, want := slot416HRd["j"], ""; got != want { - t.Fatalf("override payload[416].HRd.j = %q, want %q", got, want) - } - if got, want := slot416HRd["hCP"], ""; got != want { - t.Fatalf("override payload[416].HRd.hCP = %q, want %q", got, want) - } - if err := populateCurrentCastleStringSlots(payload, map[int]string{423: "override"}); err != nil { - t.Fatalf("populateCurrentCastleStringSlots() slot 423 override error = %v", err) - } - if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleObjectSlots() after slot 423 override error = %v", err) - } - slot424 = payload[424].(map[string]any) - if got, want := slot424["kr"], "jM4d0BtBVjE="; got != want { - t.Fatalf("override payload[424].kr = %q, want %q", got, want) - } - slot424Vj = slot424["vj"].(map[string]any) - if got, want := slot424Vj["GXV"], "G0Hl7rGh8Dc="; got != want { - t.Fatalf("override payload[424].vj.GXV = %q, want %q", got, want) - } - if got, want := slot424["RK"], ""; got != want { - t.Fatalf("override payload[424].RK = %q, want %q", got, want) - } - slot424Wxd = slot424["wXd"].(map[string]any) - slot424RtN = slot424Wxd["RtN"].(map[string]any) - if got, want := slot424RtN["o"], ""; got != want { - t.Fatalf("override payload[424].wXd.RtN.o = %q, want %q", got, want) - } - if got, want := slot424RtN["ApQ"], ""; got != want { - t.Fatalf("override payload[424].wXd.RtN.ApQ = %q, want %q", got, want) - } - if got, want := slot424RtN["G"], ""; got != want { - t.Fatalf("override payload[424].wXd.RtN.G = %q, want %q", got, want) - } - if err := populateCurrentCastleStringSlots(payload, map[int]string{40: "override"}); err != nil { - t.Fatalf("populateCurrentCastleStringSlots() slot 40 override error = %v", err) - } - if err := populateCurrentCastleObjectSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleObjectSlots() after slot 40 override error = %v", err) - } - slot41 = payload[41].(map[string]any) - if got, want := slot41["V"], "Ls9BGR3QrImUhLGh8DdWMQ=="; got != want { - t.Fatalf("override payload[41].V = %q, want %q", got, want) - } - if got, want := slot41["wh"], ""; got != want { - t.Fatalf("override payload[41].wh = %q, want %q", got, want) - } - if got, want := slot41["y"], ""; got != want { - t.Fatalf("override payload[41].y = %q, want %q", got, want) - } - if got, want := slot41["G"], ""; got != want { - t.Fatalf("override payload[41].G = %q, want %q", got, want) - } - slot41Hrc = slot41["Hrc"].(map[string]any) - if got, want := slot41Hrc["j"], ""; got != want { - t.Fatalf("override payload[41].Hrc.j = %q, want %q", got, want) - } - if err := populateCurrentCastleObjectSlots(payload, map[int]map[string]any{ - 41: {"V": "override41"}, - 77: {"AFS": "override77"}, - 99: {"n": "override99"}, - 100: {"rj": "override100"}, - 46: {"mx": "override46"}, - 128: {"ekC": "override128"}, - 173: {"t": "override173"}, - 214: {"h": "override214"}, - 290: {"d": "override"}, - 342: {"kFx": "override342"}, - 352: {"XYw": "override352"}, - 416: {"y": "override416"}, - 424: {"kr": "override424"}, - }); err != nil { - t.Fatalf("populateCurrentCastleObjectSlots() override error = %v", err) - } - slot41 = payload[41].(map[string]any) - if got, want := slot41["V"], "override41"; got != want { - t.Fatalf("override payload[41].V = %q, want %q", got, want) - } - slot77 = payload[77].(map[string]any) - if got, want := slot77["AFS"], "override77"; got != want { - t.Fatalf("override payload[77].AFS = %q, want %q", got, want) - } - slot99 = payload[99].(map[string]any) - if got, want := slot99["n"], "override99"; got != want { - t.Fatalf("override payload[99].n = %q, want %q", got, want) - } - slot100 = payload[100].(map[string]any) - if got, want := slot100["rj"], "override100"; got != want { - t.Fatalf("override payload[100].rj = %q, want %q", got, want) - } - slot46 = payload[46].(map[string]any) - if got, want := slot46["mx"], "override46"; got != want { - t.Fatalf("override payload[46].mx = %q, want %q", got, want) - } - slot128 = payload[128].(map[string]any) - if got, want := slot128["ekC"], "override128"; got != want { - t.Fatalf("override payload[128].ekC = %q, want %q", got, want) - } - slot173 = payload[173].(map[string]any) - if got, want := slot173["t"], "override173"; got != want { - t.Fatalf("override payload[173].t = %q, want %q", got, want) - } - slot214 = payload[214].(map[string]any) - if got, want := slot214["h"], "override214"; got != want { - t.Fatalf("override payload[214].h = %q, want %q", got, want) - } - slot290 = payload[290].(map[string]any) - if got, want := slot290["d"], "override"; got != want { - t.Fatalf("override payload[290].d = %q, want %q", got, want) - } - slot342 = payload[342].(map[string]any) - if got, want := slot342["kFx"], "override342"; got != want { - t.Fatalf("override payload[342].kFx = %q, want %q", got, want) - } - slot352 = payload[352].(map[string]any) - if got, want := slot352["XYw"], "override352"; got != want { - t.Fatalf("override payload[352].XYw = %q, want %q", got, want) - } - slot416 = payload[416].(map[string]any) - if got, want := slot416["y"], "override416"; got != want { - t.Fatalf("override payload[416].y = %q, want %q", got, want) - } - slot424 = payload[424].(map[string]any) - if got, want := slot424["kr"], "override424"; got != want { - t.Fatalf("override payload[424].kr = %q, want %q", got, want) - } - if err := populateCurrentCastleObjectSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { - t.Fatalf("populateCurrentCastleObjectSlots() with short payload succeeded") - } -} - -func encodeCurrentCastleObjectSlot290ValueForTest(payload []any) string { - encoded, err := encodeCurrentCastleObjectSlot290(payload) - if err != nil { - return "ERROR: " + err.Error() - } - value, _ := encoded["d"].(string) - return value -} - -func TestCurrentCastleLowerFloatEncoderSixthBatch(t *testing.T) { - cases := []struct { - slot int - want string - }{ - {slot: 169, want: "kNqJtno023Y="}, - {slot: 170, want: "wjo4fSk/u54="}, - {slot: 171, want: "Bw3Aapka7RI="}, - {slot: 178, want: "lugV26QrSCM="}, - {slot: 179, want: "nvAp5xT3kN8="}, - {slot: 181, want: "I+6oFcmOm/g="}, - {slot: 185, want: "yfI99uK0cdc="}, - {slot: 186, want: "Bra+Pl6or+g="}, - {slot: 187, want: "vM2SNbNt/WE="}, - {slot: 189, want: "51GaJEYYWQo="}, - {slot: 191, want: "jjVVwgTmLfw="}, - {slot: 193, want: "Hjvh3DH0K9A="}, - {slot: 195, want: "+pxbCViZ/HE="}, - {slot: 197, want: "7FYto4qzNis="}, - {slot: 199, want: "HVla/tDdGI8="}, - {slot: 200, want: "OsCZA8zz4Cs="}, - {slot: 201, want: "NExCiV1Lz+g="}, - {slot: 202, want: "uTE3fMA+s10="}, - {slot: 208, want: "+e4VYAvk7SY="}, - {slot: 210, want: "QI6lR6o37s8="}, - {slot: 211, want: "HN+v+Vfny+I="}, - {slot: 216, want: "Aj7xzHYVp7w="}, - {slot: 219, want: "c1HKtEVksdw="}, - {slot: 222, want: "IznkXo0O2TY="}, - {slot: 225, want: "mAlFU+jTDJM="}, - {slot: 226, want: "ohn2/Iu8m9w="}, - } - for _, tc := range cases { - t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { - encoder := currentCastleLowerFloatSlotEncoders[tc.slot] - if encoder == nil { - t.Fatalf("missing encoder for slot %d", tc.slot) - } - if got := encoder(123.456); got != tc.want { - t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) - } - }) - } -} - -func TestCurrentCastleLowerFloatEncoderSeventhBatch(t *testing.T) { - cases := []struct { - slot int - want string - }{ - {slot: 227, want: "gN+3yhvT4U0="}, - {slot: 229, want: "oCOWa+lDkz8="}, - {slot: 230, want: "XH6fDUKdHsU="}, - {slot: 232, want: "vJLiyWbWNNA="}, - {slot: 233, want: "EJdX64bHr/0="}, - {slot: 234, want: "ulzfjZAdPHU="}, - {slot: 236, want: "OtTxp4j39H8="}, - {slot: 238, want: "CIfnTJZwr34="}, - {slot: 241, want: "D34+l5zfTvM="}, - {slot: 242, want: "5xKC3Mo6/t0="}, - {slot: 243, want: "pLrtRzY3Wj8="}, - {slot: 244, want: "NKJoeoP6pzo="}, - {slot: 248, want: "ti+S+364r1s="}, - {slot: 250, want: "BOXd8qH563c="}, - {slot: 251, want: "wITFYgxBg9I="}, - {slot: 252, want: "aUvOdA+E67w="}, - {slot: 253, want: "GOIXHfbtAjU="}, - {slot: 255, want: "cpQVY1jT9Ls="}, - {slot: 259, want: "TReYPlPut/Y="}, - {slot: 262, want: "uJ0tM2U1MSI="}, - {slot: 263, want: "eJmhjt2Flws="}, - {slot: 265, want: "lDYL8aIhFtk="}, - {slot: 268, want: "/ErazXMOdDo="}, - {slot: 269, want: "sEhKD1tNyew="}, - {slot: 270, want: "7ZWPXiLkmEE="}, - {slot: 272, want: "2s3BUivXzhQ="}, - {slot: 278, want: "0DEJJnUtP6M="}, - {slot: 280, want: "QjIuuRA8Nfs="}, - {slot: 284, want: "qIPjHok0HyM="}, - {slot: 285, want: "oFbzLSL9drU="}, - {slot: 286, want: "m4UG9MFEZaw="}, - {slot: 287, want: "vLo3aXb52pE="}, - {slot: 294, want: "dKJFf/ZPQoc="}, - } - for _, tc := range cases { - t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { - encoder := currentCastleLowerFloatSlotEncoders[tc.slot] - if encoder == nil { - t.Fatalf("missing encoder for slot %d", tc.slot) - } - if got := encoder(123.456); got != tc.want { - t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) - } - }) - } -} - -func TestCurrentCastleLowerFloatEncoderEighthBatch(t *testing.T) { - cases := []struct { - slot int - want string - }{ - {slot: 295, want: "P0sxhaQmCxY="}, - {slot: 296, want: "VW86452i7oI="}, - {slot: 299, want: "I32YVhlG3W4="}, - {slot: 301, want: "GHm+V8txfVo="}, - {slot: 303, want: "ycEfdODWEmU="}, - {slot: 305, want: "NBaXhVLV9k0="}, - {slot: 306, want: "VpPjtRurh6w="}, - {slot: 307, want: "ygl5J4ExFSw="}, - {slot: 309, want: "vT9OXLOsnwQ="}, - {slot: 310, want: "7o1St8S8jz0="}, - {slot: 313, want: "s23OvClMDWQ="}, - {slot: 315, want: "qyMhZhIopIc="}, - {slot: 317, want: "hg3tURx9NUM="}, - {slot: 319, want: "SdTyth3WFUY="}, - {slot: 320, want: "BJrHWZ7pOoE="}, - {slot: 322, want: "udt86p96e6I="}, - {slot: 323, want: "9BEpqt3Vf9s="}, - {slot: 324, want: "W2NlnnJc4P8="}, - {slot: 325, want: "Wnz7SUC53JE="}, - } - for _, tc := range cases { - t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { - encoder := currentCastleLowerFloatSlotEncoders[tc.slot] - if encoder == nil { - t.Fatalf("missing encoder for slot %d", tc.slot) - } - if got := encoder(123.456); got != tc.want { - t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) - } - }) - } -} - -func TestCurrentCastleLowerFloatEncoderSkippedMidBatch(t *testing.T) { - cases := []struct { - slot int - want string - }{ - {slot: 264, want: "TmgJ/7QPiDc="}, - {slot: 271, want: "XUPAcgeC46o="}, - {slot: 275, want: "CuY/I5bnZsY="}, - {slot: 276, want: "Tzc58iYwtJM="}, - {slot: 282, want: "gLy7XjQ/fe4="}, - {slot: 283, want: "NRRsClgDEoY="}, - {slot: 291, want: "rg8/TelXQ14="}, - } - for _, tc := range cases { - t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { - encoder := currentCastleLowerFloatSlotEncoders[tc.slot] - if encoder == nil { - t.Fatalf("missing encoder for slot %d", tc.slot) - } - if got := encoder(123.456); got != tc.want { - t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) - } - }) - } -} - -func TestCurrentCastleLowerFloatEncoderNinthBatch(t *testing.T) { - cases := []struct { - slot int - want string - }{ - {slot: 327, want: "ahpGDRAsubc="}, - {slot: 331, want: "DsRPybD5pLE="}, - {slot: 332, want: "tT3ccSxNFUM="}, - {slot: 333, want: "ANr/FY7F+r0="}, - {slot: 335, want: "525Ool2+hrQ="}, - {slot: 336, want: "7FKBU/bjcks="}, - {slot: 337, want: "LX8nUxjReBQ="}, - {slot: 338, want: "YVuK46XKa+8="}, - {slot: 340, want: "3ewsVcoNnHk="}, - {slot: 343, want: "iqgpm+xrCEM="}, - {slot: 345, want: "za9IfmvODyY="}, - {slot: 346, want: "70YeEaqSIKQ="}, - {slot: 347, want: "rdMWoJ9Qs8g="}, - {slot: 353, want: "pPKJKw4bUrM="}, - {slot: 355, want: "Fkp6MN0iRik="}, - {slot: 357, want: "IW9YGosqDyI="}, - {slot: 358, want: "98XUPhmuJUY="}, - {slot: 360, want: "m21mpJG0DZw="}, - {slot: 361, want: "8JfuP6W3x4M="}, - {slot: 362, want: "STiYbHNIUNo="}, - {slot: 363, want: "DVz+oc67WKI="}, - {slot: 364, want: "AVtHrQpu2k8="}, - {slot: 365, want: "Cp3VqJFpU2c="}, - {slot: 367, want: "lDzTSCt0JHo="}, - {slot: 368, want: "YdkXPGi+ur0="}, - {slot: 370, want: "o5SYKvKul+0="}, - {slot: 371, want: "7jBde2yr0JM="}, - {slot: 372, want: "txswobMsJlE="}, - {slot: 373, want: "5MZDWZIJplE="}, - } - for _, tc := range cases { - t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { - encoder := currentCastleLowerFloatSlotEncoders[tc.slot] - if encoder == nil { - t.Fatalf("missing encoder for slot %d", tc.slot) - } - if got := encoder(123.456); got != tc.want { - t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) - } - }) - } -} - -func TestCurrentCastleLowerFloatEncoderTenthBatch(t *testing.T) { - cases := []struct { - slot int - want string - }{ - {slot: 377, want: "GB/eh/2/T7M="}, - {slot: 379, want: "D4WY8hnCpfo="}, - {slot: 380, want: "/BKN38ZP8rc="}, - {slot: 381, want: "YGn19Dv8OfA="}, - {slot: 382, want: "UbiYdIsI0MI="}, - {slot: 384, want: "pZsaTF/cu3Q="}, - {slot: 385, want: "QI4lx6q37k8="}, - {slot: 387, want: "EO5N3ypPTic="}, - {slot: 389, want: "SrqmMZi0vfM="}, - {slot: 391, want: "gLy7XjQ/fe4="}, - {slot: 396, want: "m7k+aH34GaA="}, - {slot: 397, want: "Nx2aLEHcveQ="}, - {slot: 398, want: "O6mkvv3uCaY="}, - {slot: 401, want: "cMsmLFlsSQw="}, - {slot: 402, want: "/+KligDCcpY="}, - {slot: 403, want: "RjiTkZyBmKk="}, - {slot: 404, want: "R+EDnPaQ6YI="}, - {slot: 405, want: "SquTvO+3pTk="}, - {slot: 407, want: "20D8lRHUwDU="}, - } - for _, tc := range cases { - t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { - encoder := currentCastleLowerFloatSlotEncoders[tc.slot] - if encoder == nil { - t.Fatalf("missing encoder for slot %d", tc.slot) - } - if got := encoder(123.456); got != tc.want { - t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) - } - }) - } -} - -func TestCurrentCastleLowerFloatEncoderEleventhBatch(t *testing.T) { - cases := []struct { - slot int - want string - }{ - {slot: 408, want: "pLfhbuAN954="}, - {slot: 409, want: "qElxXg1VR9s="}, - {slot: 410, want: "VLWNpvmhvyM="}, - {slot: 411, want: "DTOw4vdyk1o="}, - {slot: 412, want: "8HeXK2YHTz0="}, - {slot: 413, want: "15OSNQsWVMU="}, - {slot: 414, want: "8AaD1cpFZh0="}, - {slot: 417, want: "NIpDDRZdqmU="}, - {slot: 418, want: "VJCPMggTUcI="}, - {slot: 419, want: "4h4dwJah31A="}, - {slot: 420, want: "CNY0a0Rx0mg="}, - {slot: 426, want: "ZFy6j/vxLQA="}, - {slot: 427, want: "x71ooinSnYo="}, - } - for _, tc := range cases { - t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { - encoder := currentCastleLowerFloatSlotEncoders[tc.slot] - if encoder == nil { - t.Fatalf("missing encoder for slot %d", tc.slot) - } - if got := encoder(123.456); got != tc.want { - t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) - } - }) - } -} - -func TestCurrentCastleLowerFloatEncoderScatteredBatch(t *testing.T) { - cases := []struct { - slot int - want string - }{ - {slot: 86, want: "kCmz3FqbqXw="}, - {slot: 92, want: "8ftCGK+Im8A="}, - {slot: 135, want: "572GzjnMqc8="}, - {slot: 161, want: "dPubMOoM0wI="}, - {slot: 190, want: "iOpruaYpCsE="}, - {slot: 192, want: "BVX/KPGqVOg="}, - {slot: 215, want: "fVB4U4z0vvo="}, - {slot: 218, want: "DCqbtX7liu0="}, - {slot: 249, want: "vC/Pa1K/V60="}, - {slot: 300, want: "bqiVq5z7yAM="}, - {slot: 314, want: "/nmZJWgJQTM="}, - {slot: 392, want: "UXMUgjcSEzo="}, - } - for _, tc := range cases { - t.Run(strconv.Itoa(tc.slot), func(t *testing.T) { - encoder := currentCastleLowerFloatSlotEncoders[tc.slot] - if encoder == nil { - t.Fatalf("missing encoder for slot %d", tc.slot) - } - if got := encoder(123.456); got != tc.want { - t.Fatalf("slot %d sample = %q, want %q", tc.slot, got, tc.want) - } - }) - } -} - -func TestCurrentCastleArraySlotEncoders(t *testing.T) { - sample := []float64{0, 1, 123.456} - cases := []struct { - name string - slot int - values []float64 - want string - }{ - {name: "slot254 sample", slot: 254, values: sample, want: "Y2NjY2NjY2Oe02NjY2NjY6O5dE5lvlm2"}, - {name: "slot302 sample", slot: 302, values: sample, want: "wMDAwMDAwMBRycDAwMDAwMzlclqvW+/a"}, - {name: "slot254 empty", slot: 254, values: nil, want: ""}, - {name: "slot302 empty", slot: 302, values: nil, want: ""}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - encoder := currentCastleArraySlotEncoders[tc.slot] - if encoder == nil { - t.Fatalf("missing array encoder for slot %d", tc.slot) - } - if got := encoder(tc.values); got != tc.want { - t.Fatalf("slot %d array sample = %q, want %q", tc.slot, got, tc.want) - } - }) - } - - payload := newCurrentCastlePayloadScaffold() - if err := populateCurrentCastleArraySlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleArraySlots() error = %v", err) - } - if got, want := payload[254], encodeCurrentCastleArraySlot254(currentCastleFloat64Samples(256, 199)); got != want { - t.Fatalf("payload[254] = %q, want %q", got, want) - } - if got, want := payload[302], encodeCurrentCastleArraySlot302(currentCastleFloat64Samples(16, 13)); got != want { - t.Fatalf("payload[302] = %q, want %q", got, want) - } - if err := populateCurrentCastleArraySlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { - t.Fatalf("populateCurrentCastleArraySlots() with short payload succeeded") - } -} - -func TestCurrentCastleStringSlotEncoders(t *testing.T) { - cases := []struct { - name string - value string - want string - }{ - {name: "empty", value: "", want: ""}, - {name: "ascii", value: "abc", want: "qeqtnO+D"}, - {name: "number", value: "123.456", want: "9FL4BDprU243uXGgdFI="}, - {name: "timezone", value: "America/Chicago", want: "j1otnCU4vWSQTu+DqeqtVc1zVmeQTu+Dqepr0VO1"}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - if got := encodeCurrentCastleUTF16Scramble(tc.value); got != tc.want { - t.Fatalf("encodeCurrentCastleUTF16Scramble(%q) = %q, want %q", tc.value, got, tc.want) - } - }) - } - if got, want := encodeCurrentCastleDoubleUTF16Scramble("123.456"), "/DYPWrElN7lzDMlBf0u9ZBRw+AQ3uTprNoBGn0lBa9Hr0Q9adz54BA=="; got != want { - t.Fatalf("encodeCurrentCastleDoubleUTF16Scramble() = %q, want %q", got, want) - } - - payload := newCurrentCastlePayloadScaffold() - if err := populateCurrentCastleStringSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleStringSlots() error = %v", err) - } - expectedDefaults := map[int]string{ - 11: "isz", - 40: "NoAtnP9LeAQ=", - 89: "NoAtnP9LeAQ=", - 127: "NoAtnP9LeAQ=", - 172: "NoAtnP9LeAQ=", - 175: "NoAtnP9LeAQ=", - 213: "GraL", - 217: "NoAtnP9LeAQ=", - 231: "NoAtnP9LeAQ=", - 351: "NoAtnP9LeAQ=", - 400: "NoAtnP9LeAQ=", - 415: "NoAtnP9LeAQ=", - 423: "NoAtnP9LeAQ=", - } - for slot, want := range expectedDefaults { - if got := payload[slot]; got != want { - t.Fatalf("payload[%d] = %q, want %q", slot, got, want) - } - } - if err := populateCurrentCastleStringSlots(payload, map[int]string{89: "123.456"}); err != nil { - t.Fatalf("populateCurrentCastleStringSlots() with override error = %v", err) - } - if got, want := payload[89], "9FL4BDprU243uXGgdFI="; got != want { - t.Fatalf("payload[89] override = %q, want %q", got, want) - } - if err := populateCurrentCastleStringSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { - t.Fatalf("populateCurrentCastleStringSlots() with short payload succeeded") - } -} - -func TestCurrentCastlePackedStringSlotEncoders(t *testing.T) { - const slot0MissingProbe = "\u216c\u2166\u216c\u2129\u2097\u598c" - slot0Cases := []struct { - name string - fn func(string) string - want string - }{ - {name: "primary", fn: encodeCurrentCastleSlot0Primary, want: "Wrf7WrdxWrf7WvW2WnIXn3HD"}, - {name: "secondary", fn: encodeCurrentCastleSlot0Secondary, want: "bvpIbvorbvpIbsC5bmxS/CtC"}, - {name: "tertiary", fn: encodeCurrentCastleSlot0Tertiary, want: "HnddHneyHnddHmD4HjK3g7JZ"}, - {name: "quaternary", fn: encodeCurrentCastleSlot0Quaternary, want: "ROQFROTMROQFRAB5RFiu8MwB"}, - {name: "quinary", fn: encodeCurrentCastleSlot0Quinary, want: "A5UGA5VHA5UGA6U2A8UY00fI"}, - {name: "senary", fn: encodeCurrentCastleSlot0Senary, want: "8JO68JO08JO68JK38JCl87Sa"}, - } - for _, tc := range slot0Cases { - t.Run("slot0 "+tc.name, func(t *testing.T) { - if got := tc.fn(slot0MissingProbe); got != tc.want { - t.Fatalf("slot 0 %s encoder = %q, want %q", tc.name, got, tc.want) - } - }) - } - if got, want := encodeCurrentCastleSlot0Primary("abc"), "uHo8"; got != want { - t.Fatalf("encodeCurrentCastleSlot0Primary(sample) = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot0Secondary("abc"), "PGaQ"; got != want { - t.Fatalf("encodeCurrentCastleSlot0Secondary(sample) = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot0Quaternary("abc"), "cFQ4"; got != want { - t.Fatalf("encodeCurrentCastleSlot0Quaternary(sample) = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot0Quinary("abc"), "Gwv7"; got != want { - t.Fatalf("encodeCurrentCastleSlot0Quinary(sample) = %q, want %q", got, want) - } - - slot7Cases := []struct { - name string - fn func(string) string - want string - }{ - {name: "canvas primary", fn: encodeCurrentCastleSlot7CanvasPrimary, want: "K6zp"}, - {name: "worker timing", fn: encodeCurrentCastleSlot7WorkerTiming, want: "JNn2"}, - {name: "media ratio", fn: encodeCurrentCastleSlot7MediaRatio, want: "p1vL"}, - {name: "plugin state", fn: encodeCurrentCastleSlot7PluginState, want: "1qXk"}, - {name: "frame ratio", fn: encodeCurrentCastleSlot7FrameRatio, want: "32it"}, - {name: "worker canvas", fn: encodeCurrentCastleSlot7WorkerCanvas, want: "pM6W"}, - {name: "worker feature", fn: encodeCurrentCastleSlot7WorkerFeature, want: "Acgz"}, - {name: "navigator probe", fn: encodeCurrentCastleSlot7NavigatorProbe, want: "2qQO"}, - {name: "viewport ratio", fn: encodeCurrentCastleSlot7ViewportRatio, want: "jOua"}, - {name: "worker signal", fn: encodeCurrentCastleSlot7WorkerSignal, want: "0zoY"}, - {name: "pointer ratio", fn: encodeCurrentCastleSlot7PointerRatio, want: "KV8X"}, - } - for _, tc := range slot7Cases { - t.Run(tc.name, func(t *testing.T) { - if got := tc.fn("w:1"); got != tc.want { - t.Fatalf("slot 7 %s encoder = %q, want %q", tc.name, got, tc.want) - } - }) - } - if got, want := encodeCurrentCastleSlot7CanvasPrimary("abc"), "+WTP"; got != want { - t.Fatalf("encodeCurrentCastleSlot7CanvasPrimary(sample) = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot7WorkerTiming("abc"), "RtEw"; got != want { - t.Fatalf("encodeCurrentCastleSlot7WorkerTiming(sample) = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot7WorkerSignal("abc"), "FjFR"; got != want { - t.Fatalf("encodeCurrentCastleSlot7WorkerSignal(sample) = %q, want %q", got, want) - } - - if got, want := encodeCurrentCastleWebGPUVendor("g:1"), "3eT7"; got != want { - t.Fatalf("encodeCurrentCastleWebGPUVendor() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleWebGPULimits("g:1"), "ElQu"; got != want { - t.Fatalf("encodeCurrentCastleWebGPULimits() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleWebGPUArchitecture("g:1"), "Q4Su"; got != want { - t.Fatalf("encodeCurrentCastleWebGPUArchitecture() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastlePackedStringComponents(nil), "qj4="; got != want { - t.Fatalf("encodeCurrentCastlePackedStringComponents(nil) = %q, want %q", got, want) - } - if got, want := encodeCurrentCastlePackedStringComponents([]string{"a", "bb"}), "To4Q9k6OHdBL6Evo"; got != want { - t.Fatalf("encodeCurrentCastlePackedStringComponents(sample) = %q, want %q", got, want) - } - if got, want := encodeCurrentCastlePackedStringComponents([]string{"", "", ""}), "tUeqPqo+qj4="; got != want { - t.Fatalf("encodeCurrentCastlePackedStringComponents(empty3) = %q, want %q", got, want) - } - - payload := newCurrentCastlePayloadScaffold() - if err := populateCurrentCastlePackedStringSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastlePackedStringSlots() error = %v", err) - } - if got, want := payload[0].(string), 192; len(got) != want { - t.Fatalf("payload[0] len = %d, want %d", len(got), want) - } - if got, want := payload[7].(string), 1560; len(got) != want { - t.Fatalf("payload[7] len = %d, want %d", len(got), want) - } - if got, want := payload[119].(string), 96; len(got) != want { - t.Fatalf("payload[119] len = %d, want %d", len(got), want) - } - if err := populateCurrentCastlePackedStringSlots(payload, map[int][]string{0: {"a", "bb"}, 7: {"a", "bb"}, 119: {"a", "bb"}}); err != nil { - t.Fatalf("populateCurrentCastlePackedStringSlots() with override error = %v", err) - } - if got, want := payload[0], "To4Q9k6OHdBL6Evo"; got != want { - t.Fatalf("payload[0] override = %q, want %q", got, want) - } - if got, want := payload[7], "To4Q9k6OHdBL6Evo"; got != want { - t.Fatalf("payload[7] override = %q, want %q", got, want) - } - if got, want := payload[119], "To4Q9k6OHdBL6Evo"; got != want { - t.Fatalf("payload[119] override = %q, want %q", got, want) - } - if err := populateCurrentCastlePackedStringSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { - t.Fatalf("populateCurrentCastlePackedStringSlots() with short payload succeeded") - } -} - -func TestCurrentCastleUnitPackedStringSlotEncoders(t *testing.T) { - if got, want := encodeCurrentCastleSlot8Primary("s:1"), "ASz5"; got != want { - t.Fatalf("encodeCurrentCastleSlot8Primary() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot8Secondary("s:1"), "SKnA"; got != want { - t.Fatalf("encodeCurrentCastleSlot8Secondary() = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot8Primary("abc"), "6wur"; got != want { - t.Fatalf("encodeCurrentCastleSlot8Primary(sample) = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleSlot8Secondary("abc"), "dpa2"; got != want { - t.Fatalf("encodeCurrentCastleSlot8Secondary(sample) = %q, want %q", got, want) - } - if got, want := encodeCurrentCastleUnitPackedStringComponents([]string{"a", "bb"}), "RaK7+kWies4EdgR2"; got != want { - t.Fatalf("encodeCurrentCastleUnitPackedStringComponents(sample) = %q, want %q", got, want) - } - - payload := newCurrentCastlePayloadScaffold() - if err := populateCurrentCastleUnitPackedStringSlots(payload, nil); err != nil { - t.Fatalf("populateCurrentCastleUnitPackedStringSlots() error = %v", err) - } - if got, want := payload[8].(string), 52; len(got) != want { - t.Fatalf("payload[8] len = %d, want %d", len(got), want) - } - if err := populateCurrentCastleUnitPackedStringSlots(payload, map[int][]string{8: {"a", "bb"}}); err != nil { - t.Fatalf("populateCurrentCastleUnitPackedStringSlots() with override error = %v", err) - } - if got, want := payload[8], "RaK7+kWies4EdgR2"; got != want { - t.Fatalf("payload[8] override = %q, want %q", got, want) - } - if err := populateCurrentCastleUnitPackedStringSlots(make([]any, currentCastlePayloadSlots-1), nil); err == nil { - t.Fatalf("populateCurrentCastleUnitPackedStringSlots() with short payload succeeded") - } -} - -func TestCurrentCastlePayloadScaffold(t *testing.T) { - payload := newCurrentCastlePayloadScaffold() - if len(payload) != currentCastlePayloadSlots { - t.Fatalf("newCurrentCastlePayloadScaffold() len = %d, want %d", len(payload), currentCastlePayloadSlots) - } - for slot, value := range payload { - if value == nil { - t.Fatalf("payload[%d] is nil", slot) - } - } - if err := populateCurrentCastleAutomationSlot(payload, 0x1353003b); err != nil { - t.Fatalf("populateCurrentCastleAutomationSlot() error = %v", err) - } - if err := populateCurrentCastleHighTimingSlots(payload, map[int]float64{ - 431: 0, - 480: 123.456, - 493: 1, - }); err != nil { - t.Fatalf("populateCurrentCastleHighTimingSlots() error = %v", err) - } - payloadJSON, err := marshalCurrentCastlePayload(payload) - if err != nil { - t.Fatalf("marshalCurrentCastlePayload() error = %v", err) - } - if strings.Contains(payloadJSON, "null") { - t.Fatalf("payload JSON contains null: %s", payloadJSON) - } - var decoded []any - if err = json.Unmarshal([]byte(payloadJSON), &decoded); err != nil { - t.Fatalf("payload JSON did not unmarshal: %v", err) - } - if len(decoded) != currentCastlePayloadSlots { - t.Fatalf("decoded payload len = %d, want %d", len(decoded), currentCastlePayloadSlots) - } - if got, want := uint32(decoded[2].(float64)), uint32(2115052381); got != want { - t.Fatalf("decoded payload[2] = %d, want %d", got, want) - } - if got, want := decoded[431], "xsbGxsbGxsY="; got != want { - t.Fatalf("decoded payload[431] = %q, want %q", got, want) - } - if got, want := decoded[480], "hhjbKVyZ+PE="; got != want { - t.Fatalf("decoded payload[480] = %q, want %q", got, want) - } - token, err := createCurrentCastleWrappedPayloadToken(payload, 1760000000123) - if err != nil { - t.Fatalf("createCurrentCastleWrappedPayloadToken() error = %v", err) - } - wire, err := decodeCurrentCastleWrappedTokenForTest(token) - if err != nil { - t.Fatalf("decodeCurrentCastleWrappedTokenForTest() error = %v", err) - } - encodedTimestampLen := int(wire[0]) - decodedPayload := xorCurrentCastleString(wire[1+encodedTimestampLen:], "1760000000123") - if strings.Contains(decodedPayload, "null") { - t.Fatalf("wrapped payload contains null: %s", decodedPayload) - } - if _, err = marshalCurrentCastlePayload(payload[:currentCastlePayloadSlots-1]); err == nil { - t.Fatalf("marshalCurrentCastlePayload() with short payload succeeded") - } - payload[7] = nil - if _, err = marshalCurrentCastlePayload(payload); err == nil { - t.Fatalf("marshalCurrentCastlePayload() with nil slot succeeded") - } - if err = populateCurrentCastleAutomationSlot(make([]any, 2), 0); err == nil { - t.Fatalf("populateCurrentCastleAutomationSlot() with short payload succeeded") - } -} - -func TestCurrentCastlePayloadScaffoldAcceptedTypes(t *testing.T) { - payload := newCurrentCastlePayloadScaffold() - typeCounts := map[string]int{} - numericIndexes := make([]int, 0) - objectIndexes := make([]int, 0) - booleanIndexes := make([]int, 0) - for index, value := range payload { - switch value.(type) { - case int, int64, uint32, float64: - typeCounts["number"]++ - numericIndexes = append(numericIndexes, index) - case map[string]any: - typeCounts["object"]++ - objectIndexes = append(objectIndexes, index) - case bool: - typeCounts["boolean"]++ - booleanIndexes = append(booleanIndexes, index) - case string: - typeCounts["string"]++ - default: - t.Fatalf("payload[%d] has unexpected type %T", index, value) - } - } - if got, want := typeCounts["string"], 355; got != want { - t.Fatalf("string slot count = %d, want %d", got, want) - } - if got, want := typeCounts["number"], 125; got != want { - t.Fatalf("number slot count = %d, want %d", got, want) - } - if got, want := typeCounts["object"], 13; got != want { - t.Fatalf("object slot count = %d, want %d", got, want) - } - if got, want := typeCounts["boolean"], 1; got != want { - t.Fatalf("boolean slot count = %d, want %d", got, want) - } - if !sameInts(numericIndexes, currentCastleAcceptedNumericSlots) { - t.Fatalf("numeric slots = %#v, want %#v", numericIndexes, currentCastleAcceptedNumericSlots) - } - if !sameInts(objectIndexes, currentCastleAcceptedObjectSlots) { - t.Fatalf("object slots = %#v, want %#v", objectIndexes, currentCastleAcceptedObjectSlots) - } - if !sameInts(booleanIndexes, []int{currentCastleAcceptedBooleanSlot}) { - t.Fatalf("boolean slots = %#v, want [%d]", booleanIndexes, currentCastleAcceptedBooleanSlot) - } - data, err := json.Marshal(payload) - if err != nil { - t.Fatalf("json.Marshal(payload) error = %v", err) - } - if strings.Contains(string(data), "null") { - t.Fatalf("payload JSON contains null: %s", string(data)) - } - var decoded []any - if err = json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("json.Unmarshal(payload) error = %v", err) - } - decodedCounts := map[string]int{} - for _, value := range decoded { - switch value.(type) { - case float64: - decodedCounts["number"]++ - case map[string]any: - decodedCounts["object"]++ - case bool: - decodedCounts["boolean"]++ - case string: - decodedCounts["string"]++ - default: - t.Fatalf("decoded payload has unexpected type %T", value) - } - } - for key, want := range typeCounts { - if got := decodedCounts[key]; got != want { - t.Fatalf("decoded %s count = %d, want %d", key, got, want) - } - } -} - -func TestBuildCurrentCastlePayload(t *testing.T) { - components := make([]string, currentCastleSlot6ComponentCount) - components[0] = encodeCurrentCastleSlot6Component0("ua") - components[1] = encodeCurrentCastleSlot6Component1("probe") - components[2] = encodeCurrentCastleSlot6Component2("en-US") - components[6] = encodeCurrentCastleSlot6TU("window") - components[10] = encodeCurrentCastleSlot6AH(currentCastleSlot6HashHex("function test() { return 1; }")) - components[18] = encodeCurrentCastleSlot6EJ(currentCastleSlot6HashHex("canvas")) - components[39] = encodeCurrentCastleSlot6A8(currentCastleSlot6HashHex("document")) - expectedSlot6, err := encodeCurrentCastleSlot6(components) - if err != nil { - t.Fatalf("encodeCurrentCastleSlot6() error = %v", err) - } - payload, err := buildCurrentCastlePayload(currentCastlePayloadInput{ - AutomationBitfield: 0x1353003b, - Slot6Components: components, - NumericValues: map[int]uint32{ - 20: 1, - 26: 1, - 27: 1, - 68: 1, - 98: 1, - 152: 1, - 154: 1, - 159: 1, - 182: 1, - 203: 1, - 256: 1, - 257: currentCastleTB(1), - 258: 1, - 289: currentCastleTB(1), - 341: 1, - 348: 1, - 374: 1, - 386: 1, - 406: 1, - 428: 1, - 429: 1, - }, - LowerFloatValues: map[int]float64{ - 1: 1, - 3: 123.456, - }, - HighTimingValues: map[int]float64{ - 431: 0, - 480: 123.456, - 493: 1, - }, - }) - if err != nil { - t.Fatalf("buildCurrentCastlePayload() error = %v", err) - } - if len(payload) != currentCastlePayloadSlots { - t.Fatalf("buildCurrentCastlePayload() len = %d, want %d", len(payload), currentCastlePayloadSlots) - } - if got, want := payload[2], uint32(2115052381); got != want { - t.Fatalf("payload[2] = %v, want %v", got, want) - } - if got, want := payload[20], uint32(0x5fd1c84a); got != want { - t.Fatalf("payload[20] = %v, want %v", got, want) - } - if got, want := payload[26], uint32(0x670022bf); got != want { - t.Fatalf("payload[26] = %v, want %v", got, want) - } - if got, want := payload[68], uint32(0x76dc5125); got != want { - t.Fatalf("payload[68] = %v, want %v", got, want) - } - if got, want := payload[98], uint32(0xb0a64635); got != want { - t.Fatalf("payload[98] = %v, want %v", got, want) - } - if got, want := payload[154], uint32(0xf13ce181); got != want { - t.Fatalf("payload[154] = %v, want %v", got, want) - } - if got, want := payload[77].(map[string]any)["AFS"], "JcW/1uh5mugs0/qG3eXd5Q=="; got != want { - t.Fatalf("payload[77].AFS = %v, want %v", got, want) - } - if got, want := payload[99].(map[string]any)["n"], ""; got != want { - t.Fatalf("payload[99].n = %v, want %v", got, want) - } - if got, want := payload[99].(map[string]any)["cOw"], ""; got != want { - t.Fatalf("payload[99].cOw = %v, want %v", got, want) - } - if got, want := payload[100].(map[string]any)["rj"], "5e4bQb/W"; got != want { - t.Fatalf("payload[100].rj = %v, want %v", got, want) - } - if got, want := payload[100].(map[string]any)["KwJ"].(map[string]any)["T"], "CzejbOYagxFvwg=="; got != want { - t.Fatalf("payload[100].KwJ.T = %v, want %v", got, want) - } - if got, want := payload[46].(map[string]any)["mx"], ""; got != want { - t.Fatalf("payload[46].mx = %v, want %v", got, want) - } - if got, want := payload[128].(map[string]any)["ekC"], "jM4d0BtBVjEbQeXusaE="; got != want { - t.Fatalf("payload[128].ekC = %v, want %v", got, want) - } - if got, want := payload[128].(map[string]any)["fCH"], "8Dc="; got != want { - t.Fatalf("payload[128].fCH = %v, want %v", got, want) - } - if got, want := payload[173].(map[string]any)["t"], "gY/mGnrO"; got != want { - t.Fatalf("payload[173].t = %v, want %v", got, want) - } - if got, want := payload[173].(map[string]any)["JZX"], "rImUhLGh8DdWMQ=="; got != want { - t.Fatalf("payload[173].JZX = %v, want %v", got, want) - } - if got, want := payload[256], uint32(0x4ea7245b); got != want { - t.Fatalf("payload[256] = %v, want %v", got, want) - } - if got, want := payload[257], uint32(0x3e5af065); got != want { - t.Fatalf("payload[257] = %v, want %v", got, want) - } - if got, want := payload[289], uint32(0x00021d69); got != want { - t.Fatalf("payload[289] = %v, want %v", got, want) - } - if got, want := payload[214].(map[string]any)["h"], "LRTQyw=="; got != want { - t.Fatalf("payload[214].h = %v, want %v", got, want) - } - if got, want := payload[214].(map[string]any)["yhj"], "S+hTpr/W8DdL6PA3"; got != want { - t.Fatalf("payload[214].yhj = %v, want %v", got, want) - } - if got, want := payload[214].(map[string]any)["XI"], ""; got != want { - t.Fatalf("payload[214].XI = %v, want %v", got, want) - } - if got, want := payload[290].(map[string]any)["d"], "LRRvwgs3+WmjbOYaCzcMuQ=="; got != want { - t.Fatalf("payload[290].d = %v, want %v", got, want) - } - if got, want := payload[348], uint32(0x4ff13582); got != want { - t.Fatalf("payload[348] = %v, want %v", got, want) - } - if got, want := payload[352].(map[string]any)["XYw"], "gY/mGnrO0MsLN5Ad9+ejbA=="; got != want { - t.Fatalf("payload[352].XYw = %v, want %v", got, want) - } - if got, want := payload[27], uint32(0x0dd8798d); got != want { - t.Fatalf("payload[27] = %v, want %v", got, want) - } - if got, want := payload[152], uint32(0x0160726c); got != want { - t.Fatalf("payload[152] = %v, want %v", got, want) - } - if got, want := payload[159], uint32(1); got != want { - t.Fatalf("payload[159] = %v, want %v", got, want) - } - if got, want := payload[182], uint32(0x000003df); got != want { - t.Fatalf("payload[182] = %v, want %v", got, want) - } - if got, want := payload[203], uint32(0xc14d992a); got != want { - t.Fatalf("payload[203] = %v, want %v", got, want) - } - if got, want := payload[258], uint32(0xceb0120a); got != want { - t.Fatalf("payload[258] = %v, want %v", got, want) - } - if got, want := payload[341], uint32(0x0000793c); got != want { - t.Fatalf("payload[341] = %v, want %v", got, want) - } - if got, want := payload[342].(map[string]any)["kFx"], "CzfQy+YaCzdac9DLLRSQHQ=="; got != want { - t.Fatalf("payload[342].kFx = %v, want %v", got, want) - } - if got, want := payload[374], uint32(0x5ed52a3b); got != want { - t.Fatalf("payload[374] = %v, want %v", got, want) - } - if got, want := payload[386], uint32(0x78b37579); got != want { - t.Fatalf("payload[386] = %v, want %v", got, want) - } - if got, want := payload[406], uint32(0x4da110bf); got != want { - t.Fatalf("payload[406] = %v, want %v", got, want) - } - if got, want := payload[416].(map[string]any)["y"], "LRR6zlpzo2xac/lpkB335w=="; got != want { - t.Fatalf("payload[416].y = %v, want %v", got, want) - } - if got, want := payload[424].(map[string]any)["kr"], "jM4d0BtBVjE="; got != want { - t.Fatalf("payload[424].kr = %v, want %v", got, want) - } - if got, want := payload[428], uint32(0xc0f29076); got != want { - t.Fatalf("payload[428] = %v, want %v", got, want) - } - if got, want := payload[429], uint32(0x57fefa07); got != want { - t.Fatalf("payload[429] = %v, want %v", got, want) - } - if got := payload[6]; got != expectedSlot6 { - t.Fatalf("payload[6] = %q, want %q", got, expectedSlot6) - } - if got, want := payload[1], "KWfg4ODg4OA="; got != want { - t.Fatalf("payload[1] = %q, want %q", got, want) - } - if got, want := payload[3], "agkBHM1lE+c="; got != want { - t.Fatalf("payload[3] = %q, want %q", got, want) - } - if got, want := payload[4], "7Ozs7Ozs7Ow="; got != want { - t.Fatalf("payload[4] = %q, want %q", got, want) - } - if got, want := payload[0].(string), 192; len(got) != want { - t.Fatalf("payload[0] len = %d, want %d", len(got), want) - } - if got, want := payload[7].(string), 1560; len(got) != want { - t.Fatalf("payload[7] len = %d, want %d", len(got), want) - } - if got, want := payload[8].(string), 52; len(got) != want { - t.Fatalf("payload[8] len = %d, want %d", len(got), want) - } - if got, want := payload[254], encodeCurrentCastleArraySlot254(currentCastleFloat64Samples(256, 199)); got != want { - t.Fatalf("payload[254] = %q, want %q", got, want) - } - if got, want := payload[302], encodeCurrentCastleArraySlot302(currentCastleFloat64Samples(16, 13)); got != want { - t.Fatalf("payload[302] = %q, want %q", got, want) - } - if got, want := payload[119].(string), 96; len(got) != want { - t.Fatalf("payload[119] len = %d, want %d", len(got), want) - } - if got, want := payload[431], "xsbGxsbGxsY="; got != want { - t.Fatalf("payload[431] = %q, want %q", got, want) - } - if got, want := payload[480], "hhjbKVyZ+PE="; got != want { - t.Fatalf("payload[480] = %q, want %q", got, want) - } - if got, want := payload[493], "OTbGxsbGxsY="; got != want { - t.Fatalf("payload[493] = %q, want %q", got, want) - } - payloadJSON, err := marshalCurrentCastlePayload(payload) - if err != nil { - t.Fatalf("marshalCurrentCastlePayload() error = %v", err) - } - if strings.Contains(payloadJSON, "null") { - t.Fatalf("payload JSON contains null: %s", payloadJSON) - } - var decoded []any - if err = json.Unmarshal([]byte(payloadJSON), &decoded); err != nil { - t.Fatalf("payload JSON did not unmarshal: %v", err) - } - if got := decoded[6]; got != expectedSlot6 { - t.Fatalf("decoded payload[6] = %q, want %q", got, expectedSlot6) - } - token, err := createCurrentCastleWrappedPayloadToken(payload, 1760000000123) - if err != nil { - t.Fatalf("createCurrentCastleWrappedPayloadToken() error = %v", err) - } - wire, err := decodeCurrentCastleWrappedTokenForTest(token) - if err != nil { - t.Fatalf("decodeCurrentCastleWrappedTokenForTest() error = %v", err) - } - encodedTimestampLen := int(wire[0]) - decodedPayload := xorCurrentCastleString(wire[1+encodedTimestampLen:], "1760000000123") - if !strings.Contains(decodedPayload, expectedSlot6) { - t.Fatalf("wrapped payload missing slot 6 value") - } - if strings.Contains(decodedPayload, "null") { - t.Fatalf("wrapped payload contains null: %s", decodedPayload) - } - if _, err = buildCurrentCastlePayload(currentCastlePayloadInput{ - AutomationBitfield: 0x1353003b, - Slot6Components: components[:len(components)-1], - }); err == nil { - t.Fatalf("buildCurrentCastlePayload() with short slot 6 components succeeded") - } -} - -func TestCurrentCastleLengthPrefixUnits(t *testing.T) { - cases := []struct { - name string - input uint32 - output []uint16 - }{ - {name: "zero", input: 0, output: []uint16{0}}, - {name: "component count", input: 47, output: []uint16{47}}, - {name: "single max", input: 0x7fff, output: []uint16{0x7fff}}, - {name: "first continuation", input: 0x8000, output: []uint16{0x8000, 0x0001}}, - {name: "continuation with payload bits", input: 0x8001, output: []uint16{0x8001, 0x0001}}, - {name: "three units", input: 0x40000000, output: []uint16{0x8000, 0x8000, 0x0001}}, - {name: "uint32 max", input: 0xffffffff, output: []uint16{0xffff, 0xffff, 0x0003}}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - if got := currentCastleLengthPrefixUnits(tc.input); !sameUint16s(got, tc.output) { - t.Fatalf("currentCastleLengthPrefixUnits(%#x) = %#v, want %#v", tc.input, got, tc.output) - } - }) - } -} - -func TestCurrentCastleSlot6LengthPrefixUnits(t *testing.T) { - lengths := make([]int, currentCastleSlot6ComponentCount) - for i := range lengths { - lengths[i] = i - } - lengths[1] = 0x8000 - units, err := currentCastleSlot6LengthPrefixUnits(lengths) - if err != nil { - t.Fatalf("currentCastleSlot6LengthPrefixUnits() error = %v", err) - } - wantPrefix := []uint16{47, 0, 0x8000, 1, 2, 3, 4} - if !sameUint16s(units[:len(wantPrefix)], wantPrefix) { - t.Fatalf("slot 6 prefix start = %#v, want %#v", units[:len(wantPrefix)], wantPrefix) - } - if _, err = currentCastleSlot6LengthPrefixUnits(lengths[:len(lengths)-1]); err == nil { - t.Fatalf("currentCastleSlot6LengthPrefixUnits() with short component list succeeded") - } - lengths[7] = -1 - if _, err = currentCastleSlot6LengthPrefixUnits(lengths); err == nil { - t.Fatalf("currentCastleSlot6LengthPrefixUnits() with negative component length succeeded") - } -} - -func TestCurrentCastleSlot6RawUnits(t *testing.T) { - components := make([]string, currentCastleSlot6ComponentCount) - components[0] = "A" - components[1] = "BC" - units, err := currentCastleSlot6RawUnits(components) - if err != nil { - t.Fatalf("currentCastleSlot6RawUnits() error = %v", err) - } - wantPrefix := []uint16{47, 1, 2, 0, 0} - if !sameUint16s(units[:len(wantPrefix)], wantPrefix) { - t.Fatalf("slot 6 raw prefix = %#v, want %#v", units[:len(wantPrefix)], wantPrefix) - } - wantSuffix := []uint16{'A', 'B', 'C'} - if !sameUint16s(units[len(units)-len(wantSuffix):], wantSuffix) { - t.Fatalf("slot 6 raw suffix = %#v, want %#v", units[len(units)-len(wantSuffix):], wantSuffix) - } - if len(units) != currentCastleSlot6ComponentCount+1+len(wantSuffix) { - t.Fatalf("slot 6 raw unit len = %d, want %d", len(units), currentCastleSlot6ComponentCount+1+len(wantSuffix)) - } - encoded, err := encodeCurrentCastleSlot6(components) - if err != nil { - t.Fatalf("encodeCurrentCastleSlot6() error = %v", err) - } - const wantEncoded = "RyS7+kWiMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTMFMwUzBTO93FhE8s" - if encoded != wantEncoded { - t.Fatalf("encodeCurrentCastleSlot6() = %q, want %q", encoded, wantEncoded) - } - if _, err = currentCastleSlot6RawUnits(components[:len(components)-1]); err == nil { - t.Fatalf("currentCastleSlot6RawUnits() with short components succeeded") - } - if _, err = encodeCurrentCastleSlot6(components[:len(components)-1]); err == nil { - t.Fatalf("encodeCurrentCastleSlot6() with short components succeeded") - } -} - -func TestEncodeCurrentCastleSlot6ComponentValues(t *testing.T) { - values := make([]string, currentCastleSlot6ComponentCount) - values[0] = "toString" - values[1] = "TypeError: Cyclic __proto__ value" - values[2] = "America/Chicago" - values[3] = "ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Ti (0x00001B06) Direct3D11 vs_5_0 ps_5_0, D3D11)" - values[5] = currentCastleSlot6HashHex("function test() { return 1; }") - values[6] = "landscape-primary" - values[8] = "RangeError" - values[12] = "probably" - values[13] = "maybe" - values[14] = "1111111111111111111111111111111111111000000000" - values[15] = "Cannot read properties of undefined (reading 'b')" - values[19] = "091a1f32-3826-4bad-9250-aa14e3c0a2b2" - values[20] = "1000" - values[21] = "Google Inc." - values[22] = values[3] - values[25] = "{}" - values[29] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36" - values[31] = "en-US" - values[34] = "en-US" - values[36] = "Win32" - values[41] = "x.com" - values[44] = "en-US,en" - values[45] = "probably" - components, err := encodeCurrentCastleSlot6ComponentValues(values) - if err != nil { - t.Fatalf("encodeCurrentCastleSlot6ComponentValues() error = %v", err) - } - if len(components) != currentCastleSlot6ComponentCount { - t.Fatalf("components len = %d, want %d", len(components), currentCastleSlot6ComponentCount) - } - cases := map[int]string{ - 0: encodeCurrentCastleSlot6Component0(values[0]), - 1: encodeCurrentCastleSlot6Component1(values[1]), - 2: encodeCurrentCastleSlot6Component2(values[2]), - 3: encodeCurrentCastleSlot6UZ(values[3]), - 5: encodeCurrentCastleSlot6Component5Fallback(values[5]), - 6: encodeCurrentCastleSlot6TU(values[6]), - 8: encodeCurrentCastleSlot6FJ(values[8]), - 12: encodeCurrentCastleSlot6AG(values[12]), - 13: encodeCurrentCastleSlot6AD(values[13]), - 14: encodeCurrentCastleSlot6T7(values[14]), - 15: encodeCurrentCastleSlot6ID(values[15]), - 19: encodeCurrentCastleSlot6Component19(values[19]), - 20: encodeCurrentCastleSlot6L9(values[20]), - 21: encodeCurrentCastleSlot6TX(values[21]), - 22: encodeCurrentCastleSlot6LI(values[22]), - 25: encodeCurrentCastleSlot6F3(values[25]), - 29: encodeCurrentCastleSlot6TUpperF(values[29]), - 31: encodeCurrentCastleSlot6Component31(values[31]), - 34: encodeCurrentCastleSlot6AQ(values[34]), - 36: encodeCurrentCastleSlot6TUpperM(values[36]), - 41: encodeCurrentCastleSlot6EA(values[41]), - 44: encodeCurrentCastleSlot6TUpperX(values[44]), - 45: encodeCurrentCastleSlot6L5(values[45]), - } - for index, want := range cases { - if got := components[index]; got != want { - t.Fatalf("components[%d] = %q, want %q", index, got, want) - } - } - slot6, err := encodeCurrentCastleSlot6(components) - if err != nil { - t.Fatalf("encodeCurrentCastleSlot6() error = %v", err) - } - units, err := currentCastleSlot6RawUnits(components) - if err != nil { - t.Fatalf("currentCastleSlot6RawUnits() error = %v", err) - } - if slot6 != encodeCurrentCastleUnits(units) { - t.Fatalf("slot 6 encoding mismatch") - } - if _, err = encodeCurrentCastleSlot6ComponentValues(values[:len(values)-1]); err == nil { - t.Fatalf("encodeCurrentCastleSlot6ComponentValues() with short values succeeded") - } -} - -func TestBuildCurrentCastleSlot6RawValues(t *testing.T) { - fp := currentCastleSlot6Fingerprint{ - TimestampMillis: 1782425262310, - Timezone: "America/Chicago", - WebGLRenderer: "ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Ti (0x00001B06) Direct3D11 vs_5_0 ps_5_0, D3D11)", - WebGLVendor: "Google Inc.", - Orientation: "landscape-primary", - UserAgent: UserAgent, - Language: "en-US", - Languages: "en-US,en", - Platform: "Win32", - Host: "x.com", - PublicKey: castlePublicKey, - ClientUUID: "091a1f32-3826-4bad-9250-aa14e3c0a2b2", - FeatureBits: currentCastleDefaultFeatureBits, - PrecisionProbe: "1000", - AudioProbe: "20030107", - ProbeDateString: "3/3/1970, 6:00:00 PM", - NumericProbe: "51833740944", - WASMProbeHex: "c0ffee", - Hash5: "39a01d2b", - Hash7: "bd4be70d", - Hash10: "45eb4280", - Hash11: "8fc90f7d", - Hash16: "5d55117f", - Hash18: "42d68d60", - Hash23: "34b454a7", - Hash39: "db77b610", - } - values := buildCurrentCastleSlot6RawValues(fp) - if len(values) != currentCastleSlot6ComponentCount { - t.Fatalf("raw values len = %d, want %d", len(values), currentCastleSlot6ComponentCount) - } - cases := map[int]string{ - 0: "toString", - 1: "TypeError: Cyclic __proto__ value", - 2: fp.Timezone, - 3: fp.WebGLRenderer, - 5: fp.Hash5, - 6: fp.Orientation, - 7: fp.Hash7, - 8: "RangeError", - 9: "[]", - 10: fp.Hash10, - 11: fp.Hash11, - 12: "probably", - 13: "maybe", - 14: fp.FeatureBits, - 15: "Cannot read properties of undefined (reading 'b')", - 16: fp.Hash16, - 18: fp.Hash18, - 19: fp.ClientUUID, - 20: fp.PrecisionProbe, - 21: fp.WebGLVendor, - 22: fp.WebGLRenderer, - 23: fp.Hash23, - 24: fp.AudioProbe, - 25: "{}", - 27: fp.PublicKey, - 28: "Illegal invocation", - 29: fp.UserAgent, - 30: "r:1", - 31: fp.Language, - 33: strconv.FormatInt(currentCastleSlot6ComponentTimestampMillis(fp.TimestampMillis), 10), - 34: fp.Language, - 35: "Maximum call stack size exceeded", - 36: fp.Platform, - 37: fp.ProbeDateString, - 38: "r:1", - 39: fp.Hash39, - 41: fp.Host, - 43: fp.WASMProbeHex, - 44: fp.Languages, - 45: "probably", - 46: fp.NumericProbe, - } - for index, want := range cases { - if got := values[index]; got != want { - t.Fatalf("values[%d] = %q, want %q", index, got, want) - } - } - for _, index := range []int{4, 17, 26, 32, 40, 42} { - if values[index] != "" { - t.Fatalf("values[%d] = %q, want empty", index, values[index]) - } - } - - components, err := buildCurrentCastleSlot6Components(fp) - if err != nil { - t.Fatalf("buildCurrentCastleSlot6Components() error = %v", err) - } - if got, want := components[2], encodeCurrentCastleSlot6Component2(fp.Timezone); got != want { - t.Fatalf("components[2] = %q, want %q", got, want) - } - if got, want := components[19], encodeCurrentCastleSlot6Component19(fp.ClientUUID); got != want { - t.Fatalf("components[19] = %q, want %q", got, want) - } - if got, want := components[27], encodeCurrentCastleSlot6Component27(fp.PublicKey); got != want { - t.Fatalf("components[27] = %q, want %q", got, want) - } -} - -func TestDefaultCurrentCastleSlot6Fingerprint(t *testing.T) { - t.Setenv("TWITTER_JETFUEL_TIMEZONE", "America/Chicago") - const timestampMillis = int64(1782425262310) - const clientUUID = "091a1f32-3826-4bad-9250-aa14e3c0a2b2" - - fp := defaultCurrentCastleSlot6Fingerprint(timestampMillis, clientUUID) - values := buildCurrentCastleSlot6RawValues(fp) - if got, want := values[2], "America/Chicago"; got != want { - t.Fatalf("values[2] = %q, want %q", got, want) - } - if got, want := values[19], clientUUID; got != want { - t.Fatalf("values[19] = %q, want %q", got, want) - } - if got, want := values[21], currentCastleDefaultWebGLVendor; got != want { - t.Fatalf("values[21] = %q, want %q", got, want) - } - if got, want := values[22], currentCastleDefaultWebGLRenderer; got != want { - t.Fatalf("values[22] = %q, want %q", got, want) - } - if got, want := values[27], castlePublicKey; got != want { - t.Fatalf("values[27] = %q, want %q", got, want) - } - if got, want := values[29], UserAgent; got != want { - t.Fatalf("values[29] = %q, want %q", got, want) - } - if got, want := values[33], strconv.FormatInt(currentCastleSlot6ComponentTimestampMillis(timestampMillis), 10); got != want { - t.Fatalf("values[33] = %q, want %q", got, want) - } - if got, want := values[37], "3/3/1970, 6:00:00 PM"; got != want { - t.Fatalf("values[37] = %q, want %q", got, want) - } - exactDefaults := map[int]string{ - 5: currentCastleDefaultHash5, - 7: currentCastleDefaultHash7, - 10: currentCastleDefaultHash10, - 11: currentCastleDefaultHash11, - 16: currentCastleDefaultHash16, - 18: currentCastleDefaultHash18, - 23: currentCastleDefaultHash23, - 39: currentCastleDefaultHash39, - 43: currentCastleDefaultWASMProbeHex, - 46: currentCastleDefaultNumericProbe, - } - for index, want := range exactDefaults { - if got := values[index]; got != want { - t.Fatalf("values[%d] = %q, want %q", index, got, want) - } - } - components, err := buildCurrentCastleSlot6Components(fp) - if err != nil { - t.Fatalf("buildCurrentCastleSlot6Components() error = %v", err) - } - payload, err := buildCurrentCastlePayload(currentCastlePayloadInput{ - AutomationBitfield: currentCastleAcceptedAutomationBitfield, - Slot6Components: components, - }) - if err != nil { - t.Fatalf("buildCurrentCastlePayload() error = %v", err) - } - if got, want := payload[2], uint32(2115052381); got != want { - t.Fatalf("payload[2] = %v, want %v", got, want) - } - if payload[6] == "" { - t.Fatalf("payload[6] is empty") - } - token, err := createCurrentCastleWrappedPayloadToken(payload, timestampMillis) - if err != nil { - t.Fatalf("createCurrentCastleWrappedPayloadToken() error = %v", err) - } - if !strings.HasPrefix(token, currentCastleTokenPrefix) { - t.Fatalf("token missing current Castle prefix") - } -} - -func TestCreateCurrentCastleRequestToken(t *testing.T) { - t.Setenv("TWITTER_JETFUEL_TIMEZONE", "America/Chicago") - token, err := createCurrentCastleRequestToken("091a1f32-3826-4bad-9250-aa14e3c0a2b2") - if err != nil { - t.Fatalf("createCurrentCastleRequestToken() error = %v", err) - } - if !strings.HasPrefix(token, currentCastleTokenPrefix) { - t.Fatalf("token missing current Castle prefix") - } - t.Logf("current Castle token length: %d", len(token)) - if len(token) < 1000 { - t.Fatalf("token length = %d, want a current-format request token", len(token)) - } - if _, err = decodeCurrentCastleWrappedTokenForTest(token); err != nil { - t.Fatalf("decodeCurrentCastleWrappedTokenForTest() error = %v", err) - } -} - -func TestCompareChromeCurrentCastleTokenProbe(t *testing.T) { - token := strings.TrimSpace(os.Getenv("TWITTER_CASTLE_TOKEN_DIFF_PROBE")) - if token == "" { - t.Skip("TWITTER_CASTLE_TOKEN_DIFF_PROBE is required") - } - chromePayload, timestampMillis := decodeCurrentCastlePayloadFromTokenForTest(t, token) - components, err := buildCurrentCastleSlot6Components(defaultCurrentCastleSlot6Fingerprint(timestampMillis, "091a1f32-3826-4bad-9250-aa14e3c0a2b2")) - if err != nil { - t.Fatalf("buildCurrentCastleSlot6Components() error = %v", err) - } - goPayload, err := buildCurrentCastlePayload(currentCastlePayloadInput{ - AutomationBitfield: currentCastleAcceptedAutomationBitfield, - Slot6Components: components, - }) - if err != nil { - t.Fatalf("buildCurrentCastlePayload() error = %v", err) - } - if len(chromePayload) != len(goPayload) { - t.Fatalf("payload lengths: chrome=%d go=%d", len(chromePayload), len(goPayload)) - } - typeCounts := map[string]int{} - mismatches := make([]string, 0) - for i := range chromePayload { - typeCounts[fmt.Sprintf("chrome_%s", currentCastleJSONTypeForTest(chromePayload[i]))]++ - typeCounts[fmt.Sprintf("go_%s", currentCastleJSONTypeForTest(goPayload[i]))]++ - chromeJSON := currentCastleJSONSummaryForTest(t, chromePayload[i]) - goJSON := currentCastleJSONSummaryForTest(t, goPayload[i]) - if chromeJSON != goJSON { - mismatches = append(mismatches, fmt.Sprintf( - "%d:%s/%d/%s -> %s/%d/%s", - i, - currentCastleJSONTypeForTest(chromePayload[i]), - len(chromeJSON), - currentCastleShortHashForTest(chromeJSON), - currentCastleJSONTypeForTest(goPayload[i]), - len(goJSON), - currentCastleShortHashForTest(goJSON), - )) - } - } - sort.Strings(mismatches) - t.Logf("decoded timestamp millis: %d", timestampMillis) - t.Logf("type counts: %v", typeCounts) - t.Logf("mismatch count: %d", len(mismatches)) - limit := min(len(mismatches), 120) - for _, mismatch := range mismatches[:limit] { - t.Logf("mismatch %s", mismatch) - } - if len(mismatches) > limit { - t.Logf("mismatch output truncated: %d more", len(mismatches)-limit) - } - if slots := strings.TrimSpace(os.Getenv("TWITTER_CASTLE_TOKEN_DIFF_VALUE_SLOTS")); slots != "" { - for _, part := range strings.Split(slots, ",") { - part = strings.TrimSpace(part) - if part == "" { - continue - } - slot, err := strconv.Atoi(part) - if err != nil { - t.Fatalf("invalid slot %q in TWITTER_CASTLE_TOKEN_DIFF_VALUE_SLOTS: %v", part, err) - } - if slot < 0 || slot >= len(chromePayload) { - t.Fatalf("slot %d outside payload length %d", slot, len(chromePayload)) - } - chromeString, chromeOK := chromePayload[slot].(string) - goString, goOK := goPayload[slot].(string) - if !chromeOK || !goOK { - t.Logf("value %d chrome_type=%s go_type=%s", slot, currentCastleJSONTypeForTest(chromePayload[slot]), currentCastleJSONTypeForTest(goPayload[slot])) - continue - } - t.Logf("value %d chrome=%q go=%q", slot, chromeString, goString) - } - } - if slots := strings.TrimSpace(os.Getenv("TWITTER_CASTLE_TOKEN_DIFF_ARRAY_SLOTS")); slots != "" { - for _, part := range strings.Split(slots, ",") { - part = strings.TrimSpace(part) - if part == "" { - continue - } - slot, err := strconv.Atoi(part) - if err != nil { - t.Fatalf("invalid slot %q in TWITTER_CASTLE_TOKEN_DIFF_ARRAY_SLOTS: %v", part, err) - } - chromeString, ok := chromePayload[slot].(string) - if !ok { - t.Logf("array %d chrome_type=%s", slot, currentCastleJSONTypeForTest(chromePayload[slot])) - continue - } - values := decodeCurrentCastleFloat64ArraySlotForTest(t, slot, chromeString) - nonZero := 0 - firstNonZero := make([]string, 0, 12) - for i, value := range values { - if value == 0 { - continue - } - nonZero++ - if len(firstNonZero) < 12 { - firstNonZero = append(firstNonZero, fmt.Sprintf("%d:%g", i, value)) - } - } - preview := make([]float64, min(16, len(values))) - copy(preview, values[:len(preview)]) - t.Logf("array %d len=%d nonzero=%d first=%v first_nonzero=%v", slot, len(values), nonZero, preview, firstNonZero) - } - } - if os.Getenv("TWITTER_CASTLE_TOKEN_DIFF_SLOT6") != "" { - chromeSlot6, chromeOK := chromePayload[6].(string) - goSlot6, goOK := goPayload[6].(string) - if !chromeOK || !goOK { - t.Logf("slot6 chrome_type=%s go_type=%s", currentCastleJSONTypeForTest(chromePayload[6]), currentCastleJSONTypeForTest(goPayload[6])) - } else { - chromeComponents := decodeCurrentCastleSlot6ComponentsForTest(t, chromeSlot6) - goComponents := decodeCurrentCastleSlot6ComponentsForTest(t, goSlot6) - componentMismatches := make([]string, 0) - for i := range chromeComponents { - chromeJSON := currentCastleJSONSummaryForTest(t, chromeComponents[i]) - goJSON := currentCastleJSONSummaryForTest(t, goComponents[i]) - if chromeJSON != goJSON { - componentMismatches = append(componentMismatches, fmt.Sprintf( - "%d:%d/%s -> %d/%s", - i, - len(chromeComponents[i]), - currentCastleShortHashForTest(chromeJSON), - len(goComponents[i]), - currentCastleShortHashForTest(goJSON), - )) - } - } - t.Logf("slot6 component mismatch count: %d", len(componentMismatches)) - for _, mismatch := range componentMismatches { - t.Logf("slot6 component %s", mismatch) - } - if os.Getenv("TWITTER_CASTLE_TOKEN_DIFF_SLOT6_RAW") != "" { - for i := range chromeComponents { - chromeRaw := decodeCurrentCastleSlot6EncodedComponentForTest(t, i, chromeComponents[i]) - goRaw := decodeCurrentCastleSlot6EncodedComponentForTest(t, i, goComponents[i]) - if chromeRaw != goRaw { - t.Logf("slot6 raw component %d: chrome=%q go=%q", i, chromeRaw, goRaw) - } - } - } - } - } -} - -func decodeCurrentCastlePayloadFromTokenForTest(t *testing.T, token string) ([]any, int64) { - t.Helper() - wire, err := decodeCurrentCastleWrappedTokenForTest(token) - if err != nil { - t.Fatalf("decodeCurrentCastleWrappedTokenForTest() error = %v", err) - } - if len(wire) < 2 { - t.Fatalf("decoded wire too short: %d", len(wire)) - } - encodedTimestampLen := int(wire[0]) - if len(wire) <= 1+encodedTimestampLen { - t.Fatalf("decoded wire missing payload: wire_len=%d encoded_timestamp_len=%d", len(wire), encodedTimestampLen) - } - timestamp := decodeCurrentCastleTimestampForTest(t, wire[1:1+encodedTimestampLen]) - timestampMillis, err := strconv.ParseInt(timestamp, 10, 64) - if err != nil { - t.Fatalf("decoded timestamp %q parse error: %v", timestamp, err) - } - payloadWithChecksum := xorCurrentCastleString(wire[1+encodedTimestampLen:], timestamp) - payloadJSON := stripCurrentCastleChecksumForTest(t, payloadWithChecksum) - var payload []any - if err = json.Unmarshal([]byte(payloadJSON), &payload); err != nil { - t.Fatalf("json.Unmarshal(decoded payload) error = %v", err) - } - return payload, timestampMillis -} - -func decodeCurrentCastleTimestampForTest(t *testing.T, encoded string) string { - t.Helper() - return string(utf16.Decode(decodeCurrentCastleUnitsForTest(t, encoded))) -} - -func decodeCurrentCastleUnitsForTest(t *testing.T, encoded string) []uint16 { - t.Helper() - raw, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - t.Fatalf("base64 Castle units decode error = %v", err) - } - if len(raw)%2 != 0 { - t.Fatalf("encoded Castle units decoded to odd byte length %d", len(raw)) - } - units := make([]uint16, 0, len(raw)/2) - for i := 0; i < len(raw); i += 2 { - n := uint32(raw[i])<<8 | uint32(raw[i+1]) - n = currentCastleRotR16ForTest(n, 5) - n = (n - 60834) & 0xffff - n = currentCastleRotR16ForTest(n, 4) - n = ((n - 385) * 13069) & 0xffff - n = (46488 ^ n) & 0xffff - n = (n + 54655) & 0xffff - units = append(units, uint16(n)) - } - return units -} - -func currentCastleRotR16ForTest(value uint32, shift uint) uint32 { - return ((value >> shift) | (value << (16 - shift))) & 0xffff -} - -func stripCurrentCastleChecksumForTest(t *testing.T, payload string) string { - t.Helper() - re := regexp.MustCompile(regexp.QuoteMeta(currentCastleChecksumLabel) + `[0-9a-f]{8}`) - loc := re.FindStringIndex(payload) - if loc == nil { - t.Fatalf("decoded payload missing checksum marker") - } - return payload[:loc[0]] + payload[loc[1]:] -} - -func currentCastleJSONSummaryForTest(t *testing.T, value any) string { - t.Helper() - data, err := json.Marshal(value) - if err != nil { - t.Fatalf("json.Marshal(summary) error = %v", err) - } - return string(data) -} - -func currentCastleJSONTypeForTest(value any) string { - switch value.(type) { - case string: - return "string" - case float64: - return "number" - case bool: - return "bool" - case []any: - return "array" - case map[string]any: - return "object" - case nil: - return "null" - default: - return fmt.Sprintf("%T", value) - } -} - -func currentCastleShortHashForTest(value string) string { - sum := sha256.Sum256([]byte(value)) - return fmt.Sprintf("%x", sum[:4]) -} - -func decodeCurrentCastleFloat64ArraySlotForTest(t *testing.T, slot int, encoded string) []float64 { - t.Helper() - raw, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - t.Fatalf("base64 slot %d decode error = %v", slot, err) - } - if len(raw)%8 != 0 { - t.Fatalf("slot %d decoded to byte length %d, not divisible by 8", slot, len(raw)) - } - for i, b := range raw { - switch slot { - case 254: - raw[i] = decodeCurrentCastleArraySlot254ByteForTest(b) - case 302: - raw[i] = decodeCurrentCastleArraySlot302ByteForTest(b) - default: - t.Fatalf("unsupported array slot %d", slot) - } - } - values := make([]float64, len(raw)/8) - for i := range values { - values[i] = math.Float64frombits(binary.BigEndian.Uint64(raw[i*8:])) - } - return values -} - -func decodeCurrentCastleArraySlot254ByteForTest(b byte) byte { - n := uint32(b) - n = (n - 195) & 0xff - n = (n * 13) & 0xff // modular inverse of 185*109 mod 256 - n ^= 32 - return byte(n) -} - -func decodeCurrentCastleArraySlot302ByteForTest(b byte) byte { - n := uint32(b) ^ 192 - n = currentCastleRotR8ForTest(n, 4) - n = (n * 23) & 0xff // modular inverse of 167 mod 256 - return byte(n) -} - -func currentCastleRotR8ForTest(value uint32, shift uint) uint32 { - return ((value >> shift) | (value << (8 - shift))) & 0xff -} - -func decodeCurrentCastleSlot6ComponentsForTest(t *testing.T, encoded string) []string { - t.Helper() - units := decodeCurrentCastleUnitsForTest(t, encoded) - index := 0 - count := readCurrentCastleLengthPrefixForTest(t, units, &index) - if count != currentCastleSlot6ComponentCount { - t.Fatalf("slot 6 component count = %d, want %d", count, currentCastleSlot6ComponentCount) - } - lengths := make([]int, count) - for i := range lengths { - lengths[i] = readCurrentCastleLengthPrefixForTest(t, units, &index) - } - components := make([]string, count) - for i, length := range lengths { - if index+length > len(units) { - t.Fatalf("slot 6 component %d length %d overflows decoded unit len %d at %d", i, length, len(units), index) - } - components[i] = string(utf16.Decode(units[index : index+length])) - index += length - } - if index != len(units) { - t.Fatalf("slot 6 decoded %d units but consumed %d", len(units), index) - } - return components -} - -func readCurrentCastleLengthPrefixForTest(t *testing.T, units []uint16, index *int) int { - t.Helper() - length := 0 - shift := 0 - for { - if *index >= len(units) { - t.Fatalf("slot 6 length prefix overflow") - } - unit := units[*index] - *index = *index + 1 - length |= int(unit¤tCastleSlot6LengthMask) << shift - if unit¤tCastleSlot6LengthContinue == 0 { - return length - } - shift += currentCastleSlot6LengthShiftBits - } -} - -func decodeCurrentCastleSlot6EncodedComponentForTest(t *testing.T, index int, encoded string) string { - t.Helper() - if index < 0 || index >= currentCastleSlot6ComponentCount { - t.Fatalf("slot 6 component index %d outside range", index) - } - 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, - } - raw, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - t.Fatalf("base64 slot 6 component %d decode error: %v", index, err) - } - inverse := map[byte]byte{} - for b := 0; b < 256; b++ { - probe, err := base64.StdEncoding.DecodeString(encoders[index](string([]byte{byte(b)}))) - if err != nil { - t.Fatalf("base64 slot 6 component %d inverse probe decode error: %v", index, err) - } - if len(probe) != 1 { - t.Fatalf("slot 6 component %d inverse probe len = %d, want 1", index, len(probe)) - } - inverse[probe[0]] = byte(b) - } - out := make([]byte, len(raw)) - for i, b := range raw { - value, ok := inverse[b] - if !ok { - t.Fatalf("slot 6 component %d byte %d has no inverse for %#x", index, i, b) - } - out[i] = value - } - return string(out) -} - -func decodeCurrentCastleWrappedTokenForTest(token string) (string, error) { - if !strings.HasPrefix(token, currentCastleTokenPrefix) { - return "", errCurrentCastleTestMissingPrefix - } - raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(token, currentCastleTokenPrefix)) - if err != nil { - return "", err - } - reader := flate.NewReader(bytes.NewReader(raw)) - defer reader.Close() - out, err := io.ReadAll(reader) - if err != nil { - return "", err - } - return string(out), nil -} - -func sameUint16s(a, b []uint16) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false - } - } - return true -} - -func sameInts(a, b []int) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false - } - } - return true -} - -var errCurrentCastleTestMissingPrefix = errors.New("current Castle token missing prefix") diff --git a/pkg/twittermeow/castle_web_token_test_helpers_test.go b/pkg/twittermeow/castle_web_token_test_helpers_test.go deleted file mode 100644 index c3086492..00000000 --- a/pkg/twittermeow/castle_web_token_test_helpers_test.go +++ /dev/null @@ -1,234 +0,0 @@ -package twittermeow - -func encodeCurrentCastleSlot0Primary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b += 1 - b = byte(uint16(b) * 7) - b += 52 - return currentCastleRotL8(b, 6) - }) -} - -func encodeCurrentCastleSlot0Secondary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 163) - b = currentCastleRotL8(b, 2) - b = currentCastleRotL8(b, 3) - return currentCastleRotL8(b, 7) - }) -} - -func encodeCurrentCastleSlot0Tertiary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 185) - b = currentCastleRotL8(b, 5) - b += 212 - return b - }) -} - -func encodeCurrentCastleSlot0Quaternary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 223) - b = currentCastleRotL8(b, 5) - b ^= 159 - return b - }) -} - -func encodeCurrentCastleSlot0Quinary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 2) - b += 220 - b = currentCastleRotL8(b, 2) - b = byte(uint16(b) * 31) - return b - }) -} - -func encodeCurrentCastleSlot0Senary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b += 249 - b += 85 - b += 192 - return b - }) -} - -func encodeCurrentCastleSlot7CanvasPrimary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 133) - b += 242 - b = byte(uint16(b) * 215) - b = byte(uint16(b) * 105) - return b - }) -} - -func encodeCurrentCastleSlot7WorkerTiming(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b += 83 - b ^= 38 - b = byte(uint16(b) * 105) - b = byte(uint16(b) * 211) - return b - }) -} - -func encodeCurrentCastleSlot7MediaRatio(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 4) - b ^= 207 - b += 239 - return b - }) -} - -func encodeCurrentCastleSlot7PluginState(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 135) - b ^= 37 - b += 242 - return b - }) -} - -func encodeCurrentCastleSlot7FrameRatio(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b += 238 - b ^= 80 - b = byte(uint16(b) * 67) - return b - }) -} - -func encodeCurrentCastleSlot7WorkerCanvas(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 6) - b = currentCastleRotL8(b, 5) - b ^= 31 - return b - }) -} - -func encodeCurrentCastleSlot7WorkerFeature(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b += 105 - b ^= 187 - b = byte(uint16(b) * 211) - return b - }) -} - -func encodeCurrentCastleSlot7NavigatorProbe(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 119) - b = byte(uint16(b) * 227) - b = currentCastleRotL8(b, 5) - b += 96 - return b - }) -} - -func encodeCurrentCastleSlot7ViewportRatio(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 55) - b = byte(uint16(b) * 129) - b ^= 157 - return b - }) -} - -func encodeCurrentCastleSlot7WorkerSignal(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b += 70 - b = currentCastleRotL8(b, 5) - b += 151 - b ^= 157 - return b - }) -} - -func encodeCurrentCastleSlot7PointerRatio(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 6) - b += 202 - b += 162 - b = currentCastleRotL8(b, 5) - return b - }) -} - -func encodeCurrentCastleSlot8Primary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 5) - b = byte(uint16(b) * 45) - b ^= 87 - return b - }) -} - -func encodeCurrentCastleSlot8Secondary(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 5) - b += 154 - b = byte(uint16(b) * 201) - return b - }) -} - -func encodeCurrentCastleWebGPUVendor(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 79) - b = byte(uint16(b) * 211) - b ^= 118 - return b - }) -} - -func encodeCurrentCastleWebGPULimits(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = currentCastleRotL8(b, 4) - b = currentCastleRotL8(b, 5) - b += 72 - b = byte(uint16(b) * 187) - return b - }) -} - -func encodeCurrentCastleWebGPUArchitecture(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 59) - b = currentCastleRotL8(b, 1) - b ^= 56 - return b - }) -} - -func encodeCurrentCastleSlot6TZ(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 223) - b = currentCastleRotL8(b, 5) - b ^= 159 - return b - }) -} - -func encodeCurrentCastleSlot6TP(value string) string { - return encodeCurrentCastleTransformedUTF8(value, func(b byte) byte { - b = byte(uint16(b) * 133) - b += 242 - b = byte(uint16(b) * 215) - return byte(uint16(b) * 105) - }) -} - -func invertCurrentCastleTB(value uint32) uint32 { - value -= 956 - value -= 24 - value += 42 - value += 41 - value += 478 - value -= 433 - return value * 1884841763 -} diff --git a/pkg/twittermeow/client.go b/pkg/twittermeow/client.go index 6a44c8f0..fc2eec0f 100644 --- a/pkg/twittermeow/client.go +++ b/pkg/twittermeow/client.go @@ -71,7 +71,7 @@ type Client struct { func NewClient(cookies *cookies.Cookies, store crypto.KeyStore, logger zerolog.Logger) *Client { cli := Client{ HTTP: &http.Client{ - Transport: req.NewClient().ImpersonateChrome().SetTLSFingerprintChrome().DisableHTTP3().GetTransport(), + Transport: req.NewClient().ImpersonateChrome().DisableHTTP3().GetTransport(), Timeout: 60 * time.Second, }, Logger: logger, diff --git a/pkg/twittermeow/cookies/cookies.go b/pkg/twittermeow/cookies/cookies.go index 5c13da48..72b06385 100644 --- a/pkg/twittermeow/cookies/cookies.go +++ b/pkg/twittermeow/cookies/cookies.go @@ -43,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() diff --git a/pkg/twittermeow/cookies/cookies_test.go b/pkg/twittermeow/cookies/cookies_test.go index 294a71c1..3a3c2a5d 100644 --- a/pkg/twittermeow/cookies/cookies_test.go +++ b/pkg/twittermeow/cookies/cookies_test.go @@ -5,6 +5,28 @@ import ( "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{}} diff --git a/pkg/twittermeow/methods/html.go b/pkg/twittermeow/methods/html.go index db333724..05bb89a0 100644 --- a/pkg/twittermeow/methods/html.go +++ b/pkg/twittermeow/methods/html.go @@ -68,7 +68,7 @@ func ParseDocumentCookieAssignments(html string) map[string]string { if len(matches) == 0 { return nil } - header := http.Header{} + out := make(map[string]string) for _, match := range matches { if len(match) < 2 { continue @@ -77,12 +77,10 @@ func ParseDocumentCookieAssignments(html string) map[string]string { if err != nil || cookie == "" { continue } - header.Add("Set-Cookie", cookie) - } - resp := &http.Response{Header: header} - out := make(map[string]string) - for _, cookie := range resp.Cookies() { - out[cookie.Name] = cookie.Value + parsedCookie, err := http.ParseSetCookie(cookie) + if err == nil { + out[parsedCookie.Name] = parsedCookie.Value + } } return out } 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) From 56c82e264a44ef1e7da653c8214382fe1b3673ee Mon Sep 17 00:00:00 2001 From: highesttt Date: Thu, 9 Jul 2026 23:23:30 -0400 Subject: [PATCH 09/20] Replaced castle token gen with a webview to retrieve headers + castle tokens from --- pkg/connector/client.go | 3 + pkg/connector/dbmeta.go | 11 +- pkg/connector/login.go | 882 ++- pkg/connector/login_live_probe_test.go | 11 + pkg/connector/login_test.go | 268 + pkg/connector/msgconv.go | 4 +- pkg/twittermeow/castle_token.go | 677 +-- pkg/twittermeow/castle_token_test.go | 21 - .../castle_token_test_helpers_test.go | 7 - pkg/twittermeow/castle_web_token.go | 5329 ----------------- pkg/twittermeow/client.go | 29 +- pkg/twittermeow/headers.go | 92 +- pkg/twittermeow/headers_test.go | 67 + pkg/twittermeow/login_jetfuel.go | 94 +- .../login_jetfuel_live_probe_test.go | 10 +- pkg/twittermeow/login_web.go | 21 + pkg/twittermeow/login_web_test.go | 169 +- pkg/twittermeow/methods/html.go | 22 +- pkg/twittermeow/methods/html_test.go | 18 + 19 files changed, 1611 insertions(+), 6124 deletions(-) delete mode 100644 pkg/twittermeow/castle_token_test.go delete mode 100644 pkg/twittermeow/castle_token_test_helpers_test.go delete mode 100644 pkg/twittermeow/castle_web_token.go create mode 100644 pkg/twittermeow/headers_test.go diff --git a/pkg/connector/client.go b/pkg/connector/client.go index e7d122a4..26020430 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -98,6 +98,9 @@ func (tc *TwitterConnector) LoadUserLogin(ctx context.Context, login *bridgev2.U c := cookies.NewCookiesFromString(meta.Cookies) log := login.Log.With().Str("component", "twitter_client").Logger() client := twittermeow.NewClient(c, newUserLoginKeyStore(login, tc), log) + if meta.BrowserHeaders != nil { + client.SetBrowserHeaders(*meta.BrowserHeaders) + } client.SetCurrentUserID(ParseUserLoginID(login.ID)) login.Client = NewTwitterClient(login, tc, client) return nil diff --git a/pkg/connector/dbmeta.go b/pkg/connector/dbmeta.go index af06312d..e0678631 100644 --- a/pkg/connector/dbmeta.go +++ b/pkg/connector/dbmeta.go @@ -60,11 +60,12 @@ func (m *PortalMetadata) CanUseXChat() bool { } type UserLoginMetadata struct { - Cookies string `json:"cookies"` - SecretKey string `json:"secret_key,omitempty"` - SigningKey string `json:"signing_key,omitempty"` - SigningKeyVersion string `json:"signing_key_version,omitempty"` - PushKeys *PushKeys `json:"push_keys,omitempty"` + Cookies string `json:"cookies"` + SecretKey string `json:"secret_key,omitempty"` + SigningKey string `json:"signing_key,omitempty"` + SigningKeyVersion string `json:"signing_key_version,omitempty"` + PushKeys *PushKeys `json:"push_keys,omitempty"` + BrowserHeaders *twittermeow.BrowserHeaders `json:"browser_headers,omitempty"` Session *twittermeow.CachedSession `json:"session,omitempty"` MaxUserSequenceID string `json:"max_user_sequence_id,omitempty"` // Last processed sequence ID for incremental inbox fetching diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 539e2fed..c7993e16 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -18,12 +18,15 @@ package connector import ( "context" + "encoding/base64" + "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "time" + "unicode" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/database" @@ -49,18 +52,24 @@ type TwitterLogin struct { needsPINSetup bool useCookieLogin bool - client *twittermeow.Client - webLogin *twittermeow.WebLoginSession - webLoginPassword string - webLoginChallenge *twittermeow.WebLoginChallenge - webLoginMethods []twittermeow.WebLoginAuthMethod - profile twittermeow.CurrentUserProfile + client *twittermeow.Client + webLogin *twittermeow.WebLoginSession + webLoginIdentifier string + webLoginPassword string + webLoginCastleStage string + webLoginAuthMethod string + webLoginText string + webLoginChallenge *twittermeow.WebLoginChallenge + webLoginMethods []twittermeow.WebLoginAuthMethod + browserHeaders twittermeow.BrowserHeaders + profile twittermeow.CurrentUserProfile } var ( LoginFlowIDPassword = "password" LoginFlowIDCookies = "cookies" LoginStepIDCredentials = "fi.mau.twitter.login.enter_credentials" + LoginStepIDCastleToken = "fi.mau.twitter.login.castle_token" LoginStepIDVerification = "fi.mau.twitter.login.enter_verification" LoginStepIDAuthMethod = "fi.mau.twitter.login.select_auth_method" LoginStepIDCookies = "fi.mau.twitter.login.enter_cookies" @@ -68,10 +77,636 @@ var ( LoginStepIDComplete = "fi.mau.twitter.login.complete" loginFieldIdentifier = "identifier" loginFieldPassword = "password" + loginFieldCastleToken = "castle_token" + loginFieldBrowserUserAgent = "browser_user_agent" + loginFieldBrowserSecCHUA = "browser_sec_ch_ua" + loginFieldBrowserPlatform = "browser_sec_ch_ua_platform" + loginFieldBrowserMobile = "browser_sec_ch_ua_mobile" loginFieldVerificationCode = "verification_code" loginFieldAuthMethod = "auth_method" ) +const ( + webLoginCastleStageIdentifier = "identifier" + webLoginCastleStagePassword = "password" + webLoginCastleStageCombined = "combined" + webLoginCastleStageBeginTwoFactor = "begin_two_factor" + webLoginCastleStageAuthMethod = "auth_method" + webLoginCastleStageText = "text" + castleTokenWebviewURL = "https://x.com/robots.txt" + castleTokenContextURL = "https://x.com/i/jf/onboarding/web?mode=login" + castleTokenHeaderPrefix = "text/plain, application/x-mautrix-twitter-castle;v=" + castleTokenBatchSize = 8 +) + +var castleTokenCookieNames = []string{ + "__cf_bm", + "__cuid", + "gt", + "guest_id", + "guest_id_ads", + "guest_id_marketing", + "personalization_id", +} + +type browserHeaderField struct { + ID string + HeaderName string + Required bool + Pattern string +} + +var browserHeaderFields = []browserHeaderField{ + { + ID: loginFieldBrowserUserAgent, + HeaderName: "user-agent", + Required: true, + Pattern: `^[^\r\n]{1,1024}$`, + }, + { + ID: loginFieldBrowserSecCHUA, + HeaderName: "sec-ch-ua", + Pattern: `^[^\r\n]{1,1024}$`, + }, + { + ID: loginFieldBrowserPlatform, + HeaderName: "sec-ch-ua-platform", + Pattern: `^[^\r\n]{1,1024}$`, + }, + { + ID: loginFieldBrowserMobile, + HeaderName: "sec-ch-ua-mobile", + Pattern: `^\?[01]$`, + }, +} + +func castleTokenExtractJS(info twittermeow.JetfuelCastleTokenInfo, identifier string) string { + scriptURL, _ := json.Marshal(info.ScriptURL) + publicKey, _ := json.Marshal(info.PublicKey) + cookieNames, _ := json.Marshal(castleTokenCookieNames) + contextURL, _ := json.Marshal(castleTokenContextURL) + identifierJSON, _ := json.Marshal(identifier) + return fmt.Sprintf(`(async () => { + const scriptURL = %s; + const publicKey = %s; + const cookieNames = new Set(%s); + const contextURL = %s; + const identifier = %s; + const castleStorageKey = "fi.mau.twitter.castle_token"; + const castleTokenBatchSize = %d; + + let browserStatusText; + function showBrowserLoginStatus(message) { + try { + document.title = "Signing in to X"; + if (!browserStatusText || !browserStatusText.isConnected) { + const body = document.body || document.documentElement.appendChild(document.createElement("body")); + const container = document.createElement("main"); + container.id = "mautrix-twitter-login-status"; + container.setAttribute("role", "status"); + container.setAttribute("aria-live", "polite"); + + const title = document.createElement("h1"); + title.textContent = "Signing in to X"; + Object.assign(title.style, { + margin: "0", + color: "#eff3f4", + fontSize: "28px", + fontWeight: "700", + lineHeight: "1.2", + }); + + const progress = document.createElement("progress"); + progress.setAttribute("aria-label", "Signing in"); + Object.assign(progress.style, { + width: "220px", + height: "6px", + margin: "28px 0 22px", + accentColor: "#1d9bf0", + }); + + browserStatusText = document.createElement("p"); + Object.assign(browserStatusText.style, { + margin: "0", + color: "#8b98a5", + fontSize: "15px", + lineHeight: "1.5", + }); + + container.append(title, progress, browserStatusText); + Object.assign(container.style, { + width: "min(420px, calc(100%% - 48px))", + textAlign: "center", + }); + Object.assign(document.documentElement.style, { + minHeight: "100%%", + background: "#000000", + colorScheme: "dark", + }); + Object.assign(body.style, { + minHeight: "100vh", + margin: "0", + display: "grid", + placeItems: "center", + background: "#000000", + fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", + }); + body.replaceChildren(container); + } + browserStatusText.textContent = message; + } catch (_) {} + } + showBrowserLoginStatus("Preparing secure login..."); + + const browserLog = message => { + try { + console.log("[BrowserAuth] mautrix-twitter Castle: " + message); + } catch (_) {} + }; + function getLocalStorage() { + try { + return window.localStorage; + } catch (_) {} + return null; + } + function castleTokenResultKey(index) { + return index === 1 ? "castle_token" : "castle_token_" + index; + } + function castleTokenStorageKeyForIndex(index) { + return index === 1 ? castleStorageKey : castleStorageKey + "_" + index; + } + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + async function waitFor(predicate, message, timeout = 30000) { + const deadlineAt = Date.now() + timeout; + while (Date.now() < deadlineAt) { + const value = predicate(); + if (value) { + return value; + } + await sleep(100); + } + throw new Error(message); + } + function resetStoredResult() { + try { + const storage = getLocalStorage(); + if (!storage) { + return; + } + storage.removeItem(castleStorageKey); + for (let index = 2; index <= castleTokenBatchSize; index++) { + storage.removeItem(castleTokenStorageKeyForIndex(index)); + } + for (const name of cookieNames) { + storage.removeItem("fi.mau.twitter.cookie." + name); + } + } catch (_) {} + } + function storeBrowserAuthResult(result) { + try { + globalThis.__MAUTRIX_TWITTER_CASTLE_RESULT__ = result; + globalThis.__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__ = false; + globalThis.__BEEP_BEEP_AUTH_RESULTS__ = result; + if (typeof window !== "undefined") { + window.__BEEP_BEEP_AUTH_RESULTS__ = result; + } + } catch (_) {} + try { + const storage = getLocalStorage(); + if (!storage) { + return; + } + storage.setItem(castleStorageKey, result.castle_token || ""); + for (let index = 2; index <= castleTokenBatchSize; index++) { + const key = castleTokenResultKey(index); + if (result[key]) { + storage.setItem(castleTokenStorageKeyForIndex(index), result[key]); + } + } + for (const name of cookieNames) { + if (result[name]) { + storage.setItem("fi.mau.twitter.cookie." + name, result[name]); + } + } + } catch (_) {} + } + function storedBrowserAuthResult() { + try { + const result = globalThis.__MAUTRIX_TWITTER_CASTLE_RESULT__; + if (result && result.castle_token) { + return result; + } + } catch (_) {} + try { + const storage = getLocalStorage(); + if (!storage) { + return null; + } + const token = storage.getItem(castleStorageKey); + if (!token) { + return null; + } + const result = { castle_token: token }; + for (let index = 2; index <= castleTokenBatchSize; index++) { + const token = storage.getItem(castleTokenStorageKeyForIndex(index)); + if (token) { + result[castleTokenResultKey(index)] = token; + } + } + for (const name of cookieNames) { + const value = storage.getItem("fi.mau.twitter.cookie." + name); + if (value) { + result[name] = value; + } + } + return result; + } catch (_) { + return null; + } + } + const existingResult = storedBrowserAuthResult(); + if (existingResult) { + browserLog("returning stored result"); + return existingResult; + } + if (globalThis.__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__) { + browserLog("waiting for in-flight result"); + return await waitFor(() => storedBrowserAuthResult(), "X Castle token generation did not finish", 30000); + } + globalThis.__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__ = true; + resetStoredResult(); + + function addModules(entry, modules) { + if (!entry || typeof entry !== "object") { + return; + } + const defs = entry[1]; + if (!defs || typeof defs !== "object") { + return; + } + for (const id of Object.keys(defs)) { + modules[id] = defs[id]; + } + } + + function loadScript(doc, url, timeout = 10000) { + return new Promise((resolve, reject) => { + const script = doc.createElement("script"); + const timer = setTimeout(() => { + try { + script.remove(); + } catch (_) {} + reject(new Error("Timed out loading X Castle script")); + }, timeout); + script.src = url; + script.async = true; + script.onload = () => { + clearTimeout(timer); + resolve(); + }; + script.onerror = () => { + clearTimeout(timer); + reject(new Error("Failed to load X Castle script")); + }; + (doc.head || doc.documentElement).appendChild(script); + }); + } + + function installModuleCapture(win) { + const chunk = win.webpackChunk_twitter_responsive_web = win.webpackChunk_twitter_responsive_web || []; + const modules = {}; + for (const entry of chunk) { + addModules(entry, modules); + } + if (!chunk.__mautrixCastleCaptured) { + const nativePush = chunk.push.bind(chunk); + chunk.push = (...entries) => { + for (const entry of entries) { + addModules(entry, modules); + } + return nativePush(...entries); + }; + Object.defineProperty(chunk, "__mautrixCastleCaptured", { value: true }); + } + return modules; + } + + async function createTokenInContext(win, doc) { + const modules = installModuleCapture(win); + await loadScript(doc, scriptURL); + await waitFor(() => Object.keys(modules).length > 0, "X Castle script did not register modules", 15000); + + const cache = {}; + function req(id) { + const key = String(id); + if (cache[key]) { + return cache[key].exports; + } + const fn = modules[key]; + if (typeof fn !== "function") { + throw new Error("Missing X Castle module " + key); + } + const module = { exports: {} }; + cache[key] = module; + fn(module, module.exports, req); + return module.exports; + } + req.d = (exports, definition) => { + for (const key of Object.keys(definition)) { + if (!Object.prototype.hasOwnProperty.call(exports, key)) { + Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); + } + } + }; + req.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); + req.r = exports => { + if (typeof win.Symbol !== "undefined" && win.Symbol.toStringTag) { + Object.defineProperty(exports, win.Symbol.toStringTag, { value: "Module" }); + } + Object.defineProperty(exports, "__esModule", { value: true }); + }; + + let configure; + for (const id of Object.keys(modules)) { + try { + const exports = req(id); + if (exports && typeof exports.configure === "function") { + configure = exports.configure; + break; + } + } catch (_) {} + } + if (typeof configure !== "function") { + throw new Error("X Castle module is unavailable"); + } + + const castle = configure({ pk: publicKey }); + if (!castle || typeof castle.createRequestToken !== "function") { + throw new Error("X Castle token generator is unavailable"); + } + const tokens = []; + for (let index = 0; index < castleTokenBatchSize; index++) { + const token = await castle.createRequestToken(); + if (token) { + tokens.push(token); + } + await sleep(10); + } + return tokens; + } + + async function synthesizeCastleActivity(win, doc) { + const target = doc.body || doc.documentElement; + if (!target) { + return; + } + const input = doc.createElement("input"); + input.type = "text"; + input.autocomplete = "username"; + input.style.cssText = "position:fixed;left:24px;top:24px;width:240px;height:32px;opacity:0;pointer-events:none;"; + target.appendChild(input); + input.focus(); + const points = [[52, 48], [96, 52], [148, 60], [212, 68]]; + for (const [x, y] of points) { + const eventInit = { bubbles: true, cancelable: true, clientX: x, clientY: y, screenX: x + 16, screenY: y + 88, pointerType: "mouse", isPrimary: true, buttons: 0 }; + if (typeof win.PointerEvent === "function") { + target.dispatchEvent(new win.PointerEvent("pointermove", eventInit)); + } else { + target.dispatchEvent(new win.MouseEvent("mousemove", eventInit)); + } + await sleep(20); + } + const downInit = { bubbles: true, cancelable: true, clientX: 96, clientY: 52, screenX: 112, screenY: 140, pointerType: "mouse", isPrimary: true, buttons: 1 }; + if (typeof win.PointerEvent === "function") { + input.dispatchEvent(new win.PointerEvent("pointerdown", downInit)); + input.dispatchEvent(new win.PointerEvent("pointerup", { ...downInit, buttons: 0 })); + } + input.dispatchEvent(new win.MouseEvent("mousedown", downInit)); + input.dispatchEvent(new win.MouseEvent("mouseup", { ...downInit, buttons: 0 })); + input.dispatchEvent(new win.MouseEvent("click", { ...downInit, buttons: 0 })); + const chars = identifier || "x"; + const valueSetter = Object.getOwnPropertyDescriptor(win.HTMLInputElement.prototype, "value").set; + for (const ch of chars.slice(0, 16)) { + input.dispatchEvent(new win.KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: ch })); + valueSetter.call(input, input.value + ch); + input.dispatchEvent(new win.InputEvent("input", { bubbles: true, inputType: "insertText", data: ch })); + input.dispatchEvent(new win.KeyboardEvent("keyup", { bubbles: true, cancelable: true, key: ch })); + await sleep(15); + } + input.dispatchEvent(new win.Event("change", { bubbles: true })); + await sleep(100); + } + + async function loadFetchedContextFrame() { + const resp = await fetch(contextURL, { credentials: "include" }); + if (!resp.ok) { + throw new Error("Failed to fetch X login context: HTTP " + resp.status); + } + const html = await resp.text(); + const iframe = document.createElement("iframe"); + iframe.tabIndex = -1; + iframe.style.cssText = "position:fixed;left:-10000px;top:-10000px;width:1024px;height:768px;border:0;opacity:0;pointer-events:none;"; + document.documentElement.appendChild(iframe); + const win = iframe.contentWindow; + const doc = iframe.contentDocument; + if (!win || !doc) { + throw new Error("Synthetic X login context is not accessible"); + } + doc.open(); + doc.write(html); + doc.close(); + try { + win.history.replaceState(null, "", contextURL); + } catch (_) {} + await waitFor(() => doc.readyState === "interactive" || doc.readyState === "complete", "Synthetic X login context did not become ready", 15000); + return { win, doc }; + } + + function copyCookies(result, cookieText) { + for (const part of cookieText.split(";")) { + const idx = part.indexOf("="); + if (idx <= 0) { + continue; + } + const name = part.slice(0, idx).trim(); + if (cookieNames.has(name)) { + result[name] = part.slice(idx + 1).trim(); + } + } + } + + function quoteClientHint(value) { + return '"' + String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + } + + function captureBrowserHeaders() { + const headers = { + browser_user_agent: String(navigator.userAgent || ""), + }; + const userAgentData = navigator.userAgentData; + if (!userAgentData) { + return headers; + } + const brands = Array.from(userAgentData.brands || []); + if (brands.length > 0) { + headers.browser_sec_ch_ua = brands.map(item => + quoteClientHint(item.brand) + ";v=" + quoteClientHint(item.version) + ).join(", "); + } + if (userAgentData.platform) { + headers.browser_sec_ch_ua_platform = quoteClientHint(userAgentData.platform); + } + headers.browser_sec_ch_ua_mobile = userAgentData.mobile ? "?1" : "?0"; + return headers; + } + + try { + browserLog("loading X context"); + const context = await loadFetchedContextFrame(); + browserLog("context ready"); + browserLog("creating module token"); + showBrowserLoginStatus("Completing sign-in..."); + await synthesizeCastleActivity(context.win, context.doc); + const castleTokens = await createTokenInContext(context.win, context.doc); + const castleToken = castleTokens[0] || ""; + browserLog("module tokens ready, count " + castleTokens.length + ", first length " + String(castleToken || "").length); + const result = { castle_token: castleToken }; + Object.assign(result, captureBrowserHeaders()); + for (let index = 1; index < castleTokens.length; index++) { + result[castleTokenResultKey(index + 1)] = castleTokens[index]; + } + copyCookies(result, document.cookie); + copyCookies(result, context.doc.cookie); + storeBrowserAuthResult(result); + browserLog("result stored"); + showBrowserLoginStatus("Finishing..."); + return result; + } catch (err) { + showBrowserLoginStatus("Unable to finish signing in."); + try { + globalThis.__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__ = false; + } catch (_) {} + throw err; + } +})()`, scriptURL, publicKey, cookieNames, contextURL, identifierJSON, castleTokenBatchSize) +} + +func decodeCastleTokenInput(value string) (string, error) { + value = strings.TrimSpace(value) + if value == "" { + return "", nil + } + if idx := strings.Index(value, castleTokenHeaderPrefix); idx >= 0 { + encoded := removeCastleTokenWhitespace(value[idx+len(castleTokenHeaderPrefix):]) + decoded, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + return "", fmt.Errorf("decode Castle header token: %w", err) + } + value = string(decoded) + } + return removeCastleTokenWhitespace(value), nil +} + +func removeCastleTokenWhitespace(value string) string { + return strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 + } + return r + }, value) +} + +func castleTokenFieldID(index int) string { + if index <= 1 { + return loginFieldCastleToken + } + return fmt.Sprintf("%s_%d", loginFieldCastleToken, index) +} + +func castleTokenCookieFields() []bridgev2.LoginCookieField { + fields := []bridgev2.LoginCookieField{ + { + ID: loginFieldCastleToken, + Required: true, + Pattern: `^[\s\S]{128,}$`, + Sources: []bridgev2.LoginCookieFieldSource{ + { + Type: bridgev2.LoginCookieTypeSpecial, + Name: "fi.mau.twitter.castle_token", + }, + { + Type: bridgev2.LoginCookieTypeLocalStorage, + Name: "fi.mau.twitter.castle_token", + }, + }, + }, + } + for index := 2; index <= castleTokenBatchSize; index++ { + fieldID := castleTokenFieldID(index) + storageKey := fmt.Sprintf("fi.mau.twitter.castle_token_%d", index) + fields = append(fields, bridgev2.LoginCookieField{ + ID: fieldID, + Required: false, + Pattern: `^[\s\S]{128,}$`, + Sources: []bridgev2.LoginCookieFieldSource{ + { + Type: bridgev2.LoginCookieTypeSpecial, + Name: storageKey, + }, + { + Type: bridgev2.LoginCookieTypeLocalStorage, + Name: storageKey, + }, + }, + }) + } + for _, field := range browserHeaderFields { + fields = append(fields, bridgev2.LoginCookieField{ + ID: field.ID, + Required: field.Required, + Pattern: field.Pattern, + Sources: []bridgev2.LoginCookieFieldSource{ + { + Type: bridgev2.LoginCookieTypeRequestHeader, + Name: field.HeaderName, + RequestURLRegex: `^https://x\.com/`, + }, + { + Type: bridgev2.LoginCookieTypeSpecial, + Name: field.ID, + }, + }, + }) + } + for _, name := range castleTokenCookieNames { + fields = append(fields, bridgev2.LoginCookieField{ + ID: name, + Required: false, + Sources: []bridgev2.LoginCookieFieldSource{ + { + Type: bridgev2.LoginCookieTypeCookie, + Name: name, + CookieDomain: "x.com", + }, + { + Type: bridgev2.LoginCookieTypeCookie, + Name: name, + CookieDomain: ".x.com", + }, + { + Type: bridgev2.LoginCookieTypeSpecial, + Name: "fi.mau.twitter.cookie." + name, + }, + { + Type: bridgev2.LoginCookieTypeLocalStorage, + Name: "fi.mau.twitter.cookie." + name, + }, + }, + }) + } + return fields +} + var _ bridgev2.LoginProcessCookies = (*TwitterLogin)(nil) var _ bridgev2.LoginProcessUserInput = (*TwitterLogin)(nil) var _ bridgev2.LoginProcessWithOverride = (*TwitterLogin)(nil) @@ -137,11 +772,6 @@ func (tc *TwitterConnector) GetLoginFlows() []bridgev2.LoginFlow { 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: LoginFlowIDCookies, - }, } } @@ -164,8 +794,7 @@ func (t *TwitterLogin) Start(_ context.Context) (*bridgev2.LoginStep, error) { StepID: LoginStepIDCookies, Instructions: "Open the Login URL in an Incognito/Private browsing mode. Then, extract the cookies as a JSON object/cURL command copied from the Network tab of your browser's DevTools. After that, close the browser **before** pasting the cookies.\n\nFor example: `{\"ct0\":\"123466-...\",\"auth_token\":\"abcde-...\"}`", CookiesParams: &bridgev2.LoginCookiesParams{ - URL: "https://x.com/i/flow/login", - UserAgent: "", + URL: "https://x.com/i/flow/login", Fields: []bridgev2.LoginCookieField{ { ID: "ct0", @@ -215,6 +844,38 @@ func makeCredentialsStep(errorLine string) *bridgev2.LoginStep { } } +func makeCastleTokenStep(info twittermeow.JetfuelCastleTokenInfo, identifier, errorLine string) *bridgev2.LoginStep { + instructions := "Generating an X browser token for this login." + if errorLine != "" { + instructions = fmt.Sprintf("%s\n\n%s", errorLine, instructions) + } + return &bridgev2.LoginStep{ + Type: bridgev2.LoginStepTypeCookies, + StepID: LoginStepIDCastleToken, + Instructions: instructions, + CookiesParams: &bridgev2.LoginCookiesParams{ + URL: castleTokenWebviewURL, + ExtractJS: castleTokenExtractJS(info, identifier), + WaitForURLPattern: `^https://x\.com/robots\.txt$`, + Fields: castleTokenCookieFields(), + }, + } +} + +func (t *TwitterLogin) makeWebLoginCastleTokenStep(errorLine string) *bridgev2.LoginStep { + if t.webLogin == nil || t.webLogin.Client() == nil { + t.webLoginCastleStage = "" + return makeCredentialsStep("The X login session expired. Enter your X login details again.") + } + info := t.webLogin.Client().JetfuelCastleTokenInfo() + if !info.IsValid() { + t.webLoginCastleStage = "" + t.User.Log.Warn().Msg("X Castle web metadata missing from login page bootstrap") + return makeCredentialsStep("X did not provide the browser-token metadata needed for native login. Try again.") + } + return makeCastleTokenStep(info, t.webLoginIdentifier, errorLine) +} + func makeVerificationStep(challenge *twittermeow.WebLoginChallenge, errorLine string) *bridgev2.LoginStep { instructions := "X needs additional verification for this login." fieldName := "Verification" @@ -338,6 +999,9 @@ func (t *TwitterLogin) StartWithOverride(ctx context.Context, override *bridgev2 // Migration case: validate existing cookies and skip to passcode t.Cookies = meta.Cookies + if meta.BrowserHeaders != nil { + t.browserHeaders = *meta.BrowserHeaders + } if err := t.ensureClientForPIN(ctx); err != nil { // Cookies expired, fall back to normal flow t.User.Log.Warn().Err(err).Msg("Migration: cookies invalid, falling back to full login") @@ -353,6 +1017,9 @@ func (t *TwitterLogin) StartWithOverride(ctx context.Context, override *bridgev2 } func (t *TwitterLogin) SubmitCookies(ctx context.Context, cookies map[string]string) (*bridgev2.LoginStep, error) { + if t.isWaitingForWebLoginCastleToken() { + return t.submitWebCastleTokenInput(ctx, cookies) + } cookieStruct := twitCookies.NewCookies(cookies) t.Cookies = cookieStruct.String() @@ -393,6 +1060,9 @@ func (t *TwitterLogin) ensureClientForPIN(ctx context.Context) error { } cookieStruct := twitCookies.NewCookiesFromString(t.Cookies) t.client = twittermeow.NewClient(cookieStruct, nil, t.User.Log.With().Str("component", "login_twitter_client").Logger()) + if t.browserHeaders.UserAgent != "" { + t.client.SetBrowserHeaders(t.browserHeaders) + } profile, err := t.client.LoadMessagesPage(ctx) if err != nil { return fmt.Errorf("failed to load messages page: %w", err) @@ -630,7 +1300,11 @@ func (t *TwitterLogin) submitCredentialsInput(ctx context.Context, input map[str client := twittermeow.NewClient(twitCookies.NewCookies(nil), nil, t.User.Log.With().Str("component", "login_twitter_client").Logger()) t.webLogin = twittermeow.NewWebLoginSession(client) + t.webLoginIdentifier = identifier t.webLoginPassword = password + t.webLoginCastleStage = "" + t.webLoginAuthMethod = "" + t.webLoginText = "" t.webLoginChallenge = nil t.webLoginMethods = nil @@ -641,6 +1315,10 @@ func (t *TwitterLogin) submitCredentialsInput(ctx context.Context, input map[str if result.Status != twittermeow.WebLoginStatusNeedsIdentifier { return t.handleWebLoginResult(ctx, result) } + if t.webLogin.UsesJetfuel() { + t.webLoginCastleStage = webLoginCastleStageIdentifier + return t.makeWebLoginCastleTokenStep(""), nil + } result, err = t.webLogin.SubmitCredentials(ctx, identifier, password) if err != nil { @@ -649,6 +1327,147 @@ func (t *TwitterLogin) submitCredentialsInput(ctx context.Context, input map[str return t.handleWebLoginResult(ctx, result) } +func (t *TwitterLogin) isWaitingForWebLoginCastleToken() bool { + return t.webLogin != nil && t.webLoginCastleStage != "" && t.webLoginIdentifier != "" && t.webLoginPassword != "" +} + +func (t *TwitterLogin) submitWebCastleTokenInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { + if !t.isWaitingForWebLoginCastleToken() { + t.webLoginCastleStage = "" + return makeCredentialsStep("The X login session expired. Enter your X login details again."), nil + } + castleTokens, err := decodeCastleTokenBatchInput(input) + if err != nil { + return t.makeWebLoginCastleTokenStep("The X webview returned an invalid browser token."), nil + } + if len(castleTokens) == 0 { + return t.makeWebLoginCastleTokenStep("The X webview did not return a browser token."), nil + } + client := t.webLogin.Client() + if !client.SetBrowserHeaders(browserHeadersFromInput(input)) { + return t.makeWebLoginCastleTokenStep("The X webview did not return a valid browser fingerprint."), nil + } + t.browserHeaders = client.GetBrowserHeaders() + client.SetCookies(castleWebviewCookies(input)) + client.SetNextJetfuelCastleTokens(castleTokens) + return t.continueWebCastleLogin(ctx) +} + +func browserHeadersFromInput(input map[string]string) twittermeow.BrowserHeaders { + return twittermeow.BrowserHeaders{ + UserAgent: input[loginFieldBrowserUserAgent], + SecCHUserAgent: input[loginFieldBrowserSecCHUA], + SecCHPlatform: input[loginFieldBrowserPlatform], + SecCHMobile: input[loginFieldBrowserMobile], + } +} + +func decodeCastleTokenBatchInput(input map[string]string) ([]string, error) { + tokens := make([]string, 0, castleTokenBatchSize) + seen := make(map[string]struct{}, castleTokenBatchSize) + for index := 1; index <= castleTokenBatchSize; index++ { + token, err := decodeCastleTokenInput(input[castleTokenFieldID(index)]) + if err != nil { + return nil, err + } + if token == "" { + continue + } + if len(token) < 128 || len(token) > 20000 { + return nil, fmt.Errorf("invalid Castle token length") + } + if _, ok := seen[token]; ok { + continue + } + seen[token] = struct{}{} + tokens = append(tokens, token) + } + return tokens, nil +} + +func (t *TwitterLogin) continueWebCastleLogin(ctx context.Context) (*bridgev2.LoginStep, error) { + for attempts := 0; attempts < castleTokenBatchSize; attempts++ { + stage := t.webLoginCastleStage + t.User.Log.Debug().Str("stage", stage).Msg("Submitting X Jetfuel Castle token") + + var ( + result *twittermeow.WebLoginResult + err error + ) + switch stage { + case webLoginCastleStageIdentifier: + result, err = t.webLogin.SubmitIdentifier(ctx, t.webLoginIdentifier) + if err != nil && twittermeow.IsWebLoginPrePasswordParityError(err) { + t.webLoginCastleStage = webLoginCastleStageCombined + continue + } + case webLoginCastleStagePassword: + result, err = t.webLogin.SubmitPassword(ctx, t.webLoginPassword) + case webLoginCastleStageCombined: + result, err = t.webLogin.SubmitCombinedCredentials(ctx, t.webLoginIdentifier, t.webLoginPassword) + case webLoginCastleStageBeginTwoFactor: + result, err = t.webLogin.SubmitPendingTwoFactor(ctx) + case webLoginCastleStageAuthMethod: + methodID := t.webLoginAuthMethod + if methodID == "" { + t.webLoginCastleStage = "" + return makeAuthMethodStep(t.webLoginMethods, "Choose a verification method."), nil + } + result, err = t.webLogin.SubmitAuthMethod(ctx, methodID) + case webLoginCastleStageText: + text := t.webLoginText + if text == "" { + t.webLoginCastleStage = "" + return makeVerificationStep(t.webLoginChallenge, "Enter the X verification code."), nil + } + result, err = t.webLogin.SubmitText(ctx, text) + default: + t.webLoginCastleStage = "" + return makeCredentialsStep("The X login session expired. Enter your X login details again."), nil + } + if err != nil { + if errors.Is(err, twittermeow.ErrJetfuelCastleTokenRequired) { + if stage == webLoginCastleStagePassword || stage == webLoginCastleStageText { + t.webLoginCastleStage = webLoginCastleStageBeginTwoFactor + continue + } + return t.makeWebLoginCastleTokenStep("X needs a fresh browser token to continue this login."), nil + } + t.webLoginCastleStage = "" + if stage == webLoginCastleStageText { + return makeVerificationStep(t.webLoginChallenge, webLoginErrorInstructions(err)), nil + } + if stage == webLoginCastleStageAuthMethod { + return makeAuthMethodStep(t.webLoginMethods, webLoginErrorInstructions(err)), nil + } + return makeCredentialsStep(webLoginErrorInstructions(err)), nil + } + if stage == webLoginCastleStageAuthMethod { + t.webLoginAuthMethod = "" + } + if stage == webLoginCastleStageText { + t.webLoginText = "" + } + if result != nil && result.Status == twittermeow.WebLoginStatusNeedsPassword && t.webLoginPassword != "" { + t.webLoginCastleStage = webLoginCastleStagePassword + continue + } + t.webLoginCastleStage = "" + return t.handleWebLoginResult(ctx, result) + } + return t.makeWebLoginCastleTokenStep("X needs a fresh browser token to continue this login."), nil +} + +func castleWebviewCookies(input map[string]string) map[string]string { + out := make(map[string]string) + for _, name := range castleTokenCookieNames { + if value := strings.TrimSpace(input[name]); value != "" { + out[name] = value + } + } + return out +} + func (t *TwitterLogin) submitWebAuthMethodInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { if t.webLogin == nil { t.webLoginMethods = nil @@ -658,6 +1477,20 @@ func (t *TwitterLogin) submitWebAuthMethodInput(ctx context.Context, input map[s if methodID == "" { return nil, ErrMissingLoginInput } + if method, ok := findWebLoginAuthMethod(t.webLoginMethods, methodID); ok { + methodID = method.ID + if methodID == "" { + methodID = method.Name + } + } + if t.webLogin.UsesJetfuel() { + t.webLoginAuthMethod = methodID + t.webLoginCastleStage = webLoginCastleStageAuthMethod + if t.webLogin.Client().HasNextJetfuelCastleToken() { + return t.continueWebCastleLogin(ctx) + } + return t.makeWebLoginCastleTokenStep(""), nil + } result, err := t.webLogin.SubmitAuthMethod(ctx, methodID) if err != nil { return nil, webLoginFailureError(err) @@ -695,11 +1528,23 @@ func (t *TwitterLogin) submitWebVerificationInput(ctx context.Context, input map if text == "" { return nil, ErrMissingLoginInput } + if t.webLogin.UsesJetfuel() { + t.webLoginText = text + t.webLoginCastleStage = webLoginCastleStageText + if t.webLogin.Client().HasNextJetfuelCastleToken() { + return t.continueWebCastleLogin(ctx) + } + return t.makeWebLoginCastleTokenStep(""), nil + } result, err := t.webLogin.SubmitText(ctx, text) if err != nil { return handleWebLoginVerificationError(t.webLoginChallenge, err) } if result.Status == twittermeow.WebLoginStatusNeedsPassword && t.webLoginPassword != "" { + if t.webLogin.UsesJetfuel() { + t.webLoginCastleStage = webLoginCastleStagePassword + return t.makeWebLoginCastleTokenStep(""), nil + } result, err = t.webLogin.SubmitPassword(ctx, t.webLoginPassword) if err != nil { return handleWebLoginCredentialsError(err) @@ -728,6 +1573,10 @@ func (t *TwitterLogin) handleWebLoginResult(ctx context.Context, result *twitter return makeVerificationStep(result.Challenge, ""), nil case twittermeow.WebLoginStatusNeedsPassword: if t.webLogin != nil && t.webLoginPassword != "" { + if t.webLogin.UsesJetfuel() { + t.webLoginCastleStage = webLoginCastleStagePassword + return t.makeWebLoginCastleTokenStep(""), nil + } next, err := t.webLogin.SubmitPassword(ctx, t.webLoginPassword) if err != nil { return handleWebLoginCredentialsError(err) @@ -827,7 +1676,11 @@ func (t *TwitterLogin) completeWebLogin(ctx context.Context) (*bridgev2.LoginSte t.profile = profile t.persistClientCookiesAndUserID() t.refreshPINSetupState(ctx, "Failed to determine PIN setup state after native login, using recovery prompt") + t.webLoginIdentifier = "" t.webLoginPassword = "" + t.webLoginCastleStage = "" + t.webLoginAuthMethod = "" + t.webLoginText = "" t.webLoginChallenge = nil t.webLoginMethods = nil @@ -915,6 +1768,9 @@ func (t *TwitterLogin) submitPINInput(ctx context.Context, input map[string]stri SigningKey: t.SigningKey, SigningKeyVersion: t.SigningKeyVersion, } + if browserHeaders := t.client.GetBrowserHeaders(); browserHeaders.UserAgent != "" { + meta.BrowserHeaders = &browserHeaders + } // If this is a migration, mark it and flag for full encrypted room sync if t.isMigration { diff --git a/pkg/connector/login_live_probe_test.go b/pkg/connector/login_live_probe_test.go index 6b0423db..35d3e0cb 100644 --- a/pkg/connector/login_live_probe_test.go +++ b/pkg/connector/login_live_probe_test.go @@ -67,6 +67,9 @@ func TestLiveNativeLoginFlowProbe(t *testing.T) { t.Logf("next step: type=%s id=%s instructions=%q", next.Type, next.StepID, next.Instructions) switch next.StepID { + case LoginStepIDCastleToken: + t.Log("native credentials reached the client BrowserAuth Castle step") + return case LoginStepJuiceboxPIN, LoginStepIDVerification, LoginStepIDComplete: verificationCode := strings.TrimSpace(os.Getenv("TWITTER_LIVE_VERIFICATION_CODE")) if next.StepID == LoginStepIDVerification { @@ -145,6 +148,10 @@ func TestLiveNativeLoginStageProbe(t *testing.T) { result, err = webLogin.SubmitCredentials(ctx, identifier, password) logStage(t, "credentials", result, err) + if errors.Is(err, twittermeow.ErrJetfuelCastleTokenRequired) { + t.Log("native login reached the client BrowserAuth Castle boundary") + return + } if err != nil { t.Fatalf("SubmitCredentials() failed: %v", err) } @@ -175,6 +182,10 @@ func TestLiveIdentifierOnlyProbe(t *testing.T) { result, err = webLogin.SubmitIdentifier(ctx, identifier) logStage(t, "identifier", result, err) + if errors.Is(err, twittermeow.ErrJetfuelCastleTokenRequired) { + t.Log("identifier submission reached the client BrowserAuth Castle boundary") + return + } if err == nil && result.Status != twittermeow.WebLoginStatusNeedsPassword { t.Fatalf("SubmitIdentifier() status = %s, want password step or a login error", result.Status) } diff --git a/pkg/connector/login_test.go b/pkg/connector/login_test.go index 5282a38f..726b71f6 100644 --- a/pkg/connector/login_test.go +++ b/pkg/connector/login_test.go @@ -2,7 +2,12 @@ package connector import ( "context" + "encoding/base64" + "encoding/json" "errors" + "fmt" + "regexp" + "strconv" "strings" "testing" @@ -57,6 +62,21 @@ func TestHandleWebLoginCredentialsErrorRetriesOnlyCredentialErrors(t *testing.T) } } +func TestGetLoginFlowsAdvertisesNativePasswordOnly(t *testing.T) { + var tc TwitterConnector + flows := tc.GetLoginFlows() + + if len(flows) != 1 { + t.Fatalf("len(flows) = %d, want 1", len(flows)) + } + if flows[0].ID != LoginFlowIDPassword { + t.Fatalf("flow ID = %s, want %s", flows[0].ID, LoginFlowIDPassword) + } + if strings.Contains(strings.ToLower(flows[0].Description), "cookie") { + t.Fatalf("flow description = %q, want native login flow", flows[0].Description) + } +} + func TestMakeAuthMethodStepUsesNativeSelect(t *testing.T) { methods := []twittermeow.WebLoginAuthMethod{ {ID: "Totp", Name: "Authenticator App", Supported: true}, @@ -103,6 +123,254 @@ func TestWebLoginUnsupportedInstructionsUsesChallengeDescription(t *testing.T) { } } +func TestMakeCastleTokenStepUsesClientWebviewExtraction(t *testing.T) { + if castleTokenBatchSize < 6 { + t.Fatalf("castleTokenBatchSize = %d, want enough tokens for the six-request 2FA login path", castleTokenBatchSize) + } + info := twittermeow.JetfuelCastleTokenInfo{ + ScriptURL: "https://abs.twimg.com/responsive-web/client-web/ondemand.castle.1ff15ffa.js", + PublicKey: "castle-public-key", + } + step := makeCastleTokenStep(info, "test-user", "") + + if step.Type != bridgev2.LoginStepTypeCookies { + t.Fatalf("Type = %s, want cookies webview step", step.Type) + } + if step.StepID != LoginStepIDCastleToken { + t.Fatalf("StepID = %s, want %s", step.StepID, LoginStepIDCastleToken) + } + if step.CookiesParams == nil { + t.Fatal("CookiesParams = nil") + } + if step.CookiesParams.UserAgent != "" { + t.Fatalf("UserAgent = %q, want the webview's native user agent", step.CookiesParams.UserAgent) + } + if step.CookiesParams.URL != castleTokenWebviewURL { + t.Fatalf("URL = %q", step.CookiesParams.URL) + } + if strings.Contains(step.CookiesParams.URL, "/i/flow/login") { + t.Fatalf("URL = %q, want neutral webview page", step.CookiesParams.URL) + } + if !strings.Contains(step.CookiesParams.WaitForURLPattern, "robots") { + t.Fatalf("WaitForURLPattern = %q, want neutral X URL", step.CookiesParams.WaitForURLPattern) + } + if !strings.Contains(step.CookiesParams.ExtractJS, info.ScriptURL) || + !strings.Contains(step.CookiesParams.ExtractJS, "createRequestToken") { + t.Fatalf("ExtractJS does not load X Castle token generator") + } + if !strings.Contains(step.CookiesParams.ExtractJS, castleTokenContextURL) { + t.Fatalf("ExtractJS does not include the X login context") + } + if !strings.Contains(step.CookiesParams.ExtractJS, "showBrowserLoginStatus") || + !strings.Contains(step.CookiesParams.ExtractJS, "Signing in to X") || + !strings.Contains(step.CookiesParams.ExtractJS, "mautrix-twitter-login-status") || + !strings.Contains(step.CookiesParams.ExtractJS, "body.replaceChildren(container)") { + t.Fatalf("ExtractJS does not replace robots.txt with the visible X login status") + } + if !strings.Contains(step.CookiesParams.ExtractJS, "__BEEP_BEEP_AUTH_RESULTS__") { + t.Fatalf("ExtractJS does not store the BrowserAuth result for Desktop polling") + } + if !strings.Contains(step.CookiesParams.ExtractJS, "__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__") { + t.Fatalf("ExtractJS does not guard against repeated BrowserAuth navigation runs") + } + if !strings.Contains(step.CookiesParams.ExtractJS, "castleTokenBatchSize") { + t.Fatalf("ExtractJS does not generate a Castle token batch") + } + fields := map[string]bridgev2.LoginCookieField{} + for _, field := range step.CookiesParams.Fields { + fields[field.ID] = field + } + if field, ok := fields[loginFieldCastleToken]; !ok || !field.Required { + t.Fatalf("Fields = %#v, want required Castle token field", step.CookiesParams.Fields) + } else if !hasCookieFieldSource(field, bridgev2.LoginCookieTypeLocalStorage, "fi.mau.twitter.castle_token") { + t.Fatalf("Castle token field sources = %#v, want local_storage fallback", field.Sources) + } + for index := 2; index <= castleTokenBatchSize; index++ { + fieldID := castleTokenFieldID(index) + field, ok := fields[fieldID] + if !ok { + t.Fatalf("Fields missing optional Castle token batch field %q", fieldID) + } + if field.Required { + t.Fatalf("Castle token batch field %q is required", fieldID) + } + if !hasCookieFieldSource(field, bridgev2.LoginCookieTypeLocalStorage, "fi.mau.twitter.castle_token_"+strconv.Itoa(index)) { + t.Fatalf("Castle token batch field %q sources = %#v, want local_storage fallback", fieldID, field.Sources) + } + } + for _, expected := range browserHeaderFields { + field, ok := fields[expected.ID] + if !ok { + t.Fatalf("Fields missing browser header %q", expected.ID) + } + if field.Required != expected.Required { + t.Fatalf("browser header %q required = %t, want %t", expected.ID, field.Required, expected.Required) + } + if !hasCookieFieldSource(field, bridgev2.LoginCookieTypeRequestHeader, expected.HeaderName) { + t.Fatalf("browser header %q sources = %#v, want request header %q", expected.ID, field.Sources, expected.HeaderName) + } + if field.Sources[0].RequestURLRegex == "" || !hasCookieFieldSource(field, bridgev2.LoginCookieTypeSpecial, expected.ID) { + t.Fatalf("browser header %q sources = %#v, want x.com request extraction and webview JS fallback", expected.ID, field.Sources) + } + } + if !strings.Contains(step.CookiesParams.ExtractJS, "navigator.userAgent") || + !strings.Contains(step.CookiesParams.ExtractJS, "navigator.userAgentData") { + t.Fatal("ExtractJS does not include the webview browser-header fallback") + } + for _, name := range castleTokenCookieNames { + field, ok := fields[name] + if !ok { + t.Fatalf("Fields missing optional browser cookie %q", name) + } + if field.Required { + t.Fatalf("browser cookie field %q is required", name) + } + if !strings.Contains(step.CookiesParams.ExtractJS, name) { + t.Fatalf("ExtractJS does not include browser cookie %q", name) + } + if !hasCookieFieldSource(field, bridgev2.LoginCookieTypeLocalStorage, "fi.mau.twitter.cookie."+name) { + t.Fatalf("browser cookie field %q sources = %#v, want local_storage fallback", name, field.Sources) + } + } + for _, name := range []string{"auth_token", "ct0", "twid", "kdt"} { + if _, ok := fields[name]; ok { + t.Fatalf("Fields include auth cookie %q", name) + } + } +} + +func TestBrowserHeadersFromInput(t *testing.T) { + input := map[string]string{ + loginFieldBrowserUserAgent: "test user agent", + loginFieldBrowserSecCHUA: `"Chromium";v="150"`, + loginFieldBrowserPlatform: `"Android"`, + loginFieldBrowserMobile: "?1", + } + got := browserHeadersFromInput(input) + if got.UserAgent != input[loginFieldBrowserUserAgent] || + got.SecCHUserAgent != input[loginFieldBrowserSecCHUA] || + got.SecCHPlatform != input[loginFieldBrowserPlatform] || + got.SecCHMobile != input[loginFieldBrowserMobile] { + t.Fatalf("browserHeadersFromInput() = %#v", got) + } +} + +func TestUserLoginMetadataPersistsBrowserHeaders(t *testing.T) { + meta := UserLoginMetadata{ + Cookies: "ct0=test", + BrowserHeaders: &twittermeow.BrowserHeaders{ + UserAgent: "test user agent", + SecCHUserAgent: `"Chromium";v="150"`, + SecCHPlatform: `"Windows"`, + SecCHMobile: "?0", + }, + } + encoded, err := json.Marshal(meta) + if err != nil { + t.Fatalf("json.Marshal() error = %v", err) + } + var decoded UserLoginMetadata + if err = json.Unmarshal(encoded, &decoded); err != nil { + t.Fatalf("json.Unmarshal() error = %v", err) + } + if decoded.BrowserHeaders == nil || *decoded.BrowserHeaders != *meta.BrowserHeaders { + t.Fatalf("decoded BrowserHeaders = %#v, want %#v", decoded.BrowserHeaders, meta.BrowserHeaders) + } +} + +func hasCookieFieldSource(field bridgev2.LoginCookieField, sourceType bridgev2.LoginCookieFieldSourceType, name string) bool { + for _, source := range field.Sources { + if source.Type == sourceType && source.Name == name { + return true + } + } + return false +} + +func TestDecodeCastleTokenInputAcceptsHeaderCaptureValue(t *testing.T) { + token := strings.Repeat("castle-token-", 64) + encoded := base64.RawURLEncoding.EncodeToString([]byte(token)) + got, err := decodeCastleTokenInput(castleTokenHeaderPrefix + encoded) + if err != nil { + t.Fatalf("decodeCastleTokenInput() failed: %v", err) + } + if got != token { + t.Fatalf("decodeCastleTokenInput() = %q, want original token", got) + } + + got, err = decodeCastleTokenInput(token) + if err != nil { + t.Fatalf("decodeCastleTokenInput(raw) failed: %v", err) + } + if got != token { + t.Fatalf("decodeCastleTokenInput(raw) = %q, want original token", got) + } +} + +func TestDecodeCastleTokenInputStripsTransportWhitespace(t *testing.T) { + token := strings.Repeat("castle-token-", 64) + wrapped := token[:80] + "\r\n" + token[80:160] + "\n\t " + token[160:] + got, err := decodeCastleTokenInput(wrapped) + if err != nil { + t.Fatalf("decodeCastleTokenInput(wrapped) failed: %v", err) + } + if got != token { + t.Fatalf("decodeCastleTokenInput(wrapped) = %q, want original token", got) + } + + encoded := base64.RawURLEncoding.EncodeToString([]byte(token)) + wrappedEncoded := encoded[:80] + "\n" + encoded[80:] + got, err = decodeCastleTokenInput(castleTokenHeaderPrefix + wrappedEncoded) + if err != nil { + t.Fatalf("decodeCastleTokenInput(wrapped header) failed: %v", err) + } + if got != token { + t.Fatalf("decodeCastleTokenInput(wrapped header) = %q, want original token", got) + } +} + +func TestDecodeCastleTokenBatchInput(t *testing.T) { + tokensByIndex := make([]string, castleTokenBatchSize) + input := make(map[string]string, castleTokenBatchSize) + for index := 1; index <= castleTokenBatchSize; index++ { + token := strings.Repeat(fmt.Sprintf("castle-%d-", index), 64) + tokensByIndex[index-1] = token + input[castleTokenFieldID(index)] = token + } + input[loginFieldCastleToken] = tokensByIndex[0][:80] + "\n" + tokensByIndex[0][80:] + + tokens, err := decodeCastleTokenBatchInput(input) + if err != nil { + t.Fatalf("decodeCastleTokenBatchInput() error = %v", err) + } + if len(tokens) != castleTokenBatchSize { + t.Fatalf("len(tokens) = %d, want %d", len(tokens), castleTokenBatchSize) + } + for index := range tokens { + if tokens[index] != tokensByIndex[index] { + t.Fatalf("tokens[%d] = %q, want token %d", index, tokens[index], index+1) + } + } +} + +func TestCastleTokenFieldPatternAcceptsWrappedTransportValue(t *testing.T) { + fields := castleTokenCookieFields() + if len(fields) == 0 || fields[0].ID != loginFieldCastleToken { + t.Fatalf("first field = %#v, want Castle token field", fields) + } + token := strings.Repeat("castle-token-", 64) + wrapped := token[:80] + "\n" + token[80:] + if !regexp.MustCompile(fields[0].Pattern).MatchString(wrapped) { + t.Fatalf("Castle token field pattern %q rejected wrapped token transport", fields[0].Pattern) + } + if strings.Contains(fields[0].Pattern, "(?s)") { + t.Fatalf("Castle token field pattern %q must be JavaScript RegExp compatible", fields[0].Pattern) + } + if !strings.Contains(fields[0].Pattern, `[\s\S]`) { + t.Fatalf("Castle token field pattern %q should match newlines without JS-only-invalid flags", fields[0].Pattern) + } +} + func TestFindWebLoginAuthMethodMatchesNameOrID(t *testing.T) { methods := []twittermeow.WebLoginAuthMethod{ {ID: "Totp", Name: "Authenticator App", Supported: true}, diff --git a/pkg/connector/msgconv.go b/pkg/connector/msgconv.go index ede9b34e..6352bd54 100644 --- a/pkg/connector/msgconv.go +++ b/pkg/connector/msgconv.go @@ -654,11 +654,11 @@ func downloadFile(ctx context.Context, cli *twittermeow.Client, url string) (*ht if isPublicCDNURL(url) { // Public CDN URLs use minimal headers - no auth, no Origin/Referer - req.Header.Set("User-Agent", twittermeow.UserAgent) + req.Header.Set("User-Agent", cli.GetUserAgent()) req.Header.Set("Accept", "*/*") } else { // Authenticated requests use full headers with cookies - headers := twittermeow.BaseHeaders.Clone() + headers := cli.GetBaseHeaders() headers.Set("Cookie", cli.GetCookieString()) req.Header = headers } diff --git a/pkg/twittermeow/castle_token.go b/pkg/twittermeow/castle_token.go index c09830bc..9c2bb9bc 100644 --- a/pkg/twittermeow/castle_token.go +++ b/pkg/twittermeow/castle_token.go @@ -1,662 +1,65 @@ package twittermeow import ( - "crypto/rand" - "encoding/base64" - "encoding/binary" - "encoding/hex" - "fmt" - "math" + "errors" "net/url" - "time" + "strings" ) -const castlePublicKey = "pk_AvRa79bHyJSYSQHnRpcVtzyxetSvFerx" +var ErrJetfuelCastleTokenRequired = errors.New("x login needs a Castle token from the client webview") -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 (info JetfuelCastleTokenInfo) IsValid() bool { + return strings.TrimSpace(info.ScriptURL) != "" && strings.TrimSpace(info.PublicKey) != "" } -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 (c *Client) JetfuelCastleTokenInfo() JetfuelCastleTokenInfo { + return c.jetfuelCastleInfo } -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] +// SetNextJetfuelCastleTokens provides browser-generated Castle tokens for the +// next Jetfuel form submissions. Tokens are consumed in order and only once. +func (c *Client) SetNextJetfuelCastleTokens(tokens []string) { + c.jetfuelCastleTokenMu.Lock() + defer c.jetfuelCastleTokenMu.Unlock() + c.jetfuelCastleTokens = c.jetfuelCastleTokens[:0] + for _, token := range tokens { + if token = strings.TrimSpace(token); token != "" { + c.jetfuelCastleTokens = append(c.jetfuelCastleTokens, token) } - 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:]) +func (c *Client) takeNextJetfuelCastleToken() string { + c.jetfuelCastleTokenMu.Lock() + defer c.jetfuelCastleTokenMu.Unlock() + if len(c.jetfuelCastleTokens) == 0 { + return "" } - return blocks + token := c.jetfuelCastleTokens[0] + copy(c.jetfuelCastleTokens, c.jetfuelCastleTokens[1:]) + c.jetfuelCastleTokens = c.jetfuelCastleTokens[:len(c.jetfuelCastleTokens)-1] + return token } -func randFloat(min, max float64) float64 { - n, err := randomUint64() - if err != nil { - return min - } - return min + (float64(n>>11) / (1 << 53) * (max - min)) +func (c *Client) HasNextJetfuelCastleToken() bool { + c.jetfuelCastleTokenMu.Lock() + defer c.jetfuelCastleTokenMu.Unlock() + return len(c.jetfuelCastleTokens) > 0 } -func cryptoRandInt(minInclusive, maxExclusive int) (int, error) { - if maxExclusive <= minInclusive { - return minInclusive, nil - } - n, err := randomUint64() - if err != nil { - return 0, err +func (c *Client) addJetfuelCastleTokenToForm(form url.Values) error { + if form.Get("$castle_token") != "" { + return nil } - 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 + token := c.takeNextJetfuelCastleToken() + if token == "" { + return ErrJetfuelCastleTokenRequired } - return binary.BigEndian.Uint64(buf[:]), nil -} - -func randomBytes(length int) ([]byte, error) { - out := make([]byte, length) - _, err := rand.Read(out) - return out, err + form.Set("$castle_token", token) + return nil } diff --git a/pkg/twittermeow/castle_token_test.go b/pkg/twittermeow/castle_token_test.go deleted file mode 100644 index dc6488a9..00000000 --- a/pkg/twittermeow/castle_token_test.go +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index def25cff..00000000 --- a/pkg/twittermeow/castle_token_test_helpers_test.go +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 51583c81..00000000 --- a/pkg/twittermeow/castle_web_token.go +++ /dev/null @@ -1,5329 +0,0 @@ -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 fc2eec0f..97a85bdd 100644 --- a/pkg/twittermeow/client.go +++ b/pkg/twittermeow/client.go @@ -64,8 +64,12 @@ type Client struct { xchatProcessor *XChatEventProcessor keyManager *crypto.KeyManager - xchatToken *cachedXChatToken - xchatTokenMu sync.Mutex + jetfuelCastleTokens []string + jetfuelCastleInfo JetfuelCastleTokenInfo + browserHeaders BrowserHeaders + jetfuelCastleTokenMu sync.Mutex + xchatToken *cachedXChatToken + xchatTokenMu sync.Mutex } func NewClient(cookies *cookies.Cookies, store crypto.KeyStore, logger zerolog.Logger) *Client { @@ -95,6 +99,17 @@ func (c *Client) GetCookieString() string { return c.cookies.String() } +func (c *Client) SetCookies(values map[string]string) { + for name, value := range values { + name = strings.TrimSpace(name) + value = strings.TrimSpace(value) + if name == "" || value == "" { + continue + } + c.cookies.Set(cookies.XCookieName(name), value) + } +} + func (c *Client) SetSession(sess *CachedSession) { c.session = sess } @@ -439,6 +454,16 @@ func (c *Client) parseMainPageHTML(ctx context.Context, mainPageResp *http.Respo c.session.Country = country c.session.VerificationToken = verificationToken c.session.loadingAnims = loadingAnims + c.jetfuelCastleInfo = JetfuelCastleTokenInfo{ + ScriptURL: methods.ParseOndemandCastleURLFromScript([]byte(mainPageHTML)), + PublicKey: methods.ParseResponsiveWebCastlePublicKey(mainPageHTML), + } + if !c.jetfuelCastleInfo.IsValid() { + c.Logger.Debug(). + Bool("has_script_url", c.jetfuelCastleInfo.ScriptURL != ""). + Bool("has_public_key", c.jetfuelCastleInfo.PublicKey != ""). + Msg("X Castle web metadata not found in main page HTML") + } guestToken := methods.ParseGuestToken(mainPageHTML) if guestToken == "" { diff --git a/pkg/twittermeow/headers.go b/pkg/twittermeow/headers.go index 503811ab..daafc545 100644 --- a/pkg/twittermeow/headers.go +++ b/pkg/twittermeow/headers.go @@ -2,21 +2,33 @@ package twittermeow import ( "net/http" + "strings" "go.mau.fi/mautrix-twitter/pkg/twittermeow/cookies" "go.mau.fi/mautrix-twitter/pkg/twittermeow/data/endpoints" ) +// These defaults bootstrap requests made before BrowserAuth captures the +// client webview's actual HTTP headers. const BrowserName = "Chrome" -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 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 SecCHPlatform = `"` + OSName + `"` const SecCHMobile = "?0" const UDID = OSName + "/" + BrowserName +// BrowserHeaders contains the browser fingerprint captured from the client +// webview. Empty client-hint values are intentional for non-Chromium browsers. +type BrowserHeaders struct { + UserAgent string `json:"user_agent"` + SecCHUserAgent string `json:"sec_ch_ua,omitempty"` + SecCHPlatform string `json:"sec_ch_ua_platform,omitempty"` + SecCHMobile string `json:"sec_ch_ua_mobile,omitempty"` +} + 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"}, @@ -48,7 +60,7 @@ func (c *Client) buildHeaders(opts HeaderOpts) http.Header { opts.Extra = make(map[string]string) } - headers := BaseHeaders.Clone() + headers := c.GetBaseHeaders() if opts.WithCookies { opts.Extra["cookie"] = c.cookies.String() } @@ -93,3 +105,73 @@ func (c *Client) buildHeaders(opts HeaderOpts) http.Header { return headers } + +func normalizeBrowserHeader(value string) string { + value = strings.TrimSpace(value) + if value == "" || len(value) > 1024 { + return "" + } + for _, char := range value { + if char < 0x20 || char == 0x7f { + return "" + } + } + return value +} + +func (headers BrowserHeaders) normalized() BrowserHeaders { + headers.UserAgent = normalizeBrowserHeader(headers.UserAgent) + headers.SecCHUserAgent = normalizeBrowserHeader(headers.SecCHUserAgent) + headers.SecCHPlatform = normalizeBrowserHeader(headers.SecCHPlatform) + headers.SecCHMobile = normalizeBrowserHeader(headers.SecCHMobile) + if headers.SecCHMobile != "?0" && headers.SecCHMobile != "?1" { + headers.SecCHMobile = "" + } + if headers.UserAgent == "" { + return BrowserHeaders{} + } + return headers +} + +// SetBrowserHeaders installs a webview-captured browser fingerprint. It returns +// false when the required User-Agent is missing or unsafe to use as a header. +func (c *Client) SetBrowserHeaders(headers BrowserHeaders) bool { + headers = headers.normalized() + if headers.UserAgent == "" { + return false + } + c.browserHeaders = headers + return true +} + +func (c *Client) GetBrowserHeaders() BrowserHeaders { + return c.browserHeaders +} + +func (c *Client) GetBaseHeaders() http.Header { + headers := BaseHeaders.Clone() + browser := c.browserHeaders + if browser.UserAgent == "" { + return headers + } + headers.Set("User-Agent", browser.UserAgent) + setOptionalHeader(headers, "Sec-Ch-Ua", browser.SecCHUserAgent) + setOptionalHeader(headers, "Sec-Ch-Ua-Platform", browser.SecCHPlatform) + setOptionalHeader(headers, "Sec-Ch-Ua-Mobile", browser.SecCHMobile) + return headers +} + +func setOptionalHeader(headers http.Header, name, value string) { + if value == "" { + headers.Del(name) + } else { + headers.Set(name, value) + } +} + +func (c *Client) GetUserAgent() string { + if c.browserHeaders.UserAgent != "" { + return c.browserHeaders.UserAgent + } + return UserAgent +} diff --git a/pkg/twittermeow/headers_test.go b/pkg/twittermeow/headers_test.go new file mode 100644 index 00000000..1973530d --- /dev/null +++ b/pkg/twittermeow/headers_test.go @@ -0,0 +1,67 @@ +package twittermeow + +import ( + "testing" + + "github.com/rs/zerolog" + + "go.mau.fi/mautrix-twitter/pkg/twittermeow/cookies" +) + +func TestClientUsesCapturedBrowserHeaders(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + captured := BrowserHeaders{ + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/150.0.0.0 Safari/537.36", + SecCHUserAgent: `"Chromium";v="150", "Google Chrome";v="150", "Not_A Brand";v="99"`, + SecCHPlatform: `"Windows"`, + SecCHMobile: "?0", + } + if !client.SetBrowserHeaders(captured) { + t.Fatal("SetBrowserHeaders() rejected valid captured headers") + } + + headers := client.GetBaseHeaders() + if got := headers.Get("User-Agent"); got != captured.UserAgent { + t.Fatalf("User-Agent = %q, want %q", got, captured.UserAgent) + } + if got := headers.Get("Sec-Ch-Ua"); got != captured.SecCHUserAgent { + t.Fatalf("Sec-Ch-Ua = %q, want %q", got, captured.SecCHUserAgent) + } + if got := headers.Get("Sec-Ch-Ua-Platform"); got != captured.SecCHPlatform { + t.Fatalf("Sec-Ch-Ua-Platform = %q, want %q", got, captured.SecCHPlatform) + } + if got := headers.Get("Sec-Ch-Ua-Mobile"); got != captured.SecCHMobile { + t.Fatalf("Sec-Ch-Ua-Mobile = %q, want %q", got, captured.SecCHMobile) + } + if got := BaseHeaders.Get("User-Agent"); got != UserAgent { + t.Fatalf("captured fingerprint mutated fallback headers: %q", got) + } +} + +func TestClientOmitsUnavailableClientHints(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + userAgent := "Mozilla/5.0 (Android 15; Mobile; rv:141.0) Gecko/141.0 Firefox/141.0" + if !client.SetBrowserHeaders(BrowserHeaders{UserAgent: userAgent}) { + t.Fatal("SetBrowserHeaders() rejected Firefox user agent") + } + + headers := client.GetBaseHeaders() + if got := headers.Get("User-Agent"); got != userAgent { + t.Fatalf("User-Agent = %q, want %q", got, userAgent) + } + for _, name := range []string{"Sec-Ch-Ua", "Sec-Ch-Ua-Platform", "Sec-Ch-Ua-Mobile"} { + if got := headers.Get(name); got != "" { + t.Fatalf("%s = %q, want omitted", name, got) + } + } +} + +func TestClientRejectsUnsafeCapturedBrowserHeaders(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + if client.SetBrowserHeaders(BrowserHeaders{UserAgent: "browser\r\nX-Injected: true"}) { + t.Fatal("SetBrowserHeaders() accepted a header injection") + } + if got := client.GetUserAgent(); got != UserAgent { + t.Fatalf("GetUserAgent() = %q, want fallback %q", got, UserAgent) + } +} diff --git a/pkg/twittermeow/login_jetfuel.go b/pkg/twittermeow/login_jetfuel.go index 6a7cd0e2..c01225d7 100644 --- a/pkg/twittermeow/login_jetfuel.go +++ b/pkg/twittermeow/login_jetfuel.go @@ -3,7 +3,6 @@ package twittermeow import ( "bytes" "context" - "encoding/base64" "encoding/json" "errors" "fmt" @@ -27,6 +26,7 @@ const ( webLoginBackendJetfuel webLoginBackend = "jetfuel" jetfuelHeaderVersion = "JP-5" + jetfuelHeaderTheme = "dark" ) var ( @@ -65,7 +65,7 @@ type jetfuelLoginResponse struct { } func (wls *WebLoginSession) startJetfuel(ctx context.Context) (*WebLoginResult, error) { - if err := wls.client.loadPage(ctx, endpoints.BASE_FLOW_LOGIN_URL); err != nil { + if err := wls.client.loadPage(ctx, endpoints.JETFUEL_LOGIN_REFERER_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 { @@ -96,8 +96,10 @@ func (wls *WebLoginSession) submitJetfuelIdentifier(ctx context.Context, identif 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") + if shouldSendJetfuelViewerContextEvent() { + 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}, @@ -169,6 +171,10 @@ func isJetfuelPrePasswordParityError(err error) bool { strings.Contains(text, "official x apps") || strings.Contains(text, "use x.com")) } +func IsWebLoginPrePasswordParityError(err error) bool { + return isJetfuelPrePasswordParityError(err) +} + 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}, @@ -540,27 +546,12 @@ func (c *Client) jetfuelGet(ctx context.Context, path string) ([]byte, error) { 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") + c.Logger.Trace().Err(err).Msg("Failed to attach Castle request token") + return nil, err } 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) @@ -577,11 +568,10 @@ func (c *Client) jetfuelRequest(ctx context.Context, path, method string, body [ "sec-fetch-site": "same-origin", "timezone": jetfuelTimezone(), "x-client-transaction-id": txID, - "x-jf-client-theme": "light", + "x-jf-client-theme": jetfuelHeaderTheme, "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 } @@ -589,7 +579,7 @@ func (c *Client) jetfuelRequest(ctx context.Context, path, method string, body [ WithNonAuthBearer: true, WithCookies: true, WithXGuestToken: true, - Referer: endpoints.BASE_URL + "/", + Referer: endpoints.JETFUEL_LOGIN_REFERER_URL, Extra: extra, }) headers.Del("x-twitter-client-language") @@ -699,60 +689,8 @@ func jetfuelTimezone() string { } } -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 shouldSendJetfuelViewerContextEvent() bool { + return strings.TrimSpace(os.Getenv("TWITTER_JETFUEL_VIEWER_CONTEXT")) == "1" } func ensureLeadingSlash(path string) string { diff --git a/pkg/twittermeow/login_jetfuel_live_probe_test.go b/pkg/twittermeow/login_jetfuel_live_probe_test.go index ea9ba9a2..bac9ff31 100644 --- a/pkg/twittermeow/login_jetfuel_live_probe_test.go +++ b/pkg/twittermeow/login_jetfuel_live_probe_test.go @@ -119,17 +119,11 @@ func TestLiveJetfuelIdentifierMetadataProbe(t *testing.T) { 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") + } else { + t.Skip("TWITTER_CASTLE_TOKEN_PROBE is required; Castle tokens are now generated by the client webview") } body, err := client.jetfuelPostForm(ctx, endpoints.JETFUEL_BEGIN_LOGIN_PATH, form) if err != nil { diff --git a/pkg/twittermeow/login_web.go b/pkg/twittermeow/login_web.go index 93707e87..7dacbe89 100644 --- a/pkg/twittermeow/login_web.go +++ b/pkg/twittermeow/login_web.go @@ -131,6 +131,10 @@ func (wls *WebLoginSession) Client() *Client { return wls.client } +func (wls *WebLoginSession) UsesJetfuel() bool { + return wls != nil && wls.backend == webLoginBackendJetfuel +} + func (wls *WebLoginSession) Start(ctx context.Context) (*WebLoginResult, error) { if result, err := wls.startJetfuel(ctx); err == nil { return result, nil @@ -225,6 +229,13 @@ func (wls *WebLoginSession) SubmitCredentials(ctx context.Context, identifier, p return wls.SubmitPassword(ctx, password) } +func (wls *WebLoginSession) SubmitCombinedCredentials(ctx context.Context, identifier, password string) (*WebLoginResult, error) { + if wls.backend == webLoginBackendJetfuel { + return wls.submitJetfuelCombinedCredentials(ctx, identifier, password) + } + return wls.SubmitCredentials(ctx, identifier, password) +} + func (wls *WebLoginSession) SubmitPassword(ctx context.Context, password string) (*WebLoginResult, error) { if wls.backend == webLoginBackendJetfuel { return wls.submitJetfuelPassword(ctx, password) @@ -264,6 +275,16 @@ func (wls *WebLoginSession) SubmitAuthMethod(ctx context.Context, methodID strin return nil, fmt.Errorf("%w: auth method selection is unsupported for %s login", ErrWebLoginUnexpectedSubtask, wls.backend) } +func (wls *WebLoginSession) SubmitPendingTwoFactor(ctx context.Context) (*WebLoginResult, error) { + if wls.backend != webLoginBackendJetfuel { + return nil, fmt.Errorf("%w: two-factor prelude is unsupported for %s login", ErrWebLoginUnexpectedSubtask, wls.backend) + } + if wls.jetfuel == nil || wls.jetfuel.twoFactorAction == "" { + return nil, fmt.Errorf("%w: jetfuel two-factor action is missing", ErrWebLoginUnexpectedSubtask) + } + return wls.submitJetfuelBeginTwoFactor(ctx, wls.jetfuel.twoFactorAction) +} + func (wls *WebLoginSession) SubmitText(ctx context.Context, text string) (*WebLoginResult, error) { if wls.backend == webLoginBackendJetfuel { return wls.submitJetfuelText(ctx, text) diff --git a/pkg/twittermeow/login_web_test.go b/pkg/twittermeow/login_web_test.go index 90878115..f31bd304 100644 --- a/pkg/twittermeow/login_web_test.go +++ b/pkg/twittermeow/login_web_test.go @@ -2,7 +2,6 @@ package twittermeow import ( "context" - "encoding/base64" "encoding/json" "errors" "io" @@ -418,12 +417,160 @@ func TestJetfuelAuthMethodFormUsesSubmitID(t *testing.T) { } } +func TestJetfuelCastleTokenUsesClientProvidedOneShotToken(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + form := make(url.Values) + + if err := client.addJetfuelCastleTokenToForm(form); !errors.Is(err, ErrJetfuelCastleTokenRequired) { + t.Fatalf("addJetfuelCastleTokenToForm() error = %v, want ErrJetfuelCastleTokenRequired", err) + } + + client.SetNextJetfuelCastleTokens([]string{" castle-from-webview "}) + if err := client.addJetfuelCastleTokenToForm(form); err != nil { + t.Fatalf("addJetfuelCastleTokenToForm() error = %v", err) + } + if got := form.Get("$castle_token"); got != "castle-from-webview" { + t.Fatalf("$castle_token = %q, want webview token", got) + } + if err := client.addJetfuelCastleTokenToForm(make(url.Values)); !errors.Is(err, ErrJetfuelCastleTokenRequired) { + t.Fatalf("second addJetfuelCastleTokenToForm() error = %v, want one-shot token to be consumed", err) + } +} + +func TestJetfuelCastleTokenConsumesQueuedTokensInOrder(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.SetNextJetfuelCastleTokens([]string{" first-token ", "", "second-token"}) + + firstForm := make(url.Values) + if err := client.addJetfuelCastleTokenToForm(firstForm); err != nil { + t.Fatalf("first addJetfuelCastleTokenToForm() error = %v", err) + } + if got := firstForm.Get("$castle_token"); got != "first-token" { + t.Fatalf("first $castle_token = %q, want first-token", got) + } + if !client.HasNextJetfuelCastleToken() { + t.Fatal("HasNextJetfuelCastleToken() = false, want queued second token") + } + + secondForm := make(url.Values) + if err := client.addJetfuelCastleTokenToForm(secondForm); err != nil { + t.Fatalf("second addJetfuelCastleTokenToForm() error = %v", err) + } + if got := secondForm.Get("$castle_token"); got != "second-token" { + t.Fatalf("second $castle_token = %q, want second-token", got) + } + if client.HasNextJetfuelCastleToken() { + t.Fatal("HasNextJetfuelCastleToken() = true, want queue exhausted") + } +} + +func TestSetCookiesPreservesExistingLoginCookies(t *testing.T) { + client := NewClient(cookies.NewCookies(map[string]string{ + "att": "native-login-cookie", + "guest_id": "old-guest-id", + }), nil, zerolog.Nop()) + + client.SetCookies(map[string]string{ + "guest_id": "browser-guest-id", + "__cf_bm": "browser-cf-cookie", + }) + + if got := client.cookies.Get(cookies.XAtt); got != "native-login-cookie" { + t.Fatalf("att cookie = %q, want native login cookie to be preserved", got) + } + if got := client.cookies.Get(cookies.XGuestID); got != "browser-guest-id" { + t.Fatalf("guest_id cookie = %q, want webview cookie to update existing value", got) + } + if got := client.cookies.Get(cookies.XCookieName("__cf_bm")); got != "browser-cf-cookie" { + t.Fatalf("__cf_bm cookie = %q, want webview cookie", got) + } +} + +func TestSubmitJetfuelPasswordDefersTwoFactorPreludeUntilNextCastleToken(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.SetNextJetfuelCastleTokens([]string{"password-castle-token"}) + + var paths []string + client.HTTP = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + body, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("ReadAll(request body) error = %v", err) + } + paths = append(paths, req.URL.Path) + form := string(body) + switch req.URL.Path { + case "/i/jfapi" + endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH: + if !strings.Contains(form, "%24castle_token=password-castle-token") { + t.Fatalf("password request body = %q, want password Castle token", form) + } + responseBody := "begin_two_factor_auth\x00session_token\x0012345678-1234-1234-1234-123456789abc\x00prelude_dispatch_id\x00abcdefab-1234-1234-1234-abcdefabcdef" + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(responseBody)), + }, nil + case "/i/jfapi" + endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH: + if !strings.Contains(form, "%24castle_token=twofactor-castle-token") { + t.Fatalf("two-factor request body = %q, want second Castle token", form) + } + if !strings.Contains(form, "session_token=12345678-1234-1234-1234-123456789abc") { + t.Fatalf("two-factor request body = %q, want session token", form) + } + responseBody := "Select a method to authenticate\x00two_factor_method\x00Totp\x00BackupCode" + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(responseBody)), + }, nil + default: + t.Fatalf("unexpected request path: %s", req.URL.Path) + return nil, nil + } + })} + session := NewWebLoginSession(client) + session.backend = webLoginBackendJetfuel + session.jetfuel = &jetfuelLoginState{ + identifier: "test-user", + passwordAction: endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH, + } + + result, err := session.SubmitPassword(context.Background(), "password") + if !errors.Is(err, ErrJetfuelCastleTokenRequired) { + t.Fatalf("SubmitPassword() error = %v, want ErrJetfuelCastleTokenRequired", err) + } + if result != nil { + t.Fatalf("SubmitPassword() result = %#v, want nil while waiting for next Castle token", result) + } + if got := session.jetfuel.twoFactorAction; got != endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH { + t.Fatalf("twoFactorAction = %q, want begin two-factor action", got) + } + if len(paths) != 1 || paths[0] != "/i/jfapi"+endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH { + t.Fatalf("paths after password = %#v, want only password request", paths) + } + + client.SetNextJetfuelCastleTokens([]string{"twofactor-castle-token"}) + result, err = session.SubmitPendingTwoFactor(context.Background()) + if err != nil { + t.Fatalf("SubmitPendingTwoFactor() error = %v", err) + } + if result == nil || result.Status != WebLoginStatusNeedsAuthMethod { + t.Fatalf("SubmitPendingTwoFactor() result = %#v, want auth method chooser", result) + } + if len(paths) != 2 || paths[1] != "/i/jfapi"+endpoints.JETFUEL_BEGIN_TWO_FACTOR_AUTH_PATH { + t.Fatalf("paths after pending two-factor = %#v", paths) + } +} + func TestSubmitJetfuelAuthMethodPrefersVerificationChallenge(t *testing.T) { client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.SetNextJetfuelCastleTokens([]string{"castle-from-webview"}) 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) } + if got := req.Header.Get("x-jf-client-theme"); got != jetfuelHeaderTheme { + t.Fatalf("x-jf-client-theme = %q, want %q", got, jetfuelHeaderTheme) + } body, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("ReadAll(request body) error = %v", err) @@ -432,6 +579,9 @@ func TestSubmitJetfuelAuthMethodPrefersVerificationChallenge(t *testing.T) { if !strings.Contains(form, "two_factor_auth_method_type=Totp") { t.Fatalf("request body = %q, want Totp method", form) } + if !strings.Contains(form, "%24castle_token=castle-from-webview") { + t.Fatalf("request body = %q, want webview Castle token", 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{ @@ -469,6 +619,7 @@ func TestSubmitJetfuelAuthMethodPrefersVerificationChallenge(t *testing.T) { func TestSubmitJetfuelSMSAuthMethodReturnsPhoneChallenge(t *testing.T) { client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.SetNextJetfuelCastleTokens([]string{"sms-method-castle-token"}) 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) @@ -523,6 +674,7 @@ func TestSubmitJetfuelSMSAuthMethodReturnsPhoneChallenge(t *testing.T) { func TestSubmitJetfuelSMSAuthMethodDefaultsActionForPhoneChallenge(t *testing.T) { client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.SetNextJetfuelCastleTokens([]string{"sms-phone-challenge-castle-token"}) 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) @@ -574,6 +726,7 @@ func TestSubmitJetfuelSMSAuthMethodDefaultsActionForPhoneChallenge(t *testing.T) func TestSubmitJetfuelPhoneNumberVerificationPostsPhoneField(t *testing.T) { client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.SetNextJetfuelCastleTokens([]string{"phone-number-castle-token"}) 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) @@ -760,20 +913,6 @@ func TestJetfuelLoginResponseClassifiesBadCredentials(t *testing.T) { } } } - -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" { diff --git a/pkg/twittermeow/methods/html.go b/pkg/twittermeow/methods/html.go index 05bb89a0..29b07a38 100644 --- a/pkg/twittermeow/methods/html.go +++ b/pkg/twittermeow/methods/html.go @@ -19,6 +19,8 @@ var ( verificationTokenRegex = regexp.MustCompile(`meta name="twitter-site-verification" content="([^"]+)"`) countryCodeRegex = regexp.MustCompile(`"country":\s*"([A-Z]{2})"`) ondemandSChunkIDRegex = regexp.MustCompile(`(\d+):"ondemand\.s"`) + ondemandCastleIDRegex = regexp.MustCompile(`(\d+):"ondemand\.castle"`) + castlePublicKeyRegex = regexp.MustCompile(`"responsive_web_castle_public_key"\s*:\s*\{\s*"value"\s*:\s*"([^"]+)"`) variableIndexesRegex = regexp.MustCompile(`\[.+?\(\w{1,2}\[(\d{1,2})],16\).+?\(\w{1,2}\[(\d{1,2})],16\).+?\(\w{1,2}\[(\d{1,2})],16\).+?\(\w{1,2}\[(\d{1,2})],16\)`) ) @@ -110,7 +112,15 @@ func ParseCountry(html string) string { } func ParseOndemandSURLFromScript(js []byte) string { - chunkIDMatch := ondemandSChunkIDRegex.FindSubmatchIndex(js) + return parseOndemandChunkURLFromScript(js, "ondemand.s", ondemandSChunkIDRegex) +} + +func ParseOndemandCastleURLFromScript(js []byte) string { + return parseOndemandChunkURLFromScript(js, "ondemand.castle", ondemandCastleIDRegex) +} + +func parseOndemandChunkURLFromScript(js []byte, chunkName string, chunkIDRegex *regexp.Regexp) string { + chunkIDMatch := chunkIDRegex.FindSubmatchIndex(js) if len(chunkIDMatch) < 4 { return "" } @@ -124,5 +134,13 @@ func ParseOndemandSURLFromScript(js []byte) string { } hash := string(jsAfterNameMap[hashMatch[2]:hashMatch[3]]) - return "https://abs.twimg.com/responsive-web/client-web/ondemand.s." + hash + "a.js" + return "https://abs.twimg.com/responsive-web/client-web/" + chunkName + "." + hash + "a.js" +} + +func ParseResponsiveWebCastlePublicKey(html string) string { + match := castlePublicKeyRegex.FindStringSubmatch(html) + if len(match) < 2 { + return "" + } + return match[1] } diff --git a/pkg/twittermeow/methods/html_test.go b/pkg/twittermeow/methods/html_test.go index 02bbbcf6..65b68648 100644 --- a/pkg/twittermeow/methods/html_test.go +++ b/pkg/twittermeow/methods/html_test.go @@ -3,6 +3,7 @@ package methods import ( "io" "net/http" + "strings" "testing" "time" ) @@ -25,6 +26,23 @@ document.cookie = "guest_id=guest-value; Domain=.x.com; SameSite=None"; } } +func TestParseCastleBootstrapInfo(t *testing.T) { + html := `{"responsive_web_castle_public_key":{"value":"castle-public-key"},"lang":"en"}` + if got := ParseResponsiveWebCastlePublicKey(html); got != "castle-public-key" { + t.Fatalf("ParseResponsiveWebCastlePublicKey() = %q", got) + } + + js := []byte(`{100:"bundle.Home",15793:"ondemand.castle",16000:"other"};{100:"abc1234",15793:"1ff15ff",16000:"def5678"}`) + gotURL := ParseOndemandCastleURLFromScript(js) + wantURL := "https://abs.twimg.com/responsive-web/client-web/ondemand.castle.1ff15ffa.js" + if gotURL != wantURL { + t.Fatalf("ParseOndemandCastleURLFromScript() = %q, want %q", gotURL, wantURL) + } + if strings.Contains(gotURL, "ondemand.s") { + t.Fatalf("ParseOndemandCastleURLFromScript() = %q, want Castle chunk URL", gotURL) + } +} + func TestParseOndemandSURLFromScript(t *testing.T) { client := &http.Client{Timeout: 20 * time.Second} req, err := http.NewRequest(http.MethodGet, "https://x.com/", nil) From ec225d0cf43b2787a1c244758c4597652c7d10ce Mon Sep 17 00:00:00 2001 From: highesttt Date: Tue, 14 Jul 2026 16:58:22 -0400 Subject: [PATCH 10/20] feat: hidden webview for login --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/login.go | 1 + pkg/connector/login_test.go | 17 +++++++++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 68e70d1f..dfa1a30f 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( golang.org/x/crypto v0.53.0 golang.org/x/sync v0.21.0 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.28.1 + maunium.net/go/mautrix v0.28.2-0.20260714135907-a79bdf25be1f ) require ( diff --git a/go.sum b/go.sum index 584e6232..e453debb 100644 --- a/go.sum +++ b/go.sum @@ -182,5 +182,5 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.28.1 h1:Hic3oDMPbLbQu1fhboTRAKZcORMjzzkjxsa+SGk60b0= -maunium.net/go/mautrix v0.28.1/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY= +maunium.net/go/mautrix v0.28.2-0.20260714135907-a79bdf25be1f h1:WVy4D2xz/fFPcJp9f+XajfEBaPdBwN5p5UJ63W8760Q= +maunium.net/go/mautrix v0.28.2-0.20260714135907-a79bdf25be1f/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY= diff --git a/pkg/connector/login.go b/pkg/connector/login.go index c7993e16..7b4dec94 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -858,6 +858,7 @@ func makeCastleTokenStep(info twittermeow.JetfuelCastleTokenInfo, identifier, er ExtractJS: castleTokenExtractJS(info, identifier), WaitForURLPattern: `^https://x\.com/robots\.txt$`, Fields: castleTokenCookieFields(), + Hidden: true, }, } } diff --git a/pkg/connector/login_test.go b/pkg/connector/login_test.go index 726b71f6..e53261fe 100644 --- a/pkg/connector/login_test.go +++ b/pkg/connector/login_test.go @@ -77,6 +77,20 @@ func TestGetLoginFlowsAdvertisesNativePasswordOnly(t *testing.T) { } } +func TestCookieLoginRemainsVisible(t *testing.T) { + login := &TwitterLogin{useCookieLogin: true} + step, err := login.Start(context.Background()) + if err != nil { + t.Fatalf("Start() error = %v", err) + } + if step.CookiesParams == nil { + t.Fatal("CookiesParams = nil") + } + if step.CookiesParams.Hidden { + t.Fatal("CookiesParams.Hidden = true, want user-driven cookie login to remain visible") + } +} + func TestMakeAuthMethodStepUsesNativeSelect(t *testing.T) { methods := []twittermeow.WebLoginAuthMethod{ {ID: "Totp", Name: "Authenticator App", Supported: true}, @@ -142,6 +156,9 @@ func TestMakeCastleTokenStepUsesClientWebviewExtraction(t *testing.T) { if step.CookiesParams == nil { t.Fatal("CookiesParams = nil") } + if !step.CookiesParams.Hidden { + t.Fatal("CookiesParams.Hidden = false, want Castle token acquisition to run in a hidden webview") + } if step.CookiesParams.UserAgent != "" { t.Fatalf("UserAgent = %q, want the webview's native user agent", step.CookiesParams.UserAgent) } From 8338f2c783d9e10b97b613fdfb996f7b89a88317 Mon Sep 17 00:00:00 2001 From: highesttt Date: Thu, 16 Jul 2026 14:48:49 -0400 Subject: [PATCH 11/20] refactor: embed Castle webview script --- pkg/connector/castle_token.js | 445 +++++++++++++++++++++++++ pkg/connector/castle_token_webview.go | 44 +++ pkg/connector/login.go | 452 -------------------------- pkg/connector/login_test.go | 3 + 4 files changed, 492 insertions(+), 452 deletions(-) create mode 100644 pkg/connector/castle_token.js create mode 100644 pkg/connector/castle_token_webview.go diff --git a/pkg/connector/castle_token.js b/pkg/connector/castle_token.js new file mode 100644 index 00000000..e4d66cac --- /dev/null +++ b/pkg/connector/castle_token.js @@ -0,0 +1,445 @@ +(async ({ + scriptURL, + publicKey, + cookieNames: cookieNameList, + contextURL, + identifier, + castleTokenBatchSize, +}) => { + const cookieNames = new Set(cookieNameList); + const castleStorageKey = "fi.mau.twitter.castle_token"; + + let browserStatusText; + function showBrowserLoginStatus(message) { + try { + document.title = "Signing in to X"; + if (!browserStatusText || !browserStatusText.isConnected) { + const body = document.body || document.documentElement.appendChild(document.createElement("body")); + const container = document.createElement("main"); + container.id = "mautrix-twitter-login-status"; + container.setAttribute("role", "status"); + container.setAttribute("aria-live", "polite"); + + const title = document.createElement("h1"); + title.textContent = "Signing in to X"; + Object.assign(title.style, { + margin: "0", + color: "#eff3f4", + fontSize: "28px", + fontWeight: "700", + lineHeight: "1.2", + }); + + const progress = document.createElement("progress"); + progress.setAttribute("aria-label", "Signing in"); + Object.assign(progress.style, { + width: "220px", + height: "6px", + margin: "28px 0 22px", + accentColor: "#1d9bf0", + }); + + browserStatusText = document.createElement("p"); + Object.assign(browserStatusText.style, { + margin: "0", + color: "#8b98a5", + fontSize: "15px", + lineHeight: "1.5", + }); + + container.append(title, progress, browserStatusText); + Object.assign(container.style, { + width: "min(420px, calc(100% - 48px))", + textAlign: "center", + }); + Object.assign(document.documentElement.style, { + minHeight: "100%", + background: "#000000", + colorScheme: "dark", + }); + Object.assign(body.style, { + minHeight: "100vh", + margin: "0", + display: "grid", + placeItems: "center", + background: "#000000", + fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", + }); + body.replaceChildren(container); + } + browserStatusText.textContent = message; + } catch (_) {} + } + showBrowserLoginStatus("Preparing secure login..."); + + const browserLog = message => { + try { + console.log("[BrowserAuth] mautrix-twitter Castle: " + message); + } catch (_) {} + }; + function getLocalStorage() { + try { + return window.localStorage; + } catch (_) {} + return null; + } + function castleTokenResultKey(index) { + return index === 1 ? "castle_token" : "castle_token_" + index; + } + function castleTokenStorageKeyForIndex(index) { + return index === 1 ? castleStorageKey : castleStorageKey + "_" + index; + } + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + async function waitFor(predicate, message, timeout = 30000) { + const deadlineAt = Date.now() + timeout; + while (Date.now() < deadlineAt) { + const value = predicate(); + if (value) { + return value; + } + await sleep(100); + } + throw new Error(message); + } + function resetStoredResult() { + try { + const storage = getLocalStorage(); + if (!storage) { + return; + } + storage.removeItem(castleStorageKey); + for (let index = 2; index <= castleTokenBatchSize; index++) { + storage.removeItem(castleTokenStorageKeyForIndex(index)); + } + for (const name of cookieNames) { + storage.removeItem("fi.mau.twitter.cookie." + name); + } + } catch (_) {} + } + function storeBrowserAuthResult(result) { + try { + globalThis.__MAUTRIX_TWITTER_CASTLE_RESULT__ = result; + globalThis.__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__ = false; + globalThis.__BEEP_BEEP_AUTH_RESULTS__ = result; + if (typeof window !== "undefined") { + window.__BEEP_BEEP_AUTH_RESULTS__ = result; + } + } catch (_) {} + try { + const storage = getLocalStorage(); + if (!storage) { + return; + } + storage.setItem(castleStorageKey, result.castle_token || ""); + for (let index = 2; index <= castleTokenBatchSize; index++) { + const key = castleTokenResultKey(index); + if (result[key]) { + storage.setItem(castleTokenStorageKeyForIndex(index), result[key]); + } + } + for (const name of cookieNames) { + if (result[name]) { + storage.setItem("fi.mau.twitter.cookie." + name, result[name]); + } + } + } catch (_) {} + } + function storedBrowserAuthResult() { + try { + const result = globalThis.__MAUTRIX_TWITTER_CASTLE_RESULT__; + if (result && result.castle_token) { + return result; + } + } catch (_) {} + try { + const storage = getLocalStorage(); + if (!storage) { + return null; + } + const token = storage.getItem(castleStorageKey); + if (!token) { + return null; + } + const result = { castle_token: token }; + for (let index = 2; index <= castleTokenBatchSize; index++) { + const token = storage.getItem(castleTokenStorageKeyForIndex(index)); + if (token) { + result[castleTokenResultKey(index)] = token; + } + } + for (const name of cookieNames) { + const value = storage.getItem("fi.mau.twitter.cookie." + name); + if (value) { + result[name] = value; + } + } + return result; + } catch (_) { + return null; + } + } + const existingResult = storedBrowserAuthResult(); + if (existingResult) { + browserLog("returning stored result"); + return existingResult; + } + if (globalThis.__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__) { + browserLog("waiting for in-flight result"); + return await waitFor(() => storedBrowserAuthResult(), "X Castle token generation did not finish", 30000); + } + globalThis.__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__ = true; + resetStoredResult(); + + function addModules(entry, modules) { + if (!entry || typeof entry !== "object") { + return; + } + const defs = entry[1]; + if (!defs || typeof defs !== "object") { + return; + } + for (const id of Object.keys(defs)) { + modules[id] = defs[id]; + } + } + + function loadScript(doc, url, timeout = 10000) { + return new Promise((resolve, reject) => { + const script = doc.createElement("script"); + const timer = setTimeout(() => { + try { + script.remove(); + } catch (_) {} + reject(new Error("Timed out loading X Castle script")); + }, timeout); + script.src = url; + script.async = true; + script.onload = () => { + clearTimeout(timer); + resolve(); + }; + script.onerror = () => { + clearTimeout(timer); + reject(new Error("Failed to load X Castle script")); + }; + (doc.head || doc.documentElement).appendChild(script); + }); + } + + function installModuleCapture(win) { + const chunk = win.webpackChunk_twitter_responsive_web = win.webpackChunk_twitter_responsive_web || []; + const modules = {}; + for (const entry of chunk) { + addModules(entry, modules); + } + if (!chunk.__mautrixCastleCaptured) { + const nativePush = chunk.push.bind(chunk); + chunk.push = (...entries) => { + for (const entry of entries) { + addModules(entry, modules); + } + return nativePush(...entries); + }; + Object.defineProperty(chunk, "__mautrixCastleCaptured", { value: true }); + } + return modules; + } + + async function createTokenInContext(win, doc) { + const modules = installModuleCapture(win); + await loadScript(doc, scriptURL); + await waitFor(() => Object.keys(modules).length > 0, "X Castle script did not register modules", 15000); + + const cache = {}; + function req(id) { + const key = String(id); + if (cache[key]) { + return cache[key].exports; + } + const fn = modules[key]; + if (typeof fn !== "function") { + throw new Error("Missing X Castle module " + key); + } + const module = { exports: {} }; + cache[key] = module; + fn(module, module.exports, req); + return module.exports; + } + req.d = (exports, definition) => { + for (const key of Object.keys(definition)) { + if (!Object.prototype.hasOwnProperty.call(exports, key)) { + Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); + } + } + }; + req.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); + req.r = exports => { + if (typeof win.Symbol !== "undefined" && win.Symbol.toStringTag) { + Object.defineProperty(exports, win.Symbol.toStringTag, { value: "Module" }); + } + Object.defineProperty(exports, "__esModule", { value: true }); + }; + + let configure; + for (const id of Object.keys(modules)) { + try { + const exports = req(id); + if (exports && typeof exports.configure === "function") { + configure = exports.configure; + break; + } + } catch (_) {} + } + if (typeof configure !== "function") { + throw new Error("X Castle module is unavailable"); + } + + const castle = configure({ pk: publicKey }); + if (!castle || typeof castle.createRequestToken !== "function") { + throw new Error("X Castle token generator is unavailable"); + } + const tokens = []; + for (let index = 0; index < castleTokenBatchSize; index++) { + const token = await castle.createRequestToken(); + if (token) { + tokens.push(token); + } + await sleep(10); + } + return tokens; + } + + async function synthesizeCastleActivity(win, doc) { + const target = doc.body || doc.documentElement; + if (!target) { + return; + } + const input = doc.createElement("input"); + input.type = "text"; + input.autocomplete = "username"; + input.style.cssText = "position:fixed;left:24px;top:24px;width:240px;height:32px;opacity:0;pointer-events:none;"; + target.appendChild(input); + input.focus(); + const points = [[52, 48], [96, 52], [148, 60], [212, 68]]; + for (const [x, y] of points) { + const eventInit = { bubbles: true, cancelable: true, clientX: x, clientY: y, screenX: x + 16, screenY: y + 88, pointerType: "mouse", isPrimary: true, buttons: 0 }; + if (typeof win.PointerEvent === "function") { + target.dispatchEvent(new win.PointerEvent("pointermove", eventInit)); + } else { + target.dispatchEvent(new win.MouseEvent("mousemove", eventInit)); + } + await sleep(20); + } + const downInit = { bubbles: true, cancelable: true, clientX: 96, clientY: 52, screenX: 112, screenY: 140, pointerType: "mouse", isPrimary: true, buttons: 1 }; + if (typeof win.PointerEvent === "function") { + input.dispatchEvent(new win.PointerEvent("pointerdown", downInit)); + input.dispatchEvent(new win.PointerEvent("pointerup", { ...downInit, buttons: 0 })); + } + input.dispatchEvent(new win.MouseEvent("mousedown", downInit)); + input.dispatchEvent(new win.MouseEvent("mouseup", { ...downInit, buttons: 0 })); + input.dispatchEvent(new win.MouseEvent("click", { ...downInit, buttons: 0 })); + const chars = identifier || "x"; + const valueSetter = Object.getOwnPropertyDescriptor(win.HTMLInputElement.prototype, "value").set; + for (const ch of chars.slice(0, 16)) { + input.dispatchEvent(new win.KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: ch })); + valueSetter.call(input, input.value + ch); + input.dispatchEvent(new win.InputEvent("input", { bubbles: true, inputType: "insertText", data: ch })); + input.dispatchEvent(new win.KeyboardEvent("keyup", { bubbles: true, cancelable: true, key: ch })); + await sleep(15); + } + input.dispatchEvent(new win.Event("change", { bubbles: true })); + await sleep(100); + } + + async function loadFetchedContextFrame() { + const resp = await fetch(contextURL, { credentials: "include" }); + if (!resp.ok) { + throw new Error("Failed to fetch X login context: HTTP " + resp.status); + } + const html = await resp.text(); + const iframe = document.createElement("iframe"); + iframe.tabIndex = -1; + iframe.style.cssText = "position:fixed;left:-10000px;top:-10000px;width:1024px;height:768px;border:0;opacity:0;pointer-events:none;"; + document.documentElement.appendChild(iframe); + const win = iframe.contentWindow; + const doc = iframe.contentDocument; + if (!win || !doc) { + throw new Error("Synthetic X login context is not accessible"); + } + doc.open(); + doc.write(html); + doc.close(); + try { + win.history.replaceState(null, "", contextURL); + } catch (_) {} + await waitFor(() => doc.readyState === "interactive" || doc.readyState === "complete", "Synthetic X login context did not become ready", 15000); + return { win, doc }; + } + + function copyCookies(result, cookieText) { + for (const part of cookieText.split(";")) { + const idx = part.indexOf("="); + if (idx <= 0) { + continue; + } + const name = part.slice(0, idx).trim(); + if (cookieNames.has(name)) { + result[name] = part.slice(idx + 1).trim(); + } + } + } + + function quoteClientHint(value) { + return '"' + String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + } + + function captureBrowserHeaders() { + const headers = { + browser_user_agent: String(navigator.userAgent || ""), + }; + const userAgentData = navigator.userAgentData; + if (!userAgentData) { + return headers; + } + const brands = Array.from(userAgentData.brands || []); + if (brands.length > 0) { + headers.browser_sec_ch_ua = brands.map(item => + quoteClientHint(item.brand) + ";v=" + quoteClientHint(item.version) + ).join(", "); + } + if (userAgentData.platform) { + headers.browser_sec_ch_ua_platform = quoteClientHint(userAgentData.platform); + } + headers.browser_sec_ch_ua_mobile = userAgentData.mobile ? "?1" : "?0"; + return headers; + } + + try { + browserLog("loading X context"); + const context = await loadFetchedContextFrame(); + browserLog("context ready"); + browserLog("creating module token"); + showBrowserLoginStatus("Completing sign-in..."); + await synthesizeCastleActivity(context.win, context.doc); + const castleTokens = await createTokenInContext(context.win, context.doc); + const castleToken = castleTokens[0] || ""; + browserLog("module tokens ready, count " + castleTokens.length + ", first length " + String(castleToken || "").length); + const result = { castle_token: castleToken }; + Object.assign(result, captureBrowserHeaders()); + for (let index = 1; index < castleTokens.length; index++) { + result[castleTokenResultKey(index + 1)] = castleTokens[index]; + } + copyCookies(result, document.cookie); + copyCookies(result, context.doc.cookie); + storeBrowserAuthResult(result); + browserLog("result stored"); + showBrowserLoginStatus("Finishing..."); + return result; + } catch (err) { + showBrowserLoginStatus("Unable to finish signing in."); + try { + globalThis.__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__ = false; + } catch (_) {} + throw err; + } +})(__MAUTRIX_TWITTER_CASTLE_CONFIG__) diff --git a/pkg/connector/castle_token_webview.go b/pkg/connector/castle_token_webview.go new file mode 100644 index 00000000..29e40af3 --- /dev/null +++ b/pkg/connector/castle_token_webview.go @@ -0,0 +1,44 @@ +package connector + +import ( + _ "embed" + "encoding/json" + "fmt" + "strings" + + "go.mau.fi/mautrix-twitter/pkg/twittermeow" +) + +const castleTokenJSConfigPlaceholder = "__MAUTRIX_TWITTER_CASTLE_CONFIG__" + +//go:embed castle_token.js +var castleTokenExtractJSSource string + +type castleTokenJSConfig struct { + ScriptURL string `json:"scriptURL"` + PublicKey string `json:"publicKey"` + CookieNames []string `json:"cookieNames"` + ContextURL string `json:"contextURL"` + Identifier string `json:"identifier"` + BatchSize int `json:"castleTokenBatchSize"` +} + +func castleTokenExtractJS(info twittermeow.JetfuelCastleTokenInfo, identifier string) string { + config, err := json.Marshal(castleTokenJSConfig{ + ScriptURL: info.ScriptURL, + PublicKey: info.PublicKey, + CookieNames: castleTokenCookieNames, + ContextURL: castleTokenContextURL, + Identifier: identifier, + BatchSize: castleTokenBatchSize, + }) + if err != nil { + panic(fmt.Errorf("marshal Castle token extraction config: %w", err)) + } + + script := strings.TrimRight(castleTokenExtractJSSource, "\r\n") + if strings.Count(script, castleTokenJSConfigPlaceholder) != 1 { + panic("Castle token extraction script must contain exactly one config placeholder") + } + return strings.Replace(script, castleTokenJSConfigPlaceholder, string(config), 1) +} diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 7b4dec94..f79d2551 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -19,7 +19,6 @@ package connector import ( "context" "encoding/base64" - "encoding/json" "errors" "fmt" "net/http" @@ -140,457 +139,6 @@ var browserHeaderFields = []browserHeaderField{ }, } -func castleTokenExtractJS(info twittermeow.JetfuelCastleTokenInfo, identifier string) string { - scriptURL, _ := json.Marshal(info.ScriptURL) - publicKey, _ := json.Marshal(info.PublicKey) - cookieNames, _ := json.Marshal(castleTokenCookieNames) - contextURL, _ := json.Marshal(castleTokenContextURL) - identifierJSON, _ := json.Marshal(identifier) - return fmt.Sprintf(`(async () => { - const scriptURL = %s; - const publicKey = %s; - const cookieNames = new Set(%s); - const contextURL = %s; - const identifier = %s; - const castleStorageKey = "fi.mau.twitter.castle_token"; - const castleTokenBatchSize = %d; - - let browserStatusText; - function showBrowserLoginStatus(message) { - try { - document.title = "Signing in to X"; - if (!browserStatusText || !browserStatusText.isConnected) { - const body = document.body || document.documentElement.appendChild(document.createElement("body")); - const container = document.createElement("main"); - container.id = "mautrix-twitter-login-status"; - container.setAttribute("role", "status"); - container.setAttribute("aria-live", "polite"); - - const title = document.createElement("h1"); - title.textContent = "Signing in to X"; - Object.assign(title.style, { - margin: "0", - color: "#eff3f4", - fontSize: "28px", - fontWeight: "700", - lineHeight: "1.2", - }); - - const progress = document.createElement("progress"); - progress.setAttribute("aria-label", "Signing in"); - Object.assign(progress.style, { - width: "220px", - height: "6px", - margin: "28px 0 22px", - accentColor: "#1d9bf0", - }); - - browserStatusText = document.createElement("p"); - Object.assign(browserStatusText.style, { - margin: "0", - color: "#8b98a5", - fontSize: "15px", - lineHeight: "1.5", - }); - - container.append(title, progress, browserStatusText); - Object.assign(container.style, { - width: "min(420px, calc(100%% - 48px))", - textAlign: "center", - }); - Object.assign(document.documentElement.style, { - minHeight: "100%%", - background: "#000000", - colorScheme: "dark", - }); - Object.assign(body.style, { - minHeight: "100vh", - margin: "0", - display: "grid", - placeItems: "center", - background: "#000000", - fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", - }); - body.replaceChildren(container); - } - browserStatusText.textContent = message; - } catch (_) {} - } - showBrowserLoginStatus("Preparing secure login..."); - - const browserLog = message => { - try { - console.log("[BrowserAuth] mautrix-twitter Castle: " + message); - } catch (_) {} - }; - function getLocalStorage() { - try { - return window.localStorage; - } catch (_) {} - return null; - } - function castleTokenResultKey(index) { - return index === 1 ? "castle_token" : "castle_token_" + index; - } - function castleTokenStorageKeyForIndex(index) { - return index === 1 ? castleStorageKey : castleStorageKey + "_" + index; - } - const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); - async function waitFor(predicate, message, timeout = 30000) { - const deadlineAt = Date.now() + timeout; - while (Date.now() < deadlineAt) { - const value = predicate(); - if (value) { - return value; - } - await sleep(100); - } - throw new Error(message); - } - function resetStoredResult() { - try { - const storage = getLocalStorage(); - if (!storage) { - return; - } - storage.removeItem(castleStorageKey); - for (let index = 2; index <= castleTokenBatchSize; index++) { - storage.removeItem(castleTokenStorageKeyForIndex(index)); - } - for (const name of cookieNames) { - storage.removeItem("fi.mau.twitter.cookie." + name); - } - } catch (_) {} - } - function storeBrowserAuthResult(result) { - try { - globalThis.__MAUTRIX_TWITTER_CASTLE_RESULT__ = result; - globalThis.__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__ = false; - globalThis.__BEEP_BEEP_AUTH_RESULTS__ = result; - if (typeof window !== "undefined") { - window.__BEEP_BEEP_AUTH_RESULTS__ = result; - } - } catch (_) {} - try { - const storage = getLocalStorage(); - if (!storage) { - return; - } - storage.setItem(castleStorageKey, result.castle_token || ""); - for (let index = 2; index <= castleTokenBatchSize; index++) { - const key = castleTokenResultKey(index); - if (result[key]) { - storage.setItem(castleTokenStorageKeyForIndex(index), result[key]); - } - } - for (const name of cookieNames) { - if (result[name]) { - storage.setItem("fi.mau.twitter.cookie." + name, result[name]); - } - } - } catch (_) {} - } - function storedBrowserAuthResult() { - try { - const result = globalThis.__MAUTRIX_TWITTER_CASTLE_RESULT__; - if (result && result.castle_token) { - return result; - } - } catch (_) {} - try { - const storage = getLocalStorage(); - if (!storage) { - return null; - } - const token = storage.getItem(castleStorageKey); - if (!token) { - return null; - } - const result = { castle_token: token }; - for (let index = 2; index <= castleTokenBatchSize; index++) { - const token = storage.getItem(castleTokenStorageKeyForIndex(index)); - if (token) { - result[castleTokenResultKey(index)] = token; - } - } - for (const name of cookieNames) { - const value = storage.getItem("fi.mau.twitter.cookie." + name); - if (value) { - result[name] = value; - } - } - return result; - } catch (_) { - return null; - } - } - const existingResult = storedBrowserAuthResult(); - if (existingResult) { - browserLog("returning stored result"); - return existingResult; - } - if (globalThis.__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__) { - browserLog("waiting for in-flight result"); - return await waitFor(() => storedBrowserAuthResult(), "X Castle token generation did not finish", 30000); - } - globalThis.__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__ = true; - resetStoredResult(); - - function addModules(entry, modules) { - if (!entry || typeof entry !== "object") { - return; - } - const defs = entry[1]; - if (!defs || typeof defs !== "object") { - return; - } - for (const id of Object.keys(defs)) { - modules[id] = defs[id]; - } - } - - function loadScript(doc, url, timeout = 10000) { - return new Promise((resolve, reject) => { - const script = doc.createElement("script"); - const timer = setTimeout(() => { - try { - script.remove(); - } catch (_) {} - reject(new Error("Timed out loading X Castle script")); - }, timeout); - script.src = url; - script.async = true; - script.onload = () => { - clearTimeout(timer); - resolve(); - }; - script.onerror = () => { - clearTimeout(timer); - reject(new Error("Failed to load X Castle script")); - }; - (doc.head || doc.documentElement).appendChild(script); - }); - } - - function installModuleCapture(win) { - const chunk = win.webpackChunk_twitter_responsive_web = win.webpackChunk_twitter_responsive_web || []; - const modules = {}; - for (const entry of chunk) { - addModules(entry, modules); - } - if (!chunk.__mautrixCastleCaptured) { - const nativePush = chunk.push.bind(chunk); - chunk.push = (...entries) => { - for (const entry of entries) { - addModules(entry, modules); - } - return nativePush(...entries); - }; - Object.defineProperty(chunk, "__mautrixCastleCaptured", { value: true }); - } - return modules; - } - - async function createTokenInContext(win, doc) { - const modules = installModuleCapture(win); - await loadScript(doc, scriptURL); - await waitFor(() => Object.keys(modules).length > 0, "X Castle script did not register modules", 15000); - - const cache = {}; - function req(id) { - const key = String(id); - if (cache[key]) { - return cache[key].exports; - } - const fn = modules[key]; - if (typeof fn !== "function") { - throw new Error("Missing X Castle module " + key); - } - const module = { exports: {} }; - cache[key] = module; - fn(module, module.exports, req); - return module.exports; - } - req.d = (exports, definition) => { - for (const key of Object.keys(definition)) { - if (!Object.prototype.hasOwnProperty.call(exports, key)) { - Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); - } - } - }; - req.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); - req.r = exports => { - if (typeof win.Symbol !== "undefined" && win.Symbol.toStringTag) { - Object.defineProperty(exports, win.Symbol.toStringTag, { value: "Module" }); - } - Object.defineProperty(exports, "__esModule", { value: true }); - }; - - let configure; - for (const id of Object.keys(modules)) { - try { - const exports = req(id); - if (exports && typeof exports.configure === "function") { - configure = exports.configure; - break; - } - } catch (_) {} - } - if (typeof configure !== "function") { - throw new Error("X Castle module is unavailable"); - } - - const castle = configure({ pk: publicKey }); - if (!castle || typeof castle.createRequestToken !== "function") { - throw new Error("X Castle token generator is unavailable"); - } - const tokens = []; - for (let index = 0; index < castleTokenBatchSize; index++) { - const token = await castle.createRequestToken(); - if (token) { - tokens.push(token); - } - await sleep(10); - } - return tokens; - } - - async function synthesizeCastleActivity(win, doc) { - const target = doc.body || doc.documentElement; - if (!target) { - return; - } - const input = doc.createElement("input"); - input.type = "text"; - input.autocomplete = "username"; - input.style.cssText = "position:fixed;left:24px;top:24px;width:240px;height:32px;opacity:0;pointer-events:none;"; - target.appendChild(input); - input.focus(); - const points = [[52, 48], [96, 52], [148, 60], [212, 68]]; - for (const [x, y] of points) { - const eventInit = { bubbles: true, cancelable: true, clientX: x, clientY: y, screenX: x + 16, screenY: y + 88, pointerType: "mouse", isPrimary: true, buttons: 0 }; - if (typeof win.PointerEvent === "function") { - target.dispatchEvent(new win.PointerEvent("pointermove", eventInit)); - } else { - target.dispatchEvent(new win.MouseEvent("mousemove", eventInit)); - } - await sleep(20); - } - const downInit = { bubbles: true, cancelable: true, clientX: 96, clientY: 52, screenX: 112, screenY: 140, pointerType: "mouse", isPrimary: true, buttons: 1 }; - if (typeof win.PointerEvent === "function") { - input.dispatchEvent(new win.PointerEvent("pointerdown", downInit)); - input.dispatchEvent(new win.PointerEvent("pointerup", { ...downInit, buttons: 0 })); - } - input.dispatchEvent(new win.MouseEvent("mousedown", downInit)); - input.dispatchEvent(new win.MouseEvent("mouseup", { ...downInit, buttons: 0 })); - input.dispatchEvent(new win.MouseEvent("click", { ...downInit, buttons: 0 })); - const chars = identifier || "x"; - const valueSetter = Object.getOwnPropertyDescriptor(win.HTMLInputElement.prototype, "value").set; - for (const ch of chars.slice(0, 16)) { - input.dispatchEvent(new win.KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: ch })); - valueSetter.call(input, input.value + ch); - input.dispatchEvent(new win.InputEvent("input", { bubbles: true, inputType: "insertText", data: ch })); - input.dispatchEvent(new win.KeyboardEvent("keyup", { bubbles: true, cancelable: true, key: ch })); - await sleep(15); - } - input.dispatchEvent(new win.Event("change", { bubbles: true })); - await sleep(100); - } - - async function loadFetchedContextFrame() { - const resp = await fetch(contextURL, { credentials: "include" }); - if (!resp.ok) { - throw new Error("Failed to fetch X login context: HTTP " + resp.status); - } - const html = await resp.text(); - const iframe = document.createElement("iframe"); - iframe.tabIndex = -1; - iframe.style.cssText = "position:fixed;left:-10000px;top:-10000px;width:1024px;height:768px;border:0;opacity:0;pointer-events:none;"; - document.documentElement.appendChild(iframe); - const win = iframe.contentWindow; - const doc = iframe.contentDocument; - if (!win || !doc) { - throw new Error("Synthetic X login context is not accessible"); - } - doc.open(); - doc.write(html); - doc.close(); - try { - win.history.replaceState(null, "", contextURL); - } catch (_) {} - await waitFor(() => doc.readyState === "interactive" || doc.readyState === "complete", "Synthetic X login context did not become ready", 15000); - return { win, doc }; - } - - function copyCookies(result, cookieText) { - for (const part of cookieText.split(";")) { - const idx = part.indexOf("="); - if (idx <= 0) { - continue; - } - const name = part.slice(0, idx).trim(); - if (cookieNames.has(name)) { - result[name] = part.slice(idx + 1).trim(); - } - } - } - - function quoteClientHint(value) { - return '"' + String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; - } - - function captureBrowserHeaders() { - const headers = { - browser_user_agent: String(navigator.userAgent || ""), - }; - const userAgentData = navigator.userAgentData; - if (!userAgentData) { - return headers; - } - const brands = Array.from(userAgentData.brands || []); - if (brands.length > 0) { - headers.browser_sec_ch_ua = brands.map(item => - quoteClientHint(item.brand) + ";v=" + quoteClientHint(item.version) - ).join(", "); - } - if (userAgentData.platform) { - headers.browser_sec_ch_ua_platform = quoteClientHint(userAgentData.platform); - } - headers.browser_sec_ch_ua_mobile = userAgentData.mobile ? "?1" : "?0"; - return headers; - } - - try { - browserLog("loading X context"); - const context = await loadFetchedContextFrame(); - browserLog("context ready"); - browserLog("creating module token"); - showBrowserLoginStatus("Completing sign-in..."); - await synthesizeCastleActivity(context.win, context.doc); - const castleTokens = await createTokenInContext(context.win, context.doc); - const castleToken = castleTokens[0] || ""; - browserLog("module tokens ready, count " + castleTokens.length + ", first length " + String(castleToken || "").length); - const result = { castle_token: castleToken }; - Object.assign(result, captureBrowserHeaders()); - for (let index = 1; index < castleTokens.length; index++) { - result[castleTokenResultKey(index + 1)] = castleTokens[index]; - } - copyCookies(result, document.cookie); - copyCookies(result, context.doc.cookie); - storeBrowserAuthResult(result); - browserLog("result stored"); - showBrowserLoginStatus("Finishing..."); - return result; - } catch (err) { - showBrowserLoginStatus("Unable to finish signing in."); - try { - globalThis.__MAUTRIX_TWITTER_CASTLE_IN_PROGRESS__ = false; - } catch (_) {} - throw err; - } -})()`, scriptURL, publicKey, cookieNames, contextURL, identifierJSON, castleTokenBatchSize) -} - func decodeCastleTokenInput(value string) (string, error) { value = strings.TrimSpace(value) if value == "" { diff --git a/pkg/connector/login_test.go b/pkg/connector/login_test.go index e53261fe..43e75a8d 100644 --- a/pkg/connector/login_test.go +++ b/pkg/connector/login_test.go @@ -175,6 +175,9 @@ func TestMakeCastleTokenStepUsesClientWebviewExtraction(t *testing.T) { !strings.Contains(step.CookiesParams.ExtractJS, "createRequestToken") { t.Fatalf("ExtractJS does not load X Castle token generator") } + if strings.Contains(step.CookiesParams.ExtractJS, castleTokenJSConfigPlaceholder) { + t.Fatal("ExtractJS still contains the embedded script config placeholder") + } if !strings.Contains(step.CookiesParams.ExtractJS, castleTokenContextURL) { t.Fatalf("ExtractJS does not include the X login context") } From 07f353e3444646961ad803abdf3b5e170b312187 Mon Sep 17 00:00:00 2001 From: highesttt Date: Mon, 20 Jul 2026 17:16:29 -0400 Subject: [PATCH 12/20] fix: jetfuel issues --- pkg/connector/login.go | 3 + pkg/connector/login_test.go | 120 +++++++++++++ pkg/twittermeow/login_jetfuel.go | 38 +++- pkg/twittermeow/login_web.go | 11 +- pkg/twittermeow/login_web_test.go | 278 ++++++++++++++++++++++++++++++ 5 files changed, 444 insertions(+), 6 deletions(-) diff --git a/pkg/connector/login.go b/pkg/connector/login.go index f79d2551..ca164778 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -999,6 +999,9 @@ func (t *TwitterLogin) continueWebCastleLogin(ctx context.Context) (*bridgev2.Lo } if result != nil && result.Status == twittermeow.WebLoginStatusNeedsPassword && t.webLoginPassword != "" { t.webLoginCastleStage = webLoginCastleStagePassword + if !t.webLogin.Client().HasNextJetfuelCastleToken() { + return t.makeWebLoginCastleTokenStep("X needs a fresh browser token to continue this login."), nil + } continue } t.webLoginCastleStage = "" diff --git a/pkg/connector/login_test.go b/pkg/connector/login_test.go index 43e75a8d..ff1777c8 100644 --- a/pkg/connector/login_test.go +++ b/pkg/connector/login_test.go @@ -6,16 +6,35 @@ import ( "encoding/json" "errors" "fmt" + "io" + "net/http" "regexp" "strconv" "strings" "testing" + "github.com/rs/zerolog" "maunium.net/go/mautrix/bridgev2" "go.mau.fi/mautrix-twitter/pkg/twittermeow" + twitCookies "go.mau.fi/mautrix-twitter/pkg/twittermeow/cookies" + "go.mau.fi/mautrix-twitter/pkg/twittermeow/data/endpoints" ) +type connectorRoundTripFunc func(*http.Request) (*http.Response, error) + +func (rtf connectorRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return rtf(req) +} + +func connectorTestHTTPResponse(body string) *http.Response { + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(body)), + } +} + func TestSubmitUserInputRejectsMissingRequiredCredentialFields(t *testing.T) { login := &TwitterLogin{} tests := []map[string]string{ @@ -77,6 +96,107 @@ func TestGetLoginFlowsAdvertisesNativePasswordOnly(t *testing.T) { } } +func TestContinueWebCastleLoginRequestsFreshTokenForPasswordReplay(t *testing.T) { + t.Setenv("TWITTER_JETFUEL_VIEWER_CONTEXT", "0") + const mainPageHTML = `` + + client := twittermeow.NewClient(twitCookies.NewCookies(nil), nil, zerolog.Nop()) + passwordRequestCount := 0 + client.HTTP = &http.Client{Transport: connectorRoundTripFunc(func(req *http.Request) (*http.Response, error) { + switch { + case req.Method == http.MethodGet && req.URL.Path == "/i/jf/onboarding/web": + resp := connectorTestHTTPResponse(mainPageHTML) + resp.Header.Add("Set-Cookie", "guest_id=v1%3A123456789; Path=/; Secure") + return resp, nil + case req.Method == http.MethodGet && req.URL.Path == "/i/jfapi"+endpoints.JETFUEL_LANDING_PATH: + return connectorTestHTTPResponse("landing"), nil + case req.Method == http.MethodGet && req.URL.Path == "/i/jfapi/onboarding/web" && req.URL.Query().Get("mode") == "login": + return connectorTestHTTPResponse(endpoints.JETFUEL_BEGIN_LOGIN_PATH + "\x00username_or_email"), nil + case req.Method == http.MethodPost && req.URL.Path == "/i/jfapi"+endpoints.JETFUEL_BEGIN_LOGIN_PATH: + body, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("ReadAll(identifier request) error = %v", err) + } + if !strings.Contains(string(body), "%24castle_token=identifier-token") { + t.Fatalf("identifier request body = %q", body) + } + return connectorTestHTTPResponse(endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH + "\x00password"), nil + case req.Method == http.MethodPost && req.URL.Path == "/i/jfapi"+endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH: + passwordRequestCount++ + body, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("ReadAll(password request) error = %v", err) + } + wantToken := "first-password-token" + if passwordRequestCount == 2 { + wantToken = "replay-password-token" + } + if !strings.Contains(string(body), "%24castle_token="+wantToken) { + t.Fatalf("password request %d body = %q, want token %q", passwordRequestCount, body, wantToken) + } + if passwordRequestCount == 1 { + return connectorTestHTTPResponse(endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH + "\x00password"), nil + } + return connectorTestHTTPResponse(endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH + "\x00challenge_response\x00Enter your verification code"), nil + default: + t.Fatalf("unexpected request: %s %s", req.Method, req.URL.String()) + return nil, nil + } + })} + session := twittermeow.NewWebLoginSession(client) + result, err := session.Start(context.Background()) + if err != nil { + t.Fatalf("Start() error = %v", err) + } + if !session.UsesJetfuel() || result == nil || result.Status != twittermeow.WebLoginStatusNeedsIdentifier { + t.Fatalf("Start() result = %#v, UsesJetfuel = %t", result, session.UsesJetfuel()) + } + + client.SetNextJetfuelCastleTokens([]string{"identifier-token"}) + result, err = session.SubmitIdentifier(context.Background(), "test-user") + if err != nil || result == nil || result.Status != twittermeow.WebLoginStatusNeedsPassword { + t.Fatalf("SubmitIdentifier() result = %#v, error = %v", result, err) + } + + client.SetNextJetfuelCastleTokens([]string{"first-password-token"}) + login := &TwitterLogin{ + User: &bridgev2.User{Log: zerolog.Nop()}, + webLogin: session, + webLoginIdentifier: "test-user", + webLoginPassword: "test-password", + webLoginCastleStage: webLoginCastleStagePassword, + } + step, err := login.continueWebCastleLogin(context.Background()) + if err != nil { + t.Fatalf("first continueWebCastleLogin() error = %v", err) + } + if step == nil || step.StepID != LoginStepIDCastleToken { + t.Fatalf("first continueWebCastleLogin() step = %#v, want Castle token step", step) + } + if login.webLoginCastleStage != webLoginCastleStagePassword { + t.Fatalf("Castle stage = %q, want password", login.webLoginCastleStage) + } + if passwordRequestCount != 1 { + t.Fatalf("password request count = %d, want 1", passwordRequestCount) + } + + client.SetNextJetfuelCastleTokens([]string{"replay-password-token"}) + step, err = login.continueWebCastleLogin(context.Background()) + if err != nil { + t.Fatalf("second continueWebCastleLogin() error = %v", err) + } + if step == nil || step.StepID != LoginStepIDVerification { + t.Fatalf("second continueWebCastleLogin() step = %#v, want verification step", step) + } + if passwordRequestCount != 2 { + t.Fatalf("password request count = %d, want 2", passwordRequestCount) + } +} + func TestCookieLoginRemainsVisible(t *testing.T) { login := &TwitterLogin{useCookieLogin: true} step, err := login.Start(context.Background()) diff --git a/pkg/twittermeow/login_jetfuel.go b/pkg/twittermeow/login_jetfuel.go index c01225d7..6935cbae 100644 --- a/pkg/twittermeow/login_jetfuel.go +++ b/pkg/twittermeow/login_jetfuel.go @@ -55,6 +55,7 @@ type jetfuelLoginState struct { sessionToken string preludeDispatchID string userID string + passwordReplayUsed bool } type jetfuelLoginResponse struct { @@ -136,7 +137,8 @@ func (wls *WebLoginSession) submitJetfuelIdentifier(ctx context.Context, identif Challenge: parsed.verificationChallenge(), }, nil } - return nil, fmt.Errorf("%w: jetfuel identifier response did not expose a supported next action", ErrWebLoginUnexpectedSubtask) + wls.logUnsupportedJetfuelResponse("identifier", parsed) + return nil, ErrJetfuelIdentifierNoSupportedAction } func (wls *WebLoginSession) submitJetfuelCredentials(ctx context.Context, identifier, password string) (*WebLoginResult, error) { @@ -162,6 +164,9 @@ func (wls *WebLoginSession) submitJetfuelCredentials(ctx context.Context, identi } func isJetfuelPrePasswordParityError(err error) bool { + if errors.Is(err, ErrJetfuelIdentifierNoSupportedAction) { + return true + } var webErr *WebLoginError if !errors.As(err, &webErr) { return false @@ -217,6 +222,7 @@ func (wls *WebLoginSession) submitJetfuelCombinedCredentials(ctx context.Context }, }, nil } + wls.logUnsupportedJetfuelResponse("combined_credentials", parsed) return nil, fmt.Errorf("%w: jetfuel credentials response did not complete or expose a supported challenge", ErrWebLoginUnexpectedSubtask) } @@ -263,9 +269,39 @@ func (wls *WebLoginSession) submitJetfuelPassword(ctx context.Context, password Challenge: parsed.verificationChallenge(), }, nil } + if action := parsed.passwordAction(); action != "" { + wls.jetfuel.passwordAction = action + if !wls.jetfuel.passwordReplayUsed { + wls.jetfuel.passwordReplayUsed = true + return &WebLoginResult{ + Status: WebLoginStatusNeedsPassword, + CurrentSubtaskID: "JetfuelPassword", + Challenge: &WebLoginChallenge{ + SubtaskID: "JetfuelPassword", + Hint: "Password", + }, + }, nil + } + wls.logUnsupportedJetfuelResponse("password_replay_limit", parsed) + return nil, fmt.Errorf("%w: jetfuel password action repeated after the allowed replay", ErrWebLoginUnexpectedSubtask) + } + wls.logUnsupportedJetfuelResponse("password", parsed) return nil, fmt.Errorf("%w: jetfuel password response did not complete or expose a supported challenge", ErrWebLoginUnexpectedSubtask) } +func (wls *WebLoginSession) logUnsupportedJetfuelResponse(stage string, parsed jetfuelLoginResponse) { + if wls == nil || wls.client == nil { + return + } + wls.client.Logger.Debug(). + Str("stage", stage). + Int("response_bytes", len(parsed.raw)). + Int("string_count", len(parsed.strings)). + Int("path_count", len(parsed.paths)). + Int("field_count", len(parsed.fields)). + Msg("Jetfuel response did not expose a supported next action") +} + 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) diff --git a/pkg/twittermeow/login_web.go b/pkg/twittermeow/login_web.go index 7dacbe89..bc65520c 100644 --- a/pkg/twittermeow/login_web.go +++ b/pkg/twittermeow/login_web.go @@ -23,11 +23,12 @@ const ( ) 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") + ErrWebLoginUnexpectedSubtask = errors.New("unexpected X login subtask") + ErrJetfuelIdentifierNoSupportedAction = fmt.Errorf("%w: jetfuel identifier response did not expose a supported next action", ErrWebLoginUnexpectedSubtask) + 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 diff --git a/pkg/twittermeow/login_web_test.go b/pkg/twittermeow/login_web_test.go index f31bd304..e72aa791 100644 --- a/pkg/twittermeow/login_web_test.go +++ b/pkg/twittermeow/login_web_test.go @@ -1,6 +1,7 @@ package twittermeow import ( + "bytes" "context" "encoding/json" "errors" @@ -22,6 +23,27 @@ func (rtf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return rtf(req) } +func readJetfuelTestForm(t *testing.T, req *http.Request) url.Values { + t.Helper() + 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) + } + return form +} + +func jetfuelTestResponse(body string) *http.Response { + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(body)), + } +} + func TestSettingsListIdentifierPayloadShape(t *testing.T) { payload := onboardingTaskRequest{ FlowToken: "flow-token", @@ -486,6 +508,262 @@ func TestSetCookiesPreservesExistingLoginCookies(t *testing.T) { } } +func TestJetfuelIdentifierNoSupportedActionClassification(t *testing.T) { + if !errors.Is(ErrJetfuelIdentifierNoSupportedAction, ErrWebLoginUnexpectedSubtask) { + t.Fatalf("ErrJetfuelIdentifierNoSupportedAction must wrap ErrWebLoginUnexpectedSubtask") + } + if !isJetfuelPrePasswordParityError(ErrJetfuelIdentifierNoSupportedAction) { + t.Fatal("identifier no-action error must enable the combined-credentials fallback") + } +} + +func TestSubmitJetfuelCredentialsFallsBackToCombinedAfterIdentifierNoAction(t *testing.T) { + t.Setenv("TWITTER_JETFUEL_VIEWER_CONTEXT", "0") + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.SetNextJetfuelCastleTokens([]string{"identifier-token", "combined-token"}) + + requestCount := 0 + client.HTTP = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if req.URL.Path != "/i/jfapi"+endpoints.JETFUEL_BEGIN_LOGIN_PATH { + t.Fatalf("request path = %s, want begin_login", req.URL.Path) + } + requestCount++ + form := readJetfuelTestForm(t, req) + switch requestCount { + case 1: + if got := form.Get("username_or_email"); got != "test-user" { + t.Fatalf("identifier username_or_email = %q", got) + } + if _, ok := form["password"]; ok { + t.Fatalf("identifier form unexpectedly contains password: %#v", form) + } + if got := form.Get("$castle_token"); got != "identifier-token" { + t.Fatalf("identifier Castle token = %q", got) + } + return jetfuelTestResponse("identifier accepted"), nil + case 2: + if got := form.Get("username_or_email"); got != "test-user" { + t.Fatalf("combined username_or_email = %q", got) + } + if got := form.Get("password"); got != "test-password" { + t.Fatalf("combined password = %q", got) + } + if got := form.Get("$castle_token"); got != "combined-token" { + t.Fatalf("combined Castle token = %q", got) + } + return jetfuelTestResponse("/home"), nil + default: + t.Fatalf("unexpected request %d", requestCount) + return nil, nil + } + })} + session := NewWebLoginSession(client) + session.backend = webLoginBackendJetfuel + session.jetfuel = &jetfuelLoginState{} + + result, err := session.SubmitCredentials(context.Background(), "test-user", "test-password") + if err != nil { + t.Fatalf("SubmitCredentials() error = %v", err) + } + if result == nil || result.Status != WebLoginStatusComplete { + t.Fatalf("SubmitCredentials() result = %#v, want complete", result) + } + if requestCount != 2 { + t.Fatalf("request count = %d, want 2", requestCount) + } +} + +func TestUnsupportedJetfuelResponseLoggingOmitsResponseValues(t *testing.T) { + t.Setenv("TWITTER_JETFUEL_VIEWER_CONTEXT", "0") + var logs bytes.Buffer + client := NewClient(cookies.NewCookies(nil), nil, zerolog.New(&logs).Level(zerolog.DebugLevel)) + client.SetNextJetfuelCastleTokens([]string{"castle-secret-marker"}) + client.HTTP = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + return jetfuelTestResponse("response-secret-marker\x00opaque_field"), nil + })} + session := NewWebLoginSession(client) + session.backend = webLoginBackendJetfuel + session.jetfuel = &jetfuelLoginState{} + + result, err := session.SubmitIdentifier(context.Background(), "identifier-secret-marker") + if result != nil { + t.Fatalf("SubmitIdentifier() result = %#v, want nil", result) + } + if !errors.Is(err, ErrJetfuelIdentifierNoSupportedAction) { + t.Fatalf("SubmitIdentifier() error = %v, want ErrJetfuelIdentifierNoSupportedAction", err) + } + logged := logs.String() + for _, secret := range []string{"response-secret-marker", "opaque_field", "identifier-secret-marker", "castle-secret-marker"} { + if strings.Contains(logged, secret) { + t.Fatalf("sanitized diagnostics contain %q: %s", secret, logged) + } + } + for _, field := range []string{"\"stage\":\"identifier\"", "\"response_bytes\":", "\"string_count\":", "\"path_count\":", "\"field_count\":"} { + if !strings.Contains(logged, field) { + t.Fatalf("sanitized diagnostics missing %q: %s", field, logged) + } + } +} + +func TestSubmitJetfuelCombinedCredentialsReturnsPasswordAction(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.SetNextJetfuelCastleTokens([]string{"combined-token"}) + client.HTTP = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if req.URL.Path != "/i/jfapi"+endpoints.JETFUEL_BEGIN_LOGIN_PATH { + t.Fatalf("request path = %s, want begin_login", req.URL.Path) + } + form := readJetfuelTestForm(t, req) + if form.Get("username_or_email") != "test-user" || form.Get("password") != "test-password" { + t.Fatalf("combined form = %#v", form) + } + return jetfuelTestResponse(endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH + "\x00password"), nil + })} + session := NewWebLoginSession(client) + session.backend = webLoginBackendJetfuel + session.jetfuel = &jetfuelLoginState{} + + result, err := session.SubmitCombinedCredentials(context.Background(), "test-user", "test-password") + if err != nil { + t.Fatalf("SubmitCombinedCredentials() error = %v", err) + } + if result == nil || result.Status != WebLoginStatusNeedsPassword { + t.Fatalf("SubmitCombinedCredentials() result = %#v, want needs password", result) + } + if got := session.jetfuel.passwordAction; got != endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH { + t.Fatalf("password action = %q", got) + } +} + +func TestSubmitJetfuelPasswordAllowsOneResponseDrivenReplay(t *testing.T) { + tests := []struct { + name string + terminalBody string + wantStatus WebLoginStatus + }{ + {name: "complete", terminalBody: "/home", wantStatus: WebLoginStatusComplete}, + { + name: "verification challenge", + terminalBody: endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH + "\x00challenge_response\x00Enter your verification code", + wantStatus: WebLoginStatusNeedsText, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.SetNextJetfuelCastleTokens([]string{"combined-token", "first-password-token", "replay-password-token"}) + + passwordRequestCount := 0 + client.HTTP = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + form := readJetfuelTestForm(t, req) + if req.URL.Path == "/i/jfapi"+endpoints.JETFUEL_BEGIN_LOGIN_PATH { + if form.Get("username_or_email") != "test-user" || form.Get("password") != "test-password" { + t.Fatalf("combined form = %#v", form) + } + if got := form.Get("$castle_token"); got != "combined-token" { + t.Fatalf("combined Castle token = %q", got) + } + return jetfuelTestResponse(endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH + "\x00password"), nil + } + if req.URL.Path != "/i/jfapi"+endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH { + t.Fatalf("request path = %s, want login_enter_password", req.URL.Path) + } + passwordRequestCount++ + if got := form.Get("password"); got != "test-password" { + t.Fatalf("password = %q", got) + } + wantToken := "first-password-token" + if passwordRequestCount == 2 { + wantToken = "replay-password-token" + } + if got := form.Get("$castle_token"); got != wantToken { + t.Fatalf("password request %d Castle token = %q, want %q", passwordRequestCount, got, wantToken) + } + if passwordRequestCount == 1 { + return jetfuelTestResponse(endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH + "\x00password"), nil + } + return jetfuelTestResponse(tc.terminalBody), nil + })} + session := NewWebLoginSession(client) + session.backend = webLoginBackendJetfuel + session.jetfuel = &jetfuelLoginState{} + + result, err := session.SubmitCombinedCredentials(context.Background(), "test-user", "test-password") + if err != nil { + t.Fatalf("SubmitCombinedCredentials() error = %v", err) + } + if result == nil || result.Status != WebLoginStatusNeedsPassword { + t.Fatalf("SubmitCombinedCredentials() result = %#v, want needs password", result) + } + if session.jetfuel.passwordReplayUsed { + t.Fatal("combined password action incorrectly consumed the response-driven replay") + } + + result, err = session.SubmitPassword(context.Background(), "test-password") + if err != nil { + t.Fatalf("first SubmitPassword() error = %v", err) + } + if result == nil || result.Status != WebLoginStatusNeedsPassword { + t.Fatalf("first SubmitPassword() result = %#v, want needs password", result) + } + + result, err = session.SubmitPassword(context.Background(), "test-password") + if err != nil { + t.Fatalf("replay SubmitPassword() error = %v", err) + } + if result == nil || result.Status != tc.wantStatus { + t.Fatalf("replay SubmitPassword() result = %#v, want status %s", result, tc.wantStatus) + } + if passwordRequestCount != 2 { + t.Fatalf("password request count = %d, want 2", passwordRequestCount) + } + }) + } +} + +func TestSubmitJetfuelPasswordRejectsSecondResponseDrivenReplay(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + client.SetNextJetfuelCastleTokens([]string{"combined-token", "first-password-token", "replay-password-token", "unused-token"}) + + passwordRequestCount := 0 + client.HTTP = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if req.URL.Path == "/i/jfapi"+endpoints.JETFUEL_BEGIN_LOGIN_PATH { + return jetfuelTestResponse(endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH + "\x00password"), nil + } + if req.URL.Path != "/i/jfapi"+endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH { + t.Fatalf("request path = %s, want login_enter_password", req.URL.Path) + } + passwordRequestCount++ + return jetfuelTestResponse(endpoints.JETFUEL_LOGIN_ENTER_PASSWORD_PATH + "\x00password"), nil + })} + session := NewWebLoginSession(client) + session.backend = webLoginBackendJetfuel + session.jetfuel = &jetfuelLoginState{} + + result, err := session.SubmitCombinedCredentials(context.Background(), "test-user", "test-password") + if err != nil || result == nil || result.Status != WebLoginStatusNeedsPassword { + t.Fatalf("SubmitCombinedCredentials() result = %#v, error = %v", result, err) + } + + result, err = session.SubmitPassword(context.Background(), "test-password") + if err != nil || result == nil || result.Status != WebLoginStatusNeedsPassword { + t.Fatalf("first SubmitPassword() result = %#v, error = %v", result, err) + } + result, err = session.SubmitPassword(context.Background(), "test-password") + if result != nil { + t.Fatalf("replay SubmitPassword() result = %#v, want nil", result) + } + if !errors.Is(err, ErrWebLoginUnexpectedSubtask) { + t.Fatalf("replay SubmitPassword() error = %v, want ErrWebLoginUnexpectedSubtask", err) + } + if passwordRequestCount != 2 { + t.Fatalf("password request count = %d, want 2", passwordRequestCount) + } + if !client.HasNextJetfuelCastleToken() { + t.Fatal("third Castle token was consumed; password replay was not bounded") + } +} + func TestSubmitJetfuelPasswordDefersTwoFactorPreludeUntilNextCastleToken(t *testing.T) { client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) client.SetNextJetfuelCastleTokens([]string{"password-castle-token"}) From 101ebc6626c32730c15035e0690a3a83366006c8 Mon Sep 17 00:00:00 2001 From: highesttt Date: Mon, 20 Jul 2026 18:12:16 -0400 Subject: [PATCH 13/20] fix: added password replay --- pkg/connector/login_test.go | 4 +- pkg/twittermeow/login_jetfuel.go | 22 ++++++ pkg/twittermeow/login_web_test.go | 108 ++++++++++++++++++++++++++++-- 3 files changed, 128 insertions(+), 6 deletions(-) diff --git a/pkg/connector/login_test.go b/pkg/connector/login_test.go index ff1777c8..61759c70 100644 --- a/pkg/connector/login_test.go +++ b/pkg/connector/login_test.go @@ -96,7 +96,7 @@ func TestGetLoginFlowsAdvertisesNativePasswordOnly(t *testing.T) { } } -func TestContinueWebCastleLoginRequestsFreshTokenForPasswordReplay(t *testing.T) { +func TestContinueWebCastleLoginRequestsFreshTokenForActionlessPasswordReplay(t *testing.T) { t.Setenv("TWITTER_JETFUEL_VIEWER_CONTEXT", "0") const mainPageHTML = `` + + client := twittermeow.NewClient(twitCookies.NewCookies(nil), nil, zerolog.Nop()) + combinedRequestCount := 0 + client.HTTP = &http.Client{Transport: connectorRoundTripFunc(func(req *http.Request) (*http.Response, error) { + switch { + case req.Method == http.MethodGet && req.URL.Path == "/i/jf/onboarding/web": + resp := connectorTestHTTPResponse(mainPageHTML) + resp.Header.Add("Set-Cookie", "guest_id=v1%3A123456789; Path=/; Secure") + return resp, nil + case req.Method == http.MethodGet && req.URL.Path == "/i/jfapi"+endpoints.JETFUEL_LANDING_PATH: + return connectorTestHTTPResponse("landing"), nil + case req.Method == http.MethodGet && req.URL.Path == "/i/jfapi/onboarding/web" && req.URL.Query().Get("mode") == "login": + return connectorTestHTTPResponse(endpoints.JETFUEL_BEGIN_LOGIN_PATH + "\x00username_or_email"), nil + case req.Method == http.MethodPost && req.URL.Path == "/i/jfapi"+endpoints.JETFUEL_BEGIN_LOGIN_PATH: + combinedRequestCount++ + body, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("ReadAll(combined request) error = %v", err) + } + for _, value := range []string{"username_or_email=test-user", "password=test-password", "%24castle_token=combined-token"} { + if !strings.Contains(string(body), value) { + t.Fatalf("combined request body missing %q", value) + } + } + return connectorTestHTTPResponse("We've temporarily limited your login. Please try again later."), nil + default: + t.Fatalf("unexpected request: %s %s", req.Method, req.URL.String()) + return nil, nil + } + })} + session := twittermeow.NewWebLoginSession(client) + result, err := session.Start(context.Background()) + if err != nil { + t.Fatalf("Start() error = %v", err) + } + if !session.UsesJetfuel() || result == nil || result.Status != twittermeow.WebLoginStatusNeedsIdentifier { + t.Fatalf("Start() result = %#v, UsesJetfuel = %t", result, session.UsesJetfuel()) + } + + login := &TwitterLogin{ + User: &bridgev2.User{Log: zerolog.Nop()}, + webLogin: session, + webLoginIdentifier: "test-user", + webLoginPassword: "test-password", + } + step, err := login.continueStartedCredentialsLogin(context.Background(), result) + if err != nil { + t.Fatalf("continueStartedCredentialsLogin() error = %v", err) + } + if step == nil || step.StepID != LoginStepIDCastleToken { + t.Fatalf("continueStartedCredentialsLogin() step = %#v, want Castle token step", step) + } + if login.webLoginCastleStage != webLoginCastleStageCombined { + t.Fatalf("initial Castle stage = %q, want combined", login.webLoginCastleStage) + } + + client.SetNextJetfuelCastleTokens([]string{"combined-token", "unused-token"}) + var logs bytes.Buffer + logger := zerolog.New(&logs).With().Str("login_id", "test-login").Logger() + ctx := logger.WithContext(context.Background()) + step, err = login.continueWebCastleLogin(ctx) + if step != nil { + t.Fatalf("continueWebCastleLogin() step = %#v, want nil", step) + } + var respErr bridgev2.RespError + if !errors.As(err, &respErr) || respErr.ErrCode != ErrWebLoginFailed.ErrCode { + t.Fatalf("continueWebCastleLogin() error = %#v, want ErrWebLoginFailed", err) + } + if login.webLoginCastleStage != "" { + t.Fatalf("Castle stage = %q, want cleared", login.webLoginCastleStage) + } + if combinedRequestCount != 1 { + t.Fatalf("combined request count = %d, want 1", combinedRequestCount) + } + if !client.HasNextJetfuelCastleToken() { + t.Fatal("unused Castle token was consumed after code 399") + } + logged := logs.String() + for _, field := range []string{"\"login_id\":\"test-login\"", "\"stage\":\"combined\"", "\"error_kind\":\"x_response\"", "\"error_code\":399"} { + if !strings.Contains(logged, field) { + t.Fatalf("safe Castle failure log missing %q: %s", field, logged) + } + } + if strings.Contains(logged, "temporarily limited") || strings.Contains(logged, "test-password") || strings.Contains(logged, "combined-token") { + t.Fatalf("Castle failure log leaked response or submitted values: %s", logged) + } +} + func TestContinueWebCastleLoginRequestsFreshTokenForActionlessPasswordReplay(t *testing.T) { t.Setenv("TWITTER_JETFUEL_VIEWER_CONTEXT", "0") const mainPageHTML = `` + +func TestClientHTTPLoginUsesCastleWebviewThenCapturesLocalRequest(t *testing.T) { + t.Setenv("TWITTER_JETFUEL_VIEWER_CONTEXT", "0") + login := &TwitterLogin{ + User: &bridgev2.User{Log: zerolog.Nop()}, + useClientHTTPLogin: true, + } + + step, err := login.submitCredentialsInput(context.Background(), map[string]string{ + loginFieldIdentifier: "test-user", + loginFieldPassword: "test-password", + }) + if err != nil { + t.Fatal(err) + } + if step.StepID != LoginStepIDClientHTTPRequest || !step.CookiesParams.Hidden { + t.Fatalf("first step = %#v, want hidden client HTTP request", step) + } + + for range 20 { + if step.StepID == LoginStepIDCastleToken { + break + } + pending := login.clientHTTPTransport.PendingRequest() + if pending == nil { + t.Fatal("client HTTP request is not pending") + } + var body string + switch { + case pending.URL == endpoints.JETFUEL_LOGIN_REFERER_URL: + body = clientHTTPTestMainPage + case strings.Contains(pending.URL, "/i/jfapi"+endpoints.JETFUEL_LANDING_PATH): + body = "landing" + case strings.Contains(pending.URL, "/i/jfapi/onboarding/web?mode=login"): + body = endpoints.JETFUEL_BEGIN_LOGIN_PATH + "\x00username_or_email" + default: + body = "{}" + } + input := clientHTTPTestResponseInput(pending, body) + input["guest_id"] = "v1%3A123456789" + input["gt"] = "123456789" + step, err = login.submitClientHTTPInput(context.Background(), input) + if err != nil { + t.Fatalf("submit response for %s: %v", pending.URL, err) + } + if step.StepID != LoginStepIDClientHTTPRequest && step.StepID != LoginStepIDCastleToken { + t.Fatalf("step after %s = %#v", pending.URL, step) + } + } + + if step.StepID != LoginStepIDCastleToken || step.CookiesParams == nil || !step.CookiesParams.Hidden { + t.Fatalf("step = %#v, want native hidden Castle token step", step) + } + step, err = login.submitWebCastleTokenInput(context.Background(), clientHTTPTestCastleInput()) + if err != nil { + t.Fatal(err) + } + pending := login.clientHTTPTransport.PendingRequest() + if step.StepID != LoginStepIDClientHTTPRequest || pending == nil { + t.Fatalf("step = %#v pending = %#v, want direct client request", step, pending) + } + form, err := url.ParseQuery(string(pending.Body)) + if err != nil { + t.Fatal(err) + } + if form.Get("username_or_email") != "test-user" || form.Get("password") != "test-password" { + t.Fatalf("combined form did not preserve credentials") + } + if form.Get("$castle_token") != clientHTTPTestCastleToken(1) { + t.Fatalf("Castle form value = %q", form.Get("$castle_token")) + } + if strings.Contains(step.CookiesParams.ExtractJS, "test-password") { + t.Fatal("client HTTP request envelope contains the plaintext password") + } + if strings.Contains(step.CookiesParams.ExtractJS, "createRequestToken") || + strings.Contains(step.CookiesParams.ExtractJS, "fetch(") { + t.Fatal("client HTTP request envelope contains WebView networking code") + } + + input := clientHTTPTestResponseInput(pending, "login accepted") + input["guest_id"] = "v1%3A123456789" + input["gt"] = "123456789" + input["auth_token"] = "authenticated-cookie" + input["ct0"] = "csrf-cookie" + input["twid"] = "u%3D123456789" + step, err = login.submitClientHTTPInput(context.Background(), input) + if err != nil { + t.Fatal(err) + } + pending = login.clientHTTPTransport.PendingRequest() + if step.StepID != LoginStepIDClientHTTPRequest || pending == nil || + pending.URL != endpoints.BASE_MESSAGES_URL { + t.Fatalf("post-login step = %#v pending = %#v, want client-side messages request", step, pending) + } + + for range 30 { + if step.StepID == LoginStepIDCastleToken { + step, err = login.submitWebCastleTokenInput(context.Background(), clientHTTPTestCastleInput()) + if err != nil { + t.Fatal(err) + } + } + if step.StepID != LoginStepIDClientHTTPRequest { + break + } + pending = login.clientHTTPTransport.PendingRequest() + if pending == nil { + break + } + body := "{}" + if pending.URL == endpoints.BASE_MESSAGES_URL { + body = clientHTTPTestMainPage + } + input = clientHTTPTestResponseInput(pending, body) + input["guest_id"] = "v1%3A123456789" + input["gt"] = "123456789" + input["auth_token"] = "authenticated-cookie" + input["ct0"] = "csrf-cookie" + input["twid"] = "u%3D123456789" + step, err = login.submitClientHTTPInput(context.Background(), input) + if err != nil { + t.Fatalf("submit post-login response for %s: %v", pending.URL, err) + } + if step.StepID == LoginStepIDCastleToken { + continue + } + if step.StepID != LoginStepIDClientHTTPRequest { + break + } + } + if step.StepID != LoginStepJuiceboxPIN { + t.Fatalf("final step = %#v, want PIN step", step) + } + if login.clientHTTPTransport != nil || login.webLogin.Client().IsClientHTTPEnabled() { + t.Fatal("client HTTP transport remained enabled after login completion") + } +} + +func TestClientHTTPLoginPreservesVerificationCodeDuringPostLoginRequests(t *testing.T) { + t.Setenv("TWITTER_JETFUEL_VIEWER_CONTEXT", "0") + login := &TwitterLogin{ + User: &bridgev2.User{Log: zerolog.Nop()}, + useClientHTTPLogin: true, + } + ctx := context.Background() + + step, err := login.submitCredentialsInput(ctx, map[string]string{ + loginFieldIdentifier: "test-user", + loginFieldPassword: "test-password", + }) + if err != nil { + t.Fatal(err) + } + + for range 20 { + if step.StepID == LoginStepIDCastleToken { + break + } + pending := login.clientHTTPTransport.PendingRequest() + if pending == nil { + t.Fatal("client HTTP request is not pending") + } + body := "{}" + switch { + case pending.URL == endpoints.JETFUEL_LOGIN_REFERER_URL: + body = clientHTTPTestMainPage + case strings.Contains(pending.URL, "/i/jfapi"+endpoints.JETFUEL_LANDING_PATH): + body = "landing" + case strings.Contains(pending.URL, "/i/jfapi/onboarding/web?mode=login"): + body = endpoints.JETFUEL_BEGIN_LOGIN_PATH + "\x00username_or_email" + } + input := clientHTTPTestResponseInput(pending, body) + input["guest_id"] = "v1%3A123456789" + input["gt"] = "123456789" + step, err = login.submitClientHTTPInput(ctx, input) + if err != nil { + t.Fatalf("submit bootstrap response for %s: %v", pending.URL, err) + } + } + + if step.StepID != LoginStepIDCastleToken { + t.Fatalf("step = %#v, want native Castle token step", step) + } + step, err = login.submitWebCastleTokenInput(ctx, clientHTTPTestCastleInput()) + if err != nil { + t.Fatal(err) + } + pending := login.clientHTTPTransport.PendingRequest() + if step.StepID != LoginStepIDClientHTTPRequest || pending == nil { + t.Fatalf("step = %#v pending = %#v, want credentials request", step, pending) + } + chooserBody := "Select a method to authenticate\x00Choose the method you prefer to use for 2-step verification.\x00" + + "two_factor_method\x00Totp\x00BackupCode\x00U2fSecurityKey\x00" + + "user_id\x001127993589949243392\x00" + + "session_token\x0012345678-1234-1234-1234-123456789abc\x00" + + "prelude_dispatch_id\x00abcdefab-1234-1234-1234-abcdefabcdef\x00" + + "begin_two_factor_auth" + input := clientHTTPTestResponseInput(pending, chooserBody) + input["guest_id"] = "v1%3A123456789" + input["gt"] = "123456789" + step, err = login.submitClientHTTPInput(ctx, input) + if err != nil { + t.Fatal(err) + } + if step.StepID != LoginStepIDAuthMethod { + t.Fatalf("credentials response step = %#v, want auth method", step) + } + + step, err = login.submitWebAuthMethodInput(ctx, map[string]string{ + loginFieldAuthMethod: "Totp", + }) + if err != nil { + t.Fatal(err) + } + pending = login.clientHTTPTransport.PendingRequest() + if step.StepID != LoginStepIDClientHTTPRequest || pending == nil { + t.Fatalf("auth method submit step = %#v pending = %#v", step, pending) + } + challengeBody := "Enter the code from your authentication app.\x00challenge_response\x00" + + endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH + "\x00" + + "session_token\x0012345678-1234-1234-1234-123456789abc" + input = clientHTTPTestResponseInput(pending, challengeBody) + input["guest_id"] = "v1%3A123456789" + input["gt"] = "123456789" + step, err = login.submitClientHTTPInput(ctx, input) + if err != nil { + t.Fatal(err) + } + if step.StepID != LoginStepIDVerification { + t.Fatalf("auth method response step = %#v, want verification", step) + } + + const verificationCode = "123456" + step, err = login.submitWebVerificationInput(ctx, map[string]string{ + loginFieldVerificationCode: verificationCode, + }) + if err != nil { + t.Fatal(err) + } + pending = login.clientHTTPTransport.PendingRequest() + if step.StepID != LoginStepIDClientHTTPRequest || pending == nil { + t.Fatalf("verification submit step = %#v pending = %#v", step, pending) + } + input = clientHTTPTestResponseInput(pending, "/home") + input["guest_id"] = "v1%3A123456789" + input["gt"] = "123456789" + input["auth_token"] = "authenticated-cookie" + input["ct0"] = "csrf-cookie" + input["twid"] = "u%3D123456789" + step, err = login.submitClientHTTPInput(ctx, input) + if err != nil { + t.Fatal(err) + } + pending = login.clientHTTPTransport.PendingRequest() + if step.StepID != LoginStepIDClientHTTPRequest || pending == nil || + pending.URL != endpoints.BASE_MESSAGES_URL { + t.Fatalf("post-verification step = %#v pending = %#v, want messages request", step, pending) + } + if login.webLoginText != verificationCode { + t.Fatalf("verification code was cleared while its client HTTP operation is still pending") + } + + for range 30 { + if step.StepID == LoginStepIDCastleToken { + step, err = login.submitWebCastleTokenInput(ctx, clientHTTPTestCastleInput()) + if err != nil { + t.Fatal(err) + } + } + if step.StepID != LoginStepIDClientHTTPRequest { + break + } + pending = login.clientHTTPTransport.PendingRequest() + if pending == nil { + break + } + body := "{}" + if pending.URL == endpoints.BASE_MESSAGES_URL { + body = clientHTTPTestMainPage + } + input = clientHTTPTestResponseInput(pending, body) + input["guest_id"] = "v1%3A123456789" + input["gt"] = "123456789" + input["auth_token"] = "authenticated-cookie" + input["ct0"] = "csrf-cookie" + input["twid"] = "u%3D123456789" + step, err = login.submitClientHTTPInput(ctx, input) + if err != nil { + t.Fatalf("submit post-login response for %s: %v", pending.URL, err) + } + if step.StepID == LoginStepIDCastleToken { + continue + } + if step.StepID != LoginStepIDClientHTTPRequest { + break + } + } + if step.StepID != LoginStepJuiceboxPIN { + t.Fatalf("final step = %#v, want PIN step", step) + } + if login.webLoginText != "" { + t.Fatal("verification code remained after its client HTTP operation completed") + } +} + +func TestClientHTTPRequestFailureStopsAutomaticRetry(t *testing.T) { + login := &TwitterLogin{ + User: &bridgev2.User{Log: zerolog.Nop()}, + useClientHTTPLogin: true, + } + step, err := login.submitCredentialsInput(context.Background(), map[string]string{ + loginFieldIdentifier: "test-user", + loginFieldPassword: "test-password", + }) + if err != nil { + t.Fatal(err) + } + if step.StepID != LoginStepIDClientHTTPRequest { + t.Fatalf("initial step = %#v", step) + } + + step, err = login.submitClientHTTPInput(context.Background(), map[string]string{ + loginFieldClientHTTPError: "Failed to fetch", + }) + if err != nil { + t.Fatal(err) + } + if step.StepID != LoginStepIDCredentials { + t.Fatalf("failure step = %#v, want credentials step", step) + } + if !strings.Contains(step.Instructions, "Please try again") { + t.Fatalf("failure instructions = %q", step.Instructions) + } + if login.clientHTTPTransport != nil || login.webLogin != nil || + login.webLoginIdentifier != "" || login.webLoginPassword != "" { + t.Fatal("client HTTP failure retained retryable login state") + } +} + +func TestClientHTTPRequestStepContract(t *testing.T) { + request := &twittermeow.ClientHTTPRequest{ + ID: "client-http-7", + Method: http.MethodPost, + URL: "https://x.com/i/jfapi/onboarding/web/actions/begin_login", + Referrer: endpoints.JETFUEL_LOGIN_REFERER_URL, + Headers: map[string]string{"authorization": "Bearer public"}, + CookieHeader: "guest_id=v1%3A123; ct0=csrf", + Body: []byte("password=secret"), + } + step, err := makeClientHTTPRequestStep(request) + if err != nil { + t.Fatal(err) + } + if step.Type != bridgev2.LoginStepTypeCookies || step.StepID != LoginStepIDClientHTTPRequest || + step.CookiesParams == nil || !step.CookiesParams.Hidden { + t.Fatalf("step = %#v", step) + } + if step.CookiesParams.URL != clientHTTPDispatchURL || + !strings.Contains(step.CookiesParams.WaitForURLPattern, "beeper-client-http") { + t.Fatalf("client HTTP step does not carry its client-dispatch marker: %#v", step.CookiesParams) + } + fields := make(map[string]bridgev2.LoginCookieField) + for _, field := range step.CookiesParams.Fields { + fields[field.ID] = field + } + for _, fieldID := range []string{ + loginFieldClientHTTPRequestID, + loginFieldClientHTTPStatus, + loginFieldClientHTTPResponseHeaders, + loginFieldClientHTTPResponseBody, + loginFieldBrowserUserAgent, + "auth_token", + "ct0", + } { + if _, ok := fields[fieldID]; !ok { + t.Fatalf("client HTTP fields missing %q", fieldID) + } + } + if !fields[loginFieldClientHTTPRequestID].Required || !fields[loginFieldBrowserUserAgent].Required { + t.Fatal("request ID and browser user agent must be required") + } + if !strings.HasPrefix(step.CookiesParams.ExtractJS, clientHTTPConfigMarkerPrefix) { + t.Fatal("client HTTP request envelope is missing its protocol marker") + } + if strings.Contains(step.CookiesParams.ExtractJS, "password=secret") { + t.Fatal("client HTTP request envelope contains plaintext request body") + } + if strings.Contains(step.CookiesParams.ExtractJS, "fetch(") || + strings.Contains(step.CookiesParams.ExtractJS, "createRequestToken") { + t.Fatal("client HTTP request envelope contains WebView networking code") + } + config := decodeClientHTTPTestConfig(t, step.CookiesParams.ExtractJS) + if config.RequestID != request.ID || config.RequestURL != request.URL || + config.CookieHeader != request.CookieHeader || + config.Body != base64.RawURLEncoding.EncodeToString(request.Body) { + t.Fatalf("decoded client HTTP config = %#v", config) + } +} + +func TestClientHTTPResponseDecodeRejectsMalformedData(t *testing.T) { + valid := clientHTTPTestResponseInput(&twittermeow.ClientHTTPRequest{ + ID: "client-http-1", + URL: "https://x.com/login", + }, "response") + if _, err := decodeClientHTTPResponse(valid); err != nil { + t.Fatalf("valid response rejected: %v", err) + } + for _, mutate := range []func(map[string]string){ + func(input map[string]string) { delete(input, loginFieldClientHTTPRequestID) }, + func(input map[string]string) { input[loginFieldClientHTTPStatus] = "not-a-status" }, + func(input map[string]string) { input[loginFieldClientHTTPResponseBody] = "bad" }, + func(input map[string]string) { input[loginFieldClientHTTPResponseHeaders] = "v1.invalid" }, + func(input map[string]string) { + headers, _ := json.Marshal(map[string]string{"bad header": "value"}) + input[loginFieldClientHTTPResponseHeaders] = clientHTTPEncodedFieldPrefix + + base64.RawURLEncoding.EncodeToString(headers) + }, + } { + input := make(map[string]string, len(valid)) + for key, value := range valid { + input[key] = value + } + mutate(input) + if _, err := decodeClientHTTPResponse(input); err == nil { + t.Fatalf("malformed response accepted: %#v", input) + } + } +} + +func clientHTTPTestResponseInput(request *twittermeow.ClientHTTPRequest, body string) map[string]string { + headers, _ := json.Marshal(map[string]string{"content-type": "application/octet-stream"}) + return map[string]string{ + loginFieldClientHTTPRequestID: request.ID, + loginFieldClientHTTPStatus: "200", + loginFieldClientHTTPResponseURL: request.URL, + loginFieldClientHTTPResponseHeaders: clientHTTPEncodedFieldPrefix + base64.RawURLEncoding.EncodeToString(headers), + loginFieldClientHTTPResponseBody: clientHTTPEncodedFieldPrefix + base64.RawURLEncoding.EncodeToString([]byte(body)), + loginFieldBrowserUserAgent: "Mozilla/5.0 test client", + loginFieldBrowserSecCHUA: `"Chromium";v="150"`, + loginFieldBrowserPlatform: `"Windows"`, + loginFieldBrowserMobile: "?0", + } +} + +func clientHTTPTestCastleToken(index int) string { + return strings.Repeat("client-http-castle-"+string(rune('0'+index))+"-", 12) +} + +func clientHTTPTestCastleInput() map[string]string { + input := map[string]string{ + loginFieldBrowserUserAgent: "Mozilla/5.0 test client", + } + for index := 1; index <= castleTokenBatchSize; index++ { + input[castleTokenFieldID(index)] = clientHTTPTestCastleToken(index) + } + return input +} + +func decodeClientHTTPTestConfig(t *testing.T, extractJS string) clientHTTPConfig { + t.Helper() + if !strings.HasPrefix(extractJS, clientHTTPConfigMarkerPrefix) { + t.Fatal("client HTTP config marker prefix is missing") + } + end := strings.Index(extractJS, clientHTTPConfigMarkerSuffix) + if end < 0 { + t.Fatal("client HTTP config marker suffix is missing") + } + encoded := strings.TrimPrefix(extractJS[:end], clientHTTPConfigMarkerPrefix) + raw, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + t.Fatalf("decode client HTTP config: %v", err) + } + var config clientHTTPConfig + if err = json.Unmarshal(raw, &config); err != nil { + t.Fatalf("unmarshal client HTTP config: %v", err) + } + return config +} diff --git a/pkg/connector/login_live_probe_test.go b/pkg/connector/login_live_probe_test.go index 62b4a1bb..b6ada764 100644 --- a/pkg/connector/login_live_probe_test.go +++ b/pkg/connector/login_live_probe_test.go @@ -29,7 +29,7 @@ func TestLiveNativeLoginFlowProbe(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) defer cancel() - tc := &TwitterConnector{Config: Config{NativeLogin: true}} + tc := &TwitterConnector{Config: Config{LoginFlow: LoginFlowNative}} user := &bridgev2.User{ User: &database.User{ MXID: id.UserID("@highest:beeper.com"), diff --git a/pkg/connector/login_test.go b/pkg/connector/login_test.go index 461a7e27..7007d985 100644 --- a/pkg/connector/login_test.go +++ b/pkg/connector/login_test.go @@ -155,79 +155,105 @@ func TestHandleWebCastleStageErrorMakesPreludeFailureTerminal(t *testing.T) { } } -func TestGetLoginFlowsUsesWebViewByDefault(t *testing.T) { - var tc TwitterConnector - flows := tc.GetLoginFlows() - - if len(flows) != 1 { - t.Fatalf("len(flows) = %d, want 1", len(flows)) - } - if flows[0].ID != LoginFlowIDCookies { - t.Fatalf("flow ID = %s, want %s", flows[0].ID, LoginFlowIDCookies) - } -} - -func TestGetLoginFlowsUsesNativeOnlyWhenEnabled(t *testing.T) { - tc := TwitterConnector{Config: Config{NativeLogin: true}} - flows := tc.GetLoginFlows() - - if len(flows) != 1 { - t.Fatalf("len(flows) = %d, want 1", len(flows)) +func TestGetLoginFlowsUsesConfiguredFlow(t *testing.T) { + tests := []struct { + name string + configFlow LoginFlow + wantID string + wantBeta bool + }{ + {name: "zero value uses webview", wantID: LoginFlowIDCookies}, + {name: "webview", configFlow: LoginFlowWebView, wantID: LoginFlowIDCookies}, + {name: "native", configFlow: LoginFlowNative, wantID: LoginFlowIDPassword}, + {name: "client HTTP", configFlow: LoginFlowClientHTTP, wantID: LoginFlowIDClientHTTP, wantBeta: true}, } - if flows[0].ID != LoginFlowIDPassword { - t.Fatalf("flow ID = %s, want %s", flows[0].ID, LoginFlowIDPassword) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tc := TwitterConnector{Config: Config{LoginFlow: test.configFlow}} + flows := tc.GetLoginFlows() + if len(flows) != 1 { + t.Fatalf("len(flows) = %d, want 1", len(flows)) + } + if flows[0].ID != test.wantID { + t.Fatalf("flow ID = %s, want %s", flows[0].ID, test.wantID) + } + if strings.Contains(strings.ToLower(flows[0].Name), "beta") != test.wantBeta { + t.Fatalf("flow name = %q, want beta=%t", flows[0].Name, test.wantBeta) + } + }) } } -func TestCreateLoginRespectsNativeLoginConfig(t *testing.T) { +func TestCreateLoginRespectsLoginFlowConfig(t *testing.T) { tests := []struct { name string - nativeLogin bool + configFlow LoginFlow flowID string wantType bridgev2.LoginStepType wantStepID string wantInvalid bool }{ { - name: "default flow uses WebView", + name: "zero value uses webview", wantType: bridgev2.LoginStepTypeCookies, wantStepID: LoginStepIDCookies, }, { - name: "explicit WebView flow", + name: "explicit webview flow", + configFlow: LoginFlowWebView, flowID: LoginFlowIDCookies, wantType: bridgev2.LoginStepTypeCookies, wantStepID: LoginStepIDCookies, }, { - name: "native flow disabled", + name: "native flow disabled by webview config", + configFlow: LoginFlowWebView, flowID: LoginFlowIDPassword, wantInvalid: true, }, { - name: "default flow uses native when enabled", - nativeLogin: true, - wantType: bridgev2.LoginStepTypeUserInput, - wantStepID: LoginStepIDCredentials, + name: "default flow uses native", + configFlow: LoginFlowNative, + wantType: bridgev2.LoginStepTypeUserInput, + wantStepID: LoginStepIDCredentials, }, { - name: "explicit native flow", - nativeLogin: true, - flowID: LoginFlowIDPassword, - wantType: bridgev2.LoginStepTypeUserInput, - wantStepID: LoginStepIDCredentials, + name: "explicit native flow", + configFlow: LoginFlowNative, + flowID: LoginFlowIDPassword, + wantType: bridgev2.LoginStepTypeUserInput, + wantStepID: LoginStepIDCredentials, }, { - name: "WebView flow disabled when native enabled", - nativeLogin: true, + name: "webview flow disabled by native config", + configFlow: LoginFlowNative, flowID: LoginFlowIDCookies, wantInvalid: true, }, + { + name: "default flow uses client HTTP", + configFlow: LoginFlowClientHTTP, + wantType: bridgev2.LoginStepTypeUserInput, + wantStepID: LoginStepIDCredentials, + }, + { + name: "explicit client HTTP flow", + configFlow: LoginFlowClientHTTP, + flowID: LoginFlowIDClientHTTP, + wantType: bridgev2.LoginStepTypeUserInput, + wantStepID: LoginStepIDCredentials, + }, + { + name: "native flow disabled by client HTTP config", + configFlow: LoginFlowClientHTTP, + flowID: LoginFlowIDPassword, + wantInvalid: true, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - tc := TwitterConnector{Config: Config{NativeLogin: test.nativeLogin}} + tc := TwitterConnector{Config: Config{LoginFlow: test.configFlow}} process, err := tc.CreateLogin(context.Background(), nil, test.flowID) if test.wantInvalid { if !errors.Is(err, bridgev2.ErrInvalidLoginFlowID) { diff --git a/pkg/twittermeow/client.go b/pkg/twittermeow/client.go index 97a85bdd..3ddfe674 100644 --- a/pkg/twittermeow/client.go +++ b/pkg/twittermeow/client.go @@ -50,6 +50,8 @@ type Client struct { session *CachedSession HTTP *http.Client + clientHTTPTransport *ClientHTTPTransport + eventHandler EventHandler streamEventHandler StreamEventHandler xchatEventHandler XChatEventHandler @@ -228,6 +230,9 @@ func (c *Client) LoadMessagesPage(ctx context.Context) (CurrentUserProfile, erro profile, err := c.GetCurrentUserProfile(ctx) if err != nil { + if errors.Is(err, ErrClientHTTPRequestPending) { + return CurrentUserProfile{}, err + } if IsAuthError(err) { return CurrentUserProfile{}, err } @@ -338,11 +343,10 @@ func (c *Client) fetchCloudflareJSD(ctx context.Context, pageURL *url.URL, mainP return ErrMaxRetriesReached } -func (c *Client) fetchAndParseMainScript(ctx context.Context, scriptURL string) string { +func (c *Client) fetchAndParseMainScript(ctx context.Context, scriptURL string) (string, error) { scriptRespBody, err := c.fetchScript(ctx, scriptURL) if err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to fetch main script") - return "" + return "", err } authTokenBytes := methods.ParseBearerToken(scriptRespBody) authTokens := exslices.CastFunc(authTokenBytes, func(from []byte) string { @@ -360,7 +364,7 @@ func (c *Client) fetchAndParseMainScript(ctx context.Context, scriptURL string) Msg("Hardcoded token doesn't match fetched one") c.session.bearerToken = authTokens[0] } - return methods.ParseOndemandSURLFromScript(scriptRespBody) + return methods.ParseOndemandSURLFromScript(scriptRespBody), nil } func (c *Client) fetchAndParseSScript(ctx context.Context, scriptURL string) (*[4]int, error) { @@ -447,6 +451,9 @@ func (c *Client) parseMainPageHTML(ctx context.Context, mainPageResp *http.Respo } if mainPageResp.Request != nil && mainPageResp.Request.URL != nil { if err := c.fetchCloudflareJSD(ctx, mainPageResp.Request.URL, mainPageHTML); err != nil { + if errors.Is(err, ErrClientHTTPRequestPending) { + return err + } c.Logger.Debug().Err(err).Msg("Failed to fetch Cloudflare JSD bootstrap") } } @@ -480,12 +487,21 @@ func (c *Client) parseMainPageHTML(ctx context.Context, mainPageResp *http.Respo if mainScriptURL == "" { zerolog.Ctx(ctx).Warn().Int("status_code", mainPageResp.StatusCode).Msg("Main script URL not found in main page HTML") } else if ondemandSURL == "" { - ondemandSURL = c.fetchAndParseMainScript(ctx, mainScriptURL) + var fetchErr error + ondemandSURL, fetchErr = c.fetchAndParseMainScript(ctx, mainScriptURL) + if errors.Is(fetchErr, ErrClientHTTPRequestPending) { + return fetchErr + } else if fetchErr != nil { + zerolog.Ctx(ctx).Warn().Err(fetchErr).Msg("Failed to fetch main script") + } } if ondemandSURL == "" { c.Logger.Warn().Msg("ondemand.s URL not found in bootstrap sources") } else if indexes, err := c.fetchAndParseSScript(ctx, ondemandSURL); err != nil { + if errors.Is(err, ErrClientHTTPRequestPending) { + return err + } c.Logger.Warn().Err(err).Msg("Failed to fetch and parse s script") } else { c.session.variableIndexes = indexes diff --git a/pkg/twittermeow/client_http.go b/pkg/twittermeow/client_http.go new file mode 100644 index 00000000..cb3d4ffd --- /dev/null +++ b/pkg/twittermeow/client_http.go @@ -0,0 +1,347 @@ +package twittermeow + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "sync" +) + +const ( + ClientHTTPMaxResponseBodySize = 8 << 20 + ClientHTTPMaxResponseHeadersSize = 64 << 10 + clientHTTPMaxRequestBodySize = 4 << 20 + clientHTTPMaxExchanges = 64 +) + +var ( + ErrClientHTTPRequestPending = errors.New("client HTTP request is pending") + errClientHTTPOperationChanged = errors.New("client HTTP operation changed before completion") + errClientHTTPReplayMismatch = errors.New("client HTTP request did not match the recorded operation") +) + +type ClientHTTPRequest struct { + ID string + Method string + URL string + Referrer string + Headers map[string]string + CookieHeader string + Body []byte +} + +func (req *ClientHTTPRequest) clone() *ClientHTTPRequest { + if req == nil { + return nil + } + cloned := *req + cloned.Headers = make(map[string]string, len(req.Headers)) + for key, value := range req.Headers { + cloned.Headers[key] = value + } + cloned.Body = bytes.Clone(req.Body) + return &cloned +} + +type ClientHTTPResponse struct { + RequestID string + Status int + Headers http.Header + Body []byte + FinalURL string +} + +type clientHTTPExchange struct { + method string + url string + response ClientHTTPResponse +} + +// ClientHTTPTransport turns each outgoing HTTP request into a client login step. +// +// A connector starts an operation, runs the normal twittermeow method, submits +// the client response, and reruns the same operation. Previously submitted +// responses are replayed in order until the next real request is reached. This +// keeps request construction and response parsing in Go while the network I/O +// happens in the Beeper client's network stack. +type ClientHTTPTransport struct { + mu sync.Mutex + + operation string + exchanges []clientHTTPExchange + cursor int + pending *ClientHTTPRequest + nextID uint64 + baseTransport http.RoundTripper + baseCheckRedirect func(*http.Request, []*http.Request) error +} + +func newClientHTTPTransport() *ClientHTTPTransport { + return &ClientHTTPTransport{} +} + +func (t *ClientHTTPTransport) BeginOperation(operation string) error { + operation = strings.TrimSpace(operation) + if operation == "" { + return fmt.Errorf("client HTTP operation name is required") + } + + t.mu.Lock() + defer t.mu.Unlock() + if t.operation == "" { + t.operation = operation + } else if t.operation != operation { + return fmt.Errorf("%w: have %q, got %q", errClientHTTPOperationChanged, t.operation, operation) + } + t.cursor = 0 + return nil +} + +func (t *ClientHTTPTransport) EndOperation() error { + t.mu.Lock() + defer t.mu.Unlock() + if t.pending != nil { + return ErrClientHTTPRequestPending + } + if t.cursor != len(t.exchanges) { + return fmt.Errorf("%w: consumed %d of %d responses", errClientHTTPReplayMismatch, t.cursor, len(t.exchanges)) + } + t.resetOperationLocked() + return nil +} + +func (t *ClientHTTPTransport) ResetOperation() { + t.mu.Lock() + defer t.mu.Unlock() + t.resetOperationLocked() +} + +func (t *ClientHTTPTransport) resetOperationLocked() { + t.operation = "" + t.exchanges = nil + t.cursor = 0 + t.pending = nil +} + +func (t *ClientHTTPTransport) PendingRequest() *ClientHTTPRequest { + t.mu.Lock() + defer t.mu.Unlock() + return t.pending.clone() +} + +func (t *ClientHTTPTransport) SubmitResponse(response ClientHTTPResponse) error { + t.mu.Lock() + defer t.mu.Unlock() + if t.pending == nil { + return fmt.Errorf("no client HTTP request is pending") + } + if response.RequestID != t.pending.ID { + return fmt.Errorf("client HTTP response request ID does not match") + } + if response.Status < 100 || response.Status > 599 { + return fmt.Errorf("invalid client HTTP response status %d", response.Status) + } + if len(response.Body) > ClientHTTPMaxResponseBodySize { + return fmt.Errorf("client HTTP response body is too large") + } + if len(t.exchanges) >= clientHTTPMaxExchanges { + return fmt.Errorf("client HTTP operation has too many requests") + } + headerSize := 0 + for name, values := range response.Headers { + headerSize += len(name) + for _, value := range values { + headerSize += len(value) + } + } + if headerSize > ClientHTTPMaxResponseHeadersSize { + return fmt.Errorf("client HTTP response headers are too large") + } + if response.FinalURL != "" { + finalURL, err := url.Parse(response.FinalURL) + if err != nil || !isAllowedClientHTTPURL(finalURL) { + return fmt.Errorf("client HTTP response URL is not allowed") + } + } + response.Headers = response.Headers.Clone() + response.Body = bytes.Clone(response.Body) + t.exchanges = append(t.exchanges, clientHTTPExchange{ + method: t.pending.Method, + url: t.pending.URL, + response: response, + }) + t.pending = nil + return nil +} + +func (t *ClientHTTPTransport) RoundTrip(request *http.Request) (*http.Response, error) { + body, err := readClientHTTPRequestBody(request) + if err != nil { + return nil, err + } + + t.mu.Lock() + defer t.mu.Unlock() + if t.operation == "" { + return nil, fmt.Errorf("client HTTP request made outside an operation") + } + if t.cursor < len(t.exchanges) { + exchange := t.exchanges[t.cursor] + if request.Method != exchange.method || request.URL.String() != exchange.url { + return nil, fmt.Errorf( + "%w: response %d expected %s %s, got %s %s", + errClientHTTPReplayMismatch, + t.cursor, + exchange.method, + exchange.url, + request.Method, + request.URL.String(), + ) + } + t.cursor++ + return replayClientHTTPResponse(request, exchange.response), nil + } + if t.pending != nil { + if request.Method != t.pending.Method || request.URL.String() != t.pending.URL { + return nil, fmt.Errorf("%w: a different request arrived while waiting for the client", errClientHTTPReplayMismatch) + } + return nil, ErrClientHTTPRequestPending + } + if err = validateClientHTTPRequest(request, body); err != nil { + return nil, err + } + + t.nextID++ + t.pending = &ClientHTTPRequest{ + ID: "client-http-" + strconv.FormatUint(t.nextID, 10), + Method: request.Method, + URL: request.URL.String(), + Referrer: request.Header.Get("referer"), + Headers: clientHTTPFetchHeaders(request.Header), + CookieHeader: request.Header.Get("cookie"), + Body: bytes.Clone(body), + } + return nil, ErrClientHTTPRequestPending +} + +func readClientHTTPRequestBody(request *http.Request) ([]byte, error) { + if request.Body == nil { + return nil, nil + } + body, err := io.ReadAll(io.LimitReader(request.Body, clientHTTPMaxRequestBodySize+1)) + if err != nil { + return nil, fmt.Errorf("read client HTTP request body: %w", err) + } + request.Body = io.NopCloser(bytes.NewReader(body)) + if len(body) > clientHTTPMaxRequestBodySize { + return nil, fmt.Errorf("client HTTP request body is too large") + } + return body, nil +} + +func validateClientHTTPRequest(request *http.Request, body []byte) error { + if request.Method != http.MethodGet && request.Method != http.MethodPost { + return fmt.Errorf("client HTTP method %s is not allowed", request.Method) + } + if !isAllowedClientHTTPURL(request.URL) { + return fmt.Errorf("client HTTP URL is not allowed") + } + if len(body) > clientHTTPMaxRequestBodySize { + return fmt.Errorf("client HTTP request body is too large") + } + return nil +} + +func isAllowedClientHTTPURL(parsed *url.URL) bool { + if parsed == nil || parsed.Scheme != "https" || parsed.User != nil || parsed.Port() != "" { + return false + } + switch strings.ToLower(parsed.Hostname()) { + case "x.com", "api.x.com", "twitter.com", "abs.twimg.com": + return true + default: + return false + } +} + +func clientHTTPFetchHeaders(headers http.Header) map[string]string { + out := make(map[string]string) + for key, values := range headers { + key = strings.ToLower(strings.TrimSpace(key)) + if key == "" || isClientManagedHeader(key) { + continue + } + value := strings.TrimSpace(strings.Join(values, ", ")) + if value == "" || strings.ContainsAny(value, "\r\n") { + continue + } + out[key] = value + } + return out +} + +func isClientManagedHeader(name string) bool { + switch name { + case "accept-encoding", "connection", "content-length", "cookie", "expect", + "host", "keep-alive", "te", "trailer", "transfer-encoding", "upgrade", "via": + return true + } + return strings.HasPrefix(name, "proxy-") +} + +func replayClientHTTPResponse(request *http.Request, response ClientHTTPResponse) *http.Response { + statusText := http.StatusText(response.Status) + status := strconv.Itoa(response.Status) + if statusText != "" { + status += " " + statusText + } + finalRequest := request + if response.FinalURL != "" && response.FinalURL != request.URL.String() { + if finalURL, err := url.Parse(response.FinalURL); err == nil { + cloned := request.Clone(request.Context()) + cloned.URL = finalURL + finalRequest = cloned + } + } + return &http.Response{ + Status: status, + StatusCode: response.Status, + Header: response.Headers.Clone(), + Body: io.NopCloser(bytes.NewReader(response.Body)), + ContentLength: int64(len(response.Body)), + Request: finalRequest, + } +} + +func (c *Client) EnableClientHTTP() *ClientHTTPTransport { + if c.clientHTTPTransport != nil { + return c.clientHTTPTransport + } + transport := newClientHTTPTransport() + transport.baseTransport = c.HTTP.Transport + transport.baseCheckRedirect = c.HTTP.CheckRedirect + c.HTTP.Transport = transport + c.HTTP.CheckRedirect = nil + c.clientHTTPTransport = transport + return transport +} + +func (c *Client) DisableClientHTTP() { + if c.clientHTTPTransport == nil { + return + } + transport := c.clientHTTPTransport + transport.ResetOperation() + c.HTTP.Transport = transport.baseTransport + c.HTTP.CheckRedirect = transport.baseCheckRedirect + c.clientHTTPTransport = nil +} + +func (c *Client) IsClientHTTPEnabled() bool { + return c.clientHTTPTransport != nil +} diff --git a/pkg/twittermeow/client_http_test.go b/pkg/twittermeow/client_http_test.go new file mode 100644 index 00000000..c3ab131f --- /dev/null +++ b/pkg/twittermeow/client_http_test.go @@ -0,0 +1,213 @@ +package twittermeow + +import ( + "context" + "errors" + "io" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/rs/zerolog" + + "go.mau.fi/mautrix-twitter/pkg/twittermeow/cookies" +) + +func TestClientHTTPTransportCapturesAndReplaysRequest(t *testing.T) { + transport := newClientHTTPTransport() + if err := transport.BeginOperation("credentials"); err != nil { + t.Fatal(err) + } + + body := "$castle_token=" + url.QueryEscape("castle-from-webview") + "&password=secret" + request, err := http.NewRequest(http.MethodPost, "https://x.com/i/jfapi/onboarding/web/actions/begin_login", strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + request.Header.Set("authorization", "Bearer public") + request.Header.Set("cookie", "auth_token=secret") + request.Header.Set("origin", "https://x.com") + request.Header.Set("referer", "https://x.com/i/jf/onboarding/web?mode=login") + request.Header.Set("sec-fetch-site", "same-origin") + request.Header.Set("user-agent", "bridge user agent") + + _, err = transport.RoundTrip(request) + if !errors.Is(err, ErrClientHTTPRequestPending) { + t.Fatalf("RoundTrip() error = %v, want pending", err) + } + pending := transport.PendingRequest() + if pending == nil || pending.Method != http.MethodPost || pending.URL != request.URL.String() { + t.Fatalf("PendingRequest() = %#v", pending) + } + if pending.Referrer != request.Header.Get("referer") { + t.Fatalf("Referrer = %q", pending.Referrer) + } + if pending.CookieHeader != "auth_token=secret" { + t.Fatalf("CookieHeader = %q", pending.CookieHeader) + } + if pending.Headers["authorization"] != "Bearer public" { + t.Fatalf("authorization header missing: %#v", pending.Headers) + } + for _, forwarded := range []string{"origin", "referer", "sec-fetch-site", "user-agent"} { + if _, ok := pending.Headers[forwarded]; !ok { + t.Fatalf("end-to-end header %q was not forwarded", forwarded) + } + } + for _, forbidden := range []string{"cookie"} { + if _, ok := pending.Headers[forbidden]; ok { + t.Fatalf("session-managed header %q was forwarded", forbidden) + } + } + + err = transport.SubmitResponse(ClientHTTPResponse{ + RequestID: pending.ID, + Status: http.StatusOK, + Headers: http.Header{"Content-Type": {"application/octet-stream"}}, + Body: []byte("response body"), + FinalURL: request.URL.String(), + }) + if err != nil { + t.Fatal(err) + } + if err = transport.BeginOperation("credentials"); err != nil { + t.Fatal(err) + } + replayRequest := request.Clone(context.Background()) + replayRequest.Body = io.NopCloser(strings.NewReader(body)) + response, err := transport.RoundTrip(replayRequest) + if err != nil { + t.Fatal(err) + } + replayedBody, err := io.ReadAll(response.Body) + if err != nil { + t.Fatal(err) + } + if response.StatusCode != http.StatusOK || string(replayedBody) != "response body" { + t.Fatalf("replayed response = %d %q", response.StatusCode, replayedBody) + } + if err = transport.EndOperation(); err != nil { + t.Fatal(err) + } +} + +func TestClientHTTPTransportRejectsUnsafeRequestsAndResponses(t *testing.T) { + tests := []struct { + name string + method string + url string + }{ + {name: "plain HTTP", method: http.MethodGet, url: "http://x.com/login"}, + {name: "credentials in URL", method: http.MethodGet, url: "https://user:pass@x.com/login"}, + {name: "unknown host", method: http.MethodGet, url: "https://example.com/login"}, + {name: "explicit port", method: http.MethodGet, url: "https://x.com:443/login"}, + {name: "unsupported method", method: http.MethodDelete, url: "https://x.com/login"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + transport := newClientHTTPTransport() + if err := transport.BeginOperation("test"); err != nil { + t.Fatal(err) + } + request, err := http.NewRequest(test.method, test.url, nil) + if err != nil { + t.Fatal(err) + } + if _, err = transport.RoundTrip(request); err == nil || errors.Is(err, ErrClientHTTPRequestPending) { + t.Fatalf("RoundTrip() error = %v, want safety rejection", err) + } + }) + } + + transport := newClientHTTPTransport() + if err := transport.BeginOperation("test"); err != nil { + t.Fatal(err) + } + request, _ := http.NewRequest(http.MethodGet, "https://x.com/login", nil) + _, _ = transport.RoundTrip(request) + pending := transport.PendingRequest() + for _, response := range []ClientHTTPResponse{ + {RequestID: "wrong", Status: http.StatusOK}, + {RequestID: pending.ID, Status: 99}, + {RequestID: pending.ID, Status: http.StatusOK, FinalURL: "https://example.com/"}, + {RequestID: pending.ID, Status: http.StatusOK, Body: make([]byte, ClientHTTPMaxResponseBodySize+1)}, + } { + if err := transport.SubmitResponse(response); err == nil { + t.Fatalf("SubmitResponse(%#v) succeeded, want rejection", response) + } + } +} + +func TestClientHTTPTransportOperationAndReplayGuards(t *testing.T) { + transport := newClientHTTPTransport() + if err := transport.BeginOperation("first"); err != nil { + t.Fatal(err) + } + if err := transport.BeginOperation("second"); !errors.Is(err, errClientHTTPOperationChanged) { + t.Fatalf("BeginOperation() error = %v", err) + } + request, _ := http.NewRequest(http.MethodGet, "https://x.com/login", nil) + _, _ = transport.RoundTrip(request) + if err := transport.EndOperation(); !errors.Is(err, ErrClientHTTPRequestPending) { + t.Fatalf("EndOperation() error = %v", err) + } + pending := transport.PendingRequest() + if err := transport.SubmitResponse(ClientHTTPResponse{RequestID: pending.ID, Status: http.StatusOK}); err != nil { + t.Fatal(err) + } + if err := transport.BeginOperation("first"); err != nil { + t.Fatal(err) + } + different, _ := http.NewRequest(http.MethodGet, "https://x.com/messages", nil) + if _, err := transport.RoundTrip(different); !errors.Is(err, errClientHTTPReplayMismatch) { + t.Fatalf("RoundTrip() error = %v", err) + } +} + +func TestClientHTTPModeUsesNativeCastleTokensAndRestoresTransport(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + originalTransport := client.HTTP.Transport + transport := client.EnableClientHTTP() + if transport == nil || !client.IsClientHTTPEnabled() || client.HTTP.Transport != transport { + t.Fatal("client HTTP mode was not enabled") + } + form := url.Values{} + if err := client.addJetfuelCastleTokenToForm(form); !errors.Is(err, ErrJetfuelCastleTokenRequired) { + t.Fatalf("addJetfuelCastleTokenToForm() error = %v, want Castle token request", err) + } + client.SetNextJetfuelCastleTokens([]string{"castle-from-webview"}) + if err := client.addJetfuelCastleTokenToForm(form); err != nil { + t.Fatal(err) + } + if got := form.Get("$castle_token"); got != "castle-from-webview" { + t.Fatalf("$castle_token = %q", got) + } + client.DisableClientHTTP() + if client.IsClientHTTPEnabled() || client.HTTP.Transport != originalTransport { + t.Fatal("client HTTP mode did not restore the original transport") + } +} + +func TestMakeRequestReturnsClientHTTPPendingWithoutRetry(t *testing.T) { + client := NewClient(cookies.NewCookies(nil), nil, zerolog.Nop()) + transport := client.EnableClientHTTP() + if err := transport.BeginOperation("test"); err != nil { + t.Fatal(err) + } + started := time.Now() + _, _, err := client.MakeRequest( + context.Background(), + "https://x.com/login", + http.MethodGet, + http.Header{}, + nil, + "", + ) + if !errors.Is(err, ErrClientHTTPRequestPending) { + t.Fatalf("MakeRequest() error = %v", err) + } + if elapsed := time.Since(started); elapsed > time.Second { + t.Fatalf("MakeRequest() took %s, likely retried pending request", elapsed) + } +} diff --git a/pkg/twittermeow/http.go b/pkg/twittermeow/http.go index 71a0af95..8af1a16c 100644 --- a/pkg/twittermeow/http.go +++ b/pkg/twittermeow/http.go @@ -27,12 +27,14 @@ var ( ) func (c *Client) MakeRequest(ctx context.Context, url string, method string, headers http.Header, payload []byte, contentType types.ContentType) (*http.Response, []byte, error) { - log := zerolog.Ctx(ctx).With(). + logContext := zerolog.Ctx(ctx).With(). Str("url", url). Str("method", method). - Str("function", "MakeRequest"). - Str("request_body", string(payload)). - Logger() + Str("function", "MakeRequest") + if !c.IsClientHTTPEnabled() { + logContext = logContext.Str("request_body", string(payload)) + } + log := logContext.Logger() var attempts int for { attempts++ @@ -49,6 +51,8 @@ func (c *Client) MakeRequest(ctx context.Context, url string, method string, hea Dur("duration", dur). Msg("Request successful") return resp, respDat, nil + } else if errors.Is(err, ErrClientHTTPRequestPending) { + return resp, respDat, err } else if resp != nil && resp.StatusCode >= 400 && resp.StatusCode < 500 { log.Error(). Err(err). @@ -63,11 +67,13 @@ func (c *Client) MakeRequest(ctx context.Context, url string, method string, hea return nil, nil, fmt.Errorf("%w: %w", ErrMaxRetriesReached, err) } else if errors.Is(err, ErrRedirectAttempted) { location := resp.Header.Get("Location") - c.Logger.Err(err). + redirectLog := c.Logger.Err(err). Str("location", location). - Dur("duration", dur). - Str("request_body", string(payload)). - Msg("Redirect attempted") + Dur("duration", dur) + if !c.IsClientHTTPEnabled() { + redirectLog = redirectLog.Str("request_body", string(payload)) + } + redirectLog.Msg("Redirect attempted") return resp, nil, err } else if ctx.Err() != nil { return resp, nil, ctx.Err() @@ -109,10 +115,13 @@ func (c *Client) makeRequestDirect(ctx context.Context, url string, method strin if err != nil { return nil, nil, fmt.Errorf("%w: %w", ErrResponseReadFailed, err) } - c.Logger.Trace(). - Int("status_code", response.StatusCode). - Str("response_body", string(responseBody)). - Msg("Raw HTTP response") + responseLog := c.Logger.Trace().Int("status_code", response.StatusCode) + if c.IsClientHTTPEnabled() { + responseLog = responseLog.Int("response_bytes", len(responseBody)) + } else { + responseLog = responseLog.Str("response_body", string(responseBody)) + } + responseLog.Msg("Raw HTTP response") if response.StatusCode >= 400 { var respErr TwitterErrors if json.Unmarshal(responseBody, &respErr) == nil { diff --git a/pkg/twittermeow/login_jetfuel.go b/pkg/twittermeow/login_jetfuel.go index f5a7498e..ba9bf1f7 100644 --- a/pkg/twittermeow/login_jetfuel.go +++ b/pkg/twittermeow/login_jetfuel.go @@ -70,6 +70,9 @@ func (wls *WebLoginSession) startJetfuel(ctx context.Context) (*WebLoginResult, return nil, fmt.Errorf("failed to load X login page: %w", err) } if _, err := wls.client.jetfuelGet(ctx, endpoints.JETFUEL_LANDING_PATH); err != nil { + if errors.Is(err, ErrClientHTTPRequestPending) { + return nil, err + } wls.client.Logger.Debug().Err(err).Msg("Jetfuel login landing preflight failed") } body, err := wls.client.jetfuelGet(ctx, endpoints.JETFUEL_LOGIN_PATH) @@ -99,6 +102,9 @@ func (wls *WebLoginSession) submitJetfuelIdentifier(ctx context.Context, identif } if shouldSendJetfuelViewerContextEvent() { if err := wls.client.sendJetfuelViewerContextEvent(ctx); err != nil { + if errors.Is(err, ErrClientHTTPRequestPending) { + return nil, err + } wls.client.Logger.Debug().Err(err).Msg("Jetfuel viewer-context preflight failed") } } diff --git a/pkg/twittermeow/login_web.go b/pkg/twittermeow/login_web.go index bc65520c..63b7f33a 100644 --- a/pkg/twittermeow/login_web.go +++ b/pkg/twittermeow/login_web.go @@ -139,6 +139,8 @@ func (wls *WebLoginSession) UsesJetfuel() bool { func (wls *WebLoginSession) Start(ctx context.Context) (*WebLoginResult, error) { if result, err := wls.startJetfuel(ctx); err == nil { return result, nil + } else if errors.Is(err, ErrClientHTTPRequestPending) || errors.Is(err, ErrJetfuelCastleTokenRequired) { + return nil, err } else { wls.client.Logger.Warn().Err(err).Msg("Jetfuel login start failed, falling back to OCF login") } From b73bb0758543e763cc1e49c167d4993b57dcc926 Mon Sep 17 00:00:00 2001 From: highesttt Date: Fri, 24 Jul 2026 16:56:02 -0400 Subject: [PATCH 18/20] fix: go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index dfa1a30f..d1d6b1cf 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/tidwall/gjson v1.19.0 go.mau.fi/util v0.9.10 golang.org/x/crypto v0.53.0 + golang.org/x/net v0.56.0 golang.org/x/sync v0.21.0 gopkg.in/yaml.v3 v3.0.1 maunium.net/go/mautrix v0.28.2-0.20260714135907-a79bdf25be1f @@ -54,7 +55,6 @@ require ( go.mau.fi/zeroconfig v0.2.0 // indirect golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.56.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/text v0.38.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect From 8b848cc8f89f9a10d78534577936aef8a8ebbb74 Mon Sep 17 00:00:00 2001 From: highesttt Date: Mon, 27 Jul 2026 14:01:13 -0400 Subject: [PATCH 19/20] feat: use typed client HTTP login steps --- go.mod | 24 +-- go.sum | 48 +++--- pkg/connector/client_http_step.go | 193 +-------------------- pkg/connector/login.go | 3 - pkg/connector/login_client_http.go | 29 ++-- pkg/connector/login_client_http_test.go | 219 ++++++++---------------- pkg/twittermeow/client_http.go | 56 +++--- pkg/twittermeow/client_http_test.go | 57 ++++-- 8 files changed, 200 insertions(+), 429 deletions(-) diff --git a/go.mod b/go.mod index d1d6b1cf..e92cf55b 100644 --- a/go.mod +++ b/go.mod @@ -19,12 +19,12 @@ require ( github.com/rs/zerolog v1.35.1 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.19.0 - go.mau.fi/util v0.9.10 - golang.org/x/crypto v0.53.0 - golang.org/x/net v0.56.0 - golang.org/x/sync v0.21.0 + go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 + golang.org/x/crypto v0.54.0 + golang.org/x/net v0.57.0 + golang.org/x/sync v0.22.0 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.28.2-0.20260714135907-a79bdf25be1f + maunium.net/go/mautrix v0.29.1-0.20260727174626-385cdf9a0830 ) require ( @@ -39,8 +39,8 @@ require ( github.com/lib/pq v1.12.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.45 // indirect - github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect + github.com/mattn/go-sqlite3 v1.14.48 // indirect + github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.56.0 // indirect @@ -51,12 +51,12 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/yuin/goldmark v1.8.2 // indirect + github.com/yuin/goldmark v1.8.4 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect - golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect maunium.net/go/mauflag v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index e453debb..d2fe274f 100644 --- a/go.sum +++ b/go.sum @@ -44,12 +44,12 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk= -github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= +github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/openziti/secretstream v0.1.51 h1:j/rMfIzBNqZD5a1EKV8J4Z5QeaoSK56s/zxvvB08eSA= github.com/openziti/secretstream v0.1.51/go.mod h1:YapZv2c/SyZyohn6Q0MJkl8SUuBbs9Q6XWSiboBc1jA= -github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM= -github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca h1:GHSUVE4yOgX4E7kTRzpxCPbCOYkd3Kj8Dgdod30OI1E= +github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= @@ -83,10 +83,10 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= -github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg= -go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A= +github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= +github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 h1:lsvBEY8MJfYdV61YbwikiQvb0Al/onbmLW5wfl/0tag= +go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -97,17 +97,17 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g= +golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -117,8 +117,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -126,8 +126,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -140,8 +140,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -160,8 +160,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -182,5 +182,5 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.28.2-0.20260714135907-a79bdf25be1f h1:WVy4D2xz/fFPcJp9f+XajfEBaPdBwN5p5UJ63W8760Q= -maunium.net/go/mautrix v0.28.2-0.20260714135907-a79bdf25be1f/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY= +maunium.net/go/mautrix v0.29.1-0.20260727174626-385cdf9a0830 h1:UfpEZ0hUz6A+rYuDEVMLOZGHVkuoufNhiAZKyKx3D/k= +maunium.net/go/mautrix v0.29.1-0.20260727174626-385cdf9a0830/go.mod h1:PeLuIih5jnbwb2xKNNi0Te7AtINEQE2Uv3p6iG3RV80= diff --git a/pkg/connector/client_http_step.go b/pkg/connector/client_http_step.go index 8d895115..67b01215 100644 --- a/pkg/connector/client_http_step.go +++ b/pkg/connector/client_http_step.go @@ -1,205 +1,28 @@ package connector import ( - "encoding/base64" - "encoding/json" + "bytes" "fmt" - "net/http" - "strconv" - "strings" - "golang.org/x/net/http/httpguts" "maunium.net/go/mautrix/bridgev2" "go.mau.fi/mautrix-twitter/pkg/twittermeow" ) -const ( - clientHTTPConfigMarkerPrefix = "/*__BEEPER_CLIENT_HTTP_V1__:" - clientHTTPConfigMarkerSuffix = "__*/" - clientHTTPDispatchURL = "https://x.com/robots.txt#beeper-client-http" - loginFieldClientHTTPRequestID = "client_http_request_id" - loginFieldClientHTTPStatus = "client_http_status" - loginFieldClientHTTPResponseURL = "client_http_response_url" - loginFieldClientHTTPResponseHeaders = "client_http_response_headers" - loginFieldClientHTTPResponseBody = "client_http_response_body" - loginFieldClientHTTPError = "client_http_error" - clientHTTPEncodedFieldPrefix = "v1." -) - -var clientHTTPCookieNames = []string{ - "__cf_bm", - "__cuid", - "auth_token", - "att", - "ct0", - "dtab_local", - "gt", - "guest_id", - "guest_id_ads", - "guest_id_marketing", - "kdt", - "lang", - "night_mode", - "personalization_id", - "twid", -} - -type clientHTTPConfig struct { - RequestID string `json:"requestID"` - Method string `json:"method"` - RequestURL string `json:"requestURL"` - Referrer string `json:"referrer,omitempty"` - Headers map[string]string `json:"headers"` - CookieHeader string `json:"cookieHeader,omitempty"` - Body string `json:"body"` -} - func makeClientHTTPRequestStep(request *twittermeow.ClientHTTPRequest) (*bridgev2.LoginStep, error) { if request == nil { return nil, fmt.Errorf("client HTTP request is missing") } return &bridgev2.LoginStep{ - Type: bridgev2.LoginStepTypeCookies, + Type: bridgev2.LoginStepTypeClientHTTP, StepID: LoginStepIDClientHTTPRequest, Instructions: "Sending an X sign-in request from this device. Client HTTP is in beta.", - CookiesParams: &bridgev2.LoginCookiesParams{ - URL: clientHTTPDispatchURL, - ExtractJS: clientHTTPRequestEnvelope(request), - WaitForURLPattern: `^https://x\.com/robots\.txt#beeper-client-http$`, - Fields: clientHTTPCookieFields(), - Hidden: true, + ClientHTTPParams: &bridgev2.LoginClientHTTPParams{ + RequestID: request.ID, + Method: request.Method, + URL: request.URL, + Headers: request.Headers.Clone(), + Body: bytes.Clone(request.Body), }, }, nil } - -func clientHTTPRequestEnvelope(request *twittermeow.ClientHTTPRequest) string { - config, err := json.Marshal(clientHTTPConfig{ - RequestID: request.ID, - Method: request.Method, - RequestURL: request.URL, - Referrer: request.Referrer, - Headers: request.Headers, - CookieHeader: request.CookieHeader, - Body: base64.RawURLEncoding.EncodeToString(request.Body), - }) - if err != nil { - panic(fmt.Errorf("marshal client HTTP request config: %w", err)) - } - - encodedConfig := base64.RawURLEncoding.EncodeToString(config) - requestID, _ := json.Marshal(request.ID) - return clientHTTPConfigMarkerPrefix + encodedConfig + clientHTTPConfigMarkerSuffix + "\n" + - "(async () => ({client_http_request_id:" + string(requestID) + - ",client_http_error:\"Client HTTP requires a supported Beeper client\"}))()" -} - -func clientHTTPCookieFields() []bridgev2.LoginCookieField { - fields := []bridgev2.LoginCookieField{ - clientHTTPSpecialField(loginFieldClientHTTPRequestID, true, `^client-http-[0-9]+$`), - clientHTTPSpecialField(loginFieldClientHTTPStatus, false, `^[1-5][0-9]{2}$`), - clientHTTPSpecialField(loginFieldClientHTTPResponseURL, false, `^https://[^\r\n]{1,4096}$`), - clientHTTPSpecialField(loginFieldClientHTTPResponseHeaders, false, `^v1\.[A-Za-z0-9_-]*$`), - clientHTTPSpecialField(loginFieldClientHTTPResponseBody, false, `^v1\.[A-Za-z0-9_-]*$`), - clientHTTPSpecialField(loginFieldClientHTTPError, false, `^[^\r\n]{1,300}$`), - } - for _, field := range browserHeaderFields { - fields = append(fields, bridgev2.LoginCookieField{ - ID: field.ID, - Required: field.Required, - Pattern: field.Pattern, - Sources: []bridgev2.LoginCookieFieldSource{{ - Type: bridgev2.LoginCookieTypeSpecial, - Name: field.ID, - }}, - }) - } - for _, name := range clientHTTPCookieNames { - fields = append(fields, bridgev2.LoginCookieField{ - ID: name, - Required: false, - Sources: []bridgev2.LoginCookieFieldSource{ - {Type: bridgev2.LoginCookieTypeCookie, Name: name, CookieDomain: "x.com"}, - {Type: bridgev2.LoginCookieTypeCookie, Name: name, CookieDomain: ".x.com"}, - }, - }) - } - return fields -} - -func clientHTTPSpecialField(id string, required bool, pattern string) bridgev2.LoginCookieField { - return bridgev2.LoginCookieField{ - ID: id, - Required: required, - Pattern: pattern, - Sources: []bridgev2.LoginCookieFieldSource{{ - Type: bridgev2.LoginCookieTypeSpecial, - Name: id, - }}, - } -} - -func decodeClientHTTPResponse(input map[string]string) (twittermeow.ClientHTTPResponse, error) { - requestID := strings.TrimSpace(input[loginFieldClientHTTPRequestID]) - if requestID == "" { - return twittermeow.ClientHTTPResponse{}, fmt.Errorf("client HTTP response request ID is missing") - } - status, err := strconv.Atoi(strings.TrimSpace(input[loginFieldClientHTTPStatus])) - if err != nil { - return twittermeow.ClientHTTPResponse{}, fmt.Errorf("client HTTP response status is invalid") - } - body, err := decodeClientHTTPEncodedField( - input[loginFieldClientHTTPResponseBody], - twittermeow.ClientHTTPMaxResponseBodySize, - ) - if err != nil { - return twittermeow.ClientHTTPResponse{}, fmt.Errorf("client HTTP response body is invalid: %w", err) - } - headersJSON, err := decodeClientHTTPEncodedField( - input[loginFieldClientHTTPResponseHeaders], - twittermeow.ClientHTTPMaxResponseHeadersSize, - ) - if err != nil { - return twittermeow.ClientHTTPResponse{}, fmt.Errorf("client HTTP response headers are invalid: %w", err) - } - var headerValues map[string]string - if err = json.Unmarshal(headersJSON, &headerValues); err != nil { - return twittermeow.ClientHTTPResponse{}, fmt.Errorf("client HTTP response headers are invalid: %w", err) - } - headers := make(http.Header, len(headerValues)) - for name, value := range headerValues { - if !httpguts.ValidHeaderFieldName(name) || !httpguts.ValidHeaderFieldValue(value) { - return twittermeow.ClientHTTPResponse{}, fmt.Errorf("client HTTP response header is invalid") - } - headers.Set(name, value) - } - return twittermeow.ClientHTTPResponse{ - RequestID: requestID, - Status: status, - Headers: headers, - Body: body, - FinalURL: strings.TrimSpace(input[loginFieldClientHTTPResponseURL]), - }, nil -} - -func decodeClientHTTPEncodedField(value string, maxDecodedSize int) ([]byte, error) { - value = strings.TrimSpace(value) - if !strings.HasPrefix(value, clientHTTPEncodedFieldPrefix) { - return nil, fmt.Errorf("unsupported encoding") - } - encoded := strings.TrimPrefix(value, clientHTTPEncodedFieldPrefix) - if base64.RawURLEncoding.DecodedLen(len(encoded)) > maxDecodedSize { - return nil, fmt.Errorf("encoded value is too large") - } - return base64.RawURLEncoding.DecodeString(encoded) -} - -func clientHTTPCookies(input map[string]string) map[string]string { - out := make(map[string]string) - for _, name := range clientHTTPCookieNames { - if value := strings.TrimSpace(input[name]); value != "" { - out[name] = value - } - } - return out -} diff --git a/pkg/connector/login.go b/pkg/connector/login.go index f451d327..b7661a9e 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -605,9 +605,6 @@ func (t *TwitterLogin) StartWithOverride(ctx context.Context, override *bridgev2 } func (t *TwitterLogin) SubmitCookies(ctx context.Context, cookies map[string]string) (*bridgev2.LoginStep, error) { - if t.isWaitingForClientHTTPRequest() { - return t.submitClientHTTPInput(ctx, cookies) - } if t.isWaitingForWebLoginCastleToken() { return t.submitWebCastleTokenInput(ctx, cookies) } diff --git a/pkg/connector/login_client_http.go b/pkg/connector/login_client_http.go index 19514ef3..ee636698 100644 --- a/pkg/connector/login_client_http.go +++ b/pkg/connector/login_client_http.go @@ -3,7 +3,6 @@ package connector import ( "context" "errors" - "strings" "maunium.net/go/mautrix/bridgev2" @@ -15,6 +14,8 @@ const ( clientHTTPMaxImmediateActions = 16 ) +var _ bridgev2.LoginProcessClientHTTP = (*TwitterLogin)(nil) + func (t *TwitterLogin) isWaitingForClientHTTPRequest() bool { return t.useClientHTTPLogin && t.webLogin != nil && @@ -24,26 +25,30 @@ func (t *TwitterLogin) isWaitingForClientHTTPRequest() bool { t.clientHTTPStage != "" } -func (t *TwitterLogin) submitClientHTTPInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { +func (t *TwitterLogin) SubmitClientHTTPResponse( + ctx context.Context, + response *bridgev2.LoginClientHTTPResponse, +) (*bridgev2.LoginStep, error) { if !t.isWaitingForClientHTTPRequest() { t.stopClientHTTPLogin() return makeCredentialsStep("The X login session expired. Enter your X login details again."), nil } - if strings.TrimSpace(input[loginFieldClientHTTPError]) != "" { - return t.failClientHTTPRequest("The request did not complete on this device. Please try again.") - } - response, err := decodeClientHTTPResponse(input) - if err != nil { + if response == nil { return t.failClientHTTPRequest("The client returned an invalid HTTP response. Please try again.") } - if !t.webLogin.Client().SetBrowserHeaders(browserHeadersFromInput(input)) { - return t.failClientHTTPRequest("The client did not return a valid browser fingerprint. Please try again.") + if response.Error != "" { + return t.failClientHTTPRequest("The request did not complete on this device. Please try again.") } - if err = t.clientHTTPTransport.SubmitResponse(response); err != nil { + err := t.clientHTTPTransport.SubmitResponse(twittermeow.ClientHTTPResponse{ + RequestID: response.RequestID, + Status: response.StatusCode, + Headers: response.Headers, + Body: response.Body, + FinalURL: response.FinalURL, + }) + if err != nil { return t.failClientHTTPRequest("The client HTTP response did not match this login. Please try again.") } - t.browserHeaders = t.webLogin.Client().GetBrowserHeaders() - t.webLogin.Client().SetCookies(clientHTTPCookies(input)) return t.continueClientHTTPLogin(ctx) } diff --git a/pkg/connector/login_client_http_test.go b/pkg/connector/login_client_http_test.go index 81c25df6..7d2f6966 100644 --- a/pkg/connector/login_client_http_test.go +++ b/pkg/connector/login_client_http_test.go @@ -1,9 +1,8 @@ package connector import ( + "bytes" "context" - "encoding/base64" - "encoding/json" "net/http" "net/url" "strings" @@ -36,8 +35,9 @@ func TestClientHTTPLoginUsesCastleWebviewThenCapturesLocalRequest(t *testing.T) if err != nil { t.Fatal(err) } - if step.StepID != LoginStepIDClientHTTPRequest || !step.CookiesParams.Hidden { - t.Fatalf("first step = %#v, want hidden client HTTP request", step) + if step.Type != bridgev2.LoginStepTypeClientHTTP || step.StepID != LoginStepIDClientHTTPRequest || + step.ClientHTTPParams == nil { + t.Fatalf("first step = %#v, want typed client HTTP request", step) } for range 20 { @@ -60,9 +60,7 @@ func TestClientHTTPLoginUsesCastleWebviewThenCapturesLocalRequest(t *testing.T) body = "{}" } input := clientHTTPTestResponseInput(pending, body) - input["guest_id"] = "v1%3A123456789" - input["gt"] = "123456789" - step, err = login.submitClientHTTPInput(context.Background(), input) + step, err = login.SubmitClientHTTPResponse(context.Background(), input) if err != nil { t.Fatalf("submit response for %s: %v", pending.URL, err) } @@ -92,21 +90,16 @@ func TestClientHTTPLoginUsesCastleWebviewThenCapturesLocalRequest(t *testing.T) if form.Get("$castle_token") != clientHTTPTestCastleToken(1) { t.Fatalf("Castle form value = %q", form.Get("$castle_token")) } - if strings.Contains(step.CookiesParams.ExtractJS, "test-password") { - t.Fatal("client HTTP request envelope contains the plaintext password") + if step.ClientHTTPParams == nil || !bytes.Equal(step.ClientHTTPParams.Body, pending.Body) { + t.Fatal("client HTTP step did not carry the exact request body") } - if strings.Contains(step.CookiesParams.ExtractJS, "createRequestToken") || - strings.Contains(step.CookiesParams.ExtractJS, "fetch(") { - t.Fatal("client HTTP request envelope contains WebView networking code") + if step.CookiesParams != nil { + t.Fatal("client HTTP request unexpectedly used a cookie/WebView step") } input := clientHTTPTestResponseInput(pending, "login accepted") - input["guest_id"] = "v1%3A123456789" - input["gt"] = "123456789" - input["auth_token"] = "authenticated-cookie" - input["ct0"] = "csrf-cookie" - input["twid"] = "u%3D123456789" - step, err = login.submitClientHTTPInput(context.Background(), input) + clientHTTPTestAuthenticate(input) + step, err = login.SubmitClientHTTPResponse(context.Background(), input) if err != nil { t.Fatal(err) } @@ -135,12 +128,8 @@ func TestClientHTTPLoginUsesCastleWebviewThenCapturesLocalRequest(t *testing.T) body = clientHTTPTestMainPage } input = clientHTTPTestResponseInput(pending, body) - input["guest_id"] = "v1%3A123456789" - input["gt"] = "123456789" - input["auth_token"] = "authenticated-cookie" - input["ct0"] = "csrf-cookie" - input["twid"] = "u%3D123456789" - step, err = login.submitClientHTTPInput(context.Background(), input) + clientHTTPTestAuthenticate(input) + step, err = login.SubmitClientHTTPResponse(context.Background(), input) if err != nil { t.Fatalf("submit post-login response for %s: %v", pending.URL, err) } @@ -193,9 +182,7 @@ func TestClientHTTPLoginPreservesVerificationCodeDuringPostLoginRequests(t *test body = endpoints.JETFUEL_BEGIN_LOGIN_PATH + "\x00username_or_email" } input := clientHTTPTestResponseInput(pending, body) - input["guest_id"] = "v1%3A123456789" - input["gt"] = "123456789" - step, err = login.submitClientHTTPInput(ctx, input) + step, err = login.SubmitClientHTTPResponse(ctx, input) if err != nil { t.Fatalf("submit bootstrap response for %s: %v", pending.URL, err) } @@ -219,9 +206,7 @@ func TestClientHTTPLoginPreservesVerificationCodeDuringPostLoginRequests(t *test "prelude_dispatch_id\x00abcdefab-1234-1234-1234-abcdefabcdef\x00" + "begin_two_factor_auth" input := clientHTTPTestResponseInput(pending, chooserBody) - input["guest_id"] = "v1%3A123456789" - input["gt"] = "123456789" - step, err = login.submitClientHTTPInput(ctx, input) + step, err = login.SubmitClientHTTPResponse(ctx, input) if err != nil { t.Fatal(err) } @@ -243,9 +228,7 @@ func TestClientHTTPLoginPreservesVerificationCodeDuringPostLoginRequests(t *test endpoints.JETFUEL_FINISH_TWO_FACTOR_AUTH_PATH + "\x00" + "session_token\x0012345678-1234-1234-1234-123456789abc" input = clientHTTPTestResponseInput(pending, challengeBody) - input["guest_id"] = "v1%3A123456789" - input["gt"] = "123456789" - step, err = login.submitClientHTTPInput(ctx, input) + step, err = login.SubmitClientHTTPResponse(ctx, input) if err != nil { t.Fatal(err) } @@ -265,12 +248,8 @@ func TestClientHTTPLoginPreservesVerificationCodeDuringPostLoginRequests(t *test t.Fatalf("verification submit step = %#v pending = %#v", step, pending) } input = clientHTTPTestResponseInput(pending, "/home") - input["guest_id"] = "v1%3A123456789" - input["gt"] = "123456789" - input["auth_token"] = "authenticated-cookie" - input["ct0"] = "csrf-cookie" - input["twid"] = "u%3D123456789" - step, err = login.submitClientHTTPInput(ctx, input) + clientHTTPTestAuthenticate(input) + step, err = login.SubmitClientHTTPResponse(ctx, input) if err != nil { t.Fatal(err) } @@ -302,12 +281,8 @@ func TestClientHTTPLoginPreservesVerificationCodeDuringPostLoginRequests(t *test body = clientHTTPTestMainPage } input = clientHTTPTestResponseInput(pending, body) - input["guest_id"] = "v1%3A123456789" - input["gt"] = "123456789" - input["auth_token"] = "authenticated-cookie" - input["ct0"] = "csrf-cookie" - input["twid"] = "u%3D123456789" - step, err = login.submitClientHTTPInput(ctx, input) + clientHTTPTestAuthenticate(input) + step, err = login.SubmitClientHTTPResponse(ctx, input) if err != nil { t.Fatalf("submit post-login response for %s: %v", pending.URL, err) } @@ -342,8 +317,9 @@ func TestClientHTTPRequestFailureStopsAutomaticRetry(t *testing.T) { t.Fatalf("initial step = %#v", step) } - step, err = login.submitClientHTTPInput(context.Background(), map[string]string{ - loginFieldClientHTTPError: "Failed to fetch", + step, err = login.SubmitClientHTTPResponse(context.Background(), &bridgev2.LoginClientHTTPResponse{ + RequestID: step.ClientHTTPParams.RequestID, + Error: "Failed to fetch", }) if err != nil { t.Fatal(err) @@ -362,107 +338,69 @@ func TestClientHTTPRequestFailureStopsAutomaticRetry(t *testing.T) { func TestClientHTTPRequestStepContract(t *testing.T) { request := &twittermeow.ClientHTTPRequest{ - ID: "client-http-7", - Method: http.MethodPost, - URL: "https://x.com/i/jfapi/onboarding/web/actions/begin_login", - Referrer: endpoints.JETFUEL_LOGIN_REFERER_URL, - Headers: map[string]string{"authorization": "Bearer public"}, - CookieHeader: "guest_id=v1%3A123; ct0=csrf", - Body: []byte("password=secret"), + ID: "client-http-7", + Method: http.MethodPost, + URL: "https://x.com/i/jfapi/onboarding/web/actions/begin_login", + Headers: http.Header{ + "Authorization": {"Bearer public"}, + "Cookie": {"guest_id=v1%3A123; ct0=csrf"}, + "Referer": {endpoints.JETFUEL_LOGIN_REFERER_URL}, + "X-Multi": {"one", "two"}, + }, + Body: []byte("password=secret"), } step, err := makeClientHTTPRequestStep(request) if err != nil { t.Fatal(err) } - if step.Type != bridgev2.LoginStepTypeCookies || step.StepID != LoginStepIDClientHTTPRequest || - step.CookiesParams == nil || !step.CookiesParams.Hidden { + if step.Type != bridgev2.LoginStepTypeClientHTTP || step.StepID != LoginStepIDClientHTTPRequest || + step.ClientHTTPParams == nil || step.CookiesParams != nil { t.Fatalf("step = %#v", step) } - if step.CookiesParams.URL != clientHTTPDispatchURL || - !strings.Contains(step.CookiesParams.WaitForURLPattern, "beeper-client-http") { - t.Fatalf("client HTTP step does not carry its client-dispatch marker: %#v", step.CookiesParams) - } - fields := make(map[string]bridgev2.LoginCookieField) - for _, field := range step.CookiesParams.Fields { - fields[field.ID] = field - } - for _, fieldID := range []string{ - loginFieldClientHTTPRequestID, - loginFieldClientHTTPStatus, - loginFieldClientHTTPResponseHeaders, - loginFieldClientHTTPResponseBody, - loginFieldBrowserUserAgent, - "auth_token", - "ct0", - } { - if _, ok := fields[fieldID]; !ok { - t.Fatalf("client HTTP fields missing %q", fieldID) - } - } - if !fields[loginFieldClientHTTPRequestID].Required || !fields[loginFieldBrowserUserAgent].Required { - t.Fatal("request ID and browser user agent must be required") + params := step.ClientHTTPParams + if params.RequestID != request.ID || params.Method != request.Method || params.URL != request.URL { + t.Fatalf("client HTTP params = %#v", params) } - if !strings.HasPrefix(step.CookiesParams.ExtractJS, clientHTTPConfigMarkerPrefix) { - t.Fatal("client HTTP request envelope is missing its protocol marker") + if !bytes.Equal(params.Body, request.Body) { + t.Fatalf("client HTTP body = %q", params.Body) } - if strings.Contains(step.CookiesParams.ExtractJS, "password=secret") { - t.Fatal("client HTTP request envelope contains plaintext request body") + if got := params.Headers.Values("X-Multi"); len(got) != 2 || got[0] != "one" || got[1] != "two" { + t.Fatalf("multi-value headers = %#v", params.Headers) } - if strings.Contains(step.CookiesParams.ExtractJS, "fetch(") || - strings.Contains(step.CookiesParams.ExtractJS, "createRequestToken") { - t.Fatal("client HTTP request envelope contains WebView networking code") + if params.Headers.Get("Cookie") != request.Headers.Get("Cookie") || + params.Headers.Get("Referer") != request.Headers.Get("Referer") { + t.Fatalf("cookies or referrer were not preserved: %#v", params.Headers) } - config := decodeClientHTTPTestConfig(t, step.CookiesParams.ExtractJS) - if config.RequestID != request.ID || config.RequestURL != request.URL || - config.CookieHeader != request.CookieHeader || - config.Body != base64.RawURLEncoding.EncodeToString(request.Body) { - t.Fatalf("decoded client HTTP config = %#v", config) + request.Headers.Set("Cookie", "changed") + request.Body[0] = 'X' + if params.Headers.Get("Cookie") == "changed" || params.Body[0] == 'X' { + t.Fatal("client HTTP step did not clone mutable request data") } } -func TestClientHTTPResponseDecodeRejectsMalformedData(t *testing.T) { - valid := clientHTTPTestResponseInput(&twittermeow.ClientHTTPRequest{ - ID: "client-http-1", - URL: "https://x.com/login", - }, "response") - if _, err := decodeClientHTTPResponse(valid); err != nil { - t.Fatalf("valid response rejected: %v", err) - } - for _, mutate := range []func(map[string]string){ - func(input map[string]string) { delete(input, loginFieldClientHTTPRequestID) }, - func(input map[string]string) { input[loginFieldClientHTTPStatus] = "not-a-status" }, - func(input map[string]string) { input[loginFieldClientHTTPResponseBody] = "bad" }, - func(input map[string]string) { input[loginFieldClientHTTPResponseHeaders] = "v1.invalid" }, - func(input map[string]string) { - headers, _ := json.Marshal(map[string]string{"bad header": "value"}) - input[loginFieldClientHTTPResponseHeaders] = clientHTTPEncodedFieldPrefix + - base64.RawURLEncoding.EncodeToString(headers) +func clientHTTPTestResponseInput( + request *twittermeow.ClientHTTPRequest, + body string, +) *bridgev2.LoginClientHTTPResponse { + return &bridgev2.LoginClientHTTPResponse{ + RequestID: request.ID, + StatusCode: http.StatusOK, + FinalURL: request.URL, + Headers: http.Header{ + "Content-Type": {"application/octet-stream"}, + "Set-Cookie": { + "guest_id=v1%3A123456789; Domain=.x.com; Path=/", + "gt=123456789; Domain=.x.com; Path=/", + }, }, - } { - input := make(map[string]string, len(valid)) - for key, value := range valid { - input[key] = value - } - mutate(input) - if _, err := decodeClientHTTPResponse(input); err == nil { - t.Fatalf("malformed response accepted: %#v", input) - } + Body: []byte(body), } } -func clientHTTPTestResponseInput(request *twittermeow.ClientHTTPRequest, body string) map[string]string { - headers, _ := json.Marshal(map[string]string{"content-type": "application/octet-stream"}) - return map[string]string{ - loginFieldClientHTTPRequestID: request.ID, - loginFieldClientHTTPStatus: "200", - loginFieldClientHTTPResponseURL: request.URL, - loginFieldClientHTTPResponseHeaders: clientHTTPEncodedFieldPrefix + base64.RawURLEncoding.EncodeToString(headers), - loginFieldClientHTTPResponseBody: clientHTTPEncodedFieldPrefix + base64.RawURLEncoding.EncodeToString([]byte(body)), - loginFieldBrowserUserAgent: "Mozilla/5.0 test client", - loginFieldBrowserSecCHUA: `"Chromium";v="150"`, - loginFieldBrowserPlatform: `"Windows"`, - loginFieldBrowserMobile: "?0", - } +func clientHTTPTestAuthenticate(response *bridgev2.LoginClientHTTPResponse) { + response.Headers.Add("Set-Cookie", "auth_token=authenticated-cookie; Domain=.x.com; Path=/") + response.Headers.Add("Set-Cookie", "ct0=csrf-cookie; Domain=.x.com; Path=/") + response.Headers.Add("Set-Cookie", "twid=u%3D123456789; Domain=.x.com; Path=/") } func clientHTTPTestCastleToken(index int) string { @@ -478,24 +416,3 @@ func clientHTTPTestCastleInput() map[string]string { } return input } - -func decodeClientHTTPTestConfig(t *testing.T, extractJS string) clientHTTPConfig { - t.Helper() - if !strings.HasPrefix(extractJS, clientHTTPConfigMarkerPrefix) { - t.Fatal("client HTTP config marker prefix is missing") - } - end := strings.Index(extractJS, clientHTTPConfigMarkerSuffix) - if end < 0 { - t.Fatal("client HTTP config marker suffix is missing") - } - encoded := strings.TrimPrefix(extractJS[:end], clientHTTPConfigMarkerPrefix) - raw, err := base64.RawURLEncoding.DecodeString(encoded) - if err != nil { - t.Fatalf("decode client HTTP config: %v", err) - } - var config clientHTTPConfig - if err = json.Unmarshal(raw, &config); err != nil { - t.Fatalf("unmarshal client HTTP config: %v", err) - } - return config -} diff --git a/pkg/twittermeow/client_http.go b/pkg/twittermeow/client_http.go index cb3d4ffd..86dae031 100644 --- a/pkg/twittermeow/client_http.go +++ b/pkg/twittermeow/client_http.go @@ -10,6 +10,8 @@ import ( "strconv" "strings" "sync" + + "golang.org/x/net/http/httpguts" ) const ( @@ -26,13 +28,11 @@ var ( ) type ClientHTTPRequest struct { - ID string - Method string - URL string - Referrer string - Headers map[string]string - CookieHeader string - Body []byte + ID string + Method string + URL string + Headers http.Header + Body []byte } func (req *ClientHTTPRequest) clone() *ClientHTTPRequest { @@ -40,10 +40,7 @@ func (req *ClientHTTPRequest) clone() *ClientHTTPRequest { return nil } cloned := *req - cloned.Headers = make(map[string]string, len(req.Headers)) - for key, value := range req.Headers { - cloned.Headers[key] = value - } + cloned.Headers = req.Headers.Clone() cloned.Body = bytes.Clone(req.Body) return &cloned } @@ -154,8 +151,14 @@ func (t *ClientHTTPTransport) SubmitResponse(response ClientHTTPResponse) error } headerSize := 0 for name, values := range response.Headers { + if !httpguts.ValidHeaderFieldName(name) { + return fmt.Errorf("client HTTP response header name is invalid") + } headerSize += len(name) for _, value := range values { + if !httpguts.ValidHeaderFieldValue(value) { + return fmt.Errorf("client HTTP response header value is invalid") + } headerSize += len(value) } } @@ -218,13 +221,11 @@ func (t *ClientHTTPTransport) RoundTrip(request *http.Request) (*http.Response, t.nextID++ t.pending = &ClientHTTPRequest{ - ID: "client-http-" + strconv.FormatUint(t.nextID, 10), - Method: request.Method, - URL: request.URL.String(), - Referrer: request.Header.Get("referer"), - Headers: clientHTTPFetchHeaders(request.Header), - CookieHeader: request.Header.Get("cookie"), - Body: bytes.Clone(body), + ID: "client-http-" + strconv.FormatUint(t.nextID, 10), + Method: request.Method, + URL: request.URL.String(), + Headers: clientHTTPFetchHeaders(request.Header), + Body: bytes.Clone(body), } return nil, ErrClientHTTPRequestPending } @@ -269,25 +270,26 @@ func isAllowedClientHTTPURL(parsed *url.URL) bool { } } -func clientHTTPFetchHeaders(headers http.Header) map[string]string { - out := make(map[string]string) +func clientHTTPFetchHeaders(headers http.Header) http.Header { + out := make(http.Header) for key, values := range headers { - key = strings.ToLower(strings.TrimSpace(key)) - if key == "" || isClientManagedHeader(key) { + key = strings.TrimSpace(key) + if key == "" || isClientManagedHeader(strings.ToLower(key)) { continue } - value := strings.TrimSpace(strings.Join(values, ", ")) - if value == "" || strings.ContainsAny(value, "\r\n") { - continue + for _, value := range values { + if value == "" || strings.ContainsAny(value, "\r\n") { + continue + } + out.Add(key, value) } - out[key] = value } return out } func isClientManagedHeader(name string) bool { switch name { - case "accept-encoding", "connection", "content-length", "cookie", "expect", + case "accept-encoding", "connection", "content-length", "expect", "host", "keep-alive", "te", "trailer", "transfer-encoding", "upgrade", "via": return true } diff --git a/pkg/twittermeow/client_http_test.go b/pkg/twittermeow/client_http_test.go index c3ab131f..28c17b2b 100644 --- a/pkg/twittermeow/client_http_test.go +++ b/pkg/twittermeow/client_http_test.go @@ -32,6 +32,8 @@ func TestClientHTTPTransportCapturesAndReplaysRequest(t *testing.T) { request.Header.Set("referer", "https://x.com/i/jf/onboarding/web?mode=login") request.Header.Set("sec-fetch-site", "same-origin") request.Header.Set("user-agent", "bridge user agent") + request.Header.Add("x-multi", "one") + request.Header.Add("x-multi", "two") _, err = transport.RoundTrip(request) if !errors.Is(err, ErrClientHTTPRequestPending) { @@ -41,32 +43,30 @@ func TestClientHTTPTransportCapturesAndReplaysRequest(t *testing.T) { if pending == nil || pending.Method != http.MethodPost || pending.URL != request.URL.String() { t.Fatalf("PendingRequest() = %#v", pending) } - if pending.Referrer != request.Header.Get("referer") { - t.Fatalf("Referrer = %q", pending.Referrer) + if pending.Headers.Get("cookie") != "auth_token=secret" { + t.Fatalf("Cookie header = %q", pending.Headers.Get("cookie")) } - if pending.CookieHeader != "auth_token=secret" { - t.Fatalf("CookieHeader = %q", pending.CookieHeader) - } - if pending.Headers["authorization"] != "Bearer public" { + if pending.Headers.Get("authorization") != "Bearer public" { t.Fatalf("authorization header missing: %#v", pending.Headers) } - for _, forwarded := range []string{"origin", "referer", "sec-fetch-site", "user-agent"} { - if _, ok := pending.Headers[forwarded]; !ok { + for _, forwarded := range []string{"cookie", "origin", "referer", "sec-fetch-site", "user-agent"} { + if pending.Headers.Get(forwarded) == "" { t.Fatalf("end-to-end header %q was not forwarded", forwarded) } } - for _, forbidden := range []string{"cookie"} { - if _, ok := pending.Headers[forbidden]; ok { - t.Fatalf("session-managed header %q was forwarded", forbidden) - } + if got := pending.Headers.Values("x-multi"); len(got) != 2 || got[0] != "one" || got[1] != "two" { + t.Fatalf("multi-value header was not preserved: %#v", pending.Headers) } err = transport.SubmitResponse(ClientHTTPResponse{ RequestID: pending.ID, Status: http.StatusOK, - Headers: http.Header{"Content-Type": {"application/octet-stream"}}, - Body: []byte("response body"), - FinalURL: request.URL.String(), + Headers: http.Header{ + "Content-Type": {"application/octet-stream"}, + "Set-Cookie": {"first=1; Path=/", "second=2; Path=/"}, + }, + Body: []byte("response body"), + FinalURL: "https://x.com/home", }) if err != nil { t.Fatal(err) @@ -87,6 +87,12 @@ func TestClientHTTPTransportCapturesAndReplaysRequest(t *testing.T) { if response.StatusCode != http.StatusOK || string(replayedBody) != "response body" { t.Fatalf("replayed response = %d %q", response.StatusCode, replayedBody) } + if response.Request.URL.String() != "https://x.com/home" { + t.Fatalf("replayed final URL = %q", response.Request.URL) + } + if got := response.Header.Values("Set-Cookie"); len(got) != 2 { + t.Fatalf("replayed Set-Cookie headers = %#v", got) + } if err = transport.EndOperation(); err != nil { t.Fatal(err) } @@ -132,11 +138,29 @@ func TestClientHTTPTransportRejectsUnsafeRequestsAndResponses(t *testing.T) { {RequestID: pending.ID, Status: 99}, {RequestID: pending.ID, Status: http.StatusOK, FinalURL: "https://example.com/"}, {RequestID: pending.ID, Status: http.StatusOK, Body: make([]byte, ClientHTTPMaxResponseBodySize+1)}, + {RequestID: pending.ID, Status: http.StatusOK, Headers: http.Header{"Bad Header": {"value"}}}, + {RequestID: pending.ID, Status: http.StatusOK, Headers: http.Header{"X-Test": {"bad\rvalue"}}}, + {RequestID: pending.ID, Status: http.StatusOK, Headers: http.Header{ + "X-Test": {strings.Repeat("a", ClientHTTPMaxResponseHeadersSize+1)}, + }}, } { if err := transport.SubmitResponse(response); err == nil { t.Fatalf("SubmitResponse(%#v) succeeded, want rejection", response) } } + + largeBodyTransport := newClientHTTPTransport() + if err := largeBodyTransport.BeginOperation("test"); err != nil { + t.Fatal(err) + } + largeRequest, _ := http.NewRequest( + http.MethodPost, + "https://x.com/login", + strings.NewReader(strings.Repeat("a", clientHTTPMaxRequestBodySize+1)), + ) + if _, err := largeBodyTransport.RoundTrip(largeRequest); err == nil { + t.Fatal("oversized client HTTP request body was accepted") + } } func TestClientHTTPTransportOperationAndReplayGuards(t *testing.T) { @@ -156,6 +180,9 @@ func TestClientHTTPTransportOperationAndReplayGuards(t *testing.T) { if err := transport.SubmitResponse(ClientHTTPResponse{RequestID: pending.ID, Status: http.StatusOK}); err != nil { t.Fatal(err) } + if err := transport.SubmitResponse(ClientHTTPResponse{RequestID: pending.ID, Status: http.StatusOK}); err == nil { + t.Fatal("duplicate client HTTP response was accepted") + } if err := transport.BeginOperation("first"); err != nil { t.Fatal(err) } From 7a11ce6591914344afd8fb95b4a80829e6cda1d9 Mon Sep 17 00:00:00 2001 From: highesttt Date: Mon, 27 Jul 2026 14:19:05 -0400 Subject: [PATCH 20/20] fix: use compatible client HTTP protocol revision --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index e92cf55b..cfc1b512 100644 --- a/go.mod +++ b/go.mod @@ -19,12 +19,12 @@ require ( github.com/rs/zerolog v1.35.1 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.19.0 - go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 + go.mau.fi/util v0.9.11 golang.org/x/crypto v0.54.0 golang.org/x/net v0.57.0 golang.org/x/sync v0.22.0 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.29.1-0.20260727174626-385cdf9a0830 + maunium.net/go/mautrix v0.29.1-0.20260727181456-bf941f51d27a ) require ( diff --git a/go.sum b/go.sum index d2fe274f..fdd68b00 100644 --- a/go.sum +++ b/go.sum @@ -85,8 +85,8 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 h1:lsvBEY8MJfYdV61YbwikiQvb0Al/onbmLW5wfl/0tag= -go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= +go.mau.fi/util v0.9.11 h1:Cus1Lu/t7d3OG6VF4aYWvlUUS0Q4O1/lcpPNJZ0jsw0= +go.mau.fi/util v0.9.11/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -182,5 +182,5 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.29.1-0.20260727174626-385cdf9a0830 h1:UfpEZ0hUz6A+rYuDEVMLOZGHVkuoufNhiAZKyKx3D/k= -maunium.net/go/mautrix v0.29.1-0.20260727174626-385cdf9a0830/go.mod h1:PeLuIih5jnbwb2xKNNi0Te7AtINEQE2Uv3p6iG3RV80= +maunium.net/go/mautrix v0.29.1-0.20260727181456-bf941f51d27a h1:/aHmbVZi5ZEy+T5sNZLeAxlH53cbQjAVF9DLR94rdbg= +maunium.net/go/mautrix v0.29.1-0.20260727181456-bf941f51d27a/go.mod h1:LynuVr8N9nWsE1N4WAE+vItRACDB1pt9M3gN4SIBpeY=