1

使用 C# 加密文本时遇到一个奇怪的问题。ReSharper(我同意)建议替换using此代码中的块:

public object GetEncryptedOrDefault(object value, ICryptoTransform encryptor)
{
    if (encryptor is null)
    {
        throw new ArgumentNullException(nameof(encryptor));
    }
    var isEncryptionNeeded = value != null;
    if (isEncryptionNeeded)
    {
        using var memoryStream = new MemoryStream();
        using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
        {
            using var writer = new StreamWriter(cryptoStream);
            var valueAsText = value.ToString();
            writer.Write(valueAsText);
        }

        var encryptedData = memoryStream.ToArray();
        var encryptedText = Convert.ToBase64String(encryptedData);
        return encryptedText;
    }

    return default;
}

到这个简化的(注意使用声明而不是块):

public object GetEncryptedOrDefault(object value, ICryptoTransform encryptor)
{
    if (encryptor is null)
    {
        throw new ArgumentNullException(nameof(encryptor));
    }
    var isEncryptionNeeded = value != null;
    if (isEncryptionNeeded)
    {
        using var memoryStream = new MemoryStream();
        using var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
        using var writer = new StreamWriter(cryptoStream);
        var valueAsText = value.ToString();
        writer.Write(valueAsText);

        var encryptedData = memoryStream.ToArray();
        var encryptedText = Convert.ToBase64String(encryptedData);
        return encryptedText;
    }

    return default;
}

嗯..第一个效果很好,它能够加密文本。然而,第二个不起作用!theencryptedData是空的,因此它产生一个空的encryptedText

我看不出问题。为什么?


更新 1 感谢 Emanuel 的回答,只有当要加密的文本大于 15 个字符时,我才能使其工作。这真是奇怪。在 15 个字符以下,只有带有“老式” using 块的代码才能工作,而不是使用 using 声明的代码。

我在 Github 的这个示例 repo 中重现了这个问题。

即使这个问题与 AesManaged (我不知道)有关,为什么第一种方法会成功,而第二种方法对于任何 15 个或更少字符的文本会失败?

这是代码:

class Program
{
    static void Main(string[] args)
    {
        var encryptor = GetEncryptor();
        var text = "Under 15 characters this text causes problems";
        while (text.Length >= 0)
        {
            text = text.Substring(0, text.Length - 1);
            Console.WriteLine($"Result Method A with {text.Length} characters: {GetWorkingEncrypted(text, encryptor)}");
            Console.WriteLine($"Result Method B with {text.Length} characters: {GetNonWorkingEncrypted(text, encryptor)}");
        }
    }

    private static string GetWorkingEncrypted(string text, ICryptoTransform encryptor)
    {
        using var memoryStream = new MemoryStream();
        using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
        {
            using var writer = new StreamWriter(cryptoStream);
            writer.Write(text);
            writer.Flush();
        }

        var encryptedData = memoryStream.ToArray();
        if (encryptedData.Length == 0)
        {
            throw new Exception($"Encrypted data is 0 for text {text}");
        }
        var encryptedText = Convert.ToBase64String(encryptedData);
        return encryptedText;
    }

    private static string GetNonWorkingEncrypted(string text, ICryptoTransform encryptor)
    {
        using var memoryStream = new MemoryStream();
        using var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
        using var writer = new StreamWriter(cryptoStream);
        writer.Write(text);
        writer.Flush();

        var encryptedData = memoryStream.ToArray();
        if (encryptedData.Length == 0)
        {
            throw new Exception($"Encrypted data is 0 for text \"{text}\" with length {text.Length}");
        }
        var encryptedText = Convert.ToBase64String(encryptedData);
        return encryptedText;
    }

    private static ICryptoTransform GetEncryptor()
    {
        var aesManaged =
            new AesManaged
            {
                Padding = PaddingMode.PKCS7
            };

        return aesManaged.CreateEncryptor();
    }
}

这是执行的结果:

Result Method A with 44 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3937Peo255iHRylA9DF0lf4K+
Result Method B with 44 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 43 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934Pwaqyce+T6SG3WaqnzNRt
Result Method B with 43 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 42 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3936tIT0560Lky1gz3FXKHU3Y
Result Method B with 42 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 41 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934xr6AiKuSxRet/e8iWhLEV
Result Method B with 41 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 40 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3937gCR2Lf9zQClOlCFw51dVo
Result Method B with 40 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 39 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3936OjZ4HEtzkcIjVMUJcDzum
Result Method B with 39 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 38 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3936ti1b7pskEFKb2zJrRkVaD
Result Method B with 38 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 37 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3937rKO73A+OiHd1aAMqOd3Df
Result Method B with 37 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 36 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934/BNp0BiYZPRMUUiODp/kb
Result Method B with 36 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 35 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3935sjuGp/uE4fVOn26J1ESzH
Result Method B with 35 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 34 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+39360AAj7hDLcnbMZH7aknpDl
Result Method B with 34 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 33 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3935EfO82m/jR81he3Jt4z1h+
Result Method B with 33 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 32 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934zeVj3CoE5YIFK8/g07QmH
Result Method B with 32 characters: /ppBS775B1KRShB+QKTLZJH/fCQbNFhCvzfbFP+3934=
Result Method A with 31 characters: /ppBS775B1KRShB+QKTLZCRCNZXU9Ndp7uKLJkUXFsw=
Result Method B with 31 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 30 characters: /ppBS775B1KRShB+QKTLZJ1WbVjggwJM3uOTZ2dHx5c=
Result Method B with 30 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 29 characters: /ppBS775B1KRShB+QKTLZDiI785bQRbNeZX2aNFQvZo=
Result Method B with 29 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 28 characters: /ppBS775B1KRShB+QKTLZMmLT/ycIHWz0sjPsdfg/ys=
Result Method B with 28 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 27 characters: /ppBS775B1KRShB+QKTLZJDekWQLgx9tTUE/59ldSqs=
Result Method B with 27 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 26 characters: /ppBS775B1KRShB+QKTLZKIkr5xwCc8SS9eSnw715vk=
Result Method B with 26 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 25 characters: /ppBS775B1KRShB+QKTLZFAtZM8oTV/uTBb6OccqErc=
Result Method B with 25 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 24 characters: /ppBS775B1KRShB+QKTLZD5BAXR9qZav1rG5NnaLEQQ=
Result Method B with 24 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 23 characters: /ppBS775B1KRShB+QKTLZFof3ATUQWJqiZ2wZ6Gj4Vc=
Result Method B with 23 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 22 characters: /ppBS775B1KRShB+QKTLZNWhgIhTYyERb74rKEl8bos=
Result Method B with 22 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 21 characters: /ppBS775B1KRShB+QKTLZIsgSoHGJT3XysDLqmV9Bi0=
Result Method B with 21 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 20 characters: /ppBS775B1KRShB+QKTLZO0ZdC9DzISByS5T1Rx4hQ4=
Result Method B with 20 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 19 characters: /ppBS775B1KRShB+QKTLZBFfUwWYJ5ECKF2JexKf8Xk=
Result Method B with 19 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 18 characters: /ppBS775B1KRShB+QKTLZNkZyUqqwkELWI4JN14M2RE=
Result Method B with 18 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 17 characters: /ppBS775B1KRShB+QKTLZOKdO3s345tAlCrN+q3QV68=
Result Method B with 17 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 16 characters: /ppBS775B1KRShB+QKTLZE6HtWd1ZLwZMvy3E9Bm5CI=
Result Method B with 16 characters: /ppBS775B1KRShB+QKTLZA==
Result Method A with 15 characters: OMMFxti/svtQ/Z5fqaLaEg==
Unhandled exception. System.Exception: Encrypted data is 0 for text "Under 15 charac" with length 15
   at IssueEncryptionStreamEmpty.Program.GetNonWorkingEncrypted(String text, ICryptoTransform encryptor) in /media/sasw/Data/src/issue-encryption-stream-empty/Program.cs:line 53
   at IssueEncryptionStreamEmpty.Program.Main(String[] args) in /media/sasw/Data/src/issue-encryption-stream-empty/Program.cs:line 17

Process finished with exit code 134.

4

1 回答 1

2

Because your writer is no longer scoped to cryptoStream's using block, it is now disposed right before returning from your function (as opposed to being disposed when cryptoStream's scope ends). But it didn't flush its contents to the stream, because you didn't call Flush on it, and its AutoFlush property defaults to false.

public object GetEncryptedOrDefault(object value, ICryptoTransform encryptor)
{
    if (encryptor is null)
    {
        throw new ArgumentNullException(nameof(encryptor));
    }
    var isEncryptionNeeded = value != null;
    if (isEncryptionNeeded)
    {
        using var memoryStream = new MemoryStream();
        using var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
        using var writer = new StreamWriter(cryptoStream);
        var valueAsText = value.ToString();
        writer.Write(valueAsText);
        writer.Flush();

        var encryptedData = memoryStream.ToArray();
        var encryptedText = Convert.ToBase64String(encryptedData);
        return encryptedText;
    }

    return default;
}

In your first example, the writer is disposed (thus flushing its buffer) before using up the stream it writes to, so the stream isn't empty when trying to access it.

UPDATE: While looking through CryptoStream.Dispose's source code, I've noticed a call to FlushFinalBlock, which is the reason for the outputs being mismatched.

As for the input length problem, encryptor.InputBlockSize is equal to 16, which is why writing a text with 15 or less characters and not calling FlushFinalBlock (note that CryptoStream.Flush is a no-op) results in an empty stream.

So, the again-working code becomes:

private static string GetWorkingEncrypted(string text, ICryptoTransform encryptor)
{
    using var memoryStream = new MemoryStream();
    using var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
    using var writer = new StreamWriter(cryptoStream);
    writer.Write(text);
    writer.Flush();
    cryptoStream.FlushFinalBlock();

    var encryptedData = memoryStream.ToArray();
    var encryptedText = Convert.ToBase64String(encryptedData);
    return encryptedText;
}
于 2020-05-12T20:40:13.013 回答