Description
NodeJS v11.8.0
Linux tstserver 4.15.0-43-generic 46-Ubuntu SMP Thu Dec 6 14:45:28 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
It's Ubuntu in a VirtualBox VM on Windows 8.1
So I am getting pretty sure there is an incompatibility with compatibility between the latest Node Crypto and the Browser's SubtleCrypto.
Here is my logic (it's pretty hard to give code samples with both sides).
I was able to build keys in Node send the public key to the browser encrypt and return for a successful decrypt, so server to browser direction I did do successfully.
I also want to send something from NodeJS to the Browser securely even though it might be over SSL.
I use SubtleCrypto on the browser to generate a temporary key pair for the session (Public and Private).
This way there's no way to intercept the private key for decryption (which is were it fails with DomException).
I can receive the SubtleCrypto Public key from the browser, and use it to encrypt a message, encode it to base64 and on the browser side it receives the message in base64 decodes it to the exact same buffer array the server shows. so none of the base64 to ByteArray stuff is wrong via a visual confirmation.
Still at the nearly final decrypt stage on the Browser side it simply says DomException (which I suppose is poor error messaging on their side) and fails with a crash error.
I believe somehow what you think is needed for decryption with SubtleCrypto is wrong. Browser side can encrypt and decrypt so that is working. The other direction it's working for all stages until the final one goes poof (DomException?).
I suggested there be an example for people to help them as it is untypically difficult in general, but that idea got initially shied away from. It's really why I suggest that as it is important to demonstrate have tests and know it's not breaking etc.
That is here #25589 I might have modified that slightly but those examples are only going the other direction (node to browser). I will try to document this code here now for Browser keys to server encrypt and back to browser decrypt.
First to Generate the keys via SubtleCrypto as crypto on the browsers.
let genKeys = async function ( ciphers ) {
if( ciphers === undefined ) {
ciphers = {
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
};
}
let keys = await crypto.generateKey( ciphers , true, [ "encrypt", "decrypt" ] );
return {
ciphers : ciphers,
prvKey: keys.privateKey,
pubKey: keys.publicKey
};
};
Then we need to send the server the Public Key for encryption. This can be one function but it's in two here. I've done all possible variations in regards to no line breaks etc.
let exportKeyAsPEM = async function( key ) {
let kPrt = await crypto.exportKey( 'spki', key );
let kStr = String.fromCharCode(...new Uint8Array( kPrt ));
let kHex = btoa( kStr );
let kPem = formatKeyAsPEM( kHex );
console.log( "exportKeyAsPem kPEM:", kPem );
return kPem;
};
let formatKeyAsPEM = function( key ) {
let res = '-----BEGIN PUBLIC KEY-----\n';
while( key.length > 0 ) {
res += key.substring( 0, 64 ) + '\n';
key = key.substring( 64 );
}
res = res + "-----END PUBLIC KEY-----";
return res;
};
It's all a little spread out for debugging but since node crypto likes standard PEM file format as key argument (this is great but expected +thanks) and works without error when importing it via nothing special just my socket message object string text.
So on the server side all you really can do is use publicEncrypt with the browsers generate public key like this.
I wrap this stuff up in an object on the node side but I'll modify this here to be similar to how I posted the code from the browser which is also not the traditional 'function name()' I seem to avoid. Also note my utl.log function you can switch to console.log
[was encrypt: function( key, txt ]
let encrypt = function( key, txt ) {
if( key !== undefined && txt !== undefined ) {
// debugger;
// let te = new TextEncoder();
// let enc = te.encode( buf );
let buf = Buffer.from( txt );
let crypt = lib.crypto.publicEncrypt( key, buf );
let b64 = crypt.toString( 'base64' );
utl.log( 1, "encrypt: txt:", txt, " buf:", buf, " crypt:", crypt, " b64:", b64 );
return b64;
}
};
It seems Buffer.from is exactly the same result as one gets from many other methods like TextEncoder's encode, and many other bit's an bobbins found out in the wild.
Anyway one gets a DomException during decrypt on the Browser via the Private Key like so.
let decrypt = async function ( key, txt ) {
let rx = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
let dec, regChk = rx.exec( txt );
if( regChk == null || regChk[0] !== regChk.input ) {
let te = new TextEncoder();
dec = te.encode( txt );
console.log( "decrypt txt not base64 converted:", txt, "to:", dec );
}
else {
dec = new Uint8Array( atob( txt ).split( '' ).map( function( cca ) { return cca.charCodeAt( 0 ); } ) );
}
let cipher = { name: 'RSA-OAEP' };
let buf = await crypto.decrypt( cipher, key, dec );
let td = new TextDecoder();
let res = td.decode( buf );
console.log( 'decrypt: txt:', txt, 'dec:', dec, 'buf:', buf, 'res:', res );
return res;
};
The Regex Check is just to make sure there is a valid base64 string to avoid any dysfunctional Exception there. Also I had tried converting if not base64 but that is silly and temporary as I am unable to getting working and clean it up yet.
It's in the SubtleCrypto.decrypt that fires the unhelpful DomException error, but it really seems to me that what you're sending is not right.
If any one can spot the error on my side please do.
The string is maybe 100 let's say 50 characters in length so it fits the 256 Uint8Array size but you'd see a different length exception etc. if that was the issue.
Really by the process of elimination I'm left to believe the error is in how you've encrypted it.
I really don't see how to assess the problem further or prove it's incompatible.
The whole point of Node is to communicate with the Browser (encryption is a must even if SSL is enabled), you need to establish a working example for how this works with Chrome / V8 on the browser and the best suggested Standards there, which I believe is the SubtleCrypto and I think this is sufficient for an expert with Node's Crypto to try this and help explain what's failing.
I strongly feel there really needs to be a way to validate and demonstrate these things are working with tests that alert if something has changed.
I think maybe if the fault is on the browser (easy way out pass the buck) it would be nice to have a flag that one sets if they are encrypting for Chrome/Firefox(? meh) Browser to repair this incompatibility.
As the stuff is randomly encrypted there really no way to analyze the format is flawed. I've checked the public key with OpenSSL and encrypted something and base64 via command line and still that DomException fires.
I feel like the Browser side may not be as responsible for making it stuff work with node, so it seems important to report it here.
I've seen a thread from a few months back that got Locked due to "heated debate" so this is still a serious issue for people. I think I've provided a way to do the right thing and demonstrate more clearly to people how it works and that it works at all. Well at least in this way RSA-OAEP from the Browser Public key to node encrypt and back again. Even one that shows another method but the same ideal pubkey from browser encryption direction.
Any suggestion for a better posting via a fiddle and a nodefiddle I'd be willing to try and do it but really one needs to do this more directly serving the browser etc.
The problem is similar on Firefox.