Building Secure File Encryption with XChaCha20-Poly1305
A deep dive into the cryptographic choices behind SwatCrypt — why XChaCha20 over AES-GCM, how Argon2id key derivation works, and the pitfalls I avoided.
Read article →18 Apr 2026 7 min read
Every Windows admin has seen this dialog. "The action can't be completed because the file is open in another program." No hint at which program. No PID. No process name. Just: go away.
The built-in answer is usually Resource Monitor → CPU tab → Associated Handles. It works, but it's slow, cluttered, and doesn't help when the thing holding the file is a remote SMB session from another machine on your network.
I got tired of it and wrote two tools. One in Rust. One in pure C. Same problem, different universes. This post is about what each one does well, what they don't do (which is the interesting bit), and what you should reach for when the "file in use" dialog appears.
Windows has at least four different things that can make a file appear "locked":
An open `HANDLE` with `FILE_SHARE_*` flags that don't permit your access. This is the default case when a normal app opens a file for writing without FILE_SHARE_WRITE.
An NTFS byte-range lock via LockFileEx. Less common, but databases use this heavily.
A memory-mapped file (CreateFileMapping). These linger even after the mapping view is closed, until the section object is released.
An open file from a remote SMB session — somebody on your network has mounted your share and has a file open.
Native handles (#1–#3) need access to the kernel's handle tables. Remote SMB handles (#4) need a completely different API. Most people conflate the two, so let's not.
If what you actually want is "who on my network has my file open", the answer is `NetFileEnum` from netapi32.dll. It enumerates open files across all active SMB sessions on the local machine. You pass it an optional path prefix and it returns a FILE_INFO_3 array with the user, the path, the number of locks, and — critically — an fi3_id that you can pass to NetFileClose to forcibly terminate the open.
Both of my tools use this API. Both of them require admin elevation, because the underlying RPC to the Server service (srv.sys) rejects calls from non-admins. And both of them share a caveat worth shouting: `NetFileEnum` only returns files held open over SMB. It does not see handles held by local processes. If Notepad on the same machine is holding C:foo.txt, NetFileEnum will tell you nothing.
That's a big caveat and it's why, even with my tools installed, I still keep Process Explorer in my admin toolkit. NetFileEnum is for the "someone mounted my share and went to lunch" case. Local handles are Process Explorer / Handle.exe / NtQuerySystemInformation(SystemExtendedHandleInformation) territory.
LockSmith is ~4 MB, built with egui, and runs in ~20 MB of RAM. The whole thing is a single eframe::App that calls NetFileEnum on demand and renders the results in a table. The UI is deliberately sparse: a filter box, a refresh button, a close-selected button, and a confirmation dialog when you do the dangerous thing.
The reason I reached for egui instead of Tauri is boring but real: cold-start time. On a box where you're opening this tool because something is stuck, the difference between 0.5 seconds (egui, statically compiled) and 1.5 seconds (Tauri, WebView2 spin-up) matters. egui also means no WebView2 runtime dependency — the tool is a single executable that works on any Windows 10 or later machine without installing anything else.
The FFI surface is small:
#[link(name = "netapi32")] extern "system" { fn NetFileEnum( servername: *const u16, basepath: *const u16, username: *const u16, level: u32, bufptr: *mut *mut u8, prefmaxlen: u32, entries_read: *mut u32, total_entries: *mut u32, resume_handle: *mut u32, ) -> u32;fn NetFileClose(servername: *const u16, fileid: u32) -> u32; fn NetApiBufferFree(buffer: *mut u8) -> u32;
}
The pattern you resume against (resume_handle) is important if the machine has a lot of open files — you don't get the full set in one call. Forgetting to loop is the rookie bug.
HandleHunter does the same job. It is 134 KB. It has no C runtime — no stdio, no malloc, no memcpy, no C standard library at all.
This sounds like a pointless party trick. It isn't, and I'll tell you why. A tool you run when things are broken should be as close to "static" as possible. No runtime means no DLL loader surprises, no C runtime version mismatches on ancient Windows Server installations, no "this program cannot start because VCRUNTIME140.dll is missing" when you copy it to a recovery partition.
The cost is that you write your own everything. StrLen, MemCopy, integer-to-string conversion. Memory allocation goes through HeapAlloc on the process heap. The GUI is raw Win32 — CreateWindowExW, SendMessageW, the whole ListView dance — with DWM calls for the modern theming and rounded corners on Windows 11.
Is that a good trade? For a tool this small, yes. The ListView code is maybe 200 lines, the NetFileEnum wrapper is maybe 60, the dynamic array is 80. You can read the entire codebase in an afternoon and be confident nothing is happening that you didn't write.
For a tool any larger, you'd be insane to go this route. A 2,000-line CRT-free GUI app is a maintenance tax you don't want to pay.
Same problem. Same underlying API. Different values:
| | LockSmith | HandleHunter |
|—|—|—|
| Binary size | ~4 MB | ~134 KB |
| Dependencies | egui, Windows SDK crates | netapi32.lib + raw Win32 |
| Cold-start | ~0.5 s | instant |
| LoC (approximate) | ~2,500 | ~1,500 |
| Portability | Any Win10+ with no install | Any Win10+, no runtime, no install |
| Readability of internals | Rust is kinder | C is smaller |
The LockSmith codebase is easier to contribute to. The HandleHunter binary is the one I'd put on a USB stick for emergency use on a server I've never logged into before.
Both tools only handle the SMB case. Neither sees local handles. If Notepad on the server has C:important.txt open, these tools will shrug.
For that, you still want:
`Handle.exe` from Sysinternals, the classic. Can enumerate handles across all processes if you run it elevated.
Process Explorer → Find → Find Handle or DLL. The interactive version of the same thing.
Programmatically: `NtQuerySystemInformation` with `SystemExtendedHandleInformation`, then NtQueryObject with ObjectNameInformation to resolve the handle's path. This is how Sysinternals does it, and it's the honest answer if you want to write your own.
Why didn't I just build that into LockSmith/HandleHunter? Two reasons. First, NtQuerySystemInformation is undocumented and the structures have changed between Windows versions more than once. Building on it is fine for a one-off tool; building it into something you might have to keep working on Windows Server 2030 is a different calculation. Second, the SMB case and the local case have different audiences: the SMB case is admin hygiene (who's holding the share hostage?), the local case is app debugging (why is my service stuck?). Separating them is arguably a feature.
File locked from a network share, you're on the file server: LockSmith or HandleHunter. Both use NetFileEnum and can force-close. Prefer HandleHunter if you're running off a rescue medium, LockSmith if you want a nicer UI.
File locked by a local process, you know which process: close it normally. You've probably already done that if you're reading this.
File locked by a local process, you don't know which: Sysinternals Handle.exe (handle64.exe -a -u C:pathtofile), or Process Explorer. Until I build a Rust tool for that too.
NTFS byte-range lock held by a crashed database: you're going to have to kill the database process. There's no API to release someone else's LockFileEx lock.
The more interesting lesson isn't any specific tool — it's that "file is in use" is actually four different problems with four different solutions, and most admins reach for one tool and wonder why it doesn't work on the other three cases. If a dialog appears on your screen tomorrow, now you know which of the four it is.
Both tools are on my GitHub: LockSmith, HandleHunter. MIT licensed. PRs welcome, especially if you'd like to take a swing at the NtQuerySystemInformation path for local handles.