Защита и сбор данных пользователей
Если клиент вводит сведения на страницы регистрации изготовителя оборудования, при завершении запуска при первом запуске создаются следующие файлы:
- Userdata.blob. Зашифрованный XML-файл, содержащий все значения во всех настраиваемых пользователем элементах на страницах регистрации, включая поля сведений о клиенте и состояния флажков.
- SessionKey.blob. Создается во время шифрования Userdata.blob. Содержит ключ сеанса, необходимый для процесса расшифровки.
- Userchoices.xml. Незашифрованный XML-файл, содержащий метки и значения флажков для всех флажков, включенных на страницы регистрации.
Примечание
Если клиент щелкает Skip
первую страницу регистрации, данные не записываются и не сохраняются в этих файлах, даже флажки по умолчанию.
Метка времени для пользовательского интерфейса также добавляется в реестр Windows в следующем разделе:
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE\Stats [EndTimeStamp]
Это значение реестра создается независимо от того, включены ли страницы регистрации в OOBE. Метка времени записывается в формате UTC (универсальное координированное время); в частности, это значение, записываемое SYSTEMTIME
в реестр в виде сериализованного большого двоичного объекта данных.
Чтобы получить доступ к сведениям о клиенте и использовать их, выполните следующие действия.
-
Создайте пару открытых и закрытых ключей и поместите открытый ключ в папку
%systemroot%\system32\Oobe\Info
образа. - Соберите зашифрованные данные клиента с помощью приложения или службы, которая запускается примерно через 30 минут после завершения первого входа в систему.
- Отправьте данные на сервер для расшифровки с помощью SSL. Затем можно расшифровать ключ сеанса, чтобы расшифровать данные клиента.
Создание пары открытых/закрытых ключей
Чтобы защитить данные клиента, необходимо создать пару открытых и закрытых ключей, а открытый ключ должен быть помещен в папку %systemroot%\system32\Oobe\Info
. Если вы развертываете образы в нескольких регионах или на нескольких языках, следует поместить открытый ключ непосредственно в подкаталоги региона и языка, следуя тем же правилам, что и для файлов Oobe.xml для конкретного региона или языка, как описано в разделе Как работает Oobe.xml.
Важно!
Закрытый ключ никогда не следует размещать на компьютере клиента. Вместо этого он должен безопасно храниться на серверах, чтобы данные можно было расшифровать после отправки. Если клиент нажимает кнопку Далее на страницах регистрации, Windows использует открытый ключ для создания Sessionkey.blob в папке %systemroot%\system32\Oobe\Info
. Служба или приложение Microsoft Store должны отправлять данные на сервер по протоколу SSL. Затем необходимо расшифровать ключ сеанса, чтобы расшифровать данные клиента.
Если в папке нет открытого %systemroot%\system32\Oobe\Info
ключа, страницы регистрации не отображаются.
Создание открытых и закрытых ключей
Выполните эту последовательность вызовов для создания открытого и закрытого ключей.
Получение контекста шифрования с помощью API CryptAcquireContext. Укажите следующие значения:
-
pszProvider
равноMS_ENH_RSA_AES_PROV
-
dwProvType
равноPROV_RSA_AES
-
Создайте ключ шифрования RSA с помощью API CryptGenKey. Укажите следующие значения:
-
Algid
равноCALG_RSA_KEYX
-
dwFlags
равноCRYPT_EXPORTABLE
-
Сериализация части ключа шифрования открытого ключа из шага 2 с помощью API CryptExportKey. Укажите следующее значение:
-
dwBlobType
имеет значениеPUBLICKEYBLOB
.
-
Запишите сериализованные байты открытого ключа из шага 3 в файл Pubkey.blob с помощью стандартных функций управления файлами Windows.
Сериализация части закрытого ключа шифрования из шага 2 с помощью API CryptExportKey. Укажите это значение
-
dwBlobType
имеет значениеPRIVATEKEYBLOB
.
-
Запишите сериализованные байты закрытого ключа из шага 5 в файл Prvkey.blob с помощью стандартного API файлов Windows.
В этом фрагменте кода показано, как создать ключи:
HRESULT CryptExportKeyHelper(_In_ HCRYPTKEY hKey, _In_opt_ HCRYPTKEY hExpKey, DWORD dwBlobType, _Outptr_result_bytebuffer_(*pcbBlob) BYTE **ppbBlob, _Out_ DWORD *pcbBlob);
HRESULT WriteByteArrayToFile(_In_ PCWSTR pszPath, _In_reads_bytes_(cbData) BYTE const *pbData, DWORD cbData);
// This method generates an OEM public and private key pair and writes it to Pubkey.blob and Prvkey.blob
HRESULT GenerateKeysToFiles()
{
// Acquire crypt provider. Use provider MS_ENH_RSA_AES_PROV and provider type PROV_RSA_AES to decrypt the blob from OOBE.
HCRYPTPROV hProv;
HRESULT hr = CryptAcquireContext(&hProv, L"OEMDecryptContainer", MS_ENH_RSA_AES_PROV,
PROV_RSA_AES, CRYPT_NEWKEYSET) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
if (hr == NTE_EXISTS)
{
hr = CryptAcquireContext(&hProv, L"OEMDecryptContainer", MS_ENH_RSA_AES_PROV,
PROV_RSA_AES, 0) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
}
if (SUCCEEDED(hr))
{
// Call CryptGenKey to generate the OEM public and private key pair. OOBE expects the algorithm to be CALG_RSA_KEYX.
HCRYPTKEY hKey;
hr = CryptGenKey(hProv, CALG_RSA_KEYX, CRYPT_EXPORTABLE, &hKey) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
if (SUCCEEDED(hr))
{
// Call CryptExportKeyHelper to serialize the public key into bytes.
BYTE *pbPubBlob;
DWORD cbPubBlob;
hr = CryptExportKeyHelper(hKey, NULL, PUBLICKEYBLOB, &pbPubBlob, &cbPubBlob);
if (SUCCEEDED(hr))
{
// Call CryptExportKey again to serialize the private key into bytes.
BYTE *pbPrvBlob;
DWORD cbPrvBlob;
hr = CryptExportKeyHelper(hKey, NULL, PRIVATEKEYBLOB, &pbPrvBlob, &cbPrvBlob);
if (SUCCEEDED(hr))
{
// Now write the public key bytes into the file pubkey.blob
hr = WriteByteArrayToFile(L"pubkey.blob", pbPubBlob, cbPubBlob);
if (SUCCEEDED(hr))
{
// And write the private key bytes into the file Prvkey.blob
hr = WriteByteArrayToFile(L"prvkey.blob", pbPrvBlob, cbPrvBlob);
}
HeapFree(GetProcessHeap(), 0, pbPrvBlob);
}
HeapFree(GetProcessHeap(), 0, pbPubBlob);
}
CryptDestroyKey(hKey);
}
CryptReleaseContext(hProv, 0);
}
return hr;
}
HRESULT CryptExportKeyHelper(_In_ HCRYPTKEY hKey, _In_opt_ HCRYPTKEY hExpKey, DWORD dwBlobType, _Outptr_result_bytebuffer_(*pcbBlob) BYTE **ppbBlob, _Out_ DWORD *pcbBlob)
{
*ppbBlob = nullptr;
*pcbBlob = 0;
// Call CryptExportKey the first time to determine the size of the serialized key.
DWORD cbBlob = 0;
HRESULT hr = CryptExportKey(hKey, hExpKey, dwBlobType, 0, nullptr, &cbBlob) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
if (SUCCEEDED(hr))
{
// Allocate a buffer to hold the serialized key.
BYTE *pbBlob = reinterpret_cast<BYTE *>(CoTaskMemAlloc(cbBlob));
hr = (pbBlob != nullptr) ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr))
{
// Now export the key to the buffer.
hr = CryptExportKey(hKey, hExpKey, dwBlobType, 0, pbBlob, &cbBlob) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
if (SUCCEEDED(hr))
{
*ppbBlob = pbBlob;
*pcbBlob = cbBlob;
pbBlob = nullptr;
}
CoTaskMemFree(pbBlob);
}
}
return hr;
}
HRESULT WriteByteArrayToFile(_In_ PCWSTR pszPath, _In_reads_bytes_(cbData) BYTE const *pbData, DWORD cbData)
{
bool fDeleteFile = false;
HANDLE hFile = CreateFile(pszPath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
HRESULT hr = (hFile == INVALID_HANDLE_VALUE) ? HRESULT_FROM_WIN32(GetLastError()) : S_OK;
if (SUCCEEDED(hr))
{
DWORD cbWritten;
hr = WriteFile(hFile, pbData, cbData, &cbWritten, nullptr) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
fDeleteFile = FAILED(hr);
CloseHandle(hFile);
}
if (fDeleteFile)
{
DeleteFile(pszPath);
}
return hr;
}
Сбор зашифрованных данных клиента
Создайте и предварительно установите приложение Microsoft Store или напишите службу для запуска после первого входа, чтобы:
- Соберите зашифрованные данные клиента, включая имя пользователя из пространства имен Windows.System.User, а также метку местного времени для первого входа.
- Отправьте этот набор данных на сервер для расшифровки и использования.
Чтобы использовать приложение Microsoft Store для сбора данных, назначьте идентификатор модели пользователя приложения (AUMID) в Microsoft-Windows-Shell-Setup | OOBE | Параметр автоматической установки OEMAppId . Windows передаст метку времени, данные пользователя, ключ сеанса и данные о состоянии флажка в папку данных приложения изготовителя оборудования, которая связана с первым пользователем для входа на устройство. Например, %localappdata%\packages\[OEM app package family name]\LocalState
для этого пользователя.
Если вы создаете и запускаете службу для отправки данных, следует настроить ее запуск не менее чем через 30 минут после того, как пользователь перейдет на начальный экран, и запустить службу только один раз. Настройка запуска службы в это время гарантирует, что служба не будет потреблять системные ресурсы в фоновом режиме, пока пользователи получают первый шанс изучить начальный экран и свои приложения. Служба должна собирать данные из каталога OOBE, а также метку времени и имя пользователя, если это применимо. Служба также должна определить, какие действия следует предпринять в ответ на выбор пользователя. Например, если пользователь согласился на использование пробной версии приложения для защиты от вредоносных программ, служба должна запустить пробную версию, а не полагаться на приложение для защиты от вредоносных программ, чтобы решить, следует ли его запускать. Или, в качестве другого примера, если ваш пользователь согласился на электронные письма от вашей компании или партнерских компаний, ваша служба должна сообщить эти сведения тем, кто обрабатывает ваши маркетинговые сообщения.
Дополнительные сведения о создании службы см. в статье Разработка приложений-служб Windows.
Отправка данных на сервер для расшифровки
Служба или приложение Microsoft Store должны передавать данные на сервер по протоколу SSL. Затем необходимо расшифровать ключ сеанса, чтобы расшифровать данные клиента.
Расшифровка данных
Выполните следующую последовательность вызовов для расшифровки данных:
Получение контекста шифрования с помощью API CryptAcquireContext. Укажите следующие значения:
-
pszProvider
равноMS_ENH_RSA_AES_PROV
-
dwProvType
равноPROV_RSA_AES
-
Чтение файла закрытого ключа OEM (Prvkey.blob) с диска с помощью стандартного API файлов Windows.
Преобразуйте байты закрытого ключа в ключ шифрования с помощью API CryptImportKey.
Чтение созданного OOBE файла ключа сеанса (Sessionkey.blob) с диска с помощью стандартного API файлов Windows.
Используйте закрытый ключ из шага 3, чтобы преобразовать байты ключа сеанса в ключ шифрования с помощью API CryptImportKey.
Ключ экспорта (hPubKey) — это закрытый ключ, импортированный на шаге 3.
Чтение зашифрованных пользовательских данных при первом включении (Userdata.blob) с диска с помощью стандартного API файлов Windows.
Используйте ключ сеанса (из шага 5) для расшифровки данных пользователя с помощью CryptDecrypt.
В этом фрагменте кода показано, как расшифровать данные:
HRESULT DecryptHelper(_In_reads_bytes_(cbData) BYTE *pbData, DWORD cbData, _In_ HCRYPTKEY hPrvKey, _Outptr_result_bytebuffer_(*pcbPlain) BYTE **ppbPlain, _Out_ DWORD *pcbPlain);
HRESULT ReadFileToByteArray(_In_ PCWSTR pszPath, _Outptr_result_bytebuffer_(*pcbData) BYTE **ppbData, _Out_ DWORD *pcbData);
// This method uses the specified Userdata.blob (pszDataFilePath), Sessionkey.blob (pszSessionKeyPath), and Prvkey.blob (pszPrivateKeyPath)
// and writes the plaintext XML user data to Plaindata.xml
HRESULT UseSymmetricKeyFromFileToDecrypt(_In_ PCWSTR pszDataFilePath, _In_ PCWSTR pszSessionKeyPath, _In_ PCWSTR pszPrivateKeyPath)
{
// Acquire crypt provider. Use provider MS_ENH_RSA_AES_PROV and provider type PROV_RSA_AES to decrypt the blob from OOBE.
HCRYPTPROV hProv;
HRESULT hr = CryptAcquireContext(&hProv, L"OEMDecryptContainer", MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_NEWKEYSET) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
if (hr == NTE_EXISTS)
{
hr = CryptAcquireContext (&hProv, L"OEMDecryptContainer", MS_ENH_RSA_AES_PROV, PROV_RSA_AES, 0) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
}
if (SUCCEEDED(hr))
{
// Read in the OEM private key file.
BYTE *pbPrvBlob;
DWORD cbPrvBlob;
hr = ReadFileToByteArray(pszPrivateKeyPath, &pbPrvBlob, &cbPrvBlob);
if (SUCCEEDED(hr))
{
// Convert the private key file bytes into an HCRYPTKEY.
HCRYPTKEY hKey;
hr = CryptImportKey(hProv, pbPrvBlob, cbPrvBlob, 0, 0, &hKey) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
if (SUCCEEDED(hr))
{
// Read in the encrypted session key generated by OOBE.
BYTE *pbSymBlob;
DWORD cbSymBlob;
hr = ReadFileToByteArray(pszSessionKeyPath, &pbSymBlob, &cbSymBlob);
if (SUCCEEDED(hr))
{
// Convert the encrypted session key file bytes into an HCRYPTKEY.
// This uses the OEM private key to decrypt the session key file bytes.
HCRYPTKEY hSymKey;
hr = CryptImportKey(hProv, pbSymBlob, cbSymBlob, hKey, 0, &hSymKey) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
if (SUCCEEDED(hr))
{
// Read in the encrypted user data written by OOBE.
BYTE *pbCipher;
DWORD dwCipher;
hr = ReadFileToByteArray(pszDataFilePath, &pbCipher, &dwCipher);
if (SUCCEEDED(hr))
{
// Use the session key to decrypt the encrypted user data.
BYTE *pbPlain;
DWORD dwPlain;
hr = DecryptHelper(pbCipher, dwCipher, hSymKey, &pbPlain, &dwPlain);
if (SUCCEEDED(hr))
{
hr = WriteByteArrayToFile(L"plaindata.xml", pbPlain, dwPlain);
HeapFree(GetProcessHeap(), 0, pbPlain);
}
HeapFree(GetProcessHeap(), 0, pbCipher);
}
CryptDestroyKey(hSymKey);
}
HeapFree(GetProcessHeap(), 0, pbSymBlob);
}
else if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
{
wcout << L"Couldn't find session key file [" << pszSessionKeyPath << L"]" << endl;
}
CryptDestroyKey(hKey);
}
HeapFree(GetProcessHeap(), 0, pbPrvBlob);
}
else if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
{
wcout << L"Couldn't find private key file [" << pszPrivateKeyPath << L"]" << endl;
}
CryptReleaseContext(hProv, 0);
}
return hr;
}
HRESULT DecryptHelper(_In_reads_bytes_(cbData) BYTE *pbData, DWORD cbData, _In_ HCRYPTKEY hPrvKey, _Outptr_result_bytebuffer_(*pcbPlain) BYTE **ppbPlain, _Out_ DWORD *pcbPlain)
{
BYTE *pbCipher = reinterpret_cast<BYTE *>(HeapAlloc(GetProcessHeap(), 0, cbData));
HRESULT hr = (pbCipher != nullptr) ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr))
{
// CryptDecrypt will write the actual length of the plaintext to cbPlain.
// Any block padding that was added during CryptEncrypt won't be counted in cbPlain.
DWORD cbPlain = cbData;
memcpy(pbCipher, pbData, cbData);
hr = ResultFromWin32Bool(CryptDecrypt(hPrvKey,
0,
TRUE,
0,
pbCipher,
&cbPlain));
if (SUCCEEDED(hr))
{
*ppbPlain = pbCipher;
*pcbPlain = cbPlain;
pbCipher = nullptr;
}
HeapFree(GetProcessHeap(), 0, pbCipher);
} return hr;
}
HRESULT ReadFileToByteArray(_In_ PCWSTR pszPath, _Outptr_result_bytebuffer_(*pcbData) BYTE **ppbData, _Out_ DWORD *pcbData)
{
*ppbData = nullptr;
*pcbData = 0;
HANDLE hFile = CreateFile(pszPath, GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
HRESULT hr = (hFile == INVALID_HANDLE_VALUE) ? HRESULT_FROM_WIN32(GetLastError()) : S_OK;
if (SUCCEEDED(hr))
{
DWORD cbSize = GetFileSize(hFile, nullptr);
hr = (cbSize != INVALID_FILE_SIZE) ? S_OK : ResultFromKnownLastError();
if (SUCCEEDED(hr))
{
BYTE *pbData = reinterpret_cast<BYTE *>(CoTaskMemAlloc(cbSize));
hr = (pbData != nullptr) ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr))
{
DWORD cbRead;
hr = ReadFile(hFile, pbData, cbSize, &cbRead, nullptr) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
if (SUCCEEDED(hr))
{
*ppbData = pbData;
*pcbData = cbSize;
pbData = nullptr;
}
CoTaskMemFree(pbData);
}
}
CloseHandle(hFile);
}
return hr;
}