TOTP Secret Encryption
Overview
TOTP (Time-based One-Time Password) secrets are sensitive cryptographic keys that must be protected at rest. The SDK provides a pluggable encryption system via the SecretEncryptor interface, with two implementations:
- AESEncryptor - Production-ready AES-256-GCM encryption
- NoopEncryptor - Development-only plaintext storage (no encryption)
Production Setup
1. Generate Encryption Key
Generate a strong random encryption key using OpenSSL:
openssl rand -base64 32Example output:
XyZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFG=2. Configure Environment Variable
Add the encryption key to your .env file:
# REQUIRED for production - DO NOT commit to version control
TOTP_ENCRYPTION_KEY=XyZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFG=3. Verify Configuration
The SDK automatically selects the appropriate encryptor:
- With encryption key: Uses
AESEncryptor(production) - Without encryption key: Uses
NoopEncryptor(development only)
Production Environment Protection: The system will fail to start only when 2FA is enabled and
TOTP_ENCRYPTION_KEYis missing in production (and test endpoints are disabled). If 2FA is disabled, the key is optional.
Check the module initialization in modules/core/module.go:
// In production, require TOTP_ENCRYPTION_KEY only when 2FA is enabled
if !conf.EnableTestEndpoints &&
conf.GoAppEnvironment == "production" &&
conf.TwoFactorAuth.Enabled &&
conf.TwoFactorAuth.EncryptionKey == "" {
return serrors.E(op, serrors.Invalid, errors.New("TOTP encryption key is required in production"))
}
if conf.TwoFactorAuth.EncryptionKey != "" {
// Production: Use AES-256-GCM encryption
encryptor = pkgtwofactor.NewAESEncryptor(conf.TwoFactorAuth.EncryptionKey)
} else {
// Development: Use plaintext (NoopEncryptor)
// WARNING: Never use in production!
encryptor = pkgtwofactor.NewNoopEncryptor()
}Development Setup
For local development, you can omit the TOTP_ENCRYPTION_KEY variable. The system will use NoopEncryptor which stores secrets in plaintext.
IMPORTANT: Startup fails only when 2FA is enabled in production and
TOTP_ENCRYPTION_KEYis not set. If 2FA is disabled, the key is not required.
Security Considerations
Key Management
- Keep Secret: Never commit encryption keys to version control
- Rotate Regularly: Change encryption keys periodically (quarterly recommended)
- Access Control: Limit access to encryption keys to authorized personnel only
- Environment Isolation: Use different keys for staging and production
Key Rotation
When rotating encryption keys:
- Generate a new encryption key
- Deploy the new key to the environment
- Re-encrypt existing TOTP secrets with the new key
- Update the environment variable
Note: Key rotation requires a maintenance window to re-encrypt existing secrets.
AES-256-GCM Details
The AESEncryptor uses AES-256-GCM (Galois/Counter Mode) which provides:
- Confidentiality: Secrets are encrypted using AES-256
- Authenticity: GCM mode detects tampering attempts
- Nonce-based: Each encryption uses a unique random nonce
- Key Derivation: Input key strings are hashed with SHA-256 to derive a consistent 32-byte key
Encryption Process
- Hash encryption key string with SHA-256 → 32-byte key
- Create AES cipher with 32-byte key (AES-256)
- Wrap cipher with GCM for authenticated encryption
- Generate random 12-byte nonce (GCM standard nonce size)
- Encrypt plaintext with nonce
- Prepend nonce to ciphertext (needed for decryption)
- Encode result as base64 for safe storage
Decryption Process
- Decode base64-encoded ciphertext
- Extract nonce from beginning of ciphertext
- Create AES cipher with 32-byte key
- Wrap cipher with GCM for authenticated decryption
- Decrypt and authenticate ciphertext
- Return plaintext secret
Advanced: Custom Encryptors
For enterprise requirements, you can implement custom encryptors using cloud KMS services:
// Example: AWS KMS Encryptor
type KMSEncryptor struct {
kmsClient *kms.Client
keyID string
}
func (e *KMSEncryptor) Encrypt(ctx context.Context, plaintext string) (string, error) {
result, err := e.kmsClient.Encrypt(ctx, &kms.EncryptInput{
KeyId: aws.String(e.keyID),
Plaintext: []byte(plaintext),
})
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(result.CiphertextBlob), nil
}
func (e *KMSEncryptor) Decrypt(ctx context.Context, ciphertext string) (string, error) {
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
result, err := e.kmsClient.Decrypt(ctx, &kms.DecryptInput{
CiphertextBlob: data,
})
if err != nil {
return "", err
}
return string(result.Plaintext), nil
}Then wire it in modules/core/module.go:
// Production with AWS KMS
encryptor := NewKMSEncryptor(kmsClient, keyID)
twoFactorService := twofactor.NewTwoFactorService(
otpRepo,
recoveryCodeRepo,
userRepo,
twofactor.WithSecretEncryptor(encryptor),
// ... other options
)Testing
The SDK includes comprehensive tests for the AES encryptor:
# Run all encryptor tests
go test -v ./pkg/twofactor -run TestAESEncryptor
# Run integration tests
go test -v ./pkg/twofactor -run IntegrationTest coverage includes:
- Encrypt/decrypt round-trip
- Different keys produce different ciphertexts
- Same plaintext with same key produces different ciphertexts (random nonces)
- Invalid ciphertext detection
- Tampered ciphertext detection (GCM authentication)
- Key derivation consistency
- Base64 encoding validation
- Long plaintext handling
Compliance
AES-256-GCM encryption helps meet compliance requirements for:
- PCI-DSS: Payment Card Industry Data Security Standard
- HIPAA: Health Insurance Portability and Accountability Act
- GDPR: General Data Protection Regulation
- SOC 2: Service Organization Control 2
Consult your security team for specific compliance requirements in your jurisdiction.