hotp.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 hotp
  19  
  20  import (
  21  	"github.com/pquerna/otp"
  22  	"github.com/pquerna/otp/internal"
  23  	"io"
  24  
  25  	"crypto/hmac"
  26  	"crypto/rand"
  27  	"crypto/subtle"
  28  	"encoding/base32"
  29  	"encoding/binary"
  30  	"fmt"
  31  	"math"
  32  	"net/url"
  33  	"strings"
  34  )
  35  
  36  const debug = false
  37  
  38  // Validate a HOTP passcode given a counter and secret.
  39  // This is a shortcut for ValidateCustom, with parameters that
  40  // are compataible with Google-Authenticator.
  41  func Validate(passcode string, counter uint64, secret string) bool {
  42  	rv, _ := ValidateCustom(
  43  		passcode,
  44  		counter,
  45  		secret,
  46  		ValidateOpts{
  47  			Digits:    otp.DigitsSix,
  48  			Algorithm: otp.AlgorithmSHA1,
  49  		},
  50  	)
  51  	return rv
  52  }
  53  
  54  // ValidateOpts provides options for ValidateCustom().
  55  type ValidateOpts struct {
  56  	// Digits as part of the input. Defaults to 6.
  57  	Digits otp.Digits
  58  	// Algorithm to use for HMAC. Defaults to SHA1.
  59  	Algorithm otp.Algorithm
  60  	// Encoder to use for output code.
  61  	Encoder otp.Encoder
  62  }
  63  
  64  // GenerateCode creates a HOTP passcode given a counter and secret.
  65  // This is a shortcut for GenerateCodeCustom, with parameters that
  66  // are compataible with Google-Authenticator.
  67  func GenerateCode(secret string, counter uint64) (string, error) {
  68  	return GenerateCodeCustom(secret, counter, ValidateOpts{
  69  		Digits:    otp.DigitsSix,
  70  		Algorithm: otp.AlgorithmSHA1,
  71  	})
  72  }
  73  
  74  // GenerateCodeCustom uses a counter and secret value and options struct to
  75  // create a passcode.
  76  func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) {
  77  	//Set default value
  78  	if opts.Digits == 0 {
  79  		opts.Digits = otp.DigitsSix
  80  	}
  81  	// As noted in issue #10 and #17 this adds support for TOTP secrets that are
  82  	// missing their padding.
  83  	secret = strings.TrimSpace(secret)
  84  	if n := len(secret) % 8; n != 0 {
  85  		secret = secret + strings.Repeat("=", 8-n)
  86  	}
  87  
  88  	// As noted in issue #24 Google has started producing base32 in lower case,
  89  	// but the StdEncoding (and the RFC), expect a dictionary of only upper case letters.
  90  	secret = strings.ToUpper(secret)
  91  
  92  	secretBytes, err := base32.StdEncoding.DecodeString(secret)
  93  	if err != nil {
  94  		return "", otp.ErrValidateSecretInvalidBase32
  95  	}
  96  
  97  	buf := make([]byte, 8)
  98  	mac := hmac.New(opts.Algorithm.Hash, secretBytes)
  99  	binary.BigEndian.PutUint64(buf, counter)
 100  	if debug {
 101  		fmt.Printf("counter=%v\n", counter)
 102  		fmt.Printf("buf=%v\n", buf)
 103  	}
 104  
 105  	mac.Write(buf)
 106  	sum := mac.Sum(nil)
 107  
 108  	// "Dynamic truncation" in RFC 4226
 109  	// http://tools.ietf.org/html/rfc4226#section-5.4
 110  	offset := sum[len(sum)-1] & 0xf
 111  	value := int64(((int(sum[offset]) & 0x7f) << 24) |
 112  		((int(sum[offset+1] & 0xff)) << 16) |
 113  		((int(sum[offset+2] & 0xff)) << 8) |
 114  		(int(sum[offset+3]) & 0xff))
 115  
 116  	l := opts.Digits.Length()
 117  	switch opts.Encoder {
 118  	case otp.EncoderDefault:
 119  		mod := int32(value % int64(math.Pow10(l)))
 120  
 121  		if debug {
 122  			fmt.Printf("offset=%v\n", offset)
 123  			fmt.Printf("value=%v\n", value)
 124  			fmt.Printf("mod'ed=%v\n", mod)
 125  		}
 126  		passcode = opts.Digits.Format(mod)
 127  	case otp.EncoderSteam:
 128  		// Define the character set used by Steam Guard codes.
 129  		alphabet := []byte{
 130  			'2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C',
 131  			'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q',
 132  			'R', 'T', 'V', 'W', 'X', 'Y',
 133  		}
 134  		radix := int64(len(alphabet))
 135  
 136  		for i := 0; i < l; i++ {
 137  			digit := value % radix
 138  			value /= radix
 139  			c := alphabet[digit]
 140  			passcode += string(c)
 141  		}
 142  	}
 143  
 144  	return
 145  }
 146  
 147  // ValidateCustom validates an HOTP with customizable options. Most users should
 148  // use Validate().
 149  func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) {
 150  	passcode = strings.TrimSpace(passcode)
 151  
 152  	if len(passcode) != opts.Digits.Length() {
 153  		return false, otp.ErrValidateInputInvalidLength
 154  	}
 155  
 156  	otpstr, err := GenerateCodeCustom(secret, counter, opts)
 157  	if err != nil {
 158  		return false, err
 159  	}
 160  
 161  	if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 {
 162  		return true, nil
 163  	}
 164  
 165  	return false, nil
 166  }
 167  
 168  // GenerateOpts provides options for .Generate()
 169  type GenerateOpts struct {
 170  	// Name of the issuing Organization/Company.
 171  	Issuer string
 172  	// Name of the User's Account (eg, email address)
 173  	AccountName string
 174  	// Size in size of the generated Secret. Defaults to 10 bytes.
 175  	SecretSize uint
 176  	// Secret to store. Defaults to a randomly generated secret of SecretSize.  You should generally leave this empty.
 177  	Secret []byte
 178  	// Digits to request. Defaults to 6.
 179  	Digits otp.Digits
 180  	// Algorithm to use for HMAC. Defaults to SHA1.
 181  	Algorithm otp.Algorithm
 182  	// Reader to use for generating HOTP Key.
 183  	Rand io.Reader
 184  }
 185  
 186  var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
 187  
 188  // Generate creates a new HOTP Key.
 189  func Generate(opts GenerateOpts) (*otp.Key, error) {
 190  	// url encode the Issuer/AccountName
 191  	if opts.Issuer == "" {
 192  		return nil, otp.ErrGenerateMissingIssuer
 193  	}
 194  
 195  	if opts.AccountName == "" {
 196  		return nil, otp.ErrGenerateMissingAccountName
 197  	}
 198  
 199  	if opts.SecretSize == 0 {
 200  		opts.SecretSize = 10
 201  	}
 202  
 203  	if opts.Digits == 0 {
 204  		opts.Digits = otp.DigitsSix
 205  	}
 206  
 207  	if opts.Rand == nil {
 208  		opts.Rand = rand.Reader
 209  	}
 210  
 211  	// otpauth://hotp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
 212  
 213  	v := url.Values{}
 214  	if len(opts.Secret) != 0 {
 215  		v.Set("secret", b32NoPadding.EncodeToString(opts.Secret))
 216  	} else {
 217  		secret := make([]byte, opts.SecretSize)
 218  		_, err := io.ReadFull(opts.Rand, secret)
 219  		if err != nil {
 220  			return nil, err
 221  		}
 222  		v.Set("secret", b32NoPadding.EncodeToString(secret))
 223  	}
 224  
 225  	v.Set("issuer", opts.Issuer)
 226  	v.Set("algorithm", opts.Algorithm.String())
 227  	v.Set("digits", opts.Digits.String())
 228  
 229  	u := url.URL{
 230  		Scheme:   "otpauth",
 231  		Host:     "hotp",
 232  		Path:     "/" + opts.Issuer + ":" + opts.AccountName,
 233  		RawQuery: internal.EncodeQuery(v),
 234  	}
 235  
 236  	return otp.NewKeyFromURL(u.String())
 237  }
 238