Skip to content
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
wants to merge 8 commits into
base: KEEP-149-gcp-go-review
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions integrations/ReadME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
**GCP Cloud Key Management**
Copy link
Collaborator

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?


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].
298 changes: 298 additions & 0 deletions integrations/gcp/GCPKeyVaultStorage.go
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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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[:])
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
}
Loading