diff --git a/helper/metadata.go b/helper/metadata.go index be09475..978981d 100644 --- a/helper/metadata.go +++ b/helper/metadata.go @@ -7,18 +7,27 @@ import ( "fmt" "strings" + "github.com/ProtonMail/gopenpgp/v3/crypto" "github.com/passbolt/go-passbolt/api" "github.com/santhosh-tekuri/jsonschema" ) func GetResourceMetadata(ctx context.Context, c *api.Client, resource *api.Resource, rType *api.ResourceType) (string, error) { - _, _, metadatakey, err := GetMetadataKey(ctx, c, resource.MetadataKeyType == api.MetadataKeyTypeUserKey) - if err != nil { - return "", fmt.Errorf("Get Metadata Key: %w", err) + var metadatakey *crypto.Key + if resource.MetadataKeyType == api.MetadataKeyTypeUserKey { + tmp, err := c.GetUserPrivateKeyCopy() + if err != nil { + return "", fmt.Errorf("Get Private Key Copy: %w", err) + } + metadatakey = tmp + } else { + key, err := GetMetadataKeyById(ctx, c, resource.MetadataKeyID) + if err != nil { + return "", fmt.Errorf("Get Metadata Key by ID: %w", err) + } + metadatakey = key } - // TODO should we instead get the Metadata key of this resource by id? - decMetadata, err := c.DecryptMetadata(metadatakey, resource.Metadata) if err != nil { return "", fmt.Errorf("Decrypt Metadata: %w", err) @@ -78,7 +87,7 @@ func validateMetadata(rType *api.ResourceType, metadata string) error { err = schema.Validate(strings.NewReader(metadata)) if err != nil { - return fmt.Errorf("Validating Secret Data: %w", err) + return fmt.Errorf("Validating Metadata with Schema: %w", err) } return nil } diff --git a/helper/metadatakey.go b/helper/metadatakey.go index 14c77da..701d786 100644 --- a/helper/metadatakey.go +++ b/helper/metadatakey.go @@ -75,3 +75,55 @@ func GetMetadataKey(ctx context.Context, c *api.Client, personal bool) (string, 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/resources.go b/helper/resources.go index 5163ecb..49dfa12 100644 --- a/helper/resources.go +++ b/helper/resources.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/ProtonMail/gopenpgp/v3/crypto" "github.com/passbolt/go-passbolt/api" ) @@ -389,46 +390,150 @@ func UpdateResource(ctx context.Context, c *api.Client, resourceID, name, userna ID: resourceID, // This needs to be specified or it will revert to a legacy password ResourceTypeID: resource.ResourceTypeID, - Name: resource.Name, - Username: resource.Username, - URI: resource.URI, } - - if name != "" { - newResource.Name = name - } - if username != "" { - newResource.Username = username - } - if uri != "" { - newResource.URI = uri - } - var secretData string - switch rType.Slug { - case "password-string": - newResource.Description = resource.Description - if description != "" { - newResource.Description = description + + // Check if this is a v5 or Later Resource + if resource.Metadata != "" { + // Get Metadata + orgMetadata, err := GetResourceMetadata(ctx, c, resource, rType) + if err != nil { + return fmt.Errorf("Get Resource metadata: %w", err) } - if password != "" { - secretData = password - } else { - secret, err := c.GetSecret(ctx, resourceID) - if err != nil { - return fmt.Errorf("Getting Secret: %w", err) + + var metadataMap map[string]any + err = json.Unmarshal([]byte(orgMetadata), &metadataMap) + if err != nil { + return fmt.Errorf("Marshalling metadata: %w", err) + } + + var newMetadata []byte + switch rType.Slug { + case "v5-default": + // Modify Metadata + if name != "" { + metadataMap["name"] = name } - secretData, err = c.DecryptMessage(secret.Data) - if err != nil { - return fmt.Errorf("Decrypting Secret: %w", err) + if username != "" { + metadataMap["username"] = username } + if uri != "" { + metadataMap["uris"] = []string{uri} + } + case "v5-password-string": + // Modify Metadata + if name != "" { + metadataMap["name"] = name + } + if username != "" { + metadataMap["username"] = username + } + if uri != "" { + metadataMap["uris"] = []string{uri} + } + if description != "" { + metadataMap["description"] = description + } + case "v5-default-with-totp": + // Modify Metadata + if name != "" { + metadataMap["name"] = name + } + if username != "" { + metadataMap["username"] = username + } + if uri != "" { + metadataMap["uris"] = []string{uri} + } + case "v5-totp-standalone": + // Modify Metadata + if name != "" { + metadataMap["name"] = name + } + if uri != "" { + metadataMap["uris"] = []string{uri} + } + default: + return fmt.Errorf("Unknown ResourceType: %v", rType.Slug) } - case "password-and-description": - tmp := api.SecretDataTypePasswordAndDescription{ - Password: password, - Description: description, + + newMetadata, err = json.Marshal(&metadataMap) + if err != nil { + return fmt.Errorf("Marshalling metadata: %w", err) } - if password != "" || description != "" { + + // Validate Metadata + err = validateMetadata(rType, string(newMetadata)) + if err != nil { + return fmt.Errorf("Validating metadata: %w", err) + } + + metadataKeyID, metadataKeyType, publicMetadataKey, err := GetMetadataKey(ctx, c, true) + if err != nil { + return fmt.Errorf("Get Metadata Key: %w", err) + } + newResource.MetadataKeyID = metadataKeyID + newResource.MetadataKeyType = metadataKeyType + + encMetadata, err := c.EncryptMessageWithKey(publicMetadataKey, string(newMetadata)) + if err != nil { + return fmt.Errorf("Encrypt Metadata: %w", err) + } + newResource.Metadata = encMetadata + + // Modify Secret + switch rType.Slug { + case "v5-default": + tmp := api.SecretDataTypeV5Default{ + Password: password, + Description: description, + } + tmp.ObjectType = api.PASSBOLT_OBJECT_TYPE_SECRET_DATA + tmp.ResourceTypeID = rType.ID + if password != "" || description != "" { + secret, err := c.GetSecret(ctx, resourceID) + if err != nil { + return fmt.Errorf("Getting Secret: %w", err) + } + oldSecretData, err := c.DecryptMessage(secret.Data) + if err != nil { + return fmt.Errorf("Decrypting Secret: %w", err) + } + var oldSecret api.SecretDataTypeV5Default + err = json.Unmarshal([]byte(oldSecretData), &oldSecret) + if err != nil { + return fmt.Errorf("Parsing Decrypted Secret Data: %w", err) + } + if password == "" { + tmp.Password = oldSecret.Password + } + if description == "" { + tmp.Description = oldSecret.Description + } + } + res, err := json.Marshal(&tmp) + if err != nil { + return fmt.Errorf("Marshalling Secret Data: %w", err) + } + secretData = string(res) + case "v5-password-string": + newResource.Description = resource.Description + if description != "" { + newResource.Description = description + } + if password != "" { + secretData = password + } else { + secret, err := c.GetSecret(ctx, resourceID) + if err != nil { + return fmt.Errorf("Getting Secret: %w", err) + } + secretData, err = c.DecryptMessage(secret.Data) + if err != nil { + return fmt.Errorf("Decrypting Secret: %w", err) + } + } + case "v5-default-with-totp": secret, err := c.GetSecret(ctx, resourceID) if err != nil { return fmt.Errorf("Getting Secret: %w", err) @@ -437,72 +542,163 @@ func UpdateResource(ctx context.Context, c *api.Client, resourceID, name, userna if err != nil { return fmt.Errorf("Decrypting Secret: %w", err) } - var oldSecret api.SecretDataTypePasswordAndDescription - err = json.Unmarshal([]byte(oldSecretData), &oldSecret) + var oldSecret api.SecretDataTypeV5DefaultWithTOTP + err = json.Unmarshal([]byte(oldSecretData), &secretData) if err != nil { return fmt.Errorf("Parsing Decrypted Secret Data: %w", err) } - if password == "" { - tmp.Password = oldSecret.Password + if password != "" { + oldSecret.Password = password } - if description == "" { - tmp.Description = oldSecret.Description + if description != "" { + oldSecret.Description = description } + + res, err := json.Marshal(&oldSecret) + if err != nil { + return fmt.Errorf("Marshalling Secret Data: %w", err) + } + secretData = string(res) + case "v5-totp-standalone": + secret, err := c.GetSecret(ctx, resourceID) + if err != nil { + return fmt.Errorf("Getting Secret: %w", err) + } + oldSecretData, err := c.DecryptMessage(secret.Data) + if err != nil { + return fmt.Errorf("Decrypting Secret: %w", err) + } + var oldSecret api.SecretDataTypeTOTP + err = json.Unmarshal([]byte(oldSecretData), &secretData) + if err != nil { + return fmt.Errorf("Parsing Decrypted Secret Data: %w", err) + } + // since we don't have totp parameters we don't do anything + + res, err := json.Marshal(&oldSecret) + if err != nil { + return fmt.Errorf("Marshalling Secret Data: %w", err) + } + secretData = string(res) + default: + return fmt.Errorf("Unknown ResourceType: %v", rType.Slug) } - res, err := json.Marshal(&tmp) - if err != nil { - return fmt.Errorf("Marshalling Secret Data: %w", err) + } else { + // V4 Resource + newResource.Name = resource.Name + newResource.Username = resource.Username + newResource.URI = resource.URI + + if name != "" { + newResource.Name = name } - secretData = string(res) - case "password-description-totp": - secret, err := c.GetSecret(ctx, resourceID) - if err != nil { - return fmt.Errorf("Getting Secret: %w", err) + if username != "" { + newResource.Username = username } - oldSecretData, err := c.DecryptMessage(secret.Data) - if err != nil { - return fmt.Errorf("Decrypting Secret: %w", err) - } - var oldSecret api.SecretDataTypePasswordDescriptionTOTP - err = json.Unmarshal([]byte(oldSecretData), &secretData) - if err != nil { - return fmt.Errorf("Parsing Decrypted Secret Data: %w", err) - } - if password != "" { - oldSecret.Password = password - } - if description != "" { - oldSecret.Description = description + if uri != "" { + newResource.URI = uri } - res, err := json.Marshal(&oldSecret) - if err != nil { - return fmt.Errorf("Marshalling Secret Data: %w", err) - } - secretData = string(res) - case "totp": - secret, err := c.GetSecret(ctx, resourceID) - if err != nil { - return fmt.Errorf("Getting Secret: %w", err) - } - oldSecretData, err := c.DecryptMessage(secret.Data) - if err != nil { - return fmt.Errorf("Decrypting Secret: %w", err) - } - var oldSecret api.SecretDataTypeTOTP - err = json.Unmarshal([]byte(oldSecretData), &secretData) - if err != nil { - return fmt.Errorf("Parsing Decrypted Secret Data: %w", err) - } - // since we don't have totp parameters we don't do anything + // Secret + switch rType.Slug { + case "password-string": + newResource.Description = resource.Description + if description != "" { + newResource.Description = description + } + if password != "" { + secretData = password + } else { + secret, err := c.GetSecret(ctx, resourceID) + if err != nil { + return fmt.Errorf("Getting Secret: %w", err) + } + secretData, err = c.DecryptMessage(secret.Data) + if err != nil { + return fmt.Errorf("Decrypting Secret: %w", err) + } + } + case "password-and-description": + tmp := api.SecretDataTypePasswordAndDescription{ + Password: password, + Description: description, + } + if password != "" || description != "" { + secret, err := c.GetSecret(ctx, resourceID) + if err != nil { + return fmt.Errorf("Getting Secret: %w", err) + } + oldSecretData, err := c.DecryptMessage(secret.Data) + if err != nil { + return fmt.Errorf("Decrypting Secret: %w", err) + } + var oldSecret api.SecretDataTypePasswordAndDescription + err = json.Unmarshal([]byte(oldSecretData), &oldSecret) + if err != nil { + return fmt.Errorf("Parsing Decrypted Secret Data: %w", err) + } + if password == "" { + tmp.Password = oldSecret.Password + } + if description == "" { + tmp.Description = oldSecret.Description + } + } + res, err := json.Marshal(&tmp) + if err != nil { + return fmt.Errorf("Marshalling Secret Data: %w", err) + } + secretData = string(res) + case "password-description-totp": + secret, err := c.GetSecret(ctx, resourceID) + if err != nil { + return fmt.Errorf("Getting Secret: %w", err) + } + oldSecretData, err := c.DecryptMessage(secret.Data) + if err != nil { + return fmt.Errorf("Decrypting Secret: %w", err) + } + var oldSecret api.SecretDataTypePasswordDescriptionTOTP + err = json.Unmarshal([]byte(oldSecretData), &secretData) + if err != nil { + return fmt.Errorf("Parsing Decrypted Secret Data: %w", err) + } + if password != "" { + oldSecret.Password = password + } + if description != "" { + oldSecret.Description = description + } - res, err := json.Marshal(&oldSecret) - if err != nil { - return fmt.Errorf("Marshalling Secret Data: %w", err) + res, err := json.Marshal(&oldSecret) + if err != nil { + return fmt.Errorf("Marshalling Secret Data: %w", err) + } + secretData = string(res) + case "totp": + secret, err := c.GetSecret(ctx, resourceID) + if err != nil { + return fmt.Errorf("Getting Secret: %w", err) + } + oldSecretData, err := c.DecryptMessage(secret.Data) + if err != nil { + return fmt.Errorf("Decrypting Secret: %w", err) + } + var oldSecret api.SecretDataTypeTOTP + err = json.Unmarshal([]byte(oldSecretData), &secretData) + if err != nil { + return fmt.Errorf("Parsing Decrypted Secret Data: %w", err) + } + // since we don't have totp parameters we don't do anything + + res, err := json.Marshal(&oldSecret) + if err != nil { + return fmt.Errorf("Marshalling Secret Data: %w", err) + } + secretData = string(res) + default: + return fmt.Errorf("Unknown ResourceType: %v", rType.Slug) } - secretData = string(res) - default: - return fmt.Errorf("Unknown ResourceType: %v", rType.Slug) } err = validateSecretData(rType, secretData) @@ -520,7 +716,12 @@ func UpdateResource(ctx context.Context, c *api.Client, resourceID, name, userna return fmt.Errorf("Encrypting Secret Data for User me: %w", err) } } else { - encSecretData, err = c.EncryptMessageWithPublicKey(user.GPGKey.ArmoredKey, secretData) + publicKey, err := crypto.NewKeyFromArmored(user.GPGKey.ArmoredKey) + if err != nil { + return fmt.Errorf("Get Public Key: %w", err) + } + + encSecretData, err = c.EncryptMessageWithKey(publicKey, secretData) if err != nil { return fmt.Errorf("Encrypting Secret Data for User %v: %w", user.ID, err) } diff --git a/helper/secret.go b/helper/secret.go index f6849dc..586ac26 100644 --- a/helper/secret.go +++ b/helper/secret.go @@ -57,7 +57,7 @@ func validateSecretData(rType *api.ResourceType, secretData string) error { err = schema.Validate(strings.NewReader(secretData)) if err != nil { - return fmt.Errorf("Validating Secret Data: %w", err) + return fmt.Errorf("Validating Secret Data with Schema: %w", err) } return nil }