Would XOR'ing two Argon2 keys add extra security?

When a user registers on the app, they need to provide a password they create and we provide them with a randomly generated passphrase (e.g. Apple Cat Ladder Fire). We then use Libsodium's crypto_pwhash() to generate a key for both the user's password and the generated passphrase.

Since most users will use a low entropy password, the goal was to combine it with a higher entropy source to derive a master key using the crypto_kdf_derive_from_key() function. This master key only exists to wrap/unwrap the user's private key.

I was wondering if XOR'ing these two keys actually has any benefit or am I chasing my own tail?

I also added the XOR function I wrote in Kotlin (There isn't a native implementation that I'm aware of) to see if I'm actually XOR'ing the keys properly.

fun xorKeys(keyOne:ByteArray, keyTwo:ByteArray): ByteArray {
// Check to make sure both keys are the same size
if (keyOne.size != keyTwo.size)
throw Throwable("Key sizes don't match")

// Create a new key handle
var newKey = ByteArray(keyOne.size)

// XOR individual bytes
for (i in keyOne.indices) {
newKey[i] = keyOne[i] xor keyTwo[i]
}

// Return the new key
return newKey
}


That sounds convoluted.

The server-generated passphrase is essentially an extension to the client password. So you may just concatenate both (with a proper delimiter, or with their respective sizes being included) and hash the whole thing.

Also, from a usability perspective, the passphrase is inconvenient (and useless when using a password manager). You may use the user's email address as an extension instead.

• I should clarify that the passphrase isn't server generated. The client has a dictionary of n thousand words and randomly generates a passphrase. Similar to what OnePassword does upon their user registration. The server never sees the password nor the passphrase. Instead it just gets the salt and verifier to perform SRP later in the process. – Mr_Antivius Nov 28 '19 at 0:36
• Your point about just concatenating the strings and hashing it once sounds very reasonable. It was something I thought about briefly, but assumed it would be weaker than combining the two hashes after the fact. You way is also quicker for the end user. Also, your point about usability is something I do worry about since my target is the lay-man who probably doesn't use a password manager. Hence they would probably have a very weak and repeatable password. So I wanted to add something extra that an attacker would need in-order to compromise an account. – Mr_Antivius Nov 28 '19 at 0:37
• Since this is an online application, a second factor (TOTP) would be a more secure option. – Frank Denis Nov 28 '19 at 1:10
• So if I understand correctly, the user enters a password, the client and server perform SRP to authenticate and at the end, use TOTP for for 2FA? That's the only way I can see 2FA working in this scenario since I need a repeatable way to derive a key to unwrap the user's private key. – Mr_Antivius Nov 28 '19 at 2:37

Do what Frank Denis suggested. Password stretching algorithms make guess-and-check attacks more expensive by some constant factor. Let's call that factor $$k$$.

Let $$m$$ be (roughly) the number of user-passwords a cracker would need to try before guessing the correct one. (Or, in other words, $$m$$ would be the average cost to crack just the user-password if hashed by a plain cryptographic hash.)

That makes the cost to generate a list of user-password hashes $$\mathcal{O}(k \cdot m)$$. Likewise, if $$n$$ is the number of distinct passphrases the server may generate, then the cost of generating a list of passphrase hashes is $$\mathcal{O}(k \cdot n)$$.

With these two lists you could brute force the derived key in $$\mathcal{O}(n \cdot m)$$ time, since $$n \cdot m$$ is the number of password-passphrase pairs.

If, instead, the attacker knew the XOR of the two hashes and wanted to recover the password and passphrase, then they could find the correct pair in $$\mathcal{O}(\operatorname{max(n, m)})$$ time. But that isn't the scenario you describe. So this is just a side comment.

This makes the total cost to crack the derived key. $$\mathcal{O}(k(n + m) + n \cdot m)$$.

If you instead combine the two inputs and hash the result, then cost would be $$\mathcal{O}(k \cdot n \cdot m)$$

• Thanks for the very clear explanation along with the time factor. I guess string concatenating is the way to go. – Mr_Antivius Nov 28 '19 at 0:56