4

I'm building a web API and I need to implement ECDH to perform an end-to-end cryptography. At the server side I have a C# application and at the client side I have a Javascript application.

I'm able to exchange the keys, generate the private keys and encrypt the messages, but I'm having problems decrypting it.

I think the problem is in the exchange of the public keys. In javascript keys starts with a byte "4" and .NET keys starts with 8 bytes identifying the type and the size of the key, I need to change this bytes in order to import each key (Information that I've found here). Maybe this cause some inconsistencies.

At the client side I'm using the Web Cryptography API to handle with the ECDH. I'm implementing as follows.

Generating the keys

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

Exporting the public key like this:

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

Importing the external public key

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

And finally deriving the key

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

At the server side I'm implementing the same steps as follow. Generating Public Key

private static ECDiffieHellmanCng ecdh = new ECDiffieHellmanCng(256);

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

Exporting public key. Note that I change the first bytes

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

        return auxKey;
    }

Importing public key and deriving private key. Note that I change the first bytes

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);
}

I belive the problem is about those keys. Any way these are the encrypt and decrypt codes:

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;

I'm really don't have any idea how to resolve this problem.

Harry
  • 41
  • 4
  • Your C# code made possible the correct export of public key for javascript and import of public key for C# and @Paul answer gives the correct final shared key. Thanks to both of you. – Juan Rojas Aug 28 '21 at 06:56

3 Answers3

4

I had the same issue. After debugging more, I realised that the key generated by the C# with DeriveKeyMaterial is then hashed with SHA-256.

My solution is on the javascript to export the derivated key, hash it then import it as a new key.

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));
               });
        })
      });
  });


})
  • Definitively saving my hours of trying to make javascript - c# ECDH through REST api. Thank you @Paul! – glihm Mar 31 '20 at 08:18
0

Have you seen PKI.js? There you could find a complete implementation of all possible key encryption schemas for CMS Enveloped/Encrypted Data. Also there are live examples and here is the source code for all the examples. Please notice there is WiKi page regarding working with CMS EnvelopedData in PKI.js.

  • Thank you for your help. I didn't find any example of key exchange in the documentation and I don't think the problem is encrypting the data. Two javascript applications using the code above works perfectly as well as two .NET applications. The problem lies in this cross-plataform comunication. – Harry Sep 04 '18 at 21:30
0

Try to do it this way

Full exemple in gist

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']
    );
  }
}

How to use:

(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);
})();