[FEATURE] Add native rsa protection to password fields 86/41286/9
authorNicole Cordes <typo3@cordes.co>
Wed, 15 Jul 2015 12:31:12 +0000 (14:31 +0200)
committerWouter Wolters <typo3@wouterwolters.nl>
Fri, 7 Aug 2015 13:06:36 +0000 (15:06 +0200)
This patch adds rsa encryption to password fields. It introduces an own
rsaInput render type which turns fields into true password fields and
removes JavaScript default value '********'.
Furthermore the rsa Api is adjusted to handle the incoming data
correctly.

Releases: master
Resolves: #68166
Change-Id: I4eb8186f77e5b9215310f65d606bc49c71b19880
Reviewed-on: http://review.typo3.org/41286
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
15 files changed:
typo3/sysext/backend/Classes/Controller/EditDocumentController.php
typo3/sysext/backend/Classes/Form/Element/InputElement.php
typo3/sysext/backend/Classes/Form/NodeFactory.php
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngineValidation.js
typo3/sysext/backend/Resources/Public/JavaScript/jsfunc.tbe_editor.js
typo3/sysext/core/Documentation/Changelog/7.4/Feature-67932-RsaauthApiRewrite.rst
typo3/sysext/core/Documentation/Changelog/master/Feature-68166-RenderTypeForRsaEncryptedInputFields.rst [new file with mode: 0644]
typo3/sysext/rsaauth/Classes/Form/Element/RsaInputElement.php [new file with mode: 0644]
typo3/sysext/rsaauth/Classes/Hook/DecryptionHook.php [new file with mode: 0644]
typo3/sysext/rsaauth/Classes/RsaEncryptionDecoder.php
typo3/sysext/rsaauth/Configuration/TCA/Overrides/be_users.php [new file with mode: 0644]
typo3/sysext/rsaauth/Configuration/TCA/Overrides/fe_users.php [new file with mode: 0644]
typo3/sysext/rsaauth/Resources/Public/JavaScript/RsaEncryptionModule.js
typo3/sysext/rsaauth/ext_localconf.php

index 31d50a9..137cf92 100644 (file)
@@ -690,7 +690,7 @@ class EditDocumentController {
                $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
                $pageRenderer->addInlineLanguageLabelFile('EXT:lang/locallang_alt_doc.xlf');
                $this->doc->setModuleTemplate('EXT:backend/Resources/Private/Templates/alt_doc.html');
-               $this->doc->form = '<form action="' . htmlspecialchars($this->R_URI) . '" method="post" enctype="multipart/form-data" name="editform" onsubmit="document.editform._scrollPosition.value=(document.documentElement.scrollTop || document.body.scrollTop); return TBE_EDITOR.checkSubmit(1);">';
+               $this->doc->form = '<form action="' . htmlspecialchars($this->R_URI) . '" method="post" enctype="multipart/form-data" name="editform" onsubmit="document.editform._scrollPosition.value=(document.documentElement.scrollTop || document.body.scrollTop); return TBE_EDITOR.checkAndDoSubmit(1);">';
                // override the default jumpToUrl
                $this->doc->JScodeArray['jumpToUrl'] = '
                        function jumpToUrl(URL,formEl) {
index c5c7e17..ca06369 100644 (file)
@@ -174,9 +174,6 @@ class InputElement extends AbstractFormElement {
                if (isset($config['max']) && (int)$config['max'] > 0) {
                        $attributes['maxlength'] = (int)$config['max'];
                }
-               if (!empty($styles)) {
-                       $attributes['style'] = implode(' ', $styles);
-               }
                if (!empty($classes)) {
                        $attributes['class'] = implode(' ', $classes);
                }
index 71ee647..8eb2a98 100644 (file)
@@ -49,6 +49,8 @@ class NodeFactory {
                'check' => Element\CheckboxElement::class,
                'group' => Element\GroupElement::class,
                'input' => Element\InputElement::class,
+               // rsaInput is defined with a fallback so extensions can use it even if ext:rsaauth is not loaded
+               'rsaInput' => Element\InputElement::class,
                'imageManipulation' => Element\ImageManipulationElement::class,
                'none' => Element\NoneElement::class,
                'radio' => Element\RadioElement::class,
index b72d025..9ab52a1 100644 (file)
@@ -636,6 +636,11 @@ define('TYPO3/CMS/Backend/FormEngine', ['jquery'], function ($) {
                        // change class and value
                        $parent.find('.t3js-charcounter span').removeClass().addClass(maxlengthProperties.labelClass).text(TBE_EDITOR.labels.remainingCharacters.replace('{0}', maxlengthProperties.remainingCharacters))
                });
+               $(':password').on('focus', function() {
+                       $(this).attr('type', 'text').select();
+               }).on('blur', function() {
+                       $(this).attr('type', 'password');
+               });
        };
 
        /**
index 1c6823e..118023f 100644 (file)
@@ -130,7 +130,8 @@ define('TYPO3/CMS/Backend/FormEngineValidation', ['jquery', 'TYPO3/CMS/Backend/F
                                for (var i = 0; i < evalList.length; i++) {
                                        value = FormEngineValidation.formatValue(evalList[i], value, config)
                                }
-                               if (value.length) {
+                               // Prevent password fields to be overwritten with original value
+                               if (value.length && $humanReadableField.attr('type') != 'password') {
                                        $humanReadableField.val(value);
                                }
                                if ($checkboxField.length) {
index 0f098a4..08b885e 100644 (file)
@@ -254,7 +254,9 @@ var TBE_EDITOR = {
                // Set a short timeout to allow other JS processes to complete, in particular those from
                // EXT:backend/Resources/Public/JavaScript/FormEngine.js (reference: http://forge.typo3.org/issues/58755).
                // TODO: This should be solved in a better way when this script is refactored.
-               window.setTimeout('document[TBE_EDITOR.formname].submit()', 10);
+               window.setTimeout(function() {
+                       document.getElementsByName(TBE_EDITOR.formname).submit();
+               }, 10);
        },
        split: function(theStr1, delim, index) {
                var theStr = ""+theStr1;
index 664366a..17ac8ed 100644 (file)
@@ -51,7 +51,8 @@ Decode
 ------
 
 To decode your data you can use the method ``TYPO3\CMS\Rsaauth\RsaEncryptionDecoder::decrypt`` which can
-either handle a string or an array as parameter.
+either handle a string or an array as parameter. Data that is handled by \TYPO3\CMS\Core\DataHandling\DataHandler will
+be decoded automatically before processing.
 
 Notice: A RSA public key can only be used once to decrypt data. If you encrypt multiple fields in your form
 you have to pass an array to the decrypt function with all data you want to decrypt. The function parses the
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-68166-RenderTypeForRsaEncryptedInputFields.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-68166-RenderTypeForRsaEncryptedInputFields.rst
new file mode 100644 (file)
index 0000000..b232989
--- /dev/null
@@ -0,0 +1,23 @@
+===========================================================
+Feature: #67932 - RenderType for rsa encrypted input fields
+===========================================================
+
+Description
+===========
+
+The rsaauth extension defines an own render type for TCA input fields. Those fields will be encrypted before submitting the form.
+
+
+Impact
+======
+
+In the Backend password fields for backend and frontend users are automatically encoded before the form is submitted.
+
+Usage
+=====
+
+To encrypt your own TCA fields you can add define the render type ``rsaInput``.
+
+.. code-block:: php
+
+$GLOBALS['TCA']['be_users']['columns']['password']['config']['renderType'] = 'rsaInput';
diff --git a/typo3/sysext/rsaauth/Classes/Form/Element/RsaInputElement.php b/typo3/sysext/rsaauth/Classes/Form/Element/RsaInputElement.php
new file mode 100644 (file)
index 0000000..fd7b199
--- /dev/null
@@ -0,0 +1,178 @@
+<?php
+namespace TYPO3\CMS\Rsaauth\Form\Element;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
+use TYPO3\CMS\Backend\Form\NodeFactory;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Backend\Utility\IconUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
+
+/**
+ * Generation of form element of the type rsaInput
+ */
+class RsaInputElement extends AbstractFormElement {
+
+       /**
+        * This will render a single-line input form field, possibly with various control/validation features
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $languageService = $this->getLanguageService();
+
+               $table = $this->globalOptions['table'];
+               $fieldName = $this->globalOptions['fieldName'];
+               $row = $this->globalOptions['databaseRow'];
+               $parameterArray = $this->globalOptions['parameterArray'];
+               $resultArray = $this->initializeResultArray();
+               $resultArray['requireJsModules'] = array('TYPO3/CMS/Rsaauth/RsaEncryptionModule');
+
+               $config = $parameterArray['fieldConf']['config'];
+               $specConf = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
+               $size = MathUtility::forceIntegerInRange($config['size'] ?: $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
+               $evalList = GeneralUtility::trimExplode(',', $config['eval'], TRUE);
+               $classes = array();
+               $attributes = array(
+                       'type' => 'text',
+                       'data-rsa-encryption' => $parameterArray['itemFormElID'] . '_hidden',
+                       'value' => '',
+               );
+
+               // readonly
+               if ($this->isGlobalReadonly() || $config['readOnly']) {
+                       $itemFormElValue = $parameterArray['itemFormElValue'];
+                       $options = $this->globalOptions;
+                       $options['parameterArray'] = array(
+                               'fieldConf' => array(
+                                       'config' => $config,
+                               ),
+                               'itemFormElValue' => $itemFormElValue,
+                       );
+                       $options['renderType'] = 'none';
+                       /** @var NodeFactory $nodeFactory */
+                       $nodeFactory = $this->globalOptions['nodeFactory'];
+                       return $nodeFactory->create($options)->render();
+               }
+
+               // @todo: The whole eval handling is a mess and needs refactoring
+               foreach ($evalList as $func) {
+                       switch ($func) {
+                               case 'required':
+                                       $attributes['data-formengine-validation-rules'] = $this->getValidationDataAsJsonString(array('required' => TRUE));
+                                       break;
+                               case 'password':
+                                       $attributes['type'] = 'password';
+                                       $attributes['value'] = '********';
+                                       break;
+                               default:
+                                       // @todo: This is ugly: The code should find out on it's own whether a eval definition is a
+                                       // @todo: keyword like "date", or a class reference. The global registration could be dropped then
+                                       // Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
+                                       if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
+                                               if (class_exists($func)) {
+                                                       $evalObj = GeneralUtility::makeInstance($func);
+                                                       if (method_exists($evalObj, 'deevaluateFieldValue')) {
+                                                               $_params = array(
+                                                                       'value' => $parameterArray['itemFormElValue']
+                                                               );
+                                                               $parameterArray['itemFormElValue'] = $evalObj->deevaluateFieldValue($_params);
+                                                       }
+                                               }
+                                       }
+                       }
+               }
+               $evalList = array_filter($evalList, function($value) {
+                       return $value !== 'password';
+               });
+
+               $paramsList = array(
+                       'field' => $parameterArray['itemFormElName'],
+                       'evalList' => implode(',', $evalList),
+                       'is_in' => trim($config['is_in']),
+               );
+               // set classes
+               $classes[] = 'form-control';
+               $classes[] = 't3js-clearable';
+               $classes[] = 'hasDefaultValue';
+
+               // calculate attributes
+               $attributes['data-formengine-validation-rules'] = $this->getValidationDataAsJsonString($config);
+               $attributes['data-formengine-input-params'] = json_encode($paramsList);
+               $attributes['id'] = str_replace('.', '', uniqid('formengine-input-', TRUE));
+               $attributes['name'] = $parameterArray['itemFormElName'] . '_hr';
+               if (isset($config['max']) && (int)$config['max'] > 0) {
+                       $attributes['maxlength'] = (int)$config['max'];
+               }
+               if (!empty($classes)) {
+                       $attributes['class'] = implode(' ', $classes);
+               }
+               if (isset($config['max']) && (int)$config['max'] > 0) {
+                       $attributes['maxlength'] = (int)$config['max'];
+               }
+
+               // This is the EDITABLE form field.
+               $placeholderValue = $this->getPlaceholderValue($table, $config, $row);
+               if (!empty($placeholderValue)) {
+                       $attributes['placeholder'] = trim($languageService->sL($placeholderValue));
+               }
+
+               // Build the attribute string
+               $attributeString = '';
+               foreach ($attributes as $attributeName => $attributeValue) {
+                       $attributeString .= ' ' . $attributeName . '="' . htmlspecialchars($attributeValue) . '"';
+               }
+
+               $html = '
+                       <input'
+                       . $attributeString
+                       . $parameterArray['onFocus'] . ' />';
+
+               // This is the ACTUAL form field - values from the EDITABLE field must be transferred to this field which is the one that is written to the database.
+               $html .= '<input type="hidden" id="' . $parameterArray['itemFormElID'] . '_hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($parameterArray['itemFormElValue']) . '" />';
+
+               // Going through all custom evaluations configured for this field
+               // @todo: Similar to above code!
+               foreach ($evalList as $evalData) {
+                       if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$evalData])) {
+                               if (class_exists($evalData)) {
+                                       $evalObj = GeneralUtility::makeInstance($evalData);
+                                       if (method_exists($evalObj, 'returnFieldJS')) {
+                                               $resultArray['extJSCODE'] .= LF . 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($evalData) . '] = function(value) {' . $evalObj->returnFieldJS() . '}';
+                                       }
+                               }
+                       }
+               }
+
+               // Wrap a wizard around the item?
+               $html = $this->renderWizards(
+                       array($html),
+                       $config['wizards'],
+                       $table,
+                       $row,
+                       $fieldName,
+                       $parameterArray,
+                       $parameterArray['itemFormElName'] . '_hr', $specConf
+               );
+
+               // Add a wrapper to remain maximum width
+               $width = (int)$this->formMaxWidth($size);
+               $html = '<div class="form-control-wrap"' . ($width ? ' style="max-width: ' . $width . 'px"' : '') . '>' . $html . '</div>';
+               $resultArray['html'] = $html;
+               return $resultArray;
+       }
+
+}
diff --git a/typo3/sysext/rsaauth/Classes/Hook/DecryptionHook.php b/typo3/sysext/rsaauth/Classes/Hook/DecryptionHook.php
new file mode 100644 (file)
index 0000000..3570532
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+namespace TYPO3\CMS\Rsaauth\Hook;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\DataHandling\DataHandler;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Rsaauth\RsaEncryptionDecoder;
+
+/**
+ * Class that hooks into DataHandler and decrypts rsa encrypted data
+ */
+class DecryptionHook {
+
+       /**
+        * @param array $incomingFieldArray
+        * @param string $table
+        * @param int $id
+        * @param DataHandler $parentObject
+        */
+       public function processDatamap_preProcessFieldArray(&$incomingFieldArray, $table, $id, $parentObject) {
+               $serializedString = serialize($incomingFieldArray);
+               if (strpos($serializedString, 'rsa:') === FALSE) {
+                       return;
+               }
+
+               $rsaEncryptionDecoder = GeneralUtility::makeInstance(RsaEncryptionDecoder::class);
+               $incomingFieldArray = $rsaEncryptionDecoder->decrypt($incomingFieldArray);
+       }
+
+}
index 0ad0cf9..7f723bc 100644 (file)
@@ -14,6 +14,8 @@ namespace TYPO3\CMS\Rsaauth;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Utility\StringUtility;
+
 /**
  * This class decodes rsa protected data
  */
@@ -44,17 +46,7 @@ class RsaEncryptionDecoder implements \TYPO3\CMS\Core\SingletonInterface {
                }
 
                $decryptedData = is_array($data) ? $data : array($data);
-
-               foreach ($decryptedData as $key => $value) {
-                       if (substr($value, 0, 4) !== 'rsa:') {
-                               continue;
-                       }
-
-                       $decryptedValue = $this->getBackend()->decrypt($this->getKey(), substr($value, 4));
-                       if ($decryptedValue !== NULL) {
-                               $decryptedData[$key] = $decryptedValue;
-                       }
-               }
+               $decryptedData = $this->decryptDataArray($decryptedData);
                $this->getStorage()->put(NULL);
 
                return is_array($data) ? $decryptedData : $decryptedData[0];
@@ -68,6 +60,33 @@ class RsaEncryptionDecoder implements \TYPO3\CMS\Core\SingletonInterface {
        }
 
        /**
+        * @param array $data
+        * @return array
+        */
+       protected function decryptDataArray(array $data) {
+               foreach ($data as $key => $value) {
+                       if (empty($value)) {
+                               continue;
+                       }
+                       if (is_array($value)) {
+                               $data[$key] = $this->decryptDataArray($value);
+                               continue;
+                       }
+
+                       if (!StringUtility::beginsWith($value, 'rsa:')) {
+                               continue;
+                       }
+
+                       $decryptedValue = $this->getBackend()->decrypt($this->getKey(), substr($value, 4));
+                       if ($decryptedValue !== NULL) {
+                               $data[$key] = $decryptedValue;
+                       }
+               }
+
+               return $data;
+       }
+
+       /**
         * @return string
         */
        protected function getKey() {
diff --git a/typo3/sysext/rsaauth/Configuration/TCA/Overrides/be_users.php b/typo3/sysext/rsaauth/Configuration/TCA/Overrides/be_users.php
new file mode 100644 (file)
index 0000000..c7d0121
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+defined('TYPO3_MODE') or die();
+
+$GLOBALS['TCA']['be_users']['columns']['password']['config']['renderType'] = 'rsaInput';
diff --git a/typo3/sysext/rsaauth/Configuration/TCA/Overrides/fe_users.php b/typo3/sysext/rsaauth/Configuration/TCA/Overrides/fe_users.php
new file mode 100644 (file)
index 0000000..b19fc84
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+defined('TYPO3_MODE') or die();
+
+$GLOBALS['TCA']['fe_users']['columns']['password']['config']['renderType'] = 'rsaInput';
index c16e732..37b4650 100644 (file)
@@ -86,24 +86,30 @@ define('TYPO3/CMS/Rsaauth/RsaEncryptionModule', ['jquery', './RsaLibrary'], func
                        rsa.setPublic(publicKey[0], publicKey[1]);
                        RsaEncryption.$currentForm.find(':input[data-rsa-encryption]').each(function() {
                                var $this = $(this);
-                               var encryptedPassword = rsa.encrypt($this.val());
+                               var encryptedValue = rsa.encrypt($this.val());
                                var dataAttribute = $this.data('rsa-encryption');
+                               var rsaValue = 'rsa:' + hex2b64(encryptedValue);
 
                                if (!dataAttribute) {
-                                       $this.val('rsa:' + hex2b64(encryptedPassword));
+                                       $this.val(rsaValue);
                                } else {
                                        var $typo3Field = $('#' + dataAttribute);
-                                       $typo3Field.val('rsa:' + hex2b64(encryptedPassword));
+                                       $typo3Field.val(rsaValue);
                                        // Reset user password field to prevent it from being submitted
                                        $this.val('');
                                }
                        });
 
-                       // Create a hidden input field to fake pressing the submit button
-                       RsaEncryption.$currentForm.append('<input type="hidden" name="commandLI" value="Submit">');
-
-                       // Submit the form
-                       RsaEncryption.$currentForm.trigger('submit');
+                       // Try to fetch the field which submitted the form
+                       var $currentField = RsaEncryption.$currentForm.find('input[type=submit]:focus,input[type=image]:focus');
+                       if ($currentField.length === 1) {
+                               $currentField.trigger('click');
+                       } else {
+                               // Create a hidden input field to fake pressing the submit button
+                               RsaEncryption.$currentForm.append('<input type="hidden" name="commandLI" value="Submit">');
+                               // Submit the form
+                               RsaEncryption.$currentForm.trigger('submit');
+                       }
                }
        };
 
index e1f09ae..5539676 100644 (file)
@@ -46,3 +46,13 @@ $GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']['RsaPublicKeyGenerationControll
        \TYPO3\CMS\Rsaauth\Slot\UsernamePasswordProviderSlot::class,
        'getPageRenderer'
 );
+
+// Register automatic decryption in DataHandler
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['rsaauth'] = \TYPO3\CMS\Rsaauth\Hook\DecryptionHook::class;
+
+// Add own form element
+$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1436965601] = array(
+       'nodeName' => 'rsaInput',
+       'priority' => '70',
+       'class' => \TYPO3\CMS\Rsaauth\Form\Element\RsaInputElement::class,
+);