diff --git a/api/encryption.go b/api/encryption.go index c39eee3..2c393ee 100644 --- a/api/encryption.go +++ b/api/encryption.go @@ -33,12 +33,19 @@ func (c *Client) EncryptMessage(message string) (string, error) { } // EncryptMessageWithPublicKey encrypts a message using the provided public key and then signes the message using the users private key +// +// Deprecated: EncryptMessageWithPublicKey is deprecated. Use EncryptMessageWithKey instead func (c *Client) EncryptMessageWithPublicKey(publickey, message string) (string, error) { publicKey, err := crypto.NewKeyFromArmored(publickey) if err != nil { return "", fmt.Errorf("Get Public Key: %w", err) } + return c.EncryptMessageWithKey(publicKey, message) +} + +// EncryptMessageWithKey encrypts a message using the provided key and then signes the message using the users private key +func (c *Client) EncryptMessageWithKey(publicKey *crypto.Key, message string) (string, error) { key, err := c.userPrivateKey.Copy() if err != nil { return "", fmt.Errorf("Get Private Key Copy: %w", err) diff --git a/api/metadata.go b/api/metadata.go new file mode 100644 index 0000000..cc40425 --- /dev/null +++ b/api/metadata.go @@ -0,0 +1,81 @@ +package api + +import ( + "fmt" + + "github.com/ProtonMail/gopenpgp/v3/crypto" +) + +// ResourceMetadataTypeV5Default +type ResourceMetadataTypeV5Default struct { + ObjectType string `json:"object_type"` + ResourceTypeID string `json:"resource_type_id,omitempty"` + Name string `json:"name,omitempty"` + Username string `json:"username,omitempty"` + URIs []string `json:"uris,omitempty"` + Description string `json:"description,omitempty"` +} + +// ResourceMetadataTypeV5DefaultWithTOTP +type ResourceMetadataTypeV5DefaultWithTOTP struct { + ObjectType string `json:"object_type"` + ResourceTypeID string `json:"resource_type_id,omitempty"` + Name string `json:"name,omitempty"` + Username string `json:"username,omitempty"` + URIs []string `json:"uris,omitempty"` + Description string `json:"description,omitempty"` +} + +// ResourceMetadataTypeV5PasswordString +type ResourceMetadataTypeV5PasswordString struct { + ObjectType string `json:"object_type"` + ResourceTypeID string `json:"resource_type_id,omitempty"` + Name string `json:"name,omitempty"` + Username string `json:"username,omitempty"` + URIs []string `json:"uris,omitempty"` + Description string `json:"description,omitempty"` +} + +// ResourceMetadataTypeV5TOTPStandalone +type ResourceMetadataTypeV5TOTPStandalone struct { + ObjectType string `json:"object_type"` + ResourceTypeID string `json:"resource_type_id,omitempty"` + Name string `json:"name,omitempty"` + URIs []string `json:"uris,omitempty"` + Description string `json:"description,omitempty"` +} + +func (c *Client) DecryptMetadata(metadataKey *crypto.Key, armoredCiphertext string) (string, error) { + // TODO Get SessionKey from Cache + var sessionKey *crypto.SessionKey = nil + + if sessionKey != nil { + message, err := c.DecryptMessageWithSessionKey(sessionKey, armoredCiphertext) + // If Decrypt was successfull + if err == nil { + return message, nil + } + // if this failed, fall through + } + + metadata, newSessionKey, err := c.DecryptMessageWithPrivateKeyAndReturnSessionKey(metadataKey, armoredCiphertext) + if err != nil { + return "", fmt.Errorf("Decrypting Metadata: %w", err) + } + + // TODO Save newSessionKey to cache + _ = newSessionKey + + return metadata, nil +} + +func (c *Client) EncryptMetadata(metadataKey *crypto.Key, data string) (string, error) { + armoredCiphertext, err := c.EncryptMessageWithKey(metadataKey, data) + if err != nil { + return "", fmt.Errorf("Encrypting Metadata: %w", err) + } + + // TODO save Session Key to cache + + return armoredCiphertext, nil +} diff --git a/api/schema.go b/api/schema.go new file mode 100644 index 0000000..b9fe6d4 --- /dev/null +++ b/api/schema.go @@ -0,0 +1,231 @@ +package api + +import "encoding/json" + +// Fallback Schema, Only to be used if we encounter a Broken Server (v5.0), Not API Stable! +var ResourceSchemas = map[string]json.RawMessage{ + "v5-default": json.RawMessage(` +{ + "resource": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "maxLength": 255 + }, + "username": { + "type": "string", + "maxLength": 255, + "nullable": true + }, + "uris": { + "type": "array", + "items": { + "type": "string", + "maxLength": 1024, + "nullable": true + } + }, + "description": { + "type": "string", + "maxLength": 10000, + "nullable": true + } + } + }, + "secret": { + "type": "object", + "required": ["password"], + "properties": { + "object_type": { + "type": "string", + "enum": ["PASSBOLT_SECRET_DATA"] + }, + "password": { + "type": "string", + "maxLength": 4096, + "nullable": true + }, + "description": { + "type": "string", + "maxLength": 10000, + "nullable": true + } + } + } +}`), + "v5-password-string": json.RawMessage(` +{ + "resource": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "maxLength": 255 + }, + "username": { + "type": "string", + "maxLength": 255, + "nullable": true + }, + "uris": { + "type": "array", + "items": { + "type": "string", + "maxLength": 1024, + "nullable": true + } + }, + "description": { + "type": "string", + "maxLength": 10000, + "nullable": true + } + } + }, + "secret": { + "type": "string", + "maxLength": 4096 + } +}`), + "v5-default-with-totp": json.RawMessage(` +{ + "resource": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "maxLength": 255 + }, + "username": { + "type": "string", + "maxLength": 255, + "nullable": true + }, + "uris": { + "type": "array", + "items": { + "type": "string", + "maxLength": 1024, + "nullable": true + } + }, + "description": { + "type": "string", + "maxLength": 10000, + "nullable": true + } + } + }, + "secret": { + "type": "object", + "required": ["totp"], + "properties": { + "object_type": { + "type": "string", + "enum": ["PASSBOLT_SECRET_DATA"] + }, + "password": { + "type": "string", + "maxLength": 4096, + "nullable": true + }, + "description": { + "type": "string", + "maxLength": 10000, + "nullable": true + }, + "totp": { + "type": "object", + "required": ["secret_key", "digits", "algorithm"], + "properties": { + "algorithm": { + "type": "string", + "minLength": 4, + "maxLength": 6 + }, + "secret_key": { + "type": "string", + "maxLength": 1024 + }, + "digits": { + "type": "number", + "minimum": 6, + "maximum": 8 + }, + "period": { + "type": "number" + } + } + } + } + } +}`), + "v5-totp-standalone": json.RawMessage(` +{ + "resource": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "maxLength": 255 + }, + "username": { + "type": "string", + "maxLength": 255, + "nullable": true + }, + "uris": { + "type": "array", + "items": { + "type": "string", + "maxLength": 1024, + "nullable": true + } + }, + "description": { + "type": "string", + "maxLength": 10000, + "nullable": true + } + } + }, + "secret": { + "type": "object", + "required": ["totp"], + "properties": { + "object_type": { + "type": "string", + "enum": ["PASSBOLT_SECRET_DATA"] + }, + "totp": { + "type": "object", + "required": ["secret_key", "digits", "algorithm"], + "properties": { + "algorithm": { + "type": "string", + "minLength": 4, + "maxLength": 6 + }, + "secret_key": { + "type": "string", + "maxLength": 1024 + }, + "digits": { + "type": "number", + "minimum": 6, + "maximum": 8 + }, + "period": { + "type": "number" + } + } + } + } + } +}`), +} diff --git a/api/schema_test.go b/api/schema_test.go new file mode 100644 index 0000000..a9069e4 --- /dev/null +++ b/api/schema_test.go @@ -0,0 +1,18 @@ +package api + +import ( + "encoding/json" + "testing" +) + +func TestResourceJsonSchema(t *testing.T) { + for slug, schema := range ResourceSchemas { + var schemaDefinition ResourceTypeSchema + err := json.Unmarshal(schema, &schemaDefinition) + if err != nil { + t.Errorf("Error While Parsing Resource Schema %v: %v", slug, err) + } else { + t.Logf("Schema for type %v is ok", slug) + } + } +} diff --git a/api/secrets.go b/api/secrets.go index 8bcad6c..60c8d6a 100644 --- a/api/secrets.go +++ b/api/secrets.go @@ -41,6 +41,33 @@ type SecretDataTypePasswordDescriptionTOTP struct { TOTP SecretDataTOTP `json:"totp"` } +// SecretDataTypeV5Default +type SecretDataTypeV5Default struct { + ObjectType string `json:"object_type"` + ResourceTypeID string `json:"resource_type_id,omitempty"` + Password string `json:"password,omitempty"` + Description string `json:"description,omitempty"` +} + +// SecretDataTypeV5DefaultWithTOTP +type SecretDataTypeV5DefaultWithTOTP struct { + ObjectType string `json:"object_type"` + ResourceTypeID string `json:"resource_type_id,omitempty"` + Password string `json:"password,omitempty"` + Description string `json:"description,omitempty"` + TOTP SecretDataTOTP `json:"totp"` +} + +// SecretDataTypeV5PasswordString, is just the Password directly +type SecretDataTypeV5PasswordString string + +// SecretDataTypeV5TOTPStandalone +type SecretDataTypeV5TOTPStandalone struct { + ObjectType string `json:"object_type"` + ResourceTypeID string `json:"resource_type_id,omitempty"` + TOTP SecretDataTOTP `json:"totp"` +} + // GetSecret gets a Passbolt Secret func (c *Client) GetSecret(ctx context.Context, resourceID string) (*Secret, error) { err := checkUUIDFormat(resourceID) diff --git a/helper/metadata.go b/helper/metadata.go new file mode 100644 index 0000000..e89cb78 --- /dev/null +++ b/helper/metadata.go @@ -0,0 +1,109 @@ +package helper + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + + "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) { + keys, err := c.GetMetadataKeys(ctx, &api.GetMetadataKeysOptions{ + ContainMetadataPrivateKeys: true, + }) + if err != nil { + return "", fmt.Errorf("Get Metadata Key: %w", err) + } + + // TODO Get Key by id? + if len(keys) != 1 { + return "", fmt.Errorf("Not Exactly One Metadatakey Available") + } + + if len(keys[0].MetadataPrivateKeys) == 0 { + return "", fmt.Errorf("No Metadata Private key for our user") + } + + if len(keys[0].MetadataPrivateKeys) > 1 { + return "", 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 "", fmt.Errorf("MetadataPrivateKey is not for our user id: %v", privMetdata.UserID) + } + + decPrivMetadatakey, err := c.DecryptMessage(privMetdata.Data) + if err != nil { + return "", fmt.Errorf("Decrypt Metadata Private Key Data: %w", err) + } + + var data api.MetadataPrivateKeyData + err = json.Unmarshal([]byte(decPrivMetadatakey), &data) + if err != nil { + return "", fmt.Errorf("Parse Metadata Private Key Data") + } + + metadataPrivateKeyObj, err := api.GetPrivateKeyFromArmor(data.ArmoredKey, []byte(data.Passphrase)) + if err != nil { + return "", fmt.Errorf("Get Metadata Private Key: %w", err) + } + + decMetadata, err := c.DecryptMetadata(metadataPrivateKeyObj, resource.Metadata) + if err != nil { + return "", fmt.Errorf("Decrypt Metadata: %w", err) + } + + var schemaDefinition api.ResourceTypeSchema + err = json.Unmarshal([]byte(rType.Definition), &schemaDefinition) + if err != nil { + // Workaround for inconsistant API Responses where sometime the Schema is embedded directly and sometimes it's escaped as a string + if err.Error() == "json: cannot unmarshal string into Go value of type api.ResourceTypeSchema" { + var tmp string + err = json.Unmarshal([]byte(rType.Definition), &tmp) + if err != nil { + return "", fmt.Errorf("Workaround Unmarshal Json Schema String: %w", err) + } + + if tmp == "[]" { + // Use The Builtin Fallback Schemas in this Case + schema, ok := api.ResourceSchemas[rType.Slug] + if !ok { + return "", fmt.Errorf("Server Does not have the Required json Schema and there is no fallback available for type: %v", rType.Slug) + } + tmp = string(schema) + } + + err = json.Unmarshal([]byte(tmp), &schemaDefinition) + if err != nil { + return "", fmt.Errorf("Workaround Unmarshal Json Schema: %w", err) + } + + } else { + return "", fmt.Errorf("Unmarshal Json Schema: %w", err) + } + } + + comp := jsonschema.NewCompiler() + + err = comp.AddResource("metadata.json", bytes.NewReader(schemaDefinition.Resource)) + if err != nil { + return "", fmt.Errorf("Adding Json Schema: %w", err) + } + + schema, err := comp.Compile("metadata.json") + if err != nil { + return "", fmt.Errorf("Compiling Json Schema: %w", err) + } + + err = schema.Validate(strings.NewReader(decMetadata)) + if err != nil { + return "", fmt.Errorf("Validating Secret Data: %w", err) + } + + return decMetadata, nil +} diff --git a/helper/resources.go b/helper/resources.go index 6a4f786..9c2d31a 100644 --- a/helper/resources.go +++ b/helper/resources.go @@ -104,16 +104,26 @@ func GetResource(ctx context.Context, c *api.Client, resourceID string) (folderP } // GetResourceFromData Decrypts Resources using only local data, the Resource object must inlude the secret -func GetResourceFromData(c *api.Client, resource api.Resource, secret api.Secret, rType api.ResourceType) (folderParentID, name, username, uri, password, description string, err error) { +// With v5 This needs network calls for Metadata of v5 Resources +func GetResourceFromData(c *api.Client, resource api.Resource, secret api.Secret, rType api.ResourceType) (string, string, string, string, string, string, error) { + var name string + var username string + var uri string var pw string var desc string + ctx := context.TODO() + switch rType.Slug { case "password-string": + var err error pw, err = c.DecryptMessage(secret.Data) if err != nil { return "", "", "", "", "", "", fmt.Errorf("Decrypting Secret Data: %w", err) } + name = resource.Name + username = resource.Username + uri = resource.URI desc = resource.Description case "password-and-description": rawSecretData, err := c.DecryptMessage(secret.Data) @@ -126,6 +136,9 @@ func GetResourceFromData(c *api.Client, resource api.Resource, secret api.Secret if err != nil { return "", "", "", "", "", "", fmt.Errorf("Parsing Decrypted Secret Data: %w", err) } + name = resource.Name + username = resource.Username + uri = resource.URI pw = secretData.Password desc = secretData.Description case "password-description-totp": @@ -139,14 +152,109 @@ func GetResourceFromData(c *api.Client, resource api.Resource, secret api.Secret if err != nil { return "", "", "", "", "", "", fmt.Errorf("Parsing Decrypted Secret Data: %w", err) } + name = resource.Name + username = resource.Username + uri = resource.URI pw = secretData.Password desc = secretData.Description case "totp": + name = resource.Name + username = resource.Username + uri = resource.URI + // nothing fits into the interface in this case + case "v5-default": + rawMetadata, err := GetResourceMetadata(ctx, c, resource, rType) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Getting Metadata: %w", err) + } + + var metadata api.ResourceMetadataTypeV5Default + err = json.Unmarshal([]byte(rawMetadata), &metadata) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Parsing Decrypted Metadata: %w", err) + } + + name = metadata.Name + username = metadata.Username + if len(metadata.URIs) != 0 { + uri = metadata.URIs[0] + } + + rawSecretData, err := c.DecryptMessage(secret.Data) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Decrypting Secret Data: %w", err) + } + + var secretData api.SecretDataTypeV5Default + err = json.Unmarshal([]byte(rawSecretData), &secretData) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Parsing Decrypted Secret Data: %w", err) + } + pw = secretData.Password + desc = secretData.Description + case "v5-default-with-totp": + rawMetadata, err := GetResourceMetadata(ctx, c, resource, rType) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Getting Metadata: %w", err) + } + + var metadata api.ResourceMetadataTypeV5DefaultWithTOTP + err = json.Unmarshal([]byte(rawMetadata), &metadata) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Parsing Decrypted Metadata: %w", err) + } + + name = metadata.Name + username = metadata.Username + if len(metadata.URIs) != 0 { + uri = metadata.URIs[0] + } + + rawSecretData, err := c.DecryptMessage(secret.Data) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Decrypting Secret Data: %w", err) + } + + var secretData api.SecretDataTypeV5DefaultWithTOTP + err = json.Unmarshal([]byte(rawSecretData), &secretData) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Parsing Decrypted Secret Data: %w", err) + } + pw = secretData.Password + desc = secretData.Description + case "v5-password-string": + rawMetadata, err := GetResourceMetadata(ctx, c, resource, rType) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Getting Metadata: %w", err) + } + + var metadata api.ResourceMetadataTypeV5PasswordString + err = json.Unmarshal([]byte(rawMetadata), &metadata) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Parsing Decrypted Metadata: %w", err) + } + + name = metadata.Name + username = metadata.Username + if len(metadata.URIs) != 0 { + uri = metadata.URIs[0] + } + + // Not available in the Secret + desc = metadata.Description + + rawSecretData, err := c.DecryptMessage(secret.Data) + if err != nil { + return "", "", "", "", "", "", fmt.Errorf("Decrypting Secret Data: %w", err) + } + + pw = rawSecretData + case "v5-totp-standalone": // nothing fits into the interface in this case default: return "", "", "", "", "", "", fmt.Errorf("Unknown ResourceType: %v", rType.Slug) } - return resource.FolderParentID, resource.Name, resource.Username, resource.URI, pw, desc, nil + return resource.FolderParentID, name, username, uri, pw, desc, nil } // UpdateResource Updates all Fields.