Шифрование с проверкой подлинности вручную в .NET Core

Мне было поручено создать способ шифрования произвольного текста для приложения .NET Core со следующими требованиями:

  • Сообщения будут иметь размер от 10 байт до 1 кБ.
  • Босс не хочет, чтобы я использовал AES-GCM (что, как я знаю, было бы разумным вариантом, поскольку он уже включен в .NET Core, и я уже читал, что внедрение собственного шифрования — плохая идея) и хочет, чтобы я придумал со специальной реализацией.
  • Эта реализация не будет использоваться слишком часто, обычно только при запуске приложения.

После некоторого чтения я пришел к следующему:

Шифрование:

  1. Преобразовать сообщение в байты UTF-8 (plaintext)
  2. Создать случайный salt.
  3. С использованием saltполучить достаточно материала с PBKDF2 для aes_key и hmac_key.
  4. Создать случайный iv.
  5. Шифрование AES-CBC plaintext с aes_key и iv (ciphertext).
  6. Вычислить HMAC-SHA-256 hash из ciphertext||iv.
  7. Возврат salt||hash||iv||ciphertext (payload).

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

  1. Извлекать salt, hash, iv, ciphertext от payload.
  2. С использованием saltполучить достаточно материала с PBKDF2 для aes_key и hmac_key.
  3. Вычислить HMAC-SHA-256 computed_hash из ciphertext||iv.
  4. Если computed_hash и hash отличаются, отвергнуть payload.
  5. Расшифровка AES-CBC ciphertext с aes_key и iv (plaintext).
  6. Перерабатывать plaintext в строку UTF-8 (message).
  7. Возврат message.

Реализация выглядит следующим образом:

public class AuthenticatedEncryptionHelper
{
    private const int _SALT_LENGTH = 16;
    private const int _KEY_DERIVATION_ITERATIONS = 100000;
    private const int _CRYPTO_KEY_LENGTH = 16;
    private const int _HMAC_KEY_LENGTH = 16;
    private const int _IV_LENGTH = 16;

    public byte[] Encrypt(string message, string password)
    {
        var salt = GenerateRandomBytes(_SALT_LENGTH);
        var (enc_key, hmac_key) = DeriveKeys(password, salt);
        var iv = GenerateRandomBytes(_IV_LENGTH);
        var plaintext = Encoding.UTF8.GetBytes(message);
        var ciphertext = AesEncrypt(plaintext, enc_key, iv);
        var hmac = ComputeHmac(ciphertext, iv, hmac_key);
        var payload = GeneratePayload(salt, hmac, iv, ciphertext);
        return payload;
    }

    public string Decrypt(byte[] payload, string password)
    {
        var (salt, hmac, iv, ciphertext) = ExplodePayload(payload);
        var (dec_key, hmac_key) = DeriveKeys(password, salt);
        var computed_hmac = ComputeHmac(ciphertext, iv, hmac_key);
        if (!AreHmacsEqual(hmac, computed_hmac))
        {
            throw new Exception();
        }
        var plaintext = AesDecrypt(ciphertext, dec_key, iv);
        var message = Encoding.UTF8.GetString(plaintext);
        return message;
    }

    private byte[] GenerateRandomBytes(int length)
    {
        var result = new byte[length];
        using (var r = RandomNumberGenerator.Create())
        {
            r.GetBytes(result);
        }
        return result;
    }

    private (byte[], byte[]) DeriveKeys(string password, byte[] salt)
    {
        var length = _CRYPTO_KEY_LENGTH + _HMAC_KEY_LENGTH;
        var result = new byte[length];
        using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, _KEY_DERIVATION_ITERATIONS))
        {
            result = pbkdf2.GetBytes(length);
        }
        var r1 = new byte[_CRYPTO_KEY_LENGTH];
        var r2 = new byte[_HMAC_KEY_LENGTH];
        Buffer.BlockCopy(result, 0, r1, 0, _CRYPTO_KEY_LENGTH);
        Buffer.BlockCopy(result, _CRYPTO_KEY_LENGTH, r2, 0, _HMAC_KEY_LENGTH);
        return (r1, r2);
    }

    private byte[] AesEncrypt(byte[] plaintext, byte[] key, byte[] iv)
    {
        byte[] ciphertext;
        using (var aes = Aes.Create())
        {
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;
            aes.Key = key;
            aes.IV = iv;
            using (var encryptor = aes.CreateEncryptor())
            {
                ciphertext = encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length);
            }
        }
        return ciphertext;
    }

    private byte[] ComputeHmac(byte[] ciphertext, byte[] iv, byte[] key)
    {
        var input = new byte[iv.Length + ciphertext.Length];
        Buffer.BlockCopy(iv, 0, input, 0, iv.Length);
        Buffer.BlockCopy(ciphertext, 0, input, iv.Length, ciphertext.Length);
        byte[] result;
        using (var hash = new HMACSHA256(key))
        {
            result = hash.ComputeHash(input);
        }
        return result;
    }

    private byte[] GeneratePayload(byte[] salt, byte[] hmac, byte[] iv, byte[] ciphertext)
    {
        var result = new byte[salt.Length + hmac.Length + iv.Length + ciphertext.Length];
        var curr_index = 0;
        Buffer.BlockCopy(salt, 0, result, curr_index, salt.Length);
        curr_index += salt.Length;
        Buffer.BlockCopy(hmac, 0, result, curr_index, hmac.Length);
        curr_index += hmac.Length;
        Buffer.BlockCopy(iv, 0, result, curr_index, iv.Length);
        curr_index += iv.Length;
        Buffer.BlockCopy(ciphertext, 0, result, curr_index, ciphertext.Length);
        return result;
    }

    private (byte[], byte[], byte[], byte[]) ExplodePayload(byte[] payload)
    {
        var salt = new byte[_SALT_LENGTH];
        var hmac = new byte[32];
        var iv = new byte[_IV_LENGTH];
        var ciphertext = new byte[payload.Length - (_SALT_LENGTH + 32 + _IV_LENGTH)];
        var curr_index = 0;
        Buffer.BlockCopy(payload, curr_index, salt, 0, _SALT_LENGTH);
        curr_index += _SALT_LENGTH;
        Buffer.BlockCopy(payload, curr_index, hmac, 0, 32);
        curr_index += 32;
        Buffer.BlockCopy(payload, curr_index, iv, 0, _IV_LENGTH);
        curr_index += _IV_LENGTH;
        Buffer.BlockCopy(payload, curr_index, ciphertext, 0, ciphertext.Length);
        return (salt, hmac, iv, ciphertext);
    }

    private bool AreHmacsEqual(byte[] hmac, byte[] computedHmac)
    {
        var result = true;
        if (hmac.Length != computedHmac.Length)
        {
            result = false;
        }
        for (int i = 0; i < hmac.Length; i++)
        {
            if (hmac[i] != computedHmac[i])
            {
                result = false;
            }
        }
        return result;
    }

    private byte[] AesDecrypt(byte[] ciphertext, byte[] key, byte[] iv)
    {
        byte[] plaintext;
        using (var aes = Aes.Create())
        {
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;
            aes.Key = key;
            aes.IV = iv;
            using (var decryptor = aes.CreateDecryptor())
            {
                plaintext = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length);
            }
        }
        return plaintext;
    }
}

Любая критика этого кода (или алгоритма) будет принята с благодарностью.

1 ответ
1

Криптография

Да, CBC, за которым следует HMAC, — это нормально (также известный как encrypt-then-MAC), и вы включаете IV в расчеты, и это здорово. Вам действительно не нужно включать соль и количество итераций в расчет HMAC, поскольку они уже влияют на используемый ключ. Возможно, вам придется пересмотреть этот выбор, если вы переключитесь на GCM — включение большего количества проверенных параметров никогда не повредит.


Вы используете криптографически безопасный генератор случайных чисел, так что спасибо.


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


Вы используете ужасное имя Rfc2898DeriveBytes класс, который реализует PBKDF2. Это не первоклассный алгоритм (см., например, Argon2), но его может быть достаточно. Это предполагает, что вам в первую очередь требуется пароль; если вы можете выполнить шифрование, не требуя паролей, то это всегда лучше.


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


16 байт защищенной случайной соли точно на высоте.


Одна некрасивая проблема с PBKDF2/ Rfc2898DeriveBytes заключается в том, что он снова выполняет все итерации для каждого требуемого размера хэша, стандартизируя SHA-1/20 байт. Вам в этом случае лучше указать SHA-256 или SHA-512, иначе вам придется выполнять больше итераций, чем возможному злоумышленнику (которому достаточно 1 ключа).


HMAC-SHA-256 по умолчанию использует 256-битный ключ. Использование 16 байт/128 бит недопустимо. плохой в любом смысле, но есть и это (именно поэтому указание SHA-512 для PBKDF2 может быть полезным).


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

Код

Посмотреть вопрос/ответ на дочернем сайте StackOverflow о соглашениях об именах, особенно в отношении постоянных значений: они не должны начинаться с подчеркивания.


Блочная копия после функции PBKDF2/ не нужна:

Rfc2898DeriveBytes Класс реализует функциональность PBKDF2 с помощью генератора псевдослучайных чисел на основе HMACSHA1. Класс Rfc2898DeriveBytes принимает пароль, соль и количество итераций, а затем генерирует ключи с помощью вызовов метода GetBytes метод. Повторные вызовы этого метода не будут генерировать один и тот же ключ; вместо этого добавление двух вызовов GetBytes метод с cb значение параметра 20 эквивалентно вызову GetBytes метод один раз с cb значение параметра 40.


Более поздние версии C# позволяют использовать символы подчеркивания в литералах. 100_000 является более читаемой версией 100000.


aes.Padding = PaddingMode.PKCS7;

Хотя это значение по умолчанию, я все равно аплодирую вам за его установку; не все читатели могут знать это значение по умолчанию — эксперты по криптографии не всегда являются также специалистами по языку/среде выполнения.

Дизайн

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


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


    curr_index += 32;

Используйте здесь константу, например HMACSHA256_OUTPUT_SIZE = 32 или у вас будут проблемы, если кто-то решит использовать другой хэш.

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *