diff --git a/tfa.admin.inc b/tfa.admin.inc
index 28319af..c063e08 100644
--- a/tfa.admin.inc
+++ b/tfa.admin.inc
@@ -25,10 +25,10 @@ function tfa_admin_settings($form, $form_state) {
}
}
- // Check if mcrypt plugin is available.
- if (!extension_loaded('mcrypt')) {
+ // Check if openssl or mcrypt extensions are available.
+ if (!extension_loaded('openssl') && !extension_loaded('mcrypt')) {
// @todo allow alter in case of other encryption libs.
- drupal_set_message(t('The TFA module requires the PHP Mcrypt extension be installed on the web server. See the TFA help documentation for setup.', array('!link' => url('admin/help/tfa'))), 'error');
+ drupal_set_message(t('The TFA module requires one of the PHP OpenSSL or MCrypt extensions to be installed on the web server. See the TFA help documentation for setup.', array('!link' => url('admin/help/tfa'))), 'error');
return array();
}
diff --git a/tfa.inc b/tfa.inc
index bb9ab82..88f1760 100644
--- a/tfa.inc
+++ b/tfa.inc
@@ -376,6 +376,8 @@ class TfaSetup {
*/
abstract class TfaBasePlugin {
+ const CRYPT_VERSION = '1';
+
/**
* @var string
*/
@@ -502,12 +504,38 @@ abstract class TfaBasePlugin {
*
* Should be used when writing codes to storage.
*
- * @param string.
+ * @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);
+
+ return sprintf(
+ '%s|%s|%s',
+ self::CRYPT_VERSION,
+ $iv,
+ // Using 1 instead of the constant OPENSSL_RAW_DATA, for PHP 5.3.
+ openssl_encrypt($text, 'AES-256-CBC', $this->encryptionKey, 1, $iv)
+ );
+ }
+
+ /**
+ * Encrypt using the deprecated mcrypt extension.
+ *
+ * @param string $text
+ *
+ * @return string
+ */
+ protected function encryptWithMCrypt($text, $iv = null) {
$td = mcrypt_module_open('rijndael-128', '', 'cbc', '');
- $iv = drupal_random_bytes(mcrypt_enc_get_iv_size($td));
+ $iv = $iv ?: drupal_random_bytes(mcrypt_enc_get_iv_size($td));
$key = substr($this->encryptionKey, 0, mcrypt_enc_get_key_size($td));
@@ -528,10 +556,40 @@ abstract class TfaBasePlugin {
*
* Should be used when reading codes from storage.
*
- * @param string
- * @return string
+ * @param string $data
+ * The encrypted text.
+ *
+ * @return string|boolean
+ * The plaintext, or FALSE on failure.
*/
protected function decrypt($data) {
+ $version_prefix = self::CRYPT_VERSION . '|';
+ $version_match = strpos($data, $version_prefix) === 0;
+ // Backwards compatibility with the old MCrypt scheme.
+ if (!$version_match) {
+ if (extension_loaded('openssl')) {
+ return $this->decryptLegacyDataWithOpenSSL($data);
+ }
+ if (extension_loaded('mcrypt')) {
+ return $this->decryptLegacyDataWithMCrypt($data);
+ }
+ return FALSE;
+ }
+
+ list(, $iv, $data) = explode('|', $data, 3);
+
+ return openssl_decrypt($data, 'AES-256-CBC', $this->encryptionKey, TRUE, $iv);
+ }
+
+ /**
+ * Decrypt using the deprecated MCrypt extension.
+ *
+ * @param string $data
+ *
+ * @return string|boolean
+ * The plaintext, or FALSE on failure.
+ */
+ protected function decryptLegacyDataWithMCrypt($data) {
$td = mcrypt_module_open('rijndael-128', '', 'cbc', '');
$iv = substr($data, 0, mcrypt_enc_get_iv_size($td));
@@ -551,6 +609,64 @@ abstract class TfaBasePlugin {
return $text;
}
+ /**
+ * Use OpenSSL to decrypt data that was originally encrypted with MCrypt
+ * (by an earlier version of this module).
+ *
+ * @param string $data
+ *
+ * @return string|boolean
+ * The plaintext, or FALSE on failure.
+ */
+ protected function decryptLegacyDataWithOpenSSL($data) {
+ $key_size = 32; // Based on return value of mcrypt_enc_get_key_size($td).
+ $iv_size = 16; // Based on return value of mcrypt_enc_get_iv_size($td).
+ $key = substr($this->encryptionKey, 0, $key_size);
+ $iv = substr($data, 0, $iv_size);
+ // Using 3 instead of the constant OPENSSL_NO_PADDING, for PHP 5.3.
+ $options = 3;
+ $decrypted_data = openssl_decrypt($data, 'AES-256-CBC', $key, $options, $iv);
+
+ // Look for the message length component of the string.
+ $regex = '/[\d]+\|/';
+ preg_match_all($regex, $decrypted_data, $msg_len_matches);
+ if (empty($msg_len_matches[0])) {
+ return FALSE;
+ }
+ // There is usually null padding at the end of $decrypted_data. Since it is
+ // possible that the original string (pre-encryption) had one ore more nulls
+ // at the end, determine the maxumum number of characters that can be
+ // truncated from the end. We may end up truncating less than the maximum.
+ $possible_null_padding_length = 0;
+ $null_right_padding_regex = '/[\0]+/';
+ preg_match_all($null_right_padding_regex, $decrypted_data, $null_padding_matches);
+ if (!empty($null_padding_matches[0][0])) {
+ $possible_null_padding_length = strlen($null_padding_matches[0][0]);
+ }
+
+ // Outer loop: Find possible message length strings.
+ $start_text_offset = 0;
+ foreach ($msg_len_matches[0] as $i => $match_string) {
+ $match_string_length = strlen($match_string);
+ $pos = strpos($decrypted_data, $match_string, $start_text_offset);
+ $start_text_offset = $pos + $match_string_length;
+ $remaining_string = substr($decrypted_data, $pos + $match_string_length);
+ $possible_message_length = substr($match_string, 0, -1);
+ // Inner loop: Since random data is to the left of the message length string
+ // we want, verify the message length matches what the string says it should
+ // be. It is possible (although unlikely) that there is one or more digits
+ // in front of the length string - this code handles this use case.
+ while (strlen($possible_message_length) > 0) {
+ $test_length = strlen($remaining_string);
+ // Since it is possible for the original string (before encryption) to end
+ // with null bytes, take $possible_null_padding_length into consideration.
+ if (($possible_message_length <= $test_length) && ($possible_message_length >= ($test_length - $possible_null_padding_length))) {
+ return substr($decrypted_data, $start_text_offset, $possible_message_length);
+ }
+ $possible_message_length = substr($possible_message_length, 1);
+ }
+ }
+ return FALSE;
+ }
}
/**
diff --git a/tfa.install b/tfa.install
index cbf7943..9bd610d 100644
--- a/tfa.install
+++ b/tfa.install
@@ -26,13 +26,19 @@ function tfa_uninstall() {
*/
function tfa_requirements($phase) {
if ($phase == 'runtime') {
- if (!extension_loaded('mcrypt')) {
- $requirement_severity = REQUIREMENT_ERROR;
- $description = t('The TFA module requires the PHP Mcrypt extension be installed on the web server.');
+ if (!extension_loaded('openssl')) {
+ if (extension_loaded('mcrypt')) {
+ $requirement_severity = REQUIREMENT_WARNING;
+ $description = t('The TFA module recommends the PHP OpenSSL extension to be installed on the web server.');
+ }
+ else {
+ $requirement_severity = REQUIREMENT_ERROR;
+ $description = t('The TFA module requires either the PHP OpenSSL or MCrypt extensions to be installed on the web server.');
+ }
}
else {
$requirement_severity = REQUIREMENT_OK;
- $description= '';
+ $description = '';
}
$enabled = variable_get('tfa_enabled', 0);