197 lines
5.0 KiB
PowerShell
197 lines
5.0 KiB
PowerShell
|
|
<#
|
|||
|
|
.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
|
|||
|
|
}
|