I have public key encryption working well in Javascript. I'm using AES-GCM wrapped with RSA-OAEP. Encryption/decryption work well in the Javascript version. Now I'm trying to encrypt something server-side (python) and use my existing decryption client-side (window.crypto.subtle). It's failing when I attempt to unwrap the key with this error:
OperationError: The operation failed for an operation-specific reason
Super helpful error messages! The relevant code in python is:
from Cryptodome.Cipher import AES
from Cryptodome.Random import get_random_bytes
from Cryptodome.Cipher import PKCS1_OAEP
from Cryptodome.PublicKey import RSA
keyPub = RSA.import_key(base64.b64decode(public_key))
rsa_cipher = PKCS1_OAEP.new(keyPub)
aes = get_random_bytes(16)
aes_cipher = AES.new(aes, AES.MODE_GCM)
wrapped = str(base64.b64encode(rsa_cipher.encrypt(aes)), 'ascii')
The code to unwrap that is:
window.crypto.subtle.unwrapKey(
'raw',
self.from64(wrapped),
self.keyPair.privateKey,
{
name: "RSA-OAEP"
},
{
name: "AES-GCM"
},
false,
["decrypt"]
).then((aesKey) => {
I'm confident in the base64 conversion, because the Python code happily takes my public key, which it wouldn't if the ASN.1 was messed up.
Can you spot why the key wrap in Python is failing when I try to unwrap it in Javascript?
Unfortunately, the error message is very unspecific (as is often the case with the WebCrypto API). However, the posted WebCrypto code works, as the following code snippet shows.
(async () => {
// For this test a 512 bits RSA key was used. In practice, apply RSA key sizes >= 2048 bits for security reasons!!!
var pkcs8DerB64 = "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2gdsVIRmg5IH0rG3u3w+gHCZq5o4OMQIeomC1NTeHgxbkrfznv7TgWVzrHpr3HHK8IpLlG04/aBo6U5W2umHQQIDAQABAkEAu7wulGvZFat1Xv+19BMcgl3yhCdsB70Mi+7CH98XTwjACk4T+IYv4N53j16gce7U5fJxmGkdq83+xAyeyw8U0QIhAPIMhbtXlRS7XpkB66l5DvN1XrKRWeB3RtvcUSf30RyFAiEA5ph7eWXbXWpIhdWMoe50yffF7pW+C5z07tzAIH6DKo0CIQCyveSTr917bdIxk2V/xNHxnx7LJuMEC5DcExorNanKMQIgUxHRQU1hNgjIsXXZoKgfaHaa1jUZbmOPlNDvYYVRyS0CIB9ZZee2zubyRla4qN8PQxCJb7DiICmH7nWP7CIvcQwB"
var wrappedKeyB64 = "LVumLtkD20pvX188VR2QTcDRIOuQlw4vxGFvj9vuEOlAsIvOv/vMcC9C6qCVjI4FuzNWEZ7Xo48Pnu2LnB1vcA=="
var wrappedKey = b642ab(wrappedKeyB64);
var wrappingKey = await importWrappingKey(pkcs8DerB64)
var unwrappedKey = await window.crypto.subtle.unwrapKey(
'raw',
wrappedKey,
wrappingKey,
{name: "RSA-OAEP"},
{name: "AES-GCM"},
false,
["decrypt"]
);
console.log(unwrappedKey)
// Helper ---------------------------------------
async function importWrappingKey(pkcs8DerB64) {
return await window.crypto.subtle.importKey(
"pkcs8",
b642ab(pkcs8DerB64),
{name: "RSA-OAEP", hash: "SHA-1"},
false,
["unwrapKey"]
);
}
function b642ab(base64_string){
return Uint8Array.from(window.atob(base64_string), c => c.charCodeAt(0));
}
function ab2hex(ab) {
return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}
})();
The wrapped key was created using the posted Python code.
Inspect your code for differences. A possible bug could be the key import. Check in your code if SHA-1 is explicitly specified as digest in the 3rd parameter of the importKey()
call. The Python code uses SHA-1 by default for the OAEP and MGF1 digest (see PKCS1_OAEP.new()
).