How SHA-256 and RSA Encryption Improve User Authentication in Web Applications

Related Blogs
Akhil Gupta
Akhil Gupta

Senior Software Engineer

7 min read

Introduction

Protecting user credentials is one of the top priorities when building secure web applications. Simple methods like Base64 encoding may obscure data, but they aren't encryption and can be easily reversed. To truly safeguard usernames and passwords, stronger mechanisms like SHA-256 hashing and RSA encryption are essential.

In this blog, we'll walk you through implementing a more secure approach—combining RSA's public/private key encryption with SHA-256 hashing. You'll also learn how to upgrade from basic Base64 to this robust model, enabling end-to-end encryption for login flows and ensuring sensitive data stays protected in transit.

Prerequisites

Before getting started, make sure you have the following:

  1. Ruby on Rails Setup:
    • Ruby on Rails 6 or above.
    • Devise gem for user authentication.
  2. Frontend:
    • Basic understanding of JavaScript for client-side encryption.
  3. Environment:
    • OpenSSL library for RSA key generation and handling encryption/decryption.
    • HTTPS protocol enabled for secure data transmission (important in production).

Let's start by setting up a demo Ruby on Rails application with Devise for authentication.

Step 1: Setting Up the Ruby on Rails Application with Devise

1. Create a new Rails application:

Copy Code
    
    rails new secure_auth_demo --skip-javascript
    cd secure_auth_demo
    

2. Add the Devise gem to your Gemfile:

Copy Code
    
    gem 'devise'
    

3. Install Devise:

Copy Code
    
    bundle install
    rails generate devise:install
    rails generate devise User
    rails db:migrate
    

4. Generate Devise views:

Copy Code
    
    rails generate devise:views
    

This will create all the necessary views for user authentication, which we will modify to integrate encryption.

5. Add the RSA public/private keys:

Generate RSA keys using OpenSSL:

Copy Code
    
    openssl genpkey -algorithm RSA -out private_key.pem -aes256
    openssl rsa -pubout -in private_key.pem -out public_key.pem
    

Save the public_key.pem and private_key.pem in your config/ directory.

Alternatively, for quick demo purposes, you can generate RSA keys using this online tool. Simply copy the generated public key into your JavaScript and the private key into your .env file or environment variable.

Copy Code
    
    export ENCRYPT_PRIVATE_KEY=$(cat private_key.pem)
    

Save the public_key.pem and private_key.pem in your config/ directory. Add the private key to environment variables:

Copy Code
    
    export ENCRYPT_PRIVATE_KEY=$(cat private_key.pem)
    

Alternatively, add this to your .env file if using the dotenv gem.

6. Add the private key to environment variables:

Copy Code
    
        export ENCRYPT_PRIVATE_KEY=$(cat private_key.pem)
    

Alternatively, add this to your .env file if using the dotenv gem.

Step 2: Base64 Encoding and Decoding (Legacy Approach)

In the legacy system, Base64 encoding is used to encode sensitive data, such as the username and password, before sending it to the server. While this method offers some basic obfuscation, it does not provide real security because anyone who intercepts the encoded data can easily decode it.

  1. Encoding with Base64 (Client-Side JavaScript)

    In this method, we encode the user credentials with Base64 and add a random number as a prefix to increase the complexity slightly. Here's an example:

    Copy Code
                
        $(document).ready(function() {
        const formElem = document.querySelector('.enc-form');
        
        formElem.addEventListener('formdata', e => {
            let data = e.formData;
            
            $('[data-enc]').each(function() {
            let unencrypted = $(this);
            let encrypted = window.btoa(unencrypted.val());
            
            if (encrypted) {
                // Adding a random 4-digit prefix to the Base64-encoded value
                encrypted = generateId(4) + encrypted;
                data.set(unencrypted[0].name, encrypted);
            }
            });
        });
        });
                
            

    Here:

    • We grab each input field with data-enc.
    • We encode the input values with window.btoa() (Base64 encoding).
    • We add a random 4-character prefix (using the generateId(4) function) to further obfuscate the encoded data.
  2. Base64 Decoding on the Server-Side (Ruby on Rails)

    On the server-side, we need to decode the data that was encoded using Base64. In Ruby on Rails, you can easily decode the data like this:

    Copy Code
                
        def data_decryptor(user_params_keys)
        user_params_keys.each do |key|
            unless request.params[:user][key].blank?
            # Removing the first 4 characters (the random prefix) and decoding Base64
            request.params[:user][key] = Base64.decode64(request.params[:user][key][4..-1])
            end
        end
        end
                
            

    While this method does obfuscate the data somewhat, it is not secure because Base64 is easily reversible. Anyone who intercepts the data can simply decode it.

Add the private key to environment variables

Step 3: Transitioning to Secure SHA-256 + RSA Encryption

The Base64 encoding method discussed earlier is not secure enough to protect sensitive data. A better approach is to use SHA-256 hashing and RSA encryption. This ensures that even if the encrypted data is intercepted, it cannot be decrypted without the private key.

  1. Using SHA-256 for Hashing

    SHA-256 is a cryptographic hash function that produces a fixed-size (256-bit) hash value from input data. Unlike Base64 encoding, hashing is a one-way function, meaning that it cannot be reversed. In the context of password security, hashing is an essential technique, as the password itself should never be stored in plaintext.

    However, SHA-256 alone is not enough to protect data during transmission. To achieve end-to-end security, we will combine SHA-256 with RSA encryption.

  2. Client-Side Encryption Using RSA Public Key (JavaScript)

    In this approach, before the data is sent to the server, it is encrypted using the RSA public key. RSA is an asymmetric encryption algorithm, meaning it uses a pair of keys: a public key for encryption and a private key for decryption.

    Here's how we implement client-side encryption using the RSA public key:

    Copy Code
                
        $(document).ready(function () {
        const formElem = document.querySelector(".enc-form");
    
        formElem.addEventListener("submit", async (e) => {
            e.preventDefault();  // Prevent form submission until encryption is done
            try {
            const array = $('[data-enc]').toArray();
            let originalArr = [];
            const submitButtonName = e.submitter.name;
            const submitButtonValue = e.submitter.value;
    
            const commitInput = $('<input>').attr({
                type: 'hidden',
                name: submitButtonName,
                value: submitButtonValue
            }).appendTo(formElem);
    
            for (let i = 0; i < array.length; i++) {
                let element = $(array[i]);
                originalArr.push(element.val());
                const publicKey = await importPublicKey(publicKeyPem);
                const encryptedData = await encrypt(publicKey, element.val());
                element.val(encryptedData);
            }
    
            formElem.submit();
            for (let i = 0; i < array.length; i++) {
                let element = $(array[i]);
                element.val(originalArr[i]);
            }
    
            } catch (error) {
            console.error("Error in encryption process:", error);
            }
        });
        });
    
        async function importPublicKey(pem) {
        let binaryDer = pemToArrayBuffer(pem);
        let key = await window.crypto.subtle.importKey(
            "spki",
            binaryDer,
            { name: "RSA-OAEP", hash: "SHA-1" },
            true,
            ["encrypt"]
        );
        return key;
        }
    
        async function encrypt(publicKey, data) {
        let encoder = new TextEncoder();
        let encodedData = encoder.encode(data);
    
        let encrypted = await window.crypto.subtle.encrypt(
            { name: "RSA-OAEP" },
            publicKey,
            encodedData
        );
    
        return btoa(String.fromCharCode.apply(null, new Uint8Array(encrypted)));
        }
    
        function pemToArrayBuffer(pem) {
        let b64 = pem.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\n|\r/g, "");
        let binary = atob(b64);
        let bytes = new Uint8Array(binary.length);
        for (let i = 0; i < binary.length; i++) {
            bytes[i] = binary.charCodeAt(i);
        }
        return bytes.buffer;
        }
                
            
    • RSA Encryption: We use the Web Crypto API to encrypt the data using RSA with the RSA-OAEP encryption scheme and SHA-1 as the hash algorithm.
    • Base64 Encoding: After encryption, we convert the encrypted data to Base64 for safe transmission over HTTP.
  3. Decryption on the Server-Side (Ruby on Rails)

    On the server-side, we need to decrypt the data using the RSA private key. Here's how to implement the decryption logic:

    Copy Code
                
        def data_decryptor(user_params_keys)
        private_key_pem = ENV.fetch('ENCRYPT_PRIVATE_KEY').strip.gsub('\n', "\n")
        user_params_keys.each do |key|
            unless request.params[:user][key].blank?
            request.params[:user][key] = decrypt_rsa_key(request.params[:user][key], 
            OpenSSL::PKey::RSA.new(private_key_pem))
            end
        end
        end
    
        def decrypt_rsa_key(base64_encrypted_data, private_key)
        encrypted = Base64.strict_decode64(base64_encrypted_data)
        decrypted_data = private_key.private_decrypt(encrypted, 
        OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
        decrypted_data
        end
                
            
    • Base64 Decoding: The encrypted data is first decoded from Base64.
    • RSA Decryption: The decrypted data is obtained using the RSA private key with the PKCS1_OAEP_PADDING.
Transitioning to Secure SHA-256 + RSA Encryption

Conclusion

In this tutorial, we've walked through how to enhance the security of user authentication processes by moving from a basic and insecure approach using Base64 encoding to a more advanced and secure system using RSA encryption with SHA-256 hashing.

By adopting modern cryptographic techniques like RSA and SHA-256, you can significantly strengthen the security of your web applications, especially when handling sensitive user data such as usernames and passwords. This approach provides much-needed confidentiality and protection, ensuring that even if data is intercepted, it cannot be accessed without the appropriate decryption key.

Implementing these encryption strategies in your own web applications can help you protect your users' sensitive information, build trust, and mitigate the risk of security breaches.

Looking to Build Secure, Scalable Applications?

At Bluetick Consultants, we specialize in AI and digital transformation solutions designed with security at the core. Whether you're developing an end-to-end product or reinforcing authentication mechanisms with SHA-256 and RSA, We offer custom-built security solutions rooted in our broader digital engineering and AI expertise.

  1. Talk to our security-focused AI experts today
  2. Explore our secure digital product engineering services

Security isn't an add-on—it's the architecture. Let's build it right.

Back To Blogs


contact us