Skip to content
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
179 changes: 179 additions & 0 deletions infisical/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package infisical

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"sync"
"time"
)

const tokenExpiryBuffer = 5 * time.Second

type kmsEncryptDecrypter interface {
encrypt(plaintext string) (string, error)
decrypt(ciphertext string) (string, error)
}

type kmsClient struct {
httpClient *http.Client
baseURL string
kmsKeyID string
clientID string
clientSecret string

mu sync.RWMutex
token string
expiresAt time.Time
}

func newKmsClient(siteURL, kmsKeyID, clientID, clientSecret string) *kmsClient {
base := strings.TrimRight(siteURL, "/")
if !strings.HasSuffix(base, "/api") {
base += "/api"
}
return &kmsClient{
httpClient: &http.Client{Timeout: 30 * time.Second},
baseURL: base,
kmsKeyID: kmsKeyID,
clientID: clientID,
clientSecret: clientSecret,
}
}

type loginRequest struct {
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
}

type loginResponse struct {
AccessToken string `json:"accessToken"`
ExpiresIn int64 `json:"expiresIn"`
}

func (c *kmsClient) login() error {
body, err := json.Marshal(loginRequest{
ClientID: c.clientID,
ClientSecret: c.clientSecret,
})
if err != nil {
return fmt.Errorf("infisical-kms: failed to marshal login request: %w", err)
}

resp, err := c.httpClient.Post(
c.baseURL+"/v1/auth/universal-auth/login",
"application/json",
bytes.NewReader(body),
)
if err != nil {
return fmt.Errorf("infisical-kms: login request failed: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
msg, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("infisical-kms: login returned %d: %s", resp.StatusCode, msg)
}

var result loginResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("infisical-kms: failed to decode login response: %w", err)
}

c.mu.Lock()
c.token = result.AccessToken
c.expiresAt = time.Now().Add(time.Duration(result.ExpiresIn)*time.Second - tokenExpiryBuffer)
c.mu.Unlock()

return nil
}

func (c *kmsClient) ensureToken() error {
c.mu.RLock()
valid := c.token != "" && time.Now().Before(c.expiresAt)
c.mu.RUnlock()
if valid {
return nil
}
return c.login()
}

func (c *kmsClient) doKmsRequest(path string, reqBody, respBody interface{}) error {
if err := c.ensureToken(); err != nil {
return err
}

body, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("infisical-kms: failed to marshal request: %w", err)
}

req, err := http.NewRequest(http.MethodPost, c.baseURL+path, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("infisical-kms: failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")

c.mu.RLock()
req.Header.Set("Authorization", "Bearer "+c.token)
c.mu.RUnlock()

resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("infisical-kms: request failed: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
msg, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("infisical-kms: request returned %d: %s", resp.StatusCode, msg)
}

if err := json.NewDecoder(resp.Body).Decode(respBody); err != nil {
return fmt.Errorf("infisical-kms: failed to decode response: %w", err)
}
return nil
}

type encryptRequest struct {
Plaintext string `json:"plaintext"`
}

type encryptResponse struct {
Ciphertext string `json:"ciphertext"`
}

func (c *kmsClient) encrypt(plaintext string) (string, error) {
var resp encryptResponse
path := fmt.Sprintf("/v1/kms/keys/%s/encrypt", c.kmsKeyID)
encoded := base64.StdEncoding.EncodeToString([]byte(plaintext))
if err := c.doKmsRequest(path, encryptRequest{Plaintext: encoded}, &resp); err != nil {
return "", err
}
return resp.Ciphertext, nil
}

type decryptRequest struct {
Ciphertext string `json:"ciphertext"`
}

type decryptResponse struct {
Plaintext string `json:"plaintext"`
}

func (c *kmsClient) decrypt(ciphertext string) (string, error) {
var resp decryptResponse
path := fmt.Sprintf("/v1/kms/keys/%s/decrypt", c.kmsKeyID)
if err := c.doKmsRequest(path, decryptRequest{Ciphertext: ciphertext}, &resp); err != nil {
return "", err
}
decoded, err := base64.StdEncoding.DecodeString(resp.Plaintext)
if err != nil {
return "", fmt.Errorf("infisical-kms: failed to base64-decode plaintext: %w", err)
}
return string(decoded), nil
}
216 changes: 216 additions & 0 deletions infisical/infisical_kms.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package infisical

import (
"encoding/json"
"errors"
"fmt"
"os"

"github.com/libopenstorage/secrets"
"github.com/libopenstorage/secrets/pkg/store"
"github.com/portworx/kvdb"
"github.com/sirupsen/logrus"
)

const (
// Name of the secret store
Name = secrets.TypeInfisical
// SiteURLKey is the base URL of the Infisical instance
SiteURLKey = "INFISICAL_SITE_URL"
// ClientIDKey is the Infisical Universal Auth machine identity client ID
ClientIDKey = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"
// ClientSecretKey is the Infisical Universal Auth machine identity client secret
ClientSecretKey = "INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET"
// KMSKeyIDKey is the ID of the Infisical KMS key used for encrypt/decrypt
KMSKeyIDKey = "INFISICAL_KMS_KEY_ID"
// KvdbKey is used to setup Infisical KMS with kvdb for persistence
KvdbKey = "KMS_KVDB"
defaultSiteURL = "https://app.infisical.com"
kvdbPublicBasePath = "infisical/secrets/public/"
kvdbDataBasePath = "infisical/secrets/data/"
)

var (
// ErrKvdbNotProvided is returned when a valid kvdb instance is not provided
ErrKvdbNotProvided = errors.New("a valid kvdb.Kvdb instance must be provided via the KMS_KVDB config key")
// ErrClientIDRequired is returned when INFISICAL_UNIVERSAL_AUTH_CLIENT_ID is not set
ErrClientIDRequired = errors.New("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID is required (config key or env var)")
// ErrClientSecretRequired is returned when INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET is not set
ErrClientSecretRequired = errors.New("INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET is required (config key or env var)")
// ErrKMSKeyIDRequired is returned when INFISICAL_KMS_KEY_ID is not set
ErrKMSKeyIDRequired = errors.New("INFISICAL_KMS_KEY_ID is required (config key or env var)")
)

type infisicalKms struct {
client kmsEncryptDecrypter
ps store.PersistenceStore
}

func New(
secretConfig map[string]interface{},
) (secrets.Secrets, error) {
v, ok := secretConfig[KvdbKey]
if !ok {
return nil, ErrKvdbNotProvided
}
kv, ok := v.(kvdb.Kvdb)
if !ok {
return nil, ErrKvdbNotProvided
}
ps := store.NewKvdbPersistenceStore(kv, kvdbPublicBasePath, kvdbDataBasePath)

siteURL := configString(secretConfig, SiteURLKey, defaultSiteURL)
clientID := configString(secretConfig, ClientIDKey, "")
clientSecret := configString(secretConfig, ClientSecretKey, "")
kmsKeyID := configString(secretConfig, KMSKeyIDKey, "")

if clientID == "" {
return nil, ErrClientIDRequired
}
if clientSecret == "" {
return nil, ErrClientSecretRequired
}
if kmsKeyID == "" {
return nil, ErrKMSKeyIDRequired
}

client := newKmsClient(siteURL, kmsKeyID, clientID, clientSecret)
if err := client.login(); err != nil {
return nil, fmt.Errorf("infisical-kms: authentication failed: %w", err)
}

logrus.WithFields(logrus.Fields{
"site": siteURL,
"kmsKeyID": kmsKeyID,
}).Info("infisical-kms: authenticated successfully")

return &infisicalKms{
client: client,
ps: ps,
}, nil
}

func (k *infisicalKms) String() string {
return Name
}

func (k *infisicalKms) GetSecret(
secretId string,
keyContext map[string]string,
) (map[string]interface{}, secrets.Version, error) {
if secretId == "" {
return nil, secrets.NoVersion, secrets.ErrEmptySecretId
}

exists, err := k.ps.Exists(secretId)
if err != nil {
return nil, secrets.NoVersion, err
}
if !exists {
return nil, secrets.NoVersion, secrets.ErrInvalidSecretId
}

ciphertextBytes, err := k.ps.GetPublic(secretId)
if err != nil {
return nil, secrets.NoVersion, err
}

plaintext, err := k.client.decrypt(string(ciphertextBytes))
if err != nil {
return nil, secrets.NoVersion, fmt.Errorf("infisical-kms: decryption failed: %w", err)
}

result := make(map[string]interface{})
if err := json.Unmarshal([]byte(plaintext), &result); err != nil {
return nil, secrets.NoVersion, fmt.Errorf("infisical-kms: failed to unmarshal decrypted data: %w", err)
}

return result, secrets.NoVersion, nil
}

func (k *infisicalKms) PutSecret(
secretId string,
plainText map[string]interface{},
keyContext map[string]string,
) (secrets.Version, error) {
if secretId == "" {
return secrets.NoVersion, secrets.ErrEmptySecretId
}
if len(plainText) == 0 {
return secrets.NoVersion, secrets.ErrEmptySecretData
}

_, override := keyContext[secrets.OverwriteSecretDataInStore]

jsonBytes, err := json.Marshal(plainText)
if err != nil {
return secrets.NoVersion, fmt.Errorf("infisical-kms: failed to marshal secret data: %w", err)
}

ciphertext, err := k.client.encrypt(string(jsonBytes))
if err != nil {
return secrets.NoVersion, fmt.Errorf("infisical-kms: encryption failed: %w", err)
}

return secrets.NoVersion, k.ps.Set(secretId, []byte(ciphertext), nil, nil, override)
}

func (k *infisicalKms) DeleteSecret(
secretId string,
keyContext map[string]string,
) error {
if secretId == "" {
return secrets.ErrEmptySecretId
}
return k.ps.Delete(secretId)
}

func (k *infisicalKms) ListSecrets() ([]string, error) {
return k.ps.List()
}

func (k *infisicalKms) Encrypt(
secretId string,
plaintTextData string,
keyContext map[string]string,
) (string, error) {
return "", secrets.ErrNotSupported
}

func (k *infisicalKms) Decrypt(
secretId string,
encryptedData string,
keyContext map[string]string,
) (string, error) {
return "", secrets.ErrNotSupported
}

func (k *infisicalKms) Rencrypt(
originalSecretId string,
newSecretId string,
originalKeyContext map[string]string,
newKeyContext map[string]string,
encryptedData string,
) (string, error) {
return "", secrets.ErrNotSupported
}

func configString(config map[string]interface{}, key, fallback string) string {
if config != nil {
if v, ok := config[key]; ok {
if s, ok := v.(string); ok && s != "" {
return s
}
}
}
if env := os.Getenv(key); env != "" {
return env
}
return fallback
}

func init() {
if err := secrets.Register(Name, New); err != nil {
panic(err.Error())
}
}
Loading