From 3cd88d75531657665d9813f49ba91c7805b0d9b2 Mon Sep 17 00:00:00 2001 From: Samuel Lorch Date: Tue, 5 Aug 2025 15:44:34 +0200 Subject: [PATCH] Allow Pinning / Trusting Metadatakeys, Handle new Keys --- api/client.go | 10 ++ api/metadatakey.go | 200 ++++++++++++++++++++++++++++++++++++-- helper/metadata.go | 2 +- helper/metadatakey.go | 128 ------------------------ helper/resource_create.go | 2 +- helper/resource_update.go | 2 +- helper/share.go | 3 +- 7 files changed, 207 insertions(+), 140 deletions(-) diff --git a/api/client.go b/api/client.go index ce85767..20d93fa 100644 --- a/api/client.go +++ b/api/client.go @@ -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. diff --git a/api/metadatakey.go b/api/metadatakey.go index 49db61c..a2c8575 100644 --- a/api/metadatakey.go +++ b/api/metadatakey.go @@ -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 +} diff --git a/helper/metadata.go b/helper/metadata.go index 978981d..8cbb009 100644 --- a/helper/metadata.go +++ b/helper/metadata.go @@ -21,7 +21,7 @@ func GetResourceMetadata(ctx context.Context, c *api.Client, resource *api.Resou } metadatakey = tmp } else { - key, err := GetMetadataKeyById(ctx, c, resource.MetadataKeyID) + key, err := c.GetMetadataKeyById(ctx, resource.MetadataKeyID) if err != nil { return "", fmt.Errorf("Get Metadata Key by ID: %w", err) } diff --git a/helper/metadatakey.go b/helper/metadatakey.go index 701d786..947317d 100644 --- a/helper/metadatakey.go +++ b/helper/metadatakey.go @@ -1,129 +1 @@ package helper - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/ProtonMail/gopenpgp/v3/crypto" - "github.com/passbolt/go-passbolt/api" -) - -// 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 GetMetadataKey(ctx context.Context, c *api.Client, personal bool) (string, api.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, api.MetadataKeyTypeUserKey, key, nil - } - - keys, err := c.GetMetadataKeys(ctx, &api.GetMetadataKeysOptions{ - ContainMetadataPrivateKeys: true, - }) - if err != nil { - return "", "", nil, fmt.Errorf("Get Metadata Key: %w", err) - } - - // TODO Get Key by id? - if len(keys) != 1 { - return "", "", nil, fmt.Errorf("Not Exactly One Metadatakey Available") - } - - if len(keys[0].MetadataPrivateKeys) == 0 { - return "", "", nil, fmt.Errorf("No Metadata Private key for our user") - } - - if len(keys[0].MetadataPrivateKeys) > 1 { - return "", "", nil, fmt.Errorf("More than 1 metadata Private key for our user") - } - - var privMetdata api.MetadataPrivateKey = keys[0].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 api.MetadataPrivateKeyData - err = json.Unmarshal([]byte(decPrivMetadatakey), &data) - if err != nil { - return "", "", nil, fmt.Errorf("Parse Metadata Private Key Data") - } - - metadataPrivateKeyObj, err := api.GetPrivateKeyFromArmor(data.ArmoredKey, []byte(data.Passphrase)) - if err != nil { - return "", "", nil, fmt.Errorf("Get Metadata Private Key: %w", err) - } - - return keys[0].ID, api.MetadataKeyTypeSharedKey, metadataPrivateKeyObj, nil -} - -// GetMetadataKeyById is for fetching a specific metadatakey if needed for Decryption -func GetMetadataKeyById(ctx context.Context, c *api.Client, id string) (*crypto.Key, error) { - keys, err := c.GetMetadataKeys(ctx, &api.GetMetadataKeysOptions{ - ContainMetadataPrivateKeys: true, - }) - if err != nil { - return nil, fmt.Errorf("Get Metadata Key: %w", err) - } - var key *api.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 api.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 api.MetadataPrivateKeyData - err = json.Unmarshal([]byte(decPrivMetadatakey), &data) - if err != nil { - return nil, fmt.Errorf("Parse Metadata Private Key Data") - } - - metadataPrivateKeyObj, err := api.GetPrivateKeyFromArmor(data.ArmoredKey, []byte(data.Passphrase)) - if err != nil { - return nil, fmt.Errorf("Get Metadata Private Key: %w", err) - } - - return metadataPrivateKeyObj, nil -} diff --git a/helper/resource_create.go b/helper/resource_create.go index e8c98db..b2e4044 100644 --- a/helper/resource_create.go +++ b/helper/resource_create.go @@ -64,7 +64,7 @@ func CreateResourceV5(ctx context.Context, c *api.Client, folderParentID, name, return "", fmt.Errorf("Validating metadata: %w", err) } - metadataKeyID, metadataKeyType, publicMetadataKey, err := GetMetadataKey(ctx, c, true) + metadataKeyID, metadataKeyType, publicMetadataKey, err := c.GetMetadataKey(ctx, true) if err != nil { return "", fmt.Errorf("Get Metadata Key: %w", err) } diff --git a/helper/resource_update.go b/helper/resource_update.go index 3c6b82b..986b88e 100644 --- a/helper/resource_update.go +++ b/helper/resource_update.go @@ -113,7 +113,7 @@ func UpdateResource(ctx context.Context, c *api.Client, resourceID, name, userna return fmt.Errorf("Validating metadata: %w", err) } - metadataKeyID, metadataKeyType, publicMetadataKey, err := GetMetadataKey(ctx, c, true) + metadataKeyID, metadataKeyType, publicMetadataKey, err := c.GetMetadataKey(ctx, true) if err != nil { return fmt.Errorf("Get Metadata Key: %w", err) } diff --git a/helper/share.go b/helper/share.go index 4d0897a..e42cb80 100644 --- a/helper/share.go +++ b/helper/share.go @@ -81,13 +81,14 @@ func ShareResource(ctx context.Context, c *api.Client, resourceID string, change // if Metadata has not been shared yet then we need to do that // we assume that if MetadataKeyType is not null that this is a v5 Resource and that the other field are fine + // TODO Calculate if this should be the Shared Metadatakey or our Personal one (if we are unsharing) if resource.MetadataKeyType == api.MetadataKeyTypeUserKey { metadata, err := GetResourceMetadata(ctx, c, resource, rType) if err != nil { return fmt.Errorf("Get Metadata: %w", err) } - metadataKeyID, metadataKeyType, publicMetadataKey, err := GetMetadataKey(ctx, c, true) + metadataKeyID, metadataKeyType, publicMetadataKey, err := c.GetMetadataKey(ctx, false) if err != nil { return fmt.Errorf("Get Metadata Key: %w", err) }