From 21fc4f2b75575c0289ef92c63d5301dde92b4105 Mon Sep 17 00:00:00 2001 From: Shubhashish-Chakraborty Date: Sat, 28 Mar 2026 01:08:01 +0530 Subject: [PATCH 1/3] fix: convert pyodide dicts to js objects for WC-aes-gcm --- src/worker.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/worker.py b/src/worker.py index a0296a6..8587ca4 100644 --- a/src/worker.py +++ b/src/worker.py @@ -112,8 +112,8 @@ async def _import_aes_key(key_bytes: bytes) -> object: import js from pyodide.ffi import to_js key_buf = to_js(key_bytes, create_pyproxies=False) - algo = to_js({"name": "AES-GCM"}, create_pyproxies=False) - usages = to_js(["encrypt", "decrypt"], create_pyproxies=False) + algo = to_js({"name": "AES-GCM"}, dict_converter=js.Object.fromEntries) + usages = to_js(["encrypt", "decrypt"]) return await js.crypto.subtle.importKey("raw", key_buf, algo, False, usages) @@ -130,9 +130,15 @@ async def encrypt_aes(plaintext: str, secret: str) -> str: from pyodide.ffi import to_js key_bytes = _derive_aes_key_bytes(secret) crypto_key = await _import_aes_key(key_bytes) - iv = bytes(js.crypto.getRandomValues(to_js(bytearray(12)))) - algo = to_js({"name": "AES-GCM", "iv": to_js(iv)}, create_pyproxies=False) - data = to_js(plaintext.encode("utf-8"), create_pyproxies=False) + + # Generate random IV directly into a JS Uint8Array for clean interop + iv_array = js.Uint8Array.new(12) + js.crypto.getRandomValues(iv_array) + iv = bytes(iv_array) # Extract back to python bytes for storage + + # Using dict_converter ensures Web Crypto does not see "undefined" for the algo name + algo = to_js({"name": "AES-GCM", "iv": iv_array}, dict_converter=js.Object.fromEntries) + data = to_js(plaintext.encode("utf-8")) ct_buf = await js.crypto.subtle.encrypt(algo, crypto_key, data) ct = bytes(js.Uint8Array.new(ct_buf)) return "v1:" + base64.b64encode(iv + ct).decode("ascii") @@ -157,12 +163,12 @@ async def decrypt_aes(ciphertext: str, secret: str) -> str: except Exception as exc: capture_exception(exc, where="decrypt_aes.decode") return "[decryption error]" - key_bytes = _derive_aes_key_bytes(secret) - crypto_key = await _import_aes_key(key_bytes) - algo = to_js({"name": "AES-GCM", "iv": to_js(iv)}, create_pyproxies=False) - data = to_js(ct, create_pyproxies=False) try: - pt_buf = await js.crypto.subtle.decrypt(algo, crypto_key, data) + key_bytes = _derive_aes_key_bytes(secret) + crypto_key = await _import_aes_key(key_bytes) + algo = to_js({"name": "AES-GCM", "iv": to_js(iv)}, dict_converter=js.Object.fromEntries) + data = to_js(ct) + pt_buf = await js.crypto.subtle.decrypt(algo, crypto_key, data) return bytes(js.Uint8Array.new(pt_buf)).decode("utf-8") except Exception as exc: # Auth tag mismatch = tampered/corrupted ciphertext From da6f163b080832d358d621a7c213343beaf11072 Mon Sep 17 00:00:00 2001 From: Shubhashish-Chakraborty Date: Sat, 28 Mar 2026 01:22:55 +0530 Subject: [PATCH 2/3] coderabbit done --- src/worker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/worker.py b/src/worker.py index 8587ca4..bb45b4f 100644 --- a/src/worker.py +++ b/src/worker.py @@ -166,7 +166,8 @@ async def decrypt_aes(ciphertext: str, secret: str) -> str: try: key_bytes = _derive_aes_key_bytes(secret) crypto_key = await _import_aes_key(key_bytes) - algo = to_js({"name": "AES-GCM", "iv": to_js(iv)}, dict_converter=js.Object.fromEntries) + iv_array = to_js(iv) + algo = to_js({"name": "AES-GCM", "iv": iv_array}, dict_converter=js.Object.fromEntries) data = to_js(ct) pt_buf = await js.crypto.subtle.decrypt(algo, crypto_key, data) return bytes(js.Uint8Array.new(pt_buf)).decode("utf-8") From 2973098d7b423d55d11a7ca8bb97a866dccf1fcd Mon Sep 17 00:00:00 2001 From: Shubhashish-Chakraborty Date: Sat, 28 Mar 2026 12:57:05 +0530 Subject: [PATCH 3/3] move imports to the top --- src/worker.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/worker.py b/src/worker.py index bb45b4f..764ff3d 100644 --- a/src/worker.py +++ b/src/worker.py @@ -43,6 +43,8 @@ from workers import Response +import js +from pyodide.ffi import to_js def capture_exception(exc: Exception, req=None, _env=None, where: str = ""): """Best-effort exception logging with full traceback and request context.""" @@ -109,8 +111,6 @@ def _derive_aes_key_bytes(secret: str) -> bytes: async def _import_aes_key(key_bytes: bytes) -> object: """Import raw bytes as a Web Crypto AES-GCM CryptoKey.""" - import js - from pyodide.ffi import to_js key_buf = to_js(key_bytes, create_pyproxies=False) algo = to_js({"name": "AES-GCM"}, dict_converter=js.Object.fromEntries) usages = to_js(["encrypt", "decrypt"]) @@ -126,8 +126,6 @@ async def encrypt_aes(plaintext: str, secret: str) -> str: if not plaintext: return "" try: - import js - from pyodide.ffi import to_js key_bytes = _derive_aes_key_bytes(secret) crypto_key = await _import_aes_key(key_bytes) @@ -155,8 +153,6 @@ async def decrypt_aes(ciphertext: str, secret: str) -> str: return "" if not ciphertext.startswith("v1:"): return _decrypt_xor(ciphertext, secret) - import js - from pyodide.ffi import to_js try: raw = base64.b64decode(ciphertext[3:]) iv, ct = raw[:12], raw[12:]