Криптографія для розробників: Chapter 2

Jstify Community
10 min readJun 2, 2023

--

Вітаю товариство! Сьогодні ми продовжимо з вами розмову про криптографію для розробників.

Cписок наступних частин публікацій і попередніх:

  1. Криптографія для розробників: Chapter 1
  2. Криптографія для розробників: Chapter 3
  3. Криптографія для розробників: Chapter: 4

Хешування файлів і паролів використовуючи Node.js

Почнемо з того , що шифрування є двосторонньою операцією (ви можете зашифрувати повідомлення, а потім розшифрувати його, щоб знову отримати вихідне повідомлення), хешування є лише одностороннім. Тобто після того, як ви хешуєте повідомлення, ви жодним чином не можете отримати оригінальний відкритий текст.

Існує п’ять визначальних характеристик сучасних хеш-функцій:

  1. Детермінованість: при однаковому вхідному значенні хеш-функція систематично виробляє однакове вихідне значення (хеш-значення). Це дозволяє швидко визначати ідентичні дані та забезпечує перевірку цілісності даних.
  2. Фіксований розмір виводу: хеш-функції генерують вивід фіксованої довжини (хеші), незалежно від розміру вхідних даних. Це дозволяє ефективно зберігати та порівнювати хеші.
  3. Одностороння трансформація: хеш-функції розроблені таким чином, щоб бути незворотними, тобто обчислювально непрактично відновлювати вхідні дані з даного хеш-значення. Це робить хешування шикарним варіантом для безпечного зберігання конфіденційної інформації, такої як паролі.
  4. Ефект лавини: невелика зміна вхідних даних має призводити до значної зміни у відповідному хеш-значенні. Це забезпечує те, що навіть схожі вхідні дані створюють різні хеш-значення, що ускладнює встановлення зв’язків між вводами на основі їх хешів.
  5. Стійкість до зіткнень: знайти два різні вхідні значення, які дають однакове хеш-значення, має бути надзвичайно важко. Однак через фіксований розмір виводу ідеальна стійкість до зіткнень неможлива; мета полягає у мінімізації ймовірності зіткнень в межах практичних обмежень.

Практичне застосування хеш функцій

  1. Цілісність даних: Хеш-функції використовуються для створення контрольних сум або дайджестів повідомлень з метою перевірки цілісності переданих та збережених даних. Обчислення та порівняння значень хеша до та після передачі або зберігання дозволяють виявити випадкові або зло намірені зміни.
  2. Зберігання паролів: При зберіганні паролів користувачів зазвичай застосовують хеші паролів замість самих паролів. Під час спроби користувача увійти до системи, система обчислює хеш введеного пароля та порівнює його зі збереженим хешем. Це ускладнює можливість використання паролів зловмисником, що отримав доступ до збережених хешів паролів, для несанкціонованого доступу.
  3. Цифрові підписи: В цифрових підписах використовуються хеш-функції для забезпечення автентичності передаваних даних. Підписуючи хеш повідомлення, можна гарантувати цілісність та автентичність повідомлення без розкриття змісту, що дозволяє забезпечити безпечний обмін даними.
  4. Криптографічні операції: Хеш-функції відіграють важливу роль в різних криптографічних протоколах, серед яких функції виведення ключів (що використовуються для генерування ключів із паролів або інших секретів), коди автентифікації повідомлень (які використовуються для автентифікації комунікацій) та алгоритми proof-of-work (які використовуються в технології блокчейн, наприклад, в Bitcoin).
  5. Структури даних: Хеш-функції використовуються в структурах даних, зокрема в хеш-таблицях, які забезпечують ефективність пошуку, вставки та видалення операцій. Шляхом створення індексу на основі хеша ключа, дані можна швидко отримати та керувати ними.
  6. Перевірка дубліката: Хеш-функції можуть застосовуватися для виявлення дублікатів у великих наборах даних, перевіряючи, чи є однаковими хеші даних. Це дозволяє зекономити простір зберігання та підвищити ефективність обробки.
  7. Ідентифікація відбитків: Хеш-функції можуть використовуватися для генерування унікальних ідентифікаторів (відбитків) для даних чи об’єктів, що дозволяє їх легко порівнювати та зіставляти.

Тепер після такої великої кількості інформації, перейдімо до практики.

Хешування невеличкого речення

// Імпортуємо крипто модуль
const crypto = require('crypto');

// Функція що створює хеш використовуючи алгоритм SHA-256
function hashString(text) {
// Створюєм хеш-об’єкт із криптомодуля за вказаним алгоритмом (у цьому випадку «sha256»)
const hash = crypto.createHash('sha256');


// Оновлюєм хеш-об’єкт за допомогою нашого вхідного рядка
hash.update(text);

// Перетворюєм отриманий хеш в шістнадцятковий рядок
const result = hash.digest('hex')

return result;
}

// Тестове речення
const input = 'This is a short string';

// Передаємо у функцію наше речення
const hashedInput = hashString(input);

// Виводимо результат
console.log(`Input: ${input}`);
console.log(`Hashed Output (SHA-256): ${hashedInput}`);
Input: This is a short string
Hashed Output (SHA-256): be18814a135265c1f63dbe043bf811e450d950ae99f34763c2a2074127660399

В прикладі вище функція hashString приймає один аргумент — рядок для хешування — і створює хеш-об’єкт SHA-256. Потім він оновлює хеш-об’єкт вхідним рядком і, нарешті, перетворює його на шістнадцятковий рядок.

Хешування великих файлів

А тепер розглянемо, як ми можемо хешувати великі файли

// Імпортимо потрібні модулі
const crypto = require('crypto');
const fs = require('fs');

// Функція для створення хешу великого файлу за допомогою потоків
function hashLargeFile(filePath) {
return new Promise((resolve, reject) => {
// Створюємо потік читання для вказаного шляху до файлу
const readStream = fs.createReadStream(filePath);
// Створюємо хеш-об'єкт за вказаним алгоритмом (у цьому випадку «sha256»)
const hash = crypto.createHash('sha256');
// Передаємо дані файлу через хеш-об'єкт
readStream.pipe(hash);
// Прослуховуємо подію "data", щоб отримати остаточний хеш
hash.on('data', (hashData) => {
resolve(hashData.toString('hex'));
});
// Перевірка на помилки
readStream.on('error', (error) => {
reject(error);
});
});
}
// Умовний тестовий файл
const testFilePath = './large_file.txt';
// Власне саме хешування
hashLargeFile(testFilePath)
.then((hashedFile) => {
console.log(`File Path: ${testFilePath}`);
console.log(`Hashed Output (SHA-256): ${hashedFile}`);
})
.catch((error) => {
console.error(`Error hashing file: ${error.message}`);
});
File Path: ./large_file.txt
Hashed Output (SHA-256): 322d685841b62b2bbbc0c5a10798a83c61e0a382d3c482c5de9d22ba10bfb3d2

Функція hashLargeFile приймає один аргумент — шлях до файлу — і створює хеш-об’єкт SHA-256. Він читає файл як потік і передає дані файлу через хеш-об’єкт.

Як “зламати” хеш-функцію?

Поки не має відомих випадків зламу сучасних хеш-функцій. Хоча старіші версії даних функцій були зламані. Проте поговорімо про потенційні слабкості для їх розуміння, щоб ви могли їх превентивно захистити.

  1. Атака на принципі перебору (brute force): цей метод передбачає спробу кожного можливого значення вхідних даних, поки не буде знайдено бажане хеш-значення. Ефективність атаки залежить від розміру простору введення та складності хеш-функції.
  2. Атака словником (dictionary attack): цей метод передбачає використання попередньо обчислених хешів для списку можливих варіантів, таких як поширені паролі. Якщо у списку знайдено відповідний хеш, зловмисник може визначити вихідне значення вводу.
  3. Веселкові таблиці (Rainbow tables): це великі, попередньо розраховані таблиці хешів та їх відповідних вхідних значень. Їх можна використовувати для зворотнього визначення хешів швидко порівняно з атаками на принципі перебору або атак словником. Втім, застосування salt (додавання випадкових даних) перед хешуванням може допомогти зменшити ризик атак веселкових таблиць.
  4. Колізії: колізія виникає, коли два різних вхідних значення дають однаковий хеш як вихід. Хеш-функція вважається криптографічно безпечною, якщо знайти колізії складно. Парадокс днів народження, який є ймовірністю того, що дві людини мають однаковий день народження у даній групі, можна застосувати для виявлення колізій.
  5. Атаки на продовження довжини (Length extension attacks): деякі хеш-функції, такі як MD5 та SHA-1, можуть бути вразливими до атак на продовження довжини. У цьому випадку зловмисник може розрахувати хеш-значення вихідного повідомлення та додаткових даних, не знаючи вмісту вихідного повідомлення.

В даному переліку потенційних вразливостей я згадував Веселкові таблиці(Rainbow tables), зупинімось трошки детальніше.

Rainbow tables

Rainbow tables — це попередньо розраховані таблиці, які використовуються для зворотного визначення криптографічних хеш-функцій, зазвичай для відновлення паролів. Вони використовуються для прискорення процесу атаки перебором.

Припустимо, у нас є проста хеш-функція h(x) = x % 10, де x — вхідне значення, h(x) — значення хешу, а символ % позначає остачу від ділення.

Ми хочемо створити веселкову таблицю для цієї простої хеш-функції для всіх двоцифрових чисел як вхідних даних (від 00 до 99). Для цього ми згенеруємо значення хеша для кожного можливого вводу і збережемо його в таблиці.

А тепер розгляньмо приклад застосування у Node.js

  1. По-перше, встановимо потрібні бібліотеки:
npm install md5
npm install rainbow-table

2. Тепер створимо таблицю Rainbow з допомогою пакета “rainbow-table” і хеш-функції MD5. Збережемо таблицю в файлі:

const md5 = require("md5");
const RainbowTable = require("rainbow-table");


// Визначимо просту функцію зведення для перетворення хеша на текстове значення
function reduction(hash, index) {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let result = "";

let position = (parseInt(hash.substr(-3), 16) + index) % Math.pow(alphabet.length, 3);
for (let i = 0; i < 3; i++) {
result = alphabet[position % alphabet.length] + result;
position = Math.floor(position / alphabet.length);
}
return result;
}
// Створимо нову веселкову таблицю
const table = new RainbowTable(reduction);
// Додайте хеш-функцію, яка буде використана
table.useHashFunction((value) => {
return md5(value);
});
// Згенеруємо веселкову таблицю для паролів довжиною 3 символи
table.generate(5000);
// Збережемо таблицю рейнбоу в файлі
table.saveToFile("rainbowTable.json");

3. Тепер використовуємо згенеровану таблицю для відновлення хешу MD5:

const md5 = require("md5");
const RainbowTable = require("rainbow-table");
// Така ж функця зведення, як і раніше
function reduction(hash, index) {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let result = "";
let position = (parseInt(hash.substr(-3), 16) + index) % Math.pow(alphabet.length, 3);
for (let i = 0; i < 3; i++) {
result = alphabet[position % alphabet.length] + result;
position = Math.floor(position / alphabet.length);
}
return result;
}
// Створимо нову таблицю Rainbow
const table = new RainbowTable(reduction);
// Додайте хеш-функцію, яка буде використана
table.useHashFunction((value) => {
return md5(value);
});
// Завантажимо таблицю рейнбоу з файлу
table.loadFromFile("rainbowTable.json");
// Давайте зламаємо приклад хешу пароля
const exampleHash = md5("ABC");
const crackedPassword = table.crackHash(exampleHash);
console.log("Відновлений пароль:", crackedPassword);

Власне в реальному світі це навряд чи спрацює, але просто знати для себе, як на мене, не буде зайвим.

Salt

У криптографії сіль (salt) — це випадковий фрагмент даних, також відомий як “неперіодичний елемент” (nonce), який генерується і додається до пароля користувача перед хешуванням. Метою використання salt є збільшення безпеки зберіганих паролів, ускладнення їх злому під час застосування методів, таких як веселкові таблиці або атаки з використанням словника.

Коли користувач встановлює пароль, система генерує нову унікальну сіль та додає її до пароля. Після цього солений пароль хешується та зберігається у базі даних поряд із сіллю. Під час процесу аутентифікації до поданого пароля додається така ж сіль, хешується і результат порівнюється із збереженим хешем.

Використання солі при зберіганні паролів гарантує, що навіть якщо двоє користувачів мають однакові паролі, їхні хеші будуть унікальними завдяки різним доданим солям. Мало того, оскільки сіль є випадковою та унікальною, для зламу хешованих паролів з допомогою методів, таких як метод грубої сили, потрібно більше часу та ресурсів, що робить захищені хеші паролів безпечнішими.

Хешування паролів та отримання ключів

Для хешування паролів є спеціальний виділений пул алгоритмів, так звані “Функції похідного ключа”(KDF).

Функції похідного ключа (KDF) — це алгоритми, які використовуються для отримання секретних криптографічних ключів зі секретного значення або пароля. Вони можуть використовуватися для різних цілей, зокрема для хешування паролів, генерації ключів шифрування та захисту даних.

Список деяких широко використовуваних KDF:

  1. bcrypt: алгоритм хешування паролів, який поєднує шифр Blowfish з сіллю та адаптивним коефіцієнтом витрат, сповільнюючи процес виведення ключа та збільшуючи опір брутфорс атакам.
  2. scrypt: функція KDF з високим ступенем використання пам’яті, спеціально розроблена для підвищення опору до атак на базі ASIC та GPU. Він використовує salt, високопаралельну мішальну функцію та параметр настроювання витрат ресурсів для контролю використання пам’яті та процесора.
  3. Argon2: переможець у конкурсі хешування паролів у липні 2015 року, Argon2 має два основні варіанти: Argon2d та Argon2i. Argon2 розроблений для того, щоб бути з високим використанням пам’яті та високим опором до атак на основі часу доступу до каналу, забезпечуючи кращий захист, ніж bcrypt, scrypt або PBKDF2.

Тепер розгляньмо кожен з них на прикладі коду.

bcrypt

const bcrypt = require('bcrypt');
const password = 'YourPassword';
const saltRounds = 10;
// У цьому рядку встановлюється кількість salt раундів для використання під час хешування пароля.
// Salt раунди допомагають гарантувати безпечність хешу, додаючи йому складності.

bcrypt.hash(password, saltRounds, (err, hash) => {
if (err) throw err;
console.log('bcrypt hash:', hash);
});

scrypt

const scrypt = require('scrypt');
const password = 'YourPassword';
const salt = scrypt.randomBytes(16).toString('hex');
const keyLength = 32; // 256 bits
const N = 16384; // CPU/memory cost factor
const r = 8; // block size
const p = 1; // parallelization factor

scrypt.hash(password, {N: N, r: r, p: p}, keyLength, salt, (err, derivedKey) => {
if (err) throw err;
const key = derivedKey.toString('hex');
console.log('scrypt-derived key:', key);
});

Argon2:

const argon2 = require('argon2');
const password = 'YourPassword';

(async () => {
try {
const hash = await argon2.hash(password);
console.log('Argon2 hash:', hash);
} catch (err) {
console.error('Error hashing password:', err);
}
})();

Як ви можете бачити реалізація досить проста, але не забувайте, що це лише для прикладу.

І поговорімо про таку штуку як колізія на завершення.

Collision

У криптографії колізія відбувається, коли два різних входи дають однаковий вихід за допомогою певної функції. Це явище зазвичай пов’язане з хеш-функціями, які призначені для введення даних (або “повідомлення”) та повернення рядка байтів фіксованого розміру. В ідеалі, хеш-функція повинна мати кілька ключових властивостей, таких як детермінованість (одне й те ж введення завжди дає той самий результат) та низька ймовірність отримання одного та ж результату з двох різних введень (так зване “зіткнення”).

Колізії можуть стати проблематичними у криптографії, оскільки вони потенційно можуть підірвати захист хеш-алгоритма. Наприклад, зловмисники можуть використовувати колізії для створення підроблених цифрових підписів, зміцнення цілісності даних або компрометації таємничості системи.

Ось приклади потенційних атак:

  1. Атака на день народження: це засновано на парадоксі дня народження, який стверджує, що ймовірність того, що дві людини мають один день народження, більш імовірна, ніж припускає інтуїція. У криптографії це означає, що легше знайти два різних входи з однаковим хеш-виходом, ніж знайти певний вхід, який генерує заздалегідь визначений хеш-вихід. Атаки на день народження використовуються для пошуку колізій у хеш-функціях і потенційно можуть порушити безпеку цифрових підписів.
  2. Атака на попередній образ: це коли зловмисник намагається знайти вхідні дані, які хешують до певного цільового виводу (також відомого як попередній образ). В ідеалі хеш-функції повинні бути стійкими до атак на попередні зображення, оскільки вони можуть поставити під загрозу цілісність і автентичність цифрових даних.
  3. Друга атака прообразу: це подібно до атаки прообразу, але зловмисник намагається знайти інше введення, яке дає той самий хеш-вихід, що й даний вхід. Стійкість до другого прообразу гарантує, що надзвичайно складно згенерувати два вхідних дані з однаковим хеш-значенням, таким чином зберігаючи цілісність і безпеку системи.

Висновки:

Ми з вами розглянули сьогодні таку штуку як хеш функції. Поговорили про їхні сильні і слабкі сторони, для чого вони взагалі існують. Також поговорили про Rainbowtable, Collision, Salt. Розглянули рекомендовані алгоритми для хешування паролів. Надіюсь вам було цікаво, хоч і розказав я це досить поверхнево, і це потребує від вас більш детального дослідження самостійно.

Корисні посилання:

--

--