git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/otp/totp/totp_test.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 totp
    19  
    20  import (
    21  	"git.sr.ht/~pingoo/stdx/otp"
    22  	"github.com/stretchr/testify/assert"
    23  	"github.com/stretchr/testify/require"
    24  
    25  	"encoding/base32"
    26  	"testing"
    27  	"time"
    28  )
    29  
    30  type tc struct {
    31  	TS     int64
    32  	TOTP   string
    33  	Mode   otp.Algorithm
    34  	Secret string
    35  }
    36  
    37  var (
    38  	secSha1   = base32.StdEncoding.EncodeToString([]byte("12345678901234567890"))
    39  	secSha256 = base32.StdEncoding.EncodeToString([]byte("12345678901234567890123456789012"))
    40  	secSha512 = base32.StdEncoding.EncodeToString([]byte("1234567890123456789012345678901234567890123456789012345678901234"))
    41  
    42  	rfcMatrixTCs = []tc{
    43  		{59, "94287082", otp.AlgorithmSHA1, secSha1},
    44  		{59, "46119246", otp.AlgorithmSHA256, secSha256},
    45  		{59, "90693936", otp.AlgorithmSHA512, secSha512},
    46  		{1111111109, "07081804", otp.AlgorithmSHA1, secSha1},
    47  		{1111111109, "68084774", otp.AlgorithmSHA256, secSha256},
    48  		{1111111109, "25091201", otp.AlgorithmSHA512, secSha512},
    49  		{1111111111, "14050471", otp.AlgorithmSHA1, secSha1},
    50  		{1111111111, "67062674", otp.AlgorithmSHA256, secSha256},
    51  		{1111111111, "99943326", otp.AlgorithmSHA512, secSha512},
    52  		{1234567890, "89005924", otp.AlgorithmSHA1, secSha1},
    53  		{1234567890, "91819424", otp.AlgorithmSHA256, secSha256},
    54  		{1234567890, "93441116", otp.AlgorithmSHA512, secSha512},
    55  		{2000000000, "69279037", otp.AlgorithmSHA1, secSha1},
    56  		{2000000000, "90698825", otp.AlgorithmSHA256, secSha256},
    57  		{2000000000, "38618901", otp.AlgorithmSHA512, secSha512},
    58  		{20000000000, "65353130", otp.AlgorithmSHA1, secSha1},
    59  		{20000000000, "77737706", otp.AlgorithmSHA256, secSha256},
    60  		{20000000000, "47863826", otp.AlgorithmSHA512, secSha512},
    61  	}
    62  )
    63  
    64  // Test vectors from http://tools.ietf.org/html/rfc6238#appendix-B
    65  // NOTE -- the test vectors are documented as having the SAME
    66  // secret -- this is WRONG -- they have a variable secret
    67  // depending upon the hmac algorithm:
    68  //
    69  //	http://www.rfc-editor.org/errata_search.php?rfc=6238
    70  //
    71  // this only took a few hours of head/desk interaction to figure out.
    72  func TestValidateRFCMatrix(t *testing.T) {
    73  	for _, tx := range rfcMatrixTCs {
    74  		valid, err := ValidateCustom(tx.TOTP, tx.Secret, time.Unix(tx.TS, 0).UTC(),
    75  			ValidateOpts{
    76  				Digits:    otp.DigitsEight,
    77  				Algorithm: tx.Mode,
    78  			})
    79  		require.NoError(t, err,
    80  			"unexpected error totp=%s mode=%v ts=%v", tx.TOTP, tx.Mode, tx.TS)
    81  		require.True(t, valid,
    82  			"unexpected totp failure totp=%s mode=%v ts=%v", tx.TOTP, tx.Mode, tx.TS)
    83  	}
    84  }
    85  
    86  func TestGenerateRFCTCs(t *testing.T) {
    87  	for _, tx := range rfcMatrixTCs {
    88  		passcode, err := GenerateCodeCustom(tx.Secret, time.Unix(tx.TS, 0).UTC(),
    89  			ValidateOpts{
    90  				Digits:    otp.DigitsEight,
    91  				Algorithm: tx.Mode,
    92  			})
    93  		assert.Nil(t, err)
    94  		assert.Equal(t, tx.TOTP, passcode)
    95  	}
    96  }
    97  
    98  func TestValidateSkew(t *testing.T) {
    99  	secSha1 := base32.StdEncoding.EncodeToString([]byte("12345678901234567890"))
   100  
   101  	tests := []tc{
   102  		{29, "94287082", otp.AlgorithmSHA1, secSha1},
   103  		{59, "94287082", otp.AlgorithmSHA1, secSha1},
   104  		{61, "94287082", otp.AlgorithmSHA1, secSha1},
   105  	}
   106  
   107  	for _, tx := range tests {
   108  		valid, err := ValidateCustom(tx.TOTP, tx.Secret, time.Unix(tx.TS, 0).UTC(),
   109  			ValidateOpts{
   110  				Digits:    otp.DigitsEight,
   111  				Algorithm: tx.Mode,
   112  				Skew:      1,
   113  			})
   114  		require.NoError(t, err,
   115  			"unexpected error totp=%s mode=%v ts=%v", tx.TOTP, tx.Mode, tx.TS)
   116  		require.True(t, valid,
   117  			"unexpected totp failure totp=%s mode=%v ts=%v", tx.TOTP, tx.Mode, tx.TS)
   118  	}
   119  }
   120  
   121  func TestGenerate(t *testing.T) {
   122  	k, err := Generate(GenerateOpts{
   123  		Issuer:      "SnakeOil",
   124  		AccountName: "alice@example.com",
   125  	})
   126  	require.NoError(t, err, "generate basic TOTP")
   127  	require.Equal(t, "SnakeOil", k.Issuer(), "Extracting Issuer")
   128  	require.Equal(t, "alice@example.com", k.AccountName(), "Extracting Account Name")
   129  	require.Equal(t, 32, len(k.Secret()), "Secret is 32 bytes long as base32.")
   130  
   131  	k, err = Generate(GenerateOpts{
   132  		Issuer:      "SnakeOil",
   133  		AccountName: "alice@example.com",
   134  		SecretSize:  20,
   135  	})
   136  	require.NoError(t, err, "generate larger TOTP")
   137  	require.Equal(t, 32, len(k.Secret()), "Secret is 32 bytes long as base32.")
   138  
   139  	k, err = Generate(GenerateOpts{
   140  		Issuer:      "SnakeOil",
   141  		AccountName: "alice@example.com",
   142  		SecretSize:  13, // anything that is not divisable by 5, really
   143  	})
   144  	require.NoError(t, err, "Secret size is valid when length not divisable by 5.")
   145  	require.NotContains(t, k.Secret(), "=", "Secret has no escaped characters.")
   146  
   147  	k, err = Generate(GenerateOpts{
   148  		Issuer:      "SnakeOil",
   149  		AccountName: "alice@example.com",
   150  		Secret:      []byte("helloworld"),
   151  	})
   152  	require.NoError(t, err, "Secret generation failed")
   153  	sec, err := b32NoPadding.DecodeString(k.Secret())
   154  	require.NoError(t, err, "Secret wa not valid base32")
   155  	require.Equal(t, sec, []byte("helloworld"), "Specified Secret was not kept")
   156  }
   157  
   158  func TestGoogleLowerCaseSecret(t *testing.T) {
   159  	w, err := otp.NewKeyFromURL(`otpauth://totp/Google%3Afoo%40example.com?secret=qlt6vmy6svfx4bt4rpmisaiyol6hihca&issuer=Google`)
   160  	require.NoError(t, err)
   161  	sec := w.Secret()
   162  	require.Equal(t, "qlt6vmy6svfx4bt4rpmisaiyol6hihca", sec)
   163  
   164  	n := time.Now().UTC()
   165  	code, err := GenerateCode(w.Secret(), n)
   166  	require.NoError(t, err)
   167  
   168  	valid := Validate(code, w.Secret())
   169  	require.True(t, valid)
   170  }