From d330000c8dc46379009a7057d855ea149ae6373c Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Wed, 12 Oct 2022 16:20:08 +0200 Subject: [PATCH] Other: Put back old 2FA/two-pass flow in GUI --- go.mod | 2 +- go.sum | 4 +- internal/bridge/errors.go | 2 +- internal/frontend/grpc/service.go | 44 ++++++++ internal/frontend/grpc/service_methods.go | 123 +++++++++++++++++++--- 5 files changed, 157 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 66f832ab..ffaab172 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.0 github.com/urfave/cli/v2 v2.16.3 - gitlab.protontech.ch/go/liteapi v0.33.2-0.20221011193656-705963f7a7d9 + gitlab.protontech.ch/go/liteapi v0.33.2-0.20221012095146-bd94443eeb8e golang.org/x/exp v0.0.0-20220921164117-439092de6870 golang.org/x/net v0.1.0 golang.org/x/sys v0.1.0 diff --git a/go.sum b/go.sum index 480806fe..b4a4ee56 100644 --- a/go.sum +++ b/go.sum @@ -397,8 +397,8 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0= github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA= -gitlab.protontech.ch/go/liteapi v0.33.2-0.20221011193656-705963f7a7d9 h1:WErqL7DdcsQFNNy2Zkj8MT83HbSUbc17qptrEuVcbGA= -gitlab.protontech.ch/go/liteapi v0.33.2-0.20221011193656-705963f7a7d9/go.mod h1:NfsxXn1T81sz0gHnxuAfyCI4Agzm5UWVRyEtdQSch/4= +gitlab.protontech.ch/go/liteapi v0.33.2-0.20221012095146-bd94443eeb8e h1:UBgcmAYZ45ylLlfmc8/0evP40LwVthBHRoMgGqt4YV8= +gitlab.protontech.ch/go/liteapi v0.33.2-0.20221012095146-bd94443eeb8e/go.mod h1:NfsxXn1T81sz0gHnxuAfyCI4Agzm5UWVRyEtdQSch/4= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= diff --git a/internal/bridge/errors.go b/internal/bridge/errors.go index 6135b366..5a752226 100644 --- a/internal/bridge/errors.go +++ b/internal/bridge/errors.go @@ -12,7 +12,7 @@ var ( ErrNoSuchUser = errors.New("no such user") ErrUserAlreadyExists = errors.New("user already exists") - ErrUserAlreadyLoggedIn = errors.New("user already logged in") + ErrUserAlreadyLoggedIn = errors.New("the user is already logged in") ErrNotImplemented = errors.New("not implemented") ErrSizeTooLarge = errors.New("file is too big") diff --git a/internal/frontend/grpc/service.go b/internal/frontend/grpc/service.go index 3011bb27..63acf0d9 100644 --- a/internal/frontend/grpc/service.go +++ b/internal/frontend/grpc/service.go @@ -37,6 +37,7 @@ import ( "github.com/ProtonMail/proton-bridge/v2/pkg/restarter" "github.com/google/uuid" "github.com/sirupsen/logrus" + "gitlab.protontech.ch/go/liteapi" "google.golang.org/grpc" codes "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" @@ -65,6 +66,10 @@ type Service struct { // nolint:structcheck bridge *bridge.Bridge newVersionInfo updater.VersionInfo + authClient *liteapi.Client + auth liteapi.Auth + password []byte + log *logrus.Entry initializing sync.WaitGroup initializationDone sync.Once @@ -263,6 +268,45 @@ func (s *Service) watchEvents() { } } +func (s *Service) loginAbort() { + s.loginClean() +} + +func (s *Service) loginClean() { + s.auth = liteapi.Auth{} + s.authClient = nil + for i := range s.password { + s.password[i] = '\x00' + } + s.password = s.password[0:0] +} + +func (s *Service) finishLogin() { + defer s.loginClean() + + if len(s.password) == 0 || s.auth.UID == "" || s.authClient == nil { + s.log. + WithField("hasPass", len(s.password) != 0). + WithField("hasAuth", s.auth.UID != ""). + WithField("hasClient", s.authClient != nil). + Error("Finish login: authentication incomplete") + + _ = s.SendEvent(NewLoginError(LoginErrorType_TWO_PASSWORDS_ABORT, "Missing authentication, try again.")) + return + } + + userID, err := s.bridge.LoginUser(context.Background(), s.authClient, s.auth, s.password) + if err != nil { + s.log.WithError(err).Errorf("Finish login failed") + _ = s.SendEvent(NewLoginError(LoginErrorType_TWO_PASSWORDS_ABORT, err.Error())) + return + } + + s.log.WithField("userID", userID).Debug("Login finished") + + _ = s.SendEvent(NewLoginFinishedEvent(userID)) +} + func (s *Service) triggerReset() { defer func() { _ = s.SendEvent(NewResetFinishedEvent()) diff --git a/internal/frontend/grpc/service_methods.go b/internal/frontend/grpc/service_methods.go index 2c5db265..dc8f09a4 100644 --- a/internal/frontend/grpc/service_methods.go +++ b/internal/frontend/grpc/service_methods.go @@ -20,15 +20,18 @@ package grpc import ( "context" "encoding/base64" + "errors" "runtime" "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/proton-bridge/v2/internal/bridge" "github.com/ProtonMail/proton-bridge/v2/internal/constants" "github.com/ProtonMail/proton-bridge/v2/internal/frontend/theme" "github.com/ProtonMail/proton-bridge/v2/internal/updater" "github.com/ProtonMail/proton-bridge/v2/pkg/keychain" "github.com/ProtonMail/proton-bridge/v2/pkg/ports" "github.com/sirupsen/logrus" + "gitlab.protontech.ch/go/liteapi" "golang.org/x/exp/maps" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -364,33 +367,125 @@ func (s *Service) Login(ctx context.Context, login *LoginRequest) (*emptypb.Empt return } - // TODO: Handle different error types! - // - bad credentials - // - bad proton plan - // - user already exists - userID, err := s.bridge.LoginFull(context.Background(), login.Username, password, nil, nil) + client, auth, err := s.bridge.LoginAuth(context.Background(), login.Username, password) if err != nil { - s.log.WithError(err).Error("Cannot login user") - _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot login user")) + defer s.loginClean() + + switch { + case errors.Is(err, bridge.ErrUserAlreadyLoggedIn): + _ = s.SendEvent(NewLoginAlreadyLoggedInEvent(auth.UserID)) + + case errors.Is(err, liteapi.ErrIncorrectLoginCredentials): + _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "")) + + case errors.Is(err, liteapi.ErrPaidPlanRequired): + _ = s.SendEvent(NewLoginError(LoginErrorType_FREE_USER, "")) + + default: + _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, err.Error())) + } + return } - _ = s.SendEvent(NewLoginFinishedEvent(userID)) + s.password = password + s.authClient = client + s.auth = auth + + switch { + case auth.TwoFA.Enabled == liteapi.TOTPEnabled: + _ = s.SendEvent(NewLoginTfaRequestedEvent(login.Username)) + + case auth.PasswordMode == liteapi.TwoPasswordMode: + _ = s.SendEvent(NewLoginTwoPasswordsRequestedEvent()) + + default: + s.finishLogin() + } }() return &emptypb.Empty{}, nil } -func (s *Service) Login2FA(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) { - panic("TODO") +func (s *Service) Login2FA(ctx context.Context, login *LoginRequest) (*emptypb.Empty, error) { + s.log.WithField("username", login.Username).Debug("Login2FA") + + go func() { + defer s.panicHandler.HandlePanic() + + if s.auth.UID == "" || s.authClient == nil { + s.log.Errorf("Login 2FA: authethication incomplete %p %p", s.auth.UID, s.authClient) + _ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ABORT, "Missing authentication, try again.")) + s.loginClean() + return + } + + twoFA, err := base64Decode(login.Password) + if err != nil { + s.log.WithError(err).Error("Cannot decode 2fa code") + _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot decode 2fa code")) + s.loginClean() + return + } + + if err := s.authClient.Auth2FA(context.Background(), liteapi.Auth2FAReq{TwoFactorCode: string(twoFA)}); err != nil { + switch { + case errors.Is(err, liteapi.ErrBad2FACode): + s.log.Warn("Login 2FA: retry 2fa") + _ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ERROR, "")) + + default: + s.log.WithError(err).Warn("Login 2FA: failed") + _ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ABORT, err.Error())) + s.loginClean() + } + + return + } + + if s.auth.PasswordMode == liteapi.TwoPasswordMode { + _ = s.SendEvent(NewLoginTwoPasswordsRequestedEvent()) + return + } + + s.finishLogin() + }() + + return &emptypb.Empty{}, nil } -func (s *Service) Login2Passwords(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) { - panic("TODO") +func (s *Service) Login2Passwords(ctx context.Context, login *LoginRequest) (*emptypb.Empty, error) { + s.log.WithField("username", login.Username).Debug("Login2Passwords") + + go func() { + defer s.panicHandler.HandlePanic() + + password, err := base64Decode(login.Password) + if err != nil { + s.log.WithError(err).Error("Cannot decode mbox password") + _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot decode mbox password")) + s.loginClean() + return + } + + s.password = password + + s.finishLogin() + }() + + return &emptypb.Empty{}, nil } -func (s *Service) LoginAbort(_ context.Context, loginAbort *LoginAbortRequest) (*emptypb.Empty, error) { - panic("TODO") +func (s *Service) LoginAbort(ctx context.Context, loginAbort *LoginAbortRequest) (*emptypb.Empty, error) { + s.log.WithField("username", loginAbort.Username).Debug("LoginAbort") + + go func() { + defer s.panicHandler.HandlePanic() + + s.loginAbort() + }() + + return &emptypb.Empty{}, nil } /*