A production-oriented Go package for API key generation, parsing, hashing, verification, and storage abstraction.
gobekli_api provides a complete, secure, and reusable solution for generating and managing API keys in Go applications. It follows security best practices and is designed to be framework-agnostic, making it suitable for any Go service that needs to generate, store, and verify API keys.
The package is part of the Package Foundry ecosystem, designed for modular, production-ready Go libraries.
- Features
- Installation
- Quick Start
- Key Format
- Complete Usage Guide
- API Reference
- Complete Examples
- Why Base58?
- Security Considerations
- Design Decisions
- Testing
- License
- Cryptographically Secure: Uses
crypto/randfor secure random number generation - Base58 Encoding: User-friendly encoding without ambiguous characters (0, O, I, l)
- SHA-256 Hashing: Secure one-way hashing for persistent storage
- Constant-Time Verification: Timing-safe comparison to prevent timing attacks
- Storage Abstraction: Pluggable
Storeinterface for any database backend - Zero External Dependencies: Pure Go standard library
- Well-Tested: Comprehensive unit test coverage
go get github.com/package-foundry/gobekli_apiTo use the package in your Go code:
import (
"github.com/package-foundry/gobekli_api"
)
// Most code uses the alias "apikey" for brevityVerify the installation by building and testing:
go build ./...
go test -v ./...For detailed installation instructions, see Installation Guide.
The fastest way to get started with gobekli_api:
package main
import (
"fmt"
"log"
"github.com/package-foundry/gobekli_api"
)
func main() {
// Step 1: Create a generator with your prefix and desired key size
gen, err := apikey.NewGenerator("sk_live", 32)
if err != nil {
log.Fatal(err) // Handle error: invalid prefix or key size
}
// Step 2: Generate a new API key
key, err := gen.Generate()
if err != nil {
log.Fatal(err) // Handle error: key generation failed
}
// Step 3: Hash the key for storage (NEVER store plaintext!)
hash := apikey.HashKey(key)
// Print results
fmt.Println("Your API key:", key)
fmt.Printf("Hash (hex): %x\n", hash)
}Output:
Your API key: sk_live_4Er7pT9kQ2mX8vN6aBcD3
Hash (hex): a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd
IMPORTANT: Show the API key to the user only once at creation time! After that, only store the hash.
Keys are generated with the following structure:
<prefix>_<base58_random_part>
- Prefix: Identifies the key type (e.g.,
sk_live,sk_test,pk_live) - Separator: Underscore character (
_) - Random Part: Cryptographically secure random bytes encoded in Base58
sk_live_4Er7pT9kQ2mX8vN6aBcD3
sk_test_9Xf2qR8sT5uV1wY3zA7bC6d
pk_live_Nk4mL9pQ2rS6tU8vW0xY3z
pk_test_Qr5sT9uV2wX4yZ7aB0cD3e
| Prefix | Type | Environment |
|---|---|---|
sk_live |
Secret Key | Production |
sk_test |
Secret Key | Testing |
pk_live |
Public Key | Production |
pk_test |
Public Key | Testing |
You can also use custom prefixes like service_, internal_, partner_, etc.
The Generator type creates cryptographically secure API keys. Each call to Generate() produces a unique key.
// Create a generator for production secret keys with 32-character total length
gen, err := apikey.NewGenerator("sk_live", 32)
if err != nil {
return fmt.Errorf("failed to create generator: %w", err)
}// Create a generator for testing keys with 64-character total length
gen, err := apikey.NewGenerator("sk_test", 64)
if err != nil {
return fmt.Errorf("failed to create generator: %w", err)
}// Create a generator for public keys
gen, err := apikey.NewGenerator("pk_live", 32)
if err != nil {
return fmt.Errorf("failed to create generator: %w", err)
}// Use custom prefixes for different services
gen, err := apikey.NewGenerator("myservice_api_key", 32)
if err != nil {
return fmt.Errorf("failed to create generator: %w", err)
}// Set your own key size
gen, err := apikey.NewGenerator("sk_live", 48) // 48 characters
if err != nil {
return err
}
// Or use the default key size
gen, err := apikey.NewGenerator("sk_live", apikey.DefaultKeySize()) // 32
if err != nil {
return err
}// Access generator properties
fmt.Println("Prefix:", gen.Prefix()) // "sk_live"
fmt.Println("Key Size:", gen.KeySize()) // 32// Generate a single key
key, err := gen.Generate()
if err != nil {
return fmt.Errorf("failed to generate key: %w", err)
}
fmt.Println("Generated key:", key)
// Each call produces a unique key
for i := 0; i < 5; i++ {
key, _ := gen.Generate()
fmt.Printf("Key %d: %s\n", i+1, key)
}// Example output:
// Key 1: sk_live_4Er7pT9kQ2mX8vN6aBcD3
// Key 2: sk_live_9Xf2qR8sT5uV1wY3zA7
// Key 3: sk_live_B8cD3eF6gH9iJ0kL2mN4
// Key 4: sk_live_O5pQ7rS9tU1vW3xY5zA8b
// Key 5: sk_live_C0dE2fG4hJ6kM8nO0pQ2The generator can return errors for invalid configuration:
// Empty prefix
_, err := apikey.NewGenerator("", 32)
if errors.Is(err, apikey.ErrPrefixEmpty) {
fmt.Println("Prefix cannot be empty")
}
// Prefix with whitespace
_, err := apikey.NewGenerator("sk live", 32)
if errors.Is(err, apikey.ErrPrefixHasSpace) {
fmt.Println("Prefix cannot contain whitespace")
}
// Key size too small
_, err := apikey.NewGenerator("sk_live", 5)
if errors.Is(err, apikey.ErrInvalidKeySize) {
fmt.Println("Key size is too small")
}
// Prefix too long for key size
_, err := apikey.NewGenerator("sk_live", 9)
if errors.Is(err, apikey.ErrPrefixTooLong) {
fmt.Println("Prefix is too long for the specified key size")
}CRITICAL SECURITY NOTE: Never store plaintext API keys. Always hash them before persistence.
// Generate a key
gen, _ := apikey.NewGenerator("sk_live", 32)
key, _ := gen.Generate()
// Hash the key - returns 32 bytes (SHA-256)
hash := apikey.HashKey(key)
fmt.Printf("Hash bytes: %d\n", len(hash)) // 32
// Hash as hex string - returns 64 characters
hashHex := apikey.HashKeyHex(key)
fmt.Printf("Hash hex: %s\n", hashHex) // 64 character hex string
// Fingerprint for display - returns first 8 bytes as hex (16 chars)
fingerprint := apikey.Fingerprint(key)
fmt.Printf("Fingerprint: %s\n", fingerprint) // 16 character hex
// Also available as bytes
fpBytes := apikey.FingerprintBytes(key)
fmt.Printf("Fingerprint bytes: %d\n", len(fpBytes)) // 8// Example output:
// Key: sk_live_4Er7pT9kQ2mX8vN6aBcD3
// Hash bytes: 32
// Hash hex: a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd
// Fingerprint: a1b2c3d4e5f60718
// Fingerprint bytes: 8// Hash two keys
hash1 := apikey.HashKey(key1)
hash2 := apikey.HashKey(key2)
// Compare hashes directly (not constant-time)
if string(hash1) == string(hash2) {
fmt.Println("Same key")
}
// Or compare hex strings
if apikey.HashKeyHex(key1) == apikey.HashKeyHex(key2) {
fmt.Println("Same key")
}Validate the format of incoming keys before processing.
// Validate a key's format, prefix, and size
incomingKey := "sk_live_4Er7pT9kQ2mX8vN6aBcD3"
err := apikey.ValidateKey(incomingKey, "sk_live", 32)
if err != nil {
return fmt.Errorf("invalid key: %w", err)
}
// This function checks:
// - Key is not empty
// - Has the correct prefix
// - Has the separator
// - Has the correct length (if expectedSize > 0)// Validation with size check
err := apikey.ValidateKey("sk_live_4Er7pT9kQ2mX8vN6aBcD3", "sk_live", 32)
if err != nil {
fmt.Println("Invalid:", err)
}
// Validation without size check (0 allows any length)
err := apikey.ValidateKey("sk_live_4Er7pT9kQ2mX8vN6aBcD3", "sk_live", 0)
if err != nil {
fmt.Println("Invalid:", err)
}// Validation error examples
err := apikey.ValidateKey("", "sk_live", 32)
fmt.Println(err) // invalid API key
err := apikey.ValidateKey("wrong_4Er7pT9kQ2mX8vN6aBcD3", "sk_live", 32)
fmt.Println(err) // invalid prefix
err := apikey.ValidateKey("sk_test_Ab", "sk_live", 32)
fmt.Println(err) // key is too short
err := apikey.ValidateKey("sklive4Er7pT9kQ2mX8vN6aBcD3", "sk_live", 32)
fmt.Println(err) // missing separator// Extract the prefix from a key
key := "sk_live_4Er7pT9kQ2mX8vN6aBcD3"
prefix, err := apikey.ExtractPrefix(key)
if err != nil {
return err
}
fmt.Println("Prefix:", prefix) // "sk_live"// Extract the random payload from a key
key := "sk_live_4Er7pT9kQ2mX8vN6aBcD3"
payload, err := apikey.ExtractPayload(key)
if err != nil {
return err
}
fmt.Println("Payload:", payload) // "4Er7pT9kQ2mX8vN6aBcD3"// Check if key has one of several valid prefixes
key := "sk_live_4Er7pT9kQ2mX8vN6aBcD3"
validPrefixes := []string{"sk_live", "sk_test", "pk_live", "pk_test"}
if apikey.HasValidPrefix(key, validPrefixes) {
fmt.Println("Valid prefix")
} else {
fmt.Println("Unknown prefix")
}Verify a presented key against a stored hash using constant-time comparison.
// The key that will be verified
presentedKey := "sk_live_4Er7pT9kQ2mX8vN6aBcD3"
// The hash stored in the database (from when the key was created)
storedHash := apikey.HashKey(presentedKey)
// Verify the key - uses constant-time comparison
valid := apikey.VerifyKey(presentedKey, storedHash)
if valid {
fmt.Println("Key is valid")
} else {
fmt.Println("Key is invalid")
}// Verification returns false for invalid keys
invalidKey := "sk_live_wrongKey123456789"
valid := apikey.VerifyKey(invalidKey, storedHash)
fmt.Println("Valid:", valid) // false// Verification with hex hashes
presentedKey := "sk_live_4Er7pT9kQ2mX8vN6aBcD3"
storedHashHex := apikey.HashKeyHex(presentedKey)
valid := apikey.VerifyKeyHex(presentedKey, storedHashHex)
if valid {
fmt.Println("Key is valid")
}// Complete verification example
func verifyAPIKey(keyToVerify string, storedHash []byte) (bool, error) {
// First, validate the format
if err := apikey.ValidateKey(keyToVerify, "sk_live", 32); err != nil {
return false, fmt.Errorf("invalid format: %w", err)
}
// Then verify against the stored hash using constant-time comparison
if !apikey.VerifyKey(keyToVerify, storedHash) {
return false, nil // Key doesn't match
}
return true, nil
}Use the Record struct to store key metadata along with the hash.
import "time"
// Create a record for a new key
gen, _ := apikey.NewGenerator("sk_live", 32)
key, _ := gen.Generate()
hash := apikey.HashKey(key)
record := &apikey.Record{
ID: "key-001", // Unique identifier
Prefix: "sk_live", // Key prefix
Hash: hash, // SHA-256 hash (32 bytes)
Fingerprint: apikey.Fingerprint(key), // Short fingerprint (8 bytes)
CreatedAt: time.Now(), // Creation timestamp
// Optional fields:
// RevokedAt: nil, // Revocation timestamp (set when revoked)
// Disabled: false, // Disabled flag
// Metadata: make(map[string]interface{}), // Custom metadata
}// Access record fields
fmt.Println("ID:", record.ID)
fmt.Println("Prefix:", record.Prefix)
fmt.Printf("Hash: %x\n", record.Hash)
fmt.Println("Fingerprint:", record.Fingerprint)
fmt.Println("Created:", record.CreatedAt)
// Optional fields
if record.RevokedAt != nil {
fmt.Println("Revoked:", *record.RevokedAt)
}
fmt.Println("Disabled:", record.Disabled)// Check if a key is active (not revoked and not disabled)
if record.IsActive() {
fmt.Println("Key is active and can be used")
} else {
fmt.Println("Key is not active")
}// IsActive returns false when disabled
record.Disabled = true
fmt.Println("Active:", record.IsActive()) // false
// IsActive returns false when revoked
record.Disabled = false
now := time.Now()
record.RevokedAt = &now
fmt.Println("Active:", record.IsActive()) // falseFor production use, implement the Store interface for your database.
type Store interface {
// Create stores a new key with its record
Create(key string, record *Record) error
// GetByID retrieves a record by its ID
GetByID(id string) (*Record, error)
// GetByHash retrieves a record by its hash
GetByHash(hash []byte) (*Record, error)
// Update updates an existing record
Update(id string, record *Record) error
// Delete removes a record by its ID
Delete(id string) error
// List returns all records
List() ([]*Record, error)
// Revoke marks a key as revoked
Revoke(id string) error
// Disable marks a key as disabled
Disable(id string) error
}// Create an in-memory store for testing
store := apikey.NewMemStore()
// Create a key
gen, _ := apikey.NewGenerator("sk_live", 32)
key, _ := gen.Generate()
record := &apikey.Record{
ID: "key-001",
Prefix: "sk_live",
}
// Store the key
err := store.Create(key, record)
if err != nil {
return err
}// Get by ID
retrieved, err := store.GetByID("key-001")
if err != nil {
return err
}
fmt.Println("Retrieved:", retrieved.ID)// Get by hash
hash := apikey.HashKey(key)
retrieved, err := store.GetByHash(hash)
if err != nil {
return err
}
fmt.Println("Retrieved by hash:", retrieved.ID)// List all keys
allRecords, err := store.List()
for _, r := range allRecords {
fmt.Println("Record:", r.ID, r.Prefix, r.Fingerprint)
}// Revoke a key
err := store.Revoke("key-001")
if err != nil {
return err
}
// Now IsActive returns false
retrieved, _ := store.GetByID("key-001")
fmt.Println("Active:", retrieved.IsActive()) // false// Disable a key
err := store.Disable("key-001")
if err != nil {
return err
}// Delete a key
err := store.Delete("key-001")
if err != nil {
return err
}// Separator is the character used between prefix and random part
const Separator = "_"
// DefaultKeySize is the recommended key size
const DefaultKeySize = 32import "errors"
// Generator errors
var (
ErrPrefixEmpty = errors.New("prefix cannot be empty")
ErrPrefixTooLong = errors.New("prefix is too long for the specified key size")
ErrPrefixHasSpace = errors.New("prefix cannot contain whitespace")
ErrInvalidKeySize = errors.New("key size must be greater than prefix + separator")
)
// Parsing errors
var (
ErrInvalidKey = errors.New("invalid API key")
ErrInvalidPrefix = errors.New("invalid prefix")
ErrKeyTooShort = errors.New("key is too short")
ErrMissingSeparator = errors.New("missing prefix separator")
)
// Storage errors
var (
ErrRecordNotFound = errors.New("record not found")
ErrDuplicateKey = errors.New("key already exists")
)// NewGenerator creates a new key generator with the specified prefix and key size
func NewGenerator(prefix string, keySize int) (*Generator, error)
// DefaultKeySize returns the default key size (32)
func DefaultKeySize() int
// Generator holds the configuration for key generation
type Generator struct {
prefix string
keySize int
}
// Generate creates a new API key
func (g *Generator) Generate() (string, error)
// Prefix returns the configured prefix
func (g *Generator) Prefix() string
// KeySize returns the configured key size
func (g *Generator) KeySize() int// ValidateKey validates a key's format, prefix, and size
func ValidateKey(key string, expectedPrefix string, expectedSize int) error
// ExtractPrefix extracts the prefix from a key
func ExtractPrefix(key string) (string, error)
// ExtractPayload extracts the random payload from a key
func ExtractPayload(key string) (string, error)
// HasValidPrefix checks if a key has any of the valid prefixes
func HasValidPrefix(key string, validPrefixes []string) bool// HashKey hashes a key using SHA-256, returns 32 bytes
func HashKey(key string) []byte
// HashKeyHex hashes a key and returns hex string (64 chars)
func HashKeyHex(key string) string
// Fingerprint returns first 8 bytes as hex (16 chars)
func Fingerprint(key string) string
// FingerprintBytes returns first 8 bytes
func FingerprintBytes(key string) []byte// VerifyKey compares a key against a stored hash using constant-time comparison
func VerifyKey(presented string, storedHash []byte) bool
// VerifyKeyHex compares a key against a stored hex hash using constant-time comparison
func VerifyKeyHex(presented string, storedHashHex string) boolimport "time"
// Record represents key metadata stored in the database
type Record struct {
ID string // Unique identifier
Prefix string // Key prefix (sk_live, sk_test, etc.)
Hash []byte // SHA-256 hash (32 bytes)
Fingerprint string // Short fingerprint (8 bytes as hex)
CreatedAt time.Time // Creation timestamp
RevokedAt *time.Time // Revocation timestamp (nil if not revoked)
Disabled bool // Disabled flag
Metadata map[string]interface{} // Custom metadata
}
// IsActive returns true if the key is valid
func (r *Record) IsActive() bool
// Store defines the interface for key storage
type Store interface {
Create(key string, record *Record) error
GetByID(id string) (*Record, error)
GetByHash(hash []byte) (*Record, error)
Update(id string, record *Record) error
Delete(id string) error
List() ([]*Record, error)
Revoke(id string) error
Disable(id string) error
}
// NewMemStore creates an in-memory store (for testing)
func NewMemStore() *MemStoreThis example demonstrates creating, storing, and verifying API keys:
package main
import (
"fmt"
"log"
"time"
"github.com/package-foundry/gobekli_api"
)
func main() {
// 1. Create an in-memory store
store := apikey.NewMemStore()
// 2. Create a generator
gen, err := apikey.NewGenerator("sk_live", 32)
if err != nil {
log.Fatal(err)
}
// 3. Generate some keys
for i := 0; i < 3; i++ {
key, err := gen.Generate()
if err != nil {
log.Fatal(err)
}
// Hash for storage
hash := apikey.HashKey(key)
fingerprint := apikey.Fingerprint(key)
// Create record
record := &apikey.Record{
ID: fmt.Sprintf("key-%d", i+1),
Prefix: gen.Prefix(),
Hash: hash,
Fingerprint: fingerprint,
CreatedAt: time.Now(),
}
// Store it
if err := store.Create(key, record); err != nil {
log.Fatal(err)
}
fmt.Printf("Created: %s (Fingerprint: %s)\n", key, fingerprint)
}
// 4. List all keys
fmt.Println("\nAll keys:")
records, _ := store.List()
for _, r := range records {
status := "active"
if !r.IsActive() {
status = "inactive"
}
fmt.Printf(" - %s: %s [%s]\n", r.ID, r.Fingerprint, status)
}
// 5. Revoke the second key
fmt.Println("\nRevoking key-2...")
if err := store.Revoke("key-2"); err != nil {
log.Fatal(err)
}
// 6. List keys again
fmt.Println("\nAfter revocation:")
records, _ = store.List()
for _, r := range records {
status := "active"
if !r.IsActive() {
status = "revoked"
}
fmt.Printf(" - %s: %s [%s]\n", r.ID, r.Fingerprint, status)
}
}A complete API key service implementation:
package main
import (
"errors"
"fmt"
"log"
"time"
"github.com/package-foundry/gobekli_api"
)
// APIKeyService manages the lifecycle of API keys
type APIKeyService struct {
store apikey.Store
gen *apikey.Generator
}
// NewAPIKeyService creates a new API key service
func NewAPIKeyService(store apikey.Store, prefix string) (*APIKeyService, error) {
if store == nil {
return nil, errors.New("store is required")
}
gen, err := apikey.NewGenerator(prefix, apikey.DefaultKeySize())
if err != nil {
return nil, err
}
return &APIKeyService{
store: store,
gen: gen,
}, nil
}
// CreateKey creates a new API key and returns it once
func (s *APIKeyService) CreateKey() (string, *apikey.Record, error) {
key, err := s.gen.Generate()
if err != nil {
return "", nil, err
}
hash := apikey.HashKey(key)
fingerprint := apikey.Fingerprint(key)
record := &apikey.Record{
ID: fmt.Sprintf("key-%d", time.Now().UnixNano()),
Prefix: s.gen.Prefix(),
Hash: hash,
Fingerprint: fingerprint,
CreatedAt: time.Now(),
}
if err := s.store.Create(key, record); err != nil {
return "", nil, err
}
return key, record, nil
}
// VerifyKey verifies an API key and returns the record if valid
func (s *APIKeyService) VerifyKey(key string) (*apikey.Record, error) {
// Validate format
if err := apikey.ValidateKey(key, s.gen.Prefix(), s.gen.KeySize()); err != nil {
return nil, fmt.Errorf("invalid key format: %w", err)
}
// Hash and look up
hash := apikey.HashKey(key)
record, err := s.store.GetByHash(hash)
if err != nil {
return nil, fmt.Errorf("key not found: %w", err)
}
// Constant-time verify
if !apikey.VerifyKey(key, record.Hash) {
return nil, errors.New("invalid key")
}
// Check if active
if !record.IsActive() {
return nil, errors.New("key is revoked or disabled")
}
return record, nil
}
// RevokeKey revokes an API key by ID
func (s *APIKeyService) RevokeKey(id string) error {
record, err := s.store.GetByID(id)
if err != nil {
return err
}
if !record.IsActive() {
return errors.New("key already revoked or disabled")
}
return s.store.Revoke(id)
}
// DisableKey disables an API key by ID
func (s *APIKeyService) DisableKey(id string) error {
record, err := s.store.GetByID(id)
if err != nil {
return err
}
if !record.IsActive() {
return errors.New("key already revoked or disabled")
}
return s.store.Disable(id)
}
// ListKeys returns all key records
func (s *APIKeyService) ListKeys() ([]*apikey.Record, error) {
return s.store.List()
}
func main() {
store := apikey.NewMemStore()
service, err := NewAPIKeyService(store, "sk_live")
if err != nil {
log.Fatal(err)
}
// Create new keys
fmt.Println("Creating keys...")
for i := 0; i < 3; i++ {
key, record, err := service.CreateKey()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created: %s (ID: %s)\n", key, record.ID)
}
// Verify a key
fmt.Println("\nVerifying key...")
key, _, _ := service.CreateKey()
record, err := service.VerifyKey(key)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Verified: %s\n", record.ID)
// Try to verify again after revocation
fmt.Println("\nRevoking key...")
if err := service.RevokeKey(record.ID); err != nil {
log.Fatal(err)
}
// Should fail now
_, err = service.VerifyKey(key)
if err != nil {
fmt.Printf("As expected, verification failed: %s\n", err)
}
}Example of implementing a custom store for a SQL database:
package myapp
import (
"database/sql"
"fmt"
"time"
"github.com/package-foundry/gobekli_api"
)
// SQLStore implements apikey.Store for a SQL database
type SQLStore struct {
db *sql.DB
}
// NewSQLStore creates a new SQL store
func NewSQLStore(db *sql.DB) *SQLStore {
return &SQLStore{db: db}
}
// Create inserts a new key record
func (s *SQLStore) Create(key string, record *apikey.Record) error {
query := `
INSERT INTO api_keys (id, prefix, hash, fingerprint, created_at, revoked_at, disabled)
VALUES (?, ?, ?, ?, ?, ?, ?)
`
_, err := s.db.Exec(query,
record.ID,
record.Prefix,
record.Hash,
record.Fingerprint,
record.CreatedAt,
record.RevokedAt,
record.Disabled,
)
return err
}
// GetByID retrieves a record by ID
func (s *SQLStore) GetByID(id string) (*apikey.Record, error) {
query := `SELECT id, prefix, hash, fingerprint, created_at, revoked_at, disabled FROM api_keys WHERE id = ?`
return s.scanRow(s.db.QueryRow(query, id))
}
// GetByHash retrieves a record by hash
func (s *SQLStore) GetByHash(hash []byte) (*apikey.Record, error) {
query := `SELECT id, prefix, hash, fingerprint, created_at, revoked_at, disabled FROM api_keys WHERE hash = ?`
return s.scanRow(s.db.QueryRow(query, hash))
}
// Update updates an existing record
func (s *SQLStore) Update(id string, record *apikey.Record) error {
query := `
UPDATE api_keys
SET prefix = ?, hash = ?, fingerprint = ?, revoked_at = ?, disabled = ?
WHERE id = ?
`
_, err := s.db.Exec(query,
record.Prefix,
record.Hash,
record.Fingerprint,
record.RevokedAt,
record.Disabled,
id,
)
return err
}
// Delete removes a record
func (s *SQLStore) Delete(id string) error {
_, err := s.db.Exec("DELETE FROM api_keys WHERE id = ?", id)
return err
}
// List returns all records
func (s *SQLStore) List() ([]*apikey.Record, error) {
query := `SELECT id, prefix, hash, fingerprint, created_at, revoked_at, disabled FROM api_keys`
rows, err := s.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var records []*apikey.Record
for rows.Next() {
r, err := s.scanRow(rows)
if err != nil {
return nil, err
}
records = append(records, r)
}
return records, nil
}
// Revoke marks a key as revoked
func (s *SQLStore) Revoke(id string) error {
query := `UPDATE api_keys SET revoked_at = ? WHERE id = ?`
_, err := s.db.Exec(query, time.Now(), id)
return err
}
// Disable marks a key as disabled
func (s *SQLStore) Disable(id string) error {
query := `UPDATE api_keys SET disabled = true WHERE id = ?`
_, err := s.db.Exec(query, id)
return err
}
// scanRow helper function (simplified)
func (s *SQLStore) scanRow(row *sql.Row) (*apikey.Record, error) {
var r apikey.Record
var createdAt, revokedAt sql.NullTime
var disabled bool
var hash []byte
err := row.Scan(&r.ID, &r.Prefix, &hash, &r.Fingerprint, &createdAt, &revokedAt, &disabled)
if err != nil {
return nil, err
}
r.Hash = hash
r.CreatedAt = createdAt.Time
r.Disabled = disabled
if revokedAt.Valid {
r.RevokedAt = &revokedAt.Time
}
return &r, nil
}Base58 encoding offers several important advantages over Base64 for API keys:
-
No Ambiguous Characters
- Excludes: 0 (zero), O (capital o), I (capital i), l (lowercase L)
- These characters look similar in many fonts and can cause confusion
-
Compact Encoding
- No padding required (= signs)
- Shorter keys for the same amount of entropy
-
Proven Standard
- Used by Bitcoin, Ethereum, and other cryptocurrencies
- Well-tested and battle-hardened
-
URL-Safe
- Doesn't require URL encoding
- Suitable for headers, query parameters, and more
Example comparison:
Base64: SkRvMzJEN2U5Rk0xcjNHN2U5Rk0xcjNHN2U5Rk0xcg==
Base58: SkRvMzJDN2U5Rk0xcjNHN2U5Rk0xcjNH
// RIGHT: Show the key once, then never again
key, record, err := service.CreateKey()
if err != nil {
return err
}
displayToUser(key) // Show only once!
fmt.Println("Key ID:", record.ID) // Only show the ID after this// WRONG: Storing plaintext
// Never do this:
db.Exec("INSERT INTO keys (key) VALUES (?)", key)// Uses crypto/subtle.ConstantTimeCompare to prevent timing attacks
valid := apikey.VerifyKey(presentedKey, storedHash)// Uses crypto/rand (NOT math/rand)
randomBytes := make([]byte, 32)
if _, err := rand.Read(randomBytes); err != nil {
return err
}// SHA-256 is computationally infeasible to reverse
hash := apikey.HashKey(key)
// Cannot recover key from hash!// If a key is lost, generate a new one
// There is no way to recover the original key from the hash-
Factory Pattern: The
Generatorstruct provides a clean factory for creating keys with consistent prefix/size. -
Prefix Validation: Strict validation of prefix format (no whitespace, reasonable length).
-
Record Metadata: The
Recordstruct stores key metadata separately from the key itself. -
Validation Functions: Clear separation between key generation and key validation.
-
No Global State: All configuration is explicit and instance-based.
-
Interface-Based Storage: The
Storeinterface allows any storage backend. -
Built-In Base58: A minimal Base58 implementation is included.
-
Explicit Error Handling: Go errors are explicit, not exceptions.
-
Separate Concerns: Generation, parsing, hashing, and verification are in separate files.
Run all tests:
go test -v ./...Run with coverage:
go test -v -cover ./...Run examples:
go test -v -run TestExamples ./...- Unit Tests: Test individual functions
- Table-Driven Tests: Test edge cases with various inputs
- Uniqueness Tests: Ensure generated keys are unique
- Error Tests: Test error conditions
- Example Tests: Test complete workflows
MIT License - Copyright (c) 2026 Antonio Ognio
See LICENSE for full details.
- Issues: https://github.com/package-foundry/gobekli_api/issues
- Discussions: https://github.com/package-foundry/gobekli_api/discussions
Maintained with :heart_emoji: from Peru :peruvian_flag_emoji:. El Perú es clave. :key_emoji:
