'dp.test' // OpenSSL attributes private $hasOpenSSL = false; private $cryptMethod = 'aes-256-ctr'; private $key; // crypt key private $prefprefix = 'ds'; // prefix for user pref keys: 'test' => 'ds.test' // Sodium attributes private $hasSodium = false; // // Init and release //////////////////////////////// public function __construct() { parent::__construct(); } public function __destruct() { $this->release(); } public function init() { if (! isset($_SESSION['cryptphrase'])) { throw new Exception(tra('Unable to locate cryptphrase')); } $phraseMD5 = $_SESSION['cryptphrase']; $this->initSeed($phraseMD5); } public function initSeed($phraseMD5) { if (extension_loaded('sodium')) { $this->hasSodium = true; $this->prefprefix = 'du'; $this->key = $phraseMD5; } if (extension_loaded('openssl')) { $this->hasOpenSSL = true; $this->key = $phraseMD5; } if (extension_loaded('mcrypt') && $this->mcrypt == null) { $this->mcrypt_key = $phraseMD5; // Using Rijndael 256 in CBC mode. $this->mcrypt = mcrypt_module_open(MCRYPT_RIJNDAEL_256, '', 'cbc', ''); } } public function makeCryptPhrase($username, $cleartextPwd) { return md5($username . $cleartextPwd); } public function release() { if ($this->hasOpenSSL) { $this->key = null; } if ($this->mcrypt != null) { mcrypt_module_close($this->mcrypt); $this->mcrypt_key = null; $this->mcrypt = null; } } public function generateKey($algo) { if (extension_loaded('sodium')) { return random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); } if (extension_loaded('openssl')) { $keylen = openssl_cipher_iv_length($algo); return openssl_random_pseudo_bytes($keylen); } throw new Exception(tra('No encryption extension found.')); } public function isSupported() { return extension_loaded('sodium') || extension_loaded('openssl'); } public function algorithms() { if (extension_loaded('sodium')) { return []; } if (extension_loaded('openssl')) { return openssl_get_cipher_methods(); } return []; } // // Test/Check utilities //////////////////////////////// /** * Check if Sodium encryption is used * * @return bool */ public function hasSodiumCrypt() { return $this->hasSodium; } // Check if encryption is used (and not Base64) public function hasCrypt() { return $this->hasOpenSSL; } // Check if MCrypt module is available in case a conversion is needed public function hasMCrypt() { return $this->mcrypt != null; } // Check if any data exists the user preference. // Return true if data exit (not necessarily readable). false, if no stored data is found public function hasUserData($userprefKey, $paramName = '') { global $user; if (! empty($paramName)) { $paramName = '.' . $paramName; } $storedPwd64 = $this->get_user_preference($user, $this->prefprefix . '.' . $userprefKey . $paramName); if (empty($storedPwd64)) { // Check if old, mcrypt encrypted data exist // Decryption is done when getting data $storedPwdMCrypt = $this->get_user_preference($user, $this->mcrypt_prefprefix . '.' . $userprefKey . $paramName); if (empty($storedPwdMCrypt)) { return false; } } return true; } /** * Get the number of rows associated with the specified cryptographic method * @param string $method "mcrypt" for MCrypt, or "openssl" for OpenSSL, or "sodium" for Sodium * @return int Number of rows */ public function getUserCryptDataStats($method) { if ($method == 'mcrypt') { $pattern = 'dp.%'; } elseif ($method == 'openssl') { $pattern = 'ds.%'; } elseif ($method == 'sodium') { $pattern = 'du.%'; } else { throw new DomainException('Invalid method'); } return $this->getOne('SELECT count(*) as Nr FROM `tiki_user_preferences` WHERE `prefName` like \'' . $pattern . "'"); } // // User data utilities //////////////////////////////// /* * Encrypt and save the data in the user preferences. * The class specified prefix will be applied to the pref key. * So, is the paramName, if specified. Given... * $prefprefix = 'pwddom'; * $userprefKey = 'test' * $paramName = '' * => pwddom.test * if $paramName = 'user', then * * => pwddom.test.user */ // // Return false on failure otherwise the generated crypt text public function setUserData($userprefKey, $cleartext, $paramName = '') { global $user; if (empty($cleartext)) { return false; } $storedPwd64 = $this->encryptData($cleartext); if (! empty($paramName)) { $paramName = '.' . $paramName; } $this->set_user_preference($user, $this->prefprefix . '.' . $userprefKey . $paramName, $storedPwd64); return $storedPwd64; } /* * Encrypt and save the data in the user preferences for the specified user. * The class specified prefix will be applied to the pref key. * So, is the paramName, if specified. Given... * $username = 'myuser' * $prefprefix = 'pwddom'; * $userprefKey = 'test' * $paramName = '' * => pwddom.test * if $paramName = 'user', then * * => pwddom.test.user */ // // Return false on failure otherwise the generated crypt text public function putUserData($username, $userprefKey, $cleartext, $paramName = '') { if (empty($cleartext)) { return false; } $storedPwd64 = $this->encryptData($cleartext); if (! empty($paramName)) { $paramName = '.' . $paramName; } $this->set_user_preference($username, $this->prefprefix . '.' . $userprefKey . $paramName, $storedPwd64); return $storedPwd64; } // Get the data from the user preferences. // Decrypt and return cleartext // Return false, if no stored data is found public function getUserData($userprefKey, $paramName = '') { global $user; if (! empty($paramName)) { $paramName = '.' . $paramName; } $storedPwd64 = $this->get_user_preference($user, $this->prefprefix . '.' . $userprefKey . $paramName); if (empty($storedPwd64)) { return false; } else { $cleartext = $this->decryptData($storedPwd64); } // Check if the cleartext contain any illigal password character. // If found, it indicates that the decryption has failed. if (! ctype_print($cleartext)) { return false; } return $cleartext; } // Recover the stored cleartext data from the user preferences. // Return stored data in cleartext or false on error /* * WARNING: Not converted to OpenSSL function recoverUserData($username, $cleartextPwd, $userprefKey, $paramName = '') { if (empty($cleartextPwd)) { return false; } // Initialize using the input params $cryptlib = new CryptLib(); $phraseMD5 = md5($username.$cleartextPwd); $cryptlib->initSeed($phraseMD5); // Build the pref key if (!empty($paramName)) { $paramName = '.'.$paramName; } $prefKey = $cryptlib->prefprefix.'.'.$userprefKey.$paramName; // Get the stored data $storedPwd64 = $cryptlib->get_user_preference($username, $prefKey); if (empty($storedPwd64)) { return false; } // Decrypt $cleartext = $cryptlib->decryptData($storedPwd64); // Check if the cleartext contain any illigal password character. // If found, it indicates that the decryption has failed. if (!ctype_print ($cleartext)) { return false; } return $cleartext; } */ public function getPasswordDomains($use_prefix = false) { global $prefs; // Load the domain ddefinitions $domainsText = $prefs['feature_password_domains']; $domains = explode(',', $domainsText); // Trim whitespace from names foreach ($domains as &$dom) { $dom = trim($dom); } // Add prefix if ($use_prefix) { foreach ($domains as &$dom) { $dom = $this->prefprefix . '.' . $dom; } } return $domains; } // // Data encryption //////////////////////////////// // Encrypt data // Return encrypted data, or false on error public function encryptData($cleartextData) { if (empty($cleartextData)) { return false; } // Due to appending spaces to short input data, short cleartext data cannot end with space $pwdLen = mb_strlen($cleartextData); if ($pwdLen < 20 && $cleartextData[$pwdLen] == ' ') { throw new Exception('Data to encrypt cannot end with a space'); } // Make sure the data is at least 20 characters long // The spaces are trimmed when decrypting while (mb_strlen($cleartextData) < 20) { $cleartextData .= ' '; } // Encrypt the data $cryptData = $this->encrypt($cleartextData); if (empty($cryptData)) { return false; } // Save iv in the stored data $cryptData64 = base64_encode($cryptData); return $cryptData64; } // Decrypt data // Return cleartext data, or false on error public function decryptData($cryptData64) { if (empty($cryptData64)) { return false; } // Extract the iv and crypttext $cryptData = base64_decode($cryptData64); // Decrypt $cleartext = $this->decrypt($cryptData); return rtrim($cleartext); } // // Tiki events //////////////////////////////// // User has logged in public function onUserLogin($cleartextPwd) { global $user; // Encode the phrase $phraseMD5 = $this->makeCryptPhrase($user, $cleartextPwd); // Store the pass phrase in a session variable $_SESSION['cryptphrase'] = $phraseMD5; $this->convertMCryptDataToOpenSSL($user); $this->convertOpenSSLDataToSodium($user); $this->convertPendingEncryptionData($user); } // User has changed the password // Change/Rehash the password, given the old and the new key phrases public function onChangeUserPassword($oldCleartextPwd, $newCleartextPwd) { global $user; // Lookup pref key that are encrypted data $domains = $this->getPasswordDomains(); // Rehash encrypted preferences foreach ($domains as $userprefKey) { $rc = $this->changeUserPassword($userprefKey, md5($user . $oldCleartextPwd), md5($user . $newCleartextPwd)); // Also update the username, if defined if ($rc && $this->hasUserData($userprefKey, 'usr')) { $this->changeUserPassword($userprefKey . '.usr', md5($user . $oldCleartextPwd), md5($user . $newCleartextPwd)); } } // Save the new cryptphrase, so the new hash is readable without logging out $this->onUserLogin($newCleartextPwd); } // Change/Rehash the password, given the old and the new key phrases // Return true on success; otherwise false, e.g. if no stored password is found, or a decryption failure public function changeUserPassword($userprefKey, $oldPhraseMD5, $newPhraseMD5) { global $user; // Retrieve the old password $cryptOld = new CryptLib(); $cryptOld->initSeed($oldPhraseMD5); if (! $cryptOld->hasCrypt()) { // Crypt is not available. // Only Base64 encoding. No conversion needed return false; } $cleartextPwd = $cryptOld->getUserData($userprefKey); $cryptOld->release(); if ($cleartextPwd == false) { return false; } // Check if the cleartext contain any illigal password character. // If found, it indicates that the decryption has failed. The $oldPhraseMD5 may be incorrect? // Then, do not proceed to rehash the password if (! ctype_print($cleartextPwd)) { return false; } // Rehash and save $cryptNew = new CryptLib(); $cryptNew->initSeed($newPhraseMD5); $cryptPwd = $cryptNew->setUserData($userprefKey, $cleartextPwd); $cryptNew->release(); if ($cryptPwd == false) { return false; } // Rehashed OK return true; } // // Crypt //////////////////////////////// // Use OpenSSL if available. Otherwise Base64 encode only // Return base64 encoded string, containing either the crypttext with the iv prepended, or the cleartext if on base64 encoding is used private function encrypt($cleartext) { if ($this->hasSodiumCrypt()) { $key = str_pad(substr($this->key, 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES), SODIUM_CRYPTO_SECRETBOX_KEYBYTES); $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $crypttext = $nonce . sodium_crypto_secretbox($cleartext, $nonce, $key); } elseif ($this->hasCrypt()) { $ivSize = openssl_cipher_iv_length($this->cryptMethod); $iv = openssl_random_pseudo_bytes($ivSize); $crypttext = openssl_encrypt( $cleartext, $this->cryptMethod, $this->key, OPENSSL_RAW_DATA, $iv ); // Prepend the iv $crypttext = $iv . $crypttext; } else { $crypttext = base64_encode($cleartext); } return $crypttext; } // Use OpenSSL if available. Otherwise Base64 decode // Return cleartext private function decrypt($crypttext) { if ($this->hasSodiumCrypt()) { $key = str_pad(substr($this->key, 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES), SODIUM_CRYPTO_SECRETBOX_KEYBYTES); $nonce = substr($crypttext, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $ciphertextLength = strlen($crypttext) - SODIUM_CRYPTO_SECRETBOX_NONCEBYTES; $crypttext = substr($crypttext, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, $ciphertextLength); $rawcleartext = sodium_crypto_secretbox_open($crypttext, $nonce, $key); } elseif ($this->hasCrypt()) { $ivSize = openssl_cipher_iv_length($this->cryptMethod); $iv = substr($crypttext, 0, $ivSize); $ciphertext = substr($crypttext, $ivSize); $rawcleartext = openssl_decrypt( $ciphertext, $this->cryptMethod, $this->key, OPENSSL_RAW_DATA, $iv ); } else { // Use Base64 encoding $rawcleartext = base64_decode($crypttext); } // Clear trailing null-characters $cleartext = rtrim($rawcleartext); return $cleartext; } // // Old MCrypt coded data conversion //////////////////////////////// /** * Decrypt OpenSSL data * * @param $crypttext * @return Mixed */ private function decryptOpenSll($crypttext) { $cleartext = null; if ($this->hasCrypt()) { $crypttext = base64_decode($crypttext); $ivSize = openssl_cipher_iv_length($this->cryptMethod); $iv = substr($crypttext, 0, $ivSize); $ciphertext = substr($crypttext, $ivSize); $cleartext = openssl_decrypt( $ciphertext, $this->cryptMethod, $this->key, OPENSSL_RAW_DATA, $iv ); // Clear trailing null-characters $cleartext = rtrim($cleartext); } return $cleartext; } // Use MCrypt if available. Otherwise Base64 decode // Return cleartext private function decryptMcrypt($cryptData64) { if ($this->hasMCrypt()) { $cryptData = base64_decode($cryptData64); $ivSize = mcrypt_enc_get_iv_size($this->mcrypt); $iv = substr($cryptData, 0, $ivSize); $crypttext = substr($cryptData, $ivSize); $rawcleartext = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $this->mcrypt_key, $crypttext, MCRYPT_MODE_CBC, $iv); // Clear trailing null-characters $cleartext = rtrim($rawcleartext); } else { // Use Base64 encoding $cleartext = base64_decode($cryptData64); } return $cleartext; } private function convertMCryptDataToOpenSSL($login) { $this->init(); // Convert encrypted data, if OpenSSL is installed if ($this->hasCrypt()) { $query = 'SELECT `prefName` , `value` FROM `tiki_user_preferences` WHERE `prefName` like \'dp.%\' and `user` = ?'; $result = $this->query($query, [$login]); while ($row = $result->fetchRow()) { $orgPrefName = $row['prefName']; $storedPwdMCrypt64 = $row['value']; if ($this->hasMCrypt()) { $cleartext = $this->decryptMcrypt($storedPwdMCrypt64); // Strip dp. from prefName $prefName = str_replace('dp.', '', $orgPrefName); // Add new OpenSSL coded user data $this->setUserData($prefName, $cleartext); } // Delete old Mcrypt coded user data $userPreferences = $this->table('tiki_user_preferences', false); $userPreferences->delete(['user' => $login, 'prefName' => $orgPrefName]); } } } /** * Convert OpenSSL encrypted data to Sodium * * @param $login * @return null */ private function convertOpenSSLDataToSodium($login) { $this->init(); // Convert encrypted OpenSSL data, if Sodium is installed if ($this->hasSodiumCrypt()) { $query = 'SELECT `prefName` , `value` FROM `tiki_user_preferences` WHERE `prefName` like \'ds.%\' and `user` = ?'; $result = $this->query($query, [$login]); while ($row = $result->fetchRow()) { $orgPrefName = $row['prefName']; $storedPwdMCrypt64 = $row['value']; if ($this->hasCrypt()) { $cleartext = $this->decryptOpenSll($storedPwdMCrypt64, $orgPrefName); if (empty($cleartext)) { continue; } // Strip ds. from prefName $prefName = str_replace('ds.', '', $orgPrefName); // Add new Sodium coded user data $this->setUserData($prefName, $cleartext); } // Delete old OpenSSL coded user data $userPreferences = $this->table('tiki_user_preferences', false); $userPreferences->delete(['user' => $login, 'prefName' => $orgPrefName]); } } } /** * Convert cleartext data scheduled for encryption * * @param $login * @return null */ private function convertPendingEncryptionData($login) { $this->init(); $query = 'SELECT `prefName` , `value` FROM `tiki_user_preferences` WHERE `prefName` like \'pe.%\' and `user` = ?'; $result = $this->query($query, [$login]); while ($row = $result->fetchRow()) { $orgPrefName = $row['prefName']; $cleartext = $row['value']; // Strip ds. from prefName $prefName = str_replace('pe.', '', $orgPrefName); // Add new encrypted user data $this->setUserData($prefName, $cleartext); // Delete old cleartext user data $userPreferences = $this->table('tiki_user_preferences', false); $userPreferences->delete(['user' => $login, 'prefName' => $orgPrefName]); } } }