package auth import ( "context" "encoding/hex" "math/rand" "strconv" "strings" "testing" "github.com/go-faster/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mau.fi/mautrix-telegram/pkg/gotd/bin" "go.mau.fi/mautrix-telegram/pkg/gotd/tg" "go.mau.fi/mautrix-telegram/pkg/gotd/tgerr" "go.mau.fi/mautrix-telegram/pkg/gotd/tgmock" ) func getHex(t testing.TB, in string) []byte { res, err := hex.DecodeString(in) if err != nil { t.Fatal("failed to get hex", err) } return res } func TestClient_AuthSignIn(t *testing.T) { const ( phone = "123123" code = "1010" password = "secret" codeHash = "hash" ) ctx := context.Background() testUser := &tg.User{ID: 1} invoker := tgmock.Invoker(func(body bin.Encoder) (bin.Encoder, error) { switch req := body.(type) { case *tg.UsersGetUsersRequest: return nil, &tgerr.Error{ Code: 401, Message: "AUTH_KEY_UNREGISTERED", Type: "AUTH_KEY_UNREGISTERED", } case *tg.AuthSendCodeRequest: settings := tg.CodeSettings{} settings.SetCurrentNumber(true) assert.Equal(t, &tg.AuthSendCodeRequest{ PhoneNumber: phone, APIHash: testAppHash, APIID: testAppID, Settings: settings, }, req) return &tg.AuthSentCode{ Type: &tg.AuthSentCodeTypeApp{}, PhoneCodeHash: codeHash, }, nil case *tg.AuthSignInRequest: assert.Equal(t, &tg.AuthSignInRequest{ PhoneNumber: phone, PhoneCodeHash: codeHash, PhoneCode: code, }, req) return nil, tgerr.New(401, "SESSION_PASSWORD_NEEDED") case *tg.AccountGetPasswordRequest: algo := &tg.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow{ Salt1: getHex(t, "4D11FB6BEC38F9D2546BB0F61E4F1C99A1BC0DB8F0D5F35B1291B37B213123D7ED48F3C6794D495B"), Salt2: getHex(t, "A1B181AAFE88188680AE32860D60BB01"), G: 3, P: getHex(t, "C71CAEB9C6B1C9048E6C522F70F13F73980D40238E3E21C14934D037563D930F"+ "48198A0AA7C14058229493D22530F4DBFA336F6E0AC925139543AED44CCE7C37"+ "20FD51F69458705AC68CD4FE6B6B13ABDC9746512969328454F18FAF8C595F64"+ "2477FE96BB2A941D5BCD1D4AC8CC49880708FA9B378E3C4F3A9060BEE67CF9A4"+ "A4A695811051907E162753B56B0F6B410DBA74D8A84B2A14B3144E0EF1284754"+ "FD17ED950D5965B4B9DD46582DB1178D169C6BC465B0D6FF9CA3928FEF5B9AE4"+ "E418FC15E83EBEA0F87FA9FF5EED70050DED2849F47BF959D956850CE929851F"+ "0D8115F635B105EE2E4E15D04B2454BF6F4FADF034B10403119CD8E3B92FCC5B"), } pwd := &tg.AccountPassword{ NewAlgo: algo, NewSecureAlgo: &tg.SecurePasswordKdfAlgoPBKDF2HMACSHA512iter100000{}, } pwd.SetCurrentAlgo(algo) return pwd, nil case *tg.AuthCheckPasswordRequest: // TODO(ernado): Check actual secure remote password here. switch pwd := req.Password.(type) { case *tg.InputCheckPasswordSRP: assert.NotEmpty(t, pwd.A) assert.NotEmpty(t, pwd.M1) assert.NotEqual(t, pwd.SRPID, 0) default: t.Errorf("unexpectd pwd type %T", pwd) } return &tg.AuthAuthorization{ User: testUser, }, nil } return nil, errors.New("unexpected") }) t.Run("Manual", func(t *testing.T) { // 1. Request code from server to device. client := testClient(invoker) sentCode, err := client.SendCode(ctx, phone, SendCodeOptions{CurrentNumber: true}) require.NoError(t, err) h := sentCode.(*tg.AuthSentCode).PhoneCodeHash require.Equal(t, codeHash, h) // 2. Send code from device to server. // Server is responding with 2FA password prompt. _, signInErr := client.SignIn(ctx, phone, code, h) require.ErrorIs(t, signInErr, ErrPasswordAuthNeeded) // 3. Provide 2FA password. result, err := client.Password(ctx, password) require.NoError(t, err) require.Equal(t, testUser, result.User) }) flow := NewFlow( Constant(phone, password, CodeAuthenticatorFunc( func(ctx context.Context, _ *tg.AuthSentCode) (string, error) { return code, nil }, )), SendCodeOptions{CurrentNumber: true}, ) t.Run("AuthFlow", func(t *testing.T) { require.NoError(t, flow.Run(ctx, testClient(invoker))) }) t.Run("IfNecessary", func(t *testing.T) { require.NoError(t, testClient(invoker).IfNecessary(ctx, flow)) }) } func TestClientTestAuth(t *testing.T) { const ( codeHash = "hash" dcID = 2 ) ctx := context.Background() invoker := tgmock.Invoker(func(body bin.Encoder) (bin.Encoder, error) { switch req := body.(type) { case *tg.AuthSendCodeRequest: assert.Equal(t, &tg.AuthSendCodeRequest{ PhoneNumber: req.PhoneNumber, APIHash: testAppHash, APIID: testAppID, Settings: tg.CodeSettings{}, }, req) return &tg.AuthSentCode{ Type: &tg.AuthSentCodeTypeApp{ Length: 6, }, PhoneCodeHash: codeHash, }, nil case *tg.AuthSignInRequest: if !strings.HasPrefix(req.PhoneNumber, "99966") { t.Fatalf("unexpected phone number %s", req.PhoneNumber) } dcPart := req.PhoneNumber[5:6] assert.Equal(t, strconv.Itoa(dcID), dcPart, "dc part of phone number") assert.Equal(t, &tg.AuthSignInRequest{ PhoneNumber: req.PhoneNumber, PhoneCodeHash: codeHash, PhoneCode: strings.Repeat(dcPart, 6), }, req) return &tg.AuthAuthorization{ User: &tg.User{ID: 1}, }, nil } return nil, errors.New("unexpected") }) require.NoError(t, NewFlow( Test(rand.New(rand.NewSource(1)), dcID), SendCodeOptions{}, ).Run(ctx, testClient(invoker))) } func TestClientTestSignUp(t *testing.T) { const ( dcID = 2 codeHash = "hash" tosID = "foo" ) ctx := context.Background() invoker := tgmock.Invoker(func(body bin.Encoder) (bin.Encoder, error) { switch req := body.(type) { case *tg.AuthSendCodeRequest: assert.Equal(t, &tg.AuthSendCodeRequest{ PhoneNumber: req.PhoneNumber, APIHash: testAppHash, APIID: testAppID, Settings: tg.CodeSettings{}, }, req) return &tg.AuthSentCode{ Type: &tg.AuthSentCodeTypeApp{ Length: 6, }, PhoneCodeHash: codeHash, }, nil case *tg.AuthSignUpRequest: assert.Equal(t, &tg.AuthSignUpRequest{ PhoneNumber: req.PhoneNumber, PhoneCodeHash: codeHash, FirstName: "Test", LastName: "User", }, req) return &tg.AuthAuthorization{ User: &tg.User{ID: 1}, }, nil case *tg.HelpAcceptTermsOfServiceRequest: return &tg.BoolTrue{}, nil case *tg.AuthSignInRequest: if !strings.HasPrefix(req.PhoneNumber, "99966") { t.Fatalf("unexpected phone number %s", req.PhoneNumber) } dcPart := req.PhoneNumber[5:6] assert.Equal(t, strconv.Itoa(dcID), dcPart, "dc part of phone number") assert.Equal(t, &tg.AuthSignInRequest{ PhoneNumber: req.PhoneNumber, PhoneCodeHash: codeHash, PhoneCode: strings.Repeat(dcPart, 6), }, req) res := &tg.AuthAuthorizationSignUpRequired{} res.SetTermsOfService(tg.HelpTermsOfService{ID: tg.DataJSON{Data: tosID}}) return res, nil } return nil, errors.New("unexpected") }) require.NoError(t, NewFlow( Test(rand.New(rand.NewSource(1)), dcID), SendCodeOptions{}, ).Run(ctx, testClient(invoker))) } func TestClient_AcceptTOS(t *testing.T) { ctx := context.Background() mockTest(func(a *require.Assertions, mock *tgmock.Mock, client *Client) { mock.Expect().ThenUnregistered() a.Error(client.AcceptTOS(ctx, tg.DataJSON{ Data: `{"data":"data"}`, })) mock.Expect().ThenTrue() a.NoError(client.AcceptTOS(ctx, tg.DataJSON{ Data: `{"data":"data"}`, })) })(t) }