在C#开发中,处理用户密码、接口密钥等敏感数据时,直接使用明文存储存在极大的安全风险,而自行实现加密算法又需要额外处理密钥管理的问题。Windows系统提供的Data Protection API(简称DPAPI)可以很好地解决这个问题,它内置在系统底层,会自动管理加密密钥,开发者只需要调用对应的接口就能完成数据的加密和解密操作,非常适合本地敏感数据的保护场景。
DPAPI的核心特性
DPAPI是Windows操作系统自带的加密服务,它的核心优势在于不需要开发者手动生成、存储和管理加密密钥,系统会自动使用当前用户的登录凭据或者计算机的唯一标识来生成加密密钥。DPAPI主要有两个作用范围:
- 当前用户范围:加密的数据只有当前登录的用户可以解密,其他用户即使拿到加密后的数据也无法解密,适合存储用户个人的敏感配置信息。
- 本地计算机范围:加密的数据可以在当前计算机上的所有用户账户下解密,适合存储应用程序级别的共享敏感数据。
C#中调用DPAPI的命名空间
C#中并没有直接封装DPAPI的托管类,需要调用Windows的原生API,我们可以通过平台调用(P/Invoke)的方式调用crypt32.dll中的两个核心函数:CryptProtectData用于加密数据,CryptUnprotectData用于解密数据。首先需要定义这两个函数的签名和相关的数据结构。
加密方法的实现
首先定义需要用到的结构体和函数签名,DATA_BLOB是存储二进制数据块的结构,CRYPTPROTECT_PROMPTSTRUCT是加密时的提示信息结构,实际使用中可以为空。
using System;
using System.Runtime.InteropServices;
using System.Text;
public class DpapiHelper
{
// 定义DATA_BLOB结构体,用于存储二进制数据
[StructLayout(LayoutKind.Sequential)]
public struct DATA_BLOB
{
public int cbData;
public IntPtr pbData;
}
// 定义加密提示结构体,实际使用时可以不需要额外设置
[StructLayout(LayoutKind.Sequential)]
public struct CRYPTPROTECT_PROMPTSTRUCT
{
public int cbSize;
public int dwPromptFlags;
public IntPtr hwndApp;
public string szPrompt;
}
// 加密作用范围枚举
public enum CryptProtectFlags
{
// 仅当前用户可解密
CRYPTPROTECT_LOCAL_MACHINE = 0x04,
// 本地计算机所有用户可解密,需要配合上面的标志使用
// 不设置该标志则默认是当前用户范围
}
// 导入CryptProtectData函数,用于加密数据
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool CryptProtectData(
ref DATA_BLOB pDataIn,
string szDataDescr,
ref DATA_BLOB pOptionalEntropy,
IntPtr pvReserved,
ref CRYPTPROTECT_PROMPTSTRUCT pPromptStruct,
int dwFlags,
ref DATA_BLOB pDataOut
);
// 导入CryptUnprotectData函数,用于解密数据
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool CryptUnprotectData(
ref DATA_BLOB pDataIn,
out string ppszDataDescr,
ref DATA_BLOB pOptionalEntropy,
IntPtr pvReserved,
ref CRYPTPROTECT_PROMPTSTRUCT pPromptStruct,
int dwFlags,
ref DATA_BLOB pDataOut
);
// 导入LocalFree函数,释放非托管内存
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LocalFree(IntPtr hMem);
}
接下来实现加密的公开方法,方法接收明文字符串和可选的作用范围参数,返回加密后的Base64字符串,方便存储和传输。
public static string Encrypt(string plainText, bool useLocalMachineScope = false)
{
if (string.IsNullOrEmpty(plainText))
{
throw new ArgumentException("明文不能为空");
}
// 将明文字符串转换为字节数组
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
IntPtr plainPtr = Marshal.AllocHGlobal(plainBytes.Length);
Marshal.Copy(plainBytes, 0, plainPtr, plainBytes.Length);
// 初始化输入数据块
DATA_BLOB dataIn = new DATA_BLOB();
dataIn.cbData = plainBytes.Length;
dataIn.pbData = plainPtr;
// 初始化输出数据块
DATA_BLOB dataOut = new DATA_BLOB();
dataOut.cbData = 0;
dataOut.pbData = IntPtr.Zero;
// 初始化提示结构
CRYPTPROTECT_PROMPTSTRUCT promptStruct = new CRYPTPROTECT_PROMPTSTRUCT();
promptStruct.cbSize = Marshal.SizeOf(typeof(CRYPTPROTECT_PROMPTSTRUCT));
promptStruct.dwPromptFlags = 0;
promptStruct.hwndApp = IntPtr.Zero;
promptStruct.szPrompt = null;
// 设置加密标志,默认是当前用户范围
int flags = 0;
if (useLocalMachineScope)
{
flags = (int)CryptProtectFlags.CRYPTPROTECT_LOCAL_MACHINE;
}
try
{
// 调用加密函数
bool result = CryptProtectData(
ref dataIn,
"DPAPI加密数据", // 数据描述,可用于标识数据用途
ref dataIn, // 可选熵值,这里直接使用输入数据作为熵值,也可以自定义
IntPtr.Zero,
ref promptStruct,
flags,
ref dataOut
);
if (!result)
{
int errorCode = Marshal.GetLastWin32Error();
throw new Exception($"加密失败,错误码:{errorCode}");
}
// 将加密后的字节数组从非托管内存复制到托管数组
byte[] encryptedBytes = new byte[dataOut.cbData];
Marshal.Copy(dataOut.pbData, encryptedBytes, 0, dataOut.cbData);
// 释放非托管内存
LocalFree(dataOut.pbData);
// 返回Base64格式的加密字符串
return Convert.ToBase64String(encryptedBytes);
}
finally
{
// 释放明文字节的非托管内存
Marshal.FreeHGlobal(plainPtr);
}
}
解密方法的实现
解密方法接收加密后的Base64字符串,返回解密后的明文字符串,需要和加密时使用的范围和熵值匹配才能解密成功。
public static string Decrypt(string encryptedBase64)
{
if (string.IsNullOrEmpty(encryptedBase64))
{
throw new ArgumentException("加密字符串不能为空");
}
// 将Base64字符串转换为字节数组
byte[] encryptedBytes = Convert.FromBase64String(encryptedBase64);
IntPtr encryptedPtr = Marshal.AllocHGlobal(encryptedBytes.Length);
Marshal.Copy(encryptedBytes, 0, encryptedPtr, encryptedBytes.Length);
// 初始化输入数据块
DATA_BLOB dataIn = new DATA_BLOB();
dataIn.cbData = encryptedBytes.Length;
dataIn.pbData = encryptedPtr;
// 初始化输出数据块
DATA_BLOB dataOut = new DATA_BLOB();
dataOut.cbData = 0;
dataOut.pbData = IntPtr.Zero;
// 初始化提示结构
CRYPTPROTECT_PROMPTSTRUCT promptStruct = new CRYPTPROTECT_PROMPTSTRUCT();
promptStruct.cbSize = Marshal.SizeOf(typeof(CRYPTPROTECT_PROMPTSTRUCT));
promptStruct.dwPromptFlags = 0;
promptStruct.hwndApp = IntPtr.Zero;
promptStruct.szPrompt = null;
try
{
// 调用解密函数
bool result = CryptUnprotectData(
ref dataIn,
out string dataDescr,
ref dataIn, // 熵值需要和加密时一致
IntPtr.Zero,
ref promptStruct,
0,
ref dataOut
);
if (!result)
{
int errorCode = Marshal.GetLastWin32Error();
throw new Exception($"解密失败,错误码:{errorCode}");
}
// 将解密后的字节数组从非托管内存复制到托管数组
byte[] decryptedBytes = new byte[dataOut.cbData];
Marshal.Copy(dataOut.pbData, decryptedBytes, 0, dataOut.cbData);
// 释放非托管内存
LocalFree(dataOut.pbData);
// 将字节数组转换为明文字符串
return Encoding.UTF8.GetString(decryptedBytes);
}
finally
{
// 释放加密字节的非托管内存
Marshal.FreeHGlobal(encryptedPtr);
}
}
使用示例
下面演示如何调用上面的加密和解密方法,分别测试当前用户范围和本地计算机范围的加密效果。
class Program
{
static void Main(string[] args)
{
string sensitiveData = "my_secret_password_123";
// 测试当前用户范围加密解密
Console.WriteLine("当前用户范围加密测试:");
string encrypted1 = DpapiHelper.Encrypt(sensitiveData, false);
Console.WriteLine($"加密结果:{encrypted1}");
string decrypted1 = DpapiHelper.Decrypt(encrypted1);
Console.WriteLine($"解密结果:{decrypted1}");
Console.WriteLine();
// 测试本地计算机范围加密解密
Console.WriteLine("本地计算机范围加密测试:");
string encrypted2 = DpapiHelper.Encrypt(sensitiveData, true);
Console.WriteLine($"加密结果:{encrypted2}");
string decrypted2 = DpapiHelper.Decrypt(encrypted2);
Console.WriteLine($"解密结果:{decrypted2}");
}
}
注意事项
- DPAPI是Windows系统特有的接口,所以上述代码只能在Windows环境下运行,无法在Linux或macOS系统中使用。
- 当前用户范围的加密数据,只有加密时的登录用户才能解密,如果用户修改了登录密码或者切换了用户账户,之前加密的数据将无法解密。
- 本地计算机范围的加密数据虽然可以被当前计算机的所有用户解密,但是不能在其他计算机上解密,因为密钥和计算机硬件信息绑定。
- 如果需要在不同计算机之间共享加密数据,DPAPI并不适用,此时应该选择其他支持密钥交换的加密算法。
C#DPAPIData_Protection_API数据加密修改时间:2026-06-11 10:03:29