188 lines
6.1 KiB
Go
188 lines
6.1 KiB
Go
package postgres
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"math"
|
||
|
||
"github.com/jackc/pgx/v5"
|
||
"github.com/jackc/pgx/v5/pgconn"
|
||
|
||
"telesrv/internal/domain"
|
||
"telesrv/internal/store"
|
||
"telesrv/internal/store/postgres/sqlcgen"
|
||
)
|
||
|
||
// TempAuthKeyBindingStore 用 PostgreSQL 实现 store.TempAuthKeyBindingStore。
|
||
type TempAuthKeyBindingStore struct {
|
||
db sqlcgen.DBTX
|
||
q *sqlcgen.Queries
|
||
}
|
||
|
||
// NewTempAuthKeyBindingStore 基于 pgx 连接池(或事务)创建 TempAuthKeyBindingStore。
|
||
func NewTempAuthKeyBindingStore(db sqlcgen.DBTX) *TempAuthKeyBindingStore {
|
||
return &TempAuthKeyBindingStore{db: db, q: sqlcgen.New(db)}
|
||
}
|
||
|
||
func (s *TempAuthKeyBindingStore) Save(ctx context.Context, b domain.TempAuthKeyBinding) error {
|
||
if b.ExpiresAt <= 0 || int64(b.ExpiresAt) > math.MaxInt32 {
|
||
return store.ErrAuthKeyBindingInvalid
|
||
}
|
||
return withAuthIdentityTx(ctx, s.db, "save temp auth key binding", func(tx pgx.Tx) error {
|
||
return s.saveTx(ctx, tx, b)
|
||
})
|
||
}
|
||
|
||
func (s *TempAuthKeyBindingStore) saveTx(ctx context.Context, tx pgx.Tx, b domain.TempAuthKeyBinding) error {
|
||
rawID := authKeyIDToInt64(b.TempAuthKeyID)
|
||
permID := b.PermAuthKeyID
|
||
// Every operation that may bridge temp and permanent rows enters the
|
||
// permanent identity gate before taking the raw-key row lock. This is the
|
||
// same gate/order used by selector advance and permanent revocation.
|
||
if err := lockPermanentAuthIdentities(ctx, tx, []int64{permID}); err != nil {
|
||
return err
|
||
}
|
||
var (
|
||
tempExpiry int
|
||
tempLayer int
|
||
tempObservationID int64
|
||
)
|
||
if err := tx.QueryRow(ctx, `
|
||
SELECT expires_at, layer, layer_observation_id
|
||
FROM auth_keys
|
||
WHERE auth_key_id = $1
|
||
FOR UPDATE
|
||
`, rawID).Scan(&tempExpiry, &tempLayer, &tempObservationID); err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return store.ErrAuthKeyBindingInvalid
|
||
}
|
||
return fmt.Errorf("lock temporary auth key for binding: %w", err)
|
||
}
|
||
if tempExpiry <= 0 || tempExpiry != b.ExpiresAt || rawID == permID {
|
||
return store.ErrAuthKeyBindingInvalid
|
||
}
|
||
|
||
// The raw row serializes first bind and rebind attempts. Read the binding
|
||
// only after taking that lock, so a concurrent winner is either visible or
|
||
// still waiting behind us. A different permanent identity is immutable.
|
||
var currentPermID int64
|
||
err := tx.QueryRow(ctx, `
|
||
SELECT perm_auth_key_id
|
||
FROM temp_auth_key_bindings
|
||
WHERE temp_auth_key_id = $1
|
||
`, rawID).Scan(¤tPermID)
|
||
switch {
|
||
case err == nil && currentPermID != permID:
|
||
return store.ErrTempAuthKeyAlreadyBound
|
||
case err != nil && !errors.Is(err, pgx.ErrNoRows):
|
||
return fmt.Errorf("read existing temporary auth key binding: %w", err)
|
||
}
|
||
|
||
var (
|
||
permExpiry int
|
||
permLayer int
|
||
permObservationID int64
|
||
)
|
||
if err := tx.QueryRow(ctx, `
|
||
SELECT expires_at, layer, layer_observation_id
|
||
FROM auth_keys
|
||
WHERE auth_key_id = $1
|
||
FOR UPDATE
|
||
`, permID).Scan(&permExpiry, &permLayer, &permObservationID); err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return store.ErrAuthKeyBindingInvalid
|
||
}
|
||
return fmt.Errorf("lock permanent auth key for binding: %w", err)
|
||
}
|
||
if permExpiry != 0 {
|
||
return store.ErrAuthKeyBindingInvalid
|
||
}
|
||
|
||
mergedLayer, mergedObservationID, err := store.MergeAuthKeyLayerObservations(
|
||
tempLayer, tempObservationID,
|
||
permLayer, permObservationID,
|
||
)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
q := s.q.WithTx(tx)
|
||
n, err := q.UpsertTempAuthKeyBinding(ctx, sqlcgen.UpsertTempAuthKeyBindingParams{
|
||
TempAuthKeyID: authKeyIDToInt64(b.TempAuthKeyID),
|
||
PermAuthKeyID: b.PermAuthKeyID,
|
||
Nonce: b.Nonce,
|
||
TempSessionID: b.TempSessionID,
|
||
ExpiresAt: int32(b.ExpiresAt),
|
||
EncryptedMessage: b.EncryptedMessage,
|
||
})
|
||
if err != nil {
|
||
var pgErr *pgconn.PgError
|
||
if errors.As(err, &pgErr) && pgErr.Code == "23503" {
|
||
return store.ErrAuthKeyBindingInvalid
|
||
}
|
||
return fmt.Errorf("upsert temp auth key binding: %w", err)
|
||
}
|
||
if n == 0 {
|
||
return store.ErrAuthKeyBindingInvalid
|
||
}
|
||
keyIDs := []int64{rawID, permID}
|
||
tag, err := tx.Exec(ctx, `
|
||
UPDATE auth_keys
|
||
SET layer = $2,
|
||
layer_observation_id = $3
|
||
WHERE auth_key_id = ANY($1::bigint[])
|
||
`, keyIDs, mergedLayer, mergedObservationID)
|
||
if err != nil {
|
||
return fmt.Errorf("merge bound auth key layer defaults: %w", err)
|
||
}
|
||
if tag.RowsAffected() != int64(len(keyIDs)) {
|
||
return fmt.Errorf("merge bound auth key layer defaults: updated %d of %d locked keys", tag.RowsAffected(), len(keyIDs))
|
||
}
|
||
if _, err := tx.Exec(ctx, `
|
||
UPDATE authorizations
|
||
SET layer = $2
|
||
WHERE auth_key_id = ANY($1::bigint[])
|
||
`, keyIDs, mergedLayer); err != nil {
|
||
return fmt.Errorf("mirror bound auth key layer default: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// DeleteExpired 实现 store.TempAuthKeyBindingStore:按 auth_keys.expires_at 的部分索引
|
||
// 有界删除所有过期 temp key(含从未绑定的握手 key),binding 经 CASCADE 一并清除。
|
||
// Edge 已在准确协议时刻停止使用 key;这里的 24h 宽限只控制数据库物理回收。
|
||
func (s *TempAuthKeyBindingStore) DeleteExpired(ctx context.Context, expiredBefore int64, limit int) (int, error) {
|
||
if limit <= 0 {
|
||
return 0, nil
|
||
}
|
||
if expiredBefore <= 0 || expiredBefore > math.MaxInt32 {
|
||
return 0, fmt.Errorf("delete expired temp auth keys: invalid expiry cutoff %d", expiredBefore)
|
||
}
|
||
n, err := s.q.DeleteExpiredTempAuthKeys(ctx, sqlcgen.DeleteExpiredTempAuthKeysParams{
|
||
ExpiresAt: int32(expiredBefore),
|
||
Limit: int32(limit),
|
||
})
|
||
if err != nil {
|
||
return 0, fmt.Errorf("delete expired temp auth keys: %w", err)
|
||
}
|
||
return int(n), nil
|
||
}
|
||
|
||
func (s *TempAuthKeyBindingStore) GetByTemp(ctx context.Context, tempAuthKeyID [8]byte) (domain.TempAuthKeyBinding, bool, error) {
|
||
row, err := s.q.GetTempAuthKeyBinding(ctx, authKeyIDToInt64(tempAuthKeyID))
|
||
if err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.TempAuthKeyBinding{}, false, nil
|
||
}
|
||
return domain.TempAuthKeyBinding{}, false, fmt.Errorf("get temp auth key binding: %w", err)
|
||
}
|
||
return domain.TempAuthKeyBinding{
|
||
TempAuthKeyID: authKeyIDFromInt64(row.TempAuthKeyID),
|
||
PermAuthKeyID: row.PermAuthKeyID,
|
||
Nonce: row.Nonce,
|
||
TempSessionID: row.TempSessionID,
|
||
ExpiresAt: int(row.ExpiresAt),
|
||
EncryptedMessage: append([]byte(nil), row.EncryptedMessage...),
|
||
}, true, nil
|
||
}
|