Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

totp issuer can be optional #93

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ env:
- GO111MODULE=on

go:
- "1.15"
- "1.18"
30 changes: 17 additions & 13 deletions example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: "[email protected]",
})
if err != nil {
Expand Down Expand Up @@ -78,3 +78,7 @@ func main() {
os.Exit(1)
}
}

func ptr[T any](x T) *T {
return &x
}
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
)
2 changes: 1 addition & 1 deletion hotp/hotp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand Down
10 changes: 5 additions & 5 deletions hotp/hotp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
}
Expand All @@ -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.")

Expand Down Expand Up @@ -172,7 +172,7 @@ func TestGenerate(t *testing.T) {
Issuer: "",
AccountName: "[email protected]",
})
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{
Expand Down
5 changes: 5 additions & 0 deletions internal/ptr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package internal

func Ptr[T any](x T) *T {
return &x
}
6 changes: 3 additions & 3 deletions otp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)

Expand Down
18 changes: 13 additions & 5 deletions totp/totp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 == "" {
Expand Down Expand Up @@ -191,17 +191,25 @@ 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())

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())
}
17 changes: 9 additions & 8 deletions totp/totp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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: "[email protected]",
})
require.NoError(t, err, "generate basic TOTP")
Expand All @@ -129,30 +130,30 @@ 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: "[email protected]",
})
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: "[email protected]",
SecretSize: 20,
})
require.NoError(t, err, "generate larger TOTP")
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: "[email protected]",
SecretSize: 13, // anything that is not divisible by 5, really
})
require.NoError(t, err, "Secret size is valid when length not divisible by 5.")
require.NotContains(t, k.Secret(), "=", "Secret has no escaped characters.")

k, err = Generate(GenerateOpts{
Issuer: "SnakeOil",
Issuer: internal.Ptr("SnakeOil"),
AccountName: "[email protected]",
Secret: []byte("helloworld"),
})
Expand Down