С# BouncyCastle Исключение: блок блокировки поврежден

Я использовал код, который был опубликован здесь для шифрования и расшифровки, написанный @nerdybeardo. Однако при попытке дешифрования я получаю сообщение об ошибке "pad block corrupted".

Класс Encryptor выглядит так, который реализует шифрование, затем MAC:

/// <summary>
/// Encrypt/decrypt + HMAC using BouncyCastle (C# Java port)
/// </summary>
/// <typeparam name="TBlockCipher">The type of the block cipher.</typeparam>
/// <typeparam name="TDigest">The type of the digest.</typeparam>
/// <see cref="/info/13744/encrypt-and-decrypt-a-string/99267#99267"/>
public sealed class Encryptor<TBlockCipher, TDigest>
    where TBlockCipher : IBlockCipher, new()
    where TDigest : IDigest, new()
{
    private readonly Encoding encoding;

    private readonly byte[] key;

    private IBlockCipher blockCipher;

    private BufferedBlockCipher cipher;

    private HMac mac;

    /// <summary>
    /// Initializes a new instance of the <see cref="Encryptor{TBlockCipher, TDigest}"/> class.
    /// </summary>
    /// <param name="encoding">The encoding.</param>
    /// <param name="key">The key.</param>
    /// <param name="macKey">The mac key.</param>
    public Encryptor(Encoding encoding, byte[] key, byte[] macKey)
    {
        this.encoding = encoding;
        this.key = key;
        this.Init(key, macKey, new Pkcs7Padding());
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="Encryptor{TBlockCipher, TDigest}"/> class.
    /// </summary>
    /// <param name="encoding">The encoding.</param>
    /// <param name="key">The key.</param>
    /// <param name="macKey">The mac key.</param>
    /// <param name="padding">The padding.</param>
    public Encryptor(Encoding encoding, byte[] key, byte[] macKey, IBlockCipherPadding padding)
    {
        this.encoding = encoding;
        this.key = key;
        this.Init(key, macKey, padding);
    }

    /// <summary>
    /// Encrypts the specified plain.
    /// </summary>
    /// <param name="plain">The plain.</param>
    /// <returns></returns>
    public string Encrypt(string plain)
    {
        return Convert.ToBase64String(EncryptBytes(plain));
    }

    /// <summary>
    /// Encrypts the bytes.
    /// </summary>
    /// <param name="plain">The plain.</param>
    /// <returns></returns>
    public byte[] EncryptBytes(string plain)
    {
        byte[] input = this.encoding.GetBytes(plain);

        var iv = this.GenerateInitializationVector();

        var cipher = this.BouncyCastleCrypto(true, input, new ParametersWithIV(new KeyParameter(key), iv));
        byte[] message = CombineArrays(iv, cipher);

        this.mac.Reset();
        this.mac.BlockUpdate(message, 0, message.Length);
        var digest = new byte[this.mac.GetUnderlyingDigest().GetDigestSize()];
        this.mac.DoFinal(digest, 0);

        var result = CombineArrays(digest, message);
        return result;
    }

    /// <summary>
    /// Decrypts the bytes.
    /// </summary>
    /// <param name="bytes">The bytes.</param>
    /// <returns></returns>
    /// <exception cref="CryptoException"></exception>
    public byte[] DecryptBytes(byte[] bytes)
    {
        // split the digest into component parts
        var digest = new byte[this.mac.GetUnderlyingDigest().GetDigestSize()];
        var message = new byte[bytes.Length - digest.Length];
        var iv = new byte[this.blockCipher.GetBlockSize()];
        var cipher = new byte[message.Length - iv.Length];

        Buffer.BlockCopy(bytes, 0, digest, 0, digest.Length);
        Buffer.BlockCopy(bytes, digest.Length, message, 0, message.Length);
        if (!IsValidHMac(digest, message))
        {
            throw new CryptoException();
        }

        Buffer.BlockCopy(message, 0, iv, 0, iv.Length);
        Buffer.BlockCopy(message, iv.Length, cipher, 0, cipher.Length);

        byte[] result = this.BouncyCastleCrypto(false, cipher, new ParametersWithIV(new KeyParameter(key), iv));
        return result;
    }

    /// <summary>
    /// Decrypts the specified bytes.
    /// </summary>
    /// <param name="bytes">The bytes.</param>
    /// <returns></returns>
    public string Decrypt(byte[] bytes)
    {
        return this.encoding.GetString(DecryptBytes(bytes));
    }

    /// <summary>
    /// Decrypts the specified cipher.
    /// </summary>
    /// <param name="cipher">The cipher.</param>
    /// <returns></returns>
    public string Decrypt(string cipher)
    {
        return this.Decrypt(Convert.FromBase64String(cipher));
    }

    /// <summary>
    /// Combines the arrays.
    /// </summary>
    /// <param name="source1">The source1.</param>
    /// <param name="source2">The source2.</param>
    /// <returns></returns>
    private static byte[] CombineArrays(byte[] source1, byte[] source2)
    {
        var result = new byte[source1.Length + source2.Length];
        Buffer.BlockCopy(source1, 0, result, 0, source1.Length);
        Buffer.BlockCopy(source2, 0, result, source1.Length, source2.Length);

        return result;
    }

    /// <summary>
    /// Ares the equal.
    /// </summary>
    /// <param name="digest">The digest.</param>
    /// <param name="computed">The computed.</param>
    /// <returns></returns>
    private static bool AreEqual(byte[] digest, byte[] computed)
    {
        if (digest.Length != computed.Length)
        {
            return false;
        }

        var result = 0;
        for (var i = 0; i < digest.Length; i++)
        {
            result |= digest[i] ^ computed[i];
        }

        return result == 0;
    }

    /// <summary>
    /// Initializes the specified key.
    /// </summary>
    /// <param name="key">The key.</param>
    /// <param name="macKey">The mac key.</param>
    /// <param name="padding">The padding.</param>
    private void Init(byte[] key, byte[] macKey, IBlockCipherPadding padding)
    {
        this.blockCipher = new CbcBlockCipher(new TBlockCipher());
        this.cipher = new PaddedBufferedBlockCipher(this.blockCipher, padding);
        this.mac = new HMac(new TDigest());
        this.mac.Init(new KeyParameter(macKey));
    }

    /// <summary>
    /// Determines whether [is valid h mac] [the specified digest].
    /// </summary>
    /// <param name="digest">The digest.</param>
    /// <param name="message">The message.</param>
    /// <returns></returns>
    private bool IsValidHMac(byte[] digest, byte[] message)
    {
        this.mac.Reset();
        this.mac.BlockUpdate(message, 0, message.Length);
        var computed = new byte[this.mac.GetUnderlyingDigest().GetDigestSize()];
        this.mac.DoFinal(computed, 0);

        return AreEqual(digest, computed);
    }

    /// <summary>
    /// Bouncy Castle Cryptography.
    /// </summary>
    /// <param name="forEncrypt">if set to <c>true</c> [for encrypt].</param>
    /// <param name="input">The input.</param>
    /// <param name="parameters">The parameters.</param>
    /// <returns></returns>
    private byte[] BouncyCastleCrypto(bool forEncrypt, byte[] input, ICipherParameters parameters)
    {
        try
        {
            cipher.Init(forEncrypt, parameters);

            return this.cipher.DoFinal(input);
        }
        catch (CryptoException)
        {
            throw;
        }
    }

    /// <summary>
    /// Generates the initialization vector.
    /// </summary>
    /// <returns></returns>
    private byte[] GenerateInitializationVector()
    {
        using (var provider = new RNGCryptoServiceProvider())
        {
            // 1st block
            var result = new byte[this.blockCipher.GetBlockSize()];
            provider.GetBytes(result);

            return result;
        }
    }
}

У меня есть простая оболочка для механизма AES. Это выглядит так:

public class AesSha256Encryptor
{
    private readonly Encryptor<AesEngine, Sha256Digest> provider;

    /// <summary>
    /// Initializes a new instance of the <see cref="AesSha256Encryptor"/> class.
    /// </summary>
    /// <param name="key">The key.</param>
    /// <param name="hmacKey">The HMAC key.</param>
    public AesSha256Encryptor(byte[] key, byte[] hmacKey)
    {
        provider = new Encryptor<AesEngine, Sha256Digest>(Encoding.UTF8, key, hmacKey);
    }

    /// <summary>
    /// Encrypts the specified plain.
    /// </summary>
    /// <param name="plain">The plain.</param>
    /// <returns></returns>
    public string Encrypt(string plain)
    {
        return provider.Encrypt(plain);
    }

    /// <summary>
    /// Decrypts the specified cipher.
    /// </summary>
    /// <param name="cipher">The cipher.</param>
    /// <returns></returns>
    public string Decrypt(string cipher)
    {
        return provider.Decrypt(cipher);
    }
}

Я хотел иметь возможность иметь другую соль в строке базы данных, поэтому у меня есть менеджер ключей, который работает следующим образом:

public static class EncryptionKeyManager
{
    /// <summary>
    /// The salt length limit
    /// </summary>
    private const int SaltLengthLimit = 32;

    /// <summary>
    /// Gets the key record.
    /// </summary>
    /// <returns></returns>
    public static KeyRecord GetKeyRecord()
    {
        // get the shared passphrasefrom appsettings
        var sharedPassphrase = GetSharedPassphrase();

        // get the client passphrase from config db to sign
        var clientPassphrase = GetClientPassphrase();

        // generate secure random salt
        var salt = GetSalt();

        // get both the encryption key and hmac key
        // these will be used for Encrypt-then-Mac
        var key = GetKeyFromPassphrase(sharedPassphrase, salt);
        var hmacKey = GetKeyFromPassphrase(clientPassphrase, salt);

        return new KeyRecord
        {
            SharedKey = key,
            HmacKey = hmacKey,
            Salt = salt
        };
    }

    /// <summary>
    /// Gets the client salt.
    /// </summary>
    /// <returns></returns>
    private static string GetClientPassphrase()
    {
        var settingsService = ServiceLocator.Current.GetInstance<ISettingService>();
        return settingsService.GetSetting(ConstantConfigSettings.EncryptionSettings.ClientPassphrase, defaultValue: "<removed>");
    }

    /// <summary>
    /// Gets the shared passphrase.
    /// </summary>
    /// <returns></returns>
    private static string GetSharedPassphrase()
    {
        return ConfigurationManager.AppSettings[ConstantConfigSettings.EncryptionSettings.SharedPassphrase] ?? "<removed>";
    }

    /// <summary>
    /// Gets the key from passphrase.
    /// </summary>
    /// <param name="passphrase">The passphrase.</param>
    /// <param name="salt">The salt.</param>
    /// <returns></returns>
    private static byte[] GetKeyFromPassphrase(string passphrase, string salt)
    {
        var saltArray = Encoding.UTF8.GetBytes(salt);
        var rfcKey = new Rfc2898DeriveBytes(passphrase, saltArray, 10000);

        return rfcKey.GetBytes(32); // for a 256-bit key (32*8=128)
    }

    /// <summary>
    /// Gets the salt from a secure random generator..
    /// </summary>
    /// <param name="maximumSaltLength">Maximum length of the salt.</param>
    /// <returns></returns>
    private static string GetSalt(int maximumSaltLength = SaltLengthLimit)
    {
        var salt = new byte[maximumSaltLength];
        using (var random = new RNGCryptoServiceProvider())
        {
            random.GetNonZeroBytes(salt);
        }

        return Convert.ToBase64String(salt);
    }
}

Все это используется для шифрования:

// get key and salt from 
var keyRecord = EncryptionKeyManager.GetKeyRecord();
var aesSha256Encryptor = new AesSha256Encryptor(keyRecord.SharedKey, keyRecord.HmacKey);

// now encrypt and store, include salt
entity.AccountNumber = aesSha256Encryptor.Encrypt(accountNumber);
entity.SortCode = aesSha256Encryptor.Encrypt(sortCode);
entity.Salt = keyRecord.Salt;

Когда я хочу расшифровать, я делаю следующее:

public static class KeyManager
{
    /// <summary>
    /// Gets the key from passphrase.
    /// </summary>
    /// <param name="passphrase">The passphrase.</param>
    /// <param name="salt">The salt.</param>
    /// <returns>A byte array.</returns>
    public static byte[] GetKeyFromPassphrase(string passphrase, string salt)
    {
        var saltArray = Encoding.UTF8.GetBytes(salt);
        var rfcKey = new Rfc2898DeriveBytes(passphrase, saltArray, 10000);

        return rfcKey.GetBytes(32); // for a 256-bit key (32*8=128)
    }
}

var passphraseKey = KeyManager.GetKeyFromPassphrase(this.Passphrase, this.Salt);
var hmacKey = KeyManager.GetKeyFromPassphrase(this.ClientPassphrase, this.Salt);

var aesSha256Encryptor = new AesSha256Encryptor(passphraseKey, hmacKey);
var plaintext = aesSha256Encryptor.Decrypt(this.CipherText);

Это для приложения SAAS. Моя основная идея заключалась в том, чтобы иметь кодовую фразу, которая является основой приложения SAAS, которое используется для шифрования/дешифрования, но также имеет конкретную клиентскую кодовую фразу, которая используется для MAC. Причиной этого было распространение ключей между конечными точками (один в базе данных и один в настройке конфигурации). Соль сохраняется в базе данных, поэтому ее можно использовать для дешифрования с использованием той же соли.

Может ли кто-нибудь увидеть, что я делаю неправильно? Почему я получаю ошибку блока блока?

FYI: кодовые фразы относятся к XKCD-разновидности, то есть стиль "лошадиный аккумулятор-степлер-правильный", поэтому у них есть дефис. я Не уверен, что это красная селедка.

Я также не уверен, нужна ли уникальная соль для каждой строки или я могу просто написать код соли? Это перебор?

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

Ответ 1

Непонятно, какой код точно вызывает вашу проблему (я имею в виду, что нет минимального примера, который я мог бы просто запустить, и посмотреть, что не так), но я построил пример, который правильно расшифровывается без ошибок на основе вашего кода, поэтому вы может посмотреть на него и, вероятно, обнаружить вашу ошибку. Я сделал EncryptionKeyManager.GetSharedPassphrase() public, и он возвращает фиксированную строку horse-battery-stapler-correct. Я сделал EncryptionKeyManager.GetClientPassphrase() также общедоступным, и он возвращает фиксированный horse-battery.

class Program {
    static void Main(string[] args) {
        // get key and salt from 
        var keyRecord = EncryptionKeyManager.GetKeyRecord();
        var aesSha256Encryptor = new AesSha256Encryptor(keyRecord.SharedKey, keyRecord.HmacKey);
        string targetData = "4343423343";

        var encrypted =  aesSha256Encryptor.Encrypt(targetData);
        var salt = keyRecord.Salt;
        var decrypted = Decrypt(encrypted, salt);
        Debug.Assert(targetData == decrypted);
        Console.WriteLine(decrypted);
        Console.ReadKey();

    }

    private static string Decrypt(string data, string salt) {
        var passphraseKey = KeyManager.GetKeyFromPassphrase(EncryptionKeyManager.GetSharedPassphrase(), salt);            
        var hmacKey = KeyManager.GetKeyFromPassphrase(EncryptionKeyManager.GetClientPassphrase(), salt);
        var aesSha256Encryptor = new AesSha256Encryptor(passphraseKey, hmacKey);
        var plaintext = aesSha256Encryptor.Decrypt(data);
        return plaintext;
    }
}

Ответ 2

Это немного обескураживает, что вы получаете ошибку заполнения, это означает, что Mac проверен или не был проверен перед расшифровкой. Я предполагаю, что, поскольку вы используете две разные фразы, если вы правильно указали пароль Mac, и ваше шифрование отключено, это может иметь смысл.

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

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

Другая вещь, о которой вы говорили, - это распространение ключей, поместив их в базу данных и одну в конфиге. Разделение хранилища ключа шифрования с помощью ключа mac не является хорошим способом сделать это, если это ваша цель, но я немного неясен для вашей цели. Если ваше приложение было скомпрометировано в базе данных с помощью SQL-инъекции, можно сказать, что для дешифрования требуется только ключ шифрования, Mac должен просто подтвердить, что шифрованный текст не был подделан. Гораздо лучше зашифровать ваш зашифрованный текст с расшифрованными ключами, хранящимися в базе данных, которые были расшифрованы с ключами, хранящимися в конфиге, если вы просто просто хотите расширить хранилище ключей немного больше.