You are here

abstract class TfaBasePlugin in Two-factor Authentication (TFA) 7.2

Base plugin class.

Hierarchy

Expanded class hierarchy of TfaBasePlugin

1 string reference to 'TfaBasePlugin'
tfa_admin_settings in ./tfa.admin.inc
Admin settings form.

File

./tfa.inc, line 426
TFA module classes.

View source
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 '';
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
TfaBasePlugin::$code protected property TFA code.
TfaBasePlugin::$codeLength protected property Code Length.
TfaBasePlugin::$context protected property Context of current TFA process.
TfaBasePlugin::$encryptionKey protected property Encryption key.
TfaBasePlugin::$errorMessages protected property Error messages.
TfaBasePlugin::$isValid protected property Code is valid.
TfaBasePlugin::CRYPT_VERSION constant
TfaBasePlugin::decrypt protected function Decrypt a encrypted string.
TfaBasePlugin::decryptLegacyDataWithMcrypt protected function Decrypt using the deprecated Mcrypt extension.
TfaBasePlugin::decryptLegacyDataWithOpenSSL protected function Use OpenSSL to decrypt data that was originally encrypted with Mcrypt.
TfaBasePlugin::encrypt protected function Encrypt a plaintext string.
TfaBasePlugin::encryptWithMcrypt protected function Encrypt using the deprecated Mcrypt extension.
TfaBasePlugin::generate protected function Generate a random string of characters of length $this->codeLength.
TfaBasePlugin::getErrorMessages public function Get error messages suitable for form_set_error().
TfaBasePlugin::ready public function Determine if the plugin can run for the current TFA context. 2
TfaBasePlugin::submitForm public function Submit form. 1
TfaBasePlugin::timingSafeEquals private function A timing safe equals comparison.
TfaBasePlugin::validate protected function Validate code.
TfaBasePlugin::__construct public function Plugin constructor. 3