C# 高级教程 - 13 加密与安全

安全是现代软件开发的基石。.NET 提供了完整的安全编程基础设施,包括加密算法、哈希函数、数字签名以及安全编码实践。本文将从实际应用角度出发,讲解 .NET 中加密与安全的核心概念和最佳实践。

哈希与校验

密码哈希

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System.Security.Cryptography;

// 普通哈希(不适合密码存储 —— 速度太快,易于暴力破解)
byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes("password123"));
Console.WriteLine(Convert.ToHexString(hash));
// 60E62520C8AEF3D7B0C9A1D9F3B4C5D6...

// 密码专用哈希 —— PBKDF2(可调节迭代次数增加破解成本)
string HashPassword(string password)
{
var salt = RandomNumberGenerator.GetBytes(16);
var hash = Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
iterations: 100_000, // 推荐 ≥ 600,000
HashAlgorithmName.SHA256,
outputLength: 32);

// 返回 salt + hash(Base64 编码)
return Convert.ToBase64String(salt) + ":" + Convert.ToBase64String(hash);
}

bool VerifyPassword(string password, string storedHash)
{
var parts = storedHash.Split(':');
var salt = Convert.FromBase64String(parts[0]);
var hash = Convert.FromBase64String(parts[1]);

var computedHash = Rfc2898DeriveBytes.Pbkdf2(
password, salt, 100_000, HashAlgorithmName.SHA256, hash.Length);

return CryptographicOperations.FixedTimeEquals(hash, computedHash);
}

使用 CryptographicOperations.FixedTimeEquals 进行哈希比较,防止计时攻击(Timing Attack)。

常见哈希算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 快速校验(文件完整性检查)
string ComputeFileHash(string filePath)
{
using var stream = File.OpenRead(filePath);
var hash = SHA256.HashData(stream);
return Convert.ToHexString(hash).ToLowerInvariant();
}

// .NET 5+ 提供了更简洁的 API
var hash = SHA256.HashData(utf8Bytes);

// HMAC —— 带密钥的哈希(防止篡改)
string ComputeHmac(string message, byte[] key)
{
var hmac = HMACSHA256.HashData(key, Encoding.UTF8.GetBytes(message));
return Convert.ToHexString(hmac);
}

对称加密

加密和解密使用相同密钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class AesEncryption
{
public static (byte[] ciphertext, byte[] iv, byte[] key) Encrypt(string plaintext)
{
using var aes = Aes.Create();
aes.GenerateKey();
aes.GenerateIV();

byte[] ciphertext = aes.EncryptCbc(
Encoding.UTF8.GetBytes(plaintext),
aes.IV,
PaddingMode.PKCS7);

return (ciphertext, aes.IV, aes.Key);
}

public static string Decrypt(byte[] ciphertext, byte[] key, byte[] iv)
{
using var aes = Aes.Create();
byte[] plaintext = aes.DecryptCbc(
ciphertext, iv, PaddingMode.PKCS7);

return Encoding.UTF8.GetString(plaintext);
}
}

// .NET 6+ 推荐的 Authenticated Encryption(认证加密 —— 防止篡改)
public static byte[] EncryptAuthenticated(string plaintext, byte[] key, byte[] nonce)
{
using var aes = new AesGcm(key);
byte[] ciphertext = new byte[plaintext.Length];
byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize];

aes.Encrypt(nonce, Encoding.UTF8.GetBytes(plaintext),
ciphertext, tag);

// 返回 ciphertext + tag,解密时校验 tag 确保完整性
return [.. ciphertext, .. tag];
}

非对称加密

公钥加密,私钥解密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class RsaEncryption
{
public static (string publicKey, string privateKey) GenerateKeys()
{
using var rsa = RSA.Create(2048); // 2048 或 4096 位

// 导出公钥(共享给他人)
string publicKey = rsa.ToXmlString(false);

// 导出私钥(自己保管)
string privateKey = rsa.ToXmlString(true);

return (publicKey, privateKey);
}

public static string Encrypt(string plaintext, string publicKeyXml)
{
using var rsa = RSA.Create();
rsa.FromXmlString(publicKeyXml);

byte[] encrypted = rsa.Encrypt(
Encoding.UTF8.GetBytes(plaintext),
RSAEncryptionPadding.OaepSHA256);

return Convert.ToBase64String(encrypted);
}

public static string Decrypt(string ciphertext, string privateKeyXml)
{
using var rsa = RSA.Create();
rsa.FromXmlString(privateKeyXml);

byte[] decrypted = rsa.Decrypt(
Convert.FromBase64String(ciphertext),
RSAEncryptionPadding.OaepSHA256);

return Encoding.UTF8.GetString(decrypted);
}
}

ECDH —— 密钥交换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 双方各自生成密钥对,交换公钥,计算出相同的共享密钥
using var alice = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256);
using var bob = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256);

// 交换公钥
byte[] alicePublicKey = alice.PublicKey.ExportSubjectPublicKeyInfo();
byte[] bobPublicKey = bob.PublicKey.ExportSubjectPublicKeyInfo();

// 各自计算共享密钥
byte[] aliceShared = alice.DeriveKeyMaterial(
ECDiffieHellmanPublicKey.CreateFromSubjectPublicKeyInfo(
bobPublicKey, out _));
byte[] bobShared = bob.DeriveKeyMaterial(
ECDiffieHellmanPublicKey.CreateFromSubjectPublicKeyInfo(
alicePublicKey, out _));

// aliceShared == bobShared(相同的 32 字节对称密钥)

数字签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class DigitalSignature
{
public static byte[] SignData(string data, string privateKeyXml)
{
using var rsa = RSA.Create();
rsa.FromXmlString(privateKeyXml);

return rsa.SignData(
Encoding.UTF8.GetBytes(data),
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
}

public static bool VerifySignature(
string data, byte[] signature, string publicKeyXml)
{
using var rsa = RSA.Create();
rsa.FromXmlString(publicKeyXml);

return rsa.VerifyData(
Encoding.UTF8.GetBytes(data),
signature,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
}
}

SecureString 与安全内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Security;

// 敏感信息在内存中加密(避免纯文本泄露)
using var securePwd = new SecureString();
foreach (char c in "P@ssw0rd")
securePwd.AppendChar(c);
securePwd.MakeReadOnly();

// 使用 —— 注意 SecureString 在 .NET Core 中不再推荐
// 更推荐使用 ZeroMemory 手动清除敏感数据
byte[] sensitiveData = Encoding.UTF8.GetBytes("my-api-key");
try
{
// 使用敏感数据
}
finally
{
CryptographicOperations.ZeroMemory(sensitiveData);
}

安全编码实践

1. SQL 注入防护

1
2
3
4
5
6
7
// 危险:直接拼接 SQL
string sql = $"SELECT * FROM Users WHERE Name = '{userInput}'"; // ❌

// 安全:参数化查询
using var cmd = new SqlCommand(
"SELECT * FROM Users WHERE Name = @name");
cmd.Parameters.AddWithValue("@name", userInput);

2. XSS 防护

1
2
3
4
5
6
7
8
// 编码输出防止 XSS
string userContent = "<script>alert('xss')</script>";

// HTML 编码
string safe = System.Web.HttpUtility.HtmlEncode(userContent);

// URL 编码
string urlSafe = System.Web.HttpUtility.UrlEncode(userContent);

3. CSRF 防护

1
2
3
4
5
6
// ASP.NET Core 自带 AntiForgeryToken
[ValidateAntiForgeryToken]
public IActionResult ChangePassword(ChangePasswordModel model)
{
// 需要表单中包含有效的 AntiForgeryToken
}

4. 证书验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 生产环境:严格验证证书
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator // ❌ 仅测试
};

// 正确:使用默认验证(OS 信任链)
var safeHandler = new HttpClientHandler();

// 自定义验证
var customHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (sender, cert, chain, errors) =>
{
if (errors == SslPolicyErrors.None) return true;

// 记录错误,根据安全策略决定是否放行
return false;
}
};

加密算法选型

算法 用途 推荐状态
SHA-256/384/512 文件校验、HMAC ✅ 推荐
PBKDF2 / bcrypt / Argon2 密码存储 ✅ 推荐
AES-256-GCM 数据加密 ✅ 强力推荐
RSA 2048+ / ECDSA 非对称加密/签名 ✅ 推荐
HMAC-SHA256 消息完整性校验 ✅ 推荐
MD5 / SHA-1 任何安全用途 ❌ 已废弃
DES / 3DES 任何加密用途 ❌ 已废弃
RC4 任何用途 ❌ 已废弃

总结:安全编程不是简单地”添加加密”——而是在正确的地方使用正确的算法,并正确处理密钥、输入验证、输出编码等全链路安全问题。.NET 提供了丰富的加密 API,但开发者仍需了解何时使用对称 vs 非对称、何时使用认证加密、以及如何在性能和安全之间取得平衡。记住:永远不要自己实现加密算法,永远使用经过广泛审查的标准库。

ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
分享: