API de criptografia da Web: um estudo de caso

Bom dia amigos!



Neste tutorial, daremos uma olhada na API de criptografia da Web : uma interface de criptografia de dados do lado do cliente. Este tutorial é baseado neste artigo . Presume-se que você esteja familiarizado com a criptografia.



O que exatamente vamos fazer? Vamos escrever um servidor simples que aceitará dados criptografados do cliente e os devolveremos mediante solicitação. Os próprios dados serão processados ​​no lado do cliente.



O servidor será implementado em Node.js usando Express, o cliente em JavaScript. O bootstrap será usado para estilização.



O código do projeto está aqui .



Se você estiver interessado, por favor me siga.



Treinamento



Crie um diretório crypto-tut:



mkdir crypto-tut


Vamos lá e inicializamos o projeto:



cd crypto-tut

npm init -y


Instale express:



npm i express


Instale nodemon:



npm i -D nodemon


Editando package.json:



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


Estrutura do projeto:



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


Conteúdo 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>


Conteúdo 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



Vamos começar a criar um servidor.



Nós abrimos server.js.



Conectamos express e criamos instâncias do aplicativo e do roteador:



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


Conectamos o middleware (camada intermediária entre solicitação e resposta):



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


Criamos uma variável para armazenar dados:



let data


Processamos o recebimento de dados do cliente:



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


Processamos o envio de dados ao cliente:



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


Iniciamos o servidor:



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


Nós executamos o comando npm start. O terminal exibe a mensagem "Servidor pronto". Abertura http://localhost:3000:







Aqui é onde terminamos com o servidor, vá para o lado cliente da aplicação.



Cliente



Isto é onde a diversão começa.



Abra o arquivo client.js.



O algoritmo simétrico AES-GCM será usado para criptografia de dados. Esses algoritmos permitem o uso da mesma chave para criptografar e descriptografar.



Crie uma função de geração de chave simétrica:



// 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'])


Os dados devem ser codificados em um fluxo de bytes antes da criptografia. Isso é feito facilmente com a classe TextEncoder:



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

    return encoder.encode(data)
}


Em seguida, precisamos de um vetor de execução (vetor de inicialização, IV), que é uma sequência aleatória ou pseudo-aleatória de caracteres que é adicionada à chave de criptografia para aumentar sua segurança:



// 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))


Depois de criar as funções auxiliares, podemos implementar a função de criptografia. Esta função deve retornar uma cifra e um IV para que a cifra possa ser decodificada posteriormente:



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
        }
}


Depois de criptografar os dados com SubtleCrypto , eles são buffers de dados binários brutos. Este não é o melhor formato para transmissão e armazenamento. Vamos consertar isso.



Os dados geralmente são enviados no formato JSON e armazenados em um banco de dados. Portanto, faz sentido compactar os dados em um formato portátil. Uma maneira de fazer isso é convertendo os dados em strings 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))
)


Após o recebimento dos dados, é necessário realizar o processo reverso, ou seja, converter strings codificadas em base64 em buffers binários brutos:



// 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
}


Resta decifrar os dados recebidos. No entanto, após a descriptografia, precisamos decodificar o fluxo de bytes em seu formato original. Isso pode ser feito usando a classe TextDecoder:



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

    return decoder.decode(byteStream)
}


A função de descriptografia é o inverso da função de criptografia:



// 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)
}


Nesta fase, o conteúdo se client.jsparece com este:



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)
}


Agora vamos implementar o envio e recebimento de dados.



Criamos variáveis:



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

// 
let key


Criptografia e envio de dados:



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>   .`
}


Recebendo e descriptografando dados:



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> .`
}


Tratamento de cliques de botão:



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 o servidor para o caso. Nós abrimos http://localhost:3000. Clique no botão "Enviar mensagem":







Vemos os dados recebidos pelo servidor no terminal:



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


Clique no botão "Obter mensagem":







Vemos os mesmos dados recebidos pelo cliente no console:



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


A API de criptografia da Web abre oportunidades interessantes para protegermos informações confidenciais do lado do cliente. Outro passo para o desenvolvimento web sem servidor.



O suporte para esta tecnologia atualmente é de 96%:







Espero que tenha gostado do artigo. Obrigado pela atenção.



All Articles