Scert/backend/internal/localreset/recovery.go

127 lines
4.6 KiB
Go

package localreset
import (
"context"
"crypto/subtle"
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
)
const MaxRecoveryRequests = 32
// RecoverySession 仅供受保护的本机连接使用,与邮箱密码重置阶段互斥。
type RecoverySession interface {
RecoveryAvailable() bool
ListRecoveries(context.Context, string) (deployment.PendingRecoveryPage, error)
InspectRecovery(context.Context, string) (deployment.RecoveryInspection, error)
}
func validRecoveryRequest(request Request, nonce string) bool {
if request.Version != Version || subtle.ConstantTimeCompare([]byte(request.Nonce), []byte(nonce)) != 1 ||
request.Method != "" || request.Code != "" || request.Password != "" {
return false
}
switch request.Action {
case "recovery_list":
return request.TransactionID == "" && (request.Cursor == "" || validRecoveryID(request.Cursor)) && noDispositionRequest(request)
case "recovery_inspect":
return request.Cursor == "" && validRecoveryID(request.TransactionID) && noDispositionRequest(request)
case "recovery_prepare":
return request.Cursor == "" && validRecoveryID(request.TransactionID) && validRecoveryAction(request.RecoveryAction) && request.DispositionID == "" && request.ConfirmationToken == ""
case "recovery_execute":
return request.Cursor == "" && request.TransactionID == "" && request.RecoveryAction == "" && validRecoverySecret(request.DispositionID) && validRecoverySecret(request.ConfirmationToken)
case "recovery_result", "recovery_reconcile_prepare":
return request.Cursor == "" && request.TransactionID == "" && request.RecoveryAction == "" && validRecoverySecret(request.DispositionID) && request.ConfirmationToken == ""
}
return false
}
func validRecoveryID(value string) bool {
if len(value) == 0 || len(value) > 160 || value == "." || value == ".." {
return false
}
for _, c := range value {
if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '-' && c != '.' && c != '_' {
return false
}
}
return true
}
func (c *Client) RecoveryAvailable() bool { return c.recoveryAvailable }
func (c *Client) ListRecoveries(cursor string) (deployment.PendingRecoveryPage, error) {
response, err := c.recoveryExchange(Request{Action: "recovery_list", Cursor: cursor})
if err != nil {
return deployment.PendingRecoveryPage{}, err
}
return *response.RecoveryPage, nil
}
func (c *Client) InspectRecovery(id string) (deployment.RecoveryInspection, error) {
response, err := c.recoveryExchange(Request{Action: "recovery_inspect", TransactionID: id})
if err != nil {
return deployment.RecoveryInspection{}, err
}
return *response.Inspection, nil
}
func (c *Client) recoveryExchange(request Request) (Response, error) {
c.mu.Lock()
defer c.mu.Unlock()
fail := func() (Response, error) {
c.stage = -1
_ = c.Close()
return Response{}, ErrProtocol
}
if !c.recoveryAvailable || (c.stage != 0 && c.stage != 3) {
return fail()
}
if request.Action != "recovery_list" && request.Action != "recovery_inspect" && !c.dispositionAvailable {
return fail()
}
request.Version, request.Nonce = Version, c.nonce
if !validRecoveryRequest(request, c.nonce) || writeFrame(c.conn, request) != nil {
return fail()
}
var response Response
if readFrame(c.reader, &response) != nil || !validChallenge(response) || response.Nonce == c.nonce ||
len(response.Methods) != 0 || response.RecoveryAvailable || response.DispositionAvailable {
return fail()
}
if request.Action == "recovery_list" {
if response.RecoveryPage == nil || response.Inspection != nil || response.InspectionPart != nil || hasDispositionPayload(response) || len(response.RecoveryPage.Items) > deployment.RecoveryPageSize {
return fail()
}
previous := request.Cursor
for _, item := range response.RecoveryPage.Items {
if !validRecoveryID(item.ID) || item.ID <= previous {
return fail()
}
previous = item.ID
}
if cursor := response.RecoveryPage.NextCursor; cursor != "" &&
(len(response.RecoveryPage.Items) != deployment.RecoveryPageSize || cursor != previous) {
return fail()
}
} else if request.Action == "recovery_inspect" {
if hasDispositionPayload(response) {
return fail()
}
if response.InspectionPart != nil {
inspection, err := readInspectionParts(c.reader, response)
if err != nil {
return fail()
}
response.Inspection, response.InspectionPart = inspection, nil
}
if !validInspectionPayload(response.Inspection) || response.RecoveryPage != nil || response.Inspection.TransactionID != request.TransactionID {
return fail()
}
} else if !c.dispositionAvailable || !validDispositionResponse(request, response) {
return fail()
}
c.nonce, c.stage = response.Nonce, 3
return response, nil
}