Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.age
*.txt
playground/*
.claude/
3 changes: 3 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ linters:
- linters:
- gocritic
source: //noinspection
- linters: [gocritic]
text: "exitAfterDefer: (.*)"
path: main\.go
- linters:
- lll
path: mocks\.go
Expand Down
133 changes: 115 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@
<a href="https://pkg.go.dev/github.com/flowexec/vault"><img src="https://pkg.go.dev/badge/github.com/flowexec/vault.svg" alt="Go Reference"></a>
</p>

A Go package for secure secret storage with multiple encryption backends. Made for [flow](https://github.com/jahvon/flow) but can be used independently.
A flexible Go library for secure secret management with multiple backend providers. Made for [flow](https://github.com/jahvon/flow) but can be used independently.

## Features

- **Multiple Provider Support**: Choose from local encrypted storage, system keyring, or external CLI tools
- **Pluggable Architecture**: Easy to extend with custom providers
- **Type Safety**: Strong typing for secrets with secure memory handling
- **Thread Safe**: Concurrent access protection with read/write mutexes
- **Comprehensive API**: Full CRUD operations plus metadata and existence checks

## Quick Start

Expand Down Expand Up @@ -45,36 +53,125 @@ func main() {
}
```

## Providers
## Provider Types

### Local Encrypted Providers

### AES256 Provider
#### AES256 Provider
Stores secrets in an AES-256 encrypted file with configurable key sources.

Symmetric encryption using AES-256. Best for when you want a single encryption key shared across users / systems.
```go
provider, _, err := vault.New("my-vault",
vault.WithProvider(vault.ProviderTypeAES256),
vault.WithAESPath("~/secrets.vault"),
)
```

**Key Generation:**
```go
key, err := vault.GenerateEncryptionKey()
if err != nil {
panic(err)
}
// Store this key securely and configure vault to use it
// Store this key securely (environment variable, HSM, etc.)
```

### Age Provider
#### Age Provider
Uses the [age encryption tool](https://age-encryption.org/) with public key cryptography.

Asymmetric encryption using the [age encryption format](https://github.com/FiloSottile/age). Best for when you may have multiple users or need the ability to add/remove recipients.
```go
provider, _, err := vault.New("my-vault",
vault.WithProvider(vault.ProviderTypeAge),
vault.WithAgePath("~/secrets.age"),
)
```

**Key Generation:**
```bash
# Generate age key pair - see https://github.com/FiloSottile/age for details
age-keygen -o key.txt
# Public key: age1ql3blv6a5y...
# Private key in key.txt
age-keygen -o ~/.age/identity.txt
# Add recipients to vault configuration
```

#### Keyring Provider
Integrates with the operating system's secure keyring.

```go
provider, _, err := vault.New("my-vault",
vault.WithProvider(vault.ProviderTypeKeyring),
vault.WithKeyringService("my-app-secrets"),
)
```

No additional setup required - uses OS authentication.

#### Unencrypted Provider
Stores secrets in plain text JSON files.

```go
provider, _, err := vault.New("my-vault",
vault.WithProvider(vault.ProviderTypeUnencrypted),
vault.WithUnencryptedPath("~/dev-secrets.json"),
)
```

## Encrypted Files
### External CLI Providers

Both vault types create a single encrypted file at the specified path:
#### External Provider
Integrates with any CLI tool for secret management. Supports popular tools like Bitwarden, 1Password, HashiCorp Vault, AWS SSM, and more.

- **AES256**: `vault-{id}.enc`
- **Age**: `vault-{id}.age`
```go
config := &vault.Config{
ID: "bitwarden",
Type: vault.ProviderTypeExternal,
External: &vault.ExternalConfig{
Get: vault.CommandConfig{
CommandTemplate: "bw get password {{key}}",
},
Set: vault.CommandConfig{
CommandTemplate: "bw create item --name {{key}} --password {{value}}",
},
// ... other operations
},
}

provider, err := vault.NewExternalVaultProvider(config)
```

**External Provider Examples**

Ready-to-use configurations for popular CLI tools are available in the [`examples/`](./examples/) directory:

- **[Bitwarden](./examples/providers/bitwarden.json)**
- **[1Password](./examples/providers/1password.json)**
- **[AWS SSM](./examples/providers/aws-ssm.json)**
- **[pass](./examples/providers/pass.json)**

See the [examples README](./examples/README.md) for detailed setup instructions.

## Usage

### Basic Operations

```go
// Store a secret
secret := vault.NewSecretValue([]byte("my-secret-value"))
err = provider.SetSecret("api-key", secret)

// Retrieve the secret
retrieved, err := provider.GetSecret("api-key")
fmt.Println("Secret:", retrieved.PlainTextString())

// List all secrets
secrets, _ := provider.ListSecrets()

// Check if secret exists
exists, _ := provider.HasSecret("api-key")

// Get vault metadata
metadata := provider.Metadata()
```

### Configuration from File

```go
// Load configuration from JSON
config, err := vault.LoadConfigJSON("vault-config.json")
provider, _, err := vault.New(config.ID, vault.WithProvider(config.Type))
```
95 changes: 73 additions & 22 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,26 @@ import (
"fmt"
"os"
"path/filepath"
"time"
)

type ProviderType string

const (
ProviderTypeAES256 ProviderType = "aes256"
ProviderTypeAge ProviderType = "age"
ProviderTypeExternal ProviderType = "external"
ProviderTypeAES256 ProviderType = "aes256"
ProviderTypeAge ProviderType = "age"
ProviderTypeExternal ProviderType = "external"
ProviderTypeKeyring ProviderType = "keyring"
ProviderTypeUnencrypted ProviderType = "unencrypted"
)

type Config struct {
ID string `json:"id"`
Type ProviderType `json:"type"`
Age *AgeConfig `json:"age,omitempty"`
Aes *AesConfig `json:"aes,omitempty"`
External *ExternalConfig `json:"external,omitempty"`
ID string `json:"id"`
Type ProviderType `json:"type"`
Age *AgeConfig `json:"age,omitempty"`
Aes *AesConfig `json:"aes,omitempty"`
External *ExternalConfig `json:"external,omitempty"`
Keyring *KeyringConfig `json:"keyring,omitempty"`
Unencrypted *UnencryptedConfig `json:"unencrypted,omitempty"`
}

func (c *Config) Validate() error {
Expand All @@ -45,6 +48,16 @@ func (c *Config) Validate() error {
return fmt.Errorf("%w: external configuration required for external vault", ErrInvalidConfig)
}
return c.External.Validate()
case ProviderTypeKeyring:
if c.Keyring == nil {
return fmt.Errorf("%w: keyring configuration required for keyring vault provider", ErrInvalidConfig)
}
return c.Keyring.Validate()
case ProviderTypeUnencrypted:
if c.Unencrypted == nil {
return fmt.Errorf("%w: unencrypted configuration required for unencrypted vault provider", ErrInvalidConfig)
}
return c.Unencrypted.Validate()
default:
return fmt.Errorf("%w: unsupported vault type: %s", ErrInvalidConfig, c.Type)
}
Expand Down Expand Up @@ -167,33 +180,71 @@ func (c *AesConfig) Validate() error {
return nil
}

// CommandSet defines the command templates for external vault operations
type CommandSet struct {
Get string `json:"get"`
Set string `json:"set"`
Delete string `json:"delete"`
List string `json:"list"`
Exists string `json:"exists,omitempty"`
// CommandConfig represents a command template to be executed with its arguments
type CommandConfig struct {
// CommandTemplate for building command arguments
CommandTemplate string `json:"cmd"`
// OutputTemplate for parsing command output
OutputTemplate string `json:"output,omitempty"`
// InputTemplate for providing input to the command
InputTemplate string `json:"input,omitempty"`
}

// ExternalConfig contains external (cli command-based) vault configuration
type ExternalConfig struct {
// Command templates for operations
Commands CommandSet `json:"commands"`
// Get CommandConfig for the get operation
Get CommandConfig `json:"get,omitempty"`
// Set CommandConfig for the set operation
Set CommandConfig `json:"set,omitempty"`
// Delete CommandConfig for the delete operation
Delete CommandConfig `json:"delete,omitempty"`
// List CommandConfig for the list operation
List CommandConfig `json:"list,omitempty"`
ListSeparator string `json:"separator,omitempty"`
// Exists CommandConfig for the exists operation
Exists CommandConfig `json:"exists,omitempty"`
// Metadata CommandConfig for the metadata operation
Metadata CommandConfig `json:"metadata,omitempty"`

// Environment variables for commands
Environment map[string]string `json:"environment,omitempty"`

// Timeout for command execution
Timeout time.Duration `json:"timeout,omitempty"`
// Timeout duration string for command execution
Timeout string `json:"timeout,omitempty"`

// WorkingDir for command execution
WorkingDir string `json:"working_dir,omitempty"`
}

func (c *ExternalConfig) Validate() error {
if c.Commands.Get == "" || c.Commands.Set == "" {
return fmt.Errorf("%w: get and set commands are required for external vault", ErrInvalidConfig)
if c.Get.CommandTemplate == "" || c.Set.CommandTemplate == "" {
return fmt.Errorf("%w: get and set args template required for external vault", ErrInvalidConfig)
}
return nil
}

// UnencryptedConfig contains unencrypted (plain text) vault configuration
type UnencryptedConfig struct {
// Storage location for the vault file
StoragePath string `json:"storage_path"`
}

func (c *UnencryptedConfig) Validate() error {
if c.StoragePath == "" {
return fmt.Errorf("%w: storage path is required for unencrypted vault", ErrInvalidConfig)
}
return nil
}

// KeyringConfig contains keyring vault configuration
type KeyringConfig struct {
// Service name used for keyring operations
Service string `json:"service"`
}

func (c *KeyringConfig) Validate() error {
if c.Service == "" {
return fmt.Errorf("%w: service name is required for keyring vault", ErrInvalidConfig)
}
return nil
}
Loading