<# .SYNOPSIS Универсальный инструмент миграции настроек Zoom (Pure PowerShell) .DESCRIPTION Содержит функции ZoomExport и ZoomImport для выгрузки и загрузки настроек. #> # --- Движок C# для работы с ZMDB.dll и DPAPI --- $CSharpCode = @" using System; using System.Runtime.InteropServices; using System.Text; using System.Collections.Generic; namespace ZoomPorter { public class Engine { [DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool CryptUnprotectData(ref DATA_BLOB pDataIn, StringBuilder ppszDataDescr, ref DATA_BLOB pOptionalEntropy, IntPtr pvReserved, ref CRYPTPROTECT_PROMPTSTRUCT pPromptStruct, int dwFlags, ref DATA_BLOB pDataOut); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct DATA_BLOB { public int cbData; public IntPtr pbData; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct CRYPTPROTECT_PROMPTSTRUCT { public int cbSize; public int dwPromptFlags; public IntPtr hwndApp; public string pszPrompt; } public static string Decrypt(byte[] data) { DATA_BLOB input = new DATA_BLOB { pbData = Marshal.AllocHGlobal(data.Length), cbData = data.Length }; DATA_BLOB output = new DATA_BLOB(); CRYPTPROTECT_PROMPTSTRUCT prompt = new CRYPTPROTECT_PROMPTSTRUCT(); Marshal.Copy(data, 0, input.pbData, data.Length); try { if (CryptUnprotectData(ref input, null, ref output, IntPtr.Zero, ref prompt, 0, ref output)) { byte[] res = new byte[output.cbData]; Marshal.Copy(output.pbData, res, 0, output.cbData); return Encoding.UTF8.GetString(res); } } finally { Marshal.FreeHGlobal(input.pbData); } return null; } [DllImport("ZMDB.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int sqlite3_open(string filename, out IntPtr ppDb); [DllImport("ZMDB.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int sqlite3_key(IntPtr db, byte[] pKey, int nKey); [DllImport("ZMDB.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int sqlite3_exec(IntPtr db, string sql, IntPtr callback, IntPtr arg, out IntPtr errmsg); [DllImport("ZMDB.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int sqlite3_close(IntPtr db); [DllImport("ZMDB.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int sqlite3_prepare_v2(IntPtr db, string zSql, int nByte, out IntPtr ppStmt, IntPtr pzTail); [DllImport("ZMDB.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int sqlite3_step(IntPtr pStmt); [DllImport("ZMDB.dll", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr sqlite3_column_text(IntPtr pStmt, int iCol); [DllImport("ZMDB.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int sqlite3_column_count(IntPtr pStmt); [DllImport("ZMDB.dll", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr sqlite3_column_name(IntPtr pStmt, int iCol); [DllImport("ZMDB.dll", CallingConvention = CallingConvention.Cdecl)] public static extern int sqlite3_finalize(IntPtr pStmt); public static List> Query(IntPtr db, string sql) { var results = new List>(); IntPtr stmt; if (sqlite3_prepare_v2(db, sql, -1, out stmt, IntPtr.Zero) == 0) { int colCount = sqlite3_column_count(stmt); while (sqlite3_step(stmt) == 100) { var row = new Dictionary(); for (int i = 0; i < colCount; i++) { string name = Marshal.PtrToStringAnsi(sqlite3_column_name(stmt, i)); IntPtr vPtr = sqlite3_column_text(stmt, i); row[name] = vPtr == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(vPtr); } results.Add(row); } sqlite3_finalize(stmt); } return results; } } } "@ # --- Engine Management --- function Import-ZoomEngine { # Check if type is already loaded if (-not ([System.Management.Automation.PSTypeName]"ZoomPorter.Engine").Type) { $zoomFiles = Get-ChildItem -Path "C:\Program Files\Zoom\bin\ZMDB.dll", "$env:ProgramFiles\Zoom\bin\ZMDB.dll" -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $zoomFiles) { throw "ZMDB.dll не найдена. Проверьте путь установки Zoom." } # Add bin to path for dependencies $oldPath = [Environment]::GetEnvironmentVariable("PATH") [Environment]::SetEnvironmentVariable("PATH", "$($oldPath);$(Split-Path $zoomFiles.FullName)", "Process") try { Add-Type -TypeDefinition $CSharpCode -ErrorAction Stop } catch { Write-Error "Ошибка компиляции C# движка: $($_.Exception.Message)" throw } } } function Get-ZoomMasterKey { $ZoomDataDir = Join-Path $env:APPDATA "Zoom\data" $ini = Join-Path $ZoomDataDir "Zoom.us.ini" if (-not (Test-Path $ini)) { return $null } $line = Get-Content $ini | Select-String "win_osencrypt_key=" | Select-Object -First 1 if (-not $line) { return $null } $b64 = $line.ToString().Split("=")[1].Trim().Substring(7) while ($b64.Length % 4 -ne 0) { $b64 += "=" } try { $bytes = [Convert]::FromBase64String($b64) return [ZoomPorter.Engine]::Decrypt($bytes) } catch { return $null } } function Open-ZoomDatabase { param([string]$FilePath, [string]$Key) $db = [IntPtr]::Zero if ([ZoomPorter.Engine]::sqlite3_open($FilePath, [ref]$db) -ne 0) { return [IntPtr]::Zero } $err = [IntPtr]::Zero [ZoomPorter.Engine]::sqlite3_exec($db, "PRAGMA key = '$Key';", [IntPtr]::Zero, [IntPtr]::Zero, [ref]$err) | Out-Null $pragmas = @( "PRAGMA cipher_page_size = 1024", "PRAGMA kdf_iter = 4000", "PRAGMA cipher_hmac_algorithm = HMAC_SHA512", "PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512" ) foreach ($p in $pragmas) { [ZoomPorter.Engine]::sqlite3_exec($db, $p, [IntPtr]::Zero, [IntPtr]::Zero, [ref]$err) | Out-Null } return $db } function ZoomExport { $Path = "zoom_settings.sql" try { Import-ZoomEngine $key = Get-ZoomMasterKey } catch { Write-Error "Критическая ошибка инициализации: $_" return } if (-not $key) { throw "Ключ Zoom не найден." } $ZoomDataDir = Join-Path $env:APPDATA "Zoom\data" Write-Host "[*] Экспорт данных в $Path..." -ForegroundColor Cyan $stream = [System.IO.StreamWriter]::new($Path, $false, [System.Text.Encoding]::UTF8) $stream.WriteLine("-- Zoom Porter Pure PowerShell Export") Get-ChildItem "$ZoomDataDir/*.enc.db" | Where-Object { $_.Name -notmatch "avatar|process_monitoring" } | ForEach-Object { Write-Host " [*] База: $($_.Name)" $stream.WriteLine("`n`n-- >>> DATABASE_START: $($_.Name) <<< --") $ptr = Open-ZoomDatabase -FilePath $_.FullName -Key $key if ($ptr -ne [IntPtr]::Zero) { $tables = [ZoomPorter.Engine]::Query($ptr, "SELECT name FROM sqlite_master WHERE type='table'") foreach ($t in $tables) { $tableName = $t["name"]; if ($tableName -match "avatar") { continue } $rows = [ZoomPorter.Engine]::Query($ptr, "SELECT * FROM $tableName") if ($rows.Count -gt 0) { $stream.WriteLine("-- TABLE: $tableName ($($rows.Count) rows)") $stream.WriteLine("DELETE FROM $tableName;") foreach ($row in $rows) { $cols = $row.Keys -join '", "' $vals = foreach ($v in $row.Values) { if ($null -eq $v) { "NULL" } else { "'$($v.ToString().Replace("'", "''"))'" } } $stream.WriteLine("INSERT OR REPLACE INTO $tableName (`"$cols`") VALUES ($($vals -join ', '));") } } } [ZoomPorter.Engine]::sqlite3_close($ptr) | Out-Null } $stream.WriteLine("-- >>> DATABASE_END: $($_.Name) <<< --") } # Files $stream.WriteLine("`n`n-- >>> CONFIG_FILES_START <<< --") @("client.config", "viper.ini", "transcoding.ini", "Zoom.us.ini") | ForEach-Object { $f = Join-Path $ZoomDataDir $_ if (Test-Path $f) { $stream.WriteLine("INSERT INTO zoom_files_kv VALUES ('$_', '$((Get-Content $f -Raw).Replace("'", "''"))');") } } # Registry $stream.WriteLine("`n`n-- >>> REGISTRY_START <<< --") @("HKCU:\Software\Zoom\Zoom Chat", "HKCU:\Software\Zoom\Zoom Meetings", "HKCU:\Software\Zoom\Zoom Video") | ForEach-Object { if (Test-Path $_) { foreach ($p in (Get-ItemProperty $_).PSObject.Properties) { if ($p.Name -match "PSPath|PSParentPath|PSChildName|PSDrive|PSProvider") { continue } $v = if ($null -eq $p.Value) { "" } else { $p.Value.ToString().Replace("'", "''") } $stream.WriteLine("INSERT OR REPLACE INTO zoom_registry_kv VALUES ('$_', '$($p.Name)', '$v', 'String');") } } } $stream.Close() $dp = $ZoomDataDir.TrimEnd('\') (Get-Content $Path -Raw).Replace($dp, "{{ZOOM_DATA}}") | Set-Content $Path -Encoding UTF8 Write-Host "[SUCCESS] Экспорт завершен: $Path" -ForegroundColor Green } function ZoomImport { $Path = "zoom_settings.sql" if (-not (Test-Path $Path)) { throw "Файл $Path не найден." } try { Import-ZoomEngine $key = Get-ZoomMasterKey } catch { Write-Error "Критическая ошибка инициализации: $_" return } if (-not $key) { throw "Ключ Zoom не найден." } $ZoomDataDir = Join-Path $env:APPDATA "Zoom\data" Write-Host "[*] Импорт данных из $Path..." -ForegroundColor Cyan Get-Process Zoom, zoom_launcher -ErrorAction SilentlyContinue | Stop-Process -Force Start-Sleep -Seconds 1 $sections = [Regex]::Split((Get-Content $Path -Raw), "-- >>> (.*?) <<< --") for ($i = 1; $i -lt $sections.Length; $i += 2) { $header = $sections[$i]; $content = $sections[$i+1].Trim() if ($header -match "DATABASE_START: (.*)") { $dbName = $Matches[1].Trim() $ptr = Open-ZoomDatabase -FilePath (Join-Path $ZoomDataDir $dbName) -Key $key if ($ptr -ne [IntPtr]::Zero) { foreach ($s in ($content -split ';')) { $sc = $s.Trim().Replace("{{ZOOM_DATA}}", $ZoomDataDir) if ($sc) { [ZoomPorter.Engine]::sqlite3_exec($ptr, $sc, [IntPtr]::Zero, [IntPtr]::Zero, [ref][IntPtr]::Zero) | Out-Null } } [ZoomPorter.Engine]::sqlite3_close($ptr) | Out-Null } } elseif ($header -match "CONFIG_FILES_START") { foreach ($m in [Regex]::Matches($content, "INSERT INTO zoom_files_kv VALUES \('(.*?)', '(.*?)'\);", [System.Text.RegularExpressions.RegexOptions]::Singleline)) { $m.Groups[2].Value.Replace("''", "'") | Out-File (Join-Path $ZoomDataDir $m.Groups[1].Value) -Encoding UTF8 -Force } } elseif ($header -match "REGISTRY_START") { foreach ($m in [Regex]::Matches($content, "INSERT OR REPLACE INTO zoom_registry_kv VALUES \('(.*?)', '(.*?)', '(.*?)', '(.*?)'\);")) { $rpath = $m.Groups[1].Value if (-not (Test-Path $rpath)) { New-Item $rpath -Force | Out-Null } Set-ItemProperty -Path $rpath -Name $m.Groups[2].Value -Value $m.Groups[3].Value.Replace("''", "'") -Force 2>$null } } } Write-Host "[SUCCESS] Настройки применены." -ForegroundColor Green }