Blogartikel | Web Crypto API
Von Lukas Gruber am 28.02.2024
Dieser Artikel stellt einige Funktionen der Web Crypto API (https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) vor. Als Anwendungsbeispiel dafür soll eine Nachricht signiert und Symmetrisch verschlüsselt werden. Der für die symmetrische Verschlüsselung verwendete Key soll anschließend durch eine asymmetrische Verschlüsselung an einen anderen User weitergegeben werden.
Setup
In den nachfolgenden Kapiteln angeführte Funktionen sind Teil der nachstehenden Klasse “CryptoKeyService”. Die Funktion test() dient zum Überprüfen der Funktionsweise.
export default class CryptoKeyService {
static algorithmAsym = "RSA-OAEP";
static algorithmSym = "AES-GCM";
static algorithmSign = "ECDSA";
static async test() {
// B Side
const bAsymKeyPair = await this.generateAsymKeyPair();
// A Side
const aSymKey = await this.generateSymKey();
const wrappedSymKey = await this.wrapSymKeyWithAsymKey(bAsymKeyPair.publicKey, aSymKey);
const aSignKeyPair = await this.generateSignKeyPair();
const aSignature = await this.signMessage(aSignKeyPair.privateKey, wrappedSymKey);
// B Side
const data = "Super Secret Test Message!";
const isWrappedKeyOk = await this.verifyMessage(aSignKeyPair.publicKey, aSignature, wrappedSymKey);
console.log(isWrappedKeyOk);
const unwrappedSymKey = await this.unwrapAsymSymKey(wrappedSymKey, bAsymKeyPair.privateKey);
const bSignKeyPair = await this.generateSignKeyPair();
const bSignature = await this.signMessage(bSignKeyPair.privateKey, data);
const iv = crypto.getRandomValues(new Uint8Array(16));
const bSymEncryptedData = await this.encryptSymMessage(unwrappedSymKey, iv, data);
// A Side
const decryptedData = await this.decryptSymMessage(aSymKey, iv, bSymEncryptedData);
const isDataOk = await this.verifyMessage(bSignKeyPair.publicKey, bSignature, decryptedData);
console.log(decryptedData, isDataOk);
}
... // Functions
}
Wird die Testfunktion mit CryptoKeyService.test() aufgerufen wird der in nachstehender Grafik dargestellte Ablauf simuliert. Damit es nicht zu kompliziert wird, wird nur so getan als ob die Daten versendet worden wären. Die Konsolenausgabe gibt “Super Secret Test Message!, true” zurück.
Alle Funktionen haben gemeinsam das die zu übergebenden bzw. rückgegebenen Keys im “JSON Web Key” Format sind.
Die WebCrypto API unterstützt eine ganze Reihe von Algorithmen, wobei jeder seinen eigenen Anwendungszweck besitzt.
Für die Assymetrische Verschlüsselung wurde RSA-OAEP gewählt, für die synchrone AES-GCM und für das signieren von Nachrichten ECDSA.
Asymmetrische Verschlüsselung
Bei der assymetrischen Verschlüsselung wird ein Schlüssepaar erzeugt, welches aus aus einem privaten – beim Ersteller verbleibenden – und einem öffentlichen – den jeder haben darf – Schlüssel besteht. Eine Nachricht wird mit dem öffentlichen Schlüssel des Empfängers verschlüsselt. Der Empfänger kann diese dann mit seinem privaten Schlüssel entschlüsseln.
Anwendungszwecke sind etwa der Austausch synchroner Schlüssel, verschlüsseltes browsing oder auch digitale Signaturen. Mit der Subtle WebCrypto API lässt sich diese Form wie folgt umsetzen.
static async generateAsymKeyPair() {
const keyPair = await crypto.subtle.generateKey(
{
name: this.algorithmAsym,
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
},
true,
["encrypt", "decrypt", "wrapKey", "unwrapKey"],
);
const publicKey = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
const privateKey = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
return { publicKey, privateKey };
}
static async encryptAsymMessage(publicKey: JsonWebKey, message: string) {
const importedKey = await this.importAsymKey(publicKey, ["encrypt"]);
const encMessage = new TextEncoder().encode(message);
return await window.crypto.subtle.encrypt(this.algorithmAsym, importedKey, encMessage);
}
static async decryptAsymMessage(privateKey: JsonWebKey, encryptedMessage: ArrayBuffer) {
let returnMessage = "";
try {
const importedKey = await this.importAsymKey(privateKey, ["decrypt"]);
let decrypted = await crypto.subtle.decrypt(this.algorithmAsym, importedKey, encryptedMessage);
returnMessage = new TextDecoder().decode(decrypted);
} catch (error) {
returnMessage = "Error decrypting message";
}
return returnMessage;
}
static async importAsymKey(key: JsonWebKey, keyUsage: KeyUsage[]) {
return await crypto.subtle.importKey(
"jwk",
key,
{
name: this.algorithmAsym,
hash: "SHA-256",
},
true,
keyUsage,
);
}
Symmetrische Verschlüsselung
Bei der symmetrischen Verschlüsselung wird nur ein Schlüssel erzeugt, den sich alle Clients teilen. Die Herausforderung besteht darin diesen Schlüssel allen Teilnehmern sicher zukommen zu lassen. Vorteil hingegen ist das diese Methode deutlich performanter ist als die asynchrone Verschlüsselung.
Anwendungszweck ist daher zum Beispiel die Verschlüsselung größerer Datenmengen. Mit der Subtle WebCrypto API lässt sich diese Form wie folgt umsetzen. Anzumerken ist das iv Feld welches dem Empfänger mitgesendet wird.
static async generateSymKey() {
const key = await crypto.subtle.generateKey(
{
name: this.algorithmSym,
length: 256,
},
true,
["encrypt", "decrypt"],
);
return await await crypto.subtle.exportKey("jwk", key);
}
static async encryptSymMessage(key: JsonWebKey, iv: Uint8Array, message: string) {
const importedKey = await this.importSymKey(key, ["encrypt"]);
const encMessage = new TextEncoder().encode(message);
return await window.crypto.subtle.encrypt(
{
name: this.algorithmSym,
iv,
},
importedKey,
encMessage,
);
}
static async decryptSymMessage(key: JsonWebKey, iv: Uint8Array, encryptedMessage: ArrayBuffer) {
let returnMessage = "";
try {
const importedKey = await this.importSymKey(key, ["decrypt"]);
let decrypted = await window.crypto.subtle.decrypt(
{
name: this.algorithmSym,
iv,
},
importedKey,
encryptedMessage,
);
returnMessage = new TextDecoder().decode(decrypted);
} catch (error) {
returnMessage = "Error decrypting message";
}
return returnMessage;
}
static async importSymKey(key: JsonWebKey, keyUsage: KeyUsage[]) {
return await crypto.subtle.importKey(
"jwk",
key,
{
name: this.algorithmSym,
length: 256,
},
true,
keyUsage,
);
}
Signatur
Anders als bei der asynchronen Verschlüsselung wird beim signieren nicht der öffentliche Schlüssel sondern der private angewendet. Zur Verifizierung wird der öffentliche Schlüssel auf der Empfängerseite benötigt.
Anwendungszweck ist zur weiteren Absicherung eines Nachrichtenaustauschs, etwa um Man in the Middle Angriffe erkennen zu können. Mit der Subtle WebCrypto API lässt sich diese Form wie folgt umsetzen.
static async generateSignKeyPair() {
const keyPair = await crypto.subtle.generateKey(
{
name: this.algorithmSign,
namedCurve: "P-384",
},
true,
["sign", "verify"],
);
const publicKey = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
const privateKey = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
return { publicKey, privateKey };
}
static async signMessage(privateKey: JsonWebKey, message: string | ArrayBuffer): Promise<ArrayBuffer> {
const importedKey = await this.importSignKey(privateKey, ["sign"]);
return await window.crypto.subtle.sign(
{
name: this.algorithmSign,
hash: { name: "SHA-384" },
},
importedKey,
typeof message === "string" ? new TextEncoder().encode(message) : message,
);
}
static async verifyMessage(publicKey: JsonWebKey, signature: ArrayBuffer, message: string | ArrayBuffer): Promise<boolean> {
let returnMessage = false;
try {
const importedKey = await this.importSignKey(publicKey, ["verify"]);
return await window.crypto.subtle.verify(
{
name: this.algorithmSign,
hash: { name: "SHA-384" },
},
importedKey,
signature,
typeof message === "string" ? new TextEncoder().encode(message) : message,
);
} catch (error) {
returnMessage = false;
}
return returnMessage;
}
static async importSignKey(key: JsonWebKey, keyUsage: KeyUsage[]) {
return await crypto.subtle.importKey(
"jwk",
key,
{
name: this.algorithmSign,
namedCurve: "P-384",
},
true,
keyUsage,
);
}
Wrapping
Die Wrapping Funktion dient dazu einen bestehenden Key mit einem anderen abzusichern. So kann etwa ein synchroner Schlüssel mit einen asychronen Schlüssel gepackt werden, um diesen anschließend zu einem Client schicken zu können.
static async wrapSymKeyWithAsymKey(publicKey: JsonWebKey, symKeyToWrap: JsonWebKey) {
const importedPublicKey = await this.importAsymKey(publicKey, ["wrapKey"]);
const importedKeyToWrap = await this.importSymKey(symKeyToWrap, ["encrypt", "decrypt"]);
return await crypto.subtle.wrapKey("jwk", importedKeyToWrap, importedPublicKey, this.algorithmAsym);
}
static async unwrapAsymSymKey(wrappedSymKey: ArrayBuffer, privateKey: JsonWebKey) {
const importedPrivateKey = await this.importAsymKey(privateKey, ["unwrapKey"]);
const key = await crypto.subtle.unwrapKey("jwk", wrappedSymKey, importedPrivateKey, this.algorithmAsym, this.algorithmSym, true, [
"encrypt",
"decrypt",
]);
return await crypto.subtle.exportKey("jwk", key);
}
Schlussworte
Die WebCrypto API ist ein umfangreiches Werkzeug um simple Nachrichtenverschlüsselungen vorzunehmen. Möchte man aber tatsächlich eine hochsicherheitsanwendung entwickeln, so ist diese mit Vorsicht anzuwenden. So wird in den Mozilla Developer Docs extra darauf hingewiesen das schnell einmal etwas übersehen werden kann.
* Titelbild wurde mit Bing Image KI erstellt
The comments are closed.