Техническое задание

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

Основные задачи:

Реализация шифра Цезаря:

  • Создание и использование алфавита.
  • Алгоритм сдвига символов в соответствии с заданным ключом.

Обработка файлов:

  • Функциональность для работы с файлами (чтение, запись).
  • Обработка больших текстовых файлов.

Валидация входных данных:

  • Проверка существования файлов.
  • Допустимость ключей.

Режимы работы:

Шифрование текста:

  • Функция шифрования, принимающая файл, ключ и записывающая зашифрованный текст в новый файл.

Расшифровка текста:

  • Функция расшифровки с использованием известного ключа.

Расшифровка методом brute force (опционально):

  • Реализация метода brute force для перебора всех ключей до успешного расшифрования.

Расшифровка методом статистического анализа (опционально):

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

Разработка интерфейса пользователя:

  • Текстовое меню или (опционально) графический интерфейс

Дополнительные задачи:

  • Обработка ошибок и исключений.
  • Оптимизация для производительности.
  • Документация и тестирование.

Криптология, криптография и криптоанализ

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

Задача: написать программу, которая работает с шифром Цезаря.

Это один из самых простых и известных методов шифрования. Назвали его, само собой, в честь императора Гая Юлия Цезаря, который применял его для секретной переписки с генералами.

Шифр Цезаря — это шифр подстановки: в нем каждый символ в открытом тексте заменяется на символ, который находится на некотором постоянном числе позиций левее или правее него в алфавите.

Допустим, мы устанавливаем сдвиг на 3 (ключ = 3). В таком случае А заменится на Г, Б станет Д, и так далее.

Давай для наглядности рассмотрим игрушку для шифрования кодом Цезаря:

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

Для шифрования мы буквы из внешнего круга заменяем на стоящие рядом с ними буквы из нижнего круга. Например, если круги смещены как на картинке, А заменяем на С, а R — на T. А чтобы расшифровать все делаем наоборот: буквы из внутреннего круга заменяем на буквы из внешнего; то есть, С обратно на А, и Т обратно на R.

Допустим, песик Бобик решил написать девочке Алисе секретное сообщение. У каждого из них есть такая игрушка. Чтобы успешно зашифровать и расшифровать сообщение, они должны договориться на сколько позиций надо сместить круги своих игрушек относительно друг-друга. На картинке игрушки видно что А стала С, а В стала D, значит внутренний круг смещен на 2 позиции. Заметь, Y стала А (и не улетела в космос). Когда они договорились сместить круги на 2 позиции и Бобик передал Алисе надпись NGVU RNCA NGIQ, Алиса сразу все поняла и пошла доставать коробку с Lego.

Так как в английском алфавите только 26 букв, можно сместить круги на 1,2,...,25 позиций. Если мы сместим на 26 (или 0) позиций то буквы на окружностях совпадут. Можно почитать лекцию CS50 Криптография. Шифр Цезаря и шифр Виженера

Криптоанализ — взлом шифра

Криптография — алгоритмы шифрования данных (и не только)

Алфавит — полный набор символов, которые могут попасться в тексте, поддающемся шифровке. В шифре Цезаря важен порядок этих символов. Он может и не быть классическим порядком букв в алфавите, но его должны знать и Бобик (тот кто шифрует) и Алиса (тот кто расшифрует).

Криптоанализ статистических данных основан на том, что в каждом языке есть своя статистика использования символов. Например, в этом тексте (пока я его пишу) уже есть 14 слов “и” и 24 слова “в”. Если сохранять пробелы, то легко догадаться, что наиболее частые слова из одной буквы это скорее всего В или К или И. И если использовали шифр Цезаря с классическим порядком букв в алфавите, расшифровать этот текст будет элементарно (попробуем сдвиг для В — нечитабельно — тогда попробуем сдвиг для К, и так далее).

Хорошо, можно съедать пробелы, чтобы спрятать слова из 1 буквы. Но тогда можно легко определить гласные (они встречаются намного чаще согласных), или часто встречающиеся слова вроде ИЛИ. Даже простой перебор не составит трудностей, так как всего существует очень маленькое число вариантов для подбора (для английского алфавита 25). Поэтому Бобику и Алисе следует попробовать более сложные метод шифрования.

Отходить от буквального задания — можно. Главное — суть.

Это минимумтеоретических данных, которые понадобятся тебе для выполнения итогового проекта. Переходим к описанию задания!

С чего начать: архитектура программы

Прежде, чем начинать писать код, нужно разбить задачу на подзадачи и подумать над общей архитектурой проекта. В принципе, это твой первый большой проект, поэтому теоретически можно все запихнуть в один большой класс и в нем сделать несколько методов, и все их вызывать в главном методе main.

Назовем наш класс CaesarCipher.

В нем мы определяем алфавит, в котором мы будем работать, а также методы для шифрования, расшифровки, а в методе main — меню вызова.

public class CaesarCipher {

    // Алфавит
    private static final String ALPHABET = "тут будет наш алфавит";

    // Методы для шифрования, расшифровки, brute force, статистического анализа

    public void encrypt(String inputFile, String
outputFile, int key) {
        // Реализация шифрования
    }

    public void decrypt(String inputFile, String
outputFile, int key) {
        // Реализация расшифровки
    }

    public void bruteForce(String inputFile, String
outputFile, String optionalSampleFile) {
                // Реализация brute force
    }

    public void statisticalAnalysis(String inputFile,
String outputFile, String optionalSampleFile) {
        // Реализация статистического анализа
    }

    // Вспомогательные методы: validateInput(),
createAlphabet(), shiftCharacter(), readFile(), writeFile()

public static void main(String[] args) {
        CaesarCipher cipher = new CaesarCipher();
        // Логика меню
        // 1. Шифрование
        // 2. Расшифровка с ключом
        // 3. Brute force
        // 4. Статистический анализ
        // 0. Выход

        // Пример вызова метода шифрования:
        // cipher.encrypt("input.txt", "output.txt", 3);
            }
}
}

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

Например:

1. MainApp: Это главный класс, откуда начинается выполнение программы. Отвечает за обработку команд пользователя, вызов соответствующих методов и управление потоком работы программы.

public class MainApp {
    public static void main(String[] args) {
        // Логика для выбора режима работы, вызов
соответствующих методов
    }
}

2. Cipher: Класс, реализующий функциональность шифра Цезаря и дешифровки

public class Cipher {
    private char[] alphabet;
    public Cipher(char[] alphabet) {
        this.alphabet = alphabet;
    }
    public String encrypt(String text, int shift) {
        // Логика шифрования
    }
    public String decrypt(String encryptedText, int shift) {
        // Логика расшифровки
    }
}

3. FileManager: Отвечает за чтение и запись файлов.

public class FileManager {
    public String readFile(String filePath) {
        // Логика чтения файла
    }

    public void writeFile(String content, String filePath)
{
        // Логика записи файла
    }
}

4. Validator: Валидация входных данных, таких как существование файла, допустимость ключа.

public class Validator {
    public boolean isValidKey(int key, char[] alphabet) {
        // Проверка ключа
    }

    public boolean isFileExists(String filePath) {
        // Проверка существования файла
    }
}

5. BruteForce: Реализация метода перебора всех ключей для взлома.

public class BruteForce {
    public String decryptByBruteForce(String encryptedText,
char[] alphabet) {
        // Логика brute force
    }
}

6. StatisticalAnalyzer: Для статистического анализа при расшифровке.

public class StatisticalAnalyzer {
    public int findMostLikelyShift(String encryptedText,
char[] alphabet, String representativeText) {
        // Логика статистического анализа для определения
сдвига
    }
}

Общий подход к использованию классов:

В MainApp пользователь выбирает режим работы, например, через командную строку или простой GUI.

Затем, используя экземпляры классов FileManager, Cipher, Validator, BruteForce, и StatisticalAnalyzer, выполняются соответствующие операции, такие как чтение файла, шифрование/расшифровка, валидация и анализ.

Результаты записываются обратно в файл или выводятся пользователю.

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

Результат

Результат: программа работает в нескольких режимах

Режимы:

  • Шифровка текста
  • Расшифровка текста с помощью ключа
  • Расшифровка текста с помощью brute force (перебор всех вариантов)
  • (дополнительно) Расшифровка с помощью статистического анализа текста

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

Программа должна выполнять следующие функции:

  • Шифровка текста из заданного файла. На вход она получает адрес файла с оригинальным текстом, адрес файла в который нужно записать зашифрованный текст, и сдвиг по алфавиту (это является ключом шифра Цезаря). Не забудь проделать проверку того что а) файл оригинала по заданному адресу существует, и б) ключ от 0 и до (размер алфавита - 1) (или можете взять остаток от деления на размер алфавита).
  • Расшифровка при известном ключе. На входе — адрес зашифрованного файла и адрес куда писать расшифрованный файл, а также сдвиг по алфавиту который использовался при шифровании (ключ).
  • Расшифровка методом brute force (перебором всех возможных сдвигов); На входе - адрес зашифрованного файла, (опционально) адрес файла с текстом который является примером текста что был зашифрован (например другой труд того же автора) и адрес файла который должен содержать расшифрованный текст.
  • Расшифровка методом статистического анализа; На входе тоже самое что и для расшифровке перебором.

Не забудь проделать валидацию входных данных.

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

Подсказки

Алфавит

Создай алфавит, в котором существует задача. По условию это русский алфавит и пунктуация . , ” ’ : - ! ? ПРОБЕЛ Не забываем про пробел! Как это можно сделать? Например, можно загнать алфавит в String, несколько строк или массив String.

Или… можно создать алфавит на основе множества Set, массива или списка List. А ещё можно воспользоваться таблицей ASCII.

Помни, что алфавит не меняется, поэтому такую переменную логично сделать константой public static final. Имя таких переменных принято писать заглавными буквами.

private static final List ALPHABET = Arrays.asList('а',
'б', 'в',
'г', 'д', 'е', 'ж', 'з', 'и', 'к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т', 'у',
'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'я', '.', ',', '«', '»',
':', '!', '?', ' ');

А еще лучше применить обычный массив (так как алфавит не меняется, нет никакого смысла засовывать его в список). Массивы занимают меньше памяти, и поэтому, по возможности, надо использовать именно массивы, особенно если речь идет о примитивных типах (в списках примитивные типа оборачиваются в объекты и занимают значительно больше памяти).

private static final char[] ALPHABET = {'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з',
'и','к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
'ъ', 'ы', 'ь', 'э', 'я', '.', ',', '«', '»', '"', '\'', ':', '!', '?', ' '};

Шифровка

Для нее нужно знать сдвиг (ключ) и алфавит.

Для каждого символа оригинального текста нужно:

  • проверить что он есть в вашем алфавите. Если его нет, пропускаем этот символ.
  • найти его позицию в алфавите. Подумай, какую структуру данных нужно использовать чтобы ускорить этот процесс (раз в 15), ведь необязательно же сканировать всю библиотеку в поисках книги на букву Ы (Ладно, П).
  • найти символ на позиции смещенной на заданный сдвиг. И помним что в примере с игрушкой Y стала А (и не улетела в космос). Как это гарантировать? (можно сделать (позиция буквы + сдвиг) %( размер алфавита). Процент - оператор получения остатка от деления).
  • заменить оригинальный символ на зашифрованный

Сохранить результат в файл (чтобы избежать плохого пользователя который попробует зафигачить тебе .bash_profile или hosts валидируй имя файла вывода!)

Создать интерфейс программы / меню пользователя

Графический интерфейс можно создать с помощью JavaFX или Swing. Желательно этим заморочиться уже после создания основной программы. Однако если не успеваешь или не хочешь тратить на это время, можно создать простое текстовое меню и вывести его в консоль. Например:

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.Spec;
import java.util.Locale;
import java.io.File;

@Command(name = "cypher", subcommands = {CommandLine.HelpCommand.class },
         description = "Caesar cypher command")
public class Cypher implements Runnable {
    @Spec CommandSpec spec;
    @Command(name = "encrypt", description = "Encrypt from file to file using key")
    void encrypt(
                 @Parameters(paramLabel = "", description = "source file with text to encrypt") File src,
                 @Parameters(paramLabel = "", description = "dest file which should have encrypted text") File dest,
                 @Parameters(paramLabel = "", description = "key for encryption") int key) {
        // TODO
    }

    @Command(name = "brute force", description = "Decrypt from file to file using brute force") // |3|
    void bruteForce(
                 @Parameters(paramLabel = "", description = "source file with encrypted text") File src,
                 @Option(names = {"-r", "--representative"}, description = "file with unencrypted representative text") File representativeFile,
                 @Parameters(paramLabel = "", description = "dest file which should have decrypted text") File dest) {
        // TODO
    }

    @Command(name = "statistical decryption", description = "Decrypt from file to file using statistical analysis") // |3|
    void statisticalDecrypt(
                 @Parameters(paramLabel = "", description = "source file with encrypted text") File src,
                 @Option(names = {"-r", "--representative"}, description = "file with unencrypted representative text") File representativeFile,
                 @Parameters(paramLabel = "", description = "dest file which should have decrypted text") File dest) {
        // TODO
    }


    @Command(name = "decrypt", description = "Decrypt from file to file using statistical analysis") // |3|
    void decrypt(
                 @Parameters(paramLabel = "", description = "source file with encrypted text") File src,
                 @Parameters(paramLabel = "", description = "dest file which should have decrypted text") File dest,
                 @Parameters(paramLabel = "", description = "key for encryption") int key) {
        // TODO
    }

    @Override
    public void run() {
        throw new ParameterException(spec.commandLine(), "Specify a subcommand");
    }

    public static void main(String[] args) {
        int exitCode = new CommandLine(new Cypher()).execute(args);
        System.exit(exitCode);
    }
}

Если не хочешь разбираться с библиотекой Picocli, можешь поступить максимально просто: сделать меню с помощью циклов или оператора switch. Также нужно, чтобы программа завершала работу по желанию пользователя (например, вводом слова “exit”).

Работа с файлами

Для работы с файлами рекомендуется использовать библиотеку Java NIO, так как:

  • Это более современный и производительный API по сравнению с IO.
  • Поддерживает асинхронную обработку файлов.
  • Обеспечивает корректную работу с большими файлами.

Основные классы NIO для работы с файлами:

Path: Представляет путь к файлу или директории.

Files: Предоставляет статические методы для работы с файлами и директориями (чтение, запись, копирование, удаление и т.д.).

Charset:Представляет кодировку символов, используемую при чтении/записи текстовых файлов.

Пример чтения большого файла с помощью NIO:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;

public class FileHandler {

    public static String readFile(String filePath) throws IOException {
        Path path = Paths.get(filePath);
        byte[] bytes = Files.readAllBytes(path);
        return new String(bytes, StandardCharsets.UTF_8);
    }

    // ... другие методы для работы с файлами ...

}

Рекомендации для работы с большими файлами:

  • Чтение/запись по частям. Для очень больших файлов, которые не помещаются в оперативную память, используйте методы Files.lines() для чтения по строкам или Files.newInputStream() для чтения по блокам.
  • Буферизация. ИспользуйBufferedReader и BufferedWriter для буферизации операций ввода/вывода, что повышает производительность.

Расшифровка

Для нее нужно знать сдвиг (ключ) и алфавит.

Для каждого символа зашифрованного текста нужно:

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

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

Сохранить результат нужно в файл.

Взлом (Brute Force)

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

Но как понять получилось ли расшифровать? Используй пример текста (репрезентативный текст автора или в том же стиле). Можно составить словарь слов и составить метрику основанную на том, сколько слов совпало и какой они длины; или иную метрику которая изучает длину слов и предложений, или посмотреть какие буквы чаще всего предшествуют каким буквам или словарь наиболее частых начал слова (3 буквы), можно вообще не использовать никаких репрезентативных файлов и проверить правильность пунктуации и пробелов; Вариант с наилучшими результатами сохрани в файл вывода.

Взлом (Статистический анализ)

Дополнительное требования(опционально)

Используй пример текста (репрезентативный текст автора или в том же стиле) и составь статистику букв (например, как часто встречается на каждые 1000 символов). Кстати. Легко взломать шифр и без такого файла и анализа: попробуйте угадать пробел — это наверняка наиболее часто встречающийся символ в обычном тексте.

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

Далее посчитай отклонение для каждого возможного сдвига зашифрованной статистики относительно репрезентативной (можно для этого использовать сумму квадратов отклонения или дот-произведение векторов). Найди сдвиг, дающий минимальное отклонение и расшифруйте используя этот сдвиг.

Разбор одного из вариантов готового проекта

Проект проверяется во время прохождения его группой