270 lines
5.4 KiB
Go
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
|
|
}
|