#Requires -Version 5.1 param( [Parameter(Mandatory=$true)][ValidateSet('export','restore')][string]$Mode, [Parameter(Mandatory=$true)][string]$Path, [string]$EnvPath = (Join-Path (Split-Path -Parent $PSScriptRoot) '.env') ) $ErrorActionPreference = 'Stop' . (Join-Path $PSScriptRoot 'Initialize-SecurityEnv.ps1') function Protect-CredentialFile([string]$FilePath) { if ($env:OS -eq 'Windows_NT') { $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User $acl = New-Object System.Security.AccessControl.FileSecurity $acl.SetAccessRuleProtection($true, $false) $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($sid, 'FullControl', 'Allow'))) Set-Acl -LiteralPath $FilePath -AclObject $acl } else { & chmod 600 $FilePath; if ($LASTEXITCODE) { throw 'Cannot protect credential file' } } } if (-not (Test-Path -LiteralPath $EnvPath -PathType Leaf)) { throw 'Prepare the deployment .env first' } $EnvPath = [System.IO.Path]::GetFullPath($EnvPath) $Path = [System.IO.Path]::GetFullPath($Path) if ($EnvPath -eq $Path) { throw 'Keep the key backup separate from deployment configuration' } $source = $EnvPath if ($Mode -eq 'restore') { $source = $Path } $content = Get-Content -LiteralPath $source -Raw -Encoding UTF8 $active = Get-EnvValue $content 'CONFIG_CREDENTIAL_ENCRYPTION_ACTIVE_KEY_ID' $ring = Get-EnvValue $content 'CONFIG_CREDENTIAL_ENCRYPTION_KEYRING' $legacy = Get-EnvValue $content 'CONFIG_CREDENTIAL_ALLOW_LEGACY_PLAINTEXT' if (-not $active -or -not $ring) { throw 'Incomplete original keyring; no replacement key will be generated' } if (-not $legacy) { $legacy = 'false' } $keyContent = "CONFIG_CREDENTIAL_ENCRYPTION_ACTIVE_KEY_ID=$active`nCONFIG_CREDENTIAL_ENCRYPTION_KEYRING='$ring'`nCONFIG_CREDENTIAL_ALLOW_LEGACY_PLAINTEXT=$legacy`n" foreach ($name in @('CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY','CONFIG_JWT_SECRET')) { $match = [regex]::Match($content, "(?m)^$name=(.*)$") if ($match.Success) { $keyContent += $match.Value.TrimEnd("`r") + "`n" } elseif ($Mode -eq 'export') { $keyContent += "$name=`n" } } $tmp = $EnvPath + '.credential-keys.' + [Guid]::NewGuid().ToString('N') try { [System.IO.File]::WriteAllText($tmp, '', (New-Object System.Text.UTF8Encoding($false))) Protect-CredentialFile $tmp [System.IO.File]::WriteAllText($tmp, $keyContent, (New-Object System.Text.UTF8Encoding($false))) Initialize-SecurityEnv -Path $tmp -Mode upgrade *> $null if ($Mode -eq 'export') { $reservation = [System.IO.File]::Open($Path, 'CreateNew', 'Write', 'None'); $reservation.Dispose() Protect-CredentialFile $Path [System.IO.File]::WriteAllText($Path, $keyContent, (New-Object System.Text.UTF8Encoding($false))) Write-Host 'Exported full keyring, including old keys. Store privately and separately from database backups.' } else { $original = Get-Content -LiteralPath $EnvPath -Raw -Encoding UTF8 $previous = $EnvPath + '.pre-credential-restore.' + [Guid]::NewGuid().ToString('N') [System.IO.File]::WriteAllText($previous, '', (New-Object System.Text.UTF8Encoding($false))) Protect-CredentialFile $previous [System.IO.File]::WriteAllText($previous, $original, (New-Object System.Text.UTF8Encoding($false))) foreach ($name in @('CONFIG_CREDENTIAL_ENCRYPTION_ACTIVE_KEY_ID','CONFIG_CREDENTIAL_ENCRYPTION_KEYRING','CONFIG_CREDENTIAL_ALLOW_LEGACY_PLAINTEXT','CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY','CONFIG_JWT_SECRET')) { $saved = [regex]::Match($keyContent, "(?m)^$name=(.*)$") if (-not $saved.Success) { continue } $literalLine = $saved.Value.TrimEnd("`r") + "`r`n" $pattern = "(?m)^$name=.*(?:\r?\n)?" if ([regex]::IsMatch($original, $pattern)) { $evaluator = [System.Text.RegularExpressions.MatchEvaluator]{ param($match) return $literalLine }.GetNewClosure() $original = [regex]::Replace($original, $pattern, $evaluator) } else { $original = $original.TrimEnd("`r","`n") + "`r`n" + $literalLine } } [System.IO.File]::WriteAllText($tmp, $original, (New-Object System.Text.UTF8Encoding($false))) Move-Item -LiteralPath $tmp -Destination $EnvPath -Force Protect-CredentialFile $EnvPath Write-Host 'Restored original keyring; other configuration preserved. Recreate all related containers before database restore.' } } finally { if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Force } }