Skip to content
Advertisement

Sending RSA encoded message from JavaScript React to Python. Ciphertext length must be equal to key size. issue

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.

User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement