В этой статье, я, основываясь на материалах с ресурса ХабраХабр, покажу как превратить обычный текст в серию символов, которые не видны невооруженному человеческому глазу.
Не знаю, к какому классу методов сокрытия данных отнести такой способ, но я предполагаю, что это стеганография. Буду отталкиваться от такого представления и покажу свой вариант, который не потребует каких-либо сторонних зависимостей, а лишь некоторые функции из стандартной библиотеки.
Как же все-таки сделать текст невидимым?
Идея проста: в Unicode есть несколько символов, которые не видны при обычном отображении, и именно этот факт будем использовать. Возьмем восемь невидимых символов и запишем их в массив, а потом будем считать номер каждого из них особым «идентификационным кодом».
Отметим, что индекс находится в диапазоне от 0 до 7, что очень напоминает цифры восьмеричной системы. А это обстоятельство позволяет перевести обычную последовательность печатных символов в серию символов из уже подготовленного массива: для этого достаточно преобразовать последовательность в беззнаковое байтовое представление и каждый байт представить как некое число в восьмеричной системе, после чего использовать цифры каждого полученного числа как индексы для массива невидимых символов.
Общий принцип, я думаю, понятен, однако, для упрощения работы можно вместо массива использовать ассоциативный массив и не из восьми невидимых символов, а специально предрасчитанный массив в котором роль ключей играют байты (т.е. значения от 0 до 255), а роль значений — соответствующие байтам последовательности невидимых символов. Подобный трюк может облегчить задачу не только кодирования, но и декодирования: достаточно поменять местами ключи и значения — и массив для декодирования уже готов.
Но вернемся к кодированию.
После того как ассоциативный массив с расчитанными последовательностями готов, а строка текста под преобразования уже переведена в массив байтов, необходимо каждому байту найти соответствие из ассоциативного массива и полученные значения объединить в единую строку. Таким образом, сокрытие текста осуществлено.
Однако, мне не так повезло как тому парню с Хабрахабр, который писал реализацию на Scala, и у меня не было готовых функций для перевода байта в восьмеричное число. При этом, мне требовался не просто перевод в восьмеричное число, а представление числа в таком виде, в котором не требуется выделение из него каждой отдельной цифры. И именно поэтому мне нужна была своя версия трансформации байта в последовательность невидимых символов через восьмеричное представление, которую также пришлось писать самому:
enum INVISIBLE_CHARS = ["\u2060", "\u200B", "\u2061", "\u2062", "\u2063", "\uFEFF", "\u200C", "\u200D"]; // конвертация в невидимые символы string hideSymbol(ubyte number) { string accumulator; accumulator ~= (number & 0xc0) >> 6; accumulator ~= (number & 0x38) >> 3; accumulator ~= (number & 0x07); return accumulator .map!(a => INVISIBLE_CHARS[a]) .join .to!string; }
В этой процедуре используется моя любимая работа с битами и битовые операции: с помощью битовых операций мы выделяем отдельные цифры восьмеричного числа, используя известное соотношение между двоичной и восьмеричной системой, которое тройкам бит ставит в соответствие одну восьмеричную цифру. Данная восьмеричная цифра в дальнейшем используется в качестве индекса для массива невидимых символов (не ассоциативного), после чего индексы переводится в невидимые символы, которые затем склеиваются в одну строку.
После описания такой элементарной трансформации я свел воедино процессы кодирования и декодирования в единый класс, в котором также происходит вычисление необходимых массивов и перестановка ключей/значений местами (можете использовать эту простую функцию и в иных проектах):
import std.algorithm; import std.conv : to; import std.range; import std.stdio; import std.string; class TextHider { private { enum INVISIBLE_CHARS = ["\u2060", "\u200B", "\u2061", "\u2062", "\u2063", "\uFEFF", "\u200C", "\u200D"]; string[ubyte] symbols; ubyte[string] bytes; // конвертация в невидимые символы string hideSymbol(ubyte number) { string accumulator; accumulator ~= (number & 0xc0) >> 6; accumulator ~= (number & 0x38) >> 3; accumulator ~= (number & 0x07); return accumulator .map!(a => INVISIBLE_CHARS[a]) .join .to!string; } auto createSymbolsTable() { string[ubyte] table; foreach (ubyte e; 0..256) { table[e] = hideSymbol(e); } return table; } } this() { symbols = createSymbolsTable; bytes = symbols.swapKeyWithValues; } string encode(string text) { return text .representation .map!(a => symbols[a]) .join .to!string; } string decode(string text) { return text .chunks(3) .map!(a => bytes[a.to!string]) .array .assumeUTF; } } void main() { auto h = "Скрытый текст приветствия"; auto v = h.hideText; writeln(v); writeln(v.showText); auto tp = new TextHider; auto w = tp.encode("мяу"); w.writeln; tp.decode(w).writeln; }
Небольшой комментарий: обратная процедура (т.е. декодирование) делает все с точностью наоборот, для чего из кодированных символов собирается их байтовое представление, которое превращается в строку с помощью процедуры assumeUTF из std.string. Эта функция по своей сути является обратной для функции representation из того же самого модуля (спасибо за эту подсказку-напоминание группе dlang в Telegram).
Теперь и вы можете спрятать текст от посторонних глаз.
P.S: В текст этой статьи мы тоже поместили невидимые символы. Может быть вы их найдете?