Files
Firefox_Secure/FirefoxSessionVault.ps1
2026-06-25 15:37:48 +02:00

1173 lines
34 KiB
PowerShell

param(
[ValidateSet("Toggle", "Unlock", "Lock", "Setup", "Status", "Shortcut", "WatchClose", "Reset")]
[string]$Mode = "Toggle",
[string]$ProfilePath = ""
)
$ErrorActionPreference = "Stop"
$BaseDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$StateDir = Join-Path $BaseDir ".firefox-session-vault"
$ConfigPath = Join-Path $StateDir "config.json"
$VaultDir = Join-Path $StateDir "vault"
$RawVaultDir = Join-Path $StateDir "raw-vault"
$DecoyProfileDir = Join-Path $StateDir "decoy-profile"
$ProtectedRelativePaths = @(
"cookies.sqlite",
"cookies.sqlite-wal",
"webappsstore.sqlite",
"webappsstore.sqlite-wal",
"webappsstore.sqlite-shm",
"storage\ls-archive.sqlite",
"sessionstore.jsonlz4",
"sessionstore-backups\recovery.jsonlz4",
"sessionstore-backups\recovery.baklz4",
"sessionstore-backups\previous.jsonlz4"
)
Add-Type -AssemblyName System.Security
Add-Type -AssemblyName System.IO.Compression.FileSystem
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$CharLowerAe = [char]0x00E4
$CharLowerOe = [char]0x00F6
$CharLowerUe = [char]0x00FC
$CharEszett = [char]0x00DF
$CharUpperOe = [char]0x00D6
function Ensure-StateDir {
if (-not (Test-Path -LiteralPath $StateDir)) {
New-Item -ItemType Directory -Path $StateDir | Out-Null
}
if (-not (Test-Path -LiteralPath $VaultDir)) {
New-Item -ItemType Directory -Path $VaultDir | Out-Null
}
if (-not (Test-Path -LiteralPath $RawVaultDir)) {
New-Item -ItemType Directory -Path $RawVaultDir | Out-Null
}
}
function Convert-SecureStringToPlainText {
param([Security.SecureString]$SecureString)
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
try {
return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
}
finally {
if ($bstr -ne [IntPtr]::Zero) {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
}
}
function New-RandomBytes {
param([int]$Length)
$bytes = New-Object byte[] $Length
$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
try {
$rng.GetBytes($bytes)
return $bytes
}
finally {
$rng.Dispose()
}
}
function Get-DerivedBytes {
param(
[string]$Pin,
[byte[]]$Salt,
[int]$Iterations,
[int]$Length
)
try {
$pbkdf = [Security.Cryptography.Rfc2898DeriveBytes]::new(
$Pin,
$Salt,
$Iterations,
[Security.Cryptography.HashAlgorithmName]::SHA256
)
}
catch {
$pbkdf = New-Object Security.Cryptography.Rfc2898DeriveBytes($Pin, $Salt, $Iterations)
}
try {
return $pbkdf.GetBytes($Length)
}
finally {
$pbkdf.Dispose()
}
}
function Get-PinHash {
param(
[string]$Pin,
[byte[]]$Salt,
[int]$Iterations
)
return Get-DerivedBytes -Pin $Pin -Salt $Salt -Iterations $Iterations -Length 32
}
function Read-Config {
if (-not (Test-Path -LiteralPath $ConfigPath)) {
return $null
}
return Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json
}
function Save-Config {
param([object]$Config)
Ensure-StateDir
$Config | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $ConfigPath -Encoding UTF8
}
function Save-ConfigObject {
param([object]$Config)
Save-Config $Config
}
function Read-FirefoxProfilesIni {
$profilesIni = Join-Path $env:APPDATA "Mozilla\Firefox\profiles.ini"
if (-not (Test-Path -LiteralPath $profilesIni)) {
throw "Firefox profiles.ini wurde nicht gefunden. Starte Firefox einmal oder ${CharLowerUe}bergib -ProfilePath."
}
$sections = @()
$current = $null
foreach ($line in Get-Content -LiteralPath $profilesIni) {
$trimmed = $line.Trim()
if ($trimmed -match "^\[(.+)\]$") {
if ($current) {
$sections += [pscustomobject]$current
}
$current = [ordered]@{}
$current.Section = $matches[1]
continue
}
if ($current -and $trimmed -match "^([^=]+)=(.*)$") {
$current[$matches[1]] = $matches[2]
}
}
if ($current) {
$sections += [pscustomobject]$current
}
return $sections
}
function Convert-FirefoxProfilePath {
param([object]$Profile)
if ($Profile.IsRelative -eq "1") {
return Join-Path (Join-Path $env:APPDATA "Mozilla\Firefox") $Profile.Path
}
return $Profile.Path
}
function Resolve-FirefoxProfilePathFromProfilesIni {
$sections = Read-FirefoxProfilesIni
$install = $sections |
Where-Object { $_.Section -like "Install*" -and $_.Default } |
Select-Object -First 1
if ($install) {
$installPath = Join-Path (Join-Path $env:APPDATA "Mozilla\Firefox") $install.Default
if (Test-Path -LiteralPath $installPath) {
return $installPath
}
}
$profile = $sections |
Where-Object { $_.Section -like "Profile*" -and $_.Default -eq "1" -and $_.Path } |
Select-Object -First 1
if (-not $profile) {
$profile = $sections |
Where-Object { $_.Section -like "Profile*" -and $_.Name -eq "default-release" -and $_.Path } |
Select-Object -First 1
}
if (-not $profile) {
$profile = $sections |
Where-Object { $_.Section -like "Profile*" -and $_.Path } |
Select-Object -First 1
}
if (-not $profile) {
throw "Kein Firefox-Profil gefunden."
}
return Convert-FirefoxProfilePath $profile
}
function Join-ByteArrays {
param([byte[][]]$Arrays)
$length = 0
foreach ($array in $Arrays) {
$length += $array.Length
}
$result = New-Object byte[] $length
$offset = 0
foreach ($array in $Arrays) {
[Array]::Copy($array, 0, $result, $offset, $array.Length)
$offset += $array.Length
}
return $result
}
function Protect-BytesForUser {
param([byte[]]$Bytes)
return [Security.Cryptography.ProtectedData]::Protect(
$Bytes,
$null,
[Security.Cryptography.DataProtectionScope]::CurrentUser
)
}
function Unprotect-BytesForUser {
param([byte[]]$Bytes)
return [Security.Cryptography.ProtectedData]::Unprotect(
$Bytes,
$null,
[Security.Cryptography.DataProtectionScope]::CurrentUser
)
}
function Get-VaultKey {
param([object]$Config)
if ($Config.PSObject.Properties.Name -contains "vaultKeyProtected") {
return Unprotect-BytesForUser ([Convert]::FromBase64String($Config.vaultKeyProtected))
}
$key = New-RandomBytes 32
$Config | Add-Member -NotePropertyName "vaultKeyProtected" -NotePropertyValue ([Convert]::ToBase64String((Protect-BytesForUser $key))) -Force
$Config | Add-Member -NotePropertyName "version" -NotePropertyValue 2 -Force
Save-ConfigObject $Config
return $key
}
function Update-ConfigSchema {
param([object]$Config)
$changed = $false
$resolvedProfile = Resolve-FirefoxProfilePathFromProfilesIni
$configuredProfile = $Config.profilePath
if (-not $configuredProfile -or ($configuredProfile -ne $resolvedProfile)) {
$configuredHasSessionData = $false
if ($configuredProfile -and (Test-Path -LiteralPath $configuredProfile)) {
$configuredHasSessionData = (Test-Path -LiteralPath (Join-Path $configuredProfile "cookies.sqlite")) -or
(Test-Path -LiteralPath (Join-Path $configuredProfile "sessionstore.jsonlz4")) -or
(Test-Path -LiteralPath (Join-Path $configuredProfile "sessionstore-backups\previous.jsonlz4"))
}
if (-not $configuredHasSessionData -and (Test-Path -LiteralPath $resolvedProfile)) {
$Config | Add-Member -NotePropertyName "profilePath" -NotePropertyValue $resolvedProfile -Force
$changed = $true
}
}
$currentPaths = @($Config.protectedRelativePaths)
$currentPathText = ($currentPaths | Sort-Object) -join "`n"
$expectedPathText = ($ProtectedRelativePaths | Sort-Object) -join "`n"
if (-not $currentPaths -or $currentPathText -ne $expectedPathText) {
$Config | Add-Member -NotePropertyName "protectedRelativePaths" -NotePropertyValue $ProtectedRelativePaths -Force
$changed = $true
}
if ($Config.PSObject.Properties.Name -notcontains "version" -or [int]$Config.version -lt 2) {
$Config | Add-Member -NotePropertyName "version" -NotePropertyValue 2 -Force
$changed = $true
}
if ($changed) {
Save-ConfigObject $Config
}
return $Config
}
function Resolve-FirefoxProfile {
if ($ProfilePath) {
$resolved = Resolve-Path -LiteralPath $ProfilePath -ErrorAction Stop
return $resolved.Path
}
return Resolve-FirefoxProfilePathFromProfilesIni
}
function Get-FirefoxPath {
$candidates = @(
"$env:ProgramFiles\Mozilla Firefox\firefox.exe",
"${env:ProgramFiles(x86)}\Mozilla Firefox\firefox.exe",
"$env:LocalAppData\Mozilla Firefox\firefox.exe"
)
foreach ($candidate in $candidates) {
if ($candidate -and (Test-Path -LiteralPath $candidate)) {
return $candidate
}
}
$command = Get-Command firefox.exe -ErrorAction SilentlyContinue
if ($command) {
return $command.Source
}
throw "Firefox wurde nicht gefunden."
}
function Read-Pin {
param([string]$Prompt = "PIN")
$secure = Read-Host $Prompt -AsSecureString
return Convert-SecureStringToPlainText $secure
}
function Read-PinOrEscape {
param([string]$Prompt = "PIN")
return Show-PinDialog -Prompt $Prompt
}
function Show-PinDialog {
param([string]$Prompt = "PIN")
[Windows.Forms.Application]::EnableVisualStyles()
$backgroundColor = [Drawing.Color]::FromArgb(32, 32, 38)
$panelColor = [Drawing.Color]::FromArgb(43, 43, 50)
$textColor = [Drawing.Color]::FromArgb(238, 238, 242)
$mutedTextColor = [Drawing.Color]::FromArgb(190, 190, 198)
$accentColor = [Drawing.Color]::FromArgb(0, 120, 212)
$buttonColor = [Drawing.Color]::FromArgb(58, 58, 66)
$form = New-Object Windows.Forms.Form
$form.Text = "Firefox"
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$form.TopMost = $true
$form.ClientSize = New-Object Drawing.Size(380, 164)
$form.Font = New-Object Drawing.Font("Segoe UI", 9)
$form.BackColor = $backgroundColor
$form.ForeColor = $textColor
$label = New-Object Windows.Forms.Label
$label.Text = "$Prompt eingeben"
$label.AutoSize = $false
$label.Location = New-Object Drawing.Point(20, 18)
$label.Size = New-Object Drawing.Size(340, 22)
$label.ForeColor = $textColor
$label.BackColor = $backgroundColor
$form.Controls.Add($label)
$hint = New-Object Windows.Forms.Label
$hint.Text = "ESC ${CharLowerOe}ffnet Firefox ohne private Sitzung."
$hint.AutoSize = $false
$hint.Location = New-Object Drawing.Point(20, 42)
$hint.Size = New-Object Drawing.Size(340, 22)
$hint.ForeColor = $mutedTextColor
$hint.BackColor = $backgroundColor
$form.Controls.Add($hint)
$textBox = New-Object Windows.Forms.TextBox
$textBox.Location = New-Object Drawing.Point(20, 72)
$textBox.Size = New-Object Drawing.Size(340, 24)
$textBox.UseSystemPasswordChar = $true
$textBox.BackColor = $panelColor
$textBox.ForeColor = $textColor
$textBox.BorderStyle = "FixedSingle"
$form.Controls.Add($textBox)
$okButton = New-Object Windows.Forms.Button
$okButton.Text = "Entsperren"
$okButton.Location = New-Object Drawing.Point(164, 114)
$okButton.Size = New-Object Drawing.Size(96, 30)
$okButton.DialogResult = [Windows.Forms.DialogResult]::OK
$okButton.BackColor = $accentColor
$okButton.ForeColor = [Drawing.Color]::White
$okButton.FlatStyle = "Flat"
$okButton.FlatAppearance.BorderSize = 0
$form.Controls.Add($okButton)
$decoyButton = New-Object Windows.Forms.Button
$decoyButton.Text = "Ohne Sitzung"
$decoyButton.Location = New-Object Drawing.Point(266, 114)
$decoyButton.Size = New-Object Drawing.Size(96, 30)
$decoyButton.DialogResult = [Windows.Forms.DialogResult]::Cancel
$decoyButton.BackColor = $buttonColor
$decoyButton.ForeColor = $textColor
$decoyButton.FlatStyle = "Flat"
$decoyButton.FlatAppearance.BorderColor = [Drawing.Color]::FromArgb(80, 80, 88)
$form.Controls.Add($decoyButton)
$form.AcceptButton = $okButton
$form.CancelButton = $decoyButton
$form.Add_Shown({ $textBox.Focus() })
$result = $form.ShowDialog()
$pin = $textBox.Text
$form.Dispose()
return [pscustomobject]@{
Escaped = ($result -ne [Windows.Forms.DialogResult]::OK)
Pin = $pin
}
}
function Get-RawVaultPath {
param([string]$RelativePath)
return Join-Path $RawVaultDir $RelativePath
}
function Move-PathToRawVault {
param(
[string]$SourcePath,
[string]$RelativePath
)
$target = Get-RawVaultPath $RelativePath
if (Test-Path -LiteralPath $target) {
Remove-Item -LiteralPath $target -Recurse -Force
}
$targetParent = Split-Path -Parent $target
if (-not (Test-Path -LiteralPath $targetParent)) {
New-Item -ItemType Directory -Path $targetParent | Out-Null
}
Move-Item -LiteralPath $SourcePath -Destination $target
}
function Restore-PathFromRawVault {
param(
[string]$TargetPath,
[string]$RelativePath
)
$source = Get-RawVaultPath $RelativePath
if (-not (Test-Path -LiteralPath $source)) {
return $false
}
if (Test-Path -LiteralPath $TargetPath) {
Remove-Item -LiteralPath $TargetPath -Recurse -Force
}
if ($TargetPath.EndsWith(".sqlite")) {
Remove-SqliteTransientFilesBeforeRestore -Path $TargetPath
}
$targetParent = Split-Path -Parent $TargetPath
if (-not (Test-Path -LiteralPath $targetParent)) {
New-Item -ItemType Directory -Path $targetParent | Out-Null
}
Move-Item -LiteralPath $source -Destination $TargetPath
return $true
}
function Get-EncryptedVaultPath {
param([string]$RelativePath)
return Join-Path $VaultDir ($RelativePath + ".ffsv")
}
function Protect-PathToEncryptedVault {
param(
[string]$SourcePath,
[string]$RelativePath,
[byte[]]$Key
)
$item = Get-Item -LiteralPath $SourcePath
if (-not $item.PSIsContainer) {
Protect-BytesToVault -Plain ([IO.File]::ReadAllBytes($SourcePath)) -VaultPath (Get-EncryptedVaultPath $RelativePath) -Key $Key -PayloadType 1
return
}
$root = $item.FullName.TrimEnd('\')
$files = Get-ChildItem -LiteralPath $SourcePath -Recurse -File -Force
foreach ($file in $files) {
$childRelative = $file.FullName.Substring($root.Length).TrimStart('\')
$vaultRelative = Join-Path $RelativePath $childRelative
Protect-BytesToVault -Plain ([IO.File]::ReadAllBytes($file.FullName)) -VaultPath (Get-EncryptedVaultPath $vaultRelative) -Key $Key -PayloadType 1
}
}
function Restore-EncryptedVaultItem {
param(
[string]$TargetPath,
[string]$RelativePath,
[byte[]]$Key,
[string]$Pin,
[int]$Iterations
)
$vaultFile = Get-EncryptedVaultPath $RelativePath
if (Test-Path -LiteralPath $vaultFile) {
Unprotect-VaultToPath -VaultPath $vaultFile -TargetPath $TargetPath -Key $Key -Pin $Pin -Iterations $Iterations
Remove-Item -LiteralPath $vaultFile -Force
return $true
}
$vaultDirectory = Join-Path $VaultDir $RelativePath
if (-not (Test-Path -LiteralPath $vaultDirectory)) {
return $false
}
if (Test-Path -LiteralPath $TargetPath) {
Remove-Item -LiteralPath $TargetPath -Recurse -Force
}
New-Item -ItemType Directory -Path $TargetPath | Out-Null
$files = Get-ChildItem -LiteralPath $vaultDirectory -Recurse -File -Filter "*.ffsv" -Force
foreach ($file in $files) {
$childRelative = $file.FullName.Substring($vaultDirectory.Length).TrimStart('\')
$childRelative = $childRelative.Substring(0, $childRelative.Length - 5)
$targetChild = Join-Path $TargetPath $childRelative
Unprotect-VaultToPath -VaultPath $file.FullName -TargetPath $targetChild -Key $Key -Pin $Pin -Iterations $Iterations
Remove-Item -LiteralPath $file.FullName -Force
}
$remaining = Get-ChildItem -LiteralPath $vaultDirectory -Recurse -Force -ErrorAction SilentlyContinue
if (-not $remaining) {
Remove-Item -LiteralPath $vaultDirectory -Recurse -Force
}
return $true
}
function Get-LockRelativePaths {
param([object]$Config)
$paths = New-Object System.Collections.Generic.List[string]
foreach ($path in @($Config.protectedRelativePaths)) {
if ($path -and $path -ne "storage") {
$paths.Add($path)
}
}
$storageDefault = Join-Path $Config.profilePath "storage\default"
if (Test-Path -LiteralPath $storageDefault) {
$siteStorage = Get-ChildItem -LiteralPath $storageDefault -Directory -Force |
Where-Object { $_.Name -like "http*" -or $_.Name -like "file*" }
foreach ($site in $siteStorage) {
$paths.Add(("storage\default\" + $site.Name))
}
}
return $paths | Select-Object -Unique
}
function Restore-AllEncryptedVaultItems {
param(
[object]$Config,
[byte[]]$Key,
[string]$Pin
)
if (-not (Test-Path -LiteralPath $VaultDir)) {
return
}
$vaultFiles = Get-ChildItem -LiteralPath $VaultDir -Recurse -File -Filter "*.ffsv" -Force |
Sort-Object FullName
foreach ($vaultFile in $vaultFiles) {
$relative = $vaultFile.FullName.Substring($VaultDir.Length).TrimStart('\')
$relative = $relative.Substring(0, $relative.Length - 5)
$target = Join-Path $Config.profilePath $relative
Unprotect-VaultToPath -VaultPath $vaultFile.FullName -TargetPath $target -Key $Key -Pin $Pin -Iterations ([int]$Config.iterations)
Remove-Item -LiteralPath $vaultFile.FullName -Force
}
}
function Get-DecoyProfilePath {
Ensure-StateDir
if (-not (Test-Path -LiteralPath $DecoyProfileDir)) {
New-Item -ItemType Directory -Path $DecoyProfileDir | Out-Null
}
$prefsPath = Join-Path $DecoyProfileDir "prefs.js"
if (-not (Test-Path -LiteralPath $prefsPath)) {
@(
'user_pref("browser.startup.page", 0);',
'user_pref("browser.shell.checkDefaultBrowser", false);'
) | Set-Content -LiteralPath $prefsPath -Encoding UTF8
}
return $DecoyProfileDir
}
function Start-DecoyFirefox {
$decoyProfile = Get-DecoyProfilePath
Write-Host "${CharUpperOe}ffne Firefox ohne private Sitzung..."
Start-Process -FilePath (Get-FirefoxPath) -ArgumentList @("-no-remote", "-profile", "`"$decoyProfile`"")
}
function Start-RealFirefox {
param([string]$Profile)
Write-Host "${CharUpperOe}ffne dein Firefox-Profil..."
Start-Process -FilePath (Get-FirefoxPath) -ArgumentList @("-no-remote", "-profile", "`"$Profile`"") | Out-Null
Start-WatchOnClose -Profile $Profile
}
function Start-WatchOnClose {
param([string]$Profile)
$powershell = (Get-Process -Id $PID).Path
$arguments = @(
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-WindowStyle", "Hidden",
"-File", "`"$PSCommandPath`"",
"-Mode", "WatchClose",
"-ProfilePath", "`"$Profile`""
)
Start-Process -FilePath $powershell -ArgumentList $arguments -WindowStyle Hidden | Out-Null
}
function Invoke-WatchClose {
Start-Sleep -Seconds 5
$deadline = (Get-Date).AddSeconds(30)
while ((Get-Date) -lt $deadline) {
if (Get-Process firefox -ErrorAction SilentlyContinue) {
break
}
Start-Sleep -Seconds 1
}
while (Get-Process firefox -ErrorAction SilentlyContinue) {
Start-Sleep -Seconds 2
}
Invoke-Lock
}
function Test-Pin {
param(
[object]$Config,
[string]$Pin
)
$salt = [Convert]::FromBase64String($Config.pinSalt)
$expected = [Convert]::FromBase64String($Config.pinHash)
$actual = Get-PinHash -Pin $Pin -Salt $salt -Iterations ([int]$Config.iterations)
if ($actual.Length -ne $expected.Length) {
return $false
}
$diff = 0
for ($i = 0; $i -lt $actual.Length; $i++) {
$diff = $diff -bor ($actual[$i] -bxor $expected[$i])
}
return $diff -eq 0
}
function Get-AesFromPin {
param(
[string]$Pin,
[byte[]]$Salt,
[byte[]]$Iv,
[int]$Iterations
)
$aes = [Security.Cryptography.Aes]::Create()
$aes.Mode = [Security.Cryptography.CipherMode]::CBC
$aes.Padding = [Security.Cryptography.PaddingMode]::PKCS7
$aes.Key = Get-DerivedBytes -Pin $Pin -Salt $Salt -Iterations $Iterations -Length 32
$aes.IV = $Iv
return $aes
}
function Get-AesFromKey {
param(
[byte[]]$Key,
[byte[]]$Iv
)
$aes = [Security.Cryptography.Aes]::Create()
$aes.Mode = [Security.Cryptography.CipherMode]::CBC
$aes.Padding = [Security.Cryptography.PaddingMode]::PKCS7
$aes.Key = $Key
$aes.IV = $Iv
return $aes
}
function Remove-SqliteShm {
param([string]$Path)
if (-not ($Path.EndsWith(".sqlite"))) {
return
}
$sidecar = $Path + "-shm"
if (Test-Path -LiteralPath $sidecar) {
Remove-Item -LiteralPath $sidecar -Force
}
}
function Remove-SqliteTransientFilesBeforeRestore {
param([string]$Path)
if (-not ($Path.EndsWith(".sqlite"))) {
return
}
foreach ($suffix in @("-shm", "-wal")) {
$sidecar = $Path + $suffix
if (Test-Path -LiteralPath $sidecar) {
Remove-Item -LiteralPath $sidecar -Force
}
}
}
function Protect-BytesToVault {
param(
[byte[]]$Plain,
[string]$VaultPath,
[byte[]]$Key,
[byte]$PayloadType
)
$iv = New-RandomBytes 16
$aes = Get-AesFromKey -Key $Key -Iv $iv
try {
$encryptor = $aes.CreateEncryptor()
$cipher = $encryptor.TransformFinalBlock($Plain, 0, $Plain.Length)
$payload = Join-ByteArrays -Arrays @([byte[]](70, 70, 83, 86, 3, $PayloadType), $iv, $cipher)
$vaultParent = Split-Path -Parent $VaultPath
if (-not (Test-Path -LiteralPath $vaultParent)) {
New-Item -ItemType Directory -Path $vaultParent | Out-Null
}
[IO.File]::WriteAllBytes($VaultPath, $payload)
}
finally {
$aes.Dispose()
}
}
function Protect-Path {
param(
[string]$SourcePath,
[string]$VaultPath,
[byte[]]$Key
)
$item = Get-Item -LiteralPath $SourcePath
if ($item.PSIsContainer) {
$tempZip = Join-Path $StateDir ("vault-" + [guid]::NewGuid().ToString("N") + ".zip")
try {
[IO.Compression.ZipFile]::CreateFromDirectory($SourcePath, $tempZip, [IO.Compression.CompressionLevel]::Optimal, $false)
Protect-BytesToVault -Plain ([IO.File]::ReadAllBytes($tempZip)) -VaultPath $VaultPath -Key $Key -PayloadType 2
}
finally {
if (Test-Path -LiteralPath $tempZip) {
Remove-Item -LiteralPath $tempZip -Force
}
}
return
}
Protect-BytesToVault -Plain ([IO.File]::ReadAllBytes($SourcePath)) -VaultPath $VaultPath -Key $Key -PayloadType 1
}
function Unprotect-VaultToPath {
param(
[string]$VaultPath,
[string]$TargetPath,
[byte[]]$Key,
[string]$Pin = "",
[int]$Iterations = 0
)
$payload = [IO.File]::ReadAllBytes($VaultPath)
if ($payload.Length -lt 21 -or $payload[0] -ne 70 -or $payload[1] -ne 70 -or $payload[2] -ne 83 -or $payload[3] -ne 86) {
throw "Invalid vault file: $VaultPath"
}
$version = $payload[4]
$payloadType = 1
if ($version -eq 1) {
if (-not $Pin) {
throw "Alte Vault-Datei benötigt PIN-basiertes Entsperren: $VaultPath"
}
$salt = $payload[5..20]
$iv = $payload[21..36]
$cipher = $payload[37..($payload.Length - 1)]
$aes = Get-AesFromPin -Pin $Pin -Salt $salt -Iv $iv -Iterations $Iterations
}
elseif ($version -eq 2) {
$iv = $payload[5..20]
$cipher = $payload[21..($payload.Length - 1)]
$aes = Get-AesFromKey -Key $Key -Iv $iv
}
elseif ($version -eq 3) {
$payloadType = $payload[5]
$iv = $payload[6..21]
$cipher = $payload[22..($payload.Length - 1)]
$aes = Get-AesFromKey -Key $Key -Iv $iv
}
else {
throw "Unsupported vault file version: $version"
}
try {
$decryptor = $aes.CreateDecryptor()
$plain = $decryptor.TransformFinalBlock($cipher, 0, $cipher.Length)
if (Test-Path -LiteralPath $TargetPath) {
Remove-Item -LiteralPath $TargetPath -Recurse -Force
}
if ($payloadType -eq 2) {
$tempZip = Join-Path $StateDir ("restore-" + [guid]::NewGuid().ToString("N") + ".zip")
try {
[IO.File]::WriteAllBytes($tempZip, $plain)
New-Item -ItemType Directory -Path $TargetPath | Out-Null
[IO.Compression.ZipFile]::ExtractToDirectory($tempZip, $TargetPath)
}
finally {
if (Test-Path -LiteralPath $tempZip) {
Remove-Item -LiteralPath $tempZip -Force
}
}
}
else {
$targetParent = Split-Path -Parent $TargetPath
if (-not (Test-Path -LiteralPath $targetParent)) {
New-Item -ItemType Directory -Path $targetParent | Out-Null
}
Remove-SqliteTransientFilesBeforeRestore -Path $TargetPath
[IO.File]::WriteAllBytes($TargetPath, $plain)
}
}
finally {
$aes.Dispose()
}
}
function Stop-Firefox {
$processes = Get-Process firefox -ErrorAction SilentlyContinue
if ($processes) {
Write-Host "Schlie${CharEszett}e Firefox..."
foreach ($process in $processes) {
try {
$null = $process.CloseMainWindow()
}
catch {
}
}
$deadline = (Get-Date).AddSeconds(45)
while ((Get-Date) -lt $deadline) {
Start-Sleep -Milliseconds 500
if (-not (Get-Process firefox -ErrorAction SilentlyContinue)) {
return $true
}
}
Write-Host "Firefox wurde nicht sauber geschlossen. Sperren abgebrochen, um Sitzungsdaten nicht zu besch${CharLowerAe}digen."
Write-Host "Schlie${CharEszett}e Firefox manuell und starte den Launcher erneut."
return $false
}
return $true
}
function Set-ResumeSessionOnce {
param([string]$Profile)
$prefsPath = Join-Path $Profile "prefs.js"
$line = 'user_pref("browser.sessionstore.resume_session_once", true);'
if (-not (Test-Path -LiteralPath $prefsPath)) {
Set-Content -LiteralPath $prefsPath -Value $line -Encoding UTF8
return
}
$prefs = Get-Content -LiteralPath $prefsPath
$pattern = '^\s*user_pref\("browser\.sessionstore\.resume_session_once",'
$found = $false
$updated = foreach ($pref in $prefs) {
if ($pref -match $pattern) {
$found = $true
$line
}
else {
$pref
}
}
if (-not $found) {
$updated += $line
}
Set-Content -LiteralPath $prefsPath -Value $updated -Encoding UTF8
}
function Invoke-Setup {
Ensure-StateDir
$profile = Resolve-FirefoxProfile
Write-Host "Firefox-Profil: $profile"
while ($true) {
$pin1 = Read-Pin "New vault PIN"
$pin2 = Read-Pin "Repeat vault PIN"
try {
if ($pin1.Length -lt 4) {
Write-Host "PIN muss mindestens 4 Zeichen haben."
continue
}
if ($pin1 -ne $pin2) {
Write-Host "PINs stimmen nicht ${CharLowerUe}berein."
continue
}
$iterations = 210000
$pinSalt = New-RandomBytes 16
$pinHash = Get-PinHash -Pin $pin1 -Salt $pinSalt -Iterations $iterations
Save-Config ([pscustomobject]@{
version = 2
profilePath = $profile
iterations = $iterations
pinSalt = [Convert]::ToBase64String($pinSalt)
pinHash = [Convert]::ToBase64String($pinHash)
vaultKeyProtected = [Convert]::ToBase64String((Protect-BytesForUser (New-RandomBytes 32)))
protectedRelativePaths = $ProtectedRelativePaths
createdAt = (Get-Date).ToString("o")
})
Write-Host "Einrichtung abgeschlossen."
return
}
finally {
$pin1 = $null
$pin2 = $null
}
}
}
function Get-ConfigOrSetup {
$config = Read-Config
if (-not $config) {
Invoke-Setup
$config = Read-Config
}
return Update-ConfigSchema $config
}
function Invoke-Lock {
Ensure-StateDir
$config = Get-ConfigOrSetup
$key = Get-VaultKey $config
try {
if (-not (Stop-Firefox)) {
exit 1
}
$lockedAny = $false
foreach ($relativePath in (Get-LockRelativePaths $config)) {
$source = Join-Path $config.profilePath $relativePath
if (-not (Test-Path -LiteralPath $source)) {
continue
}
Protect-PathToEncryptedVault -SourcePath $source -RelativePath $relativePath -Key $key
Remove-Item -LiteralPath $source -Recurse -Force
Remove-SqliteShm -Path $source
$lockedAny = $true
}
if ($lockedAny) {
Write-Host "Firefox-Sitzung ist verschl${CharLowerUe}sselt gesperrt."
}
else {
Write-Host "Nichts zu sperren. Firefox ist eventuell bereits gesperrt."
}
}
finally {
$key = $null
}
}
function Invoke-Unlock {
Ensure-StateDir
$config = Get-ConfigOrSetup
$pinResult = Read-PinOrEscape "PIN"
if ($pinResult.Escaped) {
Start-DecoyFirefox
return
}
$pin = $pinResult.Pin
try {
if (-not (Test-Pin -Config $config -Pin $pin)) {
Write-Host "Falscher PIN. Firefox bleibt gesperrt."
Start-Sleep -Seconds 2
exit 1
}
if (Test-FirefoxRunning) {
if (-not (Stop-Firefox)) {
exit 1
}
}
$key = Get-VaultKey $config
foreach ($relativePath in (Get-LockRelativePaths $config)) {
$target = Join-Path $config.profilePath $relativePath
if (Restore-PathFromRawVault -TargetPath $target -RelativePath $relativePath) {
continue
}
}
Restore-AllEncryptedVaultItems -Config $config -Key $key -Pin $pin
Set-ResumeSessionOnce -Profile $config.profilePath
Start-RealFirefox -Profile $config.profilePath
}
finally {
$pin = $null
$key = $null
}
}
function Test-VaultHasFiles {
if (Test-Path -LiteralPath $RawVaultDir) {
$rawVaulted = Get-ChildItem -LiteralPath $RawVaultDir -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { -not $_.PSIsContainer } |
Select-Object -First 1
if ($rawVaulted) {
return $true
}
}
if (Test-Path -LiteralPath $VaultDir) {
$vaulted = Get-ChildItem -LiteralPath $VaultDir -Recurse -File -Filter "*.ffsv" -ErrorAction SilentlyContinue
return [bool]$vaulted
}
return $false
}
function Test-FirefoxRunning {
return [bool](Get-Process firefox -ErrorAction SilentlyContinue)
}
function Invoke-Toggle {
if ((Test-FirefoxRunning) -and -not (Test-VaultHasFiles)) {
Write-Host "Firefox ist aktiv. Sperre Sitzung..."
Invoke-Lock
return
}
if (Test-VaultHasFiles) {
Write-Host "Firefox ist gesperrt. Entsperre..."
}
else {
Write-Host "Firefox l${CharLowerAe}uft nicht. Starte ${CharLowerUe}ber Vault-Entsperrung..."
}
Invoke-Unlock
}
function New-FirefoxShortcut {
Ensure-StateDir
$firefox = Get-FirefoxPath
$shortcutPath = Join-Path $BaseDir "Firefox.lnk"
$launcherPath = Join-Path $BaseDir "FirefoxLauncher.exe"
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut($shortcutPath)
$shortcut.TargetPath = $launcherPath
$shortcut.Arguments = ""
$shortcut.WorkingDirectory = $BaseDir
$shortcut.IconLocation = "$firefox,0"
$shortcut.Description = "Firefox Session Vault"
$shortcut.WindowStyle = 1
$shortcut.Save()
Write-Host "Verkn${CharLowerUe}pfung erstellt: $shortcutPath"
}
function Invoke-Status {
$config = Read-Config
if (-not $config) {
Write-Host "Nicht eingerichtet."
return
}
$config = Update-ConfigSchema $config
$vaulted = @()
if (Test-Path -LiteralPath $VaultDir) {
$vaulted += Get-ChildItem -LiteralPath $VaultDir -Recurse -File -ErrorAction SilentlyContinue
}
if (Test-Path -LiteralPath $RawVaultDir) {
$vaulted += Get-ChildItem -LiteralPath $RawVaultDir -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { -not $_.PSIsContainer }
}
if ($vaulted) {
Write-Host "Gesperrte Elemente im Vault: $($vaulted.Count)"
}
else {
Write-Host "Keine gesperrten Dateien im Vault. Firefox-Sitzungen sind wahrscheinlich entsperrt."
}
Write-Host "Firefox-Profil: $($config.profilePath)"
}
switch ($Mode) {
"Toggle" { Invoke-Toggle }
"Setup" { Invoke-Setup }
"Lock" { Invoke-Lock }
"Unlock" { Invoke-Unlock }
"Shortcut" { New-FirefoxShortcut }
"Status" { Invoke-Status }
"WatchClose" { Invoke-WatchClose }
"Reset" {
Ensure-StateDir
Write-Host "Reset entfernt nur die Vault-Konfiguration, nicht die Firefox-Daten."
if (Test-Path -LiteralPath $ConfigPath) {
Remove-Item -LiteralPath $ConfigPath -Force
}
Write-Host "Reset abgeschlossen."
}
}