diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01f93a0205..a7e92acb83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/grype.yml b/.github/workflows/grype.yml index 67b2560958..04759ebbe4 100644 --- a/.github/workflows/grype.yml +++ b/.github/workflows/grype.yml @@ -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 diff --git a/go.mod b/go.mod index cc40739473..df7475b8b3 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 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..e1c5524671 100644 --- a/internal/action/otp.go +++ b/internal/action/otp.go @@ -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) @@ -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) @@ -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:. diff --git a/pkg/otp/otp.go b/pkg/otp/otp.go index e3f8f525f6..f3a41dd2b4 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,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) } 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)) }