In this post we will review the steps to use RSA encryption/decryption on Javascript. We use the SubtleCrypto API, which is available on on secure context pages. Will also use some functions that are not supported on old browsers.
This post is about using a-symmetric encryption for encrypt/decrypt. In case of need of a-symmetric encryption for sign/verify, check this post.
First, to check if the page is considered a secure context, use the following:
console.log('secure context', window.isSecureContext)
The SubtleCrypto API uses ArrayBuffer for it functions, but as we want to send the data to server, will will convert it from and to a base64 string using the following helper functions:
function arrayBufferToBase64(arrayBuffer) {
const bytes = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))
return window.btoa(bytes)
}
function base64ToArrayBuffer(base64String) {
const chars = window.atob(base64String)
const arrayBuffer = new ArrayBuffer(chars.length)
const bufferView = new Uint8Array(arrayBuffer)
for (let i = 0, strLen = chars.length; i < strLen; i++) {
bufferView[i] = chars.charCodeAt(i)
}
return arrayBuffer
}
Special notes for NodeJS users
As btoa() does not exist in NodeJS, we need to use the Buffer, BUT we must specify the latin1 encoding to have the same encoding as in the browser.
NodeJS version:
function arrayBufferToBase64(arrayBuffer) {
const bytes = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))
return Buffer.from(bytes,'latin1').toString('base64')
}
We now can generate a new RSA key. This is a intensive operation, and it takes ~1 second on a desktop machine.
async function generateKey() {
return await window.crypto.subtle.generateKey(
{
name: 'RSA-OAEP',
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
},
true,
['encrypt', 'decrypt'],
)
}
The generated key is actually a pair that includes a public key and a private key.
In most cases we will store the private key locally so that in case of restart, we will reuse it. To export the private key we run the following:
function convertBinaryToPem(base64Cert, label) {
var pemCert = '-----BEGIN ' + label + '-----\r\n'
var nextIndex = 0
while (nextIndex < base64Cert.length) {
if (nextIndex + 64 <= base64Cert.length) {
pemCert += base64Cert.substr(nextIndex, 64) + '\r\n'
} else {
pemCert += base64Cert.substr(nextIndex) + '\r\n'
}
nextIndex += 64
}
pemCert += '-----END ' + label + '-----\r\n'
return pemCert
}
async function exportPrivateKey(privateKey) {
const exported = await window.crypto.subtle.exportKey(
'pkcs8',
privateKey,
)
const exportedAsBase64 = arrayBufferToBase64(exported)
return convertBinaryToPem(exportedAsBase64, 'RSA PRIVATE KEY')
}
The public key can be sent to other parties. We use the export and import to do this.
const pemHeader = '-----BEGIN PUBLIC KEY-----\n'
const pemFooter = '\n-----END PUBLIC KEY-----'
function breakKeyToShortLines(publicKey) {
const maxLineLength = 64
let pemCert = ''
let nextIndex = 0
while (nextIndex < publicKey.length) {
if (nextIndex + maxLineLength <= publicKey.length) {
pemCert += publicKey.substr(nextIndex, maxLineLength) + '\n'
} else {
pemCert += publicKey.substr(nextIndex)
}
nextIndex += maxLineLength
}
return pemCert
}
async function exportSubjectPublicKey(publicKey) {
const exported = await window.crypto.subtle.exportKey(
'spki',
publicKey,
)
const exportedAsBase64 = arrayBufferToBase64(exported)
const shortLinesKey = breakKeyToShortLines(exportedAsBase64)
return `${pemHeader}${shortLinesKey}${pemFooter}`
}
function importSubjectPublicKey(pem) {
const pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length)
const binaryDer = base64ToArrayBuffer(pemContents)
return window.crypto.subtle.importKey(
'spki',
binaryDer,
{
name: 'RSA-OAEP',
hash: 'SHA-256',
},
true,
['encrypt'],
)
}
Finally we can use the encrypt and decrypt functions:
async function encryptMessage(publicKey, clearText) {
const encodedText = new TextEncoder().encode(clearText)
const cipherText = await window.crypto.subtle.encrypt(
{
name: 'RSA-OAEP',
},
publicKey,
encodedText,
)
return arrayBufferToBase64(cipherText)
}
async function decryptMessage(privateKey, cipherBase64) {
const cipherText = base64ToArrayBuffer(cipherBase64)
const encodedText = await window.crypto.subtle.decrypt(
{
name: 'RSA-OAEP',
},
privateKey,
cipherText,
)
return new TextDecoder().decode(encodedText)
}
An example for using these functions is below:
async function example() {
const keyPair = await generateKey()
console.log('key pair', keyPair)
const exportedPublicKey = await exportSubjectPublicKey(keyPair.publicKey)
console.log('exported public', exportedPublicKey)
const importedPublicKey = await importSubjectPublicKey(exportedPublicKey)
console.log('imported public', importedPublicKey)
for (let i = 0; i < 2; i++) {
const text = 'mypass123'
const encrypted = await encryptMessage(importedPublicKey, text)
console.log('encrypted', encrypted)
const decrypted = await decryptMessage(keyPair.privateKey, encrypted)
console.log('decrypted', decrypted)
}
}
Final Note
This post is part of a series of posts about encryption. The full posts list is below.
- Using ECDSA in GO to Sign and Verify messages
- A-Symmetic Encrypt and Decrypt on Golang
- Symmetric Encrypt and Decrypt in Golang
- Using ECDSA in JavaScript to Sign and Verify messages
- A-Symmetic Encrypt and Decrypt on JavaScript
- Symmetric Encrypt and Decrypt in Javascript
- Using ECDSA in Python to Sign and Verify messages
No comments:
Post a Comment