diff --git a/go.mod b/go.mod index d49f4a3..e4a5dc5 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/phasehq/golang-sdk go 1.20 + +require github.com/jamesruan/sodium v1.0.14 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6768070 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/jamesruan/sodium v1.0.14 h1:JfOHobip/lUWouxHV3PwYwu3gsLewPrDrZXO3HuBzUU= +github.com/jamesruan/sodium v1.0.14/go.mod h1:GK2+LACf7kuVQ9k7Irk0MB2B65j5rVqkz+9ylGIggZk= diff --git a/pkg/phase/phaseIO.go b/phase.go similarity index 57% rename from pkg/phase/phaseIO.go rename to phase.go index fad5a0d..81a90c4 100644 --- a/pkg/phase/phaseIO.go +++ b/phase.go @@ -73,8 +73,16 @@ type AppKeyResponse struct { Apps []App `json:"apps"` } +// UpdateSecretOptions holds all the options for updating a secret. +type UpdateSecretOptions struct { + EnvName string + AppName string + Key string + Value string + Path string +} -func (p *Phase) PhaseGet(envName, appName, keyToFind, tag string) ([]map[string]interface{}, error) { +func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[string]interface{}, error) { // Fetch user data resp, err := api.FetchPhaseUser(p.AppToken, p.Host) if err != nil { @@ -89,83 +97,78 @@ func (p *Phase) PhaseGet(envName, appName, keyToFind, tag string) ([]map[string] return nil, err } - // Identify the correct environment and application envKey, err := findEnvironmentKey(&userData, envName, appName) if err != nil { log.Printf("Failed to find environment key: %v", err) return nil, err } - // Decrypt the wrapped seed decryptedSeed, err := p.Decrypt(envKey.WrappedSeed) if err != nil { log.Printf("Failed to decrypt wrapped seed: %v", err) return nil, err } + decryptedSalt, err := p.Decrypt(envKey.WrappedSalt) + if err != nil { + log.Printf("Failed to decrypt wrapped salt: %v", err) + return nil, err + } - // Generate environment key pair publicKeyHex, privateKeyHex, err := generateEnvKeyPair(decryptedSeed) if err != nil { log.Printf("Failed to generate environment key pair: %v", err) return nil, err } - // Fetch secrets - secrets, err := api.FetchPhaseSecrets(p.AppToken, envKey.Environment.ID, p.Host) + keyDigest, err := crypto.Blake2bDigest(keyToFind, decryptedSalt) if err != nil { - log.Printf("Failed to fetch secrets: %v", err) + log.Printf("Failed to generate key digest: %v", err) return nil, err } - var foundSecrets []map[string]interface{} - keyFound := false - - for _, secret := range secrets { - decryptedKey, decryptedValue, decryptedComment, err := decryptSecret(secret, privateKeyHex, publicKeyHex) - if err != nil { - log.Printf("Failed to decrypt secret: %v\n", err) - continue - } - - if decryptedKey == keyToFind { - keyFound = true + // Fetch a single secret based on keyDigest and optional path + secret, err := api.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, path) + if err != nil { + log.Printf("Failed to fetch secret: %v", err) + return nil, err + } - // Prepare tags for inclusion in result - var stringTags []string - if secretTags, ok := secret["tags"].([]interface{}); ok { - for _, tagInterface := range secretTags { - if tagStr, ok := tagInterface.(string); ok { - stringTags = append(stringTags, tagStr) - } - } + decryptedKey, decryptedValue, decryptedComment, err := decryptSecret(secret, privateKeyHex, publicKeyHex) + if err != nil { + log.Printf("Failed to decrypt secret: %v", err) + return nil, err + } - // If a tag is provided, ensure it matches. - if tag != "" && !tagMatches(stringTags, tag) { - continue + // Verify tag match if a tag is provided + var stringTags []string + if tag != "" { + if secretTags, ok := secret["tags"].([]interface{}); ok { + for _, tagInterface := range secretTags { + if tagStr, ok := tagInterface.(string); ok { + stringTags = append(stringTags, tagStr) } } - - result := map[string]interface{}{ - "key": decryptedKey, - "value": decryptedValue, - "comment": decryptedComment, - "tags": stringTags, + if !tagMatches(stringTags, tag) { + return nil, fmt.Errorf("secret with key '%s' found, but doesn't match the provided tag '%s'", keyToFind, tag) } - - foundSecrets = append(foundSecrets, result) - break } } - if !keyFound { - log.Printf("Secret with key '%s' not found or could not be decrypted.", keyToFind) + // Extract the path directly from the secret map + secretPath, _ := secret["path"].(string) + + result := &map[string]interface{}{ + "key": decryptedKey, + "value": decryptedValue, + "comment": decryptedComment, + "tags": stringTags, + "path": secretPath, } - return foundSecrets, nil + return result, nil } - -func (p *Phase) GetAllSecrets(envName, appName, tag string) ([]map[string]interface{}, error) { +func (p *Phase) GetAllSecrets(envName, appName, tag, path string) ([]map[string]interface{}, error) { // Fetch user data resp, err := api.FetchPhaseUser(p.AppToken, p.Host) if err != nil { @@ -201,8 +204,8 @@ func (p *Phase) GetAllSecrets(envName, appName, tag string) ([]map[string]interf return nil, err } - // Fetch secrets - secrets, err := api.FetchPhaseSecrets(p.AppToken, envKey.Environment.ID, p.Host) + // Fetch secrets with optional path filtering + secrets, err := api.FetchPhaseSecrets(p.AppToken, envKey.Environment.ID, p.Host, path) if err != nil { log.Fatalf("Failed to fetch secrets: %v", err) return nil, err @@ -225,22 +228,23 @@ func (p *Phase) GetAllSecrets(envName, appName, tag string) ([]map[string]interf stringTags = append(stringTags, tagStr) } } + } - // Check for tag match if a tag is provided - if tag != "" && !tagMatches(stringTags, tag) { - continue - } - } else if tag != "" { - // If there are no tags but a tag filter is specified, skip this secret. + // Check for tag match if a tag is provided + if tag != "" && !tagMatches(stringTags, tag) { continue } - // Append decrypted secret to result list + // Extract path directly from the secret map + path, _ := secret["path"].(string) + + // Append decrypted secret with path to result list result := map[string]interface{}{ "key": decryptedKey, "value": decryptedValue, "comment": decryptedComment, "tags": stringTags, + "path": path, } decryptedSecrets = append(decryptedSecrets, result) @@ -250,26 +254,26 @@ func (p *Phase) GetAllSecrets(envName, appName, tag string) ([]map[string]interf } // CreateSecrets creates new secrets in the Phase KMS for the specified environment and application. -func (p *Phase) CreateSecrets(keyValuePairs []map[string]string, envName, appName string) error { - // Fetch user data - resp, err := api.FetchPhaseUser(p.AppToken, p.Host) - if err != nil { - log.Fatalf("Failed to fetch user data: %v", err) - return err - } - defer resp.Body.Close() +func (p *Phase) CreateSecrets(keyValuePairs []map[string]string, envName, appName string, keyPaths map[string]string) error { + // Fetch user data + resp, err := api.FetchPhaseUser(p.AppToken, p.Host) + if err != nil { + log.Fatalf("Failed to fetch user data: %v", err) + return err + } + defer resp.Body.Close() - var userData AppKeyResponse - if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { - log.Fatalf("Failed to decode user data: %v", err) - return err - } + var userData AppKeyResponse + if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { + log.Fatalf("Failed to decode user data: %v", err) + return err + } - _, envID, publicKey, err := phaseGetContext(&userData, appName, envName) - if err != nil { - log.Fatalf("Failed to get context: %v", err) - return err - } + _, envID, publicKey, err := phaseGetContext(&userData, appName, envName) + if err != nil { + log.Fatalf("Failed to get context: %v", err) + return err + } // Identify the correct environment and application envKey, err := findEnvironmentKey(&userData, envName, appName) @@ -278,249 +282,200 @@ func (p *Phase) CreateSecrets(keyValuePairs []map[string]string, envName, appNam return err } - decryptedSalt, err := p.Decrypt(envKey.WrappedSalt) - if err != nil { - log.Fatalf("Failed to decrypt wrapped salt: %v", err) - return err - } - - secrets := make([]map[string]interface{}, 0) - for _, pair := range keyValuePairs { - for key, value := range pair { - encryptedKey, err := crypto.EncryptAsymmetric(key, publicKey) - if err != nil { - log.Printf("Failed to encrypt key: %v\n", err) - continue - } + decryptedSalt, err := p.Decrypt(envKey.WrappedSalt) + if err != nil { + log.Fatalf("Failed to decrypt wrapped salt: %v", err) + return err + } - encryptedValue, err := crypto.EncryptAsymmetric(value, publicKey) - if err != nil { - log.Printf("Failed to encrypt value: %v\n", err) - continue - } + secrets := make([]map[string]interface{}, 0) + for _, pair := range keyValuePairs { + for key, value := range pair { + encryptedKey, err := crypto.EncryptAsymmetric(key, publicKey) + if err != nil { + log.Printf("Failed to encrypt key: %v\n", err) + continue + } - keyDigest, err := crypto.Blake2bDigest(key, decryptedSalt) - if err != nil { - log.Printf("Failed to generate key digest: %v\n", err) - continue - } + encryptedValue, err := crypto.EncryptAsymmetric(value, publicKey) + if err != nil { + log.Printf("Failed to encrypt value: %v\n", err) + continue + } - secret := map[string]interface{}{ - "key": encryptedKey, - "keyDigest": keyDigest, - "value": encryptedValue, - "folderId": nil, - "tags": []string{}, - "comment": "", - } - secrets = append(secrets, secret) - } - } + keyDigest, err := crypto.Blake2bDigest(key, decryptedSalt) + if err != nil { + log.Printf("Failed to generate key digest: %v\n", err) + continue + } - return api.CreatePhaseSecrets(p.AppToken, envID, secrets, p.Host) -} + // Determine the path for the secret, default to "/" if not specified + path, ok := keyPaths[key] + if !ok { + path = "/" // Default path if not provided + } -// UpdateSecret updates a secret in Phase KMS based on key and environment. -func (p *Phase) UpdateSecret(envName, key, value, appName string) error { - // Fetch user data - resp, err := api.FetchPhaseUser(p.AppToken, p.Host) - if err != nil { - log.Fatalf("Failed to fetch user data: %v", err) - return err - } - defer resp.Body.Close() + secret := map[string]interface{}{ + "key": encryptedKey, + "keyDigest": keyDigest, + "value": encryptedValue, + "path": path, + "tags": []string{}, + "comment": "", + } + secrets = append(secrets, secret) + } + } - var userData AppKeyResponse - if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { - log.Fatalf("Failed to decode user data: %v", err) - return err - } + return api.CreatePhaseSecrets(p.AppToken, envID, secrets, p.Host) +} - _, envID, publicKey, err := phaseGetContext(&userData, appName, envName) - if err != nil { - log.Fatalf("Failed to get context: %v", err) - return err - } +func (p *Phase) UpdateSecret(opts UpdateSecretOptions) error { + // Fetch user data + resp, err := api.FetchPhaseUser(p.AppToken, p.Host) + if err != nil { + log.Fatalf("Failed to fetch user data: %v", err) + return err + } + defer resp.Body.Close() - // Fetch existing secrets to find the one matching the key - secrets, err := api.FetchPhaseSecrets(p.AppToken, envID, p.Host) - if err != nil { - log.Fatalf("Failed to fetch secrets: %v", err) - return err - } + var userData AppKeyResponse + if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { + log.Fatalf("Failed to decode user data: %v", err) + return err + } - // Decrypt the wrapped seed - envKey, err := findEnvironmentKey(&userData, envName, appName) - if err != nil { - log.Fatalf("No environment found with id: %v", envID) - return err - } - decryptedSeed, err := p.Decrypt(envKey.WrappedSeed) - if err != nil { - log.Fatalf("Failed to decrypt wrapped seed: %v", err) - return err - } + envKey, err := findEnvironmentKey(&userData, opts.EnvName, opts.AppName) + if err != nil { + log.Fatalf("Failed to find environment key: %v", err) + return err + } - // Generate environment key pair - _, privateKeyHex, err := generateEnvKeyPair(decryptedSeed) - if err != nil { - log.Fatalf("Failed to generate environment key pair: %v", err) - return err - } + decryptedSalt, err := p.Decrypt(envKey.WrappedSalt) + if err != nil { + log.Fatalf("Failed to decrypt wrapped salt: %v", err) + return err + } - var secretUpdatePayload map[string]interface{} - for _, secret := range secrets { - decryptedKey, _, _, err := decryptSecret(secret, privateKeyHex, publicKey) - if err != nil { - log.Printf("Failed to decrypt secret key: %v\n", err) - continue - } + // Generate key digest + keyDigest, err := crypto.Blake2bDigest(opts.Key, decryptedSalt) + if err != nil { + log.Fatalf("Failed to generate key digest: %v", err) + return err + } - if decryptedKey == key { - encryptedKey, err := crypto.EncryptAsymmetric(key, publicKey) - if err != nil { - log.Fatalf("Failed to encrypt key: %v", err) - return err - } + // Fetch a single secret based on keyDigest + secret, err := api.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, opts.Path) + if err != nil { + log.Printf("Failed to fetch secret: %v", err) + return err + } - encryptedValue, err := crypto.EncryptAsymmetric(value, publicKey) - if err != nil { - log.Fatalf("Failed to encrypt value: %v", err) - return err - } + publicKeyHex := envKey.IdentityKey - decryptedSalt, err := p.Decrypt(envKey.WrappedSalt) - if err != nil { - log.Fatalf("Failed to decrypt wrapped salt: %v", err) - return err - } + // Encrypt the key and value with the environment's public key + encryptedKey, err := crypto.EncryptAsymmetric(opts.Key, publicKeyHex) + if err != nil { + log.Fatalf("Failed to encrypt key: %v", err) + return err + } - keyDigest, err := crypto.Blake2bDigest(key, decryptedSalt) - if err != nil { - log.Fatalf("Failed to generate key digest: %v", err) - return err - } + encryptedValue, err := crypto.EncryptAsymmetric(opts.Value, publicKeyHex) + if err != nil { + log.Fatalf("Failed to encrypt value: %v", err) + return err + } - secretID, ok := secret["id"].(string) - if !ok { - log.Fatalf("Secret ID is not a string") - return fmt.Errorf("secret ID is not a string") - } + secretID, ok := secret["id"].(string) + if !ok { + log.Fatalf("Secret ID is not a string") + return fmt.Errorf("secret ID is not a string") + } - secretUpdatePayload = map[string]interface{}{ - "id": secretID, - "key": encryptedKey, - "keyDigest": keyDigest, - "value": encryptedValue, - "folderId": nil, - "tags": []string{}, - "comment": "", - } - break - } - } + // Default path to "/" if not provided + if opts.Path == "" { + opts.Path = "/" + } - if secretUpdatePayload == nil { - log.Printf("Key '%s' doesn't exist.", key) - return fmt.Errorf("key '%s' doesn't exist", key) - } + secretUpdatePayload := map[string]interface{}{ + "id": secretID, + "key": encryptedKey, + "keyDigest": keyDigest, + "value": encryptedValue, + "path": opts.Path, + "tags": []string{}, + "comment": "", + } - // Perform the update - err = api.UpdatePhaseSecrets(p.AppToken, envID, []map[string]interface{}{secretUpdatePayload}, p.Host) - if err != nil { - log.Fatalf("Failed to update secret: %v", err) - return err - } + // Perform the update + err = api.UpdatePhaseSecrets(p.AppToken, envKey.Environment.ID, []map[string]interface{}{secretUpdatePayload}, p.Host) + if err != nil { + log.Fatalf("Failed to update secret: %v", err) + return err + } - log.Println("Success") - return nil + log.Println("Secret updated successfully") + return nil } -// DeleteSecrets deletes secrets in Phase KMS based on keys and environment. -func (p *Phase) DeleteSecrets(envName string, keysToDelete []string, appName string) ([]string, error) { - // Fetch user data - resp, err := api.FetchPhaseUser(p.AppToken, p.Host) - if err != nil { - log.Fatalf("Failed to fetch user data: %v", err) - return nil, err - } - defer resp.Body.Close() - var userData AppKeyResponse - if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { - log.Fatalf("Failed to decode user data: %v", err) - return nil, err - } - - _, envID, publicKey, err := phaseGetContext(&userData, appName, envName) - if err != nil { - log.Fatalf("Failed to get context: %v", err) - return nil, err - } +// DeleteSecret deletes a secret in Phase KMS based on a key and environment. +func (p *Phase) DeleteSecret(envName, appName, keyToDelete, path string) error { + // Fetch user data + resp, err := api.FetchPhaseUser(p.AppToken, p.Host) + if err != nil { + log.Fatalf("Failed to fetch user data: %v", err) + return err + } + defer resp.Body.Close() - secrets, err := api.FetchPhaseSecrets(p.AppToken, envID, p.Host) - if err != nil { - log.Fatalf("Failed to fetch secrets: %v", err) - return nil, err - } + var userData AppKeyResponse + if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { + log.Fatalf("Failed to decode user data: %v", err) + return err + } - // Identify the correct environment and application envKey, err := findEnvironmentKey(&userData, envName, appName) if err != nil { log.Printf("Failed to find environment key: %v", err) - return nil, err + return err } - - decryptedSeed, err := p.Decrypt(envKey.WrappedSeed) - if err != nil { - log.Fatalf("Failed to decrypt wrapped seed: %v", err) - return nil, err - } - _, privateKeyHex, err := generateEnvKeyPair(decryptedSeed) - if err != nil { - log.Fatalf("Failed to generate environment key pair: %v", err) - return nil, err - } - - var secretIDsToDelete []string - keysNotFound := make([]string, 0) + decryptedSalt, err := p.Decrypt(envKey.WrappedSalt) + if err != nil { + log.Fatalf("Failed to decrypt wrapped salt: %v", err) + return err + } - for _, key := range keysToDelete { - found := false - for _, secret := range secrets { - decryptedKey, _, _, err := decryptSecret(secret, privateKeyHex, publicKey) - if err != nil { - log.Printf("Failed to decrypt secret key: %v\n", err) - continue - } + // Generate key digest + keyDigest, err := crypto.Blake2bDigest(keyToDelete, decryptedSalt) + if err != nil { + log.Fatalf("Failed to generate key digest: %v", err) + return err + } - if decryptedKey == key { - secretID, ok := secret["id"].(string) - if !ok { - log.Printf("Secret ID is not a string for key: %v", key) - continue - } - secretIDsToDelete = append(secretIDsToDelete, secretID) - found = true - break - } - } + // Fetch the specific secret by its key digest and path + secret, err := api.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, path) + if err != nil { + log.Printf("Failed to fetch secret: %v", err) + return err + } - if !found { - keysNotFound = append(keysNotFound, key) - } - } + secretID, ok := secret["id"].(string) + if !ok { + log.Printf("Secret ID is not a string for key: %v", keyToDelete) + return fmt.Errorf("secret ID is not a string for key: %v", keyToDelete) + } - if len(secretIDsToDelete) > 0 { - err = api.DeletePhaseSecrets(p.AppToken, envID, secretIDsToDelete, p.Host) - if err != nil { - log.Fatalf("Failed to delete secrets: %v", err) - return keysNotFound, err - } - } + // Perform the delete operation for the found secret ID + err = api.DeletePhaseSecrets(p.AppToken, envKey.Environment.ID, []string{secretID}, p.Host) + if err != nil { + log.Fatalf("Failed to delete secret: %v", err) + return err + } - return keysNotFound, nil + log.Println("Secret deleted successfully") + return nil } // decryptSecret decrypts a secret's key, value, and optional comment using asymmetric decryption. diff --git a/pkg/api/client.go b/pkg/api/client.go index a227e5a..53d5878 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -202,10 +202,10 @@ func FetchWrappedKeyShare(appToken, host string) (string, error) { return wrappedKeyShare, nil } -func FetchPhaseSecrets(appToken, environmentID, host string) ([]map[string]interface{}, error) { +func FetchPhaseSecrets(appToken, environmentID, host, path string) ([]map[string]interface{}, error) { client := createHTTPClient() url := fmt.Sprintf("%s/service/secrets/", host) - + req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err @@ -213,6 +213,9 @@ func FetchPhaseSecrets(appToken, environmentID, host string) ([]map[string]inter req.Header = ConstructHTTPHeaders(appToken) req.Header.Set("Environment", environmentID) + if path != "" { + req.Header.Set("Path", path) + } resp, err := client.Do(req) if err != nil { @@ -232,6 +235,43 @@ func FetchPhaseSecrets(appToken, environmentID, host string) ([]map[string]inter return secrets, nil } +func FetchPhaseSecret(appToken, environmentID, host, keyDigest, path string) (map[string]interface{}, error) { + client := createHTTPClient() + url := fmt.Sprintf("%s/service/secrets/", host) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.Header = ConstructHTTPHeaders(appToken) + req.Header.Set("Environment", environmentID) + req.Header.Set("KeyDigest", keyDigest) + if path != "" { + req.Header.Set("Path", path) + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := handleHTTPResponse(resp); err != nil { + return nil, err + } + + var secrets []map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&secrets); err != nil { + return nil, fmt.Errorf("failed to decode JSON response: %v", err) + } + + if len(secrets) > 0 { + return secrets[0], nil + } + + return nil, fmt.Errorf("no secrets found in the response") +} func CreatePhaseSecrets(appToken, environmentID string, secrets []map[string]interface{}, host string) error { client := createHTTPClient() diff --git a/pkg/crypto/cryptoUtils.go b/pkg/crypto/cryptoUtils.go index db0496b..7c7b14e 100644 --- a/pkg/crypto/cryptoUtils.go +++ b/pkg/crypto/cryptoUtils.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/jamesruan/sodium" - "golang.org/x/crypto/blake2b" ) // Spin up an ephemeral X25519 keypair @@ -195,23 +194,29 @@ func DecryptAsymmetric(ciphertextString, privateKeyHex, publicKeyHex string) (st return plaintext, nil } -// Blake2bDigest generates a BLAKE2b hash of the input string with a salt. +// Blake2bDigest generates a BLAKE2b hash of the input string with a salt using the sodium library. func Blake2bDigest(inputStr, salt string) (string, error) { - hashSize := 32 // 32 bytes (256 bits) - - h, err := blake2b.New(hashSize, []byte(salt)) - if err != nil { - log.Printf("Failed to initialize BLAKE2b: %v\n", err) - return "", err + hashSize := 32 // 32 bytes (256 bits) as an example + var hasher *sodium.GenericHash + if len(salt) > 0 { + // Convert the salt string to a GenericHashKey. + key := sodium.GenericHashKey{Bytes: sodium.Bytes([]byte(salt))} + hasher = sodium.NewGenericHashKeyed(hashSize, key).(*sodium.GenericHash) + } else { + hasher = sodium.NewGenericHash(hashSize).(*sodium.GenericHash) } - _, err = h.Write([]byte(inputStr)) + // Write the input string to the hasher. + _, err := hasher.Write([]byte(inputStr)) if err != nil { log.Printf("Failed to write to BLAKE2b hasher: %v\n", err) return "", err } - hashed := h.Sum(nil) + // Compute the hash. + hashed := hasher.Sum(nil) + + // Encode the hash to a hexadecimal string. hexEncoded := hex.EncodeToString(hashed) return hexEncoded, nil }