RSA and AES between the browser and the server

We have an piece of code to encrypt/decrypt string which is copy pasted everywhere for decade

You can not imagine how hard it was to migrate this to another language

The rule of thumb: whenever we are talking about cryptogrgaphy we must have openssl and at least one more lanugage examples

Think of this: imagine that we take any engineer and ask him to add encryption, he probably will find something in internet, check that it is working and use it

But what if tomorrow you will tell him that we need to work with that data in another language as well - here is where the pain will begin

Another rule of thumb: DO NOT rely on defaults, they are different which leads to problems when you are trying to migrate things, be as clear as possible, thats the implicit vs explicit story

YoutTube suggested me to watch great video about cryptography concepts

https://www.youtube.com/watch?v=NuyzuNBFWxQ

And I was wondering, OK, we have some basic concepts and examples in nodejs, can we have the same in other languages?

As a results I have put examples of as much as possible implementations in a single place:

https://github.com/mac2000/cryptography

The idea was to have ready to use examples of diffent algorythms without worrying if they will work in another language

Also it was really fun and paintfull at the same time, but at least from now on I do recognize that there are different kinds of RSA keys as well as different paddings

But there is one really cool side effect

Modern JavaScript inside browser can do all actual cryptography tasks

With that in place we can do some crazy things

How about that:

  • we are not going to store any secrets and/or keys - everything will be generated on the fly each time
  • both client and server have their own RSA keys
  • client and server are going to exchange their public keys for followup encrypted transfer
  • because RSA has limits we will use AES instead
  • and to safely transfer AES key we will use partner RSA key
  • which closes the circle - it is like an HTTPS over HTTP

Here is frotnend example:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Transfer</title>
</head>
<body>
    <h1>Transfer Demo</h1>
    <p>In this demo we are going to utilize multiple cryptograpy algorythimgs to transfer secret between the client and server</p>

    <ul>
        <li>in general if data is small - RSA might be used otherwise we need AES</li>
        <li>we do not want to share/store any secrets, especially AES</li>
        <li>participants exchanging their RSA public keys which then used to encrypt AES key</li>
        <li>the data itself is encrypted with AES</li>
    </ul>

    <p>Before doing anything else we need create RSA keys</p>

    <h2>Client RSA keys</h2>

    <p><b>private.pem</b></p>
    <pre><code id="privatePem"></code></pre>

    <p><b>public.pem</b></p>
    <pre><code id="publicPem"></code></pre>

    <h2>Server public.pem</h2>

    <p>Now we need server public key</p>
    <p>Note how cool is that, keys are not stored anywhere which makes followup communication extremely safe</p>

    <p><b>public.pem</b></p>
    <pre><code id="serverPublicPem"></code></pre>

    <h2>Transmit</h2>

    <p>Now the fun part</p>

    <p>Here is our secret data we want to transfer</p>
    <p><textarea id="text" cols="30" rows="5">Hello world, mac was here</textarea></p>
    <p>Technically we may just encrypt it with public key from server and send it.</p>
    <p>But if our message is big (bigger that key size) it won't work.</p>
    <p>That's exactly why we are dealing with keys.</p>
    <p>We are going to:</p>
    <ul>
        <li>encrypt our data with AES</li>
        <li>encrypt AES key with servers public key</li>
        <li>send it all to the server</li>
    </ul>
    <p>On the server side:</p>
    <ul>
        <li>decrypt received key</li>
        <li>decrypt AES payload</li>
        <li>respond</li>
    </ul>

    <p><input id="transfer" type="button" value="Transfer" /></p>

    <div id="part2" style="display:none">

    <h2>AES</h2>
    <p>For aes ancryption we need key and initialization vector (iv)</p>
    <p>Both are arrays of random bytes</p>
    <p>We are encoding them to base64 just for us to print them</p>
    <p>key: <code id="key"></code></p>
    <p>iv: <code id="iv"></code></p>
    <p>Note that key must be exactly 32 bytes and iv - 16</p>
    <p>Having our key and iv we may encryp our text</p>
    <p><code id="aes"></code></p>
    <p>Once again encoding it in base64</p>

    <h2>RSA encrypt</h2>
    <p>Now we may encryp all that with server public key</p>
    <p><b>key:</b></p>
    <p><code id="keyenc" style="word-break: break-all"></code></p>

    <h2>Request payload</h2>
    <p>So our request payload will be:</p>
    <pre><code id="payloadJson"></code></pre>
    <p>Notes:</p>
    <ul>
        <li>JSON is used just for convenience</li>
        <li>because of JSON we need to represent bytes somehow, so we are using base64</li>
        <li>IV is not a secret so passed as is</li>
        <li>key encrypted by servers public key, so only it can decrypt it</li>
        <li>text is encrypted with AES</li>
        <li>we are sending our public key as well, so server may encrypt its response</li>
        <li>look at server logs for details of what is going on there</li>
    </ul>

    <h2>Response payload</h2>
    <p>So our server did responded to us with following payload:</p>
    <pre><code id="responseJson"></code></pre>
    <p>Notes:</p>
    <ul>
        <li>JSON and base64 used as well</li>
        <li>Server send us encrypted text</li>
        <li>Text was encrypted with a key</li>
        <li>Key was encrypted with our public key</li>
        <li>That means that only we can decrypt the text</li>
    </ul>

    <p>key: <code id="decryptedKey"></code></p>


    <h2>Response Text</h2>
    <p>Having that key and iv we can decrypt the text</p>
    <p><b><code id="decryptedText"></code></b></p>

    <p>Once again note that:</p>
    <ul>
        <li>rsa keys are generated on the fly, we are not storing them anywhere</li>
        <li>aes key and iv generated each time, also not stored anywhere</li>
        <li>with such approach it should be quite a difficult task to crack this</li>
        <li>this is only demo, indeed we may store rsa keys, we may even hardcode iv and so on, but the real fun was to make all this dynamic</li>
    </ul>

    <h2>TODO</h2>
    <p>In both, request and response we may add signature as well which will make it impossible to be in between</p>

    </div>

    <script type="module">
        function base64encode(input) {
            return btoa(String.fromCharCode(...new Uint8Array(input)))
        }

        function base64decode(input) {
            return Uint8Array.from(atob(input), c => c.charCodeAt(0))
        }

        async function generateKeys() {
            const {publicKey, privateKey} = await crypto.subtle.generateKey(
                {
                    name: "RSA-OAEP",
                    modulusLength: 2048,
                    publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
                    hash: {name: "SHA-1"},
                },
                true,
                ["encrypt", "decrypt"]
            )
            return {publicKey, privateKey}
        }

        async function exportPKCS8PrivateKeyPem(privateKey) {
            let privatePem = await crypto.subtle.exportKey('pkcs8', privateKey)
            return `-----BEGIN PRIVATE KEY-----\n` + base64encode(privatePem).match(/.{1,64}/g).join('\n') + `\n-----END PRIVATE KEY-----`;
        }

        async function exportSPKIPublicKeyPem(publicKey) {
            const publicPem = await crypto.subtle.exportKey('spki', publicKey);
            return `-----BEGIN PUBLIC KEY-----\n` + base64encode(publicPem).match(/.{1,64}/g).join('\n') + `\n-----END PUBLIC KEY-----`;
        }

        async function importSPKIPublicKeyPem(publicKey) {
            const publicKeyBase64SingleLineWithoutHeaders = publicKey.split('\n').filter(line => !line.startsWith('-----')).join('')
            return await crypto.subtle.importKey(
                "spki",
                base64decode(publicKeyBase64SingleLineWithoutHeaders),
                {name: "RSA-OAEP", hash: {name: "SHA-1"}},
                false,
                ["encrypt"]
            )
        }

        console.log('generating RSA keys')
        const {publicKey, privateKey} = await generateKeys()
        const privatePem = await exportPKCS8PrivateKeyPem(privateKey)
        const publicPem = await exportSPKIPublicKeyPem(publicKey)
        document.getElementById('privatePem').innerHTML = privatePem
        document.getElementById('publicPem').innerHTML = publicPem


        console.log('Retrieving server public key')
        const serverPublicKeyPem = await fetch('/public').then(r => r.text())
        document.getElementById('serverPublicPem').innerHTML = serverPublicKeyPem
        const serverPublicKey = await importSPKIPublicKeyPem(serverPublicKeyPem)

        async function transfer() {
            console.log('Starting transfer dance')

            const text = document.getElementById('text').value
            console.log('the text is: ' + text)

            console.log('Generating AES key and iv bytes')
            const key = crypto.getRandomValues(new Uint8Array(32))
            const iv = crypto.getRandomValues(new Uint8Array(16))
            document.getElementById('key').innerHTML = base64encode(key)
            document.getElementById('iv').innerHTML = base64encode(iv)


            console.log('Encrypting text')
            const secret = await crypto.subtle.importKey("raw", key, {name: 'AES-CBC', length: 256}, false, ["encrypt", "decrypt"])
            const encrypted = await crypto.subtle.encrypt({name: "AES-CBC", iv: iv}, secret, new TextEncoder().encode(text))
            document.getElementById('aes').innerHTML = base64encode(encrypted)

            console.log('Encrypting results with server public key')
            const keyenc = await crypto.subtle.encrypt({name: "RSA-OAEP"}, serverPublicKey, key)
            document.getElementById('keyenc').innerHTML = base64encode(keyenc)

            console.log('Preparing payload')
            const payload = {
                key: base64encode(keyenc),
                iv: base64encode(iv),
                text: base64encode(encrypted),
                publicKey: publicPem
            }
            document.getElementById('payloadJson').innerHTML = JSON.stringify(payload, null, 4)

            console.log('Sending request')
            const response = await fetch('/transmit', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)}).then(r => r.json())
            document.getElementById('responseJson').innerHTML = JSON.stringify(response, null, 4)

            console.log('Decrypting key from response')
            const decryptedKey = await crypto.subtle.decrypt({name: "RSA-OAEP"}, privateKey, base64decode(response.key))
            document.getElementById('decryptedKey').innerHTML = base64encode(decryptedKey)

            console.log('Decrypting text from response')
            const decryptor = await crypto.subtle.importKey("raw", decryptedKey, {name: 'AES-CBC', length: 256}, false, ["encrypt", "decrypt"])
            const decrypted = await crypto.subtle.decrypt({name: "AES-CBC", iv: base64decode(response.iv)}, decryptor, base64decode(response.text))
            document.getElementById('decryptedText').innerHTML = new TextDecoder('utf8').decode(decrypted)


            console.log('Display results')
            document.getElementById('part2').style.display = 'block'
        }
        document.getElementById('transfer').addEventListener('click', transfer)
        transfer().catch(console.log)
    </script>
</body>
</html>

Here is code for backend:

const {createServer} = require('http')
const {readFileSync} = require('fs')
const crypto = require('crypto')

console.log('Creating RSA keys, that will be used for communication with clients')
console.log('Notes:')
console.log('- we are not storing them in files')
console.log('- client will create its own keys and we will exchange public keys')

const {privateKey, publicKey} = crypto.generateKeyPairSync('rsa', {
    modulusLength: 2048,
    publicKeyEncoding: {type: 'spki',format: 'pem'},
    privateKeyEncoding: {type: 'pkcs8', format: 'pem'}
})

console.log(privateKey.substring(0, 60).replace('\n', '') + '...')
console.log(publicKey.substring(0, 60).replace('\n', '') + '...')


function public(req, res) {
    console.log('Got an request for public key')
    res.setHeader("Content-Type", "text/plain")
    res.writeHead(200)
    res.end(publicKey)
}

function home(_, res) {
    console.log('Got home page request, sending index.html')
    res.setHeader("Content-Type", "text/html")
    res.writeHead(200)
    res.end(readFileSync('index.html', 'utf8'))
}

async function transmit(req, res) {
    console.log('Got an request to transmit data')
    console.log('')
    let body = ''
    req.on('data', chunk => body += chunk)
    req.on('end', () => {
        const payload = JSON.parse(body)
        console.log('Payload', payload)
        console.log('')

        let iv = Buffer.from(payload.iv, 'base64')
        console.log('iv is passed as is, base64 encoded')
        console.log('iv: ' + iv.toString('base64'))
        console.log('')

        let key = crypto.privateDecrypt({key: privateKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING}, Buffer.from(payload.key, 'base64'))
        console.log('key is encrypted with our public key, so only we can decrypt it with our private key')
        console.log('key: ' + key.toString('base64'))
        console.log('')

        const decryptor = crypto.createDecipheriv('aes-256-cbc', key, iv)
        const text = Buffer.concat([decryptor.update(payload.text, 'base64'), decryptor.final()]).toString('utf8')
        console.log('text inside payload is encrypted with key and iv')
        console.log('and because key is encrypted, no one except us can decrypt text')
        console.log('text: ' + text)
        console.log('')

        console.log('having that text we may want to save/encrypt/store it somewhere')
        console.log('now we need to send response to the client')
        console.log('just for demo we are going to send him text')
        console.log('but to do it safely we also need to encrypt it')
        console.log('thats why client send us its public key')
        console.log('so we are going to encrypt data with it so only client can decrypt it')
        console.log('but once again, rsa can not be used to encrypt big data')
        console.log('thats why we are going to do everything similar to what client did:')
        console.log('- create some random key and iv')
        console.log('- aes encrypt text with this key and iv')
        console.log('- encrypt key with client public key so only he can decrypt it')
        console.log('- respond with similar payload')
        console.log('')

        console.log('Generate new random key and iv')
        key = crypto.randomBytes(32)
        console.log('key: ' + key.toString('base64'))
        iv = crypto.randomBytes(16)
        console.log('iv: ' + iv.toString('base64'))
        console.log('')

        console.log('Encrypt text')
        const encryptor = crypto.createCipheriv('aes-256-cbc', key, iv)
        const encrypted = Buffer.concat([encryptor.update(text, 'utf8'), encryptor.final()]).toString('base64');
        console.log('encrypted: ' + encrypted)
        console.log('')

        console.log('Encrypt key')
        const publicKey = crypto.createPublicKey(payload.publicKey)
        const encryptedKey = crypto.publicEncrypt({ key: publicKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, key)
        console.log('encrypted key: ' + encryptedKey.toString('base64'))
        console.log('')

        const response = {
            key: encryptedKey.toString('base64'),
            iv: iv.toString('base64'),
            text: encrypted
        }
        console.log('Response', JSON.stringify(response, null, 4))
        console.log('Notes:')
        console.log('- aes key is encrypted with client public rsa key, so only he can decrypt it')
        console.log('- iv sent as is')
        console.log('- text encrypted with aes key')
        console.log('- we are not sending our public key, because client already has it')

        res.setHeader("Content-Type", "application/json")
        res.writeHead(200)
        res.end(JSON.stringify(response))
    })
}


async function router(req, res) {
    switch(req.url) {
        case '/': return home(req, res)
        case '/public': return public(req, res)
        case '/transmit': return await transmit(req, res)
    }
    res.writeHead(404)
    res.end('Not found')
}

const server = createServer(router)

server.listen(3000, '0.0.0.0', () => console.log('Server started and listening 0.0.0.0:3000, open http://localhost:3000/'))

You may run it like so:

docker run -it --rm -p 3000:3000 -v ${PWD}/index.js:/code/index.js -v ${PWD}/index.html:/code/index.html -w /code node node index.js

Then opep http://localhost:3000/ to see detailed explanation

You should see something like that:

screenshot

Partial examples of how the same can by done in other languages can be found here

Examples for AES encrypt/decrypt for dotnet, golang, nodejs, php, python, openssl can be found here

Examples for RSA encryp/decrypt once again for many languages can be found here