API de criptograf铆a web: un estudio de caso

隆Buen dia amigos!



En este tutorial, veremos la API de criptograf铆a web : una interfaz de cifrado de datos del lado del cliente. Este tutorial se basa en este art铆culo . Se supone que est谩 algo familiarizado con el cifrado.



驴Qu茅 vamos a hacer exactamente? Escribiremos un servidor simple que aceptar谩 datos encriptados del cliente y los devolver谩 a pedido. Los datos en s铆 se procesar谩n en el lado del cliente.



El servidor se implementar谩 en Node.js usando Express, el cliente en JavaScript. Bootstrap se utilizar谩 para dise帽ar.



El c贸digo del proyecto est谩 aqu铆 .



Si est谩s interesado, s铆gueme.



Formaci贸n



Crea un directorio crypto-tut:



mkdir crypto-tut


Entramos en 茅l e inicializamos el proyecto:



cd crypto-tut

npm init -y


Instalar express:



npm i express


Instalar nodemon:



npm i -D nodemon


Edici贸n package.json:



"main": "server.js",
"scripts": {
    "start": "nodemon"
},


Estructura del proyecto:



crypto-tut
    --node_modules
    --src
        --client.js
        --index.html
        --style.css
    --package-lock.json
    --package.json
    --server.js


Contenido index.html:



<head>
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <script src="client.js" defer></source>
</head>

<body>
    <div class="container">
        <h3>Web Cryptography API Tutorial</h3>
        <input type="text" value="Hello, World!" class="form-control">
        <div class="btn-box">
            <button class="btn btn-primary btn-send">Send message</button>
            <button class="btn btn-success btn-get" disabled>Get message</button>
        </div>
        <output></output>
    </div>
</body>


Contenido style.css:



h3,
.btn-box {
    margin: .5em;
    text-align: center;
}

input,
output {
    display: block;
    margin: 1em auto;
    text-align: center;
}

output span {
    color: green;
}


Servidor



Comencemos a crear un servidor.



Abrimos server.js.



Conectamos express y creamos instancias de la aplicaci贸n y enrutador:



const express = require('express')
const app = express()
const router = express.Router()


Conectamos middleware (capa intermedia entre solicitud y respuesta):



//  
app.use(express.json({
    type: ['application/json', 'text/plain']
}))
//  
app.use(router)
//    
app.use(express.static('src'))


Creamos una variable para almacenar datos:



let data


Tratamos la recepci贸n de datos del cliente:



router.post('/secure-api', (req, res) => {
    //     
    data = req.body
    //    
    console.log(data)
    //  
    res.end()
})


Tramitamos el env铆o de datos al cliente:



router.get('/secure-api', (req, res) => {
    //     JSON,
    //     
    res.json(data)
})


Iniciamos el servidor:



app.listen(3000, () => console.log('Server ready'))


Ejecutamos el comando npm start. El terminal muestra el mensaje "Servidor listo". Apertura http://localhost:3000:







Aqu铆 es donde terminamos con el servidor, vaya al lado del cliente de la aplicaci贸n.



Cliente



Aqu铆 es donde comienza la diversi贸n.



Abra el archivo client.js.



Se utilizar谩 el algoritmo sim茅trico AES-GCM para el cifrado de datos. Dichos algoritmos permiten el uso de la misma clave para el cifrado y el descifrado.



Cree una funci贸n de generaci贸n de claves sim茅tricas:



// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey
const generateKey = async () =>
    window.crypto.subtle.generateKey({
        name: 'AES-GCM',
        length: 256,
    }, true, ['encrypt', 'decrypt'])


Los datos deben codificarse en un flujo de bytes antes del cifrado. Esto se hace f谩cilmente con la clase TextEncoder:



// https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
const encode = data => {
    const encoder = new TextEncoder()

    return encoder.encode(data)
}


A continuaci贸n, necesitamos un vector de ejecuci贸n (vector de inicializaci贸n, IV), que es una secuencia de caracteres aleatoria o pseudoaleatoria que se agrega a la clave de cifrado para aumentar su seguridad:



// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
const generateIv = () =>
    // https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
    window.crypto.getRandomValues(new Uint8Array(12))


Despu茅s de crear las funciones auxiliares, podemos implementar la funci贸n de cifrado. Esta funci贸n debe devolver un cifrado y un IV para que posteriormente se pueda decodificar el cifrado:



const encrypt = async (data, key) => {
    const encoded = encode(data)
    const iv = generateIv()
    const cipher = await window.crypto.subtle.encrypt({
        name: 'AES-GCM',
        iv
    }, key, encoded)

    return {
            cipher,
            iv
        }
}


Despu茅s de cifrar los datos con SubtleCrypto , son b煤feres de datos binarios sin procesar. Este no es el mejor formato para transmisi贸n y almacenamiento. Arreglemos esto.



Los datos generalmente se env铆an en formato JSON y se almacenan en una base de datos. Por lo tanto, tiene sentido empaquetar los datos en un formato port谩til. Una forma de hacerlo es convirtiendo los datos en cadenas base64:



// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
const pack = buffer => window.btoa(
    String.fromCharCode.apply(null, new Uint8Array(buffer))
)


Despu茅s de recibir los datos, es necesario realizar el proceso inverso, es decir convierta cadenas codificadas en base64 en b煤feres binarios sin procesar:



// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
const unpack = packed => {
    const string = window.atob(packed)
    const buffer = new ArrayBuffer(string.length)
    const bufferView = new Uint8Array(buffer)

    for (let i = 0; i < string.length; i++) {
        bufferView[i] = string.charCodeAt(i)
    }

    return buffer
}


Queda por descifrar los datos recibidos. Sin embargo, despu茅s del descifrado, necesitamos decodificar el flujo de bytes en su formato original. Esto se puede hacer usando la clase TextDecoder:



// https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
const decode = byteStream => {
    const decoder = new TextDecoder()

    return decoder.decode(byteStream)
}


La funci贸n de descifrado es la inversa de la funci贸n de cifrado:



// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/decrypt
const decrypt = async (cipher, key, iv) => {
    const encoded = await window.crypto.subtle.decrypt({
        name: 'AES-GCM',
        iv
    }, key, cipher)

    return decode(encoded)
}


En esta etapa, el contenido se client.jsve as铆:



const generateKey = async () =>
    window.crypto.subtle.generateKey({
        name: 'AES-GCM',
        length: 256,
    }, true, ['encrypt', 'decrypt'])

const encode = data => {
    const encoder = new TextEncoder()

    return encoder.encode(data)
}

const generateIv = () =>
    window.crypto.getRandomValues(new Uint8Array(12))

const encrypt = async (data, key) => {
    const encoded = encode(data)
    const iv = generateIv()
    const cipher = await window.crypto.subtle.encrypt({
        name: 'AES-GCM',
        iv
    }, key, encoded)

    return {
        cipher,
        iv
    }
}

const pack = buffer => window.btoa(
    String.fromCharCode.apply(null, new Uint8Array(buffer))
)

const unpack = packed => {
    const string = window.atob(packed)
    const buffer = new ArrayBuffer(string.length)
    const bufferView = new Uint8Array(buffer)

    for (let i = 0; i < string.length; i++) {
        bufferView[i] = string.charCodeAt(i)
    }

    return buffer
}

const decode = byteStream => {
    const decoder = new TextDecoder()

    return decoder.decode(byteStream)
}

const decrypt = async (cipher, key, iv) => {
    const encoded = await window.crypto.subtle.decrypt({
        name: 'AES-GCM',
        iv
    }, key, cipher)

    return decode(encoded)
}


Ahora implementemos el env铆o y la recepci贸n de datos.



Creamos variables:



//    ,   
const input = document.querySelector('input')
//    
const output = document.querySelector('output')

// 
let key


Encriptaci贸n y env铆o de datos:



const encryptAndSendMsg = async () => {
    const msg = input.value

     // 
    key = await generateKey()

    const {
        cipher,
        iv
    } = await encrypt(msg, key)

    //   
    await fetch('http://localhost:3000/secure-api', {
        method: 'POST',
        body: JSON.stringify({
            cipher: pack(cipher),
            iv: pack(iv)
        })
    })

    output.innerHTML = ` <span>"${msg}"</span> .<br>   .`
}


Recibir y descifrar datos:



const getAndDecryptMsg = async () => {
    const res = await fetch('http://localhost:3000/secure-api')

    const data = await res.json()

    //    
    console.log(data)

    //   
    const msg = await decrypt(unpack(data.cipher), key, unpack(data.iv))

    output.innerHTML = `   .<br> <span>"${msg}"</span> .`
}


Manejo de clics en botones:



document.querySelector('.btn-box').addEventListener('click', e => {
    if (e.target.classList.contains('btn-send')) {
        encryptAndSendMsg()

        e.target.nextElementSibling.removeAttribute('disabled')
    } else if (e.target.classList.contains('btn-get')) {
        getAndDecryptMsg()
    }
})


Reinicie el servidor por si acaso. Abrimos http://localhost:3000. Pulsamos en el bot贸n "Enviar mensaje":







Vemos los datos recibidos por el servidor en el terminal:



{
  cipher: 'j8XqWlLIrFxyfA2easXkJTLLIt9x8zLHei/tTKI=',
  iv: 'F8doVULJzbEQs3M1'
}


Haga clic en el bot贸n "Obtener mensaje":







Vemos los mismos datos recibidos por el cliente en la consola:



{
  cipher: 'j8XqWlLIrFxyfA2easXkJTLLIt9x8zLHei/tTKI=',
  iv: 'F8doVULJzbEQs3M1'
}


La API de criptograf铆a web nos abre oportunidades interesantes para proteger la informaci贸n confidencial del lado del cliente. Otro paso hacia el desarrollo web sin servidor.



El soporte para esta tecnolog铆a es actualmente del 96%:







espero que haya disfrutado del art铆culo. Gracias por su atenci贸n.



All Articles