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-142 AWS GO SDK Develop #3

Open
wants to merge 13 commits into
base: KEEP-142-aws-go-review
Choose a base branch
from
4 changes: 2 additions & 2 deletions example/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github.com/keeper-security/secrets-manager-go/core v1.6.3 h1:XEHZ8fQ2DFBISK80jWdHmzT56PFqEkXSkakqZxTD8zI=
github.com/keeper-security/secrets-manager-go/core v1.6.3/go.mod h1:dtlaeeds9+SZsbDAZnQRsDSqEAK9a62SYtqhNql+VgQ=
github.com/keeper-security/secrets-manager-go/core v1.6.4 h1:ly2XvAgDxHoHVvFXOIYlxzxBF0yoQir1KfNHUNG4eRA=
github.com/keeper-security/secrets-manager-go/core v1.6.4/go.mod h1:dtlaeeds9+SZsbDAZnQRsDSqEAK9a62SYtqhNql+VgQ=
319 changes: 319 additions & 0 deletions integrations/aws/AWSKeyValueStorage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
// -*- coding: utf-8 -*-

// _ __
// | |/ /___ ___ _ __ ___ _ _ (R)
// | ' </ -_) -_) '_ \/ -_) '_|
// |_|\_\___\___| .__/\___|_|
// |_|
//
// Keeper Secrets Manager
// Copyright 2025 Keeper Security Inc.
// Contact: [email protected]

package awskv

import (
"awskv/aws/logger"
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/aws/aws-sdk-go-v2/service/kms/types"
"github.com/keeper-security/secrets-manager-go/core"
)

type AWSConfig struct {
ClientID string
ClientSecret string
Region string
}

type AWSKeyVaultStorage struct {
configFileLocation string
config map[core.ConfigKey]interface{}
lastSavedConfigHash string
kmsClient *kms.Client
keyARN string
awsConfig *AWSConfig
}

// Creates a new instance of AWSKeyVaultStorage.
func NewAWSKeyValueStorage(configFileLocation string, KeyARN string, awsSessionConfig *AWSConfig) *AWSKeyVaultStorage {
if configFileLocation == "" {
if envConfigFileLocation, ok := os.LookupEnv("KSM_CONFIG_FILE"); ok {
configFileLocation = envConfigFileLocation
} else {
configFileLocation = core.DEFAULT_CONFIG_PATH
}
}

cfg, err := getConfig(awsSessionConfig)
if err != nil {
return nil
}

if KeyARN == "" {
logger.Errorf("Failed to create client secret credential: %v", err)
return nil
}

// Generate a new AWS KMS client
client := kms.NewFromConfig(*cfg)
awsDetails := &AWSKeyVaultStorage{
configFileLocation: configFileLocation,
config: make(map[core.ConfigKey]interface{}),
lastSavedConfigHash: "",
kmsClient: client,
keyARN: KeyARN,
awsConfig: awsSessionConfig,
}

keyData, err := awsDetails.getKeyDetails()
// If key is not type of encrypt/decrypt, client operations will fail.
if err != nil && keyData.KeyMetadata.KeyUsage != types.KeyUsageTypeEncryptDecrypt {
logger.Errorf("Failed to create client secret credential: %v", err)
return nil
}

err = awsDetails.loadConfig()
if err != nil {
return nil
}

return awsDetails
}

// Loads the decrypted configuration from the config file if encrypted config is present, else encrypts the config.
func (a *AWSKeyVaultStorage) loadConfig() error {
var config map[core.ConfigKey]interface{}
var jsonError error
var decryptionError bool
var decryptData []byte

if err := a.createConfigFileIfMissing(); err != nil {
return err
}

contents, err := os.ReadFile(a.configFileLocation)
if err != nil {
logger.Errorf("Failed to load config file %s: %s", a.configFileLocation, err.Error())
return fmt.Errorf("failed to load config file %s", a.configFileLocation)
}

if len(contents) == 0 {
logger.Errorf("Empty config file %s", a.configFileLocation)
contents = []byte("{}")
}

if err := json.Unmarshal(contents, &config); err == nil {
a.config = config
if err := a.saveConfig(config); err != nil {
return err
}

configJson, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}

a.lastSavedConfigHash = a.createHash(configJson)
} else {
jsonError = err
}

if jsonError != nil {
keydata, err := a.getKeyDetails()
if err != nil {
}

if keydata.KeyMetadata.KeySpec == types.KeySpecSymmetricDefault {
decryptData, err = decryptSymmetric(a.kmsClient, a.keyARN, 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", a.configFileLocation)
}
} else {
decryptData, err = decryptAsymmetric(a.kmsClient, a.keyARN, 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", a.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", a.configFileLocation)
}

a.config = config
a.lastSavedConfigHash = a.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", a.configFileLocation)
}

return nil
}

// Saves the encrypted updated configuration to the config file and updates the hash of the config.
func (a *AWSKeyVaultStorage) saveConfig(updatedConfig map[core.ConfigKey]interface{}) error {
configJson, err := json.Marshal(a.config)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add the comment in code wherever needed like what the method do see here
https://github.com/Keeper-Security/secrets-manager-go/blob/master/core/core.go

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the comments.

if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}

configHash := a.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 := a.createHash(updatedConfigJson)
if updatedConfigHash != configHash {
configHash = updatedConfigHash
a.config = make(map[core.ConfigKey]interface{})
for k, v := range updatedConfig {
a.config[k] = fmt.Sprintf("%v", v)
}
}
}

if configHash == a.lastSavedConfigHash {
fmt.Println("Skipped config JSON save. No changes detected.")
return nil
}

if err := a.createConfigFileIfMissing(); err != nil {
return err
}

if err := a.encryptConfig(configJson); err != nil {
return err
}

a.lastSavedConfigHash = configHash
return nil
}

// Creates the config file if does not exist and encrypts it.
func (a *AWSKeyVaultStorage) createConfigFileIfMissing() error {
if _, err := os.Stat(a.configFileLocation); !os.IsNotExist(err) {
logger.Infof("Config file already exists at: %s", a.configFileLocation)
return nil
}

dir := filepath.Dir(a.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 := a.encryptConfig([]byte("{}")); err != nil {
return err
}

logger.Infof("Config file created at: %s", a.configFileLocation)
return nil
}

// Retrieves the details of the KMS key.
func (a *AWSKeyVaultStorage) getKeyDetails() (*kms.DescribeKeyOutput, error) {
keyDetails, err := a.kmsClient.DescribeKey(context.Background(), &kms.DescribeKeyInput{
KeyId: &a.keyARN,
})

if err != nil {
logger.Errorf("Failed to get key details: %v", err)
return nil, fmt.Errorf("failed to get key details: %w", err)
}

return keyDetails, nil
}

// createHash creates an MD5 hash of the provided config data.
func (a *AWSKeyVaultStorage) 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.

Why can't we do return hex.EncodeToString(hash)?

}

// Retrieves the AWS configuration.
// If the client ID, client secret, and region are provided, it returns the configuration with the provided values else it returns the default configuration.
func getConfig(awsSessionConfig *AWSConfig) (*aws.Config, error) {
if awsSessionConfig.ClientID != "" && awsSessionConfig.ClientSecret != "" && awsSessionConfig.Region != "" {
return &aws.Config{
Credentials: credentials.NewStaticCredentialsProvider(awsSessionConfig.ClientID, awsSessionConfig.ClientSecret, ""),
Region: awsSessionConfig.Region,
}, nil
} else {
cfg, err := config.LoadDefaultConfig(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to load default config: %w", err)
}
return &cfg, nil
}
}

// Encrypts the configuration data and writes it to the config file.
func (a *AWSKeyVaultStorage) encryptConfig(config []byte) error {
keydata, err := a.getKeyDetails()
if err != nil {
return err
}

var blob []byte
if keydata.KeyMetadata.KeySpec == types.KeySpecSymmetricDefault {
blob, err = encryptSymmetric(a.kmsClient, a.keyARN, config)
if err != nil {
return fmt.Errorf("failed to encrypt config: %w", err)
}
} else {
blob, err = encryptAsymmetric(a.kmsClient, a.keyARN, config)
if err != nil {
return fmt.Errorf("failed to encrypt config: %w", err)
}
}

if err := os.WriteFile(a.configFileLocation, blob, 0644); err != nil {
return fmt.Errorf("failed to write config file %s: %w", a.configFileLocation, err)
}

logger.Debug("Config file created at: ", a.configFileLocation)
return nil
}

// Changes the KMS key used for encryption and decryption.
func (a *AWSKeyVaultStorage) ChangeKey(newKeyARN string) (bool, error) {
oldKeyARN := a.keyARN
oldKMSClient := a.kmsClient
config, err := getConfig(a.awsConfig)
if err != nil {
return false, fmt.Errorf("failed to get config: %w", err)
}

client := kms.NewFromConfig(*config)
a.kmsClient = client
a.keyARN = newKeyARN
if err := a.saveConfig(a.config); err != nil {
a.kmsClient = oldKMSClient
a.keyARN = oldKeyARN
logger.Errorf("Failed to change the key to '%s' for config '%s': %v", newKeyARN, a.configFileLocation, err)
return false, fmt.Errorf("failed to change the key for %s: %w", a.configFileLocation, err)
}

return true, nil
}
53 changes: 53 additions & 0 deletions integrations/aws/ReadME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
**AWS Key Management**

Protect Secrets Manager connection details with AWS Key Management

Keeper Secrets Manager integrates with AWS 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 AWS 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 AWS packages: aws, config, credentials, kms, kms-types
* Works with AES/RSA key types with `Encrypt` and `Decrypt` permissions.

Setup
1. Install Secret-Manager-Go Package

The Secrets Manager AWS 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 AWS Connection

configuration variables can be provided as

```
import awskv "github.com/keeper-security/secrets-manager-go/awskv"

clientOptions := &ksm.ClientOptions{
Token: "[One Time Access Token]",
Config: awskv.NewAWSKeyValueStorage(<config-file-path-with-its-name>, <key-arn>, &awskv.AWSConfig{
ClientID: "<Some Client ID>",
ClientSecret: "<Some Client Secret>",
Region: "<Cloud Region>",
}),
}
```
The storage will require an AWS credentials if not present it will fetch from environment, as well Secrets Manager configuration which will be encrypted by AWS Key Management.

Provide `ClientID` , `ClientSecret` and `Region` variables.

KeyURL must be like this `arn:<partition>:kms:<region>:<account-id>:key/<key-id>`

For more information about URL see the AWS Key Management Documentation
https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html

You're ready to use the KSM integration 👍

Using the AWS 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].
Loading