{
    "version": "https://jsonfeed.org/version/1.1",
    "title": "Explainers  dade",
    "description": "  Explainers  dade",
    "home_page_url": "https://0xda.de/",
    "feed_url": "https://0xda.de/tags/explainers/index.json",
    "language": "en",
    "authors": [
        {
            "name": "0xdade",
            "url": "https://0xda.de",
            "avatar": "https://0xda.de/img/dade-transparent-logo.png"
        }
    ],
    "items": [
        {
            "title": "Anatomy of a Hash",
            "date_published": "2023-01-15T15:50:13-07:00",
            "date_modified": "2024-03-17T13:11:03-07:00",
            "id": "https://0xda.de/blog/2023/01/anatomy-of-a-hash/",
            "url": "https://0xda.de/blog/2023/01/anatomy-of-a-hash/",
            "content_html": "\u003cblockquote\u003e\n\u003cp\u003eWhat is a hash? How do I decide which hashing algorithm to use? Where can I find 2500 words to read about password hashing?\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThese are all questions I\u0026rsquo;ve received while working as a security engineer, most often from software developers. There are tons of resources out there that resolve each of these questions, but I wanted to write up my own resource that I could refer back to or refer others to when this question inevitably comes up again.\u003c/p\u003e\n\u003cp\u003eHashing is a \u003cstrong\u003eone-way\u003c/strong\u003e function that transforms an input value into an output value \u003cstrong\u003edeterministically\u003c/strong\u003e. That means that whenever your hashing function receives a particular input, it will always produce the same output. The output values are often \u003cstrong\u003efixed-length\u003c/strong\u003e, meaning you could take the entire contents of Herman Melville\u0026rsquo;s Moby Dick, pass it through a hashing function, and end up with, say, 256 bits of data. But you could also take a short message, such as \u0026ldquo;Don\u0026rsquo;t forget to like and subscribe\u0026rdquo; and still end up with just 256 bits of data.\u003c/p\u003e\n\u003cp\u003eThese three properties, \u003cstrong\u003eone-way\u003c/strong\u003e, \u003cstrong\u003edeterministic\u003c/strong\u003e, and \u003cstrong\u003efixed-length\u003c/strong\u003e, make hashing a useful tool for security.\u003c/p\u003e\n\u003cp\u003ePerhaps one of the most prevalent places this happens is with passwords. Your password is used to log you into a website. Once upon a time both you and the website would know your password, and when you login the website would check against the password it knows, and if they matched, it would log you in. But this presented a big problem \u0026ndash; when websites would get hacked, your password was stored in \u003cem\u003eplain text\u003c/em\u003e next to your username or email. Now three parties know your password \u0026ndash; you, the website, and the attacker. The attacker can login as you and do whatever you can do.\u003c/p\u003e\n\u003cp\u003eThen along came hashing, and developers were advised to hash passwords so that the passwords couldn\u0026rsquo;t be recovered in the event of a data breach. Just pass the user\u0026rsquo;s password into this magic function and we will always spit out a value that is the same length and that can\u0026rsquo;t be reversed to reveal the password. When the user logs in, hash their password and then compare with the hashed version from the database. This had the added bonus that now you would always know exactly how long your \u003ccode\u003epassword\u003c/code\u003e field should be in the database \u0026ndash; whatever the output size of your hash function is.\u003c/p\u003e\n\u003cdiv class=\"infobox\"\u003e\n    \u003cimg class=\"infobox-img\" src=\"/img/dade-transparent-logo.png\"\u003e\n    \u003cspan\u003eEver had a password rejected because it contained characters like \u003ccode\u003e-\u003c/code\u003e, \u003ccode\u003e*\u003c/code\u003e, \u003ccode\u003e'\u003c/code\u003e, \u003ccode\u003e\u0026quot;\u003c/code\u003e, or \u003ccode\u003e;\u003c/code\u003e? These characters used to be responsible for tons of SQL injections through a combination of not hashing passwords at the application level as well as not using parameterized SQL statements. These days there are still some reasons to reject these characters, but they don\u0026rsquo;t necessarily mean that your passwords are stored in plaintext.\u003c/span\u003e\n\u003c/div\u003e\n\n\u003cp\u003eOne additonal property of good hashes for security purposes is that a small change in the input can result in a dramatic difference in the output. For example, if we were hashing the word \u0026ldquo;password\u0026rdquo; with a really awful hashing function that I\u0026rsquo;m making up on the spot, the output might look like \u0026ldquo;pswr\u0026rdquo;. (Note: This function is one-way and deterministic, but not fixed-length \u0026ndash; but it helps make the point) Using that same awful hashing function, we hash the word \u0026ldquo;passwords\u0026rdquo; and we get \u0026ldquo;pswrs\u0026rdquo;. Notice how similar this value is to the hash of \u0026ldquo;password\u0026rdquo;? Ignoring the fact that my awful hashing algorithm is just \u0026ldquo;remove every other letter\u0026rdquo;, we can see that similar inputs get similar outputs. By looking at just the outputs of the two hashes, and knowing how my hashing algorithm works, you can determine that the input values must be quite similar. Now see what happens when we use those same two inputs for the \u003ccode\u003esha256\u003c/code\u003e algorithm.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#c6d0f5;background-color:#303446;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$ echo \u0026#34;password\u0026#34; | sha256sum\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e6b3a55e0261b0304143f805a24924d0c1c44524821305f31d9277843b8a10f4e  -\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$ echo \u0026#34;passwords\u0026#34; | sha256sum\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ec1f1f36d9be6f64a85698d9db973fd07eb86641c67ef91260120a628084dd632  -\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNotice how the whole string looks completely different? Now, even having both of these hashes, I don\u0026rsquo;t really know anything about how they might relate to one another.\u003c/p\u003e\n\u003cp\u003eThere are all sorts of hashing functions, with varying properties that make them stronger or weaker than other hashing functions. Cryptographers are always analyzing hashing algorithms to find flaws in them that could weaken them. Sometimes it doesn\u0026rsquo;t even take any flaw to consider a hash weak. The rapid growth of computing power means that functions that used to be slow to perform, maybe only dozens or hundreds of executions per second, are now able to be performed millions of times per second. What does this mean for the strength of a hashing function? To answer that, we\u0026rsquo;ll need to look at how hashes are attacked.\u003c/p\u003e\n\u003ch2 id=\"rainbow-tables\"\u003eRainbow Tables\u003c/h2\u003e\n\u003cp\u003eRemember how I said one of the properties of hashing that made it appealing to security was that it was \u003cstrong\u003eone-way\u003c/strong\u003e? Well, that\u0026rsquo;s true, but by itself it doesn\u0026rsquo;t necessarily mean that a hash\u0026rsquo;s input can\u0026rsquo;t be figured out. Continuing to use passwords as our example, think about what else has historically needed to be true for passwords \u0026ndash; the people creating them needed to be able to remember them and type them in. That means that passwords were often words, short phrases, or short strings of random characters. Whatever the chosen password is, a human probably created it, and there are only so many things a human is likely to create.\u003c/p\u003e\n\u003cp\u003eAn attacker can lean into this and combine the deterministic nature of hashing functions in order to generate tons of hashes for known inputs. It used to be especially common to pre-generate all of these possible hashes based on all the inputs you wanted to guess and then store them for later in what is known as a \u003cstrong\u003eRainbow Table\u003c/strong\u003e. Then, when you get a hash, you can just look it up in your table, rather than try to compute all the possible inputs that might produce that output on-demand.\u003c/p\u003e\n\u003cp\u003eRainbow tables are a less common attack vector today for a variety of reasons: improved hashing algorithms made precomputing results more difficult, GPU-based cracking got so fast that they could search a very similar amount of hashes per second as performing a lookup in a rainbow table, as well as a trend towards optimizing hash cracking efforts towards human generated passwords. One early attempt at making rainbow tables non-viable was the introduction of salting.\u003c/p\u003e\n\u003ch2 id=\"salting-a-hash\"\u003eSalting a Hash\u003c/h2\u003e\n\u003cp\u003eSalting a hash refers to generating a unique random string of characters that gets concatenated to the input value, in our case a password, before being fed into the hash function. Now you can have a series of randomly computer generated characters that combine with the user\u0026rsquo;s provided password in order to create a harder to crack password. A good password salt will have a large keyspace and be randomly generated per user. A less optimal password salt might be something like using the user\u0026rsquo;s username or email as the salt. A good password salt is helpful in preventing rainbow table attacks because values aren\u0026rsquo;t likely to be precomputed in the rainbow table for easy retrieval.\u003c/p\u003e\n\u003cdiv class=\"infobox\"\u003e\n    \u003cimg class=\"infobox-img\" src=\"/img/joey.png\"\u003e\n    \u003cspan\u003eBut dade, how do we know the salt that was used to hash a password when it\u0026rsquo;s time to compare the user\u0026rsquo;s password with the value from the database?\u003c/span\u003e\n\u003c/div\u003e\n\n\u003cp\u003eThe salt that is used with a password hash is typically stored in plaintext along side the hash. That way you can lookup the user, concatenate their salt to the provided password, and compare against the stored hash value. The salt by itself is not useful to an attacker, and is usually a completely randomly generated string, so storing it in plaintext along side the hash isn\u0026rsquo;t a big deal.\u003c/p\u003e\n\u003cp\u003eThese salts also help protect against weak or commonly used passwords, since the same input password, e.g. \u0026ldquo;password\u0026rdquo;, will have a different salt for every user, and so every user\u0026rsquo;s hash would have to be cracked separately \u0026ndash; cracking just one instance of a \u0026ldquo;password\u0026rdquo; password won\u0026rsquo;t immediately crack all instances of \u0026ldquo;password\u0026rdquo; passwords.\u003c/p\u003e\n\u003cdiv class=\"infobox\"\u003e\n    \u003cimg class=\"infobox-img\" src=\"/img/joey.png\"\u003e\n    \u003cspan\u003eOkay dade, but I don\u0026rsquo;t want to store my salts in plaintext alongside the password. What else can I do?\u003c/span\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"peppering-a-hash\"\u003ePeppering a Hash\u003c/h3\u003e\n\u003cp\u003ePeppering a hash is a similar concept to salting a hash, in fact NIST calls it \u0026ldquo;secret salting\u0026rdquo;. It\u0026rsquo;s pretty much functionally equivalent to salting, in that its typically a random value that is concatenated to a password before being hashed. But the key difference is that a salt is usually stored alongside the hash, whereas the pepper must be stored separately \u0026ndash; such as in a Hardware Security Module.\u003c/p\u003e\n\u003cp\u003eThere are additional sub-types of peppering, including a shared-secret pepper and a per-user pepper. The shared-secret pepper is used by every user of a particular application, which means the developer only needs to keep this one value secret in the HSM (or other storage). The per-user pepper, however, requires a way to not only store the secret value for each user, but also a way to quickly identify which pepper belongs to which user so that hashing can commence. Both of these options can significantly increase the cost of attacking password hashes, as the pepper must also be guessed. But if a hashing algorithm is fast enough that it can be computed millions of times per second on a single desktop computer, then even relying on a secret pepper value might bring little comfort.\u003c/p\u003e\n\u003cp\u003eI personally haven\u0026rsquo;t seen a whole lot of use of peppering these days, as modern hashing algorithms have taken different approaches to making hashes harder to crack, such as compute-hard functions and memory-hard functions.\u003c/p\u003e\n\u003ch2 id=\"compute-hard-hashing\"\u003eCompute-hard Hashing\u003c/h2\u003e\n\u003cp\u003eCompute-hard hashing is an evolution of the hashing function that includes a sliding computation cost. Perhaps the most popular example of this is Password Based Key Derivation Function 2 (PBKDF2). PBKDF2 introduces a psuedorandom function, most commonly Hash-based Message Authentiation Code (HMAC), that gets applied to the input repeatedly in order to derive the output that is used as the key. A PBKDF2 hash contains within the stored value a number of iterations to use, and the process must be repeated that many times on a given input in order to get the desired output.\u003c/p\u003e\n\u003cp\u003ePBKDF2 also maintains the concept of salting. This means that the inputs into a PBKDF2 hashing function will include:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThe psuedorandom function to use\u003c/li\u003e\n\u003cli\u003eThe user\u0026rsquo;s provided password\u003c/li\u003e\n\u003cli\u003eThe per-user randomly generated salt\u003c/li\u003e\n\u003cli\u003eThe number of iterations to run the function\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eA neat thing about typical PBKDF2 implementations is that the number of iterations can be scaled over time because the number of iterations is typically stored alongside the hash. So even if you\u0026rsquo;re using a fast algorithm, by using a high number of iterations you are able to effectively combat the increased compute speed available to attackers. OWASP recommends 310,000 iterations of PBKDF2-HMAC-SHA256\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e, for instance.\u003c/p\u003e\n\u003cp\u003eAnother example of compute-hard hashing is \u003ca href=\"https://en.wikipedia.org/wiki/Bcrypt\"\u003ebcrypt\u003c/a\u003e, which was first published in 1999. It uses an iteration count mechanism as well in order to make the hashing function slower, thereby increasing the computation power required to crack hashes. Technically speaking, bcrypt is actually cache-hard, it works in a way that requires a bunch of data to be moved in and out of L1 cache, which is what makes it computationally expensive. But for simplicity sake, we can just remember that bcrypt takes longer based on how many iterations it has.\u003c/p\u003e\n\u003cp\u003eBut what if compute-hard hashing isn\u0026rsquo;t enough? I mean, compute power is cheap and compute-hard hashes can be highly parallelized across machines in order to significantly cut down on wall-time to crack hashes (the amount of actual time that passes, rather than cpu-time, which is the amount of time spent by a computer, summed across parallel executions).\u003c/p\u003e\n\u003ch2 id=\"memory-hard-hashing\"\u003eMemory-hard Hashing\u003c/h2\u003e\n\u003cp\u003eMemory-hard hashing is similar to compute-hard hashing in that it\u0026rsquo;s goal is to create hashes that can be configured with a scalable level of resistance to attacks. These hashes introduce a minimum amount of memory required to compute the hash, which helps to reduce how easy it is to parallelize cracking the hashes. Example hashes in this category include Argon2id and scrypt, which both allow configuring tradeoffs between the amount of CPU and RAM usage required to compute a hash.\u003c/p\u003e\n\u003cp\u003eArgon2 is the winner of the 2015 Password Hashing Competition\u003csup id=\"fnref:2\"\u003e\u003ca href=\"#fn:2\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e2\u003c/a\u003e\u003c/sup\u003e, and is recommended by OWASP as the best password hashing algorithm available\u003csup id=\"fnref1:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e.\u003c/p\u003e\n\u003ch2 id=\"identifying-a-hash\"\u003eIdentifying a Hash\u003c/h2\u003e\n\u003cp\u003eOkay so now we\u0026rsquo;ve talked about all of these different properties of hashing, some attack types against hashes, and what qualities make a good hash. What do they look like, though? Some hashes identify themselves in the output value, such as md5crypt using \u003ccode\u003e$1$\u003c/code\u003e as a prefix. Other hashes, though, do not make any attempt to make themselves identifiable, such as the sha256 examples used above.\u003c/p\u003e\n\u003cp\u003eWhenever I have a hash and I want to know what kind of hash it is, I typically head directly to the \u003ca href=\"https://hashcat.net/wiki/doku.php?id=example_hashes\"\u003eHashcat Example Hashes\u003c/a\u003e wiki page. It\u0026rsquo;s a page designed for helping debug hashcat\u003csup id=\"fnref:3\"\u003e\u003ca href=\"#fn:3\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e3\u003c/a\u003e\u003c/sup\u003e commands against known example hashes, but that also makes it perfect for trying to understand a hash that you might be looking at.\u003c/p\u003e\n\u003cp\u003eFor example, let\u0026rsquo;s look at an example Django password hash, which defaults to a PBKDF2 based hash, as discussed in the compute-hard hashing section. Django helpfully provides documentation on how it stores password hashes, the default list of password hashes, and how to use Argon2 in Django, all in it\u0026rsquo;s documentation on a page titled \u003ca href=\"https://docs.djangoproject.com/en/4.1/topics/auth/passwords/\"\u003ePassword management in Django\u003c/a\u003e.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#c6d0f5;background-color:#303446;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u0026lt;algorithm\u0026gt;$\u0026lt;iterations\u0026gt;$\u0026lt;salt\u0026gt;$\u0026lt;hash\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003eThose are the components used for storing a User’s password, separated by the dollar-sign character and consist of: the hashing algorithm, the number of algorithm iterations (work factor), the random salt, and the resulting password hash.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eFor example:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#c6d0f5;background-color:#303446;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epbkdf2_sha256$20000$H0dPx8NeajVu$GiC4k5kqbbR9qWBlsRgDywNqC2vd9kqfk7zdorEnNas=\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis tells us that the \u003ccode\u003epbkdf2_sha256\u003c/code\u003e hasher was used for this hash, with a work factor of \u003ccode\u003e20,000\u003c/code\u003e iterations, a salt of \u003ccode\u003eH0dPx8NeajVu\u003c/code\u003e, and the resulting password hash is \u003ccode\u003eGiC4k5kqbbR9qWBlsRgDywNqC2vd9kqfk7zdorEnNas=\u003c/code\u003e. Note that this example hash, from the Hashcat wiki, uses only 20,000 iterations, which is 290,000 iterations less than OWASP recommends. Django currently, at time of writing, defaults to 480,000 iterations\u003csup id=\"fnref:4\"\u003e\u003ca href=\"#fn:4\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e4\u003c/a\u003e\u003c/sup\u003e \u0026ndash; above and beyond the recommended OWASP value.\u003c/p\u003e\n\u003ch2 id=\"hashing-vs-encrypting\"\u003eHashing vs Encrypting\u003c/h2\u003e\n\u003cdiv class=\"infobox\"\u003e\n    \u003cimg class=\"infobox-img\" src=\"/img/joey.png\"\u003e\n    \u003cspan\u003eAlright, dade. I\u0026rsquo;ll encrypt my passwords going foward.\u003c/span\u003e\n\u003c/div\u003e\n\n\u003cp\u003eHold up there, Joey. It\u0026rsquo;s a common mixup to say that you\u0026rsquo;re going to encrypt passwords when what you really mean is you\u0026rsquo;re going to hash passwords. You see, there\u0026rsquo;s a very important difference between encryption and hashing. Besides the algorithms involved being different, the main thing you should know about the difference between encryption and hashing is that encryption is \u003cstrong\u003etwo-way\u003c/strong\u003e. Remember from earlier, we defined hashing as being a \u003cstrong\u003eone-way\u003c/strong\u003e function. If you encrypt something, the expectation is that there exists a method to undo the encryption, a.k.a decrypt it, and get the original value back, typically through the possession of some sort of key. But if you hash something, there isn\u0026rsquo;t a method to de-hash the value \u0026ndash; you must know (or be able to guess) the original value, hash that, and then compare in order to determine if you got the right value.\u003c/p\u003e\n\u003cp\u003eThis \u003cstrong\u003eone-way\u003c/strong\u003e property of hashing is what makes hashing passwords much safer than encrypting them, and it\u0026rsquo;s important to not confuse the two concepts.\u003c/p\u003e\n\u003ch2 id=\"wrapping-up\"\u003eWrapping Up\u003c/h2\u003e\n\u003cp\u003eI hope this has been a helpful exploration on the properties of hashing, types of attacks against hashes, and which hashes you should use. A quick recap:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHashing is a \u003cstrong\u003eone-way\u003c/strong\u003e, \u003cstrong\u003edeterministic\u003c/strong\u003e function that produces a \u003cstrong\u003efixed-length\u003c/strong\u003e output.\u003c/li\u003e\n\u003cli\u003eSalting and/or peppering can help prevent rainbow table attacks.\u003c/li\u003e\n\u003cli\u003eUsing compute-hard or memory-hard hashing is the best way to protect the secrets hidden behind the hash.\u003c/li\u003e\n\u003cli\u003eUse Argon2 if available, otherwise scrypt, bcrypt, or PBKDF2 are viable options.\u003c/li\u003e\n\u003cli\u003eEncryption is two-way, which makes it distinctly different than hashing\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAdditional thanks goes to \u003ca href=\"https://twitter.com/jmgosney\"\u003eJeremi Gosney\u003c/a\u003e for reviewing this post and providing additional insights into bcrypt, the fall of rainbow tables, and pointing out that banned characters from passwords doesn\u0026rsquo;t necessarily mean that passwords are being stored in plaintext. This post was written for a fairly beginner friendly understanding of hashing, but if you want to really understand it, Jeremi is \u003cem\u003ethe\u003c/em\u003e guy to talk to.\u003c/p\u003e\n\u003cp\u003eIf you have more questions or would like to see this article expanded to cover things I missed, please reach out to me \u003ca href=\"https://crime.st/@dade\"\u003e@dade@crime.st\u003c/a\u003e or \u003ca href=\"https://twitter.com/0xdade\"\u003e@0xdade\u003c/a\u003e.\u003c/p\u003e\n\u003cdiv class=\"footnotes\" role=\"doc-endnotes\"\u003e\n\u003chr\u003e\n\u003col\u003e\n\u003cli id=\"fn:1\"\u003e\n\u003cp\u003e\u003ca href=\"https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html\"\u003ehttps://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html\u003c/a\u003e\u0026#160;\u003ca href=\"#fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u0026#160;\u003ca href=\"#fnref1:1\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn:2\"\u003e\n\u003cp\u003e\u003ca href=\"https://www.password-hashing.net/\"\u003ehttps://www.password-hashing.net/\u003c/a\u003e\u0026#160;\u003ca href=\"#fnref:2\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn:3\"\u003e\n\u003cp\u003eHashcat is a popular open source hash cracking utility.\u0026#160;\u003ca href=\"#fnref:3\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn:4\"\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/django/django/blob/main/django/contrib/auth/hashers.py#L299\"\u003ehttps://github.com/django/django/blob/main/django/contrib/auth/hashers.py#L299\u003c/a\u003e\u0026#160;\u003ca href=\"#fnref:4\" class=\"footnote-backref\" role=\"doc-backlink\"\u003e\u0026#x21a9;\u0026#xfe0e;\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/div\u003e\n"
        }
        ]
}
