| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- /*
- Copyright © 2025 ESO Maintainer Team
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- https://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package mfa
- import (
- "crypto/hmac"
- "crypto/sha1" //nolint:gosec // not used for encryption purposes
- "crypto/sha256"
- "crypto/sha512"
- "encoding/base32"
- "encoding/binary"
- "errors"
- "fmt"
- "hash"
- "math"
- "strconv"
- "strings"
- "time"
- )
- const (
- // defaultLength for a token is 6 characters.
- defaultLength = 6
- // defaultTimePeriod for a token is 30 seconds.
- defaultTimePeriod = 30
- // defaultAlgorithm according to the RFC should be sha1.
- defaultAlgorithm = "sha1"
- )
- // options define configurable values for a TOTP token.
- type options struct {
- algorithm string
- when time.Time
- token string
- timePeriod int64
- length int
- }
- // GeneratorOptionsFunc provides a nice way of configuring the generator while allowing defaults.
- type GeneratorOptionsFunc func(*options)
- // WithToken can be used to set the token value.
- func WithToken(token string) GeneratorOptionsFunc {
- return func(o *options) {
- o.token = token
- }
- }
- // WithTimePeriod sets the time-period for the generated token. Default is 30s.
- func WithTimePeriod(timePeriod int64) GeneratorOptionsFunc {
- return func(o *options) {
- o.timePeriod = timePeriod
- }
- }
- // WithLength sets the length of the token. Defaults to 6 digits where the token can start with 0.
- func WithLength(length int) GeneratorOptionsFunc {
- return func(o *options) {
- o.length = length
- }
- }
- // WithAlgorithm configures the algorithm. Defaults to sha1.
- func WithAlgorithm(algorithm string) GeneratorOptionsFunc {
- return func(o *options) {
- o.algorithm = algorithm
- }
- }
- // WithWhen allows configuring the time when the token is generated from. Defaults to time.Now().
- func WithWhen(when time.Time) GeneratorOptionsFunc {
- return func(o *options) {
- o.when = when
- }
- }
- // generateCode generates an N digit TOTP code from the secret token.
- func generateCode(opts ...GeneratorOptionsFunc) (string, string, error) {
- defaults := &options{
- algorithm: defaultAlgorithm,
- length: defaultLength,
- timePeriod: defaultTimePeriod,
- when: time.Now(),
- }
- for _, opt := range opts {
- opt(defaults)
- }
- cleanUpToken(defaults)
- if defaults.length > math.MaxInt {
- return "", "", errors.New("length too big")
- }
- timer := uint64(math.Floor(float64(defaults.when.Unix()) / float64(defaults.timePeriod)))
- remainingTime := defaults.timePeriod - defaults.when.Unix()%defaults.timePeriod
- secretBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(defaults.token)
- if err != nil {
- return "", "", fmt.Errorf("failed to generate OTP code: %w", err)
- }
- buf := make([]byte, 8)
- shaFunc, err := getAlgorithmFunction(defaults.algorithm)
- if err != nil {
- return "", "", err
- }
- mac := hmac.New(shaFunc, secretBytes)
- binary.BigEndian.PutUint64(buf, timer)
- _, _ = mac.Write(buf)
- sum := mac.Sum(nil)
- // http://tools.ietf.org/html/rfc4226#section-5.4
- offset := sum[len(sum)-1] & 0xf
- value := ((int(sum[offset]) & 0x7f) << 24) |
- ((int(sum[offset+1] & 0xff)) << 16) |
- ((int(sum[offset+2] & 0xff)) << 8) |
- (int(sum[offset+3]) & 0xff)
- modulo := value % int(math.Pow10(defaults.length))
- format := fmt.Sprintf("%%0%dd", defaults.length)
- return fmt.Sprintf(format, modulo), strconv.Itoa(int(remainingTime)), nil
- }
- func cleanUpToken(defaults *options) {
- // Remove all spaces. Providers sometimes make it more readable by fragmentation.
- defaults.token = strings.ReplaceAll(defaults.token, " ", "")
- // The token is always uppercase.
- defaults.token = strings.ToUpper(defaults.token)
- }
- func getAlgorithmFunction(algo string) (func() hash.Hash, error) {
- switch algo {
- case "sha512":
- return sha512.New, nil
- case "sha384":
- return sha512.New384, nil
- case "sha512_256":
- return sha512.New512_256, nil
- case "sha256":
- return sha256.New, nil
- case "sha1":
- return sha1.New, nil
- default:
- return nil, fmt.Errorf("%s for hash function is invalid", algo)
- }
- }
|