Published: 28 April 2025 | Last updated: 9 September 2026
Short answer: Base64 encoding is not encryption and gives you no protection at all — anyone who intercepts it reverses it in one line of code. To genuinely protect credentials in transit, encrypt them in the browser with an RSA public key using the Web Crypto API, then decrypt them server-side with the matching private key through OpenSSL. Devise continues to handle password storage with bcrypt as normal.
In short: this guide covers RSA keypair generation, client-side encryption in JavaScript, server-side decryption inside a Rails Devise flow, and where SHA-256 does and does not belong in a login pipeline.
Who this guide is for, when to use it, and which use cases it supports
Audience: Backend and full-stack engineers working in Ruby on Rails 6 or above with Devise, plus application security engineers reviewing authentication designs.
Industry context: Fintech and BFSI, healthcare, and govtech — regulated environments where credential handling is subject to audit.
Use cases this guide supports:
- Hardening a password-based login form in an existing Rails application.
- Protecting credentials from logging middleware, reverse proxies, and monitoring agents that terminate TLS.
- Migrating a legacy Base64 login flow to genuine asymmetric encryption.
- Meeting an audit requirement for credential protection above transport-layer security alone.
When to use it: Use this approach when password-based login must stay in place and you need an additional encryption layer between browser and server.
When not to use it: Do not use it as a replacement for HTTPS, and do not choose it over a managed identity provider or passkeys if either is available to you.
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 anyone can reverse them in seconds. Therefore, to truly safeguard usernames and passwords, you need stronger mechanisms such as SHA-256 hashing and RSA encryption.
In this blog, we’ll instead walk you through 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:
- Ruby on Rails Setup:
- Ruby on Rails 6 or above.
- Devise gem for user authentication.
- Frontend:
- Basic understanding of JavaScript for client-side encryption.
- Environment:
- OpenSSL library for RSA key generation and handling encryption/decryption.
- HTTPS protocol enabled for secure data transmission (important in production) — see the OWASP Transport Layer Security Cheat Sheet.
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:
rails new secure_auth_demo --skip-javascript
cd secure_auth_demo
2. Add the Devise gem to your Gemfile:
gem 'devise'
3. Install Devise:
bundle install
rails generate devise:install
rails generate devise User
rails db:migrate
4. Generate Devise views:
rails generate devise:views
This will create all the necessary views for user authentication, which we will then modify to integrate encryption.
5. Add the RSA public/private keys:
Generate RSA keys using OpenSSL:
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. Then copy the generated public key into your JavaScript and the private key into your .env file or environment variable.
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:
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:
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, the client encodes 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.
- Encoding with Base64 (Client-Side JavaScript)In this method, we first encode the user credentials with Base64 and then 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). - Finally, we add a random 4-character prefix (using the
generateId(4)function) to further obfuscate the encoded data.
- We grab each input field with
- Base64 Decoding on the Server-Side (Ruby on Rails)On the server side, we then decode the data that the client encoded with 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 endWhile this method does obfuscate the data somewhat, it is not secure because Base64 is easily reversible. In other words, anyone who intercepts the data can simply decode it.

Step 3: Transitioning to Secure SHA-256 + RSA Encryption
However, the Base64 encoding method discussed earlier is not secure enough to protect sensitive data. Instead, a better approach is to use SHA-256 hashing and RSA encryption. As a result, even if an attacker intercepts the encrypted data, they still cannot decrypt it without the private key.
- Using SHA-256 for Hashing
SHA-256 is a cryptographic hash function, defined in NIST FIPS 180-4, that produces a fixed-size (256-bit) hash value from input data. Unlike Base64 encoding, hashing works in one direction only, so no one can reverse it. In the context of password security, hashing is an essential technique, because you should never store the password itself 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.
- Client-Side Encryption Using RSA Public Key (JavaScript)
In this approach, the browser first encrypts the data with the RSA public key, and only then sends it to the server. RSA, in contrast, is an asymmetric encryption algorithm: it uses a pair of keys, namely a public key for encryption and a private key for decryption.
Here’s how we implement client-side encryption using the Web Crypto API and the RSA public key:
$(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: Specifically, we use the Web Crypto API to encrypt the data using RSA with the
RSA-OAEPencryption scheme and SHA-1 as the hash algorithm. - Base64 Encoding: After encryption, we convert the encrypted data to Base64 for safe transmission over HTTP.
- 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:
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: First, the method decodes the encrypted payload from Base64.
- RSA Decryption: Next, the RSA private key decrypts that payload using the
PKCS1_OAEP_PADDING.

Conclusion
In conclusion, 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, because even if an attacker intercepts the data, they still cannot read it without the correct decryption key.
Ultimately, implementing these encryption strategies in your own web applications helps you protect your users’ sensitive information, build trust, and mitigate the risk of security breaches.
Frequently Asked Questions
Is Base64 encoding secure for transmitting passwords?
Short answer: no. Base64 is an encoding scheme, not an encryption scheme. It uses no key, so anyone who captures the payload can decode it instantly with a single function call. Adding a random prefix, as the legacy approach does, raises no meaningful barrier.
Do I still need HTTPS if I encrypt credentials in the browser?
Short answer: yes. HTTPS remains mandatory. Client-side RSA encryption is a defence-in-depth layer that protects credentials from logging middleware, misconfigured reverse proxies, and browser extensions — it does not protect the rest of the session, and it is not a substitute for TLS.
What is the difference between SHA-256 hashing and RSA encryption?
In short: SHA-256 is a one-way hash: it turns input into a fixed 256-bit digest that cannot be reversed, which makes it suitable for integrity checks. RSA is two-way asymmetric encryption: data encrypted with a public key can be recovered with the corresponding private key. Login flows need RSA for confidentiality in transit, and hashing for storage.
Does this replace Devise’s password hashing?
Short answer: no. Devise stores passwords using bcrypt, which is deliberately slow and salted to resist brute-force attacks. The RSA layer described here protects the password while it travels from browser to server. Once decrypted server-side, Devise handles storage exactly as before.
Where should the RSA private key live in a Rails application?
In short: In an environment variable or a secrets manager, never in version control. This guide loads it through ENV.fetch('ENCRYPT_PRIVATE_KEY'). In production, prefer Rails encrypted credentials or a managed service such as AWS Secrets Manager, and rotate the keypair on a defined schedule.
What happens if the data is longer than the RSA key size?
In short: RSA-OAEP can only encrypt a payload smaller than the key length minus padding overhead — roughly 190 bytes for a 2048-bit key with SHA-256. Usernames and passwords fit comfortably. For anything larger, use hybrid encryption: encrypt the payload with AES and encrypt only the AES key with RSA.
Looking to Build Secure, Scalable Applications?
At Bluetick Consultants, we build AI and digital transformation solutions 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.
- Talk to our security-focused AI experts today
- Explore our secure digital product engineering services
Security isn’t an add-on—it’s the architecture. Let’s build it right.