Введение


На сегодняшний день защита информации является одной из приоритетных задач IT-индустрии, особенно учитывая то, что для прослушивания трафика в наше время практически не нужно иметь специализированных знаний – в Интернете достаточно и программного обеспечения, и подробных руководств.

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

Выбор инструментов


Для написания серверной части приложения мною был выбран язык программирования Javа в сочетании с фреймворком для веб-разработки Wicket. Это не единственный инструментарий, на котором можно было реализовать поставленные задачи, но данного стека вполне достаточно для их решения, а Wicket помимо всего прочего предоставляет весьма удобный интерфейс для работы с компонентами веб-страниц. Фронтенд не подразумевал наличия в себе его-то сложного – я не ставил перед собой задачи разработать красивый и дружелюбный интерфейс, в большем приоритете была логика приложения, поэтому я ограничился классической связкой HTML/CSS/JS.

Сервер запущен на Ubunty 18.04, в Docker контейнере — но о конфигурировании сервера речь подробнее пойдет чуть ниже – с помощь контейнера сервлетов Apache Tomcat. Остальные технологии, необходимые для построения подобного приложения если понадобятся, будут упомянуты по ходу статьи.

Конфигурирование сервера


Как уже упоминалось, приложение расположено на linux-сервере, в частности, на Ubunty 18.04. Для того, что бы изолировать систему от остальных приложений, а так же упростить развертывание, было принято решение контейнеризировать процесс – с помощь Doсker выделить приложению некоторое количество CPU и памяти, список открытых портов, а все остальное – закрыть. Я не буду подробно описывать установку и настройку сервиса Docker, однако ключевые моменты упомянуть должен.

Предполагается, что все команды выполняются с правами администратора.

Для начала потребовалось установить и запустить соответствующую службу, командами

# apt install docker-сe
# systemctl start docker
# systemctl enable docker

После этого скачаем и запустим образ операционной системы – больше от контейнера нам на данный момент ничего не надо:

#docker search ubuntu
#docker pull ubuntu
#docker run -it ubuntu

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

Настройка контейнера сервлетов Tomcat


Итак, на данный момент мы предполагаем, что работаем на чистой операционной системе, где все по умолчанию. Для того, что бы иметь возможность хостить на ней Java приложение, на нее нужно установить непосредственно jdk и jvm, и задать переменные окружения с путями установки. Соответственно, тут я приведу лишь ключевые команды, более подробно процесс установки Java на Linux можно рассмотреть в сети.

Для установки JDK и JRE, а затем – настойка переменной окружения JAVA_HOME требуется ввести соответственно вот эти команды:

#apt install openjdk-8-jdk
#apt install openjdk-8-jre
#export JAVA_HOME “путь_к_дикектории_установки_JAVA”

Дальше можно перейти непосредственно к установке Tomcat.

Ключевое, на что стоит обратить внимание – на выдачу прав менеджера и администратора – по умолчанию нельзя администрировать приложения удаленно, не на локальной машине. В остальном же все стандартно – загрузка и установка пакета и запуск службы.

Написание программы


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

Однако обо всем по порядку.

Начнем с клиентской части.

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

image
Рис. 1 – Интерфейс хранилища файлов

При вводе имени файла в строку ввода файл открывается в другом окне – в редакторе. Java-код страницы, отображающей список файлов:

File directory = new File(wayToDirs);
String files = new String();
directory.mkdirs();
for (File i : directory.listFiles())
    files+=i.getName()+"\n";
TextArea listOfFiles = new TextArea("files", Model.of(""));
listOfFiles.setEnabled(false);
listOfFiles.setDefaultModelObject(files);
add(listOfFiles);

Соответствующий ему HTML-код еще более тривиален:

<textarea wicket:id = "files"  class="files" id = "files" />
<form wicket:id="selector" >
    <textarea wicket:id="name" class="input"/>
    <button wicket:id="opener" class="input" style="width: 10%">Открыть</button>
</form>

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

image
Рис. 2 – интерфейс редактора

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

TextField wayToSaveFile = new TextField("wayToSaveFile", Model.of(""));
Button saveButton = new Button("save")
{
   @Override
   public void onSubmit() {
         super.onSubmit();
         File file = new File(DirectoryInterface.wayToDirs+"/" + wayToSaveFile.getInput());
         try
         {
            FileWriter fw = new FileWriter(file);
            fw.write(textArea.getDefaultModelObject().toString());
            fw.close();
         }
         catch (Exception e) {
            System.out.println(e.getMessage());
            debud.setDefaultModelObject(e.getMessage());
         }

      }
};

Но на момент сохранения –файл уже зашифрован. Для этого в клиентской части реализованы кнопки «Зашифровать» и «Дешифровать», служащие для шифрации и дешифрации. Шифрация происходит по алгоритму Виженера, о нем я чуть больше скажу отдельно, но в функциях он показан.

Соответсвенно, клиентский код выглядит так:

<script>

   function encryptFun(){
      let outputString = "";
      let inputString = document.getElementById("text").value;
      let password = document.getElementById("passwordTextField").value;
      for (let i = 0; i<inputString.toString().length-1; i++)
         outputString+=(String.fromCharCode((inputString.toString().charCodeAt(i)+256-password.toString().charCodeAt(i%password.toString().length))%256));
      document.getElementById("text").value = outputString;
      return outputString;
   };
   function decryptFun(){
   let outputString = "";
   let inputString = document.getElementById("text").value;
   let password = document.getElementById("passwordTextField").value;
   for (let i = 0; i<inputString.toString().length-1; i++)
      outputString+=(String.fromCharCode((inputString.toString().charCodeAt(i)+256+password.toString().charCodeAt(i%password.toString().length))%256));
   document.getElementById("text").value = outputString;
   return outputString;
   };
</script>
…		<!—Часть кода опущена-?
<input wicket:id="wayToSaveFile"class="input">
<button wicket:id = "save" class="input">Save</button>
<input id = "passwordTextField" class="input">
<button id = "encrypt" onclick="encryptFun()" class="input">Зашифровать</button>
<button id = "decrypt" onclick="decryptFun()" class="input">Расшифровать</button>

Таким образом, после нажатия на кнопку шифрации, текст в поле ввода подменяется на текст, возвращенный функцией шифрования – и наоборот.

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

Алгоритм шифрации


Мною был выбран алгоритм шифрования Виженера – являющийся вариацией шифра Цезаря с переменным сдвигом по ключевому слову. На вход алгоритму подается исходный текст и ключ произвольной длинны. Тогда i-ый член исходного текста сдвигается на порядковый номер в алфавите j-ого символа пароля, где j = i%(длину пароля). Алгоритм не является самым оптимальным или устойчивым к анализу, однако при достаточной длине пароля обеспечивает некоторую надежность.

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

  1. Поиск длины ключа. Можно анализировать распределение частот в зашифрованном тексте с различным прореживанием. То есть брать текст, включающий каждую 2-ю букву зашифрованного текста, потом каждую 3-ю и т. д. Как только распределение частот букв будет сильно отличаться от равномерного (например, по энтропии), то можно говорить о найденной длине ключа.
  2. Криптоанализ. Совокупность l шифров Цезаря (где l — найденная длина ключа), которые по отдельности легко взламываются.

Тесты Фридмана и Касиски могут помочь определить длину ключа.

Более подробное обсуждение взлома Виженера опять-таки выходит за рамки данной статьи.

Выводы


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

Некоторые решения, озвученные выше являются спорными или не оптимальными, но базовая логика и фундаментальные вещи обозначены.
github.com/Toxa-p07a1330/encriptedStorage