保护和收集用户数据

如果客户在 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 值。

若要访问和使用客户信息,请执行以下步骤:

  1. 生成公钥/私钥对,并将公钥放在映像的 %systemroot%\system32\Oobe\Info 文件夹中。
  2. 使用在首次登录完成大约 30 分钟后运行的应用或服务收集加密的客户数据
  3. 使用 SSL 将数据发送到服务器进行解密。 然后,就可以解密会话密钥以解密客户数据。

创建公钥/私钥对

为了保护客户数据,你必须生成公钥/私钥对,并且公钥必须放在 %systemroot%\system32\Oobe\Info 文件夹中。 如果要将映像部署到多个区域或使用多种语言部署映像,则应将公钥直接放在特定于区域和语言的子目录下,并遵循与 Oobe.xml 工作原理中所述的特定于区域或语言的 Oobe.xml 文件相同的规则。

重要

绝不能将私钥放在客户电脑上。 应将其安全地存储在服务器上,以便在上传数据后对数据解密。 如果客户在注册页面上单击“下一步”,Windows 会使用公钥在 %systemroot%\system32\Oobe\Info 文件夹中创建 Sessionkey.blob。 服务或 Microsoft Store 应用应使用 SSL 将数据上传到服务器。 然后,你需要解密会话密钥以解密客户数据。

如果 %systemroot%\system32\Oobe\Info 文件夹中没有公钥,则不会显示注册页面。

生成公钥和私钥

按以下顺序进行调用,以生成公钥和私钥。

  1. 使用 CryptAcquireContext API 获取加密上下文。 提供以下值:

    • pszProviderMS_ENH_RSA_AES_PROV
    • dwProvTypePROV_RSA_AES
  2. 使用 CryptGenKey API 生成 RSA 加密密钥。 提供以下值:

    • AlgidCALG_RSA_KEYX
    • dwFlagsCRYPT_EXPORTABLE
  3. 使用 CryptExportKey API 序列化步骤 2 中加密密钥的公钥部分。 提供以下值:

    • dwBlobTypePUBLICKEYBLOB
  4. 使用标准 Windows 文件管理函数将步骤 3 中的序列化公钥字节写入文件 Pubkey.blob。

  5. 使用 CryptExportKey API 序列化步骤 2 中加密密钥的私钥部分。 提供以下值

    • dwBlobTypePRIVATEKEYBLOB
  6. 使用标准 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 应用,或编写要在首次登录后运行的服务,以:

  1. 收集加密的客户数据,包括来自 Windows.System.User 命名空间的用户名,以及首次登录的本地时间戳。
  2. 将该数据集上传到服务器以进行解密和使用。

若要使用 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 将数据上传到服务器。 然后,你需要解密会话密钥以解密客户数据。

解密数据

按以下顺序进行调用,以解密数据:

  1. 使用 CryptAcquireContext API 获取加密上下文。 提供以下值:

    • pszProviderMS_ENH_RSA_AES_PROV
    • dwProvTypePROV_RSA_AES
  2. 使用标准 Windows 文件 API 从磁盘读取 OEM 私钥文件 (Prvkey.blob)。

  3. 使用 CryptImportKey API 将私钥字节转换为加密密钥。

  4. 使用标准 Windows 文件 API 从磁盘读取 OOBE 生成的会话密钥文件 (Sessionkey.blob)。

  5. 使用步骤 3 中的私钥,通过 CryptImportKey API 将会话密钥字节转换为加密密钥。

  6. 导出密钥 (hPubKey) 是步骤 3 中导入的私钥。

  7. 使用标准 Windows 文件 API 从磁盘读取 OOBE 编写的加密用户数据 (Userdata.blob)。

  8. 使用会话密钥(来自步骤 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;
}