Как криптографическое приложение писал, от школьного проекта до сегодня
Привет, Пикабу!
Ощущаете отсутствие конфиденциальности в мессенджере? Мое приложение не обещает анонимность в полном объеме, оно другого рода. Я создавал его изначально как инструмент шифрования переписки в мессенджере. Идея была проста: зашифровать текст перед отправкой в мессенджер, чтобы никто сторонний (например при утечке базы данных) не смог его прочитать.
Началось все в школе, всем тогда задали сделать проектную работу на любую тему, а мне тогда в ленте попался шортс о криптографии, вот я и загорелся идеей. Я тогда только начинал программировать и первый мой язык был Паскаль, на нем и решил писать.
Я презентовал приложение сначала на проектной работе, а затем и на конференции "Шаг в будущее" на которую меня директор поставил. Результат был ошеломительным, но я бы не сказал что те люди что то понимали в криптографии, так что впечатлить их было не сложно :).
Спустя время я оставил Паскаль и выучил C# и .NET Maui. Позже я вспомнил о приложении и решил - я сделаю полностью бесплатный ремейк без рекламы и с открытым исходным кодом!
Название я выбрал сомнительное - LOKI-Cryptographer, стоило подумать о нем лучше, ну да ладно. В приложении накидал простой интерфейс вдохновляясь другим приложением. Кстати - вот тут то была моя головная боль - вид кнопок, я тогда был неопытен и использовал ImageButton из за чего они выглядели огромными и размытыми. Позже до меня дошло использовать ttf кнопки.
К сожалению скриншота приложения с ImageButton я не нашел, зато есть ранний концепт интерфейса уже с ttf кнопками
Был забавный баг с полями ввода и вывода - я поставил им фиксированный размер по ширине и забыл, а когда запустил приложение на телефоне поменьше, обнаружил что поля вытолкнули кнопки за экран.
Под капотом
Приложение кстати с открытым исходным кодом.
Вот метод шифрования на момент выкладывания статьи:
[RelayCommand]
private async Task CryptMessege()
{
try
{
if ( UserTextIn == null || UserTextIn.Length == 0) return;
byte[] MessegeToByte = Encoding.UTF8.GetBytes(UserTextIn);
byte[] CryptMessege = new byte[MessegeToByte.Length];
byte[] Nonce = new byte[12];//рандомное число
SecureRandom random = new SecureRandom();//рандомное число для кривой
byte[] RandomForBytes = new byte[32];
// Ассиметрия
random.NextBytes(RandomForBytes);
X25519PrivateKeyParameters MyPrivateKey = new X25519PrivateKeyParameters(RandomForBytes);// кривая X25519
X25519PublicKeyParameters MyPublicKey = MyPrivateKey.GeneratePublicKey();
string? a1 = await SecureStorage.Default.GetAsync("MyNowPrivateKey");// Приватный ключ
if (string.IsNullOrEmpty(a1))
{
await SecureStorage.Default.SetAsync("MyNowPrivateKey", Convert.ToBase64String(RandomForBytes));
}
else
{
SecureStorage.Default.Remove("MyNowPrivateKey");
await SecureStorage.Default.SetAsync("MyNowPrivateKey", Convert.ToBase64String(RandomForBytes));
}
if (string.IsNullOrEmpty(EphemeralKeyUser)) //если ключ пользователя null
{
byte[] MyEphemeralBytes = MyPublicKey.GetEncoded(); //промежуточный ключ пользователя
EphemeralKeyUser = Convert.ToBase64String(MyEphemeralBytes);
// сохранение своего первого ключа
string a = Preferences.Default.Get("MyEphemeralKey", string.Empty);
if (!string.IsNullOrEmpty(a))
{
Preferences.Default.Clear("MyEphemeralKey");
}
Preferences.Default.Set("MyEphemeralKey", Convert.ToBase64String(MyEphemeralBytes));
}
byte[] SomeoneEphemeralBytes = new byte[32]; // массив под чужой помежуточный ключ
if (!string.IsNullOrEmpty(SomeoneEphemeralKey))
{
SomeoneEphemeralBytes = Convert.FromBase64String(SomeoneEphemeralKey);
}
else
{// если не ввел ключ собеседника
return;
}
if (SomeoneEphemeralBytes == null || SomeoneEphemeralBytes.Length == 0) return; //подстраховка на промежуточный ключ собеседника
X25519PublicKeyParameters SomeoneRestoredEphemeral = new X25519PublicKeyParameters(SomeoneEphemeralBytes, 0);
byte[] GeneralKey = new byte[32]; // общий ключ
MyPrivateKey.GenerateSecret(SomeoneRestoredEphemeral, GeneralKey, 0);
//создание второго промеж ключа для следующего сообщения
SecureRandom random2 = new SecureRandom();
byte[] RandomForBytes2 = new byte[32];
random2.NextBytes(RandomForBytes2);
X25519PrivateKeyParameters MyPrivateKey2 = new X25519PrivateKeyParameters(RandomForBytes2);
X25519PublicKeyParameters MyPublicKey2 = MyPrivateKey.GeneratePublicKey();
byte[] MyEphemeralBytes2 = MyPublicKey2.GetEncoded();
EphemeralKeyUser = Convert.ToBase64String(MyEphemeralBytes2);
// Симметрия
try
{
RandomNumberGenerator.Fill(Nonce);
byte[] Gamma = HKDF.DeriveKey(HashAlgorithmName.SHA256, GeneralKey, MessegeToByte.Length, Nonce);
try
{
for (int i = 0; i < MessegeToByte.Length; i++)
{
CryptMessege[i] = (byte)(MessegeToByte[i] ^ Gamma[i]);
}
}
finally
{
CryptographicOperations.ZeroMemory(Gamma);
}
}
finally
{
CryptographicOperations.ZeroMemory(GeneralKey);
}
// Вывод инфы на интерфейс
try
{
string CryptMessegeBase64 = Convert.ToBase64String(CryptMessege);
string NonceBase64 = Convert.ToBase64String(Nonce);
string MyEphemeralBytes2Base64 = Convert.ToBase64String(MyEphemeralBytes2);
string Result = $"{CryptMessegeBase64}✶{MyEphemeralBytes2Base64}{NonceBase64}";
UserTextOut = Result;
}
finally
{
CryptographicOperations.ZeroMemory(MessegeToByte); // очистка остального
}
} catch (Exception) {}
}
Жаль что на Пикабу нет оформления под код. Если кратко о асимметрии:
Генерация общего ключа реализована на базе эллиптической кривой X25519.
Каждый раз генерируется новый промежуточный ключ для следующего сообщения.
Оперативная память зачищается от остатков ключей и гаммы сразу после использования.
О симметрии:
В коде используется HKDF (SHA256) для создания уникальной гаммы на основе общего ключа и одноразового числа (Nonce), а затем применяется XOR.
Открытый исходный код можете посмотреть на моей странице в GitHub
Я понимал что одних слов недостаточно чтобы добиться доверия, поэтому выбрал единственное верное решение, проект - Open Source, открытый исходный код здесь GitHub. Только лицензию прочитайте сначала.
О релизе и сторах
Поначалу мне в голову пришла идея залить на Rustore, собственно я так и сделал. Только я немного поспешил и забыл в файле конфигурации .csproj поменять идентификатор приложения:
<ApplicationId>com.companyname.loki</ApplicationId>
Ну и залил, из за этого я уже не могу его поменять. Больше проблем с Рустором не возникло. Там кстати новая версия 2.1.3 прошла модерацию. Я вначале пути думал встроить рекламу в приложение, но быстро отказался от этой идеи. Думал еще куда-нибудь выложить, но что то руки не дошли.
Вывод
Я рад что начал писать этот проект, мне нравится то, куда он меня привел, я очень многому научился и многое узнал работая над ним. Я пишу его один с декабря 2025 года.
Как уже говорил приложение полностью бесплатно, рекламы нет, работает локально, ваши данные мне даром не нужны. У меня много идей для проекта, но я не против того, чтобы вы написали свои идеи.
Я наверняка где-то накосячил, все таки я не робот, если что найдете - воспользуйтесь внутренней возможностью отправить мне сообщение об ошибке, я исправлю.
Кто хочет просмотреть фулл код сам GitHub (Там же можно скачать андройд и пк версию)
Страница в Rustore - Rustore (только андройд версия)
Мой тгк с техническими новостями - Telegram (Там дублировать пост не буду, только технические апдейты по коду)

















