Журнал LinuxFormat - перейти на главную

LXF97:Mono

Материал из Linuxformat
Перейти к: навигация, поиск
C# для начинающих

Содержание

Mono: Шифруем ваши файлы

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

!!! ПРЕДОСТЕРЕЖЕНИЕ !!!

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


Со времен тайных посланий Марии Стюарт, королевы Шотландской, про свержение королевы Елизаветы, и до системы «Энигма» в нацистской Германии, давно уже ясно, что все, что шифруется, можно и расшифровать – были бы инструменты. Марию сгубил союз сэра Фрэнсиса Уолсингема [Francis Walsingham] и Томаса Феллипса [Thomas Phelippes]: им было поручено разгадывать ее переписку, защищая королеву Елизавету от заговорщиков. Для машины Энигма роковой стала череда роковых случайностей, остроумных догадок и приемов математики. Но итог в обоих случаях один: сообщения, посылаемые в расчете на секретность, были взломаны и превратились в обычный текст.

Я рассказываю все это как предисловие к проекту этого месяца, потому что мы собираемся создать программу шифрования и дешифровки файлов. Для проекта мы применим супер-мощный Advanced Encryption Standard [Продвинутый стандарт шифрования], который оценен Национальным агентством безопасности США как пригодный для документов высокой секретности, но не обольщайтесь: уж если крупная правительственная организация захочет взломать ваши данные, она их взломает. К счастью, я уверен, что скрывать вам нечего, а значит, шифрование, изученное на этом уроке, вполне подойдет, чтобы не дать случайным зевакам пялиться в ваши данные, которые им незачем видеть!

Наш проект этого месяца будет шифровать и дешифровать файлы, основываясь на заданном вами секретном ключе шифрования – это 256-битный ключ AES, способный защитить от хакеров практически любой масти. Как вы, надеюсь, знаете, «бит» – это одна восьмая байта, так что 256-битный ключ эквивалентен 32-м байтам данных. Так уж вышло, что один байт – это ровно столько, сколько требуется для хранения одного ASCII-символа; значит, наш ключ – это пароль из 32-х символов.

Если вы не Джеймс Бонд, то, скорее всего, у вас нет 32-символьного пароля – в лучшем случае 12 символов (если вы особо заботитесь о безопасности), но наиболее вероятно – около 8. Как же создать такой 32-символьный AES-ключ, чтобы вы его не забыли или, упаси Боже, не записали его? Есть два возможных ответа:

  • 1 Дополнить ваш пароль до 32 символов. Например, если это frosties, преобразуем его в frosties000000000000000000000000. Надеюсь, вы понимаете, почему это плохое решение!
  • 2 Создать хэш-сумму ключа. Это преобразует frosties в, на первый взгляд, случайный поток символов и цифр. Замечательное свойство хэш-ключа в том, что, подавая frosties на вход хэш-алгоритма, мы всегда получаем одно и то же.

Второй вариант, очевидно, лучше, так что наша первая задача – прочесть секретный ключ пользователя и преобразовать его в хэш. Самый популярный хэш-алгоритм – Secure Hash Algorithm [Защищенный хэш-алгоритм], но он не столь защищен, как многие о нем думают; есть более новые алгоритмы SHA-256 и SHA-512, выполняющие тут же задачу с большей надежностью. Конечно, проблема с любым новым алгоритмом в том, что его защищенность не доказана, так что пока мы удовольствуемся старым добрым SHA1.

Создаем хэш

Среда .NET предоставляет на выбор несколько хэш-алгоритмов; используются они примерно одинаково. Для SHA1 необходимо создать объект SHA1Managed, передать ему строку для хэширования как поток байт, затем прочесть поток байт хэшированного значения и преобразовать его в строку. Возможность генерации хэша для любых входных данных исключительно полезна; я создам небольшой метод Sha1(), которому можно передать строку текста и получить хэш-строку, при близительно такой:

  static string Sha1(string input) {
     SHA1Managed hashgen = new SHA1Managed();
     byte[] hash = hashgen.ComputeHash(Encoding.UTF8.GetBytes(input));
     string result = “”;
     foreach (byte b in hash) result += b.ToString(“x2”);
     return result;
   }

Здесь два действительно сложных места: это преобразование ввода в последовательность байт при помощи Encoding.UTF8.GetBytes() и преобразование хэшированных байтов обратно в шестнадцатеричную строку через b.ToString(“x2”). Но они сложны лишь потому, что вы, вероятно, видите их впервые – не так все страшно, когда они упакованы в опрятный метод, трогать который вам больше не придется!

Чтобы попользоваться прелестями этой криптографии, вам понадобится добавить следующие строки в заголовок любого кода, который будет использовать упомянутый метод Sha1():

 using System.Security;
 using System.Security.Cryptography;
 using System.Text;

Вперед!

Итак, теперь, когда вы поняли, как работает хэш, я хочу, чтобы вы запустили MonoDevelop и создали новый проект с именем Tanuki (если вы распознаете намек, я немало удивлюсь). Итоговый метод Main() этого проекта должен читать имя файла и параметр + или (для шифрования или дешифрования файла), а также строку текста из командной строки, которая передастся функции Sha1() для создания ключа шифрования. Но сначала убедимся, что система Sha1() корректно работает, заставив Main() прочесть секретный ключ, хэшировать его и затем вывести результат на экран.

 static void Main(string[] args) {
   // Мы хотим прочесть секретный ключ, так что читаем некий текст и помещаем его в переменную “key”
   Console.Write(“Пожалуйста, введите ключ:);
   string key = Console.ReadLine();
   // Если ключ не указан, использовать ключ по умолчанию
   if (string.IsNullOrEmpty(key)) {
      Console.WriteLine(“Ключ не указан – считаем, что это ‘frosties’”);
      key = “frosties”;
   }
   // Создание хэша ключа
   key = Sha1(key);
   Console.WriteLine(key);
 }

Скиньте это в ваш новый проект, вместе с методом Sha1(), описанным ранее, и вы готовы к бою: попытайтесь запустить его, чтобы убедиться, что ваш генератор SHA1 работает нормально.

Секретное послание

Благодаря ReadLine() и Sha1() теперь у нас есть 40-битный ключ безопасности, 32 бита которого будут использоваться для шифрования наших файлов. Здесь начинается реальная сложность: зашифровать и расшифровать файлы с использованием .NET не так легко, и это странно, для столь широко используемой операции! Фактически, шифрование и расшифровка, вместе с использованием сети (как показал предыдущий номер), это уродливые наросты, которые, похоже, необходимы любой среде программирования, чтобы считаться полноценной.

Скорая помощь

Если вы хотите самостоятельно опробовать SHA512, просто замените класс SHA1Managed на SHA512Managed. Но помните, что это приведет к генерации ключей, отличающихся от сгенерированных SHA1!

Труднейшая часть шифрования – работа с данными, пока они блуждают между входным и выходным файлами. Вообще-то создать AES-шифровальщик предельно просто, раз у нас уже есть готовый к применению ключ. Фактически это сводится к следующему:

 RijndaelManaged aes = new RijndaelManaged();
 aes.Key = Encoding.UTF8.GetBytes(key.Substring(0, 32));
 aes.IV = Encoding.UTF8.GetBytes(key.Substring(5, 16));

Rijndael – это прежнее название алгоритма AES, до того, как он был принят в качестве стандарта. Заметьте, что мы используем Substring() для чтения первых 32-х байт ключа SHA1, поскольку это все, что нам нужно. IV – сокращение от Initialisation Vector [Вектор инициализации] и может рассматриваться как беглое предварительное шифрование перед основным шифрованием. Прогон входных данных через IV известен как «отбеливание» [от «белый шум», – прим. ред.] – перед зашифровкой ваш текст преобразуется в нечто более безликое, и если даже кто-то доберется до вашего ключа перебором миллионов комбинаций, он не будет знать, достиг ли он верного результата, потому что IV его исказил. Для AES, IV должен состоять из 16 символов, так что мы читаем из ключа SHA1 с 5-го символа по 21-й, просто для краткости.

Итак, этот кусок весь довольно простой. Более трудная часть протащить входной файл через шифровальщик в выходной файл поскольку это выполняется при помощи файловых потоков. Делается это приблизительно так:

  • Открыть файловый поток с правами записи; он будет использоваться для сохранения результата
  • Создать движок Rijndael
  • Создать базовый шифровальщик, указав ему наш движок Rijndael и выходной файловый поток
  • Прочитать входной файл в массив байтов
  • Передать массив байтов шифровальщику, для его шифрования и записи в результат
  • Закрыть открытые файлы
  • Затем, в завершение, засунем весь этот блок кода в приятный метод EncryptFile(), чтобы больше никогда его не видеть!

Превращая все это в C#, получаем следующий большой метод, с комментариями для удобства чтения:

  static void EncryptFile(string file_in, string file_out, string key) {
   // Я заключил весь метод в блок try/catch для перехвата ошибок
    try {
  FileStream output = new FileStream(file_out, FileMode.Create,FileAccess.Write);
          // Это тот же код, что и ранее
      RijndaelManaged aes = new RijndaelManaged();
      aes.Key = Encoding.UTF8.GetBytes(key.Substring(0, 32));
      aes.IV = Encoding.UTF8.GetBytes(key.Substring(5, 16));
      // Этот кусок делает всю трудную работу
      ICryptoTransform transform = aes.CreateEncryptor();
      CryptoStream cryptostream = new CryptoStream(output,transform,CryptoStreamMode.Write);
      // Вытягиваем входной файл...
      byte[] inputbytes = File.ReadAllBytes(file_in);
      // ...и проводим его через CryptoStream в выходной файл
      cryptostream.Write(inputbytes, 0, inputbytes.Length);
      // а теперь очищаем
      cryptostream.Close();
      output.Close();
   } catch (Exception e) {
      // При возникновении проблем (не найден файл и т. п.), печатаем ошибку
      Console.WriteLine(e.Message);
   }
 }

Необходимость использовать массивы байтов смахивает на откат к C, но это единственный способ корректно работать с двоичными данными. Если вы попытаетесь сохранить шифрованные данные в виде строк, то почти наверняка повредите ваши данные – будьте осторожны!

Подставляем другую щеку

Шифрование файлов без возможности их дешифрации имеет смысл, только если вы для хранения бумаг регулярно применяете шредер. Но цель Tanuki – выполнять шифрование и дешифрацию файлов, используя один и тот же ключ – означает, что нам необходим метод Decrypt(),в пару к вышеприведенному Encrypt().

Дешифрация файлов немного отличается от шифрования, потому что нам необходимо читать дешифрованные данные и записывать простой текст. Это может быть сделано путем прямого подключения выхода шифрованного потока на вход нашей операции записи, почти как “>” в командной строке. Лучший способ объяснить это – предоставить вам код с комментариями, где происходит изменение; вот он:

 static void DecryptFile(string file_in, string file_out, string key) {
   try {
     // Готовимся к чтению со входа и к записи на выход
     FileStream input = new FileStream(file_in, FileMode.Open,FileAccess.Read);
      StreamWriter output = new StreamWriter(file_out);
      RijndaelManaged aes = new RijndaelManaged();
      aes.Key = Encoding.UTF8.GetBytes(key.Substring(0, 32));
      aes.IV = Encoding.UTF8.GetBytes(key.Substring(5, 16));
      ICryptoTransform transform = aes.CreateDecryptor();
      CryptoStream cryptostream = new CryptoStream(input, transform,CryptoStreamMode.Read);
      // Записываем все, что прислал дешифровщик CryptoStream
      output.Write(new StreamReader(cryptostream).ReadToEnd());
      // Убеждаемся, что запись закончена, затем закрываем дескриптор файла
      output.Flush();
      output.Close();
   } catch (Exception e) {
      Console.WriteLine(e.Message);
   }
 }

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

Скорая помощь

Мы используем Encoding.UTF8.GetBytes() для преобразования нашей строки-ключа в последовательность байтов, но технически это не обязательно, потому что Sha1() возвращает лишь ASCII-символы, и можно обойтись Encoding.ASCII.GetBytes(). Однако в идеале мы не хотим, чтобы Encrypt() или Decrypt() вникали, в каком формате они получают данные, поэтому работа с UTF8 (известной также как Unicode) – более умное решение.

Соединяем все вместе

Теперь мы умеем шифровать файлы, дешифровывать файлы и генерировать хэши строк для использования в качестве секретных ключей; но Tanuki пока что лишь набор методов, гуляющих сами по себе. В некотором смысле это неплохо: работа с такими самодостаточными кодами означает, что у вас есть набор строительных блоков, которые можно поместить куда угодно. Более того, если когда-нибудь вы решите изменить ваш алгоритм с AES на какой-то другой, останется лишь изменить несколько строк в Encrypt() и Decrypt() – остальной код менять не нужно, потому что он и знать не знает, как работают Encrypt() и Decrypt(). Этот метод, известный как «инкапсуляция» или «скрытие данных», является фундаментальной концепцией программирования.

Итак, на данном этапе нам осталось обновить метод Main(), чтобы он читал параметры и выполнял соответствующие действия. Параметры, которые должен воспринимать Tanuki – это указание шифровать или дешифровать файл (при помощи + или –), а также имена входного и выходного файлов. В общем, метод должен:

  • 1 Проверить, налицо ли все три параметра (и выкручиваться, если их окажется больше/меньше).
  • 2 Прочесть секретный ключ пользователя из командной строки (и использовать frosties, если ничего не было введено).
  • 3 Создать хэш ключа.
  • 4 Если первый параметр +, шифровать файл.
  • 5 Если первый параметр –, дешифровать файл.

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

 static void Main(string[] args) {
   // Нам нужно ровно три аргумента!
   if (args.Length != 3) {
      Console.WriteLine(“Укажите либо +, либо - как первый параметр, потом введите входной и выходной файлы как второй и третий параметры.”);
      return; // Выкручиваемся
   }
   // Это уже было
   Console.Write(“Пожалуйста, введите ключ:);
   string key = Console.ReadLine();
   if (string.IsNullOrEmpty(key)) {
      Console.WriteLine(“Ключ не указан – считаем, что это ‘frosties’”);
      key = “frosties”;
   }
   key = Sha1(key);
   // Выбираем, что делать, согласно первому параметру
   if (args[0] ==+) {
      EncryptFile(args[1], args[2], key);
   } else {
      DecryptFile(args[1], args[2], key);
   }
 }
Забираемся глубже!

Если вы закончили Tanuki и ищете способы набраться опыта, попробуйте такие идеи:

  • Легко: Заставьте Tanuki записывать секретный журнал с информацией о том, когда каждый файл был зашифрован или дешифрован.
  • Средне: Позвольте людям выбирать другие алгоритмы шифрования, а не только AES.
  • Трудно: Позвольте пользователям указывать каталоги для шифрования. Пусть выходной файл автоматически приобретает вид файл.расширение.encrypt, т. е. /foo/bar/baz.txt превратится в /foo/bar/baz.txt.encrypt
  • Экстрим: Кэшируйте секретные ключи пользователей на 10 минут, чтобы им не приходилось постоянно вводить их при шифровании различных файлов. Подсказка: наиболее безопасный метод требует другой программы, запущенной как демон, а также связи через сокеты.

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

  • Создайте файл с именем foo.txt в вашем каталоге bin/Debug.
  • Поместите в него что-нибудь. Но только не много.
  • Теперь выполните: mono tanuki.exe + foo.txt foo-enc.txt
  • Затем: mono tanuki.exe - foo-enc.txt foo-copy.txt
  • Если все работает, то foo.txt и foo-copy.txt должны совпасть.

Нашли проблему? Нет? Хорошо, даю вам еще минуту, и пользуюсь случаем напомнить вам, что для максимальной безопасности понадобится вектор инициализации получше. У использованного мною IV есть проблема: он поставляется из того же источника, что и ключ (обработанная SHA1 версия текста, предоставленного пользователем). Все потому, что я использую одну и ту же часть хэша для создания ключа и IV, вместо того, чтобы использовать для IV несколько символов не из ключа. Помните, однако, что IV не должен храниться в секрете, потому что его основная работа – малость перемешать ваш простой текст перед тем, как он отправится на шифрование.

Работа над ошибками

Я думаю, что у вас было достаточно времени осознать проблему с параметрами в Tanuki, а потому даю ответ: если args[0] равен «+», мы шифруем файлы. В противном случае, мы их дешифруем. А вдруг ктото случайно перепутает порядок следования параметров, т.е.: mono tanuki.exe in.txt out.txt +

В этом примере Tanuki попытается дешифровать файл, а затем вылетит, потому что мы указали в качестве имени выходного файла +. Простейший способ исправить этот казус – ввести проверку, например, так:

 if (args[0] ==+) {
   EncryptFile(args[1], args[2], key);
 } else if (args[0] ==-) {
   DecryptFile(args[1], args[2], key);
 } else {
   Console.WriteLine(“Укажите либо +, либо - как первый параметр”);
 }

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

Length:
 if (!File.Exists(args[1])) {
   Console.WriteLine(“Входной файл не существует!);
   return;
 }
Пароли как параметры

У вас может быть искушение позволить людям запускать Tanuki так:

mono tanuki.exe + in.txt out.txt my_secret_key

На вид более удобно, и это вправду удобно. А заодно и ужасно небезопасно, поскольку любой может просто влезть в вашу историю Bash и узнать пароль. Единственный способ обойти это – очистить историю (выполнив history -c), но проще запросить пароль после запуска программы.


Что касается второй проблемы, ее решение зависит от того, как вы хотите с ним работать. Я специально написал Tanuki так, чтобы она работала с входным и выходным файлами, и никакая ошибка, случайно допущенная вами при вводе кода, не уничтожила ваши драгоценные файлы! Но коль скоро ваш код заработал, почему бы вообще не отбросить параметр выходного файла и выполнять шифрование прямо на месте. Как альтернатива, вы можете позволить людям запускать Tanuki с параметрами + и (шифрование и дешифрация) или ++ и –– (шифрование и дешифрация, и не выкручиваться, если выходной файл уже существует).

Скорая помощь

Скомпилированную программу Mono очень просто преобразовать обратно в более или менее понятный код, в основном потому, что большинство .NET программ содержит много легко распознаваемых встроенных функций. Так что не ожидайте выигрыша от любых усилий вроде «неясно – значит, безопасно»: Tanuki защищена, потому что защищен AES и потому что секретный ключ хранится в секрете, а не потому, что вы припрятали исходный код только для себя.

Итак, мы подошли к концу проекта этого месяца, и я надеюсь, вы видите, что теперь мы получили действительно серьезный код. Если даже вам не на 100% ясно, как работают Encrypt(), Decrypt() и Sha(), фишка в том, что они существуют как закрытые методы, о которых вам нечего беспокоиться – они работают, вот и все! В частности, я счел метод Sha1() полезным в моем собственном коде, поскольку создание хэш-значений (всего, от паролей до DVD-образов в 8 ГБ) является прекрасным способом проверки корректности полученных данных. Но в любой ситуации, когда вы не желаете, чтобы мир копался в ваших данных, шифрование – лучший выбор.

Наслаждайтесь – шифрование ваших файлов поможет вам быть на шаг впереди силовых ведомств! LXF

Персональные инструменты
купить
подписаться
Яндекс.Метрика