I am running a django app and I am trying to use a RSA 'challenge' to verify that a user has the correct private key. The keys are generated client side using the js Web Crypto API and the public key is sent to django as a jwk and the private key is stored in a client-side pem file. I want the django view to send an encrypted uuid to the client page where a user has loaded their private key file. Then the page locally decrypts the uuid and sends it back to the server for authentication. It seems like I have gotten everything to work except for the client side decryption.
Here is the relevant portion of the django/python view:
uuid = secrets.token_hex(16)
key = json.loads(request.POST.get('key')) //key is in JWK format
e = int.from_bytes(base64.b64decode(base64url_to_base64(key['e'])), "big")
n = int.from_bytes(base64.b64decode(base64url_to_base64(key['n'])), "big")
rsakey = RSA.construct((n, e), consistency_check=True)
cipher = PKCS1_OAEP.new(rsakey, SHA256)
challenge = cipher.encrypt(uuid.encode())
challenge = base64.b64encode(challenge)
Here is the relevant portion of the client-side js:
fileReader.readAsText(file);
fileReader.onload = function() {
filekey = fileReader.result;
filekey = filekey.substring(filekey.indexOf('-----BEGIN PRIVATE KEY-----'));
let pem = filekey;
const pemHeader = "-----BEGIN PRIVATE KEY-----";
const pemFooter = "-----END PRIVATE KEY-----";
const pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length);
const binaryDerString = window.atob(pemContents);
const binaryDer = str2ab(binaryDerString);
challenge = str2ab(window.atob(challenge.substring(2, challenge.length - 1));
console.log(challenge);
window.crypto.subtle.importKey(
"pkcs8",
binaryDer,
{
name: "RSA-OAEP",
hash: {name: "SHA-256"}
},
true,
["decrypt"]
).then((key) => {
const decryptedMessage = window.crypto.subtle.decrypt(
{
name: "RSA-OAEP"
},
key,
challenge
).then((txt) => {
console.log(txt);
console.log(ab2str(txt));
});
When I tried this, I get the error Uncaught (in promise) DOMException: The operation failed for an operation-specific reason
from the window.crypto.subtle.decrypt(). I think there is some issue with the format of the ciphertext as the pycryptodome Crypto.PublicKey.PKCS1_OAEP.encrypt() function takes in a byte string whereas the Web Crypto API uses arraybuffer objects. However, I do not have enough experience with js and python data types to figure it out. It could also be possible that it is an issue with constructing the key in python using the JWK.
Any help or suggestions would be greatly appreciated!
Update: As suggested by @Topaco here is some test data:
jwk public key : {"alg":"RSA-OAEP-256","e":"AQAB","ext":true,"key_ops":["encrypt"],"kty":"RSA","n":"tTW0HHD56Lv-FmDcucLOkBTCcT3ySRDtZ64MmsgFnZWGvCOAa3Q1kKYo8RAHWtjrvac_2enHF4LZlys2kS_j1kLZeyatsDWPuMDAHunRu-jscfQoSIODB1hc8YcPiG0vVLDEBY-VKozSOje6GcXWKcaYi4kFkbLmIIJgzHYoOccflAyXl_FvVHgcU5z5qYk8JjucZfqf9rzTH3HTaeCux2SMqJr6ubBmwX8-iwyC-4LBnnf27rdGL-DcMsOCq_CPWfgtx7nav9OCt51PdszsYx3JGLsbp0-iH1mSjKs4dg3ORh6KOyP2Qbq_HRALI__OKonp5FopApWSFvVDJZf3EhIHC2upnbpj-UCcjzkSSm0h3GkTr13GCqfhRz0jRvK-1Yj4PuwmXJ5kr1gxSbokqAnRL0oFicP_wTakvQOB7XpMWz2Cl3NDLDvqhocVMHZ9HwH52fD7k9IBYHXh6cVqeOwKSIy4whAyYFLmKg-57LwKB1diiSgi6MeCMG3NFafeEm3llooAmOTZZic_uD59-zfggywf6YOyBScilYBGWKxA9P-UVK76rxJIlwInDv7U1uY-8RodjPTNcGRw8RhvIyTkfgfpLRejXxAVEx0Xu-Gr4nZ2hPQgCQwP6pUL-ohN1Lz5Y0y6GWXrGcA2N1WY7GfBqrcNhWv7xXNVA1Qcei0"}
pem private key: -----BEGIN PRIVATE KEY----- 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 -----END PRIVATE KEY-----
plaintext/uuid: 060433466b8d0be7e7d435bdd43752b5
ciphertext after b64 encode in python: b'iN+uii2BAktcgSKnt2WLZ3baAJQxNYvD3AYTwumoTT+GYMKnydPVAT264XHbsHTzeLXQjIWPi1FT3Npa7PJUewDNMCKyzrZBLgyT6HOr6QIp0TJyPx83u1osPQw1clSf22ypOEeMc9JKuePDDVu1cH61gIYU4SoLMSpxf0oukVSa1Cg3Rficiscewtn36KBkBKY4M/CTde5lwiCf1x7ZtRboSsffZiUnD2dpRvZ0GcQugW7nV583mSVadtqfxdlVrXsa9hhnZhvBHuElHOByyH5GLICEKtHXZEraNTkMvnWFWpHrMz4xSX4O0c9JP2LD61t/8mnUwvHhutsxLSbULXBvcba8+e6+KYZsw14+Ol7h7235dPEYOXmLbNGU/KCQeznu9VaJMlN8CjF8MUfDPAOSIh8OcL74cKdh8U9CmhbFSOT+TjDYbvaXIFH0D3tY/nlg3C0O6uUSZmS+7qCHbNKGVvdDK0iagnWqu3+9i/hekFKP22SJtWPYw1d6d6w3vnqzwePPtFS7D8/14D4/0hjESOSpq2X1ZBhKBlatlma8HDOl9LEgxXs791fkoj4y5XevePW2r84OCuQL+6DtaUduLDWIoCLV+LnU/t8m5AuTDCEh7cpxYfgYeexggPzk7lEJtByKkrBHqC2Egwp0GCvnIZB+4oENfO+4ZqK2C4s='
Update: Was able to fix the issue, see my code blocks. The problem was that the python returns the b64 string surrounded by b''
, which don't work with atob().