Allow Pinning / Trusting Metadatakeys, Handle new Keys

This commit is contained in:
Samuel Lorch 2025-08-05 15:44:34 +02:00
parent 7d6d1c614c
commit 3cd88d7553
7 changed files with 207 additions and 140 deletions

View file

@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"path"
"time"
"github.com/ProtonMail/gopenpgp/v3/crypto"
"github.com/google/go-querystring/query"
@ -38,6 +39,15 @@ type Client struct {
// Server Settings for password expiry
passwordExpirySettings PasswordExpirySettings
// trusted metadatakey, Shared Metadata Keys which are trusted for encryption
trustedMetadataKeyFingerprint *string
trustedMetadataKeySigntime *time.Time
// MetadataKeyUpdatedCallback is Called by the Client when the Metadatakey has changed
// trusted shows if this key has been signed and thus been trusted by another client of this user
// the consumer should prompt the user about the keychange and save the new fingerprint (may be skipped if it is trusted).
// If no error is returned then the new key will be accepted and its fingerpint set in the client
MetadataKeyUpdatedCallback func(ctx context.Context, trusted bool, fingerprint string, signTime time.Time) error
// used for solving MFA challenges. You can block this to for example wait for user input.
// You shouden't run any unrelated API Calls while you are in this callback.

View file

@ -2,7 +2,12 @@ package api
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"time"
"github.com/ProtonMail/gopenpgp/v3/crypto"
)
type MetadataKeyType string
@ -40,14 +45,15 @@ type MetadataKey struct {
// MetadataPrivateKey is a MetadataPrivateKey
type MetadataPrivateKey struct {
ID string `json:"id,omitempty"`
MetadataKeyID string `json:"metadata_key_id,omitempty"`
UserID *string `json:"user_id,omitempty"` // TODO, is this nullable. The Docs says yes and no
Data string `json:"data,omitempty"`
Created Time `json:"created,omitempty"`
Modified Time `json:"modified,omitempty"`
CreatedBy *string `json:"created_by,omitempty"`
ModifiedBy *string `json:"modified_by,omitempty"`
ID string `json:"id,omitempty"`
MetadataKeyID string `json:"metadata_key_id,omitempty"`
UserID *string `json:"user_id,omitempty"` // TODO, is this nullable. The Docs says yes and no
Data string `json:"data,omitempty"`
Created Time `json:"created,omitempty"`
Modified Time `json:"modified,omitempty"`
CreatedBy *string `json:"created_by,omitempty"`
ModifiedBy *string `json:"modified_by,omitempty"`
DataSignedByCurrentUser *Time `json:"data_signed_by_current_user,omitempty"`
}
// MetadataPrivateKeyData is a MetadataPrivateKeyData
@ -60,6 +66,8 @@ type MetadataPrivateKeyData struct {
ArmoredKey string `json:"armored_key,omitempty"`
// Passphrase must be Empty for Server Keys
Passphrase string `json:"passphrase,omitempty"`
// When this key was Signed by our User for Trusting new keys which where trusted on other Devices
Signed Time `json:"signed,omitempty"`
}
// GetMetadataKeysOptions are all available query parameters
@ -70,6 +78,16 @@ type GetMetadataKeysOptions struct {
ContainMetadataPrivateKeys bool `url:"contain[metadata_private_keys],omitempty"`
}
// SetTrustedMetadatakeyFingerprint
func (c *Client) SetTrustedMetadatakeyFingerprint(fingerprint string, signTime time.Time) {
c.trustedMetadataKeyFingerprint = &fingerprint
}
// GetTrustedMetadatakeyFingerprint
func (c *Client) GetTrustedMetadatakeyFingerprint() *string {
return c.trustedMetadataKeyFingerprint
}
// GetMetadataKeys gets all Passbolt GetMetadataKeys
func (c *Client) GetMetadataKeys(ctx context.Context, opts *GetMetadataKeysOptions) ([]MetadataKey, error) {
msg, err := c.DoCustomRequestV5(ctx, "GET", "/metadata/keys.json", nil, opts)
@ -84,3 +102,169 @@ func (c *Client) GetMetadataKeys(ctx context.Context, opts *GetMetadataKeysOptio
}
return metadataKeys, nil
}
// GetMetadataKey gets a Metadata key, Personal indicates if the function should return the personal key,
// If personal keys have been disabled on the server then we return the shared key
// Returns the Key ID, Key Type and the Key itself
func (c *Client) GetMetadataKey(ctx context.Context, personal bool) (string, MetadataKeyType, *crypto.Key, error) {
// if personal is requsted and it is allowed by the server, then return that
if personal && c.MetadataKeySettings().AllowUsageOfPersonalKeys {
key, err := c.GetUserPrivateKeyCopy()
if err != nil {
return "", "", nil, fmt.Errorf("Get User Private Key: %w", err)
}
me, err := c.GetMe(ctx)
if err != nil {
return "", "", nil, fmt.Errorf("Get User Me: %w", err)
}
if me.GPGKey == nil {
return "", "", nil, fmt.Errorf("User Me GPG Key nil")
}
return me.GPGKey.ID, MetadataKeyTypeUserKey, key, nil
}
keys, err := c.GetMetadataKeys(ctx, &GetMetadataKeysOptions{
ContainMetadataPrivateKeys: true,
})
if err != nil {
return "", "", nil, fmt.Errorf("Get Metadata Key: %w", err)
}
// Get The Newest Metadata Key
metadatakey := keys[len(keys)-1]
var privateMetadataKey *MetadataPrivateKey = nil
for _, _privateMetadataKey := range metadatakey.MetadataPrivateKeys {
if *_privateMetadataKey.UserID == c.userID {
privateMetadataKey = &_privateMetadataKey
c.log("Found privateMetadataKey for our user %v", _privateMetadataKey.ID)
break
}
}
if privateMetadataKey == nil {
return "", "", nil, fmt.Errorf("No Metadata Private key for our user")
}
decPrivateMetadatakey, err := c.DecryptMessage(privateMetadataKey.Data)
if err != nil {
return "", "", nil, fmt.Errorf("Decrypt Metadata Private Key Data: %w", err)
}
var data MetadataPrivateKeyData
err = json.Unmarshal([]byte(decPrivateMetadatakey), &data)
if err != nil {
return "", "", nil, fmt.Errorf("Parse Metadata Private Key Data")
}
metadataPrivateKeyObj, err := GetPrivateKeyFromArmor(data.ArmoredKey, []byte(data.Passphrase))
if err != nil {
return "", "", nil, fmt.Errorf("Get Metadata Private Key: %w", err)
}
// Verify the key
if c.GetTrustedMetadatakeyFingerprint() == nil || metadataPrivateKeyObj.GetFingerprint() != *c.GetTrustedMetadatakeyFingerprint() {
if c.trustedMetadataKeySigntime != nil && !data.Signed.Time.After(*c.trustedMetadataKeySigntime) {
return "", "", nil, fmt.Errorf("New Metadata Key is older than the currently trusted one: %w", err)
}
userPrivateKey, err := c.GetUserPrivateKeyCopy()
if err != nil {
return "", "", nil, fmt.Errorf("Get User Private Key Copy: %w", err)
}
verify, err := c.pgp.Verify().VerificationKey(userPrivateKey).New()
verifyRes, err := verify.VerifyInline([]byte(privateMetadataKey.Data), crypto.Armor)
if err != nil {
return "", "", nil, fmt.Errorf("Verify User Metadata Private Key Signature: %w", err)
}
signedByFingerprint := hex.EncodeToString(verifyRes.SignedByFingerprint())
c.log("Metadata Private key Signed by %v", signedByFingerprint)
c.log("User key Fingerprint %v", userPrivateKey.GetFingerprint())
// Check if the Metadata Private Key was signed by our User Private key
trusted := false
if signedByFingerprint == userPrivateKey.GetFingerprint() {
trusted = true
c.log("New Metadata Private Key has been signed by our Private key")
} else {
c.log("New Metadata Private Key has failed the signature check")
}
// Callback not Defined
if c.MetadataKeyUpdatedCallback == nil {
// Fail if there is a key pinned but the signature check failed
if c.trustedMetadataKeyFingerprint != nil || !trusted {
return "", "", nil, fmt.Errorf("Metadata Key has changed, The Callback is nil, There is a Key Pinned but the new one is not trusted")
}
c.log("No Callback is defined, No Metadata key is pinned and the Signature is by our Private key, automatically trusting")
} else {
err = c.MetadataKeyUpdatedCallback(ctx, trusted, metadataPrivateKeyObj.GetFingerprint(), data.Signed.Time)
if err != nil {
return "", "", nil, fmt.Errorf("Metadata Key has changed, Callback: %w", err)
}
}
// Callback has not Returned an error, Thus the New Key has been accepted
c.SetTrustedMetadatakeyFingerprint(metadataPrivateKeyObj.GetFingerprint(), data.Signed.Time)
}
return metadatakey.ID, MetadataKeyTypeSharedKey, metadataPrivateKeyObj, nil
}
// GetMetadataKeyById is for fetching a specific metadatakey if needed for Decryption, these are not verified
func (c *Client) GetMetadataKeyById(ctx context.Context, id string) (*crypto.Key, error) {
keys, err := c.GetMetadataKeys(ctx, &GetMetadataKeysOptions{
ContainMetadataPrivateKeys: true,
})
if err != nil {
return nil, fmt.Errorf("Get Metadata Key: %w", err)
}
var key *MetadataKey
for _, k := range keys {
if k.ID == id {
key = &k
break
}
}
if key == nil {
return nil, fmt.Errorf("Metadata key not found: %v", id)
}
if len(key.MetadataPrivateKeys) == 0 {
return nil, fmt.Errorf("No Metadata Private key for our user")
}
if len(key.MetadataPrivateKeys) > 1 {
return nil, fmt.Errorf("More than 1 metadata Private key for our user")
}
var privMetdata MetadataPrivateKey = key.MetadataPrivateKeys[0]
if *privMetdata.UserID != c.GetUserID() {
return nil, fmt.Errorf("MetadataPrivateKey is not for our user id: %v", privMetdata.UserID)
}
decPrivMetadatakey, err := c.DecryptMessage(privMetdata.Data)
if err != nil {
return nil, fmt.Errorf("Decrypt Metadata Private Key Data: %w", err)
}
var data MetadataPrivateKeyData
err = json.Unmarshal([]byte(decPrivMetadatakey), &data)
if err != nil {
return nil, fmt.Errorf("Parse Metadata Private Key Data")
}
metadataPrivateKeyObj, err := GetPrivateKeyFromArmor(data.ArmoredKey, []byte(data.Passphrase))
if err != nil {
return nil, fmt.Errorf("Get Metadata Private Key: %w", err)
}
return metadataPrivateKeyObj, nil
}