/** * Copyright 2014 Paul Querna * * 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 * * http://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 hotp import ( "github.com/pquerna/otp" "crypto/hmac" "crypto/rand" "crypto/subtle" "encoding/base32" "encoding/binary" "fmt" "math" "net/url" "strings" ) const debug = false // Validate a HOTP passcode given a counter and secret. // This is a shortcut for ValidateCustom, with parameters that // are compataible with Google-Authenticator. func Validate(passcode string, counter uint64, secret string) bool { rv, _ := ValidateCustom( passcode, counter, secret, ValidateOpts{ Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }, ) return rv } // ValidateOpts provides options for ValidateCustom(). type ValidateOpts struct { // Digits as part of the input. Defaults to 6. Digits otp.Digits // Algorithm to use for HMAC. Defaults to SHA1. Algorithm otp.Algorithm } // GenerateCode creates a HOTP passcode given a counter and secret. // This is a shortcut for GenerateCodeCustom, with parameters that // are compataible with Google-Authenticator. func GenerateCode(secret string, counter uint64) (string, error) { return GenerateCodeCustom(secret, counter, ValidateOpts{ Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }) } // GenerateCodeCustom uses a counter and secret value and options struct to // create a passcode. func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) { // As noted in issue #10 and #17 this adds support for TOTP secrets that are // missing their padding. secret = strings.TrimSpace(secret) if n := len(secret) % 8; n != 0 { secret = secret + strings.Repeat("=", 8-n) } // As noted in issue #24 Google has started producing base32 in lower case, // but the StdEncoding (and the RFC), expect a dictionary of only upper case letters. secret = strings.ToUpper(secret) secretBytes, err := base32.StdEncoding.DecodeString(secret) if err != nil { return "", otp.ErrValidateSecretInvalidBase32 } buf := make([]byte, 8) mac := hmac.New(opts.Algorithm.Hash, secretBytes) binary.BigEndian.PutUint64(buf, counter) if debug { fmt.Printf("counter=%v\n", counter) fmt.Printf("buf=%v\n", buf) } mac.Write(buf) sum := mac.Sum(nil) // "Dynamic truncation" in RFC 4226 // http://tools.ietf.org/html/rfc4226#section-5.4 offset := sum[len(sum)-1] & 0xf value := int64(((int(sum[offset]) & 0x7f) << 24) | ((int(sum[offset+1] & 0xff)) << 16) | ((int(sum[offset+2] & 0xff)) << 8) | (int(sum[offset+3]) & 0xff)) l := opts.Digits.Length() mod := int32(value % int64(math.Pow10(l))) if debug { fmt.Printf("offset=%v\n", offset) fmt.Printf("value=%v\n", value) fmt.Printf("mod'ed=%v\n", mod) } return opts.Digits.Format(mod), nil } // ValidateCustom validates an HOTP with customizable options. Most users should // use Validate(). func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) { passcode = strings.TrimSpace(passcode) if len(passcode) != opts.Digits.Length() { return false, otp.ErrValidateInputInvalidLength } otpstr, err := GenerateCodeCustom(secret, counter, opts) if err != nil { return false, err } if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 { return true, nil } return false, nil } // GenerateOpts provides options for .Generate() type GenerateOpts struct { // Name of the issuing Organization/Company. Issuer string // Name of the User's Account (eg, email address) AccountName string // Size in size of the generated Secret. Defaults to 10 bytes. SecretSize uint // Secret to store. Defaults to a randomly generated secret of SecretSize. You should generally leave this empty. Secret []byte // Digits to request. Defaults to 6. Digits otp.Digits // Algorithm to use for HMAC. Defaults to SHA1. Algorithm otp.Algorithm } var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding) // Generate creates a new HOTP Key. func Generate(opts GenerateOpts) (*otp.Key, error) { // url encode the Issuer/AccountName if opts.Issuer == "" { return nil, otp.ErrGenerateMissingIssuer } if opts.AccountName == "" { return nil, otp.ErrGenerateMissingAccountName } if opts.SecretSize == 0 { opts.SecretSize = 10 } if opts.Digits == 0 { opts.Digits = otp.DigitsSix } // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example v := url.Values{} if len(opts.Secret) != 0 { v.Set("secret", b32NoPadding.EncodeToString(opts.Secret)) } else { secret := make([]byte, opts.SecretSize) _, err := rand.Read(secret) if err != nil { return nil, err } v.Set("secret", b32NoPadding.EncodeToString(secret)) } v.Set("issuer", opts.Issuer) v.Set("algorithm", opts.Algorithm.String()) v.Set("digits", opts.Digits.String()) u := url.URL{ Scheme: "otpauth", Host: "hotp", Path: "/" + opts.Issuer + ":" + opts.AccountName, RawQuery: v.Encode(), } return otp.NewKeyFromURL(u.String()) }