다음을 통해 공유


사용자 데이터 보호 및 수집

고객이 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 파일과 동일한 규칙을 따라 지역 및 언어별 하위 디렉터리 아래에 직접 공개 키를 배치해야 합니다.

중요

고객의 PC에 프라이빗 키를 배치해서는 안 됩니다. 대신 데이터를 업로드한 후 암호 해독할 수 있도록 서버에 안전하게 저장해야 합니다. 고객이 등록 페이지에서 다음을 클릭하면 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 앱을 사용하여 데이터를 수집하려면 AUMID(애플리케이션 사용자 모델 ID)를 Microsoft-Windows-Shell-Setup | OOBE | OEMAppId 무인 설정에 할당합니다. Windows는 타임스탬프, 사용자 데이터, 세션 키 및 확인란 상태 데이터를 디바이스에 로그온하는 첫 번째 사용자와 연결된 OEM 앱의 애플리케이션 데이터 폴더에 전달합니다. 예를 들어 해당 사용자의 경우 %localappdata%\packages\[OEM app package family name]\LocalState입니다.

데이터를 업로드하는 서비스를 만들어 실행하는 경우 사용자가 시작 화면으로 이동한 후 30분 이상 실행되도록 서비스를 설정하고 서비스를 한 번만 실행해야 합니다. 현재 서비스를 실행하도록 설정하면 사용자가 시작 화면 및 해당 앱을 처음으로 탐색하는 동안 서비스가 백그라운드에서 시스템 리소스를 소비하지 않습니다. 서비스는 해당하는 경우 OOBE 디렉터리 내에서 데이터와 타임스탬프 및 사용자 이름을 수집해야 합니다. 또한 서비스는 사용자의 선택에 대한 응답으로 수행할 작업을 결정해야 합니다. 예를 들어 사용자가 맬웨어 방지 앱 평가판을 옵트인한 경우 서비스는 맬웨어 방지 앱에 의존하지 않고 평가판을 시작하여 실행 여부를 결정해야 합니다. 또는 또 다른 예로, 사용자가 회사 또는 파트너 회사의 이메일을 옵트인한 경우 서비스는 마케팅 이메일을 처리하는 사람에게 해당 정보를 전달해야 합니다.

서비스를 작성하는 방법에 대한 자세한 내용은 Windows Service 애플리케이션 개발을 참조하세요.

암호 해독을 위해 서버로 데이터 보내기

서비스 또는 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. CryptImportKey API를 통해 3단계의 프라이빗 키를 사용하여 세션 키 바이트를 암호화 키로 변환합니다.

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