2023-03-22 19:41:10 +00:00
|
|
|
package vault
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
2023-06-18 12:34:20 +00:00
|
|
|
"errors"
|
2023-03-22 19:41:10 +00:00
|
|
|
"fmt"
|
2023-05-01 08:15:53 +00:00
|
|
|
"os"
|
2023-03-22 19:41:10 +00:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
2023-08-15 19:47:19 +00:00
|
|
|
"code.icb4dc0.de/buildr/buildr/internal/maps"
|
2023-03-22 19:41:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2023-06-18 12:34:20 +00:00
|
|
|
ErrVaultNotInitialized = errors.New("vault is not properly initialized")
|
|
|
|
_ json.Marshaler = (*Vault)(nil)
|
|
|
|
_ json.Unmarshaler = (*Vault)(nil)
|
2023-03-22 19:41:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type Entry struct {
|
|
|
|
Value []byte
|
|
|
|
Salt []byte
|
|
|
|
Nonce []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
func EntryFromText(txt string) (e Entry, err error) {
|
2023-06-22 16:06:56 +00:00
|
|
|
const expectedNumberOfComponents = 3
|
2023-03-22 19:41:10 +00:00
|
|
|
split := strings.Split(txt, ":")
|
2023-06-22 16:06:56 +00:00
|
|
|
if len(split) != expectedNumberOfComponents {
|
2023-03-22 19:41:10 +00:00
|
|
|
return Entry{}, fmt.Errorf("unexpected number of components for entry: %d", len(split))
|
|
|
|
}
|
|
|
|
|
|
|
|
encoding := base64.StdEncoding
|
|
|
|
|
|
|
|
if e.Nonce, err = encoding.DecodeString(split[0]); err != nil {
|
|
|
|
return Entry{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if e.Salt, err = encoding.DecodeString(split[1]); err != nil {
|
|
|
|
return Entry{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if e.Value, err = encoding.DecodeString(split[2]); err != nil {
|
|
|
|
return Entry{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return e, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e Entry) MarshalText() string {
|
|
|
|
encoding := base64.StdEncoding
|
|
|
|
val := fmt.Sprintf(
|
|
|
|
"%s:%s:%s",
|
|
|
|
encoding.EncodeToString(e.Nonce),
|
|
|
|
encoding.EncodeToString(e.Salt),
|
|
|
|
encoding.EncodeToString(e.Value),
|
|
|
|
)
|
|
|
|
return val
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e Entry) Decrypt(decrypter Decrypter, passphrase string) ([]byte, error) {
|
|
|
|
return decrypter.Decrypt(e.Value, passphrase, e.Salt, e.Nonce)
|
|
|
|
}
|
|
|
|
|
2023-05-01 08:15:53 +00:00
|
|
|
func NewVault(opts ...InitOption) (*Vault, error) {
|
|
|
|
v := &Vault{
|
|
|
|
encryption: NewAesGcmEncryption(Pbkdf2Deriver()),
|
2023-03-22 19:41:10 +00:00
|
|
|
entries: make(map[string]Entry),
|
|
|
|
}
|
2023-05-01 08:15:53 +00:00
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
if err := opt.applyToVault(v); err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
2023-06-22 16:06:56 +00:00
|
|
|
//nolint:nilnil // correct from a domain level
|
2023-05-01 08:15:53 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return v, nil
|
2023-03-22 19:41:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type Vault struct {
|
|
|
|
encryption Encryption
|
|
|
|
entries map[string]Entry
|
2023-06-22 16:06:56 +00:00
|
|
|
passphrase string
|
|
|
|
lock sync.RWMutex
|
2023-03-22 19:41:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Vault) Keys() []string {
|
|
|
|
v.lock.RLock()
|
|
|
|
defer v.lock.RUnlock()
|
|
|
|
|
|
|
|
return maps.Keys(v.entries)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Vault) GetValue(entryKey string) ([]byte, error) {
|
2023-06-18 12:34:20 +00:00
|
|
|
if v == nil {
|
|
|
|
return nil, ErrVaultNotInitialized
|
|
|
|
}
|
2023-03-22 19:41:10 +00:00
|
|
|
v.lock.RLock()
|
|
|
|
defer v.lock.RUnlock()
|
|
|
|
|
|
|
|
return v.doGetValue(entryKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Vault) SetValue(entryKey string, plainText []byte) error {
|
2023-06-18 12:34:20 +00:00
|
|
|
if v == nil {
|
|
|
|
return ErrVaultNotInitialized
|
|
|
|
}
|
|
|
|
|
2023-03-22 19:41:10 +00:00
|
|
|
v.lock.Lock()
|
|
|
|
defer v.lock.Unlock()
|
|
|
|
|
|
|
|
return v.doSetValue(entryKey, plainText)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Vault) RemoveValue(entryKey string) {
|
|
|
|
v.lock.Lock()
|
|
|
|
defer v.lock.Unlock()
|
|
|
|
|
|
|
|
delete(v.entries, entryKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Vault) doGetValue(entryKey string) ([]byte, error) {
|
|
|
|
entry, ok := v.entries[entryKey]
|
|
|
|
if !ok {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return entry.Decrypt(v.encryption, v.passphrase)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Vault) doSetValue(entryKey string, plainText []byte) error {
|
|
|
|
cipher, salt, nonce, err := v.encryption.Encrypt(plainText, v.passphrase)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
v.entries[entryKey] = Entry{
|
|
|
|
Value: cipher,
|
|
|
|
Salt: salt,
|
|
|
|
Nonce: nonce,
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-06-22 16:06:56 +00:00
|
|
|
func (v *Vault) UnmarshalJSON(data []byte) error {
|
2023-03-22 19:41:10 +00:00
|
|
|
v.lock.Lock()
|
|
|
|
defer v.lock.Unlock()
|
|
|
|
|
|
|
|
rawEntries := make(map[string]string)
|
|
|
|
v.entries = make(map[string]Entry)
|
|
|
|
|
|
|
|
if err := json.Unmarshal(data, &rawEntries); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for key, val := range rawEntries {
|
2023-06-22 16:06:56 +00:00
|
|
|
var err error
|
2023-03-22 19:41:10 +00:00
|
|
|
if v.entries[key], err = EntryFromText(val); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-06-22 16:06:56 +00:00
|
|
|
if _, err := v.entries[key].Decrypt(v.encryption, v.passphrase); err != nil {
|
2023-03-22 19:41:10 +00:00
|
|
|
return fmt.Errorf("failed to decrypt entry %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Vault) MarshalJSON() ([]byte, error) {
|
|
|
|
toSerialize := make(map[string]string, len(v.entries))
|
|
|
|
|
|
|
|
v.lock.RLock()
|
|
|
|
defer v.lock.RUnlock()
|
|
|
|
|
|
|
|
for k, v := range v.entries {
|
|
|
|
toSerialize[k] = v.MarshalText()
|
|
|
|
}
|
|
|
|
|
|
|
|
return json.Marshal(toSerialize)
|
|
|
|
}
|