monorepo/cloud/maplepress-backend/pkg/security/README.md

13 KiB

Security Package

This package provides secure password hashing and memory-safe storage for sensitive data.

Packages

Password (pkg/security/password)

Provides Argon2id-based password hashing and verification with secure default parameters following OWASP recommendations.

SecureString (pkg/security/securestring)

Memory-safe string storage using memguard to protect sensitive data like passwords and API keys from memory dumps and swap files.

SecureBytes (pkg/security/securebytes)

Memory-safe byte slice storage using memguard to protect sensitive binary data.

IPCountryBlocker (pkg/security/ipcountryblocker)

GeoIP-based country blocking using MaxMind's GeoLite2 database to block requests from specific countries.

Installation

The packages are included in the project. Required dependencies:

  • github.com/awnumar/memguard - For secure memory management
  • golang.org/x/crypto/argon2 - For password hashing
  • github.com/oschwald/geoip2-golang - For GeoIP lookups

Usage

Password Hashing

import (
    "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/password"
    "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/securestring"
)

// Create password provider
passwordProvider := password.NewPasswordProvider()

// Hash a password
plainPassword := "mySecurePassword123!"
securePass, err := securestring.NewSecureString(plainPassword)
if err != nil {
    // Handle error
}
defer securePass.Wipe() // Always wipe after use

hashedPassword, err := passwordProvider.GenerateHashFromPassword(securePass)
if err != nil {
    // Handle error
}

// Verify a password
match, err := passwordProvider.ComparePasswordAndHash(securePass, hashedPassword)
if err != nil {
    // Handle error
}
if match {
    // Password is correct
}

Secure String Storage

import "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/securestring"

// Store sensitive data securely
apiKey := "secret-api-key-12345"
secureKey, err := securestring.NewSecureString(apiKey)
if err != nil {
    // Handle error
}
defer secureKey.Wipe() // Always wipe when done

// Use the secure string
keyValue := secureKey.String() // Get the value when needed
// ... use keyValue ...

// The original string should be cleared
apiKey = ""

Secure Bytes Storage

import "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/securebytes"

// Store sensitive binary data
sensitiveData := []byte{0x01, 0x02, 0x03, 0x04}
secureData, err := securebytes.NewSecureBytes(sensitiveData)
if err != nil {
    // Handle error
}
defer secureData.Wipe()

// Use the secure bytes
data := secureData.Bytes()
// ... use data ...

// Clear the original slice
for i := range sensitiveData {
    sensitiveData[i] = 0
}

Generate Random Values

passwordProvider := password.NewPasswordProvider()

// Generate random bytes
randomBytes, err := passwordProvider.GenerateSecureRandomBytes(32)

// Generate random hex string (length * 2 characters)
randomString, err := passwordProvider.GenerateSecureRandomString(16)
// Returns a 32-character hex string

IP Country Blocking

import (
    "context"
    "net"
    "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config"
    "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/ipcountryblocker"
)

// Create the blocker (typically done via dependency injection)
cfg, _ := config.Load()
blocker := ipcountryblocker.NewProvider(cfg, logger)
defer blocker.Close()

// Check if an IP is blocked
ip := net.ParseIP("192.0.2.1")
if blocker.IsBlockedIP(context.Background(), ip) {
    // Handle blocked IP
    return errors.New("access denied: your country is blocked")
}

// Check if a country code is blocked
if blocker.IsBlockedCountry("CN") {
    // Country is in the blocked list
}

// Get country code for an IP
countryCode, err := blocker.GetCountryCode(context.Background(), ip)
if err != nil {
    // Handle error
}
// countryCode will be ISO 3166-1 alpha-2 code like "US", "CA", "GB"

Configuration:

# Environment variables
APP_GEOLITE_DB_PATH=/path/to/GeoLite2-Country.mmdb
APP_BANNED_COUNTRIES=CN,RU,KP  # Comma-separated ISO 3166-1 alpha-2 codes

Password Hashing Details

Algorithm: Argon2id

Argon2id is the recommended password hashing algorithm by OWASP. It combines:

  • Argon2i: Resistant to side-channel attacks
  • Argon2d: Resistant to GPU cracking attacks

Default Parameters

Memory:      64 MB (65536 KB)
Iterations:  3
Parallelism: 2 threads
Salt Length: 16 bytes
Key Length:  32 bytes

These parameters provide strong security while maintaining reasonable performance for authentication systems.

Hash Format

$argon2id$v=19$m=65536,t=3,p=2$<base64-salt>$<base64-hash>

Example:

$argon2id$v=19$m=65536,t=3,p=2$YWJjZGVmZ2hpamtsbW5vcA$9XJqrJ8fQvVrMz0FqJ7gBGqKvYLvLxC8HzPqKvYLvLxC

The hash includes all parameters, so it can be verified even if you change the default parameters later.

Security Best Practices

1. Always Wipe Sensitive Data

securePass, _ := securestring.NewSecureString(password)
defer securePass.Wipe() // Ensures cleanup even on panic

// ... use securePass ...

2. Clear Original Data

After creating a secure string/bytes, clear the original data:

password := "secret"
securePass, _ := securestring.NewSecureString(password)
password = "" // Clear the original string

// Even better for byte slices:
data := []byte("secret")
secureData, _ := securebytes.NewSecureBytes(data)
for i := range data {
    data[i] = 0
}

3. Minimize Exposure Time

Get values from secure storage only when needed:

// Bad - exposes value for too long
value := secureString.String()
// ... lots of code ...
useValue(value)

// Good - get value right before use
// ... lots of code ...
useValue(secureString.String())

4. Use Dependency Injection

import "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/password"

// In your Wire provider set
wire.NewSet(
    password.ProvidePasswordProvider,
    // ... other providers
)

// Use in your service
type AuthService struct {
    passwordProvider password.PasswordProvider
}

func NewAuthService(pp password.PasswordProvider) *AuthService {
    return &AuthService{passwordProvider: pp}
}

5. Handle Errors Properly

securePass, err := securestring.NewSecureString(password)
if err != nil {
    return fmt.Errorf("failed to create secure string: %w", err)
}
defer securePass.Wipe()

6. Clean Up GeoIP Resources

import "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/ipcountryblocker"

// Always close the provider when done to release database resources
blocker := ipcountryblocker.NewProvider(cfg, logger)
defer blocker.Close()

Memory Safety

How It Works

The memguard library provides:

  1. Locked Memory: Prevents sensitive data from being swapped to disk
  2. Guarded Heap: Detects buffer overflows and underflows
  3. Secure Wiping: Overwrites memory with random data before freeing
  4. Read Protection: Makes memory pages read-only when not in use

When to Use

Use secure storage for:

  • Passwords and password hashes (during verification)
  • API keys and tokens
  • Encryption keys
  • Private keys
  • Database credentials
  • OAuth secrets
  • JWT signing keys
  • Session tokens
  • Any sensitive user data

When NOT to Use

Don't use for:

  • Public data
  • Non-sensitive configuration
  • Data that needs to be logged
  • Data that will be stored long-term in memory

Performance Considerations

Password Hashing

Argon2id is intentionally slow to prevent brute-force attacks:

  • Expected time: ~50-100ms per hash on modern hardware
  • This is acceptable for authentication (login) operations
  • DO NOT use for high-throughput operations

Memory Usage

SecureString/SecureBytes use locked memory:

  • Each instance locks a page in RAM (typically 4KB minimum)
  • Don't create thousands of instances
  • Reuse instances when possible
  • Always wipe when done

Examples

Complete Login Example

func (s *AuthService) Login(ctx context.Context, email, password string) (*User, error) {
    // Create secure string from password
    securePass, err := securestring.NewSecureString(password)
    if err != nil {
        return nil, err
    }
    defer securePass.Wipe()

    // Clear the original password
    password = ""

    // Get user from database
    user, err := s.userRepo.GetByEmail(ctx, email)
    if err != nil {
        return nil, err
    }

    // Verify password
    match, err := s.passwordProvider.ComparePasswordAndHash(securePass, user.PasswordHash)
    if err != nil {
        return nil, err
    }

    if !match {
        return nil, ErrInvalidCredentials
    }

    return user, nil
}

Complete Registration Example

func (s *AuthService) Register(ctx context.Context, email, password string) (*User, error) {
    // Validate password strength first
    if len(password) < 8 {
        return nil, ErrWeakPassword
    }

    // Create secure string from password
    securePass, err := securestring.NewSecureString(password)
    if err != nil {
        return nil, err
    }
    defer securePass.Wipe()

    // Clear the original password
    password = ""

    // Hash the password
    hashedPassword, err := s.passwordProvider.GenerateHashFromPassword(securePass)
    if err != nil {
        return nil, err
    }

    // Create user with hashed password
    user := &User{
        Email:        email,
        PasswordHash: hashedPassword,
    }

    if err := s.userRepo.Create(ctx, user); err != nil {
        return nil, err
    }

    return user, nil
}

Troubleshooting

"failed to create buffer"

Problem: memguard couldn't allocate locked memory

Solutions:

  • Check system limits for locked memory (ulimit -l)
  • Reduce number of concurrent SecureString/SecureBytes instances
  • Ensure proper cleanup with Wipe()

"buffer is not alive"

Problem: Trying to use a SecureString/SecureBytes after it was wiped

Solutions:

  • Don't use secure data after calling Wipe()
  • Check your defer ordering
  • Create new instances if you need the data again

Slow Performance

Problem: Password hashing is too slow

Solutions:

  • This is by design for security
  • Don't hash passwords in high-throughput operations
  • Consider caching authentication results (with care)
  • Use async operations for registration/password changes

"failed to open GeoLite2 DB"

Problem: Cannot open the GeoIP2 database

Solutions:

  • Verify APP_GEOLITE_DB_PATH points to a valid .mmdb file
  • Download the GeoLite2-Country database from MaxMind
  • Check file permissions
  • Ensure the database file is not corrupted

"no country found for IP"

Problem: IP address lookup returns no country

Solutions:

  • This is normal for private IP ranges (10.x.x.x, 192.168.x.x, etc.)
  • The IP might not be in the GeoIP2 database
  • Update to a newer GeoLite2 database
  • By default, unknown IPs are allowed (returns false from IsBlockedIP)

IP Country Blocking Details

GeoLite2 Database

The IP country blocker uses MaxMind's GeoLite2-Country database for IP geolocation.

How to Get the Database:

  1. Create a free account at https://www.maxmind.com/en/geolite2/signup
  2. Generate a license key
  3. Download GeoLite2-Country database (.mmdb format)
  4. Set APP_GEOLITE_DB_PATH to the file location

Database Updates:

  • MaxMind updates GeoLite2 databases weekly
  • Set up automated updates for production systems
  • Database file is typically 5-10 MB

Country Codes

Uses ISO 3166-1 alpha-2 country codes:

  • US - United States
  • CA - Canada
  • GB - United Kingdom
  • CN - China
  • RU - Russia
  • KP - North Korea
  • etc.

Full list: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2

Blocking Behavior

Default Behavior:

  • If IP lookup fails → Allow (returns false)
  • If country not found → Allow (returns false)
  • If country is blocked → Block (returns true)

To block unknown IPs, modify IsBlockedIP to return true on error (line 101 in ipcountryblocker.go).

Thread Safety

The provider is thread-safe:

  • Uses sync.RWMutex for concurrent access to blocked countries map
  • GeoIP2 Reader is thread-safe by design
  • Safe to use in HTTP middleware and concurrent handlers

Performance

Lookup Speed:

  • In-memory database lookups are very fast (~microseconds)
  • Database is memory-mapped for efficiency
  • Suitable for high-traffic applications

Memory Usage:

  • GeoLite2-Country database: ~5-10 MB in memory
  • Blocked countries map: negligible (few KB)

References