From 43193345fa1388165cbe6fcbe1f954834a42c803 Mon Sep 17 00:00:00 2001 From: Samuel Lorch Date: Wed, 22 Sep 2021 10:11:55 +0200 Subject: [PATCH] Added ServerVerification Functions, Minor Cleanup --- api/auth.go | 69 +-------------------------------------------------- api/client.go | 32 ++++++++++++++++++++++++ api/misc.go | 28 +++++++++++++++++++++ api/verify.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ 6 files changed, 128 insertions(+), 68 deletions(-) create mode 100644 api/verify.go diff --git a/api/auth.go b/api/auth.go index 2f85fbc..5830459 100644 --- a/api/auth.go +++ b/api/auth.go @@ -6,19 +6,12 @@ import ( "fmt" "net/http" "net/url" - "strconv" "strings" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/helper" ) -// PublicKeyReponse the Body of a Public Key Api Request -type PublicKeyReponse struct { - Fingerprint string `json:"fingerprint"` - Keydata string `json:"keydata"` -} - // Login is used for login type Login struct { Auth *GPGAuth `json:"gpg_auth"` @@ -30,24 +23,6 @@ type GPGAuth struct { Token string `json:"user_token_result,omitempty"` } -// TODO add Server Verification Function - -// GetPublicKey gets the Public Key and Fingerprint of the Passbolt instance -func (c *Client) GetPublicKey(ctx context.Context) (string, string, error) { - msg, err := c.DoCustomRequest(ctx, "GET", "auth/verify.json", "v2", nil, nil) - if err != nil { - return "", "", fmt.Errorf("Doing Request: %w", err) - } - - var body PublicKeyReponse - err = json.Unmarshal(msg.Body, &body) - if err != nil { - return "", "", fmt.Errorf("Parsing JSON: %w", err) - } - // TODO check if that Fingerpirnt is actually from the Publickey - return body.Keydata, body.Fingerprint, nil -} - // CheckSession Check to see if you have a Valid Session func (c *Client) CheckSession(ctx context.Context) bool { _, err := c.DoCustomRequest(ctx, "GET", "auth/is-authenticated.json", "v2", nil, nil) @@ -120,24 +95,12 @@ func (c *Client) Login(ctx context.Context) error { return fmt.Errorf("Cannot Find Session Cookie!") } - // You have to get a make GET Request to get the CSRF Token which is Required for Write Operations + // Because of MFA, the custom Request Function now Fetches the CSRF token, we still need the user for his public key apiMsg, err := c.DoCustomRequest(ctx, "GET", "/users/me.json", "v2", nil, nil) if err != nil { return fmt.Errorf("Getting CSRF Token: %w", err) } - // Because of MFA, the custom Request Functin now Fetches the CSRF token, we still need the user for his public key - /* - for _, cookie := range msg.Cookies() { - if cookie.Name == "csrfToken" { - c.csrfToken = *cookie - } - } - - if c.csrfToken.Name == "" { - return fmt.Errorf("Cannot Find csrfToken Cookie!") - }*/ - // Get Users Own Public Key from Server var user User err = json.Unmarshal(apiMsg.Body, &user) @@ -176,33 +139,3 @@ func (c *Client) Logout(ctx context.Context) error { c.csrfToken = http.Cookie{} return nil } - -// GetUserID Gets the ID of the Current User -func (c *Client) GetUserID() string { - return c.userID -} - -func checkAuthTokenFormat(authToken string) error { - splitAuthToken := strings.Split(authToken, "|") - if len(splitAuthToken) != 4 { - return fmt.Errorf("Auth Token Has Wrong amount of Fields") - } - - if splitAuthToken[0] != splitAuthToken[3] { - return fmt.Errorf("Auth Token Version Fields Don't match") - } - - if !strings.HasPrefix(splitAuthToken[0], "gpgauth") { - return fmt.Errorf("Auth Token Version does not start with 'gpgauth'") - } - - length, err := strconv.Atoi(splitAuthToken[1]) - if err != nil { - return fmt.Errorf("Cannot Convert Auth Token Length Field to int: %w", err) - } - - if len(splitAuthToken[2]) != length { - return fmt.Errorf("Auth Token Data Length does not Match Length Field") - } - return nil -} diff --git a/api/client.go b/api/client.go index aeb2559..732cc32 100644 --- a/api/client.go +++ b/api/client.go @@ -39,6 +39,12 @@ type Client struct { Debug bool } +// PublicKeyReponse the Body of a Public Key Api Request +type PublicKeyReponse struct { + Fingerprint string `json:"fingerprint"` + Keydata string `json:"keydata"` +} + // NewClient Returns a new Passbolt Client. // if httpClient is nil http.DefaultClient will be used. // if UserAgent is "" "goPassboltClient/1.0" will be used. @@ -183,3 +189,29 @@ func addOptions(s, version string, opt interface{}) (string, error) { u.RawQuery = vs.Encode() return u.String(), nil } + +// GetUserID Gets the ID of the Current User +func (c *Client) GetUserID() string { + return c.userID +} + +// GetPublicKey gets the Public Key and Fingerprint of the Passbolt instance +func (c *Client) GetPublicKey(ctx context.Context) (string, string, error) { + msg, err := c.DoCustomRequest(ctx, "GET", "auth/verify.json", "v2", nil, nil) + if err != nil { + return "", "", fmt.Errorf("Doing Request: %w", err) + } + + var body PublicKeyReponse + err = json.Unmarshal(msg.Body, &body) + if err != nil { + return "", "", fmt.Errorf("Parsing JSON: %w", err) + } + + // Lets get the actual Fingerprint instead of trusting the Server + privateKeyObj, err := crypto.NewKeyFromArmored(c.userPrivateKey) + if err != nil { + return "", "", fmt.Errorf("Parsing Server Key: %w", err) + } + return body.Keydata, privateKeyObj.GetFingerprint(), nil +} diff --git a/api/misc.go b/api/misc.go index 9990219..99ee9fb 100644 --- a/api/misc.go +++ b/api/misc.go @@ -1,7 +1,10 @@ package api import ( + "fmt" "math/rand" + "strconv" + "strings" ) const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -13,3 +16,28 @@ func randStringBytesRmndr(length int) string { } return string(b) } + +func checkAuthTokenFormat(authToken string) error { + splitAuthToken := strings.Split(authToken, "|") + if len(splitAuthToken) != 4 { + return fmt.Errorf("Auth Token Has Wrong amount of Fields") + } + + if splitAuthToken[0] != splitAuthToken[3] { + return fmt.Errorf("Auth Token Version Fields Don't match") + } + + if !strings.HasPrefix(splitAuthToken[0], "gpgauth") { + return fmt.Errorf("Auth Token Version does not start with 'gpgauth'") + } + + length, err := strconv.Atoi(splitAuthToken[1]) + if err != nil { + return fmt.Errorf("Cannot Convert Auth Token Length Field to int: %w", err) + } + + if len(splitAuthToken[2]) != length { + return fmt.Errorf("Auth Token Data Length does not Match Length Field") + } + return nil +} diff --git a/api/verify.go b/api/verify.go new file mode 100644 index 0000000..c82732c --- /dev/null +++ b/api/verify.go @@ -0,0 +1,64 @@ +package api + +import ( + "context" + "fmt" + "strings" + + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/google/uuid" +) + +type GPGVerifyContainer struct { + Req GPGVerify `json:"gpg_auth"` +} + +// GPGVerify is used for verification +type GPGVerify struct { + KeyID string `json:"keyid"` + Token string `json:"server_verify_token,omitempty"` +} + +func (c *Client) SetupServerVerification(ctx context.Context) (string, string, error) { + serverKey, _, err := c.GetPublicKey(ctx) + if err != nil { + return "", "", fmt.Errorf("Getting Server Key: %w", err) + } + uuid, err := uuid.NewRandom() + if err != nil { + return "", "", fmt.Errorf("Generating UUID: %w", err) + } + token := "gpgauthv1.3.0|36|" + uuid.String() + "|gpgauthv1.3.0" + encToken, err := c.EncryptMessageWithPublicKey(serverKey, token) + if err != nil { + return "", "", fmt.Errorf("Encrypting Challange: %w", err) + } + err = c.VerifyServer(ctx, token, encToken) + if err != nil { + return "", "", fmt.Errorf("Initial Verification: %w", err) + } + return token, encToken, err +} + +func (c *Client) VerifyServer(ctx context.Context, token, encToken string) error { + privateKeyObj, err := crypto.NewKeyFromArmored(c.userPrivateKey) + if err != nil { + return fmt.Errorf("Parsing User Private Key: %w", err) + } + + data := GPGVerifyContainer{ + Req: GPGVerify{ + Token: encToken, + KeyID: privateKeyObj.GetFingerprint(), + }, + } + raw, _, err := c.DoCustomRequestAndReturnRawResponse(ctx, "POST", "/auth/verify.json", "v2", data, nil) + if err != nil && !strings.Contains(err.Error(), "The authentication failed.") { + return fmt.Errorf("Sending Verification Challange: %w", err) + } + + if raw.Header.Get("X-GPGAuth-Verify-Response") != token { + return fmt.Errorf("Server Response did not Match Saved Token") + } + return nil +} diff --git a/go.mod b/go.mod index 4166a1c..4454320 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/ProtonMail/go-crypto v0.0.0-20210707164159-52430bf6b52c // indirect github.com/ProtonMail/gopenpgp/v2 v2.2.2 github.com/google/go-querystring v1.1.0 + github.com/google/uuid v1.3.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect diff --git a/go.sum b/go.sum index 7d914dc..3665dab 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=