第二十五节:数据保护程序和Hash的最佳实现(彩虹表原理)

一. 数据保护

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     }
PasswordHasher
 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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
原文地址:https://www.cnblogs.com/yaopengfei/p/12121755.html