本指南示範開發者如何解密匯出快照 Recall 以供應用程式使用。 你會學到完整的解密過程,並附上可立即實作的程式碼範例。
匯出 Recall 快照僅支援歐洲經濟區(EEA)內的裝置。 快照 Recall 的匯出是使用者主動發起的程序,每位使用者皆需單獨進行。 匯出的快照會被加密。
了解更多如何 匯出 Recall 快照 ,或參考 Recall 總覽 了解這項 AI 支援功能的運作方式。
先決條件
匯出Recall快照的選項僅適用於歐洲經濟區(EEA)內運行最新 Windows Insider Program 預覽版的 Copilot+ PC 裝置。
在開始之前,你需要:
- 匯出快照:使用者必須先 匯出 Recall 快照 並提供儲存的資料夾路徑。
- 匯出代碼:快照匯出時提供的 32 字元 Recall 匯出代碼。
- 輸出資料夾:一個目標資料夾路徑,將與匯出快照相關的解密 .jpg 及 .json 檔案保存。
如何解密匯出快照Recall
先用範例程式碼開始解密 Recall中的匯出快照。 請依照以下步驟了解解密的運作方式。
計算匯出金鑰
使用者除了在初始Recall設定時被要求儲存的匯出程式碼外,還需要提供儲存快照的位置(資料夾路徑)。RecallRecall
Recall匯出程式碼看起來大致如下:0a0a-0a0a-1111-bbbb-2222-3c3c-3c3c-3c3c
首先,移除破折號——這樣會得到一個 32 字元的字串: 0a0a0a0a1111bbbb22223c3c3c3c3c3c
std::wstring UnexpandExportCode(std::wstring code)
{
if (code.size() > 32)
{
code.erase(std::remove(code.begin(), code.end(), ' '), code.end()); // Remove spaces
code.erase(std::remove(code.begin(), code.end(), '-'), code.end()); // Remove hyphens
}
if (code.size() != 32)
{
std::wcout << L"The export code has incorrect number of characters."<< std::endl;
}
return code;
}
接著,建立一個陣列,其中包含每一對十六進位數字對應的位元組值。
std::vector<uint8_t> HexStringToBytes(const std::wstring& hexString)
{
std::vector<uint8_t> bytes;
if (hexString.length() % 2 != 0)
{
throw std::invalid_argument("Hex string must have an even length");
}
for (size_t i = 0; i < hexString.length(); i += 2)
{
std::wstring byteString = hexString.substr(i, 2);
uint8_t byte = static_cast<uint8_t>(std::stoi(byteString, nullptr, 16));
bytes.push_back(byte);
}
return bytes;
}
接著,取該陣列計算 SHA256 雜湊值,得到一個 32 位元組的值,這就是匯出鍵。 現在,任何數量的快照都可以利用所得的匯出金鑰解密。
std::vector<uint8_t> exportKeyBytes(c_keySizeInBytes);
THROW_IF_NTSTATUS_FAILED(BCryptHash(
BCRYPT_SHA256_ALG_HANDLE,
nullptr,
0,
exportCodeBytes.data(),
static_cast<ULONG>(exportCodeBytes.size()),
exportKeyBytes.data(),
c_keySizeInBytes));
解密加密快照
快照的佈局(小端格式): | uint32_t version | uint32_t encryptedKeySize | uint32_t encryptedContentSize | uint32_t contentType | uint8_t[KeySIze] encryptedContentKey | uint8_t[ContentSize] encryptedContent |
首先,讀取四個 uint32_t 值。
EncryptedSnapshotHeader header{};
reader.ByteOrder(winrt::ByteOrder::LittleEndian);
header.Version = reader.ReadUInt32();
header.KeySize = reader.ReadUInt32();
header.ContentSize = reader.ReadUInt32();
header.ContentType = reader.ReadUInt32();
接著,確認該版本的值為 2。
if (header.Version != 2)
{
throw std::runtime_error("Insufficient data header version.");
}
接著,讀取加密鑰匙內容。
std::vector<uint8_t> keybytes(header.KeySize);
reader.ReadBytes(keybytes);
解密加密的金鑰內容
wil::unique_bcrypt_key DecryptExportKey(BCRYPT_KEY_HANDLE key, std::span<uint8_t const> encryptedKey)
{
THROW_HR_IF(E_INVALIDARG, encryptedKey.size() != c_totalSizeInBytes);
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO AuthInfo{};
BCRYPT_INIT_AUTH_MODE_INFO(AuthInfo);
AuthInfo.pbNonce = const_cast<uint8_t*>(encryptedKey.data());
AuthInfo.cbNonce = c_nonceSizeInBytes;
AuthInfo.pbTag = const_cast<uint8_t*>(encryptedKey.data() + c_nonceSizeInBytes + c_childKeySizeInBytes);
AuthInfo.cbTag = c_tagSizeInBytes;
uint8_t decryptedKey[c_childKeySizeInBytes] = { 0 };
ULONG decryptedByteCount{};
THROW_IF_FAILED(HResultFromBCryptStatus(BCryptDecrypt(
key,
const_cast<uint8_t*>(encryptedKey.data() + c_nonceSizeInBytes),
c_childKeySizeInBytes,
&AuthInfo,
nullptr,
0,
decryptedKey,
sizeof(decryptedKey),
&decryptedByteCount,
0)));
wil::unique_bcrypt_key childKey;
THROW_IF_NTSTATUS_FAILED(
BCryptGenerateSymmetricKey(BCRYPT_AES_GCM_ALG_HANDLE, &childKey, nullptr, 0, decryptedKey, c_childKeySizeInBytes, 0));
return childKey;
}
使用「exportKey」
wil::unique_bcrypt_key exportKey;
THROW_IF_NTSTATUS_FAILED(BCryptGenerateSymmetricKey(
BCRYPT_AES_GCM_ALG_HANDLE, &exportKey, nullptr, 0, exportKeyBytes.data(), static_cast<ULONG>(exportKeyBytes.size()), 0));
要取得 contentKey(加密演算法是AES_GCM)
wil::unique_bcrypt_key contentKey = DecryptExportKey(exportKey.get(), keybytes);
閱讀加密內容
std::vector<uint8_t> contentbytes(header.ContentSize);
reader.ReadBytes(contentbytes);
解密加密內容
std::vector<uint8_t> DecryptPackedData(BCRYPT_KEY_HANDLE key, std::span<uint8_t const> payload)
{
THROW_HR_IF(E_INVALIDARG, payload.size() < c_tagSizeInBytes);
const auto dataSize = payload.size() - c_tagSizeInBytes;
const auto data = payload.data();
uint8_t zeroNonce[c_nonceSizeInBytes] = { 0 };
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo{};
BCRYPT_INIT_AUTH_MODE_INFO(authInfo);
authInfo.pbNonce = zeroNonce;
authInfo.cbNonce = c_nonceSizeInBytes;
authInfo.pbTag = const_cast<uint8_t*>(payload.data() + dataSize);
authInfo.cbTag = c_tagSizeInBytes;
std::vector<uint8_t> decryptedContent(dataSize);
ULONG decryptedSize = 0;
const auto result = BCryptDecrypt(
key, const_cast<uint8_t*>(data), static_cast<ULONG>(dataSize), &authInfo, nullptr, 0, decryptedContent.data(), static_cast<ULONG>(dataSize), &decryptedSize, 0);
decryptedContent.resize(decryptedSize);
THROW_IF_FAILED(HResultFromBCryptStatus(result));
return decryptedContent;
}
以 ContentKey 進行(加密演算法為 AES_GCM)
std::vector<uint8_t> decryptedContent = DecryptPackedData(contentKey.get(), contentbytes);
將解密的 Recall 快照內容以 .jpg 影像形式輸出到指定資料夾路徑中,並附有相應的 .json 元資料
void WriteSnapshotToOutputFolder(winrt::StorageFolder const& outputFolder, winrt::hstring const& fileName, winrt::IRandomAccessStream const& decryptedStream)
預期產出將包括:
- 解密快照儲存為 .jpg 檔案。
- 對應的元資料會儲存為 .json 檔案。
兩種檔案類型會共用相同檔名,並可在指定的輸出資料夾中找到。
了解更多 Recall
- 了解更多關於 Windows Recall的資訊。
- 用匯出代碼匯出 Recall 快照
- 管理 Recall:允許匯出 Recall 及快照資訊:IT 管理員指導如何管理 Recall 公司內使用者的設定,包括匯出 Recall 快照資料的功能。
- Windows AI 的組態服務提供者(CSP)政策:允許 Recall 匯出:提供 IT 管理員指引,協助建立政策設定,決定終端使用者是否可啟用該選用 Recall 功能,包括啟用快照資料匯出的政策。