Перейти к основному содержимому
  1. Посты/

Собственный Криптографический Протокол: От Идеи до Внедрения. Cloudy — Часть 2.

·6 минут· loading · loading · ·
Техническое Flutter Dart
Оглавление

Танцуйте так, будто никто не смотрит. Шифруйтесь так, будто все смотрят.

Без сомнения, моя самая любимая часть истории Cloudy — разработка и реализация протокола ETP. Хотя назвать его революционным или “убийцей” современных решений трудно, для меня он важен по причине того, что к его созданию я пришел самостоятельно, пройдя через критику и пересмотр различных вариантов. Да, можно сказать, что я изобрел велосипед, и он, возможно, уступает современным решениям, но именно в этом процессе я нашел настоящую ценность для себя и моего приложения.

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

Концепт взаимодействия компонентов.
#

Как вообще я себе это видел в самом начале?

Тэк-с хочется чтобы писать можно и звонить можно было и вообще все, да еще и с шифрованием :D.

Пользователь заходит и регистрируется. На этом этапе создается уникальная сущность, на основе которой генерируются уникальные ключи для шифрования текста. Желательно, чтобы приватные ключи оставались исключительно на устройствах пользователей, а публичные хранились на сервере. Когда создаешь диалог что-то происходит где-то появляются ключи и этими ключами ты шифруешь сообщения и отправляешь на сервер.

Реализация.
#

Загрузка приложения и регистрация аккаунта.
#

Как и в любом взаимодействии с приложением, первым делом необходимо определить, кто вы. На первой странице пользователю предлагается заполнить обязательные поля (имя, ключевая фраза) и необязательные (интересы, аватар, пол). Обязательные поля играют ключевую роль в идентификации аккаунта, тогда как необязательные служат лишь для дополнительной персонализации. Функция формирования ключа (Key Derivation Function) отвечает за генерацию ваших приватного и публичного ключа. Для начала стоит пояснить, что такое публичный и приватный ключ. Эти ключи представляют собой часть асимметричного шифрования, где данные нельзя зашифровать и расшифровать одним и тем же ключом. Публичный и приватный ключ, в свою очередь, представляют собой набор байтов, связанных между собой математически и образующих пару, которая позволяет безопасно обмениваться данными. Благодаря сложным математическим алгоритмам, используемым при создании ключевой пары, невозможно вычислить приватный ключ, зная только публичный. Зачем это нужно, станет ясно позже.

Так вот, вернемся к созданию профиля. При вводе никнейма и ключевой фразы создается уникальный хэш-код, резервируемый за вами. Алгоритм генерации выглядит так:

String createAID(String nickname, List<String> keyPhrase) {
  String input = nickname + keyPhrase.join();
  
  var bytes = utf8.encode(input);
  var digest = sha256.convert(bytes);
  
  return digest.toString();
}

На выходе формируется уникальный идентификатор аутентификации (Authentication IDentificator). Если при регистрации другого пользователя совпадет никнейм, система сообщит об ошибке с предложением выбрать другое имя, предотвращая возможность подбора ключевой фразы для входа. Это важно, поскольку метод авторизации во многом основан на этом процессе. (Другими словами, если бы система оповещала о совпадении хэша, зная эти данные, человек мог бы авторизоваться под чужими учетными данными. Ведь по сути, логин — это никнейм, а ключевая фраза — пароль.)

Внедрение алгоритма шифрования в приложение
#

Как уже упоминалось ранее, создавая уникальный хэш, вы также генерируете:
а) Уникальное семечко, связанное с вашим никнеймом и ключевой фразой. С его помощью вы сможете всегда входить в ваш аккаунт с любого устройства, при этом не теряя диалоги. Данные остаются неизменными, и семечко также будет тем же.

Uint8List _createSeed(String nickname, List<String> keyPhrase) {
  final input = nickname + keyPhrase.join();
  final bytes = utf8.encode(input);
  final digest = sha256.convert(bytes);
  return Uint8List.fromList(digest.bytes);
}

б) уникальные ключи свойственные только вам:

Future<AsymmetricKeyPair<PublicKey, PrivateKey>> createKeyPair(
    String nickname, List<String> keyPhrase) async {
  final seed = _createSeed(nickname, keyPhrase);
  final secureRandom = _secureRandomFromSeed(seed);

  final keyGen = RSAKeyGenerator()
    ..init(ParametersWithRandom(
      RSAKeyGeneratorParameters(BigInt.parse('65537'), 2048, 64),
      secureRandom,
    ));

  final keys = keyGen.generateKeyPair();

  return keys;
}

Кстати, в методе createKeyPair используется алгоритм RSA для генерации пары ключей (публичного и приватного). RSA — это асимметричный алгоритм шифрования, обеспечивающий безопасность данных на основе сложности факторизации больших чисел. Безопасность алгоритма основана на трудности факторизации произведения двух больших простых чисел. Поскольку никнеймы и ключевые фразы уникальны, создаваемые ключи тоже уникальны для каждого пользователя. Используемый параметр BigInt.parse(‘65537’) — это открытая экспонента, а размер ключа в 2048 бит дает ощутимую криптостойкость.

Получается, у нас уже есть и ключи, и AID. К счастью, это все, что нам нужно для создания защищенного диалога. Моделируем ситуацию: я — пользователь A, ты — пользователь B, и я хочу написать тебе, имея твой публичный хэш(AID), а также с учетом того, что у тебя включен доступ для добавления в друзья.

Что происходит под капотом?
#

Генерация AES ключа.
#

AES (Advanced Encryption Standard) — симметричный алгоритм блочного шифрования, который можно использовать как для шифрования, так и для расшифрования с одним и тем же ключом. Я решил не углубляться в сложные детали и использовать уже готовое решение по нескольким причинам, основная из которых — недостаток знаний и времени для создания своего алгоритма. Люди тратят годы, а порой и поколения, чтобы разработать хотя бы один эффективный алгоритм, так что я выбрал более реалистичный подход.

Я упоминаю AES не случайно: когда я инициирую диалог с тобой, система генерирует совершенно случайный ключ, с помощью которого будет шифроваться вся переписка. Процесс выглядит примерно так:

String generateRandomAESKey() {
  final keyBytes = Uint8List(32);
  final random = Random.secure();
  
  for (int i = 0; i < keyBytes.length; i++) {
    keyBytes[i] = random.nextInt(256);
  }
  
  return base64Encode(keyBytes);
}

Шифрование симметричного ключа публичными ключами.
#

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

String encryptSymmetricKey(String symmetricKey, PublicKey publicKey) {
  final symmetricKeyBytes = base64Decode(symmetricKey);

  final encryptor = RSAEngine()
    ..init(true, PublicKeyParameter<RSAPublicKey>(publicKey as RSAPublicKey));

  final encrypted = _processInBlocks(encryptor, symmetricKeyBytes);

  return base64Encode(encrypted);
}

Затем этот же ключ шифруется твоим публичным ключом и также сохраняется в переменную. После этого эта переменная отправляется в базу данных, которая будет хранить зашифрованные ключи для диалога. Структура данных будет выглядеть примерно так:

{
  "<первые мои 4 цифры AID>": "мой зашифрованный ключ",
  "<первые твои 4 цифры AID>": "твой зашифрованный ключ"
}

Начало переписки.
#

После того как у нас есть зашифрованный публичным ключом симметричный ключ (тавтология, знаю :D ), мы можем его расшифровать, как было упомянуто ранее. Алгоритм расшифровки выглядит следующим образом:

Uint8List decryptSymmetricKey(
    String encryptedSymmetricKey, PrivateKey privateKey) {
  final symmetricKeyBytes = base64Decode(encryptedSymmetricKey);

  final decryptor = RSAEngine()
    ..init(false, PrivateKeyParameter<RSAPrivateKey>(privateKey as RSAPrivateKey));

  final decryptedSymmetricKey = _processInBlocks(decryptor, symmetricKeyBytes);

  return decryptedSymmetricKey;
}

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

Возвращаясь к одному посту выше:

Их нужно было хранить в зашифрованном виде, показывать в расшифрованном виде и отправлять в зашифрованном виде.

Я считаю, что успешно справился с этой задачей. Мой алгоритм не только не демонстрирует ключ, которым шифруется диалог, но и не хранит уязвимые данные, которые могут быть расшифрованы. Таким образом, безопасность переписки обеспечивается на каждом этапе.

Что в итоге?
#

Да, возможно, я не охватил всех тонкостей самого приложения, но здесь я постарался сосредоточиться на алгоритме шифрования ETP (Edge Transport Protocol). Этот проект привнес много нового и интересного в мою историю как разработчика. Возможно, в будущем я смогу доработать его и превратить в нечто более законченное и презентабельное. Пока же я оставлю его здесь как одну из моих любимых разработок, версию 1.0.0.

Бонус: миниатюра является иллюстрацией ETP v 0.0.0.

Иллюстрация ETP
Иллюстрация первого наброска ETP

upd 2.0: Если вы еще не использовали или не видели Cloudy в действии - я оставлю видео на github README

kmdshi/cloudy

Dart
3
0
Автор
Bogdan Lipatov
Middle Flutter Developer & Knowledge enjoyer