merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
|
|
@ -64,7 +64,10 @@ func validPhone(phone string) bool {
|
|||
}
|
||||
|
||||
func systemUserLoginForbidden(u domain.User) bool {
|
||||
return domain.IsSystemUserID(u.ID)
|
||||
// Login-facing callers deliberately use the same non-enumerating error for
|
||||
// reserved identities and irreversible tombstones. The durable authorization
|
||||
// store repeats the deleted check under the user lock to close the TOCTOU gap.
|
||||
return u.Deleted || domain.IsSystemUserID(u.ID)
|
||||
}
|
||||
|
||||
func systemLoginPhoneForbidden(phone string) bool {
|
||||
|
|
@ -270,12 +273,14 @@ func NewService(users store.UserStore, auths store.AuthorizationStore, codes sto
|
|||
}
|
||||
|
||||
// BindTempAuthKey 校验并记录 TDesktop PFS temp→perm auth key 绑定。
|
||||
func (s *Service) BindTempAuthKey(ctx context.Context, sessionID int64, binding domain.TempAuthKeyBinding) error {
|
||||
func (s *Service) BindTempAuthKey(ctx context.Context, sessionID int64, binding domain.TempAuthKeyBinding) (domain.TempAuthKeyBindingResult, error) {
|
||||
var validated domain.TempAuthKeyBindingResult
|
||||
if s.authKeys != nil {
|
||||
inner, protocolExpiresAt, err := s.validateBindTempAuthKey(ctx, sessionID, binding)
|
||||
inner, protocolExpiresAt, result, err := s.validateBindTempAuthKey(ctx, sessionID, binding)
|
||||
if err != nil {
|
||||
return err
|
||||
return domain.TempAuthKeyBindingResult{}, err
|
||||
}
|
||||
validated = result
|
||||
binding.TempSessionID = inner.TempSessionID
|
||||
// The bind request's expires_at is a signed client assertion. TDesktop
|
||||
// intentionally adds a small grace interval, while Android derives its
|
||||
|
|
@ -287,21 +292,22 @@ func (s *Service) BindTempAuthKey(ctx context.Context, sessionID int64, binding
|
|||
// The edge may admit the frame immediately before the temporary key's
|
||||
// absolute boundary and the encrypted proof may cross it. This is a temp-key
|
||||
// rotation condition, never a destructive permanent-key proof failure.
|
||||
return ErrTempAuthKeyEmpty
|
||||
return domain.TempAuthKeyBindingResult{}, ErrTempAuthKeyEmpty
|
||||
}
|
||||
if s.tempKeys == nil {
|
||||
return nil
|
||||
return validated, nil
|
||||
}
|
||||
if err := s.tempKeys.Save(ctx, binding); err != nil {
|
||||
result, err := s.tempKeys.SaveWithState(ctx, binding)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrTempAuthKeyAlreadyBound) {
|
||||
return ErrTempAuthKeyAlreadyBound
|
||||
return domain.TempAuthKeyBindingResult{}, ErrTempAuthKeyAlreadyBound
|
||||
}
|
||||
if errors.Is(err, store.ErrAuthKeyBindingInvalid) {
|
||||
return s.classifyBindingStoreInvalid(ctx, binding)
|
||||
return domain.TempAuthKeyBindingResult{}, s.classifyBindingStoreInvalid(ctx, binding)
|
||||
}
|
||||
return err
|
||||
return domain.TempAuthKeyBindingResult{}, err
|
||||
}
|
||||
return nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ResolveAuthKey 将已绑定的 temp auth_key 解析为对应 perm auth_key。
|
||||
|
|
@ -323,7 +329,7 @@ func (s *Service) ResolveAuthKey(ctx context.Context, authKeyID [8]byte) ([8]byt
|
|||
|
||||
// UserID 返回 auth_key 当前绑定的用户。未登录、或两步验证未完成时 found=false。
|
||||
func (s *Service) UserID(ctx context.Context, authKeyID [8]byte) (int64, bool, error) {
|
||||
if s == nil || s.auths == nil {
|
||||
if s == nil || s.auths == nil || s.users == nil {
|
||||
return 0, false, nil
|
||||
}
|
||||
a, found, err := s.auths.ByAuthKey(ctx, authKeyID)
|
||||
|
|
@ -334,7 +340,13 @@ func (s *Service) UserID(ctx context.Context, authKeyID [8]byte) (int64, bool, e
|
|||
// 两步验证未完成:业务鉴权视为未登录,仅允许 auth.checkPassword 继续。
|
||||
return 0, false, nil
|
||||
}
|
||||
if domain.IsSystemUserID(a.UserID) {
|
||||
u, userFound, err := s.users.ByID(ctx, a.UserID)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if !userFound || systemUserLoginForbidden(u) {
|
||||
// A stale row can exist only after an interrupted/legacy path. Fail closed
|
||||
// before it reaches the Router auth cache and retire it opportunistically.
|
||||
_ = s.auths.Delete(ctx, authKeyID)
|
||||
return 0, false, nil
|
||||
}
|
||||
|
|
@ -344,14 +356,18 @@ func (s *Service) UserID(ctx context.Context, authKeyID [8]byte) (int64, bool, e
|
|||
// PendingPasswordUserID 返回处于"待两步验证"状态的 auth_key 对应的用户。
|
||||
// UserID 对 password_pending 的 auth_key 返回未登录,auth.checkPassword 借此仍能定位待验证用户。
|
||||
func (s *Service) PendingPasswordUserID(ctx context.Context, authKeyID [8]byte) (int64, bool, error) {
|
||||
if s == nil || s.auths == nil {
|
||||
if s == nil || s.auths == nil || s.users == nil {
|
||||
return 0, false, nil
|
||||
}
|
||||
a, found, err := s.auths.ByAuthKey(ctx, authKeyID)
|
||||
if err != nil || !found || !a.PasswordPending {
|
||||
return 0, false, err
|
||||
}
|
||||
if domain.IsSystemUserID(a.UserID) {
|
||||
u, userFound, err := s.users.ByID(ctx, a.UserID)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if !userFound || systemUserLoginForbidden(u) {
|
||||
_ = s.auths.Delete(ctx, authKeyID)
|
||||
return 0, false, nil
|
||||
}
|
||||
|
|
@ -359,13 +375,24 @@ func (s *Service) PendingPasswordUserID(ctx context.Context, authKeyID [8]byte)
|
|||
}
|
||||
|
||||
// CompletePasswordSignIn 在两步验证通过后清除 password_pending,使 auth_key 转为完全授权。
|
||||
func (s *Service) CompletePasswordSignIn(ctx context.Context, authKeyID [8]byte) error {
|
||||
func (s *Service) CompletePasswordSignIn(ctx context.Context, authKeyID [8]byte, expectedUserID int64) error {
|
||||
if s == nil || s.auths == nil {
|
||||
return nil
|
||||
}
|
||||
if err := s.auths.MarkPasswordPassed(ctx, authKeyID); err != nil {
|
||||
if expectedUserID == 0 {
|
||||
return store.ErrAuthorizationStateChanged
|
||||
}
|
||||
if err := s.auths.MarkPasswordPassed(ctx, authKeyID, expectedUserID); err != nil {
|
||||
return err
|
||||
}
|
||||
// Revalidate the active account after the CAS promotion before Router caches
|
||||
// or binds the session. Account deletion can still linearize immediately
|
||||
// after the update and must leave the caller unauthorized.
|
||||
if userID, found, err := s.UserID(ctx, authKeyID); err != nil {
|
||||
return err
|
||||
} else if !found || userID != expectedUserID {
|
||||
return ErrSystemUserLoginForbidden
|
||||
}
|
||||
// This is where a 2FA account's sign-in actually finishes — finishSignIn
|
||||
// deliberately skipped the welcome message while password_pending.
|
||||
if a, found, err := s.auths.ByAuthKey(ctx, authKeyID); err == nil && found {
|
||||
|
|
@ -1423,7 +1450,11 @@ func (s *Service) AuthKeyClientInfo(ctx context.Context, authKeyID [8]byte) (dom
|
|||
if s == nil || s.authKeys == nil || authKeyID == ([8]byte{}) {
|
||||
return domain.AuthKeyClientInfo{}, false, nil
|
||||
}
|
||||
key, found, err := s.authKeys.Get(ctx, authKeyID)
|
||||
// Client metadata is a read-only projection. The physical connection's
|
||||
// first-frame Get and active-key heartbeat already own the durable orphan
|
||||
// lease, so this path must not turn every init/profile read into another
|
||||
// last_used_at write.
|
||||
key, found, err := s.authKeys.Revalidate(ctx, authKeyID)
|
||||
if err != nil || !found {
|
||||
return domain.AuthKeyClientInfo{}, found, err
|
||||
}
|
||||
|
|
@ -1499,6 +1530,16 @@ func (s *Service) ResetAuthorizations(ctx context.Context, userID int64, keepAut
|
|||
}
|
||||
|
||||
func (s *Service) bind(ctx context.Context, auth domain.Authorization, userID int64) error {
|
||||
if s == nil || s.users == nil || s.auths == nil || userID == 0 {
|
||||
return ErrSystemUserLoginForbidden
|
||||
}
|
||||
u, found, err := s.users.ByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || systemUserLoginForbidden(u) {
|
||||
return ErrSystemUserLoginForbidden
|
||||
}
|
||||
if s.authKeys != nil {
|
||||
key, found, err := s.authKeys.Get(ctx, auth.AuthKeyID)
|
||||
if err != nil {
|
||||
|
|
@ -1519,6 +1560,9 @@ func (s *Service) bind(ctx context.Context, auth domain.Authorization, userID in
|
|||
if errors.Is(err, store.ErrAuthKeyNotPermanent) {
|
||||
return ErrAuthKeyPermEmpty
|
||||
}
|
||||
if errors.Is(err, domain.ErrAccountDeleted) || errors.Is(err, domain.ErrUserNotFound) {
|
||||
return ErrSystemUserLoginForbidden
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
|
@ -1535,17 +1579,19 @@ func (s *Service) passwordNeeded(ctx context.Context, userID int64) (bool, error
|
|||
return found && settings.HasPassword, nil
|
||||
}
|
||||
|
||||
const loginMessageTpl = `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName + `!
|
||||
func loginMessageTemplate() string {
|
||||
return `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName + `!
|
||||
|
||||
This code can be used to log in to your ` + branding.ProductName + ` account. We never ask it for anything else.
|
||||
|
||||
If you didn't request this code by trying to log in on another device, simply ignore this message.`
|
||||
}
|
||||
|
||||
func (s *Service) recordLoginMessage(ctx context.Context, userID int64, code string) (domain.Message, error) {
|
||||
if s.messages == nil || s.dialogs == nil {
|
||||
return domain.Message{}, nil
|
||||
}
|
||||
body := fmt.Sprintf(loginMessageTpl, code)
|
||||
body := fmt.Sprintf(loginMessageTemplate(), code)
|
||||
codeOffset := len("Login code: ")
|
||||
msg, err := s.messages.Create(ctx, domain.Message{
|
||||
OwnerUserID: userID,
|
||||
|
|
@ -1595,53 +1641,58 @@ func (s *Service) recordWelcomeMessage(ctx context.Context, u domain.User) {
|
|||
})
|
||||
}
|
||||
|
||||
func (s *Service) validateBindTempAuthKey(ctx context.Context, sessionID int64, binding domain.TempAuthKeyBinding) (mtcrypto.BindAuthKeyInner, int, error) {
|
||||
func (s *Service) validateBindTempAuthKey(ctx context.Context, sessionID int64, binding domain.TempAuthKeyBinding) (mtcrypto.BindAuthKeyInner, int, domain.TempAuthKeyBindingResult, error) {
|
||||
if binding.ExpiresAt <= int(time.Now().Unix()) {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, ErrExpiresAtInvalid
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrExpiresAtInvalid
|
||||
}
|
||||
temp, found, err := s.authKeys.Get(ctx, binding.TempAuthKeyID)
|
||||
permID := authKeyIDFromInt64(binding.PermAuthKeyID)
|
||||
pair, err := s.authKeys.LoadBindingKeys(ctx, binding.TempAuthKeyID, permID)
|
||||
if err != nil {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, err
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, err
|
||||
}
|
||||
// expires_at in auth.bindTempAuthKey is client-supplied and must only attest
|
||||
// to a still-live binding. It may never create or reclassify a protocol key;
|
||||
// the caller normalizes durable retention to this handshake-authoritative
|
||||
// temp.ExpiresAt instead of trusting the client value.
|
||||
if !found || temp.ExpiresAt <= int(time.Now().Unix()) {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, ErrTempAuthKeyEmpty
|
||||
if !pair.TemporaryFound || pair.Temporary.ExpiresAt <= int(time.Now().Unix()) {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrTempAuthKeyEmpty
|
||||
}
|
||||
|
||||
permID := authKeyIDFromInt64(binding.PermAuthKeyID)
|
||||
perm, found, err := s.authKeys.Get(ctx, permID)
|
||||
if err != nil {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, err
|
||||
}
|
||||
if !found || perm.ExpiresAt != 0 {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, ErrEncryptedMessageInvalid
|
||||
if !pair.PermanentFound || pair.Permanent.ExpiresAt != 0 {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrEncryptedMessageInvalid
|
||||
}
|
||||
|
||||
inner, err := decryptBindAuthKeyInner(perm, binding.EncryptedMessage)
|
||||
inner, err := decryptBindAuthKeyInner(pair.Permanent, binding.EncryptedMessage)
|
||||
if err != nil {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, ErrEncryptedMessageInvalid
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrEncryptedMessageInvalid
|
||||
}
|
||||
if inner.Nonce != binding.Nonce ||
|
||||
inner.TempAuthKeyID != authKeyIDInt64(binding.TempAuthKeyID) ||
|
||||
inner.PermAuthKeyID != binding.PermAuthKeyID ||
|
||||
inner.TempSessionID != sessionID ||
|
||||
inner.ExpiresAt != binding.ExpiresAt {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, ErrEncryptedMessageInvalid
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrEncryptedMessageInvalid
|
||||
}
|
||||
if temp.ExpiresAt <= int(time.Now().Unix()) {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, ErrTempAuthKeyEmpty
|
||||
if pair.Temporary.ExpiresAt <= int(time.Now().Unix()) {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrTempAuthKeyEmpty
|
||||
}
|
||||
return inner, temp.ExpiresAt, nil
|
||||
layer, observationID, err := store.MergeAuthKeyLayerObservations(
|
||||
pair.Temporary.Layer, pair.Temporary.LayerObservationID,
|
||||
pair.Permanent.Layer, pair.Permanent.LayerObservationID,
|
||||
)
|
||||
if err != nil {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, err
|
||||
}
|
||||
return inner, pair.Temporary.ExpiresAt, domain.TempAuthKeyBindingResult{
|
||||
Layer: layer, LayerObservationID: observationID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) classifyBindingStoreInvalid(ctx context.Context, binding domain.TempAuthKeyBinding) error {
|
||||
if s == nil || s.authKeys == nil {
|
||||
return ErrEncryptedMessageInvalid
|
||||
}
|
||||
temp, found, err := s.authKeys.Get(ctx, binding.TempAuthKeyID)
|
||||
temp, found, err := s.authKeys.Revalidate(ctx, binding.TempAuthKeyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue