mirror of
https://github.com/gopasspw/gopass.git
synced 2025-12-08 19:24:54 +00:00
This change adds GoDoc comments to many of the public symbols in the `pkg/` directory. It also includes various improvements to the documentation in `README.md` and other markdown files in the `docs/` directory. This is a partial documentation effort, as requested by the user, to get a pull request submitted quickly. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
120 lines
3.0 KiB
Go
120 lines
3.0 KiB
Go
// Package otp provides functions to handle OTP secrets.
|
|
// It can parse OTP secrets from various formats and generate QR codes for them.
|
|
package otp
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image/png"
|
|
"os"
|
|
"strings"
|
|
|
|
"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 an OTP code from a given secret.
|
|
// It will look for a field named "otpauth" or "totp" or "hotp".
|
|
// If none is found it will fall back to the password.
|
|
//
|
|
//nolint:ireturn
|
|
func Calculate(name string, sec gopass.Secret) (*otp.Key, error) {
|
|
otpURL := getOTPURL(sec)
|
|
|
|
if otpURL != "" {
|
|
debug.Log("found otpauth url: %s", out.Secret(otpURL))
|
|
|
|
return otp.NewKeyFromURL(otpURL) //nolint:wrapcheck
|
|
}
|
|
|
|
// check KV entry and fall back to password if we don't have one
|
|
|
|
// TOTP
|
|
if secKey, found := sec.Get("totp"); found {
|
|
return parseOTP("totp", secKey)
|
|
}
|
|
|
|
// HOTP
|
|
if secKey, found := sec.Get("hotp"); found {
|
|
return parseOTP("hotp", secKey)
|
|
}
|
|
|
|
debug.Log("no totp secret found, falling back to password")
|
|
|
|
return parseOTP("totp", sec.Password())
|
|
}
|
|
|
|
func getOTPURL(sec gopass.Secret) string {
|
|
// check if we have a key-value entry
|
|
if url, found := sec.Get("otpauth"); found {
|
|
if strings.HasPrefix(url, "//") {
|
|
url = "otpauth:" + url
|
|
}
|
|
|
|
return url
|
|
}
|
|
|
|
// if there is no KV entry check the body
|
|
for _, line := range strings.Split(sec.Body(), "\n") {
|
|
if strings.HasPrefix(line, "otpauth://") {
|
|
return line
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func parseOTP(typ string, secKey string) (*otp.Key, error) {
|
|
if strings.HasPrefix(secKey, "otpauth://") {
|
|
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
|
|
}
|
|
|
|
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://%s/new?secret=%s&issuer=gopass", typ, secKey))
|
|
if err != nil {
|
|
debug.Log("failed to parse OTP: %s", out.Secret(secKey))
|
|
|
|
return nil, fmt.Errorf("invalid OTP secret: %w", err)
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// WriteQRFile writes the given OTP key as a QR image to disk.
|
|
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 := png.Encode(&buf, img); err != nil {
|
|
return fmt.Errorf("failed to encode as png: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(file, buf.Bytes(), 0o600); err != nil {
|
|
return fmt.Errorf("failed to write QR code: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
// ErrOathOTP is returned when the secret is not a valid OATH secret.
|
|
ErrOathOTP = fmt.Errorf("QR codes can only be generated for OATH OTPs")
|
|
// ErrType is returned when the secret is not a valid OTP type.
|
|
ErrType = fmt.Errorf("type assertion failed")
|
|
)
|