. * */ namespace OCA\Encryption; //require_once '../3rdparty/Crypt_Blowfish/Blowfish.php'; require_once realpath(dirname(__FILE__) . '/../3rdparty/Crypt_Blowfish/Blowfish.php'); /** * Class for common cryptography functionality */ class Crypt { /** * @brief return encryption mode client or server side encryption * @param string $user name (use system wide setting if name=null) * @return string 'client' or 'server' */ public static function mode($user = null) { return 'server'; } /** * @brief Create a new encryption keypair * @return array publicKey, privatekey */ public static function createKeypair() { $res = openssl_pkey_new(array('private_key_bits' => 4096)); // Get private key openssl_pkey_export($res, $privateKey); // Get public key $publicKey = openssl_pkey_get_details($res); $publicKey = $publicKey['key']; return (array( 'publicKey' => $publicKey, 'privateKey' => $privateKey )); } /** * @brief Add arbitrary padding to encrypted data * @param string $data data to be padded * @return string padded data * @note In order to end up with data exactly 8192 bytes long we must * add two letters. It is impossible to achieve exactly 8192 length * blocks with encryption alone, hence padding is added to achieve the * required length. */ public static function addPadding($data) { $padded = $data . 'xx'; return $padded; } /** * @brief Remove arbitrary padding to encrypted data * @param string $padded padded data to remove padding from * @return string unpadded data on success, false on error */ public static function removePadding($padded) { if (substr($padded, -2) === 'xx') { $data = substr($padded, 0, -2); return $data; } else { // TODO: log the fact that unpadded data was submitted for removal of padding return false; } } /** * @brief Check if a file's contents contains an IV and is symmetrically encrypted * @param $content * @return boolean * @note see also OCA\Encryption\Util->isEncryptedPath() */ public static function isCatfileContent($content) { if (!$content) { return false; } $noPadding = self::removePadding($content); // Fetch encryption metadata from end of file $meta = substr($noPadding, -22); // Fetch IV from end of file $iv = substr($meta, -16); // Fetch identifier from start of metadata $identifier = substr($meta, 0, 6); if ($identifier === '00iv00') { return true; } else { return false; } } /** * Check if a file is encrypted according to database file cache * @param string $path * @return bool */ public static function isEncryptedMeta($path) { // TODO: Use DI to get \OC\Files\Filesystem out of here // Fetch all file metadata from DB $metadata = \OC\Files\Filesystem::getFileInfo($path); // Return encryption status return isset($metadata['encrypted']) && ( bool )$metadata['encrypted']; } /** * @brief Check if a file is encrypted via legacy system * @param $data * @param string $relPath The path of the file, relative to user/data; * e.g. filename or /Docs/filename, NOT admin/files/filename * @return boolean */ public static function isLegacyEncryptedContent($data, $relPath) { // Fetch all file metadata from DB $metadata = \OC\Files\Filesystem::getFileInfo($relPath, ''); // If a file is flagged with encryption in DB, but isn't a // valid content + IV combination, it's probably using the // legacy encryption system if (isset($metadata['encrypted']) && $metadata['encrypted'] === true && !self::isCatfileContent($data) ) { return true; } else { return false; } } /** * @brief Symmetrically encrypt a string * @param $plainContent * @param $iv * @param string $passphrase * @return string encrypted file content */ public static function encrypt($plainContent, $iv, $passphrase = '') { if ($encryptedContent = openssl_encrypt($plainContent, 'AES-128-CFB', $passphrase, false, $iv)) { return $encryptedContent; } else { \OC_Log::write('Encryption library', 'Encryption (symmetric) of content failed', \OC_Log::ERROR); return false; } } /** * @brief Symmetrically decrypt a string * @param $encryptedContent * @param $iv * @param $passphrase * @throws \Exception * @return string decrypted file content */ public static function decrypt($encryptedContent, $iv, $passphrase) { if ($plainContent = openssl_decrypt($encryptedContent, 'AES-128-CFB', $passphrase, false, $iv)) { return $plainContent; } else { throw new \Exception('Encryption library: Decryption (symmetric) of content failed'); } } /** * @brief Concatenate encrypted data with its IV and padding * @param string $content content to be concatenated * @param string $iv IV to be concatenated * @returns string concatenated content */ public static function concatIv($content, $iv) { $combined = $content . '00iv00' . $iv; return $combined; } /** * @brief Split concatenated data and IV into respective parts * @param string $catFile concatenated data to be split * @returns array keys: encrypted, iv */ public static function splitIv($catFile) { // Fetch encryption metadata from end of file $meta = substr($catFile, -22); // Fetch IV from end of file $iv = substr($meta, -16); // Remove IV and IV identifier text to expose encrypted content $encrypted = substr($catFile, 0, -22); $split = array( 'encrypted' => $encrypted, 'iv' => $iv ); return $split; } /** * @brief Symmetrically encrypts a string and returns keyfile content * @param string $plainContent content to be encrypted in keyfile * @param string $passphrase * @return bool|string * @return string encrypted content combined with IV * @note IV need not be specified, as it will be stored in the returned keyfile * and remain accessible therein. */ public static function symmetricEncryptFileContent($plainContent, $passphrase = '') { if (!$plainContent) { return false; } $iv = self::generateIv(); if ($encryptedContent = self::encrypt($plainContent, $iv, $passphrase)) { // Combine content to encrypt with IV identifier and actual IV $catfile = self::concatIv($encryptedContent, $iv); $padded = self::addPadding($catfile); return $padded; } else { \OC_Log::write('Encryption library', 'Encryption (symmetric) of keyfile content failed', \OC_Log::ERROR); return false; } } /** * @brief Symmetrically decrypts keyfile content * @param $keyfileContent * @param string $passphrase * @throws \Exception * @return bool|string * @internal param string $source * @internal param string $target * @internal param string $key the decryption key * @returns string decrypted content * * This function decrypts a file */ public static function symmetricDecryptFileContent($keyfileContent, $passphrase = '') { if (!$keyfileContent) { throw new \Exception('Encryption library: no data provided for decryption'); } // Remove padding $noPadding = self::removePadding($keyfileContent); // Split into enc data and catfile $catfile = self::splitIv($noPadding); if ($plainContent = self::decrypt($catfile['encrypted'], $catfile['iv'], $passphrase)) { return $plainContent; } else { return false; } } /** * @brief Creates symmetric keyfile content using a generated key * @param string $plainContent content to be encrypted * @returns array keys: key, encrypted * @note symmetricDecryptFileContent() can be used to decrypt files created using this method * * This function decrypts a file */ public static function symmetricEncryptFileContentKeyfile($plainContent) { $key = self::generateKey(); if ($encryptedContent = self::symmetricEncryptFileContent($plainContent, $key)) { return array( 'key' => $key, 'encrypted' => $encryptedContent ); } else { return false; } } /** * @brief Create asymmetrically encrypted keyfile content using a generated key * @param string $plainContent content to be encrypted * @param array $publicKeys array keys must be the userId of corresponding user * @returns array keys: keys (array, key = userId), data * @note symmetricDecryptFileContent() can decrypt files created using this method */ public static function multiKeyEncrypt($plainContent, array $publicKeys) { // openssl_seal returns false without errors if $plainContent // is empty, so trigger our own error if (empty($plainContent)) { throw new \Exception('Cannot mutliKeyEncrypt empty plain content'); } // Set empty vars to be set by openssl by reference $sealed = ''; $shareKeys = array(); $mappedShareKeys = array(); if (openssl_seal($plainContent, $sealed, $shareKeys, $publicKeys)) { $i = 0; // Ensure each shareKey is labelled with its // corresponding userId foreach ($publicKeys as $userId => $publicKey) { $mappedShareKeys[$userId] = $shareKeys[$i]; $i++; } return array( 'keys' => $mappedShareKeys, 'data' => $sealed ); } else { return false; } } /** * @brief Asymmetrically encrypt a file using multiple public keys * @param $encryptedContent * @param $shareKey * @param $privateKey * @return bool * @internal param string $plainContent content to be encrypted * @returns string $plainContent decrypted string * @note symmetricDecryptFileContent() can be used to decrypt files created using this method * * This function decrypts a file */ public static function multiKeyDecrypt($encryptedContent, $shareKey, $privateKey) { if (!$encryptedContent) { return false; } if (openssl_open($encryptedContent, $plainContent, $shareKey, $privateKey)) { return $plainContent; } else { \OC_Log::write('Encryption library', 'Decryption (asymmetric) of sealed content failed', \OC_Log::ERROR); return false; } } /** * @brief Asymetrically encrypt a string using a public key * @param $plainContent * @param $publicKey * @return string encrypted file */ public static function keyEncrypt($plainContent, $publicKey) { openssl_public_encrypt($plainContent, $encryptedContent, $publicKey); return $encryptedContent; } /** * @brief Asymetrically decrypt a file using a private key * @param $encryptedContent * @param $privatekey * @return string decrypted file */ public static function keyDecrypt($encryptedContent, $privatekey) { $result = @openssl_private_decrypt($encryptedContent, $plainContent, $privatekey); if ($result) { return $plainContent; } return $result; } /** * @brief Generates a pseudo random initialisation vector * @return String $iv generated IV */ public static function generateIv() { if ($random = openssl_random_pseudo_bytes(12, $strong)) { if (!$strong) { // If OpenSSL indicates randomness is insecure, log error \OC_Log::write('Encryption library', 'Insecure symmetric key was generated using openssl_random_pseudo_bytes()', \OC_Log::WARN); } // We encode the iv purely for string manipulation // purposes - it gets decoded before use $iv = base64_encode($random); return $iv; } else { throw new \Exception('Generating IV failed'); } } /** * @brief Generate a pseudo random 1024kb ASCII key * @returns $key Generated key */ public static function generateKey() { // Generate key if ($key = base64_encode(openssl_random_pseudo_bytes(183, $strong))) { if (!$strong) { // If OpenSSL indicates randomness is insecure, log error throw new \Exception('Encryption library, Insecure symmetric key was generated using openssl_random_pseudo_bytes()'); } return $key; } else { return false; } } /** * @brief Get the blowfish encryption handeler for a key * @param $key string (optional) * @return \Crypt_Blowfish blowfish object * * if the key is left out, the default handeler will be used */ public static function getBlowfish($key = '') { if ($key) { return new \Crypt_Blowfish($key); } else { return false; } } /** * @param $passphrase * @return mixed */ public static function legacyCreateKey($passphrase) { // Generate a random integer $key = mt_rand(10000, 99999) . mt_rand(10000, 99999) . mt_rand(10000, 99999) . mt_rand(10000, 99999); // Encrypt the key with the passphrase $legacyEncKey = self::legacyEncrypt($key, $passphrase); return $legacyEncKey; } /** * @brief encrypts content using legacy blowfish system * @param string $content the cleartext message you want to encrypt * @param string $passphrase * @returns string encrypted content * * This function encrypts an content */ public static function legacyEncrypt($content, $passphrase = '') { $bf = self::getBlowfish($passphrase); return $bf->encrypt($content); } /** * @brief decrypts content using legacy blowfish system * @param string $content the cleartext message you want to decrypt * @param string $passphrase * @return string cleartext content * * This function decrypts an content */ private static function legacyDecrypt($content, $passphrase = '') { $bf = self::getBlowfish($passphrase); $decrypted = $bf->decrypt($content); return $decrypted; } /** * @param $data * @param string $key * @param int $maxLength * @return string */ public static function legacyBlockDecrypt($data, $key = '', $maxLength = 0) { $result = ''; while (strlen($data)) { $result .= self::legacyDecrypt(substr($data, 0, 8192), $key); $data = substr($data, 8192); } if ($maxLength > 0) { return substr($result, 0, $maxLength); } else { return rtrim($result, "\0"); } } /** * @param $legacyEncryptedContent * @param $legacyPassphrase * @param $publicKeys * @return array */ public static function legacyKeyRecryptKeyfile($legacyEncryptedContent, $legacyPassphrase, $publicKeys) { $decrypted = self::legacyBlockDecrypt($legacyEncryptedContent, $legacyPassphrase); // Encrypt plain data, generate keyfile & encrypted file $cryptedData = self::symmetricEncryptFileContentKeyfile($decrypted); // Encrypt plain keyfile to multiple sharefiles $multiEncrypted = Crypt::multiKeyEncrypt($cryptedData['key'], $publicKeys); return array( 'data' => $cryptedData['encrypted'], 'filekey' => $multiEncrypted['data'], 'sharekeys' => $multiEncrypted['keys'] ); } }