Доброго времени суток, друзья!
В этом туториале мы рассмотрим Web Cryptography API: интерфейс шифрования данных на стороне клиента.
Данный туториал основан на этой статье.
Предполагается, что вы немного знакомы с шифрованием.
Что конкретно мы будем делать?
Мы напишем простой сервер, который будет принимать зашифрованные данные от клиента и возвращать их ему по запросу. Сами данные будут обрабатываться на стороне клиента.
Сервер будет реализован на Node.js с помощью Express, клиент на JavaScript. Для стилизации будет использоваться Bootstrap.
Код проекта находится здесь.
Поиграть с кодом можно здесь.
Если вам это интересно, прошу следовать за мной.
Подготовка
Создаем директорию
crypto-tut
:
mkdir crypto-tut
Заходим в нее и инициализируем проект:
cd crypto-tutnpm init -y
Устанавливаем
express
:
npm i express
Устанавливаем
nodemon
:
npm i -D nodemon
Редактируем
package.json
:
"main": "server.js","scripts": { "start": "nodemon"},
Структура проекта:
crypto-tut --node_modules --src --client.js --index.html --style.css --package-lock.json --package.json --server.js
Содержание
index.html
:
<head> <!-- Bootstrap CSS --> <link rel="stylesheet" href="http://personeltest.ru/aways/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>
Содержание
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.js
.Подключаем express и создаем экземпляры приложения и маршрутизатора:
const express = require('express')const app = express()const router = express.Router()
Подключаем middleware (промежуточный слой между запросом и ответом):
// разбор запросаapp.use(express.json({ type: ['application/json', 'text/plain']}))// подключение роутераapp.use(router)// директория со статическими файламиapp.use(express.static('src'))
Создаем переменную для хранения данных:
let data
Обрабатываем получение данных от клиента:
router.post('/secure-api', (req, res) => { // получаем данные из тела запроса data = req.body // выводим данные в терминал console.log(data) // закрываем соединение res.end()})
Обрабатываем отправку данных клиенту:
router.get('/secure-api', (req, res) => { // данные отправляются в формате JSON, // после чего соединение автоматически закрывается res.json(data)})
Запускаем сервер:
app.listen(3000, () => console.log('Server ready'))
Выполняем команду
npm start
. В терминале появляется
сообщение Server ready. Открываем
http://localhost:3000
:На этом с сервером мы закончили, переходим к клиентской части приложения.
Клиент
Здесь начинается самое интересное.
Открываем файл
client.js
.Для шифрования данных будет использоваться симметричный алгоритм AES-GCM. Такие алгоритмы позволяют использовать один и тот же ключ для шифрования и расшифровки.
Создаем функцию генерации симметричного ключа:
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKeyconst generateKey = async () => window.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256, }, true, ['encrypt', 'decrypt'])
Перед шифрованием данные необходимо закодировать в поток байтов. Это легко сделать с помощью класса TextEncoder:
// https://developer.mozilla.org/en-US/docs/Web/API/TextEncoderconst encode = data => { const encoder = new TextEncoder() return encoder.encode(data)}
Далее, нам нужен вектор исполнения (вектор инициализации, initialization vector, IV), представляющий собой случайную или псевдослучайную последовательность символов, которую добавляют к ключу шифрования для повышения его безопасности:
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValuesconst generateIv = () => // https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams window.crypto.getRandomValues(new Uint8Array(12))
После создания вспомогательных функций, мы можем реализовать функцию шифрования. Данная функция должна возвращать шифр и IV для того, чтобы шифр можно было впоследствии декодировать:
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 }}
После шифрования данных с помощью SubtleCrypto, они представляют собой буферы необработанных двоичных данных. Это не лучший формат для передачи и хранения. Давайте это исправим.
Данные, обычно, передаются в формате JSON и хранятся в базе данных. Поэтому имеет смысл упаковать данные в портируемый формат. Одним из способов это сделать является конвертация данных в строки в формате base64:
// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-Stringconst pack = buffer => window.btoa( String.fromCharCode.apply(null, new Uint8Array(buffer)))
После получения данных необходимо выполнить обратный процесс, т.е. преобразовать строки в кодировке base64 в буферы необработанных двоичных данных:
// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-Stringconst 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}
Остается расшифровать полученные данные. Однако, после расшифровки нам необходимо декодировать поток байтов в исходный формат. Это можно сделать с помощью класса TextDecoder:
// https://developer.mozilla.org/en-US/docs/Web/API/TextDecoderconst decode = byteStream => { const decoder = new TextDecoder() return decoder.decode(byteStream)}
Функция расшифровки представляет собой инверсию функции шифрования:
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/decryptconst decrypt = async (cipher, key, iv) => { const encoded = await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, cipher) return decode(encoded)}
На данном этапе содержимое
client.js
выглядит так:
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)}
Теперь реализуем отправку и получение данных.
Создаем переменные:
// поле для ввода сообщения, которое будет зашифрованоconst input = document.querySelector('input')// контейнер для вывода результатовconst output = document.querySelector('output')// ключlet key
Шифрование и отправка данных:
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>Данные отправлены на сервер.`}
Получение и расшифровка данных:
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> расшифровано.`}
Обработка нажатия кнопок:
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() }})
На всякий случай перезапускаем сервер. Открываем
http://localhost:3000
. Нажимаем на кнопку Send
message:Видим данные, полученные сервером, в терминале:
{ cipher: 'j8XqWlLIrFxyfA2easXkJTLLIt9x8zLHei/tTKI=', iv: 'F8doVULJzbEQs3M1'}
Нажимаем на кнопку Get message:
Видим те же самые данные, полученные клиентом, в консоли:
{ cipher: 'j8XqWlLIrFxyfA2easXkJTLLIt9x8zLHei/tTKI=', iv: 'F8doVULJzbEQs3M1'}
Web Cryptography API открывает перед нами интересные возможности по защите конфиденциальной информации на стороне клиента. Еще один шаг в сторону бессерверной веб-разработки.
Поддержка данной технологии на сегодняшний день составляет 96%:
Надеюсь, статья вам понравилась. Благодарю за внимание.