├── .coveragerc ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── LICENSE ├── .gitignore ├── README.md ├── test_vectors.py └── balloon.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | 3 | exclude_lines = 4 | if __name__ == .__main__.: 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: daily -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Run tests 14 | run: python -m unittest test_vectors.py 15 | 16 | format-markdown: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Format Markdown with markdownlint 24 | run: | 25 | npm install -g markdownlint-cli 26 | markdownlint --disable MD013 --fix . 27 | git add -A 28 | git diff --cached --exit-code 29 | 30 | format-python: 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | 37 | - name: Format Python with black 38 | run: | 39 | python -m pip install --upgrade pip 40 | pip install black 41 | black . --check 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ignacio Navarro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | .idea/ 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Balloon Hashing 2 | 3 | [![GitHub license](https://img.shields.io/badge/LICENSE-MIT-GREEN)](LICENSE) 4 | 5 | An implementation in Python of Balloon Hashing. All credit to Dan Boneh, Henry Corrigan-Gibbs, and Stuart Schechter. For more information see 6 | the [research paper](https://eprint.iacr.org/2016/027.pdf) or their [website](https://crypto.stanford.edu/balloon/) for this project. All errors in the code are, of course, mine. Feel free to fix any mistakes. 7 | 8 | ## Developer 9 | 10 | Works on Python 3. 11 | 12 | Check test vectors with `python -m unittest`. 13 | 14 | ## Background 15 | 16 | Balloon Hashing is a new hashing function that, according to the paper, is: 17 | 18 | * **Built from Standard Primitives:** Builds on top of other common hashing functions. 19 | * **Has Proven Memory-Hardness Properties:** See paper. 20 | * **Resistant to Cache Attacks:** The idea is that an adversary who can observe the memory access patterns of the buffer in the algorithm (for example through cached side-channels) still can't figure out the password being cached. 21 | * **Practical:** Is as good as the best hashing functions used in production today. 22 | 23 | ## Algorithm 24 | 25 | The algorithm consists of three main parts, as explained in the paper. The first step is the expansion, in which the system fills 26 | up a buffer with pseudorandom bytes derived from the password and salt by computing repeatedly the hash function on a combination 27 | of the password and the previous hash. The second step is mixing, in which the system mixes time_cost number of times the pseudorandom 28 | bytes in the buffer. At each step in the for loop, it updates the nth block to be the hash of the n-1th block, the nth block, 29 | and delta other blocks chosen at random from the buffer. In the last step, the extraction, the system outputs as the hash the last 30 | element in the buffer. 31 | 32 | ## Usage 33 | 34 | An example will suffice to show how it works: 35 | 36 | ```python 37 | >>> import balloon as b 38 | >>> password = "buildmeupbuttercup" 39 | >>> salt = 'JqMcHqUcjinFhQKJ' 40 | >>> print(b.balloon_hash(password, salt)) 41 | 2ec8d833db5f88e584ab793950ecfb21657a3816edea8d9e73ea23c13ba2b740 42 | 43 | # A slightly more advanced usage 44 | >>> delta = 5 45 | >>> time_cost = 18 46 | >>> space_cost = 24 47 | >>> bs = b.balloon(password, salt, space_cost, time_cost, delta=delta) 48 | >>> print(bs.hex()) 49 | 69f86890cef40a7ec5f70daff1ce8e2cde233a15bffa785e7efdb5143af51bfb 50 | ``` 51 | 52 | ## Formatting 53 | 54 | ```bash 55 | pip install black 56 | black . 57 | ``` 58 | -------------------------------------------------------------------------------- /test_vectors.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from balloon import ( 4 | _balloon, 5 | balloon, 6 | balloon_hash, 7 | balloon_m, 8 | balloon_m_hash, 9 | verify, 10 | verify_m, 11 | ) 12 | 13 | 14 | class TestBalloon(unittest.TestCase): 15 | def test_invalid_params(self): 16 | test_vectors = [ 17 | {"args": ("", "", 0, 1, 1), "param": "space_cost"}, 18 | {"args": ("", "", 1, 0, 1), "param": "time_cost"}, 19 | {"args": ("", "", 1, 1, 0), "param": "delta"}, 20 | ] 21 | 22 | for test_vector in test_vectors: 23 | with self.assertRaises(ValueError) as context: 24 | # Test internal function _balloon() as it 25 | # is used by both balloon() and balloon_m() 26 | _balloon(*test_vector["args"]) 27 | self.assertEqual( 28 | str(context.exception), 29 | "'%s' must be a positive integer." % test_vector["param"], 30 | ) 31 | 32 | def test_vectors(self): 33 | test_vectors = [ 34 | { 35 | "password": "hunter42", 36 | "salt": "examplesalt", 37 | "s_cost": 1024, 38 | "t_cost": 3, 39 | "output": "716043dff777b44aa7b88dcbab12c078abecfac9d289c5b5195967aa63440dfb", 40 | }, 41 | { 42 | "password": "", 43 | "salt": "salt", 44 | "s_cost": 3, 45 | "t_cost": 3, 46 | "output": "5f02f8206f9cd212485c6bdf85527b698956701ad0852106f94b94ee94577378", 47 | }, 48 | { 49 | "password": "password", 50 | "salt": "", 51 | "s_cost": 3, 52 | "t_cost": 3, 53 | "output": "20aa99d7fe3f4df4bd98c655c5480ec98b143107a331fd491deda885c4d6a6cc", 54 | }, 55 | { 56 | "password": "\0", 57 | "salt": "\0", 58 | "s_cost": 3, 59 | "t_cost": 3, 60 | "output": "4fc7e302ffa29ae0eac31166cee7a552d1d71135f4e0da66486fb68a749b73a4", 61 | }, 62 | { 63 | "password": "password", 64 | "salt": "salt", 65 | "s_cost": 1, 66 | "t_cost": 1, 67 | "output": "eefda4a8a75b461fa389c1dcfaf3e9dfacbc26f81f22e6f280d15cc18c417545", 68 | }, 69 | ] 70 | 71 | for test_vector in test_vectors: 72 | test_params = list(test_vector.values()) 73 | self.assertEqual(balloon(*test_params[:4]).hex(), test_vector["output"]) 74 | self.assertEqual( 75 | balloon_hash(test_vector["password"], test_vector["salt"]), 76 | balloon(test_vector["password"], test_vector["salt"], 16, 20, 4).hex(), 77 | ) 78 | self.assertTrue(verify(test_vector["output"], *test_params[:4])) 79 | self.assertFalse(verify("0" * 64, *test_params[:4])) 80 | 81 | 82 | class TestBalloonM(unittest.TestCase): 83 | def test_invalid_params(self): 84 | with self.assertRaises(ValueError) as context: 85 | balloon_m("", "", 1, 1, 0, 1) 86 | self.assertEqual( 87 | str(context.exception), "'parallel_cost' must be a positive integer." 88 | ) 89 | 90 | def test_vectors(self): 91 | test_vectors = [ 92 | { 93 | "password": "hunter42", 94 | "salt": "examplesalt", 95 | "s_cost": 1024, 96 | "t_cost": 3, 97 | "p_cost": 4, 98 | "output": "1832bd8e5cbeba1cb174a13838095e7e66508e9bf04c40178990adbc8ba9eb6f", 99 | }, 100 | { 101 | "password": "", 102 | "salt": "salt", 103 | "s_cost": 3, 104 | "t_cost": 3, 105 | "p_cost": 2, 106 | "output": "f8767fe04059cef67b4427cda99bf8bcdd983959dbd399a5e63ea04523716c23", 107 | }, 108 | { 109 | "password": "password", 110 | "salt": "", 111 | "s_cost": 3, 112 | "t_cost": 3, 113 | "p_cost": 3, 114 | "output": "bcad257eff3d1090b50276514857e60db5d0ec484129013ef3c88f7d36e438d6", 115 | }, 116 | { 117 | "password": "password", 118 | "salt": "", 119 | "s_cost": 3, 120 | "t_cost": 3, 121 | "p_cost": 1, 122 | "output": "498344ee9d31baf82cc93ebb3874fe0b76e164302c1cefa1b63a90a69afb9b4d", 123 | }, 124 | { 125 | "password": "\000", 126 | "salt": "\000", 127 | "s_cost": 3, 128 | "t_cost": 3, 129 | "p_cost": 4, 130 | "output": "8a665611e40710ba1fd78c181549c750f17c12e423c11930ce997f04c7153e0c", 131 | }, 132 | { 133 | "password": "\000", 134 | "salt": "\000", 135 | "s_cost": 3, 136 | "t_cost": 3, 137 | "p_cost": 1, 138 | "output": "d9e33c683451b21fb3720afbd78bf12518c1d4401fa39f054b052a145c968bb1", 139 | }, 140 | { 141 | "password": "password", 142 | "salt": "salt", 143 | "s_cost": 1, 144 | "t_cost": 1, 145 | "p_cost": 16, 146 | "output": "a67b383bb88a282aef595d98697f90820adf64582a4b3627c76b7da3d8bae915", 147 | }, 148 | { 149 | "password": "password", 150 | "salt": "salt", 151 | "s_cost": 1, 152 | "t_cost": 1, 153 | "p_cost": 1, 154 | "output": "97a11df9382a788c781929831d409d3599e0b67ab452ef834718114efdcd1c6d", 155 | }, 156 | ] 157 | 158 | for test_vector in test_vectors: 159 | test_params = list(test_vector.values()) 160 | self.assertEqual(balloon_m(*test_params[:5]).hex(), test_vector["output"]) 161 | self.assertEqual( 162 | balloon_m_hash(test_vector["password"], test_vector["salt"]), 163 | balloon_m( 164 | test_vector["password"], test_vector["salt"], 16, 20, 4, 4 165 | ).hex(), 166 | ) 167 | self.assertTrue(verify_m(test_vector["output"], *test_params[:5])) 168 | self.assertFalse(verify_m("0" * 64, *test_params[:5])) 169 | 170 | 171 | if __name__ == "__main__": 172 | unittest.main() 173 | -------------------------------------------------------------------------------- /balloon.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import hashlib 3 | import secrets 4 | 5 | hash_functions = { 6 | "md5": hashlib.md5, 7 | "sha1": hashlib.sha1, 8 | "sha224": hashlib.sha224, 9 | "sha256": hashlib.sha256, 10 | "sha384": hashlib.sha384, 11 | "sha512": hashlib.sha512, 12 | } 13 | 14 | HASH_TYPE = "sha256" 15 | 16 | 17 | def hash_func(*args) -> bytes: 18 | """Concatenate all the arguments and hash the result. 19 | Note that the hash function used can be modified 20 | in the global parameter `HASH_TYPE`. 21 | 22 | Args: 23 | *args: Arguments to concatenate. 24 | 25 | Returns: 26 | bytes: The hashed string. 27 | """ 28 | t = b"" 29 | 30 | for arg in args: 31 | if type(arg) is int: 32 | t += arg.to_bytes(8, "little") 33 | elif type(arg) is str: 34 | t += arg.encode("utf-8") 35 | else: 36 | t += arg 37 | 38 | return hash_functions[HASH_TYPE](t).digest() 39 | 40 | 41 | def expand(buf: list[bytes], cnt: int, space_cost: int) -> int: 42 | """First step of the algorithm. Fill up a buffer with 43 | pseudorandom bytes derived from the password and salt 44 | by computing repeatedly the hash function on a combination 45 | of the password and the previous hash. 46 | 47 | Args: 48 | buf (list[bytes]): A list of hashes as bytes. 49 | cnt (int): Used in a security proof (read the paper). 50 | space_cost (int): The size of the buffer. 51 | 52 | Returns: 53 | int: Counter used in a security proof (read the paper). 54 | """ 55 | for s in range(1, space_cost): 56 | buf.append(hash_func(cnt, buf[s - 1])) 57 | cnt += 1 58 | return cnt 59 | 60 | 61 | def mix( 62 | buf: list[bytes], cnt: int, delta: int, salt: bytes, space_cost: int, time_cost: int 63 | ) -> None: 64 | """Second step of the algorithm. Mix `time_cost` number 65 | of times the pseudorandom bytes in the buffer. At each 66 | step in the for loop, update the nth block to be 67 | the hash of the n-1th block, the nth block, and `delta` 68 | other blocks chosen at random from the buffer `buf`. 69 | 70 | Args: 71 | buf (list[bytes]): A list of hashes as bytes. 72 | cnt (int): Used in a security proof (read the paper). 73 | delta (int): Number of random blocks to mix with. 74 | salt (bytes): A user defined random value for security. 75 | space_cost (int): The size of the buffer. 76 | time_cost (int): Number of rounds to mix. 77 | 78 | Returns: 79 | void: Updates the buffer and counter, but does not 80 | return anything. 81 | """ 82 | for t in range(time_cost): 83 | for s in range(space_cost): 84 | buf[s] = hash_func(cnt, buf[s - 1], buf[s]) 85 | cnt += 1 86 | for i in range(delta): 87 | idx_block = hash_func(t, s, i) 88 | other = ( 89 | int.from_bytes(hash_func(cnt, salt, idx_block), "little") 90 | % space_cost 91 | ) 92 | cnt += 1 93 | buf[s] = hash_func(cnt, buf[s], buf[other]) 94 | cnt += 1 95 | 96 | 97 | def extract(buf: list[bytes]) -> bytes: 98 | """Final step. Return the last value in the buffer. 99 | 100 | Args: 101 | buf (list[bytes]): A list of hashes as bytes. 102 | 103 | Returns: 104 | bytes: Last value of the buffer as bytes. 105 | """ 106 | return buf[-1] 107 | 108 | 109 | def balloon( 110 | password: str, salt: str, space_cost: int, time_cost: int, delta: int = 3 111 | ) -> bytes: 112 | """Main function that collects all the substeps. As 113 | previously mentioned, first expand, then mix, and 114 | finally extract. Note the result is returned as bytes, 115 | for a more friendly function with default values 116 | that returns a hex string, see the function `balloon_hash`. 117 | 118 | Args: 119 | password (str): The main string to hash. 120 | salt (str): A user defined random value for security. 121 | space_cost (int): The size of the buffer. 122 | time_cost (int): Number of rounds to mix. 123 | delta (int, optional): Number of random blocks to mix with. Defaults to 3. 124 | 125 | Returns: 126 | bytes: A series of bytes, the hash. 127 | """ 128 | # Encode salt as bytes to be passed to _balloon() 129 | return _balloon(password, salt.encode("utf-8"), space_cost, time_cost, delta) 130 | 131 | 132 | def _balloon( 133 | password: str, salt: bytes, space_cost: int, time_cost: int, delta: int = 3 134 | ) -> bytes: 135 | """For internal use. Implements steps outlined in `balloon`. 136 | 137 | Args: 138 | password (str): The main string to hash. 139 | salt (bytes): A user defined random value for security. 140 | space_cost (int): The size of the buffer. 141 | time_cost (int): Number of rounds to mix. 142 | delta (int, optional): Number of random blocks to mix with. Defaults to 3. 143 | 144 | Returns: 145 | bytes: A series of bytes, the hash. 146 | """ 147 | if not isinstance(space_cost, int) or space_cost < 1: 148 | raise ValueError("'space_cost' must be a positive integer.") 149 | if not isinstance(time_cost, int) or time_cost < 1: 150 | raise ValueError("'time_cost' must be a positive integer.") 151 | if not isinstance(delta, int) or delta < 1: 152 | raise ValueError("'delta' must be a positive integer.") 153 | buf = [hash_func(0, password, salt)] 154 | cnt = 1 155 | 156 | cnt = expand(buf, cnt, space_cost) 157 | mix(buf, cnt, delta, salt, space_cost, time_cost) 158 | return extract(buf) 159 | 160 | 161 | def balloon_hash(password: str, salt: str) -> str: 162 | """A more friendly client function that just takes 163 | a password and a salt and outputs the hash as a hex string. 164 | 165 | Args: 166 | password (str): The main string to hash. 167 | salt (str): A user defined random value for security. 168 | 169 | Returns: 170 | str: The hash as hex. 171 | """ 172 | delta = 4 173 | time_cost = 20 174 | space_cost = 16 175 | return balloon(password, salt, space_cost, time_cost, delta=delta).hex() 176 | 177 | 178 | def balloon_m( 179 | password: str, 180 | salt: str, 181 | space_cost: int, 182 | time_cost: int, 183 | parallel_cost: int, 184 | delta: int = 3, 185 | ) -> bytes: 186 | """M-core variant of the Balloon hashing algorithm. Note the result 187 | is returned as bytes, for a more friendly function with default 188 | values that returns a hex string, see the function `balloon_m_hash`. 189 | 190 | Args: 191 | password (str): The main string to hash. 192 | salt (str): A user defined random value for security. 193 | space_cost (int): The size of the buffer. 194 | time_cost (int): Number of rounds to mix. 195 | parallel_cost (int): Number of concurrent instances. 196 | delta (int, optional): Number of random blocks to mix with. Defaults to 3. 197 | 198 | Returns: 199 | bytes: A series of bytes, the hash. 200 | """ 201 | if not isinstance(parallel_cost, int) or parallel_cost < 1: 202 | raise ValueError("'parallel_cost' must be a positive integer.") 203 | output = b"" 204 | 205 | with concurrent.futures.ThreadPoolExecutor() as executor: 206 | futures = [] 207 | 208 | for p in range(parallel_cost): 209 | parallel_salt = b"" + salt.encode("utf-8") + (p + 1).to_bytes(8, "little") 210 | futures.append( 211 | executor.submit( 212 | _balloon, 213 | password, 214 | parallel_salt, 215 | space_cost, 216 | time_cost, 217 | delta=delta, 218 | ) 219 | ) 220 | completed_futures = concurrent.futures.as_completed(futures) 221 | output = next(completed_futures).result() 222 | for future in completed_futures: 223 | output = bytes([_a ^ _b for _a, _b in zip(output, future.result())]) 224 | 225 | return hash_func(password, salt, output) 226 | 227 | 228 | def balloon_m_hash(password: str, salt: str) -> str: 229 | """A more friendly client function that just takes 230 | a password and a salt and outputs the hash as a hex string. 231 | This uses the M-core variant of the Balloon hashing algorithm. 232 | 233 | Args: 234 | password (str): The main string to hash. 235 | salt (str): A user defined random value for security. 236 | 237 | Returns: 238 | str: The hash as hex. 239 | """ 240 | delta = 4 241 | time_cost = 20 242 | space_cost = 16 243 | parallel_cost = 4 244 | return balloon_m( 245 | password, salt, space_cost, time_cost, parallel_cost, delta=delta 246 | ).hex() 247 | 248 | 249 | def verify( 250 | hash: str, password: str, salt: str, space_cost: int, time_cost: int, delta: int = 3 251 | ) -> bool: 252 | """Verify that hash matches password when hashed with salt, space_cost, 253 | time_cost, and delta. 254 | 255 | Args: 256 | hash (str): The hash to check against. 257 | password (str): The password to verify. 258 | salt (str): A user defined random value for security. 259 | space_cost (int): The size of the buffer. 260 | time_cost (int): Number of rounds to mix. 261 | delta (int): Number of random blocks to mix with. Defaults to 3. 262 | 263 | Returns: 264 | bool: True if password matches hash, otherwise False. 265 | """ 266 | return secrets.compare_digest( 267 | balloon(password, salt, space_cost, time_cost, delta).hex(), hash 268 | ) 269 | 270 | 271 | def verify_m( 272 | hash: str, 273 | password: str, 274 | salt: str, 275 | space_cost: int, 276 | time_cost: int, 277 | parallel_cost: int, 278 | delta: int = 3, 279 | ) -> bool: 280 | """Verify that hash matches password when hashed with salt, space_cost, 281 | time_cost, parallel_cost, and delta. 282 | This uses the M-core variant of the Balloon hashing algorithm. 283 | 284 | Args: 285 | hash (str): The hash to check against. 286 | password (str): The password to verify. 287 | salt (str): A user defined random value for security. 288 | space_cost (int): The size of the buffer. 289 | time_cost (int): Number of rounds to mix. 290 | parallel_cost (int): Number of concurrent instances. 291 | delta (int): Number of random blocks to mix with. Defaults to 3. 292 | 293 | Returns: 294 | bool: True if password matches hash, otherwise False. 295 | """ 296 | return secrets.compare_digest( 297 | balloon_m(password, salt, space_cost, time_cost, parallel_cost, delta).hex(), 298 | hash, 299 | ) 300 | --------------------------------------------------------------------------------