I am trying to implement a kind of email verification system with a node.js server with no state.
The strategy
- User sends his email to the server
- Server generates a 4 digits code based on the email address and sends it to the user via email.
- User sends back the received code via email + the email address to the server
- Server re-generates the 4 digits code based on the email and compares it with the code sent by the user.
My implementation to generate the 4 digits code
- Create a HEX digest using HMAC SHA-256 hash function
- Take the first 3 characters of the digest
- Convert them to an integer
- If length < 4, concatenates one or multiples
0
at the end
const crypto = require('crypto')
const get4DigitsCode = (message) => {
const hash = crypto
.createHmac('sha256', Buffer.from(SECRET_KEY, 'hex'))
.update(message)
.digest('hex')
const first3HexCharacters = hash.slice(0, 3)
const int = parseInt(first3HexCharacters, 16)
let code = int.toString()
code =
Array(4 - code.length)
.fill(0)
.join("") + code
return code
}
After generating codes for 8293 email addresses, I noticed that I had 4758 duplicates. Is it normal to have this amount of duplicates for a code as this sort? Is my strategy and my implementation secure (ability to guess the code)?
The service is a mobile app, email based (a "mail to self" app). I want a 4 digit code for UX reasons. The user can read the code from the email client notification, easily memorize it and type it in the app (that he never leaves). No tedious copy and paste, not leaving the app, simply read and type. I know that multiple emails would generate the same code, but it doesn't matter as it is just for validating emails. Also I will protect the APIs against brute-force.
Can someone guess the code with this strategy (what are the risks or possible attacks?) and is my current implementation correct?
UPDATE
A better implementation thanks to @duskwuff's answer :
const crypto = require('crypto')
const get4DigitsCode = (message) => {
const hash = crypto
.createHmac('sha256', Buffer.from(SECRET, 'hex'))
.update(message)
.digest('hex');
const first4HexCharacters = hash.slice(0, 4);
const int = parseInt(first4HexCharacters, 16) % 10000;
let code = int.toString();
code =
Array(4 - code.length)
.fill(0)
.join('') + code;
return code;
};