mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# Which Problems Are Solved TOTP codes could be reused, violating [RFC 6238, section 5.2](https://datatracker.ietf.org/doc/html/rfc6238#section-5.2). # How the Problems Are Solved Transmitted codes are now recorded in each success event. As the codes amount to secrets, they are not stored verbatim but as a randomly salted HMAC-SHA256 (`crypto.HMACValue`). Each value gets a fresh random salt, so an equal code always produces a different stored value and two entries can never be recognized as being derived from the same code. This is deliberately not a replacement for proper secret storage: with the salt stored alongside the hash and a 6 digit payload, anyone with event access can brute-force the code in milliseconds. That is acceptable here because a code is worthless once its window has passed. The `HMACValue` doc comment states the same. TOTP codes are low-entropy (6 digits), pseudo random codes. Therefore it is possible for the same code to be generated more than once over the lifetime of a user. In order to prevent the (unlikely) case where users login with large time intervalls and a generated code may be equal to the one last used a long time ago, we only check for codes used within the recent period window. A simpler implementation would be simply storing the latest consumed step, but the TOTP library does not return this value. Because of the allowed skew there are always 3 steps within the validation window, so we can't calculate the current step based on time alone. The retained history spans one period more than the validation window: validity is decided by the app server's clock, while the stored events carry the database clock, and the extra period absorbs the drift between them. **Note:** a reused code pushes a distinct `user.human.mfa.otp.check.reused` event, not a failed check event. A replay therefore stays observable through the Events API without counting towards the lockout policy. Otherwise anyone who captured a code could replay it to lock the user out. # Additional Changes - Read only the latest events in recheck query # Additional Context - Closes https://github.com/zitadel/zitadel/issues/6452 --------- Co-authored-by: Livio Spring <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: muhlemmer <[email protected]>
50 lines
1.6 KiB
Go
50 lines
1.6 KiB
Go
package crypto
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
)
|
|
|
|
// HMACValue is a randomly salted HMAC-SHA256 of a payload.
|
|
// It allows checking whether a payload was seen before,
|
|
// without having to store the payload itself.
|
|
//
|
|
// It is intended for low entropy secrets, such as 6 digit TOTP codes:
|
|
// the random salt makes each value unique, even for an equal payload,
|
|
// which counters rainbow table usage and prevents that two stored values
|
|
// can be recognized as being derived from the same payload.
|
|
//
|
|
// As the salt is stored alongside the hash, an HMACValue is not suitable
|
|
// as a replacement for retrievable secrets. Use [CryptoValue] for that.
|
|
//
|
|
// Both fields are JSON serializable, so a value can be stored in an event payload.
|
|
type HMACValue struct {
|
|
Hash []byte `json:"hash"`
|
|
Salt []byte `json:"salt"`
|
|
}
|
|
|
|
// NewHMACValue returns the HMAC of payload, using a newly generated random salt.
|
|
// Two calls with an equal payload always return different values.
|
|
func NewHMACValue(payload string) *HMACValue {
|
|
salt := make([]byte, 16)
|
|
// [rand.Read] never returns an error, it panics if the system source fails.
|
|
rand.Read(salt)
|
|
hasher := hmac.New(sha256.New, salt)
|
|
hasher.Write([]byte(payload))
|
|
|
|
return &HMACValue{
|
|
Hash: hasher.Sum(nil),
|
|
Salt: salt,
|
|
}
|
|
}
|
|
|
|
// Equal reports whether payload is the one v was created from.
|
|
// The hashes are compared in constant time.
|
|
func (v *HMACValue) Equal(payload string) bool {
|
|
hasher := hmac.New(sha256.New, v.Salt)
|
|
hasher.Write([]byte(payload))
|
|
expectedHash := hasher.Sum(nil)
|
|
return hmac.Equal(v.Hash, expectedHash)
|
|
}
|