1
0
Files
vault_totp/internal/funcs_client.go
brent saner d94a46af0b initial
2025-12-16 21:45:08 -05:00

270 lines
5.4 KiB
Go

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
}