move gotd fork into repo. (#111)
- update to latest telegram layer - remove some references to fields in tg.Entities that don't exist in the schema - originally added here: https://github.com/beeper/td/commit/820929062a2ba0104397bc01235ab58a9cff780e - referenced here - https://github.com/mautrix/telegramgo/commit/124f0967ed195b5a380c9bd02e170ada9710dde3 - https://github.com/mautrix/telegramgo/commit/4205047aab2e0639217148b5d125bfaab668bd8e
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
// Package auth provides authentication on top of tg.Client.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
|
||||
)
|
||||
|
||||
// IsKeyUnregistered reports whether err is AUTH_KEY_UNREGISTERED error.
|
||||
//
|
||||
// Deprecated: use IsUnauthorized.
|
||||
func IsKeyUnregistered(err error) bool {
|
||||
return tgerr.Is(err, "AUTH_KEY_UNREGISTERED")
|
||||
}
|
||||
|
||||
// IsUnauthorized reports whether err is any 401 UNAUTHORIZED or is a 406
|
||||
// NOT_ACCEPTABLE with AUTH_KEY_DUPLICATED.
|
||||
//
|
||||
// https://core.telegram.org/api/errors#401-unauthorized
|
||||
// https://core.telegram.org/api/errors#406-not-acceptable
|
||||
func IsUnauthorized(err error) bool {
|
||||
return tgerr.IsCode(err, 401) ||
|
||||
(tgerr.IsCode(err, 406) && tgerr.Is(err, "AUTH_KEY_DUPLICATED"))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tgmock"
|
||||
)
|
||||
|
||||
const (
|
||||
testAppID = 1
|
||||
testAppHash = "hash"
|
||||
)
|
||||
|
||||
func testClient(invoker tg.Invoker) *Client {
|
||||
return &Client{
|
||||
api: tg.NewClient(invoker),
|
||||
rand: rand.Reader,
|
||||
appID: testAppID,
|
||||
appHash: testAppHash,
|
||||
}
|
||||
}
|
||||
|
||||
func mockClient(t *testing.T) (*tgmock.Mock, *Client) {
|
||||
mock := tgmock.New(t)
|
||||
return mock, NewClient(tg.NewClient(mock), testutil.ZeroRand{}, testAppID, testAppHash)
|
||||
}
|
||||
|
||||
func mockTest(cb func(
|
||||
a *require.Assertions,
|
||||
mock *tgmock.Mock,
|
||||
client *Client,
|
||||
)) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
a := require.New(t)
|
||||
m, client := mockClient(t)
|
||||
|
||||
cb(a, m, client)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// Bot performs bot authentication request.
|
||||
func (c *Client) Bot(ctx context.Context, token string) (*tg.AuthAuthorization, error) {
|
||||
auth, err := c.api.AuthImportBotAuthorization(ctx, &tg.AuthImportBotAuthorizationRequest{
|
||||
APIID: c.appID,
|
||||
APIHash: c.appHash,
|
||||
BotAuthToken: token,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := checkResult(auth)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "check")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tgmock"
|
||||
)
|
||||
|
||||
func TestClient_AuthBot(t *testing.T) {
|
||||
const token = "12345:token"
|
||||
|
||||
t.Run("AuthAuthorization", func(t *testing.T) {
|
||||
mock := tgmock.New(t)
|
||||
|
||||
testUser := &tg.User{}
|
||||
testUser.SetBot(true)
|
||||
|
||||
mock.ExpectCall(&tg.AuthImportBotAuthorizationRequest{
|
||||
BotAuthToken: token,
|
||||
APIID: testAppID,
|
||||
APIHash: testAppHash,
|
||||
}).ThenResult(&tg.AuthAuthorization{User: testUser})
|
||||
|
||||
result, err := testClient(mock).Bot(context.Background(), token)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testUser, result.User)
|
||||
})
|
||||
|
||||
t.Run("AuthAuthorizationSignUpRequired", func(t *testing.T) {
|
||||
mock := tgmock.New(t)
|
||||
|
||||
mock.ExpectCall(&tg.AuthImportBotAuthorizationRequest{
|
||||
BotAuthToken: token,
|
||||
APIID: testAppID,
|
||||
APIHash: testAppHash,
|
||||
}).ThenResult(&tg.AuthAuthorizationSignUpRequired{})
|
||||
|
||||
result, err := testClient(mock).Bot(context.Background(), token)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, result)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// Client implements Telegram authentication.
|
||||
type Client struct {
|
||||
api *tg.Client
|
||||
rand io.Reader
|
||||
appID int
|
||||
appHash string
|
||||
}
|
||||
|
||||
// NewClient initializes and returns Telegram authentication client.
|
||||
func NewClient(
|
||||
api *tg.Client,
|
||||
rand io.Reader,
|
||||
appID int,
|
||||
appHash string,
|
||||
) *Client {
|
||||
return &Client{
|
||||
api: api,
|
||||
rand: rand,
|
||||
appID: appID,
|
||||
appHash: appHash,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// NewFlow initializes new authentication flow.
|
||||
func NewFlow(auth UserAuthenticator, opt SendCodeOptions) Flow {
|
||||
return Flow{
|
||||
Auth: auth,
|
||||
Options: opt,
|
||||
}
|
||||
}
|
||||
|
||||
// Flow simplifies boilerplate for authentication flow.
|
||||
type Flow struct {
|
||||
Auth UserAuthenticator
|
||||
Options SendCodeOptions
|
||||
}
|
||||
|
||||
func (f Flow) handleSignUp(ctx context.Context, client FlowClient, phone, hash string, s *SignUpRequired) error {
|
||||
if err := f.Auth.AcceptTermsOfService(ctx, s.TermsOfService); err != nil {
|
||||
return errors.Wrap(err, "confirm TOS")
|
||||
}
|
||||
info, err := f.Auth.SignUp(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "sign up info not provided")
|
||||
}
|
||||
if _, err := client.SignUp(ctx, SignUp{
|
||||
PhoneNumber: phone,
|
||||
PhoneCodeHash: hash,
|
||||
FirstName: info.FirstName,
|
||||
LastName: info.LastName,
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "sign up")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run starts authentication flow on client.
|
||||
func (f Flow) Run(ctx context.Context, client FlowClient) error {
|
||||
if f.Auth == nil {
|
||||
return errors.New("no UserAuthenticator provided")
|
||||
}
|
||||
|
||||
phone, err := f.Auth.Phone(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get phone")
|
||||
}
|
||||
|
||||
sentCode, err := client.SendCode(ctx, phone, f.Options)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "send code")
|
||||
}
|
||||
switch s := sentCode.(type) {
|
||||
case *tg.AuthSentCode:
|
||||
hash := s.PhoneCodeHash
|
||||
code, err := f.Auth.Code(ctx, s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get code")
|
||||
}
|
||||
|
||||
_, signInErr := client.SignIn(ctx, phone, code, hash)
|
||||
if errors.Is(signInErr, ErrPasswordAuthNeeded) {
|
||||
password, err := f.Auth.Password(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get password")
|
||||
}
|
||||
if _, err := client.Password(ctx, password); err != nil {
|
||||
return errors.Wrap(err, "sign in with password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var signUpRequired *SignUpRequired
|
||||
if errors.As(signInErr, &signUpRequired) {
|
||||
return f.handleSignUp(ctx, client, phone, hash, signUpRequired)
|
||||
}
|
||||
|
||||
if signInErr != nil {
|
||||
return errors.Wrap(signInErr, "sign in")
|
||||
}
|
||||
|
||||
return nil
|
||||
case *tg.AuthSentCodeSuccess:
|
||||
switch a := s.Authorization.(type) {
|
||||
case *tg.AuthAuthorization:
|
||||
// Looks that we are already authorized.
|
||||
return nil
|
||||
case *tg.AuthAuthorizationSignUpRequired:
|
||||
if err := f.handleSignUp(ctx, client, phone, "", &SignUpRequired{
|
||||
TermsOfService: a.TermsOfService,
|
||||
}); err != nil {
|
||||
// TODO: not sure that blank hash will work here
|
||||
return errors.Wrap(err, "sign up after auth sent code success")
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.Errorf("unexpected authorization type: %T", a)
|
||||
}
|
||||
default:
|
||||
return errors.Errorf("unexpected sent code type: %T", sentCode)
|
||||
}
|
||||
}
|
||||
|
||||
// FlowClient abstracts telegram client for Flow.
|
||||
type FlowClient interface {
|
||||
SignIn(ctx context.Context, phone, code, codeHash string) (*tg.AuthAuthorization, error)
|
||||
SendCode(ctx context.Context, phone string, options SendCodeOptions) (tg.AuthSentCodeClass, error)
|
||||
Password(ctx context.Context, password string) (*tg.AuthAuthorization, error)
|
||||
SignUp(ctx context.Context, s SignUp) (*tg.AuthAuthorization, error)
|
||||
}
|
||||
|
||||
// CodeAuthenticator asks user for received authentication code.
|
||||
type CodeAuthenticator interface {
|
||||
Code(ctx context.Context, sentCode *tg.AuthSentCode) (string, error)
|
||||
}
|
||||
|
||||
// CodeAuthenticatorFunc is functional wrapper for CodeAuthenticator.
|
||||
type CodeAuthenticatorFunc func(ctx context.Context, sentCode *tg.AuthSentCode) (string, error)
|
||||
|
||||
// Code implements CodeAuthenticator interface.
|
||||
func (c CodeAuthenticatorFunc) Code(ctx context.Context, sentCode *tg.AuthSentCode) (string, error) {
|
||||
return c(ctx, sentCode)
|
||||
}
|
||||
|
||||
// UserInfo represents user info required for sign up.
|
||||
type UserInfo struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
// UserAuthenticator asks user for phone, password and received authentication code.
|
||||
type UserAuthenticator interface {
|
||||
Phone(ctx context.Context) (string, error)
|
||||
Password(ctx context.Context) (string, error)
|
||||
AcceptTermsOfService(ctx context.Context, tos tg.HelpTermsOfService) error
|
||||
SignUp(ctx context.Context) (UserInfo, error)
|
||||
CodeAuthenticator
|
||||
}
|
||||
|
||||
type noSignUp struct{}
|
||||
|
||||
func (c noSignUp) SignUp(ctx context.Context) (UserInfo, error) {
|
||||
return UserInfo{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (c noSignUp) AcceptTermsOfService(ctx context.Context, tos tg.HelpTermsOfService) error {
|
||||
return &SignUpRequired{TermsOfService: tos}
|
||||
}
|
||||
|
||||
type constantAuth struct {
|
||||
phone, password string
|
||||
CodeAuthenticator
|
||||
noSignUp
|
||||
}
|
||||
|
||||
func (c constantAuth) Phone(ctx context.Context) (string, error) {
|
||||
return c.phone, nil
|
||||
}
|
||||
|
||||
func (c constantAuth) Password(ctx context.Context) (string, error) {
|
||||
return c.password, nil
|
||||
}
|
||||
|
||||
// Constant creates UserAuthenticator with constant phone and password.
|
||||
func Constant(phone, password string, code CodeAuthenticator) UserAuthenticator {
|
||||
return constantAuth{
|
||||
phone: phone,
|
||||
password: password,
|
||||
CodeAuthenticator: code,
|
||||
}
|
||||
}
|
||||
|
||||
type envAuth struct {
|
||||
prefix string
|
||||
CodeAuthenticator
|
||||
noSignUp
|
||||
}
|
||||
|
||||
func (e envAuth) lookup(k string) (string, error) {
|
||||
env := e.prefix + k
|
||||
v, ok := os.LookupEnv(env)
|
||||
if !ok {
|
||||
return "", errors.Errorf("environment variable %q not set", env)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (e envAuth) Phone(ctx context.Context) (string, error) {
|
||||
return e.lookup("PHONE")
|
||||
}
|
||||
|
||||
func (e envAuth) Password(ctx context.Context) (string, error) {
|
||||
p, err := e.lookup("PASSWORD")
|
||||
if err != nil {
|
||||
return "", ErrPasswordNotProvided
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Env creates UserAuthenticator which gets phone and password from environment variables.
|
||||
func Env(prefix string, code CodeAuthenticator) UserAuthenticator {
|
||||
return envAuth{
|
||||
prefix: prefix,
|
||||
CodeAuthenticator: code,
|
||||
noSignUp: noSignUp{},
|
||||
}
|
||||
}
|
||||
|
||||
// ErrPasswordNotProvided means that password requested by Telegram,
|
||||
// but not provided by user.
|
||||
var ErrPasswordNotProvided = errors.New("password requested but not provided")
|
||||
|
||||
type codeOnlyAuth struct {
|
||||
phone string
|
||||
CodeAuthenticator
|
||||
noSignUp
|
||||
}
|
||||
|
||||
func (c codeOnlyAuth) Phone(ctx context.Context) (string, error) {
|
||||
return c.phone, nil
|
||||
}
|
||||
|
||||
func (c codeOnlyAuth) Password(ctx context.Context) (string, error) {
|
||||
return "", ErrPasswordNotProvided
|
||||
}
|
||||
|
||||
// CodeOnly creates UserAuthenticator with constant phone and no password.
|
||||
func CodeOnly(phone string, code CodeAuthenticator) UserAuthenticator {
|
||||
return codeOnlyAuth{
|
||||
phone: phone,
|
||||
CodeAuthenticator: code,
|
||||
}
|
||||
}
|
||||
|
||||
type testAuth struct {
|
||||
dc int
|
||||
phone string
|
||||
}
|
||||
|
||||
func (t testAuth) Phone(ctx context.Context) (string, error) { return t.phone, nil }
|
||||
func (t testAuth) Password(ctx context.Context) (string, error) { return "", ErrPasswordNotProvided }
|
||||
func (t testAuth) Code(ctx context.Context, sentCode *tg.AuthSentCode) (string, error) {
|
||||
type notFlashing interface {
|
||||
GetLength() int
|
||||
}
|
||||
|
||||
length := 5
|
||||
if sentCode != nil {
|
||||
typ, ok := sentCode.Type.(notFlashing)
|
||||
if !ok {
|
||||
return "", errors.Errorf("unexpected type: %T", sentCode.Type)
|
||||
}
|
||||
length = typ.GetLength()
|
||||
}
|
||||
|
||||
return strings.Repeat(strconv.Itoa(t.dc), length), nil
|
||||
}
|
||||
|
||||
func (t testAuth) AcceptTermsOfService(ctx context.Context, tos tg.HelpTermsOfService) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t testAuth) SignUp(ctx context.Context) (UserInfo, error) {
|
||||
return UserInfo{
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Test returns UserAuthenticator that authenticates via testing credentials.
|
||||
//
|
||||
// Can be used only with testing server. Will perform sign up if test user is
|
||||
// not registered.
|
||||
func Test(randReader io.Reader, dc int) UserAuthenticator {
|
||||
// 99966XYYYY, X = dc_id, Y = random numbers, code = X repeat 6.
|
||||
// The n value is from 0000 to 9999.
|
||||
n, err := crypto.RandInt64n(randReader, 1000)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
phone := fmt.Sprintf("99966%d%04d", dc, n)
|
||||
|
||||
return TestUser(phone, dc)
|
||||
}
|
||||
|
||||
// TestUser returns UserAuthenticator that authenticates via testing credentials.
|
||||
// Uses given phone to sign in/sign up.
|
||||
//
|
||||
// Can be used only with testing server. Will perform sign up if test user is
|
||||
// not registered.
|
||||
func TestUser(phone string, dc int) UserAuthenticator {
|
||||
return testAuth{
|
||||
dc: dc,
|
||||
phone: phone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/auth"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
func askCode(code string, err error) auth.CodeAuthenticatorFunc {
|
||||
return func(ctx context.Context, sentCode *tg.AuthSentCode) (string, error) {
|
||||
return code, err
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantAuth(t *testing.T) {
|
||||
a := require.New(t)
|
||||
authConst := auth.Constant("phone", "password", askCode("123", nil))
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := authConst.Code(ctx, nil)
|
||||
a.NoError(err)
|
||||
a.Equal("123", result)
|
||||
|
||||
result, err = authConst.Phone(ctx)
|
||||
a.NoError(err)
|
||||
a.Equal("phone", result)
|
||||
|
||||
result, err = authConst.Password(ctx)
|
||||
a.NoError(err)
|
||||
a.Equal("password", result)
|
||||
}
|
||||
|
||||
func TestCodeOnlyAuth(t *testing.T) {
|
||||
a := require.New(t)
|
||||
authCodeOnly := auth.CodeOnly("phone", askCode("123", nil))
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := authCodeOnly.Code(ctx, nil)
|
||||
a.NoError(err)
|
||||
a.Equal("123", result)
|
||||
|
||||
result, err = authCodeOnly.Phone(ctx)
|
||||
a.NoError(err)
|
||||
a.Equal("phone", result)
|
||||
|
||||
_, err = authCodeOnly.Password(ctx)
|
||||
a.ErrorIs(err, auth.ErrPasswordNotProvided)
|
||||
}
|
||||
|
||||
func TestEnvAuth(t *testing.T) {
|
||||
a := require.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
prefix := "TEST_ENV_AUTH_"
|
||||
authEnv := auth.Env(prefix, askCode("123", nil))
|
||||
|
||||
result, err := authEnv.Code(ctx, nil)
|
||||
a.NoError(err)
|
||||
a.Equal("123", result)
|
||||
|
||||
_, err = authEnv.Phone(ctx)
|
||||
a.Error(err)
|
||||
|
||||
_, err = authEnv.Password(ctx)
|
||||
a.ErrorIs(err, auth.ErrPasswordNotProvided)
|
||||
|
||||
// Set envs.
|
||||
testutil.SetEnv(t, prefix+"PHONE", "phone")
|
||||
testutil.SetEnv(t, prefix+"PASSWORD", "password")
|
||||
|
||||
result, err = authEnv.Phone(ctx)
|
||||
a.NoError(err)
|
||||
a.Equal("phone", result)
|
||||
|
||||
result, err = authEnv.Password(ctx)
|
||||
a.NoError(err)
|
||||
a.Equal("password", result)
|
||||
}
|
||||
|
||||
func TestTestAuth(t *testing.T) {
|
||||
a := require.New(t)
|
||||
ctx := context.Background()
|
||||
testAuth := auth.Test(testutil.ZeroRand{}, 2)
|
||||
|
||||
_, err := testAuth.Code(ctx, &tg.AuthSentCode{
|
||||
Type: &tg.AuthSentCodeTypeFlashCall{},
|
||||
})
|
||||
a.Error(err)
|
||||
|
||||
result, err := testAuth.Code(ctx, nil)
|
||||
a.NoError(err)
|
||||
a.Equal("22222", result)
|
||||
|
||||
result, err = testAuth.Code(ctx, &tg.AuthSentCode{
|
||||
Type: &tg.AuthSentCodeTypeApp{
|
||||
Length: 1,
|
||||
},
|
||||
})
|
||||
a.NoError(err)
|
||||
a.Equal("2", result)
|
||||
|
||||
result, err = testAuth.Phone(ctx)
|
||||
a.NoError(err)
|
||||
a.True(strings.HasPrefix(result, "999662"))
|
||||
|
||||
_, err = testAuth.Password(ctx)
|
||||
a.ErrorIs(err, auth.ErrPasswordNotProvided)
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto/srp"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// PasswordHash computes password hash to log in.
|
||||
//
|
||||
// See https://core.telegram.org/api/srp#checking-the-password-with-srp.
|
||||
func PasswordHash(
|
||||
password []byte,
|
||||
srpID int64,
|
||||
srpB, secureRandom []byte,
|
||||
alg tg.PasswordKdfAlgoClass,
|
||||
) (*tg.InputCheckPasswordSRP, error) {
|
||||
s := srp.NewSRP(crypto.DefaultRand())
|
||||
|
||||
algo, ok := alg.(*tg.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unsupported algo: %T", alg)
|
||||
}
|
||||
|
||||
a, err := s.Hash(password, srpB, secureRandom, srp.Input(*algo))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create SRP answer")
|
||||
}
|
||||
|
||||
return &tg.InputCheckPasswordSRP{
|
||||
SRPID: srpID,
|
||||
A: a.A,
|
||||
M1: a.M1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewPasswordHash computes new password hash to update password.
|
||||
//
|
||||
// Notice that NewPasswordHash mutates given alg.
|
||||
//
|
||||
// See https://core.telegram.org/api/srp#setting-a-new-2fa-password.
|
||||
func NewPasswordHash(
|
||||
password []byte,
|
||||
algo *tg.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow,
|
||||
) (hash []byte, _ error) {
|
||||
s := srp.NewSRP(crypto.DefaultRand())
|
||||
|
||||
hash, newSalt, err := s.NewHash(password, srp.Input(*algo))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create SRP answer")
|
||||
}
|
||||
algo.Salt1 = newSalt
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
var (
|
||||
emptyPassword tg.InputCheckPasswordSRPClass = &tg.InputCheckPasswordEmpty{}
|
||||
)
|
||||
|
||||
// UpdatePasswordOptions is options structure for UpdatePassword.
|
||||
type UpdatePasswordOptions struct {
|
||||
// Hint is new password hint.
|
||||
Hint string
|
||||
// Password is password callback.
|
||||
//
|
||||
// If password was requested and Password is nil, ErrPasswordNotProvided error will be returned.
|
||||
Password func(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// UpdatePassword sets new cloud password for this account.
|
||||
//
|
||||
// See https://core.telegram.org/api/srp#setting-a-new-2fa-password.
|
||||
func (c *Client) UpdatePassword(
|
||||
ctx context.Context,
|
||||
newPassword string,
|
||||
opts UpdatePasswordOptions,
|
||||
) error {
|
||||
p, err := c.api.AccountGetPassword(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get SRP parameters")
|
||||
}
|
||||
|
||||
algo, ok := p.NewAlgo.(*tg.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow)
|
||||
if !ok {
|
||||
return errors.Errorf("unsupported algo: %T", p.NewAlgo)
|
||||
}
|
||||
|
||||
newHash, err := NewPasswordHash([]byte(newPassword), algo)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "compute new password hash")
|
||||
}
|
||||
|
||||
var old = emptyPassword
|
||||
if p.HasPassword {
|
||||
if opts.Password == nil {
|
||||
return ErrPasswordNotProvided
|
||||
}
|
||||
|
||||
oldPassword, err := opts.Password(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get password")
|
||||
}
|
||||
|
||||
hash, err := PasswordHash([]byte(oldPassword), p.SRPID, p.SRPB, p.SecureRandom, p.CurrentAlgo)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "compute old password hash")
|
||||
}
|
||||
old = hash
|
||||
}
|
||||
|
||||
if _, err := c.api.AccountUpdatePasswordSettings(ctx, &tg.AccountUpdatePasswordSettingsRequest{
|
||||
Password: old,
|
||||
NewSettings: tg.AccountPasswordInputSettings{
|
||||
NewAlgo: algo,
|
||||
NewPasswordHash: newHash,
|
||||
Hint: opts.Hint,
|
||||
},
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "update password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetFailedWaitError reports that you recently requested a password reset that was cancel and need to wait until the
|
||||
// specified date before requesting another reset.
|
||||
type ResetFailedWaitError struct {
|
||||
Result tg.AccountResetPasswordFailedWait
|
||||
}
|
||||
|
||||
// Until returns time required to wait.
|
||||
func (r ResetFailedWaitError) Until() time.Duration {
|
||||
retryDate := time.Unix(int64(r.Result.RetryDate), 0)
|
||||
return time.Until(retryDate)
|
||||
}
|
||||
|
||||
// Error implements error.
|
||||
func (r *ResetFailedWaitError) Error() string {
|
||||
return fmt.Sprintf("wait to reset password (%s)", r.Until())
|
||||
}
|
||||
|
||||
// ResetPassword resets cloud password and returns time to wait until reset be performed.
|
||||
// If time is zero, password was successfully reset.
|
||||
//
|
||||
// May return ResetFailedWaitError.
|
||||
//
|
||||
// See https://core.telegram.org/api/srp#password-reset.
|
||||
func (c *Client) ResetPassword(ctx context.Context) (time.Time, error) {
|
||||
r, err := c.api.AccountResetPassword(ctx)
|
||||
if err != nil {
|
||||
return time.Time{}, errors.Wrap(err, "reset password")
|
||||
}
|
||||
switch v := r.(type) {
|
||||
case *tg.AccountResetPasswordFailedWait:
|
||||
return time.Time{}, &ResetFailedWaitError{Result: *v}
|
||||
case *tg.AccountResetPasswordRequestedWait:
|
||||
return time.Unix(int64(v.UntilDate), 0), nil
|
||||
case *tg.AccountResetPasswordOk:
|
||||
return time.Time{}, nil
|
||||
default:
|
||||
return time.Time{}, errors.Errorf("unexpected type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
// CancelPasswordReset cancels password reset.
|
||||
//
|
||||
// See https://core.telegram.org/api/srp#password-reset.
|
||||
func (c *Client) CancelPasswordReset(ctx context.Context) error {
|
||||
if _, err := c.api.AccountDeclinePasswordReset(ctx); err != nil {
|
||||
return errors.Wrap(err, "cancel password reset")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/auth"
|
||||
)
|
||||
|
||||
func ExampleClient_UpdatePassword() {
|
||||
ctx := context.Background()
|
||||
client := telegram.NewClient(telegram.TestAppID, telegram.TestAppHash, telegram.Options{})
|
||||
if err := client.Run(ctx, func(ctx context.Context) error {
|
||||
// Updating password.
|
||||
if err := client.Auth().UpdatePassword(ctx, "new_password", auth.UpdatePasswordOptions{
|
||||
// Hint sets new password hint.
|
||||
Hint: "new password hint",
|
||||
// Password will be called if old password is requested by Telegram.
|
||||
//
|
||||
// If password was requested and Password is nil, auth.ErrPasswordNotProvided error will be returned.
|
||||
Password: func(ctx context.Context) (string, error) {
|
||||
return "old_password", nil
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_ResetPassword() {
|
||||
ctx := context.Background()
|
||||
client := telegram.NewClient(telegram.TestAppID, telegram.TestAppHash, telegram.Options{})
|
||||
if err := client.Run(ctx, func(ctx context.Context) error {
|
||||
wait, err := client.Auth().ResetPassword(ctx)
|
||||
var waitErr *auth.ResetFailedWaitError
|
||||
switch {
|
||||
case errors.As(err, &waitErr):
|
||||
// Telegram requested wait until making new reset request.
|
||||
fmt.Printf("Wait until %s to reset password.\n", wait.String())
|
||||
case err != nil:
|
||||
return err
|
||||
}
|
||||
|
||||
// If returned time is zero, password was successfully reset.
|
||||
if wait.IsZero() {
|
||||
fmt.Println("Password was reset.")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Password will be reset on %s.\n", wait.String())
|
||||
return nil
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tgmock"
|
||||
)
|
||||
|
||||
func TestPasswordHash(t *testing.T) {
|
||||
a := require.New(t)
|
||||
_, err := PasswordHash(nil, 0, nil, nil, nil)
|
||||
a.Error(err, "unsupported algo")
|
||||
}
|
||||
|
||||
var testAlgo = &tg.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow{
|
||||
Salt1: []uint8{
|
||||
230, 200, 149, 125, 223, 152, 141, 72,
|
||||
},
|
||||
Salt2: []uint8{
|
||||
159, 99, 68, 130, 43, 9, 108, 255, 135, 239, 164, 38, 245, 120, 87, 182,
|
||||
},
|
||||
G: 3,
|
||||
P: []uint8{
|
||||
199, 28, 174, 185, 198, 177, 201, 4, 142, 108, 82, 47, 112, 241, 63, 115,
|
||||
152, 13, 64, 35, 142, 62, 33, 193, 73, 52, 208, 55, 86, 61, 147, 15,
|
||||
72, 25, 138, 10, 167, 193, 64, 88, 34, 148, 147, 210, 37, 48, 244, 219,
|
||||
250, 51, 111, 110, 10, 201, 37, 19, 149, 67, 174, 212, 76, 206, 124, 55,
|
||||
32, 253, 81, 246, 148, 88, 112, 90, 198, 140, 212, 254, 107, 107, 19, 171,
|
||||
220, 151, 70, 81, 41, 105, 50, 132, 84, 241, 143, 175, 140, 89, 95, 100,
|
||||
36, 119, 254, 150, 187, 42, 148, 29, 91, 205, 29, 74, 200, 204, 73, 136,
|
||||
7, 8, 250, 155, 55, 142, 60, 79, 58, 144, 96, 190, 230, 124, 249, 164,
|
||||
164, 166, 149, 129, 16, 81, 144, 126, 22, 39, 83, 181, 107, 15, 107, 65,
|
||||
13, 186, 116, 216, 168, 75, 42, 20, 179, 20, 78, 14, 241, 40, 71, 84,
|
||||
253, 23, 237, 149, 13, 89, 101, 180, 185, 221, 70, 88, 45, 177, 23, 141,
|
||||
22, 156, 107, 196, 101, 176, 214, 255, 156, 163, 146, 143, 239, 91, 154, 228,
|
||||
228, 24, 252, 21, 232, 62, 190, 160, 248, 127, 169, 255, 94, 237, 112, 5,
|
||||
13, 237, 40, 73, 244, 123, 249, 89, 217, 86, 133, 12, 233, 41, 133, 31,
|
||||
13, 129, 21, 246, 53, 177, 5, 238, 46, 78, 21, 208, 75, 36, 84, 191,
|
||||
111, 79, 173, 240, 52, 177, 4, 3, 17, 156, 216, 227, 185, 47, 204, 91,
|
||||
},
|
||||
}
|
||||
|
||||
func TestClient_UpdatePassword(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
expectCall := func(a *require.Assertions, m *tgmock.Mock, hasPassword bool) *tgmock.RequestBuilder {
|
||||
p := &tg.AccountPassword{
|
||||
HasPassword: hasPassword,
|
||||
NewAlgo: testAlgo,
|
||||
NewSecureAlgo: &tg.SecurePasswordKdfAlgoUnknown{},
|
||||
}
|
||||
if hasPassword {
|
||||
p.CurrentAlgo = testAlgo
|
||||
}
|
||||
p.SetFlags()
|
||||
return m.ExpectCall(&tg.AccountGetPasswordRequest{}).
|
||||
ThenResult(p).ExpectFunc(func(b bin.Encoder) {
|
||||
a.IsType(&tg.AccountUpdatePasswordSettingsRequest{}, b)
|
||||
r := b.(*tg.AccountUpdatePasswordSettingsRequest)
|
||||
|
||||
if !hasPassword {
|
||||
a.Equal(emptyPassword, r.Password)
|
||||
} else {
|
||||
a.NotEqual(emptyPassword, r.Password)
|
||||
}
|
||||
a.NotEmpty(r.NewSettings.NewPasswordHash)
|
||||
a.Equal("hint", r.NewSettings.Hint)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("PasswordNotRequired", mockTest(func(
|
||||
a *require.Assertions,
|
||||
m *tgmock.Mock,
|
||||
client *Client,
|
||||
) {
|
||||
m.ExpectCall(&tg.AccountGetPasswordRequest{}).ThenErr(testutil.TestError())
|
||||
a.Error(client.UpdatePassword(ctx, "", UpdatePasswordOptions{}))
|
||||
|
||||
expectCall(a, m, false).ThenTrue()
|
||||
a.NoError(client.UpdatePassword(ctx, "", UpdatePasswordOptions{
|
||||
Hint: "hint",
|
||||
}))
|
||||
}))
|
||||
|
||||
t.Run("PasswordRequired", mockTest(func(
|
||||
a *require.Assertions,
|
||||
m *tgmock.Mock,
|
||||
client *Client,
|
||||
) {
|
||||
m.ExpectCall(&tg.AccountGetPasswordRequest{}).
|
||||
ThenResult(&tg.AccountPassword{
|
||||
HasPassword: true,
|
||||
NewAlgo: testAlgo,
|
||||
CurrentAlgo: testAlgo,
|
||||
NewSecureAlgo: &tg.SecurePasswordKdfAlgoUnknown{},
|
||||
})
|
||||
a.ErrorIs(client.UpdatePassword(ctx, "", UpdatePasswordOptions{}), ErrPasswordNotProvided)
|
||||
|
||||
m.ExpectCall(&tg.AccountGetPasswordRequest{}).
|
||||
ThenResult(&tg.AccountPassword{
|
||||
HasPassword: true,
|
||||
NewAlgo: testAlgo,
|
||||
CurrentAlgo: testAlgo,
|
||||
NewSecureAlgo: &tg.SecurePasswordKdfAlgoUnknown{},
|
||||
})
|
||||
a.ErrorIs(client.UpdatePassword(ctx, "", UpdatePasswordOptions{
|
||||
Hint: "hint",
|
||||
Password: func(ctx context.Context) (string, error) {
|
||||
return "", testutil.TestError()
|
||||
},
|
||||
}), testutil.TestError())
|
||||
|
||||
expectCall(a, m, true).ThenTrue()
|
||||
a.NoError(client.UpdatePassword(ctx, "", UpdatePasswordOptions{
|
||||
Hint: "hint",
|
||||
Password: func(ctx context.Context) (string, error) {
|
||||
return "password", nil
|
||||
},
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
func TestClient_ResetPassword(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
wait := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC).Unix()
|
||||
mockTest(func(a *require.Assertions, mock *tgmock.Mock, client *Client) {
|
||||
mock.ExpectCall(&tg.AccountResetPasswordRequest{}).ThenErr(testutil.TestError())
|
||||
_, err := client.ResetPassword(ctx)
|
||||
a.Error(err)
|
||||
|
||||
mock.ExpectCall(&tg.AccountResetPasswordRequest{}).ThenResult(&tg.AccountResetPasswordFailedWait{
|
||||
RetryDate: int(wait),
|
||||
})
|
||||
var waitErr *ResetFailedWaitError
|
||||
_, err = client.ResetPassword(ctx)
|
||||
a.ErrorAs(err, &waitErr)
|
||||
a.Equal(int(wait), waitErr.Result.RetryDate)
|
||||
a.NotEmpty(waitErr.Error())
|
||||
|
||||
mock.ExpectCall(&tg.AccountResetPasswordRequest{}).ThenResult(&tg.AccountResetPasswordOk{})
|
||||
r, err := client.ResetPassword(ctx)
|
||||
a.NoError(err)
|
||||
a.True(r.IsZero())
|
||||
|
||||
mock.ExpectCall(&tg.AccountResetPasswordRequest{}).ThenResult(&tg.AccountResetPasswordRequestedWait{
|
||||
UntilDate: int(wait),
|
||||
})
|
||||
r, err = client.ResetPassword(ctx)
|
||||
a.NoError(err)
|
||||
a.False(r.IsZero())
|
||||
})(t)
|
||||
}
|
||||
|
||||
func TestClient_CancelPasswordReset(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTest(func(a *require.Assertions, mock *tgmock.Mock, client *Client) {
|
||||
mock.ExpectCall(&tg.AccountDeclinePasswordResetRequest{}).ThenErr(testutil.TestError())
|
||||
a.Error(client.CancelPasswordReset(ctx))
|
||||
|
||||
mock.ExpectCall(&tg.AccountDeclinePasswordResetRequest{}).ThenTrue()
|
||||
a.NoError(client.CancelPasswordReset(ctx))
|
||||
})(t)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package qrlogin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// AcceptQR accepts given token.
|
||||
//
|
||||
// See https://core.telegram.org/api/qr-login#accepting-a-login-token.
|
||||
func AcceptQR(ctx context.Context, raw *tg.Client, t Token) (*tg.Authorization, error) {
|
||||
auth, err := raw.AuthAcceptLoginToken(ctx, t.token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "accept")
|
||||
}
|
||||
return auth, nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package qrlogin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// MigrationNeededError reports that Telegram requested DC migration to continue login.
|
||||
type MigrationNeededError struct {
|
||||
MigrateTo *tg.AuthLoginTokenMigrateTo
|
||||
|
||||
// Tried indicates that the migration was attempted.
|
||||
//
|
||||
// Deprecated: do not use. QR login uses migrate function passed via
|
||||
// options.
|
||||
Tried bool
|
||||
}
|
||||
|
||||
// Error implements error.
|
||||
func (m *MigrationNeededError) Error() string {
|
||||
return fmt.Sprintf("migration to %d needed", m.MigrateTo.DCID)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package qrlogin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/clock"
|
||||
)
|
||||
|
||||
// Options of QR.
|
||||
type Options struct {
|
||||
Migrate func(ctx context.Context, dcID int) error
|
||||
Clock clock.Clock
|
||||
}
|
||||
|
||||
func (o *Options) setDefaults() {
|
||||
// It's okay to use zero value Migrate.
|
||||
if o.Clock == nil {
|
||||
o.Clock = clock.System
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Package qrlogin provides QR login flow implementation.
|
||||
//
|
||||
// See https://core.telegram.org/api/qr-login.
|
||||
package qrlogin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/clock"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// QR implements Telegram QR login flow.
|
||||
type QR struct {
|
||||
api *tg.Client
|
||||
appID int
|
||||
appHash string
|
||||
migrate func(ctx context.Context, dcID int) error
|
||||
clock clock.Clock
|
||||
}
|
||||
|
||||
// NewQR creates new QR
|
||||
func NewQR(api *tg.Client, appID int, appHash string, opts Options) QR {
|
||||
opts.setDefaults()
|
||||
return QR{
|
||||
api: api,
|
||||
appID: appID,
|
||||
appHash: appHash,
|
||||
clock: opts.Clock,
|
||||
migrate: opts.Migrate,
|
||||
}
|
||||
}
|
||||
|
||||
// Export exports new login token.
|
||||
//
|
||||
// See https://core.telegram.org/api/qr-login#exporting-a-login-token.
|
||||
func (q QR) Export(ctx context.Context, exceptIDs ...int64) (Token, error) {
|
||||
result, err := q.api.AuthExportLoginToken(ctx, &tg.AuthExportLoginTokenRequest{
|
||||
APIID: q.appID,
|
||||
APIHash: q.appHash,
|
||||
ExceptIDs: exceptIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(err, "export")
|
||||
}
|
||||
|
||||
t, ok := result.(*tg.AuthLoginToken)
|
||||
if !ok {
|
||||
return Token{}, errors.Errorf("unexpected type %T", result)
|
||||
}
|
||||
return NewToken(t.Token, t.Expires), nil
|
||||
}
|
||||
|
||||
// Accept accepts given token.
|
||||
//
|
||||
// See https://core.telegram.org/api/qr-login#accepting-a-login-token.
|
||||
func (q QR) Accept(ctx context.Context, t Token) (*tg.Authorization, error) {
|
||||
return AcceptQR(ctx, q.api, t)
|
||||
}
|
||||
|
||||
// Import imports accepted token.
|
||||
//
|
||||
// See https://core.telegram.org/api/qr-login#confirming-importing-the-login-token.
|
||||
func (q QR) Import(ctx context.Context) (*tg.AuthAuthorization, error) {
|
||||
result, err := q.api.AuthExportLoginToken(ctx, &tg.AuthExportLoginTokenRequest{
|
||||
APIID: q.appID,
|
||||
APIHash: q.appHash,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "import")
|
||||
}
|
||||
|
||||
switch t := result.(type) {
|
||||
case *tg.AuthLoginTokenMigrateTo:
|
||||
if q.migrate == nil {
|
||||
return nil, &MigrationNeededError{
|
||||
MigrateTo: t,
|
||||
}
|
||||
}
|
||||
if err := q.migrate(ctx, t.DCID); err != nil {
|
||||
return nil, errors.Wrap(err, "migrate")
|
||||
}
|
||||
|
||||
res, err := q.api.AuthImportLoginToken(ctx, t.Token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "import")
|
||||
}
|
||||
|
||||
success, ok := res.(*tg.AuthLoginTokenSuccess)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected type %T", res)
|
||||
}
|
||||
|
||||
auth, ok := success.Authorization.(*tg.AuthAuthorization)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected type %T", success.Authorization)
|
||||
}
|
||||
return auth, nil
|
||||
case *tg.AuthLoginTokenSuccess:
|
||||
auth, ok := t.Authorization.(*tg.AuthAuthorization)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected type %T", t.Authorization)
|
||||
}
|
||||
return auth, nil
|
||||
default:
|
||||
return nil, errors.Errorf("unexpected type %T", result)
|
||||
}
|
||||
}
|
||||
|
||||
// LoggedIn is signal channel to notify about tg.UpdateLoginToken.
|
||||
type LoggedIn <-chan struct{}
|
||||
|
||||
// OnLoginToken sets handler for given dispatcher and returns signal channel.
|
||||
func OnLoginToken(d interface {
|
||||
OnLoginToken(tg.LoginTokenHandler)
|
||||
},
|
||||
) LoggedIn {
|
||||
loggedIn := make(chan struct{})
|
||||
d.OnLoginToken(func(ctx context.Context, e tg.Entities, update *tg.UpdateLoginToken) error {
|
||||
select {
|
||||
case loggedIn <- struct{}{}:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return loggedIn
|
||||
}
|
||||
|
||||
// Auth generates new QR login token, shows it and awaits acceptation.
|
||||
//
|
||||
// NB: Show callback may be called more than once if QR expires.
|
||||
func (q QR) Auth(
|
||||
ctx context.Context,
|
||||
loggedIn LoggedIn,
|
||||
show func(ctx context.Context, token Token) error,
|
||||
exceptIDs ...int64,
|
||||
) (*tg.AuthAuthorization, error) {
|
||||
until := func(token Token) time.Duration {
|
||||
return token.Expires().Sub(q.clock.Now()).Truncate(time.Second)
|
||||
}
|
||||
|
||||
token, err := q.Export(ctx, exceptIDs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timer := q.clock.Timer(until(token))
|
||||
defer clock.StopTimer(timer)
|
||||
|
||||
for {
|
||||
if err := show(ctx, token); err != nil {
|
||||
return nil, errors.Wrap(err, "show")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-timer.C():
|
||||
t, err := q.Export(ctx, exceptIDs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token = t
|
||||
timer.Reset(until(token))
|
||||
|
||||
continue
|
||||
case <-loggedIn:
|
||||
}
|
||||
|
||||
return q.Import(ctx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package qrlogin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gotd/neo"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/constant"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tgmock"
|
||||
)
|
||||
|
||||
func testQR(t *testing.T, migrate func(ctx context.Context, dcID int) error) (*tgmock.Mock, QR) {
|
||||
mock := tgmock.New(t)
|
||||
return mock, NewQR(tg.NewClient(mock), constant.TestAppID, constant.TestAppHash, Options{
|
||||
Migrate: migrate,
|
||||
})
|
||||
}
|
||||
|
||||
var testToken = NewToken([]byte("token"), 0)
|
||||
|
||||
func TestQR_Export(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
a := require.New(t)
|
||||
mock, qr := testQR(t, nil)
|
||||
|
||||
mock.ExpectCall(&tg.AuthExportLoginTokenRequest{
|
||||
APIID: constant.TestAppID,
|
||||
APIHash: constant.TestAppHash,
|
||||
ExceptIDs: []int64{0},
|
||||
}).ThenResult(&tg.AuthLoginToken{
|
||||
Expires: 0,
|
||||
Token: testToken.token,
|
||||
})
|
||||
result, err := qr.Export(ctx, 0)
|
||||
a.NoError(err)
|
||||
a.Equal(Token{
|
||||
token: testToken.token,
|
||||
expires: time.Unix(0, 0),
|
||||
}, result)
|
||||
|
||||
mock.ExpectCall(&tg.AuthExportLoginTokenRequest{
|
||||
APIID: constant.TestAppID,
|
||||
APIHash: constant.TestAppHash,
|
||||
}).ThenResult(&tg.AuthLoginTokenMigrateTo{
|
||||
Token: testToken.token,
|
||||
})
|
||||
_, err = qr.Export(ctx)
|
||||
a.Error(err)
|
||||
|
||||
mock.ExpectCall(&tg.AuthExportLoginTokenRequest{
|
||||
APIID: constant.TestAppID,
|
||||
APIHash: constant.TestAppHash,
|
||||
}).ThenErr(testutil.TestError())
|
||||
_, err = qr.Export(ctx)
|
||||
a.ErrorIs(err, testutil.TestError())
|
||||
}
|
||||
|
||||
func TestQR_Accept(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
a := require.New(t)
|
||||
mock, qr := testQR(t, nil)
|
||||
|
||||
auth := &tg.Authorization{
|
||||
APIID: 1,
|
||||
}
|
||||
mock.ExpectCall(&tg.AuthAcceptLoginTokenRequest{
|
||||
Token: testToken.token,
|
||||
}).ThenResult(auth)
|
||||
result, err := qr.Accept(ctx, testToken)
|
||||
a.NoError(err)
|
||||
a.Equal(auth, result)
|
||||
|
||||
mock.ExpectCall(&tg.AuthAcceptLoginTokenRequest{
|
||||
Token: testToken.token,
|
||||
}).ThenErr(testutil.TestError())
|
||||
_, err = qr.Accept(ctx, testToken)
|
||||
a.ErrorIs(err, testutil.TestError())
|
||||
}
|
||||
|
||||
func TestQR_Import(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
a := require.New(t)
|
||||
mock, qr := testQR(t, nil)
|
||||
|
||||
auth := &tg.AuthAuthorization{
|
||||
User: &tg.User{ID: 10},
|
||||
}
|
||||
mock.ExpectCall(&tg.AuthExportLoginTokenRequest{
|
||||
APIID: constant.TestAppID,
|
||||
APIHash: constant.TestAppHash,
|
||||
}).ThenResult(&tg.AuthLoginTokenSuccess{
|
||||
Authorization: auth,
|
||||
})
|
||||
result, err := qr.Import(ctx)
|
||||
a.NoError(err)
|
||||
a.Equal(auth, result)
|
||||
|
||||
mock.ExpectCall(&tg.AuthExportLoginTokenRequest{
|
||||
APIID: constant.TestAppID,
|
||||
APIHash: constant.TestAppHash,
|
||||
}).ThenResult(&tg.AuthLoginTokenMigrateTo{
|
||||
DCID: 1,
|
||||
})
|
||||
_, err = qr.Import(ctx)
|
||||
var mig *MigrationNeededError
|
||||
a.ErrorAs(err, &mig)
|
||||
a.Equal(1, mig.MigrateTo.DCID)
|
||||
|
||||
mock.ExpectCall(&tg.AuthExportLoginTokenRequest{
|
||||
APIID: constant.TestAppID,
|
||||
APIHash: constant.TestAppHash,
|
||||
}).ThenResult(&tg.AuthLoginToken{
|
||||
Token: testToken.token,
|
||||
})
|
||||
_, err = qr.Import(ctx)
|
||||
a.Error(err)
|
||||
|
||||
mock.ExpectCall(&tg.AuthExportLoginTokenRequest{
|
||||
APIID: constant.TestAppID,
|
||||
APIHash: constant.TestAppHash,
|
||||
}).ThenErr(testutil.TestError())
|
||||
_, err = qr.Import(ctx)
|
||||
a.ErrorIs(err, testutil.TestError())
|
||||
}
|
||||
|
||||
func TestQR_Auth(t *testing.T) {
|
||||
a := require.New(t)
|
||||
mock := tgmock.New(t)
|
||||
clock := neo.NewTime(time.Now())
|
||||
|
||||
auth := &tg.AuthAuthorization{
|
||||
User: &tg.User{ID: 10},
|
||||
}
|
||||
mock.ExpectCall(&tg.AuthExportLoginTokenRequest{
|
||||
APIID: constant.TestAppID,
|
||||
APIHash: constant.TestAppHash,
|
||||
}).ThenResult(&tg.AuthLoginToken{
|
||||
Expires: int(clock.Now().Add(time.Minute).Unix()),
|
||||
Token: testToken.token,
|
||||
}).ExpectCall(&tg.AuthExportLoginTokenRequest{
|
||||
APIID: constant.TestAppID,
|
||||
APIHash: constant.TestAppHash,
|
||||
}).ThenResult(&tg.AuthLoginToken{
|
||||
Expires: int(clock.Now().Add(2 * time.Minute).Unix()),
|
||||
Token: testToken.token,
|
||||
}).ExpectCall(&tg.AuthExportLoginTokenRequest{
|
||||
APIID: constant.TestAppID,
|
||||
APIHash: constant.TestAppHash,
|
||||
}).ThenResult(&tg.AuthLoginTokenSuccess{
|
||||
Authorization: auth,
|
||||
})
|
||||
|
||||
qr := NewQR(tg.NewClient(mock), constant.TestAppID, constant.TestAppHash, Options{
|
||||
Clock: clock,
|
||||
})
|
||||
|
||||
show := make(chan struct{})
|
||||
done := make(chan error)
|
||||
loggedIn := make(chan struct{})
|
||||
go func() {
|
||||
_, err := qr.Auth(context.Background(), loggedIn, func(ctx context.Context, token Token) error {
|
||||
show <- struct{}{}
|
||||
return nil
|
||||
})
|
||||
done <- err
|
||||
}()
|
||||
|
||||
// Show QR first time.
|
||||
<-show
|
||||
|
||||
// Skip 1 minute, token expires.
|
||||
clock.Travel(time.Minute + 1)
|
||||
|
||||
// Show QR second time.
|
||||
<-show
|
||||
|
||||
// Emulate update, auth done.
|
||||
loggedIn <- struct{}{}
|
||||
|
||||
a.NoError(<-done)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package qrlogin
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"image"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"rsc.io/qr"
|
||||
)
|
||||
|
||||
// Token represents Telegram QR Login token.
|
||||
type Token struct {
|
||||
token []byte
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
// ParseTokenURL creates Token from given URL.
|
||||
func ParseTokenURL(u string) (Token, error) {
|
||||
parsed, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return Token{}, err
|
||||
}
|
||||
switch {
|
||||
case parsed.Scheme != "tg":
|
||||
return Token{}, errors.Errorf("unexpected scheme %q", parsed.Scheme)
|
||||
case parsed.Host != "login":
|
||||
return Token{}, errors.Errorf("wrong path %q", parsed.Host)
|
||||
}
|
||||
|
||||
q := parsed.Query()
|
||||
if q.Get("token") == "" {
|
||||
return Token{}, errors.New("token is empty")
|
||||
}
|
||||
token, err := base64.URLEncoding.DecodeString(q.Get("token"))
|
||||
if err != nil {
|
||||
return Token{}, err
|
||||
}
|
||||
|
||||
return NewToken(token, 0), nil
|
||||
}
|
||||
|
||||
// NewToken creates new Token.
|
||||
func NewToken(token []byte, expires int) Token {
|
||||
return Token{
|
||||
token: token,
|
||||
expires: time.Unix(int64(expires), 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Expires returns token expiration time.
|
||||
func (t Token) Expires() time.Time {
|
||||
return t.expires
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (t Token) String() string {
|
||||
return base64.URLEncoding.EncodeToString(t.token)
|
||||
}
|
||||
|
||||
// URL returns login URL.
|
||||
//
|
||||
// See https://core.telegram.org/api/qr-login#exporting-a-login-token.
|
||||
func (t Token) URL() string {
|
||||
return "tg://login?token=" + base64.URLEncoding.EncodeToString(t.token)
|
||||
}
|
||||
|
||||
// Image returns QR image.
|
||||
func (t Token) Image(level qr.Level) (image.Image, error) {
|
||||
code, err := qr.Encode(t.URL(), level)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "encode")
|
||||
}
|
||||
return code.Image(), nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package qrlogin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseTokenURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
u string
|
||||
want Token
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"Valid",
|
||||
"tg://login?token=AQL0cY5hVg_D1OqESdYnJVg5845qbd8FiOLpUUeyvcb28g==",
|
||||
Token{
|
||||
token: []uint8{
|
||||
0x1, 0x2, 0xf4, 0x71, 0x8e, 0x61,
|
||||
0x56, 0xf, 0xc3, 0xd4, 0xea, 0x84,
|
||||
0x49, 0xd6, 0x27, 0x25, 0x58, 0x39,
|
||||
0xf3, 0x8e, 0x6a, 0x6d, 0xdf, 0x5,
|
||||
0x88, 0xe2, 0xe9, 0x51, 0x47, 0xb2,
|
||||
0xbd, 0xc6, 0xf6, 0xf2,
|
||||
},
|
||||
expires: time.Unix(0, 0),
|
||||
},
|
||||
false,
|
||||
},
|
||||
{"InvalidSchema", "vk://login", Token{}, true},
|
||||
{"InvalidPath", "tg://aboba", Token{}, true},
|
||||
{"NoToken", "tg://login", Token{}, true},
|
||||
{"InvalidBase64", "tg://login?token=A", Token{}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := require.New(t)
|
||||
|
||||
got, err := ParseTokenURL(tt.u)
|
||||
if tt.wantErr {
|
||||
a.Error(err)
|
||||
} else {
|
||||
a.Equal(tt.want, got)
|
||||
a.NoError(err)
|
||||
a.Equal(tt.want.URL(), tt.u)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// self returns current user.
|
||||
//
|
||||
// You can use tg.User.Bot to check whether current user is bot.
|
||||
func (c *Client) self(ctx context.Context) (*tg.User, error) {
|
||||
users, err := c.api.UsersGetUsers(ctx, []tg.InputUserClass{&tg.InputUserSelf{}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := tg.UserClassArray(users).FirstAsNotEmpty()
|
||||
if !ok {
|
||||
return nil, errors.Errorf("users response count: %v", users)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tgmock"
|
||||
)
|
||||
|
||||
func TestClient_self(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTest(func(a *require.Assertions, mock *tgmock.Mock, client *Client) {
|
||||
mock.ExpectCall(&tg.UsersGetUsersRequest{
|
||||
ID: []tg.InputUserClass{&tg.InputUserSelf{}},
|
||||
}).ThenErr(testutil.TestError())
|
||||
_, err := client.self(ctx)
|
||||
a.Error(err)
|
||||
|
||||
mock.ExpectCall(&tg.UsersGetUsersRequest{
|
||||
ID: []tg.InputUserClass{&tg.InputUserSelf{}},
|
||||
}).ThenResult(&tg.UserClassVector{Elems: []tg.UserClass{&tg.UserEmpty{
|
||||
ID: 10,
|
||||
}}})
|
||||
_, err = client.self(ctx)
|
||||
a.Error(err)
|
||||
|
||||
mock.ExpectCall(&tg.UsersGetUsersRequest{
|
||||
ID: []tg.InputUserClass{&tg.InputUserSelf{}},
|
||||
}).ThenResult(&tg.UserClassVector{Elems: []tg.UserClass{&tg.User{
|
||||
Self: true,
|
||||
ID: 10,
|
||||
AccessHash: 10,
|
||||
}}})
|
||||
r, err := client.self(ctx)
|
||||
a.NoError(err)
|
||||
a.Equal(int64(10), r.ID)
|
||||
})(t)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// SignUpRequired means that log in failed because corresponding account
|
||||
// does not exist, so sign up is required.
|
||||
type SignUpRequired struct {
|
||||
TermsOfService tg.HelpTermsOfService
|
||||
}
|
||||
|
||||
// Is returns true if err is SignUpRequired.
|
||||
func (s *SignUpRequired) Is(err error) bool {
|
||||
_, ok := err.(*SignUpRequired)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *SignUpRequired) Error() string {
|
||||
return "account with provided number does not exist (sign up required)"
|
||||
}
|
||||
|
||||
// checkResult checks that `a` is *tg.AuthAuthorization and returns authorization result or error.
|
||||
func checkResult(a tg.AuthAuthorizationClass) (*tg.AuthAuthorization, error) {
|
||||
switch a := a.(type) {
|
||||
case *tg.AuthAuthorization:
|
||||
return a, nil // ok
|
||||
case *tg.AuthAuthorizationSignUpRequired:
|
||||
return nil, &SignUpRequired{
|
||||
TermsOfService: a.TermsOfService,
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf("got unexpected response %T", a)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
)
|
||||
|
||||
// Status represents authorization status.
|
||||
type Status struct {
|
||||
// Authorized is true if client is authorized.
|
||||
Authorized bool
|
||||
// User is current User object.
|
||||
User *tg.User
|
||||
}
|
||||
|
||||
// Status gets authorization status of client.
|
||||
func (c *Client) Status(ctx context.Context) (*Status, error) {
|
||||
u, err := c.self(ctx)
|
||||
if IsUnauthorized(err) {
|
||||
return &Status{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Status{
|
||||
Authorized: true,
|
||||
User: u,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IfNecessary runs given auth flow if current session is not authorized.
|
||||
func (c *Client) IfNecessary(ctx context.Context, flow Flow) error {
|
||||
auth, err := c.Status(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get auth status")
|
||||
}
|
||||
if auth.Authorized {
|
||||
return nil
|
||||
}
|
||||
if err := flow.Run(ctx, c); err != nil {
|
||||
return errors.Wrap(err, "auth flow")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test creates and runs auth flow using Test authenticator
|
||||
// if current session is not authorized.
|
||||
func (c *Client) Test(ctx context.Context, dc int) error {
|
||||
return c.IfNecessary(ctx, NewFlow(Test(c.rand, dc), SendCodeOptions{}))
|
||||
}
|
||||
|
||||
// TestUser creates and runs auth flow using TestUser authenticator
|
||||
// if current session is not authorized.
|
||||
func (c *Client) TestUser(ctx context.Context, phone string, dc int) error {
|
||||
return c.IfNecessary(ctx, NewFlow(TestUser(phone, dc), SendCodeOptions{}))
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"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 TestClient_Status(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("Authorized", func(t *testing.T) {
|
||||
mock := tgmock.NewRequire(t)
|
||||
user := &tg.User{
|
||||
Username: "user",
|
||||
}
|
||||
mock.Expect().ThenResult(&tg.UserClassVector{Elems: []tg.UserClass{user}})
|
||||
|
||||
status, err := testClient(mock).Status(ctx)
|
||||
require.NoError(t, err)
|
||||
require.True(t, status.Authorized)
|
||||
require.Equal(t, user, status.User)
|
||||
})
|
||||
|
||||
t.Run("Unauthorized", func(t *testing.T) {
|
||||
mock := tgmock.NewRequire(t)
|
||||
mock.Expect().ThenUnregistered()
|
||||
|
||||
status, err := testClient(mock).Status(ctx)
|
||||
require.NoError(t, err)
|
||||
require.False(t, status.Authorized)
|
||||
})
|
||||
|
||||
t.Run("Error", func(t *testing.T) {
|
||||
mock := tgmock.NewRequire(t)
|
||||
mock.Expect().ThenRPCErr(&tgerr.Error{
|
||||
Code: 500,
|
||||
Message: "BRUH",
|
||||
Type: "BRUH",
|
||||
})
|
||||
|
||||
_, err := testClient(mock).Status(ctx)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_IfNecessary(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("Authorized", func(t *testing.T) {
|
||||
mock := tgmock.NewRequire(t)
|
||||
testUser := &tg.User{
|
||||
Username: "user",
|
||||
}
|
||||
mock.Expect().ThenResult(&tg.UserClassVector{Elems: []tg.UserClass{testUser}})
|
||||
|
||||
// Pass empty AuthFlow because it should not be called anyway.
|
||||
require.NoError(t, testClient(mock).IfNecessary(ctx, Flow{}))
|
||||
})
|
||||
|
||||
t.Run("Error", func(t *testing.T) {
|
||||
mock := tgmock.NewRequire(t)
|
||||
mock.Expect().ThenRPCErr(&tgerr.Error{
|
||||
Code: 500,
|
||||
Message: "BRUH",
|
||||
Type: "BRUH",
|
||||
})
|
||||
|
||||
// Pass empty AuthFlow because it should not be called anyway.
|
||||
require.Error(t, testClient(mock).IfNecessary(ctx, Flow{}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_Test(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("Authorized", func(t *testing.T) {
|
||||
mock := tgmock.NewRequire(t)
|
||||
testUser := &tg.User{
|
||||
Username: "user",
|
||||
}
|
||||
mock.Expect().ThenResult(&tg.UserClassVector{Elems: []tg.UserClass{testUser}})
|
||||
|
||||
// Pass empty AuthFlow because it should not be called anyway.
|
||||
require.NoError(t, testClient(mock).Test(ctx, 2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_TestUser(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("Authorized", func(t *testing.T) {
|
||||
mock := tgmock.NewRequire(t)
|
||||
testUser := &tg.User{
|
||||
Username: "user",
|
||||
}
|
||||
mock.Expect().ThenResult(&tg.UserClassVector{Elems: []tg.UserClass{testUser}})
|
||||
|
||||
// Pass empty AuthFlow because it should not be called anyway.
|
||||
require.NoError(t, testClient(mock).TestUser(ctx, "phone", 2))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
|
||||
)
|
||||
|
||||
// ErrPasswordInvalid means that password provided to Password is invalid.
|
||||
//
|
||||
// Note that telegram does not trim whitespace characters by default, check
|
||||
// that provided password is expected and clean whitespaces if needed.
|
||||
// You can use strings.TrimSpace(password) for this.
|
||||
var ErrPasswordInvalid = errors.New("invalid password")
|
||||
|
||||
// Password performs login via secure remote password (aka 2FA).
|
||||
//
|
||||
// Method can be called after SignIn to provide password if requested.
|
||||
func (c *Client) Password(ctx context.Context, password string) (*tg.AuthAuthorization, error) {
|
||||
p, err := c.api.AccountGetPassword(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get SRP parameters")
|
||||
}
|
||||
|
||||
a, err := PasswordHash([]byte(password), p.SRPID, p.SRPB, p.SecureRandom, p.CurrentAlgo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "compute password hash")
|
||||
}
|
||||
|
||||
auth, err := c.api.AuthCheckPassword(ctx, &tg.InputCheckPasswordSRP{
|
||||
SRPID: p.SRPID,
|
||||
A: a.A,
|
||||
M1: a.M1,
|
||||
})
|
||||
if tg.IsPasswordHashInvalid(err) {
|
||||
return nil, ErrPasswordInvalid
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "check password")
|
||||
}
|
||||
result, err := checkResult(auth)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "check")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SendCodeOptions defines how to send auth code to user.
|
||||
type SendCodeOptions struct {
|
||||
// AllowFlashCall allows phone verification via phone calls.
|
||||
AllowFlashCall bool
|
||||
// Pass true if the phone number is used on the current device.
|
||||
// Ignored if AllowFlashCall is not set.
|
||||
CurrentNumber bool
|
||||
// If a token that will be included in eventually sent SMSs is required:
|
||||
// required in newer versions of android, to use the android SMS receiver APIs.
|
||||
AllowAppHash bool
|
||||
}
|
||||
|
||||
// SendCode requests code for provided phone number, returning code hash
|
||||
// and error if any. Use AuthFlow to reduce boilerplate.
|
||||
//
|
||||
// This method should be called first in user authentication flow.
|
||||
func (c *Client) SendCode(ctx context.Context, phone string, options SendCodeOptions) (tg.AuthSentCodeClass, error) {
|
||||
var settings tg.CodeSettings
|
||||
if options.AllowAppHash {
|
||||
settings.SetAllowAppHash(true)
|
||||
}
|
||||
if options.AllowFlashCall {
|
||||
settings.SetAllowFlashcall(true)
|
||||
}
|
||||
if options.CurrentNumber {
|
||||
settings.SetCurrentNumber(true)
|
||||
}
|
||||
|
||||
sentCode, err := c.api.AuthSendCode(ctx, &tg.AuthSendCodeRequest{
|
||||
PhoneNumber: phone,
|
||||
APIID: c.appID,
|
||||
APIHash: c.appHash,
|
||||
Settings: settings,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "send code")
|
||||
}
|
||||
return sentCode, nil
|
||||
}
|
||||
|
||||
// ErrPasswordAuthNeeded means that 2FA auth is required.
|
||||
//
|
||||
// Call Client.Password to provide 2FA password.
|
||||
var ErrPasswordAuthNeeded = errors.New("2FA required")
|
||||
|
||||
// SignIn performs sign in with provided user phone, code and code hash.
|
||||
//
|
||||
// If ErrPasswordAuthNeeded is returned, call Password to provide 2FA
|
||||
// password.
|
||||
//
|
||||
// To obtain codeHash, use SendCode.
|
||||
func (c *Client) SignIn(ctx context.Context, phone, code, codeHash string) (*tg.AuthAuthorization, error) {
|
||||
auth, err := c.api.AuthSignIn(ctx, &tg.AuthSignInRequest{
|
||||
PhoneNumber: phone,
|
||||
PhoneCodeHash: codeHash,
|
||||
PhoneCode: code,
|
||||
})
|
||||
if tgerr.Is(err, "SESSION_PASSWORD_NEEDED") {
|
||||
return nil, ErrPasswordAuthNeeded
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sign in")
|
||||
}
|
||||
result, err := checkResult(auth)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "check")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AcceptTOS accepts version of Terms Of Service.
|
||||
func (c *Client) AcceptTOS(ctx context.Context, id tg.DataJSON) error {
|
||||
_, err := c.api.HelpAcceptTermsOfService(ctx, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// SignUp wraps parameters for SignUp.
|
||||
type SignUp struct {
|
||||
PhoneNumber string
|
||||
PhoneCodeHash string
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
// SignUp registers a validated phone number in the system.
|
||||
//
|
||||
// To obtain codeHash, use SendCode.
|
||||
// Use AuthFlow helper to handle authentication flow.
|
||||
func (c *Client) SignUp(ctx context.Context, s SignUp) (*tg.AuthAuthorization, error) {
|
||||
auth, err := c.api.AuthSignUp(ctx, &tg.AuthSignUpRequest{
|
||||
LastName: s.LastName,
|
||||
PhoneCodeHash: s.PhoneCodeHash,
|
||||
PhoneNumber: s.PhoneNumber,
|
||||
FirstName: s.FirstName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "request")
|
||||
}
|
||||
result, err := checkResult(auth)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "check")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user