check / check (push) Waiting to run
script/lint-darwin (make lint-darwin; run by script/check, and its commands by the Dockerfile lint stage) runs go vet and golangci-lint with GOOS=darwin and cgo off. Compiling cgo for macOS needs Apple's SDK, so the three functions that call go-keychain, which is cgo there, move to keychainunlocker_cgo.go; a macOS build without cgo gets keychainunlocker_nocgo.go and the macse stub, whose errors name the missing macOS build with cgo. The rest of the keychain unlocker and its plain-Go tests are now checked; their findings are fixed without changing behaviour, and lines over 88 columns in the unchecked files are wrapped. Model: opus-5-5
581 lines
18 KiB
Go
581 lines
18 KiB
Go
//go:build darwin
|
|
|
|
package secret
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"time"
|
|
|
|
"filippo.io/age"
|
|
"git.eeqj.de/sneak/secret/pkg/agehd"
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
const (
|
|
agePrivKeyPassphraseLength = 64
|
|
// KEYCHAIN_APP_IDENTIFIER is the service name used for keychain items
|
|
//
|
|
//nolint:revive // ALL_CAPS is intentional for this constant
|
|
KEYCHAIN_APP_IDENTIFIER = "berlin.sneak.app.secret"
|
|
|
|
// keychainUnlockerType is the metadata type string for keychain unlockers.
|
|
keychainUnlockerType = "keychain"
|
|
|
|
// macOSFlag is the unlocker metadata flag of the macOS-only unlockers.
|
|
macOSFlag = "macos"
|
|
)
|
|
|
|
// keychainItemNameRegex validates keychain item names
|
|
// Allows alphanumeric characters, dots, hyphens, and underscores only
|
|
var keychainItemNameRegex = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
|
|
|
|
var (
|
|
errNotMacOS = errors.New(
|
|
"keychain unlockers are only supported on macOS")
|
|
errKeychainItemNameEmpty = errors.New("keychain item name cannot be empty")
|
|
errInvalidKeychainItemName = errors.New("invalid keychain item name format")
|
|
errUnsupportedCurrentUnlocker = errors.New(
|
|
"unsupported current unlocker type for keychain unlocker creation")
|
|
)
|
|
|
|
// KeychainUnlockerMetadata extends UnlockerMetadata with keychain-specific data
|
|
type KeychainUnlockerMetadata struct {
|
|
UnlockerMetadata
|
|
|
|
// Keychain item name
|
|
KeychainItemName string `json:"keychainItemName"`
|
|
}
|
|
|
|
// KeychainUnlocker represents a macOS Keychain-protected unlocker
|
|
type KeychainUnlocker struct {
|
|
Directory string
|
|
Metadata UnlockerMetadata
|
|
fs afero.Fs
|
|
}
|
|
|
|
// NewKeychainUnlocker creates a new KeychainUnlocker instance
|
|
func NewKeychainUnlocker(
|
|
fs afero.Fs, directory string, metadata UnlockerMetadata,
|
|
) *KeychainUnlocker {
|
|
return &KeychainUnlocker{
|
|
Directory: directory,
|
|
Metadata: metadata,
|
|
fs: fs,
|
|
}
|
|
}
|
|
|
|
// GetIdentity implements Unlocker interface for Keychain-based unlockers
|
|
func (k *KeychainUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
|
DebugWith("Getting keychain unlocker identity",
|
|
slog.String("unlocker_id", k.GetID()),
|
|
slog.String("unlocker_type", k.GetType()),
|
|
)
|
|
|
|
keychainData, err := k.readKeychainData()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer keychainData.AgePrivKeyPassphrase.Destroy()
|
|
|
|
// Step 4: Read the encrypted age private key from filesystem
|
|
agePrivKeyPath := filepath.Join(k.Directory, "priv.age")
|
|
Debug("Reading encrypted age private key", "path", agePrivKeyPath)
|
|
|
|
encryptedAgePrivKeyData, err := afero.ReadFile(k.fs, agePrivKeyPath)
|
|
if err != nil {
|
|
Debug("Failed to read encrypted age private key",
|
|
"error", err, "path", agePrivKeyPath)
|
|
|
|
return nil, fmt.Errorf("failed to read encrypted age private key: %w", err)
|
|
}
|
|
|
|
DebugWith("Read encrypted age private key",
|
|
slog.String("unlocker_id", k.GetID()),
|
|
slog.Int("encrypted_length", len(encryptedAgePrivKeyData)),
|
|
)
|
|
|
|
// Step 5: Decrypt the age private key using the passphrase from keychain
|
|
Debug("Decrypting age private key with keychain passphrase",
|
|
"unlocker_id", k.GetID())
|
|
|
|
agePrivKeyBuffer, err := DecryptWithPassphrase(
|
|
encryptedAgePrivKeyData, keychainData.AgePrivKeyPassphrase)
|
|
if err != nil {
|
|
Debug("Failed to decrypt age private key with keychain passphrase",
|
|
"error", err, "unlocker_id", k.GetID())
|
|
|
|
return nil, fmt.Errorf(
|
|
"failed to decrypt age private key with keychain passphrase: %w", err)
|
|
}
|
|
defer agePrivKeyBuffer.Destroy()
|
|
|
|
DebugWith("Successfully decrypted age private key with keychain passphrase",
|
|
slog.String("unlocker_id", k.GetID()),
|
|
slog.Int("decrypted_length", agePrivKeyBuffer.Size()),
|
|
)
|
|
|
|
// Step 6: Parse the decrypted age private key
|
|
Debug("Parsing decrypted age private key", "unlocker_id", k.GetID())
|
|
|
|
ageIdentity, err := age.ParseX25519Identity(agePrivKeyBuffer.String())
|
|
if err != nil {
|
|
Debug("Failed to parse age private key", "error", err, "unlocker_id", k.GetID())
|
|
|
|
return nil, fmt.Errorf("failed to parse age private key: %w", err)
|
|
}
|
|
|
|
DebugWith("Successfully parsed keychain age identity",
|
|
slog.String("unlocker_id", k.GetID()),
|
|
slog.String("public_key", ageIdentity.Recipient().String()),
|
|
)
|
|
|
|
return ageIdentity, nil
|
|
}
|
|
|
|
// GetType implements Unlocker interface
|
|
func (k *KeychainUnlocker) GetType() string {
|
|
return keychainUnlockerType
|
|
}
|
|
|
|
// GetMetadata implements Unlocker interface
|
|
func (k *KeychainUnlocker) GetMetadata() UnlockerMetadata {
|
|
return k.Metadata
|
|
}
|
|
|
|
// GetDirectory implements Unlocker interface
|
|
func (k *KeychainUnlocker) GetDirectory() string {
|
|
return k.Directory
|
|
}
|
|
|
|
// GetID implements Unlocker interface - generates ID from keychain item name
|
|
func (k *KeychainUnlocker) GetID() string {
|
|
// Generate ID in the format YYYY-MM-DD.HH.mm-hostname-keychain
|
|
// This matches the passphrase unlocker format
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
hostname = "unknown"
|
|
}
|
|
|
|
// Use the creation timestamp from metadata
|
|
createdAt := k.Metadata.CreatedAt
|
|
timestamp := createdAt.Format("2006-01-02.15.04")
|
|
|
|
return fmt.Sprintf("%s-%s-keychain", timestamp, hostname)
|
|
}
|
|
|
|
// Remove implements Unlocker interface - removes the keychain unlocker
|
|
func (k *KeychainUnlocker) Remove() error {
|
|
// Step 1: Get keychain item name
|
|
keychainItemName, err := k.GetKeychainItemName()
|
|
if err != nil {
|
|
Debug("Failed to get keychain item name during removal",
|
|
"error", err, "unlocker_id", k.GetID())
|
|
|
|
return fmt.Errorf("failed to get keychain item name: %w", err)
|
|
}
|
|
|
|
// Step 2: Remove from keychain
|
|
Debug("Removing keychain item", "keychain_item", keychainItemName)
|
|
|
|
err = deleteFromKeychain(keychainItemName)
|
|
if err != nil {
|
|
Debug("Failed to remove keychain item",
|
|
"error", err, "keychain_item", keychainItemName)
|
|
|
|
return fmt.Errorf("failed to remove keychain item: %w", err)
|
|
}
|
|
|
|
// Step 3: Remove directory
|
|
Debug("Removing keychain unlocker directory", "directory", k.Directory)
|
|
|
|
err = RemoveDirAtomic(k.fs, k.Directory)
|
|
if err != nil {
|
|
Debug("Failed to remove keychain unlocker directory",
|
|
"error", err, "directory", k.Directory)
|
|
|
|
return fmt.Errorf("failed to remove keychain unlocker directory: %w", err)
|
|
}
|
|
|
|
Debug("Successfully removed keychain unlocker",
|
|
"unlocker_id", k.GetID(), "keychain_item", keychainItemName)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetKeychainItemName returns the keychain item name from metadata
|
|
func (k *KeychainUnlocker) GetKeychainItemName() (string, error) {
|
|
// Load the metadata
|
|
metadataPath := filepath.Join(k.Directory, "unlocker-metadata.json")
|
|
|
|
metadataData, err := afero.ReadFile(k.fs, metadataPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read keychain metadata: %w", err)
|
|
}
|
|
|
|
var keychainMetadata KeychainUnlockerMetadata
|
|
|
|
err = json.Unmarshal(metadataData, &keychainMetadata)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse keychain metadata: %w", err)
|
|
}
|
|
|
|
return keychainMetadata.KeychainItemName, nil
|
|
}
|
|
|
|
// readKeychainData reads and parses the data this unlocker keeps in the
|
|
// keychain (steps 1 to 3 of GetIdentity). The caller must destroy the
|
|
// returned AgePrivKeyPassphrase.
|
|
func (k *KeychainUnlocker) readKeychainData() (*KeychainData, error) {
|
|
// Step 1: Get keychain item name
|
|
keychainItemName, err := k.GetKeychainItemName()
|
|
if err != nil {
|
|
Debug("Failed to get keychain item name", "error", err, "unlocker_id", k.GetID())
|
|
|
|
return nil, fmt.Errorf("failed to get keychain item name: %w", err)
|
|
}
|
|
|
|
// Step 2: Retrieve data from keychain
|
|
Debug("Retrieving data from macOS keychain", "keychain_item", keychainItemName)
|
|
|
|
keychainDataBytes, err := retrieveFromKeychain(keychainItemName)
|
|
if err != nil {
|
|
Debug("Failed to retrieve data from keychain",
|
|
"error", err, "keychain_item", keychainItemName)
|
|
|
|
return nil, fmt.Errorf("failed to retrieve data from keychain: %w", err)
|
|
}
|
|
|
|
DebugWith("Retrieved data from keychain",
|
|
slog.String("unlocker_id", k.GetID()),
|
|
slog.Int("data_length", len(keychainDataBytes)),
|
|
)
|
|
|
|
// Move the keychain data into locked memory; this wipes keychainDataBytes
|
|
keychainDataBuffer := memguard.NewBufferFromBytes(keychainDataBytes)
|
|
defer keychainDataBuffer.Destroy()
|
|
|
|
// Step 3: Parse keychain data
|
|
keychainData, err := decodeKeychainData(keychainDataBuffer)
|
|
if err != nil {
|
|
Debug("Failed to parse keychain data", "error", err, "unlocker_id", k.GetID())
|
|
|
|
return nil, fmt.Errorf("failed to parse keychain data: %w", err)
|
|
}
|
|
|
|
Debug("Parsed keychain data successfully", "unlocker_id", k.GetID())
|
|
|
|
return keychainData, nil
|
|
}
|
|
|
|
// generateKeychainUnlockerName generates a unique name for the keychain unlocker
|
|
func generateKeychainUnlockerName(vaultName string) (string, error) {
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get hostname: %w", err)
|
|
}
|
|
|
|
// Format: secret-<vault>-<hostname>-<time>
|
|
enrollmentTime := time.Now().UTC().Format(UnlockerTimeFormat)
|
|
|
|
return fmt.Sprintf("secret-%s-%s-%s", vaultName, hostname, enrollmentTime), nil
|
|
}
|
|
|
|
// getLongTermPrivateKey derives the long-term private key from mnemonic when
|
|
// it is not nil, else gets it through the current unlocker, which is given
|
|
// passphrase when it is a passphrase unlocker.
|
|
// Returns a LockedBuffer to ensure the private key is protected in memory
|
|
func getLongTermPrivateKey(
|
|
fs afero.Fs, vault VaultInterface, mnemonic, passphrase *memguard.LockedBuffer,
|
|
) (*memguard.LockedBuffer, error) {
|
|
if mnemonic != nil {
|
|
return deriveLongTermPrivateKey(fs, vault, mnemonic)
|
|
}
|
|
|
|
// Get the vault to access current unlocker
|
|
currentUnlocker, err := vault.GetCurrentUnlocker()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get current unlocker: %w", err)
|
|
}
|
|
|
|
if passphraseUnlocker, ok := currentUnlocker.(*PassphraseUnlocker); ok {
|
|
passphraseUnlocker.Passphrase = passphrase
|
|
}
|
|
|
|
// Get the current unlocker identity
|
|
currentUnlockerIdentity, err := currentUnlocker.GetIdentity()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get current unlocker identity: %w", err)
|
|
}
|
|
|
|
// Get encrypted long-term key from current unlocker, handling different types
|
|
var encryptedLtPrivKey []byte
|
|
|
|
switch currentUnlocker := currentUnlocker.(type) {
|
|
case *PassphraseUnlocker:
|
|
// Read the encrypted long-term private key from passphrase unlocker
|
|
encryptedLtPrivKey, err = afero.ReadFile(fs,
|
|
filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read encrypted long-term key "+
|
|
"from current passphrase unlocker: %w", err)
|
|
}
|
|
|
|
case *PGPUnlocker:
|
|
// Read the encrypted long-term private key from PGP unlocker
|
|
encryptedLtPrivKey, err = afero.ReadFile(fs,
|
|
filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read encrypted long-term key "+
|
|
"from current PGP unlocker: %w", err)
|
|
}
|
|
|
|
case *KeychainUnlocker:
|
|
// Read the encrypted long-term private key from another keychain
|
|
// unlocker
|
|
encryptedLtPrivKey, err = afero.ReadFile(fs,
|
|
filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read encrypted long-term key "+
|
|
"from current keychain unlocker: %w", err)
|
|
}
|
|
|
|
default:
|
|
return nil, errUnsupportedCurrentUnlocker
|
|
}
|
|
|
|
// Decrypt long-term private key using current unlocker
|
|
ltPrivKeyBuffer, err := DecryptWithIdentity(
|
|
encryptedLtPrivKey, currentUnlockerIdentity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
|
|
}
|
|
|
|
// Return the decrypted key buffer
|
|
return ltPrivKeyBuffer, nil
|
|
}
|
|
|
|
// deriveLongTermPrivateKey derives the long-term private key from mnemonic at
|
|
// the vault's derivation index, for getLongTermPrivateKey and
|
|
// getLongTermKeyForSE.
|
|
func deriveLongTermPrivateKey(
|
|
fs afero.Fs, vault VaultInterface, mnemonic *memguard.LockedBuffer,
|
|
) (*memguard.LockedBuffer, error) {
|
|
// Read vault metadata to get the correct derivation index
|
|
vaultDir, err := vault.GetDirectory()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
|
}
|
|
|
|
metadataPath := filepath.Join(vaultDir, "vault-metadata.json")
|
|
|
|
metadataBytes, err := afero.ReadFile(fs, metadataPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read vault metadata: %w", err)
|
|
}
|
|
|
|
var metadata VaultMetadata
|
|
|
|
err = json.Unmarshal(metadataBytes, &metadata)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse vault metadata: %w", err)
|
|
}
|
|
|
|
// Use mnemonic with the vault's actual derivation index
|
|
ltIdentity, err := agehd.DeriveIdentity(mnemonic.String(), metadata.DerivationIndex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"failed to derive long-term key from mnemonic: %w", err)
|
|
}
|
|
|
|
// Return the private key in a secure buffer
|
|
return memguard.NewBufferFromBytes([]byte(ltIdentity.String())), nil
|
|
}
|
|
|
|
// CreateKeychainUnlocker creates a new keychain unlocker and stores it in the
|
|
// vault. The long-term key comes from mnemonic when it is not nil, else from
|
|
// the current unlocker, as getLongTermPrivateKey describes.
|
|
func CreateKeychainUnlocker(
|
|
fs afero.Fs, stateDir string, mnemonic, passphrase *memguard.LockedBuffer,
|
|
) (*KeychainUnlocker, error) {
|
|
// Check if we're on macOS
|
|
err := checkMacOSAvailable()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get current vault using the GetCurrentVault function from the same package
|
|
vault, err := GetCurrentVault(fs, stateDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get current vault: %w", err)
|
|
}
|
|
|
|
// Generate the keychain item name
|
|
keychainItemName, err := generateKeychainUnlockerName(vault.GetName())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate keychain item name: %w", err)
|
|
}
|
|
|
|
// The unlocker directory is named after the keychain item
|
|
vaultDir, err := vault.GetDirectory()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
|
}
|
|
|
|
unlockerDir := filepath.Join(vaultDir, "unlockers.d", keychainItemName)
|
|
|
|
// Step 1: Generate a new age keypair for the keychain unlocker
|
|
ageIdentity, err := age.GenerateX25519Identity()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate age keypair: %w", err)
|
|
}
|
|
|
|
ageRecipient := ageIdentity.Recipient().String()
|
|
|
|
// Step 2: Generate a random passphrase for encrypting the age private key
|
|
agePrivKeyPassphrase, err := generateRandomPassphrase(agePrivKeyPassphraseLength)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate age private key passphrase: %w", err)
|
|
}
|
|
defer agePrivKeyPassphrase.Destroy()
|
|
|
|
// Step 3: Encrypt age private key with the generated passphrase
|
|
// Create a secure buffer for the private key
|
|
agePrivKeyStr := ageIdentity.String()
|
|
|
|
agePrivKeyBuffer := memguard.NewBufferFromBytes([]byte(agePrivKeyStr))
|
|
defer agePrivKeyBuffer.Destroy()
|
|
|
|
encryptedAgePrivKey, err := EncryptWithPassphrase(
|
|
agePrivKeyBuffer, agePrivKeyPassphrase)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encrypt age private key with passphrase: %w", err)
|
|
}
|
|
|
|
// Step 4: Get or derive the long-term private key
|
|
ltPrivKeyData, err := getLongTermPrivateKey(fs, vault, mnemonic, passphrase)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer ltPrivKeyData.Destroy()
|
|
|
|
// Step 5: Encrypt long-term private key to the new age unlocker
|
|
encryptedLtPrivKeyToAge, err := EncryptToRecipient(
|
|
ltPrivKeyData, ageIdentity.Recipient())
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"failed to encrypt long-term private key to age unlocker: %w", err)
|
|
}
|
|
|
|
// Step 6: Prepare keychain data
|
|
keychainData := KeychainData{
|
|
AgePublicKey: ageRecipient,
|
|
AgePrivKeyPassphrase: agePrivKeyPassphrase,
|
|
EncryptedLongtermKey: hex.EncodeToString(encryptedLtPrivKeyToAge),
|
|
}
|
|
|
|
keychainDataBuffer, err := keychainData.encode()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encode keychain data: %w", err)
|
|
}
|
|
defer keychainDataBuffer.Destroy()
|
|
|
|
return writeKeychainUnlocker(fs, unlockerDir, keychainItemName, ageRecipient,
|
|
encryptedAgePrivKey, encryptedLtPrivKeyToAge, keychainDataBuffer)
|
|
}
|
|
|
|
// writeKeychainUnlocker writes a new keychain unlocker into unlockerDir and
|
|
// stores its data in the keychain (steps 7 and 8 of CreateKeychainUnlocker).
|
|
func writeKeychainUnlocker(
|
|
fs afero.Fs, unlockerDir, keychainItemName, ageRecipient string,
|
|
encryptedAgePrivKey, encryptedLtPrivKey []byte,
|
|
keychainDataBuffer *memguard.LockedBuffer,
|
|
) (*KeychainUnlocker, error) {
|
|
// Step 7: Prepare enhanced metadata
|
|
keychainMetadata := KeychainUnlockerMetadata{
|
|
UnlockerMetadata: UnlockerMetadata{
|
|
Type: keychainUnlockerType,
|
|
CreatedAt: time.Now(),
|
|
Flags: []string{keychainUnlockerType, macOSFlag},
|
|
},
|
|
KeychainItemName: keychainItemName,
|
|
}
|
|
|
|
metadataBytes, err := json.MarshalIndent(keychainMetadata, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal unlocker metadata: %w", err)
|
|
}
|
|
|
|
// Step 8: Write the unlocker's files and store the data in the keychain,
|
|
// the metadata last
|
|
err = WriteDir(fs, unlockerDir, func(dir string) error {
|
|
err := WriteFileAtomic(fs, filepath.Join(dir, "pub.txt"), []byte(ageRecipient))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write age recipient: %w", err)
|
|
}
|
|
|
|
err = WriteFileAtomic(fs, filepath.Join(dir, "priv.age"), encryptedAgePrivKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write encrypted age private key: %w", err)
|
|
}
|
|
|
|
err = WriteFileAtomic(fs, filepath.Join(dir, "longterm.age"), encryptedLtPrivKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
|
}
|
|
|
|
err = storeInKeychain(keychainItemName, keychainDataBuffer)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store data in keychain: %w", err)
|
|
}
|
|
|
|
err = WriteFileAtomic(fs, filepath.Join(dir, "unlocker-metadata.json"),
|
|
metadataBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write unlocker metadata: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &KeychainUnlocker{
|
|
Directory: unlockerDir,
|
|
Metadata: keychainMetadata.UnlockerMetadata,
|
|
fs: fs,
|
|
}, nil
|
|
}
|
|
|
|
// checkMacOSAvailable verifies that we're running on macOS
|
|
func checkMacOSAvailable() error {
|
|
if runtime.GOOS != "darwin" {
|
|
return fmt.Errorf("%w, current OS: %s", errNotMacOS, runtime.GOOS)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateKeychainItemName validates that a keychain item name is safe for
|
|
// command execution
|
|
func validateKeychainItemName(itemName string) error {
|
|
if itemName == "" {
|
|
return errKeychainItemNameEmpty
|
|
}
|
|
|
|
if !keychainItemNameRegex.MatchString(itemName) {
|
|
return fmt.Errorf("%w: %s", errInvalidKeychainItemName, itemName)
|
|
}
|
|
|
|
return nil
|
|
}
|