Защита данных в .NET

ОГЛАВЛЕНИЕ

Данная статья кратко объясняет распространенные криптосистемы и подробно описывает два самых популярных шифра с закрытым ключом: DES –  наиболее широко используемый, и AES – собирающийся заменить DES.

•    Примеры кода .NET по криптологии [исходник - 38.5 Кб] [демо - 9.96 Кб]
•    Реализация AES и DES на C# [исходник - 35.7 Кб] [демо - 13.1 Кб]

1. Введение

Криптология – область, занимающаяся обеспечением безопасности и конфиденциальности. Эта область включает в себя много криптосистем, каждая из которых состоит из набора алгоритмов, направленных на обеспечение защиты данных. Сейчас криптосистемы широко применяются во всех областях цифровых технологий. Цифровые подписи, электронная почта и интернет-банкинг – лишь несколько приложений, в которых используются криптосистемы. Данная статья кратко объясняет распространенные криптосистемы и подробно описывает два самых популярных шифра с закрытым ключом: DES –  наиболее широко используемый, и AES – собирающийся заменить DES. Следует начать с обзора распространенных криптосистем.

2. Обзор распространенных криптосистем

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

2.a Симметричные (с закрытым ключом) криптосистемы

Симметричные криптосистемы используют один и тот же ключ в операциях шифрования и расшифровки. Эти две операции обычно похожи. Однако некоторые части данных операций придерживаются обратного порядка относительно друг друга (например, для AES алгоритм расшифровки придерживается обратного порядка относительно алгоритма шифрования, с небольшим изменением в некоторых других методах).
Симметричные криптосистемы делят данные на маленькие блоки и шифруют их по одному с помощью секретного ключа. Затем блоки объединяются и отправляются получателю в совокупности. На стороне получателя применяется операция расшифровки с использованием того же самого ключа, что дает восстановленные данные.

Симметричные алгоритмы очень быстрые по сравнению с асимметричными алгоритмами шифрования и хорошо подходят для выполнения криптографических преобразований над большими потоками данных. Самые популярные алгоритмы симметричного шифрования - DES, AES и TripleDES. TripleDES использует DES три раза подряд с разными ключами. DES и AES будут подробно рассмотрены позже.

Блок-схема симметричных криптосистем

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

//простая операция шифрования закрытым ключом
private void buttonEncrypt_Click(object sender, EventArgs e)
{
    string password = textBoxKey.Text;
    string salt = textBoxSalt.Text;
    string plainText = textBoxInput.Text;
    byte[]plainBytes=Encoding.ASCII.GetBytes(plainText);

    //Rfc2898DeriveBytes: используется для генерации стойких ключей
    Rfc2898DeriveBytes rfc = new Rfc2898DeriveBytes(password,
        Encoding.ASCII.GetBytes(salt));
        //неанглийские алфавиты не будут работать в кодировке ASCII

    SymmetricAlgorithm Alg = GetSelectedAlgorithm();

    Alg.Key = rfc.GetBytes(Alg.KeySize / 8);
    Alg.IV = rfc.GetBytes(Alg.BlockSize / 8);

    MemoryStream strCiphered = new MemoryStream();//для хранения зашифрованных данных

    CryptoStream strCrypto = new CryptoStream(strCiphered,
        Alg.CreateEncryptor(), CryptoStreamMode.Write);

    strCrypto.Write(plainBytes, 0, plainBytes.Length);
    strCrypto.Close();
    textBoxCiphered.Text = Convert.ToBase64String(strCiphered.ToArray());
    strCiphered.Close();
}
       
private SymmetricAlgorithm GetSelectedAlgorithm()
{
    SymmetricAlgorithm Alg = null;
    if (comboBoxAlgorithm.SelectedIndex == 0)
        Alg = new DESCryptoServiceProvider();
    else if (comboBoxAlgorithm.SelectedIndex == 1)
        Alg = new RijndaelManaged();//RijndaelManaged: алгоритм AES в .NET
    else if (comboBoxAlgorithm.SelectedIndex == 2)
        Alg = new TripleDESCryptoServiceProvider();
    else
        Alg = new RC2CryptoServiceProvider();
    return Alg;
}

2.b Асимметричные (с открытым ключом) криптосистемы

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

Асимметричные алгоритмы шифрования основаны на тяжелых математических операциях, поэтому они неэффективны в обработке больших блоков данных. Они часто применяются для безопасного обмена маленькими ключами сеанса. Асимметричные шифры используются для аутентификации и конфиденциальности. Самый популярный асимметричный алгоритм шифрования - RSA, понимание которого требует серьезной математической подготовки. Ниже показана структура RSA:

//Генерация открытого и закрытого ключей
Выбрать p,q так, чтобы p и q были двумя разными большими простыми числами
Пусть n=p*q
Пусть m=(p-1)*(q-1)
Выбрать e так, чтобы было 1<e<m и gcd(m,e)=1 (e взаимно-простой с m)
Вычислить такой d, чтобы было (d*e)mod(m) = 1

Открытый ключ: {e,n}
Закрытый ключ:{d,n}

//Шифрование
Открытый текст: M   (M<n)
Шифротекст: C = M<sup>e</sup> Mod(n)

//Расшифровка
Шифротекст: C
Открытый текст: M = C<sup>d</sup> Mod(n)

Блок-схема асимметричных криптосистем

Открытый текст шифруется [Бобом] открытым ключом Алисы, передается, затем расшифровывается [Алисой] закрытым ключом Алисы.

/*
 * В отличие от симметричных шифров, нельзя свободно выбрать
 * пары открытых и закрытых ключей, потому что эти два ключа
 * математически связаны друг с другом. Правильный способ
 * создания этих ключей в .NET - использовать асимметричный класс,
 * генерирующий данные ключи в своем конструкторе.
 */
private void GenerateKeyPairs()
{                       
    //При каждом вызове этого конструктора
    //генерируется другая пара открытого и закрытого ключей.
    rsaCipher = new RSACryptoServiceProvider();

    //извлечь открытые параметры
    textBoxPublicKey.Text = rsaCipher.ToXmlString(false);

    //извлечь закрытые параметры
    textBoxPrivateKey.Text = rsaCipher.ToXmlString(true);
}

private void buttonEncrypt_Click(object sender, EventArgs e)
{
    rsaCipher = new RSACryptoServiceProvider();

    string publicKey = textBoxPublicKey.Text;
    rsaCipher.FromXmlString(publicKey);

    string plainText = textBoxPlain.Text;
    byte[] plainBytes = Encoding.ASCII.GetBytes(plainText);

    byte[] cipheredBytes = rsaCipher.Encrypt(plainBytes, true);
    string cipheredText = Convert.ToBase64String(cipheredBytes);

    textBoxCiphered.Text = cipheredText;
}

2.c Хеширование

Хеширование – операция взятия контрольной суммы данных. Назначение этого метода – сгенерировать уникальных ключ постоянного размера [из входных данных], который не могут дать никакие другие данные, тем самым делая невозможной генерацию данных из ключа. Хеширование очень полезно для систем баз данных. Представьте базу данных, хранящую пароли пользователей. Хранить пароли в открытом виде не так безопасно, как хранить хеш-значения паролей в таблицах. Даже притом что любое хеш-значение содержится в базе данных, невозможно восстановить оригинальный пароль из этого значения. Данный метод обеспечивает такой же функционал для процессов авторизации. Единственное отличие в том, что хеш пароля пользователя сравнивается со значением в базе данных вместо сравнения самого пароля.

//хешировать ввод и отобразить результат
private void buttonComputeHash_Click(object sender, EventArgs e)
{
    byte[] input = Encoding.ASCII.GetBytes(textBoxInput.Text);

    HashAlgorithm Alg = GetSelectedAlgorithm();
    Alg.ComputeHash(input);

    textBoxHash.Text = Convert.ToBase64String(Alg.Hash);
}

private HashAlgorithm GetSelectedAlgorithm()
{
    //только для алгоритмов хеширования с помощью ключей
    Rfc2898DeriveBytes rfc = new Rfc2898DeriveBytes(textBoxKey.Text,
        Encoding.ASCII.GetBytes(textBoxSalt.Text));

    HashAlgorithm Alg = null;
    if (comboBoxAlgorithm.SelectedIndex == 0)
        Alg = new MD5CryptoServiceProvider();
    else if (comboBoxAlgorithm.SelectedIndex == 1)
        Alg = new RIPEMD160Managed();
    else if (comboBoxAlgorithm.SelectedIndex == 2)
        Alg = new SHA1CryptoServiceProvider();
    else if (comboBoxAlgorithm.SelectedIndex == 3)
        Alg = new SHA256Managed();
    else if (comboBoxAlgorithm.SelectedIndex == 4)
        Alg = new SHA384Managed();          
    else if (comboBoxAlgorithm.SelectedIndex == 5)
        Alg = new  SHA512Managed();
    else if (comboBoxAlgorithm.SelectedIndex == 6)
        Alg = new MACTripleDES(rfc.GetBytes(16));
    else if (comboBoxAlgorithm.SelectedIndex == 7)
        Alg = new HMACSHA1(rfc.GetBytes(29));

    return Alg;
}

2.d Применения цифровой подписи

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

Допустим, надо отправить сообщение получателю так, чтобы он был однозначно уверен, что сообщение отправили вы, и что никто не изменил его содержимое во время передачи. Отправка сообщения с его хешем позволит получателю проверить, принадлежит хеш сообщению или нет. Если сообщение не изменено, хеш сообщения будет таким же, как и прикрепленный хеш. Но что если кто-то другой изменит и сообщение, и хеш во время передачи? Тогда надо позаботься о таком случае.

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

Блок-схема применения цифровой подписи


3. Стандарт шифрования данных

DES является наиболее широко используемым шифром с закрытым ключом. Он имеет классическую структуру Фейстеля, состоящую из некоторого количества идентичных циклов обработки. DES использует 64-битный блок и 54-битный размер ключа. Ключ используется и в шифровании, и в расшифровке. Операция шифрования в DES выполняет циклы на рисунке.

Блок-схема шифрования DES

Рисунок изображает операцию шифрования. Инвертирование порядка применения ключей дает операцию расшифровки. Перед применением этого рисунка 16 ключей генерируются из секретного ключа. Эти фазы объяснены далее.

3.a Шифрование

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

//Алгоритм шифрование DES
private Bit[] Encrypt64Bit(Bit[] block)
{
    InitialPermutation(block);

    Bit[] Left = block[0 - 31];
    Bit[] Right = block[31 - 63];
   
    for (int i = 1; i <= 16; i++)
    {
        Temp = Left;
        Left = Right;
        Right = (Temp) Xor (F(Right, Keys[i - 1]));
    }
    block[0 - 31] = Right;
    block[32 - 63] = Left;

    RevInitialPermutation(block);

    return block;
}

3.a.1 Начальная перестановка

Эта операция принимает 64- битные входные данные и переставляет другой блок в порядке таблицы IP. Например, первый элемент таблицы IP равен 58, тогда первый элемент нового блока содержит 58-й элемент входного блока. Следовательно, второй элемент нового блока содержит 50-й элемент входного блока, а последний элемент нового блока содержит 7-й элемент входного блока.

IP = new byte[8 * 8] {   58,    50,   42 ..... 15,    7 }; 

3.a.2 Функция F()

Этот метод принимает два параметра: правый блок (32 бита) и текущий ключ (48 бита). Сначала он увеличивает правый блок в порядке таблицы выбора E, что дает 48-битный блок. Затем применяет операцию “Исключающее или“ к новому блоку и текущему ключу. Потом разделяет подвергнутый операции “Исключающее или“ блок на 8 частей (первые 6 битов становятся первой частью, вторые 6 битов становятся второй частью и т. д.). Теперь есть 8 блоков, каждый содержит 6 битов. Далее из каждого 6-битового блока генерируется 4-битовый блок по следующему правилу:

(Есть восемь таблиц S, а именно S1, S2, S3... S8. Максимальный элемент в этих таблицах - 15, то есть число битов максимального элемента - 4). Пусть первые 6 битов равны abcdef, тогда вычисляется B1 = S1[2*a+f, 8*b+4*c+2*d+e]. Пусть вторые 6 битов равны asdfgh, тогда вычисляется B2 = S2[2*a+h, 8*s+4*d+2*f+g]. Пусть восьмые 6 битов равны zxcvbn, тогда вычисляется B8 = S8[2*z+n, 8*x+4*c+2*v+b].

Каждый из 8 блоков содержит 4 бита. Это дает 4*8=32 битный блок (B1 B2 B3 B4 B5 B6 B7 B8), в итоге переставляемый в порядке таблицы P, как показано в следующем фрагменте кода:

private BitArray F(BitArray R, BitArray K)
{
    R = Table(E, R);
    BitArray B = R.Xor(K);
    BitArray S = new BitArray(8 * 4);

    int x, y;
    BitArray Temp;
    for (int i = 0; i < 8; i++)
    {
        x = (B[i * 6 + 0] ? 2 : 0) + (B[i * 6 + 5] ? 1 : 0);
        y = (B[i * 6 + 1] ? 8 : 0) + (B[i * 6 + 2] ? 4 : 0) +
            (B[i * 6 + 3] ? 2 : 0) + (B[i * 6 + 4] ? 1 : 0);

        Temp = new BitArray(new byte[] { Ss[i, 16 * x + y] });
        Copy(Temp, 0, S, i * 4, 4);
    }

    S = Table(P, S);
    return S;
}

Блок-схема функции F

3.a.3 Операция XOR

Этот метод принимает два битовых массива и применяет операцию “Исключающее или“ к каждому биту входного массива. Затем он сохраняет подвергнутые операции “Исключающее или“ биты в новый битовый массив. Реализация данного метода выглядит так:

Bit[] Xor(Bit[] Left, Bit[] Right)
{
    Bit[] Result = new Bit[Right.Length];
    for (int i = 0; i < Right.Length; i++)
        Result[i] = Left[i] ^ Right[i];
    return Result;
}

3.a.4 Инвертирование начальной перестановки

Этот метод аналогичен методу начальной перестановки, не считая того, что входные данные переставляются в порядке таблицы IP-1.

3.b Расшифровка

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

//алгоритм расшифровки DES
private Bit[] Decrypt64Bit(Bit[] block)
{
    InitialPermutation(block);

    Bit[] Left = block[0 - 31];
    Bit[] Right = block[31 - 63];

    for (int i = 1; i <= 16; i++)
    {
        Temp = Left;
        Left = Right;
        Right = (Temp)Xor(F(Right, Keys[16 - i]));
    }
    block[0 - 31] = Right;
    block[32 - 63] = Left;

    RevInitialPermutation(block);

    return block;
}

3.c Генерация ключей

Эта операция принимает 56-битный ключ и генерирует 16 разных 48-битных ключей. Сначала входной ключ переставляется в порядке таблицы PC1, что дает 56-битный блок. Потом этот новый блок делится на две части, давая два 28-битных блока, а именно C0 и D0. Затем следуют 16 циклов. В каждом цикле C(n+1) и D(n+1) переставляются путем применения сдвига влево к C(n) и D(n). Наконец, перестановка (Cn)(Dn) в порядке таблицы PC-2 дает nй ключ (C0 D0 в порядке таблицы PC-2 дает первый ключ, C1 D1 в порядке таблицы PC-2 дает второй ключ ... C15 D15 в порядке таблицы PC-2 дает пятнадцатый ключ). Количество сдвигов влево отличается в каждом цикле. C1 D1 получается из C0 D0 путем одного сдвига влево, C2 D2 получается из C1 D1 путем одного сдвига влево, C3 D3 получается из C2 D2 путем двух сдвигов влево, и так далее. Один сдвиг влево означает перемещение битов на одну позицию влево, поэтому после одного сдвига влево биты в 28 позициях являются битами, ранее бывшими в позициях 2, 3,..., 28, 1.

//количество сдвигов влево в каждом цикле
LeftShifts = new byte[16] { 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 };

Блок-схема генерации ключа DES


4. Улучшенный стандарт шифрования

AES (именуемый Рийндайл) тоже является блочным шифром, но не использует структуру Фейстеля. Размер блока AES равен 128 бит, но размер ключа различается - 128, 192 или 256 битов.

Блок-схема шифрования/расшифровки AES

4.a Шифрование

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

protected byte[] Encrypt128Bit(byte[] block)
{
    AddRoundKey(block, 0);
    //Nr=10,12 или 14 в зависимости от размера ключа
    for (int i = 1; i < Nr; i++)
    {
        SubBytes(block);
        ShiftRows(block);
        MixColumns(block);
        AddRoundKey(block, i);
    }
    SubBytes(block);
    ShiftRows(block);
    AddRoundKey(block, Nr);
    return block;
}

4.a.1 SubBytes()

Этот метод заменяет каждый байт блока в порядке Sbox. Он обеспечивает обратимое преобразование блоков во время шифрования, с обратной операцией во время расшифровки. Реализация метода очень простая, как показано:

private void SubBytes(byte[] block)
{
    for (int i = 0; i < 16; i++)
    {
        block[i] = Sbox[i];
    }
}

4.a.2 ShiftRows()

Операция ShiftRows выполняет круговые сдвиги влево рядов 1, 2 и 3 на 1, 2 и 3, как показано:

void ShiftRows(byte[] state)
{
    byte[] t = new byte[4];
    for (int r = 1; r < 4; r++)
    {
        for (int c = 0; c < 4; c++)
            t[c] = state[r * 4 + ((c + r) % 4)];
        for (int c = 0; c < 4; c++)
            state[r * 4 + c] = t[c];
    }
}

4.a.3 MixColumns()

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

Ниже приведен результат умножения столбца выше (точки обозначают умножение на конечном поле, плюсы в кругах обозначают операцию “Исключающее или“):

private void MixColumns(byte[] block)
{
    //temp=block
    byte[,] t = new byte[4, 4];
    for (int i = 0; i < 4; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            t[i, j] = block[i * 4 + j];
        }
    }
    for (int i = 0; i < 4; i++)
    {
        block[00 + i] = (byte)(M(2, t[0, i]) ^ M(3, t[1, i]) ^ t[2, i] ^ t[3, i]);
        block[04 + i] = (byte)(t[0, i] ^M(2, t[1, i])^ M(3, t[2, i]) ^  t[3, i] );
        block[08 + i] = (byte)(t[0, i] ^ t[1, i] ^ M(2, t[2, i]) ^ M(3, t[3, i]));
        block[12 + i] = (byte)(M(3, t[0, i]) ^ t[1, i] ^ t[2, i] ^ M(2, t[3, i]));
    }
}

4.a.4 AddRoundKey()

Эта операция применяет операцию “Исключающее или“ к каждому байту входного блока и текущей матрице весов (ключу).

private void AddRoundKey(byte[] block, int round)
{
    for (int i = 0; i < 16; i++)
    {
        block[i] = (byte)(block[i] ^ Keys[(round * 4 + i )]);
    }
}

4.b Расшифровка

Следующее инвертирует шаги алгоритма шифрования с некоторыми изменениями в методах, давая алгоритм расшифровки:

protected byte[] Decrypt128Bit(byte[] block)
{
    AddRoundKey(block, Nr);
    //Nr=10,12 или 14 в зависимости от размера ключа
    for (int i = Nr - 1; i > 0; i--)
    {
        InvShiftRows(block);
        InvSubBytes(block);
        AddRoundKey(block, i);
        InvMixColumns(block);
    }
    InvShiftRows(block);
    InvSubBytes(block);
    AddRoundKey(block, 0);
    return block;
}

У метода есть следующие отличия от операции шифрования:
•    Операция InvShiftRows выполняет круговые сдвиги вправо (влево в ShiftRows) рядов 1, 2 и 3 на 1, 2 и 3.
•    InvSubBytes отличается от SubBytes, так как использует InvSbox вместо Sbox.
•    InvMixColumns умножает каждый столбец на другую матрицу (есть в исходниках).

4.c Генерация ключей

Этот метод иначе называется расширением ключа. Он принимает матрицу в [4,Nk] (Nk = 4, 6 или 8) размерностях и расширяет ее до [4,4*(Nr+1)] (Nr = 10, 12 или 14) размерностей. Ниже приведен алгоритм этого метода, описанный в федеральном стандарте обработки информации-197:

KeyExpansion(byte key[4*Nk], word w[Nb*(Nr+1)], Nk)
begin
    word temp
    i = 0
    while (i < Nk)
       w[i] = word(key[4*i], key[4*i+1], key[4*i+2], key[4*i+3])
       i = i+1
    end while
    i = Nk
    while (i < Nb * (Nr+1)]
       temp = w[i-1]
       if (i mod Nk = 0)
          temp = SubWord(RotWord(temp)) xor Rcon[i/Nk]
       else if (Nk > 6 and i mod Nk = 4)
          temp = SubWord(temp)
       end if
       w[i] = w[i-Nk] xor temp
       i = i + 1
    end while
end