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

Use github.com/pquerna/otp to allow using the key period #2278

Merged
merged 5 commits into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.18.3
- uses: actions/cache@v3
with:
path: ~/go/pkg/mod
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/grype.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.18.3
- uses: actions/cache@v3
with:
path: ~/go/pkg/mod
Expand Down
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ linters:
- maligned
- nilerr
- noctx
- nosnakecase
- revive
- rowserrcheck
- scopelint
Expand Down
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)
dominikschulz marked this conversation as resolved.
Show resolved Hide resolved
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{
dominikschulz marked this conversation as resolved.
Show resolved Hide resolved
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
dominikschulz marked this conversation as resolved.
Show resolved Hide resolved
}

// 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))
dominikschulz marked this conversation as resolved.
Show resolved Hide resolved

// otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
key, err := otp.NewKeyFromURL(fmt.Sprintf("otpauth://totp/new?secret=%s&issuer=gopass", secKey))
dominikschulz marked this conversation as resolved.
Show resolved Hide resolved
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))
}