dagdelean.dev/write-ups/wu-04
Why 416 bits? I designed a block cipher around the number 13
BicBic416 is an experimental 416-bit block cipher that started with one question in my cryptology class: why do block sizes always have to be 128, 256 or 512 bits? It is open source, it passed every test I could throw at its full 16 rounds, and it is still not something you should trust with real secrets. This is the story of how it was built, how I tried to break it, and what that taught me.
It started in a cryptology class
Every cipher we covered in my cryptology class came in the same sizes. AES uses 128-bit blocks with 128-, 192- or 256-bit keys, and SHA-2 gives you 256 or 512 bits. Powers of two, everywhere.
There are good engineering reasons for that: hardware, word sizes and decades of habit. But one question would not leave me alone: what actually goes wrong if a cipher does not fit those numbers? What if I built one on multiples of 13 instead?
So 13 became the backbone. Every block is split into 13 pieces of 32 bits, and 13 × 32 = 416 bits, which is where the name comes from: a 416-bit block and a 416-bit key. The working name was Sarmal-416 (sarmal is Turkish for “spiral”), because a spiral is the shape the design kept returning to.
The short answer to my question, which took the rest of this project to earn: nothing breaks because of the size. Size is not where a cipher’s security comes from, and the rest of this article is about where it does come from.
The design: 13 words and a spiral
I started from a handful of ideas sketched on paper and turned each one into a step of the cipher. One rule was non-negotiable: every step must be reversible, or the message can never be decrypted.
The state is 13 words, S0 to S12. One round runs six steps, and the cipher repeats that round 16 times with a different round key each time:
- Add the key. XOR every word with the round key.
- Multiply by the key. Multiply each word by an odd number derived from the key, then fold the strong high bits down with
x ^= x >> 16. Odd numbers matter: they have an inverse modulo 2³², so decryption can undo the multiplication. My idea: “multiply the message by the key.” - Rotate every row differently. Word i is rotated left by (13·i + 3) mod 32 bits, which gives 13 distinct, non-zero amounts. My idea: a 13-column matrix read diagonally.
- Spiral pairs. The outermost words mix first and the pairs move inward. My idea: XOR the 1st piece with the 13th.
- The center word. S6, the 7th word, absorbs the sum of all twelve others and then spreads itself back into each of them with a different rotation. My idea: give the 7th piece a special role.
- Spiral swap. Swap the pairs from step 4 and reverse the bit order of the center word.
step 4 (pairs): S0-S12 S1-S11 S2-S10 S3-S9 S4-S8 S5-S7
step 5 (center): S6 <- sum of the other 12, then S6 -> back into all 12
Step 5 is what holds the design together. Step 4 only mixes pairs, so without the center word the state would fall apart into six independent pairs plus one loner. With it, after a single round every word depends on every other word.
The key schedule runs the master key through the same kind of mixing, adds a different constant each time, and hands every round its own keys. No two rounds share a key, which closes the door on a class of attacks called slide attacks.
From spec to code
I designed the algorithm and wrote it down as a specification: the state, the six steps, the key schedule and every constant. It also set one rule for whoever implemented it: never quietly change a design constant, report the problem instead. Turning that spec into code was handed to an AI assistant, Claude. That covered the C implementation, the test suites and the analysis tools.
The spec split the work into nine phases, and each one stayed blocked until its tests passed:
- Helper functions (rotations, bit reversal, modular inverse)
- The six round steps and their inverses
- The key schedule
- Block encryption and decryption
- Avalanche measurement
- CTR mode for messages of any length
- Benchmarks
- Fixed test vectors
- Documentation
Two things made the result trustworthy. First, the spec was precise enough that a second, independent implementation in Python could be written from it alone, and the two must agree on every test vector. Second, the decisions stayed mine: the round count, the name, adding ChaCha20 as a second layer and what got published. When the spec was silent, the implementation asked. When it had to bend a rule, such as using a division on a public length that the spec technically forbade, it said so instead of hiding it.
Breaking it on purpose
A cipher you have only tested for correctness is a cipher you know nothing about. So the second half of the project was an attempt to break BicBic416, starting with weakened versions that run fewer rounds.
Avalanche. Flip one input bit, and ideally half of the 416 output bits (208) should change. After one round the average was 192.9 bits with a huge spread (standard deviation 47). After two rounds it was 208.1 with a deviation of 10.2, exactly what pure randomness predicts. Even testing each of the 416 input bits separately, two rounds were enough.
| Test | Weakest version it breaks | Full cipher (16 rounds) |
|---|---|---|
| Differential cryptanalysis | 1 round: some output bits flip every single time | Nothing found |
| PractRand statistical suite | 1 round: fails instantly | Clean after 64 GB |
| Linear and integral analysis | Did not even detect the 1-round version | Nothing found |
| Symmetry checks (complement, rotation, mirror) | Tested at 16 rounds only | None found |
| Timing side channel | Not applicable | No leak: largest t = 2.3, threshold 4.5 |
| Fuzzing the file tool | Not applicable | 20,000 corrupted files, none accepted |
The most useful result was an uncomfortable one: the 2-round version passed 256 GB of PractRand. A differential test breaks this design at one round, yet two rounds already look random to a serious statistical suite. Passing statistical tests tells you very little, and my simple linear and integral tests were weaker still, missing even the broken 1-round version.
The numbers also lied to me once. At two rounds, one differential looked slightly suspicious (z = 4.5). Re-testing that one candidate with 67 million fresh pairs gave z = −0.9: it was noise, picked out because I had searched hundreds of candidates. Lesson: never confirm a finding with the data you found it in.
The real bugs were not in the math. All three were in the code around it:
- A race in the file tool. Decryption checked the file’s authentication tag and then read the file a second time to decrypt it. Anyone able to modify the file between the two reads could slip unverified data into the output. The fix recomputes the tag during the second pass.
- Turkish passwords produced the wrong key on Windows. A password passed through an environment variable was read in the Windows code page instead of UTF-8, so “ğüşıöç” gave a different key than the same password typed by hand. A cross-test between the web app and the command-line tool caught it.
- Undefined behavior that only Linux could see. An empty input passed a NULL pointer to
memcpywith length zero. That is undefined behavior in C even though nothing is copied, and only Linux’s sanitizer flags it. My Windows runs were clean; the first CI run on Linux failed.
An honest verdict
On a scale where 10 is AES or ChaCha20 and 1 is “broken in minutes,” BicBic416 on its own gets a 3 from me. AES and ChaCha20 earn their 10 through decades of public analysis by the best cryptographers without a practical attack.
The 3 is not about a known weakness: the full 16 rounds survived everything I tried. It is about what is unknown:
- Nobody else has looked at it yet. Historically, many ciphers designed outside the research community have fallen quickly once experts looked.
- My tests are the simple ones. They are automated versions of the most basic attacks. Real cryptanalysts build multi-round attack paths by hand.
- Some weaknesses were accepted on purpose. The key schedule can be run backwards, so leaking a single round key reveals the master key. A test recovers it in 1,000 out of 1,000 tries.
This also answers my question from the cryptology class. A 416-bit key looks stronger than AES-256, but nobody can brute-force 256 bits anyway. Security comes from years of attacks that failed, and no block size or key size can shortcut that.
That is why BicBic416 ships with ChaCha20 underneath it. In two-layer mode, the data is combined with two independent keystreams whose keys come from HKDF:
ciphertext = message XOR BicBic416-CTR(key 1) XOR ChaCha20(key 2)
An attacker has to break both. If BicBic416 falls, ChaCha20 still hides the data; if ChaCha20 ever fell, BicBic416 would still be in the way. In that mode I would rate the system an 8 or 9: confidentiality rests on ChaCha20. It is not a 10 because none of the surrounding code has been audited and the whole thing is only as strong as its key or password. Both layers come from the same secret, so a weak password opens both at once.
What it grew into
The cipher was meant to be the whole project. It turned into a small toolkit:
- BicBic-Hash. A 416-bit hash built from the cipher with the Miyaguchi-Preneel construction, the same approach Whirlpool uses. It is slow (about 16 MB/s) because the key schedule runs again for every block.
- A file encryption tool.
bicbicencrypts files with a password using PBKDF2, HKDF and an HMAC-SHA256 seal. It adds only 36 bytes per file, and-2turns on the second layer. - A web app. Text encryption and hashing run entirely in the browser; nothing is sent to a server.
- Log encryption for a SOC. This is the part I actually needed, for my community’s website. Every log line becomes one
bb416r1:token that existing log files and SIEMs can store as-is. It ships as a Python package, a single JavaScript file for Node.js, and command-line filters for any other language. Keys can be rotated without losing old logs.
Hooking it into Python’s standard logging takes a few lines:
import logging, bicbic416
cipher = bicbic416.RecordCipher.from_env() # key read from BICBIC_ANAHTAR ("anahtar" = key)
handler = logging.FileHandler("security.log", encoding="utf-8")
handler.setFormatter(bicbic416.EncryptingFormatter(cipher))
logging.getLogger("security").addHandler(handler)
Timestamps and log levels stay readable, and the message itself is encrypted. A field filter can instead encrypt only the sensitive parts, such as usernames and IP addresses, so analysts can still search by event type.
Measured speeds on one core of an Intel i5-11400H:
| What | Speed |
|---|---|
| Bulk encryption, CPU-specific build (AVX-512) | 1.3 GB/s |
| Bulk encryption, portable build | about 400 MB/s |
| Log records, Python with the C library | 78,000 per second |
| Log records, command-line tool | 59,000 per second |
| Log records, Node.js | 9,400 per second |
| Log records, pure Python | 520 per second |
The bulk speed comes from processing 4, 8 or 16 blocks at once with the compiler’s portable vector types, which took the same compiler on the same machine from about 200 MB/s to 1.3 GB/s. Even the slowest path, pure Python, is far above what a community website produces.
What I learned
- Sizes are conventions, not security. You can build on 13 instead of powers of two, and nothing breaks. Nothing gets stronger either: trust comes from time and other people’s attacks.
- The spec was the most valuable thing I wrote. It was precise enough to hand to an AI, precise enough for an independent second implementation, and it made every test meaningful.
- Passing tests proves less than it feels like. A 2-round cipher passed 256 GB of statistical testing. Tests rule out obvious mistakes; they cannot prove strength.
- Bugs hide in the glue, not the math. All three real bugs were in file handling, text encoding and a C corner case, never in the cipher itself.
- Test on more than one platform. Linux caught a bug that Windows could not see.
- Delegating the code does not delegate the responsibility. The AI wrote the code quickly. Deciding what to build, checking that it is right and being honest about its limits stayed with me.
- Label things honestly. Every page of this project says “experimental.” Writing that down early made every later decision easier.
Try it, and try to break it
Everything is open source under the MIT license:
- Code, specification and cryptanalysis report: github.com/dagdelenemre/bicbic416. The documentation is in Turkish; the README starts with an English summary.
- Web app: bicbic416.vercel.app
- Python package: installs with the command below.
pip install "git+https://github.com/dagdelenemre/bicbic416#subdirectory=python"
Please do not use it to protect real data. For that, use AES-GCM or ChaCha20-Poly1305 from an audited library.
What I would value most is someone breaking it. If you find a distinguisher, a weak key class or a hash collision at any number of rounds, the repository has an issue form for cryptanalysis findings. Attacks on an experimental cipher are not something to hide. They are the whole point of publishing it.