Have My Salt (And My Iterations Too!)

(Or why storing salt (and iterations) alongside hashed passwords is just fine)

Crypto is tricky, so getting your head around all the various bits of key material involved can be challenging. There are quite a number of good resources on this subject. I’ll be referencing various specifications and best practices here - but perhaps someone out there will benefit from my style of explanation.

Specifically, let’s talk about encrypting a Private Key file that we’ll use in some sort of Public Key Infrastructure (PKI). Of which, we’ll need the following components:

Passphrase

You’re going to need a passphrase: NIST likes to call this a “Memorized Secret“. You might know it as “password”. Whatever you want to call it, this is “Something You Know”. This is the thing that you’re encouraged to create long versions of, perhaps generate randomly and put in a vault such as Bitwarden or LastPass (well, maybe don’t use that one anymore…), so on.

A passphrase must be strong for you to have strong encryption…

I’ll say it again: A passphrase must be strong.

Good techniques for ensuring strong passphrases are actually quite limited though you’ll find many anti-patterns all around you:

  • Length: You’ll see recommendations for 8+ characters, perhaps 12 or 16… The longer isn’t necessarily the better depending on your hashing algorithm, but I’m not going to get into the details on that here. It’s fine to think “the longer the better” as a general rule of thumb.
  • Checked against top well known passphrase/password lists. Basically you’re going to block out things like “password” (that yes, people really use this all the fucking time in 2023), “Bryan1978” (…oof), etc. Resources are readily available on the web.
  • Annnnd… That’s really mostly it. “Must include X” is an anti-pattern. “No longer than Y”. Anti-pattern. Truncating input? Anti-pattern. You’ve seen these everywhere, I know. We can cry together over beers.

💡 You could generate a strong passphrase randomly and stuff it in a vault for extra layers of security. Remember to use a strong passphrase for your vault, too! In Linux you could do something like this: strings /dev/urandom | grep -o '[[:print:]]' | head -n 32 | tr -d '\n'; echo

Salt 🧂

A salt is a random value used in conjunction with the input passphrase when hashing. You might see this as “nonce” in various applications. Their purpose? Prevent against repeat (or “replay”, …) attacks (Get the name with ‘nonce’?) What that means is if I have for example pre-generated a big list of passphrase:hash values, and didn’t use the particular salt for a particular passphrase that you did, my list is of no use! It is absolutely best practice to generate a new salt for a given passphrase. And, when the salt is sufficient in it’s length and randomly generated, the chance that I have hashed passphrases with the same salt as you are, well up to the length (again!) and I suppose the RNG gods.

Hot damn, we’re on the topic a key reason for this post… salt storage! This is where it get’s a little more confusing. The salt is not considered a secret. Do you want to paste on the Internet? Probably no, but actually you could with little fear. But don’t take my word for it.

As discussed above, the salt is to prevent repeat attacks. But the salt is also required to decrypt encrypted material or come up with a matching hash given a passphrase. It is true that given the salt, an attacker can attempt to brute force your passphrase. However, the power is not in the salt. It’s the length of your passphrase covered above. Is life a little easier for an attacker if they know my salt? Actually yeah a little. The problem is however, it’s needed to decrypt. So anywhere validation/decryption occurs the salt needs to be known “in plain text” (or in raw decimal, or whatever you use). In other words, if the attacker can get their paws on your hashes, they can get their paws on your salts. It’s fine though. Don’t care about that. What we don’t want them to have is your strong passphrase.

Often, we don’t even have a choice! Let’s look at a concrete example – Encrypting a Private Key file we plan on using for certificate signing. If you’re not up on it, it’s OK. Quick explanation: A Private Key is in a way, a super duper long version of a passphrase. It’s what actually is used to encrypt data. What if a bad actor gets their hands on a Private Key? Ouch! They are now able to represent themselves (when certificate signing) as you! So let’s protect it a bit by encrypting the Private Key! Now if CptBaddiePants gets their hands on your MySigningPrivateKey.pem, they’re going to need “Something You Know” to do decrypt and use it. Hey, it’s that passphrase thing!

But guess what else you need to know to decrypt your private key? The salt that was used with it (and, depending on the algorithm, the iterations… I’ll touch on them a bit later). Now, if we expect you to have to keep track of the salt, what does that smell like? If you’re following along, it will smell an awful lot like just another password. That’s certainly not very helpful. See where this is going? We’re going to need to put it with the encrypted private key file so it can be of any use. But remember: This is OK!.

Cool, so a quick run through using OpenSSL for you to actually do the needful. In this case, we’ll create a ECDSA P-256 (in which you may see referred to as names like “secp256r1” or “prime256v1”) Private Key, then encrypt it with AES-256-CBC.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Generate the Private Key
openssl ecparam -name prime256v1 -genkey -noout -out PrivateKey.pem

# Generate Public Key to go with this Private Key
# (this is an added detail for illustration:
# The Public Key is just that: Public. Everyone can see it. Who cares.
openssl ec -in PrivateKey.pem -pubout -out PublicKey.pem

# Encrypt the Private key using a passphrase (you will be prompted for such)
openssl ec -in PrivateKey.pem -out PrivateKeyEnc.pem -aes256

# Let's look at the thing!
cat PrivateKeyEnc.pem
-----BEGIN EC PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,A5DB8977466F0618E69828745C4A4B43

P8heUlMY9/WG2M+lpJ+ipl0YcAfinYIfKIxzpUdWN9YdKWxe71Vgk+rjAsGF8a3q
7mRRh8X7tXWP9zS/JUPZV/zYgwdOY1uKZv6IZMvp3Mmg7OjKFlMUjNfveef3lb35
E2EgiS/Dzy8hfj7RjNEODksCOyuuX86wl0KTPhz6y+I=
-----END EC PRIVATE KEY-----

# Go ahead and crack this thing. I'm not going to use it for anything.
# Hell, the passphrase can be found in https://haveibeenpwned.com/!

See the line there with the encryption info? Guess what?

DEK-Info: AES-256-CBC,A5DB8977466F0618E69828745C4A4B43

Whao now bro, you’re storing my salt… But I’m not mad!

If I want to decrypt and use the PrivateKey.pem file above, I need what matters: My passphrase. Hopefully I used a good one!

💡OpenSSL will not require a strong passphrase! That’s left up to the user. Better not make it “password” though, because that’s in my list ☺

Let’s look at another example, a Linux /etc/shadow file. The format is roughly this:

1
2
3
4
5
6
7
8
9
10
11
sonic:$6$123:17736:0:99999:7:::
[--] [----] [---] - [---] ----
| | | | | |||+-----------> 9. Unused
| | | | | ||+------------> 8. Expiration date
| | | | | |+-------------> 7. Inactivity period
| | | | | +--------------> 6. Warning period
| | | | +------------------> 5. Maximum password age
| | | +----------------------> 4. Minimum password age
| | +--------------------------> 3. Last password change
| +---------------------------------> 2. Encrypted Password
+----------------------------------------> 1. Username

Where we can break down the relevant portion above a little more. See that $6$.n. labeled “Encrypted Password”? It goes like this:$<TYPE>$<PARAMS>$<HASHED>. In this particular case, type 6=SHA-512, of which uses only a salt (no iterations) = 123.

What happens if a bad actor manages to get my shadow file? While I won’t be happy, I’ll mostly not be super concerned. Of course I’ll update my passphrase.

If we were to create a PBKDF2 version of our passphrase and store it in a shadow file, it may look something like this:

1
mario:$PK2$a2pqcE1TM1lMS3VrZ0c2bXNQb3ZjZWF4VVBOTFFiQVN2X1Z5MXl3Szh4T2V3NFJ6cXdxdGdOdjVXclo3S18zYmE1eWh0bFJGd3FFZFk1U21iZG0xdmUK.250000:...hash value and so on...

Sweet! That’s 86 characters of base64 encoded salt, and 100k iterations! I’m actually making this up, as /etc/shadow doesn’t support PBKDF2 generally, but grub does! Let’s look at it’s format. It’s quite similar:

1
grub.pbkdf2.sha512.<ITERATIONS>.<SALT>.<HASHED>

Yep, I’m seeing some patterns here!

Iterations 🔁

We’ve fumbled over iterations a few times in this already, so it’s probably sinking in now: Iterations are effectively a way to wast the attackers time. As CPU and GPU power just keeps getting better and better, cracking a passphrase by simply using a secure hash such as SHA-256 may not be enough. Let’s make the attacker do that a ton of times! This need not be any secret however; in fact, if we’re using this method, like the salt we must have it in hand to decrypt – and it falls within the “parameters” group of material! What the attacker learns is a hint on how long the cracking may take. That’s really about it. You could read it something like this:

“At the time this iteration was chosen, we have speculated that it would take N amount of time to crack”.

We want that time to be a long time. A very long time. But of course cracking hardware keeps getting faster and faster creating a cat and mouse game… but that’s another whole can of worms.

So have my salt, and have my iterations too! But I’m keeping my my passphrases to myself!

I hope was helpful to some. I’m sure I’ll get nasty responses; In which I will likely just update this article with more relevant links to specifications, NIST/OWASP et al. to back these claims. Or maybe I’ll just get “you spelled $word” wrong like I usually do (…and I do usually spel gud.).

Until next time…