diff --git a/.travis.yml b/.travis.yml index 016c02c..58cba6a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,4 @@ env: - GO111MODULE=on go: - - "1.15" + - "1.18" diff --git a/example/main.go b/example/main.go index 81e3221..69e638a 100644 --- a/example/main.go +++ b/example/main.go @@ -33,23 +33,23 @@ func promptForPasscode() string { // Demo function, not used in main // Generates Passcode using a UTF-8 (not base32) secret and custom parameters -func GeneratePassCode(utf8string string) string{ - secret := base32.StdEncoding.EncodeToString([]byte(utf8string)) - passcode, err := totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{ - Period: 30, - Skew: 1, - Digits: otp.DigitsSix, - Algorithm: otp.AlgorithmSHA512, - }) - if err != nil { - panic(err) - } - return passcode +func GeneratePassCode(utf8string string) string { + secret := base32.StdEncoding.EncodeToString([]byte(utf8string)) + passcode, err := totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{ + Period: 30, + Skew: 1, + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA512, + }) + if err != nil { + panic(err) + } + return passcode } func main() { key, err := totp.Generate(totp.GenerateOpts{ - Issuer: "Example.com", + Issuer: ptr("Example.com"), AccountName: "alice@example.com", }) if err != nil { @@ -78,3 +78,7 @@ func main() { os.Exit(1) } } + +func ptr[T any](x T) *T { + return &x +} diff --git a/go.mod b/go.mod index 77f2d6c..54f2a5c 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,13 @@ module github.com/pquerna/otp -go 1.12 +go 1.18 require ( github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc github.com/stretchr/testify v1.3.0 ) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect +) diff --git a/hotp/hotp.go b/hotp/hotp.go index 13a193e..930439f 100644 --- a/hotp/hotp.go +++ b/hotp/hotp.go @@ -168,7 +168,7 @@ var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding) func Generate(opts GenerateOpts) (*otp.Key, error) { // url encode the Issuer/AccountName if opts.Issuer == "" { - return nil, otp.ErrGenerateMissingIssuer + return nil, otp.ErrEmptyIssuer } if opts.AccountName == "" { diff --git a/hotp/hotp_test.go b/hotp/hotp_test.go index ddb760a..d9d2be0 100644 --- a/hotp/hotp_test.go +++ b/hotp/hotp_test.go @@ -78,15 +78,15 @@ func TestGenerateRFCMatrix(t *testing.T) { } } -func TestGenerateCodeCustom(t *testing.T){ +func TestGenerateCodeCustom(t *testing.T) { secSha1 := base32.StdEncoding.EncodeToString([]byte("12345678901234567890")) - code, err := GenerateCodeCustom("foo",1,ValidateOpts{}) + code, err := GenerateCodeCustom("foo", 1, ValidateOpts{}) print(code) require.Equal(t, otp.ErrValidateSecretInvalidBase32, err, "Decoding of secret as base32 failed.") require.Equal(t, "", code, "Code should be empty string when we have an error.") - code, err = GenerateCodeCustom(secSha1,1,ValidateOpts{}) + code, err = GenerateCodeCustom(secSha1, 1, ValidateOpts{}) require.Equal(t, 6, len(code), "Code should be 6 digits when we have not an error.") require.NoError(t, err, "Expected no error.") } @@ -98,7 +98,7 @@ func TestValidateInvalid(t *testing.T) { ValidateOpts{ Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, - }) + }) require.Equal(t, otp.ErrValidateInputInvalidLength, err, "Expected Invalid length error.") require.Equal(t, false, valid, "Valid should be false when we have an error.") @@ -172,7 +172,7 @@ func TestGenerate(t *testing.T) { Issuer: "", AccountName: "alice@example.com", }) - require.Equal(t, otp.ErrGenerateMissingIssuer, err, "generate missing issuer") + require.Equal(t, otp.ErrEmptyIssuer, err, "generate missing issuer") require.Nil(t, k, "key should be nil on error.") k, err = Generate(GenerateOpts{ diff --git a/internal/ptr.go b/internal/ptr.go new file mode 100644 index 0000000..759bc68 --- /dev/null +++ b/internal/ptr.go @@ -0,0 +1,5 @@ +package internal + +func Ptr[T any](x T) *T { + return &x +} diff --git a/otp.go b/otp.go index 02b08f3..5d70afe 100644 --- a/otp.go +++ b/otp.go @@ -40,8 +40,8 @@ var ErrValidateSecretInvalidBase32 = errors.New("Decoding of secret as base32 fa // The user provided passcode length was not expected. var ErrValidateInputInvalidLength = errors.New("Input length unexpected") -// When generating a Key, the Issuer must be set. -var ErrGenerateMissingIssuer = errors.New("Issuer must be set") +// When generating a Key, the Issuer cannot be empty. +var ErrEmptyIssuer = errors.New("Issuer cannot be empty") // When generating a Key, the Account Name must be set. var ErrGenerateMissingAccountName = errors.New("AccountName must be set") @@ -55,8 +55,8 @@ type Key struct { // NewKeyFromURL creates a new Key from an TOTP or HOTP url. // // The URL format is documented here: -// https://github.com/google/google-authenticator/wiki/Key-Uri-Format // +// https://github.com/google/google-authenticator/wiki/Key-Uri-Format func NewKeyFromURL(orig string) (*Key, error) { s := strings.TrimSpace(orig) diff --git a/totp/totp.go b/totp/totp.go index a2fb7d5..becca90 100644 --- a/totp/totp.go +++ b/totp/totp.go @@ -131,7 +131,7 @@ func ValidateCustom(passcode string, secret string, t time.Time, opts ValidateOp // are compatible with Google-Authenticator. type GenerateOpts struct { // Name of the issuing Organization/Company. - Issuer string + Issuer *string // Name of the User's Account (eg, email address) AccountName string // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds. @@ -153,8 +153,8 @@ var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding) // Generate a new TOTP Key. func Generate(opts GenerateOpts) (*otp.Key, error) { // url encode the Issuer/AccountName - if opts.Issuer == "" { - return nil, otp.ErrGenerateMissingIssuer + if opts.Issuer != nil && *opts.Issuer == "" { + return nil, otp.ErrEmptyIssuer } if opts.AccountName == "" { @@ -191,7 +191,10 @@ func Generate(opts GenerateOpts) (*otp.Key, error) { v.Set("secret", b32NoPadding.EncodeToString(secret)) } - v.Set("issuer", opts.Issuer) + if opts.Issuer != nil { + v.Set("issuer", *opts.Issuer) + } + v.Set("period", strconv.FormatUint(uint64(opts.Period), 10)) v.Set("algorithm", opts.Algorithm.String()) v.Set("digits", opts.Digits.String()) @@ -199,9 +202,14 @@ func Generate(opts GenerateOpts) (*otp.Key, error) { u := url.URL{ Scheme: "otpauth", Host: "totp", - Path: "/" + opts.Issuer + ":" + opts.AccountName, RawQuery: internal.EncodeQuery(v), } + if opts.Issuer != nil { + u.Path = "/" + *opts.Issuer + ":" + opts.AccountName + } else { + u.Path = "/" + opts.AccountName + } + return otp.NewKeyFromURL(u.String()) } diff --git a/totp/totp_test.go b/totp/totp_test.go index 1f854b7..ccebbde 100644 --- a/totp/totp_test.go +++ b/totp/totp_test.go @@ -19,6 +19,7 @@ package totp import ( "github.com/pquerna/otp" + "github.com/pquerna/otp/internal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -61,14 +62,14 @@ var ( } ) -// // Test vectors from http://tools.ietf.org/html/rfc6238#appendix-B // NOTE -- the test vectors are documented as having the SAME // secret -- this is WRONG -- they have a variable secret // depending upon the hmac algorithm: -// http://www.rfc-editor.org/errata_search.php?rfc=6238 -// this only took a few hours of head/desk interaction to figure out. // +// http://www.rfc-editor.org/errata_search.php?rfc=6238 +// +// this only took a few hours of head/desk interaction to figure out. func TestValidateRFCMatrix(t *testing.T) { for _, tx := range rfcMatrixTCs { valid, err := ValidateCustom(tx.TOTP, tx.Secret, time.Unix(tx.TS, 0).UTC(), @@ -120,7 +121,7 @@ func TestValidateSkew(t *testing.T) { func TestGenerate(t *testing.T) { k, err := Generate(GenerateOpts{ - Issuer: "SnakeOil", + Issuer: internal.Ptr("SnakeOil"), AccountName: "alice@example.com", }) require.NoError(t, err, "generate basic TOTP") @@ -129,14 +130,14 @@ func TestGenerate(t *testing.T) { require.Equal(t, 32, len(k.Secret()), "Secret is 32 bytes long as base32.") k, err = Generate(GenerateOpts{ - Issuer: "Snake Oil", + Issuer: internal.Ptr("Snake Oil"), AccountName: "alice@example.com", }) require.NoError(t, err, "issuer with a space in the name") require.Contains(t, k.String(), "issuer=Snake%20Oil") k, err = Generate(GenerateOpts{ - Issuer: "SnakeOil", + Issuer: internal.Ptr("SnakeOil"), AccountName: "alice@example.com", SecretSize: 20, }) @@ -144,7 +145,7 @@ func TestGenerate(t *testing.T) { require.Equal(t, 32, len(k.Secret()), "Secret is 32 bytes long as base32.") k, err = Generate(GenerateOpts{ - Issuer: "SnakeOil", + Issuer: internal.Ptr("SnakeOil"), AccountName: "alice@example.com", SecretSize: 13, // anything that is not divisible by 5, really }) @@ -152,7 +153,7 @@ func TestGenerate(t *testing.T) { require.NotContains(t, k.Secret(), "=", "Secret has no escaped characters.") k, err = Generate(GenerateOpts{ - Issuer: "SnakeOil", + Issuer: internal.Ptr("SnakeOil"), AccountName: "alice@example.com", Secret: []byte("helloworld"), })