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 次的迭代次数通常太小了。
我目前正在开展一个项目,我需要在 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 次的迭代次数通常太小了。