Tabuni is a desktop toolkit for macOS, built with Java/JavaFX, aimed at keeping your files organized and your private files actually private. It's a multi-tool app — a left-hand rail switches between independent tools — rather than one do-everything screen, so new privacy-focused tools can be added over time without disturbing the ones that already exist.
Two tools ship today:
- Files — a fast, drag-and-drop file browser with three one-click cleanup
operations layered on top: organize photos/videos/files into
Category/Year/Monthfolders by their real creation date, find and remove duplicate files by checksum, and export a full metadata report (CSV/JSON). - Video Vault — encrypt videos into a personal, password-protected
.vvltvault file, then play them back in the app. Nobody without the password — not even someone with full access to the file — can recover the video. Supports encrypting one video at a time or a whole folder at once, and playing one vault or several back-to-back.
Just want to run Tabuni, not build it? Get a Tabuni-<version>.dmg from
whoever built one (see below) and follow INSTALL-DMG.md —
drag-and-drop, no Java or Maven needed.
- macOS. The Files module shells out to
open -Rto reveal files in Finder and usesDesktop.MOVE_TO_TRASH, so it's macOS-only for now. - Java 21 (JDK, not just a JRE) and Maven 3.9+. See
INSTALL.mdif you don't already have these — that guide walks through installing them from scratch, no prior experience assumed. - Internet access the first time you build, so Maven can download dependencies (JavaFX, AtlantaFX, Ikonli icons, libsodium/JNA for the vault's crypto, JUnit).
exiftoolis optional (brew install exiftool) — the Organize/Duplicates tools use it for accurate photo/video creation dates when present, and fall back to filesystem timestamps when it's missing.
# Development: compiles and launches in one step, fastest way to try changes
mvn javafx:run
# Or build a standalone runnable jar and launch that
mvn clean package
java -jar target/tabuni.jar
# Or package a .dmg to hand to someone else (no Java/Maven needed on their end)
./scripts/build-dmg.shmvn clean package also runs the test suite (mvn test on its own runs just
the tests). The packaged jar is a "fat jar" (via maven-shade-plugin) — it
bundles every dependency, so target/tabuni.jar is the only file you need to
hand someone else to run the app, as long as they have Java 21 installed.
src/main/java/com/nagacoder/tabuni/
├── Main.java — JavaFX Application entry point
├── ui/shell/ — Module interface, AppShell (root layout), ModuleRail (the left icon strip)
├── ui/modules/ — one class per tool, registered in ModuleRegistry
├── ui/ — Files module's screens (MainView, Sidebar, Toolbar, FileTableView, ...)
│ and Video Vault's screens (VaultLibraryView, VideoPlayerStage)
├── ui/dialogs/ — popup dialogs (Organize, Find Duplicates, Metadata, Encrypt Video, ...)
├── service/ — the actual logic behind each tool, independent of any UI
├── model/ — plain data classes passed between service and UI layers
└── vault/ — the .vvlt encryption format and its crypto primitives
Adding a new tool means writing a class that implements ui/shell/Module.java
and registering it in ui/modules/ModuleRegistry.java — the shell, rail, and
window-title handling are already generic.
The goal: your own computer's disk, and anyone who gets hold of a .vvlt
file, never sees plaintext video — only you, with the correct password, can
produce it. This isn't hardware DRM: once the app decrypts a frame to play
it, that frame exists in normal process memory like any other video player,
so it doesn't defend against someone who already has your unlocked computer
and the password doing a screen recording. What it defends against is the
file itself leaking — a stolen laptop, a backup uploaded somewhere, a shared
drive — without the encryption ever having to be undone first.
Where the code lives: com.nagacoder.tabuni.vault (VvltFormat,
VvltHeader, CryptoUtils, Packager, Unpacker) implements the format;
service/VaultService.java is the only thing outside that package that
touches it.
Encrypting a video generates a fresh random 256-bit key (the DEK, Data Encryption Key) that does the actual encrypting. Your password never encrypts the video directly — instead:
- Your password + a random 16-byte salt go through Argon2id (via libsodium), a memory-hard password-hashing algorithm — 128 MiB of memory and 3 iterations by default — producing a 256-bit KEK (Key Encryption Key). Argon2id is deliberately slow and memory-expensive so that brute- forcing the password (rather than the encryption itself) is impractical, unlike a fast hash like plain SHA-256.
- The KEK does exactly one small job: it encrypts (wraps) the DEK with AES-256-GCM.
- The DEK encrypts the actual video content, in 1 MiB chunks, also with AES-256-GCM.
Splitting DEK and KEK like this means changing your password later only means re-wrapping a 32-byte key, not re-encrypting the whole video (not currently exposed in the UI, but the format supports it) — and two identical videos encrypted separately still get different DEKs and different ciphertext, even with the same password.
┌─ HEADER (134 bytes, fixed size) ─────────────────────────────────┐
│ magic "VVLT" (4) │ version (1) │ SHA-256 of plaintext (32) │
│ Argon2id salt (16) │ mem cost (4) │ time cost (4) │ lanes (1) │
│ wrapped DEK: nonce+ciphertext+tag (60) │ chunk size (4) │
│ total plaintext length (8) │
└───────────────────────────────────────────────────────────────────┘
┌─ CHUNK 0 ─┬─ CHUNK 1 ─┬─ ... ─┬─ CHUNK N (partial) ─┐
│ nonce(12)+ciphertext(1 MiB)+tag(16) each, except the last chunk │
└───────────────────────────────────────────────────────────────────┘
Every chunk (bar the last) is exactly the same size on disk, so any chunk's
byte offset is header_size + i * (chunk_size + 28) — computable directly,
no index needed. That's what would let a future streaming player fetch and
decrypt just the bytes around a given playback position instead of the whole
file (today's player still fully decrypts up front — see below — but the
format was designed with that door open).
Every AES-256-GCM encryption in the format is bound with additional authenticated data (AAD): the wrapped DEK's encryption is bound to the entire rest of the header (so tampering with the KDF params or chunk size is caught as a decrypt failure, not silently misapplied), and each chunk's encryption is bound to its own index (so chunks can't be silently reordered or spliced from a different file). After decrypting, the whole plaintext is also re-hashed and checked against the SHA-256 stored in the header, as a final tamper/corruption check.
Unlike the format's own streaming-friendly design, Tabuni's player currently
takes the simple route: it fully decrypts the vault to a temporary file, then
hands that file to a javafx.scene.media.MediaPlayer embedded right in the
app (not an external player). The app owns that temp file's whole lifecycle
— it's created fresh per playback in its own throwaway temp directory, and
deleted the moment you close the player window, rather than lingering on
disk.
| Purpose | Library |
|---|---|
| Argon2id (password → key) | com.goterl:lazysodium-java (a libsodium binding) |
| AES-256-GCM (bulk + key-wrap encryption) | Java's built-in javax.crypto.Cipher |
| Embedded video playback | org.openjfx:javafx-media |
mvn testCovers the .vvlt format round-trip (encrypt → decrypt → identical
plaintext), wrong-password rejection, exact-chunk-boundary edge cases, and
the VaultService glue (temp-file handling, folder listing, name-collision
handling).
- macOS only, for the reasons above.
- The
.vvltformat doesn't store the original file extension, so decrypted temp files are always treated as.mp4. - Bulk-playing several vaults at once assumes they all share one password — any that don't are reported as skipped rather than failing the whole batch.
- No password recovery, by design — losing the password means losing the video. There is nothing to reset.