一. 数据保护
1.控制台步骤
通过Nuget安装数据保护程序集【Microsoft.AspNetCore.DataProtection】和依赖注入程序集【Microsoft.Extensions.DependencyInjection】,详见下面代码,进行数据的protect和unprotect。
PS:Core MVC程序中,已经内置上述程序集了。
1 { 2 var serviceCollection = new ServiceCollection(); 3 serviceCollection.AddDataProtection(); 4 var services = serviceCollection.BuildServiceProvider(); 5 var provider = services.GetRequiredService<IDataProtectionProvider>(); 6 var protector = provider.CreateProtector("myProtector"); 7 8 //等价于上面的两句话 9 //var provider = services.GetDataProtectionProvider(); 10 //var protector= services.GetDataProtector("myProtector"); 11 string input = "ypf001"; 12 // protect the payload 13 string protectedPayload = protector.Protect(input); 14 // unprotect the payload 15 string unprotectedPayload = protector.Unprotect(protectedPayload); 16 }
2. DataProtectionProvider 和 IDataProtector
对于多个调用方是线程安全的。 它的目的是,在组件通过调用 CreateProtector获取对IDataProtector 的引用时,它会将该引用用于多次调用 Protect 和 Unprotect。
3. 扩展
(1). 可以层级创建provider,provider.CreateProtector("purpose1").CreateProtector("purpose2")
(2). 加密器的生命周期,安装程序集【Microsoft.AspNetCore.DataProtection.Extensions】,利用ToTimeLimitedDataProtector方法。
1 try 2 { 3 string input = "this is ITimeLimitedDataProtector"; 4 var serviceCollection = new ServiceCollection(); 5 serviceCollection.AddDataProtection(); 6 var services = serviceCollection.BuildServiceProvider(); 7 var provider = services.GetRequiredService<IDataProtectionProvider>(); 8 var protector = provider.CreateProtector("myProtector").ToTimeLimitedDataProtector(); 9 //配置生命周期为5秒 10 string protectedData = protector.Protect(input, TimeSpan.FromSeconds(5)); 11 string stringDatas = protector.Unprotect(protectedData); 12 //等待6s,进行解密,解不了(报异常) 13 Task.Delay(TimeSpan.FromSeconds(6)).Wait(); 14 string stringDatas2 = protector.Unprotect(protectedData); 15 } 16 catch (Exception ex) 17 { 18 Console.WriteLine(ex.Message); 19 }
4. 配置数据保护程序的存储位置
初始化加密器时,系统会基于当前机器的运行环境默认配置,但是有些时候可能需要对这些配置做一些改变,默认支持保存到:文件系统和注册表PersistKeysToFileSystem和PersistKeysToRegistry。
1 { 2 var serviceCollection = new ServiceCollection(); 3 serviceCollection.AddDataProtection() 4 .PersistKeysToFileSystem(new System.IO.DirectoryInfo(@"d: emp-keys")); //保存磁盘 5 //.PersistKeysToRegistry(Registry.CurrentUser.OpenSubKey(@"SOFTWARESamplekeys")); //保存注册表(路径要写实际路径) 6 var services = serviceCollection.BuildServiceProvider(); 7 var provider = services.GetRequiredService<IDataProtectionProvider>(); 8 var protector = provider.CreateProtector("myProtector"); 9 10 string input = "ypf001"; 11 // protect the payload 12 string protectedPayload = protector.Protect(input); 13 // unprotect the payload 14 string unprotectedPayload = protector.Unprotect(protectedPayload); 15 }
PS:另外还可以配置 SQLServer、Redis、Azure、证书,可以配置相关的算法,详见:
https://docs.microsoft.com/zh-cn/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-3.1
上面案例都是通过控制台来演示的,Core MVC更容易,在ConfigureService中直接注册,详见官方文档:
https://docs.microsoft.com/zh-cn/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-3.1
二. Hash的最佳实践
1. Hash最佳实践
通过Nuget安装程序集【Microsoft.AspNetCore.Cryptography.KeyDerivation】,微软开发的单独包,依赖于 PBKDF2 算法实现,不依赖于数据保护系统。 方法KeyDerivation.Pbkdf2将检测当前操作系统, 并尝试选择最适合的例程实现, 在某些情况下提供更好的性能。 (在 Windows 8 上, 它提供的吞吐量Rfc2898DeriveBytes大约为10倍。)
1 { 2 var password = "ypf001"; 3 byte[] salt= Encoding.Default.GetBytes("xxx1111"); 4 Console.WriteLine($"Salt: {Convert.ToBase64String(salt)}"); 5 6 // derive a 256-bit subkey (use HMACSHA1 with 10,000 iterations) 7 string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2( 8 password: password, 9 salt: salt, 10 prf: KeyDerivationPrf.HMACSHA1, 11 iterationCount: 10000, 12 numBytesRequested: 256 / 8)); 13 Console.WriteLine($"Hashed: {hashed}"); 14 }
2. 防止彩虹表情况的攻击
大致原理, 对于通过一个字符串,利用该算法加密每次生成的值都是不同,但是可以利用VerifyHashedPassword方法对应的算法,比较生成的值和原字符串比对,每次加密生成的值和字符都能比对成功, 所以数据库存的那个加密值只是众多加密值中的一个,从而防止了彩虹表的攻击。
1 public class PasswordHasher 2 { 3 /* ======================= 4 * HASHED PASSWORD FORMATS 5 * ======================= 6 * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. 7 * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey } 8 * (All UInt32s are stored big-endian.) 9 */ 10 11 private readonly int _iterCount=1000; 12 13 private readonly RandomNumberGenerator _rng= RandomNumberGenerator.Create(); 14 15 // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized. 16 [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] 17 private static bool ByteArraysEqual(byte[] a, byte[] b) 18 { 19 if (a == null && b == null) 20 { 21 return true; 22 } 23 if (a == null || b == null || a.Length != b.Length) 24 { 25 return false; 26 } 27 var areSame = true; 28 for (var i = 0; i < a.Length; i++) 29 { 30 areSame &= (a[i] == b[i]); 31 } 32 return areSame; 33 } 34 35 /// <summary> 36 /// Returns a hashed representation of the supplied . 37 /// </summary> 38 /// <param name="password">The password to hash.</param> 39 /// <returns>A hashed representation of the supplied for the specified .</returns> 40 public virtual string HashPassword(string password) 41 { 42 if (password == null) 43 { 44 throw new ArgumentNullException(nameof(password)); 45 } 46 47 return Convert.ToBase64String(HashPassword(password, _rng)); 48 } 49 50 51 private byte[] HashPassword(string password, RandomNumberGenerator rng) 52 { 53 return HashPassword(password, rng, 54 prf: KeyDerivationPrf.HMACSHA256, 55 iterCount: _iterCount, 56 saltSize: 128 / 8, 57 numBytesRequested: 256 / 8); 58 } 59 60 private static byte[] HashPassword(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested) 61 { 62 // Produce a version 3 (see comment above) text hash. 63 byte[] salt = new byte[saltSize]; 64 rng.GetBytes(salt); 65 byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested); 66 67 var outputBytes = new byte[13 + salt.Length + subkey.Length]; 68 outputBytes[0] = 0x01; // format marker 69 WriteNetworkByteOrder(outputBytes, 1, (uint)prf); 70 WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount); 71 WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize); 72 Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length); 73 Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length); 74 return outputBytes; 75 } 76 77 private static uint ReadNetworkByteOrder(byte[] buffer, int offset) 78 { 79 return ((uint)(buffer[offset + 0]) << 24) 80 | ((uint)(buffer[offset + 1]) << 16) 81 | ((uint)(buffer[offset + 2]) << 8) 82 | ((uint)(buffer[offset + 3])); 83 } 84 85 /// <summary> 86 /// Returns a indicating the result of a password hash comparison. 87 /// </summary> 88 /// <param name="hashedPassword">The hash value for a user's stored password.</param> 89 /// <param name="providedPassword">The password supplied for comparison.</param> 90 /// <returns>A indicating the result of a password hash comparison.</returns> 91 /// <remarks>Implementations of this method should be time consistent.</remarks> 92 public virtual bool VerifyHashedPassword(string hashedPassword, string providedPassword) 93 { 94 if (hashedPassword == null) 95 { 96 throw new ArgumentNullException(nameof(hashedPassword)); 97 } 98 if (providedPassword == null) 99 { 100 throw new ArgumentNullException(nameof(providedPassword)); 101 } 102 103 byte[] decodedHashedPassword = Convert.FromBase64String(hashedPassword); 104 105 // read the format marker from the hashed password 106 if (decodedHashedPassword.Length == 0) 107 { 108 return false; 109 } 110 111 return VerifyHashedPassword(decodedHashedPassword, providedPassword, out int embeddedIterCount); 112 } 113 114 private static bool VerifyHashedPassword(byte[] hashedPassword, string password, out int iterCount) 115 { 116 iterCount = default; 117 118 try 119 { 120 // Read header information 121 KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1); 122 iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5); 123 int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9); 124 125 // Read the salt: must be >= 128 bits 126 if (saltLength < 128 / 8) 127 { 128 return false; 129 } 130 byte[] salt = new byte[saltLength]; 131 Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length); 132 133 // Read the subkey (the rest of the payload): must be >= 128 bits 134 int subkeyLength = hashedPassword.Length - 13 - salt.Length; 135 if (subkeyLength < 128 / 8) 136 { 137 return false; 138 } 139 byte[] expectedSubkey = new byte[subkeyLength]; 140 Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); 141 142 // Hash the incoming password and verify it 143 byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength); 144 return ByteArraysEqual(actualSubkey, expectedSubkey); 145 } 146 catch 147 { 148 // This should never occur except in the case of a malformed payload, where 149 // we might go off the end of the array. Regardless, a malformed payload 150 // implies verification failed. 151 return false; 152 } 153 } 154 155 private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value) 156 { 157 buffer[offset + 0] = (byte)(value >> 24); 158 buffer[offset + 1] = (byte)(value >> 16); 159 buffer[offset + 2] = (byte)(value >> 8); 160 buffer[offset + 3] = (byte)(value >> 0); 161 } 162 }
1 { 2 string password = "ypf001"; 3 PasswordHasher passwordHasher = new PasswordHasher(); 4 //生成加密值 5 string hashedPassword = passwordHasher.HashPassword(password); 6 //进行比对,ture表示验证通过 7 bool result = passwordHasher.VerifyHashedPassword(hashedPassword, password); 8 //下面是之前生成的加密值,比对也是ture也能通过 9 string has2 = "AQAAAAEAAAPoAAAAEAP2ilbJba6iVmbhGKsNB + s6Jrkxb7GuWhwX / g9t40S77o7yO6wbfM5EKHRM3MRHgQ =="; 10 string has3 = "AQAAAAEAAAPoAAAAEMbR85H1WbzrFxQ73+FS5Im0cw+UMrAf6L3LUzhvCPBIL6qF0aPaBUnPeTqykpVw8A=="; 11 bool result2 = passwordHasher.VerifyHashedPassword(has2, password); 12 bool result3 = passwordHasher.VerifyHashedPassword(has3, password); 13 }
PS:
彩虹表的原理:首先定义哈希加密函数H,Q=H(P)表示将明文密码P加密成哈希串Q;然后定义规约函数R,p=R(Q)表示将哈希串Q转换成明文p,注意p不是真正的密码P。将一个可能的密码p0交替带入H和Q两个函数进行运算,先后得到q1,p1,q2,p2,...,q(n-1),p(n-1),qn,pn。其中p是明文,q是哈希串,它们组成的链称为哈希链,n是哈希链的长度,一般大于2000。将哈希链的首尾元素p0和pn做为一个数对存入表中,中间的其它元素全部删除。由多个数对组成的表称为彩虹表。
彩虹表攻击:找到哈希串Q对应的明文密码P,利用彩虹表进行密码攻击的过程如下:c1=R(Q),将c1与彩虹表中每一个pn进行比对,如果相等,则P=p(n-1),由于彩虹表中只保存了p0和pn,因此需要重新计算该哈希链得到p(n-1);如果没找到相等的pn,计算c2=R(H(c1)),将c2与彩虹表中所有pn进行比对,如果相等,则P=p(n-2),重新计算该哈希链得到p(n-2);如果没找到相等的pn,继续计算c3...以此类推。
彩虹表概念普及:https://blog.csdn.net/whatday/article/details/88527936
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。