package security import ( "crypto/hmac" "crypto/sha256" "errors" "fmt" ) // ErrUnknownPepperVersion is returned when a credential references a pepper version that is not // configured. It means the pepper key for that version was removed before all credentials written // with it were rehashed, so verify cannot proceed. Treat it as a configuration error, not a bad // password. var ErrUnknownPepperVersion = errors.New("security: unknown pepper version") // Pepper applies a versioned secret to a password before the KDF runs. The pepper is a server held // key (loaded from the secrets file, never the database), so a stolen password hash alone cannot be // cracked offline without also stealing the pepper. Applying it as HMAC SHA 256 keeps the operation // inside the FIPS validated module. // // Versioning is what makes rotation safe: every credential records the pepper version it was written // with, so a new active key can be introduced while old hashes still verify under their stored // version and get upgraded on the next successful login. type Pepper struct { active int keys map[int][]byte } // NewPepper builds a Pepper from a map of version to key with a designated active version. Each key // must be non empty and the active version must be present. func NewPepper(active int, keys map[int][]byte) (*Pepper, error) { if len(keys) == 0 { return nil, errors.New("security: pepper needs at least one key") } copied := make(map[int][]byte, len(keys)) for v, k := range keys { if len(k) == 0 { return nil, fmt.Errorf("security: pepper version %d has an empty key", v) } b := make([]byte, len(k)) copy(b, k) copied[v] = b } if _, ok := copied[active]; !ok { return nil, fmt.Errorf("security: active pepper version %d has no key", active) } return &Pepper{active: active, keys: copied}, nil } // ActiveVersion is the version new credentials should be peppered with. func (p *Pepper) ActiveVersion() int { return p.active } // Apply returns HMAC SHA 256 of the password keyed by the requested pepper version. The output is a // fixed 32 byte value that becomes the input to PBKDF2. func (p *Pepper) Apply(version int, password string) ([]byte, error) { key, ok := p.keys[version] if !ok { return nil, ErrUnknownPepperVersion } mac := hmac.New(sha256.New, key) mac.Write([]byte(password)) return mac.Sum(nil), nil }