Files
lmvpn_client/internal/keychain/keychain_darwin.go
T

122 lines
3.6 KiB
Go

//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:"
)
// touchIDPrompt is the localized prompt shown in the Touch ID dialog.
// It is set by the UI layer at startup via SetTouchIDPrompt.
var touchIDPrompt = "authenticate to access your VPN password"
// SetTouchIDPrompt sets the localized prompt text shown in the Touch ID
// dialog when retrieving secrets from the keychain.
func SetTouchIDPrompt(prompt string) {
if prompt != "" {
touchIDPrompt = prompt
}
}
// DarwinStore implements Store using the macOS Keychain.
type DarwinStore struct{}
// New returns a macOS Keychain-backed Store. On Macs with Touch ID,
// secrets are stored with biometric (Touch ID) protection; on older
// Macs without a biometric sensor, the standard keychain accessibility
// is used as a fallback.
func New() Store {
return DarwinStore{}
}
func (DarwinStore) setItem(account, secret string) error {
// Use biometric-protected storage when Touch ID is available.
if biometricAvailable() {
return storeBiometricItemGo(ServiceName, account, secret)
}
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) {
// When Touch ID is available, use the direct CGo path which can
// show a biometric prompt for protected items.
if biometricAvailable() {
return getBiometricItemGo(ServiceName, account, touchIDPrompt)
}
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 {
// Use the direct CGo delete which works for both biometric and
// non-biometric items (and doesn't trigger a Touch ID prompt).
if biometricAvailable() {
return deleteBiometricItemGo(ServiceName, account)
}
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)
}