I receive the error “Ciphertext length must be equal to key size.” when trying to send an encoded message from javascript to python. I have encoded the bytesarray to base64 in javascript and decoded the base64 back to a bytes array in Python. Yet, the issue seems to persist.
Key Generation (Python):
RSA_SECRET_KEY = rsa.generate_private_key( public_exponent=65537, key_size=4096 ) RSA_PUBLIC_KEY = RSA_SECRET_KEY.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo )
Import Key (Javascript):
async function importRsaKey(pem) { // fetch the part of the PEM string between header and footer const pemHeader = "-----BEGIN PUBLIC KEY-----"; const pemFooter = "-----END PUBLIC KEY-----"; const pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length - 1); // base64 decode the string to get the binary data const binaryDerString = window.atob(pemContents); // // convert from a binary string to an ArrayBuffer const binaryDer = str2ab(binaryDerString); var key = await crypto.subtle.importKey( "spki", binaryDer, { name: "RSA-OAEP", hash: "SHA-256" }, true, ["encrypt"] ); return key; }
Encrypt Data (Javascript):
async function encrypt(key, msg) { return await crypto.subtle.encrypt( {name:"RSA-OAEP"}, key, str2ab(msg) ) }
Transmit Data (Javascript):
const handleSubmit = (e) => { e.preventDefault(); const rsa_key_promise = importRsaKey(public_key); rsa_key_promise .then((rsa_key) => { encrypt(rsa_key, "test") .then((data) => { axiosInstance .post(`auth/token/`, { hash: window.btoa(data), }) ... more code
Receive and decode data (Python):
def post(self, request, *args, **kwargs): response = Response(status.HTTP_400_BAD_REQUEST) try: enc = base64.b64decode(request.data['hash']) dec = settings. RSA_SECRET_KEY.decrypt(enc, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) except Exception as e: print(e) return response
Advertisement
Answer
encrypt()
returns the ciphertext as an ArrayBuffer
. However, btoa()
requires a binary string. Therefore the ArrayBuffer
must be converted to a binary string first (see ab2b64()
below). With this fix a valid ciphertext is produced:
(async () => { var pem = `-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu0O0WXzs0bPuSx0n/b5B zyqaFBgBC3U/9nfChj5idf6ae0NY9iDG749N/O5XB7dduAGq/UHemdvJTsztPWOd y0NjVkluX0g6Mm+rB6rRAqihwq3xdl44OKvVWKmWM2iYhL6DahAdjMgvN4HSGU7c HM0ZijDD89dQ2lfC7jEOloxzvja7P8ZCWM1A14VT48Z/F4uSyRkdV1SEY1ifhmSe 3dzxpR53RIblZcZsGV2Tic6pM9LDVaNiCMBLggbEy7ezvhut0wDyIM92McjNja99 8C/bhUzR4zaGPEWq1ZgiLiGM/iposa3O9JbOJ8g6yFKPArxVZfGDPcVYZhJEtNN8 QJzx1ZQdG22Nvybg8EHoESDh6Dd8SJ8yVySSnRn3N8QFANI5EblpcxlaCmb0rlpo pmmNsx2Whz4oB9kMnWzQcOhmCeK9cbZOL9/cfPljY3Nyr18U5aGIygaTBVMJ3uTE cbfGroEOLdDcv7zJuf1d4UsMOF0iioSAFvRxW57NHF8tlQaqKqPERAdg3c3Z0A0e eFtxL3fKj7/PLC6/3C4ziNu0HLK0j5Ucyheczyl5bLDgUztRLSRuOjoNC1G0zYcK DrnYNbJhuICYyyXL27ZMlIIS1j784jU3TRqP0+TtCr0vYOCiXC0lQA/I6J7QF9sB itD59zlSvyMCl96prOAH5y8CAwEAAQ== -----END PUBLIC KEY-----`; var key = await importRsaKey(pem); var ciphertext = await encrypt(key, 'The quick brown fox jumps over the lazy dog'); var base64String = ab2b64(ciphertext); // Fix: Convert ArrayBuffer to a binary string and Base64 decode! document.getElementById("ciphertext").innerHTML = base64String; // e.g. ciphertext: IH+AikadAv7hdWJgKaLYTIgmN+Pvq4c5BU+VQi6irxCSI/IA30Tp6QK/rdNZYQDlNNJIk0XvK1AgbHPW3tBOygZjFGWVdVzDbYIhhva0o1FtHZluCIhxpJLEpAFKq+sMuBTtKeCqL8S4cS6B0GaDQzBjTh2D6k+p/fjVSx00bq4aqRUsgdYoa1uT3ook66qQfcyunImx++JrkjTSw7LB5XHjcGKXH3/92ZcxWyqBr5hIRje5Bdz6xPjpuGMlaPuOseVdyHEDcrAMl874NK16tz6ThkGw6rpwZxyEDdd13QOimVrFVpfpNleZVgunj+R+SFzDZYlHSAKwpte/RNvBe/V+cxuxaV7BqCM7xeU8hDzkPxE+IQS4Yimxxzy6b3y7KtyJN8nwjTECf7T76oigNYijNSYyKKWo0sf8/sfYET8HOElEzEH+m7Yblg784++308pRodz/8I4FolrDhORyJRiD46IDkDv47XsYtOBGgJzUlcjytNnlAvHGYDMXhVEVpMOIpuxXeNYyLvFgWaYcN8FEGgtUicXdbCinQvf2Fy6qF97IjWv2zaC/WbzbfnQl6ksiKKww1d8CawBoSzh7OibpujkK1QcyokbjPSOUJoE2IZvd1bGclrh3ypZRbqWNiRl4frCvcYRzfCO8pGkd0wUjdDI1g4kKvzgGt3lshT0= async function importRsaKey(pem) { const pemHeader = "-----BEGIN PUBLIC KEY-----"; const pemFooter = "-----END PUBLIC KEY-----"; const pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length - 1); const binaryDerString = window.atob(pemContents); const binaryDer = str2ab(binaryDerString); var key = await crypto.subtle.importKey( "spki", binaryDer, { name: "RSA-OAEP", hash: "SHA-256" }, true, ["encrypt"] ); return key; } async function encrypt(key, msg) { return await crypto.subtle.encrypt( {name:"RSA-OAEP"}, key, str2ab(msg) ); } function str2ab(str) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); for (let i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; } function ab2b64(arrayBuffer) { return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))); } })();
<p style="font-family:'Courier New', monospace;" id="ciphertext"></p>
Test:
The resulting ciphertext can be decrypted with Python:
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding import base64 pkcs8Pem = '''-----BEGIN PRIVATE KEY----- MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC7Q7RZfOzRs+5L HSf9vkHPKpoUGAELdT/2d8KGPmJ1/pp7Q1j2IMbvj0387lcHt124Aar9Qd6Z28lO zO09Y53LQ2NWSW5fSDoyb6sHqtECqKHCrfF2Xjg4q9VYqZYzaJiEvoNqEB2MyC83 gdIZTtwczRmKMMPz11DaV8LuMQ6WjHO+Nrs/xkJYzUDXhVPjxn8Xi5LJGR1XVIRj WJ+GZJ7d3PGlHndEhuVlxmwZXZOJzqkz0sNVo2IIwEuCBsTLt7O+G63TAPIgz3Yx yM2Nr33wL9uFTNHjNoY8RarVmCIuIYz+Kmixrc70ls4nyDrIUo8CvFVl8YM9xVhm EkS003xAnPHVlB0bbY2/JuDwQegRIOHoN3xInzJXJJKdGfc3xAUA0jkRuWlzGVoK ZvSuWmimaY2zHZaHPigH2QydbNBw6GYJ4r1xtk4v39x8+WNjc3KvXxTloYjKBpMF Uwne5MRxt8augQ4t0Ny/vMm5/V3hSww4XSKKhIAW9HFbns0cXy2VBqoqo8REB2Dd zdnQDR54W3Evd8qPv88sLr/cLjOI27QcsrSPlRzKF5zPKXlssOBTO1EtJG46Og0L UbTNhwoOudg1smG4gJjLJcvbtkyUghLWPvziNTdNGo/T5O0KvS9g4KJcLSVAD8jo ntAX2wGK0Pn3OVK/IwKX3qms4AfnLwIDAQABAoICAAHxDEQnQu9TrcNSnJEJcXY7 61gM/anIP+8Gw9oPeIbfqmtfweLfaSCfvD/EmttmH88iGUtB7RRsTnSGNGmACGlM nBGPdlj/jzbpqHzOXRdpdy/lDM1c4blYssAWFgwXaAlsTkGBxESq6K5rJqoDgs27 pKmlosp674gsA8XjdVLDRwnwWFWrcRGpoyP46mtAqh2s4Us7eu3mXu8GwrSqg2kq esjq/XKU8XjyKznCGh8CKQf0Bflz1bbgg4foGQ9BqtfsQoufBWOoswGGIvd2m9gr Ltv9dWmlLZQfZsuLJcOTrnoOJ4K8Ghq4G5AXB+D+1iPBnyMM837m9mkshFDZpn6i dYmIKaE2DgNJSto10ZTo5kmve2rM7TaytvrTQqZbPXkP7pZO6O/bELtwvaZp0Op+ vhaUZWG5+C8gIZANv/1OAecMaDdVDbfIN+DK3ocinA6nYUE3yOBzgZkk8D4Z9A6Z SuvBw8Pr5QNpUXiaD/OHzY++OvB2mv4mgEC67rtd8MyB63fpY+jYyE9v5D7CChgY oLyEpdT+Uhtjfl5jLHj6xoF0Od2mc3PgL8yzTDpzfyGkjjkJknYJ76lxK+kSWvqi v+34hCFjcQexZmd8wObTXz95/c+2W65eGAooeQn3SP8dmtuu1J/+T1mW6eMSuPf4 ckhb859P3S7APkq+Gr2lAoIBAQDOO458+lYP68d9Ho4ZouxkDUyZy1LDqiwvJ16/ X9gdDLBLgqSFvg6QyT7Gwj2rzjHTwwlKRzB/JvQH4M26omUEMTIcGQWHYr9XSwkj Jh49MCpomOokDl/61n+UTy2et7nFzn5AEHm+vmjEUdDzEf8b3rzIL49kmzPX1Hu1 U9tMLzfTj4uW3nY3Ahn3goKBCM+mNktXRJqhw+1MkgW3eyffa2ums0CRb3nUnsdY 3rOAYZER2ukpbBI2vA4gKp1cgYBFehQhUBSup7HlRiwgQ3FSE4A3kFey1at2FpcZ /epu5PafeaFYmgjCYE5IlL+/EKSPnkf8Q+Q+KrQk/oekUAZtAoIBAQDodFmqAaor oRXgVeEOipE/fMssQGUOk2xZ8Yn9WwzPKGyhN498qooE2/RTHjngxmrrF/awGTq8 BiCyNPjVA0PW8TIUJxBX9155P+HxPpZZacpO3bYnWw4p24wizGJ1pgkAp7v1EWma VlwewKcYx/ymMfyCCwUXbw6g4z1Gi2ni5VRzuawZ9nrbbM9wvSgkWKtC80EJ+ZDY Tsr+5z4NqRAUZr61/HUiiBSu3UvH9089FcgelkELNYph/iaCmJnunIcFlEy5h/mA yIwvuGagP+yk8yYXhALQ+S/zD3b+2dzvLRDtV7b93rtgfk3B/ZAD0Dzwc03zCLpP dwWpE1JLK9KLAoIBAQCdnUjF3XD+0/zvc/W4RBsUUFG1zH3himIgW59+9VouwW7P FvZ0PI/XOebfcr49WuYb6JhmC0hWNUgV6UpyFADOFmcssDbYhLCln3RJR62ep/wR WqS/j7js9RgmGelMvy+crLcycSUKkW1ydPEThDKLc0ymVirqAe+6SOuO5prYe9HX v4I4eKayXcnIrxbcVQaWCjLEbGsdrKbkeUkjNF2B1BA/JAn53M+onvzNv85CFM8R bVP7U1wMNuc40DjZ5SNKdgWCfDiCTymXh2zb749g4gSA8rEDvWdAZf1vYO7Vd+nA ce3M0FRXcdECiaSN+sM5/AcaFi0PEgYBrAGwo3R1AoIBAQCerbX7cFF6oOavEdCk vYBzJzwGBBs3/PjM2S4KDdpLm1u0HZpMTpoSwRcimhKGVsvrmZsjEMXgTgqJu9FU j3sCwfkeeqAUfF84Q5x3svKtLKMWfRB4AxdDCYS6yGw5xVKF6PpMS0ucOHF/6KDo MLRNuveUyfL60SvaNeTBQC/S3BtvOAK8Yl3xZXChk+5QCVs3Q5hVN9BhaD/4C2B3 sL2yP4TV8/T90ojT6WpuoWqs1y6ZepYCEdVaGUSuh38kvCMLcvWA/Mob2Eqh1K3x nFFtNDH/gXTus/vAXwEq7Qt9FXVlnyfiWuXr86wezXk+sSq4NO20BnQwBJ6PkQnv GIYLAoIBABtXxV8hE9Jrk5K2jfPkGCbjs5rvVeUcTN5wMay4dHNblrTb9EIRiYPg 716yinJo71tvQRjvxBBkFjXYZ07ygebW5UmQdmvi4BkR+2gKn42sC5O6jvOQVNZY ArLCgjK9QRQ/Sd5XT9yhR4/E5e3xF9Tn6/vEbaY24SgXDPe7gw6VOS+80eEVswx8 O7pEGfJSCySIPgFmn/2gTeQtkkNj5WSfl3dXYQJ+QQ8p5Eo+2O39hPXo7ATST5Aq NI+6emgx79Ka/kfqRK0JjIb6cJLvSdsmM4APn4sZrVvRy9rSrZ+9drbNpnLua778 eWkk35z7vGmQT6CyzIhDJycFAIFkv6M= -----END PRIVATE KEY-----''' private_key = serialization.load_pem_private_key( pkcs8Pem.encode('utf-8'), password=None ) ciphertextFromJS = "IH+AikadAv7hdWJgKaLYTIgmN+Pvq4c5BU+VQi6irxCSI/IA30Tp6QK/rdNZYQDlNNJIk0XvK1AgbHPW3tBOygZjFGWVdVzDbYIhhva0o1FtHZluCIhxpJLEpAFKq+sMuBTtKeCqL8S4cS6B0GaDQzBjTh2D6k+p/fjVSx00bq4aqRUsgdYoa1uT3ook66qQfcyunImx++JrkjTSw7LB5XHjcGKXH3/92ZcxWyqBr5hIRje5Bdz6xPjpuGMlaPuOseVdyHEDcrAMl874NK16tz6ThkGw6rpwZxyEDdd13QOimVrFVpfpNleZVgunj+R+SFzDZYlHSAKwpte/RNvBe/V+cxuxaV7BqCM7xeU8hDzkPxE+IQS4Yimxxzy6b3y7KtyJN8nwjTECf7T76oigNYijNSYyKKWo0sf8/sfYET8HOElEzEH+m7Yblg784++308pRodz/8I4FolrDhORyJRiD46IDkDv47XsYtOBGgJzUlcjytNnlAvHGYDMXhVEVpMOIpuxXeNYyLvFgWaYcN8FEGgtUicXdbCinQvf2Fy6qF97IjWv2zaC/WbzbfnQl6ksiKKww1d8CawBoSzh7OibpujkK1QcyokbjPSOUJoE2IZvd1bGclrh3ypZRbqWNiRl4frCvcYRzfCO8pGkd0wUjdDI1g4kKvzgGt3lshT0=" decrypted = private_key.decrypt( base64.b64decode(ciphertextFromJS), padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)) print(decrypted.decode('utf-8')) # The quick brown fox jumps over the lazy dog
A note about the encoding of the plaintext: Keep in mind that str2ab(msg)
in encrypt()
requires a string for msg
where charCodeAt()
returns a value less than 256
, i.e. a single byte, for each character.
For the plaintext in the above example, this is satisfied. For arbitrary plaintexts, however, this is usually not true (e.g. € (U+20AC)
equals 2
bytes). Therefore, to support arbitrary plaintexts, UTF-8 encoding should be used, e.g. new TextEncoder().encode(msg)
instead of str2ab(msg)
.
For str2ab(binaryDerString)
in importRsaKey()
this is not critical, since binaryDerString
is a binary string where each character corresponds to a single byte.