Skip to content
This repository has been archived by the owner on May 22, 2021. It is now read-only.

Insecure password storage: KDF with low PBKDF2 iterations and HMAC with fixed nonce #1371

Open
Lekensteyn opened this issue Jul 15, 2019 · 0 comments

Comments

@Lekensteyn
Copy link

In #607 a concern was raised by @ehuggett about an insufficient number of iterations for the PBKDF2 function as used for processing the authentication password. This key is not used for encryption if users think so.

I think it is realistic to assume that the share URL (file.url) can be compromised at some point (e.g. because a conversation/email was leaked). If the KDF output is leaked, then 100 iterations is not sufficient.

For others wondering, the password is only used for authentication to the server, it is not used for encryption. So if (1) the URL gets leaked and (2) the server storage gets leaked, then the password has no use.

The encryption details are described at https://github.com/mozilla/send/blob/master/docs/encryption.md, but it is unfortunately rather vague about the exact parameters (hash function, iterations) and the data that is actually sent and stored on the server. Suggestion: update that document such that an analysis can be done without requiring digging through the source code.

The client code that uses the password is here:

send/app/keychain.js

Lines 110 to 120 in 3b8dbfd

async authHeader() {
const authKey = await this.authKeyPromise;
const sig = await crypto.subtle.sign(
{
name: 'HMAC'
},
authKey,
b64ToArray(this.nonce)
);
return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
}

The server code is right here:
const auth = req.header('Authorization').split(' ')[1];
const meta = await storage.metadata(id);
if (!meta) {
return res.sendStatus(404);
}
const hmac = crypto.createHmac(
'sha256',
Buffer.from(meta.auth, 'base64')
);
hmac.update(Buffer.from(meta.nonce, 'base64'));
const verifyHash = hmac.digest();
if (crypto.timingSafeEqual(verifyHash, Buffer.from(auth, 'base64'))) {
req.nonce = crypto.randomBytes(16).toString('base64');
storage.setField(id, 'nonce', req.nonce);
res.set('WWW-Authenticate', `send-v1 ${req.nonce}`);
req.authorized = true;
req.meta = meta;
} else {
res.set('WWW-Authenticate', `send-v1 ${meta.nonce}`);
req.authorized = false;
}

If I am not mistaken, the data flow when an a password is set is as follows:

// example user input
passwordKey = "user supplied password here"
shareUrl = "https://send.firefox.com/download/c5763857606a7cef/#38KeHMXW30zbPOC9aQ-CBQ"

salt = shareUrl
iterations = 100
authKey = PBKDF2(SHA-256, passwordKey, salt, iterations)
nonce = base64-decode("yRCdyQ1EMSA3mo4rqSkuNQ==")
verifyHash = HMAC-SHA256(authKey, nonce)

The fixed nonce occurs because KeyChain is constructed with no parameters. This fixed nonce is subsequently sent over the websocket which is presumably stored in plain on the server, see https://github.com/mozilla/send/blob/master/app/fileSender.js. I verified with a debugger that the nonce is fixed.

So assuming that:

  • The server storage is compromised and leaks metadata (nonce, verifyHash).
  • The URL is leaked
  • The analysis is correct and a fixed nonce is stored together with verifyHash

Then an attacker can bruteforce the original password due to the very low iteration count.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant