- Rust 96%
- Nix 4%
playfair-rs - Playfair Cipher in Rust. #
Originally was an assignment for my CS 303: Databases and Information Security class, I wanted to take some more time and implement it again. This is my solution to implementing the Playfair Cipher in Rust.
Design choices #
Some implementations omit the letter 'q' in encryption/decryption, some omit 'j', and some equate 'i' to 'j'. For my implementation, I went with 'i' = 'j', since that is what the Wikipedia article's example followed, and a random online playfair cipher website used by default. This allowed for easy verification of my implementation.
I also used a task runner (originally a Makefile, now a justfile) to enforce
strict commenting of all my functions to ensure I knew what was going on at that line and
function. As well thanks to the awesome Rust ecosystem,
it enables generation of documentation from a single just doc command. That documentation
can be found in the ./target/doc/playfair/ directory afterwards.
I also used testing extensively, for each part from keyword generation, matrix computation, character location, to full integration testing. Combining this with assertions in the code, I have a pretty good idea that my code is correct.
Examples #
You can see an example in main.rs, or here is a simple shown implementation:
Encryption steps:
use playfair::{Cipher, Playfair};
// Example from https://en.wikipedia.org/wiki/Playfair_cipher.
fn main() {
let pf = Playfair::new("playfair example");
let out = pf.encrypt("Hide the gold in the tree stump.");
// out = bmodzbxdnabekudmuixmmouvif
}
Decryption steps:
use playfair::{Cipher, Playfair};
// Example from https://en.wikipedia.org/wiki/Playfair_cipher.
fn main() {
let pf = Playfair::new("playfair example");
let out = pf.decrypt("bmodzbxdnabekudmuixmmouvif");
// out = hidethegoldinthetrexestump
// NOTE: the extra 'x' here ^ is expected since it was inserted during the encryption process.
// Read more about the Playfair cipher to understand why.
}
Development #
just ci runs every check CI enforces, in the same order, so a green run
locally means a green run on the PR:
just ci
just --list shows the individual recipes — fmt, lint, build, test, doc and
bench — if you want to run one at a time. Benchmarks are deliberately left out of ci,
being far too slow and noisy to gate a pull request on.
CI is .tangled/workflows/ci.yml, which runs just ci itself
on a NixOS microVM, so there is nothing to keep in sync by hand. It pins the toolchain
through rustup to the release the clippy deny list was tuned against.
On NixOS there is no
cconPATHby default, so anycargocommand that links fails withlinker `cc` not found, andrustfmt/clippyare separate packages fromrustc.shell.nixsupplies all of them plusjust:nix-shell # then: just ci
Releasing #
Cargo.toml is the single source of truth for the version, and the git tag
is derived from it — never the other way round. There is no manual tagging or publishing
step.
- In your PR, bump
versioninCargo.tomlfollowing semver. - Get the PR green and merge it into
main. .tangled/workflows/release.ymldoes the rest: it reads the version, checks whetherv<version>is already tagged, and if not, runs the tests, verifies the package, publishes to crates.io, then pushes the tag.
A merge that does not bump the version is simply not a release — the workflow finds the existing tag and exits. That makes it safe to merge as often as you like, and safe to re-run a failed release by merging again.
Publishing happens before tagging on purpose: crates.io is immutable and a tag is not, so a tag always means "this version really is on crates.io". In the one awkward case where the publish succeeds but the tag push fails, tag it by hand:
git tag -a v<version> -m "Release v<version>"
git push origin v<version>
Three things the release depends on:
- A
CARGO_REGISTRY_TOKENrepository secret holding a crates.io API token with publish rights on theplayfaircrate. RELEASE_HANDLEandRELEASE_APP_PASSWORDrepository secrets. A spindle hands a workflow no write access to its own repository, so the tag push mints an atproto service-auth token from this account's app password, addressed to the repository and good only forreceivePack. The app password itself can do far more than push a tag, which is why only runs of this repository's own branches ever see it.rust-versioninCargo.tomlstaying honest. Bump it whenever a change starts leaning on a newer stdlib API, so downstream users get a clear toolchain error rather than a confusing compile failure.