package security import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/base32" "errors" "fmt" ) const ( recoveryCodeBytes = 10 // 80 bits of entropy, 16 base32 characters defaultRecoveryNum = 10 ) var recoveryBase32 = base32.StdEncoding.WithPadding(base32.NoPadding) // GenerateRecoveryCodes returns count single use backup codes in plaintext. They are shown to the // user exactly once; the database stores only their PBKDF2 hash plus a lookup tag (see Tagger). Each // code is unpadded base32 with no separators, so it carries no display punctuation. func GenerateRecoveryCodes(count int) ([]string, error) { if count <= 0 { count = defaultRecoveryNum } codes := make([]string, 0, count) for i := 0; i < count; i++ { b := make([]byte, recoveryCodeBytes) if _, err := rand.Read(b); err != nil { return nil, fmt.Errorf("security: read recovery code: %w", err) } codes = append(codes, recoveryBase32.EncodeToString(b)) } return codes, nil } // Tagger derives a non secret lookup tag for a value with HMAC SHA 256 under a server held key. A // recovery attempt looks up the candidate row by tag and then runs exactly one PBKDF2 verify against // that row, instead of looping the submitted code against every stored 600000 iteration hash. That // closes the recovery code CPU amplification path while keeping the tag useless to anyone without // the server key. type Tagger struct{ key []byte } // NewTagger builds a Tagger from a server held key. func NewTagger(key []byte) (*Tagger, error) { if len(key) == 0 { return nil, errors.New("security: tagger needs a non empty key") } b := make([]byte, len(key)) copy(b, key) return &Tagger{key: b}, nil } // Tag returns the lookup tag for a value. func (t *Tagger) Tag(value string) []byte { mac := hmac.New(sha256.New, t.key) mac.Write([]byte(value)) return mac.Sum(nil) } // Equal compares two tags in constant time. func (t *Tagger) Equal(a, b []byte) bool { return hmac.Equal(a, b) }