Auth: Refine passcode recovery code comparison

This commit is contained in:
Michael Mayer 2026-06-11 09:37:40 +00:00
parent 08945d25d7
commit 71a82a9ac9
2 changed files with 19 additions and 2 deletions

View file

@ -2,6 +2,7 @@ package entity
import (
"bytes"
"crypto/subtle"
"errors"
"fmt"
"image"
@ -254,8 +255,9 @@ func (m *Passcode) Valid(code string) (valid bool, recovery bool, err error) {
return false, false, authn.ErrInvalidPasscodeKey
}
// Check if recovery code has been used.
if m.RecoveryCode == code {
// Check if recovery code has been used, comparing in constant time so the
// stored secret does not leak through response timing.
if m.RecoveryCode != "" && subtle.ConstantTimeCompare([]byte(m.RecoveryCode), []byte(code)) == 1 {
return true, true, nil
}

View file

@ -381,6 +381,21 @@ func TestPasscode_Verify(t *testing.T) {
assert.True(t, recoveryCode)
assert.Nil(t, m.VerifiedAt)
})
t.Run("EmptyRecoveryCodeNeverMatches", func(t *testing.T) {
m := &Passcode{
UID: "uqxc08w3d0ej2283",
KeyURL: "otpauth://totp/Example:alice",
RecoveryCode: "",
}
// An unset recovery code must not match any submitted code (constant-time
// compare returns 0 on a length mismatch, but the guard makes intent explicit).
valid, recoveryCode, err := m.Valid("123456")
if err != nil {
t.Fatal(err)
}
assert.False(t, valid)
assert.False(t, recoveryCode)
})
t.Run("ErrPasscodeRequired", func(t *testing.T) {
m := &Passcode{
UID: "uqxc08w3d0ej2283",