hydroxide-push/protonmail/auth.go

321 lines
6.4 KiB
Go
Raw Normal View History

2017-08-22 01:04:16 +03:00
package protonmail
import (
"encoding/base64"
"fmt"
2018-10-28 17:52:57 +02:00
"log"
2017-08-22 01:04:16 +03:00
"net/http"
"time"
2017-08-22 01:04:16 +03:00
2017-08-22 11:36:43 +03:00
"golang.org/x/crypto/openpgp"
2017-09-03 21:11:01 +03:00
"golang.org/x/crypto/openpgp/packet"
2017-08-22 01:04:16 +03:00
)
type authInfoReq struct {
2019-12-09 13:27:16 +02:00
Username string
2017-08-22 01:04:16 +03:00
}
2017-08-22 10:21:50 +03:00
type AuthInfo struct {
2017-08-22 11:16:20 +03:00
version int
modulus string
2017-08-22 10:21:50 +03:00
serverEphemeral string
2017-08-22 11:16:20 +03:00
salt string
srpSession string
2017-08-22 10:21:50 +03:00
}
2017-08-22 01:04:16 +03:00
type AuthInfoResp struct {
resp
2017-08-22 10:21:50 +03:00
AuthInfo
2017-08-22 11:16:20 +03:00
Version int
Modulus string
2017-08-22 01:04:16 +03:00
ServerEphemeral string
2017-08-22 11:16:20 +03:00
Salt string
SRPSession string
2017-08-22 01:04:16 +03:00
}
2017-08-22 10:21:50 +03:00
func (resp *AuthInfoResp) authInfo() *AuthInfo {
info := &resp.AuthInfo
info.version = resp.Version
info.modulus = resp.Modulus
info.serverEphemeral = resp.ServerEphemeral
info.salt = resp.Salt
info.srpSession = resp.SRPSession
return info
2017-08-22 01:04:16 +03:00
}
func (c *Client) AuthInfo(username string) (*AuthInfo, error) {
reqData := &authInfoReq{
2019-12-09 13:27:16 +02:00
Username: username,
2017-08-22 01:04:16 +03:00
}
req, err := c.newJSONRequest(http.MethodPost, "/auth/info", reqData)
if err != nil {
return nil, err
}
var respData AuthInfoResp
if err := c.doJSON(req, &respData); err != nil {
return nil, err
}
2017-08-22 10:21:50 +03:00
return respData.authInfo(), nil
2017-08-22 01:04:16 +03:00
}
type authReq struct {
2017-08-22 11:16:20 +03:00
Username string
SRPSession string
2017-08-22 01:04:16 +03:00
ClientEphemeral string
2017-08-22 11:16:20 +03:00
ClientProof string
2017-08-22 01:04:16 +03:00
}
type PasswordMode int
const (
PasswordSingle PasswordMode = 1
2017-08-22 11:16:20 +03:00
PasswordTwo = 2
2017-08-22 01:04:16 +03:00
)
2017-08-22 10:41:47 +03:00
type Auth struct {
ExpiresAt time.Time
2017-08-22 11:16:20 +03:00
Scope string
UID string
AccessToken string
2017-08-22 01:04:16 +03:00
RefreshToken string
UserID string
2017-08-22 11:16:20 +03:00
EventID string
2017-08-22 01:04:16 +03:00
PasswordMode PasswordMode
2019-12-09 13:27:16 +02:00
TwoFactor struct {
Enabled int
2019-12-09 13:27:16 +02:00
U2F interface{} // TODO
TOTP int
} `json:"2FA"`
2017-08-22 10:41:47 +03:00
}
type authResp struct {
resp
Auth
ExpiresIn int
2017-08-22 11:36:43 +03:00
TokenType string
2017-08-22 10:41:47 +03:00
ServerProof string
2017-08-22 01:04:16 +03:00
}
2017-08-22 10:41:47 +03:00
func (resp *authResp) auth() *Auth {
auth := &resp.Auth
auth.ExpiresAt = time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second)
2017-08-22 10:41:47 +03:00
return auth
}
func (c *Client) Auth(username, password string, info *AuthInfo) (*Auth, error) {
2017-08-22 01:04:16 +03:00
if info == nil {
var err error
if info, err = c.AuthInfo(username); err != nil {
2017-08-22 10:41:47 +03:00
return nil, err
2017-08-22 01:04:16 +03:00
}
}
proofs, err := srp([]byte(password), info)
if err != nil {
return nil, fmt.Errorf("SRP failed during auth: %v", err)
2017-08-22 01:04:16 +03:00
}
reqData := &authReq{
2017-08-22 11:16:20 +03:00
Username: username,
SRPSession: info.srpSession,
2017-08-22 01:04:16 +03:00
ClientEphemeral: base64.StdEncoding.EncodeToString(proofs.clientEphemeral),
2017-08-22 11:16:20 +03:00
ClientProof: base64.StdEncoding.EncodeToString(proofs.clientProof),
2017-08-22 01:04:16 +03:00
}
req, err := c.newJSONRequest(http.MethodPost, "/auth", reqData)
if err != nil {
2017-08-22 10:41:47 +03:00
return nil, err
2017-08-22 01:04:16 +03:00
}
var respData authResp
if err := c.doJSON(req, &respData); err != nil {
2017-08-22 10:41:47 +03:00
return nil, err
2017-08-22 01:04:16 +03:00
}
if err := proofs.VerifyServerProof(respData.ServerProof); err != nil {
2017-08-22 10:41:47 +03:00
return nil, err
2017-08-22 01:04:16 +03:00
}
auth := respData.auth()
c.uid = auth.UID
c.accessToken = auth.AccessToken
return auth, nil
2017-08-22 01:04:16 +03:00
}
2017-08-22 11:36:43 +03:00
func (c *Client) AuthTOTP(code string) (scope string, err error) {
reqData := struct {
TwoFactorCode string
}{
TwoFactorCode: code,
}
req, err := c.newJSONRequest(http.MethodPost, "/auth/2fa", reqData)
if err != nil {
return "", err
}
respData := struct {
resp
Scope string
}{}
if err := c.doJSON(req, &respData); err != nil {
return "", err
}
return respData.Scope, nil
}
2017-08-22 11:51:48 +03:00
type authRefreshReq struct {
RefreshToken string
2017-08-22 13:07:31 +03:00
// Unused but required
ResponseType string
2017-08-22 14:22:27 +03:00
GrantType string
RedirectURI string
2017-08-22 11:51:48 +03:00
}
func (c *Client) AuthRefresh(expiredAuth *Auth) (*Auth, error) {
2017-08-22 11:51:48 +03:00
reqData := &authRefreshReq{
2017-08-22 14:22:27 +03:00
RefreshToken: expiredAuth.RefreshToken,
ResponseType: "token",
GrantType: "refresh_token",
RedirectURI: "http://www.protonmail.ch",
2017-08-22 11:51:48 +03:00
}
req, err := c.newJSONRequest(http.MethodPost, "/auth/refresh", reqData)
if err != nil {
return nil, err
}
req.Header.Set("X-Pm-Uid", expiredAuth.UID)
2017-08-22 11:51:48 +03:00
var respData authResp
if err := c.doJSON(req, &respData); err != nil {
return nil, err
}
auth := respData.auth()
//auth.EventID = expiredAuth.EventID
auth.PasswordMode = expiredAuth.PasswordMode
return auth, nil
2017-08-22 11:51:48 +03:00
}
func (c *Client) ListKeySalts() (map[string][]byte, error) {
req, err := c.newRequest(http.MethodGet, "/keys/salts", nil)
if err != nil {
return nil, err
}
var respData struct {
resp
KeySalts []struct {
2019-12-09 13:27:16 +02:00
ID string
KeySalt string
}
}
if err := c.doJSON(req, &respData); err != nil {
return nil, err
}
salts := make(map[string][]byte, len(respData.KeySalts))
for _, salt := range respData.KeySalts {
if salt.KeySalt == "" {
salts[salt.ID] = nil
continue
}
payload, err := base64.StdEncoding.DecodeString(salt.KeySalt)
if err != nil {
return nil, fmt.Errorf("failed to decode key salt payload: %v", err)
}
salts[salt.ID] = payload
}
return salts, nil
}
2018-10-28 17:52:57 +02:00
func unlockKey(e *openpgp.Entity, passphraseBytes []byte) error {
var privateKeys []*packet.PrivateKey
// e.PrivateKey is a signing key
if e.PrivateKey != nil {
privateKeys = append(privateKeys, e.PrivateKey)
}
// e.Subkeys are encryption keys
for _, subkey := range e.Subkeys {
if subkey.PrivateKey != nil {
privateKeys = append(privateKeys, subkey.PrivateKey)
}
}
for _, priv := range privateKeys {
if err := priv.Decrypt(passphraseBytes); err != nil {
return err
}
}
return nil
}
func (c *Client) Unlock(auth *Auth, keySalts map[string][]byte, passphrase string) (openpgp.EntityList, error) {
c.uid = auth.UID
c.accessToken = auth.AccessToken
2017-09-03 21:11:01 +03:00
addrs, err := c.ListAddresses()
2017-09-03 21:11:01 +03:00
if err != nil {
return nil, err
2017-08-22 11:36:43 +03:00
}
var keyRing openpgp.EntityList
2018-10-28 17:52:57 +02:00
for _, addr := range addrs {
for _, key := range addr.Keys {
entity, err := key.Entity()
if err != nil {
log.Printf("warning: failed to read key %q: %v", addr.Email, err)
continue
}
2018-10-28 17:52:57 +02:00
passphraseBytes := []byte(passphrase)
if keySalt, ok := keySalts[key.ID]; ok && keySalt != nil {
passphraseBytes, err = computeKeyPassword(passphraseBytes, keySalt)
if err != nil {
return nil, err
2018-10-28 17:52:57 +02:00
}
}
2018-10-28 17:52:57 +02:00
if err := unlockKey(entity, passphraseBytes); err != nil {
2019-04-14 19:07:03 +03:00
log.Printf("warning: failed to unlock key %q %v: %v", addr.Email, entity.PrimaryKey.KeyIdString(), err)
2018-10-28 17:52:57 +02:00
continue
}
2018-10-28 17:52:57 +02:00
keyRing = append(keyRing, entity)
}
}
if len(keyRing) == 0 {
return nil, fmt.Errorf("failed to unlock any key")
}
c.keyRing = keyRing
2017-08-22 11:36:43 +03:00
return keyRing, nil
}
2017-08-22 12:19:21 +03:00
func (c *Client) Logout() error {
req, err := c.newRequest(http.MethodDelete, "/auth", nil)
if err != nil {
return err
}
if err := c.doJSON(req, nil); err != nil {
return err
}
c.uid = ""
c.accessToken = ""
c.keyRing = nil
return nil
}