162 lines
4.8 KiB
Go
162 lines
4.8 KiB
Go
package credentials
|
|
|
|
import (
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"git.ipao.vip/rogee/creator-hub/internal/account"
|
|
)
|
|
|
|
const credentialFileVersion byte = 1
|
|
|
|
type PersistentCredentialBridge struct {
|
|
directory string
|
|
key [32]byte
|
|
chmod func(*os.File, os.FileMode) error
|
|
syncDirectory func(string) error
|
|
}
|
|
|
|
func NewPersistentCredentialBridge(directory string, key []byte) (*PersistentCredentialBridge, error) {
|
|
if !filepath.IsAbs(directory) || len(key) != 32 {
|
|
return nil, errors.New("invalid credential provider configuration")
|
|
}
|
|
if err := os.MkdirAll(directory, 0o700); err != nil || os.Chmod(directory, 0o700) != nil {
|
|
return nil, errors.New("initialize credential provider")
|
|
}
|
|
bridge := &PersistentCredentialBridge{
|
|
directory: directory, chmod: (*os.File).Chmod, syncDirectory: syncCredentialDirectory,
|
|
}
|
|
copy(bridge.key[:], key)
|
|
return bridge, nil
|
|
}
|
|
|
|
func (bridge *PersistentCredentialBridge) Store(ctx context.Context, reference account.CredentialReference, key, value string) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if !validCredentialReference(reference.Provider, key) || value == "" || len(value) > 64<<10 {
|
|
return errors.New("invalid credential")
|
|
}
|
|
aead, err := bridge.aead()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nonce := make([]byte, aead.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return errors.New("encrypt credential")
|
|
}
|
|
payload := append([]byte{credentialFileVersion}, nonce...)
|
|
payload = aead.Seal(payload, nonce, []byte(value), credentialAAD(reference.Provider, key))
|
|
temporary, err := os.CreateTemp(bridge.directory, ".credential-*")
|
|
if err != nil {
|
|
return errors.New("store credential")
|
|
}
|
|
temporaryName := temporary.Name()
|
|
defer os.Remove(temporaryName)
|
|
if err = bridge.chmod(temporary, 0o600); err == nil {
|
|
_, err = temporary.Write(payload)
|
|
}
|
|
if err == nil {
|
|
err = temporary.Sync()
|
|
}
|
|
if closeErr := temporary.Close(); err == nil {
|
|
err = closeErr
|
|
}
|
|
if err != nil {
|
|
return errors.New("store credential")
|
|
}
|
|
target := bridge.path(reference.Provider, key)
|
|
if os.Rename(temporaryName, target) != nil {
|
|
return errors.New("store credential")
|
|
}
|
|
if bridge.syncDirectory(bridge.directory) != nil {
|
|
cleanupErr := os.Remove(target)
|
|
if cleanupErr == nil {
|
|
cleanupErr = bridge.syncDirectory(bridge.directory)
|
|
}
|
|
if cleanupErr != nil {
|
|
return errors.Join(errors.New("store credential"), errors.New("delete incomplete credential"))
|
|
}
|
|
return errors.New("store credential")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (bridge *PersistentCredentialBridge) Delete(ctx context.Context, reference account.CredentialReference, key string) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if !validCredentialReference(reference.Provider, key) {
|
|
return errors.New("invalid credential reference")
|
|
}
|
|
if err := os.Remove(bridge.path(reference.Provider, key)); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return errors.New("delete credential")
|
|
}
|
|
if err := bridge.syncDirectory(bridge.directory); err != nil {
|
|
return errors.New("delete credential")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (bridge *PersistentCredentialBridge) Resolve(ctx context.Context, reference account.CredentialReference, key string) ([]byte, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if !validCredentialReference(reference.Provider, key) {
|
|
return nil, errors.New("invalid credential reference")
|
|
}
|
|
payload, err := os.ReadFile(bridge.path(reference.Provider, key))
|
|
if err != nil {
|
|
return nil, errors.New("resolve credential")
|
|
}
|
|
aead, err := bridge.aead()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(payload) < 1+aead.NonceSize() || payload[0] != credentialFileVersion {
|
|
return nil, errors.New("invalid credential")
|
|
}
|
|
nonceEnd := 1 + aead.NonceSize()
|
|
value, err := aead.Open(nil, payload[1:nonceEnd], payload[nonceEnd:], credentialAAD(reference.Provider, key))
|
|
if err != nil {
|
|
return nil, errors.New("resolve credential")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (bridge *PersistentCredentialBridge) aead() (cipher.AEAD, error) {
|
|
block, err := aes.NewCipher(bridge.key[:])
|
|
if err != nil {
|
|
return nil, errors.New("initialize credential encryption")
|
|
}
|
|
return cipher.NewGCM(block)
|
|
}
|
|
|
|
func (bridge *PersistentCredentialBridge) path(provider, key string) string {
|
|
digest := sha256.Sum256(credentialAAD(provider, key))
|
|
return filepath.Join(bridge.directory, hex.EncodeToString(digest[:])+".credential")
|
|
}
|
|
|
|
func credentialAAD(provider, key string) []byte { return []byte(provider + "\x00" + key) }
|
|
|
|
func validCredentialReference(provider, key string) bool {
|
|
return (provider == "os_keyring" || provider == "secret_manager") && key != "" && len(key) <= 192
|
|
}
|
|
|
|
func syncCredentialDirectory(path string) error {
|
|
directory, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer directory.Close()
|
|
return directory.Sync()
|
|
}
|