保護並收集使用者資料

如果客戶在 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 至登錄的值。

若要讓您存取和使用客戶資訊,請執行下列步驟:

  1. 產生公開/私密金鑰組,並將公開金鑰放在 %systemroot%\system32\Oobe\Info 影像的 資料夾中。
  2. 使用在第一次登入完成之後大約 30 分鐘執行的應用程式或服務,收集加密的客戶資料
  3. 使用 SSL 將資料傳送至伺服器以進行解密 。 然後,您可以解密工作階段金鑰來解密客戶資料。

產生公開/私密金鑰組

若要保護客戶資料,您必須產生公開/私密金鑰組,而且公開金鑰必須放在 %systemroot%\system32\Oobe\Info 資料夾中。 如果您要將映射部署到多個區域或多種語言,您應該將公開金鑰直接放在區域和語言特定的子目錄中,遵循與區域或語言特定Oobe.xml檔案相同的規則,如 Oobe.xml的運作方式中所述。

重要

您絕對不能將私密金鑰放在客戶的電腦上。 相反地,它應該安全地儲存在您的伺服器上,以便在上傳資料之後解密。 如果客戶在 [註冊] 頁面上按 [下一步],Windows 會使用公開金鑰在 資料夾中建立 Sessionkey.blob %systemroot%\system32\Oobe\Info 。 您的服務或 Microsoft Store 應用程式應該使用 SSL 將資料上傳至伺服器。 接著,您必須解密工作階段金鑰,以解密客戶資料。

如果資料夾中沒有公開金鑰 %systemroot%\system32\Oobe\Info ,則不會顯示註冊頁面。

產生公開和私密金鑰

進行此一連串的呼叫,以產生公用和私密金鑰。

  1. 使用 CryptAcquireCoNtext API 取得 crypt內容。 請提供下列值:

    • pszProviderMS_ENH_RSA_AES_PROV
    • dwProvTypePROV_RSA_AES
  2. 使用 CryptGenKey API 產生 RSA crypt 金鑰。 請提供下列值:

    • AlgidCALG_RSA_KEYX
    • dwFlagsCRYPT_EXPORTABLE
  3. 使用 CryptExportKey API,從步驟 2 序列化 crypt 金鑰的公開金鑰部分。 提供此值:

    • dwBlobTypePUBLICKEYBLOB
  4. 使用標準 Windows 檔案管理功能,將步驟 3 的序列化公開金鑰位元組寫入檔案 Pubkey.blob。

  5. 使用 CryptExportKey API,從步驟 2 序列化 crypt 金鑰的私密金鑰部分。 提供此值

    • 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 應用程式收集資料,請將其應用程式使用者模型識別碼指派給 Microsoft-Windows-Shell-Setup (AUMID) |OOBE |OEMAppId 自動安裝設定。 Windows 會將時間戳記、使用者資料、工作階段金鑰和核取方塊狀態資料傳遞給 OEM 應用程式的應用程式資料檔案夾,該資料夾與第一位使用者登入裝置相關聯。 例如, %localappdata%\packages\[OEM app package family name]\LocalState 針對該使用者。

如果您建立並執行服務來上傳資料,您應該將服務設定為在使用者進入 [開始] 畫面之後至少執行 30 分鐘,並只執行服務一次。 設定您的服務目前執行可確保您的服務不會在背景取用系統資源,而使用者第一次有機會探索 [開始] 畫面及其應用程式。 服務必須從 OOBE 目錄內收集資料,以及適用時間戳記和使用者名稱。 服務也應該決定要採取的動作,以回應使用者的選擇。 例如,如果使用者加入宣告反惡意程式碼應用程式試用版,您的服務應該啟動試用版,而不是依賴反惡意程式碼應用程式來決定是否應該執行。 或者,另一個範例是,如果您的使用者加入宣告公司或合作夥伴公司的電子郵件,您的服務應該與處理行銷電子郵件的人員溝通該資訊。

如需如何撰寫服務的詳細資訊,請參閱 開發 Windows 服務應用程式

將資料傳送至伺服器以進行解密

您的服務或 Microsoft Store 應用程式應該使用 SSL 將資料上傳至伺服器。 接著,您必須解密工作階段金鑰,以解密客戶資料。

解密資料

進行此序列呼叫以解密資料:

  1. 使用 CryptAcquireCoNtext API 來取得 crypt 內容。 請提供下列值:

    • pszProviderMS_ENH_RSA_AES_PROV
    • dwProvTypePROV_RSA_AES
  2. 使用標準 Windows 檔案 API 從磁片讀取 OEM 私密金鑰檔案 (Prvkey.blob) 。

  3. 使用 CryptImportKey API將私密金鑰位元組轉換成 crypt 金鑰。

  4. 使用標準 Windows 檔案 API 從磁片讀取 OOBE 產生的工作階段金鑰檔案 (Sessionkey.blob) 。

  5. 使用步驟 3 中的私密金鑰,使用 CryptImportKey API將工作階段金鑰位元組轉換成 crypt 金鑰。

  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;
}