4

我正在构建一个 Web API,我需要实现 ECDH 来执行端到端加密。在服务器端我有一个 C# 应用程序,在客户端我有一个 Javascript 应用程序。

我能够交换密钥、生成私钥并加密消息,但我在解密它时遇到了问题。

我认为问题在于公钥的交换。在 javascript 键以字节“4”开始,.NET 键以 8 字节开始,标识键的类型和大小,我需要更改此字节以导入每个键(我在这里找到的信息)。也许这会导致一些不一致。

在客户端,我使用 Web Cryptography API 来处理 ECDH。我正在执行如下。

生成密钥

await window.crypto.subtle.generateKey(
        {
            name: "ECDH",
            namedCurve: "P-256",
        },
        false,
        ["deriveKey", "deriveBits"]
    );

像这样导出公钥:

await window.crypto.subtle.exportKey(
        "raw",
        publicKey
    );

导入外部公钥

await window.crypto.subtle.importKey(
        "raw",
        {
            name: "ECDH",
            namedCurve: "P-256",
        },
        false,
        ["deriveKey", "deriveBits"]
    )

最后推导出密钥

await window.crypto.subtle.deriveKey(
        {
            name: "ECDH",
            namedCurve: "P-256",
            public: publicKey,
        },
        privateKey,
        {
            name: "AES-CBC",
            length: 256,
        },
        false,
        ["encrypt", "decrypt"]
    )

在服务器端,我正在执行以下相同的步骤。生成公钥

private static ECDiffieHellmanCng ecdh = new ECDiffieHellmanCng(256);

public static void GeneratePublicKey()
{
    ecdh.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash;
    ecdh.HashAlgorithm = CngAlgorithm.Sha256;
    publicKey = ecdh.PublicKey.ToByteArray();
}

导出公钥。请注意,我更改了第一个字节

public static byte[] GetPublicKey()
    {
        var auxKey = publicKey.Skip(7).ToArray();
        auxKey[0] = 4;

        return auxKey;
    }

导入公钥和派生私钥。请注意,我更改了第一个字节

public static void GerarChavePrivada(byte[] bobPublicKey)
{
    byte[] aux = new byte[bobPublicKey.Length + 7];

    aux[0] = 0x45;
    aux[1] = 0x43;
    aux[2] = 0x4B;
    aux[3] = 0x31;
    aux[4] = 0x20;
    aux[5] = 0x00;
    aux[6] = 0x00;
    aux[7] = 0x00;

    for (int i = 1; i < bobPublicKey.Length; i++)
    {
        aux[7 + i] = bobPublicKey[i];
    }

    var importedKey = CngKey.Import(aux, CngKeyBlobFormat.EccPublicBlob);
    privateKey = ecdh.DeriveKeyMaterial(importedKey);
}

我相信问题出在那些钥匙上。无论如何,这些都是加密和解密代码:

Javascript

async function encrypt2(iv, key, data){
    var mensagemCriptografada;

    await window.crypto.subtle.encrypt(
        {
            name: "AES-CBC",
            iv: iv,
        },
        key,
        str2ab(data) //Data is a string and I'm converting using str2ab method.
    )

    .then(function(encrypted){
        mensagemCriptografada = encrypted;
    })
    .catch(function(err){
        console.error(err);
    });

    return mensagemCriptografada;
}

function str2ab (str) {
    var array = new Uint8Array(str.length);     
    for(var i = 0; i < str.length; i++) {
        array[i] = str.charCodeAt(i);
    }
    return array.buffer
}

C#

string decMessage = "";

        using (Aes aes = new AesCryptoServiceProvider())
        {
            aes.Key = privateKey;
            aes.IV = iv; //IV is the same used by the javascript code
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.None;

            var dec = aes.CreateDecryptor(privateKey, iv);

            var plain = dec.TransformFinalBlock(message, 0, message.Length);

            //I've tried every possible enconding.
            decMessage = Encoding.UTF8.GetString(plain); 
        }

        return decMessage;

我真的不知道如何解决这个问题。

4

3 回答 3

4

我遇到过同样的问题。经过更多调试,我意识到 C# 使用 DeriveKeyMaterial 生成的密钥然后使用 SHA-256 进行哈希处理。

我的解决方案是在 javascript 上导出派生密钥,对其进行哈希处理,然后将其作为新密钥导入。

cryptoApi().deriveKey(
  {
      name: "ECDH",
      namedCurve: "P-256", //can be "P-256", "P-384", or "P-521"
      public: ServerKey, //an ECDH public key from generateKey or importKey
  },
  ECkey.privateKey, //your ECDH private key from generateKey or importKey
  { //the key type you want to create based on the derived bits
      name: "AES-CBC", //can be any AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH", or "HMAC")
      //the generateKey parameters for that type of algorithm
      length: 256, //can be  128, 192, or 256
  },
  true, //whether the derived key is extractable (i.e. can be used in exportKey)
  ["encrypt", "decrypt"] //limited to the options in that algorithm's importKey
)
.then(function(AESKeyData){
  //returns the exported key data
  console.log(AESKeyData);

  cryptoApi().exportKey('raw',
    AESKeyData
  ).then(function (exportedAESKeyData) {
      cryptoApi().digest('SHA-256', exportedAESKeyData).then(function (HashedAESKeyValue) {
          console.log(HashedAESKeyValue);

          cryptoApi().importKey(
            'raw',
            HashedAESKeyValue,
            { //the key type you want to create based on the derived bits
                name: "AES-CBC", //can be any AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH", or "HMAC")
                //the generateKey parameters for that type of algorithm
                length: 256, //can be  128, 192, or 256
            },
            false,
            ["encrypt", "decrypt"]
        ).then(function (TrueAESKey) {

            cryptoApi().decrypt(
               {
                   name: 'AES-CBC',
                   length: 256,
                   iv: base64ToArrayBuffer(IV)
               },
               TrueAESKey,
               base64ToArrayBuffer(EncryptedData)
               ).then(function (decrypted) {
                   console.log(buf2hex(decrypted));
               });
        })
      });
  });


})
于 2019-04-23T10:20:00.843 回答
0

你见过PKI.js吗?在那里,您可以找到 CMS Enveloped/Encrypted Data 的所有可能的密钥加密模式的完整实现。还有现场示例这里是所有示例的源代码。请注意有关于在 PKI.js 中使用 CMS EnvelopedData 的 WiKi页面。

于 2018-09-01T08:06:48.927 回答
0

尝试这样做

完整的例子

class Protector {
  ab2str(buffer) {
    return new TextDecoder().decode(buffer);
  }

  str2ab(text) {
    return new TextEncoder().encode(text);
  }

  generateIv() {
    return crypto.getRandomValues(new Uint8Array(16));
  }

  /**
   * @see https://github.com/mdn/dom-examples/blob/master/web-crypto/derive-bits/ecdh.js
   */
  async generateKey() {
    this.key = await window.crypto.subtle.generateKey(
      { name: 'ECDH', namedCurve: 'P-256' },
      false,
      ['deriveBits']
    );
  }

  async encrypt(plaintext) {
    const counter = this.generateIv();
    const buffer = await crypto.subtle.decrypt({
      name: 'aes-ctr',
      counter: counter,
      length: 128
    }, this.importedKey, this.str2ab(plaintext));
    return { buffer, counter };
  }

  async decrypt(data) {
    const buffer = await crypto.subtle.decrypt({
      name: 'aes-ctr',
      counter: data.counter,
      length: 128
    }, this.importedKey, data.buffer);
    return this.ab2str(buffer);
  }

  getPublicKey() {
    return {publicKey: this.key.publicKey};
  }

  async setRemotePublicKey(key) {
    this.clientKey = key;
    
    this.sharedSecret = await window.crypto.subtle.deriveBits(
      { name: 'ECDH', namedCurve: 'P-256', public: this.clientKey.publicKey },
      this.key.privateKey,
      256
    );

    this.importedKey = await crypto.subtle.importKey(
      'raw',
      this.sharedSecret,
      'aes-ctr',
      false,
      ['encrypt', 'decrypt']
    );
  }
}

如何使用:

(async () => {
  // Generate Keys
  const pro1 = new Protector();
  await pro1.generateKey();
  const pub1 = pro1.getPublicKey();

  const pro2 = new Protector();
  await pro2.generateKey();
  const pub2 = pro2.getPublicKey();

  // Exchange Keys
  await pro1.setRemotePublicKey(pub2);
  await pro2.setRemotePublicKey(pub1);

  // Let`s Encrypt
  const crypted = await pro1.encrypt('Hello World');
  const descrypted = await pro2.decrypt(crypted);
})();
于 2020-07-01T09:59:44.773 回答