C#与Ruby之间的AES加解密

AES encryption and decryption between C# and Ruby

我目前正在开展一个项目,我需要在 C# 和 Ruby 之间移植 AES 加密并提供向后兼容性。虽然它们都可以独立工作,但我在 C# 中加密数据并在 Ruby 中解密时遇到了问题。

虽然我直觉 ruby 中数据转换为字符串的方式可能存在问题,但我不确定这一点,因为我不是该领域的专家 (安全)。

关于在 C# 中解密加密文本的 ruby 代码中需要更正的内容的任何指导都会有所帮助。

下面是我的 C# 代码。

public class Encryption
{
    private const string SECRET = "readasecret";
    static byte[] KEY = new byte[] { 222, 11, 149, 155, 122, 97, 170, 8, 40, 250, 67, 227, 129, 147, 159, 81, 108, 136, 221, 41, 247, 146, 114, 133, 232, 31, 33, 196, 130, 88, 136, 238 };
    private static readonly byte[] Salt = Encoding.ASCII.GetBytes("o6MKe324346722kbM7c5");

    public static string Encrypt(string nonCrypted)
    {
        return EncryptStringAES(nonCrypted ?? string.Empty, SECRET);
    }

    public static string Decrypt(string encrypted)
    {
        return DecryptStringAES(encrypted, SECRET);
    }

    private static string EncryptStringAES(string plainText, string sharedSecret)
    {
        //if (string.IsNullOrEmpty(plainText))
        //   throw new ArgumentNullException("plainText");
        if (string.IsNullOrEmpty(sharedSecret))
            throw new ArgumentNullException("sharedSecret");

        string outStr;                  // Encrypted string to return
        RijndaelManaged aesAlg = null;  // RijndaelManaged object used to encrypt the data.

        try
        {
            // generate the key from the shared SECRET and the salt
            var key = new Rfc2898DeriveBytes(sharedSecret, Salt);

            // Create a RijndaelManaged object
            aesAlg = new RijndaelManaged();
            aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);

            // Create a decryptor to perform the stream transform.
            ICryptoTransform encryptor = aesAlg.CreateEncryptor(KEY, aesAlg.IV);
            // Create the streams used for encryption.
            using (var msEncrypt = new MemoryStream())
            {
                // prepend the IV
                msEncrypt.Write(BitConverter.GetBytes(aesAlg.IV.Length), 0, sizeof(int));
                msEncrypt.Write(aesAlg.IV, 0, aesAlg.IV.Length);
                using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                {
                    using (var swEncrypt = new StreamWriter(csEncrypt))
                    {
                        //Write all data to the stream.
                        swEncrypt.Write(plainText);
                    }
                }
                outStr = Convert.ToBase64String(msEncrypt.ToArray());
            }
        }
        finally
        {
            // Clear the RijndaelManaged object.
            if (aesAlg != null)
                aesAlg.Clear();
        }

        // Return the encrypted bytes from the memory stream.
        return outStr;
    }

    private static string DecryptStringAES(string cipherText, string sharedSecret)
    {
        if (string.IsNullOrEmpty(cipherText))
            throw new ArgumentNullException("cipherText");
        if (string.IsNullOrEmpty(sharedSecret))
            throw new ArgumentNullException("sharedSecret");

        RijndaelManaged aesAlg = null;

        // Declare the string used to hold
        // the decrypted text.
        string plaintext;

        try
        {
            // generate the key from the shared SECRET and the salt
            var key = new Rfc2898DeriveBytes(sharedSecret, Salt);

            // Create the streams used for decryption.                
            byte[] bytes = Convert.FromBase64String(cipherText);
            using (var msDecrypt = new MemoryStream(bytes))
            {
                aesAlg = new RijndaelManaged();
                aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
                // Get the initialization vector from the encrypted stream
                aesAlg.IV = ReadByteArray(msDecrypt);
                var decryptor = aesAlg.CreateDecryptor(KEY, aesAlg.IV);
                using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                {
                    using (var srDecrypt = new StreamReader(csDecrypt))
                        plaintext = srDecrypt.ReadToEnd();
                }
            }
        }
        catch (Exception e)
        {
            return string.Empty;
        }
        finally
        {
            // Clear the RijndaelManaged object.
            if (aesAlg != null)
               aesAlg.Clear();
        }

        return plaintext;
    }

    private static byte[] ReadByteArray(Stream s)
    {
        var rawLength = new byte[sizeof(int)];
        if (s.Read(rawLength, 0, rawLength.Length) != rawLength.Length)
            throw new SystemException("Stream did not contain properly formatted byte array");

        var buffer = new byte[BitConverter.ToInt32(rawLength, 0)];
        if (s.Read(buffer, 0, buffer.Length) != buffer.Length)
            throw new SystemException("Did not read byte array properly");
        return buffer;
    }
}

等效的 Ruby 代码如下。

require 'pbkdf2'
require "openssl"
require "base64"
require "encrypted"
require "securerandom"

secret = "readasecret"
salt = "o6MKe324346722kbM7c5"
encrypt_this = "fiskbullsmacka med extra sovs"

rfc_db = PBKDF2.new(password: secret, salt: salt, iterations: 1000, key_length: 32, hash_function: :sha1).bin_string
key = rfc_db.bytes[0, 32]
puts key.inspect
cipherkey = key.pack('c*')


# ----------------- ENCRYPTION -------------------------
cipher = Encrypted::Ciph.new("256-128")
cipher.key = cipherkey
cipher.iv = cipher.generate_iv


encrypted_text = cipher.encrypt(encrypt_this)
# Convert string to byte[]
unpackENCString = encrypted_text.unpack("c*")
# Combine IV and data
combEncrypt = cipher.iv.unpack("c*").concat(encrypted_text.unpack("c*"))

# Convert byte[] to string
passingString = combEncrypt.pack("c*")

enc = Base64.encode64(passingString)

puts "Encrypted text :"+enc


# ----------------- DECRYPTION -------------------------

plain = Base64.decode64(enc)

passingbyteArray = plain.unpack("c*")

rfc_db = PBKDF2.new(password: secret, salt: salt, iterations: 1000, key_length: 32, hash_function: :sha1).bin_string
key = rfc_db.bytes[0, 32]
decipherkey = key.pack('c*')

decrypt_this = passingbyteArray[16,passingbyteArray.length() - 16].pack("c*")               #from above
decipher = Encrypted::Ciph.new("256-128")
cipher.key = decipherkey     #key used above to encrypt
cipher.iv = passingbyteArray[0,16].pack("c*")      #initialization vector used above
decrypted_text = cipher.decrypt(decrypt_this)

puts "Decrypted text: "+decrypted_text

在发布的 C# 代码中,实现了通过 PBKDF2 的密钥派生,但未使用。相反,应用了硬编码密钥 KEY。这可能是出于测试目的而完成的。

下面考虑的不是硬编码密钥,而是派生密钥。为此,在 aesAlg.CreateEncryptor()aesAlg.CreateDecryptor() 中的 C# 代码中必须传递 aesAlg.Key 而不是 KEY,派生密钥之前已分配给它。

C#代码在加密后将IV的大小(到4字节)、IV和密文依次拼接起来。解密时会发生相应的分离。
请注意,实际上没有必要存储 IV 的大小,因为它是已知的:IV 的大小等于块大小,因此对于 AES 是 16 字节。
在下文中,为简单起见,保留 IV 大小的串联。

在Ruby代码中,使用了各种加密库,尽管openssl实际上已经足够了。因此,以下实现仅适用于 openssl

使用 PBKDF2 的密钥推导是:

require "openssl"
require "base64"

# Key derivation (PBKDF2)
secret = "readasecret"
salt = "o6MKe324346722kbM7c5"
key = OpenSSL::KDF.pbkdf2_hmac(secret, salt: salt, iterations: 1000, length: 32, hash: "sha1")

加密方式为:

# Encryption
plaintext = "fiskbullsmacka med extra sovs"
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
cipher.key = key
nonce = cipher.random_iv # random IV
cipher.iv = nonce
ciphertext = cipher.update(plaintext) + cipher.final
# Concatenation
sizeIvCiphertext = ['10000000'].pack('H*').concat(nonce.concat(ciphertext))
sizeIvCiphertextB64 =  Base64.encode64(sizeIvCiphertext)
puts sizeIvCiphertextB64 # e.g. EAAAAC40tnEeaRtwutravBiH8vpn4vtjk6s9CAq/XEbyGTGMPwxENInIoAqWlZvR413Aqg==

和解密:

# Separation
sizeIvCiphertext = Base64.decode64(sizeIvCiphertextB64)
size = sizeIvCiphertext[0, 4]
iv = sizeIvCiphertext [4, 16]
ciphertext = sizeIvCiphertext[4+16, sizeIvCiphertext.length-16]
# Decryption
decipher = OpenSSL::Cipher.new('AES-256-CBC')
decipher.decrypt
decipher.key = key
decipher.iv = iv
decrypted = decipher.update(ciphertext) + decipher.final
puts decrypted # fiskbullsmacka med extra sovs

这样生成的密文可以用C#代码解密。同样,C#代码的密文可以用上面的Ruby代码解密。


请记住,这两个代码都包含一个漏洞。这些代码使用 static salt 进行密钥派生,这是不安全的。相反,应该为每个密钥推导生成一个随机盐。就像 IV 一样,盐不是秘密的,通常与 IV 和密文一起传递,例如盐 |四 |密文.
另外,对于 PBKDF2,1000 次的迭代次数通常太小了。