How to replicate C# code for creating an ECDH key pair in TypeScript?

169 Views Asked by At

I'm trying to replicate the functionality of the following C# code in TypeScript, which generates an ECDH key pair and computes a shared secret. The C# code is as follows:

public static byte[] CreateECDHAddHockKey(byte[] productKey, ref byte[] publicKey) {
    //productKey is byte[64]
    //BCRYPT_ECDH_PUBLIC_P256_MAGIC = 0x314B4345,
    //var keyType = new byte[] { 0x45, 0x43, 0x53, 0x31 };
    //var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 };
    byte[] PublicP256Header = new byte[] { 0x45, 0x43, 0x4B, 0x31, 0x20, 0x00, 0x00, 0x00 }; //8 bytes
    byte[] completeKey = new byte[productKey.Length + PublicP256Header.Length]; //8+64 = 72 bytes
    Array.Copy(PublicP256Header, completeKey, PublicP256Header.Length);
    Array.Copy(productKey, 0, completeKey, PublicP256Header.Length, productKey.Length);
    CngKey key = CngKey.Import(completeKey, CngKeyBlobFormat.EccPublicBlob);
    ECDiffieHellmanCng MyKey = new ECDiffieHellmanCng(256);
    MyKey.HashAlgorithm = CngAlgorithm.Sha256;
    MyKey.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash;
    var sharedSecret = MyKey.DeriveKeyMaterial(key); //32 bytes
    publicKey = MyKey.Key.Export(CngKeyBlobFormat.EccPublicBlob).Skip(8).ToArray(); //64 bytes
    return sharedSecret;
}

I've attempted to replicate the functionality in TypeScript using the crypto module, but I'm facing format errors and differences in output lengths. This is the closest code I have so far:

import crypto, { KeyObject } from 'crypto';
function generateKeyPairSync(data: Uint8Array): {
  publicKey: KeyObject;
  sharedSecret: KeyObject;
} {
  //generate key pair using ecc public blob
  const keyPair = crypto.generateKeyPairSync('ed25519', {
    publicKeyEncoding: {
      type: 'spki',
      format: 'der',
      data,
    },
  });

  return {
    publicKey: keyPair.publicKey,
    sharedSecret: keyPair.privateKey,
  };
}
1

There are 1 best solutions below

0
Topaco On

Ed25519 and ECDH are different algorithms. Ed25519 applies the curve edwards25519 and is used for signing/verification. ECDH operates on other curves, in your case NIST P-256 (aka prime256v1 aka secp256r1) and is intended to generate a shared secret.

Regarding the public key, it should be noted that the C# code imports/exports a raw key x|y (64 bytes), while NodeJS expects the uncompressed key 0x04|x|y (65 bytes). Accordingly, the leading 0x04 byte is to be removed or added depending on the context.
The 0x45434B3120000000 prefix is part of the proprietary MS key format and is not required for the NodeJS side.

Regarding C#, keep in mind that DeriveKeyMaterial() does not return the actual shared secret (i.e. the x coordinate of the calculated EC point), but the SHA256 hash of the shared secret, which is why on the NodeJS side this hash has to be determined as well (since SHA256 is not reversible, the C# implementation does not allow the determination of the actual shared secret).


NodeJS sample implementation for the generation/import of an ECDH key pair and for the determination of the SHA256 hash of the shared secret:

import * as crypto from 'crypto';

// 1. step: 
var ecdh = crypto.createECDH('prime256v1');
/*
// create an ECDH key pair for NIST P-256
var keyPair = ecdh.generateKeys();
var rawPrivateKeyHex = ecdh.getPrivateKey('hex');        // export raw private key (32 bytes)
var uncompressedPublicKeyHex = ecdh.getPublicKey('hex'); // export uncompressed public key: 0x04|x|y (65 bytes)
*/
// alternatively: import an ECDH key pair for NIST P-256
var rawPrivateKeyHex = '76b90be9e191e2baedb809a04f3b3a409af30c9a392ba494919bbd981b6a55a3'                                                                
var rawPublicKeyHex = 'e69f452d54691bc60ae559b18171f62d57a8e1702d55e4ceebeb0c4f548c02ace6c792a0e99c4e4d57f46e37063455b467acf935a1931d053f2994dde25fbe76'  
var uncompressedPublicKeyHex = '04' + rawPublicKeyHex; 
ecdh.setPrivateKey(rawPrivateKeyHex, 'hex');             // import raw private key (32 bytes)    
ecdh.setPublicKey(uncompressedPublicKeyHex, 'hex');      // import uncompressed public key: 0x04|x|y (65 bytes)
console.log("private: " + rawPrivateKeyHex);   
console.log("public:  " + uncompressedPublicKeyHex.substring(2)); // raw public key (64 bytes) 

// 2. step: import raw public key x|y (64 bytes) from C# side
var rawPublicKeyOtherSideHex = '7C0AEC1E4AEB49AE02DE132D0E40CFD148CFF63447B091DA41FF1F903CB50089932D7ECAE4773A40E4ABAD8727FF9F5EB041BB1BEA08AE479757292C8AFC4E23'; 
var uncompressedPublicKeyOtherSideHex = '04' + rawPublicKeyOtherSideHex; 

// 3. step: generate shared secret and SHA256 hash of shared secret
var sharedSecret = ecdh.computeSecret(uncompressedPublicKeyOtherSideHex, 'hex');    // generate shared secret
var sharedSecretSha256 = crypto.createHash('sha256').update(sharedSecret).digest(); // generate SHA256 hash of shared secret

console.log('shared secret, SHA256: ' + sharedSecretSha256.toString('hex'));

Test:

When the C# code is executed with the public key of the NodeJS side:

byte[] productKey = Convert.FromHexString("e69f452d54691bc60ae559b18171f62d57a8e1702d55e4ceebeb0c4f548c02ace6c792a0e99c4e4d57f46e37063455b467acf935a1931d053f2994dde25fbe76");

the SHA256 hash of the shared secret and public key of the C# side results in, for instance:

shared secret, SHA256: AF4A096010B0B777FB8247ADDC59847303EF1B0D9FADA6ED507ECF5F79980FC5
raw public key: 7C0AEC1E4AEB49AE02DE132D0E40CFD148CFF63447B091DA41FF1F903CB50089932D7ECAE4773A40E4ABAD8727FF9F5EB041BB1BEA08AE479757292C8AFC4E23

When the NodeJS code is executed with the public key of the C# side:

var rawPublicKeyOtherSideHex = '7C0AEC1E4AEB49AE02DE132D0E40CFD148CFF63447B091DA41FF1F903CB50089932D7ECAE4773A40E4ABAD8727FF9F5EB041BB1BEA08AE479757292C8AFC4E23'; 

the SHA256 hash of the shared secret results in:

shared secret, SHA256: af4a096010b0b777fb8247addc59847303ef1b0d9fada6ed507ecf5f79980fc5

in accordance with the value of the C# side.