Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Server side encryption #160

Merged
merged 6 commits into from
Jan 11, 2017
Merged

Implement Server side encryption #160

merged 6 commits into from
Jan 11, 2017

Conversation

brantje
Copy link
Member

@brantje brantje commented Jan 2, 2017

This improves the general security of passman by encrypting the credentials again, but this time on the server side. It used AES-256 as cipher with CBC as method.

  • Key is stretched using the PBKDF2 algorithm.
  • Hides the Initialization Vector
  • Does HMAC verification of source data

Todo:

  • Add update script to convert passwords prior to RC4
  • Add <lib>openssl</lib> under <dependencies>

@animalillo
Copy link
Collaborator

I will be away for ~1 week, will review this when I'm back.

@brantje brantje force-pushed the SSE branch 14 times, most recently from a075032 to 612bed2 Compare January 9, 2017 10:47
/**
* @var int $rounds The number of rounds to feed into PBKDF2 for key generation
*/
protected $rounds = 100;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be a good idea to bump this up to 10'000 rounds in production.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think 10000 rounds might be too much for production, also remind this is just a feature that adds an extra layer of security in case somebody unauthorized got access to the database.
Remember this code will be executed on almost every api request on passman, we need to keep it fast!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you please experiment with different values to test performance? 100 appears to be far too low.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5000 seems nice in between.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apache .htpasswd "APR1" and OpenSSL "passwd" use 1000 rounds of MD5 key stretching.
I think we are good to go with 1000 then?

source: https://en.wikipedia.org/wiki/Key_stretching

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, since openssl is 'proven' then i guess 1000 or 1500 seems enough.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1000 rounds with 50 credentials takes about 2 seconds to load.
1000 seems too high.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've tested it with 1500 rounds and it takes ~2.5 seconds to load 50 credentials, most of that time spent only stretching the encryption keys, i think 100 rounds is more than enough, as we are using sha512 instead of md5 for password stretching.

If we are paranoid this could be set to 250 or so, but I don't think password stretching is even needed for this purpose, the encryption key is the one used by nextcloud to keep passwords safe, also user dependant data is used per credential in the encryption, increasing crack time and password length. Somebody would be more lucky hacking the server and extracting the master keys than bruteforcing the password salt.

If anything, the server secret could be appended to the passwordsalt as serverside encryption key, i think that wouldn't penalize the performance and would provide a lot harder to guess encryption key.

Example from a test config file:

'passwordsalt' => 'bscvTQyWylV0ep1czZLPw5zbbEe8IQ',
'secret' => 'YTdoJZJbwgI+X05q3KQXPgPoZc/k1yhSe8x6qDAB3vBHO2wd',

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, as long as you have experimented with different values, I believe it is reasonable to leave it as is. The only thing you might want to add is a comment referring to this discussion.

@@ -49,7 +49,7 @@ class CredentialRevision extends Entity implements \JsonSerializable {
protected $credentialId;
protected $userId;
protected $created;
protected $credentialData;
public $credentialData;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why public and not using it's getter and setter?

Copy link
Collaborator

@animalillo animalillo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brantje check out my comments on this and let's see if it can be improved! :D
It's mostly OK tho.

}
throw new DoesNotExistException("Did expect one result but found none when executing");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this exception removed?

) {
$this->sharingACL = $sharingACL;
$this->shareRequest = $shareRequest;
$this->credential = $credentials;
$this->revisions = $revisions;
$this->encryptService = $encryptService;
$this->server_key = \OC::$server->getConfig()->getSystemValue('passwordsalt', '');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed here

public $encrypted_credential_fields = array(
'description', 'username', 'password', 'files', 'custom_fields', 'otp', 'email', 'tags', 'url'
);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fields before this comment should be constant i think, shouldn't be changed on runtime.

*/
public function decrypt($data_hex, $key) {

if (!function_exists('hex2bin')) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unify polyfills in a single file and avoid them inside classes so we get more code consistency and avoid possible errors and repeating code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good point. We'll need to create a Polyfills class that we can import.
But that's kina out of the scope for this PR imo.

* @returns string The encrypted data
*/
public function encrypt($data, $key) {
$salt = openssl_random_pseudo_bytes(128);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for improved security check if random_bytes is defined and use it instead of openssl_random_pseudo_bytes because it uses system random number generator instead of pseudo random numbers

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (function_exist('random_bytes')){
$key = random_bytes(128);
} else{
$key = openssl_random_pseudo_bytes(128);
}

* @param $data
* @return bool|string
*/
protected function unpad($data) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never used and not needed, delete?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used on line 161 on EncryptService.php

* @throws \Exception
*/
private function handleCredential($credential, $op) {
$service_function = ($op === 'encrypt') ? 'encrypt' : 'decrypt';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of an if i would better create a constant static like OP_ENCRYPT = "encrypt"; and OP_DECRYPT = "decrypt"; and use them instead of text, would lead to less errors while maintaining the code, but this is just an opinion. this is perfectly fine, the if is still not needed imho.

* @throws \Exception
*/
private function handleFile($file, $op) {
$service_function = ($op === 'encrypt') ? 'encrypt' : 'decrypt';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as in handleCredential

$userKey = '';
$userSuppliedKey = '';
if ($file instanceof File) {
$userSuppliedKey = $file->getSize();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe use the file label or name instead of the size as usersuppliedkey and use size . mimetype on userkey

/**
* @var int $rounds The number of rounds to feed into PBKDF2 for key generation
*/
protected $rounds = 100;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think 10000 rounds might be too much for production, also remind this is just a feature that adds an extra layer of security in case somebody unauthorized got access to the database.
Remember this code will be executed on almost every api request on passman, we need to keep it fast!

@animalillo
Copy link
Collaborator

Also would be neat to have a unit test on this EncryptService, it's a very critical point in the app!

@animalillo animalillo merged commit 5f76616 into master Jan 11, 2017
@animalillo
Copy link
Collaborator

Great job there @brantje !

@brantje brantje deleted the SSE branch January 29, 2017 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants