forked from Keeper-Security/secrets-manager-go
-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Keep-149-GCP-GO-SDK Develop #4
Open
ayusha-metron
wants to merge
8
commits into
KEEP-149-gcp-go-review
Choose a base branch
from
KEEP-149-gcp-develop
base: KEEP-149-gcp-go-review
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+890
−0
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
f845810
Keep-149-go-gcp
ayusha-metron f4c54a7
Folder name change
ayusha-metron 76504d5
Update code for different key
ayusha-metron 07ffbc1
Added ReadMe.md file
ayusha-metron 3a16393
Modify asymmetric encryption and decryption
ayusha-metron 6be467e
Added comment
ayusha-metron 198dfa4
Add logo
ayusha-metron c493810
Update logo
ayusha-metron File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
**GCP Cloud Key Management** | ||
|
||
Protect Secrets Manager connection details with GCP Cloud Key Management | ||
|
||
Keeper Secrets Manager integrates with GCP Cloud Key Management in order to provide protection for Keeper Secrets Manager configuration files. With this integration, you can protect connection details on your machine while taking advantage of Keeper's zero-knowledge encryption of all your secret credentials. | ||
Features | ||
|
||
* Encrypt and Decrypt your Keeper Secrets Manager configuration files with GCP Cloud Key Management | ||
* Protect against unauthorized access to your Secrets Manager connections | ||
* Requires only minor changes to code for immediate protection. Works with all Keeper Secrets Manager Go-Lang SDK functionality | ||
|
||
Prerequisites | ||
|
||
* Supports the Go-Lang Secrets Manager SDK. | ||
* Requires GCP Cloud packages: kms/apiv1, kmspb, core, kms | ||
* Works with just AES/RSA key types with `Encrypt` and `Decrypt` permissions. | ||
|
||
Setup | ||
1. Install Secret-Manager-Go Package | ||
|
||
The Secrets Manager GCP package are located in the Keeper Secrets Manager storage package which can be installed using | ||
|
||
> `go get github.com/keeper-security/secrets-manager-go/core` | ||
Configure GCP Connection | ||
|
||
configuration variables can be provided as | ||
|
||
``` | ||
import gcpkv "github.com/keeper-security/secrets-manager-go/gcpkv" | ||
|
||
clientOptions := &ksm.ClientOptions{ | ||
Token: "[One Time Access Token]", | ||
Config: gcpkv.NewGCPKeyVaultStorage(<config-file-path-with-its-name>, <key-arn>, &gcpkv.GCPConfig{ | ||
CredentialsFileLocation: "<Location of credential file ending with .json>", | ||
KeyResourceName: "<Key Resource Name>", | ||
}), | ||
} | ||
``` | ||
The storage will require an GCP credential file ended with .json, as well as Secrets Manager configuration which will be encrypted by GCP Cloud Key Management. | ||
|
||
Provide `CredentialsFileLocation` and `KeyResourceName` variables. | ||
|
||
KeyURL must be like this `projects/PROJECT_ID/locations/LOCATION/keyRings/KEY_RING/cryptoKeys/KEY_NAME/cryptoKeyVersions/KEY_VERSION` | ||
|
||
For more information about URL see the GCP Cloud Key Management Documentation | ||
https://cloud.google.com/kms/docs/getting-resource-ids | ||
|
||
You're ready to use the KSM integration 👍 | ||
|
||
Using the GCP Cloud Key Management Integration | ||
|
||
Review the SDK usage. Refer to the SDK (documentation) [https://docs.keeper.io/en/privileged-access-manager/secrets-manager/developer-sdk-library/golang-sdk#retrieve-secrets]. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,298 @@ | ||
// -*- coding: utf-8 -*- | ||
// _ __ | ||
// | |/ /___ ___ _ __ ___ _ _ (R) | ||
// | ' </ -_) -_) '_ \/ -_) '_| | ||
// |_|\_\___\___| .__/\___|_| | ||
// |_| | ||
// Keeper Secrets Manager | ||
// Copyright 2025 Keeper Security Inc. | ||
// Contact: [email protected] | ||
|
||
package gcpkv | ||
|
||
import ( | ||
"context" | ||
"crypto/md5" | ||
"crypto/sha1" | ||
"crypto/sha256" | ||
"crypto/sha512" | ||
"encoding/hex" | ||
"encoding/json" | ||
"fmt" | ||
"gcpkv/gcp/logger" | ||
"hash" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
kms "cloud.google.com/go/kms/apiv1" | ||
"cloud.google.com/go/kms/apiv1/kmspb" | ||
"github.com/keeper-security/secrets-manager-go/core" | ||
"google.golang.org/api/option" | ||
) | ||
|
||
type GoogleCloudKeyVaultStorage struct { | ||
configFileLocation string | ||
config map[core.ConfigKey]interface{} | ||
lastSavedConfigHash string | ||
keyResourceName string | ||
gcpKMCClient *kms.KeyManagementClient | ||
gcpConfig *GCPConfig | ||
} | ||
|
||
type GCPConfig struct { | ||
CredentialsFileLocation string | ||
KeyResourceName string | ||
} | ||
|
||
var keyDetails = map[kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm]hash.Hash{ | ||
kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_2048_SHA256: sha256.New(), | ||
kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_3072_SHA256: sha256.New(), | ||
kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_4096_SHA256: sha256.New(), | ||
kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_4096_SHA512: sha512.New(), | ||
kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_2048_SHA1: sha1.New(), | ||
kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_3072_SHA1: sha1.New(), | ||
kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_4096_SHA1: sha1.New(), | ||
} | ||
|
||
// Creates a new instance of GoogleCloudKeyVaultStorage with the provided configuration. | ||
func NewGCPKeyVaultStorage(configFileLocation string, gcpConfig *GCPConfig) *GoogleCloudKeyVaultStorage { | ||
ctx := context.Background() | ||
if configFileLocation == "" { | ||
if envConfigFileLocation, ok := os.LookupEnv("KSM_CONFIG_FILE"); ok { | ||
configFileLocation = envConfigFileLocation | ||
} else { | ||
configFileLocation = core.DEFAULT_CONFIG_PATH | ||
} | ||
} | ||
|
||
gcpKeyManagementClient, err := kms.NewKeyManagementClient(ctx, option.WithCredentialsFile(gcpConfig.CredentialsFileLocation)) | ||
if err != nil { | ||
logger.Errorf("Failed to create GCP Key Management client: %v", err) | ||
return nil | ||
} | ||
defer gcpKeyManagementClient.Close() | ||
|
||
keyDetails, err := getKeyDetails(ctx, gcpKeyManagementClient, gcpConfig.KeyResourceName) | ||
if err != nil { | ||
return nil | ||
} | ||
|
||
if keyDetails.Purpose != kmspb.CryptoKey_ENCRYPT_DECRYPT && keyDetails.Purpose != kmspb.CryptoKey_ASYMMETRIC_DECRYPT { | ||
logger.Error("The specified key is not of type ENCRYPT_DECRYPT or ASYMMETRIC_DECRYPT") | ||
return nil | ||
} | ||
|
||
gcpStorage := &GoogleCloudKeyVaultStorage{ | ||
configFileLocation: configFileLocation, | ||
config: make(map[core.ConfigKey]interface{}), | ||
lastSavedConfigHash: "", | ||
keyResourceName: gcpConfig.KeyResourceName, | ||
gcpKMCClient: gcpKeyManagementClient, | ||
gcpConfig: gcpConfig, | ||
} | ||
|
||
gcpStorage.loadConfig() | ||
return gcpStorage | ||
} | ||
|
||
// Loads the decrypted configuration from the config file if encrypted config is present, else encrypts the config. | ||
func (g *GoogleCloudKeyVaultStorage) loadConfig() error { | ||
ctx := context.Background() | ||
var config map[core.ConfigKey]interface{} | ||
var jsonError error | ||
var decryptionError bool | ||
var decryptData []byte | ||
|
||
if err := g.createConfigFileIfMissing(); err != nil { | ||
return err | ||
} | ||
|
||
contents, err := os.ReadFile(g.configFileLocation) | ||
if err != nil { | ||
logger.Errorf("Failed to load config file %s: %s", g.configFileLocation, err.Error()) | ||
return fmt.Errorf("failed to load config file %s", g.configFileLocation) | ||
} | ||
|
||
if len(contents) == 0 { | ||
logger.Errorf("Empty config file %s", g.configFileLocation) | ||
contents = []byte("{}") | ||
} | ||
|
||
if err := json.Unmarshal(contents, &config); err == nil { | ||
g.config = config | ||
if err := g.saveConfig(config); err != nil { | ||
return err | ||
} | ||
Comment on lines
+123
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the point of saving, if it is loaded from file? |
||
|
||
configJson, err := json.Marshal(config) | ||
if err != nil { | ||
return fmt.Errorf("failed to marshal config: %w", err) | ||
} | ||
|
||
g.lastSavedConfigHash = g.createHash(configJson) | ||
} else { | ||
jsonError = err | ||
} | ||
|
||
if jsonError != nil { | ||
keydata, err := getKeyDetails(ctx, g.gcpKMCClient, g.gcpConfig.KeyResourceName) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if keydata.Purpose == kmspb.CryptoKey_ENCRYPT_DECRYPT { | ||
decryptData, err = decryptionSymmetric(ctx, g.gcpKMCClient, g.gcpConfig.KeyResourceName, contents) | ||
if err != nil { | ||
decryptionError = true | ||
logger.Errorf("Failed to decrypt config file: %s", err.Error()) | ||
return fmt.Errorf("failed to decrypt config file %s", g.configFileLocation) | ||
} | ||
} else { | ||
decryptData, err = decryptAsymmetric(ctx, g.gcpKMCClient, g.gcpConfig.KeyResourceName, contents) | ||
if err != nil { | ||
decryptionError = true | ||
logger.Errorf("Failed to decrypt config file: %s", err.Error()) | ||
return fmt.Errorf("failed to decrypt config file %s", g.configFileLocation) | ||
} | ||
} | ||
|
||
if err := json.Unmarshal(decryptData, &config); err != nil { | ||
decryptionError = true | ||
logger.Errorf("Failed to parse decrypted config file: %s", err.Error()) | ||
return fmt.Errorf("failed to parse decrypted config file %s", g.configFileLocation) | ||
} | ||
|
||
g.config = config | ||
g.lastSavedConfigHash = g.createHash(decryptData) | ||
} | ||
|
||
if jsonError != nil && decryptionError { | ||
logger.Errorf("Config file is not a valid JSON file: %s", jsonError.Error()) | ||
return fmt.Errorf("%s may contain JSON format problems", g.configFileLocation) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Saves the encrypted updated configuration to the config file and updates the hash of the config. | ||
func (g *GoogleCloudKeyVaultStorage) saveConfig(updatedConfig map[core.ConfigKey]interface{}) error { | ||
ctx := context.Background() | ||
configJson, err := json.Marshal(g.config) | ||
if err != nil { | ||
return fmt.Errorf("failed to marshal config: %w", err) | ||
} | ||
|
||
configHash := g.createHash(configJson) | ||
if len(updatedConfig) > 0 { | ||
updatedConfigJson, err := json.Marshal(updatedConfig) | ||
if err != nil { | ||
return fmt.Errorf("failed to marshal updated config: %w", err) | ||
} | ||
|
||
updatedConfigHash := g.createHash(updatedConfigJson) | ||
if updatedConfigHash != configHash { | ||
configHash = updatedConfigHash | ||
g.config = make(map[core.ConfigKey]interface{}) | ||
for k, v := range updatedConfig { | ||
g.config[k] = fmt.Sprintf("%v", v) | ||
} | ||
} | ||
} | ||
|
||
if configHash == g.lastSavedConfigHash { | ||
logger.Info("Skipped config JSON save. No changes detected.") | ||
return nil | ||
} | ||
|
||
if err := g.createConfigFileIfMissing(); err != nil { | ||
return err | ||
} | ||
|
||
if err := g.encryptConfig(ctx, configJson); err != nil { | ||
return err | ||
} | ||
|
||
g.lastSavedConfigHash = configHash | ||
return nil | ||
} | ||
|
||
// Creates a hash of the given configuration data. | ||
func (g *GoogleCloudKeyVaultStorage) createHash(config []byte) string { | ||
hash := md5.Sum(config) | ||
return hex.EncodeToString(hash[:]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as AWS PR. |
||
} | ||
|
||
// Creates the config file if it does not already exist. | ||
func (g *GoogleCloudKeyVaultStorage) createConfigFileIfMissing() error { | ||
if _, err := os.Stat(g.configFileLocation); !os.IsNotExist(err) { | ||
logger.Infof("Config file already exists at: %s", g.configFileLocation) | ||
return nil | ||
} | ||
|
||
dir := filepath.Dir(g.configFileLocation) | ||
if _, err := os.Stat(dir); os.IsNotExist(err) { | ||
if err := os.MkdirAll(dir, os.ModePerm); err != nil { | ||
return fmt.Errorf("failed to create directory %s: %w", dir, err) | ||
} | ||
} | ||
|
||
if err := g.encryptConfig(context.Background(), []byte("{}")); err != nil { | ||
return err | ||
} | ||
|
||
logger.Infof("Config file created at: %s", g.configFileLocation) | ||
return nil | ||
} | ||
|
||
// Retrieves the details of the specified key from Google Cloud KMS. | ||
func getKeyDetails(ctx context.Context, client *kms.KeyManagementClient, keyResourceName string) (*kmspb.CryptoKey, error) { | ||
// Remove the cryptoKeyVersions/<version> from the keyResourceName | ||
index := strings.Index(keyResourceName, "/cryptoKeyVersions/") | ||
if index != -1 { | ||
keyResourceName = keyResourceName[:index] | ||
} | ||
|
||
req := &kmspb.GetCryptoKeyRequest{ | ||
Name: keyResourceName, | ||
} | ||
|
||
// Fetch the key details from GCP | ||
resp, err := client.GetCryptoKey(ctx, req) | ||
if err != nil { | ||
logger.Errorf("Failed to get key details: %v", err) | ||
return nil, fmt.Errorf("failed to get key details: %w", err) | ||
} | ||
|
||
return resp, nil | ||
} | ||
|
||
// Encrypts the configuration data and writes it to the config file. | ||
func (g *GoogleCloudKeyVaultStorage) encryptConfig(ctx context.Context, config []byte) error { | ||
keyDetails, err := getKeyDetails(ctx, g.gcpKMCClient, g.gcpConfig.KeyResourceName) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if keyDetails.Purpose == kmspb.CryptoKey_ENCRYPT_DECRYPT { | ||
ciphertext, err := encryptionSymmetric(ctx, g.gcpKMCClient, g.gcpConfig.KeyResourceName, config) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := os.WriteFile(g.configFileLocation, ciphertext, 0644); err != nil { | ||
return fmt.Errorf("failed to write encrypted config file: %w", err) | ||
} | ||
} else { | ||
ciphertext, err := encryptAsymmetric(ctx, g.gcpKMCClient, g.gcpConfig.KeyResourceName, config) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := os.WriteFile(g.configFileLocation, ciphertext, 0644); err != nil { | ||
return fmt.Errorf("failed to write encrypted config file: %w", err) | ||
} | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should't this be inside GCP folder?