diff --git a/CHANGELOG.md b/CHANGELOG.md index ab7223f..4b59514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable project changes are documented here. The project follows semantic versioning after the first generally available release. +## [0.4.0-rc3] - 2026-08-20 + +### Changed + +- Disconnected-Windows scanning now runs on a background thread, keeping the main window responsive while slow HDD/USB media are read. +- The progress bar uses a continuous marquee during offline scans, the scan button shows an active state, and the status line reports each scan stage. +- Offline path guidance and the folder-picker title now explicitly say that either a drive root such as `E:\` or its `E:\Windows` directory can be selected. + +### Validation + +- The user confirmed that RC2 detects the products and full license on the real connected Windows 7 x86 disk and successfully exports its public certificates. +- Release x86 build and all core tests pass after the asynchronous GUI change. + ## [0.4.0-rc2] - 2026-08-20 ### Fixed diff --git a/README.en.md b/README.en.md index ac724f4..ebfc246 100644 --- a/README.en.md +++ b/README.en.md @@ -5,7 +5,7 @@ [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![Windows 7–11](https://img.shields.io/badge/Windows-7%20SP1%E2%80%9311-0078D6.svg)](#system-requirements) -> **Status: 0.4.0 RC2.** A user successfully completed full removal on a live Windows 10 x64 system. RC1 did not fix the real disconnected Windows 7 x86 disk; RC2 replaces the unsuitable `RegLoadAppKey` path with a system-hive loader and separately supports file-backed certificate stores. The same disk still needs a repeat test. +> **Status: 0.4.0 RC3.** A user successfully completed full removal on a live Windows 10 x64 system. Detection of products and the complete license plus public-certificate export are now confirmed on a real connected Windows 7 x86 disk. RC3 adds responsive background scanning with live indication. An unofficial portable utility for backing up license identifiers and public certificates, controlled removal of installed CryptoPro products, and rescue from a disconnected Windows 7 SP1 through Windows 11 installation. @@ -35,6 +35,7 @@ The utility deliberately preserves Windows certificate stores, hardware tokens, - fallback profile discovery from `Users`, plus registry-backed user and local-machine public-certificate stores; - public-certificate discovery in `AppData\Roaming\Microsoft\SystemCertificates\My\Certificates` without accessing private keys; - copyable stage-by-stage offline diagnostics and a safe `--offline-scan` command. +- background disconnected-Windows scanning with a responsive window, continuous progress animation, and live stage text. ## System requirements @@ -55,7 +56,7 @@ CryptoPro CSP 4.x and 5.x (and available 3.x installations) are discovered from ## Disconnected Windows rescue -On the Disconnected Windows tab, choose an offline `Windows` directory or drive root and scan it read-only. Licenses and selected public certificates can be rescued without changing the disk. Advanced offline cleanup cannot run the disconnected installation's registered MSI/EXE uninstaller, so it is explicitly forced and conservative. It requires all detected products, the typed phrase `OFFLINE`, and a backup on another volume. Complete `SOFTWARE` and `SYSTEM` hives plus quarantined file copies and a recovery map are created before writes. User `NTUSER.DAT` files, certificate stores, tokens, and private-key containers remain read-only. Unknown COM/browser remnants are retained and can produce a partial result. +On the Disconnected Windows tab, choose either a drive root such as `E:\` or its `E:\Windows` directory and scan it read-only. The window remains responsive, a continuous indicator moves, and the current stage is shown; a slow HDD/USB scan may take several minutes. Licenses and selected public certificates can be rescued without changing the disk. Advanced offline cleanup cannot run the disconnected installation's registered MSI/EXE uninstaller, so it is explicitly forced and conservative. It requires all detected products, the typed phrase `OFFLINE`, and a backup on another volume. Complete `SOFTWARE` and `SYSTEM` hives plus quarantined file copies and a recovery map are created before writes. User `NTUSER.DAT` files, certificate stores, tokens, and private-key containers remain read-only. Unknown COM/browser remnants are retained and can produce a partial result. Safe scan-only CLI: diff --git a/README.md b/README.md index 9441175..8d1cc2b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![Windows 7–11](https://img.shields.io/badge/Windows-7%20SP1%E2%80%9311-0078D6.svg)](#системные-требования) -> **Статус: релиз-кандидат 0.4.0 RC2.** Полное удаление успешно проверено пользователем на живой Windows 10 x64. RC1 не исправил чтение реального диска Windows 7 x86; в RC2 ошибочный `RegLoadAppKey` заменён системным загрузчиком hive, а файловое хранилище сертификатов поддерживается отдельно. Требуется повторная проверка на том же диске. +> **Статус: релиз-кандидат 0.4.0 RC3.** Полное удаление успешно проверено пользователем на живой Windows 10 x64. На реальном подключённом диске Windows 7 x86 подтверждены обнаружение продуктов, полный номер лицензии и экспорт открытых сертификатов. RC3 добавляет отзывчивое фоновое сканирование с живой индикацией. Неофициальная portable-утилита для резервного копирования лицензий и открытых сертификатов, контролируемого удаления установленных продуктов CryptoPro и спасения данных из отключённой Windows 7 SP1–Windows 11. @@ -45,6 +45,7 @@ - резервный поиск профилей в `Users` и чтение пользовательского и машинного реестровых хранилищ сертификатов; - чтение открытых сертификатов из файлового хранилища `AppData\Roaming\Microsoft\SystemCertificates\My\Certificates` без доступа к закрытым ключам; - копируемая пошаговая диагностика офлайн-сканирования и безопасный параметр `--offline-scan`. +- фоновое сканирование отключённой Windows: окно остаётся отзывчивым, индикатор движется непрерывно, а строка состояния показывает текущий этап. ## Системные требования @@ -69,8 +70,8 @@ ## Неисправный или подключённый диск -1. Откройте вкладку «Неисправный диск» и укажите папку `Windows` отключённой установки либо корень диска. -2. Нажмите «Сканировать». Ульи `SOFTWARE`, `SYSTEM` и пользовательские `NTUSER.DAT` на этом этапе открываются только для чтения. +1. Откройте вкладку «Неисправный диск» и выберите корень диска с Windows, например `E:\`, либо саму папку `E:\Windows` — подходят оба варианта. +2. Нажмите «Сканировать». Во время чтения движется индикатор и показывается текущий этап; на медленном HDD/USB операция может занять несколько минут. Ульи `SOFTWARE`, `SYSTEM` и пользовательские `NTUSER.DAT` открываются только для чтения. 3. Кнопка «Сохранить найденные данные» извлекает полные лицензии и выбранные открытые сертификаты, не меняя отключённую Windows. 4. «Расширенная офлайн-очистка» требует выбора всех найденных продуктов, ввода `OFFLINE` и резервной папки на другом томе. До изменений копируются целиком `SOFTWARE` и `SYSTEM`, а подтверждённые файлы помещаются в карантинную копию с `recovery-map.txt`. diff --git a/docs/RELEASE_NOTES_0.4.0-rc3.md b/docs/RELEASE_NOTES_0.4.0-rc3.md new file mode 100644 index 0000000..ed8fc17 --- /dev/null +++ b/docs/RELEASE_NOTES_0.4.0-rc3.md @@ -0,0 +1,18 @@ +# КриптоПро Очистка 0.4.0 RC3 + +Этот релиз улучшает удобство проверенного офлайн-сканирования подключённого диска Windows. + +## Что изменено + +- сканирование отключённой Windows перенесено в фоновый поток, поэтому окно больше не выглядит зависшим; +- во время всей операции движется непрерывный индикатор; +- кнопка показывает состояние «Сканирование…», а строка состояния — текущий этап; +- окно остаётся отзывчивым и его можно перемещать; +- в самой вкладке и окне выбора теперь прямо написано, что можно указать корень диска (`E:\`) или папку Windows (`E:\Windows`); +- добавлено предупреждение, что чтение медленного HDD/USB может занять несколько минут и носитель нельзя отключать. + +## Подтверждённая проверка + +На реальном подключённом диске с загружаемой Windows 7 x86 версия RC2 правильно обнаружила продукты CryptoPro и полный номер лицензии, показала сертификаты и успешно экспортировала их открытую часть. Полное удаление на работающей Windows 10 x64 также ранее прошло успешно. + +RC3 не меняет правила чтения и очистки диска: первичное сканирование остаётся только для чтения, сертификатные хранилища и контейнеры закрытых ключей защищены. diff --git a/docs/TEST_MATRIX.md b/docs/TEST_MATRIX.md index f1aceb2..7552654 100644 --- a/docs/TEST_MATRIX.md +++ b/docs/TEST_MATRIX.md @@ -39,6 +39,7 @@ For each VM retain the initial JSON report, final JSON report, cleanup log, prod - 2026-08-20: version 0.3.0 RC2 running on Windows 10 x64 returned zero products and certificates for a connected, bootable Windows 7 x86 disk. Version 0.4.0 RC1 added registry-view, MSI Installer UserData, profile-fallback, and certificate-store changes, but a repeat test still returned zero results. - 2026-08-20: version 0.4.0 unit tests, non-destructive integration scan, and MSVC C++ static analysis passed. The release executable was verified as x86 with OS/subsystem version 6.01, `requireAdministrator`, system-DPI awareness, Common Controls v6, and no dynamic Visual C++ runtime dependency. - 2026-08-20: user repeat test of 0.4.0 RC1 still returned zero products and certificates on the connected Windows 7 x86 disk. Reproduction showed `RegLoadAppKey` returning `ERROR_BADDB` for a valid saved system `SOFTWARE` hive; `RegLoadKey` opened and unloaded the same hive successfully. RC2 replaces the loader and adds serialized certificate-file discovery. +- 2026-08-20: user testing of 0.4.0 RC2 on the real connected Windows 7 x86 disk confirmed product detection, the correct complete license identifier, and successful public-certificate export. RC3 moves this scan to a responsive background worker with continuous progress indication. - 2026-08-20: read-only synthetic disconnected-Windows fixture passed with 2 confirmed CryptoPro products, 4 license candidates, 1 inactive profile, and 7 public certificates from `AppData\Roaming\Microsoft\SystemCertificates\My\Certificates`; both `SOFTWARE` and `SYSTEM` temporary mounts unloaded successfully. - 2026-08-20: RC2 implementation passed unit tests, the non-destructive live integration scan, and MSVC C++ static analysis with no warnings. No `CryptoProCleanup_Offline_*` registry mounts remained after testing; sensitive hive/certificate fixture copies were removed. - The running Windows directory is rejected as an offline target. A destructive disconnected-Windows VM test is still pending. diff --git a/src/cleanup.hpp b/src/cleanup.hpp index ba46ca8..2e2bddf 100644 --- a/src/cleanup.hpp +++ b/src/cleanup.hpp @@ -10,7 +10,7 @@ namespace cpc { -constexpr wchar_t kVersion[] = L"0.4.0"; +constexpr wchar_t kVersion[] = L"0.4.0-rc3"; enum class Language { Russian, English }; enum class RiskLevel { Normal, High }; diff --git a/src/gui.cpp b/src/gui.cpp index c2f84fb..e19953c 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -14,11 +14,19 @@ #include #include #include +#include namespace cpc { namespace { constexpr UINT WM_CPC_START = WM_APP + 10; +constexpr UINT WM_CPC_OFFLINE_PROGRESS = WM_APP + 11; +constexpr UINT WM_CPC_OFFLINE_COMPLETE = WM_APP + 12; + +struct OfflineProgressMessage { + std::wstring message; + int percent = 0; +}; struct AppState { HWND window = nullptr; @@ -39,6 +47,8 @@ struct AppState { HFONT titleFont = nullptr; HBRUSH backgroundBrush = nullptr; bool busy = false; + bool offlineScanRunning = false; + std::thread offlineScanThread; }; struct ConfirmState { @@ -507,9 +517,9 @@ void ApplyLanguage(AppState& state) { SetText(state.window, IDC_SELECT_ALL_CERTS, Tr(state.language, L"Выбрать все сертификаты", L"Select all certificates")); SetText(state.window, IDC_EXPORT_CERTS, Tr(state.language, L"Сохранить выбранные открытые сертификаты...", L"Export selected public certificates...")); SetText(state.window, IDC_OFFLINE_INFO, Tr(state.language, - L"Режим спасения отключённой Windows: безопасное извлечение данных или принудительная очистка после полной резервной копии. Штатный MSI здесь запустить невозможно.", - L"Disconnected Windows rescue: safely extract data or run forced cleanup after a full recovery backup. Its registered MSI cannot run here.")); - SetText(state.window, IDC_OFFLINE_PATH_LABEL, Tr(state.language, L"Папка Windows", L"Windows folder")); + L"Выберите диск с отключённой Windows (например, E:\\) или саму папку Windows (E:\\Windows). Сканирование выполняется только для чтения; на медленном HDD/USB оно может занять несколько минут.", + L"Choose the drive containing disconnected Windows (for example, E:\\) or its Windows folder (E:\\Windows). The read-only scan may take several minutes on a slow HDD/USB drive.")); + SetText(state.window, IDC_OFFLINE_PATH_LABEL, Tr(state.language, L"Диск / Windows", L"Drive / Windows")); SetText(state.window, IDC_OFFLINE_BROWSE, Tr(state.language, L"Обзор...", L"Browse...")); SetText(state.window, IDC_OFFLINE_SCAN, Tr(state.language, L"Сканировать", L"Scan")); SetText(state.window, IDC_OFFLINE_PRODUCTS_LABEL, Tr(state.language, L"Продукты в отключённой Windows", L"Products in disconnected Windows")); @@ -555,6 +565,28 @@ void SetBusy(AppState& state, bool busy) { } } +void SetOfflineScanAnimation(AppState& state, bool scanning) { + HWND progress = GetDlgItem(state.window, IDC_PROGRESS); + if (!progress) return; + LONG_PTR style = GetWindowLongPtrW(progress, GWL_STYLE); + if (scanning) { + SetWindowLongPtrW(progress, GWL_STYLE, style | PBS_MARQUEE); + SetWindowPos(progress, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + SendMessageW(progress, PBM_SETMARQUEE, TRUE, 32); + SetText(state.window, IDC_OFFLINE_SCAN, + Tr(state.language, L"Сканирование…", L"Scanning…")); + } else { + SendMessageW(progress, PBM_SETMARQUEE, FALSE, 0); + SetWindowLongPtrW(progress, GWL_STYLE, style & ~static_cast(PBS_MARQUEE)); + SetWindowPos(progress, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + SendMessageW(progress, PBM_SETRANGE, 0, MAKELPARAM(0, 100)); + SetText(state.window, IDC_OFFLINE_SCAN, + Tr(state.language, L"Сканировать", L"Scan")); + } +} + bool CopyUnicodeText(HWND owner, const std::wstring& text) { if (!OpenClipboard(owner)) return false; EmptyClipboard(); @@ -675,12 +707,14 @@ bool TypedConfirm(AppState& state, const std::wstring& phrase, const std::wstrin ConfirmDialogProc, reinterpret_cast(&confirm)) == IDOK; } -std::wstring BrowseForFolder(HWND owner, const std::wstring& current) { +std::wstring BrowseForFolder(HWND owner, const std::wstring& current, + const std::wstring& title = {}) { IFileOpenDialog* dialog = nullptr; if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_IFileOpenDialog, reinterpret_cast(&dialog))) || !dialog) return {}; DWORD options = 0; dialog->GetOptions(&options); dialog->SetOptions(options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM); + if (!title.empty()) dialog->SetTitle(title.c_str()); if (!current.empty()) { IShellItem* folder = nullptr; if (SUCCEEDED(SHCreateItemFromParsingName(current.c_str(), nullptr, IID_IShellItem, reinterpret_cast(&folder))) && folder) { @@ -773,17 +807,8 @@ void ReadOfflineSelections(AppState& state) { } } -void DoOfflineScan(AppState& state) { - const std::wstring path = GetText(state.window, IDC_OFFLINE_PATH); - if (path.empty()) { - MessageBoxW(state.window, Tr(state.language, L"Выберите папку Windows на подключённом диске.", L"Select the Windows folder on the connected drive.").c_str(), - L"CryptoPro Cleanup Utility", MB_OK | MB_ICONWARNING); - return; - } - SetBusy(state, true); - state.logPath.clear(); - state.offline = ScanOfflineWindows(state.language, path, - [&](const std::wstring& message, int percent) { UpdateProgress(state, message, percent); }); +void FinishOfflineScan(AppState& state, OfflineScanResult&& result) { + state.offline = std::move(result); PopulateOfflineLists(state); std::wostringstream summary; if (state.offline.valid) { @@ -807,6 +832,56 @@ void DoOfflineScan(AppState& state) { ShowOfflineDiagnostics(state); } +void DoOfflineScan(AppState& state) { + const std::wstring path = GetText(state.window, IDC_OFFLINE_PATH); + if (path.empty()) { + MessageBoxW(state.window, Tr(state.language, + L"Выберите диск, содержащий отключённую Windows (например, E:\\), или саму папку Windows (E:\\Windows).", + L"Select the drive containing disconnected Windows (for example, E:\\), or the Windows folder itself (E:\\Windows).").c_str(), + L"CryptoPro Cleanup Utility", MB_OK | MB_ICONWARNING); + return; + } + SetBusy(state, true); + state.logPath.clear(); + state.offlineScanRunning = true; + SetOfflineScanAnimation(state, true); + SetText(state.window, IDC_STATUS, Tr(state.language, + L"Подготовка сканирования подключённого диска… Не отключайте носитель.", + L"Preparing to scan the connected drive… Do not disconnect it.")); + const HWND window = state.window; + const Language language = state.language; + try { + state.offlineScanThread = std::thread([window, language, path]() { + auto result = std::make_unique(); + try { + *result = ScanOfflineWindows(language, path, + [window](const std::wstring& message, int percent) { + auto update = std::make_unique(); + update->message = message; + update->percent = percent; + if (PostMessageW(window, WM_CPC_OFFLINE_PROGRESS, 0, + reinterpret_cast(update.get()))) update.release(); + }); + } catch (...) { + result->scan.warnings.push_back(Tr(language, + L"Офлайн-сканирование прервано из-за непредвиденной ошибки.", + L"The offline scan stopped because of an unexpected error.")); + result->diagnostics.push_back(L"Background offline scan failed with an unexpected exception."); + } + if (PostMessageW(window, WM_CPC_OFFLINE_COMPLETE, 0, + reinterpret_cast(result.get()))) result.release(); + }); + } catch (...) { + state.offlineScanRunning = false; + SetOfflineScanAnimation(state, false); + SetBusy(state, false); + MessageBoxW(state.window, Tr(state.language, + L"Не удалось запустить фоновое сканирование.", + L"The background scan could not be started.").c_str(), + L"CryptoPro Cleanup Utility", MB_OK | MB_ICONERROR); + } +} + void SaveOfflineData(AppState& state) { if (!state.offline.valid) { MessageBoxW(state.window, Tr(state.language, L"Сначала просканируйте отключённую Windows.", L"Scan the disconnected Windows installation first.").c_str(), @@ -1095,6 +1170,30 @@ INT_PTR CALLBACK MainDialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM if (state->resumeToken.empty()) DoScan(*state); else ResumeCleanup(*state); return TRUE; } + if (message == WM_CPC_OFFLINE_PROGRESS) { + std::unique_ptr update( + reinterpret_cast(lParam)); + if (update && state->offlineScanRunning) { + SetText(state->window, IDC_STATUS, + Tr(state->language, L"Сканирование диска — ", L"Drive scan — ") + + std::to_wstring(std::clamp(update->percent, 0, 100)) + L"%: " + update->message); + } + return TRUE; + } + if (message == WM_CPC_OFFLINE_COMPLETE) { + std::unique_ptr result( + reinterpret_cast(lParam)); + if (state->offlineScanThread.joinable()) state->offlineScanThread.join(); + state->offlineScanRunning = false; + SetOfflineScanAnimation(*state, false); + if (result) FinishOfflineScan(*state, std::move(*result)); + else SetBusy(*state, false); + return TRUE; + } + if (message == WM_SETCURSOR && state->offlineScanRunning && LOWORD(lParam) == HTCLIENT) { + SetCursor(LoadCursorW(nullptr, IDC_APPSTARTING)); + return TRUE; + } if (message == WM_COMMAND) { switch (LOWORD(wParam)) { case IDC_LANGUAGE: @@ -1118,7 +1217,11 @@ INT_PTR CALLBACK MainDialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM } return TRUE; case IDC_OFFLINE_BROWSE: if (!state->busy) { - const std::wstring folder = BrowseForFolder(dialog, GetText(dialog, IDC_OFFLINE_PATH)); + const std::wstring folder = BrowseForFolder( + dialog, GetText(dialog, IDC_OFFLINE_PATH), + Tr(state->language, + L"Выберите диск с Windows или папку Windows", + L"Choose a Windows drive or the Windows folder")); if (!folder.empty()) SetText(dialog, IDC_OFFLINE_PATH, folder); } return TRUE; @@ -1164,11 +1267,27 @@ INT_PTR CALLBACK MainDialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM } } if (message == WM_DESTROY) { + if (state->offlineScanThread.joinable()) state->offlineScanThread.join(); + MSG pending{}; + while (PeekMessageW(&pending, dialog, WM_CPC_OFFLINE_PROGRESS, + WM_CPC_OFFLINE_COMPLETE, PM_REMOVE)) { + if (pending.message == WM_CPC_OFFLINE_PROGRESS) + delete reinterpret_cast(pending.lParam); + else if (pending.message == WM_CPC_OFFLINE_COMPLETE) + delete reinterpret_cast(pending.lParam); + } if (state->titleFont) { DeleteObject(state->titleFont); state->titleFont = nullptr; } if (state->backgroundBrush) { DeleteObject(state->backgroundBrush); state->backgroundBrush = nullptr; } return TRUE; } - if (message == WM_CLOSE) { if (!state->busy) EndDialog(dialog, 0); return TRUE; } + if (message == WM_CLOSE) { + if (!state->busy) EndDialog(dialog, 0); + else if (state->offlineScanRunning) + SetText(state->window, IDC_STATUS, Tr(state->language, + L"Сканирование ещё выполняется — дождитесь завершения.", + L"The scan is still running — wait for it to finish.")); + return TRUE; + } return FALSE; }