...

Source file src/github.com/pquerna/otp/hotp/hotp.go

Documentation: github.com/pquerna/otp/hotp

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

View as plain text