|
|
|
|
@@ -60,10 +60,7 @@ func (s *SecureEnclaveUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
|
|
|
|
encryptedPath := filepath.Join(s.Directory, seLongtermFilename)
|
|
|
|
|
encryptedData, err := afero.ReadFile(s.fs, encryptedPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
|
"failed to read SE-encrypted long-term key: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
return nil, fmt.Errorf("failed to read SE-encrypted long-term key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DebugWith("Read SE-encrypted long-term key",
|
|
|
|
|
@@ -73,10 +70,7 @@ func (s *SecureEnclaveUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
|
|
|
|
// Decrypt using the Secure Enclave (ECDH happens inside SE hardware)
|
|
|
|
|
decryptedData, err := macse.Decrypt(seKeyLabel, encryptedData)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
|
"failed to decrypt long-term key with SE: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
return nil, fmt.Errorf("failed to decrypt long-term key with SE: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse the decrypted long-term private key
|
|
|
|
|
@@ -88,10 +82,7 @@ func (s *SecureEnclaveUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
|
"failed to parse long-term private key: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
return nil, fmt.Errorf("failed to parse long-term private key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DebugWith("Successfully decrypted long-term key via SE",
|
|
|
|
|
@@ -174,11 +165,7 @@ func (s *SecureEnclaveUnlocker) getSEKeyInfo() (label string, hash string, err e
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewSecureEnclaveUnlocker creates a new SecureEnclaveUnlocker instance.
|
|
|
|
|
func NewSecureEnclaveUnlocker(
|
|
|
|
|
fs afero.Fs,
|
|
|
|
|
directory string,
|
|
|
|
|
metadata UnlockerMetadata,
|
|
|
|
|
) *SecureEnclaveUnlocker {
|
|
|
|
|
func NewSecureEnclaveUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *SecureEnclaveUnlocker {
|
|
|
|
|
return &SecureEnclaveUnlocker{
|
|
|
|
|
Directory: directory,
|
|
|
|
|
Metadata: metadata,
|
|
|
|
|
@@ -195,22 +182,13 @@ func generateSEKeyLabel(vaultName string) (string, error) {
|
|
|
|
|
|
|
|
|
|
enrollmentDate := time.Now().UTC().Format("2006-01-02")
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf(
|
|
|
|
|
"%s.%s-%s-%s",
|
|
|
|
|
seKeyLabelPrefix,
|
|
|
|
|
vaultName,
|
|
|
|
|
hostname,
|
|
|
|
|
enrollmentDate,
|
|
|
|
|
), nil
|
|
|
|
|
return fmt.Sprintf("%s.%s-%s-%s", seKeyLabelPrefix, vaultName, hostname, enrollmentDate), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateSecureEnclaveUnlocker creates a new SE unlocker.
|
|
|
|
|
// The vault's long-term private key is encrypted directly by the Secure Enclave
|
|
|
|
|
// using ECIES. No intermediate age keypair is used.
|
|
|
|
|
func CreateSecureEnclaveUnlocker(
|
|
|
|
|
fs afero.Fs,
|
|
|
|
|
stateDir string,
|
|
|
|
|
) (*SecureEnclaveUnlocker, error) {
|
|
|
|
|
func CreateSecureEnclaveUnlocker(fs afero.Fs, stateDir string) (*SecureEnclaveUnlocker, error) {
|
|
|
|
|
if err := checkMacOSAvailable(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
@@ -238,20 +216,14 @@ func CreateSecureEnclaveUnlocker(
|
|
|
|
|
// Step 2: Get the vault's long-term private key
|
|
|
|
|
ltPrivKeyData, err := getLongTermKeyForSE(fs, vault)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
|
"failed to get long-term private key: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
return nil, fmt.Errorf("failed to get long-term private key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer ltPrivKeyData.Destroy()
|
|
|
|
|
|
|
|
|
|
// Step 3: Encrypt the long-term key directly with the SE (ECIES)
|
|
|
|
|
encryptedLtKey, err := macse.Encrypt(seKeyLabel, ltPrivKeyData.Bytes())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
|
"failed to encrypt long-term key with SE: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
return nil, fmt.Errorf("failed to encrypt long-term key with SE: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 4: Create unlocker directory and write files
|
|
|
|
|
@@ -263,19 +235,13 @@ func CreateSecureEnclaveUnlocker(
|
|
|
|
|
unlockerDirName := fmt.Sprintf("se-%s", filepath.Base(seKeyLabel))
|
|
|
|
|
unlockerDir := filepath.Join(vaultDir, "unlockers.d", unlockerDirName)
|
|
|
|
|
if err := fs.MkdirAll(unlockerDir, DirPerms); err != nil {
|
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
|
"failed to create unlocker directory: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
return nil, fmt.Errorf("failed to create unlocker directory: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write SE-encrypted long-term key
|
|
|
|
|
ltKeyPath := filepath.Join(unlockerDir, seLongtermFilename)
|
|
|
|
|
if err := afero.WriteFile(fs, ltKeyPath, encryptedLtKey, FilePerms); err != nil {
|
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
|
"failed to write SE-encrypted long-term key: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
return nil, fmt.Errorf("failed to write SE-encrypted long-term key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write metadata
|
|
|
|
|
@@ -308,10 +274,7 @@ func CreateSecureEnclaveUnlocker(
|
|
|
|
|
|
|
|
|
|
// getLongTermKeyForSE retrieves the vault's long-term private key
|
|
|
|
|
// either from the mnemonic env var or by unlocking via the current unlocker.
|
|
|
|
|
func getLongTermKeyForSE(
|
|
|
|
|
fs afero.Fs,
|
|
|
|
|
vault VaultInterface,
|
|
|
|
|
) (*memguard.LockedBuffer, error) {
|
|
|
|
|
func getLongTermKeyForSE(fs afero.Fs, vault VaultInterface) (*memguard.LockedBuffer, error) {
|
|
|
|
|
envMnemonic := os.Getenv(EnvMnemonic)
|
|
|
|
|
if envMnemonic != "" {
|
|
|
|
|
// Read vault metadata to get the correct derivation index
|
|
|
|
|
@@ -332,16 +295,9 @@ func getLongTermKeyForSE(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use mnemonic with the vault's actual derivation index
|
|
|
|
|
ltIdentity, err := agehd.DeriveIdentity(
|
|
|
|
|
envMnemonic,
|
|
|
|
|
metadata.DerivationIndex,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ltIdentity, err := agehd.DeriveIdentity(envMnemonic, metadata.DerivationIndex)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
|
"failed to derive long-term key from mnemonic: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return memguard.NewBufferFromBytes([]byte(ltIdentity.String())), nil
|
|
|
|
|
@@ -354,29 +310,17 @@ func getLongTermKeyForSE(
|
|
|
|
|
|
|
|
|
|
currentIdentity, err := currentUnlocker.GetIdentity()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
|
"failed to get current unlocker identity: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
return nil, fmt.Errorf("failed to get current unlocker identity: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All unlocker types store longterm.age in their directory
|
|
|
|
|
longtermPath := filepath.Join(
|
|
|
|
|
currentUnlocker.GetDirectory(),
|
|
|
|
|
"longterm.age",
|
|
|
|
|
)
|
|
|
|
|
longtermPath := filepath.Join(currentUnlocker.GetDirectory(), "longterm.age")
|
|
|
|
|
encryptedLtKey, err := afero.ReadFile(fs, longtermPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
|
"failed to read encrypted long-term key: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
return nil, fmt.Errorf("failed to read encrypted long-term key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ltPrivKeyBuffer, err := DecryptWithIdentity(
|
|
|
|
|
encryptedLtKey,
|
|
|
|
|
currentIdentity,
|
|
|
|
|
)
|
|
|
|
|
ltPrivKeyBuffer, err := DecryptWithIdentity(encryptedLtKey, currentIdentity)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to decrypt long-term key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|