Skip to content

Commit 8b00fcd

Browse files
authored
Merge pull request #55 from passbolt/v5-metadata-trusting
Allow Pinning / Trusting Metadatakeys, Handle new Keys
2 parents 7d6d1c6 + 3cd88d7 commit 8b00fcd

File tree

7 files changed

+207
-140
lines changed

7 files changed

+207
-140
lines changed

api/client.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/http"
1010
"net/url"
1111
"path"
12+
"time"
1213

1314
"github.com/ProtonMail/gopenpgp/v3/crypto"
1415
"github.com/google/go-querystring/query"
@@ -38,6 +39,15 @@ type Client struct {
3839

3940
// Server Settings for password expiry
4041
passwordExpirySettings PasswordExpirySettings
42+
// trusted metadatakey, Shared Metadata Keys which are trusted for encryption
43+
trustedMetadataKeyFingerprint *string
44+
trustedMetadataKeySigntime *time.Time
45+
46+
// MetadataKeyUpdatedCallback is Called by the Client when the Metadatakey has changed
47+
// trusted shows if this key has been signed and thus been trusted by another client of this user
48+
// the consumer should prompt the user about the keychange and save the new fingerprint (may be skipped if it is trusted).
49+
// If no error is returned then the new key will be accepted and its fingerpint set in the client
50+
MetadataKeyUpdatedCallback func(ctx context.Context, trusted bool, fingerprint string, signTime time.Time) error
4151

4252
// used for solving MFA challenges. You can block this to for example wait for user input.
4353
// You shouden't run any unrelated API Calls while you are in this callback.

api/metadatakey.go

Lines changed: 192 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ package api
22

33
import (
44
"context"
5+
"encoding/hex"
56
"encoding/json"
7+
"fmt"
8+
"time"
9+
10+
"github.com/ProtonMail/gopenpgp/v3/crypto"
611
)
712

813
type MetadataKeyType string
@@ -40,14 +45,15 @@ type MetadataKey struct {
4045

4146
// MetadataPrivateKey is a MetadataPrivateKey
4247
type MetadataPrivateKey struct {
43-
ID string `json:"id,omitempty"`
44-
MetadataKeyID string `json:"metadata_key_id,omitempty"`
45-
UserID *string `json:"user_id,omitempty"` // TODO, is this nullable. The Docs says yes and no
46-
Data string `json:"data,omitempty"`
47-
Created Time `json:"created,omitempty"`
48-
Modified Time `json:"modified,omitempty"`
49-
CreatedBy *string `json:"created_by,omitempty"`
50-
ModifiedBy *string `json:"modified_by,omitempty"`
48+
ID string `json:"id,omitempty"`
49+
MetadataKeyID string `json:"metadata_key_id,omitempty"`
50+
UserID *string `json:"user_id,omitempty"` // TODO, is this nullable. The Docs says yes and no
51+
Data string `json:"data,omitempty"`
52+
Created Time `json:"created,omitempty"`
53+
Modified Time `json:"modified,omitempty"`
54+
CreatedBy *string `json:"created_by,omitempty"`
55+
ModifiedBy *string `json:"modified_by,omitempty"`
56+
DataSignedByCurrentUser *Time `json:"data_signed_by_current_user,omitempty"`
5157
}
5258

5359
// MetadataPrivateKeyData is a MetadataPrivateKeyData
@@ -60,6 +66,8 @@ type MetadataPrivateKeyData struct {
6066
ArmoredKey string `json:"armored_key,omitempty"`
6167
// Passphrase must be Empty for Server Keys
6268
Passphrase string `json:"passphrase,omitempty"`
69+
// When this key was Signed by our User for Trusting new keys which where trusted on other Devices
70+
Signed Time `json:"signed,omitempty"`
6371
}
6472

6573
// GetMetadataKeysOptions are all available query parameters
@@ -70,6 +78,16 @@ type GetMetadataKeysOptions struct {
7078
ContainMetadataPrivateKeys bool `url:"contain[metadata_private_keys],omitempty"`
7179
}
7280

81+
// SetTrustedMetadatakeyFingerprint
82+
func (c *Client) SetTrustedMetadatakeyFingerprint(fingerprint string, signTime time.Time) {
83+
c.trustedMetadataKeyFingerprint = &fingerprint
84+
}
85+
86+
// GetTrustedMetadatakeyFingerprint
87+
func (c *Client) GetTrustedMetadatakeyFingerprint() *string {
88+
return c.trustedMetadataKeyFingerprint
89+
}
90+
7391
// GetMetadataKeys gets all Passbolt GetMetadataKeys
7492
func (c *Client) GetMetadataKeys(ctx context.Context, opts *GetMetadataKeysOptions) ([]MetadataKey, error) {
7593
msg, err := c.DoCustomRequestV5(ctx, "GET", "/metadata/keys.json", nil, opts)
@@ -84,3 +102,169 @@ func (c *Client) GetMetadataKeys(ctx context.Context, opts *GetMetadataKeysOptio
84102
}
85103
return metadataKeys, nil
86104
}
105+
106+
// GetMetadataKey gets a Metadata key, Personal indicates if the function should return the personal key,
107+
// If personal keys have been disabled on the server then we return the shared key
108+
// Returns the Key ID, Key Type and the Key itself
109+
func (c *Client) GetMetadataKey(ctx context.Context, personal bool) (string, MetadataKeyType, *crypto.Key, error) {
110+
// if personal is requsted and it is allowed by the server, then return that
111+
if personal && c.MetadataKeySettings().AllowUsageOfPersonalKeys {
112+
key, err := c.GetUserPrivateKeyCopy()
113+
if err != nil {
114+
return "", "", nil, fmt.Errorf("Get User Private Key: %w", err)
115+
}
116+
117+
me, err := c.GetMe(ctx)
118+
if err != nil {
119+
return "", "", nil, fmt.Errorf("Get User Me: %w", err)
120+
}
121+
122+
if me.GPGKey == nil {
123+
return "", "", nil, fmt.Errorf("User Me GPG Key nil")
124+
}
125+
126+
return me.GPGKey.ID, MetadataKeyTypeUserKey, key, nil
127+
}
128+
129+
keys, err := c.GetMetadataKeys(ctx, &GetMetadataKeysOptions{
130+
ContainMetadataPrivateKeys: true,
131+
})
132+
if err != nil {
133+
return "", "", nil, fmt.Errorf("Get Metadata Key: %w", err)
134+
}
135+
136+
// Get The Newest Metadata Key
137+
metadatakey := keys[len(keys)-1]
138+
var privateMetadataKey *MetadataPrivateKey = nil
139+
for _, _privateMetadataKey := range metadatakey.MetadataPrivateKeys {
140+
if *_privateMetadataKey.UserID == c.userID {
141+
privateMetadataKey = &_privateMetadataKey
142+
c.log("Found privateMetadataKey for our user %v", _privateMetadataKey.ID)
143+
break
144+
}
145+
}
146+
147+
if privateMetadataKey == nil {
148+
return "", "", nil, fmt.Errorf("No Metadata Private key for our user")
149+
}
150+
151+
decPrivateMetadatakey, err := c.DecryptMessage(privateMetadataKey.Data)
152+
if err != nil {
153+
return "", "", nil, fmt.Errorf("Decrypt Metadata Private Key Data: %w", err)
154+
}
155+
156+
var data MetadataPrivateKeyData
157+
err = json.Unmarshal([]byte(decPrivateMetadatakey), &data)
158+
if err != nil {
159+
return "", "", nil, fmt.Errorf("Parse Metadata Private Key Data")
160+
}
161+
162+
metadataPrivateKeyObj, err := GetPrivateKeyFromArmor(data.ArmoredKey, []byte(data.Passphrase))
163+
if err != nil {
164+
return "", "", nil, fmt.Errorf("Get Metadata Private Key: %w", err)
165+
}
166+
167+
// Verify the key
168+
if c.GetTrustedMetadatakeyFingerprint() == nil || metadataPrivateKeyObj.GetFingerprint() != *c.GetTrustedMetadatakeyFingerprint() {
169+
170+
if c.trustedMetadataKeySigntime != nil && !data.Signed.Time.After(*c.trustedMetadataKeySigntime) {
171+
return "", "", nil, fmt.Errorf("New Metadata Key is older than the currently trusted one: %w", err)
172+
}
173+
174+
userPrivateKey, err := c.GetUserPrivateKeyCopy()
175+
if err != nil {
176+
return "", "", nil, fmt.Errorf("Get User Private Key Copy: %w", err)
177+
}
178+
179+
verify, err := c.pgp.Verify().VerificationKey(userPrivateKey).New()
180+
verifyRes, err := verify.VerifyInline([]byte(privateMetadataKey.Data), crypto.Armor)
181+
if err != nil {
182+
return "", "", nil, fmt.Errorf("Verify User Metadata Private Key Signature: %w", err)
183+
}
184+
185+
signedByFingerprint := hex.EncodeToString(verifyRes.SignedByFingerprint())
186+
c.log("Metadata Private key Signed by %v", signedByFingerprint)
187+
c.log("User key Fingerprint %v", userPrivateKey.GetFingerprint())
188+
189+
// Check if the Metadata Private Key was signed by our User Private key
190+
trusted := false
191+
if signedByFingerprint == userPrivateKey.GetFingerprint() {
192+
trusted = true
193+
c.log("New Metadata Private Key has been signed by our Private key")
194+
} else {
195+
c.log("New Metadata Private Key has failed the signature check")
196+
}
197+
198+
// Callback not Defined
199+
if c.MetadataKeyUpdatedCallback == nil {
200+
// Fail if there is a key pinned but the signature check failed
201+
if c.trustedMetadataKeyFingerprint != nil || !trusted {
202+
return "", "", nil, fmt.Errorf("Metadata Key has changed, The Callback is nil, There is a Key Pinned but the new one is not trusted")
203+
}
204+
c.log("No Callback is defined, No Metadata key is pinned and the Signature is by our Private key, automatically trusting")
205+
206+
} else {
207+
err = c.MetadataKeyUpdatedCallback(ctx, trusted, metadataPrivateKeyObj.GetFingerprint(), data.Signed.Time)
208+
if err != nil {
209+
return "", "", nil, fmt.Errorf("Metadata Key has changed, Callback: %w", err)
210+
}
211+
}
212+
213+
// Callback has not Returned an error, Thus the New Key has been accepted
214+
c.SetTrustedMetadatakeyFingerprint(metadataPrivateKeyObj.GetFingerprint(), data.Signed.Time)
215+
}
216+
217+
return metadatakey.ID, MetadataKeyTypeSharedKey, metadataPrivateKeyObj, nil
218+
}
219+
220+
// GetMetadataKeyById is for fetching a specific metadatakey if needed for Decryption, these are not verified
221+
func (c *Client) GetMetadataKeyById(ctx context.Context, id string) (*crypto.Key, error) {
222+
keys, err := c.GetMetadataKeys(ctx, &GetMetadataKeysOptions{
223+
ContainMetadataPrivateKeys: true,
224+
})
225+
if err != nil {
226+
return nil, fmt.Errorf("Get Metadata Key: %w", err)
227+
}
228+
var key *MetadataKey
229+
for _, k := range keys {
230+
if k.ID == id {
231+
key = &k
232+
break
233+
}
234+
}
235+
236+
if key == nil {
237+
return nil, fmt.Errorf("Metadata key not found: %v", id)
238+
}
239+
240+
if len(key.MetadataPrivateKeys) == 0 {
241+
return nil, fmt.Errorf("No Metadata Private key for our user")
242+
}
243+
244+
if len(key.MetadataPrivateKeys) > 1 {
245+
return nil, fmt.Errorf("More than 1 metadata Private key for our user")
246+
}
247+
248+
var privMetdata MetadataPrivateKey = key.MetadataPrivateKeys[0]
249+
if *privMetdata.UserID != c.GetUserID() {
250+
return nil, fmt.Errorf("MetadataPrivateKey is not for our user id: %v", privMetdata.UserID)
251+
}
252+
253+
decPrivMetadatakey, err := c.DecryptMessage(privMetdata.Data)
254+
if err != nil {
255+
return nil, fmt.Errorf("Decrypt Metadata Private Key Data: %w", err)
256+
}
257+
258+
var data MetadataPrivateKeyData
259+
err = json.Unmarshal([]byte(decPrivMetadatakey), &data)
260+
if err != nil {
261+
return nil, fmt.Errorf("Parse Metadata Private Key Data")
262+
}
263+
264+
metadataPrivateKeyObj, err := GetPrivateKeyFromArmor(data.ArmoredKey, []byte(data.Passphrase))
265+
if err != nil {
266+
return nil, fmt.Errorf("Get Metadata Private Key: %w", err)
267+
}
268+
269+
return metadataPrivateKeyObj, nil
270+
}

helper/metadata.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func GetResourceMetadata(ctx context.Context, c *api.Client, resource *api.Resou
2121
}
2222
metadatakey = tmp
2323
} else {
24-
key, err := GetMetadataKeyById(ctx, c, resource.MetadataKeyID)
24+
key, err := c.GetMetadataKeyById(ctx, resource.MetadataKeyID)
2525
if err != nil {
2626
return "", fmt.Errorf("Get Metadata Key by ID: %w", err)
2727
}

helper/metadatakey.go

Lines changed: 0 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1 @@
11
package helper
2-
3-
import (
4-
"context"
5-
"encoding/json"
6-
"fmt"
7-
8-
"github.com/ProtonMail/gopenpgp/v3/crypto"
9-
"github.com/passbolt/go-passbolt/api"
10-
)
11-
12-
// GetMetadataKey gets a Metadata key, Personal indicates if the function should return the personal key,
13-
// If personal keys have been disabled on the server then we return the shared key
14-
// Returns the Key ID, Key Type and the Key itself
15-
func GetMetadataKey(ctx context.Context, c *api.Client, personal bool) (string, api.MetadataKeyType, *crypto.Key, error) {
16-
// if personal is requsted and it is allowed by the server, then return that
17-
if personal && c.MetadataKeySettings().AllowUsageOfPersonalKeys {
18-
key, err := c.GetUserPrivateKeyCopy()
19-
if err != nil {
20-
return "", "", nil, fmt.Errorf("Get User Private Key: %w", err)
21-
}
22-
23-
me, err := c.GetMe(ctx)
24-
if err != nil {
25-
return "", "", nil, fmt.Errorf("Get User Me: %w", err)
26-
}
27-
28-
if me.GPGKey == nil {
29-
return "", "", nil, fmt.Errorf("User Me GPG Key nil")
30-
}
31-
32-
return me.GPGKey.ID, api.MetadataKeyTypeUserKey, key, nil
33-
}
34-
35-
keys, err := c.GetMetadataKeys(ctx, &api.GetMetadataKeysOptions{
36-
ContainMetadataPrivateKeys: true,
37-
})
38-
if err != nil {
39-
return "", "", nil, fmt.Errorf("Get Metadata Key: %w", err)
40-
}
41-
42-
// TODO Get Key by id?
43-
if len(keys) != 1 {
44-
return "", "", nil, fmt.Errorf("Not Exactly One Metadatakey Available")
45-
}
46-
47-
if len(keys[0].MetadataPrivateKeys) == 0 {
48-
return "", "", nil, fmt.Errorf("No Metadata Private key for our user")
49-
}
50-
51-
if len(keys[0].MetadataPrivateKeys) > 1 {
52-
return "", "", nil, fmt.Errorf("More than 1 metadata Private key for our user")
53-
}
54-
55-
var privMetdata api.MetadataPrivateKey = keys[0].MetadataPrivateKeys[0]
56-
if *privMetdata.UserID != c.GetUserID() {
57-
return "", "", nil, fmt.Errorf("MetadataPrivateKey is not for our user id: %v", privMetdata.UserID)
58-
}
59-
60-
decPrivMetadatakey, err := c.DecryptMessage(privMetdata.Data)
61-
if err != nil {
62-
return "", "", nil, fmt.Errorf("Decrypt Metadata Private Key Data: %w", err)
63-
}
64-
65-
var data api.MetadataPrivateKeyData
66-
err = json.Unmarshal([]byte(decPrivMetadatakey), &data)
67-
if err != nil {
68-
return "", "", nil, fmt.Errorf("Parse Metadata Private Key Data")
69-
}
70-
71-
metadataPrivateKeyObj, err := api.GetPrivateKeyFromArmor(data.ArmoredKey, []byte(data.Passphrase))
72-
if err != nil {
73-
return "", "", nil, fmt.Errorf("Get Metadata Private Key: %w", err)
74-
}
75-
76-
return keys[0].ID, api.MetadataKeyTypeSharedKey, metadataPrivateKeyObj, nil
77-
}
78-
79-
// GetMetadataKeyById is for fetching a specific metadatakey if needed for Decryption
80-
func GetMetadataKeyById(ctx context.Context, c *api.Client, id string) (*crypto.Key, error) {
81-
keys, err := c.GetMetadataKeys(ctx, &api.GetMetadataKeysOptions{
82-
ContainMetadataPrivateKeys: true,
83-
})
84-
if err != nil {
85-
return nil, fmt.Errorf("Get Metadata Key: %w", err)
86-
}
87-
var key *api.MetadataKey
88-
for _, k := range keys {
89-
if k.ID == id {
90-
key = &k
91-
break
92-
}
93-
}
94-
95-
if key == nil {
96-
return nil, fmt.Errorf("Metadata key not found: %v", id)
97-
}
98-
99-
if len(key.MetadataPrivateKeys) == 0 {
100-
return nil, fmt.Errorf("No Metadata Private key for our user")
101-
}
102-
103-
if len(key.MetadataPrivateKeys) > 1 {
104-
return nil, fmt.Errorf("More than 1 metadata Private key for our user")
105-
}
106-
107-
var privMetdata api.MetadataPrivateKey = key.MetadataPrivateKeys[0]
108-
if *privMetdata.UserID != c.GetUserID() {
109-
return nil, fmt.Errorf("MetadataPrivateKey is not for our user id: %v", privMetdata.UserID)
110-
}
111-
112-
decPrivMetadatakey, err := c.DecryptMessage(privMetdata.Data)
113-
if err != nil {
114-
return nil, fmt.Errorf("Decrypt Metadata Private Key Data: %w", err)
115-
}
116-
117-
var data api.MetadataPrivateKeyData
118-
err = json.Unmarshal([]byte(decPrivMetadatakey), &data)
119-
if err != nil {
120-
return nil, fmt.Errorf("Parse Metadata Private Key Data")
121-
}
122-
123-
metadataPrivateKeyObj, err := api.GetPrivateKeyFromArmor(data.ArmoredKey, []byte(data.Passphrase))
124-
if err != nil {
125-
return nil, fmt.Errorf("Get Metadata Private Key: %w", err)
126-
}
127-
128-
return metadataPrivateKeyObj, nil
129-
}

0 commit comments

Comments
 (0)