Web Cryptography API: Eine Fallstudie

Guten Tag, Freunde!



In diesem Tutorial werfen wir einen Blick auf die Web Cryptography API : eine clientseitige Datenverschlüsselungsschnittstelle. Dieses Tutorial basiert auf diesem Artikel . Es wird davon ausgegangen, dass Sie mit der Verschlüsselung vertraut sind.



Was genau machen wir jetzt? Wir werden einen einfachen Server schreiben, der verschlüsselte Daten vom Client akzeptiert und auf Anfrage zurückgibt. Die Daten selbst werden clientseitig verarbeitet.



Der Server wird in Node.js mit Express implementiert, dem Client in JavaScript. Bootstrap wird für das Styling verwendet.



Der Projektcode ist hier .



Wenn Sie interessiert sind, folgen Sie mir bitte.



Ausbildung



Erstellen Sie ein Verzeichnis crypto-tut:



mkdir crypto-tut


Wir gehen darauf ein und initialisieren das Projekt:



cd crypto-tut

npm init -y


Installieren express:



npm i express


Installieren nodemon:



npm i -D nodemon


Bearbeitung package.json:



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


Projektstruktur:



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


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


Inhalt style.css:



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

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

output span {
    color: green;
}


Server



Beginnen wir mit der Erstellung eines Servers.



Wir öffnen server.js.



Wir verbinden Express und erstellen Instanzen der Anwendung und des Routers:



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


Wir verbinden Middleware (Zwischenschicht zwischen Anfrage und Antwort):



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


Wir erstellen eine Variable zum Speichern von Daten:



let data


Wir verarbeiten den Empfang von Daten vom Kunden:



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


Wir verarbeiten das Senden von Daten an den Kunden:



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


Wir starten den Server:



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


Wir führen den Befehl aus npm start. Das Terminal zeigt die Meldung "Server bereit" an. Öffnen http://localhost:3000:







Hier sind wir mit dem Server fertig. Gehen Sie zur Clientseite der Anwendung.



Klient



Hier beginnt der Spaß.



Öffnen Sie die Datei client.js.



Für die Datenverschlüsselung wird ein symmetrischer AES-GCM-Algorithmus verwendet. Solche Algorithmen ermöglichen die Verwendung desselben Schlüssels für die Ver- und Entschlüsselung.



Erstellen Sie eine symmetrische Schlüsselgenerierungsfunktion:



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


Die Daten müssen vor der Verschlüsselung in einen Bytestream codiert werden. Dies ist mit der TextEncoder-Klasse einfach möglich:



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

    return encoder.encode(data)
}


Als nächstes benötigen wir einen Ausführungsvektor (Initialisierungsvektor, IV), bei dem es sich um eine zufällige oder pseudozufällige Folge von Zeichen handelt, die dem Verschlüsselungsschlüssel hinzugefügt werden, um seine Sicherheit zu erhöhen:



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


Nach dem Erstellen der Hilfsfunktionen können wir die Verschlüsselungsfunktion implementieren. Diese Funktion muss eine Chiffre und eine IV zurückgeben, damit die Chiffre anschließend dekodiert werden kann:



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


Nach dem Verschlüsseln der Daten mit SubtleCrypto sind sie Puffer für binäre Rohdaten. Dies ist nicht das beste Format für die Übertragung und Speicherung. Lassen Sie uns das beheben.



Die Daten werden normalerweise im JSON-Format gesendet und in einer Datenbank gespeichert. Daher ist es sinnvoll, die Daten in ein tragbares Format zu packen. Eine Möglichkeit, dies zu tun, besteht darin, die Daten in base64-Zeichenfolgen zu konvertieren:



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


Nach dem Empfang der Daten müssen Sie den umgekehrten Prozess ausführen, d. H. Konvertieren Sie Base64-codierte Zeichenfolgen in binäre Rohpuffer:



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


Es bleibt, die empfangenen Daten zu entschlüsseln. Nach der Entschlüsselung müssen wir den Bytestream jedoch in sein ursprüngliches Format dekodieren. Dies kann mit der TextDecoder-Klasse erfolgen:



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

    return decoder.decode(byteStream)
}


Die Entschlüsselungsfunktion ist die Umkehrung der Verschlüsselungsfunktion:



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


Zu diesem Zeitpunkt client.jssieht der Inhalt folgendermaßen aus:



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


Lassen Sie uns nun das Senden und Empfangen von Daten implementieren.



Wir erstellen Variablen:



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

// 
let key


Datenverschlüsselung und Senden:



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


Daten empfangen und entschlüsseln:



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


Handhabung von Tastenklicks:



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


Starten Sie den Server für alle Fälle neu. Wir öffnen http://localhost:3000. Klicken Sie auf die Schaltfläche "Nachricht senden":







Wir sehen die vom Server empfangenen Daten im Terminal:



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


Klicken Sie auf die Schaltfläche "Nachricht abrufen":







In der Konsole werden dieselben vom Client empfangenen Daten angezeigt:



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


Die Web Cryptography API eröffnet uns interessante Möglichkeiten, vertrauliche Informationen auf der Client-Seite zu schützen. Ein weiterer Schritt in Richtung serverlose Webentwicklung.



Die Unterstützung für diese Technologie beträgt derzeit 96%:







Ich hoffe, Ihnen hat der Artikel gefallen. Danke für die Aufmerksamkeit.



All Articles