Web Cryptography API: пример использования

от автора

Доброго времени суток, друзья! В этом туториале мы рассмотрим Web Cryptography API: интерфейс шифрования данных на стороне клиента. Данный туториал основан на этой статье. Предполагается, что вы немного знакомы с шифрованием.

Что конкретно мы будем делать? Мы напишем простой сервер, который будет принимать зашифрованные данные от клиента и возвращать их ему по запросу. Сами данные будут обрабатываться на стороне клиента.

Сервер будет реализован на Node.js с помощью Express, клиент — на JavaScript. Для стилизации будет использоваться Bootstrap.

Код проекта находится здесь.

Поиграть с кодом можно здесь.

Если вам это интересно, прошу следовать за мной.

Подготовка

Создаем директорию crypto-tut:

mkdir crypto-tut 

Заходим в нее и инициализируем проект:

cd crypto-tut  npm 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="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> 

Содержание 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/generateKey const generateKey = async () =>     window.crypto.subtle.generateKey({         name: 'AES-GCM',         length: 256,     }, true, ['encrypt', 'decrypt']) 

Перед шифрованием данные необходимо закодировать в поток байтов. Это легко сделать с помощью класса TextEncoder:

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

Далее, нам нужен вектор исполнения (вектор инициализации, initialization vector, IV), представляющий собой случайную или псевдослучайную последовательность символов, которую добавляют к ключу шифрования для повышения его безопасности:

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

После создания вспомогательных функций, мы можем реализовать функцию шифрования. Данная функция должна возвращать шифр и 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-String const 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-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 } 

Остается расшифровать полученные данные. Однако, после расшифровки нам необходимо декодировать поток байтов в исходный формат. Это можно сделать с помощью класса TextDecoder:

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

Функция расшифровки представляет собой инверсию функции шифрования:

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

На данном этапе содержимое 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%:

Надеюсь, статья вам понравилась. Благодарю за внимание.

ссылка на оригинал статьи https://habr.com/ru/post/519378/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *