0

OK... to the point...

I have a e-mailshot program that sends out thousands of emails - each with str_replace() merge fields (fields that get replaced by a row value in a recordset).

One of the important parts is my ability to track those mails on open so I include a single server-side generated pixel...

<img src="http://...trace.php?email=<<<EMAIL>>>" alt="" height="1" width="1">

The str_replace() replaces <<<EMAIL>>> with a unique real email address.

The trace.php file reads the $_GET['email'] and either logs it or sends mail confirmation.

My issue is security :)

I want to use two-way encryption so that the $_GET variable sent in the URL is an encrypted email. The trace.php file then needs to decrypt it.

As it's being sent in a URL, It has to be in ASCII format otherwise it will corrupt before decrypting.

I can't use openssl_encrypt() & openssl_decrypt() and I'm having to work with php 5.2.0 (don't hurl abuse at me!).

Any help would be greatly appreciated!!

Federico klez Culloca
  • 26,308
  • 17
  • 56
  • 95
Richard Owens
  • 155
  • 16
  • 4
    Why, instead, don't you generate a token that you can then associate with an email address on the server side? – Federico klez Culloca Jun 18 '18 at 14:34
  • 2
    Anything that is *encrypted* can be *decrypted* and hence still represents an **attack surface**. Remove any and all attack surfaces entirely by making the values you send *meaningless, random garbage*. You'll just have to store that garbage on your side too. – deceze Jun 18 '18 at 14:38
  • I would have to create comparison tokens dynamically to store in the MySQL. Is this what you mean? Surely it would be simpler to encrypt the email string? – Richard Owens Jun 18 '18 at 14:39
  • 1
    @RichardOwens yes, that's what I (and I think @deceze) mean. And no, it wouldn't be that much harder. – Federico klez Culloca Jun 18 '18 at 14:40
  • 2
    Hash the email, store both hash and email in table. When trace.php is accesses look up hash and get email. – AbraCadaver Jun 18 '18 at 14:40
  • Thanks @deceze - good point although I'm trying to work out how I can use one way encryption but still be able to 'find' the relevant record from the database using a hashed variable... I've only used hashing for comparisons before (passwords etc.) – Richard Owens Jun 18 '18 at 14:41
  • Also, no need to hash either just insert email with auto-increment id and use that. Not really security here just obscurity. – AbraCadaver Jun 18 '18 at 14:42
  • @FedericoklezCulloca I think I know what I have to achieve but I still need an ASCII format hashed version to store in the MySQL - can anyone point me in the right direction? I'll have to run a batch query on this. – Richard Owens Jun 18 '18 at 14:45
  • @AbraCadaver surely adding an auto-increment id doesn't really obscure either? – Richard Owens Jun 18 '18 at 14:46
  • 1
    A simple numeric id is inherently meaningless. Anyone seeing `id=5` inside a URL has absolutely no information about what email that is connected to. – deceze Jun 18 '18 at 14:50
  • 1
    My email id=1776, what is my email address? – AbraCadaver Jun 18 '18 at 14:52
  • Good point!!!! I just need to find that record and echo back the mail address! Duh.... – Richard Owens Jun 18 '18 at 14:53
  • No wait... I can't do that. The trace.php file is hosted externally and the records database is hosted independently on an intranet. I can't SELECT using the $_GET data :( – Richard Owens Jun 18 '18 at 14:56
  • The use of "one way encryption" and "two way encryption" is essentially incorrect. Encryption is reversible by definition. What you are calling "one way encryption" is a cryptographic hash and is not encryption since it can not be reversed. – zaph Jun 18 '18 at 15:15
  • @zaph I am familiar with the differences. Please excuse the way in which I've used the terms. I have prevously "hashed" passwords etc. but I am looking at "encryption" here. :) – Richard Owens Jun 18 '18 at 16:03

1 Answers1

0

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);
}
?>
Luke Joshua Park
  • 9,527
  • 5
  • 27
  • 44
  • Thank you for your effort here. Regrettably, I can't call some pre-defined functions here including random_bytes() or hash_pbkdf2(). – Richard Owens Jun 19 '18 at 08:40
  • 1
    You need to upgrade your version of PHP. The effort required to implement this without those functions is substantially more than the effort required to just upgrade your PHP version. – Luke Joshua Park Jun 19 '18 at 08:41
  • I'm rapidly coming to that conclusion!! I'm just sorry I can't put your hard work to good use. Kudos. – Richard Owens Jun 19 '18 at 08:51