如果客户在 OEM 注册页面中输入信息,则在他们完成 OOBE 时,系统会创建以下文件:
- Userdata.blob。 一个加密的 XML 文件,其中包含注册页面上所有用户可配置元素中的所有值,包括客户信息字段和复选框状态。
- SessionKey.blob。 在 Userdata.blob 加密期间生成。 包含解密过程所需的会话密钥。
- Userchoices.xml。 一个未加密的 XML 文件,其中包含注册页面上所有复选框的复选框标签和值。
注意
如果客户在第一个注册页面上单击 Skip,则不会将任何数据(甚至复选框默认状态)写入或存储到这些文件。
用户的全新体验时间戳也会添加到 Windows 注册表中的此项下:
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE\Stats [EndTimeStamp]
无论注册页面是否包含在 OOBE 中,都会创建此注册表值。 时间戳以 UTC(协调世界时)格式编写;具体来说,它是作为数据的序列化 blob 写入注册表的 SYSTEMTIME 值。
若要访问和使用客户信息,请执行以下步骤:
- 生成公钥/私钥对,并将公钥放在映像的
%systemroot%\system32\Oobe\Info文件夹中。 - 使用在首次登录完成大约 30 分钟后运行的应用或服务收集加密的客户数据。
- 使用 SSL 将数据发送到服务器进行解密。 然后,就可以解密会话密钥以解密客户数据。
创建公钥/私钥对
为了保护客户数据,你必须生成公钥/私钥对,并且公钥必须放在 %systemroot%\system32\Oobe\Info 文件夹中。 如果要将映像部署到多个区域或使用多种语言部署映像,则应将公钥直接放在特定于区域和语言的子目录下,并遵循与 Oobe.xml 工作原理中所述的特定于区域或语言的 Oobe.xml 文件相同的规则。
重要
绝不能将私钥放在客户电脑上。 应将其安全地存储在服务器上,以便在上传数据后对数据解密。 如果客户在注册页面上单击“下一步”,Windows 会使用公钥在 %systemroot%\system32\Oobe\Info 文件夹中创建 Sessionkey.blob。 服务或 Microsoft Store 应用应使用 SSL 将数据上传到服务器。 然后,你需要解密会话密钥以解密客户数据。
如果 %systemroot%\system32\Oobe\Info 文件夹中没有公钥,则不会显示注册页面。
生成公钥和私钥
按以下顺序进行调用,以生成公钥和私钥。
使用 CryptAcquireContext API 获取加密上下文。 提供以下值:
pszProvider为MS_ENH_RSA_AES_PROVdwProvType为PROV_RSA_AES
使用 CryptGenKey API 生成 RSA 加密密钥。 提供以下值:
Algid为CALG_RSA_KEYXdwFlags为CRYPT_EXPORTABLE
使用 CryptExportKey API 序列化步骤 2 中加密密钥的公钥部分。 提供以下值:
dwBlobType是PUBLICKEYBLOB
使用标准 Windows 文件管理函数将步骤 3 中的序列化公钥字节写入文件 Pubkey.blob。
使用 CryptExportKey API 序列化步骤 2 中加密密钥的私钥部分。 提供以下值
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 应用收集数据,请将其应用程序用户模型 ID (AUMID) 分配给 Microsoft-Windows-Shell-Setup | OOBE | OEMAppId 无人参与设置。 Windows 会将时间戳、用户数据、会话密钥和复选框状态数据传递到 OEM 应用的应用程序数据文件夹,该文件夹与第一个登录设备的用户相关联。 例如,该用户的 %localappdata%\packages\[OEM app package family name]\LocalState。
如果创建并运行用于上传数据的服务,则应将该服务设置为在用户进入“开始”屏幕后至少 30 分钟运行,并且只运行一次。 通过将服务设置为此时运行,可确保服务在用户第一次有机会浏览“开始”屏幕及其应用时不会在后台消耗系统资源。 服务必须收集 OOBE 目录中的数据,以及时间戳和用户名(如果适用)。 服务还应确定为响应用户的选择要采取的操作。 例如,如果用户选择使用反恶意软件应用试用版,你的服务应启动试用版,而不是依靠反恶意软件应用来决定它是否应该运行。 再举一个例子,如果用户选择接收来自你的公司或合作伙伴公司的电子邮件,你的服务应将该信息传达给处理营销电子邮件的人。
有关如何编写服务的详细信息,请参阅开发 Windows 服务应用程序。
将数据发送到服务器进行解密
服务或 Microsoft Store 应用应使用 SSL 将数据上传到服务器。 然后,你需要解密会话密钥以解密客户数据。
解密数据
按以下顺序进行调用,以解密数据:
使用 CryptAcquireContext API 获取加密上下文。 提供以下值:
pszProvider为MS_ENH_RSA_AES_PROVdwProvType为PROV_RSA_AES
使用标准 Windows 文件 API 从磁盘读取 OEM 私钥文件 (Prvkey.blob)。
使用 CryptImportKey API 将私钥字节转换为加密密钥。
使用标准 Windows 文件 API 从磁盘读取 OOBE 生成的会话密钥文件 (Sessionkey.blob)。
使用步骤 3 中的私钥,通过 CryptImportKey API 将会话密钥字节转换为加密密钥。
导出密钥 (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;
}