/* * Copyright (c) 2026 Kaiy Ragur * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package remote import ( "crypto/ed25519" "fmt" "sync" "time" pb "github.com/neilalexander/yggmail/internal/remote/types" "github.com/neilalexander/yggmail/internal/utils" "google.golang.org/protobuf/proto" ) type ReqValidator struct { r map[string]time.Time m sync.RWMutex window time.Duration } func NewValidator(window time.Duration) *ReqValidator { r := &ReqValidator{ window: window, r: map[string]time.Time{}, } go r.Cleaner() return r } func (rv *ReqValidator) Validate(req *pb.Request, pub ed25519.PublicKey) (bool, error) { diff := time.Since(time.Unix(0, req.Timestamp)) if diff < 0 { diff = -diff } if diff > rv.window { return false, fmt.Errorf("request timestamp is too old or too far in future") } ok, err := VerifyRequest(req, pub) if err != nil || !ok { return false, err } rv.m.Lock() defer rv.m.Unlock() if _, exist := rv.r[utils.EncodeString(req.Signature)]; exist { return false, nil } rv.r[utils.EncodeString(req.Signature)] = time.Now() return true, nil } func (rv *ReqValidator) Cleaner() { for tn := range time.NewTicker(time.Minute).C { rv.clean(tn) } } func (rv *ReqValidator) clean(tn time.Time) { rv.m.Lock() defer rv.m.Unlock() for s, tr := range rv.r { if tr.Add(rv.window).Before(tn) { delete(rv.r, s) } } } type Signer func([]byte) ([]byte, error) func PrepareToSign(req *pb.Request) ([]byte, error) { clone := proto.Clone(req).(*pb.Request) clone.Signature = nil opts := proto.MarshalOptions{Deterministic: true} return opts.Marshal(clone) } func SignRequest(req *pb.Request, signer Signer) error { data, err := PrepareToSign(req) if err != nil { return err } req.Signature, err = signer(data) if err != nil { return err } return nil } func VerifyRequest(req *pb.Request, pubKey ed25519.PublicKey) (bool, error) { if len(req.Signature) == 0 { return false, fmt.Errorf("missing signature") } data, err := PrepareToSign(req) if err != nil { return false, err } if !ed25519.Verify(pubKey, data, req.Signature) { return false, fmt.Errorf("cryptographic signature mismatch") } return true, nil }