tfa.inc in Two-factor Authentication (TFA) 7.2
TFA module classes.
File
tfa.incView source
<?php
/**
* @file
* TFA module classes.
*/
/**
* Class Tfa.
*/
class Tfa {
/**
* Context of TFA process.
*
* @var array
*/
protected $context;
/**
* TFA validation plugin.
*
* @var TfaBasePlugin
*/
protected $validatePlugin;
/**
* TfaLoginPluginInterface objects for use during login indexed by name.
*
* @var array
*/
protected $loginPlugins = array();
/**
* TfaBasePlugin objects for use as fallback indexed by name.
*
* @var array
*/
protected $fallbackPlugins = array();
/**
* Complete.
*
* @var bool
*/
protected $complete = FALSE;
/**
* Fallback process.
*
* @var bool
*/
protected $fallback = FALSE;
/**
* TFA constructor.
*
* @param TfaBasePlugin $validate
* Validation plugin.
* @param array $context
* Context of TFA process.
* Must include key:
* - 'uid'
* Account uid of user in TFA process.
* - 'plugins'
* Array of plugin names indexed by type: 'validate', 'fallback',
* 'login'.
* @param array $fallback_plugins
* Optional TfaBasePlugin objects for use as fallback indexed by name.
* @param array $login_plugins
* Optional TfaLoginPluginInterface objects for use during login indexed by
* name.
*/
public function __construct(TfaBasePlugin $validate, array $context, array $fallback_plugins, array $login_plugins) {
$validate_plugin = $context['plugins']['validate'];
$active_plugins = array(
'validate' => $validate_plugin,
);
$this->validatePlugin = $validate;
if (!empty($login_plugins)) {
foreach ($login_plugins as $plugin_name => $login_plugin) {
$this->loginPlugins[$plugin_name] = $login_plugin;
$active_plugins['login'][] = $plugin_name;
}
}
if (!empty($fallback_plugins)) {
foreach ($fallback_plugins as $plugin_name => $fallback_plugin) {
// Skip this fallback if its same as validation.
if ($plugin_name === $validate_plugin) {
continue;
}
// Only use plugins that are ready.
if ($fallback_plugin
->ready()) {
$this->fallbackPlugins[$plugin_name] = $fallback_plugin;
$active_plugins['fallback'][] = $plugin_name;
}
}
// Mark whether a fallback plugin exists.
if (!empty($this->fallbackPlugins)) {
$this->fallback = TRUE;
}
}
$this->context = $context;
// Save only active plugins.
$this->context['plugins'] = $active_plugins;
}
/**
* Whether authentication should be allowed and not interrupted.
*
* If any plugin returns TRUE then authentication is not interrupted by TFA.
*
* @return bool
* Whether authentication should be allowed.
*/
public function loginAllowed() {
if (!empty($this->loginPlugins)) {
foreach ($this->loginPlugins as $class) {
if ($class
->loginAllowed()) {
return TRUE;
}
}
}
return FALSE;
}
/**
* Determine if TFA process is ready.
*
* @return bool
* Whether process can begin or not.
*/
public function ready() {
return $this->validatePlugin
->ready();
}
/**
* Get TFA process form from plugin.
*
* @param array $form
* The form array structure.
* @param array $form_state
* The current form state array.
*
* @return array
* Form API array.
*/
public function getForm(array $form, array &$form_state) {
$form = $this->validatePlugin
->getForm($form, $form_state);
// Allow login plugins to modify form unless validation plugin set
// '#tfa_no_login' to TRUE.
if (!empty($this->loginPlugins) && (!isset($form['#tfa_no_login']) || $form['#tfa_no_login'] === FALSE)) {
foreach ($this->loginPlugins as $class) {
if (method_exists($class, 'getForm')) {
$form = $class
->getForm($form, $form_state);
}
}
}
return $form;
}
/**
* Checks if user is allowed to continue with plugin action.
*
* @param int $window
* Number of seconds in the time window for this event.
*
* @return bool
* Whether user is allowed to continue.
*/
public function floodIsAllowed($window = '') {
if (method_exists($this->validatePlugin, 'floodIsAllowed')) {
return $this->validatePlugin
->floodIsAllowed($window);
}
return TRUE;
}
/**
* Validate form.
*
* @param array $form
* The form array structure.
* @param array $form_state
* The current form state array.
*
* @return bool
* Whether form passes validation or not.
*/
public function validateForm(array $form, array &$form_state) {
return $this->validatePlugin
->validateForm($form, $form_state);
}
/**
* Return process error messages.
*
* @return array
* Error messages.
*/
public function getErrorMessages() {
return $this->validatePlugin
->getErrorMessages();
}
/**
* Invoke submitForm() on plugins.
*
* @param array $form
* The form array structure.
* @param array $form_state
* The current form state array.
*
* @return bool
* Whether the validate plugin is complete. FALSE will cause
* tfa_form_submit() to rebuild the form for multi-step.
*/
public function submitForm(array $form, array &$form_state) {
// Handle fallback if set.
if ($this->fallback && isset($form_state['values']['fallback']) && $form_state['values']['op'] === $form_state['values']['fallback']) {
// Change context to next fallback and reset validatePlugin.
$this->context['plugins']['validate'] = array_shift($this->context['plugins']['fallback']);
$plugin = $this->fallbackPlugins[$this->context['plugins']['validate']];
$this->validatePlugin = $plugin;
if (empty($this->context['plugins']['fallback'])) {
$this->fallback = FALSE;
}
// Record which plugin is activated as fallback.
$this->context['active_fallback'] = $this->context['plugins']['validate'];
}
elseif (method_exists($this->validatePlugin, 'submitForm')) {
// Check if plugin is complete.
$this->complete = $this->validatePlugin
->submitForm($form, $form_state);
}
// Allow login plugins to handle form submit.
if (!empty($this->loginPlugins)) {
foreach ($this->loginPlugins as $class) {
if (method_exists($class, 'submitForm')) {
$class
->submitForm($form, $form_state);
}
}
}
return $this->complete;
}
/**
* Begin the TFA process.
*/
public function begin() {
// Invoke begin method on send validation plugins.
if (method_exists($this->validatePlugin, 'begin')) {
$this->validatePlugin
->begin();
}
}
/**
* Whether the TFA process has any fallback process.
*
* @return bool
* Whether the TFA process has any fallback process.
*/
public function hasFallback() {
return $this->fallback;
}
/**
* Whether it is a fallback process.
*
* @return bool
* Whether it is a fallback process.
*/
public function isFallback() {
return isset($this->context['active_fallback']);
}
/**
* Return TFA context.
*
* @return array
* TFA context.
*/
public function getContext() {
if (method_exists($this->validatePlugin, 'getPluginContext')) {
$plugin_context = $this->validatePlugin
->getPluginContext();
$this->context['validate_context'] = $plugin_context;
}
return $this->context;
}
/**
* Run TFA process finalization.
*/
public function finalize() {
// Invoke plugin finalize.
if (method_exists($this->validatePlugin, 'finalize')) {
$this->validatePlugin
->finalize();
}
// Allow login plugins to act during finalization.
if (!empty($this->loginPlugins)) {
foreach ($this->loginPlugins as $class) {
if (method_exists($class, 'finalize')) {
$class
->finalize();
}
}
}
}
}
/**
* Class TfaSetup.
*/
class TfaSetup {
/**
* Plugin being set up.
*
* @var TfaBasePlugin
*/
protected $setupPlugin;
/**
* Context of current TFA process.
*
* @var array
*/
protected $context;
/**
* TFA Setup constructor.
*
* @param TfaBasePlugin $setup_plugin
* Plugin being set up.
* @param array $context
* Context of TFA process.
* Must include key:
* - 'uid'
* Account uid of user in TFA process.
*/
public function __construct(TfaBasePlugin $setup_plugin, array $context) {
$this->setupPlugin = $setup_plugin;
$this->context = $context;
}
/**
* Run any begin setup processes.
*/
public function begin() {
// Invoke begin method on setup plugin.
if (method_exists($this->setupPlugin, 'begin')) {
$this->setupPlugin
->begin();
}
}
/**
* Get plugin form.
*
* @param array $form
* The form array structure.
* @param array $form_state
* The current form state array.
*
* @return array
* Form array structure.
*/
public function getForm(array $form, array &$form_state) {
return $this->setupPlugin
->getSetupForm($form, $form_state);
}
/**
* Validate form.
*
* @param array $form
* The form array structure.
* @param array $form_state
* The current form state array.
*
* @return bool
* Whether form passes validation or not.
*/
public function validateForm(array $form, array &$form_state) {
return $this->setupPlugin
->validateSetupForm($form, $form_state);
}
/**
* Return process error messages.
*
* @return array
* Error messages.
*/
public function getErrorMessages() {
return $this->setupPlugin
->getErrorMessages();
}
/**
* Form submission handler.
*
* @param array $form
* The form array structure.
* @param array $form_state
* The current form state array.
*
* @return bool
* Whether the form submission succeeded.
*/
public function submitForm(array $form, array &$form_state) {
return $this->setupPlugin
->submitSetupForm($form, $form_state);
}
/**
* Return TFA context.
*
* @return array
* TFA context.
*/
public function getContext() {
if (method_exists($this->setupPlugin, 'getPluginContext')) {
$plugin_context = $this->setupPlugin
->getPluginContext();
$this->context['setup_context'] = $plugin_context;
}
return $this->context;
}
}
/**
* Base plugin class.
*/
abstract class TfaBasePlugin {
const CRYPT_VERSION = '1';
/**
* TFA code.
*
* @var string
*/
protected $code;
/**
* Code Length.
*
* @var int
*/
protected $codeLength;
/**
* Context of current TFA process.
*
* @var array
*/
protected $context;
/**
* Error messages.
*
* @var array
*/
protected $errorMessages = array();
/**
* Code is valid.
*
* @var bool
*/
protected $isValid;
/**
* Encryption key.
*
* @var string
*/
protected $encryptionKey;
/**
* Plugin constructor.
*
* @param array $context
* Context of current TFA process.
* Must include key:
* - 'uid'
* Account uid of user in TFA process.
* May include keys:
* - 'validate_context'
* Plugin-specific context for use during Tfa validation.
* - 'setup_context'
* Plugin-specific context for use during TfaSetup.
*/
public function __construct(array $context = array()) {
$this->context = $context;
// Default code length is 6.
$this->codeLength = 6;
$this->isValid = FALSE;
}
/**
* Determine if the plugin can run for the current TFA context.
*
* @return bool
* Whether the plugin can run.
*/
public function ready() {
return TRUE;
}
/**
* Get error messages suitable for form_set_error().
*
* @return array
* Error messages.
*/
public function getErrorMessages() {
return $this->errorMessages;
}
/**
* Submit form.
*
* @param array $form
* The form array structure.
* @param array $form_state
* The current form state array.
*
* @return bool
* Whether plugin form handling is complete. Plugins should return FALSE to
* invoke multi-step.
*/
public function submitForm(array $form, array &$form_state) {
return $this->isValid;
}
/**
* Validate code.
*
* Note, plugins overriding validate() should be sure to set isValid property
* correctly or else also override submitForm().
*
* @param string $code
* Code to be validated.
*
* @return bool
* Whether code is valid.
*/
protected function validate($code) {
if ($this
->timingSafeEquals((string) $code, (string) $this->code)) {
$this->isValid = TRUE;
return TRUE;
}
else {
return FALSE;
}
}
/**
* A timing safe equals comparison.
*
* More info here: http://blog.ircmaxell.com/2014/11/its-all-about-time.html.
*
* @param string $safeString
* The internal (safe) value to be checked.
* @param string $userString
* The user submitted (unsafe) value.
*
* @return bool
* True if the two strings are identical.
*/
private function timingSafeEquals($safeString, $userString) {
if (function_exists('hash_equals')) {
return hash_equals($safeString, $userString);
}
$safeLen = strlen($safeString);
$userLen = strlen($userString);
if ($userLen != $safeLen) {
return FALSE;
}
$result = 0;
for ($i = 0; $i < $userLen; ++$i) {
$result |= ord($safeString[$i]) ^ ord($userString[$i]);
}
// They are only identical strings if $result is exactly 0.
return $result === 0;
}
/**
* Generate a random string of characters of length $this->codeLength.
*
* @return string
* Generated random string of characters.
*/
protected function generate() {
$string = '';
do {
$chars = strtolower(base64_encode(drupal_random_bytes($this->codeLength)));
// Remove some characters that are more difficult to distinguish or type.
$string .= strtr($chars, array(
'+' => '',
'/' => '',
'=' => '',
'-' => '',
'_' => '',
'0' => '',
'o' => '',
));
} while (strlen($string) <= $this->codeLength);
return substr($string, 0, $this->codeLength);
}
/**
* Encrypt a plaintext string.
*
* Should be used when writing codes to storage.
*
* @param string $text
* The plaintext to be encrypted.
*
* @return string
* The encrypted text.
*/
protected function encrypt($text) {
// Backwards compatibility with Mcrypt.
if (!extension_loaded('openssl') && extension_loaded('mcrypt')) {
return $this
->encryptWithMcrypt($text);
}
$iv = drupal_random_bytes(16);
// Using 1 instead of the constant OPENSSL_RAW_DATA, for PHP 5.3.
$ciphertext = openssl_encrypt($text, 'aes-256-cbc', $this->encryptionKey, 1, $iv);
$crypto_data = array(
'version' => self::CRYPT_VERSION,
'iv_base64' => base64_encode($iv),
'ciphertext_base64' => base64_encode($ciphertext),
);
$json_encoded_crypto_data = drupal_json_encode($crypto_data);
return $json_encoded_crypto_data;
}
/**
* Encrypt using the deprecated Mcrypt extension.
*
* @param string $text
* The text to encrypt.
*
* @return string
* The text encrypted using Mcrypt.
*/
protected function encryptWithMcrypt($text) {
$td = mcrypt_module_open('rijndael-128', '', 'cbc', '');
$iv = drupal_random_bytes(mcrypt_enc_get_iv_size($td));
$key = substr($this->encryptionKey, 0, mcrypt_enc_get_key_size($td));
mcrypt_generic_init($td, $key, $iv);
// Encrypt with message length so decryption can return message without
// padding.
$text = strlen($text) . '|' . $text;
$data = mcrypt_generic($td, $text);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return $iv . $data;
}
/**
* Decrypt a encrypted string.
*
* Should be used when reading codes from storage.
*
* @param string $data
* The encrypted text.
*
* @return string
* The plaintext, or empty string on failure.
*/
protected function decrypt($data) {
$crypto_data = drupal_json_decode($data);
if (empty($crypto_data['version']) || empty($crypto_data['iv_base64']) || empty($crypto_data['ciphertext_base64'])) {
// Backwards compatibility with the old Mcrypt scheme.
if (extension_loaded('mcrypt')) {
return $this
->decryptLegacyDataWithMcrypt($data);
}
if (extension_loaded('openssl')) {
return $this
->decryptLegacyDataWithOpenSSL($data);
}
return '';
}
$iv = base64_decode($crypto_data['iv_base64']);
$ciphertext = base64_decode($crypto_data['ciphertext_base64']);
return openssl_decrypt($ciphertext, 'aes-256-cbc', $this->encryptionKey, TRUE, $iv);
}
/**
* Decrypt using the deprecated Mcrypt extension.
*
* @param string $data
* The data to be decrypted.
*
* @return string
* The plaintext, or empty string on failure.
*/
protected function decryptLegacyDataWithMcrypt($data) {
$td = mcrypt_module_open('rijndael-128', '', 'cbc', '');
$iv = substr($data, 0, mcrypt_enc_get_iv_size($td));
$data = substr($data, mcrypt_enc_get_iv_size($td));
$key = substr($this->encryptionKey, 0, mcrypt_enc_get_key_size($td));
mcrypt_generic_init($td, $key, $iv);
$decrypted_text = mdecrypt_generic($td, $data);
// Return only the message and none of its padding.
list($length, $padded_data) = explode('|', $decrypted_text, 2);
$text = substr($padded_data, 0, $length);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return $text;
}
/**
* Use OpenSSL to decrypt data that was originally encrypted with Mcrypt.
*
* As used by an earlier version of this module.
*
* @param string $data
* The data to be decrypted.
*
* @return string
* The plaintext, or empty string on failure.
*
* phpcs:disable Drupal.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
*/
protected function decryptLegacyDataWithOpenSSL($data) {
// Based on return value of mcrypt_enc_get_key_size($td).
$key_size = 32;
// Based on return value of mcrypt_enc_get_iv_size($td).
$iv_size = 16;
$key = substr($this->encryptionKey, 0, $key_size);
$iv = substr($data, 0, $iv_size);
$data = substr($data, $iv_size);
// Using 3 instead of the constant OPENSSL_NO_PADDING, for PHP 5.3.
$decrypted_text = openssl_decrypt($data, 'aes-256-cbc', $key, 3, $iv);
// Return only the message and none of its padding.
if (strpos($decrypted_text, '|') !== FALSE) {
list($length, $padded_data) = explode('|', $decrypted_text, 2);
$decrypted_text = substr($padded_data, 0, $length);
return $decrypted_text;
}
else {
return '';
}
}
}
/**
* Interface TfaValidationPluginInterface.
*
* Validation plugins interact with the Tfa form processes to provide code entry
* and validate submitted codes.
*/
interface TfaValidationPluginInterface {
/**
* Get TFA process form from plugin.
*
* @param array $form
* The form array structure.
* @param array $form_state
* The current form state array.
*
* @return array
* Form API array.
*/
public function getForm(array $form, array &$form_state);
/**
* Validate form.
*
* @param array $form
* The form array structure.
* @param array $form_state
* The current form state array.
*
* @return bool
* Whether form passes validation or not.
*/
public function validateForm(array $form, array &$form_state);
}
/**
* Interface TfaLoginPluginInterface.
*
* Login plugins interact with the Tfa loginAllowed() process prior to starting
* a TFA process.
*/
interface TfaLoginPluginInterface {
/**
* Whether authentication should be interrupted.
*
* @return bool
* Indicates whether authentication should be interrupted.
*/
public function loginAllowed();
}
/**
* Interface TfaSendPluginInterface.
*
* Send plugins interact with the Tfa begin() process to communicate a code
* during the start of the TFA process.
*
* Implementations of a send plugin should also be a validation plugin.
*/
interface TfaSendPluginInterface {
/**
* TFA process begin.
*/
public function begin();
}
/**
* Interface TfaSetupPluginInterface.
*
* Setup plugins are used by TfaSetup for configuring a plugin.
*
* Implementations of a begin plugin should also be a validation plugin.
*/
interface TfaSetupPluginInterface {
/**
* Get setup form.
*
* @param array $form
* The form array structure.
* @param array $form_state
* The current form state array.
*
* @return array
* Form array structure.
*/
public function getSetupForm(array $form, array &$form_state);
/**
* Validate setup form.
*
* @param array $form
* The form array structure.
* @param array $form_state
* The current form state array.
*
* @return bool
* Indicates whether the form is valid.
*/
public function validateSetupForm(array $form, array &$form_state);
/**
* Submit setup form.
*
* @param array $form
* The form array structure.
* @param array $form_state
* The current form state array.
*
* @return bool
* Indicates whether the form submission succeeded.
*/
public function submitSetupForm(array $form, array &$form_state);
}
Classes
Name | Description |
---|---|
Tfa | Class Tfa. |
TfaBasePlugin | Base plugin class. |
TfaSetup | Class TfaSetup. |
Interfaces
Name | Description |
---|---|
TfaLoginPluginInterface | Interface TfaLoginPluginInterface. |
TfaSendPluginInterface | Interface TfaSendPluginInterface. |
TfaSetupPluginInterface | Interface TfaSetupPluginInterface. |
TfaValidationPluginInterface | Interface TfaValidationPluginInterface. |