<# .SYNOPSIS Decrypts and edits rIDE SecureLocalStorage store.dat (DPAPI CurrentUser). Works on Windows PowerShell 5.1 and PowerShell 7+. .PARAMETER UserId The userId subfolder under %AppData%\rIDE\. Use "." with -AppDataOverride "." if you're already in the folder. .PARAMETER Key Optional. If provided, prints only this key's value. .PARAMETER ListKeys Optional. If set, prints only the available keys. .PARAMETER AppDataOverride Optional. Override base AppData path (defaults to [Environment]::GetFolderPath("ApplicationData")). Example: -AppDataOverride "." to use .\store.dat in the current directory. .PARAMETER Raw Optional. Output raw strings (no pretty objects / nested JSON parsing). #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$UserId, [string]$Key, [switch]$ListKeys, [string]$AppDataOverride, [switch]$Raw ) # Ensure DPAPI types exist Add-Type -AssemblyName System.Security # ---- Helpers ---- # Convert PSObject/arrays into pure hashtables/arrays so PS 5.1 works like PS7 -AsHashtable function Convert-PSObjectToHashtable { param([Parameter(ValueFromPipeline=$true)]$InputObject) process { if ($null -eq $InputObject) { return $null } if ($InputObject -is [System.Collections.IDictionary]) { $ht = @{} foreach ($k in $InputObject.Keys) { $ht[$k] = Convert-PSObjectToHashtable $InputObject[$k] } return $ht } if ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) { $arr = @() foreach ($item in $InputObject) { $arr += ,(Convert-PSObjectToHashtable $item) } return $arr } if ($InputObject -is [psobject]) { $ht = @{} foreach ($p in $InputObject.PSObject.Properties) { $ht[$p.Name] = Convert-PSObjectToHashtable $p.Value } return $ht } return $InputObject } } function Get-StorePath { param([string]$UserId, [string]$AppDataOverride) $appData = if ($AppDataOverride) { $AppDataOverride } else { [Environment]::GetFolderPath("ApplicationData") } # Code uses "rIDE" but Windows is case-insensitive $dir = Join-Path (Join-Path $appData "rIDE") $UserId return Join-Path $dir "store.dat" } function Read-DecryptedStore { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { throw "File not found: $Path" } try { $encBytes = [System.IO.File]::ReadAllBytes($Path) } catch { throw "Unable to read file '$Path': $($_.Exception.Message)" } try { $decBytes = [System.Security.Cryptography.ProtectedData]::Unprotect( $encBytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser ) } catch { throw "DPAPI Unprotect failed. Are you running as the SAME Windows user that created the file? Inner: $($_.Exception.Message)" } $json = [System.Text.Encoding]::UTF8.GetString($decBytes) try { $obj = $json | ConvertFrom-Json $dict = Convert-PSObjectToHashtable $obj # PS5.1-safe if ($dict -isnot [hashtable]) { throw "Expected a JSON object/dictionary at the top level." } return $dict } catch { throw "Decrypted bytes were not valid JSON: $($_.Exception.Message)" } } function Try-ParseJson { param([string]$s) try { if ($null -ne $s -and ($s.TrimStart().StartsWith('{') -or $s.TrimStart().StartsWith('['))) { return $s | ConvertFrom-Json } } catch { } return $s } function Write-EncryptedStore { param( [string]$Path, [hashtable]$Store ) $json = $Store | ConvertTo-Json -Depth 10 -Compress $bytes = [System.Text.Encoding]::UTF8.GetBytes($json) $enc = [System.Security.Cryptography.ProtectedData]::Protect( $bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser ) [System.IO.File]::WriteAllBytes($Path, $enc) } function Set-StoreValue { param( [string]$UserId, [string]$Key, [string]$Value, [string]$AppDataOverride ) $path = Get-StorePath -UserId $UserId -AppDataOverride $AppDataOverride $store = Read-DecryptedStore -Path $path $store[$Key] = $Value Write-EncryptedStore -Path $path -Store $store Write-Host "Key '$Key' updated successfully in $path" } # ---- Main (print/list/single-key) ---- $path = Get-StorePath -UserId $UserId -AppDataOverride $AppDataOverride try { $store = Read-DecryptedStore -Path $path } catch { Write-Error $_.Exception.Message exit 1 } if ($ListKeys) { $store.Keys | Sort-Object exit 0 } if ($Key) { if ($store.ContainsKey($Key)) { $val = $store[$Key] if ($Raw) { $val } else { $parsed = Try-ParseJson -s $val $parsed | ConvertTo-Json -Depth 10 } exit 0 } else { Write-Error "Key '$Key' not found. Use -ListKeys to see available keys." exit 2 } } # No key specified: show entire dictionary (pretty where possible) if ($Raw) { $store.GetEnumerator() | Sort-Object Name | ForEach-Object { "{0} = {1}" -f $_.Key, $_.Value } } else { $pretty = @{} foreach ($k in $store.Keys) { $pretty[$k] = Try-ParseJson -s $store[$k] } $pretty | ConvertTo-Json -Depth 10 }