totp.go raw

   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  	"github.com/pquerna/otp"
  22  	"github.com/pquerna/otp/hotp"
  23  	"github.com/pquerna/otp/internal"
  24  	"io"
  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  	// Encoder to use for output code.
  77  	Encoder otp.Encoder
  78  }
  79  
  80  // GenerateCodeCustom takes a timepoint and produces a passcode using a
  81  // secret and the provided opts. (Under the hood, this is making an adapted
  82  // call to hotp.GenerateCodeCustom)
  83  func GenerateCodeCustom(secret string, t time.Time, opts ValidateOpts) (passcode string, err error) {
  84  	if opts.Period == 0 {
  85  		opts.Period = 30
  86  	}
  87  	counter := uint64(math.Floor(float64(t.Unix()) / float64(opts.Period)))
  88  	passcode, err = hotp.GenerateCodeCustom(secret, counter, hotp.ValidateOpts{
  89  		Digits:    opts.Digits,
  90  		Algorithm: opts.Algorithm,
  91  		Encoder:   opts.Encoder,
  92  	})
  93  	if err != nil {
  94  		return "", err
  95  	}
  96  	return passcode, nil
  97  }
  98  
  99  // ValidateCustom validates a TOTP given a user specified time and custom options.
 100  // Most users should use Validate() to provide an interpolatable TOTP experience.
 101  func ValidateCustom(passcode string, secret string, t time.Time, opts ValidateOpts) (bool, error) {
 102  	if opts.Period == 0 {
 103  		opts.Period = 30
 104  	}
 105  
 106  	counters := []uint64{}
 107  	counter := int64(math.Floor(float64(t.Unix()) / float64(opts.Period)))
 108  
 109  	counters = append(counters, uint64(counter))
 110  	for i := 1; i <= int(opts.Skew); i++ {
 111  		counters = append(counters, uint64(counter+int64(i)))
 112  		counters = append(counters, uint64(counter-int64(i)))
 113  	}
 114  
 115  	for _, counter := range counters {
 116  		rv, err := hotp.ValidateCustom(passcode, counter, secret, hotp.ValidateOpts{
 117  			Digits:    opts.Digits,
 118  			Algorithm: opts.Algorithm,
 119  			Encoder:   opts.Encoder,
 120  		})
 121  		if err != nil {
 122  			return false, err
 123  		}
 124  
 125  		if rv == true {
 126  			return true, nil
 127  		}
 128  	}
 129  
 130  	return false, nil
 131  }
 132  
 133  // GenerateOpts provides options for Generate().  The default values
 134  // are compatible with Google-Authenticator.
 135  type GenerateOpts struct {
 136  	// Name of the issuing Organization/Company.
 137  	Issuer string
 138  	// Name of the User's Account (eg, email address)
 139  	AccountName string
 140  	// Number of seconds a TOTP hash is valid for. Defaults to 30 seconds.
 141  	Period uint
 142  	// Size in size of the generated Secret. Defaults to 20 bytes.
 143  	SecretSize uint
 144  	// Secret to store. Defaults to a randomly generated secret of SecretSize.  You should generally leave this empty.
 145  	Secret []byte
 146  	// Digits to request. Defaults to 6.
 147  	Digits otp.Digits
 148  	// Algorithm to use for HMAC. Defaults to SHA1.
 149  	Algorithm otp.Algorithm
 150  	// Reader to use for generating TOTP Key.
 151  	Rand io.Reader
 152  }
 153  
 154  var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
 155  
 156  // Generate a new TOTP Key.
 157  func Generate(opts GenerateOpts) (*otp.Key, error) {
 158  	// url encode the Issuer/AccountName
 159  	if opts.Issuer == "" {
 160  		return nil, otp.ErrGenerateMissingIssuer
 161  	}
 162  
 163  	if opts.AccountName == "" {
 164  		return nil, otp.ErrGenerateMissingAccountName
 165  	}
 166  
 167  	if opts.Period == 0 {
 168  		opts.Period = 30
 169  	}
 170  
 171  	if opts.SecretSize == 0 {
 172  		opts.SecretSize = 20
 173  	}
 174  
 175  	if opts.Digits == 0 {
 176  		opts.Digits = otp.DigitsSix
 177  	}
 178  
 179  	if opts.Rand == nil {
 180  		opts.Rand = rand.Reader
 181  	}
 182  
 183  	// otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
 184  
 185  	v := url.Values{}
 186  	if len(opts.Secret) != 0 {
 187  		v.Set("secret", b32NoPadding.EncodeToString(opts.Secret))
 188  	} else {
 189  		secret := make([]byte, opts.SecretSize)
 190  		_, err := io.ReadFull(opts.Rand, secret)
 191  		if err != nil {
 192  			return nil, err
 193  		}
 194  		v.Set("secret", b32NoPadding.EncodeToString(secret))
 195  	}
 196  
 197  	v.Set("issuer", opts.Issuer)
 198  	v.Set("period", strconv.FormatUint(uint64(opts.Period), 10))
 199  	v.Set("algorithm", opts.Algorithm.String())
 200  	v.Set("digits", opts.Digits.String())
 201  
 202  	u := url.URL{
 203  		Scheme:   "otpauth",
 204  		Host:     "totp",
 205  		Path:     "/" + opts.Issuer + ":" + opts.AccountName,
 206  		RawQuery: internal.EncodeQuery(v),
 207  	}
 208  
 209  	return otp.NewKeyFromURL(u.String())
 210  }
 211