четверг, 22 декабря 2011 г.

Пример проектирования: шифрующая папка на "рабочем столе"


Приведу пример на «цепочку действий» из моей практики. Думаю, он будет интересен.

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

Для реализации шифрующей папки к проекту подключили меня.


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

На входе у меня есть два параметра:

  1. Имя входного файла или фолдера.
  2. Имя выходного (зашифрованного!) файла.

Далее описываю процесс постановки задачи по шагам:

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

bool Encrypt(const char * pszInPath, const char * pszOutFileName);

где pszInPath – имя входного файла или фолдера, а pszOutFileName – имя выходного (зашифрованного) файла.

К сожалению, такой функции НЕ оказалось.

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

Функция имеет следующий прототип:

bool fnEncrypt(const unsigned char * pszInData,
                     unsigned char * pszOutData,
                     const unsigned int nLength,
                     const char * pszPublicKey,
                     const char * pszPrivateKey);

где: pszInDataпоток входных данных, pszOutDataбуфер для выходных данных, nLengthразмер буферов, pszPublicKeyпубличный ключ, pszPrivateKeyприватный ключ.

Поскольку на вход функции fnEncrypt можно подавать только буфер с бинарными данными, возникла необходимость в обработке двух различных ситуаций:

1. Нужно зашифровать файл.
2. Нужно зашифровать папку с файлами и другими папками.

Для ситуации 1 можно составить пошаговый алгоритм действий:

  1. Открыть файл.
  2. Узнать размер файла.
  3. Выделить два буфера определенного размера (один – для входных данных, другой – для выходных).
  4. Считать данные из файла в первый буфер.
  5. Вызвать функцию fnEncrypt для шифрования данных.
  6. Создать выходной файл.
  7. Записать данные из второго буфера в выходной файл.
  8. Закрыть оба файла.

Проблема возникает на шаге 5): в функцию fnEncrypt нужно передавать НЕ только данные, но и ключи.

Шаг 2. Для получения ключей в программе оказалась функция:

bool fnReadKey(const int iKeyNum, char * pszPublicKey, const char * pszPrivateKey);

где: iKeyNumномер ключа (0 – соответствует текущему), pszPublicKeyоткрытый ключ, pszPrivateKeyзакрытый ключ.

С учетом функции fnReadKey пошаговый алгоритм был модифицирован:

  1. Открыть файл.
  2. Узнать размер файла.
  3. Выделить два буфера определенного размера (один – для входных данных, другой – для выходных).
  4. Считать данные из файла в первый буфер.
  5. Считать ключи в буферы для ключей.
  6. Вызвать функцию fnEncrypt для шифрования данных.
  7. Создать выходной файл.
  8. Записать данные из второго буфера в выходной файл.
  9. Закрыть оба файла.

Проблемы опять-таки возникла на шаге 5). Во-первых, было неясно, какой размер буфера предусмотреть для ключей. (Тут оказалось все просто – в программе была определена константа.) Во-вторых, было непонятно, каким образом выбирать ключ, которым нужно зашифровать файл.

Шаг 3. Оказалось, что для выбора ключа нужно запускать внешнюю программу – Encryption.exe. Программа показывает диалог, с помощью которого пользователь может установить текущий ключ. Далее его можно получить, вызвав fnReadKey с параметром iKeyNum, равным 0.

После выяснения всех этих подробностей получился такой алгоритм:

  1. Открыть файл.
  2. Узнать размер файла.
  3. Выделить два буфера определенного размера (один – для входных данных, другой – для выходных).
  4. Считать данные из файла в первый буфер.
  5. Запустить программу Encryption.exe при помощи CreateProcess.
  6. Вызвать функцию WaitForSingleObject для ожидания завершения процесса.
  7. Считать ключи в буферы для ключей.
  8. Вызвать функцию fnEncrypt для шифрования данных.
  9. Создать выходной файл.
  10. Записать данные из второго буфера в выходной файл.
  11. Закрыть оба файла.

Шаг 4. Аналогичным образом была рассмотрена и ситуация 2 – когда на вход подается НЕ файл, а папка с файлами и другими папками. Для того чтобы можно было зашифровать и папку, ее нужно было сначала преобразовать в файл специального вида.

Не вдаваясь в детали формата этого файла, опишем последовательность действий, которая получилась:

  1. Если на входе – папка с файлами, то – преобразовать ее в файл специального вида.
  2. Открыть файл.
  3. Узнать размер файла.
  4. Выделить два буфера определенного размера (один – для входных данных, другой – для выходных).
  5. Считать данные из файла в первый буфер.
  6. Запустить программу Encryption.exe при помощи CreateProcess.
  7. Вызвать функцию WaitForSingleObject для ожидания завершения процесса.
  8. Считать ключи в буферы для ключей при помощи функции fnReadKey.
  9. Вызвать функцию fnEncrypt для шифрования данных.
  10. Создать выходной файл.
  11. Записать данные из второго буфера в выходной файл.
  12. Закрыть оба файла.

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

Заголовок такого файла может содержать:

  1. сигнатуру;
  2. количество блоков;
  3. контрольную сумму;
  4. массив адресов всех блоков от начала файла.

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

Чтобы зашифровать файл, программа должна выполнить такую последовательность действий:

  1. Если на входе – папка с файлами, то – преобразовать ее в файл специального вида.
  2. Открыть файл.
  3. Получить размер файла.
  4. Запустить программу Encryption.exe при помощи CreateProcess.
  5. Вызвать функцию WaitForSingleObject для ожидания завершения процесса.
  6. Считать ключи в буферы для ключей при помощи функции fnReadKey.
  7. Выделить два буфера определенного размера (один – для входных данных, другой – для выходных).
  8. Рассчитать количество блоков, которое будет сохранено в зашифрованном файле.
  9. Создать выходной файл.
  10. Организовать цикл по блокам:
    1. Считать данные блока из входного файла в первый буфер.
    2. Вызвать функцию fnEncrypt для шифрования блока.
    3. Записать данные из второго буфера в выходной файл.
    4. Запомнить адрес блока в заголовке.
    5. Рассчитать контрольную сумму.
    6. Сохранить заголовок в выходном файле.
    7. Закрыть оба файла.
  11. И т.д. …………………………………

Подведем итоги:

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

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

3. Модифицировать последовательность действий, предусмотрев в ней действия, которые нейтрализуют «затык».

Примечание: Нейтрализующие действия вставляются в последовательность до «затыка».

Пример: Чтобы зашифровать данные, их нужно предварительно считать в буфер из исходного файла. Шифрование производится при помощи ключа. Поэтому предварительно ключ нужно прочитать при помощи функции fnReadKey. Прежде чем использовать ключ, пользователь должен его сначала выбрать. Для этого нужно запустить программу Encryption.exe, которая отобразит диалог выбора ключа и сохранить выбранный пользователем ключ.

Статья написана 15.10.2004, правка 21.12.2011