Це цікаво

Чому Telegram Passport — ніякий не End to End

Привіт, %username%!

В обговоренні новини про Passport розгорілися жаркі дискусії на тему безпеки останньої вироби від авторів Telegram.

Давайте подивимося, як він шифрує ваші персональні дані і поговоримо про сьогодення End-To-End.

У двох словах, як працює Passport.

  • Ви локально за допомогою пароля шифруете свої персональні дані (ім’я, email, скан паспорта, інші документи).
  • Зашифровані дані + метаінформація для завантажуються в хмару Telegram.
  • Коли потрібно авторизуватися на сервісі, клієнт завантажує дані з хмари, розшифровує їх паролем, перешифровывает на публічний ключ того сервісу, який запросив інформацію, і відправляє.

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

End to End на думку розробників полягає в тому, що хмара Telegram нібито не може розшифрувати ваші персональні дані, а бачить тільки «випадковий шум».

Давайте докладніше глянемо на код алгоритму шифрування персональних даних з десктоп клієнта, який знаходиться тут і подивимося, чи задовольняє результат його роботи критеріями End-To-End.

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

bytes::vector CountPasswordHashForSecret(
 bytes::const_span salt,
 bytes::const_span password) {
 return openssl::Sha512(bytes::concatenate(
salt,
password,
salt));
}

Тут береться випадкова сіль, двічі конкатенируется з паролем і проганяється через хеш SHA-512. На перший погляд, нічого незвичайного. Але!

На дворі 2018 рік. На одному хорошому GPU можна перебирати приблизно півтора мільярда SHA-512 в секунду. 10 GPU переберуть всі можливі поєднання 8-цифрових паролів з 94 х символьного словника (англ літери, цифри, спец символи) менше ніж за 5 днів.

Давним давно існують способи ускладнити життя тим, хто перебирає паролі на GPU, але розробники Telegram вирішили не обтяжувати себе їх впровадженням.

Далі. Хешем з пароль шифрується ще один майже випадковий ключ, який генерується так:

bytes::vector GenerateSecretBytes() {
 auto result = bytes::vector(kSecretSize);
 memset_rand(result.data(), result.size());
 const auto full = ranges::accumulate(
result,
0ULL,
 [](uint64 sum, gsl::byte value) { return sum + uchar(value); });
 const auto mod = (full % 255ULL);
 const auto add = 255ULL + 239 - mod;
 auto first = (static_cast<uchar>(result[0]) + add) % 255ULL;
 result[0] = static_cast<gsl::byte>(first);
 return result;
}

і використовується для шифрування даних разом з ще однією штукою, про яку нижче.

Випадковий він «майже», тому що розробники телеграма ніколи не чули про HMAC AEAD і замість того, щоб використовувати нормальні засоби для перевірки коректності розшифровки, вони роблять так, щоб залишок від ділення суми байт ключа дорівнював 239, що при розшифровці і перевіряють:

bool CheckBytesMod255(bytes::const_span bytes) {
 const auto full = ranges::accumulate(
bytes,
0ULL,
 [](uint64 sum, gsl::byte value) { return sum + uchar(value); });
 const auto mod = (full % 255ULL);
 return (mod == 239);
}

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

Йдемо далі. Безпосередньо метод, що шифрує дані. Тут багато букв, тому по шматках:

EncryptedData EncryptData(
 bytes::const_span bytes,
 bytes::const_span dataSecret) {
 constexpr auto kFromPadding = kMinPadding + kAlignTo - 1;
 constexpr auto kPaddingDelta = kMaxPadding - kFromPadding;
 const auto randomPadding = kFromPadding
 + (rand_value<uint32>() % kPaddingDelta);
 const auto padding = randomPadding
 - ((bytes.size() + randomPadding) % kAlignTo);
 Assert(padding >= kMinPadding && padding <= kMaxPadding);

 auto unencrypted = bytes::vector(padding + bytes.size());
 Assert(unencrypted.size() % kAlignTo == 0);

 unencrypted[0] = static_cast<gsl::byte>(padding);
 memset_rand(unencrypted.data() + 1, padding - 1);
bytes::copy(
gsl::make_span(unencrypted).subspan(padding),
bytes);

Тут до даних дописуються від 32 до 255 випадкових байт. Робиться це для того, щоб урізноманітнити
змінну dataHash. Це хеш від незашифрованих даних, змішаних з випадковими байтами.

 const auto dataHash = openssl::Sha256(unencrypted);
 const auto bytesForEncryptionKey = bytes::concatenate(
dataSecret,
dataHash);

 auto params = PrepareAesParams(bytesForEncryptionKey);
 return {
 { dataSecret.begin(), dataSecret.end() },
 { dataHash.begin(), dataHash.end() },
 Encrypt(unencrypted, std::move(params))
};
}

Тут формується ключ шифрування персональних даних. Він виходить з допомогою ще одного виклику SHA-512 від згенерованого вище майже випадкового ключа, сконкатеннированного з dataHash.

Підсумок

У хмару передаються:

  1. Хеш від персональних даних, змішаних з випадковими байтами
  2. Зашифрований паролем майже випадковий ключ
  3. Сіль
  4. Зашифровані дані

Це далеко не «випадковий шум», тут є все необхідне, включаючи ключ шифрування, захищений паролем. І це дозволяє дістатися до даних користувачів набагато, набагато швидше ніж перебирати всі можливі комбінації ключів AES (2^256).

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

Приблизний алгоритм брутфорса:

  1. Беремо пароль по порядку, генеруємо хеш від нього і солі (GPU)
  2. Спробуємо розшифрувати ключ (AES-NI)
  3. Дивимося на суму байт і відразу відсіваємо майже всі невірні паролі.
  4. Формуємо ключ-кандидат на розшифровування даних за допомогою ще одного виклику SHA-512 (GPU)
  5. Спробуємо розшифрувати перший блок даних (AES-NI)
  6. Щоб не витрачати час на повне розшифрування і ще один SHA-256, ми можемо прискорити брутфорс, перевіряючи перший байт вирівнювання так само як вони самі це роблять:

if (padding < kMinPadding
 || padding > kMaxPadding
 || padding > decrypted.size()) {

Отже, ми бачимо, що шифрування персональних даних критично залежить від складності пароля. Всі етапи перебору відмінно прискорюються апаратно. Або за допомогою GPU, або з допомогою інструкцій AES-NI. Звичайно, можна встановити довгий, безпечний пароль і сподіватися що прокотить. Але як ви самі думаєте, який відсоток з двохсот мільйонів користувачів телеграма буде робити паролі довше восьми символів?

Додайте до цього сумнівні техніки генерації ключів і перевірки валідності расшифровываемой інформації, які не використовують стандартні перевірені механізми, а прямо порушують принцип don’t roll your own crypto і стане ясно, що це не End-to-End, а збита на коліні виріб від якої неприємно пахне.

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

Справжній End-to-End

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

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

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

Але Telegram пішов своїм, особливим шляхом переизобретения криптопримітивів і ослаблення захисту. Ну а що, грошей їм відвалили, за наслідки можна не турбуватися.

P. S. якщо є бажання подивитися, як працює чат з цим E2E, у VirgilSecurity є демо проект, який можна завантажити і погратися.

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Close