diff --git a/encrypted_login.install b/encrypted_login.install index 95dd9ea34e7b51fd882477ad0191172d6147a341..f63f4533f70f4fc24cec47a45bf2b3985e3ceef4 100644 --- a/encrypted_login.install +++ b/encrypted_login.install @@ -11,47 +11,68 @@ use Drupal\Core\Database\Database; * Implements hook_schema(). */ function encrypted_login_schema() { - $schema['encrypted_login_keys'] = [ - 'description' => 'Stores the AES encryption keys.', - 'fields' => [ - 'key_id' => [ - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'Primary key for the table.', - ], - 'aes_key' => [ - 'type' => 'text', - 'size' => 'big', - 'not null' => TRUE, - 'description' => 'The AES encryption key (Base64-encoded).', - ], - 'created' => [ - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Timestamp when the key was created.', - ], - ], - 'primary key' => ['key_id'], - ]; - return $schema; + // We no longer need the encrypted_login_keys table since AES keys + // are generated client-side and not stored in the database. + return []; } /** * Implements hook_install(). */ function encrypted_login_install() { - // Generate a random 256-bit AES key. - $key = random_bytes(32); - - // Insert the key into the database. - \Drupal::database()->insert('encrypted_login_keys') - ->fields([ - 'aes_key' => base64_encode($key), - 'created' => \Drupal::time()->getRequestTime(), - ]) - ->execute(); - - \Drupal::logger('encrypted_login')->info('AES key generated and stored in the database.'); + // Generate RSA Key Pair with stronger key length and proper padding. + $config = [ + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + 'digest_alg' => 'sha256', + ]; + + try { + // Generate new key pair. + $res = openssl_pkey_new($config); + if ($res === FALSE) { + throw new \Exception('Failed to generate RSA key pair: ' . openssl_error_string()); + } + + // Export private key. + if (!openssl_pkey_export($res, $private_key)) { + throw new \Exception('Failed to export private key: ' . openssl_error_string()); + } + + // Get public key. + $key_details = openssl_pkey_get_details($res); + if ($key_details === FALSE) { + throw new \Exception('Failed to get key details: ' . openssl_error_string()); + } + $public_key = $key_details['key']; + + // Store RSA keys securely in state API. + \Drupal::state()->set('encrypted_login.rsa_private_key', $private_key); + \Drupal::state()->set('encrypted_login.rsa_public_key', $public_key); + + \Drupal::logger('encrypted_login')->info('RSA key pair successfully generated and stored.'); + } + catch (\Exception $e) { + \Drupal::logger('encrypted_login')->error('Failed to initialize encrypted login: @message', ['@message' => $e->getMessage()]); + throw $e; + } +} + +/** + * Implements hook_uninstall(). + */ +function encrypted_login_uninstall() { + // Clean up state variables. + \Drupal::state()->delete('encrypted_login.rsa_private_key'); + \Drupal::state()->delete('encrypted_login.rsa_public_key'); +} + +/** + * Remove encrypted_login_keys table and clean up old AES keys. + */ +function encrypted_login_update_8001() { + $schema = Database::getConnection()->schema(); + if ($schema->tableExists('encrypted_login_keys')) { + $schema->dropTable('encrypted_login_keys'); + } } diff --git a/encrypted_login.libraries.yml b/encrypted_login.libraries.yml index 43e53957b6eaea8263f5001b4ea15b2f56871bf0..d69e69456bc93ddb0272d974b00a7210c37ade89 100644 --- a/encrypted_login.libraries.yml +++ b/encrypted_login.libraries.yml @@ -3,5 +3,6 @@ encrypted_login: js: js/encrypted_login.js: {} https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js: {} + https://cdnjs.cloudflare.com/ajax/libs/jsencrypt/2.3.1/jsencrypt.min.js: {} dependencies: - core/jquery diff --git a/encrypted_login.module b/encrypted_login.module index 75e21890f1ccc48418935b40f5f48464a0c93b32..033e8e621f99b12020a7d3113094a2562f5ca21d 100644 --- a/encrypted_login.module +++ b/encrypted_login.module @@ -13,6 +13,12 @@ use Drupal\Core\Form\FormStateInterface; function encrypted_login_form_alter(&$form, FormStateInterface $form_state, $form_id) { if ($form_id === 'user_login_form') { // Add hidden fields for encrypted credentials. + $form['encrypted_aes_key'] = [ + '#type' => 'hidden', + '#default_value' => '', + '#attributes' => ['id' => 'edit-encrypted-aes-key'], + ]; + $form['encrypted_password'] = [ '#type' => 'hidden', '#default_value' => '', @@ -22,10 +28,10 @@ function encrypted_login_form_alter(&$form, FormStateInterface $form_state, $for // Attach the JavaScript library. $form['#attached']['library'][] = 'encrypted_login/encrypted_login'; - // Add custom validation as the first validator + // Add custom validation as the first validator. array_unshift($form['#validate'], 'encrypted_login_validate_encrypted_credentials'); - // Disable the default required validation for the original fields + // Disable the default required validation for the original fields. $form['pass']['#required'] = FALSE; } } @@ -34,60 +40,47 @@ function encrypted_login_form_alter(&$form, FormStateInterface $form_state, $for * Custom validation handler for encrypted credentials. */ function encrypted_login_validate_encrypted_credentials(array &$form, FormStateInterface $form_state) { + $encrypted_aes_key = $form_state->getValue('encrypted_aes_key'); $encrypted_password = $form_state->getValue('encrypted_password'); + $aes_key = ''; + $private_key = \Drupal::state()->get('encrypted_login.rsa_private_key'); + + // Decrypt the AES key using RSA. + if (!openssl_private_decrypt(base64_decode($encrypted_aes_key), $aes_key, $private_key)) { + \Drupal::logger('encrypted_login')->error('RSA decryption of AES key failed.'); + $form_state->setError($form, t('Authentication error. Please try again.')); + return; + } + + // Convert the decrypted base64 AES key to raw binary. + $raw_aes_key = base64_decode($aes_key); if (empty($encrypted_password)) { - \Drupal::logger('encrypted_login')->error('Empty encrypted credentials received'); + \Drupal::logger('encrypted_login')->error('Empty encrypted credentials received.'); $form_state->setError($form, t('Login credentials are required.')); return; } - // Fetch the AES key from the database. - $aes_key = encrypted_login_get_aes_key(); - - if (!$aes_key) { - \Drupal::logger('encrypted_login')->error('AES key not found.'); + if (!$raw_aes_key) { + \Drupal::logger('encrypted_login')->error('AES key decryption failed, invalid key.'); $form_state->setError($form, t('Authentication error. Please try again.')); return; } - // Decrypt password. - $password = encrypted_login_decrypt_aes($encrypted_password, $aes_key); - + // Decrypt the password using the raw AES key. + $password = encrypted_login_decrypt_aes($encrypted_password, $raw_aes_key); if (empty($password)) { \Drupal::logger('encrypted_login')->error('Decryption failed or empty values received.'); $form_state->setError($form, t('Invalid login credentials.')); return; } - // Set the decrypted values in the form state + // Set the decrypted password for further processing. $form_state->setValue('pass', $password); - // Remove the encrypted values to prevent them from interfering + // Remove the encrypted value. $form_state->unsetValue('encrypted_password'); - - // Clear any previous validation errors $form_state->clearErrors(); - -} - -/** - * Fetches the latest AES key from the database. - */ -function encrypted_login_get_aes_key() { - $result = \Drupal::database()->select('encrypted_login_keys', 'k') - ->fields('k', ['aes_key']) - ->orderBy('created', 'DESC') - ->range(0, 1) - ->execute() - ->fetchAssoc(); - - if ($result) { - // Log the fetched AES key. - \Drupal::logger('encrypted_login')->info('Fetched AES Key Base64: @aes_key', ['@aes_key' => $result['aes_key']]); - } - - return $result ? base64_decode($result['aes_key']) : FALSE; } /** @@ -95,48 +88,35 @@ function encrypted_login_get_aes_key() { */ function encrypted_login_decrypt_aes($encrypted_data, $key) { try { - // Validate input data if (empty($encrypted_data) || empty($key)) { - \Drupal::logger('encrypted_login')->error('Empty encrypted data or key provided'); + \Drupal::logger('encrypted_login')->error('Empty encrypted data or key provided.'); return FALSE; } - // First base64 decode of the entire encrypted data $decoded_data = base64_decode($encrypted_data, TRUE); if ($decoded_data === FALSE) { - \Drupal::logger('encrypted_login')->error('Invalid base64 encoded data'); + \Drupal::logger('encrypted_login')->error('Invalid base64 encoded data.'); return FALSE; } - // Split IV and ciphertext $parts = explode('::', $decoded_data, 2); if (count($parts) !== 2) { - \Drupal::logger('encrypted_login')->error('Invalid encrypted data format - missing separator'); + \Drupal::logger('encrypted_login')->error('Invalid encrypted data format.'); return FALSE; } - // Additional base64 decode for IV and ciphertext separately $iv = base64_decode($parts[0], TRUE); $ciphertext = base64_decode($parts[1], TRUE); - if ($iv === FALSE || $ciphertext === FALSE) { - \Drupal::logger('encrypted_login')->error('Invalid base64 encoding for IV or ciphertext'); + \Drupal::logger('encrypted_login')->error('Invalid base64 encoding for IV or ciphertext.'); return FALSE; } - // Ensure the IV is exactly 16 bytes if (strlen($iv) !== 16) { - \Drupal::logger('encrypted_login')->error('Invalid IV length after decode: @length bytes (expected 16)', ['@length' => strlen($iv)]); + \Drupal::logger('encrypted_login')->error('Invalid IV length after decode.'); return FALSE; } - // Log lengths for debugging - \Drupal::logger('encrypted_login')->debug('Key length: @length bytes, IV length: @iv_length bytes', [ - '@length' => strlen($key), - '@iv_length' => strlen($iv), - ]); - - // Decrypt the ciphertext $decrypted_data = openssl_decrypt( $ciphertext, 'aes-256-cbc', @@ -146,15 +126,14 @@ function encrypted_login_decrypt_aes($encrypted_data, $key) { ); if ($decrypted_data === FALSE) { - $error = openssl_error_string(); - \Drupal::logger('encrypted_login')->error('Decryption failed: @error', ['@error' => $error]); + \Drupal::logger('encrypted_login')->error('Decryption failed: ' . openssl_error_string()); return FALSE; } return $decrypted_data; } catch (\Exception $e) { - \Drupal::logger('encrypted_login')->error('Decryption error: @message', ['@message' => $e->getMessage()]); + \Drupal::logger('encrypted_login')->error('Decryption error: ' . $e->getMessage()); return FALSE; } } diff --git a/encrypted_login.routing.yml b/encrypted_login.routing.yml index 45e2eac6b520bd862395ea7f2aa6feb68a096da6..90b4680c10ffda2529316bb32500b483de642bb0 100644 --- a/encrypted_login.routing.yml +++ b/encrypted_login.routing.yml @@ -1,6 +1,6 @@ encrypted_login.get_aes_key: - path: '/encrypted_login/get_aes_key' + path: '/encrypted_login/getPublicKey' defaults: - _controller: '\Drupal\encrypted_login\Controller\EncryptedLoginController::getAesKey' + _controller: '\Drupal\encrypted_login\Controller\EncryptedLoginController::getPublicKey' requirements: _permission: 'access content' diff --git a/js/encrypted_login.js b/js/encrypted_login.js index ebb27e6e9b4e483b7ca171bdb20a10051a59b5d6..886b02ed7532812c9eeddb1dc17999209edeba2f 100644 --- a/js/encrypted_login.js +++ b/js/encrypted_login.js @@ -1,35 +1,72 @@ -(function ($, Drupal) { +(function ($, Drupal, once) { Drupal.behaviors.encryptedLogin = { attach: function (context, settings) { - // Fetch the AES key from the server. - $.get('/encrypted_login/get_aes_key', function (response) { - const aesKeyBase64 = response.key; - const aesKey = CryptoJS.enc.Base64.parse(aesKeyBase64); - - $('.user-login-form').on('submit', function (event) { - event.preventDefault(); - - const password = $(this).find('#edit-pass').val(); - - // Generate a 16-byte IV for password encryption. - const ivPassword = CryptoJS.lib.WordArray.random(16); - const encryptedPassword = CryptoJS.AES.encrypt( - CryptoJS.enc.Utf8.parse(password), - aesKey, - { iv: ivPassword } + async function initEncryption() { + try { + const response = await $.get('/encrypted_login/getPublicKey'); + const publicKey = response.public_key || response.key; + if (!publicKey) { + console.error("Failed to retrieve RSA public key."); + return; + } + + const cryptoKey = await window.crypto.subtle.generateKey( + { name: "AES-GCM", length: 256 }, + true, + ["encrypt", "decrypt"] ); - const encryptedPasswordCombined = CryptoJS.enc.Base64.stringify(ivPassword) + '::' + encryptedPassword.toString(); + const exportedKey = await window.crypto.subtle.exportKey("raw", cryptoKey); + const aesKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(exportedKey))); + + const aesKeyWordArray = CryptoJS.enc.Base64.parse(aesKeyBase64); + + once('encrypted-login', '.user-login-form', context).forEach(function (element) { + $(element).on('submit', async function (event) { + event.preventDefault(); + const $form = $(element); + const password = $form.find('#edit-pass').val(); + if (!password) { + console.error("No password entered."); + return; + } + try { + const iv = CryptoJS.lib.WordArray.random(16); + + const encrypted = CryptoJS.AES.encrypt( + CryptoJS.enc.Utf8.parse(password), + aesKeyWordArray, + { iv: iv } + ); + + const ivBase64 = CryptoJS.enc.Base64.stringify(iv); + const encryptedPasswordCombined = ivBase64 + "::" + encrypted.toString(); + const encryptedPasswordFinal = btoa(encryptedPasswordCombined); + + const jsEncrypt = new JSEncrypt(); + jsEncrypt.setPublicKey(publicKey); + const encryptedAesKey = jsEncrypt.encrypt(aesKeyBase64); + if (!encryptedAesKey) { + console.error("RSA encryption of AES key failed."); + return; + } - // Set encrypted values in hidden fields. - $('#edit-encrypted-password').val(btoa(encryptedPasswordCombined)); + $form.find('#edit-encrypted-password').val(encryptedPasswordFinal); + $form.find('#edit-encrypted-aes-key').val(encryptedAesKey); - // Clear plaintext fields. - $('#edit-pass').val(''); + $form.find('#edit-pass').val(''); + $form.off('submit'); + $form.submit(); + } catch (error) { + console.error("Encryption process failed:", error); + } + }); + }); + } catch (error) { + console.error("Failed to initialize encryption:", error); + } + } - // Submit the form. - this.submit(); - }); - }); + initEncryption(); } }; -})(jQuery, Drupal); +})(jQuery, Drupal, once); diff --git a/src/Controller/EncryptedLoginController.php b/src/Controller/EncryptedLoginController.php index a11d2c8f0bfe8a13015ff6536b97be7f1d3ec520..c81a054946379199e5502485a6c08a3b53a0dc7c 100644 --- a/src/Controller/EncryptedLoginController.php +++ b/src/Controller/EncryptedLoginController.php @@ -4,33 +4,55 @@ namespace Drupal\encrypted_login\Controller; use Drupal\Core\Controller\ControllerBase; use Symfony\Component\HttpFoundation\JsonResponse; +use Drupal\Core\State\StateInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** - * Provides a controller for handling AES key retrieval. + * Provides a controller for handling RSA public key retrieval. */ class EncryptedLoginController extends ControllerBase { /** - * Retrieves the latest AES key for client-side encryption. + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * Constructs a new EncryptedLoginController. + * + * @param \Drupal\Core\State\StateInterface $state + * The state service. + */ + public function __construct(StateInterface $state) { + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('state') + ); + } + + /** + * Retrieves the RSA public key for client-side encryption. * * @return \Symfony\Component\HttpFoundation\JsonResponse - * A JSON response containing the AES key or an error message. + * A JSON response containing the RSA public key or an error message. */ - public function getAesKey() { - // Fetch the latest AES key from the 'encrypted_login_keys' table. - $key = \Drupal::database()->select('encrypted_login_keys', 'k') - ->fields('k', ['aes_key']) - ->orderBy('created', 'DESC') - ->range(0, 1) - ->execute() - ->fetchField(); - - // Return the AES key if found, otherwise return an error response. - if ($key) { - return new JsonResponse(['key' => $key]); + public function getPublicKey(): JsonResponse { + // Fetch the RSA public key from Drupal's state API. + $public_key = $this->state->get('encrypted_login.rsa_public_key'); + + if ($public_key) { + return new JsonResponse(['public_key' => $public_key]); } - return new JsonResponse(['error' => 'AES key not found.'], 500); + return new JsonResponse(['error' => 'RSA public key not found.'], 500); } }