mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
368 lines
11 KiB
Go
368 lines
11 KiB
Go
// Copyright (c) 2020 Proton Technologies AG
|
|
//
|
|
// This file is part of ProtonMail Bridge.
|
|
//
|
|
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package pmapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"math/rand"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
|
|
"github.com/ProtonMail/proton-bridge/pkg/srp"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
a "github.com/stretchr/testify/assert"
|
|
r "github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var aLongTimeAgo = time.Unix(233431200, 0)
|
|
|
|
var testIdentity = &pmcrypto.Identity{
|
|
Name: "UserID",
|
|
Email: "",
|
|
}
|
|
|
|
const (
|
|
testUsername = "jason"
|
|
testAPIPassword = "apple"
|
|
|
|
testUID = "729ad6012421d67ad26950dc898bebe3a6e3caa2" //nolint[gosec]
|
|
testAccessToken = "de0423049b44243afeec7d9c1d99be7b46da1e8a" //nolint[gosec]
|
|
testAccessTokenOld = "feb3159ac63fb05119bcf4480d939278aa746926" //nolint[gosec]
|
|
testRefreshToken = "a49b98256745bb497bec20e9b55f5de16f01fb52" //nolint[gosec]
|
|
testRefreshTokenNew = "b894b4c4f20003f12d486900d8b88c7d68e67235" //nolint[gosec]
|
|
)
|
|
|
|
var testAuthInfo = &AuthInfo{
|
|
TwoFA: &TwoFactorInfo{TOTP: 1},
|
|
|
|
version: 4,
|
|
salt: "yKlc5/CvObfoiw==",
|
|
modulus: "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nW2z5HBi8RvsfYzZTS7qBaUxxPhsfHJFZpu3Kd6s1JafNrCCH9rfvPLrfuqocxWPgWDH2R8neK7PkNvjxto9TStuY5z7jAzWRvFWN9cQhAKkdWgy0JY6ywVn22+HFpF4cYesHrqFIKUPDMSSIlWjBVmEJZ/MusD44ZT29xcPrOqeZvwtCffKtGAIjLYPZIEbZKnDM1Dm3q2K/xS5h+xdhjnndhsrkwm9U9oyA2wxzSXFL+pdfj2fOdRwuR5nW0J2NFrq3kJjkRmpO/Genq1UW+TEknIWAb6VzJJJA244K/H8cnSx2+nSNZO3bbo6Ys228ruV9A8m6DhxmS+bihN3ttQ==\n-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwl4EARYIABAFAlwB1j0JEDUFhcTpUY8mAAD8CgEAnsFnF4cF0uSHKkXa1GIa\nGO86yMV4zDZEZcDSJo0fgr8A/AlupGN9EdHlsrZLmTA1vhIx+rOgxdEff28N\nkvNM7qIK\n=q6vu\n-----END PGP SIGNATURE-----\n",
|
|
srpSession: "9b2946bbd9055f17c34940abdce0c3d3",
|
|
serverEphemeral: "5tfigcLKoM0DPWYB+EqYE7QlqsiT63iOVlO5ZX0lTMEILSsrRdVCYrN8L3zkinsAjUZ/cx5wIS7N05k66uZb+ZE3lFOJS2s1BkqLvCrGxYL0e3n5YAnzHYlvCCJKXw/sK57ntfF1OOoblBXX6dw5LjeeDglEep2/DaE0TjD8WUpq4Ls2HlQGn9wrC7dFO2lJXsMhRffxKghiOsdvCLXDmwXginzn/LFezA8KrDsWOBSEGntwpg3s1xFj5h8BqtRHvC0igmoscqgw+3GCMTJ0NZAQ/L+5aJ/0ccL0WBK208ltCNl+/X6Sz0kpyvOP4RqFJhC1auVDJ9AjZQYSYZ1NEQ==",
|
|
}
|
|
|
|
// testAuth has default values which are adjusted in each test.
|
|
var testAuth = &Auth{
|
|
EventID: "NcKPtU5eMNPMrDkIMbEJrgMtC9yQ7Xc5ZBT-tB3UtV1rZ324RWfCIdBI758q0UnsfywS8CkNenIQlWLIX_dUng==",
|
|
ExpiresIn: 86400,
|
|
RefreshToken: "feb3159ac63fb05119bcf4480d939278aa746926",
|
|
Scope: "full mail payments reset keys",
|
|
|
|
accessToken: testAccessToken,
|
|
uid: testUID,
|
|
}
|
|
|
|
var testAuth2FA = &Auth2FA{
|
|
Scope: "full mail payments reset keys",
|
|
}
|
|
|
|
var testAuthRefreshReq = AuthRefreshReq{
|
|
ResponseType: "token",
|
|
GrantType: "refresh_token",
|
|
RefreshToken: testRefreshToken,
|
|
UID: testUID,
|
|
RedirectURI: "https://protonmail.ch",
|
|
State: "random_string",
|
|
}
|
|
|
|
var testAuthReq = AuthReq{
|
|
Username: testUsername,
|
|
ClientProof: "axfvYdl9iXZjY6zQ+hBYmY7X3TDc/9JtSvrmyZXhDxjxkXB3Hro27t1KItmFIJloItY5sLZDs0eEEZJI34oFZD4ViSG0kfB7ZXcCZ9Jse+U5OFu4vdnPTGolnSofRMEs1NR6ePXzH7mQ10qoq43ity3ve2vmhQNuJNlHAPynKf2WqKOgxq7mmkBzEpXES4mIhwwgVbOygKcUSvguz5E5g13ATF0ZX2d9SJWAbZ262Tks+h99Cdk/dOfgLQhr0nO/r0cpwP84W2RWU2Q34LNkKuuQHkjmxelgBleGq54tCbhoCAYPP6vapgrQjNoVAC/dkjIIAoNL9bJSIynFM5znAA==",
|
|
ClientEphemeral: "mK+eSMosfZO/Cs5s+vcbjpsN7F8UAObwlKKnCy/z9FpoMRM2PfTe5ywLBgffmLYaapPq7XOxaqaj08kcZLHcM1fIA2JQZZTKPnESN1qAQztJ3/YHMI0op6yBgzx9803OjIznjCD2B3XBSMOHIG4oG0UwocsIX32hiMnYlMMkt8NGrityPlnmEbxpRna3fu9LEZ+v0uo6PjKCrO7+9E3uaMi64HadXBfyx2raBFFwA+yh7FvE7U+hl3AJclEre4d8pmfhMdxXze1soJI8fMuqaa07rY0r0rF5mLLTuqTIGRFkU1qG9loq9+IMsSwgkt1P3ghW63JK7Y6LWdDy0d6cAg==",
|
|
SRPSession: "9b2946bbd9055f17c34940abdce0c3d3",
|
|
}
|
|
|
|
var testAuth2FAReq = Auth2FAReq{
|
|
TwoFactorCode: "424242",
|
|
}
|
|
|
|
func init() {
|
|
logrus.SetLevel(logrus.DebugLevel)
|
|
srp.RandReader = rand.New(rand.NewSource(42))
|
|
}
|
|
|
|
func TestClient_AuthInfo(t *testing.T) {
|
|
finish, c := newTestServerCallbacks(t,
|
|
func(tb testing.TB, w http.ResponseWriter, r *http.Request) string {
|
|
Ok(t, checkMethodAndPath(r, "POST", "/auth/info"))
|
|
|
|
var infoReq AuthInfoReq
|
|
Ok(t, json.NewDecoder(r.Body).Decode(&infoReq))
|
|
Equals(t, infoReq.Username, testUsername)
|
|
|
|
return "/auth/info/post_response.json"
|
|
},
|
|
)
|
|
defer finish()
|
|
|
|
info, err := c.AuthInfo(testCurrentUser.Name)
|
|
Ok(t, err)
|
|
Equals(t, testAuthInfo, info)
|
|
}
|
|
|
|
// TestClient_Auth reflects changes from proton/backend-communcation#3.
|
|
func TestClient_Auth(t *testing.T) {
|
|
srp.RandReader = rand.New(rand.NewSource(42))
|
|
finish, c := newTestServerCallbacks(t,
|
|
func(tb testing.TB, w http.ResponseWriter, req *http.Request) string {
|
|
a.Nil(t, checkMethodAndPath(req, "POST", "/auth"))
|
|
|
|
var authReq AuthReq
|
|
r.Nil(t, json.NewDecoder(req.Body).Decode(&authReq))
|
|
r.Equal(t, testAuthReq, authReq)
|
|
|
|
return "/auth/post_response.json"
|
|
},
|
|
routeGetUsers,
|
|
routeGetAddresses,
|
|
routeGetSalts,
|
|
)
|
|
defer finish()
|
|
|
|
auth, err := c.Auth(testUsername, testAPIPassword, testAuthInfo)
|
|
r.Nil(t, err)
|
|
|
|
r.True(t, c.user.KeyRing().FirstKeyID != "", "Parsing First key ID issue")
|
|
|
|
exp := &Auth{}
|
|
*exp = *testAuth
|
|
exp.accessToken = testAccessToken
|
|
exp.RefreshToken = testRefreshToken
|
|
exp.KeySalt = "abc"
|
|
a.Equal(t, exp, auth)
|
|
}
|
|
|
|
func TestClient_Auth2FA(t *testing.T) {
|
|
finish, c := newTestServerCallbacks(t,
|
|
func(tb testing.TB, w http.ResponseWriter, r *http.Request) string {
|
|
Ok(t, checkMethodAndPath(r, "POST", "/auth/2fa"))
|
|
|
|
var info2FAReq Auth2FAReq
|
|
Ok(t, json.NewDecoder(r.Body).Decode(&info2FAReq))
|
|
Equals(t, info2FAReq.TwoFactorCode, testAuth2FAReq.TwoFactorCode)
|
|
|
|
return "/auth/2fa/post_response.json"
|
|
},
|
|
routeGetUsers,
|
|
routeGetAddresses,
|
|
routeGetSalts,
|
|
)
|
|
defer finish()
|
|
|
|
c.uid = testUID
|
|
c.accessToken = testAccessToken
|
|
auth2FA, err := c.Auth2FA(testAuth2FAReq.TwoFactorCode, testAuth)
|
|
Ok(t, err)
|
|
|
|
Equals(t, testAuth2FA, auth2FA)
|
|
}
|
|
|
|
func TestClient_Auth2FA_Fail(t *testing.T) {
|
|
finish, c := newTestServerCallbacks(t,
|
|
func(tb testing.TB, w http.ResponseWriter, r *http.Request) string {
|
|
Ok(t, checkMethodAndPath(r, "POST", "/auth/2fa"))
|
|
|
|
var info2FAReq Auth2FAReq
|
|
Ok(t, json.NewDecoder(r.Body).Decode(&info2FAReq))
|
|
Equals(t, info2FAReq.TwoFactorCode, testAuth2FAReq.TwoFactorCode)
|
|
|
|
return "/auth/2fa/post_401_bad_password.json"
|
|
},
|
|
)
|
|
defer finish()
|
|
|
|
c.uid = testUID
|
|
c.accessToken = testAccessToken
|
|
_, err := c.Auth2FA(testAuth2FAReq.TwoFactorCode, testAuth)
|
|
Equals(t, ErrBad2FACode, err)
|
|
}
|
|
|
|
func TestClient_Auth2FA_Retry(t *testing.T) {
|
|
finish, c := newTestServerCallbacks(t,
|
|
func(tb testing.TB, w http.ResponseWriter, r *http.Request) string {
|
|
Ok(t, checkMethodAndPath(r, "POST", "/auth/2fa"))
|
|
|
|
var info2FAReq Auth2FAReq
|
|
Ok(t, json.NewDecoder(r.Body).Decode(&info2FAReq))
|
|
Equals(t, info2FAReq.TwoFactorCode, testAuth2FAReq.TwoFactorCode)
|
|
|
|
return "/auth/2fa/post_422_bad_password.json"
|
|
},
|
|
)
|
|
defer finish()
|
|
|
|
c.uid = testUID
|
|
c.accessToken = testAccessToken
|
|
_, err := c.Auth2FA(testAuth2FAReq.TwoFactorCode, testAuth)
|
|
Equals(t, ErrBad2FACodeTryAgain, err)
|
|
}
|
|
|
|
func TestClient_Unlock(t *testing.T) {
|
|
finish, c := newTestServerCallbacks(t,
|
|
routeGetUsers,
|
|
routeGetAddresses,
|
|
)
|
|
defer finish()
|
|
c.uid = testUID
|
|
c.accessToken = testAccessToken
|
|
|
|
_, err := c.Unlock("wrong")
|
|
a.True(t, IsUnlockError(err), "expected error, pasword is wrong")
|
|
|
|
_, err = c.Unlock(testMailboxPassword)
|
|
a.Nil(t, err)
|
|
a.Equal(t, testUID, c.uid)
|
|
a.Equal(t, testAccessToken, c.accessToken)
|
|
|
|
// second try should not fail because there is an unlocked key already
|
|
_, err = c.Unlock("wrong")
|
|
a.Nil(t, err)
|
|
}
|
|
|
|
func TestClient_Unlock_EncPrivKey(t *testing.T) {
|
|
finish, c := newTestServerCallbacks(t,
|
|
routeGetUsers,
|
|
routeGetAddresses,
|
|
)
|
|
defer finish()
|
|
c.uid = testUID
|
|
c.accessToken = testAccessToken
|
|
|
|
_, err := c.Unlock(testMailboxPassword)
|
|
Ok(t, err)
|
|
Equals(t, testUID, c.uid)
|
|
Equals(t, testAccessToken, c.accessToken)
|
|
}
|
|
|
|
func routeAuthRefresh(tb testing.TB, w http.ResponseWriter, r *http.Request) string {
|
|
Ok(tb, checkMethodAndPath(r, "POST", "/auth/refresh"))
|
|
Ok(tb, checkHeader(r.Header, "x-pm-uid", testUID))
|
|
|
|
var refreshReq AuthRefreshReq
|
|
Ok(tb, json.NewDecoder(r.Body).Decode(&refreshReq))
|
|
Equals(tb, testAuthRefreshReq, refreshReq)
|
|
|
|
return "/auth/refresh/post_response.json"
|
|
}
|
|
|
|
// TestClient_AuthRefresh reflects changes from proton/backend-communcation#11.
|
|
func TestClient_AuthRefresh(t *testing.T) {
|
|
finish, c := newTestServerCallbacks(t,
|
|
routeAuthRefresh,
|
|
)
|
|
defer finish()
|
|
c.uid = "" // Testing that we always send correct `x-pm-uid`.
|
|
c.accessToken = "oldToken"
|
|
|
|
auth, err := c.AuthRefresh(testUID + ":" + testRefreshToken)
|
|
Ok(t, err)
|
|
|
|
exp := &Auth{}
|
|
*exp = *testAuth
|
|
exp.accessToken = testAccessToken
|
|
exp.KeySalt = ""
|
|
exp.EventID = ""
|
|
exp.ExpiresIn = 360000
|
|
exp.RefreshToken = testRefreshTokenNew
|
|
Equals(t, exp, auth)
|
|
}
|
|
|
|
func routeAuthRefreshHasUID(tb testing.TB, w http.ResponseWriter, r *http.Request) string {
|
|
Ok(tb, checkMethodAndPath(r, "POST", "/auth/refresh"))
|
|
Ok(tb, checkHeader(r.Header, "x-pm-uid", testUID))
|
|
|
|
var refreshReq AuthRefreshReq
|
|
Ok(tb, json.NewDecoder(r.Body).Decode(&refreshReq))
|
|
Equals(tb, testAuthRefreshReq, refreshReq)
|
|
|
|
return "/auth/refresh/post_resp_has_uid.json"
|
|
}
|
|
|
|
// TestClient_AuthRefresh reflects changes from proton/backend-communcation#3.
|
|
func TestClient_AuthRefresh_HasUID(t *testing.T) {
|
|
finish, c := newTestServerCallbacks(t,
|
|
routeAuthRefreshHasUID,
|
|
)
|
|
defer finish()
|
|
c.uid = testUID
|
|
c.accessToken = "oldToken"
|
|
|
|
auth, err := c.AuthRefresh(testUID + ":" + testRefreshToken)
|
|
Ok(t, err)
|
|
|
|
exp := &Auth{}
|
|
*exp = *testAuth
|
|
exp.accessToken = testAccessToken
|
|
exp.KeySalt = ""
|
|
exp.EventID = ""
|
|
exp.ExpiresIn = 360000
|
|
exp.RefreshToken = testRefreshTokenNew
|
|
Equals(t, exp, auth)
|
|
}
|
|
|
|
func TestClient_Logout(t *testing.T) {
|
|
finish, c := newTestServerCallbacks(t,
|
|
func(tb testing.TB, w http.ResponseWriter, r *http.Request) string {
|
|
Ok(t, checkMethodAndPath(r, "DELETE", "/auth"))
|
|
Ok(t, isAuthReq(r, testUID, testAccessToken))
|
|
return "auth/delete_response.json"
|
|
},
|
|
)
|
|
defer finish()
|
|
c.uid = testUID
|
|
c.accessToken = testAccessToken
|
|
|
|
c.Logout()
|
|
|
|
// TODO: Check that the client is logged out and sensitive data is cleared eventually.
|
|
}
|
|
|
|
func TestClient_DoUnauthorized(t *testing.T) {
|
|
finish, c := newTestServerCallbacks(t,
|
|
func(tb testing.TB, w http.ResponseWriter, r *http.Request) string {
|
|
Ok(t, checkMethodAndPath(r, "GET", "/"))
|
|
return httpResponse(http.StatusUnauthorized)
|
|
},
|
|
routeAuthRefresh,
|
|
func(tb testing.TB, w http.ResponseWriter, r *http.Request) string {
|
|
Ok(t, checkMethodAndPath(r, "GET", "/"))
|
|
Ok(t, isAuthReq(r, testUID, testAccessToken))
|
|
return httpResponse(http.StatusOK)
|
|
},
|
|
)
|
|
defer finish()
|
|
|
|
c.uid = testUID
|
|
c.accessToken = testAccessTokenOld
|
|
c.expiresAt = aLongTimeAgo
|
|
c.cm.tokens[c.userID] = testUID + ":" + testRefreshToken
|
|
|
|
req, err := c.NewRequest("GET", "/", nil)
|
|
Ok(t, err)
|
|
|
|
res, err := c.Do(req, true)
|
|
Ok(t, err)
|
|
|
|
defer Ok(t, res.Body.Close())
|
|
}
|