totp

package
v0.0.0-...-6067653 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2025 License: Apache-2.0 Imports: 18 Imported by: 0

README

TOTP Package

A secure Time-based One-Time Password (TOTP) implementation with AES-256 encryption for two-factor authentication systems.

Overview

The totp package provides a complete solution for implementing secure two-factor authentication (2FA) using Time-based One-Time Passwords (TOTP). It offers functionality for generating and validating TOTP codes compliant with RFC 6238, managing secrets with AES-256-GCM encryption, and handling recovery codes. The package is thread-safe and suitable for concurrent use in production applications.

Features

  • RFC 6238 compliant TOTP generation and validation
  • AES-256-GCM encryption for secure storage of TOTP secrets
  • Secure recovery code generation and validation
  • QR code URI generation compatible with all authenticator apps
  • Constant-time comparison to prevent timing attacks
  • Comprehensive error handling with proper error wrapping
  • Thread-safe implementation with no global state

Usage

Setting Up Encryption
import (
	"encoding/base64"
	"fmt"
	"github.com/dmitrymomot/saaskit/pkg/totp"
)

// Generate a new encryption key (store this securely)
key, err := totp.GenerateEncryptionKey()
if err != nil {
	switch {
	case errors.Is(err, totp.ErrFailedToGenerateEncryptionKey):
		// Handle specific error
	default:
		// Handle other errors
	}
}
// Encode key for storage in environment variables
encodedKey := base64.StdEncoding.EncodeToString(key)
fmt.Println("Save this key in your secure configuration:", encodedKey)
// Returns: A base64-encoded 32-byte encryption key
Using the CLI Tool

For convenience, you can use the included CLI tool to generate both a TOTP secret key and an encoded encryption key:

# Run the CLI tool from the project root
go run internal/pkg/totp/cmd/main.go

This will output both values:

Generated Encoded Encryption Key (for TOTP_ENCRYPTION_KEY env var):
———
OUVdVgolBab6kePjN3s5fUZiqTdOIydh+zL5vn0Eu30=
———

You can then set the encryption key in your environment:

export TOTP_ENCRYPTION_KEY="OUVdVgolBab6kePjN3s5fUZiqTdOIydh+zL5vn0Eu30="
Generating and Encrypting TOTP Secrets
// Generate a new TOTP secret for a user
secret, err := totp.GenerateSecretKey()
if err != nil {
	// Handle error
}
// Returns: A base32-encoded secret like "JBSWY3DPEHPK3PXP"

// Encrypt the secret before storage
encryptedSecret, err := totp.EncryptSecret(secret, key)
if err != nil {
	// Handle error
}
// Returns: A base64-encoded encrypted string

// Create a URI for QR code display
params := totp.TOTPParams{
	Secret:      secret,
	AccountName: "user@example.com",
	Issuer:      "MyApp",
}
uri, err := totp.GetTOTPURI(params)
if err != nil {
	// Handle error
}
// Returns: "otpauth://totp/MyApp:user%40example.com?algorithm=SHA1&digits=6&issuer=MyApp&period=30&secret=JBSWY3DPEHPK3PXP"

// Use this URI with a QR code generator library to display to the user
Validating TOTP Codes
// Retrieve encrypted secret from storage and decrypt it
key, err := totp.LoadEncryptionKey() // Load from environment
if err != nil {
	// Handle error
}

secret, err := totp.DecryptSecret(encryptedSecret, key)
if err != nil {
	// Handle error
}

// Validate a TOTP code provided by the user
userProvidedCode := "123456" // From user input

valid, err := totp.ValidateTOTP(secret, userProvidedCode)
if err != nil {
	switch {
	case errors.Is(err, totp.ErrInvalidSecret):
		// Handle invalid secret format
	case errors.Is(err, totp.ErrInvalidOTP):
		// Handle invalid OTP format
	default:
		// Handle other errors
	}
}

if valid {
	// Authentication successful, grant access
} else {
	// Authentication failed
}

// Generate a current TOTP code (useful for testing)
currentCode, err := totp.GenerateTOTP(secret)
if err != nil {
	// Handle error
}
// Returns: A 6-digit code like "123456"
Recovery Codes
// Generate a set of recovery codes (typically done during 2FA setup)
recoveryCodes, err := totp.GenerateRecoveryCodes(8) // Generate 8 codes
if err != nil {
	// Handle error
}
// Returns: ["1A2B3C4D", "5E6F7G8H", ...] (8 codes)

// Hash and store codes in database
var hashedCodes []string
for _, code := range recoveryCodes {
	hashedCode := totp.HashRecoveryCode(code)
	hashedCodes = append(hashedCodes, hashedCode)
	// Store hashedCode in database
}

// Provide the original unhashed codes to user for backup

// Validating a recovery code during account recovery
userProvidedCode := "1A2B3C4D" // From user input

// Check against stored hashed codes
for _, hashedCode := range hashedCodes {
	if totp.VerifyRecoveryCode(userProvidedCode, hashedCode) {
		// Valid recovery code - remove from database and reset 2FA
		break
	}
}
Custom TOTP Parameters
// Create custom TOTP parameters
customParams := totp.TOTPParams{
	Secret:      secret,
	AccountName: "user@example.com",
	Issuer:      "MyApp",
	Algorithm:   "SHA1",
	Digits:      8,        // 8 digits instead of default 6
	Period:      60,       // 60 second period instead of default 30
}

// Generate URI with custom parameters
customURI, err := totp.GetTOTPURI(customParams)
if err != nil {
	// Handle error
}

// ValidateTOTP uses default parameters (6 digits, 30 seconds)
// For custom validation, you would need to implement it using the GenerateHOTP function

Best Practices

  1. Secret Management:

    • Always encrypt TOTP secrets before storage
    • Store encryption keys securely (environment variables, key vaults)
    • Use different encryption keys for different environments
    • Never log or expose secrets in plaintext
  2. Authentication Security:

    • Implement rate limiting for TOTP attempts
    • Add exponential backoff for repeated failures
    • Set up proper audit logging for authentication attempts
    • Consider browser/IP fingerprinting for additional security
  3. Recovery Codes:

    • Only store hashed recovery codes
    • Use a one-time-use policy for recovery codes
    • Notify users when recovery codes are used
    • Allow generation of new recovery codes
  4. Implementation:

    • Use constant-time comparison for validation
    • Follow RFC 6238 specification
    • Maintain backward compatibility when rotating TOTP algorithms
    • Test with actual authenticator apps (Google Authenticator, Authy)

API Reference

Types
// TOTPParams contains the parameters for TOTP URI generation
type TOTPParams struct {
	Secret      string // TOTP secret key (required)
	AccountName string // Name of the account (required)
	Issuer      string // Name of the issuer (required)
	Algorithm   string // Algorithm used (optional, default "SHA1")
	Digits      int    // Number of digits (optional, default 6)
	Period      int    // Period in seconds (optional, default 30)
}
TOTP Functions
// GenerateSecretKey generates a new Base32-encoded secret key for TOTP
func GenerateSecretKey() (string, error)

// GetTOTPURI creates a properly encoded TOTP URI for use with authenticator apps
func GetTOTPURI(params TOTPParams) (string, error)

// ValidateTOTP validates the TOTP code provided by the user
func ValidateTOTP(secret, otp string) (bool, error)

// GenerateTOTP generates a TOTP code for the current time period
func GenerateTOTP(secret string) (string, error)

// GenerateHOTP generates an HOTP code (internal function)
func GenerateHOTP(key []byte, counter int64, digits int) int

// GenerateTOTPWithTime generates a TOTP code for a specific time
func GenerateTOTPWithTime(secret string, t time.Time) (string, error)
Encryption Functions
// EncryptSecret encrypts a TOTP secret using AES-256-GCM
func EncryptSecret(plainText string, key []byte) (string, error)

// DecryptSecret decrypts an encrypted TOTP secret
func DecryptSecret(cipherTextBase64 string, key []byte) (string, error)

// GenerateEncryptionKey creates a new random 32-byte key for AES-256
func GenerateEncryptionKey() ([]byte, error)

// GenerateEncodedEncryptionKey generates a base64-encoded encryption key
func GenerateEncodedEncryptionKey() (string, error)

// LoadEncryptionKey loads the encryption key from environment variables
func LoadEncryptionKey() ([]byte, error)
Recovery Code Functions
// GenerateRecoveryCodes generates a set of recovery codes
func GenerateRecoveryCodes(count int) ([]string, error)

// HashRecoveryCode creates a hash of a recovery code for secure storage
func HashRecoveryCode(code string) string

// VerifyRecoveryCode performs a secure constant-time comparison
func VerifyRecoveryCode(code, hashedCode string) bool
Error Types
var ErrFailedToEncryptSecret = errors.New("failed to encrypt TOTP secret")
var ErrFailedToDecryptSecret = errors.New("failed to decrypt TOTP secret")
var ErrInvalidCipherTooShort = errors.New("cipher text too short")
var ErrFailedToGenerateEncryptionKey = errors.New("failed to generate encryption key")
var ErrFailedToLoadEncryptionKey = errors.New("failed to load encryption key")
var ErrInvalidEncryptionKeyLength = errors.New("invalid encryption key length")
var ErrFailedToGenerateSecretKey = errors.New("failed to generate TOTP secret key")
var ErrFailedToValidateTOTP = errors.New("failed to validate TOTP")
var ErrMissingSecret = errors.New("missing secret")
var ErrInvalidSecret = errors.New("invalid secret")
var ErrMissingAccountName = errors.New("missing account name")
var ErrMissingIssuer = errors.New("missing issuer")
var ErrEncryptionKeyNotSet = errors.New("TOTP encryption key not set")
var ErrInvalidOTP = errors.New("invalid OTP format")
var ErrInvalidRecoveryCodeCount = errors.New("invalid recovery code count, must be greater than 0")
var ErrFailedToGenerateRecoveryCode = errors.New("failed to generate recovery code")
var ErrFailedToGenerateTOTP = errors.New("failed to generate TOTP")
Configuration

The package uses the following environment variable:

TOTP_ENCRYPTION_KEY = base64-encoded 32-byte key

Documentation

Overview

Package totp provides a high-level API for generating, encrypting, validating, and managing Time-based One-Time Passwords (TOTP) and related recovery codes.

This package bundles together everything an application needs to implement multi-factor authentication based on RFC 6238 including secret key creation, URI generation compatible with authenticator applications, one-time-password generation/validation, AES-256 encryption helpers for safely persisting secrets, and secure recovery-code utilities.

By keeping functionality self-contained the package eliminates direct dependencies on third-party TOTP libraries and allows services to remain framework-agnostic while still following contemporary security best-practices.

Architecture

Internally the package is divided into three cohesive layers.

  • crypto – helpers in aes256.go implement symmetric encryption/decryption of the secret key with AES-256-GCM as well as random key generation utilities.

  • totp – functions in otp.go provide secret key generation (GenerateSecretKey), HOTP/TOTP code calculation (GenerateTOTP/ValidateTOTP/GenerateHOTP) and convenient URI construction (GetTOTPURI) for onboarding to Google Authenticator, 1Password and compatible apps.

  • recovery – helpers in recovery.go create, hash and verify single-use recovery codes that can be offered to users in case they permanently lose access to their authenticator device.

Configuration such as the encryption key is loaded once per process via the env tag aware loader in config.go. The required environment variable name is TOTP_ENCRYPTION_KEY and it must contain a Base64 encoded 32-byte key suitable for AES-256.

Usage

The minimal happy path for enrolling a user looks like this:

package main

import (
    "fmt"
    "github.com/dmitrymomot/saaskit/pkg/totp"
)

func main() {
    // 1. Create a brand-new secret
    secret, _ := totp.GenerateSecretKey()

    // 2. Persist the secret encrypted in your datastore
    var cfg totp.Config
    // Load config from environment using github.com/caarlos0/env
    // env.Parse(&cfg)
    key, _ := totp.GetEncryptionKey(cfg)
    encSecret, _ := totp.EncryptSecret(secret, key)

    // 3. Display the bootstrap URI/QR code to the user
    uri, _ := totp.GetTOTPURI(totp.TOTPParams{
        Secret:      secret,
        AccountName: "alice@example.com",
        Issuer:      "Acme",
    })
    fmt.Println(uri)

    // 4. Later – validate an OTP provided by the user
    ok, _ := totp.ValidateTOTP(secret, "123456")
    fmt.Println(ok)
}

Additional helpers exist for generating time-window agnostic codes (GenerateTOTPWithTime), producing export-friendly Base64 keys (GenerateEncodedEncryptionKey) and creating or verifying recovery codes (GenerateRecoveryCodes, HashRecoveryCode, VerifyRecoveryCode).

Error Handling

Every exported operation returns a descriptive error that may be wrapped using errors.Join. Inspect errors with errors.Is against package level sentinels such as ErrInvalidSecret, ErrFailedToEncryptSecret, ErrInvalidOTP etc.

See Also

  • RFC 4226 – HMAC-Based One-Time Password (HOTP) Algorithm
  • RFC 6238 – Time-Based One-Time Password (TOTP) Algorithm

To explore more usage scenarios refer to the package level examples and unit-tests.

Index

Constants

View Source
const (
	DefaultDigits    = 6      // Standard 6-digit TOTP codes
	DefaultPeriod    = 30     // 30-second validity window (RFC 6238 standard)
	DefaultAlgorithm = "SHA1" // HMAC-SHA1 algorithm (RFC 6238 standard)
)
View Source
const (
	AESKeySize = 32 // Required key size for AES-256 (256 bits / 8 = 32 bytes)
)

Variables

View Source
var (
	ErrFailedToEncryptSecret         = errors.New("failed to encrypt TOTP secret")
	ErrFailedToDecryptSecret         = errors.New("failed to decrypt TOTP secret")
	ErrInvalidCipherTooShort         = errors.New("cipher text too short")
	ErrFailedToGenerateEncryptionKey = errors.New("failed to generate encryption key")
	ErrFailedToLoadEncryptionKey     = errors.New("failed to load encryption key")
	ErrInvalidEncryptionKeyLength    = errors.New("invalid encryption key length")
	ErrFailedToGenerateSecretKey     = errors.New("failed to generate TOTP secret key")
	ErrFailedToValidateTOTP          = errors.New("failed to validate TOTP")
	ErrMissingSecret                 = errors.New("missing secret")
	ErrInvalidSecret                 = errors.New("invalid secret")
	ErrMissingAccountName            = errors.New("missing account name")
	ErrMissingIssuer                 = errors.New("missing issuer")
	ErrEncryptionKeyNotSet           = errors.New("TOTP encryption key not set")
	ErrInvalidOTP                    = errors.New("invalid OTP format")
	ErrInvalidRecoveryCodeCount      = errors.New("invalid recovery code count, must be greater than 0")
	ErrFailedToGenerateRecoveryCode  = errors.New("failed to generate recovery code")
	ErrFailedToGenerateTOTP          = errors.New("failed to generate TOTP")
)
View Source
var (
	// ValidateSecretKeyRegex ensures Base32 format: uppercase A-Z, digits 2-7, optional padding
	ValidateSecretKeyRegex = regexp.MustCompile("^[A-Z2-7]+=*$")
)

Functions

func DecryptSecret

func DecryptSecret(cipherTextBase64 string, key []byte) (string, error)

DecryptSecret decrypts the encrypted TOTP secret. Expects the ciphertext as a base64-encoded string.

func EncryptSecret

func EncryptSecret(plainText string, key []byte) (string, error)

EncryptSecret encrypts the TOTP secret using AES-256-GCM. Returns the ciphertext as a base64-encoded string.

func GenerateEncodedEncryptionKey

func GenerateEncodedEncryptionKey() (string, error)

GenerateEncodedEncryptionKey generates a new random 32-byte key suitable for AES-256 encryption. Returns the generated key as a base64-encoded string or an error if the random number generation fails. This function is useful for generating a new key and storing it in the configuration.

func GenerateEncryptionKey

func GenerateEncryptionKey() ([]byte, error)

GenerateEncryptionKey creates a new random 32-byte key suitable for AES-256 encryption. Returns the generated key or an error if the random number generation fails.

func GenerateHOTP

func GenerateHOTP(key []byte, counter int64, digits int) int

GenerateHOTP implements RFC 4226 HMAC-based One-Time Password algorithm. The algorithm converts a counter value into a numeric code using HMAC-SHA1.

func GenerateRecoveryCodes

func GenerateRecoveryCodes(count int) ([]string, error)

GenerateRecoveryCodes creates cryptographically secure backup codes for account recovery. Each code is a 16-character hexadecimal string (64 bits of entropy).

func GenerateSecretKey

func GenerateSecretKey() (string, error)

GenerateSecretKey generates a new Base32-encoded secret key for TOTP.

func GenerateTOTP

func GenerateTOTP(secret string) (string, error)

GenerateTOTP generates a time-based one-time password for the current 30-second window. The secret must be a valid Base32-encoded string.

func GenerateTOTPWithTime

func GenerateTOTPWithTime(secret string, t time.Time) (string, error)

GenerateTOTPWithTime generates a TOTP code for the 30-second window containing the specified time. Useful for testing or generating codes for specific moments.

func GetEncryptionKey

func GetEncryptionKey(cfg Config) ([]byte, error)

GetEncryptionKey decodes the encryption key from the configuration. The key must be a 32-byte base64-encoded string. Returns the decoded key bytes or an error if decoding fails or the key length is invalid.

func GetTOTPURI

func GetTOTPURI(params TOTPParams) (string, error)

GetTOTPURI creates a properly encoded TOTP URI for use with authenticator apps. The URI format follows the Key Uri Format specification: https://github.com/google/google-authenticator/wiki/Key-Uri-Format

func HashRecoveryCode

func HashRecoveryCode(code string) string

HashRecoveryCode creates a SHA-256 hash for secure storage of recovery codes.

func ValidateTOTP

func ValidateTOTP(secret, otp string) (bool, error)

ValidateTOTP validates the TOTP code provided by the user.

func VerifyRecoveryCode

func VerifyRecoveryCode(code, hashedCode string) bool

VerifyRecoveryCode performs constant-time comparison to prevent timing attacks. Essential for security: comparison time must not reveal information about where differences occur.

Types

type Config

type Config struct {
	EncryptionKey string `env:"TOTP_ENCRYPTION_KEY,required"`
}

type TOTPParams

type TOTPParams struct {
	Secret      string // Base32-encoded TOTP secret key (required)
	AccountName string // User identifier like email (required)
	Issuer      string // Service name displayed in authenticator apps (required)
	Algorithm   string // HMAC algorithm (optional, defaults to SHA1)
	Digits      int    // Number of digits in generated codes (optional, defaults to 6)
	Period      int    // Code validity period in seconds (optional, defaults to 30)
}

TOTPParams contains the parameters for TOTP URI generation

func (TOTPParams) GetDefaults

func (p TOTPParams) GetDefaults() TOTPParams

GetDefaults returns a copy with RFC 6238 standard defaults applied to zero-valued fields

func (TOTPParams) Validate

func (p TOTPParams) Validate() error

Validate ensures all required TOTP parameters are present and valid

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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