Однажды от моего начальника, (который, помимо того, что является владельцем компании, в которой я работаю еще и практикующий адвокат), поступило задание создать ресурс для анонимной переписки.
В своей работе для разных «серых» переговоров он использовал Telegram Павла Дурова, но смущало его, что в этом и подобных ресурсах предполагается регистрация. Пусть никто и не верифицирует пользователя, но в итоге переписка всегда сводится к тому, что общаются субъект с никнеймом Х и субъект в никнеймом Y и какими-то сохраненными при регистрации данными.
Для того, чтобы, в случае копирования переписки было проблематично ее соединять по календарным сессиям между одними и теми же пользователями, каждый раз необходимо заново проходить регистрацию и искать своего собеседника, либо светить имена пользователей в оффлайне.
ТЗ от моего шефа звучало примерно так: «Идем самым простым путем. Мне нужен чат. Каждый раз, заходя в него, я получаю номер анонимного чата и высылаю его вне Интернета своему собеседнику. Он заходит на соответствующий ресурс, вводит номер и присоединяется к моему чату. Переписка нигде не сохраняется и уничтожается, как только мы прекращаем общение. Номера каждый раз должны быть уникальными, чтобы, даже в случае копирования экранов невозможно было пришить переписку в качестве доказательств в уголовном деле.
Чат должен быть работоспособен в TOR браузере».
Концепция
Для начала были определены некоторые цели и требования:
Отказоустойчивость. Сделать все возможное, что бы чат работал даже при маленькой ширине канала, больших пингах и любых возможностях хостинга.
Нет регистрации. Все манипуляции, кроме прямого общения, должны быть делегированы скрипту: регистрация пользователя в системе, создание комнаты чата.
Шифрование, шифрование и еще раз шифрование. Все должно быть зашифровано. Абсолютно.
Простой интерфейс, требующий минимум от пользователя.
Отказоустойчивость
Если быть честным, то по первому пункту я попал на все три ситуации. Канал общий на всех сотрудников (dl: 4 mb/s, ul: 0.3mb/s) и раздается одним роутером, что дает маленький канал и большие пинги. А хостинг, предоставленный под это творение не сильно желал принимать много запросов в минуту.
От данных параметров я и начал проводить исследование в своих JS-скриптах. Выяснилась любопытная вещь: если делать два запроса, средствами AJAX, одновременно то сервер медленно умирает очень долго обрабатывает запросы и в итоге выдает «Aborted».
Решение пришло быстро и заключалось всего в нескольких строках:
А на тот момент, когда сервер просто выдавал ошибки в PHP вместо конкретных страниц, пришлось убить все выводимые ошибки.
error_reporting(0);
На этом я остановился, ибо больше ничего не надо, думаю.
Регистрация?
Не люблю решать за пользователей, но в этом контексте все было решено заранее:
$user_id = rand(1000000000, 9999999999);
А вот на счет номеров чата, надо было подумать. Формат я взял цифро-буквенный, ибо обычное число 8 * 10^9 брутфорсом можно и обойти, с сегодняшними мощностями ПК. Это было мало вероятно, но все же.
Процесс самой регистрации пользователя проходит в единственный этап. В БД делается две записи вида:
Последнее свойство первой записи — md5-hash псевдослучайной фразы. Служит подобием публичного идентификатора.
/index.php?chat=e02aeb13ad760933a5f88eb4589ebcbd
А для уверенности, что в чат просто так не попадут по данному URL, при заходе делается проверка по второй записи, отвечающую за связь пользователя с чатом.
А как же самое сладкое?
В качестве алгоритма шифрования я выбрал Anubis, т. к. его реализации доступны на PHP и JS. С последней реализацией, пришлось повозиться лично.
Ты ему даешь свой секрет, а он его даже не шифрует...
По сути, реализация на JS, была просто портирована из PHP, на что указывают несколько функций.
Для примера
function microtime (get_as_float) {
var now = new Date().getTime() / 1000;
var s = parseInt(now, 10);
return (get_as_float) ? now : (Math.round((now - s) * 1000) / 1000) + ' ' + s;
}
function ord(str) {
var ch = str.charCodeAt(0);
if (ch>0xFF) ch-=0x350;
return ch;
}
Проблема была в том, что какой бы ключ шифрования не вводи, все расшифровывается. Тоесть этот алгоритм, по своей сути, совсем не шифровал данные.
Путем долгих изысканий и вставки console.log(), я пришел к выводу, что функция String.fromCharCode() выдает значения больше 0xFF.
Подробнее
var crypt = function (block, roundKey) {
...
//map cipher state to byte array block (mu^{-1}):
for (i = 0, pos = 0; i < 4; i++) {
w = inter[i];
block[pos++] = String.fromCharCode(w >> 24);
block[pos++] = String.fromCharCode(w >> 16);
block[pos++] = String.fromCharCode(w >> 8);
block[pos++] = String.fromCharCode(w);
}
return block;
}
Данная часть кода в принципе ничего не делала с данными, т. к. block[pos++] работала логически не правильно. А так же String.fromCharCode() возвращала данные больше 0xFF, что приводило к неправильным вычислениям блоков.
Решением проблемы стало такое преобразование:
var crypt = function (block, roundKey) {
...
function fixedFromCharCode (codePt) {
if (codePt > 0x00FF || codePt < 0x0) {
codePt = codePt % 0x100;
if(codePt < 0x0) {
codePt = codePt - 0xFF00;
}
return String.fromCharCode(codePt);
} else {
return String.fromCharCode(codePt);
}
}
block = "";
//map cipher state to byte array block (mu^{-1}):
for (i = 0, pos = 0; i < 4; i++) {
w = inter[i];
block += fixedFromCharCode(w >> 24);
block += fixedFromCharCode(w >> 16);
block += fixedFromCharCode(w >> 8);
block += fixedFromCharCode(w);
}
return block;
}
Еще одна проблема заключалась в функции hash_hmac, которая использует библиотеку CryptoJS.HmacSHA256. Данная функция выдавала данные в шестнадцатеричном формате, когда было необходимо в двоичном.
Подробнее
Была простая, незаметная функция:
function hash_hmac(algo, data, key) {
var hash = CryptoJS.HmacSHA256(data, key);
return hash.toString(CryptoJS.enc.Hex);
}
И вот, что бы все это дело работало правильно, я привел ее к такому виду:
function hash_hmac(algo, data, key) {
var hash = CryptoJS.HmacSHA256(data, key);
hash = hash.toString(CryptoJS.enc.Hex);
for(var i = 0, output = ''; i < hash.length; i+=2) {
output += String.fromCharCode(parseInt(hash.substr(i, 2), 16));
}
return output;
}
А последняя проблема была чисто наша, чисто русская — кирилические и др. символы. Все решилось простым преобразованием в Base64.
Шифрование реализовано таки образом:
У каждого пользователя есть свой открытый ключ, который генерируется при регистрации. Все данные, которыми обменивается браузер и сервер, зашифрованы и расшифровываются только на стороне клиента, то есть с помощью JS. А вот когда серверные скрипты принимают данные и записывают их в БД, они шифруются уже собственным закрытым ключом.
В конце концов о проекте
Это простое решения для анонимной переписки, которое не требует от пользователя ничего. Даже придумавыть имена, ники и т.д. Даже если сервер положить на лопатки и угнать базу сообщений, мало вероятно, что она кому-нибудь чем-нибудь поможет, ведь привязать случайный 10-и значный номер к реальному человеку не удастся. SHSHCHAT
Проект будет развиваться, совершенствоваться, поэтому конструктивную критику я выслушаю в Ваших комментариях.