1 Modern Classical Cryptography on Old Game Consoles (2026-08-04)

This post’s ID is 6694892671569328580.

merope, demeter, and daphne (display output is from daphne)

I own a few “old” game consoles. There’s demeter, a Sony Playstation 2 with a busted Graphics Synthesizer (in such a way that she has graphical output, it’s just completely illegible–what fun!), merope, a Microsoft Xbox 360 with a broken disk drive, and daphne, a Nintendo Wii that is actually in great shape, relatively speaking.

In general, I try to put Unix-like operating systems on as many of my machines as possible:

and the game consoles are no exception. It’s entirely doable. There was even an official Playstation 2 Linux kit! It costs quite a bit on eBay, but it can still be bought!

I also happen to maintain a highly portable, zero-allocation, zero-dependency library of classical cryptographic primitives, mostly for personal use (I have odd requirements at times). It’s called TinyCrypT. It’s nowhere near the 1.0.0 tag at time of writing (I’m still working on CI integration, soundness proofs, constant-time proofs, and additional test vectors from sources like Wycheproof and CCTV). Nevertheless, it has reached the point where I get to do some very fun things.

You see where I’m going here, right?

1.1 NetBSD on the Nintendo Wii

This is genuinely as easy as it gets. Most Wiis can be softmodded with an SD card and patience; from there, you literally just flash the officially-supported NetBSD image for the Wii to the card and can boot right from the Homebrew Channel. Recent builds even support the onboard WLAN adapter!

1.2 Debian on the Microsoft Xbox 360

This is harder. There are actually softmods these days for the 360, but unlike the Wii they’re not persistent; you have to have the USB stick used inserted every time. Perfect for us, though, because that same pen drive can be used to hold the swap and root filesystem! If you’re going to do this, I would highly recommend following this guide by the Free60 developers, though I will note that I made some modifications such as using Dropbear and the Runit init system instead of OpenSSH and SystemD. Also, for whatever reason, Linux kept mounting the root filesystem as read-only, so I honestly just passed init=/bin/sh as a kernel parameter and would run mount -o remount,rw / followed by exec /sbin/init. Not a fully unattended boot, but neither of the other consoles can boot unattended either.

It’s honestly insane that modern mainline Debian can run almost completely unmodified (with a custom kernel) on such an old console.

1.3 Linux (nonspecific) on the Sony Playstation 2

This was the hardest to get working. I didn’t want to use the actual Linux for Playstation 2 kit, because it’s expensive and highly outdated (I already had enough issues with trying to SSH into the Xbox 360 when I was trying out a port of Ubuntu 10.10; modern OpenSSH really does not like using RSA or DSA). That meant I was voyaging into unknown territory.

Somewhat unknown. See, there’s actually modern work on the topic, by a man named Fredrik Noring who maintains a downstream fork of the Linux kernel designed specifically to run on the MIPS R5900 (also known as the Emotion Engine: the Playstation 2’s main processor). Full disclosure: I actually started trying to run Linux on demeter a long time ago, but ran into a stumbling block: while remarkably mature, Noring’s fork does not support the Playstation 2 hard disk bay or network adaptor (yes, it is specifically spelled that way). Moreover, I wasn’t able to get networking working with a random off-the-shelf USB-to-Ethernet adapter (at a guess, the driver’s memory requirements exceeded what the kernel could provide on such limited hardware). This was all complicated by the fact that I had no actually readable text output; I was essentially working blind. A familiar feeling to me; this is what embedded engineering felt like before I discovered the wonderful world of the hardware debugger.

However, I’m not going to potentially destroy demeter to try and get UART output.

As such, I went and bought the exact USB adapter mentioned in the fork’s wiki page on the topic, and set up the initramfs and kernel according to the directions on the wiki. This part was remarkably easy for me; “get Linux running in this weird way on this weird hardware” is a large part of my day job. I will note that, for whatever reason, I was not able to get DHCP working; udhcpc simply didn’t work, and I had to set a static IP (10.42.0.201, please don’t hack me). Dropbear was finicky, but I got it working eventually.

In the end, demeter actually ran the benchmark fully from initramfs, as I didn’t feel like setting up a proper root filesystem (there’s a guide on the Gentoo wiki for doing so, but it assumes that you’re running Gentoo on the build machine and I didn’t feel like setting up a Docker container).

1.4 TinyCrypT in strange places

Finally, all of the consoles are running Unix-like operating systems. It’s time to run some code.

I will note that all of these benchmarks were performed with SIMD disabled (though the only explicitly vectorized part of TinyCrypT is the ChaCha20 implementation, which also has a scalar counterpart). The IBM Broadway chip used in the Wii has no integer SIMD (though I’m experimenting with running Ascon or ChaCha20 on its Macronix DSP chip, which is admittedly usually used for audio), and while the Emotion Engine and Xenon processors used in the Playstation and Xbox have integer SIMD, emitting it from C code (even using vector extensions) is not directly supported by GCC or LLVM (it’s listed as future work). I’m considering getting my hands dirty in raw assembly, though – stay tuned!

I was not joking when I said TinyCrypT aims to be portable. Although the 32-bit implementations of a couple primitives had fallen a bit out of date, I was able to wrangle them up enough to get them building and running on the target platforms easily enough. The current build system uses zig cc as the compiler, which uses Clang/LLVM under the hood (but thankfully takes care of building Musl and the compiler runtimes and such for us). Unfortunately, Zig 0.16.0 doesn’t use Clang 24, which is the first version in several years to support the MIPS R5900; it supports all of the other platforms, though.

I initially experimented with building upstream Clang against upstream Musl (with this patch from Noring), but ran into a remarkably stubborn issue: no matter how I set up all the ABIs against each other, printf did not work:

~ # ./benchmark-demeter
Took 2143272160.2143272120 seconds to compute 0 16KiB SHA-256 hashes
Took 2143272160.2143272120 seconds to compute 0 16KiB SHA-512 hashes

It appears to be a difference in the expected calling convention, but I genuinely after hours of work could not get it to work. If you have any idea what specifically is broken here, by all means, contact me and let me know. I’m very curious.

I eventually (after 24 [!] rebuilds of Musl) gave up and built everything with GCC using musl-cross-make, and it finally worked (after some back-and-forth over floating point ABIs; I elected to use -msoft-float on everything since the EE’s floating point ISA is not IEEE-754 compliant).

To bring everything together, I put some elbow grease into optimizing some previously-neglected parts of TinyCrypT for smaller systems, fixing 32-bit Poly1305 and scalar ChaCha20, unrolling loops, and bringing the 32-bit version of Ed25519 out of the stone age. This was also a great opportunity to do constant time proofs using BINSEC; I actually determined that BINSEC/SSE 0.11.2 will assume by default that PowerPC(32/64) is little-endian (while Broadway and Xenon are big-endian), which led to a fun little patch.

1.5 The results

Drumroll, please. This is after all the optimization work, the final result:

On the Microsoft Xbox 360 (merope):

juniper@merope:~$ ./benchmark-merope
Took 52.680654068 seconds to compute 65536 16KiB SHA-256 hashes
Took 40.685832029 seconds to compute 65536 16KiB SHA-512 hashes
Took 92.106022690 seconds to compute 65536 16KiB ChaCha20 round-trips
Took 0.248235705 seconds to compute 64 16KiB Ed25519 signatures

On the Nintendo Wii (daphne):

daphne$ ./benchmark-daphne
Took 60.301309811 seconds to compute 65536 16KiB SHA-256 hashes
Took 88.360353267 seconds to compute 65536 16KiB SHA-512 hashes
Took 50.883900329 seconds to compute 65536 16KiB ChaCha20 round-trips
Took 1.001362156 seconds to compute 64 16KiB Ed25519 signatures

And last, but not least, on the Sony Playstation 2 (demeter):

~ # ./benchmark-demeter
Took 187.469833380 seconds to compute 65536 16KiB SHA-256 hashes
Took 981.200900719 seconds to compute 65536 16KiB SHA-512 hashes
Took 161.581046629 seconds to compute 65536 16KiB ChaCha20 round-trips
Took 3.202291443 seconds to compute 64 16KiB Ed25519 signatures

….

Okay, elephant in the room: demeter’s result was kind of abysmal. See, the main issue (as far as I can tell) is her vanishingly small caches; SHA-2 depends heavily on a lookup table of constants, not to mention the fact that the data being hashed is constantly being brought into and evicted out of the cache.

merope’s performance isn’t really a surprise; she’s the only 64-bit system, which means she can simply do certain operations much more efficiently (in addition to her higher clock speed). Additionally, the current 64-bit implementation of Ed25519 (as of this writing) uses a precomputed lookup table similar to what is described in the original Ed25519 paper, which provides a significant speedup at the cost of ~80KiB of RAM. The 32-bit version does not, as it actually (as currently implemented) is extremely vulnerable to cache timing attacks (look at the code! it indexes on secrets extensively! the horror!). That, of course, will change before tag 1.0.0.

daphne was in the middle of the road, but better than naive implementations. There’s really not much to say. Her port of SHA-2 was able to be proven constant-time using BINSEC/SSE, though, which is fun (if you’re into that sort of thing).

1.6 Acknowledgements

Vi doesn’t actually need my thanks. I gave her my Wii Mini (hyale) to thank her for giving me the Xbox. Nevertheless.


Comments

Please, no LLM-generated comments or content. If I wanted to know what Claude thought, I would indeed have asked Claude.