-
Notifications
You must be signed in to change notification settings - Fork 107
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
Conversation
93f13b8
to
dc6d25c
Compare
I will be away for ~1 week, will review this when I'm back. |
a075032
to
612bed2
Compare
/** | ||
* @var int $rounds The number of rounds to feed into PBKDF2 for key generation | ||
*/ | ||
protected $rounds = 100; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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',
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this 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"); |
There was a problem hiding this comment.
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', ''); |
There was a problem hiding this comment.
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' | ||
); | ||
|
There was a problem hiding this comment.
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')) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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!
Also would be neat to have a unit test on this EncryptService, it's a very critical point in the app! |
934f211
to
c9e614d
Compare
Great job there @brantje ! |
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.
Todo:
<lib>openssl</lib>
under<dependencies>