mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Refs #26033 -- Added password hasher support for Argon2 v1.3.
The previous version of Argon2 uses encoded hashes of the form: $argon2d$m=8,t=1,p=1$<salt>$<data> The new version of Argon2 adds its version into the hash: $argon2d$v=19$m=8,t=1,p=1$<salt>$<data> This lets Django handle both version properly.
This commit is contained in:
		
				
					committed by
					
						 Tim Graham
						Tim Graham
					
				
			
			
				
	
			
			
			
						parent
						
							1ba0b22a7a
						
					
				
				
					commit
					a5033dbc58
				
			| @@ -327,11 +327,11 @@ class Argon2PasswordHasher(BasePasswordHasher): | |||||||
|  |  | ||||||
|     def verify(self, password, encoded): |     def verify(self, password, encoded): | ||||||
|         argon2 = self._load_library() |         argon2 = self._load_library() | ||||||
|         algorithm, data = encoded.split('$', 1) |         algorithm, rest = encoded.split('$', 1) | ||||||
|         assert algorithm == self.algorithm |         assert algorithm == self.algorithm | ||||||
|         try: |         try: | ||||||
|             return argon2.low_level.verify_secret( |             return argon2.low_level.verify_secret( | ||||||
|                 force_bytes('$' + data), |                 force_bytes('$' + rest), | ||||||
|                 force_bytes(password), |                 force_bytes(password), | ||||||
|                 type=argon2.low_level.Type.I, |                 type=argon2.low_level.Type.I, | ||||||
|             ) |             ) | ||||||
| @@ -339,29 +339,30 @@ class Argon2PasswordHasher(BasePasswordHasher): | |||||||
|             return False |             return False | ||||||
|  |  | ||||||
|     def safe_summary(self, encoded): |     def safe_summary(self, encoded): | ||||||
|         algorithm, variety, raw_pars, salt, data = encoded.split('$', 5) |         (algorithm, variety, version, time_cost, memory_cost, parallelism, | ||||||
|         pars = dict(bit.split('=', 1) for bit in raw_pars.split(',')) |             salt, data) = self._decode(encoded) | ||||||
|         assert algorithm == self.algorithm |         assert algorithm == self.algorithm | ||||||
|         assert len(pars) == 3 and 't' in pars and 'm' in pars and 'p' in pars |  | ||||||
|         return OrderedDict([ |         return OrderedDict([ | ||||||
|             (_('algorithm'), algorithm), |             (_('algorithm'), algorithm), | ||||||
|             (_('variety'), variety), |             (_('variety'), variety), | ||||||
|             (_('memory cost'), int(pars['m'])), |             (_('version'), version), | ||||||
|             (_('time cost'), int(pars['t'])), |             (_('memory cost'), memory_cost), | ||||||
|             (_('parallelism'), int(pars['p'])), |             (_('time cost'), time_cost), | ||||||
|  |             (_('parallelism'), parallelism), | ||||||
|             (_('salt'), mask_hash(salt)), |             (_('salt'), mask_hash(salt)), | ||||||
|             (_('hash'), mask_hash(data)), |             (_('hash'), mask_hash(data)), | ||||||
|         ]) |         ]) | ||||||
|  |  | ||||||
|     def must_update(self, encoded): |     def must_update(self, encoded): | ||||||
|         algorithm, variety, raw_pars, salt, data = encoded.split('$', 5) |         (algorithm, variety, version, time_cost, memory_cost, parallelism, | ||||||
|         pars = dict([bit.split('=', 1) for bit in raw_pars.split(',')]) |             salt, data) = self._decode(encoded) | ||||||
|         assert algorithm == self.algorithm |         assert algorithm == self.algorithm | ||||||
|         assert len(pars) == 3 and 't' in pars and 'm' in pars and 'p' in pars |         argon2 = self._load_library() | ||||||
|         return ( |         return ( | ||||||
|             self.time_cost != int(pars['t']) or |             argon2.low_level.ARGON2_VERSION != version or | ||||||
|             self.memory_cost != int(pars['m']) or |             self.time_cost != time_cost or | ||||||
|             self.parallelism != int(pars['p']) |             self.memory_cost != memory_cost or | ||||||
|  |             self.parallelism != parallelism | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def harden_runtime(self, password, encoded): |     def harden_runtime(self, password, encoded): | ||||||
| @@ -369,6 +370,33 @@ class Argon2PasswordHasher(BasePasswordHasher): | |||||||
|         # hardening algorithm. |         # hardening algorithm. | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  |     def _decode(self, encoded): | ||||||
|  |         """ | ||||||
|  |         Split an encoded hash and return: ( | ||||||
|  |             algorithm, variety, version, time_cost, memory_cost, | ||||||
|  |             parallelism, salt, data, | ||||||
|  |         ). | ||||||
|  |         """ | ||||||
|  |         bits = encoded.split('$') | ||||||
|  |         if len(bits) == 5: | ||||||
|  |             # Argon2 < 1.3 | ||||||
|  |             algorithm, variety, raw_params, salt, data = bits | ||||||
|  |             version = 0x10 | ||||||
|  |         else: | ||||||
|  |             assert len(bits) == 6 | ||||||
|  |             algorithm, variety, raw_version, raw_params, salt, data = bits | ||||||
|  |             assert raw_version.startswith('v=') | ||||||
|  |             version = int(raw_version[len('v='):]) | ||||||
|  |         params = dict(bit.split('=', 1) for bit in raw_params.split(',')) | ||||||
|  |         assert len(params) == 3 and all(x in params for x in ('t', 'm', 'p')) | ||||||
|  |         time_cost = int(params['t']) | ||||||
|  |         memory_cost = int(params['m']) | ||||||
|  |         parallelism = int(params['p']) | ||||||
|  |         return ( | ||||||
|  |             algorithm, variety, version, time_cost, memory_cost, parallelism, | ||||||
|  |             salt, data, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class BCryptSHA256PasswordHasher(BasePasswordHasher): | class BCryptSHA256PasswordHasher(BasePasswordHasher): | ||||||
|     """ |     """ | ||||||
|   | |||||||
| @@ -142,7 +142,7 @@ Running all the tests | |||||||
| If you want to run the full suite of tests, you'll need to install a number of | If you want to run the full suite of tests, you'll need to install a number of | ||||||
| dependencies: | dependencies: | ||||||
|  |  | ||||||
| *  argon2-cffi_ 16.0.0+ | *  argon2-cffi_ 16.1.0+ | ||||||
| *  bcrypt_ | *  bcrypt_ | ||||||
| *  docutils_ | *  docutils_ | ||||||
| *  enum34_ (Python 2 only) | *  enum34_ (Python 2 only) | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							| @@ -49,7 +49,7 @@ setup( | |||||||
|     ]}, |     ]}, | ||||||
|     extras_require={ |     extras_require={ | ||||||
|         "bcrypt": ["bcrypt"], |         "bcrypt": ["bcrypt"], | ||||||
|         "argon2": ["argon2-cffi >= 16.0.0"], |         "argon2": ["argon2-cffi >= 16.1.0"], | ||||||
|     }, |     }, | ||||||
|     zip_safe=False, |     zip_safe=False, | ||||||
|     classifiers=[ |     classifiers=[ | ||||||
|   | |||||||
| @@ -457,12 +457,44 @@ class TestUtilsHashPassArgon2(SimpleTestCase): | |||||||
|         self.assertTrue(is_password_usable(blank_encoded)) |         self.assertTrue(is_password_usable(blank_encoded)) | ||||||
|         self.assertTrue(check_password('', blank_encoded)) |         self.assertTrue(check_password('', blank_encoded)) | ||||||
|         self.assertFalse(check_password(' ', blank_encoded)) |         self.assertFalse(check_password(' ', blank_encoded)) | ||||||
|  |         # Old hashes without version attribute | ||||||
|  |         encoded = ( | ||||||
|  |             'argon2$argon2i$m=8,t=1,p=1$c29tZXNhbHQ$gwQOXSNhxiOxPOA0+PY10P9QFO' | ||||||
|  |             '4NAYysnqRt1GSQLE55m+2GYDt9FEjPMHhP2Cuf0nOEXXMocVrsJAtNSsKyfg' | ||||||
|  |         ) | ||||||
|  |         self.assertTrue(check_password('secret', encoded)) | ||||||
|  |         self.assertFalse(check_password('wrong', encoded)) | ||||||
|  |  | ||||||
|     def test_argon2_upgrade(self): |     def test_argon2_upgrade(self): | ||||||
|         self._test_argon2_upgrade('time_cost', 'time cost', 1) |         self._test_argon2_upgrade('time_cost', 'time cost', 1) | ||||||
|         self._test_argon2_upgrade('memory_cost', 'memory cost', 16) |         self._test_argon2_upgrade('memory_cost', 'memory cost', 16) | ||||||
|         self._test_argon2_upgrade('parallelism', 'parallelism', 1) |         self._test_argon2_upgrade('parallelism', 'parallelism', 1) | ||||||
|  |  | ||||||
|  |     def test_argon2_version_upgrade(self): | ||||||
|  |         hasher = get_hasher('argon2') | ||||||
|  |         state = {'upgraded': False} | ||||||
|  |         encoded = ( | ||||||
|  |             'argon2$argon2i$m=8,t=1,p=1$c29tZXNhbHQ$gwQOXSNhxiOxPOA0+PY10P9QFO' | ||||||
|  |             '4NAYysnqRt1GSQLE55m+2GYDt9FEjPMHhP2Cuf0nOEXXMocVrsJAtNSsKyfg' | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         def setter(password): | ||||||
|  |             state['upgraded'] = True | ||||||
|  |  | ||||||
|  |         old_m = hasher.memory_cost | ||||||
|  |         old_t = hasher.time_cost | ||||||
|  |         old_p = hasher.parallelism | ||||||
|  |         try: | ||||||
|  |             hasher.memory_cost = 8 | ||||||
|  |             hasher.time_cost = 1 | ||||||
|  |             hasher.parallelism = 1 | ||||||
|  |             self.assertTrue(check_password('secret', encoded, setter, 'argon2')) | ||||||
|  |             self.assertTrue(state['upgraded']) | ||||||
|  |         finally: | ||||||
|  |             hasher.memory_cost = old_m | ||||||
|  |             hasher.time_cost = old_t | ||||||
|  |             hasher.parallelism = old_p | ||||||
|  |  | ||||||
|     def _test_argon2_upgrade(self, attr, summary_key, new_value): |     def _test_argon2_upgrade(self, attr, summary_key, new_value): | ||||||
|         hasher = get_hasher('argon2') |         hasher = get_hasher('argon2') | ||||||
|         self.assertEqual('argon2', hasher.algorithm) |         self.assertEqual('argon2', hasher.algorithm) | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| argon2-cffi == 16.0.0 | argon2-cffi >= 16.1.0 | ||||||
| bcrypt | bcrypt | ||||||
| docutils | docutils | ||||||
| geoip2 | geoip2 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user