1package mfa
2
3import (
4	"bytes"
5	"errors"
6	"fmt"
7	"image/png"
8	"sync"
9	"time"
10
11	"github.com/pquerna/otp"
12	"github.com/pquerna/otp/totp"
13)
14
15// TOTPHMacAlgo is the enumerable for the possible HMAC algorithms for Time-based one time passwords
16type TOTPHMacAlgo = string
17
18// supported TOTP HMAC algorithms
19const (
20	TOTPAlgoSHA1   TOTPHMacAlgo = "sha1"
21	TOTPAlgoSHA256 TOTPHMacAlgo = "sha256"
22	TOTPAlgoSHA512 TOTPHMacAlgo = "sha512"
23)
24
25var (
26	cleanupTicker   *time.Ticker
27	cleanupDone     chan bool
28	usedPasscodes   sync.Map
29	errPasscodeUsed = errors.New("this passcode was already used")
30)
31
32// TOTPConfig defines the configuration for a Time-based one time password
33type TOTPConfig struct {
34	Name   string       `json:"name" mapstructure:"name"`
35	Issuer string       `json:"issuer" mapstructure:"issuer"`
36	Algo   TOTPHMacAlgo `json:"algo" mapstructure:"algo"`
37	algo   otp.Algorithm
38}
39
40func (c *TOTPConfig) validate() error {
41	if c.Name == "" {
42		return errors.New("totp: name is mandatory")
43	}
44	if c.Issuer == "" {
45		return errors.New("totp: issuer is mandatory")
46	}
47	switch c.Algo {
48	case TOTPAlgoSHA1:
49		c.algo = otp.AlgorithmSHA1
50	case TOTPAlgoSHA256:
51		c.algo = otp.AlgorithmSHA256
52	case TOTPAlgoSHA512:
53		c.algo = otp.AlgorithmSHA512
54	default:
55		return fmt.Errorf("unsupported totp algo %#v", c.Algo)
56	}
57	return nil
58}
59
60// validatePasscode validates a TOTP passcode
61func (c *TOTPConfig) validatePasscode(passcode, secret string) (bool, error) {
62	key := fmt.Sprintf("%v_%v", secret, passcode)
63	if _, ok := usedPasscodes.Load(key); ok {
64		return false, errPasscodeUsed
65	}
66	match, err := totp.ValidateCustom(passcode, secret, time.Now().UTC(), totp.ValidateOpts{
67		Period:    30,
68		Skew:      1,
69		Digits:    otp.DigitsSix,
70		Algorithm: c.algo,
71	})
72	if match && err == nil {
73		usedPasscodes.Store(key, time.Now().Add(1*time.Minute).UTC())
74	}
75	return match, err
76}
77
78// generate generates a new TOTP secret and QR code for the given username
79func (c *TOTPConfig) generate(username string, qrCodeWidth, qrCodeHeight int) (string, string, []byte, error) {
80	key, err := totp.Generate(totp.GenerateOpts{
81		Issuer:      c.Issuer,
82		AccountName: username,
83		Digits:      otp.DigitsSix,
84		Algorithm:   c.algo,
85	})
86	if err != nil {
87		return "", "", nil, err
88	}
89	var buf bytes.Buffer
90	img, err := key.Image(qrCodeWidth, qrCodeHeight)
91	if err != nil {
92		return "", "", nil, err
93	}
94	err = png.Encode(&buf, img)
95	return key.Issuer(), key.Secret(), buf.Bytes(), err
96}
97
98func cleanupUsedPasscodes() {
99	usedPasscodes.Range(func(key, value interface{}) bool {
100		exp, ok := value.(time.Time)
101		if !ok || exp.Before(time.Now().UTC()) {
102			usedPasscodes.Delete(key)
103		}
104		return true
105	})
106}
107