, * Michael Stilkerich * * This file is part of RCMCardDAV. * * RCMCardDAV is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * RCMCardDAV 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with RCMCardDAV. If not, see . */ declare(strict_types=1); namespace MStilkerich\RCMCardDAV\Frontend; use Exception; use rcube; use MStilkerich\RCMCardDAV\Config; /** * Various utility functions used in the Frontend. * * @psalm-import-type PasswordStoreScheme from AdminSettings */ class Utils { public static function replacePlaceholdersUsername(string $username, bool $quoteRegExp = false): string { $rcusername = (string) $_SESSION['username']; $rcusernameParts = explode('@', $rcusername, 2); $transTable = [ '%u' => $rcusername, '%l' => $rcusernameParts[0], '%d' => $rcusernameParts[1] ?? '', // %V parses username for macOS, replaces periods and @ by _, work around bugs in contacts.app '%V' => strtr($rcusername, "@.", "__") ]; if ($quoteRegExp) { $transTable = array_map( function (string $s): string { return preg_quote($s); }, $transTable ); } return strtr($username, $transTable); } public static function replacePlaceholdersUrl(string $url, bool $quoteRegExp = false): string { // currently same as for username return self::replacePlaceholdersUsername($url, $quoteRegExp); } public static function replacePlaceholdersPassword(string $password): string { if ($password == '%p') { $rcube = rcube::get_instance(); $password = $rcube->decrypt((string) $_SESSION['password']); if ($password === false) { $password = ""; } } return $password; } /** * Parses a time string to seconds. * * The time string must have the format HH[:MM[:SS]]. If the format does not match, an exception is thrown. * * @param string $timeStr The time string to parse * @return int The time in seconds */ public static function parseTimeParameter(string $timeStr): int { if (preg_match('/^(\d+)(:([0-5]?\d))?(:([0-5]?\d))?$/', $timeStr, $match)) { $ret = 0; $ret += intval($match[1] ?? 0) * 3600; $ret += intval($match[3] ?? 0) * 60; $ret += intval($match[5] ?? 0); } else { throw new Exception("Time string $timeStr could not be parsed"); } return $ret; } /** * Converts a password to storage format according to the password storage scheme setting. * * @param string $clear The password in clear text. * @return string The password in storage format (e.g., encrypted with user password as key) */ public static function encryptPassword(string $clear): string { $infra = Config::inst(); $admPrefs = $infra->admPrefs(); $scheme = $admPrefs->pwStoreScheme; if (strcasecmp($scheme, 'plain') === 0) { return $clear; } if (strcasecmp($scheme, 'encrypted') === 0) { try { // encrypted with IMAP password $rcube = rcube::get_instance(); $imap_password = self::getDesKey(); $rcube->config->set('carddav_des_key', $imap_password); $crypted = $rcube->encrypt($clear, 'carddav_des_key'); // there seems to be no way to unset a preference $rcube->config->set('carddav_des_key', null); if ($crypted === false) { throw new Exception("Password encryption with user password failed"); } return '{ENCRYPTED}' . $crypted; } catch (Exception $e) { $logger = Config::inst()->logger(); $logger->warning( "Could not encrypt password with 'encrypted' method, falling back to 'des_key': " . $e->getMessage() ); $scheme = 'des_key'; } } if (strcasecmp($scheme, 'des_key') === 0) { // encrypted with global des_key $rcube = rcube::get_instance(); $crypted = $rcube->encrypt($clear); if ($crypted === false) { throw new Exception("Could not encrypt password with 'des_key' method"); } return '{DES_KEY}' . $crypted; } // otherwise, it's BASE64 return '{BASE64}' . base64_encode($clear); } /** * Decrypts a password in the database according to the scheme used to encrypt or encode the password. * * In case of error, an error is logged and an empty string is returned. */ public static function decryptPassword(string $crypt): string { $logger = Config::inst()->logger(); $rcube = rcube::get_instance(); $key = null; $clear = ''; try { if (strpos($crypt, '{ENCRYPTED}') === 0) { $crypt = substr($crypt, strlen('{ENCRYPTED}')); $imap_password = self::getDesKey(); $key = 'carddav_des_key'; $rcube->config->set($key, $imap_password); } elseif (strpos($crypt, '{DES_KEY}') === 0) { $crypt = substr($crypt, strlen('{DES_KEY}')); $key = 'des_key'; } elseif (strpos($crypt, '{BASE64}') === 0) { $crypt = substr($crypt, strlen('{BASE64}')); $clear = base64_decode($crypt, true); if ($clear === false) { throw new Exception('not a valid base64 string'); } } else { // unknown scheme, assume cleartext $clear = $crypt; } if (isset($key)) { $clear = $rcube->decrypt($crypt, $key); if ($clear === false) { throw new Exception("decryption with $key failed"); } } } catch (Exception $e) { $logger->warning("Cannot decrypt password: " . $e->getMessage()); $clear = ''; } // not strictly needed, but lets not keep the cleartext IMAP password in rcube_config if ($key === 'carddav_des_key') { $rcube->config->set($key, null); } return $clear; } /** * Returns the 24-byte decryption key derived from the user's password. * * The returned string is made exactly 24 bytes by repeating the IMAP password or, if it is longer, truncating it. * This should not be required anymore, because openssl_encrypt() (and assumedly also openssl_decrypt()) pad the * password themselves if needed, but this is kept for backwards compatibility with existing encrypted passwords. * * In case of error, this function throws an exception. */ private static function getDesKey(): string { $rcube = rcube::get_instance(); // if the user logged in via OAuth, we do not have a password to use for encryption / decryption of carddav // passwords; roundcube sets SESSION[password] to the encrypted 'Bearer ', so we need to // specifically check if oauth is used for login if (isset($_SESSION['oauth_token'])) { throw new Exception("No password available to use for encryption because user logged in via OAuth2"); } $imapPw = $rcube->decrypt((string) $_SESSION['password']); if ($imapPw === false || strlen($imapPw) == 0) { throw new Exception("No password available to use for encryption"); } $imapPw = str_pad($imapPw, 24, $imapPw); return substr($imapPw, 0, 24); } } // vim: ts=4:sw=4:expandtab:fenc=utf8:ff=unix:tw=120