Encrypting and Decrypting with AES GCM from Angular to C#

huangapple go评论75阅读模式
英文:

Encrypting and Decrypting with AES GCM from Angular to C#

问题

在C#中实现与您现有的Angular加密和解密方法兼容的方法,可以使用以下代码片段:

public static string Encrypt(string plainText, string password)
{
    try
    {
        var iv = new byte[10]; // 用于初始化向量 (IV) 的大小
        using (var aes = new AesGcm())
        {
            using Rfc2898DeriveBytes pbkdf2 = new(Encoding.UTF8.GetBytes(password), iv, 100000, HashAlgorithmName.SHA256);
            byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
            byte[] cipherBytes = new byte[plainBytes.Length];

            aes.Encrypt(iv, plainBytes, cipherBytes, null);

            var encryptedData = new byte[iv.Length + cipherBytes.Length];
            iv.CopyTo(encryptedData, 0);
            cipherBytes.CopyTo(encryptedData, iv.Length);

            return Convert.ToBase64String(encryptedData);
        }
    }
    catch (Exception e)
    {
        throw new Exception($"Encryption Error: {e.Message}");
    }
}

public static string Decrypt(string encryptedText, string password)
{
    try
    {
        var encryptedData = Convert.FromBase64String(encryptedText);
        var ivSizeBytes = 10;
        var iv = encryptedData.Take(ivSizeBytes).ToArray();
        var cipherBytes = encryptedData.Skip(ivSizeBytes).ToArray();

        using Rfc2898DeriveBytes pbkdf2 = new(Encoding.UTF8.GetBytes(password), iv, 100000, HashAlgorithmName.SHA256);
        byte[] plainBytes = new byte[cipherBytes.Length];

        using var aes = new AesGcm(pbkdf2.GetBytes(32));
        aes.Decrypt(iv, cipherBytes, null, plainBytes);

        return Encoding.UTF8.GetString(plainBytes);
    }
    catch (Exception e)
    {
        throw new Exception($"Decryption Error: {e.Message}");
    }
}

这些方法将使用C#中的AesGcm进行加密和解密,与您的Angular方法兼容。

英文:

How can I implement encryption and decryption methods in C# that are compatible with my existing Angular encryption and decryption methods? Currently, I have two methods in Angular for encryption and decryption, but I also need to be able to perform these operations in the back-end using C#. Here are my Angular methods:

 async encryptStringAES256(key: string, xSalt: string, data: string): Promise<any> {
    return new Promise((resolve, reject) => {
      const salt = new Uint8Array(this.toUin8Arry(xSalt));
      const decryptedData = new Uint8Array(this.stringToArray(data));
      this.getKeyMaterial(key).then( async (res: any) => {
        const xkey = await window.crypto.subtle.deriveKey(
          {
            name: 'PBKDF2',
            iterations: 100000,
            salt,
            hash: 'SHA-256'
          },
          res,
          { name: 'AES-GCM', length: 256},
          true,
          ['encrypt', 'decrypt']
        );
        window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: salt }, xkey, decryptedData).then(
          encrypted => {
            resolve(this.arrayToString(new Uint8Array(encrypted)));
          })
        .catch(err => {
           // catch
        });
      });
    });
  }

  async decryptStringAES256(key: string, xSalt: string, data: string): Promise<any> {
      const salt = new Uint8Array(this.toUin8Arry(xSalt));
      const encryptedData = new Uint8Array(this.stringToArray(data));
      this.getKeyMaterial(key).then( async (res: any) => {
        const xkey = await window.crypto.subtle.deriveKey(
          {
            name: 'PBKDF2',
            iterations: 100000,
            salt,
            hash: 'SHA-256'
          },
          res,
          { name: 'AES-GCM', length: 256},
          true,
          ['encrypt', 'decrypt']
        );
        window.crypto.subtle.decrypt({ name: 'AES-GCM', iv: salt }, xkey, encryptedData).then(
          decrypted => {
            const dec = this.arrayToString(new Uint8Array(decrypted));
            resolve(dec);
          })
          .catch(err => {
            reject(data);
          });
      });
    });
  }

  getKeyMaterial(password: string): any {
    const enc = new TextEncoder();
    return window.crypto.subtle.importKey(
      'raw',
      enc.encode(password),
      'PBKDF2',
      false,
      ['deriveBits', 'deriveKey']
    );
  }

c#:

    public static void Decrypt(byte[] sourceData, string password)
    {
        try
        {
            var encryptedData = sourceData.AsSpan();
            var key = Convert.FromBase64String(password).AsSpan();
            var ivSizeBytes = 10;
            var tagSizeBytes = 0; 

            var cipherSize = encryptedData.Length - tagSizeBytes - ivSizeBytes;

            var iv = encryptedData.Slice(0, ivSizeBytes);

            var cipherBytes = encryptedData.Slice(ivSizeBytes, cipherSize);

            var tagStart = ivSizeBytes + cipherSize;
            var tag = encryptedData.Slice(tagStart);

            using Rfc2898DeriveBytes pbkdf2 = new(Encoding.UTF8.GetBytes(password), iv.ToArray(), 100000, HashAlgorithmName.SHA256); 

            Span<byte> plainBytes = cipherSize < 1024
                ? stackalloc byte[cipherSize]
                : new byte[cipherSize];
            using var aes = new AesGcm(pbkdf2.GetBytes(32));
            aes.Decrypt(iv, cipherBytes, tag, plainBytes);

            var x = Encoding.UTF8.GetString(plainBytes);
        }
        catch (Exception e)
        {
            throw new Exception($"Fehler Decrypt: {e.Message}");
        }
    }

答案1

得分: 1

  • 在JavaScript代码中,盐也被用作IV(即IV和IV长度对应于盐和盐长度)。
    这种耦合方式有缺点,将在最后一部分中进行解释。
  • WebCrypto API隐式连接了密文和标签:ciphertext|tag。由于没有为标签指定长度,默认长度为16字节。
    在使用AesGcm进行解密时,需要将密文和标签分开(基于已知标签长度)。

以下的C#代码用于解密基于你发布的代码:

var encryptedData = Convert.FromBase64String("avgPDxTwl9Nbr7FUnTgKvXXQ1Rrl2q2R0jBgsrn1F9i5GEyGg/vaw3i3Vkg1zUz3Wby/juz2yXE6ZpU=");
var salt = Convert.FromBase64String("MDEyMzQ1Njc4OTAx");
var password = "my password";
var plaintext = Decrypt(encryptedData, salt, password);
Console.WriteLine(Encoding.UTF8.GetString(plaintext)); // The quick brown fox jumps over the lazy dog

public static byte[] Decrypt(byte[] encryptedData, byte[] salt, string password)
{
    var ciphertext = encryptedData[0..^16];
    var tag = encryptedData[^16..];
    using Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt, 100000, HashAlgorithmName.SHA256);
    using var aes = new AesGcm(pbkdf2.GetBytes(32));
    var plaintext = new byte[ciphertext.Length];
    aes.Decrypt(salt, ciphertext, tag, plaintext);
    return plaintext;
}

密文是使用发布的JavaScript代码生成的,替换了所有缺失的方法(toUin8Arry()stringToArray()arrayToString()等),并将Base64用作盐和密文的编码:

(async () => {
    var plaintext = new TextEncoder().encode('The quick brown fox jumps over the lazy dog');
    var password = 'my password';
    var salt = b642ab('MDEyMzQ1Njc4OTAx');
    var ciphertextB64 = await encryptStringAES256(password, salt, plaintext);
    console.log(ciphertextB64);

    function b642ab(base64_string) {
        return Uint8Array.from(window.atob(base64_string), c => c.charCodeAt(0));
    }

    function ab2b64(arrayBuffer) {
        return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
    }

    async function encryptStringAES256(password, salt, plaintext) {
        const keyMaterial = await getKeyMaterial(password);
        const key = await window.crypto.subtle.deriveKey(
            {
                name: 'PBKDF2',
                iterations: 100000,
                salt,
                hash: 'SHA-256'
            },
            keyMaterial,
            {
                name: 'AES-GCM',
                length: 256
            },
            true,
            ['encrypt', 'decrypt']
        );
        const ciphertext = await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: salt }, key, plaintext);
        return ab2b64(ciphertext); // avgPDxTwl9Nbr7FUnTgKvXXQ1Rrl2q2R0jBgsrn1F9i5GEyGg/vaw3i3Vkg1zUz3Wby/juz2yXE6ZpU=
    }

    async function getKeyMaterial(password) {
        const enc = new TextEncoder();
        return window.crypto.subtle.importKey(
            'raw',
            enc.encode(password),
            'PBKDF2',
            false,
            ['deriveBits', 'deriveKey']
        );
    }
})();

安全性:

通常出于安全原因,每次加密都必须使用随机盐,以便每次加密都应用不同的密钥(假定密码固定不变)。
在使用的架构中,这一点尤为重要,因为盐也用作IV。静态盐会导致密钥/IV对的重复使用,这对于GCM来说是一个严重的漏洞。

将盐和IV耦合在一起的另一个缺点是,必须选择12字节的盐长度,以便使用GCM推荐的12字节IV长度。
GCM可以处理其他IV长度,但这会降低性能和兼容性。对于这个示例,问题尤为严重:AesGcm仅支持推荐的12字节IV长度。
如果使用不同的盐长度,就不能应用AesGcm,必须切换到例如BouncyCastle。

在JavaScript端的正确实现将在加密期间生成随机盐(例如16字节)和随机IV(12字节)。
加密后,数据将被连接在一起:salt|iv|ciphertext|tagciphertext|tag由WebCrypto隐式连接)。
这4个组件都不是机密的,所以它们的泄漏不是关键的。
在解密端,数据根据已知的盐、IV和标签长度进行分离,并进行解密。

英文:

> and I don't know how to get the iv and tag from the sourceData, and I
> also don't know what length they have.

  • In the JavaScript code, the salt is also used as IV (i.e. IV and IV length correspond to salt and salt length).
    This coupling has disadvantages, which will be explained in the last section.
  • The WebCrypto API implicitly concatenates ciphertext and tag: ciphertext|tag. Since no length is specified for the tag, the default length of 16 bytes is applied.
    When decrypting with AesGcm, ciphertext and tag are to be separated (based on the known length of the tag).

The following C# code for decryption is based on the code you posted:

var encryptedData = Convert.FromBase64String("avgPDxTwl9Nbr7FUnTgKvXXQ1Rrl2q2R0jBgsrn1F9i5GEyGg/vaw3i3Vkg1zUz3Wby/juz2yXE6ZpU=");
var salt = Convert.FromBase64String("MDEyMzQ1Njc4OTAx");
var password = "my password";
var plaintext = Decrypt(encryptedData, salt, password);
Console.WriteLine(Encoding.UTF8.GetString(plaintext)); // The quick brown fox jumps over the lazy dog

public static byte[] Decrypt(byte[] encryptedData, byte[] salt, string password)
{
    var ciphertext = encryptedData[0..^16];
    var tag = encryptedData[^16..];
    using Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt, 100000, HashAlgorithmName.SHA256);
    using var aes = new AesGcm(pbkdf2.GetBytes(32));
    var plaintext = new byte[ciphertext.Length];
    aes.Decrypt(salt, ciphertext, tag, plaintext);
    return plaintext;
}

The ciphertext was generated using the posted JavaScript code, replacing all missing methods (toUin8Arry(), stringToArray(), arrayToString(), etc.) and using Base64 as the encoding for salt and ciphertext:

<!-- begin snippet: js hide: true console: true babel: false -->

<!-- language: lang-js -->

(async () =&gt; {
var plaintext = new TextEncoder().encode(&#39;The quick brown fox jumps over the lazy dog&#39;);
var password = &#39;my password&#39;
var salt = b642ab(&#39;MDEyMzQ1Njc4OTAx&#39;);
var ciphertextB64 = await encryptStringAES256(password, salt, plaintext);
console.log(ciphertextB64);
function b642ab(base64_string){
return Uint8Array.from(window.atob(base64_string), c =&gt; c.charCodeAt(0));
}
function ab2b64(arrayBuffer) {
return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}
async function encryptStringAES256(password, salt, plaintext) {
const keyMaterial = await getKeyMaterial(password);
const key = await window.crypto.subtle.deriveKey(
{
name: &#39;PBKDF2&#39;,
iterations: 100000,
salt,
hash: &#39;SHA-256&#39;
},
keyMaterial,
{
name: &#39;AES-GCM&#39;, 
length: 256
},
true,
[&#39;encrypt&#39;, &#39;decrypt&#39;]
);
const ciphertext = await window.crypto.subtle.encrypt({ name: &#39;AES-GCM&#39;, iv: salt }, key, plaintext)
return ab2b64(ciphertext); // avgPDxTwl9Nbr7FUnTgKvXXQ1Rrl2q2R0jBgsrn1F9i5GEyGg/vaw3i3Vkg1zUz3Wby/juz2yXE6ZpU=
}
async function getKeyMaterial(password) {
const enc = new TextEncoder();
return window.crypto.subtle.importKey(
&#39;raw&#39;,
enc.encode(password),
&#39;PBKDF2&#39;,
false,
[&#39;deriveBits&#39;, &#39;deriveKey&#39;]
);
}
})();

<!-- end snippet -->


Security:

In general, for security reasons, a random salt must be used for each encryption so that each encryption applies a different key (assuming a fixed password).
With the architecture used, this is even more necessary since the salt is also applied as IV. A static salt would therefore lead to reuse of key/IV pairs, which is a serious vulnerability for GCM.

Another drawback of coupling salt and IV is that the salt length must be chosen to 12 bytes in order to use the IV length of 12 bytes recommended for GCM.
GCM can handle other IV lengths, but this comes at the cost of performance and compatibility.
The latter is a problem especially with this example: AesGcm only supports the recommended IV length of 12 bytes (here). If you use a different salt length, you cannot apply AesGcm and have to switch to e.g. BouncyCastle.

A correct implementation on the JavaScript side would generate a random salt (e.g. 16 bytes) and a random IV (12 bytes) during encryption.
After encryption, the data would be concatenated: salt|iv|ciphertext|tag (ciphertext|tag is implicitly concatenated by WebCrypto). None of the 4 components are secret, so their disclosure is not critical.
On the decryption side, the data is separated based on the known length of salt, IV, and tag, and decryption is performed.

huangapple
  • 本文由 发表于 2023年3月9日 18:05:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/75683077.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定