Jump to content

Has Processwire an Encryption Library?


Orkun
 Share

Recommended Posts

Hi Guys

I have an Question about Encryption/Decryption in Processwire. Does Processwire has an encryption Library which provides two-way data encryption for sensitive data? I mean something like the Encryption Library from Codeligniter

I saw the Password.php class in the core which uses some encryption magic, but I don't think, that the purpose of the password class is to provide a two-way data encryption. Because I don't see how it should be possible that I can read/decrypt the password from the database since it hashed(one-way encryption). 

Why I need a two-way data encryption or. why I am asking?

In the near Future I must make a "Password/-Login Manager(included Customer / project management)" where a Processwire User can create Logins(FTP, MySQL, Processwire Login etc...) and assign them to different Projects. Since the Passwords for the Logins are very sensitive data, they must be stored encrypted in the Database, but they must also be listed in Plaintext in the Password Manager Interface. Because of that I am writing this thread to collect some Information about Encrypting/Decrypting in Processwire and some experience from other developers regarding Encrypting/Decrypting.

Why I am developing a Password/-Login Manager?

My Company uses Active Collab for managing Customer Logins/Projects etc. It works very well! The Problem is that the Password Manager Plugin doesn't exist anymore in the new update of the Tool. So we decided, that i develop a Password Manager in the beautiful CMS/CMF Processwire. It also serves as a perfect final exam for my education as a apprentice in Web-Developement/Informatics.

Finally a Code Snippet regarding the Encryption Library from Codeligniter:

include 'Crypter Class/Classes/Encrypt.php';

$data = $page->pwmessage; //normal text field in Processwire

$ci_crypter = new CI_Encrypt(); 

$ci_crypter->set_key("r7kl-icfc-8ext-p"); 
$ci_crypter->set_cipher(MCRYPT_RIJNDAEL_256);
$ci_crypter->set_mode(MCRYPT_MODE_ECB);

$ci_encrypted = $ci_crypter->encode($data, $ci_crypter->get_key());
$ci_decrypted = $ci_crypter->decode($ci_encrypted, $ci_crypter->get_key());

$outBody .= "<strong>Original:</strong> ".$data."<br>";
$outBody .= "<strong>Encrypted:</strong> ".$ci_encrypted."<br>";
$outBody .= "<strong>Decrypted:</strong> ".$ci_decrypted."<br>";

post-3125-0-32004600-1454940646_thumb.pn

  • Like 1
Link to comment
Share on other sites

I got an email from a knowledgeable ProcessWire member about a possible issue with this Fieldtype.

PLEASE DO NOT USE THIS FIELDTYPE RIGHT NOW.

Thanks for that mail!

---

I have a Fieldtype and Inputfield that does what you ask: https://github.com/Da-Fecto/FieldtypeMyCrypt. Passwords are salted again with the ProcessWire salt in /site/config.php, so encrypted passwords only work in 1 install.

When using this Fieldtype, be sure that you use https else your passwords will still be plain text when submitting :-)

Edited by Martijn Geerts
  • Like 6
Link to comment
Share on other sites

When using this Fieldtype, be sure that you use https else your passwords will still be plain text when submitting :-)

In addition, when using https, make sure that your connection is using a strong protocol version and cipher suite.

Reference:

https://www.owasp.org/index.php/Transport_Layer_Protection_Cheat_Sheet

  • Like 3
Link to comment
Share on other sites

@Nukro: A few points that may help you get into the right direction:

Never use ECB mode. With the same key, it always produces the same output for the same input, thus making patterns obvious and rainbow table attacks cheap. If you see an example on a web page that uses ECB, navigate somewhere else (that includes the examples at php.net).

Never store keys in clear text or with weak encryption on the same system where you hold the data (or have it easily accessible), or you're replacing security by obscurity.

Whenever you use symetric encryption, use an initialization vector (and make sure your cipher mode, e.g. CBC, supports IVs). It is much like the salt in password hashes and needs to be random for each encrypted entry and reasonably unique. Prepend the IV before you store the encrypted data and extract it for decryption.

Depending on how you model your system, implementing a secure system can get slightly tricky. If visibility of information shouldn't be limited to a single PW user, they all need to know the key (let's call it the 'master key') used. To avoid storing it in plain text, encrypt and store the master key for every user. Use a secure, repeatable hashing function to generate a long enough encryption key from the user's password for this step. On password change, re-encrypt and store the key (and, again, don't forget to use IVs). Of course, in such a system, all user passwords need to be strong, or an attacker with access to the DB contents could easily crack the encryption to the master key through brute force. To populate the encrypted master key for new users (or if you add the feature to a system with existing users) you have to assign their password from an account that already has the correct master key. This way, at least no plain encryption key is ever stored in the system.

There are more elaborate (and, of course, even more secure) approaches, but these would require an automated pulic/private key infrastructure and off-site services. That's what the "good guys" among the big internet companies use, but things get really complex at that point.

  • Like 7
Link to comment
Share on other sites

@BitPoet, @horst, @cstevensjr, @Martijn Geerts

Thank you for your advices!

I have some Questions/Points to discuss to/with you @BitPoet

Never use ECB mode. With the same key, it always produces the same output for the same input, thus making patterns obvious and rainbow table attacks cheap. If you see an example on a web page that uses ECB, navigate somewhere else (that includes the examples at php.net).

The Encrypt.php class from Codeligniter uses CBC as default mode so no need to set extra mode via set_mode function (check)

       /**
	 * Get Mcrypt Mode Value
	 *
	 * @return	int
	 */
	protected function _get_mode() {
		if ($this->_mcrypt_mode === NULL) {
			return $this->_mcrypt_mode = MCRYPT_MODE_CBC;
		}

		return $this->_mcrypt_mode;
	}

------------------------------------------------------------------------------------------------------------------------------------------------------------

Never store keys in clear text or with weak encryption on the same system where you hold the data (or have it easily accessible), or you're replacing security by obscurity.

I changed the Encrypt.php class and added a new function called "generate_key". 

Is this enough?

        /**
	 * Create a random key
	 *
	 * @param	int	$length	Output length
	 * @return	string
	 */
	public function generate_key($password) {
		$iterations = 1000;
		$salt = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
		$hashed_key = hash_pbkdf2("sha256", $password, $salt, $iterations, 120);
		return $hashed_key;
	}

--------------------------------------------------------------------------------------------------------------------------------------------------------------

Whenever you use symetric encryption, use an initialization vector (and make sure your cipher mode, e.g. CBC, supports IVs). It is much like the salt in password hashes and needs to be random for each encrypted entry and reasonably unique. Prepend the IV before you store the encrypted data and extract it for decryption.

In the Encrypt.php class the "encode" function uses an other function called "mcrypt_encode" which prepends an iv to the encrypted data.

        /**
	 * Encrypt using Mcrypt
	 *
	 * @param	string
	 * @param	string
	 * @return	string
	 */
	public function mcrypt_encode($data, $key) {
		$init_size = mcrypt_get_iv_size($this->_get_cipher(), $this->_get_mode());
		$init_vect = mcrypt_create_iv($init_size, MCRYPT_RAND);
		$encrypted_data = $init_vect.mcrypt_encrypt($this->_get_cipher(), $key, $data, $this->_get_mode(), $init_vect);

		return $this->_add_cipher_noise($encrypted_data, $key);
	}

--------------------------------------------------------------------------------------------------------------------------------------------------------------

Depending on how you model your system, implementing a secure system can get slightly tricky. If visibility of information shouldn't be limited to a single PW user, they all need to know the key (let's call it the 'master key') used. 

As for as I know the Passwords Visibility should be limited to single PW users (Which Passwords can the workers see? ).

To avoid storing it in plain text, encrypt and store the master key for every user. Use a secure, repeatable hashing function to generate a long enough encryption key from the user's password for this step. On password change, re-encrypt and store the key (and, again, don't forget to use IVs). Of course, in such a system, all user passwords need to be strong, or an attacker with access to the DB contents could easily crack the encryption to the master key through brute force. To populate the encrypted master key for new users (or if you add the feature to a system with existing users) you have to assign their password from an account that already has the correct master key. This way, at least no plain encryption key is ever stored in the system.

Can you further explain this part respectively make an example?

For encrypting the key I use the hash_pbkdf2 hashing-function, is this enough? 

Do I have to make an Frontend Registration(handled via API) where the workers can sign up(take the password and give it as parameter to "generate_key" function where the return value of this function will be the "master key", which is saved in a field called "e.g master_key" under the user template in processwire?)

I'm sorry if I am asking stupid or too much questions. I am very meticulous since it is my first time with Encryption/Decryption and also because it is my final exam(I am doing preparatory work at the moment, which I have also must document :D ).

Here is also the altered Version of the Encrypt.php class.

Tried out the current class on a template:

<?php

include 'Crypter Class/Classes/Encrypt.php';

$form = $modules->get("InputfieldForm");
$form->action = $page->url;
$form->method = "post";
$form->id = "myForm";

$field = $modules->get("InputfieldText");
$field->label = "Password";
$field->attr("id+name", "passwort");
$field->required = 1;
$field->columnWidth = 40;
$form->append($field);

$submit = $modules->get("InputfieldSubmit");
$submit->attr("value", "save");
$submit->attr("id+name", "submit");
$form->append($submit);

if($input->post->submit){

	$form->processInput($input->post);

	if($form->getErrors()) {
    	$outBody .= $form->render();
   	}else {

        $data = $form->get("passwort")->value;

       	$crypter = new CI_Encrypt();
        
        $masterkeypage = $pages->get("template=secret");
        $masterkeypage->setOutputFormatting(false);

        //At the moment it is statically respectively no user pw is assigned
       	$masterkeypage->masterkey = $crypter->generate_key("userpassword?");

        if($masterkeypage->save()){
            $page->of(false);
            $page->pwmessage = $crypter->encode($data, $masterkeypage->masterkey);
            if($page->save()){
                $session->redirect($page->url);
            }
        }

    }
}else {
    $crypter = new CI_Encrypt();
    $masterkeypage = $pages->get("template=secret");

    $decryptedtext = $crypter->decode($page->pwmessage, $masterkeypage->masterkey);

    $outBody .= "<h4><strong>Save encrypted Password in Database</strong></h4>";
    $outBody .= "<strong>Encrypted:</strong> ".$page->pwmessage."<br>";
    $outBody .= "<strong>Decrypted:</strong> ".$decryptedtext."<br>";
    $outBody .= "<strong>Key</strong>: $masterkeypage->masterkey <br/>";
    $outBody .= "<br />";
    $outBody .= $form->render();
}

post-3125-0-39039200-1455193286_thumb.pn

PS: Sorry for this long post :P

Link to comment
Share on other sites

I must admit i dont know that much about encryption myself, so I have used the following example with a little modification.

http://stackoverflow.com/questions/16600708/how-do-you-encrypt-and-decrypt-a-php-string/16606352#16606352

Do not use BLOWFISH and EBC (this it too predictable) as in the example, instead of that i'm using RIJNDAEL_128 (or 256 if you like) and CBC

I also recommend to use a long encryption key, maybe something above 1024 characters, or even 2048 long

function encrypt($pure_string, $encryption_key) {
    $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
    $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
    $encrypted_string = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $encryption_key, utf8_encode($pure_string), MCRYPT_MODE_CBC, $iv);
    return strtr(base64_encode($iv.$encrypted_string), '+/=', '-_.');
}

function decrypt($encrypted_string, $encryption_key) {
    $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
    $encrypted_string_dec = base64_decode(strtr($encrypted_string, '-_.', '+/='));
    $iv_dec = substr($encrypted_string_dec, 0, $iv_size);
    $decrypted_string = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $encryption_key, substr($encrypted_string_dec, $iv_size), MCRYPT_MODE_CBC, $iv_dec);
    return trim(utf8_decode($decrypted_string));
}

update: The base64 encode/decode i threw in so that it should be possible to use the encrypted strings as url segments and so are able to use it together with procache

Edited by Raymond Geerts
  • Like 3
Link to comment
Share on other sites

@Nukro: What you have so far looks good to me. Your question with the user registration is a valid one, and re-reading my post I found that I left out a step (I was working on our intranet site at the moment where I always have the user password available due to http basic auth, but that's not a given). Thankfully, the scenario gets a little less complicated if each user only needs to see their own passwords.

Basically, yes, you need to populate your user's key in the registration (or password change) step.

The flow should look like this:

1) User logs in and PW checks if the key in the user template is already populated

2a) If not, create one, encrypt it with the hash of the user's password and store it

2b) If yes, decrypt it using the password hash

Now you have the unencrypted key in memory. But you probably don't want the user to have to enter their password every time they view a stored login (or you'd be as good as done here).

So, to be able to use the key while the user stays logged in, you need to store it in a way that it can be decrypted without giving the password. The best option for that is session storage. Thus the next steps are:

3) Create a random key (session key) and store it in a session cookie (lifetime 0)

4) Encrypt the user's key using that session key and store it in the user's session

5) Now, with every consecutive visit while logged in, you can read the encrypted real key from the session and decrypt it using the session key in the cookie

Even though the encrypted key is stored in the session, it's at no time saved in plain text.

6) Make sure that if a user changes their password, they need to give both their old and new password and re-encryped and store the key again.

You have only one question left now: do you need to add protection against password loss? If yes, it gets a little more involved (though not awfully so). You can add a recovery mechanism like this:

- Create a public/private key pair for the "recovery agent" that is long enough to asymetrically encrypt the user's key. Keep the private key out of the site (store it in a safe place where you find it again) and make the public key accessible to your code

- In steps 2a and 6, also encrypt the user's key with the recovery agent's public key (let's call the result the backup key) and store it somewhere (e.g. another hidden field in the user template)

- Create a password reset form where the admin/recovery agent can re-set the user's passwords and enter the private key.

Now, when a user loses their password, the following steps happen:

- When the admin/recovery agent assigns the new password, decrypt the backup key using the private key, re-encrypt it with the user's password hash and store it just like in steps 2a and 6

- You best make sure that the user changes their password again. Now the regular mechanism of step 6 works again

  • Like 1
Link to comment
Share on other sites

I am very thankful for that Tutorial @BitPoet!

Unfortunately I found out from my Supervisor that the passwords shouldn't be limited to single users... He declared me the Scenaro like this:

- Every User(Worker) can see all Projects(Pages).

- A Customer(Page) can have multiple children Projects Pages / A Project Page can only have 1 unique Customer parent Page.

  (Parent/Children Scenario fits well)

- A Project Page can have multiple children "Services" Pages. 

- A Service Page can have multiple  children "Credential" Pages.

-  Admin can assign mutliple Service Pages to a user (He defines which services a user can access/see)

- When a User has access to Service Page, he can see all Credentials/Childrens of it.

I think for this setup it will be needed a Master Key Page with a  "Master Key Field" which all PW-Users has access to it to see the Credentials in Cleartext?

Setup in PW-Tree Format:

- Home

- Customers

        |-> Customer 1

                   |-> Project 1(e.g Website blabla)

                              |-> Service 1(e.g Processwire Logins)

                                        |-> Credential 1 (e.g Username: user1 / Password: password1)

                                        |-> Credential 2 (e.g Username: user2 / Password: password2)

                                        ...

                              |-> Service 2(e.g MySQL Logins)

                                        ...

                              ...

                   |-> Project 2

                   |-> Project 3

                   ...

        |-> Customer 2

        |-> Customer 3

- PW-Users (Workers)

         -> PW-User 1

         -> PW-User 2

         -> PW-User 3

           ...

Link to comment
Share on other sites

A single master key page (or storage location) is definitely involved, but it's not enough (you need to secure that key too, or you'll deliver the encrypted information in the database together with the key to decrypt it to the hypothetical attacker).

You could still use my approach from above, just instead of creating a fresh key from scratch, use what I suggested as a password recovery mechanism to populate the users' profiles with the master key encrypted using their password hashes (basically, leave out step 2a). Everything else like creating a temporary session key and storing the key for field decryption and encryption - just in this scenario the master key - session-encrypted stays.

So the steps would be:

  • Once: First step is again to create an asymmetric public/private key pair for admin, save the private key off-site (best print it out too and store it in the safe) and puts the public key into config.php (you could e.g. use openssl to generate that key pair)
  • Once: Admin creates a symmetric key (master key), encrypts it using the public key and stores it
  • Admin creates users and assigns their passwords (or changes the passwords for existing users)
  • In the same step, admin caclulates the user's password hash, decrypts the master key in config.php using his private key, then encrypts the master key with the user's password hash and stores it in a field in the user's profile

Now, steps 1 to 6 with the exclusion of step 2a from above once again work, only instead of separate keys, every user decrypts and encrypts their copy of the master key.

This should be a simple as it gets while you still avoid storing an unencrypted key at any point. There would be the theoretical possibility to use the admin's password hash to symmetrically encrypt and decrypt the master key in config, but that would mean there could be only one admin user (it would also introduce a predetermined breaking point when admin changes their password and forgets to update config.php with the re-encrypted key, which would result in garbage in the key copies assigned to users from this point on and, in the worst case, service information encrypted using such an invalid key).

This system still has one known weakness: if an attacker acquires a copy of the database, they only need to crack one password to decrypt all service credentials.

If you want to avoid that, things get exponentially more complicated because you would need to create a unique key for every service, store an admin-encrypted version of it somewhere and store (as well as update) a table of key <-> service mappings for every user. In turn, as reseting the user password every time access to a service is granted isn't practicable, you'd need a mechanism to encrypt the key for the user without knowing the user's password, leaving you with asymmetric encryption for that part. The user can't be asked to carry around and type in their private key though, so you need to store that private key for them encrypted using the user's password hash. It's another layer of indirection, and, as mentioned in my first reply, you'd need some kind of automated key infrastructure to make this easy to handle in code, and you'd also have to update two key fields and the whole mapping table with every password change (I'm not even going into the possible mayhem if password change by the user and service grant by the admin manage to overlap).

  • Like 2
Link to comment
Share on other sites

I made a new class to handle Keys and so on. You find it here

CI_Encrypt class extends the Keys class.

basic-crypt.php

    $crypter = new CI_Encrypt();

    $crypter->initAsymmetricKeyPair(); //Create the Asymmetric Key Pair
    $crypter->createPrivateKeyFile(); //Extract Private Key and save it as a File
    $crypter->createPublicKeyFile(); //Extract Public Key and save it as a File

    //Encrypt the Random Key with the Public Key
    $encryptedKey = $crypter->openssl_encrypt_key($crypter->createRandomKey(128), $config->publicKey);

    var_dump($crypter->createRandomKey(128));
    print_r("<br />");
    print_r("<br />");
    var_dump($config->publicKey);
    print_r("<br />");
    print_r("<br />");
    var_dump($encryptedKey);

    //Create a File with the encrypted Master Key to serve it for the config.php file
    $crypter->createEncryptedMasterKeyFile($encryptedKey);

    $outBody .= "<strong>Public Key(config.php):</strong> ".$config->publicKey."<br /><br />";
    $outBody .= "<strong>Master Key(config.php):</strong> ".$config->masterKey."<br /><hr />";

config.php

/**
 * Encryption/Decryption Settings
 *
 */

$config->publicKey = file_get_contents('./site/templates/publickey.txt', TRUE);
$config->masterKey = file_get_contents('./site/templates/masterkey.txt', TRUE);

I dont know why, but the openssl_encrypt_key() function which I wrote doesn't accept the createRandomKey() function which I also wrote. 

I always get a empty var_dump.

post-3125-0-40974700-1455610391_thumb.pn

post-3125-0-23570400-1455610390_thumb.pn

Link to comment
Share on other sites

@Nukro: I'm pretty sure that you need to use the openssl_pkey_get_[private|public] functions to read back a key from a PEM file to get the encrypt/decrypt functions to work.

It still gives me a empty var_dump when I change the config.php file:

From: $config->publicKey = file_get_contents('./site/templates/publickey.txt', TRUE);

To: $config->publicKey = openssl_pkey_get_public('file://./site/templates/publickey.pem');

post-3125-0-63452200-1455628787_thumb.pn

post-3125-0-38264600-1455628783_thumb.pn

I think it has to do something with the $crypter->createRandomKey(128) function. Because it encrypts when I type a simple string instead of $crypter->createRandomKey(128).

Link to comment
Share on other sites

You may be running into a key length issue if your asymetric key has less bits than the data (plus a small amount of padding that openssl adds to close some attack vectors) you try to encode. In your example you have 172 bytes of data, which is too long for a 1024 bit RSA key.

You can explicitly state the desired key length through the private_key_bits configarg when you generate a key pair with openssl_pkey_new.

  • Like 1
Link to comment
Share on other sites

You may be running into a key length issue if your asymetric key has less bits than the data (plus a small amount of padding that openssl adds to close some attack vectors) you try to encode. In your example you have 172 bytes of data, which is too long for a 1024 bit RSA key.

You can explicitly state the desired key length through the private_key_bits configarg when you generate a key pair with openssl_pkey_new.

Thank you BitPoet, you were right! I set the private_key_bits configarg to 2048 bits and it worked. I get a encrypted key which is 344 bytes long. 

I don't know how I could forget that.

172 bytes = 1376 bits

(1 byte(by eight :) ) = 8 bits)

Link to comment
Share on other sites

  • 4 weeks later...

Hi @BitPoet

Would it be possible to wrap up this whole process in a Fieldtype module? Something like "FieldtypeCrypt" which has a different configuration options like a button called "Created All Key Components" which would do this process:

1. Create a asymmetric Key pair

2. Exctract the private Key and save it as a PEM-File under site/modules/FieldtypeCrypt/Keys/privatekey.pem

3. Extract the public Key and save it as a PEM-File under site/modules/FieldtypeCrypt/Keys/publickey.pem

4. Create the masterkey and encrypt it with the publickey and save the result as file under /site/modules/FieldtypeCrypt/Keys/masterkey.pem

and so on...

It's the first time I will be make a Fieldtype module so I have to read me in, in that Topic. I've found this thread where Kongondo mentions some other helpful threads for creating fieldtype modules.

When I want to use the fieldtype in the frontend forms so I have to also make a Inputfield for that, am I right?

Greetings Nukro

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...