<?php
class XenForo_Model_Tfa extends XenForo_Model
{
/**
* @return XenForo_Tfa_AbstractProvider[]
*/
public function getValidProviders()
{
$providers = $this->_getDb()->fetchPairs("
SELECT provider_id, provider_class
FROM xf_tfa_provider
WHERE active = 1
ORDER BY priority
");
$output = array();
foreach ($providers AS $id => $class)
{
if (class_exists($class))
{
$class = XenForo_Application::resolveDynamicClass($class);
$output[$id] = new $class($id);
}
}
return $output;
}
/**
* @param string $providerId
*
* @return XenForo_Tfa_AbstractProvider|null
*/
public function getValidProvider($providerId)
{
$class = $this->_getDb()->fetchOne("
SELECT provider_class
FROM xf_tfa_provider
WHERE provider_id = ?
AND active = 1
", $providerId);
if ($class && class_exists($class))
{
$class = XenForo_Application::resolveDynamicClass($class);
return new $class($providerId);
}
else
{
return null;
}
}
public function getUserTfaEntries($userId)
{
return $this->fetchAllKeyed("
SELECT *
FROM xf_user_tfa
WHERE user_id = ?
", 'provider_id', $userId);
}
public function getUserTfaEntry($userId, $providerId)
{
return $this->_getDb()->fetchRow("
SELECT *
FROM xf_user_tfa
WHERE user_id = ?
AND provider_id = ?
", array($userId, $providerId));
}
public function getTfaConfigurationForUser($userId, &$userData)
{
$userData = $this->getUserTfaEntries($userId);
$providers = $this->getValidProviders();
$providers = $this->filterProvidersForUserEnabled($providers, $userData);
return $providers;
}
public function userRequiresTfa($userId)
{
$providers = $this->getValidProviders();
$userData = $this->getUserTfaEntries($userId);
$providers = $this->filterProvidersForUserEnabled($providers, $userData);
if (!$providers)
{
return false;
}
if (count($providers) == 1)
{
/** @var XenForo_Tfa_AbstractProvider $provider */
$provider = reset($providers);
if ($provider->getProviderId() == 'backup')
{
return false;
}
}
return true;
}
/**
* @param XenForo_Tfa_AbstractProvider[] $providers
* @param array $userData
*
* @return XenForo_Tfa_AbstractProvider[]
*/
public function filterProvidersForUserEnabled(array $providers, array $userData)
{
foreach ($providers AS $id => $provider)
{
if (!isset($userData[$id]))
{
unset($providers[$id]);
}
}
return $providers;
}
/**
* @param XenForo_Tfa_AbstractProvider[] $providers
* @param array $userData
*
* @return array
*/
public function prepareTfaProviderList(array $providers, array $userData = array())
{
$output = array();
foreach ($providers AS $id => $provider)
{
$enabled = isset($userData[$id]);
$canEnable = $provider->canEnable();
if (!$enabled && !$canEnable)
{
continue;
}
$output[$id] = array(
'provider_id' => $id,
'title' => $provider->getTitle(),
'description' => $provider->getDescription(),
'enabled' => $enabled,
'canEnable' => !$enabled && $canEnable,
'canDisable' => $enabled && $provider->canDisable(),
'canManage' => $enabled && $provider->canManage()
);
}
return $output;
}
public function enableUserTfaProvider($userId, $providerId, array $providerData)
{
$db = $this->_getDb();
$db->beginTransaction();
$db->query("
INSERT INTO xf_user_tfa
(user_id, provider_id, provider_data, last_used_date)
VALUES
(?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
provider_data = VALUES(provider_data),
last_used_date = VALUES(last_used_date)
", array($userId, $providerId, serialize($providerData), XenForo_Application::$time));
$success = $db->update('xf_user_option', array('use_tfa' => 1), 'user_id = ' . $db->quote($userId));
if ($success)
{
/** @var XenForo_Model_UserChangeLog $userChangeLog */
$userChangeLog = $this->getModelFromCache('XenForo_Model_UserChangeLog');
$userChangeLog->logChanges($userId, array('use_tfa' => array(0, 1)));
}
$db->commit();
return true;
}
public function enableBackupTfaProviderIfNeeded(array $user)
{
$provider = $this->getValidProvider('backup');
if (!$provider)
{
return false;
}
$data = $this->getUserTfaEntry($user['user_id'], 'backup');
if ($data)
{
return false;
}
$providerData = $provider->generateInitialData($user, array());
$this->enableUserTfaProvider($user['user_id'], 'backup', $providerData);
return true;
}
public function disableUserTfaProvider($userId, $providerId)
{
$db = $this->_getDb();
$db->beginTransaction();
$this->_getDb()->query("
DELETE FROM xf_user_tfa
WHERE user_id = ?
AND provider_id = ?
", array($userId, $providerId));
if (!$this->userRequiresTfa($userId))
{
// this delete is to clear out the backup codes and any disabled providers since
// we're setting use_tfa to 0
$this->disableTfaForUser($userId);
}
$db->commit();
}
public function disableTfaForUser($userId)
{
$db = $this->_getDb();
$db->beginTransaction();
$db->delete('xf_user_tfa', 'user_id = ' . $db->quote($userId));
$success = $db->update('xf_user_option', array('use_tfa' => 0), 'user_id = ' . $db->quote($userId));
$this->deleteUserTrustedRecords($userId);
if ($success)
{
/** @var XenForo_Model_UserChangeLog $userChangeLog */
$userChangeLog = $this->getModelFromCache('XenForo_Model_UserChangeLog');
$userChangeLog->logChanges($userId, array('use_tfa' => array(1, 0)));
}
$db->commit();
}
public function updateUserProvider($userId, $providerId, array $providerData, $updateLastUsed = false)
{
$update = array('provider_data' => serialize($providerData));
if ($updateLastUsed)
{
$update['last_used_date'] = XenForo_Application::$time;
}
$db = $this->_getDb();
$db->update('xf_user_tfa', $update,
'user_id = ' . $db->quote($userId) . ' AND provider_id = ' . $db->quote($providerId)
);
}
public function createTrustedKey($userId, $trustedUntil = null)
{
if ($trustedUntil === null)
{
$trustedUntil = XenForo_Application::$time + 86400 * 30;
// jitter between 0 and 96 hours (4 days). This attempts to reduce situations where multiple
// devices all expire at almost identical times
$offsetJitter = mt_rand(0, 4 * 24) * 3600;
$trustedUntil += $offsetJitter;
}
$key = XenForo_Application::generateRandomString(32);
$this->_getDb()->query("
INSERT IGNORE INTO xf_user_tfa_trusted
(user_id, trusted_key, trusted_until)
VALUES
(?, ?, ?)
", array($userId, $key, $trustedUntil));
return $key;
}
public function getUserTrustedRecord($userId, $trustedKey)
{
return $this->_getDb()->fetchRow("
SELECT *
FROM xf_user_tfa_trusted
WHERE user_id = ?
AND trusted_key = ?
AND trusted_until >= ?
", array($userId, $trustedKey, XenForo_Application::$time));
}
public function countUserTrustedRecords($userId, $notTrustedKey = null)
{
if ($notTrustedKey)
{
$notKeySql = 'AND trusted_key <> ' . $this->_getDb()->quote($notTrustedKey);
}
else
{
$notKeySql = '';
}
return $this->_getDb()->fetchOne("
SELECT COUNT(*)
FROM xf_user_tfa_trusted
WHERE user_id = ?
{$notKeySql}
", array($userId));
}
public function deleteUserTrustedRecord($userId, $trustedKey)
{
$this->_getDb()->query("
DELETE FROM xf_user_tfa_trusted
WHERE user_id = ?
AND trusted_key = ?
", array($userId, $trustedKey));
}
public function deleteUserTrustedRecords($userId, $notTrustedKey = null)
{
if ($notTrustedKey)
{
$notKeySql = 'AND trusted_key <> ' . $this->_getDb()->quote($notTrustedKey);
}
else
{
$notKeySql = '';
}
$this->_getDb()->query("
DELETE FROM xf_user_tfa_trusted
WHERE user_id = ?
{$notKeySql}
", array($userId));
}
public function pruneTrustedKeys($cutOff = null)
{
if ($cutOff === null)
{
$cutOff = XenForo_Application::$time;
}
$db = $this->_getDb();
return $db->delete('xf_user_tfa_trusted', 'trusted_until < ' . $db->quote($cutOff));
}
public function getTfaAttemptLimits()
{
return array(
// [time, max attempts]
array(60 * 5, 8),
array(60, 4),
);
}
public function logFailedTfaAttempt($userId)
{
$this->_getDb()->insert('xf_tfa_attempt', array(
'user_id' => $userId,
'attempt_date' => XenForo_Application::$time
));
}
public function clearTfaAttemptsForUser($userId)
{
$this->_getDb()->delete('xf_tfa_attempt', 'user_id = ' . $this->_getDb()->quote($userId));
}
public function countTfaAttemptsInTime($cutOff, $userId)
{
$db = $this->_getDb();
return $db->fetchOne("
SELECT COUNT(*)
FROM xf_tfa_attempt
WHERE attempt_date > ?
AND user_id = ?
", array($cutOff, $userId));
}
public function isTfaAttemptLimited($userId)
{
$limits = $this->getTfaAttemptLimits();
foreach ($limits AS $limit)
{
$timeLimit = $limit[0];
$attemptLimit = $limit[1];
$attempts = $this->countTfaAttemptsInTime(XenForo_Application::$time - $timeLimit, $userId);
if ($attempts >= $attemptLimit)
{
return true;
}
}
return false;
}
public function pruneFailedTfaAttempts($cutOff = null)
{
if ($cutOff === null)
{
$cutOff = XenForo_Application::$time - 86400;
}
$db = $this->_getDb();
$db->delete('xf_tfa_attempt', 'attempt_date < ' . $db->quote($cutOff));
}
}