пятница, октября 05, 2007

Где хранить ключи?

Когда нам надо защитить данные системы в БД, в файле, при пересылке, мы просто берем и шифруем их каким ни будь симметричным алгоритмом. Данные зашифрованы, и тут перед нами встает проблема, куда теперь девать ключ, которым зашифрованы данные. Ведь их потом надо расшифровать, причем этим же ключом. С такой проблемой сталкиваются многие разработчики и каждый решает ее по своему с разной степени кривизны и навороченности. Сталкивался с ней и я, причем несколько раз за последнее время. И решал, с разной степенью навороченности и кривизны :) Опытом своих экзерциций я и хочу поделиться.

Начнем с очевидных вещей. Первое, ключ следует хранить отдельно от защищаемых данных. Если шифруемые данные хранятся в БД, то ключ там хранить не стоит.

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

Третий момент менее очевидный и касается он только многопользовательских систем. В таких системах необходимо обеспечить доступ авторизованных пользователей к ключам. Стратегий может быть две: либо каждый пользователь шифрует свои личные данные персональным ключом, либо все пользователи шифруют общие (shared) данные одним ключом. Выбор стратегии зависит от функциональных требований, а сама стратегия накладывает ограничения на способ хранения ключей.

Итак, задача номер один - защитить (зашифровать) ключ, задача номер два – придумать, где его хранить.

Для шифрования ключа можно, например, использовать ассиметричный алгоритм (RSA), но опять же - где хранить ключи ассиметричного алгоритма? Замкнутый круг. В .Net 1.1 для этой цели я пытался использовать CSPParameters но результат получался не удовлетворительный (конкретно, не удавалось использовать CspProviderFlags.UseMachineKeyStore для пользователей с ограниченными правами). Хорошо если пользователи вводят пароль при входе в систему, тогда этот пароль можно использовать для защиты персонального ключа. А если ключ должен быть общий? Или пользователи используют windows integrated аутентификацию и не вводят никаких паролей? К счастью этой же проблемой озаботились парни из Рэдмонда и написали для этих целей Data Protection API (DPAPI). В .Net 1.1 надо было делать managed обертку для DPAPI. В .Net 2.0 появился специальный класс System.Security.Cryptography.ProtectedData. Его методы Protect и Unprotect замечательно подходят для таких задач, как шифрование симметричного ключа:


byte[] salt = new byte[] { 143, 22, 104, 10, 12, 83 };
byte[] cryptedKey = ProtectedData.Protect(key, salt, DataProtectionScope.CurrentUser);
byte[] cryptedIV = ProtectedData.Protect(IV, salt, DataProtectionScope.CurrentUser);



Здесь key и IV это и есть наш симметричный ключ (и вектор инициализации), в salt - это хвост, который добавляется для повышения криптостойкости полученного результата. DataProtectionScope определяет область действия защиты. Если данные зашифрованы в DataProtectionScope.CurrentUser расшифровать их можно будет только для того же пользователя. Если область действия LocalMachine – то любой пользователь на этой машине.

Итак, с защитой ключа все ясно. Теперь определимся с местом хранения. Первым делом, напрашивается хранение в файле. Но существует более удобный способ - System.IO.IsolatedStorage. Не следует думать что IsolatedStorage представляет из себя какое то защищенное хранилище, которое может повысить защищенность хранимых данных. Никакой дополнительной защиты IsolatedStorage не дает. Его скорее следует рассматривать, как некую виртуальную песочницу, которая позволяет, с одной стороны, абстрагироваться от файловой системы, а с другой стороны ограничить область видимости до необходимого уровня. Область видимости ограничена локальной машиной или пользователем, а внутри дополнительно приложением, AppDomain-ом или конкретной сборкой. Реально, данные IsolatedStorage сохраняются в пользовательском профайле. Таким образом, IsolatedStorage избавляет нас от необходимости настройки разрешений NTFS, а также от опасности того, что данные одного приложения будут затерты или прочитаны другим приложением.

Итак, мы шифруем наш симметричный ключ с помощью ProtectData и затем сохраняем его в IsolatedStorageFile:



private byte[] _key; // ключ
private byte[] _iv; // вектор инициализации
private string _name; // имя под которым будем хранить ключ
private DataProtectionScope _scope; // область видимости


private IsolatedStorageFile GetIsolatedStorage()
{
// в зависимости от области видимости получаем IsolatedStorageFile
if (_scope == DataProtectionScope.LocalMachine)
return IsolatedStorageFile.GetMachineStoreForDomain();
else return IsolatedStorageFile.GetUserStoreForDomain();
}

byte[] salt = new byte[] { 143, 25, 4, 30, 11, 83 };
// сохраняем ключ
void SaveInStorage()
{
// сначала шифруем
byte[] cryptedKey = ProtectedData.Protect(_key, salt, _scope);
byte[] cryptedIV = ProtectedData.Protect(_iv, salt, _scope);

// потом сохраняем
IsolatedStorageFile iStorage = GetIsolatedStorage();
using (BinaryWriter writer = new BinaryWriter(new IsolatedStorageFileStream(_name, FileMode.Create, iStorage)))
{
// записываем длину зашифрованного ключа
writer.Write(cryptedKey.Length);
// записываем длину зашифрованного IV
writer.Write(cryptedIV.Length);
// записываем зашифрованный ключ
writer.Write(cryptedKey);
// записываем зашифрованный IV
writer.Write(cryptedIV);
}
}

// поднимаем ключ
void LoadFromStorage()
{
byte[] encryptedKey;
byte[] encryptedIV;
// читаем ключ из IsolatedStorageFile
IsolatedStorageFile iStorage = GetIsolatedStorage();
using (BinaryReader reader = new BinaryReader(new IsolatedStorageFileStream(_name, FileMode.Open, iStorage)))
{
// сначала читаем длину ключа
int keyLength = reader.ReadInt32();
// потом читаем длину IV
int ivLength = reader.ReadInt32();

encryptedKey = new byte[keyLength];
encryptedIV = new byte[ivLength];
// затем читаем ключ и IV
reader.Read(encryptedKey, 0, encryptedKey.Length);
reader.Read(encryptedIV, 0, encryptedIV.Length);

// расшифровываем ключ и IV
_key = ProtectedData.Unprotect(encryptedKey, salt, DataProtectionScope.LocalMachine);
_iv = ProtectedData.Unprotect(encryptedIV, salt, DataProtectionScope.LocalMachine);
}
}



Здесь приведен фрагмент кода класса SymmetricKeyContainer, который инкапсулирует в себе логику защиты и хранения симметричного ключа. Этот класс входит в небольшую библиотеку CryptoUtils, которую я разместил в Google Code Project Hosting, в основном с целью опробовать этот новый инструмент Google. И надо сказать, я остался им вполне доволен. Полностью исходный код класса можно взять в репозитории проекта (svn) здесь: http://cryptoutils.googlecode.com/svn/

3 комментария:

Анонимный комментирует...

Получается, что любая сторонняя программа, запушенная под текущим юзером может прочитать и дешифровать ключ. Или я что-то не понимаю?

Sergey Rozovik комментирует...

Я ждал подобного вопроса :)
Ответ, и да, и нет.
Если рассматривать не злонамеренный код, то ответ отрицательный. IsolatedStorage обеспечивает изолированное хранение данных, и в нашем случае, даже если две программы будут использовать наш код и хранить свои ключи с одинаковыми именами они не пересекутся.
Если же мы будем рассматривать злонамеренный код, то тут тоже не все так просто. В .Net существует механизм под названием CAS (Code Access Security), который делит весь управляемый код на различные уровни доверия (trusted level). Чтобы завладеть нашим ключом, злонамеренный код должен иметь уровень FullTrust (точнее, ему потребуются разрешения DataProtectionPermission, IsolatedStorageFilePermission и ReflectionPermission). И тут встает вопрос, как злонамеренный код код может попасть в FullTrust? Только если пользователь самолично загрузит и исполнит его на свей машине.
Все мы видим как ведется многолетняя война между системами защиты проприетарного ПО и его взлома. Для защиты программ используются много более хитроумные методы чем описанный. И взломщики всегда выигрывают. Все потому, что не существует надежного метода защиты секрета в программе от злонамеренного кода, который имеет полный доступ к environment в котором выполняется программа. Если злонамеренный код запущен от имени текущего пользователя и имеет FullTrust, то да, он сможет украсть наш ключ.
И что же делать?
Выход давно найден. В тех случаях когда необходимо защитить секрет от других программ, выполняющихся в том же контексте безопасности, следует хранить секрет за пределами этого контекста. Это делают, к примеру, системы аппаратного ключа.

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

Анонимный комментирует...

Храните ключи в HSM. Избавите себя от многих проблем :-)