mirror of
https://github.com/davidallendj/magellan.git
synced 2025-12-20 11:37:01 -07:00
feat(secrets): implement SecretStore interface and StaticStore/LocalStore for credential management
This commit is contained in:
parent
ccce61694b
commit
ee1fc327e2
13 changed files with 531 additions and 34 deletions
|
|
@ -8,6 +8,7 @@ import (
|
||||||
urlx "github.com/OpenCHAMI/magellan/internal/url"
|
urlx "github.com/OpenCHAMI/magellan/internal/url"
|
||||||
magellan "github.com/OpenCHAMI/magellan/pkg"
|
magellan "github.com/OpenCHAMI/magellan/pkg"
|
||||||
"github.com/OpenCHAMI/magellan/pkg/auth"
|
"github.com/OpenCHAMI/magellan/pkg/auth"
|
||||||
|
"github.com/OpenCHAMI/magellan/pkg/secrets"
|
||||||
"github.com/cznic/mathutil"
|
"github.com/cznic/mathutil"
|
||||||
magellan "github.com/davidallendj/magellan/internal"
|
magellan "github.com/davidallendj/magellan/internal"
|
||||||
"github.com/davidallendj/magellan/internal/cache/sqlite"
|
"github.com/davidallendj/magellan/internal/cache/sqlite"
|
||||||
|
|
@ -59,10 +60,10 @@ var CollectCmd = &cobra.Command{
|
||||||
if concurrency <= 0 {
|
if concurrency <= 0 {
|
||||||
concurrency = mathutil.Clamp(len(scannedResults), 1, 10000)
|
concurrency = mathutil.Clamp(len(scannedResults), 1, 10000)
|
||||||
}
|
}
|
||||||
|
// Create a StaticSecretStore to hold the username and password
|
||||||
|
secrets := secrets.NewStaticStore(username, password)
|
||||||
_, err = magellan.CollectInventory(&scannedResults, &magellan.CollectParams{
|
_, err = magellan.CollectInventory(&scannedResults, &magellan.CollectParams{
|
||||||
URI: host,
|
URI: host,
|
||||||
Username: username,
|
|
||||||
Password: password,
|
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
Concurrency: concurrency,
|
Concurrency: concurrency,
|
||||||
Verbose: verbose,
|
Verbose: verbose,
|
||||||
|
|
@ -70,7 +71,7 @@ var CollectCmd = &cobra.Command{
|
||||||
OutputPath: outputPath,
|
OutputPath: outputPath,
|
||||||
ForceUpdate: forceUpdate,
|
ForceUpdate: forceUpdate,
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
})
|
}, secrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("failed to collect data")
|
log.Error().Err(err).Msgf("failed to collect data")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
cmd/crawl.go
16
cmd/crawl.go
|
|
@ -5,8 +5,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
urlx "github.com/davidallendj/magellan/internal/url"
|
urlx "github.com/OpenCHAMI/magellan/internal/url"
|
||||||
"github.com/davidallendj/magellan/pkg/crawler"
|
"github.com/OpenCHAMI/magellan/pkg/crawler"
|
||||||
|
"github.com/OpenCHAMI/magellan/pkg/secrets"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
@ -35,11 +36,14 @@ var CrawlCmd = &cobra.Command{
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
staticStore := &secrets.StaticStore{
|
||||||
|
Username: viper.GetString("crawl.username"),
|
||||||
|
Password: viper.GetString("crawl.password"),
|
||||||
|
}
|
||||||
systems, err := crawler.CrawlBMCForSystems(crawler.CrawlerConfig{
|
systems, err := crawler.CrawlBMCForSystems(crawler.CrawlerConfig{
|
||||||
URI: args[0],
|
URI: args[0],
|
||||||
Username: cmd.Flag("username").Value.String(),
|
CredentialStore: staticStore,
|
||||||
Password: cmd.Flag("password").Value.String(),
|
Insecure: cmd.Flag("insecure").Value.String() == "true",
|
||||||
Insecure: cmd.Flag("insecure").Value.String() == "true",
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error crawling BMC: %v", err)
|
log.Fatalf("Error crawling BMC: %v", err)
|
||||||
|
|
|
||||||
5
go.mod
5
go.mod
|
|
@ -16,7 +16,10 @@ require (
|
||||||
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8
|
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/rs/zerolog v1.33.0
|
require (
|
||||||
|
github.com/rs/zerolog v1.33.0
|
||||||
|
golang.org/x/crypto v0.32.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/davidallendj/magellan/pkg/client"
|
"github.com/OpenCHAMI/magellan/pkg/client"
|
||||||
"github.com/davidallendj/magellan/pkg/crawler"
|
"github.com/OpenCHAMI/magellan/pkg/crawler"
|
||||||
|
"github.com/OpenCHAMI/magellan/pkg/secrets"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
|
@ -48,7 +49,7 @@ type CollectParams struct {
|
||||||
//
|
//
|
||||||
// Requests can be made to several of the nodes using a goroutine by setting the q.Concurrency
|
// Requests can be made to several of the nodes using a goroutine by setting the q.Concurrency
|
||||||
// property value between 1 and 10000.
|
// property value between 1 and 10000.
|
||||||
func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[string]any, error) {
|
func CollectInventory(assets *[]RemoteAsset, params *CollectParams, store secrets.SecretStore) ([]map[string]any, error) {
|
||||||
// check for available remote assets found from scan
|
// check for available remote assets found from scan
|
||||||
if assets == nil {
|
if assets == nil {
|
||||||
return nil, fmt.Errorf("no assets found")
|
return nil, fmt.Errorf("no assets found")
|
||||||
|
|
@ -117,10 +118,9 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin
|
||||||
systems []crawler.InventoryDetail
|
systems []crawler.InventoryDetail
|
||||||
managers []crawler.Manager
|
managers []crawler.Manager
|
||||||
config = crawler.CrawlerConfig{
|
config = crawler.CrawlerConfig{
|
||||||
URI: fmt.Sprintf("%s:%d", sr.Host, sr.Port),
|
URI: fmt.Sprintf("%s:%d", sr.Host, sr.Port),
|
||||||
Username: params.Username,
|
CredentialStore: store,
|
||||||
Password: params.Password,
|
Insecure: true,
|
||||||
Insecure: true,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
systems, err := crawler.CrawlBMCForSystems(config)
|
systems, err := crawler.CrawlBMCForSystems(config)
|
||||||
|
|
@ -260,10 +260,15 @@ func FindMACAddressWithIP(config crawler.CrawlerConfig, targetIP net.IP) (string
|
||||||
// gofish (at least for now). If there's a need for grabbing more
|
// gofish (at least for now). If there's a need for grabbing more
|
||||||
// manager information in the future, we can move the logic into
|
// manager information in the future, we can move the logic into
|
||||||
// the crawler.
|
// the crawler.
|
||||||
|
bmc_creds, err := config.GetUserPass()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get credentials for URI: %s", config.URI)
|
||||||
|
}
|
||||||
|
|
||||||
client, err := gofish.Connect(gofish.ClientConfig{
|
client, err := gofish.Connect(gofish.ClientConfig{
|
||||||
Endpoint: config.URI,
|
Endpoint: config.URI,
|
||||||
Username: config.Username,
|
Username: bmc_creds.Username,
|
||||||
Password: config.Password,
|
Password: bmc_creds.Password,
|
||||||
Insecure: config.Insecure,
|
Insecure: config.Insecure,
|
||||||
BasicAuth: true,
|
BasicAuth: true,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,29 @@
|
||||||
package crawler
|
package crawler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/OpenCHAMI/magellan/pkg/secrets"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/stmcginnis/gofish"
|
"github.com/stmcginnis/gofish"
|
||||||
"github.com/stmcginnis/gofish/redfish"
|
"github.com/stmcginnis/gofish/redfish"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CrawlerConfig struct {
|
type CrawlerConfig struct {
|
||||||
URI string // URI of the BMC
|
URI string // URI of the BMC
|
||||||
Username string // Username for the BMC
|
Insecure bool // Whether to ignore SSL errors
|
||||||
Password string // Password for the BMC
|
CredentialStore secrets.SecretStore
|
||||||
Insecure bool // Whether to ignore SSL errors
|
}
|
||||||
|
|
||||||
|
func (cc *CrawlerConfig) GetUserPass() (BMCUsernamePassword, error) {
|
||||||
|
return loadBMCCreds(*cc)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BMCUsernamePassword struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EthernetInterface struct {
|
type EthernetInterface struct {
|
||||||
|
|
@ -82,11 +92,20 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) {
|
||||||
systems []InventoryDetail
|
systems []InventoryDetail
|
||||||
rf_systems []*redfish.ComputerSystem
|
rf_systems []*redfish.ComputerSystem
|
||||||
)
|
)
|
||||||
|
// get username and password from secret store
|
||||||
|
bmc_creds, err := loadBMCCreds(config)
|
||||||
|
if err != nil {
|
||||||
|
event := log.Error()
|
||||||
|
event.Err(err)
|
||||||
|
event.Msg("failed to load BMC credentials")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// initialize gofish client
|
// initialize gofish client
|
||||||
client, err := gofish.Connect(gofish.ClientConfig{
|
client, err := gofish.Connect(gofish.ClientConfig{
|
||||||
Endpoint: config.URI,
|
Endpoint: config.URI,
|
||||||
Username: config.Username,
|
Username: bmc_creds.Username,
|
||||||
Password: config.Password,
|
Password: bmc_creds.Password,
|
||||||
Insecure: config.Insecure,
|
Insecure: config.Insecure,
|
||||||
BasicAuth: true,
|
BasicAuth: true,
|
||||||
})
|
})
|
||||||
|
|
@ -149,12 +168,21 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) {
|
||||||
// 6. Fetches the list of managers from the ServiceRoot.
|
// 6. Fetches the list of managers from the ServiceRoot.
|
||||||
// 7. Returns the list of managers and any error encountered during the process.
|
// 7. Returns the list of managers and any error encountered during the process.
|
||||||
func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) {
|
func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) {
|
||||||
|
|
||||||
|
// get username and password from secret store
|
||||||
|
bmc_creds, err := loadBMCCreds(config)
|
||||||
|
if err != nil {
|
||||||
|
event := log.Error()
|
||||||
|
event.Err(err)
|
||||||
|
event.Msg("failed to load BMC credentials")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
// initialize gofish client
|
// initialize gofish client
|
||||||
var managers []Manager
|
var managers []Manager
|
||||||
client, err := gofish.Connect(gofish.ClientConfig{
|
client, err := gofish.Connect(gofish.ClientConfig{
|
||||||
Endpoint: config.URI,
|
Endpoint: config.URI,
|
||||||
Username: config.Username,
|
Username: bmc_creds.Username,
|
||||||
Password: config.Password,
|
Password: bmc_creds.Password,
|
||||||
Insecure: config.Insecure,
|
Insecure: config.Insecure,
|
||||||
BasicAuth: true,
|
BasicAuth: true,
|
||||||
})
|
})
|
||||||
|
|
@ -344,3 +372,22 @@ func walkManagers(rf_managers []*redfish.Manager, baseURI string) ([]Manager, er
|
||||||
}
|
}
|
||||||
return managers, nil
|
return managers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadBMCCreds(config CrawlerConfig) (BMCUsernamePassword, error) {
|
||||||
|
creds, err := config.CredentialStore.GetSecretByID(config.URI)
|
||||||
|
if err != nil {
|
||||||
|
event := log.Error()
|
||||||
|
event.Err(err)
|
||||||
|
event.Msg("failed to get credentials from secret store")
|
||||||
|
return BMCUsernamePassword{}, err
|
||||||
|
}
|
||||||
|
var bmc_creds BMCUsernamePassword
|
||||||
|
err = json.Unmarshal([]byte(creds), &bmc_creds)
|
||||||
|
if err != nil {
|
||||||
|
event := log.Error()
|
||||||
|
event.Err(err)
|
||||||
|
event.Msg("failed to unmarshal credentials")
|
||||||
|
return BMCUsernamePassword{}, err
|
||||||
|
}
|
||||||
|
return bmc_creds, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ func rawConnect(address string, protocol string, timeoutSeconds int, keepOpenOnl
|
||||||
)
|
)
|
||||||
|
|
||||||
// try to conntect to host (expects host in format [10.0.0.0]:443)
|
// try to conntect to host (expects host in format [10.0.0.0]:443)
|
||||||
target := fmt.Sprintf("%s:%s", uri.Hostname(), uri.Port())
|
target := net.JoinHostPort(uri.Hostname(), uri.Port())
|
||||||
conn, err := net.DialTimeout(protocol, target, timeoutDuration)
|
conn, err := net.DialTimeout(protocol, target, timeoutDuration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
asset.State = false
|
asset.State = false
|
||||||
|
|
|
||||||
75
pkg/secrets/encryption.go
Normal file
75
pkg/secrets/encryption.go
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Derive a unique AES key per SecretID using HKDF
|
||||||
|
func deriveAESKey(masterKey []byte, secretID string) []byte {
|
||||||
|
salt := []byte(secretID)
|
||||||
|
hkdf := hkdf.New(sha256.New, masterKey, salt, nil)
|
||||||
|
derivedKey := make([]byte, 32) // AES-256 key
|
||||||
|
io.ReadFull(hkdf, derivedKey)
|
||||||
|
return derivedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt data using AES-GCM
|
||||||
|
func encryptAESGCM(key, plaintext []byte) (string, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
aesGCM, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, aesGCM.NonceSize())
|
||||||
|
_, err = io.ReadFull(rand.Reader, nonce)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil)
|
||||||
|
return hex.EncodeToString(ciphertext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt data using AES-GCM
|
||||||
|
func decryptAESGCM(key []byte, encryptedData string) (string, error) {
|
||||||
|
data, err := hex.DecodeString(encryptedData)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
aesGCM, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceSize := aesGCM.NonceSize()
|
||||||
|
if len(data) < nonceSize {
|
||||||
|
return "", fmt.Errorf("ciphertext too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||||
|
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(plaintext), nil
|
||||||
|
}
|
||||||
41
pkg/secrets/encryption_test.go
Normal file
41
pkg/secrets/encryption_test.go
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeriveAESKey(t *testing.T) {
|
||||||
|
masterKey := []byte("testmasterkey")
|
||||||
|
secretID := "mySecretID"
|
||||||
|
key1 := deriveAESKey(masterKey, secretID)
|
||||||
|
key2 := deriveAESKey(masterKey, secretID)
|
||||||
|
|
||||||
|
if len(key1) != 32 {
|
||||||
|
t.Errorf("derived key should be 32 bytes, got %d", len(key1))
|
||||||
|
}
|
||||||
|
if string(key1) != string(key2) {
|
||||||
|
t.Errorf("keys derived from same secretID should match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecryptAESGCM(t *testing.T) {
|
||||||
|
masterKey := []byte("anotherTestMasterKey")
|
||||||
|
secretID := "testSecret"
|
||||||
|
plaintext := "Hello, secrets!"
|
||||||
|
|
||||||
|
key := deriveAESKey(masterKey, secretID)
|
||||||
|
|
||||||
|
encrypted, err := encryptAESGCM(key, []byte(plaintext))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encryption failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted, err := decryptAESGCM(key, encrypted)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decryption failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decrypted != plaintext {
|
||||||
|
t.Errorf("expected %q, got %q", plaintext, decrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
129
pkg/secrets/localstore.go
Normal file
129
pkg/secrets/localstore.go
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Structure to store encrypted secrets in a JSON file
|
||||||
|
type LocalSecretStore struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
masterKey []byte
|
||||||
|
filename string
|
||||||
|
Secrets map[string]string `json:"secrets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocalSecretStore(masterKeyHex, filename string, create bool) (*LocalSecretStore, error) {
|
||||||
|
var secrets map[string]string
|
||||||
|
|
||||||
|
masterKey, err := hex.DecodeString(masterKeyHex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to generate masterkey from hex representation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||||
|
if !create {
|
||||||
|
return nil, fmt.Errorf("file %s does not exist", filename)
|
||||||
|
}
|
||||||
|
file, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to create file %s: %v", filename, err)
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
secrets = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if secrets == nil {
|
||||||
|
secrets, err = loadSecrets(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to load secrets from file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LocalSecretStore{
|
||||||
|
masterKey: masterKey,
|
||||||
|
filename: filename,
|
||||||
|
Secrets: secrets,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateMasterKey creates a 32-byte random key and returns it as a hex string.
|
||||||
|
func GenerateMasterKey() (string, error) {
|
||||||
|
key := make([]byte, 32) // 32 bytes for AES-256
|
||||||
|
_, err := rand.Read(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(key), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSecretByID decrypts the secret using the master key and returns it
|
||||||
|
func (l *LocalSecretStore) GetSecretByID(secretID string) (string, error) {
|
||||||
|
l.mu.RLock()
|
||||||
|
encrypted, exists := l.Secrets[secretID]
|
||||||
|
l.mu.RUnlock()
|
||||||
|
if !exists {
|
||||||
|
return "", fmt.Errorf("no secret found for %s", secretID)
|
||||||
|
}
|
||||||
|
|
||||||
|
derivedKey := deriveAESKey(l.masterKey, secretID)
|
||||||
|
return decryptAESGCM(derivedKey, encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreSecretByID encrypts the secret using the master key and stores it in the JSON file
|
||||||
|
func (l *LocalSecretStore) StoreSecretByID(secretID, secret string) error {
|
||||||
|
derivedKey := deriveAESKey(l.masterKey, secretID)
|
||||||
|
encryptedSecret, err := encryptAESGCM(derivedKey, []byte(secret))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
l.Secrets[secretID] = encryptedSecret
|
||||||
|
err = saveSecrets(l.filename, l.Secrets)
|
||||||
|
l.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSecrets returns a copy of secret IDs to secrets stored in memory
|
||||||
|
func (l *LocalSecretStore) ListSecrets() (map[string]string, error) {
|
||||||
|
l.mu.RLock()
|
||||||
|
defer l.mu.RUnlock()
|
||||||
|
|
||||||
|
secretsCopy := make(map[string]string)
|
||||||
|
for key, value := range l.Secrets {
|
||||||
|
secretsCopy[key] = value
|
||||||
|
}
|
||||||
|
return secretsCopy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves secrets back to the JSON file
|
||||||
|
func saveSecrets(jsonFile string, store map[string]string) error {
|
||||||
|
file, err := os.OpenFile(jsonFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(file)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
return encoder.Encode(store)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads the secrets JSON file
|
||||||
|
func loadSecrets(jsonFile string) (map[string]string, error) {
|
||||||
|
file, err := os.Open(jsonFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to open secret file %s:%v", jsonFile, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
store := make(map[string]string)
|
||||||
|
decoder := json.NewDecoder(file)
|
||||||
|
err = decoder.Decode(&store)
|
||||||
|
return store, err
|
||||||
|
}
|
||||||
151
pkg/secrets/localstore_test.go
Normal file
151
pkg/secrets/localstore_test.go
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewLocalSecretStore(t *testing.T) {
|
||||||
|
masterKey, err := GenerateMasterKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate master key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := "test_secrets.json"
|
||||||
|
defer os.Remove(filename)
|
||||||
|
|
||||||
|
store, err := NewLocalSecretStore(masterKey, filename, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create LocalSecretStore: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if store.filename != filename {
|
||||||
|
t.Errorf("Expected filename %s, got %s", filename, store.filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hex.EncodeToString(store.masterKey) != masterKey {
|
||||||
|
t.Errorf("Expected master key %s, got %s", masterKey, hex.EncodeToString(store.masterKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateMasterKey(t *testing.T) {
|
||||||
|
key, err := GenerateMasterKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate master key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key) != 64 { // 32 bytes in hex representation
|
||||||
|
t.Errorf("Expected key length 64, got %d", len(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreAndGetSecretByID(t *testing.T) {
|
||||||
|
masterKey, err := GenerateMasterKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate master key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := "test_secrets.json"
|
||||||
|
defer os.Remove(filename)
|
||||||
|
|
||||||
|
store, err := NewLocalSecretStore(masterKey, filename, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create LocalSecretStore: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretID := "test_secret"
|
||||||
|
secretValue := "my_secret_value"
|
||||||
|
|
||||||
|
err = store.StoreSecretByID(secretID, secretValue)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to store secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
retrievedSecret, err := store.GetSecretByID(secretID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if retrievedSecret != secretValue {
|
||||||
|
t.Errorf("Expected secret value %s, got %s", secretValue, retrievedSecret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreAndGetSecretJSON(t *testing.T) {
|
||||||
|
masterKey, err := GenerateMasterKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate master key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := "test_secrets.json"
|
||||||
|
defer os.Remove(filename)
|
||||||
|
|
||||||
|
store, err := NewLocalSecretStore(masterKey, filename, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create LocalSecretStore: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretID := "json_creds"
|
||||||
|
jsonSecret := `{"username":"testUser","password":"testPass"}`
|
||||||
|
|
||||||
|
if err := store.StoreSecretByID(secretID, jsonSecret); err != nil {
|
||||||
|
t.Fatalf("Failed to store JSON secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
retrieved, err := store.GetSecretByID(secretID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get JSON secret by ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if retrieved != jsonSecret {
|
||||||
|
t.Errorf("Expected %s, got %s", jsonSecret, retrieved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListSecrets(t *testing.T) {
|
||||||
|
masterKey, err := GenerateMasterKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate master key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := "test_secrets.json"
|
||||||
|
defer os.Remove(filename)
|
||||||
|
|
||||||
|
store, err := NewLocalSecretStore(masterKey, filename, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create LocalSecretStore: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretID1 := "test_secret_1"
|
||||||
|
secretValue1 := "my_secret_value_1"
|
||||||
|
secretID2 := "test_secret_2"
|
||||||
|
secretValue2 := "my_secret_value_2"
|
||||||
|
|
||||||
|
err = store.StoreSecretByID(secretID1, secretValue1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to store secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.StoreSecretByID(secretID2, secretValue2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to store secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := store.ListSecrets()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to list secrets: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(secrets) != 2 {
|
||||||
|
t.Errorf("Expected 2 secrets, got %d", len(secrets))
|
||||||
|
}
|
||||||
|
|
||||||
|
if secrets[secretID1] != store.Secrets[secretID1] {
|
||||||
|
t.Errorf("Expected secret value %s, got %s", store.Secrets[secretID1], secrets[secretID1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if secrets[secretID2] != store.Secrets[secretID2] {
|
||||||
|
t.Errorf("Expected secret value %s, got %s", store.Secrets[secretID2], secrets[secretID2])
|
||||||
|
}
|
||||||
|
}
|
||||||
7
pkg/secrets/main.go
Normal file
7
pkg/secrets/main.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
type SecretStore interface {
|
||||||
|
GetSecretByID(secretID string) (string, error)
|
||||||
|
StoreSecretByID(secretID, secret string) error
|
||||||
|
ListSecrets() (map[string]string, error)
|
||||||
|
}
|
||||||
28
pkg/secrets/staticstore.go
Normal file
28
pkg/secrets/staticstore.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type StaticStore struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStaticStore creates a new StaticStore with the given username and password.
|
||||||
|
func NewStaticStore(username, password string) *StaticStore {
|
||||||
|
return &StaticStore{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StaticStore) GetSecretByID(secretID string) (string, error) {
|
||||||
|
return fmt.Sprintf(`{"username":"%s","password":"%s"}`, s.Username, s.Password), nil
|
||||||
|
}
|
||||||
|
func (s *StaticStore) StoreSecretByID(secretID, secret string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *StaticStore) ListSecrets() (map[string]string, error) {
|
||||||
|
return map[string]string{
|
||||||
|
"static_creds": fmt.Sprintf(`{"username":"%s","password":"%s"}`, s.Username, s.Password),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
@ -14,8 +14,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/davidallendj/magellan/pkg/client"
|
"github.com/OpenCHAMI/magellan/pkg/client"
|
||||||
"github.com/davidallendj/magellan/pkg/crawler"
|
"github.com/OpenCHAMI/magellan/pkg/crawler"
|
||||||
|
"github.com/OpenCHAMI/magellan/pkg/secrets"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -126,12 +127,17 @@ func TestExpectedOutput(t *testing.T) {
|
||||||
t.Fatalf("failed while waiting for emulator: %v", err)
|
t.Fatalf("failed while waiting for emulator: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initialize a credential store
|
||||||
|
staticStore := &secrets.StaticStore{
|
||||||
|
Username: *username,
|
||||||
|
Password: *password,
|
||||||
|
}
|
||||||
|
|
||||||
systems, err := crawler.CrawlBMCForSystems(
|
systems, err := crawler.CrawlBMCForSystems(
|
||||||
crawler.CrawlerConfig{
|
crawler.CrawlerConfig{
|
||||||
URI: *host,
|
URI: *host,
|
||||||
Username: *username,
|
CredentialStore: staticStore,
|
||||||
Password: *password,
|
Insecure: true,
|
||||||
Insecure: true,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue