0

I am creating a web app (HTML, CSS, JavaScript, PHP & MySQL) where the users register, and only logged users can create and save personal Notes (encrypted) in a MySQL Server database I have for this.

I don't like external libraries, etc. I like pure PHP. With the 2 functions I have created, the idea is that not even I as the developer/provider can decrypt the Notes the users have. I like transparency, and these 2 functions will be published on the website so everyone can see how the encryption/decryption is performed. I think this will generate Trust in my users, knowing that not even I as the provider can decrypt their notes, only they can do it.

Important

I have a file named constants.php that has the following content:

<?php
define('ITERATIONS', '100');
define('HASH_LENGHT', '128');
define('PEPPER_KEY', '0187H{UM7*JkbYJFhFG&');
define('PEPPER_IV', 'MlI@J86yJK^45bvCXaWeN');

define('PEPPER_RANDOM_SALT',
[
    'LO86ti6uJ^4v6$^#4c35vhng227',
    'KUh5rBKNm547bVC6V%b7237c4ee',
    'FCVrvyjMNBvCE4Y5YuyiuK9PL9m',
    'KUGbj4hIH6H5rFGbvr5UHTNHHCM',
    'MNjH65tCvH2rwEHbhnj87543d34',
    '6yuhKLmuyngb43x%$$x#cn$$%()',
    '$^v546&##75B*^%^(*$#5#31254',
    'nnui66544G34IYIv5$rR5Y34545'
]);
?>

Step 1: Registration

register.html has a form where the user enters his personal data and the data is sent to the page register.php that has this content:

require("constants.php");

/* Clean Form data */
$email = htmlentities(strtolower($_POST["email"]), ENT_QUOTES, "UTF-8");
$password = htmlentities($_POST["password"], ENT_QUOTES, "UTF-8");

/* Get a Pepper Random Salt */
$number = array_rand(PEPPER_RANDOM_SALT, 1);
$random_salt = PEPPER_RANDOM_SALT[$number];

/* Combine the password with the Random Salt */
$hash = hash_pbkdf2("sha256", $password, $random_salt, ITERATIONS, HASH_LENGHT);

...
...
Here I validate if the user already exists in the MySQL table, if not, I create the user and 
the password that will be saved in the MySQL database is going to be $hash
...
...

What I am doing here is, I get the password the user entered in the form and I get (1) random Pepper I use as Salt. I know the PHP function password_hash() exists for this but, I like to create the hash by my own way.

Step 2: Login

The page login.html has a Form where the user enters his email & password. The data is sent to login.php which has this content:

require("constants.php");

/* Clean Form data */
$email = htmlentities(strtolower($_POST["email"]), ENT_QUOTES, "UTF-8");
$password = htmlentities($_POST["password"], ENT_QUOTES, "UTF-8");

/* Combine the Password with each one of the Random Salts trying to find a match */
foreach(PEPPER_RANDOM_SALT as $random_salt){
    $hash = hash_pbkdf2("sha256", $password, $random_salt, ITERATIONS, HASH_LENGHT);
    $stmt = $db->prepare("SELECT * FROM `login` WHERE `email` = ? AND `password` = ?");
    $stmt->execute(array($email, $hash));
    if($stmt->rowCount() > 0){
        @session_start();
        $_SESSION["email"] = $email;
        $_SESSION["passphrase"] = $hash;
        exit("VALID");
    }
}
/* Destroy Session */
@session_start();
@session_unset();
@session_destroy();
exit("INVALID");

What I am doing here is, I get the email and password the user sent, then I go one-by-one of the Random Peppers Salt I use as Salt and I generate a hash. Then I try to find if that hash exists in the MySQL database. If there is a match, I start a PHP Session and I see the variables $_SESSION["email"] and $_SESSION["passphrase"].

Step 3: Encrypt

Let's assume the user is already logged in. The user is on the page new_note.html where there is a textarea where the user writes his note, he sent the data to the page encrypt.php that has this content:

Note: The variable $data is the data coming from the textarea.

require("constants.php");

/* Get the Passphrase from the Session */
@session_start();
@$passphrase = $_SESSION["passphrase"];

/* Validate if Session is active */
if($passphrase == NULL) { exit("Passphrase is required to Encrypt"); }

/* Get one Random Salt */
$number = array_rand(PEPPER_RANDOM_SALT, 1);
$random_salt = PEPPER_RANDOM_SALT[$number];

/* Combine the Passphrase + Random Salt + Pepper Key */
$key = hash_pbkdf2("sha256", $passphrase.PEPPER_KEY, $random_salt, ITERATIONS, HASH_LENGHT);

/* Combine the Passphrase + Random Salt + Pepper IV */
$iv = hash_pbkdf2("sha256", $passphrase.PEPPER_IV, $random_salt, ITERATIONS, 16);

/* Encrypt */
$output = openssl_encrypt($data, "AES-256-CBC", $key, 0, $iv);
echo $output;

To generate $key, I get the variable $_SESSION["passphrase"] generated when the user logged in and I combine it with the Pepper Key (found in constants.php) and I use (1) Random Pepper Salt as Salt. Then, I generate the hash.

To generate $iv, I get the variable $_SESSION["passphrase"] generated when the user logged in and I combine it with the Pepper IV (found in constants.php) and I use (1) Random Pepper Salt as Salt. Then, I generate the hash.

I then, proceed to Encrypt the data in the variable $data. This encrypted data is going to be saved in the MySQL database.

To give you an example, these are the real output of the string This is a demo:

  • sO26BJpswoHdiz/12FmkUg==
  • FL2hufBtea6TybOujbtg0w==
  • 6okuXFat5qmkuzlLkmza7w==

So, it generates different outputs for the same string which is AMAZING!

Step 4: Decrypt

When the user wants read one of his Notes, the data has to be decrypted. I use the following script:

Note: The variable $data is the data coming from the MySQL database.

require("constants.php");

/* Get the Passphrase from the Session */
@session_start();
@$passphrase = $_SESSION["passphrase"];

/* Validate if Session is active */
if($passphrase == NULL) { exit("Passphrase is required to Decrypt"); }

/* Combine the Passphrase with each one of the Random Salts trying to find a match */
foreach(PEPPER_RANDOM_SALT as $random_salt){
    
    /* Combine Passphrase + Random Salt + Pepper Key */
    $key = hash_pbkdf2("sha256", $passphrase.PEPPER_KEY, $random_salt, ITERATIONS, HASH_LENGHT);
    
    /* Combine Passphrase + Random Salt + Pepper IV */
    $iv = hash_pbkdf2("sha256", $passphrase.PEPPER_IV, $random_salt, ITERATIONS, 16);

    /* Decrypt */
    $output = openssl_decrypt($data, "AES-256-CBC", $key, 0, $iv);
    if(!$output == NULL){
        echo $output;
    }
}

To generate $key, I get the variable $_SESSION["passphrase"] generated when the user logged in and I combine it with the Pepper Key (found in constants.php) and I use (1) Random Pepper Salt as Salt. Then, I generate the hash.

To generate $iv, I get the variable $_SESSION["passphrase"] generated when the user logged in and I combine it with the Pepper IV (found in constants.php) and I use (1) Random Pepper Salt as Salt. Then, I generate the hash.

I then, proceed to create a hash using each one of the Pepper Random Salt as Salt expecting for an output. If there is an output, that means, the data could be decrypted using the Pepper Salt X and then, I show the decrypted data. If the output is empty, means the data could not be decrypted.

Overview

I am not creating an Encryption/Decryption model or something like that. I am just generating the hashes for $key and $iv in my own way, the data at the end is encrypted/decrypted using the built-in PHP functions openssl_encrypt and openssl_decrypt. I think the way I am doing it is fine but, feedbacks and any suggestions are very welcome to make this really secure. I have discovered the Peppers 2 days ago and I am using them. The idea of the Peppers is that, if my MySQL Database gets hacked or something, the hacker does not have a way to encrypt/decrypt data because there Peppers are not saved in the MySQL Database. The peppers are inside the file constants.php so, the hacker has no way to guest the constants PEPPER_KEY, PEPPER_IV and each one of the PEPPER_RANDOM_SALT. The Peppers I posted in this page, are dummy. In my website I have different Pepper values.

The source code of the file constants.php is NEVER going to be released. The only source code I am going to release is the content of Step 3 and Step 4. So whoever has technical background in PHP, can see how the encryption/decryption is performed and to realize that I never save clear-text data. I only save the encrypted data in the database. I am looking for full transparency where only the user knows how to decrypt data for his Notes.

Note: I am not a PHP developer, I just learned PHP by reading example codes posted everywhere on internet pages. I am just a simple Call Center agent but, I was able to accomplish these codes and I am very happy for it. Later on, I am planning to apply as a PHP developer for a company as Remote employee to have more time because this Call Center job is taking me nowhere.

10
  • 2
    You say in your intro, 'the idea is that not even me as the developer/provider can decrypt the Notes'. But, in step 4 of your process, it is your server that is doing the decryption. Therefore (at least once the user has provided his/her credentials), you can decrypt the user's secrets. It sounds like what you are trying to build is a 'zero access' system, where the server has 'zero access' to the user's secrets. For this, you need to do all encryption/decryption client-side (e.g. javascript). See proton.me/blog/zero-access-encryption for some interesting reading on this subject.
    – mti2935
    Commented Feb 6, 2023 at 0:40
  • 1
    The point is that since it's server side encryption, a simple snippet added to constants.php to fwrite(var_dump($_POST)) to capture.txt would be enough to decrypt the data out of band using the captured passwords. Or you could alter the web server to record all inbound and outbound traffic to get the unencrypted data.
    – wireghoul
    Commented Feb 6, 2023 at 2:43
  • 2
    On an different note, your salt implementation is incorrect, each hash should have its own unique salt, you've got a pre-populated list of data which may not be sufficiently random.
    – wireghoul
    Commented Feb 6, 2023 at 2:45
  • 2
    What you need to do is to look up "key management" for user-managed encryption. And you need to learn the established design patterns for log in, authentication, password hashing, etc. Your goal of "not even I as the provider can decrypt their notes" is not met. You can trivially decrypt their notes because your function does it using the key that your functions generate on the fly. Schneier's Law "Any person can invent a security system so clever that she or he can't think of how to break it."
    – schroeder
    Commented Feb 7, 2023 at 10:48
  • 1
    @schroeder +1 for invoking Schneier’s Law.
    – mti2935
    Commented Feb 7, 2023 at 11:24

1 Answer 1

3

Firstly, don't bother thinking that you can take someone else's secret in the clear and then convince them that you were unable to keep a copy, and don't expose your source-code via some client-facing function. Logically, it is impossible unless the user encrypts the data before it leaves their device. Just accept that this is the limit, focus on getting your server-side right, and point your users to a good quality client-side encryption mechanism instead.

Also please go back and read the comments made against your earlier q from yesterday, and take a note of the one that warns against changing the user's secret input. Don't do this - it will probably lead to unintended (bad) consequences somehow.

Anyway, there are a number of problems with the scheme as presented:

  1. the value of ITERATIONS is too low for PBKDF2/SHA256
  2. storing the user's password in the session is a bad idea - this can leak to storage on the server in clear text
  3. your scheme doesn't have a keyed mac (aka. tag) to ensure the ciphertext hasn't been interfered with prior to decryption
  4. consider sodium_crypto_box with secret and public key-pair derived from the user's secret during login

ITERATIONS

This value at 100 for hash_pbkdf2 is woefully inadequate - this number should be at least 100,000 and 250,000 would be better. With a value of 100 your users would need to choose high-entropy passwords otherwise they will be vulnerable to password forcing, in the event that your database of pwd hashes is stolen. Consider the use of sodium_crypto_pwhash instead (discussed subsequently).

$_SESSION["passphrase"]

Choose a unique passphrase for your test user, then grep the php session files for this value. (You should be able to find it pretty easily on the server.) These can hang around for quite a while depending on the config on your server. Maybe we don't need to bother cracking your pwd store if your user's passwords are all on disc (: ... thanks for letting me know by the way - that client-facing source-code was really handy!

NO MESSAGE AUTHENTICATION CODE

This can cause a problem in your decrypt logic, because you can't verify that the ciphertext isn't being interfered with during the operation - you just decrypt and deliver this result to the user. Depending on how you implement decrypt, it may also open you up to at least one attack that can be used to extract the plaintext. This is why AEAD ciphers are now the correct choice in this situation - by design they incorporate a mac function as part of the decrypt operation. If you don't use one you need to do this manually after encryption, eg. using $hmac512 = hash_init('sha512', HASH_HMAC, $hmackey) then hash_update($hmac512, $ciph).

ALTERNATIVE SOLUTION

A better idea in this case is to use one-way asymmetric encryption when encrypting secrets, deriving a secret and public key-pair from the user's passphrase. The public key can safely be stored in the $_SESSION while the secret key can be discarded and re-derived during decrypt (ie. forcing the user to re-supply pwd), or possibly stored within a https-only $_COOKIE which keeps it out of session storage. Your app might allow the user to decide what they want? Either way, the server only has the secret for a little while to do what it needs to.

For roughly the same amount of code, almost all criticisms you've accumulated over the last couple of days can be avoided, by using the Libsodium sodium_crypto_x family of functions instead SArciszewski'17.

Feed the output of sodium_crypto_pwhash into a corresponding secret and public key-pair generator for your encryption key. Then use this to encrypt $data for the user. (If you have a need for more than one key, you can also extend the output using $length and then take a slice, maybe to sign $ciph for some reason?). From within php -a ...

// the users's password
$secret_input = 'aw3sumdud';
// the user's salt, unique per-user
$a2id_salt = random_bytes(SODIUM_CRYPTO_PWHASH_SALTBYTES);
// the plaintext
$data = 'top-s3cr3t nookyular codez';

// approx. 2-sec delay (~2020 cpu php7.4)
$a2id_ops = 4;
$a2id_membytes = 1024*1024*1024;  //1gib

// if need be, adjust both downward until the delay is tolerable, but no faster than say ~250msec in 2023, ie. 
echo SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE.PHP_EOL;  # 2
echo SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE.PHP_EOL;  # 67108864 (64mib)

// note $length is set long enough for two separate keys
// (you don't need to do this if not signing/ hashing anything)
$sk_signk_seeds = sodium_crypto_pwhash(
  (SODIUM_CRYPTO_BOX_SEEDBYTES + SODIUM_CRYPTO_SIGN_SEEDBYTES)
  ,$secret_input                        // string $password,
  ,$a2id_salt                           // string $salt,
  ,$a2id_ops                            // int $opslimit,
  ,$a2id_membytes                       // int $memlimit,
  ,SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13  // int $algo
);

$skpk = sodium_crypto_box_seed_keypair(
  substr($sk_signk_seeds, 0, SODIUM_CRYPTO_BOX_SEEDBYTES)
);
$_SESSION["user_public_key"] = sodium_crypto_box_publickey($skpk);

// you don't need to sign anything because crypto_box is aead
// , but if you did, then you could eg. verify $ciph after
// retrieving from the db before deriving the user's decrypt key
// (keep the user's $public_sign_key in their db record)

$signkpk = sodium_crypto_sign_seed_keypair(
  substr($sk_signk_seeds, SODIUM_CRYPTO_BOX_SEEDBYTES, SODIUM_CRYPTO_SIGN_SEEDBYTES)
);
$signsk = sodium_crypto_sign_secretkey($signkpk);
$public_sign_key = sodium_crypto_sign_publickey($signkpk);

//... encrypt
$ciph = sodium_crypto_box_seal($data, $_SESSION["user_public_key"]);

//... sign
$sig = sodium_crypto_sign_detached($ciph, $signsk);

//... verify
$isgood = sodium_crypto_sign_verify_detached($sig, $ciph, $public_sign_key);

//... decrypt
$note = sodium_crypto_box_seal_open($ciph, $skpk);

// debug
echo 'Verified? '
  . $isgood
  . PHP_EOL
  . sodium_bin2base64($ciph, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING)
  . PHP_EOL
  . $note;

//... clean-up
sodium_memzero($note);
sodium_memzero($data);
sodium_memzero($secret_input);
sodium_memzero($sk_signk_seeds);
sodium_memzero($skpk);
sodium_memzero($signkpk);
sodium_memzero($signsk);

CONCLUSION

The proposed alternative demonstrates the use of a memory-hard password-based kdf to generate keys for other cryptographic operations Libsodium, FDenis etal. However, it leaves a number of details unspecified (least of all proper error handling!) such that it must not be copied blindly:

  • how do we check to see if the user should be allowed to log in?
  • what if the user wants to change their pwd?
  • what if we decide next year to make the pwd kdf harder (slower)?
1
  • We do not perform code review unless there is a specific question asked. This was previously closed and there is no question asked.
    – schroeder
    Commented Feb 7, 2023 at 10:44

Not the answer you're looking for? Browse other questions tagged .