2017-01-02 20:09:03 +08:00
< ? php
/**
* Nextcloud - passman
*
* @ copyright Copyright ( c ) 2016 , Sander Brand ( brantje @ gmail . com )
* @ copyright Copyright ( c ) 2016 , Marcos Zuriaga Miguel ( wolfi @ wolfi . es )
* @ license GNU AGPL version 3 or any later version
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < http :// www . gnu . org / licenses />.
*
*/
namespace OCA\Passman\Service ;
// Class copied from http://stackoverflow.com/questions/5089841/two-way-encryption-i-need-to-store-passwords-that-can-be-retrieved?answertab=votes#tab-top
// Upgraded to use openssl
2017-01-02 22:25:41 +08:00
use Icewind\SMB\Exception\Exception ;
use OCA\Passman\Db\Credential ;
use OCA\Passman\Db\File ;
2017-01-12 01:09:10 +08:00
/**
* A class to handle secure encryption and decryption of arbitrary data
*
* Note that this is not just straight encryption . It also has a few other
* features in it to make the encrypted data far more secure . Note that any
* other implementations used to decrypt data will have to do the same exact
* operations .
*
* Security Benefits :
*
* - Uses Key stretching
* - Hides the Initialization Vector
* - Does HMAC verification of source data
*
*/
2017-01-02 20:09:03 +08:00
class EncryptService {
/**
* Supported cipher algorithms accompanied by their key / block sizes in bytes
*
* OpenSSL has no equivalent of mcrypt_get_key_size () and mcrypt_get_block_size () hence sizes stored here .
*
* @ var array
*/
2017-01-12 01:09:10 +08:00
const SUPPORTED_ALGORITHMS = array (
2017-01-02 22:25:41 +08:00
'aes-256-cbc' => array ( 'name' => 'AES-256' , 'keySize' => 32 , 'blockSize' => 32 ),
2017-01-02 20:09:03 +08:00
'bf' => array ( 'name' => 'BF' , 'keySize' => 16 , 'blockSize' => 8 ),
'des' => array ( 'name' => 'DES' , 'keySize' => 7 , 'blockSize' => 8 ),
'des-ede3' => array ( 'name' => 'DES-EDE3' , 'keySize' => 21 , 'blockSize' => 8 ), // 3 different 56-bit keys
'cast5' => array ( 'name' => 'CAST5' , 'keySize' => 16 , 'blockSize' => 8 ),
);
2017-01-12 01:09:10 +08:00
const OP_ENCRYPT = 'encrypt' ;
const OP_DECRYPT = 'decrypt' ;
2017-01-02 20:09:03 +08:00
2017-01-12 01:09:10 +08:00
// The fields of a credential which are encrypted
2017-01-02 22:25:41 +08:00
public $encrypted_credential_fields = array (
2017-08-13 20:23:23 +08:00
'description' , 'username' , 'password' , 'files' , 'custom_fields' , 'otp' , 'email' , 'tags' , 'url' , 'icon'
2017-01-02 22:25:41 +08:00
);
2017-01-12 01:09:10 +08:00
// Contains the server key
2017-01-02 22:25:41 +08:00
private $server_key ;
2017-01-02 20:09:03 +08:00
/**
2017-01-02 22:25:41 +08:00
* @ var string $cipher The openssl cipher to use for this instance
2017-01-02 20:09:03 +08:00
*/
protected $cipher = '' ;
/**
* @ var int $rounds The number of rounds to feed into PBKDF2 for key generation
*/
protected $rounds = 100 ;
/**
* Constructor !
*
2017-01-02 22:25:41 +08:00
* @ param SettingsService $settings
2017-01-02 20:09:03 +08:00
*/
2017-01-02 22:25:41 +08:00
public function __construct ( SettingsService $settings ) {
2017-01-12 01:09:10 +08:00
$this -> cipher = $settings -> getAppSetting ( 'server_side_encryption' , 'aes-256-cbc' );
$password_salt = \OC :: $server -> getConfig () -> getSystemValue ( 'passwordsalt' , '' );
$secret = \OC :: $server -> getConfig () -> getSystemValue ( 'secret' , '' );
$this -> server_key = $password_salt . $secret ;
$this -> rounds = $settings -> getAppSetting ( 'rounds_pbkdf2_stretching' , 100 );
2017-01-02 22:25:41 +08:00
}
/**
* Create an encryption key . Based on given parameters
*
* @ param string $userKey The user key to use . This should be specific to this user .
* @ param string $serverKey The server key
* @ param string $userSuppliedKey A key from the credential ( eg guid , name or tags )
* @ return string
*/
public static function makeKey ( $userKey , $serverKey , $userSuppliedKey ) {
$key = hash_hmac ( 'sha512' , $userKey , $serverKey );
$key = hash_hmac ( 'sha512' , $key , $userSuppliedKey );
return $key ;
2017-01-02 20:09:03 +08:00
}
/**
* Get the maximum key size for the selected cipher and mode of operation
*
* @ return int Value is in bytes
*/
public function getKeySize () {
2017-01-12 01:09:10 +08:00
return EncryptService :: SUPPORTED_ALGORITHMS [ $this -> cipher ][ 'keySize' ];
2017-01-02 20:09:03 +08:00
}
/**
* Decrypt the data with the provided key
*
* @ param string $data_hex The encrypted datat to decrypt
* @ param string $key The key to use for decryption
*
* @ returns string | false The returned string if decryption is successful
* false if it is not
*/
public function decrypt ( $data_hex , $key ) {
if ( ! function_exists ( 'hex2bin' )) {
function hex2bin ( $str ) {
$sbin = " " ;
$len = strlen ( $str );
for ( $i = 0 ; $i < $len ; $i += 2 ) {
$sbin .= pack ( " H* " , substr ( $str , $i , 2 ));
}
return $sbin ;
}
}
$data = hex2bin ( $data_hex );
$salt = substr ( $data , 0 , 128 );
$enc = substr ( $data , 128 , - 64 );
$mac = substr ( $data , - 64 );
list ( $cipherKey , $macKey , $iv ) = $this -> getKeys ( $salt , $key );
2017-01-12 01:09:10 +08:00
if ( ! $this -> hash_equals ( hash_hmac ( 'sha512' , $enc , $macKey , true ), $mac )) {
2017-01-02 20:09:03 +08:00
return false ;
}
2017-01-02 22:25:41 +08:00
$dec = openssl_decrypt ( $enc , $this -> cipher , $cipherKey , true , $iv );
2017-01-02 20:09:03 +08:00
$data = $this -> unpad ( $dec );
return $data ;
}
/**
* Encrypt the supplied data using the supplied key
*
* @ param string $data The data to encrypt
* @ param string $key The key to encrypt with
*
* @ returns string The encrypted data
*/
public function encrypt ( $data , $key ) {
2017-01-12 01:09:10 +08:00
if ( function_exists ( 'random_bytes' )) {
$salt = random_bytes ( 128 );
} else {
$salt = openssl_random_pseudo_bytes ( 128 );
}
list ( $cipherKey , $macKey , $iv ) = $this -> getKeys ( $salt , $key );
$data = $this -> pad ( $data );
2017-01-02 22:25:41 +08:00
$enc = openssl_encrypt ( $data , $this -> cipher , $cipherKey , true , $iv );
2017-01-02 20:09:03 +08:00
$mac = hash_hmac ( 'sha512' , $enc , $macKey , true );
$data = bin2hex ( $salt . $enc . $mac );
return $data ;
}
/**
* Generates a set of keys given a random salt and a master key
*
* @ param string $salt A random string to change the keys each encryption
* @ param string $key The supplied key to encrypt with
*
* @ returns array An array of keys ( a cipher key , a mac key , and a IV )
*/
protected function getKeys ( $salt , $key ) {
2017-01-02 22:25:41 +08:00
$ivSize = openssl_cipher_iv_length ( $this -> cipher );
$keySize = openssl_cipher_iv_length ( $this -> cipher );
2017-01-02 20:09:03 +08:00
$length = 2 * $keySize + $ivSize ;
2017-01-12 01:09:10 +08:00
$key = $this -> pbkdf2 ( 'sha512' , $key , $salt , $this -> rounds , $length );
2017-01-02 20:09:03 +08:00
$cipherKey = substr ( $key , 0 , $keySize );
$macKey = substr ( $key , $keySize , $keySize );
$iv = substr ( $key , 2 * $keySize );
return array ( $cipherKey , $macKey , $iv );
}
2017-01-02 22:25:41 +08:00
protected function hash_equals ( $a , $b ) {
2017-01-12 01:09:10 +08:00
if ( function_exists ( 'random_bytes' )) {
$key = random_bytes ( 128 );
} else {
$key = openssl_random_pseudo_bytes ( 128 );
}
2017-01-02 20:09:03 +08:00
return hash_hmac ( 'sha512' , $a , $key ) === hash_hmac ( 'sha512' , $b , $key );
}
/**
* Stretch the key using the PBKDF2 algorithm
*
* @ see http :// en . wikipedia . org / wiki / PBKDF2
*
* @ param string $algo The algorithm to use
* @ param string $key The key to stretch
* @ param string $salt A random salt
* @ param int $rounds The number of rounds to derive
* @ param int $length The length of the output key
*
* @ returns string The derived key .
*/
protected function pbkdf2 ( $algo , $key , $salt , $rounds , $length ) {
$size = strlen ( hash ( $algo , '' , true ));
$len = ceil ( $length / $size );
$result = '' ;
for ( $i = 1 ; $i <= $len ; $i ++ ) {
$tmp = hash_hmac ( $algo , $salt . pack ( 'N' , $i ), $key , true );
$res = $tmp ;
for ( $j = 1 ; $j < $rounds ; $j ++ ) {
$tmp = hash_hmac ( $algo , $tmp , $key , true );
$res ^= $tmp ;
}
$result .= $res ;
}
return substr ( $result , 0 , $length );
}
/**
* Pad the data with a random char chosen by the pad amount .
*
* @ param $data
* @ return string
*/
protected function pad ( $data ) {
$length = $this -> getKeySize ();
$padAmount = $length - strlen ( $data ) % $length ;
2017-01-02 22:25:41 +08:00
if ( $padAmount === 0 ) {
2017-01-02 20:09:03 +08:00
$padAmount = $length ;
}
return $data . str_repeat ( chr ( $padAmount ), $padAmount );
}
/**
* Unpad the the data
*
* @ param $data
* @ return bool | string
*/
protected function unpad ( $data ) {
$length = $this -> getKeySize ();
$last = ord ( $data [ strlen ( $data ) - 1 ]);
if ( $last > $length ) return false ;
if ( substr ( $data , - 1 * $last ) !== str_repeat ( chr ( $last ), $last )) {
return false ;
}
return substr ( $data , 0 , - 1 * $last );
}
2017-01-02 22:25:41 +08:00
/**
* Encrypt a credential
*
* @ param Credential | array $credential the credential to decrypt
* @ return Credential | array
*/
public function decryptCredential ( $credential ) {
2017-01-12 01:09:10 +08:00
return $this -> handleCredential ( $credential , EncryptService :: OP_DECRYPT );
2017-01-02 22:25:41 +08:00
}
/**
* Encrypt a credential
*
* @ param Credential | array $credential the credential to encrypt
* @ return Credential | array
* @ throws \Exception
*/
public function encryptCredential ( $credential ) {
2017-01-12 01:09:10 +08:00
return $this -> handleCredential ( $credential , EncryptService :: OP_ENCRYPT );
2017-01-02 22:25:41 +08:00
}
2017-01-03 05:06:55 +08:00
2017-01-12 01:09:10 +08:00
private function extractKeysFromCredential ( $credential ) {
2017-01-03 05:06:55 +08:00
$userKey = '' ;
$userSuppliedKey = '' ;
if ( $credential instanceof Credential ) {
$userSuppliedKey = $credential -> getLabel ();
$sk = $credential -> getSharedKey ();
$userKey = ( isset ( $sk )) ? $sk : $credential -> getUserId ();
}
if ( is_array ( $credential )) {
$userSuppliedKey = $credential [ 'label' ];
$userKey = ( isset ( $credential [ 'shared_key' ])) ? $credential [ 'shared_key' ] : $credential [ 'user_id' ];
}
return array ( $userKey , $userSuppliedKey );
}
2017-01-02 22:25:41 +08:00
/**
* Handles the encryption / decryption of a credential
*
* @ param Credential | array $credential the credential to encrypt
* @ return Credential | array
* @ throws \Exception
*/
2017-01-12 01:09:10 +08:00
private function handleCredential ( $credential , $service_function ) {
2017-01-03 05:06:55 +08:00
list ( $userKey , $userSuppliedKey ) = $this -> extractKeysFromCredential ( $credential );
2017-01-12 01:09:10 +08:00
$key = $this -> makeKey ( $userKey , $this -> server_key , $userSuppliedKey );
2017-01-02 22:25:41 +08:00
foreach ( $this -> encrypted_credential_fields as $field ) {
if ( $credential instanceof Credential ) {
$field = str_replace ( ' ' , '' , str_replace ( '_' , ' ' , ucwords ( $field , '_' )));
$set = 'set' . $field ;
$get = 'get' . $field ;
$credential -> { $set }( $this -> { $service_function }( $credential -> { $get }(), $key ));
2017-01-03 05:06:55 +08:00
}
if ( is_array ( $credential )) {
2017-01-02 22:25:41 +08:00
$credential [ $field ] = $this -> { $service_function }( $credential [ $field ], $key );
}
}
return $credential ;
}
/**
* Encrypt a file
*
* @ param File | array $file
* @ return File | array
*/
public function encryptFile ( $file ) {
2017-01-12 01:09:10 +08:00
return $this -> handleFile ( $file , EncryptService :: OP_ENCRYPT );
2017-01-02 22:25:41 +08:00
}
/**
* Decrypt a file
*
* @ param File | array $file
* @ return File | array
*/
public function decryptFile ( $file ) {
2017-01-12 01:09:10 +08:00
return $this -> handleFile ( $file , EncryptService :: OP_DECRYPT );
2017-01-02 22:25:41 +08:00
}
/**
* Handles the encryption / decryption of a File
*
* @ param File | array $file the credential to encrypt
* @ return File | array
* @ throws \Exception
*/
2017-01-12 01:09:10 +08:00
private function handleFile ( $file , $service_function ) {
2017-01-03 05:06:55 +08:00
$userKey = '' ;
$userSuppliedKey = '' ;
2017-01-02 22:25:41 +08:00
if ( $file instanceof File ) {
2017-01-12 03:43:52 +08:00
$userSuppliedKey = $file -> getSize ();
2017-01-02 22:25:41 +08:00
$userKey = md5 ( $file -> getMimetype ());
2017-01-03 05:06:55 +08:00
}
if ( is_array ( $file )) {
2017-01-02 22:25:41 +08:00
$userSuppliedKey = $file [ 'size' ];
$userKey = md5 ( $file [ 'mimetype' ]);
}
2017-01-12 01:09:10 +08:00
$key = $this -> makeKey ( $userKey , $this -> server_key , $userSuppliedKey );
2017-01-02 22:25:41 +08:00
if ( $file instanceof File ) {
$file -> setFilename ( $this -> { $service_function }( $file -> getFilename (), $key ));
$file -> setFileData ( $this -> { $service_function }( $file -> getFileData (), $key ));
2017-01-03 05:06:55 +08:00
}
if ( is_array ( $file )) {
2017-01-02 22:25:41 +08:00
$file [ 'filename' ] = $this -> { $service_function }( $file [ 'filename' ], $key );
$file [ 'file_data' ] = $this -> { $service_function }( $file [ 'file_data' ], $key );
}
2017-01-03 05:06:55 +08:00
2017-01-02 22:25:41 +08:00
return $file ;
}
2017-01-02 20:09:03 +08:00
}