While many of the comments you have received offer other valid ways of solving the problem e.g. a table of email addresses with primary keys, I am of the position that the best way to solve the problem is the way you originally intended: including the email address encrypted in the query URL.
I feel that this way is better because:
- Computing the email address does not require database access. Database bottle-necking is generally the biggest offender for high-latency requests.
- Encryption means that the same email address will produce a different IV/ciphertext pair each time you encrypt it. Thus, if you send multiple emails at different times (say, for two different marketing campaigns), the URL will be different each time. This may not have an effect, but it does provide a security advantage in that an attacker can't "pretend" that an email has been opened simply by visiting a URL.
The issue is that for this way to be better, you have to do it well. I've included an excerpt in PHP from this repository below. If you can't use openssl_*
then upgrade your PHP version. Do not, ever, use the mcrypt_
functions. They are deprecated for a reason. You may need to hex encode instead of base64 encode the email addresses as is done in the example below.
<?php
define("ALGORITHM_NAME", "aes-128-gcm");
define("ALGORITHM_NONCE_SIZE", 12);
define("ALGORITHM_TAG_SIZE", 16);
define("ALGORITHM_KEY_SIZE", 16);
define("PBKDF2_NAME", "sha256");
define("PBKDF2_SALT_SIZE", 16);
define("PBKDF2_ITERATIONS", 32767);
function encryptString($plaintext, $password) {
// Generate a 128-bit salt using a CSPRNG.
$salt = random_bytes(PBKDF2_SALT_SIZE);
// Derive a key.
$key = hash_pbkdf2(PBKDF2_NAME, $password, $salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, true);
// Encrypt and prepend salt and return as base64 string.
return base64_encode($salt . encrypt($plaintext, $key));
}
function decryptString($base64CiphertextAndNonceAndSalt, $password) {
// Decode the base64.
$ciphertextAndNonceAndSalt = base64_decode($base64CiphertextAndNonceAndSalt);
// Retrieve the salt and ciphertextAndNonce.
$salt = substr($ciphertextAndNonceAndSalt, 0, PBKDF2_SALT_SIZE);
$ciphertextAndNonce = substr($ciphertextAndNonceAndSalt, PBKDF2_SALT_SIZE);
// Derive the key.
$key = hash_pbkdf2(PBKDF2_NAME, $password, $salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, true);
// Decrypt and return result.
return decrypt($ciphertextAndNonce, $key);
}
function encrypt($plaintext, $key) {
// Generate a 96-bit nonce using a CSPRNG.
$nonce = random_bytes(ALGORITHM_NONCE_SIZE);
// Encrypt and prepend nonce.
$ciphertext = openssl_encrypt($plaintext, ALGORITHM_NAME, $key, OPENSSL_RAW_DATA, $nonce, $tag);
return $nonce . $ciphertext . $tag;
}
function decrypt($ciphertextAndNonce, $key) {
// Retrieve the nonce and ciphertext.
$nonce = substr($ciphertextAndNonce, 0, ALGORITHM_NONCE_SIZE);
$ciphertext = substr($ciphertextAndNonce, ALGORITHM_NONCE_SIZE, strlen($ciphertextAndNonce) - ALGORITHM_NONCE_SIZE - ALGORITHM_TAG_SIZE);
$tag = substr($ciphertextAndNonce, strlen($ciphertextAndNonce) - ALGORITHM_TAG_SIZE);
// Decrypt and return result.
return openssl_decrypt($ciphertext, ALGORITHM_NAME, $key, OPENSSL_RAW_DATA, $nonce, $tag);
}
?>