encryption

package
v1.2.1 Latest Latest
Warning

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

Go to latest
Published: Feb 22, 2026 License: MIT Imports: 13 Imported by: 0

README

Database Encryption Package

A GORM plugin for automatic field-level encryption and hashing in PostgreSQL databases. Simply annotate your struct fields with tags, and GORM will automatically encrypt/decrypt them.

Features

  • Automatic Encryption/Decryption: Uses AES-256-GCM for secure encryption
  • Tag-Based Configuration: Mark fields with encrypt:"true" tag
  • Searchable Hashing: Generate searchable hashes with hash:"FieldName" tag
  • GORM Integration: Seamless integration with GORM hooks
  • Thread-Safe: Safe for concurrent operations
  • Field Caching: Optimized performance with reflection caching

Installation

go get github.com/cgholdings/go-common/database/encryption

Quick Start

1. Define Your Model
type User struct {
    ID          uint   `gorm:"primarykey"`
    Name        string
    Email       string
    SSN         string `gorm:"type:text" encrypt:"true"`
    SSNHash     string `gorm:"type:varchar(64);index" hash:"SSN"`
    CreditCard  string `gorm:"type:text" encrypt:"true"`
    CCHash      string `gorm:"type:varchar(64);index" hash:"CreditCard"`
}
2. Setup GORM with Encryption Plugin
package main

import (
    "github.com/cgholdings/go-common/database/encryption"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

func main() {
    // Generate a new encryption key (do this once and save it securely)
    key, err := encryption.GenerateKey(32) // 32 bytes = AES-256
    if err != nil {
        panic(err)
    }

    // Create encryptor
    encryptor, err := encryption.NewEncryptor(key)
    if err != nil {
        panic(err)
    }

    // Setup database
    dsn := "host=localhost user=postgres password=pass dbname=dbname port=5432 sslmode=disable"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        panic(err)
    }

    // Register encryption plugin
    db.Use(encryption.NewPlugin(encryptor))

    // Auto migrate
    db.AutoMigrate(&User{})
}
3. Use Your Models Normally
// Create - automatic encryption
user := User{
    Name:       "John Doe",
    Email:      "john@example.com",
    SSN:        "123-45-6789",
    CreditCard: "4111111111111111",
}
db.Create(&user)
// SSN and CreditCard are encrypted in database
// SSNHash and CCHash are automatically populated with hashes

// Query - automatic decryption
var retrieved User
db.First(&retrieved, user.ID)
fmt.Println(retrieved.SSN) // "123-45-6789" - decrypted automatically

// Search by hash
ssnHash := encryption.Hash("123-45-6789")
var found User
db.Where("ssn_hash = ?", ssnHash).First(&found)
// SSN is still encrypted in database but decrypted when retrieved

// Update - automatic re-encryption
retrieved.SSN = "999-88-7777"
db.Save(&retrieved)
// New SSN is encrypted, and SSNHash is updated automatically

Configuration Options

Using Environment Variables
// Set environment variable
// export ENCRYPTION_KEY="base64-encoded-key-here"

config := encryption.DefaultConfig()
encryptor, err := encryption.NewEncryptorFromConfig(config)
if err != nil {
    panic(err)
}

db.Use(encryption.NewPlugin(encryptor))
Using Base64 Key String
// Generate key string once and save it
keyString, _ := encryption.GenerateKeyString(32)
fmt.Println(keyString) // Save this in your config

// Later, load from config
config := &encryption.Config{
    KeyString: "your-base64-encoded-key-here",
}

encryptor, err := encryption.NewEncryptorFromConfig(config)
if err != nil {
    panic(err)
}

db.Use(encryption.NewPlugin(encryptor))
Using Custom Environment Variable
config := &encryption.Config{
    KeyEnvVar: "MY_CUSTOM_KEY_VAR",
}

encryptor, err := encryption.NewEncryptorFromConfig(config)
if err != nil {
    panic(err)
}

db.Use(encryption.NewPlugin(encryptor))

Key Management

Generating Keys
// Generate 32-byte key for AES-256 (recommended)
key, err := encryption.GenerateKey(32)

// Generate 24-byte key for AES-192
key, err := encryption.GenerateKey(24)

// Generate 16-byte key for AES-128
key, err := encryption.GenerateKey(16)

// Generate as base64 string
keyString, err := encryption.GenerateKeyString(32)
fmt.Println(keyString) // Store this securely
Storing Keys Securely

DO NOT hardcode keys in your source code. Use one of these methods:

  1. Environment Variables (Recommended for development)
export ENCRYPTION_KEY="your-base64-key"
  1. Secret Management Services (Recommended for production)

    • AWS Secrets Manager
    • HashiCorp Vault
    • Google Secret Manager
    • Azure Key Vault
  2. Configuration Files (with proper permissions)

// Load from encrypted config file
keyString := loadFromSecureConfig()
config := &encryption.Config{KeyString: keyString}

Usage Examples

Encryption Tag Options
type Model struct {
    Field1 string `encrypt:"true"`   // ✓ Will be encrypted
    Field2 string `encrypt:"1"`      // ✓ Will be encrypted
    Field3 string `encrypt:"yes"`    // ✓ Will be encrypted
    Field4 string `encrypt:"false"`  // ✗ Not encrypted
    Field5 string `encrypt:"no"`     // ✗ Not encrypted
    Field6 string `encrypt:"0"`      // ✗ Not encrypted
    Field7 string                     // ✗ Not encrypted (no tag)
}
Hash Tag for Searchable Fields

The hash tag creates a searchable hash of another field:

type User struct {
    Email      string `gorm:"type:text" encrypt:"true"`
    EmailHash  string `gorm:"type:varchar(64);index" hash:"Email"`
}

// Search by email without decrypting all records
emailHash := encryption.Hash("user@example.com")
db.Where("email_hash = ?", emailHash).First(&user)
Manual Encryption/Decryption

You can also use the encryptor manually:

encryptor, _ := encryption.NewEncryptor(key)

// Encrypt
ciphertext, err := encryptor.Encrypt("sensitive data")

// Decrypt
plaintext, err := encryptor.Decrypt(ciphertext)

// Hash
hash := encryption.Hash("searchable value")
Batch Operations

The plugin works seamlessly with batch operations:

users := []User{
    {Name: "User1", SSN: "111-11-1111"},
    {Name: "User2", SSN: "222-22-2222"},
    {Name: "User3", SSN: "333-33-3333"},
}

// All SSNs encrypted automatically
db.Create(&users)

// All SSNs decrypted automatically
var retrieved []User
db.Find(&retrieved)
Handling Empty Fields

Empty strings are handled gracefully:

user := User{
    Name: "John",
    SSN:  "", // Empty field
}
db.Create(&user)
// Empty fields remain empty (not encrypted)

Security Best Practices

  1. Key Size: Use 32-byte keys (AES-256) for maximum security
  2. Key Rotation: Plan for key rotation in production
  3. Key Storage: Never commit keys to version control
  4. Hashing: Use hash fields for searching, never search encrypted fields directly
  5. TLS/SSL: Always use encrypted connections to your database
  6. Access Control: Limit access to encryption keys
  7. Audit: Log access to encrypted data

Performance Considerations

  • Caching: The plugin caches struct field information for optimal performance
  • Indexes: Create indexes on hash fields for fast searches
  • Batch Operations: Use batch inserts/updates when possible
  • Field Selection: Only select encrypted fields when needed
// Efficient: Only get name (no decryption needed)
db.Select("name").Find(&users)

// Less efficient: Decrypts all fields
db.Find(&users)

Limitations

  • Only string fields are supported for encryption
  • Encrypted fields cannot be used in WHERE clauses (use hash fields instead)
  • Encrypted field length will be longer than plaintext (use type:text in GORM)

Database Schema Considerations

Encrypted Field Column Types

Encrypted data is base64-encoded and includes a nonce. The ciphertext will be longer than the plaintext:

// Good - use TEXT for encrypted fields
SSN string `gorm:"type:text" encrypt:"true"`

// Bad - VARCHAR may truncate
SSN string `gorm:"type:varchar(100)" encrypt:"true"` // May be too short!
Hash Field Column Types

Hash fields always produce 64-character hex strings:

// Perfect size for SHA-256 hashes
SSNHash string `gorm:"type:varchar(64);index" hash:"SSN"`
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255),
    email VARCHAR(255),
    ssn TEXT,                    -- Encrypted field (TEXT type)
    ssn_hash VARCHAR(64),        -- Hash field for searching
    credit_card TEXT,            -- Encrypted field
    cc_hash VARCHAR(64)          -- Hash field
);
CREATE INDEX idx_ssn_hash ON users (ssn_hash);
CREATE INDEX idx_cc_hash ON users (cc_hash);

Troubleshooting

Decryption Errors

If you see decryption errors, common causes:

  • Wrong encryption key
  • Data was not encrypted with this package
  • Database column truncated the ciphertext (use TEXT type)
  • Key was changed after data was encrypted
Performance Issues
  • Add indexes to hash fields used in WHERE clauses
  • Use Select() to limit fields retrieved
  • Consider using read replicas for queries
  • Monitor query performance with GORM's logger

Examples

See the *_test.go files for comprehensive examples of:

  • Basic encryption/decryption
  • GORM integration
  • Hash-based searching
  • Batch operations
  • Configuration options

License

MIT License - see LICENSE file for details

Documentation

Index

Examples

Constants

View Source
const (
	// EncryptTagName is the struct tag name used to mark fields for encryption
	EncryptTagName = "encrypt"
	// HashTagName is the struct tag name used to mark fields for hashing
	HashTagName = "hash"
	// PluginName is the name of the GORM plugin
	PluginName = "encryption"
)

Variables

View Source
var (
	// ErrInvalidKeySize is returned when the encryption key size is invalid
	ErrInvalidKeySize = errors.New("encryption key must be 16, 24, or 32 bytes for AES-128, AES-192, or AES-256")
	// ErrEncryptionFailed is returned when encryption fails
	ErrEncryptionFailed = errors.New("encryption failed")
	// ErrDecryptionFailed is returned when decryption fails
	ErrDecryptionFailed = errors.New("decryption failed")
	// ErrInvalidCiphertext is returned when the ciphertext is invalid
	ErrInvalidCiphertext = errors.New("invalid ciphertext: too short or malformed")
)

Functions

func GenerateKey

func GenerateKey(size int) ([]byte, error)

GenerateKey generates a random encryption key of the specified size size should be 16, 24, or 32 for AES-128, AES-192, or AES-256

func GenerateKeyString

func GenerateKeyString(size int) (string, error)

GenerateKeyString generates a random encryption key and returns it as a base64-encoded string

func Hash

func Hash(value string) string

Hash generates a SHA-256 hash of the input string This is useful for creating searchable hashes of encrypted fields Returns a hex-encoded hash string

Example

ExampleHash demonstrates searching encrypted fields using hashes

package main

import (
	"fmt"
	"log"

	"github.com/cgholdings/go-common/database/encryption"
	"gorm.io/gorm"
)

// User model with encrypted fields
type User struct {
	ID         uint   `gorm:"primarykey"`
	Name       string `gorm:"type:varchar(255)"`
	Email      string `gorm:"type:varchar(255)"`
	SSN        string `gorm:"type:text" encrypt:"true"`
	SSNHash    string `gorm:"type:varchar(64);index" hash:"SSN"`
	CreditCard string `gorm:"type:text" encrypt:"true"`
	CCHash     string `gorm:"type:varchar(64);index" hash:"CreditCard"`
}

func main() {
	// Setup (omitted for brevity - see ExampleGORMIntegration)
	var db *gorm.DB

	// Search for a user by SSN using the hash
	searchSSN := "123-45-6789"
	ssnHash := encryption.Hash(searchSSN)

	var user User
	if err := db.Where("ssn_hash = ?", ssnHash).First(&user).Error; err != nil {
		log.Fatal(err)
	}

	// The SSN field is automatically decrypted
	fmt.Printf("Found user: %s, SSN: %s\n", user.Name, user.SSN)
}
Example (Searching)

ExampleHash_searching demonstrates manual hashing

package main

import (
	"fmt"

	"github.com/cgholdings/go-common/database/encryption"
)

func main() {
	// Hash a value for searching
	ssn := "123-45-6789"
	hash := encryption.Hash(ssn)

	fmt.Printf("SSN: %s\n", ssn)
	fmt.Printf("Hash: %s\n", hash)

	// Hash is deterministic
	hash2 := encryption.Hash(ssn)
	fmt.Printf("Hashes match: %v\n", hash == hash2)

	// Different values produce different hashes
	hash3 := encryption.Hash("999-88-7777")
	fmt.Printf("Different hashes: %v\n", hash != hash3)
}

func HashBytes

func HashBytes(value []byte) string

HashBytes generates a SHA-256 hash of the input bytes Returns a hex-encoded hash string

Types

type Config

type Config struct {
	// Key is the encryption key (16, 24, or 32 bytes)
	Key []byte
	// KeyString is the base64-encoded encryption key (alternative to Key)
	KeyString string
	// KeyEnvVar is the environment variable name containing the encryption key
	KeyEnvVar string
}

Config holds the configuration for the encryption plugin

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns a default configuration that reads the key from the ENCRYPTION_KEY environment variable

Example

ExampleDefaultConfig demonstrates loading config from environment

package main

import (
	"fmt"
	"log"

	"github.com/cgholdings/go-common/database/encryption"
)

func main() {
	// Set environment variable: export ENCRYPTION_KEY="base64-encoded-key"

	// Load config from default environment variable
	config := encryption.DefaultConfig()

	_, err := encryption.NewEncryptorFromConfig(config)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Encryptor created from environment variable\n")
}

type Encryptor

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

Encryptor handles encryption and decryption operations

func NewEncryptor

func NewEncryptor(key []byte) (*Encryptor, error)

NewEncryptor creates a new Encryptor with the provided key The key should be 16, 24, or 32 bytes for AES-128, AES-192, or AES-256

Example

ExampleNewEncryptor demonstrates basic encryption/decryption

package main

import (
	"fmt"
	"log"

	"github.com/cgholdings/go-common/database/encryption"
)

func main() {
	// Generate a new encryption key (32 bytes for AES-256)
	key, err := encryption.GenerateKey(32)
	if err != nil {
		log.Fatal(err)
	}

	// Create encryptor
	encryptor, err := encryption.NewEncryptor(key)
	if err != nil {
		log.Fatal(err)
	}

	// Encrypt data
	plaintext := "sensitive-data-123"
	ciphertext, err := encryptor.Encrypt(plaintext)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Encrypted: %s\n", ciphertext)

	// Decrypt data
	decrypted, err := encryptor.Decrypt(ciphertext)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Decrypted: %s\n", decrypted)
}

func NewEncryptorFromConfig

func NewEncryptorFromConfig(config *Config) (*Encryptor, error)

NewEncryptorFromConfig creates a new Encryptor from a Config

Example

ExampleNewEncryptorFromConfig demonstrates using a base64-encoded key

package main

import (
	"fmt"
	"log"

	"github.com/cgholdings/go-common/database/encryption"
)

func main() {
	// Generate a key string once and save it securely
	keyString, err := encryption.GenerateKeyString(32)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Generated key (save this securely): %s\n", keyString)

	// Later, load from config
	config := &encryption.Config{
		KeyString: keyString,
	}

	encryptor, err := encryption.NewEncryptorFromConfig(config)
	if err != nil {
		log.Fatal(err)
	}

	// Use encryptor...
	_ = encryptor
}

func NewEncryptorFromString

func NewEncryptorFromString(keyString string) (*Encryptor, error)

NewEncryptorFromString creates a new Encryptor from a base64-encoded key string

func (*Encryptor) Decrypt

func (e *Encryptor) Decrypt(ciphertext string) (string, error)

Decrypt decrypts the base64-encoded ciphertext using AES-256-GCM

func (*Encryptor) Encrypt

func (e *Encryptor) Encrypt(plaintext string) (string, error)

Encrypt encrypts the plaintext using AES-256-GCM Returns base64-encoded ciphertext with nonce prepended

type Plugin

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

Plugin is a GORM plugin that automatically encrypts and decrypts fields

func NewPlugin

func NewPlugin(encryptor *Encryptor) *Plugin

NewPlugin creates a new encryption plugin for GORM

Example

ExampleNewPlugin demonstrates GORM plugin usage

package main

import (
	"fmt"
	"log"

	"github.com/cgholdings/go-common/database/encryption"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

// User model with encrypted fields
type User struct {
	ID         uint   `gorm:"primarykey"`
	Name       string `gorm:"type:varchar(255)"`
	Email      string `gorm:"type:varchar(255)"`
	SSN        string `gorm:"type:text" encrypt:"true"`
	SSNHash    string `gorm:"type:varchar(64);index" hash:"SSN"`
	CreditCard string `gorm:"type:text" encrypt:"true"`
	CCHash     string `gorm:"type:varchar(64);index" hash:"CreditCard"`
}

func main() {
	// Generate encryption key
	key, err := encryption.GenerateKey(32)
	if err != nil {
		log.Fatal(err)
	}

	// Create encryptor
	encryptor, err := encryption.NewEncryptor(key)
	if err != nil {
		log.Fatal(err)
	}

	// Setup database
	dsn := "host=localhost user=postgres password=pass dbname=dbname port=5432 sslmode=disable"
	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}

	// Register encryption plugin
	if err := db.Use(encryption.NewPlugin(encryptor)); err != nil {
		log.Fatal(err)
	}

	// Auto migrate
	if err := db.AutoMigrate(&User{}); err != nil {
		log.Fatal(err)
	}

	// Create user - SSN and CreditCard will be encrypted automatically
	user := User{
		Name:       "John Doe",
		Email:      "john@example.com",
		SSN:        "123-45-6789",
		CreditCard: "4111111111111111",
	}

	if err := db.Create(&user).Error; err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Created user with ID: %d\n", user.ID)
	fmt.Printf("SSN Hash: %s\n", user.SSNHash)

	// Query user - encrypted fields will be decrypted automatically
	var retrieved User
	if err := db.First(&retrieved, user.ID).Error; err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Retrieved SSN: %s\n", retrieved.SSN)
}
Example (Batch)

ExampleNewPlugin_batch demonstrates batch create and query

package main

import (
	"fmt"
	"log"

	"gorm.io/gorm"
)

// User model with encrypted fields
type User struct {
	ID         uint   `gorm:"primarykey"`
	Name       string `gorm:"type:varchar(255)"`
	Email      string `gorm:"type:varchar(255)"`
	SSN        string `gorm:"type:text" encrypt:"true"`
	SSNHash    string `gorm:"type:varchar(64);index" hash:"SSN"`
	CreditCard string `gorm:"type:text" encrypt:"true"`
	CCHash     string `gorm:"type:varchar(64);index" hash:"CreditCard"`
}

func main() {
	// Setup (omitted for brevity)
	var db *gorm.DB

	// Create multiple users - all encrypted automatically
	users := []User{
		{Name: "User1", SSN: "111-11-1111", CreditCard: "4111111111111111"},
		{Name: "User2", SSN: "222-22-2222", CreditCard: "4222222222222222"},
		{Name: "User3", SSN: "333-33-3333", CreditCard: "4333333333333333"},
	}

	if err := db.Create(&users).Error; err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Created %d users\n", len(users))

	// Query all users - all decrypted automatically
	var retrieved []User
	if err := db.Find(&retrieved).Error; err != nil {
		log.Fatal(err)
	}

	for _, user := range retrieved {
		fmt.Printf("User: %s, SSN: %s\n", user.Name, user.SSN)
	}
}
Example (Update)

ExampleNewPlugin_update demonstrates updating encrypted fields

package main

import (
	"fmt"
	"log"

	"gorm.io/gorm"
)

// User model with encrypted fields
type User struct {
	ID         uint   `gorm:"primarykey"`
	Name       string `gorm:"type:varchar(255)"`
	Email      string `gorm:"type:varchar(255)"`
	SSN        string `gorm:"type:text" encrypt:"true"`
	SSNHash    string `gorm:"type:varchar(64);index" hash:"SSN"`
	CreditCard string `gorm:"type:text" encrypt:"true"`
	CCHash     string `gorm:"type:varchar(64);index" hash:"CreditCard"`
}

func main() {
	// Setup (omitted for brevity)
	var db *gorm.DB
	var user User

	// Update SSN - will be re-encrypted automatically
	user.SSN = "999-88-7777"

	if err := db.Save(&user).Error; err != nil {
		log.Fatal(err)
	}

	// SSNHash is also updated automatically
	fmt.Printf("Updated SSN, new hash: %s\n", user.SSNHash)
}

func NewPluginFromConfig

func NewPluginFromConfig(config *Config) (*Plugin, error)

NewPluginFromConfig creates a new encryption plugin from a Config

func (*Plugin) Initialize

func (p *Plugin) Initialize(db *gorm.DB) error

Initialize initializes the plugin and registers callbacks

func (*Plugin) Name

func (p *Plugin) Name() string

Name returns the plugin name

Jump to

Keyboard shortcuts

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