Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
520
cloud/maplepress-backend/pkg/security/README.md
Normal file
520
cloud/maplepress-backend/pkg/security/README.md
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
# 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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
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**:
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
- [Argon2 RFC](https://tools.ietf.org/html/rfc9106)
|
||||
- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
|
||||
- [memguard Documentation](https://github.com/awnumar/memguard)
|
||||
- [Alex Edwards: How to Hash and Verify Passwords With Argon2 in Go](https://www.alexedwards.net/blog/how-to-hash-and-verify-passwords-with-argon2-in-go)
|
||||
- [MaxMind GeoLite2 Free Geolocation Data](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data)
|
||||
- [ISO 3166-1 Country Codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
|
||||
Loading…
Add table
Add a link
Reference in a new issue