keyring

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 28, 2025 License: MIT Imports: 8 Imported by: 0

README

OmniVault Keyring

Build Status Lint Status Go Report Card Docs License

Cross-platform OS credential store provider for OmniVault. Store and retrieve secrets securely using your operating system's native credential manager.

Features

  • Cross-Platform: Works on macOS, Windows, and Linux
  • Native Security: Uses OS-provided secure storage (not custom encryption)
  • Multi-Field Secrets: Store complex credentials with multiple fields (username, password, host, etc.)
  • OmniVault Compatible: Implements the vault.Vault interface for seamless integration
  • URI Resolution: Use keyring:// URIs with OmniVault's resolver
  • Listing Support: Enumerate stored secrets (via internal index)
  • Zero Configuration: Works out of the box with sensible defaults

Supported Platforms

Platform Backend Storage Location
macOS Keychain ~/Library/Keychains/login.keychain-db
Windows Credential Manager Windows Credential Locker
Linux Secret Service API GNOME Keyring or KWallet

Installation

go get github.com/agentplexus/omnivault-keyring
Linux Prerequisites

On Linux, you need a Secret Service implementation:

# Debian/Ubuntu (GNOME Keyring)
sudo apt-get install gnome-keyring libsecret-1-0

# Fedora
sudo dnf install gnome-keyring libsecret

# Arch Linux
sudo pacman -S gnome-keyring libsecret

# For headless servers, you may need to start the daemon:
eval $(gnome-keyring-daemon --start --components=secrets)
export GNOME_KEYRING_CONTROL

Quick Start

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/agentplexus/omnivault-keyring"
    "github.com/agentplexus/omnivault/vault"
)

func main() {
    ctx := context.Background()

    // Create a keyring provider
    kr := keyring.New(keyring.Config{
        ServiceName: "myapp",
    })

    // Store a secret
    err := kr.Set(ctx, "api-key", &vault.Secret{Value: "sk-12345"})
    if err != nil {
        log.Fatal(err)
    }

    // Retrieve the secret
    secret, err := kr.Get(ctx, "api-key")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("API Key:", secret.Value)

    // Clean up
    kr.Delete(ctx, "api-key")
}

Usage Examples

Basic Secret Storage
import (
    "github.com/agentplexus/omnivault-keyring"
    "github.com/agentplexus/omnivault/vault"
)

// Create provider with service name
kr := keyring.New(keyring.Config{
    ServiceName: "myapp",
})

// Or use the shorthand
kr := keyring.NewWithServiceName("myapp")

// Store a simple secret
kr.Set(ctx, "api-key", &vault.Secret{Value: "secret-value"})

// Retrieve
secret, err := kr.Get(ctx, "api-key")
if err != nil {
    if errors.Is(err, vault.ErrSecretNotFound) {
        // Handle missing secret
    }
}
fmt.Println(secret.Value)

// Check existence
exists, _ := kr.Exists(ctx, "api-key")

// Delete
kr.Delete(ctx, "api-key")
Multi-Field Secrets (JSON Format)

Store complex credentials with multiple fields like database connections, OAuth tokens, or API configurations:

// Enable JSON format for multi-field support
kr := keyring.New(keyring.Config{
    ServiceName: "myapp",
    JSONFormat:  true,
})

// Store database credentials
err := kr.Set(ctx, "database/production", &vault.Secret{
    Value: "super-secret-password",  // Primary value (the password)
    Fields: map[string]string{
        "username": "db_admin",
        "host":     "db.example.com",
        "port":     "5432",
        "database": "myapp_prod",
        "sslmode":  "require",
    },
    Metadata: vault.Metadata{
        Tags: map[string]string{
            "environment": "production",
            "team":        "backend",
        },
    },
})

// Retrieve and access fields
secret, _ := kr.Get(ctx, "database/production")
fmt.Printf("Host: %s\n", secret.Fields["host"])
fmt.Printf("Port: %s\n", secret.Fields["port"])
fmt.Printf("User: %s\n", secret.Fields["username"])
fmt.Printf("Pass: %s\n", secret.Value)

// Build a connection string
connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s",
    secret.Fields["username"],
    secret.Value,
    secret.Fields["host"],
    secret.Fields["port"],
    secret.Fields["database"],
    secret.Fields["sslmode"],
)
OAuth Token Storage
kr := keyring.New(keyring.Config{
    ServiceName: "myapp",
    JSONFormat:  true,
})

// Store OAuth tokens
kr.Set(ctx, "oauth/github", &vault.Secret{
    Value: "gho_xxxxxxxxxxxx",  // Access token
    Fields: map[string]string{
        "refresh_token": "ghr_xxxxxxxxxxxx",
        "token_type":    "bearer",
        "scope":         "repo,user",
        "expires_at":    "2024-12-31T23:59:59Z",
    },
})

// Retrieve tokens
secret, _ := kr.Get(ctx, "oauth/github")
accessToken := secret.Value
refreshToken := secret.Fields["refresh_token"]
Integration with OmniVault Client

Use keyring as a backend for the OmniVault client:

import (
    "github.com/agentplexus/omnivault"
    "github.com/agentplexus/omnivault-keyring"
)

// Create keyring provider
kr := keyring.New(keyring.Config{
    ServiceName: "myapp",
    JSONFormat:  true,
})

// Create OmniVault client with keyring backend
client, err := omnivault.NewClient(omnivault.Config{
    CustomVault: kr,
})
if err != nil {
    log.Fatal(err)
}
defer client.Close()

// Use the standard OmniVault API
client.SetValue(ctx, "api/token", "secret-token")
token, _ := client.GetValue(ctx, "api/token")

// Get specific field from a multi-field secret
username, _ := client.GetField(ctx, "database/prod", "username")
Multi-Provider Setup with Resolver

Combine keyring with other providers using OmniVault's resolver:

import (
    "github.com/agentplexus/omnivault"
    "github.com/agentplexus/omnivault-keyring"
    "github.com/agentplexus/omnivault/providers/env"
    "github.com/agentplexus/omnivault/providers/file"
)

// Create resolver
resolver := omnivault.NewResolver()

// Register multiple providers
resolver.Register("keyring", keyring.New(keyring.Config{
    ServiceName: "myapp",
}))
resolver.Register("env", env.New())

fileVault, _ := file.New(file.Config{
    Directory: "/etc/myapp/secrets",
})
resolver.Register("file", fileVault)

// Resolve secrets from different sources using URIs
dbPassword, _ := resolver.Resolve(ctx, "keyring://database/password")
apiKey, _ := resolver.Resolve(ctx, "env://API_KEY")
certData, _ := resolver.Resolve(ctx, "file://tls/cert.pem")

// Resolve conditionally - returns value as-is if not a secret URI
config := map[string]string{
    "database_url":  "keyring://database/url",
    "api_key":       "env://API_KEY",
    "log_level":     "info",  // Not a secret URI, returned as-is
}
resolved, _ := resolver.ResolveMap(ctx, config)
Listing and Enumerating Secrets
kr := keyring.New(keyring.Config{
    ServiceName: "myapp",
})

// Store some secrets
kr.Set(ctx, "database/prod", &vault.Secret{Value: "pass1"})
kr.Set(ctx, "database/staging", &vault.Secret{Value: "pass2"})
kr.Set(ctx, "api/github", &vault.Secret{Value: "token1"})
kr.Set(ctx, "api/stripe", &vault.Secret{Value: "token2"})

// List all secrets
all, _ := kr.List(ctx, "")
// Returns: ["database/prod", "database/staging", "api/github", "api/stripe"]

// List secrets by prefix
dbSecrets, _ := kr.List(ctx, "database/")
// Returns: ["database/prod", "database/staging"]

apiSecrets, _ := kr.List(ctx, "api/")
// Returns: ["api/github", "api/stripe"]
Application Configuration Pattern

A common pattern for application secrets:

package config

import (
    "context"
    "os"

    "github.com/agentplexus/omnivault"
    "github.com/agentplexus/omnivault-keyring"
    "github.com/agentplexus/omnivault/providers/env"
)

type Config struct {
    DatabaseURL string
    APIKey      string
    JWTSecret   string
}

func LoadConfig(ctx context.Context) (*Config, error) {
    // Create resolver with keyring for secrets, env for non-sensitive config
    resolver := omnivault.NewResolver()
    resolver.Register("keyring", keyring.NewWithServiceName("myapp"))
    resolver.Register("env", env.New())

    // In development, secrets might be in env vars
    // In production, they're in the system keyring
    var secretScheme string
    if os.Getenv("ENV") == "production" {
        secretScheme = "keyring"
    } else {
        secretScheme = "env"
    }

    dbURL, err := resolver.Resolve(ctx, secretScheme+"://DATABASE_URL")
    if err != nil {
        return nil, err
    }

    apiKey, err := resolver.Resolve(ctx, secretScheme+"://API_KEY")
    if err != nil {
        return nil, err
    }

    jwtSecret, err := resolver.Resolve(ctx, secretScheme+"://JWT_SECRET")
    if err != nil {
        return nil, err
    }

    return &Config{
        DatabaseURL: dbURL,
        APIKey:      apiKey,
        JWTSecret:   jwtSecret,
    }, nil
}

Configuration Reference

type Config struct {
    // ServiceName is the service/application name used to namespace secrets.
    //
    // On macOS: Appears as the "Where" field in Keychain Access
    // On Windows: Part of the target name in Credential Manager
    // On Linux: The "application" attribute in Secret Service
    //
    // Default: "omnivault"
    // Recommended: Use your application name (e.g., "myapp", "acme-api")
    ServiceName string

    // JSONFormat enables storing secrets as JSON objects.
    //
    // When true:
    //   - Secrets are serialized as JSON
    //   - Multi-field secrets (Fields map) are supported
    //   - Metadata is preserved
    //
    // When false:
    //   - Only the Value field is stored as plain text
    //   - More compatible with external tools reading the keyring
    //
    // Default: false
    JSONFormat bool
}

API Reference

Constructor Functions
// New creates a provider with full configuration
func New(config Config) *Provider

// NewWithServiceName creates a provider with just a service name
func NewWithServiceName(serviceName string) *Provider
Vault Interface Methods
// Get retrieves a secret by path
func (p *Provider) Get(ctx context.Context, path string) (*vault.Secret, error)

// Set stores a secret at the given path
func (p *Provider) Set(ctx context.Context, path string, secret *vault.Secret) error

// Delete removes a secret
func (p *Provider) Delete(ctx context.Context, path string) error

// Exists checks if a secret exists
func (p *Provider) Exists(ctx context.Context, path string) (bool, error)

// List returns all paths matching the prefix
func (p *Provider) List(ctx context.Context, prefix string) ([]string, error)

// Name returns "keyring"
func (p *Provider) Name() string

// Capabilities returns supported operations
func (p *Provider) Capabilities() vault.Capabilities

// Close releases resources
func (p *Provider) Close() error
Provider-Specific Methods
// ServiceName returns the configured service name
func (p *Provider) ServiceName() string

// Backend returns the OS backend name
// Returns: "macOS Keychain", "Windows Credential Manager",
//          or "Secret Service (GNOME Keyring/KWallet)"
func (p *Provider) Backend() string

URI Scheme

When using with OmniVault's resolver, use the keyring:// scheme:

keyring://path/to/secret[#field]

Examples:
  keyring://api-key                    → Get api-key secret
  keyring://database/production        → Get database/production secret
  keyring://oauth/github#refresh_token → Get refresh_token field from oauth/github

Platform-Specific Notes

macOS

Viewing Secrets:

  1. Open Keychain Access (/Applications/Utilities/Keychain Access.app)
  2. Select login keychain in the sidebar
  3. Search for your service name
  4. Double-click an entry and check "Show password"

Security:

  • First access prompts for keychain password
  • Apps may request "Always Allow" access
  • Secrets are encrypted with the user's login password

Code Signing:

  • Unsigned apps may trigger additional security prompts
  • For production, sign your application with a Developer ID
Windows

Viewing Secrets:

  1. Open Control PanelUser AccountsCredential Manager
  2. Click Windows Credentials
  3. Look for entries starting with your service name

Security:

  • Protected by Windows user account
  • Accessible only when logged in as that user
  • Encrypted using DPAPI
Linux

Viewing Secrets (GNOME):

  1. Open Seahorse (Passwords and Keys)
  2. Look under Login keyring
  3. Search for your service name

Viewing Secrets (KDE):

  1. Open KWalletManager
  2. Look for entries under your service name

Headless Servers:

# Start the keyring daemon
eval $(gnome-keyring-daemon --start --components=secrets)
export GNOME_KEYRING_CONTROL

# Or use dbus-run-session for scripts
dbus-run-session -- your-app

Security:

  • Secrets encrypted with login password
  • Keyring unlocked automatically on login (usually)
  • May require manual unlock on headless systems

Error Handling

import (
    "errors"
    "github.com/agentplexus/omnivault/vault"
)

secret, err := kr.Get(ctx, "my-secret")
if err != nil {
    switch {
    case errors.Is(err, vault.ErrSecretNotFound):
        // Secret doesn't exist
        log.Println("Secret not found, using default")

    case errors.Is(err, vault.ErrAccessDenied):
        // Permission denied (e.g., keychain locked)
        log.Println("Access denied - is the keychain unlocked?")

    case errors.Is(err, vault.ErrClosed):
        // Provider was closed
        log.Println("Provider is closed")

    default:
        // Other error (network, daemon not running, etc.)
        log.Printf("Error accessing keyring: %v", err)
    }
}

Limitations

  1. No Native Enumeration: OS keyrings don't support listing all entries. This provider maintains an internal index to enable List(), stored as a special keyring entry.

  2. Service Name Scope: Secrets are namespaced by service name. Different service names create separate "buckets" of secrets.

  3. String Values Only: OS keyrings store strings. Binary data should be base64-encoded.

  4. User Scope: Secrets are tied to the current user account and cannot be shared across users.

  5. Size Limits:

    • macOS: ~16KB per secret
    • Windows: ~2.5KB per credential
    • Linux: Varies by implementation

Security Considerations

  • Don't log secrets: Never log secret values, even in debug mode
  • Clear memory: Go doesn't guarantee memory clearing, but avoid keeping secrets in memory longer than needed
  • Service name: Use a unique service name to avoid conflicts with other applications
  • Access control: On shared systems, be aware that other processes running as the same user can access the keyring

Troubleshooting

"secret not found" on Linux

Ensure the Secret Service daemon is running:

# Check if running
ps aux | grep gnome-keyring

# Start it
gnome-keyring-daemon --start --components=secrets
Keychain prompts on every access (macOS)

Your application may need to be code-signed, or you need to grant "Always Allow" access in the Keychain Access prompt.

"access denied" errors
  • macOS: Keychain may be locked. Unlock via Keychain Access or security unlock-keychain
  • Linux: Keyring may require manual unlock. Check Seahorse or KWalletManager
  • Windows: Ensure you're running as the user who created the credentials

Contributing

Contributions are welcome! Please submit pull requests or create issues for bugs and feature requests.

License

MIT License - see LICENSE for details.

Documentation

Overview

Package keyring provides a cross-platform vault implementation using OS credential stores.

Supported backends:

  • macOS: Keychain
  • Windows: Credential Manager
  • Linux: Secret Service (GNOME Keyring, KWallet)

Usage:

v := keyring.New(keyring.Config{
    ServiceName: "myapp",
})
err := v.Set(ctx, "api-key", &vault.Secret{Value: "secret123"})
secret, err := v.Get(ctx, "api-key")

With omnivault client:

client, _ := omnivault.NewClient(omnivault.Config{
    CustomVault: keyring.New(keyring.Config{ServiceName: "myapp"}),
})

With resolver:

resolver := omnivault.NewResolver()
resolver.Register("keyring", keyring.New(keyring.Config{ServiceName: "myapp"}))
value, _ := resolver.Resolve(ctx, "keyring://api-key")

Index

Constants

View Source
const (
	// DefaultServiceName is the default service name used if none is provided.
	DefaultServiceName = "omnivault"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// ServiceName is the service/application name used to namespace secrets.
	// On macOS, this appears as the "Where" field in Keychain Access.
	// On Windows, this is part of the target name.
	// Default: "omnivault"
	ServiceName string

	// JSONFormat stores secrets as JSON with metadata support.
	// When false, only simple string values are stored.
	// Default: false
	JSONFormat bool

	// OnIndexError is called when an error occurs during index operations.
	// Index operations are used to track stored keys for List() functionality.
	// These errors are non-fatal (Get/Set/Delete still work) but may cause
	// List() to return incomplete results.
	// If nil, index errors are silently ignored.
	OnIndexError func(op string, err error)
}

Config holds configuration for the keyring provider.

type Provider

type Provider struct {
	// contains filtered or unexported fields
}

Provider implements vault.Vault using OS credential stores.

func New

func New(config Config) *Provider

New creates a new keyring provider with the given configuration.

func NewWithServiceName

func NewWithServiceName(serviceName string) *Provider

NewWithServiceName creates a new keyring provider with the specified service name.

func (*Provider) Backend

func (p *Provider) Backend() string

Backend returns the name of the OS keyring backend being used.

func (*Provider) Capabilities

func (p *Provider) Capabilities() vault.Capabilities

Capabilities returns the provider capabilities.

func (*Provider) Close

func (p *Provider) Close() error

Close marks the provider as closed.

func (*Provider) Delete

func (p *Provider) Delete(ctx context.Context, path string) error

Delete removes a secret from the OS keyring.

func (*Provider) Exists

func (p *Provider) Exists(ctx context.Context, path string) (bool, error)

Exists checks if a secret exists in the OS keyring.

func (*Provider) Get

func (p *Provider) Get(ctx context.Context, path string) (*vault.Secret, error)

Get retrieves a secret from the OS keyring.

func (*Provider) List

func (p *Provider) List(ctx context.Context, prefix string) ([]string, error)

List returns all secret paths matching the prefix. Note: OS keyrings don't natively support enumeration, so this relies on an internal index that tracks stored keys.

func (*Provider) Name

func (p *Provider) Name() string

Name returns the provider name.

func (*Provider) ServiceName

func (p *Provider) ServiceName() string

ServiceName returns the configured service name.

func (*Provider) Set

func (p *Provider) Set(ctx context.Context, path string, secret *vault.Secret) error

Set stores a secret in the OS keyring.

Directories

Path Synopsis
Example usage of omnivault-keyring
Example usage of omnivault-keyring

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL