init
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
// Package keychain stores and retrieves secrets (passwords, JWT tokens)
|
||||
// in the macOS Keychain. On non-darwin platforms it falls back to an
|
||||
// in-memory store (to be replaced with a platform-appropriate backend).
|
||||
//
|
||||
// Secrets are keyed by the server profile name. The Keychain service
|
||||
// name is the application bundle identifier.
|
||||
package keychain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"lmvpn/internal/paths"
|
||||
)
|
||||
|
||||
// ServiceName is the Keychain service under which all lmvpn secrets
|
||||
// are stored.
|
||||
const ServiceName = paths.BundleID
|
||||
|
||||
// ErrNotFound is returned when a secret is not present in the store.
|
||||
var ErrNotFound = fmt.Errorf("secret not found")
|
||||
|
||||
// Store is the secret storage interface.
|
||||
type Store interface {
|
||||
SetPassword(profileName, password string) error
|
||||
GetPassword(profileName string) (string, error)
|
||||
DeletePassword(profileName string) error
|
||||
SetToken(profileName, token string) error
|
||||
GetToken(profileName string) (string, error)
|
||||
DeleteToken(profileName string) error
|
||||
DeleteAll(profileName string) error
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//go:build darwin
|
||||
|
||||
package keychain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/keybase/go-keychain"
|
||||
)
|
||||
|
||||
// keyPrefixes distinguish password vs token entries in the keychain.
|
||||
const (
|
||||
passwordAccountPrefix = "password:"
|
||||
tokenAccountPrefix = "token:"
|
||||
)
|
||||
|
||||
// DarwinStore implements Store using the macOS Keychain.
|
||||
type DarwinStore struct{}
|
||||
|
||||
// New returns a macOS Keychain-backed Store.
|
||||
func New() Store {
|
||||
return DarwinStore{}
|
||||
}
|
||||
|
||||
func (DarwinStore) setItem(account, secret string) error {
|
||||
item := keychain.NewItem()
|
||||
item.SetSecClass(keychain.SecClassGenericPassword)
|
||||
item.SetService(ServiceName)
|
||||
item.SetAccount(account)
|
||||
item.SetData([]byte(secret))
|
||||
item.SetAccessible(keychain.AccessibleAfterFirstUnlock)
|
||||
// Delete any existing item first (Add fails on duplicates).
|
||||
_ = keychain.DeleteItem(item)
|
||||
if err := keychain.AddItem(item); err != nil {
|
||||
return fmt.Errorf("keychain add %s: %w", account, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (DarwinStore) getItem(account string) (string, error) {
|
||||
item := keychain.NewItem()
|
||||
item.SetSecClass(keychain.SecClassGenericPassword)
|
||||
item.SetService(ServiceName)
|
||||
item.SetAccount(account)
|
||||
item.SetMatchLimit(keychain.MatchLimitOne)
|
||||
item.SetReturnData(true)
|
||||
results, err := keychain.QueryItem(item)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("keychain query %s: %w", account, err)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
return string(results[0].Data), nil
|
||||
}
|
||||
|
||||
func (DarwinStore) deleteItem(account string) error {
|
||||
item := keychain.NewItem()
|
||||
item.SetSecClass(keychain.SecClassGenericPassword)
|
||||
item.SetService(ServiceName)
|
||||
item.SetAccount(account)
|
||||
return keychain.DeleteItem(item)
|
||||
}
|
||||
|
||||
func (s DarwinStore) SetPassword(profileName, password string) error {
|
||||
return s.setItem(passwordAccountPrefix+profileName, password)
|
||||
}
|
||||
|
||||
func (s DarwinStore) GetPassword(profileName string) (string, error) {
|
||||
return s.getItem(passwordAccountPrefix + profileName)
|
||||
}
|
||||
|
||||
func (s DarwinStore) DeletePassword(profileName string) error {
|
||||
return s.deleteItem(passwordAccountPrefix + profileName)
|
||||
}
|
||||
|
||||
func (s DarwinStore) SetToken(profileName, token string) error {
|
||||
return s.setItem(tokenAccountPrefix+profileName, token)
|
||||
}
|
||||
|
||||
func (s DarwinStore) GetToken(profileName string) (string, error) {
|
||||
return s.getItem(tokenAccountPrefix + profileName)
|
||||
}
|
||||
|
||||
func (s DarwinStore) DeleteToken(profileName string) error {
|
||||
return s.deleteItem(tokenAccountPrefix + profileName)
|
||||
}
|
||||
|
||||
func (s DarwinStore) DeleteAll(profileName string) error {
|
||||
_ = s.DeletePassword(profileName)
|
||||
return s.DeleteToken(profileName)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//go:build !darwin
|
||||
|
||||
package keychain
|
||||
|
||||
// MemStore is an in-memory fallback used on non-darwin platforms.
|
||||
type MemStore struct {
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
// New returns an in-memory Store (non-darwin fallback).
|
||||
func New() Store {
|
||||
return &MemStore{data: make(map[string]string)}
|
||||
}
|
||||
|
||||
func (m *MemStore) SetPassword(profileName, password string) error {
|
||||
m.data["password:"+profileName] = password
|
||||
return nil
|
||||
}
|
||||
func (m *MemStore) GetPassword(profileName string) (string, error) {
|
||||
v, ok := m.data["password:"+profileName]
|
||||
if !ok {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
func (m *MemStore) DeletePassword(profileName string) error {
|
||||
delete(m.data, "password:"+profileName)
|
||||
return nil
|
||||
}
|
||||
func (m *MemStore) SetToken(profileName, token string) error {
|
||||
m.data["token:"+profileName] = token
|
||||
return nil
|
||||
}
|
||||
func (m *MemStore) GetToken(profileName string) (string, error) {
|
||||
v, ok := m.data["token:"+profileName]
|
||||
if !ok {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
func (m *MemStore) DeleteToken(profileName string) error {
|
||||
delete(m.data, "token:"+profileName)
|
||||
return nil
|
||||
}
|
||||
func (m *MemStore) DeleteAll(profileName string) error {
|
||||
m.DeletePassword(profileName)
|
||||
m.DeleteToken(profileName)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user