Add authentication support
This commit is contained in:
parent
0fcdcadc0c
commit
dac767c4bf
|
@ -0,0 +1,38 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/emersion/hydroxide/protonmail"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &protonmail.Client{
|
||||||
|
RootURL: "https://dev.protonmail.com/api",
|
||||||
|
AppVersion: "Web_3.11.1",
|
||||||
|
ClientID: "Web",
|
||||||
|
ClientSecret: "4957cc9a2e0a2a49d02475c9d013478d",
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
|
||||||
|
fmt.Printf("Username: ")
|
||||||
|
scanner.Scan()
|
||||||
|
username := scanner.Text()
|
||||||
|
|
||||||
|
fmt.Printf("Password: ")
|
||||||
|
scanner.Scan()
|
||||||
|
password := scanner.Text()
|
||||||
|
|
||||||
|
fmt.Printf("2FA code: ")
|
||||||
|
scanner.Scan()
|
||||||
|
code := scanner.Text()
|
||||||
|
|
||||||
|
err := c.Auth(username, password, code, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
package protonmail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authInfoReq struct {
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthInfoResp struct {
|
||||||
|
resp
|
||||||
|
Version int
|
||||||
|
TwoFactor int
|
||||||
|
Modulus string
|
||||||
|
ServerEphemeral string
|
||||||
|
Salt string
|
||||||
|
SRPSession string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthInfo struct {
|
||||||
|
TwoFactor int
|
||||||
|
version int
|
||||||
|
modulus string
|
||||||
|
serverEphemeral string
|
||||||
|
salt string
|
||||||
|
srpSession string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) AuthInfo(username string) (*AuthInfo, error) {
|
||||||
|
reqData := &authInfoReq{
|
||||||
|
ClientID: c.ClientID,
|
||||||
|
ClientSecret: c.ClientSecret,
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AuthInfo{
|
||||||
|
TwoFactor: respData.TwoFactor,
|
||||||
|
version: respData.Version,
|
||||||
|
modulus: respData.Modulus,
|
||||||
|
serverEphemeral: respData.ServerEphemeral,
|
||||||
|
salt: respData.Salt,
|
||||||
|
srpSession: respData.SRPSession,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type authReq struct {
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
Username string
|
||||||
|
SRPSession string
|
||||||
|
ClientEphemeral string
|
||||||
|
ClientProof string
|
||||||
|
TwoFactorCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PasswordMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PasswordSingle PasswordMode = 1
|
||||||
|
PasswordTwo = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type authResp struct {
|
||||||
|
resp
|
||||||
|
AccessToken string
|
||||||
|
ExpiresIn int
|
||||||
|
TokenType string
|
||||||
|
Scope string
|
||||||
|
UID string `json:"Uid"`
|
||||||
|
RefreshToken string
|
||||||
|
EventID string
|
||||||
|
ServerProof string
|
||||||
|
PasswordMode PasswordMode
|
||||||
|
PrivateKey string
|
||||||
|
KeySalt string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Auth(username, password, twoFactorCode string, info *AuthInfo) error {
|
||||||
|
if info == nil {
|
||||||
|
var err error
|
||||||
|
if info, err = c.AuthInfo(username); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%#v\n", info)
|
||||||
|
|
||||||
|
proofs, err := srp([]byte(password), info)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqData := &authReq{
|
||||||
|
ClientID: c.ClientID,
|
||||||
|
ClientSecret: c.ClientSecret,
|
||||||
|
Username: username,
|
||||||
|
SRPSession: info.srpSession,
|
||||||
|
ClientEphemeral: base64.StdEncoding.EncodeToString(proofs.clientEphemeral),
|
||||||
|
ClientProof: base64.StdEncoding.EncodeToString(proofs.clientProof),
|
||||||
|
TwoFactorCode: twoFactorCode,
|
||||||
|
}
|
||||||
|
log.Printf("%#v\n", reqData)
|
||||||
|
|
||||||
|
req, err := c.newJSONRequest(http.MethodPost, "/auth", reqData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var respData authResp
|
||||||
|
if err := c.doJSON(req, &respData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%#v\n", respData)
|
||||||
|
|
||||||
|
if err := proofs.VerifyServerProof(respData.ServerProof); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
// Package protonmail implements a ProtonMail API client.
|
||||||
|
package protonmail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Version = 2
|
||||||
|
|
||||||
|
const headerAPIVersion = "X-Pm-Apiversion"
|
||||||
|
|
||||||
|
type resp struct {
|
||||||
|
Code int
|
||||||
|
*apiError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resp) Err() error {
|
||||||
|
if err := r.apiError; err != nil {
|
||||||
|
return r.apiError
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type maybeError interface {
|
||||||
|
Err() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiError struct {
|
||||||
|
Message string `json:"Error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err apiError) Error() string {
|
||||||
|
return err.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is a ProtonMail API client.
|
||||||
|
type Client struct {
|
||||||
|
HTTPClient *http.Client
|
||||||
|
RootURL string
|
||||||
|
AppVersion string
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) newRequest(method, path string, body io.Reader) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequest(method, c.RootURL + path, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("X-Pm-Appversion", c.AppVersion)
|
||||||
|
req.Header.Set(headerAPIVersion, strconv.Itoa(Version))
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) newJSONRequest(method, path string, body interface{}) (*http.Request, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := json.NewEncoder(&b).Encode(body); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.newRequest(method, path, &b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) do(req *http.Request) (*http.Response, error) {
|
||||||
|
httpClient := c.HTTPClient
|
||||||
|
if httpClient == nil {
|
||||||
|
httpClient = http.DefaultClient
|
||||||
|
}
|
||||||
|
return httpClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) doJSON(req *http.Request, respData interface{}) error {
|
||||||
|
resp, err := c.do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(respData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if maybeError, ok := respData.(maybeError); ok {
|
||||||
|
if err := maybeError.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,215 @@
|
||||||
|
package protonmail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha512"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/emersion/go-bcrypt"
|
||||||
|
"golang.org/x/crypto/openpgp"
|
||||||
|
"golang.org/x/crypto/openpgp/clearsign"
|
||||||
|
openpgperrors "golang.org/x/crypto/openpgp/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var randReader io.Reader = rand.Reader
|
||||||
|
|
||||||
|
func decodeModulus(msg string) ([]byte, error) {
|
||||||
|
block, _ := clearsign.Decode([]byte(msg))
|
||||||
|
if block == nil {
|
||||||
|
return nil, errors.New("invalid modulus signed PGP block")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check signature key
|
||||||
|
_, err := openpgp.CheckDetachedSignature(nil, bytes.NewReader(block.Plaintext), block.ArmoredSignature.Body)
|
||||||
|
if err != nil && err != openpgperrors.ErrUnknownIssuer {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.DecodeString(string(block.Plaintext))
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandHash(b []byte) []byte {
|
||||||
|
var expanded []byte
|
||||||
|
var part [64]byte
|
||||||
|
|
||||||
|
part = sha512.Sum512(append(b, 0))
|
||||||
|
expanded = append(expanded, part[:]...)
|
||||||
|
|
||||||
|
part = sha512.Sum512(append(b, 1))
|
||||||
|
expanded = append(expanded, part[:]...)
|
||||||
|
|
||||||
|
part = sha512.Sum512(append(b, 2))
|
||||||
|
expanded = append(expanded, part[:]...)
|
||||||
|
|
||||||
|
part = sha512.Sum512(append(b, 3))
|
||||||
|
expanded = append(expanded, part[:]...)
|
||||||
|
|
||||||
|
return expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashPassword(version int, password, salt, modulus []byte) ([]byte, error) {
|
||||||
|
switch version {
|
||||||
|
case 3, 4:
|
||||||
|
salt = append(salt, []byte("proton")...)
|
||||||
|
hashed, err := bcrypt.GenerateFromPasswordAndSalt(password, 10, salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashed = bytes.Replace(hashed, []byte("$2a$"), []byte("$2y$"), 1)
|
||||||
|
return expandHash(append([]byte(hashed), modulus...)), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported auth version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reverse(b []byte) {
|
||||||
|
for i := 0; i < len(b)/2; i++ {
|
||||||
|
j := len(b) - 1 - i
|
||||||
|
b[i], b[j] = b[j], b[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func itoa(i *big.Int, l int) []byte {
|
||||||
|
b := i.Bytes()
|
||||||
|
reverse(b)
|
||||||
|
padding := make([]byte, l/8 - len(b))
|
||||||
|
b = append(b, padding...)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func atoi(b []byte) *big.Int {
|
||||||
|
reverse(b)
|
||||||
|
return big.NewInt(0).SetBytes(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
type proofs struct {
|
||||||
|
clientEphemeral []byte
|
||||||
|
clientProof []byte
|
||||||
|
expectedServerProof []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://github.com/ProtonMail/WebClient/blob/public/src/app/authentication/services/srp.js#L13
|
||||||
|
func generateProofs(l int, hash func([]byte) []byte, modulusBytes, hashedBytes, serverEphemeralBytes []byte) (*proofs, error) {
|
||||||
|
generator := big.NewInt(2)
|
||||||
|
|
||||||
|
multiplier := atoi(hash(append(itoa(generator, l), modulusBytes...)))
|
||||||
|
modulus := atoi(modulusBytes)
|
||||||
|
hashed := atoi(hashedBytes)
|
||||||
|
serverEphemeral := atoi(serverEphemeralBytes)
|
||||||
|
|
||||||
|
modulusMinusOne := big.NewInt(0).Sub(modulus, big.NewInt(1))
|
||||||
|
if modulus.BitLen() != l {
|
||||||
|
return nil, errors.New("SRP modulus has incorrect size")
|
||||||
|
}
|
||||||
|
|
||||||
|
multiplier = multiplier.Mod(multiplier, modulus)
|
||||||
|
|
||||||
|
if multiplier.Cmp(big.NewInt(1)) <= 0 || multiplier.Cmp(modulusMinusOne) >= 0 {
|
||||||
|
return nil, errors.New("SRP multiplier is out of bounds")
|
||||||
|
}
|
||||||
|
if generator.Cmp(big.NewInt(1)) <= 0 || generator.Cmp(modulusMinusOne) >= 0 {
|
||||||
|
return nil, errors.New("SRP generator is out of bounds")
|
||||||
|
}
|
||||||
|
if serverEphemeral.Cmp(big.NewInt(1)) <= 0 || serverEphemeral.Cmp(modulusMinusOne) >= 0 {
|
||||||
|
return nil, errors.New("SRP server ephemeral is out of bounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check primality
|
||||||
|
// TODO: Check safe primality
|
||||||
|
|
||||||
|
var clientSecret, clientEphemeral, scramblingParam *big.Int
|
||||||
|
for {
|
||||||
|
for {
|
||||||
|
var err error
|
||||||
|
clientSecret, err = rand.Int(randReader, modulusMinusOne)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientSecret.Cmp(big.NewInt(int64(l)*2)) <= 0 { // Very unlikely
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
clientEphemeral = big.NewInt(0).Exp(generator, clientSecret, modulus)
|
||||||
|
scramblingParam = atoi(hash(append(itoa(clientEphemeral, l), itoa(serverEphemeral, l)...)))
|
||||||
|
if scramblingParam.Cmp(big.NewInt(0)) == 0 { // Very unlikely
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
subtracted := big.NewInt(0).Sub(serverEphemeral, big.NewInt(0).Mod(big.NewInt(0).Mul(big.NewInt(0).Exp(generator, hashed, modulus), multiplier), modulus))
|
||||||
|
if subtracted.Cmp(big.NewInt(0)) < 0 {
|
||||||
|
subtracted.Add(subtracted, modulus)
|
||||||
|
}
|
||||||
|
exponent := big.NewInt(0).Mod(big.NewInt(0).Add(big.NewInt(0).Mul(scramblingParam, hashed), clientSecret), modulusMinusOne)
|
||||||
|
sharedSession := big.NewInt(0).Exp(subtracted, exponent, modulus)
|
||||||
|
|
||||||
|
var clientProof []byte
|
||||||
|
clientProof = append(clientProof, itoa(clientEphemeral, l)...)
|
||||||
|
clientProof = append(clientProof, itoa(serverEphemeral, l)...)
|
||||||
|
clientProof = append(clientProof, itoa(sharedSession, l)...)
|
||||||
|
clientProof = hash(clientProof)
|
||||||
|
|
||||||
|
var serverProof []byte
|
||||||
|
serverProof = append(serverProof, itoa(clientEphemeral, l)...)
|
||||||
|
serverProof = append(serverProof, clientProof...)
|
||||||
|
serverProof = append(serverProof, itoa(sharedSession, l)...)
|
||||||
|
serverProof = hash(serverProof)
|
||||||
|
|
||||||
|
return &proofs{
|
||||||
|
clientEphemeral: itoa(clientEphemeral, l),
|
||||||
|
clientProof: clientProof,
|
||||||
|
expectedServerProof: serverProof,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *proofs) VerifyServerProof(serverProofString string) error {
|
||||||
|
serverProof, err := base64.StdEncoding.DecodeString(serverProofString)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare(p.expectedServerProof, serverProof) != 1 {
|
||||||
|
return errors.New("invalid server proof")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://github.com/ProtonMail/WebClient/blob/public/src/app/authentication/services/srp.js#L135
|
||||||
|
func srp(password []byte, info *AuthInfo) (*proofs, error) {
|
||||||
|
modulus, err := decodeModulus(info.modulus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
serverEphemeral, err := base64.StdEncoding.DecodeString(info.serverEphemeral)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
salt, err := base64.StdEncoding.DecodeString(info.salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashed, err := hashPassword(info.version, password, salt, modulus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proofs, err := generateProofs(2048, expandHash, modulus, hashed, serverEphemeral)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return proofs, nil
|
||||||
|
}
|
Loading…
Reference in New Issue