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-148 Oracle Go SDK Develop #6

Open
wants to merge 2 commits into
base: KEEP-148-oracle-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
14 changes: 14 additions & 0 deletions integrations/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module oraclekv

go 1.23.5

require (
github.com/keeper-security/secrets-manager-go/core v1.6.4
github.com/oracle/oci-go-sdk/v65 v65.84.0
)

require (
github.com/gofrs/flock v0.8.1 // indirect
github.com/sony/gobreaker v0.5.0 // indirect
golang.org/x/sys v0.8.0 // indirect
)
29 changes: 29 additions & 0 deletions integrations/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
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=
github.com/oracle/oci-go-sdk/v65 v65.84.0 h1:NCEiq42gwrFJPLmIMxz4QnZSM4Wmp6n+sjpznBDg060=
github.com/oracle/oci-go-sdk/v65 v65.84.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
294 changes: 294 additions & 0 deletions integrations/oracle/OracleKeyVaultStorage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
// -*- coding: utf-8 -*-
// _ __
// | |/ /___ ___ _ __ ___ _ _ (R)
// | ' </ -_) -_) '_ \/ -_) '_|
// |_|\_\___\___| .__/\___|_|
// |_|
// Keeper Secrets Manager
// Copyright 2025 Keeper Security Inc.
// Contact: [email protected]

package oraclekv

import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"oraclekv/oracle/logger"
"os"
"path/filepath"

"github.com/keeper-security/secrets-manager-go/core"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/keymanagement"
)

type KeyConfig struct {
KeyId string
KeyVersionID string
VaultManagementEndpoint string
VaultCryptoEndpoint string
Profile string
ProfileConfigPath string
}

type OracleKeyVaultStorage struct {
configFileLocation string
config map[core.ConfigKey]interface{}
lastSavedConfigHash string
keyResourceName string
oracleKMSClient keymanagement.KmsCryptoClient
keyConfig *KeyConfig
}

// Creates a new OracleKeyVaultStorage instance.
func NewOracleKeyVaultStorage(configFileLocation string, keyConfig *KeyConfig) *OracleKeyVaultStorage {
var client keymanagement.KmsCryptoClient
var err error
if configFileLocation == "" {
if envConfigFileLocation, ok := os.LookupEnv("KSM_CONFIG_FILE"); ok {
configFileLocation = envConfigFileLocation
} else {
configFileLocation = core.DEFAULT_CONFIG_PATH
}
}

if keyConfig.Profile == "" && keyConfig.ProfileConfigPath == "" {
client, err = keymanagement.NewKmsCryptoClientWithConfigurationProvider(common.DefaultConfigProvider(), keyConfig.VaultCryptoEndpoint)
if err != nil {
logger.Errorf("Failed to create Oracle KMS crypto client: %v", err)
return nil
}
} else {
client, err = keymanagement.NewKmsCryptoClientWithConfigurationProvider(common.CustomProfileConfigProvider(keyConfig.ProfileConfigPath, keyConfig.Profile), keyConfig.VaultCryptoEndpoint)
if err != nil {
logger.Errorf("Failed to create Oracle KMS crypto client: %v", err)
return nil
}
}

keyDetails, err := getKeyDetails(keyConfig)
if err != nil {
logger.Errorf("Failed to get key details: %v", err)
return nil
}

if keyDetails.KeyShape.Algorithm != keymanagement.KeyShapeAlgorithmAes && keyDetails.KeyShape.Algorithm != keymanagement.KeyShapeAlgorithmRsa {
logger.Errorf("Unsupported key encryption algorithm: %v", keyDetails)
return nil
}

oracleStorage := &OracleKeyVaultStorage{
config: make(map[core.ConfigKey]interface{}),
lastSavedConfigHash: "",
configFileLocation: configFileLocation,
keyResourceName: keyConfig.KeyId,
oracleKMSClient: client,
keyConfig: keyConfig,
}

oracleStorage.loadConfig()
return oracleStorage
}

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

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

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

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

if err := json.Unmarshal(contents, &config); err == nil {
o.config = config
if err := o.saveConfig(config, false); err != nil {
logger.Errorf("Failed to save config: %v", err)
return err
}

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

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

if jsonError != nil {
keydata, err := getKeyDetails(o.keyConfig)
if err != nil {
return err
}

if keydata.KeyShape.Algorithm == keymanagement.KeyShapeAlgorithmAes {
decryptData, err = decryptSymmetric(o.oracleKMSClient, *o.keyConfig, contents)
if err != nil {
decryptError = true
logger.Errorf("Failed to decrypt config file: %s", err.Error())
return fmt.Errorf("failed to decrypt config file %s", o.configFileLocation)
}

} else {
decryptData, err = decryptAsymmetric(o.oracleKMSClient, *o.keyConfig, contents)
if err != nil {
decryptError = true
logger.Errorf("Failed to decrypt config file: %s", err.Error())
return fmt.Errorf("failed to decrypt config file %s", o.configFileLocation)
}
}

if err := json.Unmarshal(decryptData, &config); err != nil {
decryptError = true
logger.Errorf("Failed to parse decrypted config file: %s", err.Error())
return fmt.Errorf("failed to parse decrypted config file %s", o.configFileLocation)
}

o.config = config
o.lastSavedConfigHash = o.createHash(decryptData)
}

if jsonError != nil && decryptError {
logger.Errorf("Config file is not a valid JSON file: %s", jsonError.Error())
return fmt.Errorf("%s may contain JSON format problems", o.configFileLocation)
}

return nil
}

// Saves the encrypted updated configuration to the config file and updates the hash of the config.
func (o *OracleKeyVaultStorage) saveConfig(updatedConfig map[core.ConfigKey]interface{}, force bool) error {
configJson, err := json.Marshal(o.config)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}

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

if !force && configHash == o.lastSavedConfigHash {
logger.Info("Skipped config JSON save. No changes detected.")
return nil
}

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

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

o.lastSavedConfigHash = configHash
return nil
}

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

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

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

// Creates a hash of the given configuration data.
func (g *OracleKeyVaultStorage) createHash(config []byte) string {
hash := md5.Sum(config)
return hex.EncodeToString(hash[:])
}

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

if keydata.KeyShape.Algorithm == keymanagement.KeyShapeAlgorithmAes {
encryptedData, err := encryptSymmetric(o.oracleKMSClient, *o.keyConfig, config)
if err != nil {
logger.Errorf("Failed to encrypt config: %v", err)
return err
}
if err := os.WriteFile(o.configFileLocation, encryptedData, 0644); err != nil {
return fmt.Errorf("failed to write encrypted config file: %w", err)
}
} else {
encryptedData, err := encryptAsymmetric(o.oracleKMSClient, *o.keyConfig, config)
if err != nil {
logger.Errorf("Failed to encrypt config: %v", err)
return err
}
if err := os.WriteFile(o.configFileLocation, encryptedData, 0644); err != nil {
return fmt.Errorf("failed to write encrypted config file: %w", err)
}
}

return nil
}

// Update and save the config according to new Key.
func (o *OracleKeyVaultStorage) ChangeKey(updatedKeyConfig *KeyConfig) (bool, error) {
oldKeyConfig := o.keyConfig
oldKMSClient := o.oracleKMSClient
newKMSClient, err := keymanagement.NewKmsCryptoClientWithConfigurationProvider(common.DefaultConfigProvider(), updatedKeyConfig.VaultCryptoEndpoint)
if err != nil {
logger.Errorf("Failed to create Oracle KMS crypto client: %v", err)
return false, nil
}

o.keyConfig = updatedKeyConfig
o.oracleKMSClient = newKMSClient
if err := o.saveConfig(o.config, true); err != nil {
o.keyConfig = oldKeyConfig
o.oracleKMSClient = oldKMSClient
logger.Errorf("Failed to change the key to '%v' for config '%s': %v", updatedKeyConfig, o.configFileLocation, err)
return false, fmt.Errorf("failed to change the key for %s: %w", o.configFileLocation, err)
}

return true, nil
}
33 changes: 33 additions & 0 deletions integrations/oracle/ReadME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Oracle Key Management
Keeper Secrets Manager integrates with **Oracle Key Management Service (OCI KMS)** to provide protection for Keeper Secrets Manager configuration files. With this integration, you can secure connection details on your machine while leveraging Keeper's **zero-knowledge encryption** for all your secret credentials.

## Features
* Encrypt and decrypt your Keeper Secrets Manager configuration files using **OCI KMS**.
* Protect against unauthorized access to your **Secrets Manager connections**.
* Requires only minor code modifications for immediate protection. Works with all Keeper Secrets Manager **GoLang SDK** functionality.

## Prerequisites
* Supports the GoLang Secrets Manager SDK.
* Requires the oci-keymanagement package from OCI SDK.
* OCI KMS Key needs `ENCRYPT` and `DECRYPT` permissions.

## Setup

1. Install KSM Storage Module

The Secrets Manager oracle KSM module can be installed using npm

> `go get github.com/keeper-security/secrets-manager-go/core`
2. Configure oracle Connection

By default, the oci-keymanagement library will use the **default OCI configuration file** (`~/.oci/config`).

See the (OCI documentation)[https://docs.oracle.com/en-us/iaas/Content/API/Concepts/sdkconfig.htm] for more details.

1. Add oracle KMS Storage to Your Code

Now that the oracle connection has been configured, you need to tell the Secrets Manager SDK to utilize the KMS as storage.

To do this, use `OciKeyValueStorage` as your Secrets Manager storage in the SecretsManager constructor.

The storage will require an `Config file location`, `configuration profile`(if there are multiple profile configurations) and the OCI `KMS endpoint` as well as the name of the Secrets Manager configuration file which will be encrypted by Oracle KMS.
Loading