mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-05-15 21:28:17 +02:00
8b955c4742
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
63 lines
1.4 KiB
Go
63 lines
1.4 KiB
Go
package service
|
||
|
||
import (
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// totpWindow is the lifetime of a used-token entry.
|
||
// pquerna/otp totp.Validate uses Skew:1 (current ±1 period × 30s = 90s total window).
|
||
const totpWindow = 90 * time.Second
|
||
|
||
type TOTPReplayCache struct {
|
||
mu sync.Mutex
|
||
entries map[string]time.Time // "userID:token" -> usedAt
|
||
}
|
||
|
||
func NewTOTPReplayCache() *TOTPReplayCache {
|
||
c := &TOTPReplayCache{entries: make(map[string]time.Time)}
|
||
go c.runCleanup()
|
||
return c
|
||
}
|
||
|
||
func (c *TOTPReplayCache) key(userID, token string) string {
|
||
return userID + ":" + token
|
||
}
|
||
|
||
// isUsed reports whether token has already been consumed for userID within the window.
|
||
func (c *TOTPReplayCache) isUsed(userID, token string) bool {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
usedAt, ok := c.entries[c.key(userID, token)]
|
||
if !ok {
|
||
return false
|
||
}
|
||
if time.Since(usedAt) > totpWindow {
|
||
delete(c.entries, c.key(userID, token))
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// markUsed records token as consumed for userID.
|
||
func (c *TOTPReplayCache) markUsed(userID, token string) {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
c.entries[c.key(userID, token)] = time.Now()
|
||
}
|
||
|
||
func (c *TOTPReplayCache) runCleanup() {
|
||
ticker := time.NewTicker(totpWindow)
|
||
defer ticker.Stop()
|
||
for range ticker.C {
|
||
c.mu.Lock()
|
||
now := time.Now()
|
||
for k, usedAt := range c.entries {
|
||
if now.Sub(usedAt) > totpWindow {
|
||
delete(c.entries, k)
|
||
}
|
||
}
|
||
c.mu.Unlock()
|
||
}
|
||
}
|