checking in
This commit is contained in:
5
TODO
Normal file
5
TODO
Normal file
@@ -0,0 +1,5 @@
|
||||
- https://github.com/hashicorp/vault/issues/3043
|
||||
- https://github.com/hashicorp/vault/issues/31684
|
||||
https://github.com/openbao/openbao/issues/2233
|
||||
- https://github.com/hashicorp/vault/issues/31685
|
||||
https://github.com/openbao/openbao/issues/2234
|
||||
39
build.sh
39
build.sh
@@ -10,6 +10,10 @@ declare -A os_sfx=(
|
||||
["windows"]='exe'
|
||||
["darwin"]='app'
|
||||
)
|
||||
declare -A tgts=(
|
||||
["amd64"]='x86_64'
|
||||
["arm64"]='arm64'
|
||||
)
|
||||
|
||||
BUILD_TIME="$(date '+%s')"
|
||||
BUILD_USER="$(whoami)"
|
||||
@@ -77,38 +81,39 @@ set -u
|
||||
|
||||
# And finally build.
|
||||
mkdir -p ./bin/
|
||||
go mod tidy
|
||||
go get -u ./...
|
||||
go mod tidy
|
||||
export CGO_ENABLED=0
|
||||
_origdir="$(pwd)"
|
||||
_pfx=''
|
||||
#for cmd in 'discord' 'irc' 'msteams' 'slack' 'xmpp'; do
|
||||
_pfx='vault_totp_'
|
||||
for cmd_dir in cmd/*; do
|
||||
cmd_dir="${_origdir}/${cmd_dir}"
|
||||
cmd="$(basename "${cmd_dir}")"
|
||||
echo "${cmd}..."
|
||||
_bin="${_pfx}${cmd}"
|
||||
#echo "${cmd_dir} => ${_bin}..."
|
||||
echo "${_bin}..."
|
||||
if [ ! -f "${cmd_dir}/main.go" ]; then
|
||||
continue
|
||||
fi
|
||||
cd "${cmd_dir}"
|
||||
_bin="${_pfx}${cmd}"
|
||||
for ga in 'amd64' 'arm64'; do
|
||||
as="${ga}"
|
||||
if [[ "${ga}" == 'amd64' ]]; then
|
||||
as='x86_64'
|
||||
fi
|
||||
echo -e "\t${as}..."
|
||||
for osnm in "${!os_sfx[@]}"; do
|
||||
echo -e "\t\t${osnm}: "
|
||||
for ga in "${!tgts[@]}"; do
|
||||
echo -e "\t${ga}..."
|
||||
for osnm in "${!os_sfx[@]}"; do
|
||||
echo -e "\t\t${osnm}: "
|
||||
_sfx="${os_sfx[$osnm]}"
|
||||
bin="${_origdir}/bin/${_bin}-${as}-${CURRENT_VER:-dev}.${_sfx}"
|
||||
_a="${tgts[$ga]}"
|
||||
bin="${_origdir}/bin/${_bin}-${_a}-${CURRENT_VER:-dev}.${_sfx}"
|
||||
export GOOS="${osnm}"
|
||||
export GOARCH="${ga}"
|
||||
echo -e -n "\t\tBuilding '${bin}'..."
|
||||
echo -e "\t\t\tBuilding '${bin}'..."
|
||||
go build \
|
||||
-o "${bin}" \
|
||||
-ldflags \
|
||||
"${LDFLAGS_STR}"
|
||||
# "${LDFLAGS_STR}" \
|
||||
"${LDFLAGS_STR}"
|
||||
# "${LDFLAGS_STR}" \
|
||||
# *.go
|
||||
echo " Done."
|
||||
echo -e "\t\t\tDone."
|
||||
done
|
||||
echo -e "\tDone."
|
||||
done
|
||||
|
||||
34
cmd/add/args.go
Normal file
34
cmd/add/args.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`time`
|
||||
|
||||
"r00t2.io/vault_totp/internal"
|
||||
)
|
||||
|
||||
type (
|
||||
Args struct {
|
||||
internal.CommonArgs
|
||||
AddArgs
|
||||
ExplicitOtp `group:"Explicit OTP Options" env-namespace:"VTOTP"`
|
||||
}
|
||||
AddArgs struct {
|
||||
VaultTotpMnt string `env:"VTOTP_MNT" short:"m" long:"mount" default:"totp" description:"The Vault TOTP generator mount (a 'TOTP secrets' mount) to add to."`
|
||||
VaultKV2MntPath *string `env:"KV2_MNTP" short:"M" long:"store" description:"The spec to store the QR code and key via otpauth URI (Vault TOTP mounts can't return secret keys). '<account>/<issuer>' will be appended to the path.\nMust be provided in the form of '[<mount>:]<path>'; e.g. 'foo:bar/baz' or 'bar/baz'.\nIf this argument is provided but the mount is not provided, 'secrets' will be used.\nIf this argument isn't provided at all, no QR code/URI storage will be performed."`
|
||||
QrImgPath []string `env:"VTOTP_QR" short:"q" long:"qr-img" description:"Path to QR image to extract OTPAuth URLs from. Either -q/--qr-img, -f/--otp-file, -u/--otp-url, -e/--secret-service, and/or -x/--explicit-otp must be specified." validate:"required_without=OtpFile OtpUrl OtpExplicit,filepath"`
|
||||
OtpFile []string `env:"VTOTP_FILE" short:"f" long:"otp-file" description:"Path to file containing OTPAuth URLs in plaintext, one per line. Either -q/--qr-img, -f/--otp-file, -u/--otp-url, -e/--secret-service, and/or -x/--explicit-otp must be specified." validate:"required_without=QrImgPath OtpUrl OtpExplicit,filepath"`
|
||||
OtpUrl []string `env:"VTOTP_URL" short:"u" long:"otp-url" description:"Explicit OTPAuth URL. Either -q/--qr-img, -f/--otp-file, -u/--otp-url, -e/--secret-service, and/or -x/--explicit-otp must be specified." validate:"required_without=QrImgPath OtpFile OtpExplicit,url"`
|
||||
SecretService bool `env:"VTOTP_SSVC" short:"e" long:"secret-service" description:"Export from the https://extensions.gnome.org/extension/6793/totp/ GNOME extension. Either -q/--qr-img, -f/--otp-file, -u/--otp-url, -e/--secret-service, and/or -x/--explicit-otp must be specified."`
|
||||
OtpExplicit bool `short:"x" long:"explicit-otp" description:"If specified, use the explicit OTP specification under the 'Explicit OTP Options' group. Either -q/--qr-img, -f/--otp-file, -u/--otp-url, -e/--secret-service, and/or -x/--explicit-otp must be specified."`
|
||||
}
|
||||
ExplicitOtp struct {
|
||||
Type string `env:"XTYP" short:"y" long:"type" choice:"totp" choice:"hotp" default:"totp" hidden:"true" description:"The OTP type. Only used if -x/--explicit-otp is specified." validate:"required_with=OtpExplicit,oneof=totp hotp"`
|
||||
Counter uint64 `env:"XCTR" short:"c" long:"counter" hidden:"true" description:"The initial counter value (if -y/--type='hotp'). Only used if -x/--explicit-otp is specified." validate:"required_with=OtpExplicit,required_if=Type hotp"`
|
||||
Account string `env:"XACCT" short:"n" long:"name" description:"Name of the TOTP account (should be just the username). Only used if -x/--explicit-otp is specified." validate:"required_with=OtpExplicit"`
|
||||
Issuer string `env:"XISS" short:"i" long:"issuer" description:"Issuer of the TOTP (this is generally the service name you're authing to). Only used if -x/--explicit-otp is specified." validate:"required_with=OtpExplicit"`
|
||||
Secret string `env:"XSSKEY" short:"s" long:"shared-secret" description:"The shared secret key in Base32 string format (with no padding). Only used if -x/--explicit-otp is specified." validate:"required_with=OtpExplicit,base32"`
|
||||
Algorithm string `env:"XALGO" short:"g" long:"algo" choice:"md5" choice:"sha1" choice:"sha256" choice:"sha512" description:"The hashing/checksum algorithm. Only used if -x/--explicit-otp is specified." validate:"required_with=OtpExplicit,oneof=md5 sha1 sha256 sha512"`
|
||||
Digits int `env:"XDIG" short:"l" long:"digits" choice:"6" choice:"8" description:"Number of digits for the generated code. Only used if -x/--explicit-otp is specified." validate:"required_with=OtpExplicit,oneof=6 8"`
|
||||
Period time.Duration `env:"XTIME" short:"p" long:"period" default:"30s" description:"The period that a generated code is valid for. Only used if -x/--explicit-otp is specified." validate:"required_with=OtpExplicit,required_if=Type totp"`
|
||||
}
|
||||
)
|
||||
94
cmd/add/consts.go
Normal file
94
cmd/add/consts.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`sync`
|
||||
|
||||
`github.com/jessevdk/go-flags`
|
||||
`r00t2.io/goutils/logging`
|
||||
)
|
||||
|
||||
var (
|
||||
logger logging.Logger
|
||||
args *Args = new(Args)
|
||||
parser *flags.Parser = flags.NewParser(args, flags.Default)
|
||||
)
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
existingOtp map[string]struct{} = make(map[string]struct{})
|
||||
urlChan chan parsedUrl = make(chan parsedUrl)
|
||||
doneChan chan bool = make(chan bool, 1)
|
||||
vaultReady chan bool = make(chan bool, 1)
|
||||
)
|
||||
|
||||
const (
|
||||
// stdScheme is a public standard, documented at https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
stdScheme string = "otpauth://"
|
||||
/*
|
||||
googleScheme is a "wrapper" around that that has never been,
|
||||
to my knowledge, publicly formally documented because we can't have nice things.
|
||||
*/
|
||||
googleScheme string = "otpauth-migration://"
|
||||
|
||||
// vaultSep is used to join the issuer and account in a name.
|
||||
vaultSep string = "."
|
||||
// defStoreMnt is used if no mount was provided (but a path was) to -M/--store.
|
||||
defStoreMnt string = "secrets"
|
||||
|
||||
/*
|
||||
ssCollectionNm is the SecretService collection name used
|
||||
by https://extensions.gnome.org/extension/6793/totp/
|
||||
(https://github.com/dkosmari/gnome-shell-extension-totp)
|
||||
*/
|
||||
ssCollectionNm string = "OTP"
|
||||
|
||||
// TODO: Adding to SS
|
||||
/*
|
||||
Will look something like this:
|
||||
|
||||
if svc, err = gosecret.NewService(); err != nil {
|
||||
return
|
||||
}
|
||||
defer svc.Close()
|
||||
|
||||
if coll, err = svc.GetCollection(ssCollectionNm); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
secret = gosecret.NewSecret(
|
||||
svc.Session,
|
||||
[]byte{}, // nil?
|
||||
gosecret.SecretValue(sharedTotpKey),
|
||||
ssContentType,
|
||||
)
|
||||
|
||||
itemAttrs = map[string]string{
|
||||
"type": "TOTP",
|
||||
"algorithm": "(MD5|SHA-1|SHA-256|SHA-512)",
|
||||
"digits": "(6|8)",
|
||||
"issuer": "<Service Name>",
|
||||
"name": "<Username>",
|
||||
"period": "<seconds>"
|
||||
ssSchemaAttr: ssSchemaVal,
|
||||
}
|
||||
|
||||
if item, err = coll.CreateItem(
|
||||
"<LAST_ID+1>:<issuer>:<name>",
|
||||
itemAttrs,
|
||||
secret,
|
||||
true,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
*/
|
||||
// ssSchemaAttr is used when adding a secret to the SecretService.
|
||||
ssSchemaAttr string = "xdg:schema"
|
||||
/*
|
||||
ssSchemaVal is the value for the attribute specified by ssSchemaAttr.
|
||||
|
||||
It is also the value used as the [r00t2.io/gosecret.Item.SecretType] when adding.
|
||||
*/
|
||||
ssSchemaVal string = "org.gnome.shell.extensions.totp"
|
||||
// ssContentType is the value to use for [r00t2.io/gosecret.Secret.ContentType] when adding.
|
||||
ssContentType string = "text/plain"
|
||||
)
|
||||
4
cmd/add/doc.go
Normal file
4
cmd/add/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
vault_totp_add: Add existing TOTP configurations to Vault (Vault as a TOTP generator).
|
||||
*/
|
||||
package main
|
||||
645
cmd/add/funcs.go
Normal file
645
cmd/add/funcs.go
Normal file
@@ -0,0 +1,645 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`bufio`
|
||||
`bytes`
|
||||
`context`
|
||||
`encoding/base64`
|
||||
`fmt`
|
||||
`image`
|
||||
`image/color`
|
||||
_ `image/gif`
|
||||
_ `image/jpeg`
|
||||
`image/png`
|
||||
_ `image/png`
|
||||
`io`
|
||||
`os`
|
||||
`strconv`
|
||||
`strings`
|
||||
`sync`
|
||||
|
||||
`github.com/creachadair/otp/otpauth`
|
||||
`github.com/hashicorp/vault-client-go`
|
||||
`github.com/hashicorp/vault-client-go/schema`
|
||||
`github.com/makiuchi-d/gozxing`
|
||||
`github.com/makiuchi-d/gozxing/qrcode`
|
||||
`github.com/pquerna/otp`
|
||||
`r00t2.io/gosecret`
|
||||
`r00t2.io/sysutils/paths`
|
||||
`r00t2.io/vault_totp/common`
|
||||
`r00t2.io/vault_totp/internal`
|
||||
)
|
||||
|
||||
// convertToGray converts image.Image `img` to an 8-bit grayscale image for QR codes.
|
||||
func convertToGray(img image.Image) (g *image.Gray) {
|
||||
|
||||
var x int
|
||||
var y int
|
||||
var rgba color.Color
|
||||
var bounds image.Rectangle
|
||||
|
||||
bounds = img.Bounds()
|
||||
|
||||
g = image.NewGray(bounds)
|
||||
|
||||
for x = 0; x < bounds.Max.X; x++ {
|
||||
for y = 0; y < bounds.Max.Y; y++ {
|
||||
rgba = img.At(x, y)
|
||||
// g.Set(x, y, rgba)
|
||||
g.Set(x, y, color.GrayModel.Convert(rgba))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
convertToMono converts image.Gray `img` (see convertToGray) to a monochrome (2-bit) grayscale image for QR codes.
|
||||
|
||||
If `invert` is true, the bits will be flipped.
|
||||
(This works best if the original image is sent through convertToMono with invert == false *first*).
|
||||
*/
|
||||
func convertToMono(img image.Image, invert bool) (bw *image.Paletted) {
|
||||
|
||||
var x int
|
||||
var y int
|
||||
var g color.Gray
|
||||
var palette color.Palette
|
||||
var bounds image.Rectangle
|
||||
var hiBit color.Color = color.White
|
||||
var loBit color.Color = color.Black
|
||||
|
||||
if invert {
|
||||
hiBit = color.Black
|
||||
loBit = color.White
|
||||
}
|
||||
palette = color.Palette{
|
||||
loBit,
|
||||
hiBit,
|
||||
}
|
||||
|
||||
bounds = img.Bounds()
|
||||
// bw = image.NewGray(bounds)
|
||||
bw = image.NewPaletted(bounds, palette)
|
||||
|
||||
for x = 0; x < bounds.Max.X; x++ {
|
||||
for y = 0; y < bounds.Max.Y; y++ {
|
||||
g = img.At(x, y).(color.Gray)
|
||||
// between 0-255 (uint8)
|
||||
if g.Y > 128 {
|
||||
bw.Set(x, y, hiBit)
|
||||
} else {
|
||||
bw.Set(x, y, loBit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// decQR reduces some repetition in parseQrFile. err == nil indicates successfull decode.
|
||||
func decQR(img image.Image) (res *gozxing.Result, err error) {
|
||||
|
||||
var lum gozxing.LuminanceSource
|
||||
var bmp *gozxing.BinaryBitmap
|
||||
var binarizer gozxing.Binarizer
|
||||
var dec gozxing.Reader = qrcode.NewQRCodeReader()
|
||||
var hints map[gozxing.DecodeHintType]interface{} = map[gozxing.DecodeHintType]interface{}{
|
||||
gozxing.DecodeHintType_TRY_HARDER: true,
|
||||
}
|
||||
|
||||
lum = gozxing.NewLuminanceSourceFromImage(img)
|
||||
binarizer = gozxing.NewGlobalHistgramBinarizer(lum)
|
||||
if bmp, err = gozxing.NewBinaryBitmap(binarizer); err != nil {
|
||||
return
|
||||
}
|
||||
if res, err = dec.Decode(bmp, hints); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// normalizeName returns a normalized Vault-safe version of the name.
|
||||
func normalizeName(issuer, account string) (s string) {
|
||||
|
||||
var i string = issuer
|
||||
var a string = account
|
||||
|
||||
common.VaultEscape(&i)
|
||||
common.VaultEscape(&a)
|
||||
|
||||
s = fmt.Sprintf("%s%s%s", i, vaultSep, a)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseExplicit parses an explicit OTP specification.
|
||||
func parseExplicit(e ExplicitOtp) {
|
||||
|
||||
var ok bool
|
||||
var p parsedUrl = parsedUrl{
|
||||
u: nil,
|
||||
source: fmt.Sprintf("%#v", e),
|
||||
sourceType: "explicit",
|
||||
}
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
p.vaultName = normalizeName(e.Issuer, e.Account)
|
||||
p.u = &otpauth.URL{
|
||||
Type: strings.ToLower(e.Type),
|
||||
Issuer: e.Issuer,
|
||||
Account: e.Account,
|
||||
RawSecret: e.Secret,
|
||||
Algorithm: e.Algorithm,
|
||||
Digits: e.Digits,
|
||||
Period: int(e.Period.Seconds()),
|
||||
Counter: 0,
|
||||
}
|
||||
|
||||
if _, ok = existingOtp[p.vaultName]; ok {
|
||||
logger.Debug("parseExplicit: Found existing otp entry for '%s'; skipping.", p.vaultName)
|
||||
return
|
||||
}
|
||||
// Simple as.
|
||||
urlChan <- p
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseAsync is a helper for other parsers.
|
||||
func parseAsync(uriStr, src, srcType string, pwg *sync.WaitGroup) {
|
||||
|
||||
var ok bool
|
||||
var sL string
|
||||
var err error
|
||||
var u *otpauth.URL
|
||||
var multi []*otpauth.URL
|
||||
var p parsedUrl = parsedUrl{
|
||||
vaultName: "",
|
||||
u: nil,
|
||||
source: src,
|
||||
sourceType: srcType,
|
||||
}
|
||||
|
||||
defer pwg.Done()
|
||||
|
||||
sL = strings.ToLower(uriStr)
|
||||
if strings.HasPrefix(sL, stdScheme) {
|
||||
if p.u, err = otpauth.ParseURL(uriStr); err != nil {
|
||||
logger.Err("parseAsync: Failed to parse standard URL '%s' for %s '%s': %v", uriStr, srcType, src, err)
|
||||
return
|
||||
}
|
||||
p.vaultName = normalizeName(p.u.Issuer, p.u.Account)
|
||||
if _, ok = existingOtp[p.vaultName]; ok {
|
||||
logger.Debug("parseAsync: Found existing otp entry for '%s'; skipping.", p.vaultName)
|
||||
return
|
||||
}
|
||||
urlChan <- p
|
||||
} else if strings.HasPrefix(sL, googleScheme) {
|
||||
if multi, err = otpauth.ParseMigrationURL(uriStr); err != nil {
|
||||
logger.Err("parseAsync: Failed to parse Google URLs for %s '%s': %v", srcType, src, err)
|
||||
return
|
||||
}
|
||||
for _, u = range multi {
|
||||
p = parsedUrl{
|
||||
vaultName: normalizeName(u.Issuer, u.Account),
|
||||
u: new(otpauth.URL),
|
||||
source: src,
|
||||
sourceType: srcType,
|
||||
}
|
||||
if _, ok = existingOtp[p.vaultName]; ok {
|
||||
logger.Debug("parseAsync: Found existing otp entry for '%s'; skipping.", p.vaultName)
|
||||
continue
|
||||
}
|
||||
*p.u = *u
|
||||
urlChan <- p
|
||||
}
|
||||
} else {
|
||||
logger.Err("parseAsync: invalid uriStr '%s' for %s '%s'", uriStr, srcType, src)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseOtpFile reads in file at filepath `fpath`, reading a TOTP URI per-line.
|
||||
func parseOtpFile(fpath string) {
|
||||
|
||||
var err error
|
||||
var f *os.File
|
||||
var line string
|
||||
var buf *bufio.Reader
|
||||
var pwg sync.WaitGroup
|
||||
var fp string = fpath
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
if err = paths.RealPath(&fp); err != nil {
|
||||
logger.Err("parseOtpFile: Failed to canonize filepath '%s': %v", fp, err)
|
||||
return
|
||||
}
|
||||
|
||||
if f, err = os.Open(fp); err != nil {
|
||||
logger.Err("parseOtpFile: Failed to open file '%s': %v", fp, err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
var fErr error
|
||||
if fErr = f.Close(); fErr != nil {
|
||||
logger.Err("parseOtpFile: Failed to close file '%s': %v", fp, fErr)
|
||||
}
|
||||
}()
|
||||
buf = bufio.NewReader(f)
|
||||
|
||||
for {
|
||||
line, err = buf.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
logger.Err("parseOtpFile: Failed to read from file '%s': %v", fp, err)
|
||||
return
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
pwg.Add(1)
|
||||
go parseAsync(line, fp, "file", &pwg)
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pwg.Wait()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseOtpUri parses in an already-formatted TOTP URI.
|
||||
func parseOtpUri(uriStr string) {
|
||||
|
||||
var pwg sync.WaitGroup
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
pwg.Add(1)
|
||||
go parseAsync(uriStr, "URI", "<CLI>", &pwg)
|
||||
pwg.Wait()
|
||||
|
||||
// parseAsync handles the channel send etc.
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseQrFile reads in QR code image at filepath `fpath`.
|
||||
func parseQrFile(fpath string) {
|
||||
|
||||
var err error
|
||||
var f *os.File
|
||||
var s string
|
||||
var bw image.Image
|
||||
var img image.Image
|
||||
var pwg sync.WaitGroup
|
||||
var res *gozxing.Result
|
||||
var fp string = fpath
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
if err = paths.RealPath(&fp); err != nil {
|
||||
logger.Err("parseQrFile: Failed to canonize filepath '%s': %v", fp, err)
|
||||
return
|
||||
}
|
||||
|
||||
if f, err = os.Open(fp); err != nil {
|
||||
logger.Err("parseQrFile: Failed to open file '%s': %v", fpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
if img, s, err = image.Decode(f); err != nil {
|
||||
logger.Err("parseQrFile: Failed to decode image '%s': %v", fp, err)
|
||||
return
|
||||
}
|
||||
logger.Debug("parseQrFile: Decoded image '%s', decoded as '%s'", fp, s)
|
||||
|
||||
/*
|
||||
PHASE 1: LUMINANCE
|
||||
Luminance is performed at every phase, but this is the only modification
|
||||
this phase.
|
||||
This is done to reduce noise in QR images and does a much
|
||||
better job with "vanity" QR, where they abuse EC to put a logo in
|
||||
the center or summat. Note that it still doesn't work for QR codes
|
||||
surrounded by a black boundary and uses black encoding.
|
||||
https://github.com/makiuchi-d/gozxing/issues/76
|
||||
We have a couple things to try for that though if this fails.
|
||||
*/
|
||||
if res, err = decQR(img); err != nil {
|
||||
/*
|
||||
PHASE 2: CONVERT TO GRAYSCALE
|
||||
If the above wasn't successful, try with a grayscale image.
|
||||
*/
|
||||
bw = convertToGray(img)
|
||||
if res, err = decQR(bw); err != nil {
|
||||
/*
|
||||
PHASE 3: CONVERT TO MONOCHROME
|
||||
If the above wasn't successful, try with a monochrome image.
|
||||
*/
|
||||
bw = convertToMono(bw, false)
|
||||
if res, err = decQR(bw); err != nil {
|
||||
/*
|
||||
PHASE 4
|
||||
INVERTED MONOCHROME
|
||||
If the above wasn't successful, try with the monochrome inverted.
|
||||
*/
|
||||
bw = convertToMono(bw, true)
|
||||
if res, err = decQR(bw); err != nil {
|
||||
// I'm all out of tricks.
|
||||
logger.Err("parseQrFile: Failed to decode image '%s' as original, grayscale, monochrome, or inverted monochrome: %v", fp, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pwg.Add(1)
|
||||
go parseAsync(res.GetText(), fp, "QR code file", &pwg)
|
||||
pwg.Wait()
|
||||
|
||||
// parseAsync handles the channel send etc.
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseSs reads in TOTP information from the TOTP GNOME extension storage in SecretService.
|
||||
func parseSs() {
|
||||
|
||||
var err error
|
||||
var pwg sync.WaitGroup
|
||||
var item *gosecret.Item
|
||||
var svc *gosecret.Service
|
||||
var items []*gosecret.Item
|
||||
var coll *gosecret.Collection
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
if svc, err = gosecret.NewService(); err != nil {
|
||||
logger.Err("parseSs: Failed to attach to gosecret service: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
var sErr error
|
||||
if sErr = svc.Close(); sErr != nil {
|
||||
logger.Warning("parseSs: Received error closing gosecret service: %v", sErr)
|
||||
}
|
||||
}()
|
||||
|
||||
if coll, err = svc.GetCollection(ssCollectionNm); err != nil {
|
||||
logger.Err("parseSs: Failed to fetch gosecret collection '%s': %v", ssCollectionNm, err)
|
||||
return
|
||||
}
|
||||
|
||||
if items, err = coll.Items(); err != nil {
|
||||
logger.Err("parseSs: Failed to fetch gosecret items from '%s': %v", ssCollectionNm, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, item = range items {
|
||||
pwg.Add(1)
|
||||
go parseSsItemAsync(*item, &pwg)
|
||||
}
|
||||
|
||||
pwg.Wait()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseSsItemAsync is used by parseSs.
|
||||
func parseSsItemAsync(item gosecret.Item, pwg *sync.WaitGroup) {
|
||||
|
||||
var err error
|
||||
var i int
|
||||
var ok bool
|
||||
var s string
|
||||
var iss string
|
||||
var acct string
|
||||
var p parsedUrl
|
||||
var spl []string
|
||||
|
||||
defer pwg.Done()
|
||||
|
||||
// Fallback values from the name, though these *should* be attributes in the Item.
|
||||
// OTP plugin uses secret names like:
|
||||
// <id>:<issuer>:<account>
|
||||
// where <id> is a zero-indexed unsigned integer that determines a secret's position.
|
||||
// No, I don't know why he didn't just set the ordering/weight as an item attribute.
|
||||
spl = strings.SplitN(item.LabelName, ":", 3)
|
||||
|
||||
if iss, ok = item.Attrs["issuer"]; !ok {
|
||||
switch len(spl) {
|
||||
case 1:
|
||||
iss = spl[0]
|
||||
default:
|
||||
iss = spl[1]
|
||||
}
|
||||
}
|
||||
if acct, ok = item.Attrs["name"]; !ok {
|
||||
switch len(spl) {
|
||||
case 1:
|
||||
acct = spl[0]
|
||||
case 2:
|
||||
acct = spl[1]
|
||||
default:
|
||||
acct = spl[2]
|
||||
}
|
||||
}
|
||||
|
||||
p = parsedUrl{
|
||||
vaultName: normalizeName(iss, acct),
|
||||
u: nil,
|
||||
source: fmt.Sprintf("%s/%s", ssCollectionNm, item.LabelName),
|
||||
sourceType: "SecretService",
|
||||
}
|
||||
if _, ok = existingOtp[p.vaultName]; ok {
|
||||
logger.Debug("parseSsItemAsync: Found existing otp entry for '%s'; skipping.", p.vaultName)
|
||||
return
|
||||
}
|
||||
|
||||
p.u = &otpauth.URL{
|
||||
Type: "",
|
||||
Issuer: iss,
|
||||
Account: acct,
|
||||
RawSecret: string(item.Secret.Value),
|
||||
Algorithm: "",
|
||||
Digits: 0,
|
||||
Period: 0,
|
||||
Counter: 0,
|
||||
}
|
||||
|
||||
if s, ok = item.Attrs["type"]; ok {
|
||||
p.u.Type = strings.ToLower(s)
|
||||
} else {
|
||||
p.u.Type = "totp"
|
||||
}
|
||||
|
||||
if s, ok = item.Attrs["algorithm"]; ok {
|
||||
p.u.Algorithm = strings.ToUpper(
|
||||
strings.ReplaceAll(s, "-", ""),
|
||||
)
|
||||
} else {
|
||||
p.u.Algorithm = "SHA256" // Probably the most common.
|
||||
}
|
||||
|
||||
if s, ok = item.Attrs["digits"]; ok {
|
||||
if i, err = strconv.Atoi(s); err != nil {
|
||||
logger.Err("parseSsItemAsync: Failed to parse digits '%s' for %s '%s': %v", s, p.sourceType, p.source, err)
|
||||
return
|
||||
}
|
||||
p.u.Digits = i
|
||||
} else {
|
||||
p.u.Digits = 6
|
||||
}
|
||||
|
||||
if s, ok = item.Attrs["period"]; ok {
|
||||
if i, err = strconv.Atoi(s); err != nil {
|
||||
logger.Err("parseSsItemAsync: Failed to parse perid '%s' for %s '%s': %v", s, p.sourceType, p.source, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
p.u.Period = 30
|
||||
}
|
||||
|
||||
urlChan <- p
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// readUrls reads in parsed OTPAuth URLs from various sources and writes them to Vault.
|
||||
func readUrls() {
|
||||
|
||||
var err error
|
||||
var u parsedUrl
|
||||
var vc *vault.Client
|
||||
var pwg sync.WaitGroup
|
||||
var ctx context.Context = context.Background()
|
||||
|
||||
if vc, err = internal.GetVaultClient(&args.VaultArgs); err != nil {
|
||||
logger.Err("readUrls: Failed to get Vault client: %v", err)
|
||||
vaultReady <- false
|
||||
return
|
||||
}
|
||||
|
||||
if existingOtp, err = internal.ListTotpKeys(ctx, vc, args.AddArgs.VaultTotpMnt); err != nil {
|
||||
logger.Err("readUrls: Failed to list totp keys: %v", err)
|
||||
vaultReady <- false
|
||||
return
|
||||
}
|
||||
vaultReady <- true
|
||||
|
||||
for u = range urlChan {
|
||||
if u.u != nil {
|
||||
logger.Debug("readUrls: Read in '%s' from %s '%s'", u.u.String(), u.sourceType, u.source)
|
||||
pwg.Add(1)
|
||||
go storeUrl(u, ctx, vc, &pwg)
|
||||
if _, err = vc.Secrets.TotpCreateKey(
|
||||
ctx,
|
||||
u.vaultName,
|
||||
schema.TotpCreateKeyRequest{
|
||||
AccountName: "",
|
||||
Algorithm: "",
|
||||
Digits: 0,
|
||||
Exported: false,
|
||||
Generate: false,
|
||||
Issuer: "",
|
||||
Key: "",
|
||||
KeySize: 0,
|
||||
Period: "",
|
||||
QrSize: 0,
|
||||
Skew: 0,
|
||||
Url: u.u.String(),
|
||||
},
|
||||
vault.WithMountPath(args.AddArgs.VaultTotpMnt),
|
||||
); err != nil {
|
||||
logger.Err("readUrls: Failed to create key '%s' on '%s': %v", u.vaultName, args.AddArgs.VaultTotpMnt, err)
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pwg.Wait()
|
||||
|
||||
doneChan <- true
|
||||
}
|
||||
|
||||
func storeUrl(p parsedUrl, vctx context.Context, vc *vault.Client, pwg *sync.WaitGroup) {
|
||||
|
||||
var err error
|
||||
var mnt string
|
||||
var qrB64 string
|
||||
var secPath string
|
||||
var img image.Image
|
||||
var otpk *otp.Key
|
||||
var dat map[string]interface{}
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
defer pwg.Done()
|
||||
|
||||
if args.AddArgs.VaultKV2MntPath == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// QR CODE. Any failures here should be non-fatal; capturing the secret is more important.
|
||||
if otpk, err = otp.NewKeyFromURL(p.u.String()); err != nil {
|
||||
logger.Err("storeUrl: Failed to create OTP key for '%s': %v", p.u.String(), err)
|
||||
} else {
|
||||
if img, err = otpk.Image(512, 512); err != nil {
|
||||
logger.Err("storeUrl: Failed to create QR code for '%s': %v", p.u.String(), err)
|
||||
} else {
|
||||
// WHY DOES IT CREATE A 16-BIT IMAGE.
|
||||
// IT'S MONOCHROME; IT USES A WHOLE-ASS 1 BIT OF FIDELITY.
|
||||
// We can save a whole-ass ~17% reduction by just converting to 8-bit,
|
||||
// but we save ~32% reduction by going to 1-bit (true monochrome).
|
||||
img = convertToMono(convertToGray(img), false)
|
||||
if err = png.Encode(buf, img); err != nil {
|
||||
logger.Err("storeUrl: Failed to encode QR code for '%s': %v", p.u.String(), err)
|
||||
} else {
|
||||
qrB64 = base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mnt, secPath = internal.SplitVaultPathspec2(*args.AddArgs.VaultKV2MntPath)
|
||||
if mnt == "" {
|
||||
mnt = defStoreMnt
|
||||
}
|
||||
secPath = path.Join(secPath, p.u.Issuer, p.u.Account)
|
||||
|
||||
dat = map[string]interface{}{
|
||||
// PNG image data, 512 x 512, 16-bit grayscale, non-interlaced
|
||||
"qr_b64": qrB64,
|
||||
"uri_str": p.u.String(),
|
||||
"generator": fmt.Sprintf(
|
||||
"[%s]%s",
|
||||
args.AddArgs.VaultTotpMnt,
|
||||
),
|
||||
"uri": map[string]any{
|
||||
"account": p.u.Account,
|
||||
"algorithm": p.u.Algorithm,
|
||||
"digits": p.u.Digits,
|
||||
"issuer": p.u.Issuer,
|
||||
"period": p.u.Period,
|
||||
"secret": p.u.RawSecret,
|
||||
},
|
||||
}
|
||||
|
||||
if _, err = vc.Secrets.KvV2Write(
|
||||
vctx,
|
||||
secPath,
|
||||
schema.KvV2WriteRequest{
|
||||
Data: dat,
|
||||
// Options: nil,
|
||||
},
|
||||
vault.WithMountPath(mnt),
|
||||
); err != nil {
|
||||
logger.Err("storeUrl: Failed to store key '%s' from %s '%s': %v", p.vaultName, p.sourceType, p.source, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
73
cmd/add/main.go
Normal file
73
cmd/add/main.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"r00t2.io/vault_totp/internal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var s string
|
||||
var doExit bool
|
||||
var vaultOk bool
|
||||
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
if doExit, err = internal.PrepParser("add", args.CommonArgs, parser); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if doExit {
|
||||
return
|
||||
}
|
||||
logger = internal.Logger
|
||||
|
||||
if err = internal.Validate(args); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
go readUrls()
|
||||
|
||||
vaultOk = <-vaultReady
|
||||
if !vaultOk {
|
||||
log.Panicln("Could not get OK from Vault client.")
|
||||
}
|
||||
|
||||
if args.AddArgs.OtpFile != nil && len(args.AddArgs.OtpFile) > 0 {
|
||||
for _, s = range args.AddArgs.OtpFile {
|
||||
wg.Add(1)
|
||||
go parseOtpFile(s)
|
||||
}
|
||||
}
|
||||
if args.AddArgs.OtpExplicit {
|
||||
wg.Add(1)
|
||||
go parseExplicit(args.ExplicitOtp)
|
||||
}
|
||||
if args.AddArgs.OtpUrl != nil && len(args.AddArgs.OtpUrl) > 0 {
|
||||
for _, s = range args.AddArgs.OtpUrl {
|
||||
wg.Add(1)
|
||||
go parseOtpUri(s)
|
||||
}
|
||||
}
|
||||
if args.AddArgs.QrImgPath != nil && len(args.AddArgs.QrImgPath) > 0 {
|
||||
for _, s = range args.AddArgs.QrImgPath {
|
||||
wg.Add(1)
|
||||
go parseQrFile(s)
|
||||
}
|
||||
}
|
||||
if args.AddArgs.SecretService {
|
||||
wg.Add(1)
|
||||
go parseSs()
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(urlChan)
|
||||
}()
|
||||
|
||||
<-doneChan
|
||||
|
||||
logger.Debug("main: Done.")
|
||||
}
|
||||
14
cmd/add/types.go
Normal file
14
cmd/add/types.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`github.com/creachadair/otp/otpauth`
|
||||
)
|
||||
|
||||
type (
|
||||
parsedUrl struct {
|
||||
vaultName string
|
||||
u *otpauth.URL
|
||||
source string
|
||||
sourceType string
|
||||
}
|
||||
)
|
||||
23
cmd/gen/args.go
Normal file
23
cmd/gen/args.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"r00t2.io/vault_totp/internal"
|
||||
)
|
||||
|
||||
type (
|
||||
Args struct {
|
||||
internal.CommonArgs
|
||||
GenArgs
|
||||
}
|
||||
GenArgs struct {
|
||||
VaultTotpMnt string `env:"VTOTP_MNT" short:"m" long:"mount" default:"totp" description:"The Vault TOTP generator mount (a 'TOTP secrets' mount) to fetch a code for -k/--key from (or list key names from)."`
|
||||
KeyNm []string `env:"VTOTP_GENK" short:"k" long:"key" description:"Key name(s) to generate code(s) for."`
|
||||
Readable bool `env:"VTOTP_RD" short:"R" long:"readable" description:"If specified, the code will be spaced out to be a bit more readable."`
|
||||
Repeat int `env:"VTOTP_RPT" short:"r" long:"repeat" description:"If non-zero, repeat code generation this many times. A negative number will generate codes indefinitely until ctrl-c is pressed/the program is killed. The default is to not repeat (0 == a single code, 1 == 2 codes generated, etc.)."`
|
||||
// The below are hidden (hidden:"true") until either https://github.com/hashicorp/vault/issues/31684 and/or https://github.com/openbao/openbao/issues/2233
|
||||
// I kind of fudge it for now. TODO.
|
||||
NoCtr bool `hidden:"true" env:"VTOTP_NOCTR" short:"q" long:"no-ctr" description:"If specified, do not perform a countdown of validity; just print the generated code to the terminal and exit immediately after."`
|
||||
PrintExpiry bool `hidden:"true" env:"VTOTP_EXPIRY" short:"e" long:"expiry" description:"If -q/--no-ctr is specified, also print the validity duration and expiration time (but do not animate a countdown, just print the validity/expiration and code and exit). The validity is always printed if a counter is."`
|
||||
Plain []bool `hidden:"true" env:"VTOTP_PLN" short:"p" long:"plain" description:"If specified, use a countdown timer more friendly to non-unicode terminals. Can be repeated for to three levels of increasing 'plain-ness'. Has no effect if -q/--no-ctr is specified. (Level 3 plainness is restricted to ASCII; no UTF-8.)"`
|
||||
}
|
||||
)
|
||||
90
cmd/gen/consts.go
Normal file
90
cmd/gen/consts.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`context`
|
||||
`os`
|
||||
`sync`
|
||||
|
||||
`github.com/hashicorp/vault-client-go`
|
||||
`github.com/jessevdk/go-flags`
|
||||
`github.com/pterm/pterm`
|
||||
`r00t2.io/goutils/logging`
|
||||
)
|
||||
|
||||
var (
|
||||
logger logging.Logger
|
||||
args *Args = new(Args)
|
||||
parser *flags.Parser = flags.NewParser(args, flags.Default)
|
||||
)
|
||||
|
||||
var (
|
||||
vc *vault.Client
|
||||
wg sync.WaitGroup
|
||||
cancelFunc context.CancelFunc
|
||||
ctx context.Context = context.Background()
|
||||
// keyed on vault TOTP key name
|
||||
otpCfgs map[string]*otpCfg = make(map[string]*otpCfg)
|
||||
// keyed on Vault TOTP key name
|
||||
kinfo map[string]map[string]any = make(map[string]map[string]any)
|
||||
)
|
||||
|
||||
var (
|
||||
// This looks too goofy with the half-hour face.
|
||||
// It'd be better if I could do it in 15m increments,
|
||||
// but that doesn't exist in UTF-8 - half-hour-past is the closest fidelity we get...
|
||||
clocksFull []string = []string{
|
||||
"🕛", "🕧", "🕐", "🕜",
|
||||
"🕑", "🕝", "🕒", "🕞",
|
||||
"🕓", "🕟", "🕔", "🕠",
|
||||
"🕕", "🕡", "🕖", "🕢",
|
||||
"🕗", "🕣", "🕘", "🕤",
|
||||
"🕙", "🕥", "🕚", "🕦",
|
||||
}
|
||||
|
||||
// So do it hourly instead.
|
||||
clocksHourly []string = []string{
|
||||
"🕛", "🕐", "🕑", "🕒",
|
||||
"🕓", "🕔", "🕕", "🕖",
|
||||
"🕗", "🕘", "🕙", "🕚",
|
||||
}
|
||||
|
||||
// Level 1 "plain" (the clocks are level 0)
|
||||
plain1 []string = []string{
|
||||
"◷", "◶", "◵", "◴",
|
||||
}
|
||||
plain2 []string = []string{
|
||||
"⠈⠁", "⠈⠑", "⠈⠱", "⠈⡱", "⢀⡱", "⢄⡱", "⢄⡱", "⢆⡱", "⢎⡱",
|
||||
}
|
||||
// plain3 should restrict to pure ASCII
|
||||
plain3 []string = []string{
|
||||
"|", "/", "-", "\\", "-", "|",
|
||||
}
|
||||
|
||||
// indexed on level of plainness
|
||||
spinnerChars [][]string = [][]string{
|
||||
clocksHourly,
|
||||
plain1,
|
||||
plain2,
|
||||
plain3,
|
||||
}
|
||||
|
||||
// charSet is set to one of spinnerChars depending on args.GenArgs.Plain level.
|
||||
charSet []string
|
||||
|
||||
/*
|
||||
Previously I was going to use https://pkg.go.dev/github.com/chelnak/ysmrr for this.
|
||||
Namely because I *thought* it was the only spinner lib that can do multiple spinners at once.
|
||||
(Even submitted a PR, https://github.com/chelnak/ysmrr/pull/88)
|
||||
|
||||
HOWEVER, all spinners are synced to use the same animation... which means they all use the same
|
||||
frequency/rate of update.
|
||||
I want to sync it so the "animation" ends when the TOTP runs out (or thereabouts).
|
||||
|
||||
pterm to the rescue.
|
||||
*/
|
||||
codeMulti pterm.MultiPrinter = pterm.MultiPrinter{
|
||||
IsActive: false,
|
||||
Writer: os.Stdout,
|
||||
UpdateDelay: 0,
|
||||
}
|
||||
)
|
||||
4
cmd/gen/doc.go
Normal file
4
cmd/gen/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
vault_totp_gen: Generate a TOTP code from a TOTP secret.
|
||||
*/
|
||||
package main
|
||||
180
cmd/gen/funcs.go
Normal file
180
cmd/gen/funcs.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`context`
|
||||
`encoding/json`
|
||||
`fmt`
|
||||
`log`
|
||||
`net/http`
|
||||
`path`
|
||||
`strconv`
|
||||
`time`
|
||||
|
||||
`github.com/davecgh/go-spew/spew`
|
||||
`github.com/hashicorp/vault-client-go`
|
||||
`github.com/pterm/pterm`
|
||||
`r00t2.io/vault_totp/common`
|
||||
`r00t2.io/vault_totp/errs`
|
||||
)
|
||||
|
||||
// displayCode fetches a code (getCode) and displays it (or rather, sets up the display for it) according to the settings from args global.
|
||||
func displayCode(keyNm string) {
|
||||
|
||||
var err error
|
||||
var code string
|
||||
var cfg *otpCfg
|
||||
var itersLeft int
|
||||
var infinite bool
|
||||
var spinner pterm.SpinnerPrinter
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
infinite = args.GenArgs.Repeat < 0
|
||||
|
||||
if args.GenArgs.NoCtr {
|
||||
if args.GenArgs.Repeat
|
||||
}
|
||||
|
||||
if code, cfg, err = getCode(ctx, keyNm, args.GenArgs.VaultTotpMnt, args.GenArgs.Readable); err != nil {
|
||||
logger.Err("displayCode: Received error getting TOTP code and configuration: %v")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
expiryCb is a callback used to pull the ['Date' header] from the response.
|
||||
|
||||
See [hashicorp/vault#31684], [openbao/openbao#2233].
|
||||
|
||||
['Date' header]: https://datatracker.ietf.org/doc/html/rfc9110#section-6.6.1
|
||||
[hashicorp/vault#31684]: https://github.com/hashicorp/vault/issues/31684
|
||||
[openbao/openbao#2233]: https://github.com/openbao/openbao/issues/2233
|
||||
*/
|
||||
func expiryCb(req *http.Request, resp *http.Response) {
|
||||
|
||||
var err error
|
||||
var i any
|
||||
var n int
|
||||
var ok bool
|
||||
var keyNm string
|
||||
|
||||
keyNm = path.Base(req.URL.Path)
|
||||
|
||||
otpCfgs[keyNm] = &otpCfg{
|
||||
keyNm: keyNm,
|
||||
respDate: time.Time{},
|
||||
period: 0,
|
||||
timeStep: 0,
|
||||
expiry: time.Time{},
|
||||
}
|
||||
|
||||
if i, ok = kinfo["period"]; !ok {
|
||||
logger.Err("expiryCb: No period found for key '%s' in kinfo", keyNm)
|
||||
return
|
||||
}
|
||||
|
||||
switch t := i.(type) {
|
||||
case string:
|
||||
// If it's an int string, it's seconds.
|
||||
if n, err = strconv.Atoi(t); err != nil {
|
||||
logger.Warning("expiryCb: Invalid period integer for key '%s': %#v: %v", keyNm, i, err)
|
||||
// It's not a pure int string, so try a time.Duration string (e.g. "30s").
|
||||
if otpCfgs[keyNm].period, err = time.ParseDuration(t); err != nil {
|
||||
logger.Err("expiryCb: Invalid period duration for key '%s': %#v: %v", keyNm, i, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
case json.Number:
|
||||
// But I think it's actually a json.Number...
|
||||
if n, err = strconv.Atoi(string(t)); err != nil {
|
||||
logger.Warning("expiryCb: Invalid period json.Number for key '%s': %#v: %v", i, keyNm, err)
|
||||
return
|
||||
}
|
||||
case int:
|
||||
n = t
|
||||
default:
|
||||
logger.Err("expiryCb: Invalid period type for key '%s' (%#T): %#v", keyNm, i, i)
|
||||
return
|
||||
}
|
||||
if otpCfgs[keyNm].period == 0 && n != 0 {
|
||||
// Golang is weird like this but basically time.Duration(n) isn't *actually* meaningful,
|
||||
// it's just necessary for type matching.
|
||||
otpCfgs[keyNm].period = time.Second * time.Duration(n)
|
||||
} else if n == 0 {
|
||||
logger.Err("expiryCb: Could not derive time primitive for key '%s' from '%#v'", keyNm, i)
|
||||
return
|
||||
}
|
||||
|
||||
if otpCfgs[keyNm].respDate, err = http.ParseTime(resp.Header.Get("Date")); err != nil {
|
||||
logger.Err(
|
||||
"expiryCb: received error parsing 'Date' header ('%s') for key '%s': %v",
|
||||
resp.Header.Get("Date"), keyNm, err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
otpCfgs[keyNm].timeStep = common.TimeStepFromTime(otpCfgs[keyNm].respDate, otpCfgs[keyNm].period)
|
||||
otpCfgs[keyNm].startTimeStep, otpCfgs[keyNm].expiry = common.TimeStepToTime(otpCfgs[keyNm].timeStep, otpCfgs[keyNm].period)
|
||||
|
||||
logger.Debug("expiryCb: Derived expiration for key '%s':\n%s", keyNm, spew.Sdump(otpCfgs[keyNm]))
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
getCode gets an OTP code from Vault (and populates the corresponding cfg).
|
||||
|
||||
The code will be split with a space if `readable` is true.
|
||||
*/
|
||||
func getCode(ctx context.Context, keyNm, mntPt string, readable bool) (code string, cfg *otpCfg, err error) {
|
||||
|
||||
var i any
|
||||
var ok bool
|
||||
var resp *vault.Response[map[string]interface{}]
|
||||
|
||||
if resp, err = vc.Secrets.TotpGenerateCode(
|
||||
ctx,
|
||||
keyNm,
|
||||
vault.WithMountPath(mntPt),
|
||||
vault.WithResponseCallbacks(expiryCb),
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if i, ok = resp.Data["code"]; !ok {
|
||||
err = errs.ErrNoCode
|
||||
logger.Err("getCode: Key '%s': %v", keyNm, err)
|
||||
return
|
||||
}
|
||||
|
||||
if cfg, ok = otpCfgs[keyNm]; !ok {
|
||||
err = errs.ErrNoCfg
|
||||
logger.Err("getCode: Key '%s': %v", keyNm, err)
|
||||
return
|
||||
}
|
||||
|
||||
switch t := i.(type) {
|
||||
case string:
|
||||
code = t
|
||||
case json.Number:
|
||||
code = string(t)
|
||||
case int:
|
||||
code = strconv.Itoa(t)
|
||||
default:
|
||||
logger.Err("getCode: Invalid type for key '%s' (%#T): %#v", keyNm, i, i)
|
||||
err = errs.ErrNoCode
|
||||
return
|
||||
}
|
||||
|
||||
if readable {
|
||||
switch len(code) {
|
||||
case 6:
|
||||
code = fmt.Sprintf("%s %s", code[:3], code[3:])
|
||||
case 8:
|
||||
code = fmt.Sprintf("%s %s", code[:4], code[4:])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
104
cmd/gen/main.go
Normal file
104
cmd/gen/main.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`log`
|
||||
`os`
|
||||
`os/signal`
|
||||
`sort`
|
||||
`strings`
|
||||
`time`
|
||||
|
||||
`github.com/chelnak/ysmrr`
|
||||
`github.com/chelnak/ysmrr/pkg/animations`
|
||||
`r00t2.io/vault_totp/common`
|
||||
`r00t2.io/vault_totp/internal`
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var idx int
|
||||
var rpt int
|
||||
var doExit bool
|
||||
var keyNm string
|
||||
var keyNms []string
|
||||
var ticker *time.Ticker
|
||||
var keys map[string]struct{} = make(map[string]struct{})
|
||||
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
ctx, cancelFunc = signal.NotifyContext(ctx, common.ProgEndSigs...)
|
||||
|
||||
if doExit, err = internal.PrepParser("gen", args.CommonArgs, parser); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if doExit {
|
||||
return
|
||||
}
|
||||
logger = internal.Logger
|
||||
|
||||
if err = internal.Validate(args); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if vc, err = internal.GetVaultClient(&args.VaultArgs); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if args.GenArgs.KeyNm == "" {
|
||||
if keys, err = internal.ListTotpKeys(ctx, vc, args.GenArgs.VaultTotpMnt); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
keyNms = make([]string, 0, len(keys))
|
||||
for keyNm, _ = range args.GenArgs.KeyNm {
|
||||
keyNms[idx] = keyNm
|
||||
idx++
|
||||
}
|
||||
sort.Strings(keyNms)
|
||||
fmt.Printf(
|
||||
"No key name provided.\n"+
|
||||
"Existing key names at mount '%s' are:\n\n"+
|
||||
"\t%s\n",
|
||||
args.GenArgs.VaultTotpMnt,
|
||||
strings.Join(keyNms, "\n\t"),
|
||||
)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if kinfo, err = internal.GetTotpKeys(ctx, vc, args.GenArgs.VaultTotpMnt); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if kinfo == nil || len(kinfo) == 0 {
|
||||
log.Panicln("no TOTP configuration found")
|
||||
}
|
||||
|
||||
if !args.GenArgs.NoCtr {
|
||||
if len(args.GenArgs.Plain) > 3 {
|
||||
args.GenArgs.Plain = args.GenArgs.Plain[:3]
|
||||
}
|
||||
charSet = spinnerChars[len(args.GenArgs.Plain)]
|
||||
}
|
||||
|
||||
if args.GenArgs.Repeat < 0 {
|
||||
// ticker = time.NewTicker(cfg.period + (time.Millisecond * time.Duration(500)))
|
||||
ticker = time.NewTicker(cfg.period)
|
||||
breakLoop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break breakLoop
|
||||
case <-ticker.C:
|
||||
// GET CODE
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for rpt = args.GenArgs.Repeat; rpt >= 0; rpt-- {
|
||||
fmt.Fprintf(os.Stderr, "(Issuance round #%d/%d)\n", args.GenArgs.Repeat-rpt, rpt)
|
||||
// GET CODE
|
||||
}
|
||||
}
|
||||
|
||||
// Force close any remaining timing loops, etc.
|
||||
cancelFunc()
|
||||
}
|
||||
39
cmd/gen/types.go
Normal file
39
cmd/gen/types.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`time`
|
||||
)
|
||||
|
||||
type (
|
||||
// otpCfg is used to pull in several different TOTP information. TODO.
|
||||
otpCfg struct {
|
||||
// keyNm is the key name this configuration info is associated with.
|
||||
keyNm string
|
||||
/*
|
||||
respDate is pulled in *separately* from the `date` HTTP header during *code generation*.
|
||||
|
||||
It's very unlikely to be accurate to the TOTP code generation from Vault.
|
||||
|
||||
https://github.com/hashicorp/vault/issues/31684
|
||||
https://github.com/openbao/openbao/issues/2233
|
||||
*/
|
||||
respDate time.Time
|
||||
// period is fetched during *initialization*.
|
||||
period time.Duration
|
||||
/*
|
||||
timeStep is the time step identifier.
|
||||
|
||||
It's created via:
|
||||
|
||||
otpCfg.timeStep = int64(math.Floor(float64(otpCfg.respDate.Unix()) / float64(optCfg.period)))
|
||||
*/
|
||||
timeStep int64
|
||||
// startTimeStep is when beginning of the current period.
|
||||
startTimeStep time.Time
|
||||
/*
|
||||
expiry is the exact timestamp (at least to the level that is... reasonably determinable,
|
||||
currently, depending on the above issues under respDate) that the code expires.
|
||||
*/
|
||||
expiry time.Time
|
||||
}
|
||||
)
|
||||
18
cmd/kill/args.go
Normal file
18
cmd/kill/args.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`r00t2.io/vault_totp/internal`
|
||||
)
|
||||
|
||||
type (
|
||||
Args struct {
|
||||
internal.CommonArgs
|
||||
KillArgs
|
||||
}
|
||||
KillArgs struct {
|
||||
VaultTotpMnt string `env:"VTOTP_MNT" short:"m" long:"mount" default:"totp" description:"The Vault TOTP generator mount (a 'TOTP secrets' mount) to remove from."`
|
||||
KeyNms []string `env:"VTOTP_DELKEYS" short:"k" long:"key" description:"Key name(s) to delete. If not specified, you will be prompted to delete all existing (see -f/--force)."`
|
||||
Force bool `env:"VTOTP_FORCE" short:"f" long:"force" description:"Force the deletion of keys (don't prompt for confirmation). If explicit -k/--key key name(s) are provided, specifying this will consolidate all individual prompts into a single one (see -F/--force2). If deleting all keys (no -k/--keys specified), specifying this will bypass the confirmation."`
|
||||
Force2 bool `env:"VTOTP_FORCE2" short:"F" long:"force2" description:"If -k/--key names were specified and -f/--force was specified, specifying this option in *addition* to -f/--force bypasses the consolidated confirmation as well. Has no effect if no -k/--key is specified or if -f/--force was not specified."`
|
||||
}
|
||||
)
|
||||
23
cmd/kill/consts.go
Normal file
23
cmd/kill/consts.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`context`
|
||||
`sync`
|
||||
|
||||
`github.com/hashicorp/vault-client-go`
|
||||
`github.com/jessevdk/go-flags`
|
||||
`r00t2.io/goutils/logging`
|
||||
)
|
||||
|
||||
var (
|
||||
logger logging.Logger
|
||||
args *Args = new(Args)
|
||||
parser *flags.Parser = flags.NewParser(args, flags.Default)
|
||||
)
|
||||
|
||||
var (
|
||||
vc *vault.Client
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context = context.Background()
|
||||
existingOtp map[string]struct{} = make(map[string]struct{})
|
||||
)
|
||||
4
cmd/kill/doc.go
Normal file
4
cmd/kill/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
vault_totp_kill: Remove configured TOTP generators.
|
||||
*/
|
||||
package main
|
||||
30
cmd/kill/funcs.go
Normal file
30
cmd/kill/funcs.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`github.com/hashicorp/vault-client-go`
|
||||
)
|
||||
|
||||
// killKeyAsync kills key keyNm if found in the Vault mount.
|
||||
func killKeyAsync(keyNm string) {
|
||||
|
||||
var err error
|
||||
var ok bool
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
if _, ok = existingOtp[keyNm]; !ok {
|
||||
logger.Warning("Key '%s' does not exist on '%s'; skipping.", keyNm, args.KillArgs.VaultTotpMnt)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = vc.Secrets.TotpDeleteKey(
|
||||
ctx,
|
||||
keyNm,
|
||||
vault.WithMountPath(args.KillArgs.VaultTotpMnt),
|
||||
); err != nil {
|
||||
logger.Err("killKeyAsync: Failed to delete key '%s' on '%s': %v", keyNm, args.KillArgs.VaultTotpMnt, err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
121
cmd/kill/main.go
Normal file
121
cmd/kill/main.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`bufio`
|
||||
`fmt`
|
||||
`log`
|
||||
`maps`
|
||||
`os`
|
||||
`slices`
|
||||
`strings`
|
||||
|
||||
`r00t2.io/vault_totp/internal`
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var p string
|
||||
var doExit bool
|
||||
var keyNm string
|
||||
var doGlobal bool
|
||||
var keyNms []string
|
||||
var rdr *bufio.Reader = bufio.NewReader(os.Stdin)
|
||||
var keys map[string]struct{} = make(map[string]struct{})
|
||||
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
if doExit, err = internal.PrepParser("kill", args.CommonArgs, parser); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if doExit {
|
||||
return
|
||||
}
|
||||
logger = internal.Logger
|
||||
|
||||
if err = internal.Validate(args); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if vc, err = internal.GetVaultClient(&args.VaultArgs); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if existingOtp, err = internal.ListTotpKeys(ctx, vc, args.KillArgs.VaultTotpMnt); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if len(existingOtp) == 0 {
|
||||
fmt.Printf("No existing TOTP keys found at '%s'.\n", args.KillArgs.VaultTotpMnt)
|
||||
os.Exit(0)
|
||||
}
|
||||
if args.KillArgs.KeyNms == nil || len(args.KillArgs.KeyNms) == 0 {
|
||||
for keyNm = range maps.Keys(existingOtp) {
|
||||
keys[keyNm] = struct{}{}
|
||||
keyNms = append(keyNms, keyNm)
|
||||
}
|
||||
doGlobal = !args.KillArgs.Force
|
||||
} else {
|
||||
if args.KillArgs.Force {
|
||||
doGlobal = !args.KillArgs.Force2
|
||||
}
|
||||
for _, keyNm = range args.KillArgs.KeyNms {
|
||||
if !args.KillArgs.Force {
|
||||
p = ""
|
||||
fmt.Printf("Delete TOTP '[%s]/%s'? (y/N)\n", args.KillArgs.VaultTotpMnt, keyNm)
|
||||
if p, err = rdr.ReadString('\n'); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if strings.HasPrefix(
|
||||
strings.ToLower(
|
||||
strings.TrimSpace(
|
||||
p,
|
||||
),
|
||||
),
|
||||
"y",
|
||||
) {
|
||||
keys[keyNm] = struct{}{}
|
||||
keyNms = append(keyNms, keyNm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
if keyNms == nil || len(keyNms) == 0 {
|
||||
fmt.Printf("No keys selected for deletion on '%s'.\n", args.KillArgs.VaultTotpMnt)
|
||||
os.Exit(0)
|
||||
}
|
||||
slices.Sort(keyNms)
|
||||
|
||||
if doGlobal {
|
||||
fmt.Printf(
|
||||
"Will delete the following TOTP keys on mount '%s':\n"+
|
||||
"\t* %s"+
|
||||
"\n",
|
||||
args.KillArgs.VaultTotpMnt,
|
||||
strings.Join(keyNms, "\n\t*"),
|
||||
)
|
||||
fmt.Println("Is this OK? (y/N)")
|
||||
p = ""
|
||||
if p, err = rdr.ReadString('\n'); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
fmt.Println()
|
||||
if !strings.HasPrefix(
|
||||
strings.ToLower(
|
||||
strings.TrimSpace(
|
||||
p,
|
||||
),
|
||||
),
|
||||
"y",
|
||||
) {
|
||||
fmt.Println("Exiting.")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
for _, keyNm = range keys {
|
||||
wg.Add(1)
|
||||
go killKeyAsync(keyNm)
|
||||
}
|
||||
|
||||
log.Println("Done.")
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`log`
|
||||
`os`
|
||||
|
||||
`update_vault_totp/internal`
|
||||
|
||||
"golang.org/x/term"
|
||||
"r00t2.io/sysutils/envs"
|
||||
)
|
||||
|
||||
func getToken() (tok string, err error) {
|
||||
|
||||
var p1 []byte
|
||||
var oldState *term.State
|
||||
|
||||
if envs.HasEnv(internal.VaultTokEnv) {
|
||||
tok = os.Getenv(internal.VaultTokEnv)
|
||||
return
|
||||
}
|
||||
|
||||
// Prompt for it instead
|
||||
fmt.Println("Vault token needed.\nVault token (will not be echoed back):")
|
||||
if oldState, err = term.GetState(internal.TermFd); err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err = term.Restore(internal.TermFd, oldState); err != nil {
|
||||
log.Println("restore failed:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if p1, err = term.ReadPassword(internal.TermFd); err != nil {
|
||||
return
|
||||
}
|
||||
tok = string(p1)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var errs []error
|
||||
var tok string
|
||||
var c *internal.Client
|
||||
|
||||
if tok, err = getToken(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if c, err = internal.New(tok, internal.DefAddr, internal.TgtMnt, internal.CollNm); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
c.DeleteAllVaultKeys()
|
||||
|
||||
if err = c.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if errs = c.Errors(); len(errs) > 0 {
|
||||
for _, err = range errs {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Done.")
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`log`
|
||||
`os`
|
||||
|
||||
`update_vault_totp/internal`
|
||||
|
||||
"golang.org/x/term"
|
||||
"r00t2.io/sysutils/envs"
|
||||
)
|
||||
|
||||
func getToken() (tok string, err error) {
|
||||
|
||||
var p1 []byte
|
||||
var oldState *term.State
|
||||
|
||||
if envs.HasEnv(internal.VaultTokEnv) {
|
||||
tok = os.Getenv(internal.VaultTokEnv)
|
||||
return
|
||||
}
|
||||
|
||||
// Prompt for it instead
|
||||
fmt.Println("Vault token needed.\nVault token (will not be echoed back):")
|
||||
if oldState, err = term.GetState(internal.TermFd); err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err = term.Restore(internal.TermFd, oldState); err != nil {
|
||||
log.Println("restore failed:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if p1, err = term.ReadPassword(internal.TermFd); err != nil {
|
||||
return
|
||||
}
|
||||
tok = string(p1)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var errs []error
|
||||
var tok string
|
||||
var c *internal.Client
|
||||
|
||||
if tok, err = getToken(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if c, err = internal.New(tok, internal.DefAddr, internal.TgtMnt, internal.CollNm); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
c.Sync()
|
||||
|
||||
if err = c.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if errs = c.Errors(); len(errs) > 0 {
|
||||
for _, err = range errs {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Done.")
|
||||
}
|
||||
27
cmd/user/args.go
Normal file
27
cmd/user/args.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`r00t2.io/vault_totp/internal`
|
||||
)
|
||||
|
||||
type (
|
||||
Args struct {
|
||||
internal.CommonArgs
|
||||
Mounts Mounts `env-namespace:"VTOTP_MNT" group:"Vault Mounts" namespace:"vmnt"`
|
||||
Gen GenOpts `env-namespace:"VTOTP_GEN" group:"TOTP Seed Generation"`
|
||||
}
|
||||
Mounts struct {
|
||||
Auth string `env:"AUTHN" short:"a" long:"auth" default:"ldap" description:"The authentication mountpoint for users/entities. Must currently be mounted/configured."`
|
||||
TotpPath string `env:"PATH" short:"T" long:"path" default:"admin:totp/{{- (index .aliases 0).name -}}" description:"The <mount>:<path> specifier for where to save the TOTP secret/seed data. It is normally non-retrievable after generation. The mount name must be a pre-existing KVv2 mount. If the secret exists, it will be overwritten/merged at the root secret level. Supports text/template against the data returned by -l/--lookup-only. Set to an explicitly empty string to disable."`
|
||||
}
|
||||
GenOpts struct {
|
||||
Force bool `env:"FORCE" short:"f" long:"force" description:"Force regenerating the TOTP seed/secret if already set."`
|
||||
Silent bool `env:"SHH" short:"s" long:"silent" description:"If specified, do not print the TOTP secret information; only store (-T/--totp-path)/dump (-q/--qr-dir) it."`
|
||||
QrDir string `env:"QRDIR" short:"q" long:"qr-dir" default:"qr_codes" description:"Use this directory to dump QR codes (relative paths are resolved to runtime's current working directory). The files will be named after the matched -e/--entities."`
|
||||
NoQr bool `env:"NOQR" short:"Q" long:"disable-qr" description:"If specified, do not dump QR codes to -q/--qr-dir."`
|
||||
PrintQr bool `env:"PQ" short:"p" long:"print-qr" description:"If specified, QR codes will be printed to the console. Overridden by -s/--silent."`
|
||||
EntityLookup map[string]string `env:"ENT" short:"e" long:"entity" required:"yes" description:"The lookup criteria for an entity. At least one MUST be provided. Takes the format of e.g. '-e \"<key>:<val>\"' where <key> is one of 'name', 'id', 'alias_id', or 'alias' and <val> is the matching criteria. (You are likely looking for '-e \"alias:<username>\"'). The entity must exist or already be created."`
|
||||
LookupOnly bool `env:"DRY" short:"l" long:"lookup-only" description:"If specified, only print the JSON object for the matched entity and exit. This can serve as both guidance for -T/--mnt-totp-path and to ensure you are matching the correct entity."`
|
||||
LookupFmt []string `env:"LFMT" short:"L" long:"lookup-fmt" choice:"dump" choice:"json" description:"The output format for the result of -l/--lookup-only, if specified. More than one may be specified. The default is 'json'."`
|
||||
}
|
||||
)
|
||||
44
cmd/user/consts.go
Normal file
44
cmd/user/consts.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/vault-client-go"
|
||||
vAPI "github.com/hashicorp/vault/api"
|
||||
"r00t2.io/goutils/logging"
|
||||
)
|
||||
|
||||
const (
|
||||
logFlags int = log.LstdFlags | log.Lmsgprefix
|
||||
logFlagsDebug int = logFlags | log.Llongfile
|
||||
)
|
||||
|
||||
var (
|
||||
logger *logging.MultiLogger
|
||||
args *Args = new(Args)
|
||||
ctx context.Context = context.Background()
|
||||
)
|
||||
|
||||
var (
|
||||
apiTok string
|
||||
apiUrl *url.URL
|
||||
api *vAPI.Client
|
||||
apiCfg *vAPI.Config
|
||||
apiTls *vAPI.TLSConfig
|
||||
client *vault.Client
|
||||
clientOpts []vault.ClientOption
|
||||
// clientCfg *vault.ClientConfiguration
|
||||
// reqOpts []vault.RequestOption
|
||||
)
|
||||
|
||||
var (
|
||||
totpUrl *url.URL
|
||||
authMntId string
|
||||
authMntAccessor string
|
||||
totpMethodId string
|
||||
totpMethod map[string]interface{}
|
||||
entity map[string]interface{}
|
||||
totpSeed map[string]interface{}
|
||||
)
|
||||
11
cmd/user/doc.go
Normal file
11
cmd/user/doc.go
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
vault_totp_user: Configure TOTP authentication for logging into Vault for Vault users/entities (Vault as a TOTP provider).
|
||||
|
||||
This allows to either [(re)set] TOTP for a Vault user (entity) or to [remove] the existing TOTP configuration for their user.
|
||||
Note that if an auth mount requires 2FA, removing the TOTP configuration does *not* remove the TOTP requirement for the user!
|
||||
They will not be able to log in without a valid TOTP code.
|
||||
|
||||
[(re)set]: https://developer.hashicorp.com/vault/api-docs/system/mfa/totp#administratively-generate-a-totp-mfa-secret
|
||||
[remove]: https://developer.hashicorp.com/vault/api-docs/system/mfa/totp#administratively-destroy-totp-mfa-secret
|
||||
*/
|
||||
package main
|
||||
666
cmd/user/funcs.go
Normal file
666
cmd/user/funcs.go
Normal file
@@ -0,0 +1,666 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
`encoding/base64`
|
||||
"encoding/json"
|
||||
`errors`
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
`maps`
|
||||
"net/url"
|
||||
"os"
|
||||
`path/filepath`
|
||||
"strings"
|
||||
`text/template`
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/vault-client-go"
|
||||
"github.com/hashicorp/vault-client-go/schema"
|
||||
vAPI "github.com/hashicorp/vault/api"
|
||||
"github.com/mdp/qrterminal/v3"
|
||||
"golang.org/x/term"
|
||||
"r00t2.io/goutils/stringsx"
|
||||
`r00t2.io/sysutils/paths`
|
||||
)
|
||||
|
||||
func configure() (err error) {
|
||||
|
||||
var buf []byte
|
||||
var apiSecret *vAPI.Secret
|
||||
var apiHealth *vAPI.HealthResponse
|
||||
var clientMap *vault.Response[map[string]interface{}]
|
||||
var clientHealth *vault.Response[schema.SealStatusResponse]
|
||||
|
||||
logger.Debug("vault_totp.configure: Configuring client(s).")
|
||||
|
||||
if apiUrl, err = url.Parse(args.VaultConn.URI); err != nil {
|
||||
logger.Err("vault_totp.configure: Received error parsing Vault URI '%s': %v", args.VaultConn.URI, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = getTok(&args.VaultConn.Token); err != nil {
|
||||
logger.Err("vault_totp.configure: Received error getting token: %v", err)
|
||||
return
|
||||
}
|
||||
args.Mounts.Auth = strings.TrimRight(args.Mounts.Auth, "/")
|
||||
if len(args.Gen.LookupFmt) == 0 {
|
||||
args.Gen.LookupFmt = []string{
|
||||
"json",
|
||||
"tpl_ref",
|
||||
}
|
||||
}
|
||||
|
||||
if !args.NoConfirm {
|
||||
buf = make([]byte, 1)
|
||||
fmt.Printf(
|
||||
"Do the following settings look correct? (Y/n)\n"+
|
||||
"\tURL: %s\n"+
|
||||
"\tToken (masked): %s\n"+
|
||||
"Y/n: ",
|
||||
apiUrl.String(),
|
||||
stringsx.Redact(apiTok, "*", 5, 5, false),
|
||||
)
|
||||
if _, err = fmt.Scanln(&buf); err != nil {
|
||||
if err.Error() == "unexpected newline" {
|
||||
err = nil
|
||||
} else {
|
||||
logger.Err("vault_totp.configure: Received error receiving configuration confirmation: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if buf[0] != 0x00 && strings.ToLower(string(buf[0])) != "y" {
|
||||
logger.Warning("vault_totp.configure: User deferred execution.")
|
||||
fmt.Println("Please re-run with any reconfiguration necessary. Quitting.")
|
||||
err = fmt.Errorf("user deferred")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
apiCfg = vAPI.DefaultConfig()
|
||||
apiCfg.Address = apiUrl.String()
|
||||
|
||||
clientOpts = []vault.ClientOption{
|
||||
vault.WithEnvironment(),
|
||||
vault.WithAddress(apiUrl.String()),
|
||||
}
|
||||
|
||||
if strings.ToLower(apiUrl.Scheme) == "https" {
|
||||
// I don't know why Vault/Hashicorp insists on using their own TLS configuration structs instead of crypto/tls.Config.
|
||||
// It's super lame and dumb and non-idiomatic.
|
||||
apiTls = &vAPI.TLSConfig{
|
||||
Insecure: args.VaultConn.Insecure,
|
||||
}
|
||||
if err = apiCfg.ConfigureTLS(apiTls); err != nil {
|
||||
logger.Err("vault_totp.configure: Received error setting API client TLS: %v", err)
|
||||
return
|
||||
}
|
||||
// And this is even MORE dumb. You can only set this during the client initialization.
|
||||
clientOpts = append(
|
||||
clientOpts,
|
||||
vault.WithTLS(
|
||||
vault.TLSConfiguration{
|
||||
InsecureSkipVerify: args.VaultConn.Insecure,
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
if api, err = vAPI.NewClient(apiCfg); err != nil {
|
||||
logger.Err("vault_totp.configure: Received error initializing API client: %v", err)
|
||||
return
|
||||
}
|
||||
api.SetToken(apiTok)
|
||||
|
||||
if client, err = vault.New(clientOpts...); err != nil {
|
||||
logger.Err("vault_totp.configure: Received error initializing client: %v", err)
|
||||
return
|
||||
}
|
||||
if err = client.SetToken(apiTok); err != nil {
|
||||
logger.Err("vault_totp.configure: Received error setting token on client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Health checks.
|
||||
if apiHealth, err = api.Sys().Health(); err != nil {
|
||||
logger.Err("vault_totp.configure: Received error getting health from API client: %v", err)
|
||||
return
|
||||
}
|
||||
if clientHealth, err = client.System.SealStatus(ctx); err != nil {
|
||||
logger.Err("vault_totp.configure: Received error getting health from client: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Debug("vault_totp.configure: API health:\n%s", spew.Sdump(apiHealth))
|
||||
logger.Debug("vault_totp.configure: Client health:\n%s", spew.Sdump(clientHealth))
|
||||
|
||||
// Token checks.
|
||||
if apiSecret, err = api.Auth().Token().LookupSelf(); err != nil {
|
||||
logger.Err("vault_totp.configure: Received error getting self token from API: %v", err)
|
||||
return
|
||||
}
|
||||
if clientMap, err = client.Auth.TokenLookUpSelf(ctx); err != nil {
|
||||
logger.Err("vault_totp.configure: Received error getting self token from client: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Debug("vault_totp.configure: API self token:\n%s", spew.Sdump(apiSecret))
|
||||
logger.Debug("vault_totp.configure: Client self token:\n%s", spew.Sdump(clientMap))
|
||||
|
||||
logger.Debug("vault_totp.configure: Finished configuring client(s).")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func genTotp() (err error) {
|
||||
|
||||
var maxKeyLen int
|
||||
var resp *vault.Response[map[string]interface{}]
|
||||
var req schema.MfaAdminGenerateTotpSecretRequest = schema.MfaAdminGenerateTotpSecretRequest{
|
||||
EntityId: entity["id"].(string),
|
||||
MethodId: totpMethodId,
|
||||
}
|
||||
|
||||
logger.Debug(
|
||||
"vault_totp.genTotp: Setting/resetting TOTP for entity ID '%s' on '%s' ('%s' on '%s')...",
|
||||
req.EntityId, req.MethodId,
|
||||
entity["aliases"].([]interface{})[0].(map[string]interface{})["name"], args.Mounts.Auth,
|
||||
)
|
||||
|
||||
if resp, err = client.Identity.MfaAdminGenerateTotpSecret(ctx, req); err != nil {
|
||||
logger.Err("vault_totp.genTotp: Received error (re-)generating TOTP secret: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
totpSeed = resp.Data
|
||||
|
||||
if totpSeed == nil || len(totpSeed) == 0 {
|
||||
// If there wasn't an error but the return's empty/nil, the TOTP exists.
|
||||
if args.Gen.Force {
|
||||
logger.Debug("vault_totp.genTotp: Clearing existing TOTP secret.")
|
||||
if resp, err = client.Identity.MfaAdminDestroyTotpSecret(
|
||||
ctx,
|
||||
schema.MfaAdminDestroyTotpSecretRequest{
|
||||
EntityId: entity["id"].(string),
|
||||
MethodId: totpMethodId,
|
||||
},
|
||||
); err != nil {
|
||||
logger.Err("vault_totp.genTotp: Received error clearing existing TOTP: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Debug("vault_totp.genTotp: Successfully cleared TOTP secret; received response %#v", resp)
|
||||
if err = genTotp(); err != nil {
|
||||
logger.Err("vault_totp.genTotp: Received error re-trying generating TOTP secret: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !args.Gen.Silent {
|
||||
fmt.Println("TOTP secret already exists and force was not enabled.")
|
||||
totpSeed = nil
|
||||
}
|
||||
logger.Debug("vault_totp.genTotp: TOTP secret existed and force was not enabled.")
|
||||
}
|
||||
} else {
|
||||
if totpUrl, err = url.Parse(totpSeed["url"].(string)); err != nil {
|
||||
logger.Err("vault_totp.genTotp: Received error parsing TOTP URL: %v", err)
|
||||
return
|
||||
}
|
||||
if !args.Gen.Silent {
|
||||
// Print it to STDOUT
|
||||
if args.Gen.PrintQr {
|
||||
// qrterminal.Generate(totpUrl.String(), qrterminal.H, os.Stdout)
|
||||
qrterminal.GenerateWithConfig(
|
||||
totpUrl.String(),
|
||||
qrterminal.Config{
|
||||
Level: qrterminal.L,
|
||||
Writer: os.Stdout,
|
||||
HalfBlocks: false,
|
||||
BlackChar: qrterminal.BLACK,
|
||||
BlackWhiteChar: qrterminal.BLACK_WHITE,
|
||||
WhiteChar: qrterminal.WHITE,
|
||||
WhiteBlackChar: qrterminal.WHITE_BLACK,
|
||||
QuietZone: 1,
|
||||
WithSixel: false,
|
||||
},
|
||||
)
|
||||
fmt.Println()
|
||||
}
|
||||
fmt.Printf("\n----\nTOTP:\t%s\n\n", strings.TrimPrefix(totpUrl.Path, "/"))
|
||||
// I hate that there's not an easier way to do this besides iterating twice.
|
||||
for k := range maps.Keys(totpUrl.Query()) {
|
||||
if len(k) > maxKeyLen {
|
||||
maxKeyLen = len(k)
|
||||
}
|
||||
}
|
||||
for k, v := range totpUrl.Query() {
|
||||
for _, s := range v {
|
||||
fmt.Printf("%-*s\t%s\n", maxKeyLen, k+":", s)
|
||||
}
|
||||
}
|
||||
fmt.Print("----\n\n")
|
||||
}
|
||||
if err = writeTotp(); err != nil {
|
||||
logger.Err("vault_totp.genTotp: Received error writing TOTP QR to persistent storage: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("vault_totp.genTotp: Generated TOTP.")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getAuthMnt() (err error) {
|
||||
|
||||
var ok bool
|
||||
var i interface{}
|
||||
var isl []interface{}
|
||||
var auths map[string]*vAPI.AuthMount
|
||||
var enforcements *vault.Response[schema.StandardListResponse]
|
||||
var enforcement *vault.Response[map[string]interface{}]
|
||||
// var totpList *vault.Response[schema.StandardListResponse]
|
||||
var totpResp *vault.Response[map[string]interface{}]
|
||||
var enforcedMfa map[string]struct{} = make(map[string]struct{})
|
||||
|
||||
logger.Debug("vault_totp.getAuthMnt: Fetching auth accessor for '%s'.", args.Mounts.Auth)
|
||||
|
||||
if auths, err = api.Sys().ListAuth(); err != nil {
|
||||
logger.Err("vault_totp.getAuthMnt: Received error getting list of enabled auths: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range auths {
|
||||
if strings.TrimRight(k, "/") != args.Mounts.Auth {
|
||||
continue
|
||||
}
|
||||
// It's not actually the accessor (v.Accessor), despite what some docs say, that the MFA uses.
|
||||
// But we need it for the entity lookup.
|
||||
authMntAccessor = v.Accessor
|
||||
// TOTP uses the UUID.
|
||||
authMntId = v.UUID
|
||||
logger.Debug("vault_totp.getAuthMnt: Found auth mount '%s':\n%s", args.Mounts.Auth, spew.Sdump(v))
|
||||
break
|
||||
}
|
||||
|
||||
// I think they intentionally make this a pain in the ass so you pay for Enterprise.
|
||||
if enforcements, err = client.Identity.MfaListLoginEnforcements(ctx); err != nil {
|
||||
logger.Err("vault_totp.getAuthMnt: Received error getting list of enabled TOTP enforcements: %v", err)
|
||||
return
|
||||
}
|
||||
for _, e := range enforcements.Data.Keys {
|
||||
if enforcement, err = client.Identity.MfaReadLoginEnforcement(ctx, e); err != nil {
|
||||
logger.Err("vault_totp.getAuthMnt: Received error reading MFA login enforcement '%s': %v", e, err)
|
||||
return
|
||||
}
|
||||
if i, ok = enforcement.Data["auth_method_accessors"]; !ok {
|
||||
continue
|
||||
}
|
||||
if isl, ok = i.([]interface{}); !ok {
|
||||
continue
|
||||
}
|
||||
for _, am := range isl {
|
||||
if authMntAccessor == am.(string) {
|
||||
logger.Debug(
|
||||
"vault_totp.getAuthMnt: Found enforcement for auth at mount '%s' ('%s'):\n%s",
|
||||
args.Mounts.Auth, authMntAccessor, spew.Sdump(am),
|
||||
)
|
||||
if isl, ok = enforcement.Data["mfa_method_ids"].([]interface{}); ok {
|
||||
for _, mMI := range isl {
|
||||
enforcedMfa[mMI.(string)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
if totpList, err = client.Identity.MfaListTotpMethods(ctx); err != nil {
|
||||
logger.Err("vault_totp.getAuthMnt: Received error getting list of enabled TOTP methods: %v", err)
|
||||
return
|
||||
}
|
||||
for _, t := range totpList.Data.Keys {
|
||||
*/
|
||||
for t := range enforcedMfa {
|
||||
if totpResp, err = client.Identity.MfaReadTotpMethod(ctx, t); err != nil {
|
||||
logger.Err("vault_totp.getAuthMnt: Received error reading TOTP method ID '%s': %v", t, err)
|
||||
return
|
||||
}
|
||||
totpMethod = totpResp.Data
|
||||
totpMethodId = t
|
||||
logger.Debug(
|
||||
"vault_totp.getAuthMnt: Found TOTP MFA method ID '%s' for '%s':\n%s",
|
||||
t, authMntAccessor, spew.Sdump(totpMethod),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
logger.Debug("vault_totp.getAuthMnt: Set auth ID to '%s' for auth mount '%s'.", authMntId, args.Mounts.Auth)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getEnt() (err error) {
|
||||
|
||||
var resp *vault.Response[map[string]interface{}]
|
||||
var req schema.EntityLookUpRequest
|
||||
|
||||
logger.Debug("vault_totp.getEnt: Fetching/finding entity.")
|
||||
|
||||
if err = getAuthMnt(); err != nil {
|
||||
logger.Err("vault_totp.getEnt: Received error fetching auth mount accessor: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range args.Gen.EntityLookup {
|
||||
switch k {
|
||||
case "name":
|
||||
req.Name = v
|
||||
case "id":
|
||||
req.Id = v
|
||||
case "alias":
|
||||
req.AliasName = v
|
||||
req.AliasMountAccessor = authMntAccessor
|
||||
case "alias_id":
|
||||
req.AliasId = v
|
||||
}
|
||||
}
|
||||
|
||||
if resp, err = client.Identity.EntityLookUp(ctx, req); err != nil {
|
||||
logger.Err("vault_totp.getEnt: Received error looking up entity with %#v: %v", req, err)
|
||||
return
|
||||
}
|
||||
|
||||
entity = resp.Data
|
||||
|
||||
logger.Debug("vault_totp.getEnt: Found entity:\n%s", spew.Sdump(entity))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getTok(tok *string) (err error) {
|
||||
|
||||
var b []byte
|
||||
var b2 []byte
|
||||
var state *term.State
|
||||
var fd int = int(os.Stdin.Fd())
|
||||
|
||||
if tok != nil && *tok != "" {
|
||||
apiTok = *tok
|
||||
return
|
||||
}
|
||||
|
||||
if state, err = term.GetState(fd); err != nil {
|
||||
logger.Debug("vault_totp.getTok: Received error getting current state: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
var tErr error
|
||||
if state != nil {
|
||||
if tErr = term.Restore(fd, state); tErr != nil {
|
||||
logger.Err("vault_totp.getTok: Received error restoring terminal state: %v", tErr)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}()
|
||||
|
||||
fmt.Print("Please enter your Vault token (will not echo back): ")
|
||||
if b, err = term.ReadPassword(fd); err != nil {
|
||||
logger.Err("vault_totp.getTok: Received error reading Vault token (initial): %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Print("\nConfirm: ")
|
||||
if b2, err = term.ReadPassword(fd); err != nil {
|
||||
logger.Err("vault_totp.getTok: Received error reading Vault token (confirm): %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Println()
|
||||
if !bytes.Equal(b, b2) {
|
||||
err = fmt.Errorf(
|
||||
"original token (CRC32 %x, length %d) "+
|
||||
"does not match confirmation (CRC32 %x, length %d)",
|
||||
crc32.ChecksumIEEE(b), len(string(b)),
|
||||
crc32.ChecksumIEEE(b2), len(string(b2)),
|
||||
)
|
||||
logger.Err("vault_totp.getTok: Error when confirming token: %v")
|
||||
return
|
||||
}
|
||||
|
||||
apiTok = string(b)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func printEntity() (err error) {
|
||||
|
||||
var b []byte
|
||||
var rendered bool
|
||||
var sb strings.Builder
|
||||
var seen map[string]struct{} = make(map[string]struct{})
|
||||
|
||||
sb.WriteString("----\n")
|
||||
for _, i := range args.Gen.LookupFmt {
|
||||
if _, rendered = seen[i]; rendered {
|
||||
continue
|
||||
}
|
||||
switch i {
|
||||
case "dump":
|
||||
spew.Fdump(os.Stdout, entity)
|
||||
case "json":
|
||||
if b, err = json.MarshalIndent(entity, "", " "); err != nil {
|
||||
logger.Err("vault_totp.printEntity: Received error rendering to JSON: %v", err)
|
||||
return
|
||||
}
|
||||
sb.Write(b)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
sb.WriteString("\n----\n")
|
||||
seen[i] = struct{}{}
|
||||
}
|
||||
|
||||
fmt.Print(sb.String())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func renderVPath() (mnt, vpath string, err error) {
|
||||
|
||||
var sl []string
|
||||
var buf bytes.Buffer
|
||||
var tpl *template.Template = template.New("").Option("missingkey=error")
|
||||
|
||||
logger.Debug("vault_totp.renderVPath: Rendering path for entity TOTP storage.")
|
||||
|
||||
if args.Mounts.TotpPath == "" {
|
||||
logger.Warning("vault_totp.renderVPath: TOTP persistent Vault storage disabled.")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = tpl.Parse(args.Mounts.TotpPath); err != nil {
|
||||
logger.Err("vault_totp.renderVPath: Error parsing template: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = tpl.Execute(&buf, entity); err != nil {
|
||||
logger.Err("vault_totp.renderVPath: Error executing template: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
sl = strings.SplitN(buf.String(), ":", 2)
|
||||
|
||||
switch len(sl) {
|
||||
case 0:
|
||||
err = fmt.Errorf("no mount and/or path; empty TOTP pathspec")
|
||||
return
|
||||
case 1:
|
||||
err = fmt.Errorf("only mount or path specified; invalid TOTP pathspec")
|
||||
return
|
||||
}
|
||||
if sl[0] == "" {
|
||||
err = fmt.Errorf("no mount specified; invalid TOTP pathspec")
|
||||
return
|
||||
}
|
||||
if sl[1] == "" {
|
||||
err = fmt.Errorf("no path specified; invalid TOTP pathspec")
|
||||
return
|
||||
}
|
||||
|
||||
mnt = sl[0]
|
||||
vpath = sl[1]
|
||||
|
||||
logger.Debug("vault_totp.renderVPath: Rendered vpath for TOTP: '%s:%s'", mnt, vpath)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func writeTotp() (err error) {
|
||||
|
||||
var b []byte
|
||||
var fnm string
|
||||
|
||||
logger.Debug("vault_totp.writeTotp: Writing TOTP to file/Vault.")
|
||||
|
||||
if totpSeed == nil {
|
||||
logger.Debug("vault_totp.writeTotp: No TOTP seed/secret; NO-OP.")
|
||||
return
|
||||
}
|
||||
|
||||
if !args.Gen.NoQr {
|
||||
if err = paths.RealPath(&args.Gen.QrDir); err != nil {
|
||||
logger.Err("vault_totp.writeTotp: Received error canonizing QR directory '%s': %v", args.Gen.QrDir, err)
|
||||
return
|
||||
}
|
||||
if err = os.MkdirAll(args.Gen.QrDir, 0o0700); err != nil {
|
||||
logger.Err("vault_totp.writeTotp: Received error creating QR directory '%s': %v", args.Gen.QrDir, err)
|
||||
return
|
||||
}
|
||||
fnm = filepath.Join(args.Gen.QrDir, fmt.Sprintf("totp_%s.png", entity["id"].(string)))
|
||||
// wildly misnamed field
|
||||
if b, err = base64.StdEncoding.DecodeString(totpSeed["barcode"].(string)); err != nil {
|
||||
logger.Err("vault_totp.writeTotp: Received error decoding QR code base64: %v", err)
|
||||
return
|
||||
}
|
||||
if err = os.WriteFile(fnm, b, 0o0600); err != nil {
|
||||
logger.Err("vault_totp.writeTotp: Received error writing QR code to disk: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(args.Mounts.TotpPath) != "" {
|
||||
if err = writeTotpVault(); err != nil {
|
||||
logger.Err("vault_totp.writeTotp: Received error writing TOTP seed/secret to Vault: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("vault_totp.writeTotp: Wrote TOTP to persistent storage (if specified/configured).")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func writeTotpVault() (err error) {
|
||||
|
||||
// var b []byte
|
||||
var mnt string
|
||||
var vpath string
|
||||
var baseUri *url.URL
|
||||
var uiItemUri *url.URL
|
||||
var apiItemUri *url.URL
|
||||
var presp *vAPI.KVSecret
|
||||
var dat map[string]interface{}
|
||||
var datUri map[string]interface{}
|
||||
var rresp *vault.Response[schema.KvV2ReadResponse]
|
||||
var wresp *vault.Response[schema.KvV2WriteResponse]
|
||||
var respErr *vault.ResponseError = new(vault.ResponseError)
|
||||
var params []vault.RequestOption = make([]vault.RequestOption, 0)
|
||||
|
||||
logger.Debug("vault_totp.writeTotpVault: Rendering path for entity TOTP storage.")
|
||||
|
||||
if mnt, vpath, err = renderVPath(); err != nil {
|
||||
logger.Err("vault_totp.writeTotpVault: Received error rendering Vault path: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if baseUri, err = url.Parse(api.Address()); err != nil {
|
||||
logger.Err("vault_totp.writeTotpVault: Received error parsing Vault non-specialized base URI: %v", err)
|
||||
return
|
||||
}
|
||||
uiItemUri = baseUri.JoinPath("ui", "vault", "secrets", mnt, "kv", url.PathEscape(vpath))
|
||||
apiItemUri = baseUri.JoinPath("v1", mnt, "data", url.PathEscape(vpath))
|
||||
|
||||
dat = map[string]interface{}{
|
||||
"uri_str": totpUrl.String(),
|
||||
"qr_b64": totpSeed["barcode"].(string),
|
||||
}
|
||||
datUri = make(map[string]interface{})
|
||||
for k, v := range totpUrl.Query() {
|
||||
datUri[k] = v[0] // I don't think the otp:// URLs have multi-val query params...
|
||||
}
|
||||
dat["uri"] = datUri
|
||||
|
||||
params = append(
|
||||
params,
|
||||
vault.WithMountPath(mnt),
|
||||
)
|
||||
|
||||
if rresp, err = client.Secrets.KvV2Read(ctx, vpath, params...); err != nil {
|
||||
if errors.As(err, &respErr) {
|
||||
if respErr.StatusCode != 404 {
|
||||
spew.Dump(err)
|
||||
logger.Err("vault_totp.writeTotpVault: Received error reading Vault secret at mount '%s' path '%s': %v", mnt, vpath, err)
|
||||
return
|
||||
} else {
|
||||
err = nil
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if rresp != nil && rresp.Data.Data != nil {
|
||||
/*
|
||||
// Yeah this is dumb. This is what happens when you try to do too much magic and try to "protect" developers from themselves.
|
||||
// The proper solution is we need smarter developers, not more Fisher-Price Babby's First Code nonsense.
|
||||
if b, err = json.Marshal(dat); err != nil {
|
||||
logger.Err("vault_totp.writeTotpVault: Received error with merge encoding of existing data: %v", err)
|
||||
return
|
||||
}
|
||||
dat = rresp.Data.Data
|
||||
if err = json.Unmarshal(b, &dat); err != nil {
|
||||
logger.Err("vault_totp.writeTotpVault: Received error while unmarshaling merge data: %v", err)
|
||||
return
|
||||
}
|
||||
*/
|
||||
// We'll do a patch instead (https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#patch-secret).
|
||||
// Fuck it.
|
||||
if presp, err = api.KVv2(mnt).Patch(ctx, vpath, dat); err != nil {
|
||||
logger.Err("vault_totp.writeTotpVault: Received error patching/updating secret '%s' on mount '%s': %v", vpath, mnt, err)
|
||||
return
|
||||
}
|
||||
logger.Debug("vault_totp.writeTotpVault: Updated secret '%s:%s':\n%s", mnt, vpath, spew.Sdump(presp))
|
||||
} else {
|
||||
if wresp, err = client.Secrets.KvV2Write(
|
||||
ctx,
|
||||
vpath,
|
||||
schema.KvV2WriteRequest{
|
||||
Data: dat,
|
||||
},
|
||||
params...,
|
||||
); err != nil {
|
||||
logger.Err("vault_totp.writeTotpVault: Received error creating new secret '%s' on mount '%s': %v", vpath, mnt, err)
|
||||
return
|
||||
}
|
||||
logger.Debug("vault_totp.writeTotpVault: Wrote secret '%s:%s':\n%s", mnt, vpath, spew.Sdump(wresp))
|
||||
}
|
||||
|
||||
if !args.Gen.Silent {
|
||||
fmt.Printf(
|
||||
"TOTP stored at %s:%s\n\n"+
|
||||
"Web UI URL:\t%s\n"+
|
||||
"API URL:\t%s\n",
|
||||
mnt, vpath,
|
||||
uiItemUri.String(),
|
||||
apiItemUri.String(),
|
||||
)
|
||||
}
|
||||
|
||||
logger.Debug("vault_totp.writeTotpVault: Wrote TOTP seed/secret to Vault.")
|
||||
|
||||
return
|
||||
}
|
||||
64
cmd/user/main.go
Normal file
64
cmd/user/main.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/jessevdk/go-flags"
|
||||
`r00t2.io/vault_totp/internal`
|
||||
)
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
var parser *flags.Parser = flags.NewParser(args, flags.Default)
|
||||
|
||||
parser.NamespaceDelimiter = "-"
|
||||
parser.EnvNamespaceDelimiter = "_"
|
||||
|
||||
if _, err = parser.Parse(); err != nil {
|
||||
switch {
|
||||
case errors.As(err, &flagsErr):
|
||||
switch {
|
||||
case errors.Is(flagsErr.Type, flags.ErrHelp), errors.Is(flagsErr.Type, flags.ErrCommandRequired), errors.Is(flagsErr.Type, flags.ErrRequired): // These print their relevant messages by themselves.
|
||||
return
|
||||
default:
|
||||
log.Panicln(err)
|
||||
}
|
||||
default:
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
|
||||
defer internal.Logger.Shutdown()
|
||||
|
||||
logger.Debug("Initialized with args:\n%v", spew.Sdump(args))
|
||||
|
||||
logger.Debug("main: Configuring.")
|
||||
if err = configure(); err != nil {
|
||||
logger.Err("main: Received error while configuring: %v", err)
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
logger.Debug("main: Fetching entity.")
|
||||
if err = getEnt(); err != nil {
|
||||
logger.Err("main: Received error while fetching entity: %v", err)
|
||||
log.Panicln(err)
|
||||
}
|
||||
if args.Gen.LookupOnly {
|
||||
if err = printEntity(); err != nil {
|
||||
logger.Err("main: Received error while rendering entity out: %v", err)
|
||||
log.Panicln(err)
|
||||
}
|
||||
logger.Debug("main: Done.")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("main: Generating TOTP seed/secret.")
|
||||
if err = genTotp(); err != nil {
|
||||
logger.Err("main: Received error while generating TOTP secret: %v", err)
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
logger.Debug("main: Done.")
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
`update_vault_totp/internal`
|
||||
|
||||
sysdUtil "github.com/coreos/go-systemd/v22/util"
|
||||
`github.com/jessevdk/go-flags`
|
||||
`r00t2.io/goutils/logging`
|
||||
)
|
||||
|
||||
var (
|
||||
isSystemd bool = sysdUtil.IsRunningSystemd()
|
||||
)
|
||||
|
||||
const (
|
||||
logFlags int = log.LstdFlags | log.Lmsgprefix
|
||||
logFlagsDebug int = logFlags | log.Llongfile
|
||||
)
|
||||
|
||||
var (
|
||||
logger *logging.MultiLogger
|
||||
args *internal.Args = new(internal.Args)
|
||||
logFlagsRuntime int = logFlags
|
||||
parser *flags.Parser = flags.NewParser(args, flags.Default)
|
||||
)
|
||||
@@ -1,72 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`fmt`
|
||||
`log`
|
||||
`os`
|
||||
|
||||
`update_vault_totp/version`
|
||||
|
||||
`github.com/davecgh/go-spew/spew`
|
||||
`github.com/jessevdk/go-flags`
|
||||
`r00t2.io/goutils/logging`
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
parser.EnvNamespaceDelimiter = ""
|
||||
if _, err = parser.Parse(); err != nil {
|
||||
var flagsErr *flags.Error
|
||||
switch {
|
||||
case errors.As(err, &flagsErr):
|
||||
switch {
|
||||
case errors.Is(flagsErr.Type, flags.ErrHelp), errors.Is(flagsErr.Type, flags.ErrCommandRequired), errors.Is(flagsErr.Type, flags.ErrRequired): // These print their relevant messages by themselves.
|
||||
return
|
||||
default:
|
||||
log.Panicln(err)
|
||||
}
|
||||
default:
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
|
||||
if version.Ver, err = version.Version(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
// If args.Version or args.DetailVersion are true, just print them and exit.
|
||||
if args.DetailVersion || args.Version {
|
||||
if args.Version {
|
||||
fmt.Println(version.Ver.Short())
|
||||
return
|
||||
} else if args.DetailVersion {
|
||||
fmt.Println(version.Ver.Detail())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// We want to set up logging before anything else.
|
||||
if args.DoDebug {
|
||||
logFlagsRuntime = logFlagsDebug
|
||||
}
|
||||
logger = logging.GetMultiLogger(args.DoDebug, "ZorkBot")
|
||||
if err = logger.AddDefaultLogger(
|
||||
"default",
|
||||
logFlagsRuntime,
|
||||
"/var/log/zorkbot/zorkbot.log", "~/logs/zorkbot.log",
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if err = logger.Setup(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
defer logger.Shutdown()
|
||||
logger.Info("main: ZorkBot version %v", version.Ver.Short())
|
||||
logger.Debug("main: ZorkBot version (extended):\n%v", version.Ver.Detail())
|
||||
logger.Debug("main: Invocation args:\n%s", spew.Sdump(args))
|
||||
}
|
||||
34
common/consts.go
Normal file
34
common/consts.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
`os`
|
||||
`syscall`
|
||||
`time`
|
||||
)
|
||||
|
||||
const (
|
||||
/*
|
||||
DefaultTimePeriod is the default time period for TOTP.
|
||||
|
||||
See [RFC 6238 § 4.1], [RFC 6238 § 4.1], [RFC 6238 § 6], and [RFC 6238 Appendix A].
|
||||
|
||||
[RFC 6238 § 4.1]: https://datatracker.ietf.org/doc/html/rfc6238#section-4.1
|
||||
[RFC 6238 § 5.2]: https://datatracker.ietf.org/doc/html/rfc6238#section-5.2
|
||||
[RFC 6238 § 6]: https://datatracker.ietf.org/doc/html/rfc6238#section-6
|
||||
[RFC 6238 Appendix A]: https://datatracker.ietf.org/doc/html/rfc6238#appendix-A
|
||||
*/
|
||||
DefaultTimePeriod time.Duration = time.Second * 30
|
||||
|
||||
// VaultRepl is used to replace any invalid characters in a name.
|
||||
VaultRepl rune = '_'
|
||||
)
|
||||
|
||||
var (
|
||||
// ProgEndSigs are used to trap signals and gracefully quit. Note that these don't really have a Windows equivalent.
|
||||
ProgEndSigs []os.Signal = []os.Signal{
|
||||
os.Interrupt, // SIGINT, signal 2 (ctrl+c)
|
||||
syscall.SIGQUIT, // signal 3 (ctrl + \\) (SIGINT but with coredump)
|
||||
os.Kill, // SIGKILL, signal 9 (useless to capture since the program can't ignore/trap it)
|
||||
syscall.SIGTERM, // signal 15 (`kill` default)
|
||||
}
|
||||
)
|
||||
145
common/funcs.go
Normal file
145
common/funcs.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
`math`
|
||||
`time`
|
||||
)
|
||||
|
||||
/*
|
||||
CurrentTimeStep returns the current time step using the given
|
||||
[time.Duration] `period`.
|
||||
|
||||
If `period` is equal to time.Duration(0), it will be set to [DefaultTimePeriod].
|
||||
*/
|
||||
func CurrentTimeStep(period time.Duration) (ts int64) {
|
||||
return TimeStepFromTime(time.Now(), period)
|
||||
}
|
||||
|
||||
/*
|
||||
OffsetTimeStep is like [CurrentTimeStep] but returns the time step next/previous
|
||||
to the current specified by `offset` (`offset` may be negative or positive).
|
||||
*/
|
||||
func OffsetTimeStep(period time.Duration, offset int64) (ts int64) {
|
||||
|
||||
var offsetDuration time.Duration
|
||||
|
||||
period = normalizePeriod(period)
|
||||
|
||||
offsetDuration = period * time.Duration(offset)
|
||||
|
||||
ts = TimeStepFromTime(time.Now().Add(offsetDuration), period)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
OffsetTimeStepToTime is like [TimeStepToTime] but returns the next/previous
|
||||
time range in relation to time step `ts` specified by `offset` (`offset` may be negative or positive).
|
||||
*/
|
||||
func OffsetTimeStepToTime(ts int64, period time.Duration, offset int64) (start, end time.Time) {
|
||||
return TimeStepToTime(ts+offset, period)
|
||||
}
|
||||
|
||||
/*
|
||||
TimeStepFromTime returns a time step (see [RFC 6238 § 1.2], [RFC 6238 § 5.2])
|
||||
from a provided generation time `t` as a [time.Time] and a life length
|
||||
`period` as a [time.Duration].
|
||||
|
||||
If `period` is equal to time.Duration(0), it will be set to [DefaultTimePeriod].
|
||||
|
||||
[RFC 6238 § 1.2]: https://datatracker.ietf.org/doc/html/rfc6238#section-1.2
|
||||
[RFC 6238 § 5.2]: https://datatracker.ietf.org/doc/html/rfc6238#section-5.2
|
||||
*/
|
||||
func TimeStepFromTime(t time.Time, period time.Duration) (ts int64) {
|
||||
|
||||
period = normalizePeriod(period)
|
||||
|
||||
ts = int64(math.Floor(float64(t.Unix()) / period.Seconds()))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TimeStepToTime returns a [time.Time] `start` and `end` from a time step `ts`
|
||||
and a lifetime period `period` (as a [time.Duration]).
|
||||
|
||||
If `period` is equal to time.Duration(0), it will be set to [DefaultTimePeriod].
|
||||
*/
|
||||
func TimeStepToTime(ts int64, period time.Duration) (start, end time.Time) {
|
||||
|
||||
var fromEpoch time.Duration
|
||||
|
||||
period = normalizePeriod(period)
|
||||
fromEpoch = (time.Second * time.Duration(ts*int64(period.Seconds()))).Truncate(time.Second)
|
||||
|
||||
start = time.Unix(0, 0).Add(fromEpoch).Truncate(time.Second)
|
||||
end = start.Add(period).Truncate(time.Second)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// VaultEscape is used to normalize a string to a Vault-safe name (for e.g. secrets).
|
||||
func VaultEscape(s *string) {
|
||||
|
||||
var c rune
|
||||
var idx int
|
||||
var last rune
|
||||
var norm []rune
|
||||
var reduced []rune = make([]rune, 0)
|
||||
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
norm = make([]rune, 0, len(*s))
|
||||
|
||||
// https://github.com/hashicorp/vault/issues/5645#issuecomment-434404750
|
||||
// It seems they relaxed this a bit since then, as hyphens and periods are also allowed.
|
||||
// https://github.com/hashicorp/vault/blob/main/sdk/framework/path.go#L26
|
||||
// At time of writing, the regex is: (?P<name>\\w(([\\w-.@]+)?\\w)?)
|
||||
// Per comments: "alphanumeric characters along with -, . and @."
|
||||
for _, c = range *s {
|
||||
// If it's "safe" chars, it's fine
|
||||
if (c == '-' || c == '.') || // 0x2d, 0x2e
|
||||
(c >= '0' && c <= '9') || // 0x30 to 0x39
|
||||
(c == '@') || // 0x40
|
||||
(c >= 'A' && c <= 'Z') || // 0x41 to 0x5a
|
||||
(c == '_') || // 0x5f
|
||||
(c >= 'a' && c <= 'z') { // 0x61 to 0x7a
|
||||
norm = append(norm, c)
|
||||
continue
|
||||
}
|
||||
// Otherwise normalize it to a safe char
|
||||
norm = append(norm, VaultRepl)
|
||||
}
|
||||
|
||||
// And remove repeating sequential replacers.
|
||||
for idx, c = range norm[:] {
|
||||
if idx == 0 {
|
||||
last = c
|
||||
reduced = append(reduced, c)
|
||||
continue
|
||||
}
|
||||
if c == last && last == VaultRepl {
|
||||
continue
|
||||
}
|
||||
reduced = append(reduced, c)
|
||||
last = c
|
||||
}
|
||||
|
||||
*s = string(reduced)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// normalizedPeriod returns `period` if non-zero, otherwise [DefaultTimePeriod].
|
||||
func normalizePeriod(period time.Duration) (normalized time.Duration) {
|
||||
|
||||
if period != time.Duration(0) {
|
||||
normalized = period
|
||||
return
|
||||
}
|
||||
|
||||
normalized = DefaultTimePeriod
|
||||
|
||||
return
|
||||
}
|
||||
11
errs/errors.go
Normal file
11
errs/errors.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package errs
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNilVault error = errors.New("provided VaultArgs is nil")
|
||||
ErrNoCfg error = errors.New("no TOTP configuration found")
|
||||
ErrNoCode error = errors.New("no code found returned")
|
||||
)
|
||||
53
go.mod
53
go.mod
@@ -1,31 +1,72 @@
|
||||
module update_vault_totp
|
||||
module r00t2.io/vault_totp
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/chelnak/ysmrr v0.6.0
|
||||
github.com/creachadair/otp v0.5.2
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/go-playground/validator/v10 v10.29.0
|
||||
github.com/hashicorp/vault-client-go v0.4.3
|
||||
github.com/hashicorp/vault/api v1.22.0
|
||||
github.com/jessevdk/go-flags v1.6.1
|
||||
golang.org/x/mod v0.23.0
|
||||
github.com/makiuchi-d/gozxing v0.1.1
|
||||
github.com/mdp/qrterminal/v3 v3.2.1
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/pterm/pterm v0.12.82
|
||||
golang.org/x/mod v0.31.0
|
||||
golang.org/x/term v0.38.0
|
||||
r00t2.io/gosecret v1.1.5
|
||||
r00t2.io/goutils v1.13.0
|
||||
r00t2.io/sysutils v1.15.0
|
||||
r00t2.io/goutils v1.14.0
|
||||
r00t2.io/sysutils v1.15.1
|
||||
)
|
||||
|
||||
// https://github.com/chelnak/ysmrr/pull/88
|
||||
replace github.com/chelnak/ysmrr v0.6.0 => /opt/dev/third-party/ysmrr
|
||||
|
||||
require (
|
||||
atomicgo.dev/cursor v0.2.0 // indirect
|
||||
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||
atomicgo.dev/schedule v0.1.0 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/containerd/console v1.0.5 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
|
||||
github.com/creachadair/wirepb v0.0.0-20251201055919-954ef89c4c71 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
|
||||
193
go.sum
193
go.sum
@@ -1,66 +1,225 @@
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=
|
||||
atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=
|
||||
atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=
|
||||
atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
|
||||
atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
|
||||
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
|
||||
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
|
||||
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
|
||||
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
|
||||
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
|
||||
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
|
||||
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
|
||||
github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
|
||||
github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
|
||||
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
||||
github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
|
||||
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
|
||||
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
|
||||
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||
github.com/creachadair/mds v0.25.2 h1:xc0S0AfDq5GX9KUR5sLvi5XjA61/P6S5e0xFs1vA18Q=
|
||||
github.com/creachadair/mds v0.25.2/go.mod h1:+s4CFteFRj4eq2KcGHW8Wei3u9NyzSPzNV32EvjyK/Q=
|
||||
github.com/creachadair/otp v0.5.2 h1:3krOkIHA8X1YAnv9lGZi2NlyCZepcsAQkZwKGK5L+9c=
|
||||
github.com/creachadair/otp v0.5.2/go.mod h1:0cUM4kl9cQmePbIEy1J1O+esh+S3zDM9s2Foo71KzKA=
|
||||
github.com/creachadair/wirepb v0.0.0-20251201055919-954ef89c4c71 h1:H3EV2OFbtKgFAcxGJiLTNar9krJ9yfsgpE37/QY9vN8=
|
||||
github.com/creachadair/wirepb v0.0.0-20251201055919-954ef89c4c71/go.mod h1:dHvCVZSsk3Y/fhY0mUex0U2XBZszomOe7PzlghOtHnQ=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk=
|
||||
github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/godbus/dbus/v5 v5.2.1 h1:I4wwMdWSkmI57ewd+elNGwLRf2/dtSaFz1DujfWYvOk=
|
||||
github.com/godbus/dbus/v5 v5.2.1/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||
github.com/hashicorp/vault-client-go v0.4.3 h1:zG7STGVgn/VK6rnZc0k8PGbfv2x/sJExRKHSUg3ljWc=
|
||||
github.com/hashicorp/vault-client-go v0.4.3/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY=
|
||||
github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0=
|
||||
github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM=
|
||||
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
|
||||
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I=
|
||||
github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
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.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
|
||||
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
|
||||
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
|
||||
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
|
||||
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
|
||||
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
|
||||
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
|
||||
github.com/pterm/pterm v0.12.82 h1:+D9wYhCaeaK0FIQoZtqbNQuNpe2lB2tajKKsTd5paVQ=
|
||||
github.com/pterm/pterm v0.12.82/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
r00t2.io/gosecret v1.1.5 h1:pmlsCR8VKeI9+BN7uvUqThP/BtgoSch580XOWin+qH4=
|
||||
r00t2.io/gosecret v1.1.5/go.mod h1:24jo8x5lOmWAJZJZ1J3FxhoT9+KOXSZ5pQWWzFdvdtM=
|
||||
r00t2.io/goutils v1.13.0 h1:103QR2eUu42TNF7h9r6YgahbSgRAJ8VtFxSzw9rMaXI=
|
||||
r00t2.io/goutils v1.13.0/go.mod h1:/0M+6fW2VAgdtAvwGE9oNV259MoEZeJ84rLCZsxe8uI=
|
||||
r00t2.io/sysutils v1.15.0 h1:FSnREfbXDhBQEO7LMpnRQeKlPshozxk9XHw3YgWRgRg=
|
||||
r00t2.io/sysutils v1.15.0/go.mod h1:28qB0074EIRQ8Sy/ybaA5jC3qA32iW2aYLkMCRhyAFM=
|
||||
r00t2.io/goutils v1.14.0 h1:mvEiJLrTy/hx7ZX2TzCm/y0be2TcTu822m++qsSQGLc=
|
||||
r00t2.io/goutils v1.14.0/go.mod h1:68jkIl/QYxEEVmVz8k1a7QI9vAA4faQUIcjSHSbtgHw=
|
||||
r00t2.io/sysutils v1.15.1 h1:0EVZZAxTFqQN6jjfjqUKkXye0LMshUA5MO7l3Wd6wH8=
|
||||
r00t2.io/sysutils v1.15.1/go.mod h1:T0iOnaZaSG5NE1hbXTqojRZc0ia/u8TB73lV7zhMz58=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
package internal
|
||||
|
||||
type (
|
||||
Args struct {
|
||||
Version bool `short:"v" long:"version" description:"Print the version and exit."`
|
||||
DetailVersion bool `short:"V" long:"detail" description:"Print detailed version info and exit."`
|
||||
DoDebug bool `env:"VTOTP_DEBUG" short:"d" long:"debug" description:"If specified, enable debug logging. This may log potentially sensitive information, so be careful."`
|
||||
VaultToken string `env:"VAULT_TOKEN" short:"t" long:"vault-token" description:"Vault token to use. If empty/not defined, you will be securely prompted."`
|
||||
VaultAddr string `env:"VAULT_ADDR" short:"a" long:"vault-addr" default:"https://clandestine.r00t2.io/" description:"Vault address to use."`
|
||||
QrImgPath string `env:"VTOTP_QR" short:"q" long:"qr-img" description:"Path to QR image to extract OTPAuth URLs from. Either -q/--qr-img, -f/--otp-file, -u/--otp-url, and/or -x/--explicit must be specified." validate:"required_without=OtpFile OtpUrl OtpExplicit,filepath"`
|
||||
OtpFile string `env:"VTOTP_FILE" short:"f" long:"otp-file" description:"Path to file containing OTPAuth URLs in plaintext, one per line. Either -q/--qr-img, -f/--otp-file, -u/--otp-url, and/or -x/--explicit must be specified." validate:"required_without=QrImgPath OtpUrl OtpExplicit,filepath"`
|
||||
OtpUrl string `env:"VTOTP_URL" short:"u" long:"otp-url" description:"Explicit OTPAuth URL. Either -q/--qr-img, -f/--otp-file, -u/--otp-url, and/or -x/--explicit must be specified." validate:"required_without=QrImgPath OtpFile OtpExplicit,url"`
|
||||
OtpExplicit bool `short:"x" long:"otp-explicit" description:"If specified, use the explicit OTP specification under the EXPLICIT TOTP group."`
|
||||
ExplicitOtp ExplicitOtp `group:"EXPLICIT TOTP" env-namespace:"VTOTP_X"`
|
||||
CommonArgs struct {
|
||||
Version bool `short:"v" long:"version" description:"Print the version and exit."`
|
||||
DetailVersion bool `short:"V" long:"detail" description:"Print detailed version info and exit."`
|
||||
DoDebug bool `env:"VTOTP_DEBUG" short:"d" long:"debug" description:"If specified, enable debug logging. This may log potentially sensitive information, so be careful."`
|
||||
VaultArgs `group:"Common Vault Options" env-namespace:"VAULT" namespace:"vault"`
|
||||
}
|
||||
ExplicitOtp struct {
|
||||
Type string `env:"TYP" short:"y" long:"type" choice:"totp" choice:"hotp" default:"totp" hidden:"true" description:"The OTP type." validate:"required_with=OtpExplicit,oneof=totp hotp"`
|
||||
Counter uint64 `env:CTR" short:"c" long:"counter" hidden:"true" description:"The initial counter value (if -y/--type='hotp')." validate:"required_with=OtpExplicit,required_if=Type hotp"`
|
||||
Account string `env:"ACCT" short:"n" long:"name" description:"Name of the TOTP account (should be just the username)." validate:"required_with=OtpExplicit"`
|
||||
Issuer string `env:"ISS" short:"i" long:"issuer" description:"Issuer of the TOTP (this is generally the service name you're authing to)." validate:"required_with=OtpExplicit"`
|
||||
Secret string `env:"SSKEY" short:"s" long:"shared-secret" description:"The shared secret key in Base32 string format (with no padding)." validate:"required_with=OtpExplicit,base32"`
|
||||
Algorithm string `env:"ALGO" short:"g" long:"algo" choice:"md5" choice:"sha1" choice:"sha256" choice:"sha512" description:"The hashing/checksum algorithm." validate:"required_with=OtpExplicit,oneof=md5 sha1 sha256 sha512"`
|
||||
Digits int `env:"DIG" short:"l" long:"digits" choice:"6" choice:"8" description:"Number of digits for the generated code." validate:"required_with=OtpExplicit,oneof=6 8"`
|
||||
Period time.Duration `env:TIME" short:"p" long:"period" default:"30s" description:"The period that a generated code is valid for." validate:"required_with=OtpExplicit,required_if=Type totp"`
|
||||
VaultArgs struct {
|
||||
Insecure bool `env:"SKIP_VERIFY" short:"S" long:"insecure" description:"If specified and -u/--uri is using HTTPS, do not require TLS verification (self-signed certificates, etc.)"`
|
||||
Token string `env:"TOKEN" short:"t" long:"token" description:"Vault token to use. If empty/not defined, you will be securely prompted."`
|
||||
Addr string `env:"ADDR" short:"a" long:"addr" default:"https://clandestine.r00t2.io/" description:"Vault address to use."`
|
||||
SniName *string `env:"TLS_SERVER_NAME" short:"S" long:"sni" description:"If specified, use this as the SNI name instead of the host from -a/--addr."`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
`os`
|
||||
`log`
|
||||
|
||||
`github.com/go-playground/validator/v10`
|
||||
`r00t2.io/goutils/logging`
|
||||
)
|
||||
|
||||
const (
|
||||
ParseNsDelim string = "-"
|
||||
ParseEnvNsDelim string = "_"
|
||||
)
|
||||
|
||||
const (
|
||||
cmdPfx string = "vault_totp"
|
||||
logFlags int = log.LstdFlags | log.Lmsgprefix
|
||||
logFlagsDebug int = logFlags | log.Llongfile
|
||||
)
|
||||
|
||||
const (
|
||||
DefAddr string = "https://clandestine.r00t2.io/"
|
||||
VaultTokEnv string = "VAULT_TOKEN"
|
||||
CollNm string = "OTP"
|
||||
TgtMnt string = "totp_bts.work"
|
||||
// These attrs need to be added to new SecretService TOTP secrets
|
||||
// ~/.local/share/gnome-shell/extensions/totp@dkosmari.github.com/schemas/org.gnome.shell.extensions.totp.gschema.xml
|
||||
|
||||
SsSchemaName string = "xdg:schema"
|
||||
SsSchemaVal string = "org.gnome.shell.extensions.totp" // also the gosecret.ItemType
|
||||
)
|
||||
|
||||
var (
|
||||
TermFd int = int(os.Stdin.Fd())
|
||||
Logger *logging.MultiLogger
|
||||
validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled())
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -2,102 +2,371 @@ package internal
|
||||
|
||||
import (
|
||||
`context`
|
||||
`errors`
|
||||
`fmt`
|
||||
`log`
|
||||
`os`
|
||||
`strings`
|
||||
`sync`
|
||||
|
||||
`github.com/hashicorp/vault-client-go`
|
||||
`r00t2.io/gosecret`
|
||||
`github.com/hashicorp/vault-client-go/schema`
|
||||
`github.com/jessevdk/go-flags`
|
||||
`golang.org/x/term`
|
||||
`r00t2.io/goutils/logging`
|
||||
`r00t2.io/goutils/multierr`
|
||||
`r00t2.io/vault_totp/errs`
|
||||
`r00t2.io/vault_totp/version`
|
||||
)
|
||||
|
||||
func New(vaultTok, vaultAddr, vaultMnt, collNm string) (c *Client, err error) {
|
||||
/*
|
||||
GetTotpKey fetches the key info for a key named `keyNm` at TOTP secrets mountpoint `mntPt` using Vault client `vc`.
|
||||
|
||||
c = &Client{
|
||||
// lastIdx: 0,
|
||||
vtok: vaultTok,
|
||||
vaddr: vaultAddr,
|
||||
scollNm: collNm,
|
||||
vmnt: vaultMnt,
|
||||
errsDone: make(chan bool, 1),
|
||||
errChan: make(chan error),
|
||||
// vc: nil,
|
||||
wg: sync.WaitGroup{},
|
||||
ctx: context.Background(),
|
||||
// ssvc: nil,
|
||||
// scoll: nil,
|
||||
mErr: multierr.NewMultiError(nil),
|
||||
// inSS: nil,
|
||||
// inVault: nil,
|
||||
If `mntPt` is empty, it will be set to "totp".
|
||||
*/
|
||||
func GetTotpKey(ctx context.Context, keyNm, mntPt string, vc *vault.Client) (kinfo map[string]any, err error) {
|
||||
|
||||
var resp *vault.Response[map[string]interface{}]
|
||||
|
||||
if strings.TrimSpace(mntPt) == "" {
|
||||
mntPt = "totp"
|
||||
}
|
||||
|
||||
if c.vc, err = vault.New(vault.WithAddress(c.vaddr)); err != nil {
|
||||
return
|
||||
}
|
||||
if err = c.vc.SetToken(c.vtok); err != nil {
|
||||
if resp, err = vc.Secrets.TotpReadKey(
|
||||
ctx,
|
||||
keyNm,
|
||||
vault.WithMountPath(mntPt),
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if c.ssvc, err = gosecret.NewService(); err != nil {
|
||||
return
|
||||
}
|
||||
if c.scoll, err = c.ssvc.GetCollection(collNm); err != nil {
|
||||
return
|
||||
kinfo = resp.Data
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetTotpKeys is like [ListTotpKeys] except it returns the configuration info
|
||||
for each key as well. (Except the secret - https://github.com/hashicorp/vault/issues/3043)
|
||||
|
||||
keyNms, if not specified, will fetch info for all keys on the mountpoint `mntPt`.
|
||||
*/
|
||||
func GetTotpKeys(ctx context.Context, vc *vault.Client, mntPt string, keyNms ...string) (keyInfo map[string]map[string]any, err error) {
|
||||
|
||||
var totpNm string
|
||||
var mut sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
var errChan chan error
|
||||
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
var opts []vault.RequestOption = make([]vault.RequestOption, 0)
|
||||
var listResp *vault.Response[schema.StandardListResponse]
|
||||
var respErr *vault.ResponseError = new(vault.ResponseError)
|
||||
|
||||
if strings.TrimSpace(mntPt) != "" {
|
||||
opts = append(opts, vault.WithMountPath(mntPt))
|
||||
}
|
||||
|
||||
go c.readErrs()
|
||||
if keyNms == nil || len(keyNms) == 0 {
|
||||
if listResp, err = vc.Secrets.TotpListKeys(
|
||||
ctx,
|
||||
opts...,
|
||||
); err != nil {
|
||||
if errors.As(err, &respErr) && respErr.StatusCode == 404 {
|
||||
// Is OK; no keys exist yet.
|
||||
keyInfo = make(map[string]map[string]any)
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
keyNms = listResp.Data.Keys
|
||||
}
|
||||
|
||||
c.wg.Add(2)
|
||||
go c.getSS()
|
||||
go c.getVault()
|
||||
keyInfo = make(map[string]map[string]any)
|
||||
if keyNms == nil || len(keyNms) == 0 {
|
||||
return
|
||||
}
|
||||
errChan = make(chan error, len(keyNms))
|
||||
for _, totpNm = range keyNms {
|
||||
wg.Add(1)
|
||||
go getTotpKeyAsync(ctx, totpNm, mntPt, vc, &mut, errChan, &wg, keyInfo)
|
||||
}
|
||||
|
||||
c.wg.Wait()
|
||||
|
||||
if !c.mErr.IsEmpty() {
|
||||
err = c.mErr
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
for err = range errChan {
|
||||
if err != nil {
|
||||
mErr.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if !mErr.IsEmpty() {
|
||||
err = mErr
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func normalizeVaultNm(nm string) (normalized string) {
|
||||
// GetVaultClient returns a Vault client from the provided args.
|
||||
func GetVaultClient(args *VaultArgs) (c *vault.Client, err error) {
|
||||
|
||||
var c rune
|
||||
var idx int
|
||||
var last rune
|
||||
var repl rune = '_'
|
||||
var reduced []rune = make([]rune, 0)
|
||||
var norm []rune = make([]rune, 0, len(nm))
|
||||
var tok string
|
||||
var vc *vault.Client
|
||||
var opts []vault.ClientOption
|
||||
var vaultTls vault.TLSConfiguration
|
||||
|
||||
for _, c = range nm {
|
||||
// If it's "safe" chars, it's fine
|
||||
if (c == '-' || c == '.') || // 0x2d, 0x2e
|
||||
(c >= '0' && c <= '9') || // 0x30 to 0x39
|
||||
(c == '@') || // 0x40
|
||||
(c >= 'A' && c <= 'Z') || // 0x41 to 0x5a
|
||||
(c == '_') || // 0x5f
|
||||
(c >= 'a' && c <= 'z') { // 0x61 to 0x7a
|
||||
norm = append(norm, c)
|
||||
continue
|
||||
}
|
||||
// Otherwise normalize it to a safe char
|
||||
norm = append(norm, repl)
|
||||
if args == nil {
|
||||
err = errs.ErrNilVault
|
||||
return
|
||||
}
|
||||
|
||||
// And remove repeating sequential replacers.
|
||||
for idx, c = range norm[:] {
|
||||
if idx == 0 {
|
||||
last = c
|
||||
reduced = append(reduced, c)
|
||||
continue
|
||||
}
|
||||
if c == last && last == repl {
|
||||
continue
|
||||
}
|
||||
reduced = append(reduced, c)
|
||||
last = c
|
||||
tok = args.Token
|
||||
if err = GetVaultToken(&tok); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
normalized = string(reduced)
|
||||
opts = []vault.ClientOption{vault.WithAddress(args.Addr)}
|
||||
if args.Insecure || args.SniName != nil {
|
||||
vaultTls = vault.TLSConfiguration{
|
||||
InsecureSkipVerify: args.Insecure,
|
||||
}
|
||||
if args.SniName != nil {
|
||||
vaultTls.ServerName = *args.SniName
|
||||
}
|
||||
opts = append(
|
||||
opts,
|
||||
vault.WithTLS(vaultTls),
|
||||
)
|
||||
}
|
||||
|
||||
if vc, err = vault.New(opts...); err != nil {
|
||||
return
|
||||
}
|
||||
if err = vc.SetToken(tok); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c = vc
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetVaultToken standardizes the Vault token fetching/lookup.
|
||||
func GetVaultToken(tok *string) (err error) {
|
||||
|
||||
var p1 []byte
|
||||
var oldState *term.State
|
||||
var termFd int = int(os.Stdin.Fd())
|
||||
|
||||
if tok != nil && len(strings.TrimSpace(*tok)) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if oldState, err = term.GetState(termFd); err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println("Vault token needed.\nVault token (will not be echoed back):")
|
||||
defer func() {
|
||||
if err = term.Restore(termFd, oldState); err != nil {
|
||||
log.Println("restore failed:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if p1, err = term.ReadPassword(termFd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if tok == nil {
|
||||
tok = new(string)
|
||||
}
|
||||
*tok = string(p1)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
ListTotpKeys returns a list (map, really, for easier lookup) of TOTP key names at `mntpt`
|
||||
with [github.com/hashicorp/vault-client-go.Client] `vc` and [context.Context] `ctx`.
|
||||
If `mntpt` is empty, the default ("totp") will be used.
|
||||
|
||||
If no TOTP keys are found at the mount, `keyNms` will be empty but not nil.
|
||||
|
||||
See [ListTotpKeys] if you want additional information about each key.
|
||||
*/
|
||||
func ListTotpKeys(ctx context.Context, vc *vault.Client, mntPt string) (keyNms map[string]struct{}, err error) {
|
||||
|
||||
var totpNm string
|
||||
var opts []vault.RequestOption = make([]vault.RequestOption, 0)
|
||||
var listResp *vault.Response[schema.StandardListResponse]
|
||||
var respErr *vault.ResponseError = new(vault.ResponseError)
|
||||
|
||||
if strings.TrimSpace(mntPt) != "" {
|
||||
opts = append(opts, vault.WithMountPath(mntPt))
|
||||
}
|
||||
|
||||
if listResp, err = vc.Secrets.TotpListKeys(
|
||||
ctx,
|
||||
opts...,
|
||||
); err != nil {
|
||||
if errors.As(err, &respErr) && respErr.StatusCode == 404 {
|
||||
// Is OK; no keys exist yet.
|
||||
keyNms = make(map[string]struct{})
|
||||
err = nil
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
keyNms = make(map[string]struct{})
|
||||
for _, totpNm = range listResp.Data.Keys {
|
||||
keyNms[totpNm] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PrepParser properly initializes the parser and logger in a standardized way.
|
||||
func PrepParser(cmd string, args CommonArgs, p *flags.Parser) (doExit bool, err error) {
|
||||
|
||||
var logFlagsRuntime int = logFlags
|
||||
var flagsErr *flags.Error = new(flags.Error)
|
||||
|
||||
p.NamespaceDelimiter = ParseNsDelim
|
||||
p.EnvNamespaceDelimiter = ParseEnvNsDelim
|
||||
|
||||
if _, err = p.Parse(); err != nil {
|
||||
switch {
|
||||
case errors.As(err, &flagsErr):
|
||||
switch {
|
||||
case errors.Is(flagsErr.Type, flags.ErrHelp),
|
||||
errors.Is(flagsErr.Type, flags.ErrCommandRequired),
|
||||
errors.Is(flagsErr.Type, flags.ErrRequired):
|
||||
// These print their relevant messages by themselves.
|
||||
err = nil
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if version.Ver, err = version.Version(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If args.Version or args.DetailVersion are true, just print them and exit.
|
||||
if args.DetailVersion || args.Version {
|
||||
doExit = true
|
||||
if args.Version {
|
||||
fmt.Println(version.Ver.Short())
|
||||
return
|
||||
} else if args.DetailVersion {
|
||||
fmt.Println(version.Ver.Detail())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if args.DoDebug {
|
||||
logFlagsRuntime = logFlagsDebug
|
||||
}
|
||||
Logger = logging.GetMultiLogger(
|
||||
args.DoDebug,
|
||||
fmt.Sprintf(
|
||||
"Vault TOTP [%s_%s]",
|
||||
cmdPfx, cmd,
|
||||
),
|
||||
)
|
||||
if err = Logger.AddDefaultLogger(
|
||||
"default",
|
||||
logFlagsRuntime,
|
||||
"/var/log/vault_totp/vault_totp.log", "~/logs/vault_totp.log",
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if err = Logger.Setup(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
Logger.Info("main: Vault TOTP version %v", version.Ver.Short())
|
||||
Logger.Debug("main: Vault TOTP version (extended):\n%v", version.Ver.Detail())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SplitVaultPathspec splits a <mount>[:<path>] into separate components. If no path is provided or it is empty, "/" will be used.
|
||||
func SplitVaultPathspec(spec string) (mount, secretPath string) {
|
||||
|
||||
var spl []string = strings.SplitN(spec, ":", 2)
|
||||
|
||||
mount = spl[0]
|
||||
switch len(spl) {
|
||||
case 1:
|
||||
secretPath = "/"
|
||||
case 2:
|
||||
if strings.TrimSpace(spl[1]) == "" {
|
||||
secretPath = "/"
|
||||
} else {
|
||||
secretPath = spl[1]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SplitVaultPathspec2 splits a [<mount>:]<path> into separate components.
|
||||
func SplitVaultPathspec2(spec string) (mount, secretPath string) {
|
||||
|
||||
var spl []string = strings.SplitN(spec, ":", 2)
|
||||
|
||||
switch len(spl) {
|
||||
case 1:
|
||||
secretPath = spl[0]
|
||||
case 2:
|
||||
mount = spl[0]
|
||||
if strings.TrimSpace(spl[1]) == "" {
|
||||
secretPath = "/"
|
||||
} else {
|
||||
secretPath = spl[1]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Validate validates the passed struct `s`. A nil err means validation succeeded.
|
||||
func Validate(s any) (err error) {
|
||||
|
||||
if err = validate.Struct(s); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getTotpKeyAsync fetches key info for key named `nm` from Vault `vc`.
|
||||
func getTotpKeyAsync(
|
||||
ctx context.Context,
|
||||
keyNm, mntPt string,
|
||||
vc *vault.Client,
|
||||
mut *sync.Mutex, errChan chan error, wg *sync.WaitGroup,
|
||||
m map[string]map[string]any,
|
||||
) {
|
||||
|
||||
var err error
|
||||
var kinfo map[string]any
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
if kinfo, err = GetTotpKey(ctx, keyNm, mntPt, vc); err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
// We can wait to hold the lock until we're actually inserting into the map.
|
||||
mut.Lock()
|
||||
defer mut.Unlock()
|
||||
m[keyNm] = kinfo
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`fmt`
|
||||
`math`
|
||||
`net/http`
|
||||
`strconv`
|
||||
`strings`
|
||||
|
||||
`github.com/creachadair/otp/otpauth`
|
||||
`github.com/hashicorp/vault-client-go`
|
||||
`github.com/hashicorp/vault-client-go/schema`
|
||||
`r00t2.io/gosecret`
|
||||
`r00t2.io/goutils/multierr`
|
||||
)
|
||||
|
||||
func (c *Client) Close() (err error) {
|
||||
|
||||
if err = c.ssvc.Close(); err != nil {
|
||||
c.errChan <- err
|
||||
}
|
||||
|
||||
close(c.errChan)
|
||||
|
||||
<-c.errsDone
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) DeleteAllVaultKeys() {
|
||||
|
||||
var name string
|
||||
|
||||
for name, _ = range c.inVault {
|
||||
c.wg.Add(1)
|
||||
go func(nm string) {
|
||||
var vErr error
|
||||
var resp *vault.Response[map[string]interface{}]
|
||||
|
||||
defer c.wg.Done()
|
||||
|
||||
if resp, vErr = c.vc.Secrets.TotpDeleteKey(
|
||||
c.ctx,
|
||||
nm,
|
||||
vault.WithMountPath(c.vmnt),
|
||||
); vErr != nil {
|
||||
c.errChan <- vErr
|
||||
return
|
||||
}
|
||||
_ = resp
|
||||
}(name)
|
||||
}
|
||||
|
||||
c.wg.Wait()
|
||||
}
|
||||
|
||||
func (c *Client) Errors() (errs []error) {
|
||||
|
||||
errs = make([]error, 0)
|
||||
|
||||
if c.mErr == nil || c.mErr.IsEmpty() {
|
||||
return
|
||||
}
|
||||
|
||||
errs = c.mErr.Errors
|
||||
c.mErr = multierr.NewMultiError(nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// currently it doesn't update out-of-sync keys and only syncs from SecretService to Vault. TODO.
|
||||
func (c *Client) Sync() {
|
||||
|
||||
c.addVault()
|
||||
|
||||
c.wg.Wait()
|
||||
}
|
||||
|
||||
func (c *Client) addVault() {
|
||||
|
||||
var name string
|
||||
var wsec wrappedSsSecret
|
||||
|
||||
for name, wsec = range c.inSS {
|
||||
c.wg.Add(1)
|
||||
go func(nm string, sec wrappedSsSecret) {
|
||||
var vErr error
|
||||
var i int
|
||||
var ok bool
|
||||
var digSize int
|
||||
var numSeconds int
|
||||
// var digSize int32
|
||||
var otpUrl *otpauth.URL
|
||||
var req schema.TotpCreateKeyRequest
|
||||
var resp *vault.Response[map[string]interface{}]
|
||||
|
||||
defer c.wg.Done()
|
||||
|
||||
if _, ok = c.inVault[nm]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
if i, vErr = strconv.Atoi(sec.secret.Attrs["digits"]); vErr != nil {
|
||||
c.errChan <- vErr
|
||||
return
|
||||
}
|
||||
if i > math.MaxInt32 {
|
||||
c.errChan <- fmt.Errorf("digits too large (%d > %d)", i, math.MaxInt32)
|
||||
return
|
||||
}
|
||||
// digSize = int32(i)
|
||||
digSize = i
|
||||
|
||||
if i, vErr = strconv.Atoi(sec.secret.Attrs["period"]); vErr != nil {
|
||||
c.errChan <- vErr
|
||||
return
|
||||
}
|
||||
if i > math.MaxInt32 {
|
||||
c.errChan <- fmt.Errorf("period too large (%d > %d)", i, math.MaxInt32)
|
||||
return
|
||||
}
|
||||
numSeconds = i
|
||||
|
||||
otpUrl = &otpauth.URL{
|
||||
Type: "TOTP", // TODO: https://github.com/dkosmari/gnome-shell-extension-totp/issues/15
|
||||
Issuer: sec.secret.Attrs["issuer"],
|
||||
Account: sec.secret.Attrs["name"],
|
||||
RawSecret: string(sec.secret.Secret.Value),
|
||||
Algorithm: strings.ToUpper(strings.ReplaceAll(sec.secret.Attrs["algorithm"], "-", "")),
|
||||
Digits: digSize,
|
||||
Period: numSeconds,
|
||||
Counter: 0,
|
||||
}
|
||||
fmt.Printf("Adding '%s' to '%s'\n", nm, c.vmnt)
|
||||
// https://developer.hashicorp.com/vault/api-docs/secret/totp#create-key
|
||||
// https://pkg.go.dev/github.com/hashicorp/vault-client-go/schema#TotpCreateKeyRequest
|
||||
req = schema.TotpCreateKeyRequest{
|
||||
// AccountName: sec.secret.Attrs["name"],
|
||||
// Algorithm: strings.ToUpper(strings.ReplaceAll(sec.secret.Attrs["algorithm"], "-", "")),
|
||||
// Digits: digSize,
|
||||
// Exported: false,
|
||||
// Generate: false,
|
||||
// Issuer: sec.secret.Attrs["issuer"],
|
||||
// Key: string(sec.secret.Secret.Value),
|
||||
// // KeySize: 0,
|
||||
// Period: sec.secret.Attrs["period"],
|
||||
// // QrSize: 0,
|
||||
// // Skew: 0,
|
||||
Url: otpUrl.String(),
|
||||
}
|
||||
if resp, vErr = c.vc.Secrets.TotpCreateKey(
|
||||
c.ctx,
|
||||
nm,
|
||||
req,
|
||||
vault.WithMountPath(c.vmnt),
|
||||
); vErr != nil {
|
||||
c.errChan <- fmt.Errorf("error adding '%s': %v", nm, vErr)
|
||||
return
|
||||
}
|
||||
_ = resp
|
||||
}(name, wsec)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) getSS() {
|
||||
|
||||
var err error
|
||||
var idx int
|
||||
var nm string
|
||||
var spl []string
|
||||
var item *gosecret.Item
|
||||
var secrets []*gosecret.Item
|
||||
|
||||
defer c.wg.Done()
|
||||
|
||||
if secrets, err = c.scoll.Items(); err != nil {
|
||||
c.errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
c.inSS = make(map[string]wrappedSsSecret)
|
||||
for _, item = range secrets {
|
||||
// Unlike KVv2 names, Vault is *very* picky about key names. Boo.
|
||||
// nm = fmt.Sprintf("%s/%s", item.Attrs["issuer"], item.Attrs["name"])
|
||||
nm = fmt.Sprintf(
|
||||
"%s"+
|
||||
"."+
|
||||
"%s",
|
||||
normalizeVaultNm(item.Attrs["issuer"]),
|
||||
normalizeVaultNm(item.Attrs["name"]),
|
||||
)
|
||||
fmt.Printf("'%s' from '%s' => '%s'\n", item.Attrs["name"], item.Attrs["issuer"], nm)
|
||||
spl = strings.SplitN(item.LabelName, ":", 2)
|
||||
if idx, err = strconv.Atoi(spl[0]); err != nil {
|
||||
c.errChan <- err
|
||||
continue
|
||||
}
|
||||
c.inSS[nm] = wrappedSsSecret{
|
||||
id: idx,
|
||||
strippedNm: nm,
|
||||
nm: item.LabelName,
|
||||
secret: item,
|
||||
}
|
||||
if idx > c.lastIdx {
|
||||
c.lastIdx = idx
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) getVault() {
|
||||
|
||||
var err error
|
||||
var nm string
|
||||
var nms []string
|
||||
var vErr *vault.ResponseError
|
||||
var totpKey *vault.Response[map[string]interface{}]
|
||||
|
||||
defer c.wg.Done()
|
||||
|
||||
var resp *vault.Response[schema.StandardListResponse]
|
||||
|
||||
resp, err = c.vc.Secrets.TotpListKeys(
|
||||
c.ctx,
|
||||
vault.WithMountPath(c.vmnt),
|
||||
)
|
||||
if err != nil {
|
||||
// no TOTP yet
|
||||
if errors.As(err, &vErr) && vErr.StatusCode == http.StatusNotFound {
|
||||
c.inVault = make(map[string]map[string]interface{})
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
c.errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
nms = resp.Data.Keys
|
||||
c.inVault = make(map[string]map[string]interface{})
|
||||
|
||||
for _, nm = range nms {
|
||||
if totpKey, err = c.vc.Secrets.TotpReadKey(
|
||||
c.ctx,
|
||||
nm,
|
||||
vault.WithMountPath(c.vmnt),
|
||||
); err != nil {
|
||||
c.errChan <- err
|
||||
continue
|
||||
}
|
||||
c.inVault[nm] = totpKey.Data
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) readErrs() {
|
||||
|
||||
var err error
|
||||
|
||||
for err = range c.errChan {
|
||||
if err != nil {
|
||||
c.mErr.AddError(err)
|
||||
}
|
||||
}
|
||||
|
||||
c.errsDone <- true
|
||||
}
|
||||
Reference in New Issue
Block a user