From 7f3308be86207dfc106fa668d52d95efca328ae8 Mon Sep 17 00:00:00 2001 From: Nimish Date: Wed, 14 Feb 2024 16:32:56 +0530 Subject: [PATCH 1/4] chore: wip organized package --- .../cryptoUtils.go => phase/crypto/crypto.go | 78 +++++++++++ pkg/api/client.go => phase/network/network.go | 2 +- phase.go => phase/phase.go | 123 ++++-------------- 3 files changed, 101 insertions(+), 102 deletions(-) rename pkg/crypto/cryptoUtils.go => phase/crypto/crypto.go (75%) rename pkg/api/client.go => phase/network/network.go (99%) rename phase.go => phase/phase.go (80%) diff --git a/pkg/crypto/cryptoUtils.go b/phase/crypto/crypto.go similarity index 75% rename from pkg/crypto/cryptoUtils.go rename to phase/crypto/crypto.go index 7c7b14e..aac0293 100644 --- a/pkg/crypto/cryptoUtils.go +++ b/phase/crypto/crypto.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/jamesruan/sodium" + "github.com/phasehq/golang-sdk/phase/network" ) // Spin up an ephemeral X25519 keypair @@ -194,6 +195,83 @@ func DecryptAsymmetric(ciphertextString, privateKeyHex, publicKeyHex string) (st return plaintext, nil } +// Decrypt decrypts the provided ciphertext using the Phase encryption mechanism. +func DecryptWrappedKeyShare(Keyshare1 string, Keyshare0 string, AppToken string, Keyshare1UnwrapKey string, PssUserPublicKey string, Host string) (string, error) { + // Fetch the wrapped key share using the app token and host + wrappedKeyShare, err := network.FetchAppKey(AppToken, Host) + if err != nil { + log.Fatalf("Failed to fetch wrapped key share: %v", err) + return "", err + } + + // Decode the wrapped key share from hex, not base64 + wrappedKeyShareBytes, err := hex.DecodeString(wrappedKeyShare) + if err != nil { + log.Fatalf("Failed to decode wrapped key share from hex: %v", err) + return "", err + } + + // Decode Keyshare1UnwrapKey from hex, ensuring it's correctly sized + keyshare1UnwrapKeyBytes, err := hex.DecodeString(Keyshare1UnwrapKey) + if err != nil { + log.Fatalf("Failed to decode Keyshare1UnwrapKey from hex: %v", err) + return "", err + } + if len(keyshare1UnwrapKeyBytes) != 32 { // Sodium expects a 32-byte key + log.Fatalf("Incorrect Keyshare1UnwrapKey size: expected 32 bytes, got %d", len(keyshare1UnwrapKeyBytes)) + return "", err + } + + keyshare1, err := DecryptRaw(wrappedKeyShareBytes, sodium.KXSessionKey{Bytes: keyshare1UnwrapKeyBytes}) + if err != nil { + log.Fatalf("Failed to decrypt wrapped key share: %v", err) + return "", err + } + + // Reconstruct the application's private key + appPrivateKey, err := ReconstructSecret(Keyshare0, string(keyshare1)) + if err != nil { + log.Fatalf("Failed to reconstruct application's private key: %v", err) + return "", err + } + + // Decrypt the ciphertext using the application's private key + plaintext, err := DecryptAsymmetric(Keyshare1, appPrivateKey, PssUserPublicKey) + if err != nil { + log.Fatalf("Failed to decrypt ciphertext: %v", err) + return "", err + } + + return plaintext, nil +} + +func GenerateEnvKeyPair(seed string) (publicKeyHex, privateKeyHex string, err error) { + seedBytes, err := hex.DecodeString(seed) + if err != nil { + return "", "", err + } + if len(seedBytes) != 32 { + return "", "", fmt.Errorf("incorrect seed length: expected 32 bytes, got %d", len(seedBytes)) + } + + // Prepare the seed as KXSeed + var seedKX sodium.KXSeed + copy(seedKX.Bytes[:], seedBytes) + + // Allocate slice if KXSeed.Bytes is a slice + seedKX.Bytes = make([]byte, len(seedBytes)) + copy(seedKX.Bytes, seedBytes) + + // Generate key pair from seed + keyPair := sodium.SeedKXKP(seedKX) + + publicKeyHex = hex.EncodeToString(keyPair.PublicKey.Bytes[:]) + privateKeyHex = hex.EncodeToString(keyPair.SecretKey.Bytes[:]) + + return publicKeyHex, privateKeyHex, nil +} + + // 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) as an example diff --git a/pkg/api/client.go b/phase/network/network.go similarity index 99% rename from pkg/api/client.go rename to phase/network/network.go index 53d5878..8ccf054 100644 --- a/pkg/api/client.go +++ b/phase/network/network.go @@ -1,4 +1,4 @@ -package api +package network import ( "bytes" diff --git a/phase.go b/phase/phase.go similarity index 80% rename from phase.go rename to phase/phase.go index 81a90c4..d5e78c5 100644 --- a/phase.go +++ b/phase/phase.go @@ -1,15 +1,13 @@ package phase import ( - "encoding/hex" "encoding/json" "fmt" "log" "strings" - "github.com/jamesruan/sodium" - "github.com/phasehq/golang-sdk/pkg/api" - "github.com/phasehq/golang-sdk/pkg/crypto" + "github.com/phasehq/golang-sdk/phase/crypto" + "github.com/phasehq/golang-sdk/phase/network" ) // Phase struct to hold parsed service token information and host. @@ -84,7 +82,7 @@ type UpdateSecretOptions struct { 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) + resp, err := network.FetchPhaseUser(p.AppToken, p.Host) if err != nil { log.Printf("Failed to fetch user data: %v", err) return nil, err @@ -103,18 +101,18 @@ func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[st return nil, err } - decryptedSeed, err := p.Decrypt(envKey.WrappedSeed) + decryptedSeed, err := crypto.DecryptWrappedKeyShare(envKey.WrappedSeed, p.Keyshare0, p.AppToken, p.Keyshare1UnwrapKey, p.PssUserPublicKey, p.Host) if err != nil { log.Printf("Failed to decrypt wrapped seed: %v", err) return nil, err } - decryptedSalt, err := p.Decrypt(envKey.WrappedSalt) + decryptedSalt, err := crypto.DecryptWrappedKeyShare(envKey.WrappedSalt, p.Keyshare0, p.AppToken, p.Keyshare1UnwrapKey, p.PssUserPublicKey, p.Host) if err != nil { log.Printf("Failed to decrypt wrapped salt: %v", err) return nil, err } - publicKeyHex, privateKeyHex, err := generateEnvKeyPair(decryptedSeed) + publicKeyHex, privateKeyHex, err := crypto.GenerateEnvKeyPair(decryptedSeed) if err != nil { log.Printf("Failed to generate environment key pair: %v", err) return nil, err @@ -127,7 +125,7 @@ func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[st } // Fetch a single secret based on keyDigest and optional path - secret, err := api.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, path) + secret, err := network.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, path) if err != nil { log.Printf("Failed to fetch secret: %v", err) return nil, err @@ -170,7 +168,7 @@ func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[st func (p *Phase) GetAllSecrets(envName, appName, tag, path string) ([]map[string]interface{}, error) { // Fetch user data - resp, err := api.FetchPhaseUser(p.AppToken, p.Host) + resp, err := network.FetchPhaseUser(p.AppToken, p.Host) if err != nil { log.Fatalf("Failed to fetch user data: %v", err) return nil, err @@ -191,21 +189,21 @@ func (p *Phase) GetAllSecrets(envName, appName, tag, path string) ([]map[string] } // Decrypt the wrapped seed - decryptedSeed, err := p.Decrypt(envKey.WrappedSeed) + decryptedSeed, err := crypto.DecryptWrappedKeyShare(envKey.WrappedSeed, p.Keyshare0, p.AppToken, p.Keyshare1UnwrapKey, p.PssUserPublicKey, p.Host) if err != nil { log.Fatalf("Failed to decrypt wrapped seed: %v", err) return nil, err } // Generate environment key pair - publicKeyHex, privateKeyHex, err := generateEnvKeyPair(decryptedSeed) + publicKeyHex, privateKeyHex, err := crypto.GenerateEnvKeyPair(decryptedSeed) if err != nil { log.Fatalf("Failed to generate environment key pair: %v", err) return nil, err } // Fetch secrets with optional path filtering - secrets, err := api.FetchPhaseSecrets(p.AppToken, envKey.Environment.ID, p.Host, path) + secrets, err := network.FetchPhaseSecrets(p.AppToken, envKey.Environment.ID, p.Host, path) if err != nil { log.Fatalf("Failed to fetch secrets: %v", err) return nil, err @@ -256,7 +254,7 @@ func (p *Phase) GetAllSecrets(envName, appName, tag, path string) ([]map[string] // 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, keyPaths map[string]string) error { // Fetch user data - resp, err := api.FetchPhaseUser(p.AppToken, p.Host) + resp, err := network.FetchPhaseUser(p.AppToken, p.Host) if err != nil { log.Fatalf("Failed to fetch user data: %v", err) return err @@ -282,7 +280,7 @@ func (p *Phase) CreateSecrets(keyValuePairs []map[string]string, envName, appNam return err } - decryptedSalt, err := p.Decrypt(envKey.WrappedSalt) + decryptedSalt, err := crypto.DecryptWrappedKeyShare(envKey.WrappedSalt, p.Keyshare0, p.AppToken, p.Keyshare1UnwrapKey, p.PssUserPublicKey, p.Host) if err != nil { log.Fatalf("Failed to decrypt wrapped salt: %v", err) return err @@ -327,12 +325,12 @@ func (p *Phase) CreateSecrets(keyValuePairs []map[string]string, envName, appNam } } - return api.CreatePhaseSecrets(p.AppToken, envID, secrets, p.Host) + return network.CreatePhaseSecrets(p.AppToken, envID, secrets, p.Host) } func (p *Phase) UpdateSecret(opts UpdateSecretOptions) error { // Fetch user data - resp, err := api.FetchPhaseUser(p.AppToken, p.Host) + resp, err := network.FetchPhaseUser(p.AppToken, p.Host) if err != nil { log.Fatalf("Failed to fetch user data: %v", err) return err @@ -351,7 +349,7 @@ func (p *Phase) UpdateSecret(opts UpdateSecretOptions) error { return err } - decryptedSalt, err := p.Decrypt(envKey.WrappedSalt) + decryptedSalt, err := crypto.DecryptWrappedKeyShare(envKey.WrappedSalt, p.Keyshare0, p.AppToken, p.Keyshare1UnwrapKey, p.PssUserPublicKey, p.Host) if err != nil { log.Fatalf("Failed to decrypt wrapped salt: %v", err) return err @@ -365,7 +363,7 @@ func (p *Phase) UpdateSecret(opts UpdateSecretOptions) error { } // Fetch a single secret based on keyDigest - secret, err := api.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, opts.Path) + secret, err := network.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, opts.Path) if err != nil { log.Printf("Failed to fetch secret: %v", err) return err @@ -408,7 +406,7 @@ func (p *Phase) UpdateSecret(opts UpdateSecretOptions) error { } // Perform the update - err = api.UpdatePhaseSecrets(p.AppToken, envKey.Environment.ID, []map[string]interface{}{secretUpdatePayload}, p.Host) + err = network.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 @@ -422,7 +420,7 @@ func (p *Phase) UpdateSecret(opts UpdateSecretOptions) error { // 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) + resp, err := network.FetchPhaseUser(p.AppToken, p.Host) if err != nil { log.Fatalf("Failed to fetch user data: %v", err) return err @@ -441,7 +439,7 @@ func (p *Phase) DeleteSecret(envName, appName, keyToDelete, path string) error { return err } - decryptedSalt, err := p.Decrypt(envKey.WrappedSalt) + decryptedSalt, err := crypto.DecryptWrappedKeyShare(envKey.WrappedSalt, p.Keyshare0, p.AppToken, p.Keyshare1UnwrapKey, p.PssUserPublicKey, p.Host) if err != nil { log.Fatalf("Failed to decrypt wrapped salt: %v", err) return err @@ -455,7 +453,7 @@ func (p *Phase) DeleteSecret(envName, appName, keyToDelete, path string) error { } // Fetch the specific secret by its key digest and path - secret, err := api.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, path) + secret, err := network.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, path) if err != nil { log.Printf("Failed to fetch secret: %v", err) return err @@ -468,7 +466,7 @@ func (p *Phase) DeleteSecret(envName, appName, keyToDelete, path string) error { } // Perform the delete operation for the found secret ID - err = api.DeletePhaseSecrets(p.AppToken, envKey.Environment.ID, []string{secretID}, p.Host) + err = network.DeletePhaseSecrets(p.AppToken, envKey.Environment.ID, []string{secretID}, p.Host) if err != nil { log.Fatalf("Failed to delete secret: %v", err) return err @@ -575,80 +573,3 @@ func tagMatches(secretTags []string, userTag string) bool { } return false } - - -// Decrypt decrypts the provided ciphertext using the Phase encryption mechanism. -func (p *Phase) Decrypt(ciphertext string) (string, error) { - // Fetch the wrapped key share using the app token and host - wrappedKeyShare, err := api.FetchAppKey(p.AppToken, p.Host) - if err != nil { - log.Fatalf("Failed to fetch wrapped key share: %v", err) - return "", err - } - - // Decode the wrapped key share from hex, not base64 - wrappedKeyShareBytes, err := hex.DecodeString(wrappedKeyShare) - if err != nil { - log.Fatalf("Failed to decode wrapped key share from hex: %v", err) - return "", err - } - - // Decode Keyshare1UnwrapKey from hex, ensuring it's correctly sized - keyshare1UnwrapKeyBytes, err := hex.DecodeString(p.Keyshare1UnwrapKey) - if err != nil { - log.Fatalf("Failed to decode Keyshare1UnwrapKey from hex: %v", err) - return "", err - } - if len(keyshare1UnwrapKeyBytes) != 32 { // Sodium expects a 32-byte key - log.Fatalf("Incorrect Keyshare1UnwrapKey size: expected 32 bytes, got %d", len(keyshare1UnwrapKeyBytes)) - return "", err - } - - keyshare1, err := crypto.DecryptRaw(wrappedKeyShareBytes, sodium.KXSessionKey{Bytes: keyshare1UnwrapKeyBytes}) - if err != nil { - log.Fatalf("Failed to decrypt wrapped key share: %v", err) - return "", err - } - - // Reconstruct the application's private key - appPrivateKey, err := crypto.ReconstructSecret(p.Keyshare0, string(keyshare1)) - if err != nil { - log.Fatalf("Failed to reconstruct application's private key: %v", err) - return "", err - } - - // Decrypt the ciphertext using the application's private key - plaintext, err := crypto.DecryptAsymmetric(ciphertext, appPrivateKey, p.PssUserPublicKey) - if err != nil { - log.Fatalf("Failed to decrypt ciphertext: %v", err) - return "", err - } - - return plaintext, nil -} - -func generateEnvKeyPair(seed string) (publicKeyHex, privateKeyHex string, err error) { - seedBytes, err := hex.DecodeString(seed) - if err != nil { - return "", "", err - } - if len(seedBytes) != 32 { - return "", "", fmt.Errorf("incorrect seed length: expected 32 bytes, got %d", len(seedBytes)) - } - - // Prepare the seed as KXSeed - var seedKX sodium.KXSeed - copy(seedKX.Bytes[:], seedBytes) - - // Allocate slice if KXSeed.Bytes is a slice - seedKX.Bytes = make([]byte, len(seedBytes)) - copy(seedKX.Bytes, seedBytes) - - // Generate key pair from seed - keyPair := sodium.SeedKXKP(seedKX) - - publicKeyHex = hex.EncodeToString(keyPair.PublicKey.Bytes[:]) - privateKeyHex = hex.EncodeToString(keyPair.SecretKey.Bytes[:]) - - return publicKeyHex, privateKeyHex, nil -} From 87fd13224feafd3b9a14d70442723ff5310d904b Mon Sep 17 00:00:00 2001 From: Nimish Date: Wed, 14 Feb 2024 17:23:20 +0530 Subject: [PATCH 2/4] chore: organized code --- phase/crypto/crypto.go | 39 ++++++++ phase/misc/const.go | 56 +++++++++++ phase/misc/misc.go | 50 ++++++++++ phase/network/network.go | 16 +-- phase/phase.go | 210 +++++++++------------------------------ 5 files changed, 195 insertions(+), 176 deletions(-) create mode 100644 phase/misc/const.go create mode 100644 phase/misc/misc.go diff --git a/phase/crypto/crypto.go b/phase/crypto/crypto.go index aac0293..097cab1 100644 --- a/phase/crypto/crypto.go +++ b/phase/crypto/crypto.go @@ -195,6 +195,45 @@ func DecryptAsymmetric(ciphertextString, privateKeyHex, publicKeyHex string) (st return plaintext, nil } +// decryptSecret decrypts a secret's key, value, and optional comment using asymmetric decryption. +func DecryptSecret(secret map[string]interface{}, privateKeyHex, publicKeyHex string) (decryptedKey string, decryptedValue string, decryptedComment string, err error) { + // Decrypt the key + key, ok := secret["key"].(string) + if !ok { + err = fmt.Errorf("key is not a string") + return + } + decryptedKey, err = DecryptAsymmetric(key, privateKeyHex, publicKeyHex) + if err != nil { + log.Printf("Failed to decrypt key: %v\n", err) + return + } + + // Decrypt the value + value, ok := secret["value"].(string) + if !ok { + err = fmt.Errorf("value is not a string") + return + } + decryptedValue, err = DecryptAsymmetric(value, privateKeyHex, publicKeyHex) + if err != nil { + log.Printf("Failed to decrypt value: %v\n", err) + return + } + + // Decrypt the comment if it exists + comment, ok := secret["comment"].(string) + if ok && comment != "" { + decryptedComment, err = DecryptAsymmetric(comment, privateKeyHex, publicKeyHex) + if err != nil { + log.Printf("Failed to decrypt comment: %v\n", err) + err = nil + } + } + + return decryptedKey, decryptedValue, decryptedComment, nil +} + // Decrypt decrypts the provided ciphertext using the Phase encryption mechanism. func DecryptWrappedKeyShare(Keyshare1 string, Keyshare0 string, AppToken string, Keyshare1UnwrapKey string, PssUserPublicKey string, Host string) (string, error) { // Fetch the wrapped key share using the app token and host diff --git a/phase/misc/const.go b/phase/misc/const.go new file mode 100644 index 0000000..9650eed --- /dev/null +++ b/phase/misc/const.go @@ -0,0 +1,56 @@ +package misc + +import ( + "regexp" +) + +const ( + Version = "1.0" + PhVersion = "v1" + PhaseCloudAPIHost = "https://console.phase.dev" +) + +var ( + VerifySSL = false + PhaseDebug = false +) + +var ( + + // Compiled regex patterns + PssUserPattern = regexp.MustCompile(`^pss_user:v(\d+):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64})$`) + PssServicePattern = regexp.MustCompile(`^pss_service:v(\d+):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64})$`) + CrossEnvPattern = regexp.MustCompile(`\$\{(.+?)\.(.+?)\}`) + LocalRefPattern = regexp.MustCompile(`\$\{([^.]+?)\}`) +) + + +type Environment struct { + ID string `json:"id"` + Name string `json:"name"` + EnvType string `json:"env_type"` +} + +type EnvironmentKey struct { + ID string `json:"id"` + Environment Environment `json:"environment"` + IdentityKey string `json:"identity_key"` + WrappedSeed string `json:"wrapped_seed"` + WrappedSalt string `json:"wrapped_salt"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + DeletedAt *string `json:"deleted_at"` + User *string `json:"user"` +} + +type App struct { + ID string `json:"id"` + Name string `json:"name"` + Encryption string `json:"encryption"` + EnvironmentKeys []EnvironmentKey `json:"environment_keys"` +} + +type AppKeyResponse struct { + WrappedKeyShare string `json:"wrapped_key_share"` + Apps []App `json:"apps"` +} diff --git a/phase/misc/misc.go b/phase/misc/misc.go new file mode 100644 index 0000000..bdef2c4 --- /dev/null +++ b/phase/misc/misc.go @@ -0,0 +1,50 @@ +package misc + +import ( + "fmt" + "strings" +) + +// phaseGetContext finds the matching application and environment, returning their IDs and the public key. +func PhaseGetContext(userData AppKeyResponse, appName, envName string) (string, string, string, error) { + for _, app := range userData.Apps { + if app.Name == appName { + for _, envKey := range app.EnvironmentKeys { + if envKey.Environment.Name == envName { + return app.ID, envKey.Environment.ID, envKey.IdentityKey, nil + } + } + } + } + return "", "", "", fmt.Errorf("matching context not found") +} + +func FindEnvironmentKey(userData AppKeyResponse, envName, appName string) (*EnvironmentKey, error) { + for _, app := range userData.Apps { + if appName == "" || app.Name == appName { + for _, envKey := range app.EnvironmentKeys { + if envKey.Environment.Name == envName { + return &envKey, nil // Note the address-of operator (&) before envKey + } + } + } + } + return nil, fmt.Errorf("environment key not found") +} + +// normalizeTag replaces underscores with spaces and converts the string to lower case. +func normalizeTag(tag string) string { + return strings.ToLower(strings.Replace(tag, "_", " ", -1)) +} + +// tagMatches checks if the user-provided tag partially matches any of the secret tags. +func TagMatches(secretTags []string, userTag string) bool { + normalizedUserTag := normalizeTag(userTag) + for _, tag := range secretTags { + normalizedSecretTag := normalizeTag(tag) + if strings.Contains(normalizedSecretTag, normalizedUserTag) { + return true + } + } + return false +} diff --git a/phase/network/network.go b/phase/network/network.go index 8ccf054..2c4543e 100644 --- a/phase/network/network.go +++ b/phase/network/network.go @@ -13,11 +13,8 @@ import ( "os/user" "runtime" "strings" -) -var ( - verifySSL = os.Getenv("PHASE_VERIFY_SSL") != "false" - phaseDebug = os.Getenv("PHASE_DEBUG") == "true" + "github.com/phasehq/golang-sdk/phase/misc" ) func ConstructHTTPHeaders(appToken string) http.Header { @@ -28,11 +25,10 @@ func ConstructHTTPHeaders(appToken string) http.Header { return headers } - func GetUserAgent() string { details := []string{} - cliVersion := "phase-golang-sdk/v1.0.0" + cliVersion := "phase-golang-sdk/" + misc.Version details = append(details, cliVersion) osType := runtime.GOOS @@ -52,10 +48,9 @@ func GetUserAgent() string { return strings.Join(details, " ") } - func createHTTPClient() *http.Client { client := &http.Client{} - if !verifySSL { + if !misc.VerifySSL { client.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } @@ -81,7 +76,6 @@ func handleHTTPResponse(resp *http.Response) error { return nil } - func FetchPhaseUser(appToken, host string) (*http.Response, error) { client := createHTTPClient() url := fmt.Sprintf("%s/service/secrets/tokens/", host) @@ -128,7 +122,6 @@ type AppKeyResponse struct { } `json:"apps"` } - func FetchAppKey(appToken, host string) (string, error) { client := createHTTPClient() url := fmt.Sprintf("%s/service/secrets/tokens/", host) @@ -161,8 +154,7 @@ func FetchWrappedKeyShare(appToken, host string) (string, error) { client := &http.Client{} // Check if SSL verification should be skipped - verifySSL := os.Getenv("PHASE_VERIFY_SSL") != "false" - if !verifySSL { + if !misc.VerifySSL { client.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } diff --git a/phase/phase.go b/phase/phase.go index d5e78c5..534f723 100644 --- a/phase/phase.go +++ b/phase/phase.go @@ -4,9 +4,9 @@ import ( "encoding/json" "fmt" "log" - "strings" "github.com/phasehq/golang-sdk/phase/crypto" + "github.com/phasehq/golang-sdk/phase/misc" "github.com/phasehq/golang-sdk/phase/network" ) @@ -21,56 +21,6 @@ type Phase struct { Host string } -// Init initializes a new instance of Phase with the provided service token and host. -func Init(serviceToken, host string) *Phase { - // Split the service token by ':' to extract its components. - parts := strings.Split(serviceToken, ":") - if len(parts) != 6 { - log.Fatalf("Service token format is invalid: expected 6 parts, got %d", len(parts)) - } - - // Create a new Phase instance with parsed service token components. - return &Phase{ - Prefix: parts[0], - PesVersion: parts[1], - AppToken: parts[2], - PssUserPublicKey: parts[3], - Keyshare0: parts[4], - Keyshare1UnwrapKey: parts[5], - Host: host, - } -} - -type Environment struct { - ID string `json:"id"` - Name string `json:"name"` - EnvType string `json:"env_type"` -} - -type EnvironmentKey struct { - ID string `json:"id"` - Environment Environment `json:"environment"` - IdentityKey string `json:"identity_key"` - WrappedSeed string `json:"wrapped_seed"` - WrappedSalt string `json:"wrapped_salt"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - DeletedAt *string `json:"deleted_at"` - User *string `json:"user"` -} - -type App struct { - ID string `json:"id"` - Name string `json:"name"` - Encryption string `json:"encryption"` - EnvironmentKeys []EnvironmentKey `json:"environment_keys"` -} - -type AppKeyResponse struct { - WrappedKeyShare string `json:"wrapped_key_share"` - Apps []App `json:"apps"` -} - // UpdateSecretOptions holds all the options for updating a secret. type UpdateSecretOptions struct { EnvName string @@ -80,6 +30,36 @@ type UpdateSecretOptions struct { Path string } +// Init initializes a new instance of Phase with the provided service token and host. +func Init(serviceToken, host string, debug bool) *Phase { + // Validate the service token against the pattern. + matches := misc.PssServicePattern.FindStringSubmatch(serviceToken) + if matches == nil || len(matches) != 6 { + log.Fatalf("Error: Invalid Phase Service Token.") + } + + // Use default host if none is specified. + if host == "" { + host = misc.PhaseCloudAPIHost + } + + // Use default host if none is specified. + if host == "" { + host = misc.PhaseCloudAPIHost + } + + // Create a new Phase instance with parsed service token components. + return &Phase{ + Prefix: "pss_service", + PesVersion: matches[1], + AppToken: matches[2], + PssUserPublicKey: matches[3], + Keyshare0: matches[4], + Keyshare1UnwrapKey: matches[5], + Host: host, + } +} + func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[string]interface{}, error) { // Fetch user data resp, err := network.FetchPhaseUser(p.AppToken, p.Host) @@ -89,13 +69,13 @@ func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[st } defer resp.Body.Close() - var userData AppKeyResponse + var userData misc.AppKeyResponse if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { log.Printf("Failed to decode user data: %v", err) return nil, err } - envKey, err := findEnvironmentKey(&userData, envName, appName) + envKey, err := misc.FindEnvironmentKey(userData, envName, appName) if err != nil { log.Printf("Failed to find environment key: %v", err) return nil, err @@ -131,7 +111,7 @@ func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[st return nil, err } - decryptedKey, decryptedValue, decryptedComment, err := decryptSecret(secret, privateKeyHex, publicKeyHex) + decryptedKey, decryptedValue, decryptedComment, err := crypto.DecryptSecret(secret, privateKeyHex, publicKeyHex) if err != nil { log.Printf("Failed to decrypt secret: %v", err) return nil, err @@ -146,7 +126,7 @@ func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[st stringTags = append(stringTags, tagStr) } } - if !tagMatches(stringTags, tag) { + if !misc.TagMatches(stringTags, tag) { return nil, fmt.Errorf("secret with key '%s' found, but doesn't match the provided tag '%s'", keyToFind, tag) } } @@ -175,14 +155,14 @@ func (p *Phase) GetAllSecrets(envName, appName, tag, path string) ([]map[string] } defer resp.Body.Close() - var userData AppKeyResponse + var userData misc.AppKeyResponse if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { log.Fatalf("Failed to decode user data: %v", err) return nil, err } // Identify the correct environment and application - envKey, err := findEnvironmentKey(&userData, envName, appName) + envKey, err := misc.FindEnvironmentKey(userData, envName, appName) if err != nil { log.Fatalf("Failed to find environment key: %v", err) return nil, err @@ -212,7 +192,7 @@ func (p *Phase) GetAllSecrets(envName, appName, tag, path string) ([]map[string] decryptedSecrets := make([]map[string]interface{}, 0) for _, secret := range secrets { // Decrypt key, value, and optional comment - decryptedKey, decryptedValue, decryptedComment, err := decryptSecret(secret, privateKeyHex, publicKeyHex) + decryptedKey, decryptedValue, decryptedComment, err := crypto.DecryptSecret(secret, privateKeyHex, publicKeyHex) if err != nil { log.Printf("Failed to decrypt secret: %v\n", err) continue @@ -229,7 +209,7 @@ func (p *Phase) GetAllSecrets(envName, appName, tag, path string) ([]map[string] } // Check for tag match if a tag is provided - if tag != "" && !tagMatches(stringTags, tag) { + if tag != "" && !misc.TagMatches(stringTags, tag) { continue } @@ -261,20 +241,20 @@ func (p *Phase) CreateSecrets(keyValuePairs []map[string]string, envName, appNam } defer resp.Body.Close() - var userData AppKeyResponse + var userData misc.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) + _, envID, publicKey, err := misc.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) + envKey, err := misc.FindEnvironmentKey(userData, envName, appName) if err != nil { log.Printf("Failed to find environment key: %v", err) return err @@ -337,13 +317,13 @@ func (p *Phase) UpdateSecret(opts UpdateSecretOptions) error { } defer resp.Body.Close() - var userData AppKeyResponse + var userData misc.AppKeyResponse if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { log.Fatalf("Failed to decode user data: %v", err) return err } - envKey, err := findEnvironmentKey(&userData, opts.EnvName, opts.AppName) + envKey, err := misc.FindEnvironmentKey(userData, opts.EnvName, opts.AppName) if err != nil { log.Fatalf("Failed to find environment key: %v", err) return err @@ -427,13 +407,13 @@ func (p *Phase) DeleteSecret(envName, appName, keyToDelete, path string) error { } defer resp.Body.Close() - var userData AppKeyResponse + var userData misc.AppKeyResponse if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { log.Fatalf("Failed to decode user data: %v", err) return err } - envKey, err := findEnvironmentKey(&userData, envName, appName) + envKey, err := misc.FindEnvironmentKey(userData, envName, appName) if err != nil { log.Printf("Failed to find environment key: %v", err) return err @@ -475,101 +455,3 @@ func (p *Phase) DeleteSecret(envName, appName, keyToDelete, path string) error { log.Println("Secret deleted successfully") return nil } - -// decryptSecret decrypts a secret's key, value, and optional comment using asymmetric decryption. -func decryptSecret(secret map[string]interface{}, privateKeyHex, publicKeyHex string) (decryptedKey string, decryptedValue string, decryptedComment string, err error) { - // Decrypt the key - key, ok := secret["key"].(string) - if !ok { - err = fmt.Errorf("key is not a string") - return - } - decryptedKey, err = crypto.DecryptAsymmetric(key, privateKeyHex, publicKeyHex) - if err != nil { - log.Printf("Failed to decrypt key: %v\n", err) - return - } - - // Decrypt the value - value, ok := secret["value"].(string) - if !ok { - err = fmt.Errorf("value is not a string") - return - } - decryptedValue, err = crypto.DecryptAsymmetric(value, privateKeyHex, publicKeyHex) - if err != nil { - log.Printf("Failed to decrypt value: %v\n", err) - return - } - - // Decrypt the comment if it exists - comment, ok := secret["comment"].(string) - if ok && comment != "" { - decryptedComment, err = crypto.DecryptAsymmetric(comment, privateKeyHex, publicKeyHex) - if err != nil { - log.Printf("Failed to decrypt comment: %v\n", err) - // We decide not to return an error here because comments are optional and failure to decrypt them - // should not prevent the rest of the secret data from being used. - err = nil - } - } - - return decryptedKey, decryptedValue, decryptedComment, nil -} - - -// Helper function to check if a slice contains a string -func contains(slice []string, str string) bool { - for _, v := range slice { - if v == str { - return true - } - } - return false -} - -// phaseGetContext finds the matching application and environment, returning their IDs and the public key. -func phaseGetContext(userData *AppKeyResponse, appName, envName string) (string, string, string, error) { - for _, app := range userData.Apps { - if app.Name == appName { - for _, envKey := range app.EnvironmentKeys { - if envKey.Environment.Name == envName { - return app.ID, envKey.Environment.ID, envKey.IdentityKey, nil - } - } - } - } - return "", "", "", fmt.Errorf("matching context not found") -} - - -func findEnvironmentKey(userData *AppKeyResponse, envName, appName string) (*EnvironmentKey, error) { - for _, app := range userData.Apps { - if appName == "" || app.Name == appName { - for _, envKey := range app.EnvironmentKeys { - if envKey.Environment.Name == envName { - return &envKey, nil // This should now match the expected return type - } - } - } - } - return nil, fmt.Errorf("environment key not found") -} - - -// normalizeTag replaces underscores with spaces and converts the string to lower case. -func normalizeTag(tag string) string { - return strings.ToLower(strings.Replace(tag, "_", " ", -1)) -} - -// tagMatches checks if the user-provided tag partially matches any of the secret tags. -func tagMatches(secretTags []string, userTag string) bool { - normalizedUserTag := normalizeTag(userTag) - for _, tag := range secretTags { - normalizedSecretTag := normalizeTag(tag) - if strings.Contains(normalizedSecretTag, normalizedUserTag) { - return true - } - } - return false -} From 3c44849a4c0fcb9fc17753e76942397a38bd15a7 Mon Sep 17 00:00:00 2001 From: Nimish Date: Wed, 14 Feb 2024 18:10:01 +0530 Subject: [PATCH 3/4] feat: added option structs --- phase/phase.go | 93 +++++++++++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 35 deletions(-) diff --git a/phase/phase.go b/phase/phase.go index 534f723..15edef4 100644 --- a/phase/phase.go +++ b/phase/phase.go @@ -21,13 +21,41 @@ type Phase struct { Host string } -// UpdateSecretOptions holds all the options for updating a secret. -type UpdateSecretOptions struct { +type GetSecretOptions struct { + EnvName string + AppName string + KeyToFind string + Tag string + SecretPath string +} + +type GetAllSecretsOptions struct { + EnvName string + AppName string + Tag string + SecretPath string +} + +type CreateSecretsOptions struct { + KeyValuePairs []map[string]string + EnvName string + AppName string + SecretPath map[string]string +} + +type SecretUpdateOptions struct { EnvName string AppName string Key string Value string - Path string + SecretPath string +} + +type DeleteSecretOptions struct { + EnvName string + AppName string + KeyToDelete string + SecretPath string } // Init initializes a new instance of Phase with the provided service token and host. @@ -43,11 +71,6 @@ func Init(serviceToken, host string, debug bool) *Phase { host = misc.PhaseCloudAPIHost } - // Use default host if none is specified. - if host == "" { - host = misc.PhaseCloudAPIHost - } - // Create a new Phase instance with parsed service token components. return &Phase{ Prefix: "pss_service", @@ -60,7 +83,7 @@ func Init(serviceToken, host string, debug bool) *Phase { } } -func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[string]interface{}, error) { +func (p *Phase) Get(opts GetSecretOptions) (*map[string]interface{}, error) { // Fetch user data resp, err := network.FetchPhaseUser(p.AppToken, p.Host) if err != nil { @@ -75,7 +98,7 @@ func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[st return nil, err } - envKey, err := misc.FindEnvironmentKey(userData, envName, appName) + envKey, err := misc.FindEnvironmentKey(userData, opts.EnvName, opts.AppName) if err != nil { log.Printf("Failed to find environment key: %v", err) return nil, err @@ -98,14 +121,14 @@ func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[st return nil, err } - keyDigest, err := crypto.Blake2bDigest(keyToFind, decryptedSalt) + keyDigest, err := crypto.Blake2bDigest(opts.KeyToFind, decryptedSalt) if err != nil { log.Printf("Failed to generate key digest: %v", err) return nil, err } // Fetch a single secret based on keyDigest and optional path - secret, err := network.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, path) + secret, err := network.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, opts.SecretPath) if err != nil { log.Printf("Failed to fetch secret: %v", err) return nil, err @@ -119,15 +142,15 @@ func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[st // Verify tag match if a tag is provided var stringTags []string - if tag != "" { + if opts.Tag != "" { if secretTags, ok := secret["tags"].([]interface{}); ok { for _, tagInterface := range secretTags { if tagStr, ok := tagInterface.(string); ok { stringTags = append(stringTags, tagStr) } } - if !misc.TagMatches(stringTags, tag) { - return nil, fmt.Errorf("secret with key '%s' found, but doesn't match the provided tag '%s'", keyToFind, tag) + if !misc.TagMatches(stringTags, opts.Tag) { + return nil, fmt.Errorf("secret with key '%s' found, but doesn't match the provided tag '%s'", opts.KeyToFind, opts.Tag) } } } @@ -146,7 +169,7 @@ func (p *Phase) PhaseGet(envName, appName, keyToFind, tag, path string) (*map[st return result, nil } -func (p *Phase) GetAllSecrets(envName, appName, tag, path string) ([]map[string]interface{}, error) { +func (p *Phase) GetAll(opts GetAllSecretsOptions) ([]map[string]interface{}, error) { // Fetch user data resp, err := network.FetchPhaseUser(p.AppToken, p.Host) if err != nil { @@ -162,7 +185,7 @@ func (p *Phase) GetAllSecrets(envName, appName, tag, path string) ([]map[string] } // Identify the correct environment and application - envKey, err := misc.FindEnvironmentKey(userData, envName, appName) + envKey, err := misc.FindEnvironmentKey(userData, opts.EnvName, opts.AppName) if err != nil { log.Fatalf("Failed to find environment key: %v", err) return nil, err @@ -183,7 +206,7 @@ func (p *Phase) GetAllSecrets(envName, appName, tag, path string) ([]map[string] } // Fetch secrets with optional path filtering - secrets, err := network.FetchPhaseSecrets(p.AppToken, envKey.Environment.ID, p.Host, path) + secrets, err := network.FetchPhaseSecrets(p.AppToken, envKey.Environment.ID, p.Host, opts.SecretPath) if err != nil { log.Fatalf("Failed to fetch secrets: %v", err) return nil, err @@ -209,7 +232,7 @@ func (p *Phase) GetAllSecrets(envName, appName, tag, path string) ([]map[string] } // Check for tag match if a tag is provided - if tag != "" && !misc.TagMatches(stringTags, tag) { + if opts.Tag != "" && !misc.TagMatches(stringTags, opts.Tag) { continue } @@ -232,7 +255,7 @@ func (p *Phase) GetAllSecrets(envName, appName, tag, path string) ([]map[string] } // 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, keyPaths map[string]string) error { +func (p *Phase) Create(opts CreateSecretsOptions) error { // Fetch user data resp, err := network.FetchPhaseUser(p.AppToken, p.Host) if err != nil { @@ -247,14 +270,14 @@ func (p *Phase) CreateSecrets(keyValuePairs []map[string]string, envName, appNam return err } - _, envID, publicKey, err := misc.PhaseGetContext(userData, appName, envName) + _, envID, publicKey, err := misc.PhaseGetContext(userData, opts.AppName, opts.EnvName) if err != nil { log.Fatalf("Failed to get context: %v", err) return err } // Identify the correct environment and application - envKey, err := misc.FindEnvironmentKey(userData, envName, appName) + envKey, err := misc.FindEnvironmentKey(userData, opts.EnvName, opts.AppName) if err != nil { log.Printf("Failed to find environment key: %v", err) return err @@ -267,7 +290,7 @@ func (p *Phase) CreateSecrets(keyValuePairs []map[string]string, envName, appNam } secrets := make([]map[string]interface{}, 0) - for _, pair := range keyValuePairs { + for _, pair := range opts.KeyValuePairs { for key, value := range pair { encryptedKey, err := crypto.EncryptAsymmetric(key, publicKey) if err != nil { @@ -288,7 +311,7 @@ func (p *Phase) CreateSecrets(keyValuePairs []map[string]string, envName, appNam } // Determine the path for the secret, default to "/" if not specified - path, ok := keyPaths[key] + path, ok := opts.SecretPath[key] if !ok { path = "/" // Default path if not provided } @@ -308,7 +331,7 @@ func (p *Phase) CreateSecrets(keyValuePairs []map[string]string, envName, appNam return network.CreatePhaseSecrets(p.AppToken, envID, secrets, p.Host) } -func (p *Phase) UpdateSecret(opts UpdateSecretOptions) error { +func (p *Phase) Update(opts SecretUpdateOptions) error { // Fetch user data resp, err := network.FetchPhaseUser(p.AppToken, p.Host) if err != nil { @@ -343,7 +366,7 @@ func (p *Phase) UpdateSecret(opts UpdateSecretOptions) error { } // Fetch a single secret based on keyDigest - secret, err := network.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, opts.Path) + secret, err := network.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, opts.SecretPath) if err != nil { log.Printf("Failed to fetch secret: %v", err) return err @@ -371,8 +394,8 @@ func (p *Phase) UpdateSecret(opts UpdateSecretOptions) error { } // Default path to "/" if not provided - if opts.Path == "" { - opts.Path = "/" + if opts.SecretPath == "" { + opts.SecretPath = "/" } secretUpdatePayload := map[string]interface{}{ @@ -380,7 +403,7 @@ func (p *Phase) UpdateSecret(opts UpdateSecretOptions) error { "key": encryptedKey, "keyDigest": keyDigest, "value": encryptedValue, - "path": opts.Path, + "path": opts.SecretPath, "tags": []string{}, "comment": "", } @@ -398,7 +421,7 @@ func (p *Phase) UpdateSecret(opts UpdateSecretOptions) error { // DeleteSecret deletes a secret in Phase KMS based on a key and environment. -func (p *Phase) DeleteSecret(envName, appName, keyToDelete, path string) error { +func (p *Phase) Delete(opts DeleteSecretOptions) error { // Fetch user data resp, err := network.FetchPhaseUser(p.AppToken, p.Host) if err != nil { @@ -413,7 +436,7 @@ func (p *Phase) DeleteSecret(envName, appName, keyToDelete, path string) error { return err } - envKey, err := misc.FindEnvironmentKey(userData, envName, appName) + envKey, err := misc.FindEnvironmentKey(userData, opts.EnvName, opts.AppName) if err != nil { log.Printf("Failed to find environment key: %v", err) return err @@ -426,14 +449,14 @@ func (p *Phase) DeleteSecret(envName, appName, keyToDelete, path string) error { } // Generate key digest - keyDigest, err := crypto.Blake2bDigest(keyToDelete, decryptedSalt) + keyDigest, err := crypto.Blake2bDigest(opts.KeyToDelete, decryptedSalt) if err != nil { log.Fatalf("Failed to generate key digest: %v", err) return err } // Fetch the specific secret by its key digest and path - secret, err := network.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, path) + secret, err := network.FetchPhaseSecret(p.AppToken, envKey.Environment.ID, p.Host, keyDigest, opts.SecretPath) if err != nil { log.Printf("Failed to fetch secret: %v", err) return err @@ -441,8 +464,8 @@ func (p *Phase) DeleteSecret(envName, appName, keyToDelete, path string) error { 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) + log.Printf("Secret ID is not a string for key: %v", opts.KeyToDelete) + return fmt.Errorf("secret ID is not a string for key: %v", opts.KeyToDelete) } // Perform the delete operation for the found secret ID From 2a44bda069220ccc8ae9341ce892a46b1b9efa54 Mon Sep 17 00:00:00 2001 From: Nimish Date: Wed, 14 Feb 2024 18:16:55 +0530 Subject: [PATCH 4/4] feat: added a README --- README.md | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 061d3d2..f4c221a 100644 --- a/README.md +++ b/README.md @@ -1 +1,121 @@ -# golang-sdk \ No newline at end of file +# Phase Secrets Management SDK + +The Phase Secrets SDK provides a Go package for managing secrets in your application environments using the Phase service. This SDK let's you create, retrieve, update, and delete secrets, with end-to-end encryption with just a few lines of code. + +## Installation + +To start using the Phase SDK in your Go project, install it using `go get`: + +```bash +go get github.com/phasehq/golang-sdk/phase +``` + +Make sure to import the SDK in your Go files: + +```go +import "github.com/phasehq/golang-sdk/phase" +``` + +## Configuration + +Before you can interact with the Phase service, you need to initialize the SDK with your service token and the host information. + +```go +package main + +import ( + "log" + "github.com/phasehq/golang-sdk/phase" +) + +func main() { + serviceToken := "pss_service:v1:....." + host := "https://console.phase.dev" // Change this for a self hosted instance of Phase + debug := false + + phaseClient := phase.Init(serviceToken, host, debug) + if phaseClient == nil { + log.Fatal("Failed to initialize Phase client") + } +} +``` + +## Creating a Secret + +To create new secrets, define key-value pairs, specify the environment and application name, and optionally set paths for each key. + +```go +opts := phase.CreateSecretsOptions{ + KeyValuePairs: []map[string]string{ + {"API_KEY": "api_secret"}, + }, + EnvName: "Production", + AppName: "MyApp", + SecretPath: map[string]string{"API_KEY": "/api/keys"}, // Optional, default path: / +} + +err := phaseClient.Create(opts) +if err != nil { + log.Fatalf("Failed to create secret: %v", err) +} +``` + +## Retrieving a Secret + +To retrieve a secret, provide the environment name, application name, key to find, and optionally a tag and path. + +```go +getOpts := phase.GetSecretOptions{ + EnvName: "Production", + AppName: "MyApp", + KeyToFind: "API_KEY", +} + +secret, err := phaseClient.Get(getOpts) +if err != nil { + log.Fatalf("Failed to get secret: %v", err) +} else { + log.Printf("Secret: %+v", secret) +} +``` + +## Updating a Secret + +To update an existing secret, provide the new value along with the environment name, application name, key, and optionally the path. + +```go +updateOpts := phase.SecretUpdateOptions{ + EnvName: "Production", + AppName: "MyApp", + Key: "API_KEY", + Value: "my_updated_api_secret", + SecretPath: "/api/keys", // Optional, default path: / +} + +err := phaseClient.Update(updateOpts) +if err != nil { + log.Fatalf("Failed to update secret: %v", err) +} +``` + +## Deleting a Secret + +To delete a secret, specify the environment name, application name, key to delete, and optionally the path. + +```go +deleteOpts := phase.DeleteSecretOptions{ + EnvName: "Production", + AppName: "MyApp", + KeyToDelete: "API_KEY", + SecretPath: "/api/keys", // Optional, default path: / +} + +err := phaseClient.Delete(deleteOpts) +if err != nil { + log.Fatalf("Failed to delete secret: %v", err) +} +``` + +For more information and advanced usage, refer to the official Phase documentation. + +---