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." } }