在C#中加密和解密字符串

436

在C#中,最现代(最佳)的方法是什么来满足以下需求?

string encryptedString = SomeStaticClass.Encrypt(sourceString);

string decryptedString = SomeStaticClass.Decrypt(encryptedString);

但是最好不要涉及盐、密钥、使用 byte[] 等操作。

我一直在搜索,但是找到的结果让我感到困惑(您可以查看类似的 SO 问题列表来了解这是一个具有误导性的问题)。


24
加密货币并不简单。阅读http://blogs.msdn.com/b/ericlippert/archive/2011/09/27/keep-it-secret-keep-it-safe.aspx。 - SLaks
所有加密操作都基于字节数组。请使用 Encoding.UTF8 - SLaks
9
如果回答这个问题的时候只是简单地建议某些加密算法而没有讨论身份、密钥管理、完整性等内容,那么这样的回答完全没有价值。 - dtb
7
@dtb:我认为你夸大了这种情况,OP只需要一个简单的类,它可能需要输入密码、盐值和向量,并提供对称加密。显然他不太关心功能的目的。 - Tarik
@Tarik 不,他并没有夸大情况。密码学可以用来保护系统安全。但是如果不描述具体的系统,应用密码学就毫无意义(正如被接受的答案所述)。 - Maarten Bodewes
7个回答

924

更新于2015年12月23日:由于这个答案似乎得到了很多赞,因此我已经根据评论和反馈修复了愚蠢的错误并总体上改进了代码。请参见文章末尾的具体改进列表。

正如其他人所说,加密学并不简单,最好避免“自行开发”加密算法。

但是,您可以“自行开发”一个包装类,用于包装内置的RijndaelManaged加密类之类的东西。

Rijndael是当前高级加密标准的算法名称,因此您肯定在使用可以被认为是“最佳实践”的加密算法。

RijndaelManaged类确实通常需要您与字节数组、盐、密钥、初始化向量等打交道,但这正是可以在您的“包装器”类中抽象出的详细信息。

下面的类是我写的一段时间前执行您想要的事情的简单单个方法调用,允许使用基于字符串的密码加密基于字符串的纯文本,并且表示为字符串的结果也被加密。当然,有一个相应的方法使用相同的密码解密加密的字符串。

与此代码的第一版本不同,该版本将在每次生成随机盐和IV值时使用完全相同的盐和IV值。由于盐和IV必须在给定字符串的加密和解密之间相同,因此在加密时将盐和IV附加到密文中,并再次从中提取它们以执行解密。这样做的结果是,使用完全相同的密码加密完全相同的纯文本每次都会得到完全不同的密文结果。

使用这种方法的“强度”来自使用RijndaelManaged类为您执行加密,以及使用System.Security.Cryptography命名空间的Rfc2898DeriveBytes函数,该函数将使用基于您提供的基于字符串的密码的标准和安全算法(具体来说是PBKDF2)生成加密密钥。(请注意,这是第一版使用旧PBKDF1算法的改进。)

最后需要注意的是,这仍然是一个未经身份验证的加密。加密本身只提供隐私(即消息对第三方未知),而身份认证加密旨在同时提供隐私和真实性(即接收者知道消息是由发送者发送的)。

如果不知道您的确切需求,很难判断此处的代码是否足够安全,但它已经被设计为在相对简单实现与“质量”之间取得良好的平衡。例如,如果您加密字符串的“接收者”直接从可信“发送者”接收字符串,则可能甚至无需进行身份验证

如果您需要更复杂的、提供身份认证加密的东西,请查看此文章中的实现。

以下是代码:

using System;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using System.Linq;

namespace EncryptStringSample
{
    public static class StringCipher
    {
        // This constant is used to determine the keysize of the encryption algorithm in bits.
        // We divide this by 8 within the code below to get the equivalent number of bytes.
        private const int Keysize = 256;

        // This constant determines the number of iterations for the password bytes generation function.
        private const int DerivationIterations = 1000;

        public static string Encrypt(string plainText, string passPhrase)
        {
            // Salt and IV is randomly generated each time, but is preprended to encrypted cipher text
            // so that the same Salt and IV values can be used when decrypting.  
            var saltStringBytes = Generate256BitsOfRandomEntropy();
            var ivStringBytes = Generate256BitsOfRandomEntropy();
            var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
            using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
            {
                var keyBytes = password.GetBytes(Keysize / 8);
                using (var symmetricKey = new RijndaelManaged())
                {
                    symmetricKey.BlockSize = 256;
                    symmetricKey.Mode = CipherMode.CBC;
                    symmetricKey.Padding = PaddingMode.PKCS7;
                    using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes))
                    {
                        using (var memoryStream = new MemoryStream())
                        {
                            using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                            {
                                cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
                                cryptoStream.FlushFinalBlock();
                                // Create the final bytes as a concatenation of the random salt bytes, the random iv bytes and the cipher bytes.
                                var cipherTextBytes = saltStringBytes;
                                cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray();
                                cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray();
                                memoryStream.Close();
                                cryptoStream.Close();
                                return Convert.ToBase64String(cipherTextBytes);
                            }
                        }
                    }
                }
            }
        }

        public static string Decrypt(string cipherText, string passPhrase)
        {
            // Get the complete stream of bytes that represent:
            // [32 bytes of Salt] + [32 bytes of IV] + [n bytes of CipherText]
            var cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText);
            // Get the saltbytes by extracting the first 32 bytes from the supplied cipherText bytes.
            var saltStringBytes = cipherTextBytesWithSaltAndIv.Take(Keysize / 8).ToArray();
            // Get the IV bytes by extracting the next 32 bytes from the supplied cipherText bytes.
            var ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(Keysize / 8).Take(Keysize / 8).ToArray();
            // Get the actual cipher text bytes by removing the first 64 bytes from the cipherText string.
            var cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip((Keysize / 8) * 2).Take(cipherTextBytesWithSaltAndIv.Length - ((Keysize / 8) * 2)).ToArray();

            using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
            {
                var keyBytes = password.GetBytes(Keysize / 8);
                using (var symmetricKey = new RijndaelManaged())
                {
                    symmetricKey.BlockSize = 256;
                    symmetricKey.Mode = CipherMode.CBC;
                    symmetricKey.Padding = PaddingMode.PKCS7;
                    using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes))
                    {
                        using (var memoryStream = new MemoryStream(cipherTextBytes))
                        {
                            using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
                            using (var streamReader = new StreamReader(cryptoStream, Encoding.UTF8))
                            {
                                return streamReader.ReadToEnd();
                            }
                        }
                    }
                }
            }
        }

        private static byte[] Generate256BitsOfRandomEntropy()
        {
            var randomBytes = new byte[32]; // 32 Bytes will give us 256 bits.
            using (var rngCsp = new RNGCryptoServiceProvider())
            {
                // Fill the array with cryptographically secure random bytes.
                rngCsp.GetBytes(randomBytes);
            }
            return randomBytes;
        }
    }
}

以上的类可以通过类似以下代码的方式轻松使用:

using System;

namespace EncryptStringSample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Please enter a password to use:");
            string password = Console.ReadLine();
            Console.WriteLine("Please enter a string to encrypt:");
            string plaintext = Console.ReadLine();
            Console.WriteLine("");

            Console.WriteLine("Your encrypted string is:");
            string encryptedstring = StringCipher.Encrypt(plaintext, password);
            Console.WriteLine(encryptedstring);
            Console.WriteLine("");

            Console.WriteLine("Your decrypted string is:");
            string decryptedstring = StringCipher.Decrypt(encryptedstring, password);
            Console.WriteLine(decryptedstring);
            Console.WriteLine("");

            Console.WriteLine("Press any key to exit...");
            Console.ReadLine();
        }
    }
}

您可以在这里下载一个简单的VS2013示例解决方案(其中包括一些单元测试)

更新 23/Dec/2015: 代码的具体改进列表如下:

  • 修复了一个愚蠢的错误,导致加密和解密之间的编码不同。由于生成盐和IV值的机制已更改,因此不再需要编码。
  • 由于盐/IV更改,先前的代码注释错误地指出UTF8编码16个字符字符串产生32个字节不再适用(因为不再需要编码)。
  • 已将弃用的PBKDF1算法替换为更现代的PBKDF2算法。
  • 密码派生现在已正确加盐,而以前根本没有加盐(又一个愚蠢的错误被解决了)。

11
代码审查: 由于您正在生成超过160位的密钥材料,因此PBKDF2更为合理。但是,与上述声明相反,您的PBKDF1甚至没有加盐。关于IV的评论是不正确的,一个16个字符的UTF8字符串是16个字节,它仍然可以工作,因为IV大小实际上是基于块大小而不是密钥大小的。在解密中,IV被ASCII解码而不是UTF8解码。 您的答案谈到了抽象化盐和IV,但实际上您只是将它们删除了 -- 不是现代或安全的做法。 - jbtule
7
@CraigTP的解释不错,但是1000次迭代太少了。当PBKDF2标准在2000年被制定时,建议的最小迭代次数为1000,但该参数旨在随着CPU速度的增加而逐渐增加。到2005年,Kerberos标准建议使用4096次迭代,Apple iOS 3使用了2000次,iOS 4使用了10000次,而2011年,LastPass在JavaScript客户端中使用5000次迭代,在服务器端哈希中使用100000次迭代。 - Ogglas
14
有没有更新.NET Core的机会,因为RijndaelManaged()类在Core中不可用? - onedevteam.com
23
我不能回答这个问题,所以我在这里评论一下。就.NET Standard 2而言,块大小必须为128。以下是它如何改变您的代码:https://github.com/nopara73/DotNetEssentials/blob/master/DotNetEssentials/Crypto/StringCipher.cs - nopara73
5
似乎在.NET 6中这不再起作用,解密的数据被截断了。 - Thurfir
显示剩余50条评论

139
using System.IO;
using System.Text;
using System.Security.Cryptography;

public static class EncryptionHelper
{
    public static string Encrypt(string clearText)
    {
        string EncryptionKey = "abc123";
        byte[] clearBytes = Encoding.Unicode.GetBytes(clearText);
        using (Aes encryptor = Aes.Create())
        {
            Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(EncryptionKey, new byte[] { 0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76 });
            encryptor.Key = pdb.GetBytes(32);
            encryptor.IV = pdb.GetBytes(16);
            using (MemoryStream ms = new MemoryStream())
            {
                using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateEncryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(clearBytes, 0, clearBytes.Length);
                    cs.Close();
                }
                clearText = Convert.ToBase64String(ms.ToArray());
            }
        }
        return clearText;
    }
    public static string Decrypt(string cipherText)
    {
        string EncryptionKey = "abc123";
        cipherText = cipherText.Replace(" ", "+");
        byte[] cipherBytes = Convert.FromBase64String(cipherText);
        using (Aes encryptor = Aes.Create())
        {
            Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(EncryptionKey, new byte[] { 0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76 });
            encryptor.Key = pdb.GetBytes(32);
            encryptor.IV = pdb.GetBytes(16);
            using (MemoryStream ms = new MemoryStream())
            {
                using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateDecryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(cipherBytes, 0, cipherBytes.Length);
                    cs.Close();
                }
                cipherText = Encoding.Unicode.GetString(ms.ToArray());
            }
        }
        return cipherText;
    }
}

5
最好不要在方法中硬编码加密密钥。 - user1914368
13
这非常适用于许多情况下我们不需要盐复杂性的简单实用方法。我只需将加密密钥设置为参数,就能直接成功使用原始代码。 - shelbypereira
6
为了使这种方法具有可移植性,您可以将密钥作为方法参数传递。例如:public static string Encrypt(string clearText, string encryptionKey) 这样一来,您就可以为每个方法调用使用唯一的密钥。 - sojim2
8
我的代码分析器警告变量 cs 被多次释放。在 EncryptDecrypt 方法中,我们不需要重复的 cs.Close() 语句,因为一旦控制流离开 using 块,它们都会被释放。 - Jatin Sanghvi
14
为什么要用“+”代替空格? - Simon
显示剩余11条评论

45
如果您的目标是不支持 RijndaelManaged 的 ASP.NET Core,您可以使用 IDataProtectionProvider
首先,配置应用程序以使用数据保护:
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDataProtection();
    }
    // ...
}

然后,您将能够注入 IDataProtectionProvider 实例并使用它来加密/解密数据:

public class MyService : IService
{
    private const string Purpose = "my protection purpose";
    private readonly IDataProtectionProvider _provider;

    public MyService(IDataProtectionProvider provider)
    {
        _provider = provider;
    }

    public string Encrypt(string plainText)
    {
        var protector = _provider.CreateProtector(Purpose);
        return protector.Protect(plainText);
    }

    public string Decrypt(string cipherText)
    {
        var protector = _provider.CreateProtector(Purpose);
        return protector.Unprotect(cipherText);
    }
}

阅读这篇文章获取更多详情。


2
最适合我的是使用asp.netcore 3.1,谢谢。 - SexyBoooom
17
关于您提供的文章,需要说明的是:"_加密需要密钥,这些密钥由数据保护系统创建和管理。密钥的默认生命周期为90天,并根据环境存储在适当的位置。密钥是临时的,因此数据保护API主要设计用于短期数据保护场景_。" 如果您使用此方法,请注意您的密钥会在默认情况下在90天后过期。 - Lars Christensen
1
这应该是被接受的答案,因为作者提到/询问了“最现代(最好)的方式”,并且“涉及盐、密钥”的实现最少。 - sharpc
只是想提一下,services.AddDataProtection()必须独立存在,或者如果您将它们连接起来,则必须是最后一个services.Add...()方法,因为它不返回IServiceCollection。 - IngoB

31

尝试使用这个类:

public class DataEncryptor
{
    TripleDESCryptoServiceProvider symm;

    #region Factory
    public DataEncryptor()
    {
        this.symm = new TripleDESCryptoServiceProvider();
        this.symm.Padding = PaddingMode.PKCS7;
    }
    public DataEncryptor(TripleDESCryptoServiceProvider keys)
    {
        this.symm = keys;
    }

    public DataEncryptor(byte[] key, byte[] iv)
    {
        this.symm = new TripleDESCryptoServiceProvider();
        this.symm.Padding = PaddingMode.PKCS7;
        this.symm.Key = key;
        this.symm.IV = iv;
    }

    #endregion

    #region Properties
    public TripleDESCryptoServiceProvider Algorithm
    {
        get { return symm; }
        set { symm = value; }
    }
    public byte[] Key
    {
        get { return symm.Key; }
        set { symm.Key = value; }
    }
    public byte[] IV
    {
        get { return symm.IV; }
        set { symm.IV = value; }
    }

    #endregion

    #region Crypto

    public byte[] Encrypt(byte[] data) { return Encrypt(data, data.Length); }
    public byte[] Encrypt(byte[] data, int length)
    {
        try
        {
            // Create a MemoryStream.
            var ms = new MemoryStream();

            // Create a CryptoStream using the MemoryStream 
            // and the passed key and initialization vector (IV).
            var cs = new CryptoStream(ms,
                symm.CreateEncryptor(symm.Key, symm.IV),
                CryptoStreamMode.Write);

            // Write the byte array to the crypto stream and flush it.
            cs.Write(data, 0, length);
            cs.FlushFinalBlock();

            // Get an array of bytes from the 
            // MemoryStream that holds the 
            // encrypted data.
            byte[] ret = ms.ToArray();

            // Close the streams.
            cs.Close();
            ms.Close();

            // Return the encrypted buffer.
            return ret;
        }
        catch (CryptographicException ex)
        {
            Console.WriteLine("A cryptographic error occured: {0}", ex.Message);
        }
        return null;
    }

    public string EncryptString(string text)
    {
        return Convert.ToBase64String(Encrypt(Encoding.UTF8.GetBytes(text)));
    }

    public byte[] Decrypt(byte[] data) { return Decrypt(data, data.Length); }
    public byte[] Decrypt(byte[] data, int length)
    {
        try
        {
            // Create a new MemoryStream using the passed 
            // array of encrypted data.
            MemoryStream ms = new MemoryStream(data);

            // Create a CryptoStream using the MemoryStream 
            // and the passed key and initialization vector (IV).
            CryptoStream cs = new CryptoStream(ms,
                symm.CreateDecryptor(symm.Key, symm.IV),
                CryptoStreamMode.Read);

            // Create buffer to hold the decrypted data.
            byte[] result = new byte[length];

            // Read the decrypted data out of the crypto stream
            // and place it into the temporary buffer.
            cs.Read(result, 0, result.Length);
            return result;
        }
        catch (CryptographicException ex)
        {
            Console.WriteLine("A cryptographic error occured: {0}", ex.Message);
        }
        return null;
    }

    public string DecryptString(string data)
    {
        return Encoding.UTF8.GetString(Decrypt(Convert.FromBase64String(data))).TrimEnd('\0');
    }

    #endregion

}

并像这样使用它:

string message="A very secret message here.";
DataEncryptor keys=new DataEncryptor();
string encr=keys.EncryptString(message);

// later
string actual=keys.DecryptString(encr);

3
这并不是一个糟糕的答案,因为多样性可能会使加密算法更加强大。毕竟,没有任何一种加密机制是完美的,所以我认为不需要对这种特定方法的鲁棒性发出警告。 - Bigger
1
这个回答还不错。TDES在保护ATM交易过程中经常被用于电话线路传输。我点了+1,不确定为什么有人点了-1。 - psyklopz
3
多样性并非保持加密算法强度的原因,强度取决于数学。DES(即使是三重DES)是一种过时的算法,在现今大多数应用中已不再足够强大。 - Eric W
1
这正是我期望的应用。我只需要一种方法来确保用户无法选择序列中的下一个整数,而不是使用已插入380的链接,他们将得到一些随机字符串,这样他们就无法使用381了。 - Andrew MacNaughton
1
这个解决方案基于.NET 2.0,最近微软已经更新了加密命名空间。我怀疑我的答案现在已经过时了。 - John Alexiou
显示剩余3条评论

21

SecureString似乎不是一个好的选择,但当您在像银行应用程序这样的安全环境中工作时,它是一件好事... - Tarik
这真的取决于你需要做什么。我第一次接触它是因为我们需要在政府应用程序中加密内存凭据。 - Ulises
除了 Rijndael,他们还提供 AES(和其他加密算法): https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.aes?view=netframework-4.7.2 - Ioanna
1
SecureString已被弃用,微软建议不要使用它。请参考https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md。 - Menahem

13
你可能正在寻找ProtectedData类,该类使用用户的登录凭据对数据进行加密。

4
我阅读了MSDN文档,但它并没有说明如果我们将这些加密数据移动到具有不同凭据的其他计算机上会发生什么。那么我们仍然能够解密它吗? - Tarik
1
@Braveyard 只需在导出时解密,然后在新机器上再次加密即可。 - Richard Anthony Freeman-Hein
2
@RichardHein:我也这么想,但似乎不是一个明智的建议。当我移动信息时,我不喜欢以纯人类可读格式进行操作,否则所有这些加密工具的意义何在。 - Tarik
@Braveyard 然后在旧的机器上解密 - 使用共享密钥进行传输的加密 - 在旧机器上解密 - 使用共享密钥在新机器上加密 - 使用新机器密钥加密? - Richard Anthony Freeman-Hein
1
@RichardHein:如果你想在不同机器之间传输数据,ProtectedData 不是正确的工具。(除非你在一个域中,这样他们就可以在任何地方使用相同的用户) - SLaks
@SLaks:顺便提一下,即使在一个域中,这也不一定有效。在我的领域里肯定行不通,如果你在Azure上,甚至可能在同一个Web角色上也行不通。根据我最近的经验,DataProtectionScope.CurrentUser只能在同一台(物理)机器上可靠地使用。 - Tewr

-2

9
RSA是一种非对称的加密算法,这可能不是他想要的。 - SLaks

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接