【问题标题】:c# BouncyCastle Exception: pad block corruptedc# BouncyCastle 异常:垫块损坏
【发布时间】:2016-04-08 15:32:28
【问题描述】:

我一直在使用@nerdybeardo 编写的here 发布的代码进行加密和解密。但是,我在尝试解密时收到错误“垫块损坏”。

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="https://stackoverflow.com/a/13511671/119624"/>
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 的特定客户端密码短语。这样做的原因是在端点之间传播密钥(一个在数据库中,一个在配置设置中)。 salt 被保存到数据库中,以便可以使用相同的 salt 进行解密。

谁能看到我做错了什么?为什么会出现垫块错误?

仅供参考:密码是XKCD variety“horse-battery-stapler-correct”样式,所以它们有连字符。不过我不确定这是否是一个红鲱鱼。

我也不确定是否需要每行唯一的盐,或者我是否可以对盐进行硬编码?是不是有点矫枉过正?

更新 对于发现这一点的任何人,错误只是我认为正在使用的密码不正确。结果是填充错误。

【问题讨论】:

    标签: c# encryption cryptography aes bouncycastle


    【解决方案1】:

    目前尚不清楚究竟是什么代码导致了您的问题(我的意思是没有最小的示例我可以运行并查看问题所在),但我构建了一个示例,它可以根据您的代码正确解密而没有错误,所以您可以查看它并可能发现您的错误。 我公开了EncryptionKeyManager.GetSharedPassphrase(),它返回固定字符串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;
        }
    }
    

    【讨论】:

    • 谢谢。我已经将它放入一个小型控制台应用程序中,它似乎工作正常,但是当我从数据库中获取实际数据时它没有。我开始怀疑问题是否出在将数据写入数据库然后读回时损坏。数据类型是当前的 'nvarchar(max)',其中 'varchar(max)' 对于 base64 编码的字符串可能就足够了(尽管我不确定为什么使用 'nvarchar' 会有所不同)。
    • 好吧,如果您要存储二进制数据 - 使用 varbinary 列,为什么要转换为\from base64 并存储为字符串?至于您的问题,首先检查您的加密是否正确。从数据库中获取精确的加密数据和盐并馈送到您的控制台应用程序。如果没问题 - 然后加密部分没问题 - 在解密过程中添加更多日志记录,以检查加密数据在哪里损坏。
    • 说实话,我不相信我必须使用的 ORM,但我可以试一试。我刚刚在本地运行了应用程序(SQL Server Express),并且在将 base64 字符串写入数据库之前和之后比较了它们。结果是相等的。是否有任何与数据库排序相关的内容可能导致字符串损坏?
    • 谁知道呢,这就是为什么我建议对二进制数据使用二进制列...但是,这个 base64 字符串是否在测试控制台应用程序中正确解密?
    • 好的,我找到了错误。问题是我期待的密码不正确。结果是填充错误。我有点红鲱鱼。您的测试代码有帮助,所以我会将您的答案标记为正确的。
    【解决方案2】:

    您遇到填充错误有点令人不安,这意味着在解密之前已验证或未检查 mac。我猜因为你使用了两个不同的密码,如果你的 mac 密码是正确的并且你的加密是关闭的,这可能是有意义的。

    填充错误泄漏的担忧是针对选择的密文攻击,因此只需扫描您借用的过于复杂的类,看起来应该检查 mac,但是,如果您输入了不正确的 mac,我至少会再次检查密码短语,它不会给你一个填充错误,因为如果它这样做意味着你正在使用的那个类有问题。

    如果您使用密码短语,每行的盐分是最低要求,而不是矫枉过正。正如您在my example. 中看到的那样,我更喜欢每次加密的盐

    您提到的另一件事是通过将一个放在数据库中和一个放在配置中来传播密钥。如果这是您的目标,那么将加密密钥的存储与 mac 密钥分开不是一个好方法,但我对您的目标有点不清楚。如果您的应用程序在数据库中被 sql 注入入侵,可以说,只需要加密密钥来解密,mac 就可以验证密文没有被篡改。如果您只是想进一步扩展密钥存储,最好使用存储在数据库中的解密密钥加密您的密文,这些解密密钥是使用存储在配置中的密钥解密的。

    【讨论】:

      猜你喜欢
      • 2015-08-04
      • 1970-01-01
      • 2013-11-26
      • 2012-08-11
      • 1970-01-01
      • 2013-07-07
      • 2010-10-26
      • 1970-01-01
      相关资源
      最近更新 更多