Skip to content

Commit

Permalink
Use github.com/pquerna/otp to allow using the key period
Browse files Browse the repository at this point in the history
RELEASE_NOTES=[BUGFIX] Use OTP key period

Fixes #2276

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
  • Loading branch information
dominikschulz committed Jul 20, 2022
1 parent 832d2cd commit 7bfd575
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 48 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
github.com/mitchellh/go-ps v1.0.0
github.com/muesli/crunchy v0.4.0
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/pquerna/otp v1.3.0
github.com/schollz/closestmatch v0.0.0-20190308193919-1fbe626be92e
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.8.0
Expand All @@ -46,6 +47,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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
29 changes: 19 additions & 10 deletions internal/action/otp.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,15 @@ import (
"github.com/gopasspw/gopass/internal/store"
"github.com/gopasspw/gopass/pkg/clipboard"
"github.com/gopasspw/gopass/pkg/ctxutil"
"github.com/gopasspw/gopass/pkg/debug"
"github.com/gopasspw/gopass/pkg/otp"
"github.com/gopasspw/gopass/pkg/termio"
"github.com/mattn/go-tty"
potp "github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/urfave/cli/v2"
)

const (
// we might want to replace this with the currently un-exported step value
// from twofactor.FromURL if it gets ever exported.
otpPeriod = 30
)

// OTP implements OTP token handling for TOTP and HOTP.
func (s *Action) OTP(c *cli.Context) error {
ctx := ctxutil.WithGlobalFlags(c)
Expand Down Expand Up @@ -108,18 +105,30 @@ 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.GenerateCodeCustom(two.Secret(), time.Now(), totp.ValidateOpts{
Period: uint(two.Period()),
Skew: 1,
Digits: potp.DigitsSix,
Algorithm: potp.AlgorithmSHA1,
})
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)
expiresAt := now.Add(time.Duration(two.Period()) * time.Second).Truncate(time.Duration(two.Period()) * time.Second)
secondsLeft := int(time.Until(expiresAt).Seconds())
bar := termio.NewProgressBar(int64(secondsLeft))
bar.Hidden = skip

debug.Log("OTP period: %ds", two.Period())

if clip {
if err := clipboard.CopyTo(ctx, fmt.Sprintf("token for %s", name), []byte(token), s.cfg.ClipTimeout); err != nil {
return exit.Error(exit.IO, err, "failed to copy to clipboard: %s", err)
Expand All @@ -145,7 +154,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:.
Expand Down
64 changes: 30 additions & 34 deletions pkg/otp/otp.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Expand All @@ -45,47 +45,43 @@ func Calculate(name string, sec gopass.Secret) (twofactor.OTP, string, error) {
}

if strings.HasPrefix(secKey, "otpauth://") {
return twofactor.FromURL(secKey) //nolint:wrapcheck
debug.Log("parsing otpauth:// URL %q", out.Secret(secKey))

k, err := otp.NewKeyFromURL(secKey)
if err != nil {
return nil, fmt.Errorf("failed to parse otpauth URL: %w", err)
}

return k, nil
}

otp, err := twofactor.NewGoogleTOTP(twofactor.Pad(secKey))
debug.Log("assembling otpauth URL from secret only (%q), using defaults", out.Secret(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)
debug.Log("failed to parse OTP: %s", out.Secret(secKey))

return nil, fmt.Errorf("invalid OTP secret: %w", 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)
}

Expand Down
8 changes: 4 additions & 4 deletions pkg/otp/otp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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))
})
Expand All @@ -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))
}

0 comments on commit 7bfd575

Please sign in to comment.