git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/otp/totp/totp.go (about)

     1  /**
     2   *  Copyright 2014 Paul Querna
     3   *
     4   *  Licensed under the Apache License, Version 2.0 (the "License");
     5   *  you may not use this file except in compliance with the License.
     6   *  You may obtain a copy of the License at
     7   *
     8   *      http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   *  Unless required by applicable law or agreed to in writing, software
    11   *  distributed under the License is distributed on an "AS IS" BASIS,
    12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   *  See the License for the specific language governing permissions and
    14   *  limitations under the License.
    15   *
    16   */
    17  
    18  package totp
    19  
    20  import (
    21  	"io"
    22  
    23  	"git.sr.ht/~pingoo/stdx/otp"
    24  	"git.sr.ht/~pingoo/stdx/otp/hotp"
    25  
    26  	"crypto/rand"
    27  	"encoding/base32"
    28  	"math"
    29  	"net/url"
    30  	"strconv"
    31  	"time"
    32  )
    33  
    34  // Validate a TOTP using the current time.
    35  // A shortcut for ValidateCustom, Validate uses a configuration
    36  // that is compatible with Google-Authenticator and most clients.
    37  func Validate(passcode string, secret string) bool {
    38  	rv, _ := ValidateCustom(
    39  		passcode,
    40  		secret,
    41  		time.Now().UTC(),
    42  		ValidateOpts{
    43  			Period:    30,
    44  			Skew:      1,
    45  			Digits:    otp.DigitsSix,
    46  			Algorithm: otp.AlgorithmSHA1,
    47  		},
    48  	)
    49  	return rv
    50  }
    51  
    52  // GenerateCode creates a TOTP token using the current time.
    53  // A shortcut for GenerateCodeCustom, GenerateCode uses a configuration
    54  // that is compatible with Google-Authenticator and most clients.
    55  func GenerateCode(secret string, t time.Time) (string, error) {
    56  	return GenerateCodeCustom(secret, t, ValidateOpts{
    57  		Period:    30,
    58  		Skew:      1,
    59  		Digits:    otp.DigitsSix,
    60  		Algorithm: otp.AlgorithmSHA1,
    61  	})
    62  }
    63  
    64  // ValidateOpts provides options for ValidateCustom().
    65  type ValidateOpts struct {
    66  	// Number of seconds a TOTP hash is valid for. Defaults to 30 seconds.
    67  	Period uint
    68  	// Periods before or after the current time to allow.  Value of 1 allows up to Period
    69  	// of either side of the specified time.  Defaults to 0 allowed skews.  Values greater
    70  	// than 1 are likely sketchy.
    71  	Skew uint
    72  	// Digits as part of the input. Defaults to 6.
    73  	Digits otp.Digits
    74  	// Algorithm to use for HMAC. Defaults to SHA1.
    75  	Algorithm otp.Algorithm
    76  }
    77  
    78  // GenerateCodeCustom takes a timepoint and produces a passcode using a
    79  // secret and the provided opts. (Under the hood, this is making an adapted
    80  // call to hotp.GenerateCodeCustom)
    81  func GenerateCodeCustom(secret string, t time.Time, opts ValidateOpts) (passcode string, err error) {
    82  	if opts.Period == 0 {
    83  		opts.Period = 30
    84  	}
    85  	counter := uint64(math.Floor(float64(t.Unix()) / float64(opts.Period)))
    86  	passcode, err = hotp.GenerateCodeCustom(secret, counter, hotp.ValidateOpts{
    87  		Digits:    opts.Digits,
    88  		Algorithm: opts.Algorithm,
    89  	})
    90  	if err != nil {
    91  		return "", err
    92  	}
    93  	return passcode, nil
    94  }
    95  
    96  // ValidateCustom validates a TOTP given a user specified time and custom options.
    97  // Most users should use Validate() to provide an interpolatable TOTP experience.
    98  func ValidateCustom(passcode string, secret string, t time.Time, opts ValidateOpts) (bool, error) {
    99  	if opts.Period == 0 {
   100  		opts.Period = 30
   101  	}
   102  
   103  	counters := []uint64{}
   104  	counter := int64(math.Floor(float64(t.Unix()) / float64(opts.Period)))
   105  
   106  	counters = append(counters, uint64(counter))
   107  	for i := 1; i <= int(opts.Skew); i++ {
   108  		counters = append(counters, uint64(counter+int64(i)))
   109  		counters = append(counters, uint64(counter-int64(i)))
   110  	}
   111  
   112  	for _, counter := range counters {
   113  		rv, err := hotp.ValidateCustom(passcode, counter, secret, hotp.ValidateOpts{
   114  			Digits:    opts.Digits,
   115  			Algorithm: opts.Algorithm,
   116  		})
   117  
   118  		if err != nil {
   119  			return false, err
   120  		}
   121  
   122  		if rv == true {
   123  			return true, nil
   124  		}
   125  	}
   126  
   127  	return false, nil
   128  }
   129  
   130  // GenerateOpts provides options for Generate().  The default values
   131  // are compatible with Google-Authenticator.
   132  type GenerateOpts struct {
   133  	// Name of the issuing Organization/Company.
   134  	Issuer string
   135  	// Name of the User's Account (eg, email address)
   136  	AccountName string
   137  	// Number of seconds a TOTP hash is valid for. Defaults to 30 seconds.
   138  	Period uint
   139  	// Size in size of the generated Secret. Defaults to 20 bytes.
   140  	SecretSize uint
   141  	// Secret to store. Defaults to a randomly generated secret of SecretSize.  You should generally leave this empty.
   142  	Secret []byte
   143  	// Digits to request. Defaults to 6.
   144  	Digits otp.Digits
   145  	// Algorithm to use for HMAC. Defaults to SHA1.
   146  	Algorithm otp.Algorithm
   147  	// Reader to use for generating TOTP Key.
   148  	Rand io.Reader
   149  }
   150  
   151  var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
   152  
   153  // Generate a new TOTP Key.
   154  func Generate(opts GenerateOpts) (*otp.Key, error) {
   155  	// url encode the Issuer/AccountName
   156  	if opts.Issuer == "" {
   157  		return nil, otp.ErrGenerateMissingIssuer
   158  	}
   159  
   160  	if opts.AccountName == "" {
   161  		return nil, otp.ErrGenerateMissingAccountName
   162  	}
   163  
   164  	if opts.Period == 0 {
   165  		opts.Period = 30
   166  	}
   167  
   168  	if opts.SecretSize == 0 {
   169  		opts.SecretSize = 20
   170  	}
   171  
   172  	if opts.Digits == 0 {
   173  		opts.Digits = otp.DigitsSix
   174  	}
   175  
   176  	if opts.Rand == nil {
   177  		opts.Rand = rand.Reader
   178  	}
   179  
   180  	// otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
   181  
   182  	v := url.Values{}
   183  	if len(opts.Secret) != 0 {
   184  		v.Set("secret", b32NoPadding.EncodeToString(opts.Secret))
   185  	} else {
   186  		secret := make([]byte, opts.SecretSize)
   187  		_, err := opts.Rand.Read(secret)
   188  		if err != nil {
   189  			return nil, err
   190  		}
   191  		v.Set("secret", b32NoPadding.EncodeToString(secret))
   192  	}
   193  
   194  	v.Set("issuer", opts.Issuer)
   195  	v.Set("period", strconv.FormatUint(uint64(opts.Period), 10))
   196  	v.Set("algorithm", opts.Algorithm.String())
   197  	v.Set("digits", opts.Digits.String())
   198  
   199  	u := url.URL{
   200  		Scheme:   "otpauth",
   201  		Host:     "totp",
   202  		Path:     "/" + opts.Issuer + ":" + opts.AccountName,
   203  		RawQuery: v.Encode(),
   204  	}
   205  
   206  	return otp.NewKeyFromURL(u.String())
   207  }