How to Write a String (or Encrypted Bytes) to a File or Socket in Python
Reviewed
Keep ciphertext as bytes end to end. Write it to a file with open(path, 'wb'), send it over a socket with sock.sendall(cipher), and never decode arbitrary bytes as UTF-8. If the ciphertext has to travel as text — JSON, a URL, or printed output — base64-encode it first.
Asked on Stack Overflow Open this example in the editorOpens as a new program; running code requires JavaScript.
Run the binary-file and base64 round trip
Pyodide has an in-memory filesystem, so this file write works in the browser. The output starts with “round-trip OK: café ☕” and then “as text:” followed by base64. The random key makes the base64 value different on each run; no fixed ciphertext is expected. cipher.bin is a virtual file, not a downloaded file on your computer, and may disappear when the worker is restarted.
import base64
import os
def xor_bytes(data: bytes, key: bytes) -> bytes:
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
message = "café ☕" # accented characters + emoji
plain = message.encode("utf-8")
key = os.urandom(len(plain)) # random, same length as the message: a real one-time pad
cipher = xor_bytes(plain, key)
# Write the ciphertext as bytes — this works.
with open("cipher.bin", "wb") as f:
f.write(cipher)
with open("cipher.bin", "rb") as f:
loaded = f.read()
assert xor_bytes(loaded, key).decode("utf-8") == message
print("round-trip OK:", message)
# If the ciphertext must travel as text (JSON, URL, print): base64.
print("as text:", base64.b64encode(cipher).decode("ascii"))Why decoding ciphertext as UTF-8 is wrong
XOR can produce arbitrary byte values from 0 to 255. Arbitrary byte sequences need not be valid UTF-8, so cipher.decode('utf-8') can raise UnicodeDecodeError. The character 'é' is encoded as two UTF-8 bytes, 0xC3 and 0xA9; changing those bytes independently does not preserve a valid text encoding.
In Python 3, str is Unicode text and bytes is raw data. open(path, 'w') accepts text and encodes it; open(path, 'wb') accepts bytes and preserves them. Encode plaintext before XOR with message.encode('utf-8'), then decode only after reversing XOR with the correct key.
If ciphertext must travel as text, use base64.b64encode(cipher).decode('ascii'), then base64-decode at the receiver before decrypting. For URLs, percent-encode ordinary base64 or use URL-safe base64. Latin-1 decoding never fails because it maps each byte to a code point, but it does not turn encrypted bytes into meaningful text and downstream re-encoding can change the bytes.
CPython socket version — not runnable in the browser
This is a CPython-only illustration, not a runnable browser demo. It assumes an already connected sock and an existing cipher value. Desktop sockets are not available through this site's browser editor. sendall and recv operate on bytes; recv(4096) is only one chunk, not a guarantee of receiving an entire message. A real protocol also needs framing and a receive loop.
# CPython version — sockets don't run in the browser.
sock.sendall(cipher) # bytes in, no encoding
data = sock.recv(4096) # bytes out, no decodingThe one-time-pad requirements are strict
A one-time pad is secure only when its key is truly random, exactly as long as the encoded plaintext, secret, and never reused. The sample generates a fresh os.urandom(len(plain)) key for each message. The modulo operation in xor_bytes is not permission to reuse a short key; repeating-key XOR is not a one-time pad.
Getting the same secret key securely to the receiver is the difficult real-world part, not the XOR. This educational demo also provides no message authentication; use a reviewed authenticated-encryption construction for application security rather than treating this example as a complete encryption protocol.