From b7fce5c1a85776b49a4310daa145811675360819 Mon Sep 17 00:00:00 2001 From: Dominik Schulz Date: Mon, 18 Jul 2022 22:47:23 +0200 Subject: [PATCH] Use github.com/pquerna/otp to allow using the key period RELEASE_NOTES=[BUGFIX] Use OTP key period Fixes #2276 Signed-off-by: Dominik Schulz --- go.mod | 2 ++ go.sum | 4 ++++ internal/action/otp.go | 12 +++++++--- pkg/otp/otp.go | 53 +++++++++++++++--------------------------- pkg/otp/otp_test.go | 8 +++---- 5 files changed, 38 insertions(+), 41 deletions(-) diff --git a/go.mod b/go.mod index cc40739473..2ecabf6964 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( require ( filippo.io/edwards25519 v1.0.0 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -53,6 +54,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/kr/pretty v0.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pquerna/otp v1.3.0 // indirect github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 // indirect github.com/rs/zerolog v1.27.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index 3e164e92ea..32d2cbd097 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/caspr-io/yamlpath v0.0.0-20200722075116-502e8d113a9b h1:2K3B6Xm7/lnhOugeGB3nIk50bZ9zhuJvXCEfUuL68ik= github.com/caspr-io/yamlpath v0.0.0-20200722075116-502e8d113a9b/go.mod h1:4rP9T6iHCuPAIDKdNaZfTuuqSIoQQvFctNWIAUI1rlg= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= @@ -102,6 +104,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs= +github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= diff --git a/internal/action/otp.go b/internal/action/otp.go index 4c5bf6246a..6322fcd628 100644 --- a/internal/action/otp.go +++ b/internal/action/otp.go @@ -14,6 +14,7 @@ import ( "github.com/gopasspw/gopass/pkg/otp" "github.com/gopasspw/gopass/pkg/termio" "github.com/mattn/go-tty" + "github.com/pquerna/otp/totp" "github.com/urfave/cli/v2" ) @@ -108,11 +109,16 @@ func (s *Action) otp(ctx context.Context, name, qrf string, clip, pw, recurse bo return nil default: } - two, label, err := otp.Calculate(name, sec) + + two, err := otp.Calculate(name, sec) if err != nil { return exit.Error(exit.Unknown, err, "No OTP entry found for %s: %s", name, err) } - token := two.OTP() + + token, err := totp.GenerateCode(two.Secret(), time.Now()) + if err != nil { + return exit.Error(exit.Unknown, err, "Failed to compute OTP token for %s: %s", name, err) + } now := time.Now() expiresAt := now.Add(otpPeriod * time.Second).Truncate(otpPeriod * time.Second) @@ -145,7 +151,7 @@ func (s *Action) otp(ctx context.Context, name, qrf string, clip, pw, recurse bo } if qrf != "" { - return otp.WriteQRFile(two, label, qrf) + return otp.WriteQRFile(two, qrf) } // let us wait until next OTP code:. diff --git a/pkg/otp/otp.go b/pkg/otp/otp.go index e3f8f525f6..59ddbdcce0 100644 --- a/pkg/otp/otp.go +++ b/pkg/otp/otp.go @@ -1,19 +1,21 @@ package otp import ( + "bytes" "fmt" + "image/png" "os" "strings" - "github.com/gokyle/twofactor" "github.com/gopasspw/gopass/internal/out" "github.com/gopasspw/gopass/pkg/debug" "github.com/gopasspw/gopass/pkg/gopass" + "github.com/pquerna/otp" ) // Calculate will compute a OTP code from a given secret. //nolint:ireturn -func Calculate(name string, sec gopass.Secret) (twofactor.OTP, string, error) { +func Calculate(name string, sec gopass.Secret) (*otp.Key, error) { otpURL, found := sec.Get("otpauth") if found && strings.HasPrefix(otpURL, "//") { otpURL = "otpauth:" + otpURL @@ -31,12 +33,10 @@ func Calculate(name string, sec gopass.Secret) (twofactor.OTP, string, error) { if otpURL != "" { debug.Log("found otpauth url: %s", out.Secret(otpURL)) - return twofactor.FromURL(otpURL) //nolint:wrapcheck + return otp.NewKeyFromURL(otpURL) //nolint:wrapcheck } // check yaml entry and fall back to password if we don't have one - label := name - secKey, found := sec.Get("totp") if !found { debug.Log("no totp secret found, falling back to password") @@ -45,47 +45,32 @@ func Calculate(name string, sec gopass.Secret) (twofactor.OTP, string, error) { } if strings.HasPrefix(secKey, "otpauth://") { - return twofactor.FromURL(secKey) //nolint:wrapcheck + return otp.NewKeyFromURL(otpURL) //nolint:wrapcheck } - otp, err := twofactor.NewGoogleTOTP(twofactor.Pad(secKey)) + // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example + key, err := otp.NewKeyFromURL(fmt.Sprintf("otpauth://totp/new?secret=%s&issuer=gopass", secKey)) if err != nil { - return otp, label, fmt.Errorf("invalid OTP secret %q: %w", secKey, err) + return nil, fmt.Errorf("invalid OTP secret %q: %w", secKey, err) } - return otp, label, nil + return key, nil } // WriteQRFile writes the given OTP code as a QR image to disk. -func WriteQRFile(otp twofactor.OTP, label, file string) error { - var qr []byte - - var err error - - switch otp.Type() { - case twofactor.OATH_HOTP: - hotp, ok := otp.(*twofactor.HOTP) - if !ok { - return fmt.Errorf("Type assertion failed on twofactor.HOTP: %w", ErrType) - } - - qr, err = hotp.QR(label) - case twofactor.OATH_TOTP: - totp, ok := otp.(*twofactor.TOTP) - if !ok { - return fmt.Errorf("Type assertion failed on twofactor.TOTP: %w", ErrType) - } - - qr, err = totp.QR(label) - default: - err = ErrOathOTP +func WriteQRFile(key *otp.Key, file string) error { + // Convert TOTP key into a QR code encoded as a PNG image. + var buf bytes.Buffer + img, err := key.Image(200, 200) + if err != nil { + return fmt.Errorf("failed to encode qr code: %w", err) } - if err != nil { - return fmt.Errorf("failed to write qr file: %w", err) + if err := png.Encode(&buf, img); err != nil { + return fmt.Errorf("failed to encode as png: %w", err) } - if err := os.WriteFile(file, qr, 0o600); err != nil { + if err := os.WriteFile(file, buf.Bytes(), 0o600); err != nil { return fmt.Errorf("failed to write QR code: %w", err) } diff --git a/pkg/otp/otp_test.go b/pkg/otp/otp_test.go index e5ec8b232a..3b088522ca 100644 --- a/pkg/otp/otp_test.go +++ b/pkg/otp/otp_test.go @@ -6,8 +6,8 @@ import ( "path/filepath" "testing" - "github.com/gokyle/twofactor" "github.com/gopasspw/gopass/pkg/gopass/secrets/secparse" + "github.com/pquerna/otp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,7 +37,7 @@ func TestCalculate(t *testing.T) { s, err := secparse.Parse(tc) require.NoError(t, err) - otp, _, err := Calculate("test", s) + otp, err := Calculate("test", s) assert.NoError(t, err, string(tc)) assert.NotNil(t, otp, string(tc)) }) @@ -56,7 +56,7 @@ func TestWrite(t *testing.T) { tf := filepath.Join(td, "qr.png") - otp, label, err := twofactor.FromURL(totpURL) + key, err := otp.NewKeyFromURL(totpURL) assert.NoError(t, err) - assert.NoError(t, WriteQRFile(otp, label, tf)) + assert.NoError(t, WriteQRFile(key, tf)) }