保護並收集使用者資料
如果客戶在 OEM 註冊頁面中輸入資訊,則會在完成 OOBE 時建立下列檔案:
- Userdata.blob。 加密的 XML 檔案,其中包含註冊頁面上所有使用者可設定元素中的所有值,包括客戶資訊欄位和核取方塊狀態。
- SessionKey.blob。 在 Userdata.blob 加密期間產生。 包含解密程式所需的工作階段金鑰。
- Userchoices.xml。 未加密的 XML 檔案,其中包含註冊頁面上所含所有核取方塊的核取方塊標籤和值。
注意
如果客戶按一下 Skip
第一個註冊頁面,則不會將任何資料寫入或儲存到這些檔案,甚至是核取方塊預設狀態。
使用者現用體驗的時間戳記也會新增至此機碼下的 Windows 登錄:
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE\Stats [EndTimeStamp]
不論註冊頁面是否包含在 OOBE 中,都會建立此登錄值。 時間戳記是以 UTC (國際標準時間) 格式撰寫;具體而言,它是寫入 SYSTEMTIME
為序列化資料 Blob 至登錄的值。
若要讓您存取和使用客戶資訊,請執行下列步驟:
-
產生公開/私密金鑰組,並將公開金鑰放在
%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
,則不會顯示註冊頁面。
產生公開和私密金鑰
進行此一連串的呼叫,以產生公用和私密金鑰。
使用 CryptAcquireCoNtext API 取得 crypt內容。 請提供下列值:
-
pszProvider
是MS_ENH_RSA_AES_PROV
-
dwProvType
是PROV_RSA_AES
-
使用 CryptGenKey API 產生 RSA crypt 金鑰。 請提供下列值:
-
Algid
是CALG_RSA_KEYX
-
dwFlags
是CRYPT_EXPORTABLE
-
使用 CryptExportKey API,從步驟 2 序列化 crypt 金鑰的公開金鑰部分。 提供此值:
-
dwBlobType
是PUBLICKEYBLOB
-
使用標準 Windows 檔案管理功能,將步驟 3 的序列化公開金鑰位元組寫入檔案 Pubkey.blob。
使用 CryptExportKey API,從步驟 2 序列化 crypt 金鑰的私密金鑰部分。 提供此值
-
dwBlobType
是PRIVATEKEYBLOB
-
使用標準 Windows 檔案 API,將步驟 5 的序列化私密金鑰位元組寫入至檔案 Prvkey.blob。
此程式碼片段示範如何產生金鑰:
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 應用程式收集資料,請將其應用程式使用者模型識別碼指派給 Microsoft-Windows-Shell-Setup (AUMID) |OOBE |OEMAppId 自動安裝設定。 Windows 會將時間戳記、使用者資料、工作階段金鑰和核取方塊狀態資料傳遞給 OEM 應用程式的應用程式資料檔案夾,該資料夾與第一位使用者登入裝置相關聯。 例如, %localappdata%\packages\[OEM app package family name]\LocalState
針對該使用者。
如果您建立並執行服務來上傳資料,您應該將服務設定為在使用者進入 [開始] 畫面之後至少執行 30 分鐘,並只執行服務一次。 設定您的服務目前執行可確保您的服務不會在背景取用系統資源,而使用者第一次有機會探索 [開始] 畫面及其應用程式。 服務必須從 OOBE 目錄內收集資料,以及適用時間戳記和使用者名稱。 服務也應該決定要採取的動作,以回應使用者的選擇。 例如,如果使用者加入宣告反惡意程式碼應用程式試用版,您的服務應該啟動試用版,而不是依賴反惡意程式碼應用程式來決定是否應該執行。 或者,另一個範例是,如果您的使用者加入宣告公司或合作夥伴公司的電子郵件,您的服務應該與處理行銷電子郵件的人員溝通該資訊。
如需如何撰寫服務的詳細資訊,請參閱 開發 Windows 服務應用程式。
將資料傳送至伺服器以進行解密
您的服務或 Microsoft Store 應用程式應該使用 SSL 將資料上傳至伺服器。 接著,您必須解密工作階段金鑰,以解密客戶資料。
解密資料
進行此序列呼叫以解密資料:
使用 CryptAcquireCoNtext API 來取得 crypt 內容。 請提供下列值:
-
pszProvider
是MS_ENH_RSA_AES_PROV
-
dwProvType
是PROV_RSA_AES
-
使用標準 Windows 檔案 API 從磁片讀取 OEM 私密金鑰檔案 (Prvkey.blob) 。
使用 CryptImportKey API將私密金鑰位元組轉換成 crypt 金鑰。
使用標準 Windows 檔案 API 從磁片讀取 OOBE 產生的工作階段金鑰檔案 (Sessionkey.blob) 。
使用步驟 3 中的私密金鑰,使用 CryptImportKey API將工作階段金鑰位元組轉換成 crypt 金鑰。
(hPubKey) 匯出金鑰是步驟 3 中匯入的私密金鑰。
使用標準 Windows 檔案 API 從磁片讀取 OOBE 寫入的加密使用者資料 (Userdata.blob) 。
使用步驟 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;
}