├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.6" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 49 | dependencies = [ 50 | "anstyle", 51 | "windows-sys", 52 | ] 53 | 54 | [[package]] 55 | name = "anyhow" 56 | version = "1.0.95" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 59 | 60 | [[package]] 61 | name = "clap" 62 | version = "4.5.23" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" 65 | dependencies = [ 66 | "clap_builder", 67 | "clap_derive", 68 | ] 69 | 70 | [[package]] 71 | name = "clap_builder" 72 | version = "4.5.23" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" 75 | dependencies = [ 76 | "anstream", 77 | "anstyle", 78 | "clap_lex", 79 | "strsim", 80 | ] 81 | 82 | [[package]] 83 | name = "clap_derive" 84 | version = "4.5.18" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 87 | dependencies = [ 88 | "heck", 89 | "proc-macro2", 90 | "quote", 91 | "syn", 92 | ] 93 | 94 | [[package]] 95 | name = "clap_lex" 96 | version = "0.7.4" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 99 | 100 | [[package]] 101 | name = "colorchoice" 102 | version = "1.0.3" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 105 | 106 | [[package]] 107 | name = "heck" 108 | version = "0.5.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 111 | 112 | [[package]] 113 | name = "is_terminal_polyfill" 114 | version = "1.70.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 117 | 118 | [[package]] 119 | name = "proc-macro2" 120 | version = "1.0.92" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 123 | dependencies = [ 124 | "unicode-ident", 125 | ] 126 | 127 | [[package]] 128 | name = "quote" 129 | version = "1.0.38" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 132 | dependencies = [ 133 | "proc-macro2", 134 | ] 135 | 136 | [[package]] 137 | name = "same-file" 138 | version = "1.0.6" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 141 | dependencies = [ 142 | "winapi-util", 143 | ] 144 | 145 | [[package]] 146 | name = "strsim" 147 | version = "0.11.1" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 150 | 151 | [[package]] 152 | name = "stupidfs" 153 | version = "0.2.1" 154 | dependencies = [ 155 | "anyhow", 156 | "clap", 157 | "walkdir", 158 | ] 159 | 160 | [[package]] 161 | name = "syn" 162 | version = "2.0.93" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" 165 | dependencies = [ 166 | "proc-macro2", 167 | "quote", 168 | "unicode-ident", 169 | ] 170 | 171 | [[package]] 172 | name = "unicode-ident" 173 | version = "1.0.14" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 176 | 177 | [[package]] 178 | name = "utf8parse" 179 | version = "0.2.2" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 182 | 183 | [[package]] 184 | name = "walkdir" 185 | version = "2.5.0" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 188 | dependencies = [ 189 | "same-file", 190 | "winapi-util", 191 | ] 192 | 193 | [[package]] 194 | name = "winapi-util" 195 | version = "0.1.9" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 198 | dependencies = [ 199 | "windows-sys", 200 | ] 201 | 202 | [[package]] 203 | name = "windows-sys" 204 | version = "0.59.0" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 207 | dependencies = [ 208 | "windows-targets", 209 | ] 210 | 211 | [[package]] 212 | name = "windows-targets" 213 | version = "0.52.6" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 216 | dependencies = [ 217 | "windows_aarch64_gnullvm", 218 | "windows_aarch64_msvc", 219 | "windows_i686_gnu", 220 | "windows_i686_gnullvm", 221 | "windows_i686_msvc", 222 | "windows_x86_64_gnu", 223 | "windows_x86_64_gnullvm", 224 | "windows_x86_64_msvc", 225 | ] 226 | 227 | [[package]] 228 | name = "windows_aarch64_gnullvm" 229 | version = "0.52.6" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 232 | 233 | [[package]] 234 | name = "windows_aarch64_msvc" 235 | version = "0.52.6" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 238 | 239 | [[package]] 240 | name = "windows_i686_gnu" 241 | version = "0.52.6" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 244 | 245 | [[package]] 246 | name = "windows_i686_gnullvm" 247 | version = "0.52.6" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 250 | 251 | [[package]] 252 | name = "windows_i686_msvc" 253 | version = "0.52.6" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 256 | 257 | [[package]] 258 | name = "windows_x86_64_gnu" 259 | version = "0.52.6" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 262 | 263 | [[package]] 264 | name = "windows_x86_64_gnullvm" 265 | version = "0.52.6" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 268 | 269 | [[package]] 270 | name = "windows_x86_64_msvc" 271 | version = "0.52.6" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 274 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stupidfs" 3 | description = "More files per file: hide files by storing them in the metadata of other files" 4 | repository = "https://github.com/GoldenStack/stupidfs" 5 | version = "0.2.1" 6 | edition = "2021" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | anyhow = "1.0.95" 11 | clap = { version = "4.5.23", features = ["derive"] } 12 | walkdir = "2.5.0" 13 | 14 | [profile.release] 15 | codegen-units = 1 16 | lto = true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2025 GoldenStack 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stupidfs 2 | 3 | ### More files per file: hide files by storing them in the metadata of other files 4 | 5 | --- 6 | 7 | Want to hide files inside other files? Want to make the most out of your 8 | filesystem? 9 | 10 | stupidfs stores files in the metadata of your files, allowing your filesystem to 11 | hold more files per file. 12 | 13 | This can be used as a stealthy way to hide data, or as a useless waste of time. 14 | 15 | ## How does it work? 16 | 17 | stupidfs stores information in the 'last modified date' of files in the target 18 | directory. The data is stored in the sub-second portion of the timestamp, so it 19 | shouldn't have any visible effect when applied to a directory. 20 | 21 | stupidfs is a good[[citation needed](https://en.wikipedia.org/wiki/Wikipedia:Citation_needed)] way to store files 22 | inconspicuously, since it's quite hard to tell if a directory contains data 23 | stored under stupidfs. in fact, the data itself is somewhat volatile: on my 24 | machine, copy pasting the folder updates the last modified date, rendering 25 | useless the idea of copying data from an existing drive for later use. 26 | 27 | ## Usage 28 | 29 | First, run `cargo install stupidfs` or compile it from source. 30 | 31 | To read, give stupidfs a directory and read the data from it. 32 | ```sh 33 | stupidfs -o ./data 34 | ``` 35 | 36 | To write, give stupidfs a directory and pipe data into it. 37 | 38 | ```sh 39 | echo "Hello, world!" | stupidfs -i ./data 40 | ``` 41 | 42 | Some notes: 43 | - Output mode is enabled by default, so the `-o` is optional. 44 | - A warning will be produced while writing if extra bytes are provided. 45 | - One file is required for every two bytes of input. 46 | 47 | stupidfs works on files with at least 128 nanosecond granularity. This includes 48 | NTFS and ext4, and doesn't include FAT32 and ext4 with small inodes. 49 | 50 | 51 | ## Visibility 52 | stupidfs stores data in the last few bits of the last modification date of each 53 | file (only up to granularity of 128ns so that NTFS is supported). The first 54 | few bits of the sub-second portion are not touched, meaning the actual change 55 | in the timestamp is very small (up to 8.388ms). 56 | 57 | ## Accuracy 58 | A filesystem supporting 128ns timestamp resolution does not necessarily mean the 59 | kernel and hardware support timestamps of such a resolution. 60 | 61 | If the hardware doesn't support this resolution or the kernel doesn't update 62 | the system time that often, machine times might end up clustered around (or 63 | exactly on) some greatest common divisor (e.g. microseconds). If this is the 64 | case, it'll be pretty easy to figure out which files were artificially modified. 65 | 66 | The fact that the last 128ns is untouched helps with this somewhat since 67 | modifying dates at a lower resolution is compatible with all higher resolutions, 68 | but this still shouldn't be relied on. 69 | 70 | ## Information 71 | Technically, more than 2 bytes of information is used per file, as other file 72 | metadata (e.g. filenames) is used to determine where a given file's bytes are 73 | situated within the actual stored data. This isn't particularly important 74 | though, since it's not correlated with actual file contents and can't be used to 75 | reconstruct anything—it's just an interesting technicality that is probably 76 | worth mentioning. 77 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::{File, Metadata}, io::{stdin, stdout, Read, Write}, path::PathBuf, time::{Duration, SystemTime}}; 2 | 3 | use anyhow::Result; 4 | use clap::Parser; 5 | use walkdir::WalkDir; 6 | 7 | #[derive(Parser, Debug)] 8 | #[command(version, about)] 9 | struct Args { 10 | 11 | /// The path of the root directory in which files will be stored 12 | #[clap(default_value = ".")] 13 | pub path: PathBuf, 14 | 15 | /// Writes all input to the root directory 16 | #[clap(short, group = "mode")] 17 | pub input: bool, 18 | 19 | /// Outputs all data stored in the root directory 20 | #[clap(short, group = "mode", default_value = "true")] 21 | pub output: bool, 22 | 23 | } 24 | 25 | 26 | fn main() -> Result<()> { 27 | let args = Args::parse(); 28 | 29 | 30 | let files = WalkDir::new(args.path) 31 | .sort_by_file_name() 32 | .into_iter() 33 | .filter_map(Result::ok) 34 | .filter_map(|dir| dir.metadata().ok().filter(Metadata::is_file).map(|meta| (dir, meta))); 35 | 36 | if args.input { 37 | let mut input = stdin().lock(); 38 | 39 | let mut data = [0u8; 2]; 40 | 41 | for (dir, _) in files { 42 | data.fill(0); 43 | input.read(&mut data)?; 44 | 45 | set(&File::open(dir.path())?, data)?; 46 | } 47 | 48 | let mut provided = 0; 49 | while let Some(read) = input.read(&mut [0; 1000]).ok().filter(|&c| c > 0) { 50 | provided += read; 51 | } 52 | 53 | if provided > 0 { 54 | eprintln!("Failed to write last {provided} byte(s): not enough files"); 55 | } 56 | } else if args.output { 57 | let mut out = stdout().lock(); 58 | 59 | for (_, meta) in files { 60 | let data = get(meta)?; 61 | out.write(&data)?; 62 | } 63 | 64 | out.flush()?; 65 | } else { 66 | // Unreachable because flags cannot be disabled, 67 | // and output is true by default. 68 | unreachable!(); 69 | } 70 | 71 | 72 | Ok(()) 73 | } 74 | 75 | /// Gets the two bytes from a file's metadata, reading from some of its last 76 | /// modified date. 77 | fn get(metadata: Metadata)-> Result<[u8; 2]> { 78 | let nanos = metadata.modified()? 79 | .duration_since(SystemTime::UNIX_EPOCH)? 80 | .subsec_nanos(); 81 | 82 | let data = ((nanos >> 7) & 0xffff) as u16; 83 | 84 | Ok(data.to_le_bytes()) 85 | } 86 | 87 | /// Sets the two bytes in a file's metadata, modifying some of its last 88 | /// modified date. 89 | fn set(file: &File, data: [u8; 2]) -> Result<()> { 90 | let time = file.metadata()?.modified()?; 91 | 92 | // Read the nanosecond part of the time 93 | let nanos = time.duration_since(SystemTime::UNIX_EPOCH)?.subsec_nanos() as u64; 94 | 95 | // Clear the 16 destination bits 96 | let new_nanos = nanos & !(0xffff << 7); 97 | 98 | // Add them back from our trusted source 99 | let new_nanos = new_nanos | (u16::from_le_bytes(data) as u64) << 7; 100 | 101 | // Subtract old nanos and add new ones 102 | let new_time = time - Duration::from_nanos(nanos) + Duration::from_nanos(new_nanos); 103 | 104 | file.set_modified(new_time)?; 105 | 106 | Ok(()) 107 | } 108 | --------------------------------------------------------------------------------