git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/otp/hotp/hotp_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 hotp
    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  )
    28  
    29  type tc struct {
    30  	Counter uint64
    31  	TOTP    string
    32  	Mode    otp.Algorithm
    33  	Secret  string
    34  }
    35  
    36  var (
    37  	secSha1 = base32.StdEncoding.EncodeToString([]byte("12345678901234567890"))
    38  
    39  	rfcMatrixTCs = []tc{
    40  		{0, "755224", otp.AlgorithmSHA1, secSha1},
    41  		{1, "287082", otp.AlgorithmSHA1, secSha1},
    42  		{2, "359152", otp.AlgorithmSHA1, secSha1},
    43  		{3, "969429", otp.AlgorithmSHA1, secSha1},
    44  		{4, "338314", otp.AlgorithmSHA1, secSha1},
    45  		{5, "254676", otp.AlgorithmSHA1, secSha1},
    46  		{6, "287922", otp.AlgorithmSHA1, secSha1},
    47  		{7, "162583", otp.AlgorithmSHA1, secSha1},
    48  		{8, "399871", otp.AlgorithmSHA1, secSha1},
    49  		{9, "520489", otp.AlgorithmSHA1, secSha1},
    50  	}
    51  )
    52  
    53  // Test values from http://tools.ietf.org/html/rfc4226#appendix-D
    54  func TestValidateRFCMatrix(t *testing.T) {
    55  
    56  	for _, tx := range rfcMatrixTCs {
    57  		valid, err := ValidateCustom(tx.TOTP, tx.Counter, tx.Secret,
    58  			ValidateOpts{
    59  				Digits:    otp.DigitsSix,
    60  				Algorithm: tx.Mode,
    61  			})
    62  		require.NoError(t, err,
    63  			"unexpected error totp=%s mode=%v counter=%v", tx.TOTP, tx.Mode, tx.Counter)
    64  		require.True(t, valid,
    65  			"unexpected totp failure totp=%s mode=%v counter=%v", tx.TOTP, tx.Mode, tx.Counter)
    66  	}
    67  }
    68  
    69  func TestGenerateRFCMatrix(t *testing.T) {
    70  	for _, tx := range rfcMatrixTCs {
    71  		passcode, err := GenerateCodeCustom(tx.Secret, tx.Counter,
    72  			ValidateOpts{
    73  				Digits:    otp.DigitsSix,
    74  				Algorithm: tx.Mode,
    75  			})
    76  		assert.Nil(t, err)
    77  		assert.Equal(t, tx.TOTP, passcode)
    78  	}
    79  }
    80  
    81  func TestValidateInvalid(t *testing.T) {
    82  	secSha1 := base32.StdEncoding.EncodeToString([]byte("12345678901234567890"))
    83  
    84  	valid, err := ValidateCustom("foo", 11, secSha1,
    85  		ValidateOpts{
    86  			Digits:    otp.DigitsSix,
    87  			Algorithm: otp.AlgorithmSHA1,
    88  		})
    89  	require.Equal(t, otp.ErrValidateInputInvalidLength, err, "Expected Invalid length error.")
    90  	require.Equal(t, false, valid, "Valid should be false when we have an error.")
    91  
    92  	valid, err = ValidateCustom("foo", 11, secSha1,
    93  		ValidateOpts{
    94  			Digits:    otp.DigitsEight,
    95  			Algorithm: otp.AlgorithmSHA1,
    96  		})
    97  	require.Equal(t, otp.ErrValidateInputInvalidLength, err, "Expected Invalid length error.")
    98  	require.Equal(t, false, valid, "Valid should be false when we have an error.")
    99  
   100  	valid, err = ValidateCustom("000000", 11, secSha1,
   101  		ValidateOpts{
   102  			Digits:    otp.DigitsSix,
   103  			Algorithm: otp.AlgorithmSHA1,
   104  		})
   105  	require.NoError(t, err, "Expected no error.")
   106  	require.Equal(t, false, valid, "Valid should be false.")
   107  
   108  	valid = Validate("000000", 11, secSha1)
   109  	require.Equal(t, false, valid, "Valid should be false.")
   110  }
   111  
   112  // This tests for issue #10 - secrets without padding
   113  func TestValidatePadding(t *testing.T) {
   114  	valid, err := ValidateCustom("831097", 0, "JBSWY3DPEHPK3PX",
   115  		ValidateOpts{
   116  			Digits:    otp.DigitsSix,
   117  			Algorithm: otp.AlgorithmSHA1,
   118  		})
   119  	require.NoError(t, err, "Expected no error.")
   120  	require.Equal(t, true, valid, "Valid should be true.")
   121  }
   122  
   123  func TestValidateLowerCaseSecret(t *testing.T) {
   124  	valid, err := ValidateCustom("831097", 0, "jbswy3dpehpk3px",
   125  		ValidateOpts{
   126  			Digits:    otp.DigitsSix,
   127  			Algorithm: otp.AlgorithmSHA1,
   128  		})
   129  	require.NoError(t, err, "Expected no error.")
   130  	require.Equal(t, true, valid, "Valid should be true.")
   131  }
   132  
   133  func TestGenerate(t *testing.T) {
   134  	k, err := Generate(GenerateOpts{
   135  		Issuer:      "SnakeOil",
   136  		AccountName: "alice@example.com",
   137  	})
   138  	require.NoError(t, err, "generate basic TOTP")
   139  	require.Equal(t, "SnakeOil", k.Issuer(), "Extracting Issuer")
   140  	require.Equal(t, "alice@example.com", k.AccountName(), "Extracting Account Name")
   141  	require.Equal(t, 16, len(k.Secret()), "Secret is 16 bytes long as base32.")
   142  
   143  	k, err = Generate(GenerateOpts{
   144  		Issuer:      "SnakeOil",
   145  		AccountName: "alice@example.com",
   146  		SecretSize:  20,
   147  	})
   148  	require.NoError(t, err, "generate larger TOTP")
   149  	require.Equal(t, 32, len(k.Secret()), "Secret is 32 bytes long as base32.")
   150  
   151  	k, err = Generate(GenerateOpts{
   152  		Issuer:      "",
   153  		AccountName: "alice@example.com",
   154  	})
   155  	require.Equal(t, otp.ErrGenerateMissingIssuer, err, "generate missing issuer")
   156  	require.Nil(t, k, "key should be nil on error.")
   157  
   158  	k, err = Generate(GenerateOpts{
   159  		Issuer:      "Foobar, Inc",
   160  		AccountName: "",
   161  	})
   162  	require.Equal(t, otp.ErrGenerateMissingAccountName, err, "generate missing account name.")
   163  	require.Nil(t, k, "key should be nil on error.")
   164  
   165  	k, err = Generate(GenerateOpts{
   166  		Issuer:      "SnakeOil",
   167  		AccountName: "alice@example.com",
   168  		SecretSize:  17, // anything that is not divisable by 5, really
   169  	})
   170  	require.NoError(t, err, "Secret size is valid when length not divisable by 5.")
   171  	require.NotContains(t, k.Secret(), "=", "Secret has no escaped characters.")
   172  
   173  	k, err = Generate(GenerateOpts{
   174  		Issuer:      "SnakeOil",
   175  		AccountName: "alice@example.com",
   176  		Secret:      []byte("helloworld"),
   177  	})
   178  	require.NoError(t, err, "Secret generation failed")
   179  	sec, err := b32NoPadding.DecodeString(k.Secret())
   180  	require.NoError(t, err, "Secret wa not valid base32")
   181  	require.Equal(t, sec, []byte("helloworld"), "Specified Secret was not kept")
   182  }