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