Context
Years ago, I partially wrote / mostly found a JavaScript class that calculated keypairs for WireGuard setups - a little crypto math experiment that somehow worked. Fast-forward to today, I needed to port it into Python so it could integrate into a more modern automation pipeline.
At first glance, it seemed like a straightforward translation: replace Float64Array with Python lists, swap Node's crypto with secrets, and wire it all together. But cryptographic math written in JavaScript hides some nasty traps, especially around data types and bitwise logic.
The Problem
When I tried generating a keypair with the first Python translation, it failed with:
TypeError: unsupported operand type(s) for ^: 'float' and 'float'
That's the Python interpreter politely telling me, "You can't XOR floating-point numbers." And it was right - JavaScript had silently let me get away with type coercion that Python won't tolerate.
The issue came from how I recreated the “field elements” used in curve math. The original JS used Float64Array, which stores numbers as doubles. When I ported that directly into Python lists (or even NumPy arrays of floats), I ended up trying to perform bitwise XORs on floats - which is nonsense in Python's world.
What We Discovered
The fix wasn't complicated once the root cause was clear:
import numpy as np
@staticmethod
def gf(init=None):
r = np.zeros(16, dtype=np.int64) # integers, not floats
if init:
for i, value in enumerate(init):
r[i] = value
return r
Switching from float to int64 made all the bitwise operations (^, &, >>) behave correctly.
It also highlighted an important lesson: understand what your data represents before choosing how to store it.
In crypto code, arrays that look like numeric data are often acting as fixed-size containers for bitwise manipulation - meaning integers are the right choice, not floats.
Why It Works
- JavaScript's loose typing silently converted between integer and float operations.
- Python, being explicit, forces you to pick - and punishes ambiguity.
- Once all operations were integer-safe, the math behaved identically to the JS version.
The rest of the port fell into place:
- Replaced
crypto.randomFillSyncwithsecrets.token_bytes. - Used Python's
base64for key encoding. - Verified the structure by matching generated keys against WireGuard's own expectations.
The Takeaway
Translating code across languages isn't about syntax - it's about semantics. Every programming language has its own mental model for numbers, arrays, and binary logic. JavaScript hides those differences; Python exposes them.
The general rules I came away with:
- Respect the math. Cryptographic routines aren't forgiving. Every misplaced bit matters.
- Don't trust JavaScript's type coercion. Recreate intent, not behavior.
- Use explicit integer types when bitwise math is involved.
- Test early - WireGuard keys make a great validation target, since the spec is deterministic.
What started as a translation ended up being a good reminder of how much implicit behavior JavaScript quietly papers over - and how refreshing it can be to work in a language that forces you to face that head-on.