Простое решение для анонимной переписки. От идеи к воплощению

в 10:42, , рубрики: ajax, php, анонимность в интернете, общение, метки: ,

Здравствуй, %USERNAME%!

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

В своей работе для разных «серых» переговоров он использовал Telegram Павла Дурова, но смущало его, что в этом и подобных ресурсах предполагается регистрация. Пусть никто и не верифицирует пользователя, но в итоге переписка всегда сводится к тому, что общаются субъект с никнеймом Х и субъект в никнеймом Y и какими-то сохраненными при регистрации данными.

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

ТЗ от моего шефа звучало примерно так: «Идем самым простым путем. Мне нужен чат. Каждый раз, заходя в него, я получаю номер анонимного чата и высылаю его вне Интернета своему собеседнику. Он заходит на соответствующий ресурс, вводит номер и присоединяется к моему чату. Переписка нигде не сохраняется и уничтожается, как только мы прекращаем общение. Номера каждый раз должны быть уникальными, чтобы, даже в случае копирования экранов невозможно было пришить переписку в качестве доказательств в уголовном деле.
Чат должен быть работоспособен в TOR браузере».

Простое решение для анонимной переписки. От идеи к воплощению

Концепция

Для начала были определены некоторые цели и требования:

  1. Отказоустойчивость. Сделать все возможное, что бы чат работал даже при маленькой ширине канала, больших пингах и любых возможностях хостинга.
  2. Нет регистрации. Все манипуляции, кроме прямого общения, должны быть делегированы скрипту: регистрация пользователя в системе, создание комнаты чата.
  3. Шифрование, шифрование и еще раз шифрование. Все должно быть зашифровано. Абсолютно.
  4. Простой интерфейс, требующий минимум от пользователя.

Отказоустойчивость

Если быть честным, то по первому пункту я попал на все три ситуации. Канал общий на всех сотрудников (dl: 4 mb/s, ul: 0.3mb/s) и раздается одним роутером, что дает маленький канал и большие пинги. А хостинг, предоставленный под это творение не сильно желал принимать много запросов в минуту.

От данных параметров я и начал проводить исследование в своих JS-скриптах. Выяснилась любопытная вещь: если делать два запроса, средствами AJAX, одновременно то сервер медленно умирает очень долго обрабатывает запросы и в итоге выдает «Aborted».
Решение пришло быстро и заключалось всего в нескольких строках:

if(chat_function.load.status_load) {
	chat_function.load.status_load = false;
	$.ajax({
		type: 'POST',
		url: 'hello/world.php',
		data: 'chat=000000000',
		timeout: 5000,
		error: function() {
			chat_function.load.status_load = true;
		},
		success: function (data) {
			...
			chat_function.load.status_load = true;
		}
	});
}

chat_vars.intervals.load_messages = setInterval(chat_function.load_message, rand(1000, 2000));

А на тот момент, когда сервер просто выдавал ошибки в PHP вместо конкретных страниц, пришлось убить все выводимые ошибки.

error_reporting(0);

На этом я остановился, ибо больше ничего не надо, думаю.

Регистрация?

Не люблю решать за пользователей, но в этом контексте все было решено заранее:

$user_id = rand(1000000000, 9999999999);

А вот на счет номеров чата, надо было подумать. Формат я взял цифро-буквенный, ибо обычное число 8 * 10^9 брутфорсом можно и обойти, с сегодняшними мощностями ПК. Это было мало вероятно, но все же.

Процесс самой регистрации пользователя проходит в единственный этап. В БД делается две записи вида:

chat_id		| chat_time_start	| ch_code
"28BF195420"	| "1409297213"		| "e02aeb13ad760933a5f88eb4589ebcbd"

id	| user_id	| chat_id	| user_time_registered	| user_time_last_seen
"1"	| "1163272808"	| "28BF195420"	| "1409297213"		| "1409297213"

Последнее свойство первой записи — 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.

Подробнее

encrypt: function(data, hex, b64) {
	if(b64 != undefined && b64)  var blocks = dataPrepare(Base64.encode(data));

	...
},

decrypt : function (data, hex, b64) {
	...

	if(b64 != undefined && b64) return Base64.decode(decrypted);
		
	return decrypted;
}

А теперь к скуке

Шифрование реализовано таки образом:
У каждого пользователя есть свой открытый ключ, который генерируется при регистрации. Все данные, которыми обменивается браузер и сервер, зашифрованы и расшифровываются только на стороне клиента, то есть с помощью JS. А вот когда серверные скрипты принимают данные и записывают их в БД, они шифруются уже собственным закрытым ключом.

В конце концов о проекте

Это простое решения для анонимной переписки, которое не требует от пользователя ничего. Даже придумавыть имена, ники и т.д. Даже если сервер положить на лопатки и угнать базу сообщений, мало вероятно, что она кому-нибудь чем-нибудь поможет, ведь привязать случайный 10-и значный номер к реальному человеку не удастся.
SHSHCHAT

Проект будет развиваться, совершенствоваться, поэтому конструктивную критику я выслушаю в Ваших комментариях.

Автор: GRIDark

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js