You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python与UE5.1 C++跨语言调用DPAPI加解密报错87咨询

DPAPI跨Python与UE C++加解密错误(错误码87)

环境说明

使用Python 3.11.0与Unreal Engine 5.1的C++开发环境。

问题描述

在Python中用DPAPI加密用户输入后,在UE C中调用CryptUnprotectData解密返回错误码87(无效参数);反过来用C加密、Python解密也会报同样错误。但同语言内的加解密操作完全正常。

Python代码

# DPAPI access library
# This file uses code originally created by Crusher Joe:
# http://article.gmane.org/gmane.comp.python.ctypes/420
# And modified by Wayne Koorts:
# http://stackoverflow.com/questions/463832/using-dpapi-with-python

from ctypes import *
from ctypes.wintypes import DWORD
from getpass import getpass
import os

##

LocalFree = windll.kernel32.LocalFree
memcpy = cdll.msvcrt.memcpy
CryptProtectData = windll.crypt32.CryptProtectData
CryptUnprotectData = windll.crypt32.CryptUnprotectData


my_dir = 'C:\\Users\\User\\Desktop\\PythonCrypt'
file_name = 'EncryptedToken.txt'
encrypted_file_path = os.path.join(my_dir, file_name)


class DATA_BLOB(Structure):
    _fields_ = [("cbData", DWORD), ("pbData", POINTER(c_char))]


def getData(blobOut):
    cbData = int(blobOut.cbData)
    pbData = blobOut.pbData
    buffer = c_buffer(cbData)
    memcpy(buffer, pbData, cbData)
    LocalFree(pbData)
    return buffer.raw


def Win32CryptProtectData(plainText):
    bufferIn = c_buffer(plainText, len(plainText))
    blobIn = DATA_BLOB(len(plainText), bufferIn)
    blobOut = DATA_BLOB()

    if CryptProtectData(byref(blobIn), None, None, None, None, 0, byref(blobOut)):
        return getData(blobOut)
    else:
        print("CryptProtectData failed, Error:  " + str(GetLastError()))
        print("Returning an empty string")
        return ""


def Win32CryptUnprotectData(cipherText):
    bufferIn = c_buffer(cipherText, len(cipherText))
    blobIn = DATA_BLOB(len(cipherText), bufferIn)
    blobOut = DATA_BLOB()

    if CryptUnprotectData(byref(blobIn), None, None, None, None, 0, byref(blobOut)):
        print("CryptUnprotectData successful")
        return getData(blobOut)
    else:
        print("CryptUnprotectData failed, Error:  " + str(GetLastError()))
        print("Returning an empty string")
        return ""


# Encrypts bytes and saves to file
def cryptData(file_path, text_bytes):
    WriteBytesToFile(file_path, Win32CryptProtectData(text_bytes))


# Reads byte file and returns decrypted bytes
def decryptData(file_path):
    readFile = ReadBytesFromFile(file_path)
    return Win32CryptUnprotectData(readFile)


def WriteBytesToFile(file_path, bytes_to_write):
    with open(file_path, "wb") as wf:
        wf.write(bytes_to_write)


def ReadBytesFromFile(file_path):
    with open(file_path, "rb") as rf:
        return rf.read()


if __name__ == '__main__':
    # Prompt user for string password
    Password = getpass("Enter your password: ")

    # Convert string to bytes, and encrypt
    cryptData(encrypted_file_path, bytes(Password, 'utf-8'))

UE C++代码

  • XLOG为替代UE_LOG的日志宏
  • 仅保留Encrypt代码作完整性展示,实际仅运行Decrypt()
#define ENCRYPTED_TOKEN_FILE_NAME TEXT("EncryptedToken.txt")

bool CryptUtil::Encrypt(FString InString)
{
    // Encrypt data from DATA_BLOB DataIn to DATA_BLOB DataOut
    DATA_BLOB DataIn, DataOut;
    TArray<uint8> Data;
    const int32 Size = InString.Len();
    Data.AddUninitialized(Size);

    // Create entropy data blob
    DATA_BLOB entropyBlob;

    // Convert FString to BYTE
    StringToBytes(*InString, Data.GetData(), Size);

    // Declare and initialize the DataIn structure
    BYTE* PbDataInput = Data.GetData();
    const DWORD CBDataInput = Size;
    DataIn.pbData = PbDataInput;
    DataIn.cbData = CBDataInput;

    // Begin protect phase
    if (CryptProtectData(
        &DataIn,        // Unencrypted data in
        nullptr,        // Optional description string
        nullptr,        // Optional entropy
        nullptr,        // Reserved
        nullptr,        // Optional prompt struct
        0,                  // Flags
        &DataOut))      // Encrypted data out
    {
        // If we're saving a new token, delete any existing encrypted files
        const FString TokenFile = GetEncryptedTokenFile();
        if (!TokenFile.IsEmpty())
        {
            if (FPaths::ValidatePath(TokenFile) && FPaths::FileExists(TokenFile))
            {
                IFileManager& FileManager = IFileManager::Get();
                FileManager.Delete(*TokenFile);
            }
        }

        // Convert from FString
        const char* EncryptedTokenFile = StringCast<ANSICHAR>(*TokenFile).Get();

        // Write encrypted data out to file
        std::ofstream EncryptedFile(EncryptedTokenFile, std::ios::out | std::ios::binary);
        EncryptedFile.write(reinterpret_cast<char*>(&DataOut.cbData), sizeof(DataOut.cbData));
        EncryptedFile.write(reinterpret_cast<char*>(DataOut.pbData), DataOut.cbData);
        EncryptedFile.close();

        // Release memory
        SecureZeroMemory(DataOut.pbData, DataOut.cbData);
        LocalFree(DataOut.pbData);
    }
    else
    {
        XLOG(LogConsoleResponse, Error, TEXT("Encryption error using CryptProtectData."));
        return false;
    }

    return true;
}

bool CryptUtil::Decrypt(FString& OutString)
{
    // Decrypt data from DATA_BLOB DataIn to DATA_BLOB DataOut
    DATA_BLOB DataIn, DataOut;

    // Create entropy data blob
    DATA_BLOB entropyBlob;

    // Convert from FString
    const char* EncryptedTokenFile = StringCast<ANSICHAR>(*GetEncryptedTokenFile()).Get();

    std::ifstream ReadEncryptedFile(EncryptedTokenFile, std::ios::in | std::ios::binary);
    if (!ReadEncryptedFile.is_open())
    {
        XLOG(LogConsoleResponse, Error, TEXT("Cannot open {%s}."), *GetEncryptedTokenFile());
        return false;
    }

    // Read encrypted data from file
    ReadEncryptedFile.read(reinterpret_cast<char*>(&DataIn.cbData), sizeof(DataIn.cbData));
    DataIn.pbData = new BYTE[DataIn.cbData];
    ReadEncryptedFile.read(reinterpret_cast<char*>(DataIn.pbData), DataIn.cbData);

    // Begin unprotect phase.
    if (CryptUnprotectData(
        &DataIn,        // Encrypted data in
        nullptr,        // Optional description string
        nullptr,        // Optional entropy
        nullptr,        // Reserved
        nullptr,        // Optional prompt struct
        0,              // Flags
        &DataOut))      // Unncrypted data out
    {

        // Convert BYTE to FString
        OutString = BytesToString(DataOut.pbData, DataOut.cbData);
        XLOG(LogConsoleResponse, Verbose, TEXT("CryptUnprotectData was successful!"));

        // Release memory
        SecureZeroMemory(DataOut.pbData, DataOut.cbData);
        LocalFree(DataOut.pbData);
    }
    else
    {
        XLOG(LogConsoleResponse, Error, TEXT("CryptUnprotectData failed, Error: %s"), *GetLastError());
        return false;
    }

    return true;
}

FString CryptUtil::GetEncryptedTokenFile()
{
    const FString tokenFile = FPaths::Combine("C:\\Users\\User\\Desktop\\PythonCrypt", ENCRYPTED_TOKEN_FILE_NAME);
    return tokenFile;
}

已尝试方案

  • 将文件保存为文本文件而非二进制文件,无效
  • 确认同语言内加解密正常,仅跨语言操作报错

问题根源与修复方案

核心原因

跨语言加解密失败是因为Python和C++对加密数据的存储/读取格式不一致:

  • Python仅存储DATA_BLOB的pbData(加密后的原始字节流)
  • C++存储的是cbData(数据长度) + pbData的组合数据

两边的DATA_BLOB结构不匹配,导致CryptUnprotectData收到无效参数,触发错误87。

修复方案(二选一即可)

方案1:统一存储为「仅加密字节流」

修改C++代码,让加密时只写入pbData,解密时直接读取整个文件作为pbData:

// 修改Encrypt函数的存储部分
std::ofstream EncryptedFile(EncryptedTokenFile, std::ios::out | std::ios::binary);
// 去掉写入cbData的代码,仅写入pbData
EncryptedFile.write(reinterpret_cast<char*>(DataOut.pbData), DataOut.cbData);
EncryptedFile.close();

// 修改Decrypt函数的读取部分
std::vector<BYTE> encryptedData(std::istreambuf_iterator<char>(ReadEncryptedFile), {});
DataIn.cbData = encryptedData.size();
DataIn.pbData = encryptedData.data();

方案2:统一存储为「cbData + pbData」

修改Python代码,加密时先写入数据长度,解密时先读取长度再读取对应字节:

import struct

# 修改cryptData函数
def cryptData(file_path, text_bytes):
    encrypted_bytes = Win32CryptProtectData(text_bytes)
    with open(file_path, "wb") as wf:
        # 以小端序写入DWORD类型的长度(和Windows字节序一致)
        wf.write(struct.pack('<L', len(encrypted_bytes)))
        wf.write(encrypted_bytes)

# 修改decryptData函数
def decryptData(file_path):
    with open(file_path, "rb") as rf:
        # 先读取4字节的长度
        cbData = struct.unpack('<L', rf.read(4))[0]
        # 读取对应长度的加密数据
        cipherText = rf.read(cbData)
    return Win32CryptUnprotectData(cipherText)

额外注意事项

  • 确保两边DPAPI的调用标志一致(当前均为0,无需修改)
  • 加密时的熵参数保持一致(当前均为nullptr,无需修改)
  • 字节序必须统一:Windows的DWORD为小端序,Python打包时需指定<L

内容的提问来源于stack exchange,提问作者MashedPotato6587

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 02:30:14