├── .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

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 |
--------------------------------------------------------------------------------