New code verification service
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
This commit is contained in:
181
api/server/internal/server/confirmationimp/store.go
Normal file
181
api/server/internal/server/confirmationimp/store.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package confirmationimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/confirmation"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
var (
|
||||
errConfirmationNotFound = errors.New("confirmation not found or expired")
|
||||
errConfirmationUsed = errors.New("confirmation already used")
|
||||
errConfirmationMismatch = errors.New("confirmation code mismatch")
|
||||
errConfirmationAttemptsExceeded = errors.New("confirmation attempts exceeded")
|
||||
errConfirmationCooldown = errors.New("confirmation cooldown active")
|
||||
errConfirmationResendLimit = errors.New("confirmation resend limit reached")
|
||||
)
|
||||
|
||||
type ConfirmationStore struct {
|
||||
db confirmation.DB
|
||||
}
|
||||
|
||||
func NewStore(db confirmation.DB) *ConfirmationStore {
|
||||
return &ConfirmationStore{db: db}
|
||||
}
|
||||
|
||||
func (s *ConfirmationStore) Create(
|
||||
ctx context.Context,
|
||||
accountRef primitive.ObjectID,
|
||||
destination string,
|
||||
target model.ConfirmationTarget,
|
||||
cfg Config,
|
||||
generator func() (string, error),
|
||||
) (string, *model.ConfirmationCode, error) {
|
||||
if err := s.db.DeleteTuple(ctx, accountRef, destination, target); err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
code, _, rec, err := s.buildRecord(accountRef, destination, target, cfg, generator)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if err := s.db.Create(ctx, rec); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return code, rec, nil
|
||||
}
|
||||
|
||||
func (s *ConfirmationStore) Resend(
|
||||
ctx context.Context,
|
||||
accountRef primitive.ObjectID,
|
||||
destination string,
|
||||
target model.ConfirmationTarget,
|
||||
cfg Config,
|
||||
generator func() (string, error),
|
||||
) (string, *model.ConfirmationCode, error) {
|
||||
now := time.Now().UTC()
|
||||
active, err := s.db.FindActive(ctx, accountRef, destination, target, now.Unix())
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
return s.Create(ctx, accountRef, destination, target, cfg, generator)
|
||||
}
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if active.ResendCount >= active.ResendLimit {
|
||||
return "", nil, errConfirmationResendLimit
|
||||
}
|
||||
if now.Before(active.CooldownUntil) {
|
||||
return "", nil, errConfirmationCooldown
|
||||
}
|
||||
|
||||
code, salt, updated, err := s.buildRecord(accountRef, destination, target, cfg, generator)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
// Preserve attempt counters but bump resend count.
|
||||
updated.ID = active.ID
|
||||
updated.CreatedAt = active.CreatedAt
|
||||
updated.Attempts = active.Attempts
|
||||
updated.ResendCount = active.ResendCount + 1
|
||||
updated.Salt = salt
|
||||
|
||||
if err := s.db.Update(ctx, updated); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return code, updated, nil
|
||||
}
|
||||
|
||||
func (s *ConfirmationStore) Verify(
|
||||
ctx context.Context,
|
||||
accountRef primitive.ObjectID,
|
||||
destination string,
|
||||
target model.ConfirmationTarget,
|
||||
code string,
|
||||
) error {
|
||||
now := time.Now().UTC()
|
||||
rec, err := s.db.FindActive(ctx, accountRef, destination, target, now.Unix())
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
return errConfirmationNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rec.Used {
|
||||
return errConfirmationUsed
|
||||
}
|
||||
|
||||
rec.Attempts++
|
||||
if rec.Attempts > rec.MaxAttempts {
|
||||
rec.Used = true
|
||||
_ = s.db.Update(ctx, rec)
|
||||
return errConfirmationAttemptsExceeded
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare(rec.CodeHash, hashCode(rec.Salt, code)) != 1 {
|
||||
_ = s.db.Update(ctx, rec)
|
||||
return errConfirmationMismatch
|
||||
}
|
||||
|
||||
rec.Used = true
|
||||
return s.db.Update(ctx, rec)
|
||||
}
|
||||
|
||||
func (s *ConfirmationStore) buildRecord(
|
||||
accountRef primitive.ObjectID,
|
||||
destination string,
|
||||
target model.ConfirmationTarget,
|
||||
cfg Config,
|
||||
generator func() (string, error),
|
||||
) (string, []byte, *model.ConfirmationCode, error) {
|
||||
code, err := generator()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
salt, err := newSalt()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
rec := model.NewConfirmationCode(accountRef)
|
||||
rec.Destination = destination
|
||||
rec.Target = target
|
||||
rec.CodeHash = hashCode(salt, code)
|
||||
rec.Salt = salt
|
||||
rec.ExpiresAt = now.Add(cfg.TTL)
|
||||
rec.MaxAttempts = cfg.MaxAttempts
|
||||
rec.ResendLimit = cfg.ResendLimit
|
||||
rec.CooldownUntil = now.Add(cfg.Cooldown)
|
||||
rec.Used = false
|
||||
rec.Attempts = 0
|
||||
rec.ResendCount = 0
|
||||
rec.CreatedAt = now
|
||||
rec.UpdatedAt = now
|
||||
|
||||
return code, salt, rec, nil
|
||||
}
|
||||
|
||||
func hashCode(salt []byte, code string) []byte {
|
||||
h := sha256.New()
|
||||
h.Write(salt)
|
||||
h.Write([]byte(code))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func newSalt() ([]byte, error) {
|
||||
buf := make([]byte, 16)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
Reference in New Issue
Block a user