c# BouncyCastle 异常:填充块已损坏

c# BouncyCastle Exception: pad block corrupted

我一直在使用发布的代码 here 来加密和解密,由@nerdybeardo 编写。但是,我在尝试解密时遇到错误 "pad block corrupted"。

Encryptor class 看起来像这样,它实现了加密然后 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=""/>
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 应用程序的。我的基本想法是有一个密码短语,它是用于 encrypt/decrypt 的 SAAS 应用程序的核心,但也有一个用于 MAC 的特定客户端密码短语。这样做的原因是在端点之间传播密钥(一个在数据库中,一个在配置设置中)。盐被保存到数据库中,以便可以使用相同的盐进行解密。

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

仅供参考:密码短语是 XKCD variety "horse-battery-stapler-correct" 风格,所以里面有连字符。不过我不确定这是否是一个转移话题。

我也不确定是否需要每行唯一的盐,或者我是否可以对盐进行硬编码?是不是太过分了?

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

不清楚究竟是什么代码导致了你的问题(我的意思是没有最小的例子我可以 运行 看看有什么问题),但我构建了一个正确解密的例子,基于在您的代码上,这样您就可以查看它并可能发现您的错误。 我制作了 EncryptionKeyManager.GetSharedPassphrase() public,它 returns 固定了字符串 horse-battery-stapler-correct。我制作了 EncryptionKeyManager.GetClientPassphrase() 也 public 并且它 returns 修复了 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;
    }
}

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

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

如果您使用的是密码短语,则每行的盐分是最低的,并且不过分。正如您在 my example.

中看到的那样,我更喜欢按加密加盐

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