[JavaScript, Node.JS, Программирование, Разработка веб-сайтов] Web Cryptography API: пример использования   
    
    
        
    
    
    
    
            
    
        
            
                
                                    
                
                                    
                
                    
                
            
        
    
    
        
            
                
                
                    
                         
                         
                       
                    
                        Автор 
                        Сообщение 
                    
                                        
                        
                            
                                
                                
                                                                                                            news_bot ®
                                                                        
                                                                                                                                                
                                                                            
                                                                                                                
                                            Стаж: 7 лет 8 месяцев                                        
                                                                                                                
                                            Сообщений: 27286                                        
                                                                                                                                                
                                                             
                            
                                
                             
                         
                        
                            
                                
                                    
                                        
                                        
 
Доброго времени суток, друзья!
В этом туториале мы рассмотрим 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%:

Надеюсь, статья вам понравилась. Благодарю за внимание.
===========
 Источник:
habr.com
===========
Похожие новости:
- [JavaScript, TypeScript, SvelteJS] Svelte <3 TypeScript (перевод)
 
- [DevOps, Kubernetes, Интервью, Серверное администрирование] Команда поддержки систем хранения данных Bloomberg полагается на открытый исходный код и SDS (перевод)
 
- Прекращение разработки библиотеки Moment.js, имеющей 12 млн загрузок в неделю
 
- [Карьера в IT-индустрии, Программирование, Учебный процесс в IT] «Раньше я не представлял лекции в МГУ по Zoom». Интервью с преподавателем Техносферы
 
- [Высокая производительность, Программирование, Проектирование и рефакторинг] Почему так важна иммутабельность (перевод)
 
- [Программирование, Управление разработкой, Управление персоналом, Карьера в IT-индустрии] Как начать подгорать, но не выгореть в проекте, где «было срочно нужно»
 
- [Поисковая оптимизация, Разработка веб-сайтов] SiteAnalyzer 2.2 — бесплатный аудит сайта
 
- [Разработка веб-сайтов, API] Как мы использовали GraphQL в разработке на примере интернет-каталога
 
- [Управление разработкой, Разработка для интернета вещей, Производство и разработка электроники, Дизайн, Электроника для начинающих] Кухня промдизайна #1: почти идеальная разработка корпуса модуля IRRIOT
 
- [Виртуализация, Облачные сервисы, Производство и разработка электроники] Привет, Habr! Давай знакомиться — мы Getmobit и мы здесь новенькие
 
Теги для поиска: #_javascript, #_node.js, #_programmirovanie (Программирование), #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_programmirovanie (программирование), #_razrabotka (разработка), #_webcryptographyapi, #_subtlecrypto, #_cryptography, #_crypto, #_kriptografija (криптография), #_shifrovanie (шифрование), #_shifr (шифр), #_javascript, #_node.js, #_programmirovanie (
Программирование
), #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
                                        
                                        
                                        
                                     
                                    
                                    
                                                                    
                                                                                             
                         
                        
                            
                                                                    
                                                             
                         
                    
                    
                
                
            
        
    
    
    
    
    
            
    
            
    
        
    
    
        
                        Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
    
    
        
        Текущее время: 04-Ноя 14:39
Часовой пояс: UTC + 5 
            
    
                
| Автор | Сообщение | 
|---|---|
| 
                                
                                
                                                                                                            news_bot ®
                                                                        
                                                                                                                                                 
                                                                            
                                                                                                                
                                            Стаж: 7 лет 8 месяцев                                          | 
                            |
                                ![]() Доброго времени суток, друзья! В этом туториале мы рассмотрим 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%: ![]() Надеюсь, статья вам понравилась. Благодарю за внимание. =========== Источник: habr.com =========== Похожие новости: 
 Программирование ), #_razrabotka_vebsajtov ( Разработка веб-сайтов )  | 
                        |
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
    Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 04-Ноя 14:39
Часовой пояс: UTC + 5