API de cryptographie Web: une étude de cas

Bonne journée, mes amis!



Dans ce didacticiel, nous examinerons l' API de cryptographie Web : une interface de chiffrement de données côté client. Ce tutoriel est basé sur cet article . On suppose que vous êtes assez familier avec le cryptage.



Qu'allons-nous faire exactement? Nous écrirons un serveur simple qui acceptera les données cryptées du client et les renverrons sur demande. Les données elles-mêmes seront traitées côté client.



Le serveur sera implémenté dans Node.js en utilisant Express, le client en JavaScript. Bootstrap sera utilisé pour le style.



Le code du projet est ici .



Si vous êtes intéressé, suivez-moi.



Entraînement



Créez un répertoire crypto-tut:



mkdir crypto-tut


On y va et on initialise le projet:



cd crypto-tut

npm init -y


Installer express:



npm i express


Installer nodemon:



npm i -D nodemon


Édition package.json:



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


Structure du projet:



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


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


Contenu style.css:



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

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

output span {
    color: green;
}


Serveur



Commençons par créer un serveur.



Nous ouvrons server.js.



Nous connectons express et créons des instances de l'application et du routeur:



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


Nous connectons le middleware (couche intermédiaire entre la requête et la réponse):



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


Nous créons une variable pour stocker les données:



let data


Nous traitons la réception des données du client:



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


Nous traitons l'envoi de données au client:



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


Nous démarrons le serveur:



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


Nous exécutons la commande npm start. Le terminal affiche le message "Serveur prêt". Ouverture http://localhost:3000:







C'est là que nous en avons terminé avec le serveur, allez du côté client de l'application.



Client



C'est là que le plaisir commence.



Ouvrez le fichier client.js.



L'algorithme symétrique AES-GCM sera utilisé pour le cryptage des données. De tels algorithmes permettent l'utilisation de la même clé pour le cryptage et le décryptage.



Créez une fonction de génération de clé symétrique:



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


Les données doivent être encodées dans un flux d'octets avant le cryptage. Cela se fait facilement avec la classe TextEncoder:



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

    return encoder.encode(data)
}


Ensuite, nous avons besoin d'un vecteur d'exécution (vecteur d'initialisation, IV), qui est une séquence aléatoire ou pseudo-aléatoire de caractères qui est ajoutée à la clé de chiffrement pour augmenter sa sécurité:



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


Après avoir créé les fonctions d'assistance, nous pouvons implémenter la fonction de cryptage. Cette fonction doit retourner un chiffrement et un IV pour que le chiffrement puisse être décodé ultérieurement:



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


Après avoir chiffré les données avec SubtleCrypto , ce sont des tampons de données binaires brutes. Ce n'est pas le meilleur format pour la transmission et le stockage. Corrigeons ça.



Les données sont généralement envoyées au format JSON et stockées dans une base de données. Par conséquent, il est logique de regrouper les données dans un format portable. Pour ce faire, vous pouvez convertir les données en chaînes 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))
)


Après avoir reçu les données, il est nécessaire d'effectuer le processus inverse, c'est-à-dire convertir les chaînes encodées en base64 en tampons binaires bruts:



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


Reste à déchiffrer les données obtenues. Cependant, après le décryptage, nous devons décoder le flux d'octets dans son format d'origine. Cela peut être fait en utilisant la classe TextDecoder:



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

    return decoder.decode(byteStream)
}


La fonction de déchiffrement est l'inverse de la fonction de chiffrement:



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


À ce stade, le contenu client.jsressemble à ceci:



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


Maintenant, implémentons l'envoi et la réception de données.



Nous créons des variables:



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

// 
let key


Cryptage et envoi des données:



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


Réception et décryptage des données:



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


Gestion des clics sur les boutons:



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


Redémarrez le serveur au cas où. Nous ouvrons http://localhost:3000. Cliquez sur le bouton "Envoyer message":







On voit les données reçues par le serveur dans le terminal:



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


Cliquez sur le bouton "Get message":







Nous voyons les mêmes données reçues par le client dans la console:



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


L'API de cryptographie Web nous ouvre des opportunités intéressantes pour protéger les informations confidentielles du côté client. Une autre étape vers le développement Web sans serveur.



Le support pour cette technologie est actuellement de 96%:







j'espère que vous avez apprécié l'article. Merci de votre attention.



All Articles