Moving PII Redaction Off the CPU: An FPGA Experiment
Every redaction tool I have built runs on a general-purpose computer. Phileas runs in a JVM, PhEye runs its models in Python, and Philter wraps both of them in a service with an API in front. That is a reasonable way to build software and it is where all the interesting detection work happens, but it does mean the sensitive data lands in host memory and passes through an operating system, an allocator, a scheduler, and a network stack before anything gets around to redacting it.
So I figured I would put some college hardware engineering work to use on a Sipeed Tang Primer 20K and see what happened. What if the redaction happened before the data reached a general-purpose computer at all?
The result is phileas-pii-fpga , where text goes in one UART and redacted text comes out the other. A Social Security Number in the stream gets replaced character by character as it passes through, hyphens kept, and the whole thing is built with an open toolchain.

Why even bother?
It is worth getting one thing out of the way early, which is that none of this is about speed. “PII redaction on an FPGA” sounds like a performance story and it is not one, because a many-core server will beat this at bulk redaction without breaking a sweat. What you get instead is a much smaller thing to trust.
A software redaction service is a big trusted computing base. It is your code, plus a runtime, plus the dependency tree, plus a kernel, plus whatever else happens to be sharing the machine. Any one of those can have a memory-safety bug, and in principle any of them can reach the buffer holding text that has not been redacted yet. Most of that surface is not code you wrote and not code you can realistically audit.
The FPGA version came out at 1,982 LUT4s and 802 flip-flops, which is nine percent and five percent of this part, and nothing at all sits underneath it. There is no operating system, no allocator, and nothing else on the part that can reach its memory. There is no memory-safety bug to exploit because there is no memory model to violate, and there is no route for unredacted bytes other than the one the logic actually implements. It is small enough that you could read the whole data path in an afternoon and know what it does.
The cost of all that is that you cannot change a rule without a synthesis run, which is a real limitation and one I will come back to.
How the matcher works
The engine keeps a 13-byte sliding window, which is the eleven bytes of an SSN plus one boundary byte on each end.
index: 0 1 2 3 4 5 6 7 8 9 10 11 12
role: pre D D D - D D - D D D D post
Each input byte shifts the window along by one, and the whole window gets tested against the pattern on every shift. The test only happens at that one alignment, which sounds like it would miss things, but every byte passes through every index eventually. A candidate starting at any offset gets tested when its first byte reaches index 1, and at that moment all eleven of its bytes are still in the window and available to be masked.
The two boundary bytes are what reject the near misses. Something like 1234-56-7890 fails because a match needs a non-digit on each side. The padding shifted in ahead of the first real byte is 0x00, which is conveniently not a digit, so the start and end of a stream behave like boundaries without needing a special case anywhere in the match test.
The nice property is that it costs exactly one clock per byte no matter what the byte is. There is no backtracking, so you cannot feed it adversarial input and make it fall over the way you can with some regular expression engines. It also means the timing of the output tells you nothing about what the engine found, since a byte that was part of an SSN takes exactly as long as one that was not.
Redaction and policy are set at runtime over the same serial link using a three-byte command protocol. You can turn redaction off, switch the mask character from X to *, or ask it to preserve the last four digits, all without rebuilding the bitstream.
Proving instead of testing
There is a normal testbench, and it does the normal thing well enough. It drives real UART frames in and decodes real UART frames coming out, with 21 checks covering pass-through, redaction, SSNs landing right at the end of a stream, near misses, policy changes in the middle of a stream, sustained back-to-back traffic, and so on. The same suite also runs against the synthesised netlist using Gowin’s own cell models, which is a nice way to close the gap between “my source code behaves” and “the thing the toolchain produced behaves.”
make formal runs SymbiYosys over the engine and asks a different question. Not “did it work on these inputs” but “is this true of every input sequence up to this depth.” The buffer properties go further than that and are proved unbounded by k-induction, so they hold for any trace of any length. The invariant that makes the induction close is that the number of bytes owed out is exactly the number of occupied window slots.
What it does not do
It handles exactly one pattern, and real redaction work needs a lot of them. For names and addresses and organizations it needs the NLP models in PhEye rather than anything fixed, so nothing here replaces that, and I would not want it to.
It is also not the fast way to redact a corpus. Bulk redaction is embarrassingly parallel and a server full of cores will beat this comfortably on throughput. The repository publishes that comparison in ANALYSIS.md rather than hiding it, because a benchmark you have to go looking for is a benchmark somebody is going to assume was bad.
Rules are baked into the bitstream, so adding a pattern means a synthesis run rather than a config reload. If your rule set changes every week then this whole approach is a non-starter.
It has also never been measured on a board. Everything I have reported comes out of simulation and synthesis. Timing closes with a lot of margin and the gate-level netlist behaves the same as the RTL, but only real silicon will tell you about signal integrity, metastability in the input synchroniser, or how any of it behaves across voltage and temperature.
Detection here is pattern-based and probabilistic, the same as everywhere else in the toolkit. It is designed to reduce how much sensitive data gets through rather than to catch every instance, and you should validate the output against your own data.
None of this is new
Pushing pattern matching into hardware is an old idea, and it has been sold commercially for years in network security equipment where inspection has to keep pace with the link it is watching. I am not claiming to have invented anything here. What is worth something in a small open implementation is not novelty, it is that you can read the whole thing.
Where it might go
The interesting direction is not a faster SSN matcher, but the kind of deployment where redaction has to happen somewhere there is no computer to run software on, or where the thing downstream is not trusted with the original bytes in the first place. That might be a tap on a link, a sensor feed, or a capture path inside an air-gapped network, and it might just as easily be a device that ships its data off to somewhere you do not control.
If anyone does want to push the throughput, width is the lever rather than clock speed. The match test is already a flat expression over per-byte classifier bits at a single alignment, so testing a lot of alignments at once is mostly just copies of a shallow expression sharing the same classifier bits. I have not built that and the numbers in ANALYSIS.md for it are estimates, clearly marked as such.
For now it is a small experiment that works, Apache 2.0 and public. The RTL, the testbench, the formal harness, the Docker build, and the analysis are all in phileas-pii-fpga
. If you have a Tang Primer 20K to hand, make docker-all will build it.