├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── SECURITY.md ├── aft-crypto ├── Cargo.toml └── src │ ├── bip39.rs │ ├── data.rs │ ├── errors.rs │ ├── exchange.rs │ ├── lib.rs │ └── password_generator.rs ├── aft-relay.service ├── aft ├── Cargo.toml └── src │ ├── clients.rs │ ├── config.rs │ ├── constants.rs │ ├── errors.rs │ ├── main.rs │ ├── relay.rs │ ├── sender.rs │ └── utils.rs ├── assets └── fail2ban │ ├── aft-relay-filter.conf │ └── aft-relay.conf ├── docs ├── CONFIG.md └── PROTOCOL.md └── install.sh /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | target-branch: master 8 | - package-ecosystem: "cargo" 9 | directory: "aft/" 10 | schedule: 11 | interval: "daily" 12 | target-branch: master 13 | - package-ecosystem: "cargo" 14 | directory: "aft-crypto/" 15 | schedule: 16 | interval: "daily" 17 | target-branch: master 18 | - package-ecosystem: github-actions 19 | directory: "/" 20 | schedule: 21 | interval: daily 22 | target-branch: master 23 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | tags: ["[0-9]+.[0-9]+.[0-9]+"] 7 | paths-ignore: 8 | - "LICENSE-MIT" 9 | - "LICENSE-APACHE" 10 | - "README.md" 11 | - "SECURITY.MD" 12 | - ".gitignore" 13 | - "docs/**" 14 | - "install.sh" 15 | - "aft-relay.service" 16 | pull_request: 17 | branches: [ "master" ] 18 | 19 | env: 20 | CARGO_TERM_COLOR: always 21 | 22 | jobs: 23 | build: 24 | strategy: 25 | matrix: 26 | include: 27 | - build: Linux x86-64 28 | os: ubuntu-22.04 29 | target: x86_64-unknown-linux-gnu 30 | archive_name: aft-linux-x86_64.gz 31 | 32 | - build: Windows x86-64 33 | os: windows-latest 34 | target: x86_64-pc-windows-gnu 35 | archive_name: aft-windows-gnu-x86_64.zip 36 | 37 | - build: Windows x86-64 MSVC 38 | os: windows-latest 39 | target: x86_64-pc-windows-msvc 40 | archive_name: aft-windows-msvc-x86_64.zip 41 | 42 | - build: macOS x86-64 43 | os: macos-latest 44 | target: x86_64-apple-darwin 45 | archive_name: aft-macos-x86_64.gz 46 | 47 | - build: macOS AArch64/ARM 48 | os: macos-latest 49 | target: aarch64-apple-darwin 50 | archive_name: aft-macos-aarch64.gz 51 | runs-on: ${{ matrix.os }} 52 | steps: 53 | - uses: actions/checkout@v4 54 | 55 | - name: Install Rust 56 | run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal 57 | 58 | - name: Add target 59 | run: rustup target add ${{ matrix.target }} 60 | 61 | - name: Build release 62 | run: | 63 | cargo update 64 | cargo build --features full --release --verbose --target ${{ matrix.target }} 65 | 66 | - name: Compress Windows 67 | if: matrix.os == 'windows-latest' 68 | shell: pwsh 69 | run: Get-ChildItem -Path ./target -Recurse -Filter 'aft.exe' | Compress-Archive -DestinationPath ./${{ matrix.archive_name }} 70 | 71 | - name: Compress Linux/macOS 72 | if: matrix.os != 'windows-latest' 73 | run: gzip -cN target/**/release/aft > ${{ matrix.archive_name }} 74 | 75 | - uses: actions/upload-artifact@v4.6.0 76 | with: 77 | name: ${{ matrix.archive_name }} 78 | path: ./${{ matrix.archive_name }} 79 | 80 | publish-release: 81 | runs-on: ubuntu-latest 82 | needs: [build] 83 | permissions: 84 | contents: write 85 | env: 86 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | 88 | steps: 89 | - uses: actions/checkout@v4 90 | 91 | - uses: actions/download-artifact@v4 92 | with: 93 | path: ~/archives/ 94 | 95 | - name: Create release 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | id: create_release 99 | if: startsWith(github.ref, 'refs/tags/') 100 | run: | 101 | TAG=$(git describe --tags --abbrev=0) 102 | gh release create $TAG --draft --notes "# What's new since the latest release" --generate-notes --title "v$TAG" ~/archives/**/* 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | # vim files 3 | *.swp 4 | *.swo 5 | *.swn 6 | *.swm 7 | 8 | *.txt 9 | *.tmp 10 | 11 | # project's files 12 | .config 13 | .blocks 14 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aead" 22 | version = "0.5.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" 25 | dependencies = [ 26 | "crypto-common", 27 | "generic-array", 28 | ] 29 | 30 | [[package]] 31 | name = "aes" 32 | version = "0.8.3" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" 35 | dependencies = [ 36 | "cfg-if", 37 | "cipher", 38 | "cpufeatures", 39 | ] 40 | 41 | [[package]] 42 | name = "aes-gcm" 43 | version = "0.10.3" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" 46 | dependencies = [ 47 | "aead", 48 | "aes", 49 | "cipher", 50 | "ctr", 51 | "ghash", 52 | "subtle", 53 | ] 54 | 55 | [[package]] 56 | name = "aft" 57 | version = "8.0.3" 58 | dependencies = [ 59 | "aft-crypto", 60 | "env_logger", 61 | "json", 62 | "log", 63 | "rayon", 64 | "rpassword", 65 | "sha2", 66 | "tokio", 67 | "whirlwind", 68 | ] 69 | 70 | [[package]] 71 | name = "aft-crypto" 72 | version = "1.2.6" 73 | dependencies = [ 74 | "aes-gcm", 75 | "rand", 76 | "rand_core 0.6.4", 77 | "x25519-dalek", 78 | "zeroize", 79 | ] 80 | 81 | [[package]] 82 | name = "aho-corasick" 83 | version = "1.1.2" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 86 | dependencies = [ 87 | "memchr", 88 | ] 89 | 90 | [[package]] 91 | name = "allocator-api2" 92 | version = "0.2.21" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 95 | 96 | [[package]] 97 | name = "anstream" 98 | version = "0.6.12" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540" 101 | dependencies = [ 102 | "anstyle", 103 | "anstyle-parse", 104 | "anstyle-query", 105 | "anstyle-wincon", 106 | "colorchoice", 107 | "utf8parse", 108 | ] 109 | 110 | [[package]] 111 | name = "anstyle" 112 | version = "1.0.6" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 115 | 116 | [[package]] 117 | name = "anstyle-parse" 118 | version = "0.2.3" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 121 | dependencies = [ 122 | "utf8parse", 123 | ] 124 | 125 | [[package]] 126 | name = "anstyle-query" 127 | version = "1.0.2" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 130 | dependencies = [ 131 | "windows-sys 0.52.0", 132 | ] 133 | 134 | [[package]] 135 | name = "anstyle-wincon" 136 | version = "3.0.2" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 139 | dependencies = [ 140 | "anstyle", 141 | "windows-sys 0.52.0", 142 | ] 143 | 144 | [[package]] 145 | name = "backtrace" 146 | version = "0.3.69" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 149 | dependencies = [ 150 | "addr2line", 151 | "cc", 152 | "cfg-if", 153 | "libc", 154 | "miniz_oxide", 155 | "object", 156 | "rustc-demangle", 157 | ] 158 | 159 | [[package]] 160 | name = "bitflags" 161 | version = "2.9.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 164 | 165 | [[package]] 166 | name = "block-buffer" 167 | version = "0.10.4" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 170 | dependencies = [ 171 | "generic-array", 172 | ] 173 | 174 | [[package]] 175 | name = "bytes" 176 | version = "1.5.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 179 | 180 | [[package]] 181 | name = "cc" 182 | version = "1.0.84" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" 185 | dependencies = [ 186 | "libc", 187 | ] 188 | 189 | [[package]] 190 | name = "cfg-if" 191 | version = "1.0.0" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 194 | 195 | [[package]] 196 | name = "cipher" 197 | version = "0.4.4" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 200 | dependencies = [ 201 | "crypto-common", 202 | "inout", 203 | ] 204 | 205 | [[package]] 206 | name = "colorchoice" 207 | version = "1.0.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 210 | 211 | [[package]] 212 | name = "cpufeatures" 213 | version = "0.2.11" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" 216 | dependencies = [ 217 | "libc", 218 | ] 219 | 220 | [[package]] 221 | name = "crossbeam-deque" 222 | version = "0.8.5" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 225 | dependencies = [ 226 | "crossbeam-epoch", 227 | "crossbeam-utils", 228 | ] 229 | 230 | [[package]] 231 | name = "crossbeam-epoch" 232 | version = "0.9.18" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 235 | dependencies = [ 236 | "crossbeam-utils", 237 | ] 238 | 239 | [[package]] 240 | name = "crossbeam-utils" 241 | version = "0.8.20" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 244 | 245 | [[package]] 246 | name = "crypto-common" 247 | version = "0.1.6" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 250 | dependencies = [ 251 | "generic-array", 252 | "rand_core 0.6.4", 253 | "typenum", 254 | ] 255 | 256 | [[package]] 257 | name = "ctr" 258 | version = "0.9.2" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" 261 | dependencies = [ 262 | "cipher", 263 | ] 264 | 265 | [[package]] 266 | name = "curve25519-dalek" 267 | version = "4.1.3" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" 270 | dependencies = [ 271 | "cfg-if", 272 | "cpufeatures", 273 | "curve25519-dalek-derive", 274 | "fiat-crypto", 275 | "rustc_version", 276 | "subtle", 277 | "zeroize", 278 | ] 279 | 280 | [[package]] 281 | name = "curve25519-dalek-derive" 282 | version = "0.1.1" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" 285 | dependencies = [ 286 | "proc-macro2", 287 | "quote", 288 | "syn", 289 | ] 290 | 291 | [[package]] 292 | name = "digest" 293 | version = "0.10.7" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 296 | dependencies = [ 297 | "block-buffer", 298 | "crypto-common", 299 | ] 300 | 301 | [[package]] 302 | name = "either" 303 | version = "1.13.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 306 | 307 | [[package]] 308 | name = "env_filter" 309 | version = "0.1.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" 312 | dependencies = [ 313 | "log", 314 | "regex", 315 | ] 316 | 317 | [[package]] 318 | name = "env_logger" 319 | version = "0.11.6" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" 322 | dependencies = [ 323 | "anstream", 324 | "anstyle", 325 | "env_filter", 326 | "humantime", 327 | "log", 328 | ] 329 | 330 | [[package]] 331 | name = "equivalent" 332 | version = "1.0.2" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 335 | 336 | [[package]] 337 | name = "fiat-crypto" 338 | version = "0.2.4" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "53a56f0780318174bad1c127063fd0c5fdfb35398e3cd79ffaab931a6c79df80" 341 | 342 | [[package]] 343 | name = "foldhash" 344 | version = "0.1.4" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" 347 | 348 | [[package]] 349 | name = "generic-array" 350 | version = "0.14.7" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 353 | dependencies = [ 354 | "typenum", 355 | "version_check", 356 | ] 357 | 358 | [[package]] 359 | name = "getrandom" 360 | version = "0.2.11" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 363 | dependencies = [ 364 | "cfg-if", 365 | "libc", 366 | "wasi 0.11.0+wasi-snapshot-preview1", 367 | ] 368 | 369 | [[package]] 370 | name = "getrandom" 371 | version = "0.3.2" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 374 | dependencies = [ 375 | "cfg-if", 376 | "libc", 377 | "r-efi", 378 | "wasi 0.14.2+wasi-0.2.4", 379 | ] 380 | 381 | [[package]] 382 | name = "ghash" 383 | version = "0.5.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" 386 | dependencies = [ 387 | "opaque-debug", 388 | "polyval", 389 | ] 390 | 391 | [[package]] 392 | name = "gimli" 393 | version = "0.28.0" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 396 | 397 | [[package]] 398 | name = "hashbrown" 399 | version = "0.15.2" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 402 | dependencies = [ 403 | "allocator-api2", 404 | "equivalent", 405 | "foldhash", 406 | ] 407 | 408 | [[package]] 409 | name = "hermit-abi" 410 | version = "0.3.9" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 413 | 414 | [[package]] 415 | name = "humantime" 416 | version = "2.1.0" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 419 | 420 | [[package]] 421 | name = "inout" 422 | version = "0.1.3" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" 425 | dependencies = [ 426 | "generic-array", 427 | ] 428 | 429 | [[package]] 430 | name = "json" 431 | version = "0.12.4" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" 434 | 435 | [[package]] 436 | name = "libc" 437 | version = "0.2.169" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 440 | 441 | [[package]] 442 | name = "log" 443 | version = "0.4.27" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 446 | 447 | [[package]] 448 | name = "memchr" 449 | version = "2.6.4" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 452 | 453 | [[package]] 454 | name = "miniz_oxide" 455 | version = "0.7.1" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 458 | dependencies = [ 459 | "adler", 460 | ] 461 | 462 | [[package]] 463 | name = "mio" 464 | version = "1.0.1" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" 467 | dependencies = [ 468 | "hermit-abi", 469 | "libc", 470 | "wasi 0.11.0+wasi-snapshot-preview1", 471 | "windows-sys 0.52.0", 472 | ] 473 | 474 | [[package]] 475 | name = "object" 476 | version = "0.32.1" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 479 | dependencies = [ 480 | "memchr", 481 | ] 482 | 483 | [[package]] 484 | name = "opaque-debug" 485 | version = "0.3.0" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 488 | 489 | [[package]] 490 | name = "pin-project-lite" 491 | version = "0.2.13" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 494 | 495 | [[package]] 496 | name = "polyval" 497 | version = "0.6.1" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" 500 | dependencies = [ 501 | "cfg-if", 502 | "cpufeatures", 503 | "opaque-debug", 504 | "universal-hash", 505 | ] 506 | 507 | [[package]] 508 | name = "ppv-lite86" 509 | version = "0.2.17" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 512 | 513 | [[package]] 514 | name = "proc-macro2" 515 | version = "1.0.69" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 518 | dependencies = [ 519 | "unicode-ident", 520 | ] 521 | 522 | [[package]] 523 | name = "quote" 524 | version = "1.0.33" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 527 | dependencies = [ 528 | "proc-macro2", 529 | ] 530 | 531 | [[package]] 532 | name = "r-efi" 533 | version = "5.2.0" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 536 | 537 | [[package]] 538 | name = "rand" 539 | version = "0.9.1" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 542 | dependencies = [ 543 | "rand_chacha", 544 | "rand_core 0.9.3", 545 | ] 546 | 547 | [[package]] 548 | name = "rand_chacha" 549 | version = "0.9.0" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 552 | dependencies = [ 553 | "ppv-lite86", 554 | "rand_core 0.9.3", 555 | ] 556 | 557 | [[package]] 558 | name = "rand_core" 559 | version = "0.6.4" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 562 | dependencies = [ 563 | "getrandom 0.2.11", 564 | ] 565 | 566 | [[package]] 567 | name = "rand_core" 568 | version = "0.9.3" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 571 | dependencies = [ 572 | "getrandom 0.3.2", 573 | ] 574 | 575 | [[package]] 576 | name = "rayon" 577 | version = "1.10.0" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 580 | dependencies = [ 581 | "either", 582 | "rayon-core", 583 | ] 584 | 585 | [[package]] 586 | name = "rayon-core" 587 | version = "1.12.1" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 590 | dependencies = [ 591 | "crossbeam-deque", 592 | "crossbeam-utils", 593 | ] 594 | 595 | [[package]] 596 | name = "regex" 597 | version = "1.10.2" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 600 | dependencies = [ 601 | "aho-corasick", 602 | "memchr", 603 | "regex-automata", 604 | "regex-syntax", 605 | ] 606 | 607 | [[package]] 608 | name = "regex-automata" 609 | version = "0.4.3" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 612 | dependencies = [ 613 | "aho-corasick", 614 | "memchr", 615 | "regex-syntax", 616 | ] 617 | 618 | [[package]] 619 | name = "regex-syntax" 620 | version = "0.8.2" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 623 | 624 | [[package]] 625 | name = "rpassword" 626 | version = "7.3.1" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" 629 | dependencies = [ 630 | "libc", 631 | "rtoolbox", 632 | "windows-sys 0.48.0", 633 | ] 634 | 635 | [[package]] 636 | name = "rtoolbox" 637 | version = "0.0.2" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" 640 | dependencies = [ 641 | "libc", 642 | "windows-sys 0.48.0", 643 | ] 644 | 645 | [[package]] 646 | name = "rustc-demangle" 647 | version = "0.1.23" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 650 | 651 | [[package]] 652 | name = "rustc_version" 653 | version = "0.4.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 656 | dependencies = [ 657 | "semver", 658 | ] 659 | 660 | [[package]] 661 | name = "semver" 662 | version = "1.0.20" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" 665 | 666 | [[package]] 667 | name = "serde" 668 | version = "1.0.192" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" 671 | dependencies = [ 672 | "serde_derive", 673 | ] 674 | 675 | [[package]] 676 | name = "serde_derive" 677 | version = "1.0.192" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" 680 | dependencies = [ 681 | "proc-macro2", 682 | "quote", 683 | "syn", 684 | ] 685 | 686 | [[package]] 687 | name = "sha2" 688 | version = "0.10.9" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 691 | dependencies = [ 692 | "cfg-if", 693 | "cpufeatures", 694 | "digest", 695 | ] 696 | 697 | [[package]] 698 | name = "socket2" 699 | version = "0.5.5" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" 702 | dependencies = [ 703 | "libc", 704 | "windows-sys 0.48.0", 705 | ] 706 | 707 | [[package]] 708 | name = "subtle" 709 | version = "2.5.0" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 712 | 713 | [[package]] 714 | name = "syn" 715 | version = "2.0.39" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 718 | dependencies = [ 719 | "proc-macro2", 720 | "quote", 721 | "unicode-ident", 722 | ] 723 | 724 | [[package]] 725 | name = "tokio" 726 | version = "1.45.1" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 729 | dependencies = [ 730 | "backtrace", 731 | "bytes", 732 | "libc", 733 | "mio", 734 | "pin-project-lite", 735 | "socket2", 736 | "tokio-macros", 737 | "windows-sys 0.52.0", 738 | ] 739 | 740 | [[package]] 741 | name = "tokio-macros" 742 | version = "2.5.0" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 745 | dependencies = [ 746 | "proc-macro2", 747 | "quote", 748 | "syn", 749 | ] 750 | 751 | [[package]] 752 | name = "typenum" 753 | version = "1.17.0" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 756 | 757 | [[package]] 758 | name = "unicode-ident" 759 | version = "1.0.12" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 762 | 763 | [[package]] 764 | name = "universal-hash" 765 | version = "0.5.1" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" 768 | dependencies = [ 769 | "crypto-common", 770 | "subtle", 771 | ] 772 | 773 | [[package]] 774 | name = "utf8parse" 775 | version = "0.2.1" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 778 | 779 | [[package]] 780 | name = "version_check" 781 | version = "0.9.4" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 784 | 785 | [[package]] 786 | name = "wasi" 787 | version = "0.11.0+wasi-snapshot-preview1" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 790 | 791 | [[package]] 792 | name = "wasi" 793 | version = "0.14.2+wasi-0.2.4" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 796 | dependencies = [ 797 | "wit-bindgen-rt", 798 | ] 799 | 800 | [[package]] 801 | name = "whirlwind" 802 | version = "0.1.1" 803 | source = "git+https://github.com/fortress-build/whirlwind.git#52bf3c859f2242a53828b0b9e225e5299167db58" 804 | dependencies = [ 805 | "crossbeam-utils", 806 | "hashbrown", 807 | "tokio", 808 | ] 809 | 810 | [[package]] 811 | name = "windows-sys" 812 | version = "0.48.0" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 815 | dependencies = [ 816 | "windows-targets 0.48.5", 817 | ] 818 | 819 | [[package]] 820 | name = "windows-sys" 821 | version = "0.52.0" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 824 | dependencies = [ 825 | "windows-targets 0.52.3", 826 | ] 827 | 828 | [[package]] 829 | name = "windows-targets" 830 | version = "0.48.5" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 833 | dependencies = [ 834 | "windows_aarch64_gnullvm 0.48.5", 835 | "windows_aarch64_msvc 0.48.5", 836 | "windows_i686_gnu 0.48.5", 837 | "windows_i686_msvc 0.48.5", 838 | "windows_x86_64_gnu 0.48.5", 839 | "windows_x86_64_gnullvm 0.48.5", 840 | "windows_x86_64_msvc 0.48.5", 841 | ] 842 | 843 | [[package]] 844 | name = "windows-targets" 845 | version = "0.52.3" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" 848 | dependencies = [ 849 | "windows_aarch64_gnullvm 0.52.3", 850 | "windows_aarch64_msvc 0.52.3", 851 | "windows_i686_gnu 0.52.3", 852 | "windows_i686_msvc 0.52.3", 853 | "windows_x86_64_gnu 0.52.3", 854 | "windows_x86_64_gnullvm 0.52.3", 855 | "windows_x86_64_msvc 0.52.3", 856 | ] 857 | 858 | [[package]] 859 | name = "windows_aarch64_gnullvm" 860 | version = "0.48.5" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 863 | 864 | [[package]] 865 | name = "windows_aarch64_gnullvm" 866 | version = "0.52.3" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" 869 | 870 | [[package]] 871 | name = "windows_aarch64_msvc" 872 | version = "0.48.5" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 875 | 876 | [[package]] 877 | name = "windows_aarch64_msvc" 878 | version = "0.52.3" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" 881 | 882 | [[package]] 883 | name = "windows_i686_gnu" 884 | version = "0.48.5" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 887 | 888 | [[package]] 889 | name = "windows_i686_gnu" 890 | version = "0.52.3" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" 893 | 894 | [[package]] 895 | name = "windows_i686_msvc" 896 | version = "0.48.5" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 899 | 900 | [[package]] 901 | name = "windows_i686_msvc" 902 | version = "0.52.3" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" 905 | 906 | [[package]] 907 | name = "windows_x86_64_gnu" 908 | version = "0.48.5" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 911 | 912 | [[package]] 913 | name = "windows_x86_64_gnu" 914 | version = "0.52.3" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" 917 | 918 | [[package]] 919 | name = "windows_x86_64_gnullvm" 920 | version = "0.48.5" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 923 | 924 | [[package]] 925 | name = "windows_x86_64_gnullvm" 926 | version = "0.52.3" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" 929 | 930 | [[package]] 931 | name = "windows_x86_64_msvc" 932 | version = "0.48.5" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 935 | 936 | [[package]] 937 | name = "windows_x86_64_msvc" 938 | version = "0.52.3" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" 941 | 942 | [[package]] 943 | name = "wit-bindgen-rt" 944 | version = "0.39.0" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 947 | dependencies = [ 948 | "bitflags", 949 | ] 950 | 951 | [[package]] 952 | name = "x25519-dalek" 953 | version = "2.0.1" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" 956 | dependencies = [ 957 | "curve25519-dalek", 958 | "rand_core 0.6.4", 959 | "serde", 960 | "zeroize", 961 | ] 962 | 963 | [[package]] 964 | name = "zeroize" 965 | version = "1.8.1" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 968 | dependencies = [ 969 | "zeroize_derive", 970 | ] 971 | 972 | [[package]] 973 | name = "zeroize_derive" 974 | version = "1.4.2" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 977 | dependencies = [ 978 | "proc-macro2", 979 | "quote", 980 | "syn", 981 | ] 982 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "aft", 4 | "aft-crypto" 5 | ] 6 | resolver = "2" 7 | 8 | [workspace.package] 9 | authors = ["dd-dreams"] 10 | homepage = "https://github.com/dd-dreams/aft" 11 | license = "MIT OR Apache-2.0" 12 | 13 | [profile.release] 14 | lto = true 15 | strip = "debuginfo" 16 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright © 2023 dd-dreams 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | and associated documentation files (the "Software"), to deal in the Software without 5 | restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or 10 | substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 16 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #

aft
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/dd-dreams/aft/.github%2Fworkflows%2Frust.yml)![GitHub release (with filter)](https://img.shields.io/github/v/release/dd-dreams/aft) 2 | 3 | aft (Advanced File Transfer) is a minimal and secure tool for sharing files between two parties easily and efficiently. Works in Windows, Linux and macOS. 4 | 5 | # Features 6 | - Encryption. 7 | - Fast. 8 | - Lightweight (on RAM and storage). 9 | - Security is top priority. 10 | - Peer to Peer mode. 11 | - Relay mode. 12 | - Blocking senders. 13 | - No IP self-lookup. 14 | - fail2ban support. 15 | 16 | # Modes 17 | There are a couple of modes to use with this program: 18 | ## Peer to Peer 19 | The sender is directly connected to the receiver, and the transfer process is happening directly. 20 | ## Relay 21 | Allows using a relay instead of two devices connecting to each other directly. It allows a few benefits such as: 22 | - No port forward needed on the receiver's end; 23 | - No direct contact between the receiver and the sender; 24 | - Better privacy - no IP sharing; 25 | - Ability to block senders. 26 | 27 | # Usage 28 | ``` 29 | aft - file transfer done easily 30 | 31 | Usage: 32 | aft sender [--address

] [--port ] [--identifier ] 33 | aft receiver [-p ] 34 | aft download -a
[-p ] [-i ] 35 | aft relay [-p ] 36 | aft [options ...] 37 | 38 | Positional arguments: 39 | mode 40 | 41 | Optional arguments: 42 | -a --address ADDRESS Address. 43 | -p --port PORT Port. 44 | -i --identifier IDENTIFIER Identifier to find the receiver. Used only when its not P2P. 45 | -v --verbose VERBOSE Verbose level. Default is 1 (warnings only). Range 1-3. 46 | -c --config CONFIG Config location. 47 | -v --version Show version. 48 | -e --encryption ALGORITHM Possible values: [AES128, AES256]. 49 | -t --threads THREADS Number of threads to use. 50 | -s --checksum Check checksum at the end. Only relevant if mode == sender. 51 | ``` 52 | 53 | # Installation 54 | 55 | ## Install using cargo 56 | Run `cargo install aft`, and you should be able to run the program immediately. 57 | 58 | ## Automatic install 59 | Run the following command to install aft: `curl --proto '=https' -sf https://raw.githubusercontent.com/dd-dreams/aft/master/install.sh | sudo sh -s -- install`. 60 | 61 | If you want to modify the config, you can create a new file at your home directory (`%USERPROFILE%` for Windows and `~/` for Unix) within `.aft` directory, named: "config". 62 | Look into `docs/CONFIG.md` to see more. 63 | 64 | Run the following command to uninstall aft: `curl --proto '=https' -sf https://raw.githubusercontent.com/dd-dreams/aft/master/install.sh | sudo sh -s -- uninstall`. 65 | 66 | ## Manual install 67 | Navigate to the [releases](https://github.com/dd-dreams/aft/releases) page and choose your platform. 68 | For Windows you can export the archive contents by double clicking. 69 | For Linux and macOS you can use `gzip` for extracting the contents. `gzip` should be included by default in the OS. 70 | Run: `gzip -dN `. You can export the program anywhere you like, but make sure you add it to PATH so you can easily access it. 71 | 72 | ### Systemd setup 73 | - Copy the `aft` program into `/usr/local/bin/`. 74 | - Copy `aft-relay.service` into `/etc/systemd/system/`. 75 | - Start the program with: `sudo systemctl start aft-relay`. 76 | 77 | Notice that the service requires a new user called `aft`. If you want the service to be ran with root, remove the `User=aft` line, though it's not recommended for security reasons. 78 | 79 | This service only runs the relay mode. 80 | 81 | ## fail2ban setup 82 | - Copy `assets/fail2ban/aft-relay-filter.conf` into `/etc/fail2ban/filter.d/`. 83 | - Copy `assets/fail2ban/aft-relay.conf` into `/etc/fail2ban/jail.d/` 84 | - Restart the service: `sudo systemctl restart fail2ban` 85 | 86 | You can modify the bantime and maxretries in `aft-relay.conf`. 87 | 88 | ### Notice 89 | fail2ban only works on relay mode. fail2ban doesn't work on Windows. 90 | 91 | # Building 92 | Building is really simple: `cargo build --release` and the output will be at `target/release/aft`. 93 | 94 | ## License 95 | 96 | Licensed under either of 97 | 98 | * Apache License, Version 2.0 99 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 100 | * MIT license 101 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 102 | 103 | at your option. 104 | 105 | ## Contribution 106 | 107 | Unless you explicitly state otherwise, any contribution intentionally submitted 108 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 109 | dual licensed as above, without any additional terms or conditions. 110 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | Please do not open a GitHub Issue for security issues you encounter. Instead, please refer to GitHub Security tab, and report there. 5 | 6 | Currently only the latest version is supported. 7 | -------------------------------------------------------------------------------- /aft-crypto/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aft-crypto" 3 | version = "1.2.6" 4 | edition = "2021" 5 | authors.workspace = true 6 | homepage.workspace = true 7 | license.workspace = true 8 | repository = "https://github.com/dd-dreams/aft/tree/master/aft-crypto" 9 | description = "Cryptography library for aft." 10 | 11 | [dependencies] 12 | rand_core = {version = "0.6", default-features = false, features=["getrandom"]} 13 | aes-gcm = "0.10" 14 | x25519-dalek = "2" 15 | rand = "0.9" 16 | zeroize = "1.8" 17 | -------------------------------------------------------------------------------- /aft-crypto/src/bip39.rs: -------------------------------------------------------------------------------- 1 | //! BIP39 wordlist. 2 | 3 | pub fn create_wordlist() -> Vec<&'static str> { 4 | vec![ 5 | "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", 6 | "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", 7 | "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", 8 | "addict", "address", "adjust", "admit", "adult", "advance", "advice", "aerobic", "affair", 9 | "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", 10 | "aisle", "alarm", "album", "alcohol", "alert", "alien", "all", "alley", "allow", "almost", 11 | "alone", "alpha", "already", "also", "alter", "always", "amateur", "amazing", "among", 12 | "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", "animal", 13 | "ankle", "announce", "annual", "another", "answer", "antenna", "antique", "anxiety", "any", 14 | "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", 15 | "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", "arrive", 16 | "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", 17 | "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", 18 | "auction", "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", 19 | "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis", "baby", 20 | "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball", "bamboo", "banana", 21 | "banner", "bar", "barely", "bargain", "barrel", "base", "basic", "basket", "battle", 22 | "beach", "bean", "beauty", "because", "become", "beef", "before", "begin", "behave", 23 | "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", "better", 24 | "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird", "birth", 25 | "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", 26 | "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", 27 | "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", "bottom", 28 | "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", 29 | "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", 30 | "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", 31 | "bulb", "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", 32 | "business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", 33 | "cage", "cake", "call", "calm", "camera", "camp", "can", "canal", "cancel", "candy", 34 | "cannon", "canoe", "canvas", "canyon", "capable", "capital", "captain", "car", "carbon", 35 | "card", "cargo", "carpet", "carry", "cart", "case", "cash", "casino", "castle", "casual", 36 | "cat", "catalog", "catch", "category", "cattle", "caught", "cause", "caution", "cave", 37 | "ceiling", "celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk", 38 | "champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap", "check", 39 | "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", "chimney", "choice", 40 | "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen", 41 | "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", "clever", 42 | "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth", 43 | "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", 44 | "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", 45 | "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", 46 | "connect", "consider", "control", "convince", "cook", "cool", "copper", "copy", "coral", 47 | "core", "corn", "correct", "cost", "cotton", "couch", "country", "couple", "course", 48 | "cousin", "cover", "coyote", "crack", "cradle", "craft", "cram", "crane", "crash", 49 | "crater", "crawl", "crazy", "cream", "credit", "creek", "crew", "cricket", "crime", 50 | "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", "cruel", "cruise", 51 | "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", 52 | "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", 53 | "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", 54 | "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", 55 | "deer", "defense", "define", "defy", "degree", "delay", "deliver", "demand", "demise", 56 | "denial", "dentist", "deny", "depart", "depend", "deposit", "depth", "deputy", "derive", 57 | "describe", "desert", "design", "desk", "despair", "destroy", "detail", "detect", 58 | "develop", "device", "devote", "diagram", "dial", "diamond", "diary", "dice", "diesel", 59 | "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", 60 | "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", 61 | "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", 62 | "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", 63 | "drama", "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", 64 | "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", 65 | "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", 66 | "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", 67 | "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", 68 | "else", "embark", "embody", "embrace", "emerge", "emotion", "employ", "empower", "empty", 69 | "enable", "enact", "end", "endless", "endorse", "enemy", "energy", "enforce", "engage", 70 | "engine", "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", 71 | "entire", "entry", "envelope", "episode", "equal", "equip", "era", "erase", "erode", 72 | "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", 73 | "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", 74 | "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", 75 | "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", 76 | "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", 77 | "fame", "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", 78 | "father", "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", 79 | "feel", "female", "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", 80 | "field", "figure", "file", "film", "filter", "final", "find", "fine", "finger", "finish", 81 | "fire", "firm", "first", "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", 82 | "flash", "flat", "flavor", "flee", "flight", "flip", "float", "flock", "floor", "flower", 83 | "fluid", "flush", "fly", "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", 84 | "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", 85 | "found", "fox", "fragile", "frame", "frequent", "fresh", "friend", "fringe", "frog", 86 | "front", "frost", "frown", "frozen", "fruit", "fuel", "fun", "funny", "furnace", "fury", 87 | "future", "gadget", "gain", "galaxy", "gallery", "game", "gap", "garage", "garbage", 88 | "garden", "garlic", "garment", "gas", "gasp", "gate", "gather", "gauge", "gaze", "general", 89 | "genius", "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle", 90 | "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass", "glide", 91 | "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", "goat", "goddess", "gold", 92 | "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", "grace", "grain", 93 | "grant", "grape", "grass", "gravity", "great", "green", "grid", "grief", "grit", "grocery", 94 | "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun", "gym", 95 | "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", "hard", "harsh", 96 | "harvest", "hat", "have", "hawk", "hazard", "head", "health", "heart", "heavy", "hedgehog", 97 | "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", "hill", "hint", 98 | "hip", "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow", "home", 99 | "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host", "hotel", "hour", 100 | "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", "hunt", "hurdle", 101 | "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", 102 | "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose", 103 | "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", 104 | "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", 105 | "inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", 106 | "inside", "inspire", "install", "intact", "interest", "into", "invest", "invite", 107 | "involve", "iron", "island", "isolate", "issue", "item", "ivory", "jacket", "jaguar", 108 | "jar", "jazz", "jealous", "jeans", "jelly", "jewel", "job", "join", "joke", "journey", 109 | "joy", "judge", "juice", "jump", "jungle", "junior", "junk", "just", "kangaroo", "keen", 110 | "keep", "ketchup", "key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit", 111 | "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know", "lab", "label", 112 | "labor", "ladder", "lady", "lake", "lamp", "language", "laptop", "large", "later", "latin", 113 | "laugh", "laundry", "lava", "law", "lawn", "lawsuit", "layer", "lazy", "leader", "leaf", 114 | "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend", 115 | "length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty", "library", 116 | "license", "life", "lift", "light", "like", "limb", "limit", "link", "lion", "liquid", 117 | "list", "little", "live", "lizard", "load", "loan", "lobster", "local", "lock", "logic", 118 | "lonely", "long", "loop", "lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", 119 | "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet", 120 | "maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate", "mango", 121 | "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", "marriage", 122 | "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", 123 | "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", 124 | "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", 125 | "message", "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", 126 | "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", 127 | "mixed", "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", 128 | "monster", "month", "moon", "moral", "more", "morning", "mosquito", "mother", "motion", 129 | "motor", "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", 130 | "muscle", "museum", "mushroom", "music", "must", "mutual", "myself", "mystery", "myth", 131 | "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", "neck", "need", 132 | "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral", 133 | "never", "news", "next", "nice", "night", "noble", "noise", "nominee", "noodle", "normal", 134 | "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", 135 | "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", 136 | "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", 137 | "often", "oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", 138 | "online", "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", 139 | "orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich", 140 | "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over", "own", "owner", 141 | "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "palm", "panda", 142 | "panel", "panic", "panther", "paper", "parade", "parent", "park", "parrot", "party", 143 | "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", 144 | "peace", "peanut", "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", 145 | "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical", 146 | "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", 147 | "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", "play", 148 | "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", 149 | "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", 150 | "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", 151 | "prefer", "prepare", "present", "pretty", "prevent", "price", "pride", "primary", "print", 152 | "priority", "prison", "private", "prize", "problem", "process", "produce", "profit", 153 | "program", "project", "promote", "proof", "property", "prosper", "protect", "proud", 154 | "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil", 155 | "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", 156 | "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", 157 | "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", 158 | "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", 159 | "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", 160 | "recycle", "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", 161 | "reject", "relax", "release", "relief", "rely", "remain", "remember", "remind", "remove", 162 | "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", "require", 163 | "rescue", "resemble", "resist", "resource", "response", "result", "retire", "retreat", 164 | "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", "rice", 165 | "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple", "risk", 166 | "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", 167 | "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber", 168 | "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", "safe", 169 | "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", 170 | "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene", 171 | "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", 172 | "script", "scrub", "sea", "search", "season", "seat", "second", "secret", "section", 173 | "security", "seed", "seek", "segment", "select", "sell", "seminar", "senior", "sense", 174 | "sentence", "series", "service", "session", "settle", "setup", "seven", "shadow", "shaft", 175 | "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine", "ship", 176 | "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder", "shove", "shrimp", 177 | "shrug", "shuffle", "shy", "sibling", "sick", "side", "siege", "sight", "sign", "silent", 178 | "silk", "silly", "silver", "similar", "simple", "since", "sing", "siren", "sister", 179 | "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin", "skirt", "skull", 180 | "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan", "slot", 181 | "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", "snake", "snap", 182 | "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft", "solar", "soldier", 183 | "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", "soul", "sound", 184 | "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak", "special", 185 | "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit", "split", 186 | "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", "square", 187 | "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", 188 | "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", 189 | "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", 190 | "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", 191 | "submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", 192 | "summer", "sun", "sunny", "sunset", "super", "supply", "supreme", "sure", "surface", 193 | "surge", "surprise", "surround", "survey", "suspect", "sustain", "swallow", "swamp", 194 | "swap", "swarm", "swear", "sweet", "swift", "swim", "swing", "switch", "sword", "symbol", 195 | "symptom", "syrup", "system", "table", "tackle", "tag", "tail", "talent", "talk", "tank", 196 | "tape", "target", "task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten", 197 | "tenant", "tennis", "tent", "term", "test", "text", "thank", "that", "theme", "then", 198 | "theory", "there", "they", "thing", "this", "thought", "three", "thrive", "throw", "thumb", 199 | "thunder", "ticket", "tide", "tiger", "tilt", "timber", "time", "tiny", "tip", "tired", 200 | "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", 201 | "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top", 202 | "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", "toward", 203 | "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", 204 | "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", "trigger", 205 | "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", 206 | "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", 207 | "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", "unable", 208 | "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", 209 | "unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", 210 | "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", 211 | "useful", "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", 212 | "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", 213 | "vendor", "venture", "venue", "verb", "verify", "version", "very", "vessel", "veteran", 214 | "viable", "vibrant", "vicious", "victory", "video", "view", "village", "vintage", "violin", 215 | "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", "void", 216 | "volcano", "volume", "vote", "voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", 217 | "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave", "way", 218 | "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", 219 | "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when", "where", "whip", 220 | "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", 221 | "wink", "winner", "winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman", 222 | "wonder", "wood", "wool", "word", "work", "world", "worry", "worth", "wrap", "wreck", 223 | "wrestle", "wrist", "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", 224 | "zebra", "zero", "zone", "zoo", 225 | ] 226 | } 227 | -------------------------------------------------------------------------------- /aft-crypto/src/data.rs: -------------------------------------------------------------------------------- 1 | //! Data encryption with AEAD (Authenticated encryption). 2 | pub use aes_gcm::{ 3 | aead::{generic_array::GenericArray, rand_core::RngCore, AeadInPlace, KeyInit, OsRng}, 4 | Aes128Gcm, Aes256Gcm, Nonce, Tag, TagSize 5 | }; 6 | use crate::exchange::KEY_LENGTH; 7 | use crate::errors::EncryptionErrors; 8 | use zeroize::Zeroize; 9 | 10 | pub type Result = core::result::Result; 11 | pub type AesGcm128Enc = EncAlgo; 12 | pub type AesGcm256Enc = EncAlgo; 13 | 14 | /// Nonce size (in bytes) of AES-GCM (excludes 0) 15 | pub const AES_GCM_NONCE_SIZE: usize = 12; 16 | pub const AES_GCM_TAG_SIZE: usize = 16; 17 | 18 | #[derive(Debug, PartialEq, Eq)] 19 | pub enum Algo { 20 | Aes128, 21 | Aes256, 22 | Unknown, 23 | } 24 | 25 | impl From<&str> for Algo { 26 | fn from(v: &str) -> Self { 27 | match v { 28 | "aes128" => Algo::Aes128, 29 | "aes256" => Algo::Aes256, 30 | _ => Algo::Unknown 31 | } 32 | } 33 | } 34 | 35 | impl From<&Algo> for &str { 36 | fn from(v: &Algo) -> Self { 37 | match v { 38 | Algo::Aes128 => "aes128", 39 | Algo::Aes256 => "aes256", 40 | Algo::Unknown => "unknown" 41 | } 42 | } 43 | } 44 | 45 | 46 | // Creates a new AES-GCM encryptor. 47 | macro_rules! create_aes_gcm_encryptor { 48 | ($key:expr, $aesgcm:ident) => {{ 49 | let arr_key = GenericArray::from_slice($key); 50 | $aesgcm::new(arr_key) 51 | }}; 52 | } 53 | 54 | #[macro_export] 55 | /// Quickly decrypt AES-GCM data. 56 | macro_rules! decrypt_aes_gcm { 57 | ($encryptor:expr, $data:expr) => { 58 | $encryptor.decrypt( 59 | &$data[..$data.len()-AES_GCM_NONCE_SIZE], &$data[$data.len()-AES_GCM_NONCE_SIZE..]) 60 | .expect("Could not decrypt") 61 | } 62 | } pub use decrypt_aes_gcm; 63 | 64 | pub trait EncryptorBase 65 | where 66 | CiAlgo: AeadInPlace, 67 | { 68 | /// Encrypt data without changing the original data. 69 | fn encrypt(&self, data: &[u8]) -> Result> { 70 | let mut encrypted_data = data.to_vec(); 71 | self.encrypt_in_place(&mut encrypted_data)?; 72 | 73 | Ok(encrypted_data) 74 | } 75 | 76 | /// Decrypt data without changing the original data. 77 | fn decrypt(&self, data: &[u8], nonce: &[u8]) -> Result> { 78 | let mut decrypted_data = data.to_vec(); 79 | self.decrypt_in_place(&mut decrypted_data, nonce)?; 80 | 81 | Ok(decrypted_data) 82 | } 83 | 84 | /// Encrypt data in-place. 85 | fn encrypt_in_place(&self, data: &mut Vec) -> Result<()> { 86 | // The nonce is 12 bytes (96 bits) long. 87 | // According to NIST 38D, 12 bytes should be used for efficiency and simplicity. 88 | let mut nonce = vec![0; AES_GCM_NONCE_SIZE]; 89 | OsRng.fill_bytes(&mut nonce); 90 | 91 | // Note: authentication tag is appended to the encrypted data. 92 | if self.get_encryptor().encrypt_in_place(Nonce::from_slice(&nonce), b"", data).is_err() { 93 | return Err(EncryptionErrors::FailedEncrypt); 94 | } 95 | 96 | // Adding nonce to data 97 | data.append(&mut nonce); 98 | 99 | Ok(()) 100 | } 101 | 102 | /// Decrypt data in-place. 103 | fn decrypt_in_place(&self, data: &mut Vec, nonce: &[u8]) -> Result<()> { 104 | let nonce = Nonce::from_slice(nonce); 105 | if self.get_encryptor().decrypt_in_place(nonce, b"", data).is_err() { 106 | return Err(EncryptionErrors::FailedDecrypt); 107 | } 108 | 109 | Ok(()) 110 | } 111 | 112 | fn decrypt_in_place_detached(&self, data: &mut [u8], nonce: &[u8]) -> Result<()> { 113 | if data.len() < AES_GCM_TAG_SIZE { 114 | return Err(EncryptionErrors::InvalidLength); 115 | } 116 | 117 | let tag_pos = data.len() - AES_GCM_TAG_SIZE; 118 | let (d, tag) = data.as_mut().split_at_mut(tag_pos); 119 | // TODO: remove expect 120 | self.get_encryptor(). 121 | decrypt_in_place_detached(nonce.into(), b"", d, Tag::from_slice(tag)).expect("FailedDecrypting"); 122 | 123 | Ok(()) 124 | } 125 | 126 | fn get_encryptor(&self) -> &CiAlgo; 127 | } 128 | 129 | /// Struct to represent an object to encrypt data with some encryption algorithm. 130 | pub struct EncAlgo { 131 | key: [u8; KEY_LENGTH], 132 | encryptor_func: fn(&[u8]) -> T, 133 | encryptor: T, 134 | } 135 | 136 | impl EncAlgo { 137 | pub fn new(key: &[u8; KEY_LENGTH], encryptor_func: fn(&[u8]) -> T) -> Self { 138 | Self { 139 | key: *key, 140 | encryptor_func, 141 | encryptor: encryptor_func(key), 142 | } 143 | } 144 | } 145 | 146 | impl Clone for EncAlgo { 147 | fn clone(&self) -> Self { 148 | Self { 149 | key: self.key.clone(), 150 | encryptor_func: self.encryptor_func, 151 | encryptor: (self.encryptor_func)(&self.key), 152 | } 153 | } 154 | } 155 | 156 | /// Creates a new AES-GCM-128 encryptor. 157 | /// [`key`] must be at least 16 bytes long. 158 | pub fn create_128_encryptor(key: &[u8]) -> Aes128Gcm { 159 | // AES-128 uses 16 bytes keys 160 | create_aes_gcm_encryptor!(&key[..16], Aes128Gcm) 161 | } 162 | 163 | /// Creates a new AES-GCM-256 encryptor. 164 | /// [`key`] must be at least 32 bytes long. 165 | pub fn create_256_encryptor(key: &[u8]) -> Aes256Gcm { 166 | // AES-256 uses 32 bytes keys 167 | create_aes_gcm_encryptor!(&key[..32], Aes256Gcm) 168 | } 169 | 170 | impl EncryptorBase for EncAlgo 171 | where 172 | CiAlgo: AeadInPlace, 173 | { 174 | fn get_encryptor(&self) -> &CiAlgo { 175 | &self.encryptor 176 | } 177 | } 178 | 179 | /// Safe Data. Zeros when dropped. 180 | pub struct SData(pub T); 181 | 182 | impl Drop for SData { 183 | fn drop(&mut self) { 184 | self.0.zeroize(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /aft-crypto/src/errors.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum EncryptionErrors { 3 | FailedEncrypt, 4 | FailedDecrypt, 5 | IncorrectPassword, 6 | InvalidLength, 7 | } 8 | -------------------------------------------------------------------------------- /aft-crypto/src/exchange.rs: -------------------------------------------------------------------------------- 1 | //! Very small module for exchanging keys using x25519. 2 | use rand_core::OsRng; 3 | pub use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret}; 4 | 5 | pub const KEY_LENGTH: usize = 32; 6 | 7 | pub struct X25519Key { 8 | shared_secret: SharedSecret, 9 | } 10 | 11 | impl X25519Key { 12 | /// Generates a new shared secret. 13 | pub fn new(secret: EphemeralSecret, their_pk: &PublicKey) -> Self { 14 | X25519Key { 15 | shared_secret: X25519Key::exchange(secret, their_pk), 16 | } 17 | } 18 | 19 | /// Generates a secret key and a public key. 20 | pub fn generate_keys() -> (PublicKey, EphemeralSecret) { 21 | let secret = EphemeralSecret::random_from_rng(OsRng); 22 | (PublicKey::from(&secret), secret) 23 | } 24 | 25 | /// Combine `secret` and `pk` into a shared secret key. 26 | fn exchange(secret: EphemeralSecret, pk: &PublicKey) -> SharedSecret { 27 | secret.diffie_hellman(pk) 28 | } 29 | 30 | pub fn as_bytes(&self) -> &[u8; 32] { 31 | self.shared_secret.as_bytes() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /aft-crypto/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod bip39; 2 | pub mod data; 3 | pub mod errors; 4 | pub mod exchange; 5 | pub mod password_generator; 6 | -------------------------------------------------------------------------------- /aft-crypto/src/password_generator.rs: -------------------------------------------------------------------------------- 1 | //! Passphrase generator with high entropy. 2 | //! This module will try to use the OS provided wordlists, 3 | //! but if there are none, it will use BIP39 wordlist. 4 | use crate::bip39; 5 | use rand::{thread_rng, Rng}; 6 | 7 | pub const DELIMITER: char = '-'; 8 | 9 | /// Linux and macOS wordlist path. Windows doesn't have a native one. 10 | const UNIX_WORDLIST: &str = "/usr/share/dict/words"; 11 | 12 | /// Generate a unique passphrase using a wordlist. 13 | /// Generates a passphrase and not a password because its easier to remember. 14 | pub fn generate_passphrase(len: u8) -> String { 15 | if !["windows"].contains(&std::env::consts::OS) { 16 | if let Ok(content) = std::fs::read_to_string(UNIX_WORDLIST) { 17 | let wordlist: Vec<&str> = content.split('\n').collect(); 18 | return random_passphrase(&wordlist, len); 19 | } 20 | } 21 | 22 | random_passphrase(&bip39::create_wordlist(), len) 23 | } 24 | 25 | /// Generates a random passphrase. 26 | fn random_passphrase(wordlist: &[&str], len: u8) -> String { 27 | let mut passphrase = String::new(); 28 | let mut rng = thread_rng(); 29 | for _ in 0..len { 30 | let random_index = rng.gen_range(0..wordlist.len()); 31 | passphrase.push_str(wordlist[random_index]); 32 | passphrase.push(DELIMITER); 33 | } 34 | 35 | passphrase.pop(); 36 | 37 | passphrase.make_ascii_lowercase(); 38 | passphrase 39 | } 40 | -------------------------------------------------------------------------------- /aft-relay.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=systemd service for aft relay. 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/usr/local/bin/aft relay 7 | Restart=on-failure 8 | MemoryDenyWriteExecute=true 9 | NoNewPrivileges=true 10 | ProtectSystem=strict 11 | ProtectHome=true 12 | PrivateTmp=true 13 | PrivateDevices=true 14 | PrivateIPC=true 15 | PrivateUsers=true 16 | ProtectHostname=true 17 | ProtectClock=true 18 | ProtectKernelTunables=true 19 | ProtectKernelModules=true 20 | ProtectKernelLogs=true 21 | ProtectControlGroups=true 22 | RestrictAddressFamilies=AF_INET AF_INET6 23 | RestrictNamespaces=true 24 | RemoveIPC=true 25 | ProtectProc=invisible 26 | LockPersonality=true 27 | StandardOutput=append:/var/log/aft-relay.log 28 | StandardError=append:/var/log/aft-relay.log 29 | ReadWritePaths=/var/log/aft-relay.log 30 | LogsDirectory=/var/log 31 | User=aft 32 | 33 | [Install] 34 | WantedBy=multi-user.target 35 | -------------------------------------------------------------------------------- /aft/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aft" 3 | version = "8.0.3" 4 | edition = "2021" 5 | authors.workspace = true 6 | homepage.workspace = true 7 | license.workspace = true 8 | repository = "https://github.com/dd-dreams/aft" 9 | description = "Transfer files easily and fast." 10 | readme = "../README.md" 11 | keywords = ["cli", "peer-to-peer", "relay", "file-transfer", "decentralized"] 12 | 13 | [dependencies] 14 | tokio = {version = "1", features = ["io-std", "io-util", "net", "sync", "rt-multi-thread", "macros"], optional = true } 15 | env_logger = "0.11" 16 | json = "0.12" 17 | log = "0.4" 18 | sha2 = "0.10" 19 | rpassword = "7.2" 20 | aft-crypto = {path = "../aft-crypto", version = "1"} 21 | rayon = "1.10" 22 | whirlwind = { git = "https://github.com/fortress-build/whirlwind.git", optional = true } 23 | 24 | [features] 25 | default = ["clients", "sender"] 26 | relay = ["dep:tokio", "dep:whirlwind"] 27 | clients = [] 28 | sender = [] 29 | full = ["clients", "sender", "relay"] 30 | -------------------------------------------------------------------------------- /aft/src/clients.rs: -------------------------------------------------------------------------------- 1 | //! Clients (Receiver and Downloader). 2 | use crate::{ 3 | constants::{ 4 | AFT_DIRNAME, BLOCKED_FILENAME, CLIENT_RECV, MAX_CHECKSUM_LEN, MAX_CONTENT_LEN, 5 | MAX_IDENTIFIER_LEN, MAX_METADATA_LEN, RELAY, SHA_256_LEN, SIGNAL_LEN, 6 | }, 7 | errors::Errors, 8 | utils::{ 9 | bytes_to_string, get_accept_input, get_home_dir, mut_vec, send_identifier, FileOperations, 10 | Signals, 11 | }, 12 | }; 13 | use aft_crypto::{ 14 | 15 | data::{AeadInPlace, EncAlgo, EncryptorBase, SData, AES_GCM_NONCE_SIZE, AES_GCM_TAG_SIZE, decrypt_aes_gcm}, 16 | exchange::{PublicKey, X25519Key, KEY_LENGTH}, 17 | }; 18 | use log::{debug, error, info}; 19 | use rayon::prelude::*; 20 | use sha2::{Digest, Sha256}; 21 | use std::{ 22 | io::{self, BufReader, Read, Write, IoSlice}, 23 | net::{TcpListener, TcpStream}, 24 | time, 25 | }; 26 | 27 | /// Opens a file. 28 | /// 29 | /// Returns the file object, and boolean saying if it was newly created or opened. 30 | /// Error when there was an error creating or opening a file. 31 | fn checks_open_file(filename: &str) -> io::Result<(FileOperations, bool)> { 32 | let path = &format!(r"{}/{}/.{}.tmp", get_home_dir(), AFT_DIRNAME, if filename.is_empty() {"null"} else {filename}); 33 | 34 | if FileOperations::is_file_exists(path) { 35 | let mut file = FileOperations::new(path)?; 36 | // New data is added at the end 37 | file.seek_end(0)?; 38 | Ok((file, true)) 39 | } else { 40 | let file = FileOperations::new_create(path)?; 41 | Ok((file, false)) 42 | } 43 | } 44 | 45 | /// A safe writer. Acts like a normal writer only that it encrypts the connection. 46 | pub struct SWriter(pub W, pub EncAlgo); 47 | 48 | #[cfg(feature = "relay")] 49 | struct UserBlocks { 50 | file: FileOperations, 51 | } 52 | 53 | impl Write for SWriter 54 | where 55 | T: AeadInPlace, 56 | W: Write, 57 | { 58 | fn write(&mut self, buf: &[u8]) -> io::Result { 59 | let enc_buf = self.1.encrypt(buf).expect("Could not encrypt."); 60 | Ok(self.0.write(&enc_buf)? - AES_GCM_NONCE_SIZE - AES_GCM_TAG_SIZE) 61 | } 62 | 63 | fn flush(&mut self) -> io::Result<()> { 64 | self.0.flush() 65 | } 66 | } 67 | 68 | impl Read for SWriter 69 | where 70 | T: AeadInPlace, 71 | W: Read, 72 | { 73 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 74 | let mut read_buf = Vec::with_capacity(buf.len() + AES_GCM_NONCE_SIZE + AES_GCM_TAG_SIZE); 75 | 76 | let bytes_read = 77 | (&mut self.0).take((buf.len() + AES_GCM_NONCE_SIZE + AES_GCM_TAG_SIZE) as u64).read(&mut read_buf)?; 78 | 79 | if bytes_read == 0 { 80 | return Ok(0) 81 | } 82 | 83 | let (data, nonce) = read_buf.split_at(read_buf.len() - AES_GCM_NONCE_SIZE); 84 | let dec_buf = self.1.decrypt(data, nonce).expect("Could not decrypt."); 85 | buf[..dec_buf.len()].copy_from_slice(&dec_buf); 86 | 87 | Ok(bytes_read - AES_GCM_NONCE_SIZE - AES_GCM_TAG_SIZE) 88 | } 89 | } 90 | 91 | impl SWriter 92 | where 93 | T: AeadInPlace, 94 | W: Write, 95 | { 96 | /// Better implementation of `write`. Instead of creating a new buffer to encrypt to, it writes 97 | /// and encrypts "in place". 98 | /// 99 | /// Use this method for better efficiency. 100 | pub fn write_ext(&mut self, buf: &mut Vec) -> io::Result<()> { 101 | // Automatically adds the tag and the nonce. 102 | self.1.encrypt_in_place(buf).expect("Could not encrypt."); 103 | self.0.write_all(buf)?; 104 | 105 | buf.truncate(buf.len() - AES_GCM_TAG_SIZE - AES_GCM_NONCE_SIZE); 106 | Ok(()) 107 | } 108 | } 109 | 110 | impl SWriter 111 | where 112 | T: AeadInPlace, 113 | W: Read, 114 | { 115 | /// Better implementation of `read`. Instead of creating a new buffer to read to, it reads "in 116 | /// place". 117 | /// 118 | /// Use this method for better efficiency. 119 | pub fn read_ext(&mut self, buf: &mut Vec) -> io::Result<()> { 120 | buf.extend_from_slice(&[0; AES_GCM_TAG_SIZE]); 121 | // Reading the encrypted chunk 122 | self.0.read_exact(buf)?; 123 | let mut nonce = [0; AES_GCM_NONCE_SIZE]; 124 | // Reading the nonce 125 | self.0.read_exact(&mut nonce)?; 126 | 127 | // This method automatically removes the tag 128 | self.1.decrypt_in_place(buf, &nonce).expect("Could not decrypt."); 129 | 130 | Ok(()) 131 | } 132 | } 133 | 134 | #[cfg(feature = "relay")] 135 | impl UserBlocks { 136 | /// Constructor. 137 | pub fn new(path: &str) -> io::Result { 138 | Ok(UserBlocks { 139 | file: FileOperations::new(path)?, 140 | }) 141 | } 142 | 143 | /// Checks if an IP is blocked. 144 | pub fn check_block(&mut self, ip: &[u8]) -> io::Result { 145 | let mut content = Vec::new(); 146 | self.file.seek_start(0)?; 147 | self.file.file.get_mut().read_to_end(&mut content)?; 148 | 149 | // Split at newline 150 | for line in content.split(|i| i == &10u8) { 151 | if line == ip { 152 | return Ok(true); 153 | } 154 | } 155 | 156 | Ok(false) 157 | } 158 | 159 | pub fn add_block(&mut self, ip: &[u8]) -> io::Result<()> { 160 | self.file.write(&[ip, &[10u8]].concat())?; 161 | Ok(()) 162 | } 163 | } 164 | 165 | pub trait BaseSocket 166 | where 167 | T: AeadInPlace + Sync, 168 | { 169 | /// Returns the writer used in the connection. 170 | fn get_writer(&self) -> &SWriter; 171 | 172 | /// Returns a mutable writer used in the connection. 173 | fn get_mut_writer(&mut self) -> &mut SWriter; 174 | 175 | /// Reads a signal from the endpoint. 176 | /// 177 | /// Returns the signal. 178 | fn read_signal(&mut self) -> io::Result { 179 | let mut signal = vec![0; SIGNAL_LEN]; 180 | self.get_mut_writer().read_ext(&mut signal)?; 181 | let signal = bytes_to_string(&signal); 182 | Ok(signal.as_str().into()) 183 | } 184 | 185 | /// Reads a signal from a relay. 186 | /// 187 | /// Returns the signal. 188 | fn read_signal_relay(&mut self) -> io::Result { 189 | let mut signal = vec![0; SIGNAL_LEN]; 190 | self.get_mut_writer().0.read_exact(&mut signal)?; 191 | let signal = bytes_to_string(&signal); 192 | 193 | Ok(signal.as_str().into()) 194 | } 195 | 196 | /// Reads the metadata. 197 | /// 198 | /// Returns a JSON object of the metadata. 199 | fn read_metadata(&mut self) -> io::Result { 200 | let mut metadata = vec![0; MAX_METADATA_LEN]; 201 | self.get_mut_writer().read_ext(&mut metadata)?; 202 | 203 | let metadata_json = json::parse(&{ 204 | let metadata_string = bytes_to_string(&metadata); 205 | // Reading the metadata is a fixed size, and len(metadata) <= MAX_METADATA_LEN, so we 206 | // need to split `metadata`. 207 | match metadata_string.split_once('\0') { 208 | None => metadata_string, 209 | Some(v) => v.0.to_string(), 210 | } 211 | }).expect("Couldn't convert metadata buffer to JSON."); 212 | log::trace!("{}", metadata_json.pretty(2)); 213 | 214 | Ok(metadata_json) 215 | } 216 | 217 | /// Reads chunks of the file from the endpoint and writes them into a file object. 218 | /// Only the receiver uses this method. 219 | /// 220 | /// Returns the file-checksum of the sender's. 221 | fn read_write_data(&mut self, file: &mut FileOperations, supposed_len: u64, num_threads: usize, 222 | will_checksum: bool) -> Result, Errors> { 223 | const AES_ADD: usize = AES_GCM_NONCE_SIZE + AES_GCM_TAG_SIZE; 224 | const CHUNK_SIZE: usize = MAX_CONTENT_LEN + AES_ADD; 225 | 226 | info!("Reading file chunks ..."); 227 | 228 | let mut buffer = vec![0; CHUNK_SIZE * num_threads]; 229 | let encryptor = self.get_writer().1.clone(); 230 | let mut reader = BufReader::with_capacity(buffer.len(), self.get_mut_writer().0.try_clone()?); 231 | 232 | while file.len()? <= supposed_len { 233 | reader.read_exact(&mut buffer)?; 234 | 235 | buffer.par_chunks_exact_mut(CHUNK_SIZE).for_each(|chunk| { 236 | let (data, nonce) = chunk.split_at_mut(chunk.len()-AES_GCM_NONCE_SIZE); 237 | encryptor.decrypt_in_place_detached(data, nonce).expect("Can't decrypt"); 238 | }); 239 | 240 | let io_sliced_buf: Vec = buffer.par_chunks_exact(CHUNK_SIZE).map(|chunk| 241 | IoSlice::new(&chunk[..chunk.len()-AES_ADD])).collect(); 242 | 243 | file.file.write_vectored(&io_sliced_buf)?; 244 | } 245 | 246 | file.set_len(supposed_len)?; 247 | 248 | let mut checksum = [0; MAX_CHECKSUM_LEN + AES_ADD]; 249 | if will_checksum { 250 | reader.read_exact(&mut checksum)?; 251 | } 252 | 253 | // Returns the sender's checksum 254 | Ok( 255 | if will_checksum { decrypt_aes_gcm!(self.get_writer().1, checksum) } else {checksum.to_vec()} 256 | ) 257 | } 258 | 259 | /// Returns true if checksums are equal, false if they're not. 260 | /// 261 | /// Returns error when there is a connection error. 262 | /// Checks the starting checksum. Encryption must be enabled. 263 | /// 264 | /// Returns bool if the local checksum equal to the sender's checksum. 265 | fn check_starting_checksum(&mut self, file: &mut FileOperations, end_pos: u64) -> io::Result { 266 | debug!("Computing starting checksum ..."); 267 | file.compute_checksum(end_pos)?; 268 | 269 | self.get_mut_writer().write_ext(&mut file.checksum())?; 270 | let mut checksum_bytes = vec![0; SHA_256_LEN]; 271 | self.get_mut_writer().read_ext(&mut checksum_bytes)?; 272 | 273 | Ok(checksum_bytes == file.checksum()) 274 | } 275 | 276 | /// Gets shared secret from both endpoints and creates a new "encryptor" object to encrypt the 277 | /// connection. 278 | fn shared_secret(&mut self) -> io::Result<()>; 279 | 280 | /// The main function for downloading in a P2P mode (sender -> receiver) or from a relay. 281 | /// 282 | /// Returns false if the checksum step failed. 283 | fn download(&mut self, num_threads: usize) -> Result { 284 | debug!("Getting metadata"); 285 | let metadata = self.read_metadata()?; 286 | 287 | let sizeb = metadata["metadata"]["size"].as_u64().unwrap_or(0); 288 | let sizemb = sizeb / 10_u64.pow(6); 289 | info!("Incoming {}MB file", sizemb); 290 | 291 | let filename = metadata["metadata"]["filename"].as_str().unwrap_or("null") 292 | .split('/').last().unwrap_or("null") 293 | .split('\\').last().unwrap_or("null"); 294 | 295 | // If a file with the same name exists in the current directory, then exit. 296 | if FileOperations::is_file_exists(filename) { 297 | error!("Won't overwrite file."); 298 | return Err(Errors::BasFileChcks); 299 | } 300 | 301 | let (mut file, existed) = checks_open_file(filename)?; 302 | let file_len = file.len()?; 303 | 304 | self.get_mut_writer() 305 | .write_ext(mut_vec!(if existed && file.len()? != sizeb { 306 | file_len.to_le_bytes() 307 | } else { 308 | [0; 8] 309 | }))?; 310 | 311 | // If there is an eavesdropper, he won't be able to know if the file exists on the 312 | // receiver's computer or not, because some checksum is written anyway. 313 | if !self.check_starting_checksum(&mut file, file_len)? { 314 | error!("Checksum not equal."); 315 | info!("Starting from 0 since the file was modified"); 316 | file.reset_checksum(); 317 | file.seek_start(0)?; 318 | } else { 319 | file.seek_end(0)?; 320 | } 321 | 322 | let filename = metadata["metadata"]["filename"].as_str().unwrap_or("null"); 323 | let will_checksum = metadata["metadata"]["will_checksum"].as_bool().unwrap_or(false); 324 | 325 | let recv_checksum = self.read_write_data(&mut file, sizeb, num_threads, will_checksum)?; 326 | 327 | if will_checksum { 328 | info!("Verifiying ..."); 329 | file.compute_checksum(u64::MAX)?; 330 | 331 | // If the checksum isn't valid 332 | if recv_checksum != file.checksum() { 333 | error!("Checksum not equal."); 334 | if get_accept_input("Keep the file? ").expect("Couldn't read answer") != 'y' { 335 | FileOperations::rm(&format!("{}/{}/.{}.tmp", get_home_dir(), AFT_DIRNAME, filename))?; 336 | } 337 | return Ok(false); 338 | } 339 | } 340 | 341 | let modified_time = metadata["metadata"]["modified"].as_u64().unwrap_or(0); 342 | file.file.get_mut().set_modified(time::SystemTime::UNIX_EPOCH + time::Duration::from_secs(modified_time))?; 343 | 344 | FileOperations::rename(&format!("{}/{}/.{}.tmp", get_home_dir(), AFT_DIRNAME, filename), filename)?; 345 | 346 | // Confirm the transfer 347 | self.get_mut_writer().write_ext(&mut Signals::OK.as_bytes().to_vec())?; 348 | 349 | Ok(true) 350 | } 351 | } 352 | 353 | pub trait Crypto { 354 | /// Exchanges the public key between two parties. 355 | /// 356 | /// Returns the other party public key. 357 | fn exchange_pk(&mut self, pk: PublicKey) -> io::Result; 358 | 359 | /// Generates a public key and a secret key and finally a shared secret. 360 | /// 361 | /// Returns a shared secret. 362 | fn gen_shared_secret(&mut self) -> io::Result { 363 | info!("Exchanging keys"); 364 | let (pk, secret) = X25519Key::generate_keys(); 365 | 366 | Ok(X25519Key::new(secret, &self.exchange_pk(pk)?)) 367 | } 368 | } 369 | 370 | #[cfg(feature = "relay")] 371 | pub struct Downloader { 372 | writer: SWriter, 373 | ident: String, 374 | gen_encryptor: fn(&[u8]) -> T, 375 | blocks: UserBlocks, 376 | } 377 | 378 | #[cfg(feature = "relay")] 379 | impl BaseSocket for Downloader 380 | where 381 | T: AeadInPlace + Sync, 382 | { 383 | fn get_writer(&self) -> &SWriter { 384 | &self.writer 385 | } 386 | 387 | fn get_mut_writer(&mut self) -> &mut SWriter { 388 | &mut self.writer 389 | } 390 | 391 | fn shared_secret(&mut self) -> io::Result<()> { 392 | let shared_key = self.gen_shared_secret()?; 393 | self.writer.1 = EncAlgo::new(shared_key.as_bytes(), self.gen_encryptor); 394 | Ok(()) 395 | } 396 | } 397 | 398 | #[cfg(feature = "relay")] 399 | impl Crypto for Downloader 400 | where 401 | T: AeadInPlace, 402 | { 403 | fn exchange_pk(&mut self, pk: PublicKey) -> io::Result { 404 | let mut other_pk = [0; 32]; 405 | 406 | // Writing the public key 407 | debug!("Writing public key"); 408 | self.writer.0.write_all(pk.as_bytes())?; 409 | // Getting endpoint's public key 410 | debug!("Getting public key"); 411 | self.writer.0.read_exact(&mut other_pk)?; 412 | 413 | Ok(PublicKey::from(other_pk)) 414 | } 415 | } 416 | 417 | #[cfg(feature = "relay")] 418 | impl Downloader 419 | where 420 | T: AeadInPlace + Sync, 421 | { 422 | /// Constructor. Connects to `remote_ip` automatically. 423 | pub fn new(remote_ip: &str, ident: String, encryptor_func: fn(&[u8]) -> T) -> Self { 424 | let socket = TcpStream::connect(remote_ip).expect("Couldn't connect."); 425 | Downloader { 426 | ident, 427 | writer: SWriter(socket, EncAlgo::::new(&[0; KEY_LENGTH], encryptor_func)), 428 | gen_encryptor: encryptor_func, 429 | blocks: UserBlocks::new(&format!("{}/{}/{}", get_home_dir(), AFT_DIRNAME, BLOCKED_FILENAME)).expect("Couldn't open blocked users file."), 430 | } 431 | } 432 | 433 | /// Checks if the receiver is connected to a relay. 434 | /// 435 | /// Returns true if yes, and false if not. 436 | pub fn is_connected_to_relay(&mut self) -> io::Result { 437 | let mut relay_or_client = [0; 1]; 438 | self.writer.0.read_exact(&mut relay_or_client)?; 439 | Ok(relay_or_client[0] == RELAY) 440 | } 441 | 442 | /// The main method when connecting to a relay. Handles the transferring process. 443 | pub fn init(&mut self, num_threads: usize) -> Result { 444 | if !self.is_connected_to_relay()? { 445 | return Err(Errors::NotRelay); 446 | } 447 | 448 | // Write to the relay the client connecting is a receiver 449 | self.writer.0.write_all(&[CLIENT_RECV])?; 450 | 451 | if !send_identifier(self.ident.as_bytes(), &mut self.writer.0)? { 452 | return Err(Errors::InvalidIdent); 453 | } 454 | 455 | info!("Waiting for requests ..."); 456 | loop { 457 | 458 | loop { 459 | match self.read_signal_relay()? { 460 | Signals::StartFt => break, 461 | // Connectivity check 462 | Signals::Other => self.writer.0.write_all(&[1])?, 463 | Signals::Error => { 464 | return Err(Errors::IdentUnaval); 465 | } 466 | s => panic!("Invalid signal when reading signal from relay. {}", s), 467 | } 468 | } 469 | 470 | // Read the sender's identifier 471 | let mut sen_ident_bytes = [0; MAX_IDENTIFIER_LEN]; 472 | self.writer.0.read_exact(&mut sen_ident_bytes)?; 473 | let sen_ident = &bytes_to_string(&sen_ident_bytes); 474 | 475 | // Read the sender's hashed IP 476 | let mut sen_hashed_ip = [0; SHA_256_LEN]; 477 | self.writer.0.read_exact(&mut sen_hashed_ip)?; 478 | 479 | // If this IP isn't blocked 480 | if !self.blocks.check_block(&sen_hashed_ip)? { 481 | match get_accept_input(&format!("{} wants to send you a file (y/n/b): ", sen_ident))? { 482 | // Yes 483 | 'y' => break, 484 | // No 485 | 'n' => (), 486 | // Block 487 | 'b' => self.blocks.add_block(&sen_hashed_ip)?, 488 | // Invalid input 489 | _ => panic!("Invalid input"), 490 | }; 491 | } 492 | 493 | // If the receiver rejected/blocked him 494 | self.writer.0.write_all(Signals::Error.as_bytes())?; 495 | } 496 | 497 | // Write that the receiver accepts the request 498 | self.writer.0.write_all(Signals::OK.as_bytes())?; 499 | 500 | // Exchange secret key with the sender 501 | self.shared_secret()?; 502 | 503 | self.download(num_threads) 504 | } 505 | } 506 | 507 | pub struct Receiver { 508 | writer: SWriter, 509 | gen_encryptor: fn(&[u8]) -> T, 510 | } 511 | 512 | impl BaseSocket for Receiver 513 | where 514 | T: AeadInPlace + Sync, 515 | { 516 | fn get_writer(&self) -> &SWriter { 517 | &self.writer 518 | } 519 | 520 | fn get_mut_writer(&mut self) -> &mut SWriter { 521 | &mut self.writer 522 | } 523 | 524 | fn shared_secret(&mut self) -> io::Result<()> { 525 | let shared_key = self.gen_shared_secret()?; 526 | self.writer.1 = EncAlgo::new(shared_key.as_bytes(), self.gen_encryptor); 527 | Ok(()) 528 | } 529 | } 530 | 531 | impl Crypto for Receiver 532 | where 533 | T: AeadInPlace, 534 | { 535 | fn exchange_pk(&mut self, pk: PublicKey) -> io::Result { 536 | let mut other_pk = [0; 32]; 537 | 538 | // Writing the public key 539 | debug!("Writing public key"); 540 | self.writer.0.write_all(pk.as_bytes())?; 541 | // Getting endpoint's public key 542 | debug!("Getting public key"); 543 | self.writer.0.read_exact(&mut other_pk)?; 544 | 545 | Ok(PublicKey::from(other_pk)) 546 | } 547 | } 548 | 549 | impl Receiver 550 | where 551 | T: AeadInPlace + Sync, 552 | { 553 | /// Constructor. Creates a listener on `addr` automatically. 554 | pub fn new(addr: &str, encryptor_func: fn(&[u8]) -> T) -> Self { 555 | let listener = TcpListener::bind(addr).expect("Couldn't bind to address"); 556 | let (socket, _) = listener.accept().expect("Couldn't accept connection"); 557 | info!("Connected to sender"); 558 | 559 | Receiver { 560 | writer: SWriter(socket, EncAlgo::::new(&[0; KEY_LENGTH], encryptor_func)), 561 | gen_encryptor: encryptor_func, 562 | } 563 | } 564 | 565 | /// Authenticates with the sender's end. 566 | /// 567 | /// Returns true if the password received from the sender is the correct password, else false. 568 | pub fn auth(&mut self, correct_pass: SData) -> io::Result { 569 | info!("Authenticating ..."); 570 | 571 | // Sha256 is 256 bits => 256 / 8 => 32 572 | let mut pass = SData(vec![0; 32]); 573 | self.writer.read_ext(&mut pass.0)?; 574 | 575 | let mut sha = Sha256::new(); 576 | sha.update(&correct_pass.0); 577 | 578 | if pass.0 == sha.finalize().as_slice() { 579 | self.writer.write_ext(mut_vec!(Signals::OK.as_bytes()))?; 580 | Ok(true) 581 | } else { 582 | self.writer.write_ext(mut_vec!(Signals::Error.as_bytes()))?; 583 | Ok(false) 584 | } 585 | } 586 | 587 | /// The main function for receiving in P2P mode (sender -> receiver). 588 | pub fn receive(&mut self, pass: SData, num_threads: usize) -> Result { 589 | // Write to the sender that its connecting to a receiver 590 | self.writer.0.write_all(&[CLIENT_RECV])?; 591 | 592 | self.shared_secret()?; 593 | 594 | if !self.auth(pass)? { 595 | return Err(Errors::InvalidPass); 596 | } 597 | 598 | self.download(num_threads) 599 | } 600 | } 601 | -------------------------------------------------------------------------------- /aft/src/config.rs: -------------------------------------------------------------------------------- 1 | //! Handles the config file. 2 | use crate::errors::ErrorsConfig; 3 | use log::error; 4 | use std::{fs::File, io::prelude::*, path::Path}; 5 | 6 | const VERBOSE_OPTION: &str = "verbose"; 7 | const IDENTIFIER_OPTION: &str = "identifier"; 8 | const MODE_OPTION: &str = "mode"; 9 | const DELIMITER: &str = "="; 10 | const OPTIONS: [&str; 3] = [VERBOSE_OPTION, IDENTIFIER_OPTION, MODE_OPTION]; 11 | 12 | enum Options { 13 | Verbose(u8), 14 | Identifier(String), 15 | DefaultMode(u8), 16 | None, 17 | } 18 | 19 | pub struct Config { 20 | verbose: Options, 21 | identifier: Options, 22 | } 23 | 24 | impl Config { 25 | /// Builds a new config object. 26 | pub fn new(path: &str) -> Result { 27 | let path = Path::new(path); 28 | if !path.is_dir() { 29 | let mut config = String::new(); 30 | File::open(path)?.read_to_string(&mut config)?; 31 | return Config::generate_config(config); 32 | } 33 | Ok(Config::default()) 34 | } 35 | 36 | fn generate_config(content: String) -> Result { 37 | let mut verbose = Options::None; 38 | let mut identifier = Options::None; 39 | let mut mode = Options::None; 40 | 41 | for (index, line) in content.lines().enumerate() { 42 | let line_split: Vec<&str> = line.split(DELIMITER).collect(); 43 | // "option = value" 44 | if line_split.len() != 2 || !OPTIONS.contains(&line_split[0]) { 45 | error!("Bad syntax, line: {}", index); 46 | return Err(ErrorsConfig::WrongSyntax); 47 | } 48 | 49 | match line_split[0].to_lowercase().as_str().trim() { 50 | VERBOSE_OPTION => { 51 | if let Options::None = verbose { 52 | let value = Config::get_char_val(&line_split, index)?; 53 | if value > '0' && value < '3' { 54 | // safe to unwrap because we checked if its a digit or not 55 | verbose = Options::Verbose(value.to_digit(10).unwrap() as u8); 56 | } 57 | } else { 58 | error!("Already assigned a value, line: {}", index); 59 | return Err(ErrorsConfig::WrongSyntax); 60 | } 61 | } 62 | IDENTIFIER_OPTION => { 63 | if let Options::None = identifier { 64 | identifier = Options::Identifier(line_split[1].to_string()); 65 | } else { 66 | error!("Already assigned a value, line: {}", index); 67 | return Err(ErrorsConfig::WrongSyntax); 68 | } 69 | } 70 | MODE_OPTION => { 71 | if let Options::None = mode { 72 | let value = Config::get_char_val(&line_split, index)?; 73 | // modes: 1=client, 2=receiver, 3=download and 4=relay. 74 | if value > '0' && value < '5' { 75 | // safe to unwrap because we checked if its a digit or not 76 | mode = Options::DefaultMode(value.to_digit(10).unwrap() as u8); 77 | } 78 | } else { 79 | error!("Already assigned a value, line: {}", index); 80 | return Err(ErrorsConfig::WrongSyntax); 81 | } 82 | } 83 | _ => { 84 | error!("No such option, line: {}", index); 85 | return Err(ErrorsConfig::NoOption); 86 | } 87 | } 88 | } 89 | 90 | Ok(Config { 91 | verbose, 92 | identifier, 93 | }) 94 | } 95 | 96 | fn get_char_val(tokens: &[&str], index: usize) -> Result { 97 | let value = tokens[1].trim().chars().next(); 98 | if value.is_none() { 99 | error!("Bad syntax, line: {}", index); 100 | return Err(ErrorsConfig::WrongSyntax); 101 | } 102 | Ok(value.unwrap()) 103 | } 104 | 105 | /// Returns verbose number if set, else, returns 0 (info only). 106 | pub fn get_verbose(&self) -> u8 { 107 | if let Options::Verbose(val) = self.verbose { 108 | val 109 | } else { 110 | // info only 111 | 3 112 | } 113 | } 114 | 115 | /// Returns the identifier if set, else, returns None. 116 | pub fn get_identifier(&self) -> Option<&String> { 117 | if let Options::Identifier(val) = &self.identifier { 118 | Some(val) 119 | } else { 120 | None 121 | } 122 | } 123 | } 124 | 125 | impl Default for Config { 126 | fn default() -> Self { 127 | Config { 128 | verbose: Options::None, 129 | identifier: Options::None, 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /aft/src/constants.rs: -------------------------------------------------------------------------------- 1 | /// The default aft port. 2 | pub const DEFAULT_PORT: u16 = 1122; 3 | /// Maximum filetype length. 4 | pub const MAX_TYPE_LEN: usize = 20; 5 | /// Maximum name length. 6 | // 50 is an optimal name length. 7 | pub const MAX_FILENAME_LEN: usize = 50; 8 | /// Maximum length of "size" in JSON `metadata`. 9 | // 20 = len(u64::Max) 10 | pub const MAX_SIZE_LEN: usize = 20; 11 | /// Maximum length of the "modified" in the `metadata` JSON. 12 | pub const MAX_MODIFIED_LEN: usize = 12; 13 | /// Maximum username length. 14 | pub const MAX_IDENTIFIER_LEN: usize = 10; 15 | /// Maximum buffer length that is received from a stream. 16 | pub const MAX_METADATA_LEN: usize = MAX_FILENAME_LEN + MAX_TYPE_LEN + MAX_SIZE_LEN + MAX_MODIFIED_LEN + 40 /* 40 = other chars such as { */; 17 | /// Maximum size of a chunk. 18 | pub const MAX_CONTENT_LEN: usize = 16384; 19 | /// Maximum checksum length (Sha256 length in bytes). 20 | pub const MAX_CHECKSUM_LEN: usize = 32; 21 | /// Length of a blocks column. 22 | pub const MAX_BLOCKS_LEN: usize = 3000; 23 | /// Code for a client that sends data. 24 | pub const CLIENT_SEND: u8 = 0; 25 | /// Code for a client that receives data. 26 | pub const CLIENT_RECV: u8 = 1; 27 | /// Code for a relay, acting as a proxy. 28 | pub const RELAY: u8 = 2; 29 | /// Signal length. 30 | pub const SIGNAL_LEN: usize = 6; 31 | /// SHA-256 hash length in bytes. 32 | pub const SHA_256_LEN: usize = 32; 33 | /// Blocked user filename. 34 | pub const BLOCKED_FILENAME: &str = ".blocks"; 35 | /// aft directory name. 36 | pub const AFT_DIRNAME: &str = ".aft"; 37 | -------------------------------------------------------------------------------- /aft/src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::{error, fmt, io::Error as ioError}; 2 | 3 | #[derive(Debug)] 4 | pub enum Errors { 5 | /// Represents a wrong response from the relay or the client. 6 | WrongResponse, 7 | /// Represents a wrong format buffer from the relay or the client. 8 | WrongFormat, 9 | /// Used when there is no file extension in metadata buffer. 10 | NoFileExtension, 11 | /// Stream buffer is too big. 12 | BufferTooBig, 13 | /// When requesting from a socket to download. 14 | NotRelay, 15 | /// When the client don't have the receiver's identifier. 16 | NoReceiverIdentifier, 17 | /// Invalid identifier. 18 | InvalidIdent, 19 | /// Didn't pass basic file checks. 20 | BasFileChcks, 21 | /// Invalid signal. 22 | InvalidSignal, 23 | /// Incorrect password. 24 | InvalidPass, 25 | /// Identifier unavailable. 26 | IdentUnaval, 27 | /// Input/output errors. 28 | IO(ioError), 29 | } 30 | 31 | #[derive(Debug)] 32 | pub enum ErrorsConfig { 33 | WrongSyntax, 34 | AlreadyAssigned, 35 | NoOption, 36 | IO(ioError), 37 | } 38 | 39 | impl fmt::Display for Errors { 40 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 41 | match self { 42 | Errors::WrongResponse => write!(f, "Wrong response."), 43 | Errors::WrongFormat => write!(f, "Wrong format."), 44 | Errors::NoFileExtension => write!(f, "No file extension."), 45 | Errors::BufferTooBig => write!(f, "Buffer too big."), 46 | Errors::NotRelay => write!(f, "Not a relay."), 47 | Errors::NoReceiverIdentifier => write!(f, "No receiver identifier."), 48 | Errors::InvalidIdent => write!(f, "Invalid identifier/s. Check if the identifier is too long or empty."), 49 | Errors::BasFileChcks => write!(f, "Didn't pass basic file checks."), 50 | Errors::InvalidPass => write!(f, "Incorrect password."), 51 | Errors::IdentUnaval => write!(f, "The provided identifier is not available."), 52 | Errors::InvalidSignal => write!(f, "Received an invalid signal."), 53 | Errors::IO(err) => write!(f, "IO: {:?}", err), 54 | } 55 | } 56 | } 57 | 58 | impl fmt::Display for ErrorsConfig { 59 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 60 | match self { 61 | ErrorsConfig::WrongSyntax => write!(f, "Bad syntax."), 62 | ErrorsConfig::AlreadyAssigned => write!(f, "Already assigned a value to this option."), 63 | ErrorsConfig::NoOption => write!(f, "No such option."), 64 | ErrorsConfig::IO(err) => write!(f, "IO: {:?}", err), 65 | } 66 | } 67 | } 68 | 69 | impl From for Errors { 70 | fn from(err: ioError) -> Self { 71 | Errors::IO(err) 72 | } 73 | } 74 | 75 | impl From for ErrorsConfig { 76 | fn from(err: ioError) -> Self { 77 | ErrorsConfig::IO(err) 78 | } 79 | } 80 | 81 | impl error::Error for Errors {} 82 | impl error::Error for ErrorsConfig {} 83 | -------------------------------------------------------------------------------- /aft/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Main. 2 | #[cfg(feature = "clients")] 3 | pub mod clients; 4 | pub mod config; 5 | pub mod constants; 6 | pub mod errors; 7 | #[cfg(feature = "relay")] 8 | pub mod relay; 9 | #[cfg(feature = "sender")] 10 | pub mod sender; 11 | pub mod utils; 12 | 13 | 14 | use aft_crypto::{ 15 | bip39, 16 | data::{create_128_encryptor, create_256_encryptor, Algo, SData}, 17 | password_generator::generate_passphrase, 18 | }; 19 | use config::Config; 20 | use log::{error, info, Level}; 21 | use std::{env::args as args_fn, io::Write, net::{Ipv4Addr, ToSocketAddrs}}; 22 | 23 | const SENDER_MODE: u8 = 1; 24 | const RECEIVER_MODE: u8 = 2; 25 | const DOWNLOAD_MODE: u8 = 3; 26 | const RELAY_MODE: u8 = 4; 27 | const DESCR_MSG: &str = "aft - file transfer done easily"; 28 | const USAGE_MSG: &str = "Usage: 29 | aft sender [--address
] [--port ] [--identifier ] 30 | aft receiver [-p ] 31 | aft download -a
[-p ] [-i ] 32 | aft relay [-p ] 33 | aft [options ...]"; 34 | const POSITIONAL_ARGS_MSG: &str = "Positional arguments: 35 | mode 36 | "; 37 | const OPTIONS_ARGS_MSG: &str = "Optional arguments: 38 | -a --address ADDRESS Address. 39 | -p --port PORT Port. 40 | -i --identifier IDENTIFIER Identifier to find the receiver. Used only when its not P2P. 41 | -v --verbose VERBOSE Verbose level. Default is 1 (warnings only). Range 1-3. 42 | -c --config CONFIG Config location. 43 | -v --version Show version. 44 | -e --encryption ALGORITHM Possible values: [AES128, AES256]. 45 | -t --threads THREADS Number of threads to use. 46 | -s --checksum Check checksum at the end. Only relevant if mode == sender."; 47 | const PASSPHRASE_DEFAULT_LEN: u8 = 6; 48 | 49 | macro_rules! create_sender { 50 | ($algo:ident, $cliargs:ident, $sen_ident:expr, $addr:ident, $pass:ident) => { 51 | { 52 | let mut sender = sender::Sender::new($addr, $algo, $cliargs.checksum, $cliargs.algo); 53 | 54 | let init = sender.init($cliargs.filename, $sen_ident, 55 | $cliargs.identifier, $pass); 56 | 57 | match init { 58 | Ok(b) => if !b {return;}, 59 | Err(e) => {error!("{e}"); return;} 60 | } 61 | if let Err(e) = sender.send_chunks($cliargs.threads) { 62 | error!("Connection error: {}", e); 63 | } 64 | } 65 | } 66 | } 67 | 68 | struct CliArgs<'a> { 69 | mode: u8, 70 | address: Option, 71 | port: u16, 72 | identifier: Option<&'a str>, 73 | verbose: u8, 74 | filename: &'a str, 75 | algo: Algo, 76 | threads: usize, 77 | pub checksum: bool, 78 | } 79 | 80 | impl<'a> CliArgs<'a> { 81 | pub fn new(mode: u8) -> Self { 82 | CliArgs { 83 | mode, 84 | address: None, 85 | port: constants::DEFAULT_PORT, 86 | identifier: None, 87 | verbose: 1, 88 | filename: "", 89 | algo: Algo::Aes128, 90 | // SAFETY: 4 != 0 91 | threads: std::thread::available_parallelism() 92 | .unwrap_or(std::num::NonZero::new(4).unwrap()).get(), 93 | checksum: false, 94 | } 95 | } 96 | 97 | pub fn is_relay_receiver(&self) -> bool { 98 | [RELAY_MODE, RECEIVER_MODE].contains(&self.mode) 99 | } 100 | 101 | pub fn is_sender(&self) -> bool { 102 | self.mode == SENDER_MODE 103 | } 104 | 105 | pub fn set_address(&mut self, address: String) -> bool { 106 | if self.mode == RELAY_MODE { 107 | return false; 108 | } 109 | self.address = Some(address); 110 | true 111 | } 112 | 113 | pub fn set_port(&mut self, port: u16) { 114 | self.port = port; 115 | } 116 | 117 | pub fn set_identifier(&mut self, identifier: &'a str) -> bool { 118 | if self.mode == RELAY_MODE { 119 | return false; 120 | } 121 | self.identifier = Some(identifier); 122 | true 123 | } 124 | 125 | pub fn set_verbose(&mut self, verbose: u8) -> bool { 126 | if (1..=3).contains(&verbose) { 127 | return false; 128 | } 129 | self.verbose = verbose; 130 | true 131 | } 132 | 133 | pub fn set_filename(&mut self, filename: &'a str) -> bool { 134 | if [RELAY_MODE, DOWNLOAD_MODE, RECEIVER_MODE].contains(&self.mode) || filename.is_empty() { 135 | return false; 136 | } 137 | self.filename = filename; 138 | true 139 | } 140 | 141 | pub fn set_algo(&mut self, algo: &str) { 142 | self.algo = algo.to_lowercase().as_str().into(); 143 | } 144 | 145 | pub fn set_threads(&mut self, threads: usize) -> bool { 146 | if threads == 0 { 147 | return false; 148 | } 149 | self.threads = threads; 150 | true 151 | } 152 | } 153 | 154 | /// Checks if the terminal supports ANSI escape codes. 155 | fn check_support_ansi() -> bool { 156 | if cfg!(windows) { 157 | if let Ok(term) = std::env::var("TERM") { 158 | if !term.starts_with("xterm") { 159 | return false; 160 | } 161 | } 162 | } 163 | 164 | // Unix machines support ANSI escape codes out of the box. 165 | true 166 | 167 | } 168 | 169 | /// Builds the logger. 170 | fn build_logger(level: &str) { 171 | let env = env_logger::Env::default().default_filter_or(level); 172 | let mut binding = env_logger::Builder::from_env(env); 173 | let builder = if ["trace", "debug"].contains(&level) { 174 | binding.format(|buf, record| { 175 | let color; 176 | let level = record.level(); 177 | if !check_support_ansi() { 178 | return writeln!(buf, "[{} {}] {}", buf.timestamp(), level, record.args()); 179 | } 180 | else if level == Level::Warn { 181 | // Yellow color 182 | color = "\x1B[0;33m"; 183 | } else if level == Level::Error { 184 | // Red color 185 | color = "\x1B[0;91m"; 186 | } else { 187 | // Green color 188 | color = "\x1B[0;92m"; 189 | } 190 | writeln!(buf, "[{} {color}{}\x1B[0;0m] {}", buf.timestamp(), level, record.args()) 191 | }) 192 | } else { 193 | binding.format(|buf, record| { 194 | let msg; 195 | let level = record.level(); 196 | if [Level::Warn, Level::Error].contains(&level) { 197 | msg = if check_support_ansi() {"\x1B[0;91m[!]\x1B[0;0m"} else {"[!]"}; 198 | } else { 199 | msg = if check_support_ansi() {"\x1B[0;92m[*]\x1B[0;0m"} else {"[*]"}; 200 | } 201 | writeln!(buf, "{msg} {}", record.args()) 202 | }) 203 | }.target(env_logger::Target::Stdout); 204 | 205 | builder.init(); 206 | } 207 | 208 | /// Generates code-phrase from an IP address. This only supports IPv4 addresses. 209 | /// 210 | /// Returns the code-phrase. 211 | fn generate_code_from_pub_ip() -> String { 212 | let pub_ip = utils::get_pub_ip().expect("Couldn't get public IP address"); 213 | let octets = utils::ip_to_octets(&pub_ip).map(|octet| octet as usize); 214 | // An octet maximum size is 256 215 | let wordlist = &bip39::create_wordlist()[..=255]; 216 | 217 | let mut codes = String::new(); 218 | 219 | for octet in octets { 220 | codes.push_str(wordlist[octet]); 221 | codes.push('-'); 222 | } 223 | 224 | // Remove the last dash 225 | codes.pop(); 226 | 227 | codes 228 | } 229 | 230 | /// Gets the IP from a generated code-phrase. Only supports IPv4 addresses. 231 | /// Basically the reversed edition of `generate_code_from_pub_ip`. 232 | /// 233 | /// Returns the IP. 234 | fn get_ip_from_code(codes: &str) -> String { 235 | let wordlist = &bip39::create_wordlist()[..=255]; 236 | 237 | let mut pub_ip = String::new(); 238 | 239 | for code in codes.split('-') { 240 | for (i, word) in wordlist.iter().enumerate() { 241 | if word == &code { 242 | pub_ip.push_str(&i.to_string()); 243 | pub_ip.push('.'); 244 | } 245 | } 246 | } 247 | 248 | pub_ip.pop(); 249 | 250 | pub_ip 251 | } 252 | 253 | fn create_aft_dir() -> std::io::Result<()> { 254 | let path = &format!("{}/{}", utils::get_home_dir(), constants::AFT_DIRNAME); 255 | if std::path::Path::new(path).exists() { 256 | return Ok(()); 257 | } 258 | std::fs::create_dir(path) 259 | } 260 | 261 | #[cfg(feature = "relay")] 262 | #[tokio::main] 263 | async fn run_relay(port: u16) { 264 | info!("Running relay"); 265 | relay::init(&format!("0.0.0.0:{}", port)).await.unwrap(); 266 | } 267 | 268 | fn main() { 269 | let args: Vec = args_fn().collect(); 270 | if args.len() == 1 || args.len() > 16 { 271 | println!("{}\n\n{}\n\n{}\n{}", DESCR_MSG, USAGE_MSG, POSITIONAL_ARGS_MSG, OPTIONS_ARGS_MSG); 272 | return; 273 | } 274 | 275 | let mut config = Config::new(&format!("{}/{}/config", utils::get_home_dir(), constants::AFT_DIRNAME)) 276 | .unwrap_or_default(); 277 | let mut verbose_mode = config.get_verbose(); 278 | 279 | if args.len() - 1 == 1 && ["--version"].contains(&args[1].as_str()) { 280 | println!("aft v{}", env!("CARGO_PKG_VERSION")); 281 | return; 282 | } 283 | 284 | let mut cliargs = CliArgs::new(match args[1].to_lowercase().as_str() { 285 | "sender" => SENDER_MODE, 286 | "receiver" => RECEIVER_MODE, 287 | "download" => DOWNLOAD_MODE, 288 | "relay" => RELAY_MODE, 289 | _ => { 290 | println!("Invalid mode."); 291 | return; 292 | } 293 | }); 294 | 295 | if !cliargs.is_relay_receiver() && args.len() < 3 { 296 | println!("Not enough arguments provided."); 297 | return; 298 | } 299 | 300 | let mut i = 2; 301 | while i < args.len() { 302 | let arg = &args[i]; 303 | i += 1; 304 | 305 | match arg.as_str() { 306 | "-a" | "--address" => { 307 | if cliargs.is_relay_receiver() { 308 | println!("Can't use {} argument when mode==relay | receiver", arg); 309 | return; 310 | } 311 | 312 | // Remove http(s):// since aft doesn't support HTTPS. 313 | let no_http_addr = args[i].replace("http://", "").replace("https://", ""); 314 | // If it's an IP 315 | let addr = if format!("{}:{}", no_http_addr, cliargs.port).parse::().is_ok() { 316 | no_http_addr 317 | // If It's some domain or some other address 318 | } else { 319 | match (no_http_addr, cliargs.port).to_socket_addrs() { 320 | Ok(v) => v, 321 | Err(_) => { 322 | error!("Address is invalid."); 323 | return; 324 | } 325 | }.next().expect("Couldn't resolve address.").ip().to_string() 326 | }; 327 | 328 | cliargs.set_address(addr); 329 | }, 330 | "-p" | "--port" => cliargs.set_port(if let Ok(v) = args[i].parse() { 331 | v 332 | } else { 333 | println!("Not a port."); 334 | return; 335 | }), 336 | "-i" | "--identifier" => { 337 | if cliargs.is_relay_receiver() { 338 | println!("Can't use {} argument when mode==relay,receiver", arg); 339 | return; 340 | } 341 | cliargs.set_identifier(&args[i]); 342 | }, 343 | "-v" | "--verbose" => { 344 | if !cliargs.set_verbose(if let Ok(v) = args[i].parse() { 345 | verbose_mode = v; 346 | v 347 | } else { 348 | println!("Invalid verbose level."); 349 | return; 350 | }) { 351 | println!("Invalid verbose level."); 352 | } 353 | }, 354 | "-c" | "--config" => { 355 | config = match Config::new(&args[i]) { 356 | Ok(v) => v, 357 | Err(_) => { 358 | println!("Invalid config location"); 359 | return; 360 | } 361 | } 362 | }, 363 | "-e" | "--encryption" => cliargs.set_algo(&args[i]), 364 | 365 | "-t" | "--threads" => if !cliargs.set_threads(args[i].parse().expect("Invalid threads input")) { 366 | println!("Invalid number of threads"); 367 | return; 368 | }, 369 | "-s" | "--checksum" => { 370 | cliargs.checksum = true; 371 | i -= 1; 372 | }, 373 | _ => { 374 | if cliargs.is_sender() && i == args.len() { 375 | cliargs.set_filename(&args[i-1]); 376 | } else { 377 | println!("Unknown argument {}", arg); 378 | return; 379 | } 380 | } 381 | } 382 | i += 1; 383 | } 384 | 385 | let verbose_mode = match verbose_mode { 386 | 1 => "warn", 387 | 2 => "info", 388 | 3 => "debug", 389 | _ => "trace", 390 | }; 391 | build_logger(verbose_mode); 392 | create_aft_dir().expect("Couldn't create directory"); 393 | 394 | if cliargs.mode == RELAY_MODE { 395 | #[cfg(not(feature = "relay"))] 396 | { 397 | error!("Relay is not supported for this executable."); 398 | } 399 | 400 | #[cfg(feature = "relay")] 401 | run_relay(cliargs.port); 402 | } else if cliargs.mode == RECEIVER_MODE { 403 | #[cfg(not(feature = "clients"))] 404 | { 405 | error!("Receiver is not supported for this executable."); 406 | return; 407 | } 408 | 409 | let mut pass = SData(rpassword::prompt_password("Password (press Enter to generate one): ").expect("Couldn't read password")); 410 | if pass.0.is_empty() { 411 | pass = SData(generate_passphrase(PASSPHRASE_DEFAULT_LEN)); 412 | println!("Generated passphrase: {}", pass.0); 413 | } 414 | println!("Code: {}", generate_code_from_pub_ip()); 415 | info!("Running receiver"); 416 | 417 | #[cfg(feature = "clients")] 418 | { 419 | let res = match cliargs.algo { 420 | Algo::Aes128 => 421 | clients::Receiver::new(&format!("0.0.0.0:{}", cliargs.port), create_128_encryptor).receive(pass, cliargs.threads), 422 | Algo::Aes256 => 423 | clients::Receiver::new(&format!("0.0.0.0:{}", cliargs.port), create_256_encryptor).receive(pass, cliargs.threads), 424 | _ => {error!("Unknown encryption algorithm."); return} 425 | }; 426 | 427 | match res { 428 | Ok(b) => if b {info!("Finished successfully.")}, 429 | Err(e) => error!("{}", e), 430 | } 431 | } 432 | } else if cliargs.mode == DOWNLOAD_MODE { 433 | #[cfg(not(feature = "relay"))] 434 | { 435 | error!("Downloading is not supported for this executable."); 436 | return; 437 | } 438 | 439 | info!("Running downloader"); 440 | let identifier = if let Some(ident) = cliargs.identifier { 441 | ident 442 | } else if let Some(ident) = config.get_identifier() { 443 | ident 444 | } else { 445 | error!("Identifier not set."); 446 | return; 447 | }.to_string(); 448 | 449 | let addr = &format!("{}:{}",cliargs.address.expect("No address specified"), cliargs.port); 450 | #[cfg(feature = "relay")] 451 | { 452 | let res = match cliargs.algo { 453 | Algo::Aes128 => clients::Downloader::new(addr, identifier, create_128_encryptor).init(cliargs.threads), 454 | Algo::Aes256=> clients::Downloader::new(addr, identifier, create_256_encryptor).init(cliargs.threads), 455 | _ => {error!("Unknown encryption algorithm."); return} 456 | }; 457 | 458 | match res { 459 | Ok(b) => if b {info!("Finished successfully.")}, 460 | Err(e) => error!("{}", e), 461 | } 462 | } 463 | } else if cliargs.mode == SENDER_MODE { 464 | #[cfg(not(feature = "sender"))] 465 | { 466 | error!("Sending is not supported for this executable."); 467 | return; 468 | } 469 | 470 | info!("Running sender"); 471 | let pass = SData(rpassword::prompt_password("Password: ").expect("Couldn't read password")); 472 | let addr = match cliargs.address { 473 | Some(ip) => ip.to_string(), 474 | None => { 475 | let codes = utils::get_input("Code: ").expect("Coudln't read codes"); 476 | get_ip_from_code(&codes) 477 | } 478 | }; 479 | let addr = &format!("{}:{}", &addr, cliargs.port); 480 | 481 | #[cfg(feature = "sender")] 482 | match cliargs.algo { 483 | Algo::Aes128 => create_sender!( 484 | create_128_encryptor, 485 | cliargs, 486 | config.get_identifier().unwrap_or(&String::new()), 487 | addr, pass 488 | ), 489 | Algo::Aes256 => create_sender!( 490 | create_256_encryptor, 491 | cliargs, 492 | config.get_identifier().unwrap_or(&String::new()), 493 | addr, pass 494 | ), 495 | _ => {error!("Unknown encryption algorithm."); return} 496 | } 497 | } else { 498 | error!("Unknown mode."); 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /aft/src/relay.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "relay")] 2 | //! Handling relay functionality. 3 | use crate::{ 4 | constants::{CLIENT_RECV, MAX_IDENTIFIER_LEN, RELAY, SIGNAL_LEN}, 5 | utils::{bytes_to_string, Signals}, 6 | }; 7 | use log::{debug, error, info}; 8 | use sha2::{Digest, Sha256}; 9 | use std::{io, sync::Arc}; 10 | use tokio::{ 11 | io::{AsyncReadExt, AsyncWriteExt, copy_bidirectional}, 12 | net::{TcpListener, TcpStream}, 13 | }; 14 | use whirlwind::ShardMap; 15 | 16 | 17 | type Identifier = String; 18 | type ClientsHashMap = ShardMap; 19 | 20 | macro_rules! error_conn { 21 | ($comm:expr, $ip:expr) => { 22 | error_conn!($comm, $ip, return) 23 | }; 24 | ($comm:expr, $ip:expr, $step:expr) => { 25 | match $comm { 26 | Ok(v) => v, 27 | Err(e) => { 28 | error!("Connection error: {:?} {}", e, $ip); 29 | $step; 30 | } 31 | } 32 | }; 33 | } 34 | 35 | async fn handle_sender(sender: &mut TcpStream, clients: Arc, recv_identifier: &String, sen_identifier: &str) -> 36 | io::Result 37 | { 38 | let mut receiver; 39 | let sender_ip = sender.peer_addr()?; 40 | 41 | debug!("{} wants to transfer to {}", sen_identifier, recv_identifier); 42 | 43 | { 44 | // If the receiver is not online 45 | if !is_ident_exists(clients.clone(), recv_identifier).await { 46 | info!("{} is not online", recv_identifier); 47 | sender.write_all(Signals::Error.as_bytes()).await?; 48 | return Ok(false); 49 | } else { 50 | // The receiver is online 51 | sender.write_all(Signals::OK.as_bytes()).await?; 52 | } 53 | 54 | receiver = clients.remove(recv_identifier).await.unwrap(); 55 | } 56 | 57 | // Read signal from the sender 58 | let signal = read_signal(sender).await?; 59 | receiver.write_all(signal.as_bytes()).await?; 60 | 61 | let hashed_sen_ip = { 62 | let mut sha = Sha256::new(); 63 | sha.update(sender.peer_addr()?.ip().to_string()); 64 | sha.finalize() 65 | }; 66 | // Write the sender's identifier 67 | receiver.write_all(sen_identifier.as_bytes()).await?; 68 | // Write to the sender the hashed IP of the sender's, so he can continue blocking 69 | // him. 70 | receiver.write_all(&hashed_sen_ip).await?; 71 | 72 | let acceptance = read_signal(&mut receiver).await?; 73 | 74 | // Write to sender if the receiver accepted the file transfer 75 | sender.write_all(acceptance.as_bytes()).await?; 76 | 77 | match acceptance { 78 | Signals::Error => { 79 | debug!("{} rejected {}", recv_identifier, sender_ip); 80 | // Keep the receiver listening 81 | clients.insert(recv_identifier.to_string(), receiver).await; 82 | return Ok(false) 83 | }, 84 | Signals::OK => 85 | debug!("{} accepted request from {}. Transfer started.", recv_identifier, sender_ip), 86 | s => { 87 | error!("Invalid signal: {}", s); 88 | return Ok(false); 89 | } 90 | } 91 | 92 | copy_bidirectional(sender, &mut receiver).await?; 93 | 94 | Ok(true) 95 | } 96 | 97 | pub async fn is_ident_exists(clients: Arc, identifier: &String) -> bool { 98 | clients.contains_key(identifier).await 99 | } 100 | 101 | async fn read_identifier(socket: &mut TcpStream) -> io::Result { 102 | let mut identifier = [0; MAX_IDENTIFIER_LEN]; 103 | socket.read_exact(&mut identifier).await?; 104 | 105 | Ok(bytes_to_string(&identifier)) 106 | } 107 | 108 | /// Initializes the relay and starts receiving connections. 109 | /// 110 | /// Error when there is a connection error. 111 | pub async fn init(address: &str) -> io::Result<()> { 112 | let listener = TcpListener::bind(address).await?; 113 | let hashmap_clients = Arc::new(ClientsHashMap::new()); 114 | 115 | info!("Listening ..."); 116 | loop { 117 | let (mut socket, addr) = error_conn!(listener.accept().await, "", continue); 118 | info!("New connection from: {:?}", addr); 119 | 120 | let clients = hashmap_clients.clone(); 121 | tokio::spawn(async move { 122 | // Write to the socket that its connecting to a relay 123 | error_conn!(socket.write_u8(RELAY).await, addr); 124 | 125 | // Read what the client wants: download or sending 126 | let command = error_conn!(socket.read_u8().await, addr); 127 | if command == CLIENT_RECV { 128 | let identifier = error_conn!(read_identifier(&mut socket).await, addr); 129 | if identifier.is_empty() { 130 | debug!("{} provided invalid identifier", addr); 131 | return; 132 | } 133 | 134 | if let Some(mut recv_sock) = clients.get_mut(&identifier).await { 135 | // Connectivity check 136 | error_conn!(recv_sock.write_all(Signals::Other.as_bytes()).await, addr); 137 | 138 | if recv_sock.read_u8().await.is_err() { 139 | debug!("{} disconnected", identifier); 140 | clients.remove(&identifier).await; 141 | } else { 142 | debug!("Signaling to {}: \"{}\" identifier is not available", addr, identifier); 143 | // Signal that someone is already connected with this identifier 144 | error_conn!(socket.write_all(Signals::Error.as_bytes()).await, addr); 145 | return; 146 | } 147 | } 148 | clients.insert(identifier, socket).await; 149 | } 150 | // The sender (socket = sender) 151 | else { 152 | // Read the receiver's identifier 153 | let recv_identifier = error_conn!(read_identifier(&mut socket).await, addr); 154 | // Read the sender's identifier 155 | let sen_identifier = error_conn!(read_identifier(&mut socket).await, addr); 156 | if recv_identifier.is_empty() || sen_identifier.is_empty() { 157 | debug!("Invalid identifier/s from {}", addr); 158 | return; 159 | } 160 | 161 | error_conn!( 162 | handle_sender(&mut socket, clients, &recv_identifier, &sen_identifier).await, 163 | addr); 164 | } 165 | }); 166 | } 167 | } 168 | 169 | async fn read_signal(socket: &mut TcpStream) -> io::Result { 170 | let mut signal = [0u8; SIGNAL_LEN]; 171 | socket.read_exact(&mut signal).await?; 172 | let signal = bytes_to_string(&signal); 173 | Ok(signal.as_str().into()) 174 | } 175 | -------------------------------------------------------------------------------- /aft/src/sender.rs: -------------------------------------------------------------------------------- 1 | //! Handling sender. 2 | use crate::{ 3 | clients::{BaseSocket, Crypto, SWriter}, 4 | constants::{ 5 | CLIENT_SEND, CLIENT_RECV, MAX_CONTENT_LEN, MAX_METADATA_LEN, RELAY, 6 | }, 7 | errors::Errors, 8 | utils::{ 9 | download_speed, error_other, mut_vec, progress_bar, send_identifier, FileOperations, 10 | Signals 11 | }, 12 | }; 13 | use aft_crypto::{ 14 | data::{AeadInPlace, Algo, EncAlgo, EncryptorBase, SData}, 15 | exchange::{PublicKey, KEY_LENGTH}, 16 | }; 17 | use json; 18 | use log::{debug, error, info, warn}; 19 | use rayon::prelude::*; 20 | use sha2::{Digest, Sha256}; 21 | use std::{ 22 | io::{self, BufReader, BufWriter, Read, Write, IoSlice}, 23 | net::TcpStream, 24 | path::Path, 25 | time::SystemTime, 26 | }; 27 | 28 | fn update_pb(curr_bars_count: &mut u8, pb_length: u64, bytes_transferred: u64) { 29 | *curr_bars_count = (bytes_transferred / (pb_length + 1)).try_into().unwrap_or(0); 30 | progress_bar(*curr_bars_count, 50); 31 | } 32 | 33 | fn basic_file_checks(path: &Path) -> io::Result { 34 | if path.metadata()?.len() == 0 { 35 | error!("File is empty"); 36 | return Ok(false); 37 | } 38 | 39 | if path.extension().is_none() { 40 | warn!("No file extension."); 41 | } 42 | 43 | if path.is_dir() { 44 | error!("Inputted a name of a directory and not a file"); 45 | return Err(error_other!("Not a file")); 46 | } 47 | 48 | Ok(true) 49 | } 50 | 51 | /// A struct that represents a sender. 52 | pub struct Sender { 53 | writer: SWriter, 54 | file_path: String, 55 | current_pos: u64, 56 | gen_encryptor: fn(&[u8]) -> T, 57 | will_checksum: bool, 58 | algo: Algo, 59 | } 60 | 61 | impl BaseSocket for Sender 62 | where 63 | T: AeadInPlace + Sync, 64 | { 65 | fn get_writer(&self) -> &SWriter { 66 | &self.writer 67 | } 68 | 69 | fn get_mut_writer(&mut self) -> &mut SWriter { 70 | &mut self.writer 71 | } 72 | 73 | fn shared_secret(&mut self) -> io::Result<()> { 74 | let shared_key = self.gen_shared_secret()?; 75 | self.writer.1 = EncAlgo::new(shared_key.as_bytes(), self.gen_encryptor); 76 | Ok(()) 77 | } 78 | } 79 | 80 | impl Crypto for Sender { 81 | fn exchange_pk(&mut self, pk: PublicKey) -> io::Result { 82 | let mut other_pk = [0; 32]; 83 | 84 | // Getting endpoint's public key 85 | debug!("Getting public key"); 86 | self.writer.0.read_exact(&mut other_pk)?; 87 | // Writing the public key 88 | debug!("Writing public key"); 89 | self.writer.0.write_all(pk.as_bytes())?; 90 | 91 | Ok(PublicKey::from(other_pk)) 92 | } 93 | } 94 | 95 | impl Sender 96 | where 97 | T: AeadInPlace + Sync, 98 | { 99 | /// Constructs a new Sender struct, and connects to `remote_ip`. 100 | pub fn new(remote_addr: &str, encryptor_func: fn(&[u8]) -> T, will_checksum: bool, algo: Algo) -> Self { 101 | let socket = TcpStream::connect(remote_addr).expect("Couldn't connect."); 102 | Sender { 103 | writer: SWriter(socket, EncAlgo::::new(&[0u8; KEY_LENGTH], encryptor_func)), 104 | file_path: String::new(), 105 | current_pos: 0, 106 | gen_encryptor: encryptor_func, 107 | will_checksum, 108 | algo, 109 | } 110 | } 111 | 112 | /// Signals to the endpoint to start the file transfer process. 113 | fn signal_start(&mut self) -> io::Result<()> { 114 | self.get_mut_writer().0.write_all(Signals::StartFt.as_bytes())?; 115 | Ok(()) 116 | } 117 | 118 | /// If the sender is connecting to a relay. 119 | /// 120 | /// # Errors 121 | /// When there is a connection error. 122 | /// 123 | /// Returns false when the identifier is too long. 124 | fn if_relay(&mut self, rece_ident: &str, sen_ident: &str) -> Result { 125 | // Notify the relay that this client is a sender 126 | self.writer.0.write_all(&[CLIENT_SEND])?; 127 | 128 | if !(send_identifier(rece_ident.as_bytes(), &mut self.writer.0)? 129 | && send_identifier(sen_ident.as_bytes(), &mut self.writer.0)?) 130 | { 131 | return Err(Errors::InvalidIdent); 132 | } 133 | 134 | match self.read_signal_relay()? { 135 | Signals::OK => Ok(true), 136 | Signals::Error => Ok(false), 137 | _ => Err(Errors::InvalidSignal) 138 | } 139 | } 140 | 141 | fn get_starting_pos(&mut self) -> io::Result<()> { 142 | // Starting position from receiver 143 | let mut file_pos_bytes = vec![0u8; 8]; 144 | debug!("Getting starting position ..."); 145 | self.writer.read_ext(&mut file_pos_bytes)?; 146 | self.current_pos = u64::from_le_bytes(file_pos_bytes.try_into().unwrap_or_default()); 147 | debug!("Starting position: {}", self.current_pos); 148 | 149 | Ok(()) 150 | } 151 | 152 | pub fn auth(&mut self, pass: SData) -> io::Result { 153 | let pass_hashed = { 154 | let mut sha = Sha256::new(); 155 | sha.update(&pass.0); 156 | sha.finalize() 157 | }; 158 | 159 | debug!("Authenticating ..."); 160 | self.writer.write_ext(mut_vec!(pass_hashed))?; 161 | 162 | Ok(self.read_signal()? == Signals::OK) 163 | } 164 | 165 | fn relay_init(&mut self, sen_ident: &str, rece_ident: Option<&str>) -> Result { 166 | if sen_ident.is_empty() || rece_ident.unwrap_or_default().is_empty() { 167 | return Err(Errors::InvalidIdent); 168 | } 169 | debug!("Connected to a relay"); 170 | if let Some(ident) = rece_ident { 171 | if !self.if_relay(ident, sen_ident)? { 172 | error!("{ident} not online"); 173 | return Ok(false); 174 | } 175 | } else { 176 | return Err(Errors::NoReceiverIdentifier); 177 | } 178 | 179 | debug!("Signaling to start"); 180 | // Write to the endpoint to start the transfer 181 | self.signal_start()?; 182 | 183 | match self.read_signal_relay()? { 184 | Signals::OK => info!("Receiver accepted."), 185 | Signals::Error => { 186 | error!("Receiver rejected."); 187 | return Ok(false); 188 | } 189 | s => { 190 | error!("Received invalid signal: {}", s); 191 | return Err(Errors::InvalidSignal); 192 | } 193 | } 194 | 195 | self.shared_secret()?; 196 | 197 | Ok(true) 198 | } 199 | 200 | /// Initial connection sends a JSON data formatted, with some metadata. 201 | /// It will usually look like the following: 202 | /// ```json 203 | /// { 204 | /// "metadata": { 205 | /// "filetype": "", 206 | /// "filename": "", 207 | /// "size": "", 208 | /// "modified": "", 209 | /// "will_checksum": bool, 210 | /// "algo": "" 211 | /// } 212 | /// } 213 | /// ``` 214 | /// Make sure `socket` is still valid and have not disconnected. 215 | /// 216 | /// Returns true if the transfer completed successfully, else false. 217 | /// 218 | /// Returns error when: 219 | /// - `path` doesn't exist. 220 | /// - Connection error. 221 | /// - JSON metadata is too big when one of the following are too big: 222 | /// - Filetype. 223 | /// - Filename. 224 | /// - File size. 225 | /// - Modified date. 226 | /// 227 | /// Returns false if something went wrong (such as the identifier is too long, or when the 228 | /// receiver isn't online). 229 | pub fn init(&mut self, path: &str, sen_ident: &str, receiver_identifier: Option<&str>, pass: SData) -> Result { 230 | let file_path = Path::new(path); 231 | 232 | if !basic_file_checks(file_path)? { 233 | return Err(Errors::BasFileChcks); 234 | } 235 | 236 | self.file_path = path.to_string(); 237 | 238 | let mut relay_or_receiver = [0u8; 1]; 239 | self.writer.0.read_exact(&mut relay_or_receiver)?; 240 | match relay_or_receiver[0] { 241 | RELAY => { 242 | if !self.relay_init(sen_ident, receiver_identifier)? { 243 | return Ok(false) 244 | } 245 | }, 246 | CLIENT_RECV => { 247 | self.shared_secret()?; 248 | if !self.auth(pass)? { 249 | return Err(Errors::InvalidPass); 250 | } 251 | }, 252 | _ => {error!("Not a relay or a receiver."); return Err(Errors::WrongResponse)} 253 | } 254 | 255 | let parsed = json::object! { 256 | metadata: { 257 | filetype: file_path.extension().unwrap_or_default().to_str().unwrap(), 258 | filename: file_path.file_name().unwrap().to_str().unwrap(), 259 | size: file_path.metadata()?.len(), 260 | modified: file_path.metadata()?.modified()?.duration_since(std::time::UNIX_EPOCH) 261 | .unwrap_or_default().as_secs(), 262 | will_checksum: self.will_checksum, 263 | algo: Into::<&str>::into(&self.algo) 264 | } 265 | }; 266 | 267 | if parsed.dump().len() > MAX_METADATA_LEN { 268 | error!("Metadata size is too big"); 269 | return Err(Errors::BufferTooBig); 270 | } 271 | 272 | let dump = parsed.dump(); 273 | let metadata_vec_bytes = dump.as_bytes(); 274 | let mut full_metadata = vec![0; MAX_METADATA_LEN]; 275 | full_metadata[..metadata_vec_bytes.len()].copy_from_slice(metadata_vec_bytes); 276 | 277 | // Write metadata 278 | debug!("Sending metadata"); 279 | self.writer.write_ext(&mut full_metadata)?; 280 | 281 | self.get_starting_pos()?; 282 | Ok(true) 283 | } 284 | 285 | /// After the *initial connection*, we send chunks. Every chunk is data from the file. 286 | /// 287 | /// Returns error when a connection error has occurred. 288 | pub fn send_chunks(&mut self, num_threads: usize) -> io::Result<()> { 289 | let mut file = FileOperations::new(&self.file_path)?; 290 | 291 | if !self.check_starting_checksum(&mut file, self.current_pos)? && self.current_pos != 0 { 292 | error!("Checksum not equal."); 293 | info!("Starting from 0 since the file was modified"); 294 | file.reset_checksum(); 295 | self.current_pos = 0; 296 | file.seek_start(0)?; 297 | } else { 298 | file.seek_start(self.current_pos)?; 299 | } 300 | 301 | let file_size = file.len()?; 302 | let mut curr_bars_count = 0u8; 303 | // Add a new bar to progress bar when x bytes have been transferred 304 | let pb_length = file_size / 50; 305 | 306 | debug!("Writing chunks"); 307 | info!("Sending file ..."); 308 | if self.current_pos != 0 { 309 | update_pb(&mut curr_bars_count, pb_length, self.current_pos); 310 | } 311 | 312 | let system_time = SystemTime::now(); 313 | let mut before = 0; 314 | let mut bytes_sent_sec = 0; 315 | 316 | // We are using a new writer because of now we use BufWriter instead of TcpStream's "slow" 317 | // implementation of writing. 318 | let mut new_writer = BufWriter::new(self.writer.0.try_clone()?); 319 | 320 | let mut buffer = vec![0; num_threads * MAX_CONTENT_LEN]; 321 | let mut file_reader = BufReader::new(file.file.get_mut()); 322 | 323 | while self.current_pos != file_size { 324 | let read_size = file_reader.read(&mut buffer)?; 325 | // If we reached EOF 326 | if read_size == 0 { 327 | break; 328 | } 329 | 330 | bytes_sent_sec += read_size; 331 | self.current_pos += read_size as u64; 332 | 333 | let encrypted_buffer: Vec> = buffer.par_chunks_exact(MAX_CONTENT_LEN).map(|chunk| 334 | self.writer.1.encrypt(chunk).expect("Could not encrypt") 335 | ).collect(); 336 | let io_sliced_buf: Vec = encrypted_buffer.iter() 337 | .map(|x| IoSlice::new(x)).collect(); 338 | 339 | let _written_bytes = new_writer.write_vectored(&io_sliced_buf)?; 340 | 341 | // Progress bar 342 | update_pb(&mut curr_bars_count, pb_length, self.current_pos); 343 | 344 | match system_time.elapsed() { 345 | Ok(elapsed) => { 346 | // update the download speed every 1 second 347 | if elapsed.as_secs() != before { 348 | before = elapsed.as_secs(); 349 | download_speed(bytes_sent_sec); 350 | bytes_sent_sec = 0; 351 | } 352 | } 353 | Err(e) => error!("An error occurred while printing download speed: {}", e), 354 | } 355 | } 356 | 357 | println!(); 358 | debug!("Reached EOF"); 359 | 360 | if self.will_checksum { 361 | info!("Verifying ..."); 362 | file.compute_checksum(u64::MAX)?; 363 | 364 | debug!("Ending file transfer and writing checksum"); 365 | self.writer.write_ext(&mut file.checksum())?; 366 | } 367 | 368 | self.writer.0.shutdown(std::net::Shutdown::Write)?; 369 | 370 | if self.read_signal()? == Signals::OK { 371 | info!("Finished successfully"); 372 | } else { 373 | error!("Transfer has not completed."); 374 | } 375 | 376 | Ok(()) 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /aft/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::MAX_IDENTIFIER_LEN; 2 | use log::{debug, error, info}; 3 | use sha2::{Digest, Sha256}; 4 | /// Module for various utilities used in other modules. 5 | use std::{ 6 | fs::{self, File}, 7 | io::{self, prelude::*, SeekFrom, BufReader, BufWriter}, 8 | net::{Ipv4Addr, TcpStream}, 9 | }; 10 | 11 | #[derive(Debug, PartialEq, Eq)] 12 | pub enum Signals { 13 | /// End file transfer. 14 | EndFt, 15 | /// Start the transfer. 16 | StartFt, 17 | /// Ok. 18 | OK, 19 | /// Error. 20 | Error, 21 | /// Unspecific signal. Customized by the environment. 22 | Other, 23 | /// Unknown signal. 24 | Unknown, 25 | } 26 | 27 | impl From<&str> for Signals { 28 | fn from(v: &str) -> Self { 29 | match v { 30 | "FTSIG1" => Signals::EndFt, 31 | "FTSIG2" => Signals::StartFt, 32 | "FTSIG3" => Signals::OK, 33 | "FTSIG4" => Signals::Error, 34 | "FTSIG5" => Signals::Other, 35 | _ => Signals::Unknown, 36 | } 37 | } 38 | } 39 | 40 | impl From<&Signals> for &str { 41 | fn from(v: &Signals) -> Self { 42 | match v { 43 | Signals::EndFt => "FTSIG1", 44 | Signals::StartFt => "FTSIG2", 45 | Signals::OK => "FTSIG3", 46 | Signals::Error => "FTSIG4", 47 | Signals::Other => "FTSIG5", 48 | _ => "FTSIG", 49 | } 50 | } 51 | } 52 | 53 | impl std::fmt::Display for Signals { 54 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 55 | match *self { 56 | Signals::EndFt => write!(f, "End file transfer successfully."), 57 | Signals::StartFt => write!(f, "Start the transfer."), 58 | Signals::OK => write!(f, "Ok."), 59 | Signals::Error => write!(f, "Error."), 60 | Signals::Other => write!(f, "Other."), 61 | _ => write!(f, "Unknown signal."), 62 | } 63 | } 64 | } 65 | 66 | impl Signals { 67 | pub fn as_str(&self) -> &str { 68 | self.into() 69 | } 70 | 71 | pub fn as_bytes(&self) -> &[u8] { 72 | self.as_str().as_bytes() 73 | } 74 | } 75 | 76 | /// Macro to shorten "other" errors. 77 | macro_rules! error_other { 78 | ($E:expr) => { 79 | std::io::Error::new(io::ErrorKind::Other, $E) 80 | }; 81 | } pub(crate) use error_other; 82 | 83 | macro_rules! mut_vec { 84 | ($s:expr) => { 85 | &mut $s.to_vec() 86 | }; 87 | } pub(crate) use mut_vec; 88 | 89 | /// Represents a client file. Provides special methods that are used in this program. 90 | pub struct FileOperations { 91 | pub file: BufWriter, 92 | hasher: Sha256, 93 | } 94 | 95 | impl FileOperations { 96 | /// New FileOperations object and opens `path`. 97 | pub fn new(path: &str) -> io::Result { 98 | let file = FileOperations::open_w_file(path)?; 99 | Ok(FileOperations { 100 | file: BufWriter::new(file), 101 | hasher: Sha256::new(), 102 | }) 103 | } 104 | 105 | /// Create file at `path` and return FileOperations object. 106 | /// 107 | /// Error when there is an IO error. 108 | pub fn new_create(path: &str) -> io::Result { 109 | let file = FileOperations::create_file(path)?; 110 | Ok(FileOperations { 111 | file: BufWriter::new(file), 112 | hasher: Sha256::new(), 113 | }) 114 | } 115 | 116 | /// Opens a file, given a filename (or a path). 117 | /// 118 | /// Error when there is an IO error. 119 | pub fn open_file(filename: &str) -> io::Result { 120 | info!("Opening file: {}", filename); 121 | File::open(filename) 122 | } 123 | 124 | /// Opens a file in write and read only mode. 125 | /// 126 | /// Error when there is an IO error. 127 | pub fn open_w_file(filename: &str) -> io::Result { 128 | debug!("Opening/Creating file in write mode: {}", filename); 129 | let file = File::options().write(true).read(true).create(true).open(filename)?; 130 | Ok(file) 131 | } 132 | 133 | /// Function to create a file, given a filename (or a path). 134 | /// 135 | /// Error when there is an IO error. 136 | pub fn create_file(filename: &str) -> io::Result { 137 | debug!("Creating/overwriting file: {}", filename); 138 | let file = File::options().truncate(true).create(true).write(true).read(true).open(filename)?; 139 | Ok(file) 140 | } 141 | 142 | pub fn write(&mut self, buffer: &[u8]) -> io::Result<()> { 143 | if !buffer.is_empty() { 144 | self.file.write_all(buffer)?; 145 | } 146 | Ok(()) 147 | } 148 | 149 | /// Seeks to the start + pos. 150 | pub fn seek_start(&mut self, pos: u64) -> io::Result { 151 | self.file.seek(SeekFrom::Start(pos)) 152 | } 153 | 154 | /// Seeks to the end - pos. 155 | pub fn seek_end(&mut self, pos: i64) -> io::Result { 156 | self.file.seek(SeekFrom::End(-pos)) 157 | } 158 | 159 | /// Returns the current cursor position in file. 160 | pub fn get_index(&mut self) -> io::Result { 161 | self.file.stream_position() 162 | } 163 | 164 | /// Returns the length of the file. 165 | pub fn len(&self) -> io::Result { 166 | Ok(self.file.get_ref().metadata()?.len()) 167 | } 168 | 169 | /// Returns whether the file is empty. 170 | pub fn is_empty(&self) -> io::Result { 171 | Ok(self.len()? == 0) 172 | } 173 | 174 | /// Checks if a file exists. 175 | pub fn is_file_exists(path: &str) -> bool { 176 | std::path::Path::new(path).is_file() 177 | } 178 | 179 | /// Returns the current checksum of the file. 180 | pub fn checksum(&self) -> Vec { 181 | self.hasher.clone().finalize().to_vec() 182 | } 183 | 184 | /// Computes the checksum of the current file content. Note this will reset the cursor. 185 | pub fn compute_checksum(&mut self, end_pos: u64) -> io::Result<()> { 186 | let mut buffer = [0u8; 1024]; 187 | 188 | self.reset_checksum(); 189 | self.seek_start(0)?; 190 | 191 | let mut reader = BufReader::new(self.file.get_mut()); 192 | let mut read_bytes = 0; 193 | 194 | loop { 195 | let bytes = reader.read(&mut buffer)?; 196 | if bytes == 0 || read_bytes == end_pos { 197 | break; 198 | } 199 | read_bytes += bytes as u64; 200 | self.hasher.update(buffer); 201 | } 202 | 203 | Ok(()) 204 | } 205 | 206 | /// Updates the checksum. 207 | pub fn update_checksum(&mut self, buffer: &[u8]) { 208 | self.hasher.update(buffer); 209 | } 210 | 211 | /// Resets the checksum. 212 | pub fn reset_checksum(&mut self) { 213 | self.hasher.reset(); 214 | } 215 | 216 | pub fn set_len(&mut self, len: u64) -> io::Result<()> { 217 | self.file.get_mut().set_len(len) 218 | } 219 | 220 | /// Removes a file. 221 | pub fn rm(path: &str) -> io::Result<()> { 222 | fs::remove_file(path) 223 | } 224 | 225 | /// Rename `filename` to `new_filename`. 226 | pub fn rename(filename: &str, new_filename: &str) -> io::Result<()> { 227 | fs::rename(filename, new_filename) 228 | } 229 | } 230 | 231 | /// Transforms bytes slice to a string (&str). 232 | pub fn bytes_to_string(buffer: &[u8]) -> String { 233 | String::from_utf8_lossy(buffer).to_string() 234 | } 235 | 236 | /// Prints a progress bar. 237 | pub fn progress_bar(pos: u8, max: u8) { 238 | if pos == max { 239 | // clear screen 240 | print!("\r\n"); 241 | } else { 242 | print!("\r[{}>{}] {}%", "=".repeat(pos as usize), " ".repeat((max-1 - pos) as usize), pos*2+2); 243 | } 244 | } 245 | 246 | /// Adds to the progress bar a download speed. 247 | pub fn download_speed(bytes_sent: usize) { 248 | let mb: f32 = bytes_sent as f32 / 1000000.0; 249 | print!(" {:.2}MB/s", mb); 250 | } 251 | 252 | pub fn get_input(msg: &str) -> io::Result { 253 | let mut input = String::new(); 254 | print!("{}", msg); 255 | io::stdout().flush()?; 256 | io::stdin().read_line(&mut input)?; 257 | // Removing \n 258 | input.pop(); 259 | 260 | Ok(input) 261 | } 262 | 263 | pub fn get_accept_input(msg: &str) -> io::Result { 264 | let res = get_input(msg)?.chars().next().unwrap_or_default(); 265 | Ok(if ['y', 'b'].contains(&res) { res } else { 'n' }) 266 | } 267 | 268 | /// Sends an identifier through a socket. 269 | /// 270 | /// Returns false if the identifier is too long. 271 | pub fn send_identifier(ident: &[u8], socket: &mut TcpStream) -> io::Result { 272 | if ident.len() != MAX_IDENTIFIER_LEN { 273 | error!("Identifier length != {MAX_IDENTIFIER_LEN}"); 274 | return Ok(false); 275 | } 276 | // Write the identifier of this receiver 277 | socket.write_all(ident)?; 278 | 279 | Ok(true) 280 | } 281 | 282 | pub fn get_pub_ip() -> io::Result { 283 | let mut stream = TcpStream::connect("api.ipify.org:80")?; 284 | let request = "GET / HTTP/1.0\r\nHost: api.ipify.org\r\nAccept: */*\r\n\r\n".as_bytes(); 285 | 286 | stream.write_all(request)?; 287 | 288 | let mut response = [0; 500]; 289 | let bytes_read = stream.read(&mut response)?; 290 | if bytes_read != 0 { 291 | let respo_str = bytes_to_string(&response[..bytes_read]); 292 | if let Some(ip) = respo_str.lines().last() { 293 | return Ok(ip.to_string()); 294 | } 295 | } 296 | 297 | Ok(String::new()) 298 | } 299 | 300 | pub fn ip_to_octets(ip_str: &str) -> [u8; 4] { 301 | let ip: Ipv4Addr = ip_str.parse().expect("IP format is incorrect."); 302 | ip.octets() 303 | } 304 | 305 | pub fn get_home_dir() -> String { 306 | std::env::var( 307 | if cfg!(windows) { 308 | "USERPROFILE" 309 | } else { 310 | "HOME" 311 | } 312 | ).unwrap_or_default() 313 | } 314 | -------------------------------------------------------------------------------- /assets/fail2ban/aft-relay-filter.conf: -------------------------------------------------------------------------------- 1 | [Definition] 2 | failregex = DEBUG\] .* rejected :.* 3 | ignoregex = 4 | -------------------------------------------------------------------------------- /assets/fail2ban/aft-relay.conf: -------------------------------------------------------------------------------- 1 | [aft-relay] 2 | enabled = true 3 | logpath = /var/log/aft-relay.log 4 | filter = aft-relay-filter 5 | maxretry = 4 6 | bantime = 3600 7 | -------------------------------------------------------------------------------- /docs/CONFIG.md: -------------------------------------------------------------------------------- 1 | # The `aft` config 2 | The `aft` config is a very simple and minimal config file. The config should be edited by the user. 3 | 4 | ## Format 5 | The format is very basic, and it's followed by the following rule: `key\s?=\s?value`. `key` and `value` would be discussed in the next section. Every option needs to be on a separate line. 6 | 7 | ## Options 8 | The config file only has 3 options: 9 | - `verbose=i`: where `i` is 1-3 where 3 is the most verbose and 1 is the least. `verbose=1` prints only errors; `verbose=2` prints errors and info; `verbose=3` prints any log possible. 10 | - `mode=mode`: where `mode` can be one of the following: `relay || download || receiver || sender`. 11 | - `identifier=string`: where `string` MUST BE a fixed string length of 10. This is used with relays to identify a receiver. 12 | 13 | ## Example 14 | ``` 15 | verbose=3 16 | identifier=usertester 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/PROTOCOL.md: -------------------------------------------------------------------------------- 1 | # The aft protocol 2 | `aft` supports two modes: Peer to Peer (P2P) and relay mode. 3 | The transferring process on both modes is basically the same, only the difference is instead of connecting to each other directly (in P2P mode), there is a relay sitting between them. The relay gets very little information, as you will see later. 4 | 5 | The main benefit between the two modes is not needing to open a port so you can accept files. Opening a port can be problematic since some ISP's do not allow it because they use NAT, or you just don't want to risk yourself with a port always open on your device. Relay mode also hides your IP from the sender. For example when you accept a file from some stranger, you don't want them to know your IP. 6 | 7 | ## The protocol - technical 8 | When the sender connects to some address, it needs to know whether it's a relay or not. So the first thing the sender does is reading 1 byte and checking if it's a relay. If it is, it will initiate relay mode. Otherwise P2P mode. 9 | 10 | ## Relay mode 11 | The connection between the sender/receiver and the relay is not encrypted by default. It's up to the implementation if to support TLS or not. But, the protocol does not reveal any sensitive information plainly, so encryption is not needed at the start. 12 | 13 | ### One step - first handshake 14 | The relay needs to know when a client connects to him: if he is the receiver or the sender, so the client writes 1 byte which signals if he is the receiver or the sender. 15 | 16 | #### If the client a receiver 17 | The receiver will write his identifier so the sender can know whom to send to. The server will write back a signal whether this identifier is available or not. If it's not, the client will disconnect. After that, the client will wait for a signal to start the file transfer. 18 | 19 | #### If the client a sender 20 | The sender will write the receiver's identifier AND his identifier, so the receiver can decide whether to accept him or not. The relay server will check if the receiver's identifier exists (basically if the receiver is online). If it does not, the sender will disconnect. 21 | 22 | ### Step two - acceptance 23 | Before the second handshake, the receiver receives a signal from the relay server that someone wants to send him a file. The relay server sends to the receiver the sender's identifier AND the sender's SHA-256 hashed IP. The receiver has three options: 24 | - accepting the request, and moving on to the next handshake; 25 | - rejecting the request, and waiting again for a file transfer request; 26 | - blocking the sender, and waiting again for a request. 27 | 28 | The relay server doesn't care if the receiver blocked the sender or rejected the request, so the blocking happens on the receiver's side. 29 | 30 | Before we discuss the second handshake, we will discuss the first handshake for the receiver in P2P mode: 31 | 32 | ## P2P mode 33 | 34 | ### Step one - first handshake 35 | When the sender connects to the receiver, the receiver will write 1 byte indicating he is a receiver and not a relay. After that, they initiate the second handshake (discussed later). 36 | 37 | ### Step two - acceptance 38 | The receiver should have a SHA-256 hashed password ready (or hashed later) for authentication. When the connection is encrypted, the sender will write his presumed authentication password, which is SHA-256 hashed, and the receiver will compare it to his hash. If they match, the receiver will signal the sender he can start the transfer. Otherwise, they will disconnect from each other. 39 | 40 | ## Second handshake - public keys exchange 41 | From now on, both modes act in the same way exactly, only that the relay server will forward the requests from the sender to the receiver and otherway. 42 | 43 | Once the first handshake is done, AND the receiver accepted the request, we can move to the next handshake, which involves exchanging encryption keys. The receiver will send his public key to the sender/relay. The sender in return will send his public key to the receiver/relay. It's up to the implementation what is the key length. In relay mode, the relay should NOT care what encryption algorithm is used. 44 | 45 | From now on, the connection is completely encrypted, and in relay mode the relay has no eyes on the actual content. 46 | 47 | ## Pre-transfer - metadata 48 | The sender will send information about the file in a JSON format. An example for the JSON can look like the following: 49 | ```json 50 | { 51 | "metadata": { 52 | "filetype": "", 53 | "filename": "", 54 | "size": "", 55 | "modified": "" 56 | } 57 | } 58 | ``` 59 | It's up to the implementation what keys-values will exist. 60 | If the receiver accepts the request (he can deny based on the metadata content), the receiver will check if a previous interrupted transfer of the same file occurred. If: 61 | - false: it will send 0 (for the current position). 62 | - true: it will send the current position. 63 | 64 | The sender will send the current computed checksum based on the file position. If the receiver wants to continue, the actual transfer will start. 65 | 66 | ## The transfer 67 | The sender will send **encrypted** chunks of the file. Each chunk will be a fixed size pre-configured on the sender and the receiver/relay sides. When there are no more chunks, the sender will signal that. 68 | 69 | ## At the end 70 | The sender will send a computed SHA-256 checksum of the file, and the receiver will compare it with his checksum. The sender doesn't care if they match. After all of these stages, both the sender and the receiver will finally disconnect from each-other/relay. 71 | 72 | # Note 73 | This file may be updated along the way, for example because of security issues. 74 | 75 | The protocol was designed by dd-dreams ([GitHub](https://github.com/dd-dreams "GitHub")). 76 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | _OSTYPE="$(uname -s)" 4 | CPUTYPE="$(uname -m)" 5 | 6 | if [ $# != 1 ]; then 7 | echo "Bad arguments. Use install or uninstall." 8 | exit 1 9 | fi 10 | 11 | echo "Make sure you ran this script with sudo." 12 | 13 | if [ $1 != "install" ] && [ $1 != "uninstall" ]; then 14 | echo "Invalid command" 15 | exit 1 16 | fi 17 | 18 | if [ $_OSTYPE = "Darwin" ] && [ $1 = "install" ]; then 19 | echo "Installing aft for macOS" 20 | if [ $CPUTYPE = "arm64" ]; then 21 | URL="https://github.com/dd-dreams/aft/releases/latest/download/aft-macos-aarch64.gz" 22 | else 23 | URL="https://github.com/dd-dreams/aft/releases/latest/download/aft-macos-x86_64.gz" 24 | fi 25 | # Other Unix types might work, but this script currently doesn't support them. 26 | elif [ $_OSTYPE = "Linux" ] || [ "$(echo $_OSTYPE | grep '.*BSD')" ] && [ $1 = "install" ]; then 27 | if [ $CPUTYPE = "arm64" ]; then 28 | echo "Incompatible architecture" 29 | exit 1 30 | fi 31 | echo "Installing aft for Linux/BSD" 32 | URL="https://github.com/dd-dreams/aft/releases/latest/download/aft-linux-x86_64.gz" 33 | elif [ $1 = "install" ]; then 34 | echo "Incompatible OS" 35 | exit 1 36 | elif [ $1 = "uninstall" ]; then 37 | rm /usr/local/bin/aft > /dev/null 2>&1 && echo "aft uninstalled" || echo "aft not installed" 38 | rm /etc/systemd/system/aft-relay.service > /dev/null 2>&1 39 | exit 0 40 | fi 41 | 42 | curl -L $URL > /tmp/aft.gz 43 | gzip -dcN /tmp/aft.gz > /usr/local/bin/aft 44 | chmod +x /usr/local/bin/aft 45 | 46 | if [ $_OSTYPE = "Linux" ] && [ "$(ps 1 | grep 'systemd')" ]; then 47 | curl https://raw.githubusercontent.com/dd-dreams/aft/master/aft-relay.service > /etc/systemd/system/aft-relay.service 48 | systemctl daemon-reload 49 | fi 50 | --------------------------------------------------------------------------------