211 lines
6.5 KiB
Go
211 lines
6.5 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"crypto/subtle"
|
|
"time"
|
|
|
|
"telesrv/internal/store"
|
|
)
|
|
|
|
func (s *CodeStore) VerifyLogin(_ context.Context, hash, phone, code string, keepForSignUp bool, defaultMaxAttempts int) (store.LoginCodeVerifyResult, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
entry, ok := s.liveCodeLocked(hash)
|
|
if !ok {
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyMissing}, nil
|
|
}
|
|
record := entry.code
|
|
if record.Version != store.PhoneCodeVersionCurrent {
|
|
s.deleteCodeLocked(hash, record)
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyMissing}, nil
|
|
}
|
|
// Sign-up verification is a terminal state for VerifyLogin. Keep the marker
|
|
// for the one caller that already received signUpRequired, but do not report
|
|
// a second Accepted result or let later wrong-code calls exhaust it.
|
|
if record.SignUpVerified {
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyMissing}, nil
|
|
}
|
|
if record.Purpose != "" || record.Phone != phone || !loginCodeVerifiable(record) || record.Code == "" || code == "" {
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyInvalid, Record: record}, nil
|
|
}
|
|
if subtle.ConstantTimeCompare([]byte(record.Code), []byte(code)) != 1 {
|
|
revision, err := store.NewPhoneCodeRevisionToken()
|
|
if err != nil {
|
|
return store.LoginCodeVerifyResult{}, err
|
|
}
|
|
record.Attempts++
|
|
record.Revision = revision
|
|
entry.code = record
|
|
maxAttempts := record.MaxAttempts
|
|
if maxAttempts <= 0 {
|
|
maxAttempts = defaultMaxAttempts
|
|
}
|
|
if maxAttempts <= 0 {
|
|
maxAttempts = 1
|
|
}
|
|
if record.Attempts >= maxAttempts {
|
|
s.deleteCodeLocked(hash, record)
|
|
} else {
|
|
s.m[hash] = entry
|
|
}
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyInvalid, Record: record}, nil
|
|
}
|
|
|
|
if keepForSignUp {
|
|
if record.IssuedUserID != 0 {
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyInvalid, Record: record}, nil
|
|
}
|
|
revision, err := store.NewPhoneCodeRevisionToken()
|
|
if err != nil {
|
|
return store.LoginCodeVerifyResult{}, err
|
|
}
|
|
record.SignUpVerified = true
|
|
record.Revision = revision
|
|
entry.code = record
|
|
s.m[hash] = entry
|
|
} else {
|
|
s.deleteCodeLocked(hash, record)
|
|
}
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyAccepted, Record: record}, nil
|
|
}
|
|
|
|
func (s *CodeStore) VerifyScoped(_ context.Context, hash string, scope store.PhoneCodeScope, code string, defaultMaxAttempts int) (store.LoginCodeVerifyResult, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if !scope.Valid() || s.scopes[scope] != hash {
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyMissing}, nil
|
|
}
|
|
entry, ok := s.liveCodeLocked(hash)
|
|
if !ok {
|
|
// liveCodeLocked removes the index when it can decode the stored scope;
|
|
// also close the stale-index-only case.
|
|
if s.scopes[scope] == hash {
|
|
delete(s.scopes, scope)
|
|
}
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyMissing}, nil
|
|
}
|
|
record := entry.code
|
|
if record.Version != store.PhoneCodeVersionCurrent || record.Scope() != scope || record.SignUpVerified || record.Code == "" {
|
|
// Fail closed on a legacy or internally inconsistent record. Clean both
|
|
// the scope encoded in the record and the scope that selected this hash.
|
|
s.deleteCodeLocked(hash, record)
|
|
if s.scopes[scope] == hash {
|
|
delete(s.scopes, scope)
|
|
}
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyMissing}, nil
|
|
}
|
|
if code == "" {
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyInvalid, Record: record}, nil
|
|
}
|
|
if subtle.ConstantTimeCompare([]byte(record.Code), []byte(code)) != 1 {
|
|
revision, err := store.NewPhoneCodeRevisionToken()
|
|
if err != nil {
|
|
return store.LoginCodeVerifyResult{}, err
|
|
}
|
|
record.Attempts++
|
|
record.Revision = revision
|
|
entry.code = record
|
|
maxAttempts := record.MaxAttempts
|
|
if maxAttempts <= 0 {
|
|
maxAttempts = defaultMaxAttempts
|
|
}
|
|
if maxAttempts <= 0 {
|
|
maxAttempts = 1
|
|
}
|
|
if record.Attempts >= maxAttempts {
|
|
s.deleteCodeLocked(hash, record)
|
|
} else {
|
|
s.m[hash] = entry
|
|
}
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyInvalid, Record: record}, nil
|
|
}
|
|
|
|
s.deleteCodeLocked(hash, record)
|
|
return store.LoginCodeVerifyResult{Status: store.LoginCodeVerifyAccepted, Record: record}, nil
|
|
}
|
|
|
|
func (s *CodeStore) ConsumeSignUpVerified(_ context.Context, hash, phone string) (store.PhoneCode, bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
entry, ok := s.liveCodeLocked(hash)
|
|
if !ok {
|
|
return store.PhoneCode{}, false, nil
|
|
}
|
|
record := entry.code
|
|
if record.Version != store.PhoneCodeVersionCurrent {
|
|
s.deleteCodeLocked(hash, record)
|
|
return store.PhoneCode{}, false, nil
|
|
}
|
|
if record.Purpose != "" || record.Phone != phone || !loginCodeVerifiable(record) || record.IssuedUserID != 0 || !record.SignUpVerified {
|
|
return store.PhoneCode{}, false, nil
|
|
}
|
|
s.deleteCodeLocked(hash, record)
|
|
return record, true, nil
|
|
}
|
|
|
|
func (s *CodeStore) TakeLoginCode(_ context.Context, hash, phone string) (store.PhoneCode, bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
entry, ok := s.liveCodeLocked(hash)
|
|
if !ok {
|
|
return store.PhoneCode{}, false, nil
|
|
}
|
|
record := entry.code
|
|
if record.Version != store.PhoneCodeVersionCurrent {
|
|
s.deleteCodeLocked(hash, record)
|
|
return store.PhoneCode{}, false, nil
|
|
}
|
|
if record.SignUpVerified {
|
|
return store.PhoneCode{}, false, nil
|
|
}
|
|
if record.Purpose != "" || record.Phone != phone || !loginCodeTakeable(record) {
|
|
return store.PhoneCode{}, false, nil
|
|
}
|
|
s.deleteCodeLocked(hash, record)
|
|
return record, true, nil
|
|
}
|
|
|
|
func (s *CodeStore) InvalidateLoginCode(_ context.Context, hash, phone string) (bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
entry, ok := s.liveCodeLocked(hash)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
record := entry.code
|
|
if record.Version != store.PhoneCodeVersionCurrent {
|
|
s.deleteCodeLocked(hash, record)
|
|
return false, nil
|
|
}
|
|
if record.Purpose != "" || record.Phone != phone || !loginCodeTakeable(record) {
|
|
return false, nil
|
|
}
|
|
s.deleteCodeLocked(hash, record)
|
|
return true, nil
|
|
}
|
|
|
|
func loginCodeVerifiable(record store.PhoneCode) bool {
|
|
return record.Channel == store.PhoneCodeChannelPhone || record.Channel == store.PhoneCodeChannelSMS || record.Channel == store.PhoneCodeChannelEmailLogin
|
|
}
|
|
|
|
func loginCodeTakeable(record store.PhoneCode) bool {
|
|
return loginCodeVerifiable(record) || record.Channel == store.PhoneCodeChannelEmailSetupRequired
|
|
}
|
|
|
|
func (s *CodeStore) liveCodeLocked(hash string) (codeEntry, bool) {
|
|
entry, ok := s.m[hash]
|
|
if !ok {
|
|
return codeEntry{}, false
|
|
}
|
|
if time.Now().After(entry.expires) {
|
|
s.deleteCodeLocked(hash, entry.code)
|
|
return codeEntry{}, false
|
|
}
|
|
return entry, true
|
|
}
|