94 lines
3.2 KiB
Go
94 lines
3.2 KiB
Go
package deployment
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"errors"
|
|
"sync/atomic"
|
|
)
|
|
|
|
var ErrRecoveryConfirmation = errors.New("人工处置确认无效、已使用或已过期,请重新核查")
|
|
|
|
// RecoveryConfirmation 的副本共享消费状态,不能通过复制对象重复授权。
|
|
// 本机会话持有对象,终端仅收到短时令牌和脱敏摘要;令牌不写入数据库。
|
|
type RecoveryConfirmation struct {
|
|
state *recoveryConfirmationState
|
|
}
|
|
|
|
type recoveryConfirmationState struct {
|
|
review RecoveryReview
|
|
token [sha256.Size]byte
|
|
intent RecoveryDispositionIntent
|
|
used atomic.Bool
|
|
}
|
|
|
|
func (c RecoveryConfirmation) Summary() RecoveryReviewSummary {
|
|
if c.state == nil {
|
|
return RecoveryReviewSummary{}
|
|
}
|
|
return c.state.review.Summary()
|
|
}
|
|
|
|
func (c RecoveryConfirmation) DispositionID() string {
|
|
if c.state == nil {
|
|
return ""
|
|
}
|
|
return c.state.intent.ID
|
|
}
|
|
|
|
func (s *Service) PrepareRecoveryConfirmation(ctx context.Context, id, operator string, action RecoveryAction, resolve func(context.Context, string) (Server, error)) (RecoveryConfirmation, string, error) {
|
|
review, err := s.PrepareRecoveryReview(ctx, id, operator, action, resolve)
|
|
if err != nil {
|
|
return RecoveryConfirmation{}, "", err
|
|
}
|
|
return s.newRecoveryConfirmation(ctx, review)
|
|
}
|
|
|
|
func (s *Service) newRecoveryConfirmation(ctx context.Context, review RecoveryReview) (RecoveryConfirmation, string, error) {
|
|
if s == nil || review.service != s {
|
|
return RecoveryConfirmation{}, "", ErrRecoveryConfirmation
|
|
}
|
|
var entropy [96]byte
|
|
if _, err := rand.Read(entropy[:]); err != nil {
|
|
return RecoveryConfirmation{}, "", ErrRecoveryConfirmation
|
|
}
|
|
token := hex.EncodeToString(entropy[:32])
|
|
intent, err := review.DispositionIntent(hex.EncodeToString(entropy[32:64]), hex.EncodeToString(entropy[64:]))
|
|
if err != nil {
|
|
return RecoveryConfirmation{}, "", err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return RecoveryConfirmation{}, "", err
|
|
}
|
|
return RecoveryConfirmation{state: &recoveryConfirmationState{
|
|
review: review, intent: intent, token: sha256.Sum256([]byte(token)),
|
|
}}, token, nil
|
|
}
|
|
|
|
// consumeRecoveryConfirmation 每个对象只允许一次确认尝试,错误身份、错误令牌和取消均不可重放。
|
|
// 成功消费仍不是远端写授权,执行器必须取得持久化占用并重新检查现场。
|
|
func (s *Service) consumeRecoveryConfirmation(ctx context.Context, c RecoveryConfirmation, operator, token string) (RecoveryReview, RecoveryDispositionIntent, error) {
|
|
fail := func(err error) (RecoveryReview, RecoveryDispositionIntent, error) {
|
|
return RecoveryReview{}, RecoveryDispositionIntent{}, err
|
|
}
|
|
state := c.state
|
|
if state == nil || !state.used.CompareAndSwap(false, true) {
|
|
return fail(ErrRecoveryConfirmation)
|
|
}
|
|
if s == nil || state.review.service != s || s.now == nil || !validReviewOperator(operator) || state.review.operator != operator ||
|
|
!s.now().Before(state.review.summary.ExpiresAt) || len(token) != 64 {
|
|
return fail(ErrRecoveryConfirmation)
|
|
}
|
|
digest := sha256.Sum256([]byte(token))
|
|
if subtle.ConstantTimeCompare(digest[:], state.token[:]) != 1 {
|
|
return fail(ErrRecoveryConfirmation)
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return fail(err)
|
|
}
|
|
return state.review, state.intent, nil
|
|
}
|