<commit_before><?php
/**
 * Nextcloud - spreedme
 *
 * This file is licensed under the Affero General Public License version 3 or
 * later. See the COPYING file.
 *
 * @author Leon <<EMAIL>>
 * @copyright struktur AG 2016
 */

namespace OCA\SpreedME\Security;

use OCA\SpreedME\Errors\ErrorCodes;
use OCA\SpreedME\Helper\Helper;
use OCA\SpreedME\Settings\Settings;

class Security {

	// TODO(leon): Rename all of the methods and variables.
	// This got so confusing.

	// Increase this with every major update of the signing process to invalidate old signatures
	const COMBO_VERSION = 2;
	const TP_VERSION = 2;

	private function __construct() {

	}

	private static function getSignedUsercomboArray($version, $userid, $key, $max_age, $expiration = 0) {
		if ($expiration > 0) {
			$useridcombo = sprintf('%s:%s:%s', intval($expiration), $userid, $version);
		} else {
			$useridcombo = sprintf('%s:%s:%s', (time() + $max_age), $userid, $version);
		}
		$secret = base64_encode(hash_hmac('sha256', $useridcombo, $key, true));

		return array(
			'useridcombo' => $useridcombo,
			'secret' => $secret,
		);
	}

	public static function getSignedCombo($userid, $expiration = 0) {
		$version = self::COMBO_VERSION;
		$key = Helper::getConfigValue('SPREED_WEBRTC_SHAREDSECRET');
		$max_age = Settings::SPREED_WEBRTC_USERCOMBO_MAX_AGE;

		if ($expiration > 0) {
			return self::getSignedUsercomboArray(
				$version,
				$userid,
				$key,
				false,
				$expiration + $max_age // So we have time to authenticate
			);
		} else {
			return self::getSignedUsercomboArray(
				$version,
				$userid,
				$key,
				$max_age
			);
		}
	}

	private static function requireEnabledTemporaryPassword() {
		if (Helper::getConfigValue('OWNCLOUD_TEMPORARY_PASSWORD_LOGIN_ENABLED') !== true) {
			throw new \Exception('Temporary Passwords not enabled in config/config.php', ErrorCodes::TEMPORARY_PASSWORD_NOT_ENABLED);
		}
	}

	private static function decorateUserId($userid, $prefix) {
		// Prefix userid with ext/ or int/ and append /uniqueid
		$uniqueid = uniqid('', true);
		return sprintf('%s/%s/%s', $prefix, $userid, $uniqueid);
	}

	// TODO(leon): Refactor, this is shitty
	public static function generateTemporaryPassword($userid, $expiration = 0, $version = self::TP_VERSION, $forValidation = false) {
		self::requireEnabledTemporaryPassword();

		// Only prevent certain characters and decorate userid if we don't want to validate a given TP
		if (!$forValidation) {
			$disallowed = array(':', '/');
			foreach ($disallowed as $char) {
				if (strpos($userid, $char) !== false) {
					throw new \Exception('userid may not contain one of these symbols: ' . join(' or ', $disallowed), ErrorCodes::TEMPORARY_PASSWORD_INVALID_USERID);
				}
			}

			$userid = self::decorateUserId($userid, 'ext');
		}

		$key = Helper::getConfigValue('OWNCLOUD_TEMPORARY_PASSWORD_SIGNING_KEY');
		$max_age = 60 * 60 * 2;

		if ($expiration > 0) {
			// Use a fixed expiration date
			$signed_combo_array = self::getSignedUsercomboArray(
				$version,
				$userid,
				$key,
				false,
				$expiration
			);
		} else {
			// Dynamically expire after x seconds
			$signed_combo_array = self::getSignedUsercomboArray(
				$version,
				$userid,
				$key,
				$max_age
			);
		}

		return $signed_combo_array['useridcombo'] . ':' . $signed_combo_array['secret'];
	}

	private static function validateTemporaryPassword($tp) {
		self::requireEnabledTemporaryPassword();

		$split = explode(':', $tp);

		// The 4 parts: expiraton, userid, version, hmac (signature of previous 3 parts)
		if (count($split) !== 4) {
			// Invalid tp part length
			return false;
		}

		list($expiration, $userid, $version, $hmac) = $split;

		if (time() > $expiration) {
			// Expired
			return false;
		}

		if (intval($version) !== self::TP_VERSION) {
			// Unsupported / outdated version
			return false;
		}

		$calctp = self::generateTemporaryPassword($userid, $expiration, $version, true); // Set forValidation flag
		if (self::constantTimeEquals($tp, $calctp) !== true) {
			// Incorrect hmac
			return false;
		}

		return true;
	}

	public static function getSignedComboFromTemporaryPassword($tp) {
		self::requireEnabledTemporaryPassword();

		if (self::validateTemporaryPassword($tp) !== true) {
			throw new \Exception('Invalid Temporary Password', ErrorCodes::TEMPORARY_PASSWORD_INVALID);
		}

		// TODO(leon): Do we need to split again?
		$split = explode(':', $tp);
		list($expiration, $userid, $version, $hmac) = $split;

		return self::getSignedCombo($userid);
	}

	public static function getRandomHexString($length) {
		return \OC::$server->getSecureRandom()->getMediumStrengthGenerator()->generate($length, '0123456789abcdef');
	}

	public static function regenerateSharedSecret() {
		$key = Security::getRandomHexString(256 / 4); // 256 bit
		Helper::setDatabaseConfigValueIfEnabled('SPREED_WEBRTC_SHAREDSECRET', $key);
		return $key;
	}

	public static function regenerateTemporaryPasswordSigningKey() {
		$key = Security::getRandomHexString(256 / 4); // 256 bit
		Helper::setDatabaseConfigValueIfEnabled('OWNCLOUD_TEMPORARY_PASSWORD_SIGNING_KEY', $key);
	}

	public static function constantTimeEquals($a, $b) {
		$alen = strlen($a);
		$blen = strlen($b);

		if ($alen !== $blen) {
			return false;
		}

		$result = 0;
		for ($i = 0; $i < $alen; $i++) {
			$result |= (ord($a[$i]) ^ ord($b[$i]));
		}
<commit_msg>Use 'mb_strlen' instead of 'strlen' for calculating a string's length<commit_after><?php
/**
 * Nextcloud - spreedme
 *
 * This file is licensed under the Affero General Public License version 3 or
 * later. See the COPYING file.
 *
 * @author Leon <<EMAIL>>
 * @copyright struktur AG 2016
 */

namespace OCA\SpreedME\Security;

use OCA\SpreedME\Errors\ErrorCodes;
use OCA\SpreedME\Helper\Helper;
use OCA\SpreedME\Settings\Settings;

class Security {

	// TODO(leon): Rename all of the methods and variables.
	// This got so confusing.

	// Increase this with every major update of the signing process to invalidate old signatures
	const COMBO_VERSION = 2;
	const TP_VERSION = 2;

	private function __construct() {

	}

	private static function getSignedUsercomboArray($version, $userid, $key, $max_age, $expiration = 0) {
		if ($expiration > 0) {
			$useridcombo = sprintf('%s:%s:%s', intval($expiration), $userid, $version);
		} else {
			$useridcombo = sprintf('%s:%s:%s', (time() + $max_age), $userid, $version);
		}
		$secret = base64_encode(hash_hmac('sha256', $useridcombo, $key, true));

		return array(
			'useridcombo' => $useridcombo,
			'secret' => $secret,
		);
	}

	public static function getSignedCombo($userid, $expiration = 0) {
		$version = self::COMBO_VERSION;
		$key = Helper::getConfigValue('SPREED_WEBRTC_SHAREDSECRET');
		$max_age = Settings::SPREED_WEBRTC_USERCOMBO_MAX_AGE;

		if ($expiration > 0) {
			return self::getSignedUsercomboArray(
				$version,
				$userid,
				$key,
				false,
				$expiration + $max_age // So we have time to authenticate
			);
		} else {
			return self::getSignedUsercomboArray(
				$version,
				$userid,
				$key,
				$max_age
			);
		}
	}

	private static function requireEnabledTemporaryPassword() {
		if (Helper::getConfigValue('OWNCLOUD_TEMPORARY_PASSWORD_LOGIN_ENABLED') !== true) {
			throw new \Exception('Temporary Passwords not enabled in config/config.php', ErrorCodes::TEMPORARY_PASSWORD_NOT_ENABLED);
		}
	}

	private static function decorateUserId($userid, $prefix) {
		// Prefix userid with ext/ or int/ and append /uniqueid
		$uniqueid = uniqid('', true);
		return sprintf('%s/%s/%s', $prefix, $userid, $uniqueid);
	}

	// TODO(leon): Refactor, this is shitty
	public static function generateTemporaryPassword($userid, $expiration = 0, $version = self::TP_VERSION, $forValidation = false) {
		self::requireEnabledTemporaryPassword();

		// Only prevent certain characters and decorate userid if we don't want to validate a given TP
		if (!$forValidation) {
			$disallowed = array(':', '/');
			foreach ($disallowed as $char) {
				if (strpos($userid, $char) !== false) {
					throw new \Exception('userid may not contain one of these symbols: ' . join(' or ', $disallowed), ErrorCodes::TEMPORARY_PASSWORD_INVALID_USERID);
				}
			}

			$userid = self::decorateUserId($userid, 'ext');
		}

		$key = Helper::getConfigValue('OWNCLOUD_TEMPORARY_PASSWORD_SIGNING_KEY');
		$max_age = 60 * 60 * 2;

		if ($expiration > 0) {
			// Use a fixed expiration date
			$signed_combo_array = self::getSignedUsercomboArray(
				$version,
				$userid,
				$key,
				false,
				$expiration
			);
		} else {
			// Dynamically expire after x seconds
			$signed_combo_array = self::getSignedUsercomboArray(
				$version,
				$userid,
				$key,
				$max_age
			);
		}

		return $signed_combo_array['useridcombo'] . ':' . $signed_combo_array['secret'];
	}

	private static function validateTemporaryPassword($tp) {
		self::requireEnabledTemporaryPassword();

		$split = explode(':', $tp);

		// The 4 parts: expiraton, userid, version, hmac (signature of previous 3 parts)
		if (count($split) !== 4) {
			// Invalid tp part length
			return false;
		}

		list($expiration, $userid, $version, $hmac) = $split;

		if (time() > $expiration) {
			// Expired
			return false;
		}

		if (intval($version) !== self::TP_VERSION) {
			// Unsupported / outdated version
			return false;
		}

		$calctp = self::generateTemporaryPassword($userid, $expiration, $version, true); // Set forValidation flag
		if (self::constantTimeEquals($tp, $calctp) !== true) {
			// Incorrect hmac
			return false;
		}

		return true;
	}

	public static function getSignedComboFromTemporaryPassword($tp) {
		self::requireEnabledTemporaryPassword();

		if (self::validateTemporaryPassword($tp) !== true) {
			throw new \Exception('Invalid Temporary Password', ErrorCodes::TEMPORARY_PASSWORD_INVALID);
		}

		// TODO(leon): Do we need to split again?
		$split = explode(':', $tp);
		list($expiration, $userid, $version, $hmac) = $split;

		return self::getSignedCombo($userid);
	}

	public static function getRandomHexString($length) {
		return \OC::$server->getSecureRandom()->getMediumStrengthGenerator()->generate($length, '0123456789abcdef');
	}

	public static function regenerateSharedSecret() {
		$key = Security::getRandomHexString(256 / 4); // 256 bit
		Helper::setDatabaseConfigValueIfEnabled('SPREED_WEBRTC_SHAREDSECRET', $key);
		return $key;
	}

	public static function regenerateTemporaryPasswordSigningKey() {
		$key = Security::getRandomHexString(256 / 4); // 256 bit
		Helper::setDatabaseConfigValueIfEnabled('OWNCLOUD_TEMPORARY_PASSWORD_SIGNING_KEY', $key);
	}

	public static function constantTimeEquals($a, $b) {
		$alen = mb_strlen($a, '8bit');
		$blen = mb_strlen($b, '8bit');

		if ($alen !== $blen) {
			return false;
		}

		$result = 0;
		for ($i = 0; $i < $alen; $i++) {
			$result |= (ord($a[$i]) ^ ord($b[$i]));
		}

{'fraction_non_alphanumeric': 0.14189562972524433, 'fraction_numerical': 0.009773188272174074, 'mean_word_length': 4.23503861003861, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2843559', 'n_tokens_mistral': 4185, 'n_tokens_neox': 3676, 'n_words': 875}