ECDH между C# и JavaScript
Я создаю веб-API, и мне нужно реализовать ECDH для выполнения сквозной криптографии. На стороне сервера у меня есть приложение C#, а на стороне клиента - приложение Javascript.
Я могу обмениваться ключами, генерировать закрытые ключи и шифровать сообщения, но у меня возникают проблемы с расшифровкой.
Я думаю, что проблема в обмене открытыми ключами. В javascript ключи начинаются с байта "4", а ключи.NET начинаются с 8 байтов, определяющих тип и размер ключа, мне нужно изменить эти байты, чтобы импортировать каждый ключ (информация, которую я нашел здесь). Может быть, это вызывает некоторые несоответствия.
На стороне клиента я использую 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;
Я действительно понятия не имею, как решить эту проблему.
1 ответ
Я была такая же проблема. После дополнительной отладки я понял, что ключ, сгенерированный 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));
});
})
});
});
})
Вы видели PKI.js? Там вы можете найти полную реализацию всех возможных схем шифрования ключей для CMS Enveloped/Encrypted Data. Также есть живые примеры и вот исходный код для всех примеров. Обратите внимание, что есть страница WiKi для работы с CMS EnvelopedData в PKI.js.
Попробуй сделать это так
Полный пример в сущности
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);
})();