 I'm Drew, I am the Senior Director of Technology for a firm called Solarity. We are a consulting firm that do, my group focuses primarily on technology. I'm a technology strategist, architect, we have teams that build things. And I, for whatever reason, I happen to have gotten heavily into the media world. So a lot of media publishers, you know, PBS, National Geographic, Gannett, Tegna, Time Warner Cable, you know, a whole bunch of publishers, you know, for better or for worse. Since we have an intimate crowd, I'd be happy to sort of poll the audience to see where your interests most lie, because I have a lot of stuff here. We can sort of focus more or less on various pieces of it. So first, who kind of knows Django inside and out in this room? Okay, that's perfect. That's really good. So we'll learn a lot about Django in this particular case. Who has attempted to crack passwords before? All right, good. We'll have some fun there, too. So let's just talk, first talk, why are we here today? A few things I want to touch on. It wouldn't be here if it weren't going to touch on some Postgres. We're definitely going to touch on Django and Python. We're going to talk about one of my clients, National Geographic, and a specific set of projects I was working with. We're going to do some black hattery, and that should be fun. And to set the stage, give a little bit of a background for what we'll do for most of this talk, I want to give you some background on this particular project. So a few years ago, I was hired to basically redevelop a site for National Geographic. It was a reinvention, a reimagination of a previous site called My Shot. You know, thinking forward, it's now called Your Shot. And really, this was a photo community for National Geographic. That's a very photo-centric brand. They have lots of professional photographers out in the world taking really cool photos. Your Shot is the community of amateur photographers who voluntarily, you know, upload their photos to this community. They have photos get shared, it's, you know, they get commented on. What distinguishes it from any other photo community is the engagement for the National Geographic editors and photographers. They are, you know, there's, well, when we started this project, there were something like 350,000 users of the old site, and not much engagement from the actual natural photography community, the professional community. This new site was intended to really kind of get highly engagement, which brought tons of new users to this site. So, there's a few other things about this site. It's a beautiful site, mostly because the photography is beautiful. There's, you know, everything else stands out of the way. The thing I'm most interested here is the login portion, right? This tiny piece of functionality in the scheme of things, but really ended up being one of the more interesting aspects. And usually I don't personally get hands on it when I'm typing code in, you know, on this project. This is one where I just had to because it was too much fun to do. So users must log in, right? But we weren't actually building this site with the login, like I would normally would, we're dependent on a single sign-on platform that Nat Geo was building at the exact same time that I was building your shot. So, you know, we have these two moving trains, hopefully we're going to meet at the end. That was one of the really sort of difficult challenges in this project. But in the end, your shot was going to be the very first integration on this single sign-on platform called membership. And here's some of the architecture of your shot, you know, at the time it was using Python 2 and Django. I don't actually remember what version. You know, obviously Postgres has its core data store. Lots of elastic search. We have some really interesting things with elastic search in this project. You know, we're doing asynchronous queuing with RabbitMQ, caching with mCache. Akamai was their preferred CDN at the time. And that's sort of the Python side where, you know, PsychoPG2 for, you know, the Python library for connecting to Postgres. Pillow is, so there used to be a library called, as there still is, Python Image Library, which is PIL, which does a lot of really cool things for manipulating images. I think like image magic and stuff like that. But it was a nightmare to install. So Alex Clark, who I know, actually repackaged it and called it PILO. It's sort of a softer version of PIL. It was the exact same thing, it actually is PIL at its core. But it doesn't have all the headaches that PIL does. Celery is, you know, Python library for connecting, really Django, to connecting to RabbitMQ, et cetera. Haystack for elastic search integration. And lots of social sharing happening on this site. Giga was the platform we used for that. And there's all sorts of mapping going on. That was a map box for that. And just one quick piece about the architecture beyond that and how it all ties together. You see on the far right side of your screen a box called membership, that's really the single sign-on platform. And all these green boxes are getting built at the very same time we're building the orange and yellow and white and red boxes. So our main challenge here was we had this legacy site called MyShot. It had 356,000 something users in it. I had no idea what your MyShot was built in at the time. We hadn't really had much access into this database. Our challenge was to migrate 356,000 user logins into a new platform. I didn't control this membership thing completely invisibly. And that was really a challenge. I felt like it would be fun to do, and it really was. So just to sort of set a little more context, this site was built originally about three, four years ago. I've done multiple iterations since then, and it was a dedicated mobile experience was built. This talk is generally based on the experiences I've been through during this process. So where do we start? So I dig in my hands on the actual original database, which we have an example of here. This is the user's table. This is an example of the user's table. Well, when it was a Postgres database, that was interesting and welcome to learn. The passwords are encrypted and it follows this format, which at the time I was not terribly familiar with. I also found this thing, a Perl script, which seemed to be inserting user records into the database using crypt. Again, for me at the time, that was somewhat unfamiliar because we're used to using frameworks like Django, which kind of has its own way of handling passwords and things like that. So that's the starting point. Now, how do we get to getting these users migrated over? Well, the first approach that was recommended by our client, or actually wasn't the client, someone maybe a client's boss, why don't you reach out to all your users? And by that, we really mean let's email them all and tell them we're moving to a new site and they should go here and create a new password in the system, and that'll be great. And our product owner is basically just a simple answer, no, that's not happening. We're going to solve this completely seamlessly, no problem at all. The next thing that was fun, which we knew the effort was doomed from the get go, but we thought we'd try anyway and see where we get. So let's try and crack all these passwords. The site was built, if I did my site four years ago, or the original one was built maybe 15 years ago, surely the passwords are not terribly secure at that point, and by the way, there really was no password policy on the original site. So it's not to say they were all simple passwords. There might be some simple passwords, but there also might be some terribly complicated passwords. We simply didn't know at the time. So let's take a look at what cracking passwords looks like. I thought that would be super fun to try. Well, how do you do it? So we've got some people familiar with this. We took a brute force approach. The way you normally do that is you identify with algorithm. You encrypt every possible password with that same algorithm, and then you simply compare matches. So let's actually take a look at one of these examples. How about this? What does that look like to everybody? Anybody? Very simple, right? That's a raw MD5 one-way hash. And just to keep it simple through these examples, I basically limited, OK, this is a four-character password that's hashed with a alphanumeric car set. So no other characters beyond letters numbers. And with that set, we'll have something like 15 million possible passwords. It's still somewhat a lot, but not a lot in the scheme of things. Have we gone longer passwords and extended character set? So how might you go about cracking one of these things using brute force? So let's take a look at a simple Python script that does exactly that. So I asked about Django. What about Python in this room? All right, cool. It should be easy to follow. The only thing that confused people are the white space. So if you can get over that, it's a very simple to follow language. And you come to love it eventually. So real quick, we're going to walk through this file. We have the car set, which is basically all the ASCII letters plus all the digits. We have a really cool map function here that generates the entire list of possible four-character passwords in that character set, which is all 14, almost 15 million of them. And then we're going to go loop through. Oh, by the way, the hash we're passing into it is through the sysrv1. It's basically we're using a command line. We're passing a hash to this Python script. And it's going to go loop through hashlibMD5, the whole thing, and then see if it matches what we've actually passed in. If it does, great, we've cracked this password. So that script taking into account that example, MD5 hash we presented earlier, knowing that it's only a four-character letter or number of password, gets cracked by this in five seconds. Password was Thor, cracked in five and a half seconds. This is a single-threaded process on my MacBook Pro. It's a very short password. It's got a limited character set. Things get much more complicated very quickly. But for these examples, we'll just take a look at that. And we'll use that example throughout the next couple of minutes. So I wrote a Python script to do this just because I thought it was fun to do that. There are plenty of tools out there for doing this without writing it yourself. A super fun one is John the Ripper. So John the Ripper actually does the exact same thing. If you see my script here, I basically saved that same hash to a text file and ran John on it, hinting at it what type of encryption it was. And it took like three seconds to guess that thing. And you can see towards the bottom how many guesses per second it's making and something like, I don't know, like 500,000, something like that. That was faster than my MacBook Pro Python script. And had we optimized that process even further, it could have gone so much faster. So in my particular case, on this MacBook Pro, which is a few years old now, so something like 15 million possible keys. Now it found it in five seconds. But that doesn't mean it examined every 50 million keys, which it found the one it needed and stopped. To hit all 15 million, it would take about 18 seconds or so, which is about some tiny fraction of a second per attempt for a four character password. If we take that same example and say, let's just consider eight to 12 character passwords in the same character set, we very, very quickly get out of control. That's like three sextillion something. I can't even tell you what the rest of the numbers mean, but it's a lot, right? And that's exactly how I feel. Again, eight to 12 is still not even close to the password space we're actually considering for reality. So at this point, I start to sweat. I really am saying, well, this is no way we're going to be able to do this. Well, what are things you could do to crack that? Well, using the same technique I've started with, well, let's just say, OK, we've got fractions of a second per attempt, three sextillion attempts, or possible possibilities. That's about how many summing seconds equals about 126 million years to go through all possibilities. That's way more than it really takes. Because on average, we're not going to have to go through the whole entire key space every single time. We're going to probably, on average, hit about half that. So let's just say, 63 million years on average. Or 12 per six. Not really. Not really. So how do you speed that up? How might you speed it up? Well, you can leverage not just your CPU, but you also your GPU. That'd be kind of cool to take advantage of that. We could use parallel processing. We could use the cloud. And by the way, I did a calculation. If I was going to run, if I was going to try and run 126 million years of CPU on EC2, if I did it on the C4 larges, which are the lowest level compute optimized. And by the way, I didn't actually do a test. So it may or may not be faster than my MacBook Pro. I don't know. I'm going to assume it's about the same. But if I were going to run 1 million, billion, something like 1.1 trillion C4 larges for one hour, I could crack every possible combination of those passwords. It would cost me $101 billion. Or it's really an interesting thing. A better way to do it is to start cheating. Pezzas are not as random as we think they are. People are usually setting them based on some words or numbers or birth date. So by taking that approach, and John has a nice way of taking word lists and making manguling them so they become password-like, you really kind of short-circuit a lot of that process. But still, that takes a long-ass time. Another thing we can do with MD5 hashes is simply just Google it. Ah, look at that. It took me to Google 0.51 seconds. It took me longer than that to type this thing in here. So just by why is that showing up? Why did that make it so easy? The first hit is md5decoder.org. What does that look like? It tells me, flat out, Thor is half with MD5 is that string I've been interested in. Someone did that for me already. And the reason that, as you can see, it does it for all sorts of other algorithms as well. And that list goes down, way down. This exists mostly because of rainbow tables, right? People pre-compute all the MD5 hashes of just about every possible thing you could think of. And they just put it out there. So you're basically reversing the cryptographic hash functions and basically mapping them. So hash to, say, for a clear text. Rainbow cracks are a pretty cool resource where you can buy all sorts of crazy rainbow tables. And if I were to generate one, you would find something like that. So I have a, by the way, I have a GitHub repository. I just put some sample scripts in. There'll be a link to that here. It has Python scripts for generating rainbow tables, cracking MD5 hashes, encrypting MD5 hashes, et cetera. So this is the output of one of those scripts. And somewhere in the middle, you see my Thor with my hash right there. It's in there. So now it's a matter of not just trying every possible combination, you see me look it up. Well, that's unfortunate. But obviously, there's plenty of ways to defend against rainbow tables. One is use a salt. So by using a salt and a unique salt for every possible thing you're salting, you no longer have one answer to what's the hash of Thor. There's unlimited possible hashes of Thor. So that's one key stretching, another approach that actually makes you do multiple iterations of the whatever hash algorithm you're using. So if it took 125 million years, as we said before, for that original key space, if we have to iterate over that eight, just eight times, so now we're at a billion years very quickly. So that's very important to consider. And by the way, Django, today in Django 111 automatically does 36,000 iterations of its default algorithm plus a salt. And using strong or really long passwords is really important too, because at some point, even rainbow tables become unwieldy. You can't have a rainbow table 25-character password just like gigantic, and the world starts exploding at that point. So that's why it's really important, long, salted, stretched passwords. So that's where we are. That's how we're cracking passwords. That's the general approach to these things. They don't have any better approaches. I'd love to hear it because, nope, all right. All right, cool. So what about our passwords now? So now we're back to where I started from. We have these table full of users and passwords. And at the time, I wasn't sure what those passwords meant. So how do we crack these passwords? Or can we crack these passwords? Well, like this format. Little research and sure enough, oh, that's the Crip function. That's the PG Crypto extension in Postgres, where one represents the algorithm, which in this case was MD5 Cript. The second bit is the salt, and the third part is the actual hashed password. And altogether, we get the exact thing we saw in the database, this $9.1, et cetera. So salted MD5 Cript hashed generated by the PG Crypt extension in Postgres, as we saw in the insert statement early on in that Perl script. All right, let's go Google it. What? Not found, right? When you salt something, it becomes very hard to have a lookup table for every possible salt for every possible combinational letters and characters. All right, that's not going to work. What about just using our same brute force approaches? Well, I want to take that same output, put it in a file, and run it up through John. And well, I actually did find it. It cracked it in something like, I don't know, minute and 37 seconds. Well, terrible. That's only one. That's one password, and a very simple password at that. So it is brute forceable, but still much slower than it had been before. And by the way, it has to do a complete lookup for every single password, because they've got unique salts. So at that point, we start giving up on breaking the passwords, even though however much fun it was. So let's talk more about some Django-based approaches. Well, the first thing is, when we're talking about authentication, let's consider a Django off-backend. So for those who are not terribly familiar with Django, Django is a Python-based application development framework that does all sorts of different things. It uses a lot for API backends for probably most popularly web-based applications. Database-driven web-based applications, one of the most powerful features is its object relational mapper, ORM, which works really nicely with Postgres. And I will admit to starting my Django career tied to my SQL, got deep into that world, and people are telling me Postgres is better. And my initial pushback was, replication is so much harder. Well, it's not as hard anymore, and it's a really great platform. So almost everything now I do is Postgres with Django. So Django has all sorts of pluggable and replaceable features. It's a great framework, and it has out of the box lots of tools to do things so simply. But any of those assumptions that Django makes can be completely replaced or extended very easily. So in this case, by default, Django has one built-in off back end. And it's going to assume that you are using a relational database that uses its own managed table of users called off user. And that's going to map to its internal, like what it assumes a user is. It's got a sort of a Django model or definition of what a user is. And that out of the box has lots of really cool capabilities and functionality. So most Django applications tend to try and use that. So the default Django back end, you can see at the top here, it says Django can trip off back end's model back end. That's the only one you see by default. If you don't do anything, that's what's there. And that just looks up in its own database when you enter a username and password. It calls the authenticate method, passing a username and password. And then it will look up in that particular table, make sure you've matched the username and password, and return you yay or nay. That's basically how it works. So my approach here was to try and create a custom off back end that replaces the built-in one, or actually not replaces, you can run them both. So I have both listed here. It tries the model back end first. And if that does not work, it tries the next one down the list, and so on. You can have really as many back ends as you want. It only gets run when you're actually logging somebody in. It's not a occurrence that happens like every request, except for a new person comes to you to log in. And to write a custom back end really requires just two methods, get user by ID and authenticate, passing in some credentials. And the credentials could be just about anything. You think about using password. People log people in, they're all sorts of wacky things, cookies and other API calls and things like that, tokens. So in this particular case, we're going to try it, we're going to try it, we're actually going to do it, to create a Django off back end that uses this legacy database, right? So let's assume, there's all sorts of ways we could consider doing this. Let's assume I've taken that user's table from the old legacy platform and just simply put the same table in my database application. You call that legacy users. I have username and password and nothing more. If my user login succeeds by matching that username and password, we're going to let the user in. So the first thing we're going to do is we have to implement get user. So it's going to be simple. Now, we don't have to do this, but I'm going to, in the authenticate method, if my username and password matches and succeeds, I'm going to create an actual Django user record in my database, because I want to use that user model so much, it's just so handy, we have to have a record to at least do that. But we're not going to use it for logging in with this model, with this back end. So when we're getting the user, we're just going to do the normal lookup, using the user model and get by ID, okay? Or fail if user does not exist. So that's the easy one. The second one, now we've implemented the authenticate method. Well, it's fairly straightforward, it's kind of like we're taking that, what that Perl script would have been doing if it was matching people and just doing the same thing in our database. So it expects the username and password. We're going to validate against this legacy database using the pgcryptocrypt method. And really the query we're kind of running, Django is going to run, it looks something like this here, select one from legacy users where username is what I passed in as username and password equals crypt password with the database password. And you guys might be more familiar with this, but I'm just using whatever the database has as its password into the salt, which is really the first part of that password anyway. So the crypt is going to extract it out there and insult it with whatever the original salt was. So what that looks like here in Django is, let's see. We've got basically authenticate method, I'm making my database connection. We're running that particular command, right? I've passed in username and password into the method, we're going to execute and try to affect one record back. If I have a record, when I'm done with that process, I'm going to actually try and get the user if it exists in my database, or I'm going to create a new user. So you see towards the bottom, user equals username equals username. I'm creating a set unusable password. This is a way that I can never use this particular table to actually log anybody in. If I didn't specify that, it would leave a blank password, which actually is a totally valid password, not at all a safe one. But Django has a notion of unusable passwords. There's certain characters you can define that make a password unusable, and this is what it actually generates. So save the user, return the user, we're done. And what that looks like, I see, look at my tables here. So I have legacy users and I have off user. If I select start from legacy users, I see my two favorite musicians right there and their passwords. This is the legacy table, right? We're going to see I start, this is my Django admin interface, the users list. I have one user, that's me, and no other users. So after I implement my off back end, which really is a matter of the code, exactly the code you saw, and I activate it in my Django settings file, which you saw in the first thing, so those two model back ends. I can now try to log in as KingBuzzo with my favorite four character password, and it'll actually let me in. Sure enough, I see my Django admin, by the way, this is Django admin interface. Something you kind of get for free with Django. Any, you know, just ability to query your database and all the fields through a web-based interface. So we see KingBuzzo has the username and I have no password set. That's actually the same thing I would say if it had just a blank password, or never set the password. In this case, it is unusable, and I show at the bottom here what the password actually looks like. It's got this like, you know, I think a pound sign at the beginning, which is the defined, unusable password starter, and then I think there's something like 40 characters after that. That's your unusable password. And that actually worked. That actually worked kind of fine. But it feels incomplete, right? We still have this legacy table sitting around and you know, we're always going to keep authenticating against that. We certainly could have when I created, you know, when I logged in through that legacy database that somebody could have created that same user and give them the same password they had in our new system, and then let them log in through the internal system. That certainly is a possibility we could have done. But Django has actually a nicer way of doing that exact thing. So let's take a look at password hashers. So first, what's a Django password hasher, right? It basically determines what algorithms Django understands when you enter in a password to a login field through the authenticate method. It's all sort of different ways, and they've done a really nice job making it very flexible and upgradable, because you might have built an application, you know, five years ago and suddenly, wait, 3,000 iterations is not long enough. You do 30,000 iterations of this password hash. Very simple to actually just adjust that, and then suddenly just, you know, or it's a completely different algorithm that's so much more efficient and stronger. Let's do that, or I should say less efficient is better in this case. So by default, Django uses the PBKDF2 algorithm with the SHA-256 hash. Password stretching, 36,000 iterations, that's in Django 111, I think it was 30,000 in Django 110, and a random salt. So that's pretty good, that's what we were talking about here. So, oh, I should have shown that slide. So you see, this is sort of my record, you can sort of see the algorithm, iterations, salt, and hash, you know, sort of messed on my admin user page. And if we interrogate what the database actually stores, you know, have the algorithm, dollar sign, number of iterations, dollar sign, salt, dollar sign, hash. That actually comes out of the Django documentation. That's not actually true because the algorithm determines what comes after it. So in this case, that's the default algorithm has a iterations and a salt and a hash. But really, I can define whatever I want after the algorithm part. So if I look at the password of a user, you can see that's what it says there as an actual example. And the default Django hashers looks like this. So, you know, the number, the interesting thing about this is it'll understand any one of these hashers if I had a password encrypted with any one of those. But only the first one is like the primary one. So if I happen to have a password encrypted with, you know, becrypt password hasher, which is the bottom of the list, it'll run through all these, except my input logged me in, but then it'll re-encrypt my password and store it using the primary algorithm. Which is a really nice way to keep your passwords fresh and up to date and secure. So how do we create a custom one? Let's talk about that. Well, this is a subclass based password hasher. That's a nice way to start. When you create your own hasher, you must override the algorithm, you know, attribute and three methods that are required, verifying code in safe summary. And other methods too, they also recommend using a hard and run time. I'll talk about that real quick, but algorithm is simply just the name of my algorithm, right? I'm creating a new one I can use whatever the heck I want as long as I'm not already used by an existing one. Verify is, you know, that's where you're taking an input and verifying it against some other bit of input or really what's in the database in that case. And code taking your input and hashing it. Safe summary simply creates that blob right there I'm pointing to, right? That's like, you know, it's basically a dictionary of the algorithm plus the iterations, that whole thing is there. That's where the safe summary comes from. So for my custom algorithm I need to have, what does that supposed to look like? So let's take a look. Let's first start with algorithm, salt and encode. By the way, salt is where we generate a salt for this particular algorithm. Must upgrade determines whether or not the algorithm we're using needs to be upgraded. So in this case I would say it should be true all the time. Right? This is a catch all situation for legacy users where we should always want to get it upgraded. It doesn't actually do anything. It's just sort of a, it's indicated to anyone who's introspecting the database and that user. And hard and run time, if a user was, had a password encrypted with, you know, let's say 50,000 iterations and we really wanted to be 100,000 or we wanted to be as slow as if there was 100,000, you know, iterations, we can actually just run fake iterations in a hard and run time method. We'll just slow the whole process down, right? Making any attacks much more painful for the attacker. So a custom password hasher. We're creating something called PGCrypto, a new algorithm. Salt, we're simply using gen salt from Postgres, okay? And returning that value. In code, well we're passing in a password and a salt if we don't have the salt, we're pulling out a new salt and we're gonna query basically, select, correct, password, salt. And returning that value and we're prefixing it with the algorithm at dollar sign. So this at the bottom is that sort of, you know, how we create a string or mapping those values on the right to the ones on the left. And we end up with something that looks like PGCrypto dollar sign and then the rest of the thing when that came out of the database. The rest of this hasher method looks like this. Verify, we're taking that password and encoded, which is really the encoded password and trying to match them. So we take the one that's passed in, right? We're splitting on the dollar sign to get the actual algorithm off. We're encoding the thing, the password. The PG hash is the salt, right? That's the original password. So we're taking the same salt with the new password we're testing and trying to encode that. And then we're comparing the thing that got passed in hash plus what's in the database. And we're using this really super fancy constant time compare method, which is nothing more than a string comparison, basically. And safe summary, that's basically what I described before. In this case, we have algorithm and hash. I don't care about iterations. I don't care about anything else here. And keeping it very simple. And again, because this is not intended to be a terribly, first of all, Postgres, PGCrypto does some slowing down of its own. And it's not intended to be a permanent sort of login solution. So we're just kind of passing the hardened runtime here simply to suppress the warning we would get if we didn't. So let's take a look at what this actually does. All right, let's actually demo. So I'm gonna go create a new user in the database, right? I wanna have an actual record. And I'm gonna update the, I would have a migration process to do this, but in this case, we're just updating that user's record and setting the password to the PGCrypto hash prefix with PGCrypto dollar sign. So we see the PGCrypto dollar sign, dollar sign, one dollar sign, salt dollar sign, hash. Which is exactly what we want. Where my username is King Diamond. And you see at the time, it shows me my safe summary, algorithm PGCrypto and the hash, beginnings of the hash. And we'll log in. King Diamond and my favorite four letter password. And now, the same users got logged in, first of all, and their password got upgraded to the primary algorithm. And that's actually really cool. Now they're just a regular user in the platform, just like everybody else. Now the interesting part about this is how flexible this can be. We built this very specifically for one project. And it worked very well. I was able to, but I didn't own that code base, that SSO platform was being built by someone else. But I wanted to do this. So basically, rather than saying here's some code, like just figure out how it works. Wrote this up as an open source library and packages up, now it's in Python, you know, PyPy repository. So here, use this library. And I pretend that I didn't really create the thing that something that everyone else uses and they should use it too. And they did. And it actually worked completely seamlessly. We got them all migrated. Well, we migrated them with the PGCrypto sort of hashes and anyone logged in had no, you know, all this was a nice looking site and more functionality. So, we're gonna lose some time for questions. So at this point, I just wanted to say thank you. I, like five minutes before I walked in here, I uploaded to this GitHub repository bunch of sample files, some rainbow tables, you know, a sample Django application that uses both the auth backend and the, or the password hasher. You know, a bunch of interesting Python scripts to hack stuff and code stuff. So, well, I'm gonna check that out. And we have any questions? It's a good question, because I actually didn't need to do that. I really don't ever need to call that salt method in this particular case, because we're only ever really comparing it to what's in the database, right? So the salt for verification should always be what's in the database, right? That's what we're gonna use is to salt everything. Now to make this, since they open source it, so people can use it, and they can use it as their primary thing if they wanted to. And we need to support the whole interface, including general, gen salt. So if we were creating a new password, we would use gen salt, and then use that as the salt for it. Yes, exactly. So we'll see, let me see if I can, oh, you can't see this. All right, so for verify, right? We're basically using, we're taking a password, right? And it's also like matching what's in the database, password fields, okay? So for what's in the database we called encoded, we're chopping off the PG hash, which is the secondary piece of it minus our algorithm. That's what's in the Postgres database to start. And then we're encoding our new password that someone's attempting to enter using the hash in the database, okay? So that's the salt always, okay? We only need to worry about generating a new salt ever if we're creating a new user. Yeah, it doesn't actually say here how I got the salt in the first place, but for verify, it's using an existing record in the database to a password attempt, okay? Encode is when we're creating, so verify uses encode, but if I was creating a new record, it would use encode for the password. And in that case, there's no salt being passed to it. There's nothing to start with. It's gonna call self-salt in that particular case. And then that pulls it from BGCrypto's GenSalt method. In the open source version, so this is MD5, that was what's in our legacy application. It's also a different algorithm we could pass in here too. And I believe, I pulled this thing up. Here's the GitHub repo with these different interesting files here. Cracking, hashing files, creating a rainbow table, salting and stretching passwords. Some example hashes, so these are the ones I was using in that example, right? Simple hash. And, but going to this guy, which is the open source library. So that actual, that algorithm, GenSalt is defined by the configuration. So that can mean that any thing that's installed on the server can be used. And in fact, you don't actually have to have Postgres as your primary database. I shouldn't say that in this room, but all it needs is access to some Postgres database with BGCrypto installed on it, right? Even if it has no data in it. Because all it's ever running is GenSalt and Cript, right? So in this case, we have the ability to define a alternative database. Because this could be used on someone. Someone has to use a non-Postgres database. As long as they can run GenSalt and Cript, it'll work just fine. On something, on another one. Anyone else? Well thank you all for your time. Appreciate you coming. Thank you.