mirror of
https://github.com/rosenpass/rosenpass.git
synced 2025-12-09 06:10:30 -08:00
Compare commits
178 Commits
dev/update
...
dev/karo/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
877c15a018 | ||
|
|
35519e7baa | ||
|
|
78af5d1dc4 | ||
|
|
9cc860fdeb | ||
|
|
ae3fbde0a3 | ||
|
|
4725a2d628 | ||
|
|
a6bac74d48 | ||
|
|
b9a34f4238 | ||
|
|
46e855b266 | ||
|
|
c0b91fd729 | ||
|
|
97dff8453d | ||
|
|
a3d4686104 | ||
|
|
cee0678817 | ||
|
|
a996f194c7 | ||
|
|
447be89414 | ||
|
|
ef4f550abc | ||
|
|
4737cd2b2a | ||
|
|
9336794e4d | ||
|
|
096bac6ee5 | ||
|
|
161826979a | ||
|
|
c435b772d2 | ||
|
|
8805ef7c38 | ||
|
|
cca02dc8d1 | ||
|
|
d4350195eb | ||
|
|
1c5e4ecf95 | ||
|
|
b15947b815 | ||
|
|
cacbf8535c | ||
|
|
f6d9da4a18 | ||
|
|
68f73e264d | ||
|
|
d5f68dcbd2 | ||
|
|
96581ed118 | ||
|
|
553b058759 | ||
|
|
85286c146f | ||
|
|
0f58b36c5b | ||
|
|
737781c8bc | ||
|
|
4ea1c76b81 | ||
|
|
5251721bcf | ||
|
|
a789f801ab | ||
|
|
be06f8adec | ||
|
|
03d3c70e2e | ||
|
|
94ba99d89b | ||
|
|
667a994253 | ||
|
|
9561ea4a47 | ||
|
|
fb641f8568 | ||
|
|
6e16956bc7 | ||
|
|
eeb738b649 | ||
|
|
2d20ad6335 | ||
|
|
df3d1821c8 | ||
|
|
6048ebd3d9 | ||
|
|
cd7558594f | ||
|
|
022cdc4ffa | ||
|
|
06d4e289a5 | ||
|
|
f9dce3fc9a | ||
|
|
d9f3c8fb96 | ||
|
|
0ea9f1061e | ||
|
|
737f0bc9f9 | ||
|
|
32ebd18107 | ||
|
|
f04cff6d57 | ||
|
|
2c1a0a7451 | ||
|
|
74fdb44680 | ||
|
|
c3adbb7cf3 | ||
|
|
fa583ec6ae | ||
|
|
aa76db1e1c | ||
|
|
c5699b5259 | ||
|
|
d3c52fdf64 | ||
|
|
b18f05ae19 | ||
|
|
d8839ba341 | ||
|
|
7022a93378 | ||
|
|
c9da9b8591 | ||
|
|
b483612cb7 | ||
|
|
a30805f8a0 | ||
|
|
a9b0a90ab5 | ||
|
|
2bc138e614 | ||
|
|
f97781039f | ||
|
|
5eda161cf2 | ||
|
|
a473fe6d9b | ||
|
|
e2c46f1ff0 | ||
|
|
c8b804b39d | ||
|
|
e56798b04c | ||
|
|
b76d18e3c8 | ||
|
|
a9792c3143 | ||
|
|
cb2c1c12ee | ||
|
|
08514d69e5 | ||
|
|
baf2d68070 | ||
|
|
cc7f7a4b4d | ||
|
|
5b701631b5 | ||
|
|
402158b706 | ||
|
|
e95636bf70 | ||
|
|
744e2bcf3e | ||
|
|
8c82ca18fb | ||
|
|
208e79c3a7 | ||
|
|
6ee023c9e9 | ||
|
|
6f75d34934 | ||
|
|
6b364a35dc | ||
|
|
2b6d10f0aa | ||
|
|
cb380b89d1 | ||
|
|
f703933e7f | ||
|
|
d02a5d2eb7 | ||
|
|
c7273e6f88 | ||
|
|
85eca49a5b | ||
|
|
9943f1336b | ||
|
|
bb2a0732cc | ||
|
|
1275b992a0 | ||
|
|
196767964f | ||
|
|
d4e9770fe6 | ||
|
|
8e2f6991d1 | ||
|
|
af0db88939 | ||
|
|
6601742903 | ||
|
|
9436281350 | ||
|
|
f3399907b9 | ||
|
|
0cea8c5eff | ||
|
|
5b3f4da23e | ||
|
|
c13badb697 | ||
|
|
cc7757a0db | ||
|
|
d267916445 | ||
|
|
03bc89a582 | ||
|
|
19b31bcdf0 | ||
|
|
939d216027 | ||
|
|
05fbaff2dc | ||
|
|
1d1c0e9da7 | ||
|
|
e19b724673 | ||
|
|
f879ad5020 | ||
|
|
29e7087cb5 | ||
|
|
637a08d222 | ||
|
|
6416c247f4 | ||
|
|
4b3b7e41e4 | ||
|
|
325fb915f0 | ||
|
|
43cb0c09c5 | ||
|
|
0836a2eb28 | ||
|
|
ca7df013d5 | ||
|
|
1209d68718 | ||
|
|
8806494899 | ||
|
|
582d27351a | ||
|
|
61136d79eb | ||
|
|
71bd406201 | ||
|
|
ce63cf534a | ||
|
|
d3ff19bdb9 | ||
|
|
3b6d0822d6 | ||
|
|
533afea129 | ||
|
|
da5b281b96 | ||
|
|
b9e873e534 | ||
|
|
a3b339b180 | ||
|
|
b4347c1382 | ||
|
|
0745019e10 | ||
|
|
2369006342 | ||
|
|
0fa6176d06 | ||
|
|
22bdeaf8f1 | ||
|
|
5731272844 | ||
|
|
bc7cef9de0 | ||
|
|
4cdcc35c3e | ||
|
|
a8f1292cbf | ||
|
|
ae5c5ed2b4 | ||
|
|
c483452a6a | ||
|
|
4ce331d299 | ||
|
|
d81eb7e2ed | ||
|
|
61043500ba | ||
|
|
9c4752559d | ||
|
|
6aec7acdb8 | ||
|
|
337cc1b4b4 | ||
|
|
387a266a49 | ||
|
|
179970b905 | ||
|
|
8b769e04c1 | ||
|
|
810bdf5519 | ||
|
|
d3a666bea0 | ||
|
|
2b8f780584 | ||
|
|
6aea3c0c1f | ||
|
|
e4fdfcae08 | ||
|
|
48e629fff7 | ||
|
|
6321bb36fc | ||
|
|
2f9ff487ba | ||
|
|
c0c06cd1dc | ||
|
|
e9772effa6 | ||
|
|
cf68f15674 | ||
|
|
dd5d45cdc9 | ||
|
|
17a6aed8a6 | ||
|
|
3f9926e353 | ||
|
|
f4ab2ac891 | ||
|
|
de51c1005f |
14
.ci/boot_race/a.toml
Normal file
14
.ci/boot_race/a.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
public_key = "rp-a-public-key"
|
||||
secret_key = "rp-a-secret-key"
|
||||
listen = ["127.0.0.1:9999"]
|
||||
verbosity = "Verbose"
|
||||
|
||||
[api]
|
||||
listen_path = []
|
||||
listen_fd = []
|
||||
stream_fd = []
|
||||
|
||||
[[peers]]
|
||||
public_key = "rp-b-public-key"
|
||||
endpoint = "127.0.0.1:9998"
|
||||
key_out = "rp-b-key-out.txt"
|
||||
14
.ci/boot_race/b.toml
Normal file
14
.ci/boot_race/b.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
public_key = "rp-b-public-key"
|
||||
secret_key = "rp-b-secret-key"
|
||||
listen = ["127.0.0.1:9998"]
|
||||
verbosity = "Verbose"
|
||||
|
||||
[api]
|
||||
listen_path = []
|
||||
listen_fd = []
|
||||
stream_fd = []
|
||||
|
||||
[[peers]]
|
||||
public_key = "rp-a-public-key"
|
||||
endpoint = "127.0.0.1:9999"
|
||||
key_out = "rp-a-key-out.txt"
|
||||
48
.ci/boot_race/run.sh
Normal file
48
.ci/boot_race/run.sh
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
iterations="$1"
|
||||
sleep_time="$2"
|
||||
config_a="$3"
|
||||
config_b="$4"
|
||||
|
||||
PWD="$(pwd)"
|
||||
EXEC="$PWD/target/release/rosenpass"
|
||||
|
||||
i=0
|
||||
while [ "$i" -ne "$iterations" ]; do
|
||||
echo "=> Iteration $i"
|
||||
|
||||
# flush the PSK files
|
||||
echo "A" >rp-a-key-out.txt
|
||||
echo "B" >rp-b-key-out.txt
|
||||
|
||||
# start the two instances
|
||||
echo "Starting instance A"
|
||||
"$EXEC" exchange-config "$config_a" &
|
||||
PID_A=$!
|
||||
sleep "$sleep_time"
|
||||
echo "Starting instance B"
|
||||
"$EXEC" exchange-config "$config_b" &
|
||||
PID_B=$!
|
||||
|
||||
# give the key exchange some time to complete
|
||||
sleep 3
|
||||
|
||||
# kill the instances
|
||||
kill $PID_A
|
||||
kill $PID_B
|
||||
|
||||
# compare the keys
|
||||
if cmp -s rp-a-key-out.txt rp-b-key-out.txt; then
|
||||
echo "Keys match"
|
||||
else
|
||||
echo "::warning title=Key Exchange Race Condition::The key exchange resulted in different keys. Delay was ${sleep_time}s."
|
||||
# TODO: set this to 1 when the race condition is fixed
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# give the instances some time to shut down
|
||||
sleep 2
|
||||
|
||||
i=$((i + 1))
|
||||
done
|
||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -4,3 +4,7 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
4
.github/workflows/doc-upload.yml
vendored
4
.github/workflows/doc-upload.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Clone rosenpass-website repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: rosenpass/rosenpass-website
|
||||
ref: main
|
||||
|
||||
158
.github/workflows/nix.yaml
vendored
158
.github/workflows/nix.yaml
vendored
@@ -19,11 +19,11 @@ jobs:
|
||||
needs:
|
||||
- i686-linux---rosenpass
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -35,11 +35,11 @@ jobs:
|
||||
- ubuntu-latest
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -52,11 +52,11 @@ jobs:
|
||||
needs:
|
||||
- i686-linux---rosenpass
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -67,11 +67,11 @@ jobs:
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -84,11 +84,11 @@ jobs:
|
||||
needs:
|
||||
- x86_64-darwin---rosenpass
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -103,11 +103,11 @@ jobs:
|
||||
- x86_64-darwin---rp
|
||||
- x86_64-darwin---rosenpass-oci-image
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -119,11 +119,11 @@ jobs:
|
||||
- macos-13
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -135,11 +135,11 @@ jobs:
|
||||
- macos-13
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -152,11 +152,11 @@ jobs:
|
||||
needs:
|
||||
- x86_64-darwin---rosenpass
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -167,11 +167,11 @@ jobs:
|
||||
runs-on:
|
||||
- macos-13
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -184,11 +184,11 @@ jobs:
|
||||
needs:
|
||||
- x86_64-linux---rosenpass
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -201,11 +201,11 @@ jobs:
|
||||
needs:
|
||||
- x86_64-linux---proverif-patched
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -217,11 +217,11 @@ jobs:
|
||||
- ubuntu-latest
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -236,11 +236,11 @@ jobs:
|
||||
- x86_64-linux---rosenpass-static-oci-image
|
||||
- x86_64-linux---rp-static
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -258,13 +258,13 @@ jobs:
|
||||
# - run: |
|
||||
# DEBIAN_FRONTEND=noninteractive
|
||||
# sudo apt-get update -q -y && sudo apt-get install -q -y qemu-system-aarch64 qemu-efi binfmt-support qemu-user-static
|
||||
# - uses: actions/checkout@v3
|
||||
# - uses: cachix/install-nix-action@v22
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: cachix/install-nix-action@v30
|
||||
# with:
|
||||
# nix_path: nixpkgs=channel:nixos-unstable
|
||||
# extra_nix_config: |
|
||||
# system = aarch64-linux
|
||||
# - uses: cachix/cachix-action@v12
|
||||
# - uses: cachix/cachix-action@v15
|
||||
# with:
|
||||
# name: rosenpass
|
||||
# authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -276,11 +276,11 @@ jobs:
|
||||
- ubuntu-latest
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -295,13 +295,13 @@ jobs:
|
||||
- run: |
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
sudo apt-get update -q -y && sudo apt-get install -q -y qemu-system-aarch64 qemu-efi binfmt-support qemu-user-static
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
extra_nix_config: |
|
||||
system = aarch64-linux
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -316,13 +316,13 @@ jobs:
|
||||
- run: |
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
sudo apt-get update -q -y && sudo apt-get install -q -y qemu-system-aarch64 qemu-efi binfmt-support qemu-user-static
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
extra_nix_config: |
|
||||
system = aarch64-linux
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -335,11 +335,11 @@ jobs:
|
||||
needs:
|
||||
- x86_64-linux---rosenpass
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -355,13 +355,13 @@ jobs:
|
||||
- run: |
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
sudo apt-get update -q -y && sudo apt-get install -q -y qemu-system-aarch64 qemu-efi binfmt-support qemu-user-static
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
extra_nix_config: |
|
||||
system = aarch64-linux
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -373,11 +373,11 @@ jobs:
|
||||
- ubuntu-latest
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -389,11 +389,11 @@ jobs:
|
||||
- ubuntu-latest
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -406,11 +406,11 @@ jobs:
|
||||
needs:
|
||||
- x86_64-linux---rosenpass-static
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -422,11 +422,11 @@ jobs:
|
||||
- ubuntu-latest
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -437,11 +437,11 @@ jobs:
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -452,11 +452,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -465,7 +465,7 @@ jobs:
|
||||
- name: Build
|
||||
run: nix build .#packages.x86_64-linux.whitepaper --print-build-logs
|
||||
- name: Deploy PDF artifacts
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: result/
|
||||
|
||||
58
.github/workflows/qc.yaml
vendored
58
.github/workflows/qc.yaml
vendored
@@ -16,8 +16,8 @@ jobs:
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actionsx/prettier@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actionsx/prettier@v3
|
||||
with:
|
||||
args: --check .
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
name: Shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run ShellCheck
|
||||
uses: ludeeus/action-shellcheck@master
|
||||
|
||||
@@ -33,15 +33,15 @@ jobs:
|
||||
name: Rust Format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run Rust Formatting Script
|
||||
run: bash format_rust_code.sh --mode check
|
||||
|
||||
cargo-bench:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -61,16 +61,14 @@ jobs:
|
||||
steps:
|
||||
- name: Install mandoc
|
||||
run: sudo apt-get install -y mandoc
|
||||
- uses: actions/checkout@v3
|
||||
- name: Check rosenpass.1
|
||||
run: doc/check.sh doc/rosenpass.1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check rp.1
|
||||
run: doc/check.sh doc/rp.1
|
||||
|
||||
cargo-audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -78,8 +76,8 @@ jobs:
|
||||
cargo-clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -97,8 +95,8 @@ jobs:
|
||||
cargo-doc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -121,8 +119,8 @@ jobs:
|
||||
# - ubuntu is x86-64
|
||||
# - macos-13 is also x86-64 architecture
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -140,8 +138,8 @@ jobs:
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -150,10 +148,10 @@ jobs:
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- uses: cachix/install-nix-action@v21
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
@@ -162,8 +160,8 @@ jobs:
|
||||
cargo-fuzz:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -195,20 +193,20 @@ jobs:
|
||||
codecov:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: rustup default nightly
|
||||
- run: rustup component add llvm-tools-preview
|
||||
- run: |
|
||||
cargo install cargo-llvm-cov || true
|
||||
cargo llvm-cov \
|
||||
--workspace\
|
||||
--all-features \
|
||||
--lcov \
|
||||
--output-path coverage.lcov
|
||||
cargo install grcov || true
|
||||
./coverage_report.sh
|
||||
# If using tarapulin
|
||||
#- run: cargo install cargo-tarpaulin
|
||||
#- run: cargo tarpaulin --out Xml
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: ./coverage.lcov
|
||||
files: ./target/grcov/lcov
|
||||
verbose: true
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
14
.github/workflows/regressions.yml
vendored
14
.github/workflows/regressions.yml
vendored
@@ -16,10 +16,22 @@ jobs:
|
||||
multi-peer:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: cargo build --bin rosenpass --release
|
||||
- run: python misc/generate_configs.py
|
||||
- run: chmod +x .ci/run-regression.sh
|
||||
- run: .ci/run-regression.sh 100 20
|
||||
- run: |
|
||||
[ $(ls -1 output/ate/out | wc -l) -eq 100 ]
|
||||
|
||||
boot-race:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: cargo build --bin rosenpass --release
|
||||
- run: chmod +x .ci/boot_race/run.sh
|
||||
- run: cargo run --release --bin rosenpass gen-keys .ci/boot_race/a.toml
|
||||
- run: cargo run --release --bin rosenpass gen-keys .ci/boot_race/b.toml
|
||||
- run: .ci/boot_race/run.sh 5 2 .ci/boot_race/a.toml .ci/boot_race/b.toml
|
||||
- run: .ci/boot_race/run.sh 5 1 .ci/boot_race/a.toml .ci/boot_race/b.toml
|
||||
- run: .ci/boot_race/run.sh 5 0 .ci/boot_race/a.toml .ci/boot_race/b.toml
|
||||
|
||||
24
.github/workflows/release.yaml
vendored
24
.github/workflows/release.yaml
vendored
@@ -11,18 +11,18 @@ jobs:
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build release
|
||||
run: nix build .#release-package --print-build-logs
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: ${{ contains(github.ref_name, 'rc') }}
|
||||
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }}
|
||||
@@ -32,18 +32,18 @@ jobs:
|
||||
runs-on:
|
||||
- macos-13
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build release
|
||||
run: nix build .#release-package --print-build-logs
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: ${{ contains(github.ref_name, 'rc') }}
|
||||
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }}
|
||||
@@ -53,18 +53,18 @@ jobs:
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v12
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: rosenpass
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
- name: Build release
|
||||
run: nix build .#release-package --print-build-logs
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: ${{ contains(github.ref_name, 'rc') }}
|
||||
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }}
|
||||
|
||||
@@ -1,38 +1,41 @@
|
||||
**Making a new Release of Rosenpass — Cooking Recipe**
|
||||
# Contributing to Rosenpass
|
||||
|
||||
If you have to change a file, do what it takes to get the change as commit on the main branch, then **start from step 0**.
|
||||
If any other issue occurs
|
||||
## Common operations
|
||||
|
||||
0. Make sure you are in the root directory of the project
|
||||
- `cd "$(git rev-parse --show-toplevel)"`
|
||||
1. Make sure you locally checked out the head of the main branch
|
||||
- `git stash --include-untracked && git checkout main && git pull`
|
||||
2. Make sure all tests pass
|
||||
- `cargo test`
|
||||
3. Make sure the current version in `rosenpass/Cargo.toml` matches that in the [last release on GitHub](https://github.com/rosenpass/rosenpass/releases)
|
||||
- Only normal releases count, release candidates and draft releases can be ignored
|
||||
4. Pick the kind of release that you want to make (`major`, `minor`, `patch`, `rc`, ...)
|
||||
- See `cargo release --help` for more information on the available release types
|
||||
- Pick `rc` if in doubt
|
||||
5. Try to release a new version
|
||||
- `cargo release rc --package rosenpass`
|
||||
- An issue was reported? Go fix it, start again with step 0!
|
||||
6. Actually make the release
|
||||
- `cargo release rc --package rosenpass --execute`
|
||||
- Tentatively wait for any interactions, such as entering ssh keys etc.
|
||||
- You may be asked for your ssh key multiple times!
|
||||
### Apply code formatting
|
||||
|
||||
**Frequently Asked Questions (FAQ)**
|
||||
Format rust code:
|
||||
|
||||
- You have untracked files, which `cargo release` complains about?
|
||||
- `git stash --include-untracked`
|
||||
- You cannot push to crates.io because you are not logged in?
|
||||
- Follow the steps displayed in [`cargo login`](https://doc.rust-lang.org/cargo/commands/cargo-login.html)
|
||||
- How is the release page added to [GitHub Releases](https://github.com/rosenpass/rosenpass/releases) itself?
|
||||
- Our CI Pipeline will create the release, once `cargo release` pushed the new version tag to the repo. The new release should pop up almost immediately in [GitHub Releases](https://github.com/rosenpass/rosenpass/releases) after the [Actions/Release](https://github.com/rosenpass/rosenpass/actions/workflows/release.yaml) pipeline started.
|
||||
- No new release pops up in the `Release` sidebar element on the [main page](https://github.com/rosenpass/rosenpass)
|
||||
- Did you push a `rc` release? This view only shows non-draft release, but `rc` releases are considered as draft. See [Releases](https://github.com/rosenpass/rosenpass/releases) page to see all (including draft!) releases.
|
||||
- The release page was created on GitHub, but there are no assets/artifacts other than the source code tar ball/zip?
|
||||
- The artifacts are generated and pushed automatically to the release, but this takes some time (a couple of minutes). You can check the respective CI pipeline: [Actions/Release](https://github.com/rosenpass/rosenpass/actions/workflows/release.yaml), which should start immediately after `cargo release` pushed the new release tag to the repo. The release artifacts only are added later to the release, once all jobs in bespoke pipeline finished.
|
||||
- How are the release artifacts generated, and what are they?
|
||||
- The release artifacts are built using one Nix derivation per platform, `nix build .#release-package`. It contains both statically linked versions of `rosenpass` itself and OCI container images.
|
||||
```bash
|
||||
cargo fmt
|
||||
```
|
||||
|
||||
Format rust code in markdown files:
|
||||
|
||||
```bash
|
||||
./format_rust_code.sh --mode fix
|
||||
```
|
||||
|
||||
### Spawn a development environment with nix
|
||||
|
||||
```bash
|
||||
nix develop .#fullEnv
|
||||
```
|
||||
|
||||
You need to [install this nix package manager](https://wiki.archlinux.org/title/Nix) first.
|
||||
|
||||
### Run our test
|
||||
|
||||
Make sure to increase the stack size available; some of our cryptography operations require a lot of stack memory.
|
||||
|
||||
```bash
|
||||
RUST_MIN_STACK=8388608 cargo test --workspace --all-features
|
||||
```
|
||||
|
||||
### Generate coverage reports
|
||||
|
||||
Keep in mind that many of Rosenpass' tests are doctests, so to get an accurate read on our code coverage, you have to include doctests:
|
||||
|
||||
```bash
|
||||
./coverage_report.sh
|
||||
```
|
||||
|
||||
187
Cargo.lock
generated
187
Cargo.lock
generated
@@ -109,18 +109,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.89"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
|
||||
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.3.2"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
|
||||
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
@@ -201,7 +201,7 @@ dependencies = [
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
"which",
|
||||
]
|
||||
|
||||
@@ -381,9 +381,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.20"
|
||||
version = "4.5.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
|
||||
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -391,16 +391,25 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.20"
|
||||
version = "4.5.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
||||
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex 0.7.2",
|
||||
"clap_lex 0.7.4",
|
||||
"strsim 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01"
|
||||
dependencies = [
|
||||
"clap 4.5.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.18"
|
||||
@@ -410,7 +419,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -424,9 +433,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.2"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "clap_mangen"
|
||||
version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbae9cbfdc5d4fa8711c09bd7b83f644cb48281ac35bf97af3e47b0675864bdf"
|
||||
dependencies = [
|
||||
"clap 4.5.23",
|
||||
"roff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
@@ -594,7 +613,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -642,7 +661,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -664,18 +683,18 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core 0.20.10",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.3.2"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
|
||||
checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -717,7 +736,7 @@ dependencies = [
|
||||
"darling 0.20.10",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -737,7 +756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||
dependencies = [
|
||||
"derive_builder_core 0.20.2",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -796,12 +815,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -890,7 +909,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1034,12 +1053,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.4.0"
|
||||
@@ -1191,9 +1204,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.159"
|
||||
version = "0.2.168"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
|
||||
|
||||
[[package]]
|
||||
name = "libcrux"
|
||||
@@ -1229,13 +1242,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.7"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
|
||||
checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1331,11 +1343,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
|
||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
@@ -1641,9 +1652,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.0.10"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f7f0a8d620d71c457dd1d47df76bb18960378da56af4527aaa10f515eee732e"
|
||||
checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8"
|
||||
dependencies = [
|
||||
"cobs",
|
||||
"embedded-io 0.4.0",
|
||||
@@ -1668,7 +1679,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1801,12 +1812,20 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "roff"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
|
||||
|
||||
[[package]]
|
||||
name = "rosenpass"
|
||||
version = "0.3.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.5.20",
|
||||
"clap 4.5.23",
|
||||
"clap_complete",
|
||||
"clap_mangen",
|
||||
"command-fds",
|
||||
"criterion",
|
||||
"derive_builder 0.20.2",
|
||||
@@ -1831,6 +1850,7 @@ dependencies = [
|
||||
"rustix",
|
||||
"serde",
|
||||
"serial_test",
|
||||
"signal-hook",
|
||||
"stacker",
|
||||
"static_assertions",
|
||||
"tempfile",
|
||||
@@ -1845,6 +1865,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rosenpass-cipher-traits"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rosenpass-oqs",
|
||||
"rosenpass-secret-memory",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rosenpass-ciphers"
|
||||
@@ -1945,7 +1970,7 @@ name = "rosenpass-wireguard-broker"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.5.20",
|
||||
"clap 4.5.23",
|
||||
"derive_builder 0.20.2",
|
||||
"env_logger",
|
||||
"libc",
|
||||
@@ -1984,9 +2009,11 @@ dependencies = [
|
||||
"rosenpass-util",
|
||||
"rosenpass-wireguard-broker",
|
||||
"rtnetlink",
|
||||
"serde",
|
||||
"stacker",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml",
|
||||
"x25519-dalek",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -2032,15 +2059,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.37"
|
||||
version = "0.38.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
|
||||
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2087,22 +2114,22 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
version = "1.0.216"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
version = "1.0.216"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2128,9 +2155,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serial_test"
|
||||
version = "3.1.1"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d"
|
||||
checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
@@ -2142,13 +2169,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serial_test_derive"
|
||||
version = "3.1.1"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67"
|
||||
checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2157,6 +2184,16 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
@@ -2262,9 +2299,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.79"
|
||||
version = "2.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
|
||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2279,9 +2316,9 @@ checksum = "b4e17d8598067a8c134af59cd33c1c263470e089924a11ab61cf61690919fe3b"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.13.0"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
|
||||
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
@@ -2313,22 +2350,22 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.64"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.64"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2343,9 +2380,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.40.0"
|
||||
version = "1.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
|
||||
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -2367,7 +2404,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2494,7 +2531,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -2516,7 +2553,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -2611,7 +2648,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2622,7 +2659,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2911,7 +2948,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2931,5 +2968,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
23
Cargo.toml
23
Cargo.toml
@@ -35,7 +35,7 @@ doc-comment = "0.3.3"
|
||||
base64ct = { version = "1.6.0", default-features = false }
|
||||
zeroize = "1.8.1"
|
||||
memoffset = "0.9.1"
|
||||
thiserror = "1.0.64"
|
||||
thiserror = "1.0.69"
|
||||
paste = "1.0.15"
|
||||
env_logger = "0.10.2"
|
||||
toml = "0.7.8"
|
||||
@@ -47,11 +47,13 @@ memsec = { git = "https://github.com/rosenpass/memsec.git", rev = "aceb9baee8aec
|
||||
rand = "0.8.5"
|
||||
typenum = "1.17.0"
|
||||
log = { version = "0.4.22" }
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
arbitrary = { version = "1.3.2", features = ["derive"] }
|
||||
anyhow = { version = "1.0.89", features = ["backtrace", "std"] }
|
||||
mio = { version = "1.0.2", features = ["net", "os-poll"] }
|
||||
clap = { version = "4.5.23", features = ["derive"] }
|
||||
clap_mangen = "0.2.24"
|
||||
clap_complete = "4.5.38"
|
||||
serde = { version = "1.0.216", features = ["derive"] }
|
||||
arbitrary = { version = "1.4.1", features = ["derive"] }
|
||||
anyhow = { version = "1.0.94", features = ["backtrace", "std"] }
|
||||
mio = { version = "1.0.3", features = ["net", "os-poll"] }
|
||||
oqs-sys = { version = "0.9.1", default-features = false, features = [
|
||||
'classic_mceliece',
|
||||
'kyber',
|
||||
@@ -64,17 +66,18 @@ chacha20poly1305 = { version = "0.10.1", default-features = false, features = [
|
||||
zerocopy = { version = "0.7.35", features = ["derive"] }
|
||||
home = "0.5.9"
|
||||
derive_builder = "0.20.1"
|
||||
tokio = { version = "1.40", features = ["macros", "rt-multi-thread"] }
|
||||
postcard = { version = "1.0.10", features = ["alloc"] }
|
||||
tokio = { version = "1.42", features = ["macros", "rt-multi-thread"] }
|
||||
postcard = { version = "1.1.1", features = ["alloc"] }
|
||||
libcrux = { version = "0.0.2-pre.2" }
|
||||
hex-literal = { version = "0.4.1" }
|
||||
hex = { version = "0.4.3" }
|
||||
heck = { version = "0.5.0" }
|
||||
libc = { version = "0.2" }
|
||||
uds = { git = "https://github.com/rosenpass/uds" }
|
||||
signal-hook = "0.3.17"
|
||||
|
||||
#Dev dependencies
|
||||
serial_test = "3.1.1"
|
||||
serial_test = "3.2.0"
|
||||
tempfile = "3"
|
||||
stacker = "0.1.17"
|
||||
libfuzzer-sys = "0.4"
|
||||
@@ -87,4 +90,4 @@ procspawn = { version = "1.0.1", features = ["test-support"] }
|
||||
#Broker dependencies (might need cleanup or changes)
|
||||
wireguard-uapi = { version = "3.0.0", features = ["xplatform"] }
|
||||
command-fds = "0.2.3"
|
||||
rustix = { version = "0.38.37", features = ["net", "fs"] }
|
||||
rustix = { version = "0.38.42", features = ["net", "fs", "process"] }
|
||||
|
||||
@@ -10,3 +10,8 @@ repository = "https://github.com/rosenpass/rosenpass"
|
||||
readme = "readme.md"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dev-dependencies]
|
||||
rosenpass-oqs = { workspace = true }
|
||||
rosenpass-secret-memory = { workspace = true }
|
||||
anyhow = {workspace = true}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
Rosenpass internal library providing traits for cryptographic primitives.
|
||||
|
||||
This is an internal library; not guarantee is made about its API at this point in time.
|
||||
This is an internal library; no guarantee is made about its API at this point in time.
|
||||
|
||||
@@ -5,10 +5,128 @@
|
||||
//!
|
||||
//! Conceptually KEMs are akin to public-key encryption, but instead of encrypting
|
||||
//! arbitrary data, KEMs are limited to the transmission of keys, randomly chosen during
|
||||
//!
|
||||
//! encapsulation.
|
||||
//! The [KEM] Trait describes the basic API offered by a Key Encapsulation
|
||||
//! Mechanism. Two implementations for it are provided, [StaticKEM] and [EphemeralKEM].
|
||||
//!
|
||||
//! The [Kem] Trait describes the basic API offered by a Key Encapsulation
|
||||
//! Mechanism. Two implementations for it are provided:
|
||||
//! [Kyber512](../../rosenpass_oqs/kyber_512/enum.Kyber512.html) and
|
||||
//! [ClassicMceliece460896](../../rosenpass_oqs/classic_mceliece_460896/enum.ClassicMceliece460896.html).
|
||||
//!
|
||||
//! An example where Alice generates a keypair and gives her public key to Bob, for Bob to
|
||||
//! encapsulate a symmetric key and Alice to decapsulate it would look as follows.
|
||||
//! In the example, we are using Kyber512, but any KEM that correctly implements the [Kem]
|
||||
//! trait could be used as well.
|
||||
//!```rust
|
||||
//! use rosenpass_cipher_traits::Kem;
|
||||
//! use rosenpass_oqs::Kyber512;
|
||||
//! # use rosenpass_secret_memory::{secret_policy_use_only_malloc_secrets, Secret};
|
||||
//!
|
||||
//! type MyKem = Kyber512;
|
||||
//! secret_policy_use_only_malloc_secrets();
|
||||
//! let mut alice_sk: Secret<{ MyKem::SK_LEN }> = Secret::zero();
|
||||
//! let mut alice_pk: [u8; MyKem::PK_LEN] = [0; MyKem::PK_LEN];
|
||||
//! MyKem::keygen(alice_sk.secret_mut(), &mut alice_pk)?;
|
||||
//!
|
||||
//! let mut bob_shk: Secret<{ MyKem::SHK_LEN }> = Secret::zero();
|
||||
//! let mut bob_ct: [u8; MyKem::CT_LEN] = [0; MyKem::CT_LEN];
|
||||
//! MyKem::encaps(bob_shk.secret_mut(), &mut bob_ct, &mut alice_pk)?;
|
||||
//!
|
||||
//! let mut alice_shk: Secret<{ MyKem::SHK_LEN }> = Secret::zero();
|
||||
//! MyKem::decaps(alice_shk.secret_mut(), alice_sk.secret_mut(), &mut bob_ct)?;
|
||||
//!
|
||||
//! # assert_eq!(alice_shk.secret(), bob_shk.secret());
|
||||
//! # Ok::<(), anyhow::Error>(())
|
||||
//!```
|
||||
//!
|
||||
//! Implementing the [Kem]-trait for a KEM is easy. Mostly, you must format the KEM's
|
||||
//! keys, and ciphertext as `u8` slices. Below, we provide an example for how the trait can
|
||||
//! be implemented using a **HORRIBLY INSECURE** DummyKem that only uses static values for keys
|
||||
//! and ciphertexts as an example.
|
||||
//!```rust
|
||||
//!# use rosenpass_cipher_traits::Kem;
|
||||
//!
|
||||
//! struct DummyKem {}
|
||||
//! impl Kem for DummyKem {
|
||||
//!
|
||||
//! // For this DummyKem, using String for errors is sufficient.
|
||||
//! type Error = String;
|
||||
//!
|
||||
//! // For this DummyKem, we will use a single `u8` for everything
|
||||
//! const SK_LEN: usize = 1;
|
||||
//! const PK_LEN: usize = 1;
|
||||
//! const CT_LEN: usize = 1;
|
||||
//! const SHK_LEN: usize = 1;
|
||||
//!
|
||||
//! fn keygen(sk: &mut [u8], pk: &mut [u8]) -> Result<(), Self::Error> {
|
||||
//! if sk.len() != Self::SK_LEN {
|
||||
//! return Err("sk does not have the correct length!".to_string());
|
||||
//! }
|
||||
//! if pk.len() != Self::PK_LEN {
|
||||
//! return Err("pk does not have the correct length!".to_string());
|
||||
//! }
|
||||
//! sk[0] = 42;
|
||||
//! pk[0] = 21;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//!
|
||||
//! fn encaps(shk: &mut [u8], ct: &mut [u8], pk: &[u8]) -> Result<(), Self::Error> {
|
||||
//! if pk.len() != Self::PK_LEN {
|
||||
//! return Err("pk does not have the correct length!".to_string());
|
||||
//! }
|
||||
//! if ct.len() != Self::CT_LEN {
|
||||
//! return Err("ct does not have the correct length!".to_string());
|
||||
//! }
|
||||
//! if shk.len() != Self::SHK_LEN {
|
||||
//! return Err("shk does not have the correct length!".to_string());
|
||||
//! }
|
||||
//! if pk[0] != 21 {
|
||||
//! return Err("Invalid public key!".to_string());
|
||||
//! }
|
||||
//! ct[0] = 7;
|
||||
//! shk[0] = 17;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//!
|
||||
//! fn decaps(shk: &mut [u8], sk: &[u8], ct: &[u8]) -> Result<(), Self::Error> {
|
||||
//! if sk.len() != Self::SK_LEN {
|
||||
//! return Err("sk does not have the correct length!".to_string());
|
||||
//! }
|
||||
//! if ct.len() != Self::CT_LEN {
|
||||
//! return Err("ct does not have the correct length!".to_string());
|
||||
//! }
|
||||
//! if shk.len() != Self::SHK_LEN {
|
||||
//! return Err("shk does not have the correct length!".to_string());
|
||||
//! }
|
||||
//! if sk[0] != 42 {
|
||||
//! return Err("Invalid public key!".to_string());
|
||||
//! }
|
||||
//! if ct[0] != 7 {
|
||||
//! return Err("Invalid ciphertext!".to_string());
|
||||
//! }
|
||||
//! shk[0] = 17;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! }
|
||||
//! # use rosenpass_secret_memory::{secret_policy_use_only_malloc_secrets, Secret};
|
||||
//! #
|
||||
//! # type MyKem = DummyKem;
|
||||
//! # secret_policy_use_only_malloc_secrets();
|
||||
//! # let mut alice_sk: Secret<{ MyKem::SK_LEN }> = Secret::zero();
|
||||
//! # let mut alice_pk: [u8; MyKem::PK_LEN] = [0; MyKem::PK_LEN];
|
||||
//! # MyKem::keygen(alice_sk.secret_mut(), &mut alice_pk)?;
|
||||
//!
|
||||
//! # let mut bob_shk: Secret<{ MyKem::SHK_LEN }> = Secret::zero();
|
||||
//! # let mut bob_ct: [u8; MyKem::CT_LEN] = [0; MyKem::CT_LEN];
|
||||
//! # MyKem::encaps(bob_shk.secret_mut(), &mut bob_ct, &mut alice_pk)?;
|
||||
//! #
|
||||
//! # let mut alice_shk: Secret<{ MyKem::SHK_LEN }> = Secret::zero();
|
||||
//! # MyKem::decaps(alice_shk.secret_mut(), alice_sk.secret_mut(), &mut bob_ct)?;
|
||||
//! #
|
||||
//! # assert_eq!(alice_shk.secret(), bob_shk.secret());
|
||||
//! #
|
||||
//! # Ok::<(), String>(())
|
||||
//!```
|
||||
//!
|
||||
|
||||
/// Key Encapsulation Mechanism
|
||||
///
|
||||
|
||||
@@ -2,100 +2,196 @@ use anyhow::Result;
|
||||
use rosenpass_secret_memory::Secret;
|
||||
use rosenpass_to::To;
|
||||
|
||||
use crate::subtle::incorrect_hmac_blake2b as hash;
|
||||
use crate::keyed_hash as hash;
|
||||
|
||||
pub use hash::KEY_LEN;
|
||||
|
||||
///
|
||||
///```rust
|
||||
/// # use rosenpass_ciphers::hash_domain::{HashDomain, HashDomainNamespace, SecretHashDomain, SecretHashDomainNamespace};
|
||||
/// use rosenpass_secret_memory::Secret;
|
||||
/// # rosenpass_secret_memory::secret_policy_use_only_malloc_secrets();
|
||||
///
|
||||
/// const PROTOCOL_IDENTIFIER: &str = "MY_PROTOCOL:IDENTIFIER";
|
||||
/// # fn do_doc_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// // create use once hash domain for the protocol identifier
|
||||
/// let mut hash_domain = HashDomain::zero();
|
||||
/// hash_domain = hash_domain.mix(PROTOCOL_IDENTIFIER.as_bytes())?;
|
||||
/// // upgrade to reusable hash domain
|
||||
/// let hash_domain_namespace: HashDomainNamespace = hash_domain.dup();
|
||||
/// // derive new key
|
||||
/// let key_identifier = "my_key_identifier";
|
||||
/// let key = hash_domain_namespace.mix(key_identifier.as_bytes())?.into_value();
|
||||
/// // derive a new key based on a secret
|
||||
/// const MY_SECRET_LEN: usize = 21;
|
||||
/// let my_secret_bytes = "my super duper secret".as_bytes();
|
||||
/// let my_secret: Secret<21> = Secret::from_slice("my super duper secret".as_bytes());
|
||||
/// let secret_hash_domain: SecretHashDomain = hash_domain_namespace.mix_secret(my_secret)?;
|
||||
/// // derive a new key based on the secret key
|
||||
/// let new_key_identifier = "my_new_key_identifier".as_bytes();
|
||||
/// let new_key = secret_hash_domain.mix(new_key_identifier)?.into_secret();
|
||||
///
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// # do_doc_test().unwrap();
|
||||
///
|
||||
///```
|
||||
///
|
||||
|
||||
// TODO Use a proper Dec interface
|
||||
/// A use-once hash domain for a specified key that can be used directly.
|
||||
/// The key must consist of [KEY_LEN] many bytes. If the key must remain secret,
|
||||
/// use [SecretHashDomain] instead.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HashDomain([u8; KEY_LEN]);
|
||||
/// A reusable hash domain for a namespace identified by the key.
|
||||
/// The key must consist of [KEY_LEN] many bytes. If the key must remain secret,
|
||||
/// use [SecretHashDomainNamespace] instead.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HashDomainNamespace([u8; KEY_LEN]);
|
||||
/// A use-once hash domain for a specified key that can be used directly
|
||||
/// by wrapping it in [Secret]. The key must consist of [KEY_LEN] many bytes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SecretHashDomain(Secret<KEY_LEN>);
|
||||
/// A reusable secure hash domain for a namespace identified by the key and that keeps the key secure
|
||||
/// by wrapping it in [Secret]. The key must consist of [KEY_LEN] many bytes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SecretHashDomainNamespace(Secret<KEY_LEN>);
|
||||
|
||||
impl HashDomain {
|
||||
/// Creates a nw [HashDomain] initialized with a all-zeros key.
|
||||
pub fn zero() -> Self {
|
||||
Self([0u8; KEY_LEN])
|
||||
}
|
||||
|
||||
/// Turns this [HashDomain] into a [HashDomainNamespace], keeping the key.
|
||||
pub fn dup(self) -> HashDomainNamespace {
|
||||
HashDomainNamespace(self.0)
|
||||
}
|
||||
|
||||
/// Turns this [HashDomain] into a [SecretHashDomain] by wrapping the key into a [Secret]
|
||||
/// and creating a new [SecretHashDomain] from it.
|
||||
pub fn turn_secret(self) -> SecretHashDomain {
|
||||
SecretHashDomain(Secret::from_slice(&self.0))
|
||||
}
|
||||
|
||||
// TODO: Protocol! Use domain separation to ensure that
|
||||
/// Creates a new [HashDomain] by mixing in a new key `v`. Specifically,
|
||||
/// it evaluates [hash::hash] with this HashDomain's key as the key and `v`
|
||||
/// as the `data` and uses the result as the key for the new [HashDomain].
|
||||
///
|
||||
pub fn mix(self, v: &[u8]) -> Result<Self> {
|
||||
Ok(Self(hash::hash(&self.0, v).collect::<[u8; KEY_LEN]>()?))
|
||||
}
|
||||
|
||||
/// Creates a new [SecretHashDomain] by mixing in a new key `v`
|
||||
/// by calling [SecretHashDomain::invoke_primitive] with this
|
||||
/// [HashDomain]'s key as `k` and `v` as `d`.
|
||||
pub fn mix_secret<const N: usize>(self, v: Secret<N>) -> Result<SecretHashDomain> {
|
||||
SecretHashDomain::invoke_primitive(&self.0, v.secret())
|
||||
}
|
||||
|
||||
/// Gets the key of this [HashDomain].
|
||||
pub fn into_value(self) -> [u8; KEY_LEN] {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl HashDomainNamespace {
|
||||
/// Creates a new [HashDomain] by mixing in a new key `v`. Specifically,
|
||||
/// it evaluates [hash::hash] with the key of this HashDomainNamespace key as the key and `v`
|
||||
/// as the `data` and uses the result as the key for the new [HashDomain].
|
||||
pub fn mix(&self, v: &[u8]) -> Result<HashDomain> {
|
||||
Ok(HashDomain(
|
||||
hash::hash(&self.0, v).collect::<[u8; KEY_LEN]>()?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Creates a new [SecretHashDomain] by mixing in a new key `v`
|
||||
/// by calling [SecretHashDomain::invoke_primitive] with the key of this
|
||||
/// [HashDomainNamespace] as `k` and `v` as `d`.
|
||||
///
|
||||
/// It requires that `v` consists of exactly [KEY_LEN] many bytes.
|
||||
pub fn mix_secret<const N: usize>(&self, v: Secret<N>) -> Result<SecretHashDomain> {
|
||||
SecretHashDomain::invoke_primitive(&self.0, v.secret())
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretHashDomain {
|
||||
/// Create a new [SecretHashDomain] with the given key `k` and data `d` by calling
|
||||
/// [hash::hash] with `k` as the `key` and `d` s the `data`, and using the result
|
||||
/// as the content for the new [SecretHashDomain].
|
||||
/// Both `k` and `d` have to be exactly [KEY_LEN] bytes in length.
|
||||
pub fn invoke_primitive(k: &[u8], d: &[u8]) -> Result<SecretHashDomain> {
|
||||
let mut r = SecretHashDomain(Secret::zero());
|
||||
hash::hash(k, d).to(r.0.secret_mut())?;
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Creates a new [SecretHashDomain] that is initialized with an all zeros key.
|
||||
pub fn zero() -> Self {
|
||||
Self(Secret::zero())
|
||||
}
|
||||
|
||||
/// Turns this [SecretHashDomain] into a [SecretHashDomainNamespace].
|
||||
pub fn dup(self) -> SecretHashDomainNamespace {
|
||||
SecretHashDomainNamespace(self.0)
|
||||
}
|
||||
|
||||
/// Creates a new [SecretHashDomain] from a [Secret] `k`.
|
||||
///
|
||||
/// It requires that `k` consist of exactly [KEY_LEN] bytes.
|
||||
pub fn danger_from_secret(k: Secret<KEY_LEN>) -> Self {
|
||||
Self(k)
|
||||
}
|
||||
|
||||
/// Creates a new [SecretHashDomain] by mixing in a new key `v`. Specifically,
|
||||
/// it evaluates [hash::hash] with this [SecretHashDomain]'s key as the key and `v`
|
||||
/// as the `data` and uses the result as the key for the new [SecretHashDomain].
|
||||
///
|
||||
/// It requires that `v` consists of exactly [KEY_LEN] many bytes.
|
||||
pub fn mix(self, v: &[u8]) -> Result<SecretHashDomain> {
|
||||
Self::invoke_primitive(self.0.secret(), v)
|
||||
}
|
||||
|
||||
/// Creates a new [SecretHashDomain] by mixing in a new key `v`
|
||||
/// by calling [SecretHashDomain::invoke_primitive] with the key of this
|
||||
/// [HashDomainNamespace] as `k` and `v` as `d`.
|
||||
///
|
||||
/// It requires that `v` consists of exactly [KEY_LEN] many bytes.
|
||||
pub fn mix_secret<const N: usize>(self, v: Secret<N>) -> Result<SecretHashDomain> {
|
||||
Self::invoke_primitive(self.0.secret(), v.secret())
|
||||
}
|
||||
|
||||
/// Get the secret key data from this [SecretHashDomain].
|
||||
pub fn into_secret(self) -> Secret<KEY_LEN> {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Evaluate [hash::hash] with this [SecretHashDomain]'s data as the `key` and
|
||||
/// `dst` as the `data` and stores the result as the new data for this [SecretHashDomain].
|
||||
///
|
||||
/// It requires that both `v` and `d` consist of exactly [KEY_LEN] many bytes.
|
||||
pub fn into_secret_slice(mut self, v: &[u8], dst: &[u8]) -> Result<()> {
|
||||
hash::hash(v, dst).to(self.0.secret_mut())
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretHashDomainNamespace {
|
||||
/// Creates a new [SecretHashDomain] by mixing in a new key `v`. Specifically,
|
||||
/// it evaluates [hash::hash] with the key of this HashDomainNamespace key as the key and `v`
|
||||
/// as the `data` and uses the result as the key for the new [HashDomain].
|
||||
///
|
||||
/// It requires that `v` consists of exactly [KEY_LEN] many bytes.
|
||||
pub fn mix(&self, v: &[u8]) -> Result<SecretHashDomain> {
|
||||
SecretHashDomain::invoke_primitive(self.0.secret(), v)
|
||||
}
|
||||
|
||||
/// Creates a new [SecretHashDomain] by mixing in a new key `v`
|
||||
/// by calling [SecretHashDomain::invoke_primitive] with the key of this
|
||||
/// [HashDomainNamespace] as `k` and `v` as `d`.
|
||||
///
|
||||
/// It requires that `v` consists of exactly [KEY_LEN] many bytes.
|
||||
pub fn mix_secret<const N: usize>(&self, v: Secret<N>) -> Result<SecretHashDomain> {
|
||||
SecretHashDomain::invoke_primitive(self.0.secret(), v.secret())
|
||||
}
|
||||
@@ -103,6 +199,7 @@ impl SecretHashDomainNamespace {
|
||||
// TODO: This entire API is not very nice; we need this for biscuits, but
|
||||
// it might be better to extract a special "biscuit"
|
||||
// labeled subkey and reinitialize the chain with this
|
||||
/// Get the secret key data from this [SecretHashDomain].
|
||||
pub fn danger_into_secret(self) -> Secret<KEY_LEN> {
|
||||
self.0
|
||||
}
|
||||
|
||||
@@ -2,12 +2,25 @@ use static_assertions::const_assert;
|
||||
|
||||
pub mod subtle;
|
||||
|
||||
/// All keyed primitives in this crate use 32 byte keys
|
||||
pub const KEY_LEN: usize = 32;
|
||||
const_assert!(KEY_LEN == aead::KEY_LEN);
|
||||
const_assert!(KEY_LEN == xaead::KEY_LEN);
|
||||
const_assert!(KEY_LEN == hash_domain::KEY_LEN);
|
||||
|
||||
/// Keyed hashing
|
||||
///
|
||||
/// This should only be used for implementation details; anything with relevance
|
||||
/// to the cryptographic protocol should use the facilities in [hash_domain], (though
|
||||
/// hash domain uses this module internally)
|
||||
pub mod keyed_hash {
|
||||
pub use crate::subtle::incorrect_hmac_blake2b::{
|
||||
hash, KEY_LEN, KEY_MAX, KEY_MIN, OUT_MAX, OUT_MIN,
|
||||
};
|
||||
}
|
||||
|
||||
/// Authenticated encryption with associated data
|
||||
/// Chacha20poly1305 is used.
|
||||
pub mod aead {
|
||||
#[cfg(not(feature = "experiment_libcrux"))]
|
||||
pub use crate::subtle::chacha20poly1305_ietf::{decrypt, encrypt, KEY_LEN, NONCE_LEN, TAG_LEN};
|
||||
@@ -18,6 +31,7 @@ pub mod aead {
|
||||
}
|
||||
|
||||
/// Authenticated encryption with associated data with a constant nonce
|
||||
/// XChacha20poly1305 is used.
|
||||
pub mod xaead {
|
||||
pub use crate::subtle::xchacha20poly1305_ietf::{
|
||||
decrypt, encrypt, KEY_LEN, NONCE_LEN, TAG_LEN,
|
||||
@@ -26,6 +40,13 @@ pub mod xaead {
|
||||
|
||||
pub mod hash_domain;
|
||||
|
||||
/// This crate includes two key encapsulation mechanisms.
|
||||
/// Namely ClassicMceliece460896 (also referred to as `StaticKem` sometimes) and
|
||||
/// Kyber512 (also referred to as `EphemeralKem` sometimes).
|
||||
///
|
||||
/// See [rosenpass_oqs::ClassicMceliece460896]
|
||||
/// and [rosenpass_oqs::Kyber512] for more details on the specific KEMS.
|
||||
///
|
||||
pub mod kem {
|
||||
pub use rosenpass_oqs::ClassicMceliece460896 as StaticKem;
|
||||
pub use rosenpass_oqs::Kyber512 as EphemeralKem;
|
||||
|
||||
@@ -9,19 +9,43 @@ use blake2::Blake2bMac;
|
||||
use rosenpass_to::{ops::copy_slice, with_destination, To};
|
||||
use rosenpass_util::typenum2const;
|
||||
|
||||
/// Specify that the used implementation of BLAKE2b is the MAC version of BLAKE2b
|
||||
/// with output and key length of 32 bytes (see [Blake2bMac]).
|
||||
type Impl = Blake2bMac<U32>;
|
||||
|
||||
type KeyLen = <Impl as KeySizeUser>::KeySize;
|
||||
type OutLen = <Impl as OutputSizeUser>::OutputSize;
|
||||
|
||||
/// The key length for BLAKE2b supported by this API. Currently 32 Bytes.
|
||||
const KEY_LEN: usize = typenum2const! { KeyLen };
|
||||
/// The output length for BLAKE2b supported by this API. Currently 32 Bytes.
|
||||
const OUT_LEN: usize = typenum2const! { OutLen };
|
||||
|
||||
/// Minimal key length supported by this API.
|
||||
pub const KEY_MIN: usize = KEY_LEN;
|
||||
/// maximal key length supported by this API.
|
||||
pub const KEY_MAX: usize = KEY_LEN;
|
||||
/// minimal output length supported by this API.
|
||||
pub const OUT_MIN: usize = OUT_LEN;
|
||||
/// maximal output length supported by this API.
|
||||
pub const OUT_MAX: usize = OUT_LEN;
|
||||
|
||||
/// Hashes the given `data` with the [Blake2bMac] hash function under the given `key`.
|
||||
/// The both the length of the output the length of the key 32 bytes (or 256 bits).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
///```rust
|
||||
/// # use rosenpass_ciphers::subtle::blake2b::hash;
|
||||
/// use rosenpass_to::To;
|
||||
/// let zero_key: [u8; 32] = [0; 32];
|
||||
/// let data: [u8; 32] = [255; 32];
|
||||
/// // buffer for the hash output
|
||||
/// let mut hash_data: [u8; 32] = [0u8; 32];
|
||||
///
|
||||
/// assert!(hash(&zero_key, &data).to(&mut hash_data).is_ok(), "Hashing has to return OK result");
|
||||
///```
|
||||
///
|
||||
#[inline]
|
||||
pub fn hash<'a>(key: &'a [u8], data: &'a [u8]) -> impl To<[u8], anyhow::Result<()>> + 'a {
|
||||
with_destination(|out: &mut [u8]| {
|
||||
@@ -36,7 +60,6 @@ pub fn hash<'a>(key: &'a [u8], data: &'a [u8]) -> impl To<[u8], anyhow::Result<(
|
||||
let tmp = GenericArray::from_mut_slice(tmp.as_mut());
|
||||
h.finalize_into(tmp);
|
||||
copy_slice(tmp.as_ref()).to(out);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,10 +6,39 @@ use chacha20poly1305::aead::generic_array::GenericArray;
|
||||
use chacha20poly1305::ChaCha20Poly1305 as AeadImpl;
|
||||
use chacha20poly1305::{AeadCore, AeadInPlace, KeyInit, KeySizeUser};
|
||||
|
||||
/// The key length is 32 bytes or 256 bits.
|
||||
pub const KEY_LEN: usize = typenum2const! { <AeadImpl as KeySizeUser>::KeySize };
|
||||
/// The MAC tag length is 16 bytes or 128 bits.
|
||||
pub const TAG_LEN: usize = typenum2const! { <AeadImpl as AeadCore>::TagSize };
|
||||
/// The nonce length is 12 bytes or 96 bits.
|
||||
pub const NONCE_LEN: usize = typenum2const! { <AeadImpl as AeadCore>::NonceSize };
|
||||
|
||||
/// Encrypts using ChaCha20Poly1305 as implemented in [RustCrypto](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305).
|
||||
/// `key` MUST be chosen (pseudo-)randomly and `nonce` MOST NOT be reused. The `key` slice MUST have
|
||||
/// a length of [KEY_LEN]. The `nonce` slice MUST have a length of [NONCE_LEN]. The last [TAG_LEN] bytes
|
||||
/// written in `ciphertext` are the tag guaranteeing integrity. `ciphertext` MUST have a capacity of
|
||||
/// `plaintext.len()` + [TAG_LEN].
|
||||
///
|
||||
/// # Examples
|
||||
///```rust
|
||||
/// # use rosenpass_ciphers::subtle::chacha20poly1305_ietf::{encrypt, TAG_LEN, KEY_LEN, NONCE_LEN};
|
||||
///
|
||||
/// const PLAINTEXT_LEN: usize = 43;
|
||||
/// let plaintext = "post-quantum cryptography is very important".as_bytes();
|
||||
/// assert_eq!(PLAINTEXT_LEN, plaintext.len());
|
||||
/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY
|
||||
/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE
|
||||
/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes();
|
||||
/// let mut ciphertext_buffer = [0u8;PLAINTEXT_LEN + TAG_LEN];
|
||||
///
|
||||
/// let res: anyhow::Result<()> = encrypt(&mut ciphertext_buffer, key, nonce, additional_data, plaintext);
|
||||
/// assert!(res.is_ok());
|
||||
/// # let expected_ciphertext: &[u8] = &[239, 104, 148, 202, 120, 32, 77, 27, 246, 206, 226, 17,
|
||||
/// # 83, 78, 122, 116, 187, 123, 70, 199, 58, 130, 21, 1, 107, 230, 58, 77, 18, 152, 31, 159, 80,
|
||||
/// # 151, 72, 27, 236, 137, 60, 55, 180, 31, 71, 97, 199, 12, 60, 155, 70, 221, 225, 110, 132, 191,
|
||||
/// # 8, 114, 85, 4, 25];
|
||||
/// # assert_eq!(expected_ciphertext, &ciphertext_buffer);
|
||||
///```
|
||||
#[inline]
|
||||
pub fn encrypt(
|
||||
ciphertext: &mut [u8],
|
||||
@@ -26,6 +55,33 @@ pub fn encrypt(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrypts a `ciphertext` and verifies the integrity of the `ciphertext` and the additional data
|
||||
/// `ad`. using ChaCha20Poly1305 as implemented in [RustCrypto](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305).
|
||||
///
|
||||
/// The `key` slice MUST have a length of [KEY_LEN]. The `nonce` slice MUST have a length of
|
||||
/// [NONCE_LEN]. The plaintext buffer must have a capacity of `ciphertext.len()` - [TAG_LEN].
|
||||
///
|
||||
/// # Examples
|
||||
///```rust
|
||||
/// # use rosenpass_ciphers::subtle::chacha20poly1305_ietf::{decrypt, TAG_LEN, KEY_LEN, NONCE_LEN};
|
||||
/// let ciphertext: &[u8] = &[239, 104, 148, 202, 120, 32, 77, 27, 246, 206, 226, 17,
|
||||
/// 83, 78, 122, 116, 187, 123, 70, 199, 58, 130, 21, 1, 107, 230, 58, 77, 18, 152, 31, 159, 80,
|
||||
/// 151, 72, 27, 236, 137, 60, 55, 180, 31, 71, 97, 199, 12, 60, 155, 70, 221, 225, 110, 132, 191,
|
||||
/// 8, 114, 85, 4, 25]; // this is the ciphertext generated by the example for the encryption
|
||||
/// const PLAINTEXT_LEN: usize = 43;
|
||||
/// assert_eq!(PLAINTEXT_LEN + TAG_LEN, ciphertext.len());
|
||||
///
|
||||
/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY
|
||||
/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE
|
||||
/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes();
|
||||
/// let mut plaintext_buffer = [0u8; PLAINTEXT_LEN];
|
||||
///
|
||||
/// let res: anyhow::Result<()> = decrypt(&mut plaintext_buffer, key, nonce, additional_data, ciphertext);
|
||||
/// assert!(res.is_ok());
|
||||
/// let expected_plaintext = "post-quantum cryptography is very important".as_bytes();
|
||||
/// assert_eq!(expected_plaintext, plaintext_buffer);
|
||||
///
|
||||
///```
|
||||
#[inline]
|
||||
pub fn decrypt(
|
||||
plaintext: &mut [u8],
|
||||
|
||||
@@ -3,10 +3,40 @@ use rosenpass_to::To;
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// The key length is 32 bytes or 256 bits.
|
||||
pub const KEY_LEN: usize = 32; // Grrrr! Libcrux, please provide me these constants.
|
||||
/// The MAC tag length is 16 bytes or 128 bits.
|
||||
pub const TAG_LEN: usize = 16;
|
||||
/// The nonce length is 12 bytes or 96 bits.
|
||||
pub const NONCE_LEN: usize = 12;
|
||||
|
||||
/// Encrypts using ChaCha20Poly1305 as implemented in [libcrux](https://github.com/cryspen/libcrux).
|
||||
/// Key and nonce MUST be chosen (pseudo-)randomly. The `key` slice MUST have a length of
|
||||
/// [KEY_LEN]. The `nonce` slice MUST have a length of [NONCE_LEN]. The last [TAG_LEN] bytes
|
||||
/// written in `ciphertext` are the tag guaranteeing integrity. `ciphertext` MUST have a capacity of
|
||||
/// `plaintext.len()` + [TAG_LEN].
|
||||
///
|
||||
/// # Examples
|
||||
///```rust
|
||||
/// # use rosenpass_ciphers::subtle::chacha20poly1305_ietf_libcrux::{encrypt, TAG_LEN, KEY_LEN, NONCE_LEN};
|
||||
///
|
||||
/// const PLAINTEXT_LEN: usize = 43;
|
||||
/// let plaintext = "post-quantum cryptography is very important".as_bytes();
|
||||
/// assert_eq!(PLAINTEXT_LEN, plaintext.len());
|
||||
/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY
|
||||
/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE
|
||||
/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes();
|
||||
/// let mut ciphertext_buffer = [0u8; PLAINTEXT_LEN + TAG_LEN];
|
||||
///
|
||||
/// let res: anyhow::Result<()> = encrypt(&mut ciphertext_buffer, key, nonce, additional_data, plaintext);
|
||||
/// assert!(res.is_ok());
|
||||
/// # let expected_ciphertext: &[u8] = &[239, 104, 148, 202, 120, 32, 77, 27, 246, 206, 226, 17,
|
||||
/// # 83, 78, 122, 116, 187, 123, 70, 199, 58, 130, 21, 1, 107, 230, 58, 77, 18, 152, 31, 159, 80,
|
||||
/// # 151, 72, 27, 236, 137, 60, 55, 180, 31, 71, 97, 199, 12, 60, 155, 70, 221, 225, 110, 132, 191,
|
||||
/// # 8, 114, 85, 4, 25];
|
||||
/// # assert_eq!(expected_ciphertext, &ciphertext_buffer);
|
||||
///```
|
||||
///
|
||||
#[inline]
|
||||
pub fn encrypt(
|
||||
ciphertext: &mut [u8],
|
||||
@@ -33,6 +63,33 @@ pub fn encrypt(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrypts a `ciphertext` and verifies the integrity of the `ciphertext` and the additional data
|
||||
/// `ad`. using ChaCha20Poly1305 as implemented in [libcrux](https://github.com/cryspen/libcrux).
|
||||
///
|
||||
/// The `key` slice MUST have a length of [KEY_LEN]. The `nonce` slice MUST have a length of
|
||||
/// [NONCE_LEN]. The plaintext buffer must have a capacity of `ciphertext.len()` - [TAG_LEN].
|
||||
///
|
||||
/// # Examples
|
||||
///```rust
|
||||
/// # use rosenpass_ciphers::subtle::chacha20poly1305_ietf_libcrux::{decrypt, TAG_LEN, KEY_LEN, NONCE_LEN};
|
||||
/// let ciphertext: &[u8] = &[239, 104, 148, 202, 120, 32, 77, 27, 246, 206, 226, 17,
|
||||
/// 83, 78, 122, 116, 187, 123, 70, 199, 58, 130, 21, 1, 107, 230, 58, 77, 18, 152, 31, 159, 80,
|
||||
/// 151, 72, 27, 236, 137, 60, 55, 180, 31, 71, 97, 199, 12, 60, 155, 70, 221, 225, 110, 132, 191,
|
||||
/// 8, 114, 85, 4, 25]; // this is the ciphertext generated by the example for the encryption
|
||||
/// const PLAINTEXT_LEN: usize = 43;
|
||||
/// assert_eq!(PLAINTEXT_LEN + TAG_LEN, ciphertext.len());
|
||||
///
|
||||
/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY
|
||||
/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE
|
||||
/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes();
|
||||
/// let mut plaintext_buffer = [0u8; PLAINTEXT_LEN];
|
||||
///
|
||||
/// let res: anyhow::Result<()> = decrypt(&mut plaintext_buffer, key, nonce, additional_data, ciphertext);
|
||||
/// assert!(res.is_ok());
|
||||
/// let expected_plaintext = "post-quantum cryptography is very important".as_bytes();
|
||||
/// assert_eq!(expected_plaintext, plaintext_buffer);
|
||||
///
|
||||
///```
|
||||
#[inline]
|
||||
pub fn decrypt(
|
||||
plaintext: &mut [u8],
|
||||
|
||||
@@ -6,10 +6,15 @@ use rosenpass_to::{ops::copy_slice, with_destination, To};
|
||||
|
||||
use crate::subtle::blake2b;
|
||||
|
||||
/// The key length, 32 bytes or 256 bits.
|
||||
pub const KEY_LEN: usize = 32;
|
||||
/// The minimal key length, identical to [KEY_LEN]
|
||||
pub const KEY_MIN: usize = KEY_LEN;
|
||||
/// The maximal key length, identical to [KEY_LEN]
|
||||
pub const KEY_MAX: usize = KEY_LEN;
|
||||
/// The minimal output length, see [blake2b::OUT_MIN]
|
||||
pub const OUT_MIN: usize = blake2b::OUT_MIN;
|
||||
/// The maximal output length, see [blake2b::OUT_MAX]
|
||||
pub const OUT_MAX: usize = blake2b::OUT_MAX;
|
||||
|
||||
/// This is a woefully incorrect implementation of hmac_blake2b.
|
||||
@@ -19,6 +24,22 @@ pub const OUT_MAX: usize = blake2b::OUT_MAX;
|
||||
///
|
||||
/// This will be replaced, likely by Kekkac at some point soon.
|
||||
/// <https://github.com/rosenpass/rosenpass/pull/145>
|
||||
///
|
||||
/// # Examples
|
||||
///```rust
|
||||
/// # use rosenpass_ciphers::subtle::incorrect_hmac_blake2b::hash;
|
||||
/// use rosenpass_to::To;
|
||||
/// let key: [u8; 32] = [0; 32];
|
||||
/// let data: [u8; 32] = [255; 32];
|
||||
/// // buffer for the hash output
|
||||
/// let mut hash_data: [u8; 32] = [0u8; 32];
|
||||
///
|
||||
/// assert!(hash(&key, &data).to(&mut hash_data).is_ok(), "Hashing has to return OK result");
|
||||
/// # let expected_hash: &[u8] = &[5, 152, 135, 141, 151, 106, 147, 8, 220, 95, 38, 66, 29, 33, 3,
|
||||
/// 104, 250, 114, 131, 119, 27, 56, 59, 44, 11, 67, 230, 113, 112, 20, 80, 103];
|
||||
/// # assert_eq!(hash_data, expected_hash);
|
||||
///```
|
||||
///
|
||||
#[inline]
|
||||
pub fn hash<'a>(key: &'a [u8], data: &'a [u8]) -> impl To<[u8], anyhow::Result<()>> + 'a {
|
||||
const IPAD: [u8; KEY_LEN] = [0x36u8; KEY_LEN];
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/// This module provides the following cryptographic schemes:
|
||||
/// - [blake2b]: The blake2b hash function
|
||||
/// - [chacha20poly1305_ietf]: The Chacha20Poly1305 AEAD as implemented in [RustCrypto](https://crates.io/crates/chacha20poly1305) (only used when the feature `experiment_libcrux` is disabled).
|
||||
/// - [chacha20poly1305_ietf_libcrux]: The Chacha20Poly1305 AEAD as implemented in [libcrux](https://github.com/cryspen/libcrux) (only used when the feature `experiment_libcrux` is enabled).
|
||||
/// - [incorrect_hmac_blake2b]: An (incorrect) hmac based on [blake2b].
|
||||
/// - [xchacha20poly1305_ietf] The Chacha20Poly1305 AEAD as implemented in [RustCrypto](https://crates.io/crates/chacha20poly1305)
|
||||
pub mod blake2b;
|
||||
#[cfg(not(feature = "experiment_libcrux"))]
|
||||
pub mod chacha20poly1305_ietf;
|
||||
|
||||
@@ -6,10 +6,41 @@ use chacha20poly1305::aead::generic_array::GenericArray;
|
||||
use chacha20poly1305::XChaCha20Poly1305 as AeadImpl;
|
||||
use chacha20poly1305::{AeadCore, AeadInPlace, KeyInit, KeySizeUser};
|
||||
|
||||
/// The key length is 32 bytes or 256 bits.
|
||||
pub const KEY_LEN: usize = typenum2const! { <AeadImpl as KeySizeUser>::KeySize };
|
||||
/// The MAC tag length is 16 bytes or 128 bits.
|
||||
pub const TAG_LEN: usize = typenum2const! { <AeadImpl as AeadCore>::TagSize };
|
||||
/// The nonce length is 24 bytes or 192 bits.
|
||||
pub const NONCE_LEN: usize = typenum2const! { <AeadImpl as AeadCore>::NonceSize };
|
||||
|
||||
/// Encrypts using XChaCha20Poly1305 as implemented in [RustCrypto](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305).
|
||||
/// `key` and `nonce` MUST be chosen (pseudo-)randomly. The `key` slice MUST have a length of
|
||||
/// [KEY_LEN]. The `nonce` slice MUST have a length of [NONCE_LEN].
|
||||
/// In contrast to [chacha20poly1305_ietf::encrypt](crate::subtle::chacha20poly1305_ietf::encrypt) and
|
||||
/// [chacha20poly1305_ietf_libcrux::encrypt](crate::subtle::chacha20poly1305_ietf_libcrux::encrypt),
|
||||
/// `nonce` is also written into `ciphertext` and therefore ciphertext MUST have a length
|
||||
/// of at least [NONCE_LEN] + `plaintext.len()` + [TAG_LEN].
|
||||
///
|
||||
/// # Examples
|
||||
///```rust
|
||||
/// # use rosenpass_ciphers::subtle::xchacha20poly1305_ietf::{encrypt, TAG_LEN, KEY_LEN, NONCE_LEN};
|
||||
/// const PLAINTEXT_LEN: usize = 43;
|
||||
/// let plaintext = "post-quantum cryptography is very important".as_bytes();
|
||||
/// assert_eq!(PLAINTEXT_LEN, plaintext.len());
|
||||
/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY
|
||||
/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE
|
||||
/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes();
|
||||
/// let mut ciphertext_buffer = [0u8; NONCE_LEN + PLAINTEXT_LEN + TAG_LEN];
|
||||
///
|
||||
///
|
||||
/// let res: anyhow::Result<()> = encrypt(&mut ciphertext_buffer, key, nonce, additional_data, plaintext);
|
||||
/// # assert!(res.is_ok());
|
||||
/// # let expected_ciphertext: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
/// # 0, 0, 0, 0, 8, 241, 229, 253, 200, 81, 248, 30, 183, 149, 134, 168, 149, 87, 109, 49, 159, 108,
|
||||
/// # 206, 89, 51, 232, 232, 197, 163, 253, 254, 208, 73, 76, 253, 13, 247, 162, 133, 184, 177, 44,
|
||||
/// # 73, 138, 176, 193, 61, 248, 61, 183, 164, 192, 214, 168, 4, 1, 62, 243, 36, 48, 149, 164, 6];
|
||||
/// # assert_eq!(expected_ciphertext, &ciphertext_buffer);
|
||||
///```
|
||||
#[inline]
|
||||
pub fn encrypt(
|
||||
ciphertext: &mut [u8],
|
||||
@@ -28,6 +59,38 @@ pub fn encrypt(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrypts a `ciphertext` and verifies the integrity of the `ciphertext` and the additional data
|
||||
/// `ad`. using XChaCha20Poly1305 as implemented in [RustCrypto](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305).
|
||||
///
|
||||
/// The `key` slice MUST have a length of [KEY_LEN]. The `nonce` slice MUST have a length of
|
||||
/// [NONCE_LEN]. The plaintext buffer must have a capacity of `ciphertext.len()` - [TAG_LEN] - [NONCE_LEN].
|
||||
///
|
||||
/// In contrast to [chacha20poly1305_ietf::decrypt](crate::subtle::chacha20poly1305_ietf::decrypt) and
|
||||
/// [chacha20poly1305_ietf_libcrux::decrypt](crate::subtle::chacha20poly1305_ietf_libcrux::decrypt),
|
||||
/// `ciperhtext` MUST include the as it is not given otherwise.
|
||||
///
|
||||
/// # Examples
|
||||
///```rust
|
||||
/// # use rosenpass_ciphers::subtle::xchacha20poly1305_ietf::{decrypt, TAG_LEN, KEY_LEN, NONCE_LEN};
|
||||
/// let ciphertext: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
/// # 0, 0, 0, 0, 8, 241, 229, 253, 200, 81, 248, 30, 183, 149, 134, 168, 149, 87, 109, 49, 159, 108,
|
||||
/// # 206, 89, 51, 232, 232, 197, 163, 253, 254, 208, 73, 76, 253, 13, 247, 162, 133, 184, 177, 44,
|
||||
/// # 73, 138, 176, 193, 61, 248, 61, 183, 164, 192, 214, 168, 4, 1, 62, 243, 36, 48, 149, 164, 6];
|
||||
/// // this is the ciphertext generated by the example for the encryption
|
||||
/// const PLAINTEXT_LEN: usize = 43;
|
||||
/// assert_eq!(PLAINTEXT_LEN + TAG_LEN + NONCE_LEN, ciphertext.len());
|
||||
///
|
||||
/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY
|
||||
/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE
|
||||
/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes();
|
||||
/// let mut plaintext_buffer = [0u8; PLAINTEXT_LEN];
|
||||
///
|
||||
/// let res: anyhow::Result<()> = decrypt(&mut plaintext_buffer, key, additional_data, ciphertext);
|
||||
/// assert!(res.is_ok());
|
||||
/// let expected_plaintext = "post-quantum cryptography is very important".as_bytes();
|
||||
/// assert_eq!(expected_plaintext, plaintext_buffer);
|
||||
///
|
||||
///```
|
||||
#[inline]
|
||||
pub fn decrypt(
|
||||
plaintext: &mut [u8],
|
||||
|
||||
45
coverage_report.sh
Executable file
45
coverage_report.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
OUTPUT_DIR="target/grcov"
|
||||
|
||||
log() {
|
||||
echo >&2 "$@"
|
||||
}
|
||||
|
||||
exc() {
|
||||
echo '$' "$@"
|
||||
"$@"
|
||||
}
|
||||
|
||||
main() {
|
||||
exc cd "$(dirname "$0")"
|
||||
|
||||
local open="0"
|
||||
if [[ "$1" == "--open" ]]; then
|
||||
open="1"
|
||||
fi
|
||||
|
||||
exc cargo llvm-cov --all-features --workspace --doctests --branch
|
||||
|
||||
exc cp -rv target/llvm-cov-target/doctestbins target/llvm-cov-target/debug/deps/doctestbins
|
||||
exc rm -rf "${OUTPUT_DIR}"
|
||||
exc mkdir -p "${OUTPUT_DIR}"
|
||||
exc grcov target/llvm-cov-target/ --llvm -s . --branch \
|
||||
--binary-path ./target/llvm-cov-target/debug/deps \
|
||||
--ignore-not-existing --ignore '../*' --ignore "/*" \
|
||||
--excl-line '^\s*#\[(derive|repr)\(' \
|
||||
-t lcov,html,markdown -o "${OUTPUT_DIR}"
|
||||
|
||||
if (( "${open}" == 1 )); then
|
||||
xdg-open "${PWD}/${OUTPUT_DIR}/html/index.html"
|
||||
fi
|
||||
|
||||
log ""
|
||||
log "Generated reports in \"${PWD}/${OUTPUT_DIR}\"."
|
||||
log "Open \"${PWD}/${OUTPUT_DIR}/html/index.html\" to view HTML report."
|
||||
log ""
|
||||
}
|
||||
|
||||
main "$@"
|
||||
114
doc/rosenpass.1
114
doc/rosenpass.1
@@ -1,114 +0,0 @@
|
||||
.Dd $Mdocdate$
|
||||
.Dt ROSENPASS 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm rosenpass
|
||||
.Nd builds post-quantum-secure VPNs
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op COMMAND
|
||||
.Op Ar OPTIONS ...
|
||||
.Op Ar ARGS ...
|
||||
.Sh DESCRIPTION
|
||||
.Nm
|
||||
performs cryptographic key exchanges that are secure against quantum-computers
|
||||
and then outputs the keys.
|
||||
These keys can then be passed to various services, such as wireguard or other
|
||||
vpn services, as pre-shared-keys to achieve security against attackers with
|
||||
quantum computers.
|
||||
.Pp
|
||||
This is a research project and quantum computers are not thought to become
|
||||
practical in fewer than ten years.
|
||||
If you are not specifically tasked with developing post-quantum secure systems,
|
||||
you probably do not need this tool.
|
||||
.Ss COMMANDS
|
||||
.Bl -tag -width Ds
|
||||
.It Ar gen-keys --secret-key <file-path> --public-key <file-path>
|
||||
Generate a keypair to use in the exchange command later.
|
||||
Send the public-key file to your communication partner and keep the private-key
|
||||
file secret!
|
||||
.It Ar exchange private-key <file-path> public-key <file-path> [ OPTIONS ] PEERS
|
||||
Start a process to exchange keys with the specified peers.
|
||||
You should specify at least one peer.
|
||||
.Pp
|
||||
Its
|
||||
.Ar OPTIONS
|
||||
are as follows:
|
||||
.Bl -tag -width Ds
|
||||
.It Ar listen <ip>[:<port>]
|
||||
Instructs
|
||||
.Nm
|
||||
to listen on the specified interface and port.
|
||||
By default,
|
||||
.Nm
|
||||
will listen on all interfaces and select a random port.
|
||||
.It Ar verbose
|
||||
Extra logging.
|
||||
.El
|
||||
.El
|
||||
.Ss PEER
|
||||
Each
|
||||
.Ar PEER
|
||||
is defined as follows:
|
||||
.Qq peer public-key <file-path> [endpoint <ip>[:<port>]] [preshared-key <file-path>] [outfile <file-path>] [wireguard <dev> <peer> <extra_params>]
|
||||
.Pp
|
||||
Providing a
|
||||
.Ar PEER
|
||||
instructs
|
||||
.Nm
|
||||
to exchange keys with the given peer and write the resulting PSK into the given
|
||||
output file.
|
||||
You must either specify the outfile or wireguard output option.
|
||||
.Pp
|
||||
The parameters of
|
||||
.Ar PEER
|
||||
are as follows:
|
||||
.Bl -tag -width Ds
|
||||
.It Ar endpoint <ip>[:<port>]
|
||||
Specifies the address where the peer can be reached.
|
||||
This will be automatically updated after the first successful key exchange with
|
||||
the peer.
|
||||
If this is unspecified, the peer must initiate the connection.
|
||||
.It Ar preshared-key <file-path>
|
||||
You may specify a pre-shared key which will be mixed into the final secret.
|
||||
.It Ar outfile <file-path>
|
||||
You may specify a file to write the exchanged keys to.
|
||||
If this option is specified,
|
||||
.Nm
|
||||
will write a notification to standard out every time the key is updated.
|
||||
.It Ar wireguard <dev> <peer> <extra_params>
|
||||
This allows you to directly specify a wireguard peer to deploy the
|
||||
pre-shared-key to.
|
||||
You may specify extra parameters you would pass to
|
||||
.Qq wg set
|
||||
besides the preshared-key parameter which is used by
|
||||
.Nm .
|
||||
This makes it possible to add peers entirely from
|
||||
.Nm .
|
||||
.El
|
||||
.Sh EXIT STATUS
|
||||
.Ex -std
|
||||
.Sh SEE ALSO
|
||||
.Xr rp 1 ,
|
||||
.Xr wg 1
|
||||
.Rs
|
||||
.%A Karolin Varner
|
||||
.%A Benjamin Lipp
|
||||
.%A Wanja Zaeske
|
||||
.%A Lisa Schmidt
|
||||
.%D 2023
|
||||
.%T Rosenpass
|
||||
.%U https://rosenpass.eu/whitepaper.pdf
|
||||
.Re
|
||||
.Sh STANDARDS
|
||||
This tool is the reference implementation of the Rosenpass protocol, as
|
||||
specified within the whitepaper referenced above.
|
||||
.Sh AUTHORS
|
||||
Rosenpass was created by Karolin Varner, Benjamin Lipp, Wanja Zaeske,
|
||||
Marei Peischl, Stephan Ajuvo, and Lisa Schmidt.
|
||||
.Pp
|
||||
This manual page was written by
|
||||
.An Clara Engler
|
||||
.Sh BUGS
|
||||
The bugs are tracked at
|
||||
.Lk https://github.com/rosenpass/rosenpass/issues .
|
||||
19
flake.nix
19
flake.nix
@@ -109,16 +109,35 @@
|
||||
proverif-patched
|
||||
];
|
||||
};
|
||||
# TODO: Write this as a patched version of the default environment
|
||||
devShells.fullEnv = pkgs.mkShell {
|
||||
inherit (pkgs.proof-proverif) CRYPTOVERIF_LIB;
|
||||
inputsFrom = [ pkgs.rosenpass ];
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cargo-release
|
||||
rustfmt
|
||||
nodePackages.prettier
|
||||
nushell # for the .ci/gen-workflow-files.nu script
|
||||
proverif-patched
|
||||
inputs.fenix.packages.${system}.complete.toolchain
|
||||
pkgs.cargo-llvm-cov
|
||||
pkgs.grcov
|
||||
];
|
||||
};
|
||||
devShells.coverage = pkgs.mkShell {
|
||||
inputsFrom = [ pkgs.rosenpass ];
|
||||
nativeBuildInputs = [
|
||||
inputs.fenix.packages.${system}.complete.toolchain
|
||||
pkgs.cargo-llvm-cov
|
||||
pkgs.grcov
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
checks = {
|
||||
systemd-rosenpass = pkgs.testers.runNixOSTest ./tests/systemd/rosenpass.nix;
|
||||
systemd-rp = pkgs.testers.runNixOSTest ./tests/systemd/rp.nix;
|
||||
|
||||
cargo-fmt = pkgs.runCommand "check-cargo-fmt"
|
||||
{ inherit (self.devShells.${system}.default) nativeBuildInputs buildInputs; } ''
|
||||
cargo fmt --manifest-path=${./.}/Cargo.toml --check --all && touch $out
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
template: rosenpass
|
||||
title: Rosenpass
|
||||
author:
|
||||
- Karolin Varner = Independent Researcher
|
||||
- Benjamin Lipp = Max Planck Institute for Security and Privacy (MPI-SP)
|
||||
- Karolin Varner = Rosenpass e.V., Max Planck Institute for Security and Privacy (MPI-SP)
|
||||
- Benjamin Lipp = Rosenpass e.V., Max Planck Institute for Security and Privacy (MPI-SP)
|
||||
- Wanja Zaeske
|
||||
- Lisa Schmidt = {Scientific Illustrator – \\url{mullana.de}}
|
||||
- Prabhpreet Dua
|
||||
@@ -383,9 +383,18 @@ fn load_biscuit(nct) {
|
||||
"biscuit additional data",
|
||||
spkr, sidi, sidr);
|
||||
let pt : Biscuit = XAEAD::dec(k, n, ct, ad);
|
||||
|
||||
// Find the peer and apply retransmission protection
|
||||
lookup_peer(pt.peerid);
|
||||
assert(pt.biscuit_no <= peer.biscuit_used);
|
||||
|
||||
// In December 2024, the InitConf retransmission mechanisim was redesigned
|
||||
// in a backwards-compatible way. See the changelog.
|
||||
//
|
||||
// -- 2024-11-30, Karolin Varner
|
||||
if (protocol_version!(< "0.3.0")) {
|
||||
// Ensure that the biscuit is used only once
|
||||
assert(pt.biscuit_no <= peer.biscuit_used);
|
||||
}
|
||||
|
||||
// Restore the chaining key
|
||||
ck ← pt.ck;
|
||||
@@ -501,7 +510,7 @@ LAST_UNDER_LOAD_WINDOW = 1 //seconds
|
||||
|
||||
The initiator deals with packet loss by storing the messages it sends to the responder and retransmitting them in randomized, exponentially increasing intervals until they get a response. Receiving RespHello terminates retransmission of InitHello. A Data or EmptyData message serves as acknowledgement of receiving InitConf and terminates its retransmission.
|
||||
|
||||
The responder does not need to do anything special to handle RespHello retransmission – if the RespHello package is lost, the initiator retransmits InitHello and the responder can generate another RespHello package from that. InitConf retransmission needs to be handled specifically in the responder code because accepting an InitConf retransmission would reset the live session including the nonce counter, which would cause nonce reuse. Implementations must detect the case that `biscuit_no = biscuit_used` in ICR5, skip execution of ICR6 and ICR7, and just transmit another EmptyData package to confirm that the initiator can stop transmitting InitConf.
|
||||
The responder uses less complex form of the same mechanism: The responder never retransmits RespHello, instead the responder generates a new RespHello message if InitHello is retransmitted. Responder confirmation messages of completed handshake (EmptyData) messages are retransmitted by storing the most recent InitConf messages (or their hashes) and caching the associated EmptyData messages. Through this cache, InitConf retransmission is detected and the associated EmptyData message is retransmitted.
|
||||
|
||||
### Interaction with cookie reply system
|
||||
|
||||
@@ -515,6 +524,76 @@ When the responder is under load and it recieves an InitConf message, the messag
|
||||
|
||||
# Changelog
|
||||
|
||||
### 0.3.x
|
||||
|
||||
#### 2024-10-30 – InitConf retransmission updates
|
||||
|
||||
\vspace{0.5em}
|
||||
|
||||
Author: Karolin Varner
|
||||
Issue: [#331](https://github.com/rosenpass/rosenpass/issues/331)
|
||||
PR: [#513](https://github.com/rosenpass/rosenpass/pull/513)
|
||||
|
||||
\vspace{0.5em}
|
||||
|
||||
We redesign the InitConf retransmission mechanism to use a hash table. This avoids the need for the InitConf handling code to account for InitConf retransmission specifically and moves the retransmission logic into less-sensitive code.
|
||||
|
||||
Previously, we would specifically account for InitConf retransmission in the InitConf handling code by checking the biscuit number: If the biscuit number was higher than any previously seen biscuit number, then this must be a new key-exchange being completed; if the biscuit number was exactly the highest seen biscuit number, then the InitConf message is interpreted as an InitConf retransmission; in this case, an entirely new EmptyData (responder confirmation) message was generated as confirmation that InitConf has been received and that the initiator can now cease opportunistic retransmission of InitConf.
|
||||
|
||||
This mechanism was a bit brittle; even leading to a very minor but still relevant security issue, necessitating the release of Rosenpass maintenance version 0.2.2 with a [fix for the problem](https://github.com/rosenpass/rosenpass/pull/329). We had processed the InitConf message, correctly identifying that InitConf was a retransmission, but we failed to pass this information on to the rest of the code base, leading to double emission of the same "hey, we have a new cryptographic session key" even if the `outfile` option was used to integrate Rosenpass into some external application. If this event was used anywhere to reset a nonce, then this could have led to a nonce-misuse, although for the use with WireGuard this is not an issue.
|
||||
|
||||
By removing all retransmission handling code from the cryptographic protocol, we are taking structural measures to exclude the possibilities of similar issues.
|
||||
|
||||
- In section "Dealing With Package Loss" we replace
|
||||
|
||||
\begin{quote}
|
||||
The responder does not need to do anything special to handle RespHello retransmission – if the RespHello package is lost, the initiator retransmits InitHello and the responder can generate another RespHello package from that. InitConf retransmission needs to be handled specifically in the responder code because accepting an InitConf retransmission would reset the live session including the nonce counter, which would cause nonce reuse. Implementations must detect the case that `biscuit_no = biscuit_used` in ICR5, skip execution of ICR6 and ICR7, and just transmit another EmptyData package to confirm that the initiator can stop transmitting InitConf.
|
||||
\end{quote}
|
||||
|
||||
by
|
||||
|
||||
\begin{quote}
|
||||
The responder uses less complex form of the same mechanism: The responder never retransmits RespHello, instead the responder generates a new RespHello message if InitHello is retransmitted. Responder confirmation messages of completed handshake (EmptyData) messages are retransmitted by storing the most recent InitConf messages (or their hashes) and caching the associated EmptyData messages. Through this cache, InitConf retransmission is detected and the associated EmptyData message is retransmitted.
|
||||
\end{quote}
|
||||
|
||||
- In function `load_biscuit` we replace
|
||||
|
||||
``` {=tex}
|
||||
\begin{quote}
|
||||
\begin{minted}{pseudorust}
|
||||
assert(pt.biscuit_no <= peer.biscuit_used);
|
||||
\end{minted}
|
||||
\end{quote}
|
||||
```
|
||||
|
||||
by
|
||||
|
||||
``` {=tex}
|
||||
\begin{quote}
|
||||
\begin{minted}{pseudorust}
|
||||
// In December 2024, the InitConf retransmission mechanisim was redesigned
|
||||
// in a backwards-compatible way. See the changelog.
|
||||
//
|
||||
// -- 2024-11-30, Karolin Varner
|
||||
if (protocol_version!(< "0.3.0")) {
|
||||
// Ensure that the biscuit is used only once
|
||||
assert(pt.biscuit_no <= peer.biscuit_used);
|
||||
}
|
||||
\end{minted}
|
||||
\end{quote}
|
||||
```
|
||||
|
||||
#### 2024-04-16 – Denial of Service Mitigation
|
||||
|
||||
\vspace{0.5em}
|
||||
|
||||
Author: Prabhpreet Dua
|
||||
Issue: [#137](https://github.com/rosenpass/rosenpass/issues/137)
|
||||
PR: [#142](https://github.com/rosenpass/rosenpass/pull/142)
|
||||
|
||||
\vspace{0.5em}
|
||||
|
||||
- Added denial of service mitigation using the WireGuard cookie mechanism
|
||||
- Added section "Denial of Service Mitigation and Cookies", and modify "Dealing with Packet Loss" for DoS cookie mechanism
|
||||
|
||||
\printbibliography
|
||||
|
||||
@@ -20,7 +20,7 @@ in
|
||||
runCommandNoCC "lace-result" { } ''
|
||||
mkdir {bin,$out}
|
||||
tar -cvf $out/rosenpass-${stdenvNoCC.hostPlatform.system}-${version}.tar \
|
||||
-C ${package} bin/rosenpass \
|
||||
-C ${package} bin/rosenpass lib/systemd \
|
||||
-C ${rp} bin/rp
|
||||
cp ${oci-image} \
|
||||
$out/rosenpass-oci-image-${stdenvNoCC.hostPlatform.system}-${version}.tar.gz
|
||||
|
||||
@@ -12,6 +12,8 @@ let
|
||||
extensions = [
|
||||
"lock"
|
||||
"rs"
|
||||
"service"
|
||||
"target"
|
||||
"toml"
|
||||
];
|
||||
# Files to explicitly include
|
||||
@@ -69,6 +71,13 @@ rustPlatform.buildRustPackage {
|
||||
|
||||
hardeningDisable = lib.optional isStatic "fortify";
|
||||
|
||||
postInstall = ''
|
||||
mkdir -p $out/lib/systemd/system
|
||||
install systemd/rosenpass@.service $out/lib/systemd/system
|
||||
install systemd/rp@.service $out/lib/systemd/system
|
||||
install systemd/rosenpass.target $out/lib/systemd/system
|
||||
'';
|
||||
|
||||
meta = {
|
||||
inherit (cargoToml.package) description homepage;
|
||||
license = with lib.licenses; [ mit asl20 ];
|
||||
|
||||
@@ -23,6 +23,12 @@ rosenpass help
|
||||
|
||||
Follow [quick start instructions](https://rosenpass.eu/#start) to get a VPN up and running.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are generally welcome. Join our [Matrix Chat](https://matrix.to/#/#rosenpass:matrix.org) if you are looking for guidance on how to contribute or for people to collaborate with.
|
||||
|
||||
We also have a – as of now, very minimal – [contributors guide](CONTRIBUTING.md).
|
||||
|
||||
## Software architecture
|
||||
|
||||
The [rosenpass tool](./src/) is written in Rust and uses liboqs[^liboqs]. The tool establishes a symmetric key and provides it to WireGuard. Since it supplies WireGuard with key through the PSK feature using Rosenpass+WireGuard is cryptographically no less secure than using WireGuard on its own ("hybrid security"). Rosenpass refreshes the symmetric key every two minutes.
|
||||
|
||||
@@ -47,6 +47,8 @@ env_logger = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
clap_complete = { workspace = true }
|
||||
clap_mangen = { workspace = true }
|
||||
mio = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
zerocopy = { workspace = true }
|
||||
@@ -58,8 +60,9 @@ hex-literal = { workspace = true, optional = true }
|
||||
hex = { workspace = true, optional = true }
|
||||
heck = { workspace = true, optional = true }
|
||||
command-fds = { workspace = true, optional = true }
|
||||
rustix = { workspace = true }
|
||||
rustix = { workspace = true, optional = true }
|
||||
uds = { workspace = true, optional = true, features = ["mio_1xx"] }
|
||||
signal-hook = { workspace = true, optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
@@ -74,15 +77,17 @@ tempfile = { workspace = true }
|
||||
rustix = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["experiment_api"]
|
||||
default = []
|
||||
experiment_memfd_secret = ["rosenpass-wireguard-broker/experiment_memfd_secret"]
|
||||
experiment_libcrux = ["rosenpass-ciphers/experiment_libcrux"]
|
||||
experiment_api = [
|
||||
"hex-literal",
|
||||
"uds",
|
||||
"command-fds",
|
||||
"rustix",
|
||||
"rosenpass-util/experiment_file_descriptor_passing",
|
||||
"rosenpass-wireguard-broker/experiment_api",
|
||||
]
|
||||
internal_signal_handling_for_coverage_reports = ["signal-hook"]
|
||||
internal_testing = []
|
||||
internal_bin_gen_ipc_msg_types = ["hex", "heck"]
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
use anyhow::bail;
|
||||
use anyhow::Result;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
/// Invokes a troff compiler to compile a manual page
|
||||
fn render_man(compiler: &str, man: &str) -> Result<String> {
|
||||
let out = Command::new(compiler).args(["-Tascii", man]).output()?;
|
||||
if !out.status.success() {
|
||||
bail!("{} returned an error", compiler);
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(out.stdout)?)
|
||||
}
|
||||
|
||||
/// Generates the manual page
|
||||
fn generate_man() -> String {
|
||||
// This function is purposely stupid and redundant
|
||||
|
||||
let man = render_man("mandoc", "./doc/rosenpass.1");
|
||||
if let Ok(man) = man {
|
||||
return man;
|
||||
}
|
||||
|
||||
let man = render_man("groff", "./doc/rosenpass.1");
|
||||
if let Ok(man) = man {
|
||||
return man;
|
||||
}
|
||||
|
||||
"Cannot render manual page. Please visit https://rosenpass.eu/docs/manuals/\n".into()
|
||||
}
|
||||
|
||||
fn man() {
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let man = generate_man();
|
||||
let path = out_dir.join("rosenpass.1.ascii");
|
||||
|
||||
let mut file = File::create(&path).unwrap();
|
||||
file.write_all(man.as_bytes()).unwrap();
|
||||
|
||||
println!("cargo:rustc-env=ROSENPASS_MAN={}", path.display());
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// For now, rerun the build script on every time, as the build script
|
||||
// is not very expensive right now.
|
||||
println!("cargo:rerun-if-changed=./");
|
||||
man();
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Note: This is business logic; tested through the integration tests in
|
||||
// rosenpass/tests/
|
||||
|
||||
use std::{borrow::BorrowMut, collections::VecDeque, os::fd::OwnedFd};
|
||||
|
||||
use anyhow::Context;
|
||||
@@ -20,37 +23,80 @@ use crate::{
|
||||
|
||||
use super::{supply_keypair_response_status, Server as ApiServer};
|
||||
|
||||
/// Stores the state of the API handler.
|
||||
///
|
||||
/// This is used in the context [ApiHandlerContext]; [ApiHandlerContext] exposes both
|
||||
/// the [AppServer] and the API handler state.
|
||||
///
|
||||
/// [ApiHandlerContext] is what actually contains the API handler functions.
|
||||
#[derive(Debug)]
|
||||
pub struct ApiHandler {
|
||||
_dummy: (),
|
||||
}
|
||||
|
||||
impl ApiHandler {
|
||||
/// Construct an [Self]
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self { _dummy: () }
|
||||
}
|
||||
}
|
||||
|
||||
/// The implementation of the API requires both access to its own state [ApiHandler] and to the
|
||||
/// [AppServer] the API is supposed to operate on.
|
||||
///
|
||||
/// This trait provides both; it implements a pattern to allow for multiple - **potentially
|
||||
/// overlapping** mutable references to be passed to the API handler functions.
|
||||
///
|
||||
/// This relatively complex scheme is chosen to appease the borrow checker: We want flexibility
|
||||
/// with regard to where the [ApiHandler] is stored and we need a mutable reference to
|
||||
/// [ApiHandler]. We also need a mutable reference to [AppServer]. Achieving this by using the
|
||||
/// direct method would be impossible because the [ApiHandler] is actually stored somewhere inside
|
||||
/// [AppServer]. The borrow checker does not allow this.
|
||||
///
|
||||
/// What we have instead is – in practice – a reference to [AppServer] and a function (as part of
|
||||
/// the trait) that extracts an [ApiHandler] reference from [AppServer], which is allowed by the
|
||||
/// borrow checker. A benefit of the use of a trait here is that we could, if desired, also store
|
||||
/// the [ApiHandler] outside [AppServer]. It really depends on the trait.
|
||||
pub trait ApiHandlerContext {
|
||||
/// Retrieve the [ApiHandler]
|
||||
fn api_handler(&self) -> &ApiHandler;
|
||||
/// Retrieve the [AppServer]
|
||||
fn app_server(&self) -> &AppServer;
|
||||
/// Retrieve the [ApiHandler]
|
||||
fn api_handler_mut(&mut self) -> &mut ApiHandler;
|
||||
/// Retrieve the [AppServer]
|
||||
fn app_server_mut(&mut self) -> &mut AppServer;
|
||||
}
|
||||
|
||||
/// This is the Error raised by [ApiServer::supply_keypair]; it contains both
|
||||
/// the underlying error message as well as the status value
|
||||
/// returned by the API.
|
||||
///
|
||||
/// [ApiServer::supply_keypair] generally constructs a [Self] by using one of the
|
||||
/// utility functions [SupplyKeypairErrorExt].
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("Error in SupplyKeypair")]
|
||||
struct SupplyKeypairError {
|
||||
/// The status code communicated via the Rosenpass API
|
||||
status: u128,
|
||||
/// The underlying error that caused the Rosenpass API level Error
|
||||
#[source]
|
||||
cause: anyhow::Error,
|
||||
}
|
||||
|
||||
trait SupplyKeypairErrorExt<T> {
|
||||
/// Imbue any Error (that can be represented as [anyhow::Error]) with
|
||||
/// an arbitrary error code
|
||||
fn e_custom(self, status: u128) -> Result<T, SupplyKeypairError>;
|
||||
/// Imbue any Error (that can be represented as [anyhow::Error]) with
|
||||
/// the [supply_keypair_response_status::INTERNAL_ERROR] error code
|
||||
fn einternal(self) -> Result<T, SupplyKeypairError>;
|
||||
/// Imbue any Error (that can be represented as [anyhow::Error]) with
|
||||
/// the [supply_keypair_response_status::KEYPAIR_ALREADY_SUPPLIED] error code
|
||||
fn ealready_supplied(self) -> Result<T, SupplyKeypairError>;
|
||||
/// Imbue any Error (that can be represented as [anyhow::Error]) with
|
||||
/// the [supply_keypair_response_status::INVALID_REQUEST] error code
|
||||
fn einvalid_req(self) -> Result<T, SupplyKeypairError>;
|
||||
}
|
||||
|
||||
|
||||
@@ -140,8 +140,10 @@ impl Message for SupplyKeypairRequest {
|
||||
pub mod supply_keypair_response_status {
|
||||
pub const OK: u128 = 0;
|
||||
pub const KEYPAIR_ALREADY_SUPPLIED: u128 = 1;
|
||||
// TODO: This is not actually part of the API. Remove.
|
||||
pub const INTERNAL_ERROR: u128 = 2;
|
||||
pub const INVALID_REQUEST: u128 = 3;
|
||||
/// TODO: Deprectaed, remove
|
||||
pub const IO_ERROR: u128 = 4;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ use std::{collections::VecDeque, os::fd::OwnedFd};
|
||||
use zerocopy::{ByteSlice, ByteSliceMut};
|
||||
|
||||
pub trait Server {
|
||||
/// This implements the handler for the [crate::api::RequestMsgType::Ping] API message
|
||||
///
|
||||
/// It merely takes a buffer and returns that same buffer.
|
||||
fn ping(
|
||||
&mut self,
|
||||
req: &PingRequest,
|
||||
@@ -10,6 +13,47 @@ pub trait Server {
|
||||
res: &mut PingResponse,
|
||||
) -> anyhow::Result<()>;
|
||||
|
||||
/// Supply the cryptographic server keypair through file descriptor passing in the API
|
||||
///
|
||||
/// This implements the handler for the [crate::api::RequestMsgType::SupplyKeypair] API message.
|
||||
///
|
||||
/// # File descriptors
|
||||
///
|
||||
/// 1. The secret key (size must match exactly); the file descriptor must be backed by either
|
||||
/// of
|
||||
/// - file-system file
|
||||
/// - [memfd](https://man.archlinux.org/man/memfd.2.en)
|
||||
/// - [memfd_secret](https://man.archlinux.org/man/memfd.2.en)
|
||||
/// 2. The public key (size must match exactly); the file descriptor must be backed by either
|
||||
/// of
|
||||
/// - file-system file
|
||||
/// - [memfd](https://man.archlinux.org/man/memfd.2.en)
|
||||
/// - [memfd_secret](https://man.archlinux.org/man/memfd.2.en)
|
||||
///
|
||||
/// # API Return Status
|
||||
///
|
||||
/// 1. [crate::api::supply_keypair_response_status::OK] - Indicates success
|
||||
/// 2. [crate::api::supply_keypair_response_status::KEYPAIR_ALREADY_SUPPLIED] – The endpoint was used but
|
||||
/// the server already has server keys
|
||||
/// 3. [crate::api::supply_keypair_response_status::INVALID_REQUEST] – Malformed request; could be:
|
||||
/// - Missing file descriptors for public key
|
||||
/// - File descriptors contain data of invalid length
|
||||
/// - Invalid file descriptor type
|
||||
///
|
||||
/// # Description
|
||||
///
|
||||
/// At startup, if no server keys are specified in the rosenpass configuration, and if the API
|
||||
/// is enabled, the Rosenpass process waits for server keys to be supplied to the API. Before
|
||||
/// then, any messages for the rosenpass cryptographic protocol are ignored and dropped – all
|
||||
/// cryptographic operations require access to the server keys.
|
||||
///
|
||||
/// Both private and public keys are specified through file descriptors and both are read from
|
||||
/// their respective file descriptors into process memory. A file descriptor based transport is
|
||||
/// used because of the excessive size of Classic McEliece public keys (100kb and up).
|
||||
///
|
||||
/// The file descriptors for the keys need not be backed by a file on disk. You can supply a
|
||||
/// [memfd](https://man.archlinux.org/man/memfd.2.en) or [memfd_secret](https://man.archlinux.org/man/memfd_secret.2.en)
|
||||
/// backed file descriptor if the server keys are not backed by a file system file.
|
||||
fn supply_keypair(
|
||||
&mut self,
|
||||
req: &super::SupplyKeypairRequest,
|
||||
@@ -17,6 +61,27 @@ pub trait Server {
|
||||
res: &mut super::SupplyKeypairResponse,
|
||||
) -> anyhow::Result<()>;
|
||||
|
||||
/// Supply a new UDP listen socket through file descriptor passing via the API
|
||||
///
|
||||
/// This implements the handler for the [crate::api::RequestMsgType::AddListenSocket] API message.
|
||||
///
|
||||
/// # File descriptors
|
||||
///
|
||||
/// 1. The listen socket; must be backed by a UDP network listen socket
|
||||
///
|
||||
/// # API Return Status
|
||||
///
|
||||
/// 1. [crate::api::add_listen_socket_response_status::OK] - Indicates success
|
||||
/// 2. [add_listen_socket_response_status::INVALID_REQUEST] – Malformed request; could be:
|
||||
/// - Missing file descriptors for public key
|
||||
/// - Invalid file descriptor type
|
||||
/// 3. [crate::api::add_listen_socket_response_status::INTERNAL_ERROR] – Some other, non-fatal error
|
||||
/// occured. Check the logs on log
|
||||
///
|
||||
/// # Description
|
||||
///
|
||||
/// This endpoint allows you to supply a UDP listen socket; it will be used to perform
|
||||
/// cryptographic key exchanges via the Rosenpass protocol.
|
||||
fn add_listen_socket(
|
||||
&mut self,
|
||||
req: &super::AddListenSocketRequest,
|
||||
|
||||
@@ -88,7 +88,7 @@ impl MioConnection {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn shoud_close(&self) -> bool {
|
||||
pub fn should_close(&self) -> bool {
|
||||
let exhausted = self
|
||||
.buffers
|
||||
.as_ref()
|
||||
@@ -262,7 +262,7 @@ pub trait MioConnectionContext {
|
||||
}
|
||||
|
||||
fn should_close(&self) -> bool {
|
||||
self.mio_connection().shoud_close()
|
||||
self.mio_connection().should_close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! The bulk code relating to the Rosenpass unix socket API
|
||||
|
||||
mod api_handler;
|
||||
mod boilerplate;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/// This contains the bulk of the rosenpass server IO handling code whereas
|
||||
/// the actual cryptographic code lives in the [crate::protocol] module
|
||||
use anyhow::bail;
|
||||
|
||||
use anyhow::Context;
|
||||
@@ -49,33 +51,78 @@ use crate::{
|
||||
use rosenpass_util::attempt;
|
||||
use rosenpass_util::b64::B64Display;
|
||||
|
||||
const MAX_B64_KEY_SIZE: usize = 32 * 5 / 3;
|
||||
const MAX_B64_PEER_ID_SIZE: usize = 32 * 5 / 3;
|
||||
/// The maximum size of a base64 encoded symmetric key (estimate)
|
||||
pub const MAX_B64_KEY_SIZE: usize = 32 * 5 / 3;
|
||||
/// The maximum size of a base64 peer ID (estimate)
|
||||
pub const MAX_B64_PEER_ID_SIZE: usize = 32 * 5 / 3;
|
||||
|
||||
/// The zero IPv4 address; this is generally used to tell network servers to choose any interface
|
||||
/// when listening
|
||||
const IPV4_ANY_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
|
||||
/// The zero IPv6 address; this is generally used to tell network servers to choose any interface
|
||||
/// when listening
|
||||
const IPV6_ANY_ADDR: Ipv6Addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
/// Ratio of blocking epoll(7) polls to non-blocking polls at which the rosenpass server
|
||||
/// assumes it is under load (i.e. a DOS attack may be happening)
|
||||
const UNDER_LOAD_RATIO: f64 = 0.5;
|
||||
/// Period at which the DOS detection code updates whether there is an "under load" status
|
||||
const DURATION_UPDATE_UNDER_LOAD_STATUS: Duration = Duration::from_millis(500);
|
||||
|
||||
const BROKER_ID_BYTES: usize = 8;
|
||||
pub const BROKER_ID_BYTES: usize = 8;
|
||||
|
||||
fn ipv4_any_binding() -> SocketAddr {
|
||||
/// IPv4 address that tells the network layer to listen on any interface
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [AppServer::new].
|
||||
pub fn ipv4_any_binding() -> SocketAddr {
|
||||
// addr, port
|
||||
SocketAddr::V4(SocketAddrV4::new(IPV4_ANY_ADDR, 0))
|
||||
}
|
||||
|
||||
fn ipv6_any_binding() -> SocketAddr {
|
||||
/// IPv6 address that tells the network layer to listen on any interface
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [AppServer::new].
|
||||
pub fn ipv6_any_binding() -> SocketAddr {
|
||||
// addr, port, flowinfo, scope_id
|
||||
SocketAddr::V6(SocketAddrV6::new(IPV6_ANY_ADDR, 0, 0, 0))
|
||||
}
|
||||
|
||||
/// This is used to assign indices to MIO (epoll) event sources
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MioTokenDispenser {
|
||||
counter: usize,
|
||||
pub counter: usize,
|
||||
}
|
||||
|
||||
impl MioTokenDispenser {
|
||||
/// Produces a single IO event source ID
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Use is quite straightforward:
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass::app_server::MioTokenDispenser;
|
||||
/// use mio::Token;
|
||||
///
|
||||
/// let mut dispenser = MioTokenDispenser {
|
||||
/// counter: 0
|
||||
/// };
|
||||
///
|
||||
/// let t1 = dispenser.dispense();
|
||||
/// let t2 = dispenser.dispense();
|
||||
///
|
||||
/// assert_ne!(t1, t2);
|
||||
///
|
||||
/// // If you inspected the output, you would find that the dispenser is really just a counter.
|
||||
/// // Though this is an implementation detail
|
||||
/// assert_eq!(t1, Token(0));
|
||||
/// assert_eq!(t2, Token(1));
|
||||
/// ```
|
||||
///
|
||||
pub fn dispense(&mut self) -> Token {
|
||||
let r = self.counter;
|
||||
self.counter += 1;
|
||||
@@ -83,42 +130,132 @@ impl MioTokenDispenser {
|
||||
}
|
||||
}
|
||||
|
||||
/// List of WireGuard brokers
|
||||
///
|
||||
/// Each WireGuard peer ([AppPeer]) is assigned a broker ([AppPeer::broker_peer]).
|
||||
///
|
||||
/// When a new has been exchanged, then its associated broker is called to transmit the key
|
||||
/// to WireGuard (or to do something else).
|
||||
///
|
||||
/// Brokers live in [AppServer::brokers]. They are added/removed from the AppServer via
|
||||
/// [AppServer::register_broker] and [AppServer::unregister_broker]. PSKs are distributed
|
||||
/// to their respective brokers via [AppPeerPtr::set_psk].
|
||||
///
|
||||
/// Note that the entire broker system is an experimental feature; it is not up to the
|
||||
/// same quality standards as the rest of the code. In particular, the use of [BROKER_ID_BYTES]
|
||||
/// and a hash map is simultaneously overengineered and not really that useful for what it is
|
||||
/// doing.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BrokerStore {
|
||||
/// The collection of WireGuard brokers. See [Self].
|
||||
pub store: HashMap<
|
||||
Public<BROKER_ID_BYTES>,
|
||||
Box<dyn WireguardBrokerMio<Error = anyhow::Error, MioError = anyhow::Error>>,
|
||||
>,
|
||||
}
|
||||
|
||||
/// Reference to a broker imbued with utility methods
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrokerStorePtr(pub Public<BROKER_ID_BYTES>);
|
||||
|
||||
/// This is the broker configuration for a particular broker peer
|
||||
#[derive(Debug)]
|
||||
pub struct BrokerPeer {
|
||||
/// Reference to the broker used for this particular peer
|
||||
ptr: BrokerStorePtr,
|
||||
/// Configuration for a WireGuard broker.
|
||||
///
|
||||
/// This is woefully overengineered and there is very little reason why the broker
|
||||
/// configuration should not live in the particular WireGuard broker.
|
||||
peer_cfg: Box<dyn WireguardBrokerCfg>,
|
||||
}
|
||||
|
||||
impl BrokerPeer {
|
||||
/// Create a broker peer
|
||||
pub fn new(ptr: BrokerStorePtr, peer_cfg: Box<dyn WireguardBrokerCfg>) -> Self {
|
||||
Self { ptr, peer_cfg }
|
||||
}
|
||||
|
||||
/// Retrieve the pointer to WireGuard PSK broker used with this peer
|
||||
pub fn ptr(&self) -> &BrokerStorePtr {
|
||||
&self.ptr
|
||||
}
|
||||
}
|
||||
|
||||
/// IO/BusinessLogic information for a particular protocol peer.
|
||||
///
|
||||
/// There is a one-to-one correspondence between this struct and [crate::protocol::Peer];
|
||||
/// whereas the struct in the protocol module stores information specific to the cryptographic layer,
|
||||
/// this struct stores information IO information.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct AppPeer {
|
||||
/// If set, then [AppServer::output_key] will write generated output keys
|
||||
/// to a file configured here and produce information on standard out to
|
||||
/// notify the calling process that
|
||||
pub outfile: Option<PathBuf>,
|
||||
/// If this option is set, then [AppServer::output_key] will send generated output
|
||||
/// keys to the broker configured here
|
||||
pub broker_peer: Option<BrokerPeer>,
|
||||
/// This is the network address configured for a particular peer at program start.
|
||||
///
|
||||
/// I.e. this is the address the rosenpass program will send [crate::msgs::InitHello]
|
||||
/// packets to, trying to exchange a key.
|
||||
///
|
||||
/// Note that the remote peer may connect with another address. See [Self::current_endpoint].
|
||||
pub initial_endpoint: Option<Endpoint>,
|
||||
/// The network address currently used for a particular peer.
|
||||
///
|
||||
/// This is not necessarily the address that was configured at program start (see [Self::initial_endpoint]),
|
||||
/// because the remote peer can initiate handshakes from an arbitrary network address.
|
||||
///
|
||||
/// If another peer successfully connects to this one from any address, then this field will
|
||||
/// be updated to reflect which address this was.
|
||||
pub current_endpoint: Option<Endpoint>,
|
||||
}
|
||||
|
||||
impl AppPeer {
|
||||
/// Retrieve the [Endpoint] associated with this peer.
|
||||
///
|
||||
/// I.e. the [Self::current_endpoint] if set and [Self::initial_endpoint] otherwise.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass::app_server::{Endpoint, AppPeer};
|
||||
/// use rosenpass_util::functional::run;
|
||||
///
|
||||
/// let mut peer = AppPeer {
|
||||
/// outfile: None,
|
||||
/// broker_peer: None,
|
||||
/// initial_endpoint: Some(Endpoint::discovery_from_hostname("0.0.0.0:0".to_string())?),
|
||||
/// current_endpoint: Some(Endpoint::discovery_from_hostname("0.0.0.0:1".to_string())?),
|
||||
/// };
|
||||
///
|
||||
/// fn same(a: Option<&Endpoint>, b: Option<&Endpoint>) -> bool {
|
||||
/// if a.is_none() && b.is_none() {
|
||||
/// return true;
|
||||
/// }
|
||||
///
|
||||
/// run(|| Some(std::ptr::eq(a?, b?)) )
|
||||
/// .unwrap_or(a.is_some() == b.is_some())
|
||||
/// }
|
||||
///
|
||||
/// assert!(same(peer.endpoint(), peer.current_endpoint.as_ref()));
|
||||
///
|
||||
/// let mut tmp = None;
|
||||
/// std::mem::swap(&mut tmp, &mut peer.initial_endpoint);
|
||||
/// assert!(same(peer.endpoint(), peer.current_endpoint.as_ref()));
|
||||
///
|
||||
/// std::mem::swap(&mut tmp, &mut peer.initial_endpoint);
|
||||
/// std::mem::swap(&mut tmp, &mut peer.current_endpoint);
|
||||
/// assert!(same(peer.endpoint(), peer.initial_endpoint.as_ref()));
|
||||
///
|
||||
/// peer.initial_endpoint = None;
|
||||
/// peer.current_endpoint = None;
|
||||
/// assert!(peer.endpoint().is_none());
|
||||
///
|
||||
/// Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
pub fn endpoint(&self) -> Option<&Endpoint> {
|
||||
self.current_endpoint
|
||||
.as_ref()
|
||||
@@ -126,6 +263,8 @@ impl AppPeer {
|
||||
}
|
||||
}
|
||||
|
||||
/// No longer in use since we have the broker system (see [BrokerPeer])
|
||||
/// TODO: Remove
|
||||
#[derive(Default, Debug)]
|
||||
pub struct WireguardOut {
|
||||
// impl KeyOutput
|
||||
@@ -134,12 +273,20 @@ pub struct WireguardOut {
|
||||
pub extra_params: Vec<String>,
|
||||
}
|
||||
|
||||
/// Used to indicate whether the rosenpass server is in normal operating
|
||||
/// conditions or under load (i.e. a DOS attack is happening)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum DoSOperation {
|
||||
UnderLoad,
|
||||
Normal,
|
||||
}
|
||||
/// Integration test helpers for AppServer
|
||||
///
|
||||
/// TODO: Remove; this is no way to write integration tests
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [AppServer]
|
||||
#[derive(Debug, Builder)]
|
||||
#[builder(pattern = "owned")]
|
||||
pub struct AppServerTest {
|
||||
@@ -151,41 +298,100 @@ pub struct AppServerTest {
|
||||
pub termination_handler: Option<std::sync::mpsc::Receiver<()>>,
|
||||
}
|
||||
|
||||
/// This represents a some source of IO operations in the context of the Rosenpass server
|
||||
///
|
||||
/// I.e. this identifies some structure that could be marked as "ready for IO" by [mio]
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum AppServerIoSource {
|
||||
/// IO source refers to a socket in [AppServer::sockets]
|
||||
Socket(usize),
|
||||
/// IO source refers to a PSK broker in [AppServer::brokers]
|
||||
PskBroker(Public<BROKER_ID_BYTES>),
|
||||
/// IO source refers to some IO sources used in the API;
|
||||
/// see [AppServer::api_manager]
|
||||
#[cfg(feature = "experiment_api")]
|
||||
MioManager(crate::api::mio::MioManagerIoSource),
|
||||
}
|
||||
|
||||
/// Number of epoll(7) events Rosenpass can receive at a time
|
||||
const EVENT_CAPACITY: usize = 20;
|
||||
|
||||
/// Holds the state of the application, namely the external IO
|
||||
/// This holds pretty much all of the state of the Rosenpass application
|
||||
/// including the cryptographic state in [Self::crypto_site]
|
||||
///
|
||||
/// Responsible for file IO, network IO
|
||||
// TODO add user control via unix domain socket and stdin/stdout
|
||||
/// Responsible for file IO, network IO, etc…
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
#[doc = "```ignore"]
|
||||
#[doc = include_str!("../tests/app_server_example.rs")]
|
||||
#[doc = "```"]
|
||||
#[derive(Debug)]
|
||||
pub struct AppServer {
|
||||
/// Contains the actual cryptographic implementation.
|
||||
///
|
||||
/// Because the API supports initializing the server with a keypair
|
||||
/// and CryptoServer needs to be initialized with a keypair, the struct
|
||||
/// struct is wrapped in a ConstructionSite
|
||||
pub crypto_site: ConstructionSite<BuildCryptoServer, CryptoServer>,
|
||||
/// The UDP sockets used to send and receive protocol messages
|
||||
pub sockets: Vec<mio::net::UdpSocket>,
|
||||
/// Buffer for [mio] (epoll(7), async IO handling) IO events
|
||||
pub events: mio::Events,
|
||||
/// Supplemental buffer for [mio] events. See the inline documentation of [AppServer::try_recv]
|
||||
/// for details.
|
||||
pub short_poll_queue: VecDeque<mio::event::Event>,
|
||||
/// We have two different polling modes; long polling (legacy) and short polling (new and
|
||||
/// somewhat experimental). See [AppServer::try_recv] for details
|
||||
pub performed_long_poll: bool,
|
||||
/// Events produced by [mio] refer to IO sources by a numeric token assigned to them
|
||||
/// (see [MioTokenDispenser]). This index associateds mio token with the specific IO
|
||||
/// source stored in this object
|
||||
pub io_source_index: HashMap<mio::Token, AppServerIoSource>,
|
||||
/// Asynchronous IO source
|
||||
pub mio_poll: mio::Poll,
|
||||
/// MIO associates IO sources with numeric tokens. This struct takes care of generating these
|
||||
/// tokens
|
||||
pub mio_token_dispenser: MioTokenDispenser,
|
||||
/// Helpers handling communication with WireGuard; these take a generated key and forward it to
|
||||
/// WireGuard
|
||||
pub brokers: BrokerStore,
|
||||
/// This is our view of the peers; generally every peer in here is associated with one peer in
|
||||
/// CryptoServer
|
||||
pub peers: Vec<AppPeer>,
|
||||
/// If set to [Verbosity::Verbose], then some extra information will be printed
|
||||
/// at the info log level
|
||||
pub verbosity: Verbosity,
|
||||
/// Used by [AppServer::try_recv] to ensure that all packages have been read
|
||||
/// from the UDP sockets
|
||||
pub all_sockets_drained: bool,
|
||||
/// Whether network message handling determined that a Denial of Service attack is happening
|
||||
pub under_load: DoSOperation,
|
||||
/// State kept by the [AppServer::try_recv] for polling
|
||||
pub blocking_polls_count: usize,
|
||||
/// State kept by the [AppServer::try_recv] for polling
|
||||
pub non_blocking_polls_count: usize,
|
||||
/// State kept by the [AppServer::try_recv] for polling
|
||||
pub unpolled_count: usize,
|
||||
/// State kept by the [AppServer::try_recv] for polling
|
||||
pub last_update_time: Instant,
|
||||
/// Used by integration tests to force [Self] into DoS condition
|
||||
/// and to terminate the AppServer after the test is complete
|
||||
pub test_helpers: Option<AppServerTest>,
|
||||
/// Helper for integration tests running rosenpass as a subprocess
|
||||
/// to terminate properly upon receiving an appropriate system signal.
|
||||
///
|
||||
/// This is primarily needed for coverage testing, since llvm-cov does not
|
||||
/// write coverage reports to disk when a process is stopped by the default
|
||||
/// signal handler.
|
||||
///
|
||||
/// See <https://github.com/rosenpass/rosenpass/issues/385>
|
||||
#[cfg(feature = "internal_signal_handling_for_coverage_reports")]
|
||||
pub term_signal: terminate::TerminateRequested,
|
||||
#[cfg(feature = "experiment_api")]
|
||||
/// The Rosenpass unix socket API handler; this is an experimental
|
||||
/// feature that can be used to embed Rosenpass in external applications
|
||||
/// via communication by unix socket
|
||||
pub api_manager: crate::api::mio::MioManager,
|
||||
}
|
||||
|
||||
@@ -199,14 +405,19 @@ pub struct AppServer {
|
||||
pub struct SocketPtr(pub usize);
|
||||
|
||||
impl SocketPtr {
|
||||
/// Retrieve the concrete udp socket associated with the pointer
|
||||
pub fn get<'a>(&self, srv: &'a AppServer) -> &'a mio::net::UdpSocket {
|
||||
&srv.sockets[self.0]
|
||||
}
|
||||
|
||||
/// Retrieve the concrete udp socket associated with the pointer, mutably
|
||||
pub fn get_mut<'a>(&self, srv: &'a mut AppServer) -> &'a mut mio::net::UdpSocket {
|
||||
&mut srv.sockets[self.0]
|
||||
}
|
||||
|
||||
/// Send a UDP packet to another address.
|
||||
///
|
||||
/// Merely forwards to [mio::net::UdpSocket::send_to]
|
||||
pub fn send_to(&self, srv: &AppServer, buf: &[u8], addr: SocketAddr) -> anyhow::Result<()> {
|
||||
self.get(srv).send_to(buf, addr)?;
|
||||
Ok(())
|
||||
@@ -214,28 +425,41 @@ impl SocketPtr {
|
||||
}
|
||||
|
||||
/// Index based pointer to a Peer
|
||||
///
|
||||
/// This allows retrieving both the io-oriented and the cryptographic information
|
||||
/// about a peer.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct AppPeerPtr(pub usize);
|
||||
|
||||
impl AppPeerPtr {
|
||||
/// Takes an index based handle and returns the actual peer
|
||||
/// Takes an pointer from the cryptography subsystem
|
||||
/// in [AppServer::crypto_site] and derives the associated AppPeerPtr
|
||||
/// in [AppServer]
|
||||
pub fn lift(p: PeerPtr) -> Self {
|
||||
Self(p.0)
|
||||
}
|
||||
|
||||
/// Returns an index based handle to one Peer
|
||||
/// Turns this pointer into a cryptographic peer pointer for [CryptoServer]
|
||||
/// in [AppServer::crypto_site]
|
||||
pub fn lower(&self) -> PeerPtr {
|
||||
PeerPtr(self.0)
|
||||
}
|
||||
|
||||
/// Retrieve the [AppPeer] pointed to by [Self]
|
||||
pub fn get_app<'a>(&self, srv: &'a AppServer) -> &'a AppPeer {
|
||||
&srv.peers[self.0]
|
||||
}
|
||||
|
||||
/// Retrieve the [AppPeer] pointed to by [Self], mutably
|
||||
pub fn get_app_mut<'a>(&self, srv: &'a mut AppServer) -> &'a mut AppPeer {
|
||||
&mut srv.peers[self.0]
|
||||
}
|
||||
|
||||
/// Use the associated WireGuard PSK broker via [BrokerStorePtr]
|
||||
/// to upload a new PSK.
|
||||
///
|
||||
/// If no PSK broker is set and [AppPeer::outfile] is none, then
|
||||
/// this prints a warning
|
||||
pub fn set_psk(&self, server: &mut AppServer, psk: &Secret<WG_KEY_LEN>) -> anyhow::Result<()> {
|
||||
if let Some(broker) = server.peers[self.0].broker_peer.as_ref() {
|
||||
let config = broker.peer_cfg.create_config(psk);
|
||||
@@ -248,17 +472,34 @@ impl AppPeerPtr {
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of [AppServer::poll].
|
||||
///
|
||||
/// Instructs [AppServer::event_loop_without_error_handling] on how to proceed.
|
||||
#[derive(Debug)]
|
||||
pub enum AppPollResult {
|
||||
/// Erase the key for a given peer. Corresponds to [crate::protocol::PollResult::DeleteKey]
|
||||
DeleteKey(AppPeerPtr),
|
||||
/// Send an initiation to the given peer. Corresponds to [crate::protocol::PollResult::SendInitiation]
|
||||
SendInitiation(AppPeerPtr),
|
||||
/// Send a retransmission to the given peer. Corresponds to
|
||||
/// [crate::protocol::PollResult::SendRetransmission]
|
||||
SendRetransmission(AppPeerPtr),
|
||||
/// Received a network message.
|
||||
///
|
||||
/// This is the only case without a correspondence in [crate::protocol::PollResult]
|
||||
ReceivedMessage(usize, Endpoint),
|
||||
}
|
||||
|
||||
/// The reason why we are outputting a key
|
||||
#[derive(Debug)]
|
||||
pub enum KeyOutputReason {
|
||||
/// The reason is that a new key for the given peer was successfully exchanged
|
||||
Exchanged,
|
||||
/// The reason is, that no key could be exchanged with the peer before the output
|
||||
/// key lifetime was reached; a [AppPollResult::DeleteKey] event was issued.
|
||||
///
|
||||
/// The key we output in this case is chosen randomly and serves to securely
|
||||
/// erase whatever key is currently stored.
|
||||
Stale,
|
||||
}
|
||||
|
||||
@@ -297,14 +538,21 @@ impl std::fmt::Display for Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
/// A network address bound to a particular socket.
|
||||
///
|
||||
/// We need the information which socket is used because different listen sockets
|
||||
/// might be on different networks.
|
||||
#[derive(Debug)]
|
||||
pub struct SocketBoundEndpoint {
|
||||
/// The socket the address can be reached under; this is generally
|
||||
/// determined when we actually receive an RespHello message
|
||||
socket: SocketPtr,
|
||||
/// Just the address
|
||||
/// The network address
|
||||
addr: SocketAddr,
|
||||
/// identifier
|
||||
/// Byte representation of this socket bound network address.
|
||||
/// Generated through [SocketBoundEndpoint::to_bytes].
|
||||
///
|
||||
/// Read through [HostIdentification::encode]
|
||||
bytes: (usize, [u8; SocketBoundEndpoint::BUFFER_SIZE]),
|
||||
}
|
||||
|
||||
@@ -315,16 +563,22 @@ impl std::fmt::Display for SocketBoundEndpoint {
|
||||
}
|
||||
|
||||
impl SocketBoundEndpoint {
|
||||
/// Length in bytes of the serialized socket index
|
||||
const SOCKET_SIZE: usize = usize::BITS as usize / 8;
|
||||
/// Length in bytes of the serialized ipv6 address
|
||||
const IPV6_SIZE: usize = 16;
|
||||
/// Length in bytes of the serialized port
|
||||
const PORT_SIZE: usize = 2;
|
||||
/// Length in bytes of the serialized ipv6 address scope (see [SocketAddrV6::scope_id])
|
||||
const SCOPE_ID_SIZE: usize = 4;
|
||||
|
||||
/// Length in size of
|
||||
const BUFFER_SIZE: usize = SocketBoundEndpoint::SOCKET_SIZE
|
||||
+ SocketBoundEndpoint::IPV6_SIZE
|
||||
+ SocketBoundEndpoint::PORT_SIZE
|
||||
+ SocketBoundEndpoint::SCOPE_ID_SIZE;
|
||||
|
||||
/// Produce a new [Self]
|
||||
pub fn new(socket: SocketPtr, addr: SocketAddr) -> Self {
|
||||
let bytes = Self::to_bytes(&socket, &addr);
|
||||
Self {
|
||||
@@ -334,6 +588,7 @@ impl SocketBoundEndpoint {
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes [HostIdentification::encode] for [Self]. Value cached in [Self::bytes].
|
||||
fn to_bytes(
|
||||
socket: &SocketPtr,
|
||||
addr: &SocketAddr,
|
||||
@@ -368,12 +623,18 @@ impl HostIdentification for SocketBoundEndpoint {
|
||||
}
|
||||
|
||||
impl Endpoint {
|
||||
/// Start discovery from some addresses
|
||||
/// Given a list of potential network addresses, start peer discovery.
|
||||
///
|
||||
/// Will send initiations to different addresses given here on each [crate::msgs::InitHello]
|
||||
/// retransmission during the peer discovery phase.
|
||||
pub fn discovery_from_addresses(addresses: Vec<SocketAddr>) -> Self {
|
||||
Endpoint::Discovery(HostPathDiscoveryEndpoint::from_addresses(addresses))
|
||||
}
|
||||
|
||||
/// Start endpoint discovery from a hostname
|
||||
/// Given a hostname, start peer discovery.
|
||||
///
|
||||
/// Will send initiations to different addresses assigned to the host name
|
||||
/// on each [crate::msgs::InitHello] retransmission during the peer discovery phase.
|
||||
pub fn discovery_from_hostname(hostname: String) -> anyhow::Result<Self> {
|
||||
let host = HostPathDiscoveryEndpoint::lookup(hostname)?;
|
||||
Ok(Endpoint::Discovery(host))
|
||||
@@ -404,6 +665,8 @@ impl Endpoint {
|
||||
Some(Self::discovery_from_addresses(addrs))
|
||||
}
|
||||
|
||||
/// Send a message to the address referenced by this endpoint or to one of
|
||||
/// the endpoints if we are in the peer discovery phase for this endpoint
|
||||
pub fn send(&self, srv: &AppServer, buf: &[u8]) -> anyhow::Result<()> {
|
||||
use Endpoint::*;
|
||||
match self {
|
||||
@@ -412,6 +675,9 @@ impl Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
/// List of addresses this endpoint may be associated with.
|
||||
///
|
||||
/// During peer discovery, this can be multiple addresses.
|
||||
fn addresses(&self) -> &[SocketAddr] {
|
||||
use Endpoint::*;
|
||||
match self {
|
||||
@@ -449,7 +715,14 @@ impl Endpoint {
|
||||
// TODO: We might consider adjusting the retransmission handling to account for host-path discovery
|
||||
#[derive(Debug)]
|
||||
pub struct HostPathDiscoveryEndpoint {
|
||||
scouting_state: Cell<(usize, usize)>, // addr_off, sock_off
|
||||
/// Round robin index the next [Self::send_scouting] call should send packets to
|
||||
///
|
||||
/// (address offset, socket offset)
|
||||
///
|
||||
/// Including the socket here accounts for the fact that some network addresses may be
|
||||
/// reachable only through particular UDP sockets
|
||||
scouting_state: Cell<(usize, usize)>,
|
||||
/// List of addresses fir oeer discovery
|
||||
addresses: Vec<SocketAddr>,
|
||||
}
|
||||
|
||||
@@ -460,6 +733,7 @@ impl std::fmt::Display for HostPathDiscoveryEndpoint {
|
||||
}
|
||||
|
||||
impl HostPathDiscoveryEndpoint {
|
||||
/// Initiate a peer discovery process through a list of potential addresses
|
||||
pub fn from_addresses(addresses: Vec<SocketAddr>) -> Self {
|
||||
let scouting_state = Cell::new((0, 0));
|
||||
Self {
|
||||
@@ -468,7 +742,7 @@ impl HostPathDiscoveryEndpoint {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lookup a hostname
|
||||
/// Initiate a peer discovery process through hostname lookup
|
||||
pub fn lookup(hostname: String) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
addresses: ToSocketAddrs::to_socket_addrs(&hostname)?.collect(),
|
||||
@@ -476,10 +750,14 @@ impl HostPathDiscoveryEndpoint {
|
||||
})
|
||||
}
|
||||
|
||||
/// List of address candidates for the peer
|
||||
pub fn addresses(&self) -> &Vec<SocketAddr> {
|
||||
&self.addresses
|
||||
}
|
||||
|
||||
/// Calculates and stores the next value for [Self::scouting_state]
|
||||
/// given the address and socket we just sent a scouting [crate::msgs::InitHello] message
|
||||
/// to
|
||||
fn insert_next_scout_offset(&self, srv: &AppServer, addr_no: usize, sock_no: usize) {
|
||||
self.scouting_state.set((
|
||||
(addr_no + 1) % self.addresses.len(),
|
||||
@@ -534,6 +812,11 @@ impl HostPathDiscoveryEndpoint {
|
||||
}
|
||||
|
||||
impl AppServer {
|
||||
/// Construct a new AppServer
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [Self].
|
||||
pub fn new(
|
||||
keypair: Option<(SSk, SPk)>,
|
||||
addrs: Vec<SocketAddr>,
|
||||
@@ -633,6 +916,8 @@ impl AppServer {
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
#[cfg(feature = "internal_signal_handling_for_coverage_reports")]
|
||||
term_signal: terminate::TerminateRequested::new()?,
|
||||
crypto_site,
|
||||
peers: Vec::new(),
|
||||
verbosity,
|
||||
@@ -656,22 +941,37 @@ impl AppServer {
|
||||
})
|
||||
}
|
||||
|
||||
/// Access the cryptographic protocol server
|
||||
///
|
||||
/// This may return an error if [Self] was initialized without a keypair
|
||||
/// and no keypair has been supplied since then.
|
||||
///
|
||||
/// I.e. will return an error if [Self::crypto_site] is not fully initialized
|
||||
pub fn crypto_server(&self) -> anyhow::Result<&CryptoServer> {
|
||||
self.crypto_site
|
||||
.product_ref()
|
||||
.context("Cryptography handler not initialized")
|
||||
}
|
||||
|
||||
/// Access the cryptographic protocol server, mutably
|
||||
///
|
||||
/// This may return an error if [Self] was initialized without a keypair
|
||||
/// and no keypair has been supplied since then.
|
||||
///
|
||||
/// I.e. will return an error if [Self::crypto_site] is not fully initialized
|
||||
pub fn crypto_server_mut(&mut self) -> anyhow::Result<&mut CryptoServer> {
|
||||
self.crypto_site
|
||||
.product_mut()
|
||||
.context("Cryptography handler not initialized")
|
||||
}
|
||||
|
||||
/// If set to [Verbosity::Verbose], then some extra information will be printed
|
||||
/// at the info log level
|
||||
pub fn verbose(&self) -> bool {
|
||||
matches!(self.verbosity, Verbosity::Verbose)
|
||||
}
|
||||
|
||||
/// Used by [Self::new] to register a new udp listen source
|
||||
pub fn register_listen_socket(&mut self, mut sock: mio::net::UdpSocket) -> anyhow::Result<()> {
|
||||
let mio_token = self.mio_token_dispenser.dispense();
|
||||
self.mio_poll
|
||||
@@ -683,16 +983,19 @@ impl AppServer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Used to register a source of IO such as a listen socket with [Self::io_source_index]
|
||||
pub fn register_io_source(&mut self, token: mio::Token, io_source: AppServerIoSource) {
|
||||
let prev = self.io_source_index.insert(token, io_source);
|
||||
assert!(prev.is_none());
|
||||
}
|
||||
|
||||
/// Unregister an IO source registered with [Self::register_io_source]
|
||||
pub fn unregister_io_source(&mut self, token: mio::Token) {
|
||||
let value = self.io_source_index.remove(&token);
|
||||
assert!(value.is_some(), "Removed IO source that does not exist");
|
||||
}
|
||||
|
||||
/// Register a new WireGuard PSK broker
|
||||
pub fn register_broker(
|
||||
&mut self,
|
||||
broker: Box<dyn WireguardBrokerMio<Error = anyhow::Error, MioError = anyhow::Error>>,
|
||||
@@ -715,6 +1018,7 @@ impl AppServer {
|
||||
Ok(BrokerStorePtr(ptr))
|
||||
}
|
||||
|
||||
/// Unregister a WireGuard PSK broker registered with [Self::register_broker]
|
||||
pub fn unregister_broker(&mut self, ptr: BrokerStorePtr) -> Result<()> {
|
||||
let mut broker = self
|
||||
.brokers
|
||||
@@ -726,6 +1030,11 @@ impl AppServer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register a new protocol peer
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [Self::new].
|
||||
pub fn add_peer(
|
||||
&mut self,
|
||||
psk: Option<SymKey>,
|
||||
@@ -754,18 +1063,40 @@ impl AppServer {
|
||||
Ok(AppPeerPtr(pn))
|
||||
}
|
||||
|
||||
pub fn listen_loop(&mut self) -> anyhow::Result<()> {
|
||||
/// Main IO handler; this generally does not terminate
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [Self::new].
|
||||
pub fn event_loop(&mut self) -> anyhow::Result<()> {
|
||||
const INIT_SLEEP: f64 = 0.01;
|
||||
const MAX_FAILURES: i32 = 10;
|
||||
let mut failure_cnt = 0;
|
||||
|
||||
loop {
|
||||
let msgs_processed = 0usize;
|
||||
let err = match self.event_loop() {
|
||||
let err = match self.event_loop_without_error_handling() {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => e,
|
||||
};
|
||||
|
||||
#[cfg(feature = "internal_signal_handling_for_coverage_reports")]
|
||||
{
|
||||
let terminated_by_signal = err
|
||||
.downcast_ref::<std::io::Error>()
|
||||
.filter(|e| e.kind() == std::io::ErrorKind::Interrupted)
|
||||
.filter(|_| self.term_signal.value())
|
||||
.is_some();
|
||||
if terminated_by_signal {
|
||||
log::warn!(
|
||||
"\
|
||||
Terminated by signal; this signal handler is correct during coverage testing \
|
||||
but should be otherwise disabled"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// This should not happen…
|
||||
failure_cnt = if msgs_processed > 0 {
|
||||
0
|
||||
@@ -790,7 +1121,10 @@ impl AppServer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn event_loop(&mut self) -> anyhow::Result<()> {
|
||||
/// IO handler without proactive restarts after errors.
|
||||
///
|
||||
/// This is used internally in [Self::event_loop].
|
||||
pub fn event_loop_without_error_handling(&mut self) -> anyhow::Result<()> {
|
||||
let (mut rx, mut tx) = (MsgBuf::zero(), MsgBuf::zero());
|
||||
|
||||
/// if socket address for peer is known, call closure
|
||||
@@ -909,6 +1243,8 @@ impl AppServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper for [Self::event_loop_without_error_handling] to handle network messages
|
||||
/// under DoS condition
|
||||
fn handle_msg_under_load(
|
||||
&mut self,
|
||||
endpoint: &Endpoint,
|
||||
@@ -925,6 +1261,8 @@ impl AppServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Used as a helper by [Self::event_loop_without_error_handling] when
|
||||
/// a new output key has been echanged
|
||||
pub fn output_key(
|
||||
&mut self,
|
||||
peer: AppPeerPtr,
|
||||
@@ -974,6 +1312,10 @@ impl AppServer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Poll for events from the cryptographic server ([Self::crypto_server()])
|
||||
/// and for IO events through [Self::poll].
|
||||
///
|
||||
/// Used internally in [Self::event_loop_without_error_handling]
|
||||
pub fn poll(&mut self, rx_buf: &mut [u8]) -> anyhow::Result<AppPollResult> {
|
||||
use crate::protocol::PollResult as C;
|
||||
use AppPollResult as A;
|
||||
@@ -1005,7 +1347,9 @@ impl AppServer {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Tries to receive a new message
|
||||
/// Tries to receive a new message from the network sockets.
|
||||
///
|
||||
/// Used internally in [Self::poll]
|
||||
///
|
||||
/// - might wait for an duration up to `timeout`
|
||||
/// - returns immediately if an error occurs
|
||||
@@ -1177,6 +1521,7 @@ impl AppServer {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Internal helper for [Self::try_recv]
|
||||
fn perform_mio_poll_and_register_events(&mut self, timeout: Duration) -> io::Result<()> {
|
||||
self.mio_poll.poll(&mut self.events, Some(timeout))?;
|
||||
// Fill the short poll buffer with the acquired events
|
||||
@@ -1187,6 +1532,7 @@ impl AppServer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Internal helper for [Self::try_recv]
|
||||
fn try_recv_from_mio_token(
|
||||
&mut self,
|
||||
buf: &mut [u8],
|
||||
@@ -1203,6 +1549,7 @@ impl AppServer {
|
||||
self.try_recv_from_io_source(buf, io_source)
|
||||
}
|
||||
|
||||
/// Internal helper for [Self::try_recv]
|
||||
fn try_recv_from_io_source(
|
||||
&mut self,
|
||||
buf: &mut [u8],
|
||||
@@ -1233,6 +1580,7 @@ impl AppServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper for [Self::try_recv]
|
||||
fn try_recv_from_listen_socket(
|
||||
&mut self,
|
||||
buf: &mut [u8],
|
||||
@@ -1268,6 +1616,20 @@ impl AppServer {
|
||||
}
|
||||
|
||||
#[cfg(feature = "experiment_api")]
|
||||
/// This is a wrapper around a reference to [AppServer] that
|
||||
/// dishes out references to [AppServer::api_manager] and
|
||||
/// to [AppServer] itself.
|
||||
///
|
||||
/// It really just implements [crate::api::mio::MioManagerContext] which
|
||||
/// provides the methods operating on [crate::api::mio::MioManager] provided
|
||||
/// through a trait.
|
||||
///
|
||||
/// This is a rather complicated way of providing just a few functions. The entire
|
||||
/// point of this exercise is to decouple the code in the API from [AppServer] and
|
||||
/// this file a bit, despite those functions all needing access to [AppServer].
|
||||
///
|
||||
/// We want the code to live in its own module instead of expanding and expanding the source
|
||||
/// file with [AppServer] more and more.
|
||||
struct MioManagerFocus<'a>(&'a mut AppServer);
|
||||
|
||||
#[cfg(feature = "experiment_api")]
|
||||
@@ -1288,3 +1650,48 @@ impl crate::api::mio::MioManagerContext for MioManagerFocus<'_> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// These signal handlers are used exclusively used during coverage testing
|
||||
/// to ensure that the llvm-cov can produce reports during integration tests
|
||||
/// with multiple processes where subprocesses are terminated via kill(2).
|
||||
///
|
||||
/// llvm-cov does not support producing coverage reports when the process exits
|
||||
/// through a signal, so this is necessary.
|
||||
///
|
||||
/// The functionality of exiting gracefully upon reception of a terminating signal
|
||||
/// is desired for the production variant of Rosenpass, but we should make sure
|
||||
/// to use a higher quality implementation; in particular, we should use signalfd(2).
|
||||
///
|
||||
#[cfg(feature = "internal_signal_handling_for_coverage_reports")]
|
||||
mod terminate {
|
||||
use signal_hook::flag::register as sig_register;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
/// Automatically register a signal handler for common termination signals;
|
||||
/// whether one of these signals was issued can be polled using [Self::value].
|
||||
///
|
||||
/// The signal handler is not removed when this struct goes out of scope.
|
||||
#[derive(Debug)]
|
||||
pub struct TerminateRequested {
|
||||
value: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl TerminateRequested {
|
||||
/// Register signal handlers watching for common termination signals
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
let value = Arc::new(AtomicBool::new(false));
|
||||
for sig in signal_hook::consts::TERM_SIGNALS.iter().copied() {
|
||||
sig_register(sig, Arc::clone(&value))?;
|
||||
}
|
||||
Ok(Self { value })
|
||||
}
|
||||
|
||||
/// Check whether a termination signal has been set
|
||||
pub fn value(&self) -> bool {
|
||||
self.value.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use heck::ToShoutySnakeCase;
|
||||
|
||||
use rosenpass_ciphers::{hash_domain::HashDomain, KEY_LEN};
|
||||
|
||||
/// Recursively calculate a concrete hash value for an API message type
|
||||
fn calculate_hash_value(hd: HashDomain, values: &[&str]) -> Result<[u8; KEY_LEN]> {
|
||||
match values.split_first() {
|
||||
Some((head, tail)) => calculate_hash_value(hd.mix(head.as_bytes())?, tail),
|
||||
@@ -10,6 +11,7 @@ fn calculate_hash_value(hd: HashDomain, values: &[&str]) -> Result<[u8; KEY_LEN]
|
||||
}
|
||||
}
|
||||
|
||||
/// Print a hash literal for pasting into the Rosenpass source code
|
||||
fn print_literal(path: &[&str]) -> Result<()> {
|
||||
let val = calculate_hash_value(HashDomain::zero(), path)?;
|
||||
let (last, prefix) = path.split_last().context("developer error!")?;
|
||||
@@ -33,6 +35,8 @@ fn print_literal(path: &[&str]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tree of domain separators where each leaf represents
|
||||
/// an API message ID
|
||||
#[derive(Debug, Clone)]
|
||||
enum Tree {
|
||||
Branch(String, Vec<Tree>),
|
||||
@@ -68,6 +72,7 @@ impl Tree {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper for generating hash-based message IDs for the IPC API
|
||||
fn main() -> Result<()> {
|
||||
let tree = Tree::Branch(
|
||||
"Rosenpass IPC API".to_owned(),
|
||||
|
||||
@@ -24,8 +24,8 @@ use {
|
||||
rosenpass_util::fd::claim_fd,
|
||||
rosenpass_wireguard_broker::brokers::mio_client::MioBrokerClient,
|
||||
rosenpass_wireguard_broker::WireguardBrokerMio,
|
||||
rustix::fd::AsRawFd,
|
||||
rustix::net::{socketpair, AddressFamily, SocketFlags, SocketType},
|
||||
std::os::fd::AsRawFd,
|
||||
std::os::unix::net,
|
||||
std::process::Command,
|
||||
std::thread,
|
||||
@@ -41,17 +41,17 @@ pub enum BrokerInterface {
|
||||
|
||||
/// struct holding all CLI arguments for `clap` crate to parse
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about)]
|
||||
#[command(author, version, about, long_about, arg_required_else_help = true)]
|
||||
pub struct CliArgs {
|
||||
/// lowest log level to show – log messages at higher levels will be omitted
|
||||
/// Lowest log level to show
|
||||
#[arg(long = "log-level", value_name = "LOG_LEVEL", group = "log-level")]
|
||||
log_level: Option<log::LevelFilter>,
|
||||
|
||||
/// show verbose log output – sets log level to "debug"
|
||||
/// Show verbose log output – sets log level to "debug"
|
||||
#[arg(short, long, group = "log-level")]
|
||||
verbose: bool,
|
||||
|
||||
/// show no log output – sets log level to "error"
|
||||
/// Show no log output – sets log level to "error"
|
||||
#[arg(short, long, group = "log-level")]
|
||||
quiet: bool,
|
||||
|
||||
@@ -59,28 +59,42 @@ pub struct CliArgs {
|
||||
#[cfg(feature = "experiment_api")]
|
||||
api: crate::api::cli::ApiCli,
|
||||
|
||||
/// path of the wireguard_psk broker socket to connect to
|
||||
/// Path of the `wireguard_psk` broker socket to connect to
|
||||
#[cfg(feature = "experiment_api")]
|
||||
#[arg(long, group = "psk-broker-specs")]
|
||||
psk_broker_path: Option<PathBuf>,
|
||||
|
||||
/// fd of the wireguard_spk broker socket to connect to
|
||||
/// File descriptor of the `wireguard_psk` broker socket to connect to
|
||||
///
|
||||
/// when this command is called from another process, the other process can open and bind the
|
||||
/// Unix socket for the psk broker connection to use themselves, passing it to this process --
|
||||
/// in Rust this can be achieved using the
|
||||
/// [command-fds](https://docs.rs/command-fds/latest/command_fds/) crate
|
||||
/// When this command is called from another process, the other process can
|
||||
/// open and bind the Unix socket for the PSK broker connection to use
|
||||
/// themselves, passing it to this process - in Rust this can be achieved
|
||||
/// using the [command-fds](https://docs.rs/command-fds/latest/command_fds/)
|
||||
/// crate
|
||||
#[cfg(feature = "experiment_api")]
|
||||
#[arg(long, group = "psk-broker-specs")]
|
||||
psk_broker_fd: Option<i32>,
|
||||
|
||||
/// spawn a psk broker locally using a socket pair
|
||||
/// Spawn a PSK broker locally using a socket pair
|
||||
#[cfg(feature = "experiment_api")]
|
||||
#[arg(short, long, group = "psk-broker-specs")]
|
||||
psk_broker_spawn: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: CliCommand,
|
||||
pub command: Option<CliCommand>,
|
||||
|
||||
/// Generate man pages for the CLI
|
||||
///
|
||||
/// This option is used to generate man pages for Rosenpass in the specified
|
||||
/// directory and exit.
|
||||
#[clap(long, value_name = "out_dir")]
|
||||
pub generate_manpage: Option<PathBuf>,
|
||||
|
||||
/// Generate completion file for a shell
|
||||
///
|
||||
/// This option is used to generate completion files for the specified shell
|
||||
#[clap(long, value_name = "shell")]
|
||||
pub print_completions: Option<clap_complete::Shell>,
|
||||
}
|
||||
|
||||
impl CliArgs {
|
||||
@@ -135,20 +149,20 @@ impl CliArgs {
|
||||
/// represents a command specified via CLI
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum CliCommand {
|
||||
/// Start Rosenpass in server mode and carry on with the key exchange
|
||||
/// Start Rosenpass key exchanges based on a configuration file
|
||||
///
|
||||
/// This will parse the configuration file and perform the key exchange
|
||||
/// with the specified peers. If a peer's endpoint is specified, this
|
||||
/// Rosenpass instance will try to initiate a key exchange with the peer,
|
||||
/// otherwise only initiation attempts from the peer will be responded to.
|
||||
/// This will parse the configuration file and perform key exchanges with
|
||||
/// the specified peers. If a peer's endpoint is specified, this Rosenpass
|
||||
/// instance will try to initiate a key exchange with the peer; otherwise,
|
||||
/// only initiation attempts from other peers will be responded to.
|
||||
ExchangeConfig { config_file: PathBuf },
|
||||
|
||||
/// Start in daemon mode, performing key exchanges
|
||||
/// Start Rosenpass key exchanges based on command line arguments
|
||||
///
|
||||
/// The configuration is read from the command line. The `peer` token
|
||||
/// always separates multiple peers, e. g. if the token `peer` appears
|
||||
/// in the WIREGUARD_EXTRA_ARGS it is not put into the WireGuard arguments
|
||||
/// but instead a new peer is created.
|
||||
/// The configuration is read from the command line. The `peer` token always
|
||||
/// separates multiple peers, e.g., if the token `peer` appears in the
|
||||
/// WIREGUARD_EXTRA_ARGS, it is not put into the WireGuard arguments but
|
||||
/// instead a new peer is created.
|
||||
/* Explanation: `first_arg` and `rest_of_args` are combined into one
|
||||
* `Vec<String>`. They are only used to trick clap into displaying some
|
||||
* guidance on the CLI usage.
|
||||
@@ -177,7 +191,10 @@ pub enum CliCommand {
|
||||
config_file: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Generate a demo config file
|
||||
/// Generate a demo config file for Rosenpass
|
||||
///
|
||||
/// The generated config file will contain a single peer and all common
|
||||
/// options.
|
||||
GenConfig {
|
||||
config_file: PathBuf,
|
||||
|
||||
@@ -186,19 +203,19 @@ pub enum CliCommand {
|
||||
force: bool,
|
||||
},
|
||||
|
||||
/// Generate the keys mentioned in a configFile
|
||||
/// Generate secret & public key for Rosenpass
|
||||
///
|
||||
/// Generates secret- & public-key to their destination. If a config file
|
||||
/// is provided then the key file destination is taken from there.
|
||||
/// Otherwise the
|
||||
/// Generates secret & public key to their destination. If a config file is
|
||||
/// provided then the key file destination is taken from there, otherwise
|
||||
/// the destination is taken from the CLI arguments.
|
||||
GenKeys {
|
||||
config_file: Option<PathBuf>,
|
||||
|
||||
/// where to write public-key to
|
||||
/// Where to write public key to
|
||||
#[clap(short, long)]
|
||||
public_key: Option<PathBuf>,
|
||||
|
||||
/// where to write secret-key to
|
||||
/// Where to write secret key to
|
||||
#[clap(short, long)]
|
||||
secret_key: Option<PathBuf>,
|
||||
|
||||
@@ -207,25 +224,27 @@ pub enum CliCommand {
|
||||
force: bool,
|
||||
},
|
||||
|
||||
/// Deprecated - use gen-keys instead
|
||||
/// Validate a configuration file
|
||||
///
|
||||
/// This command will validate the configuration file and print any errors
|
||||
/// it finds. If the configuration file is valid, it will print a success.
|
||||
/// Defined secret & public keys are checked for existence and validity.
|
||||
Validate { config_files: Vec<PathBuf> },
|
||||
|
||||
/// DEPRECATED - use the gen-keys command instead
|
||||
#[allow(rustdoc::broken_intra_doc_links)]
|
||||
#[allow(rustdoc::invalid_html_tags)]
|
||||
#[command(hide = true)]
|
||||
Keygen {
|
||||
// NOTE yes, the legacy keygen argument initially really accepted "privet-key", not "secret-key"!
|
||||
// NOTE yes, the legacy keygen argument initially really accepted
|
||||
// "private-key", not "secret-key"!
|
||||
/// public-key <PATH> private-key <PATH>
|
||||
args: Vec<String>,
|
||||
},
|
||||
|
||||
/// Validate a configuration
|
||||
Validate { config_files: Vec<PathBuf> },
|
||||
|
||||
/// Show the rosenpass manpage
|
||||
// TODO make this the default, but only after the manpage has been adjusted once the CLI stabilizes
|
||||
Man,
|
||||
}
|
||||
|
||||
impl CliArgs {
|
||||
/// runs the command specified via CLI
|
||||
/// Runs the command specified via CLI
|
||||
///
|
||||
/// ## TODO
|
||||
/// - This method consumes the [`CliCommand`] value. It might be wise to use a reference...
|
||||
@@ -236,26 +255,17 @@ impl CliArgs {
|
||||
) -> anyhow::Result<()> {
|
||||
use CliCommand::*;
|
||||
match &self.command {
|
||||
Man => {
|
||||
let man_cmd = std::process::Command::new("man")
|
||||
.args(["1", "rosenpass"])
|
||||
.status();
|
||||
|
||||
if !(man_cmd.is_ok() && man_cmd.unwrap().success()) {
|
||||
println!(include_str!(env!("ROSENPASS_MAN")));
|
||||
}
|
||||
}
|
||||
GenConfig { config_file, force } => {
|
||||
Some(GenConfig { config_file, force }) => {
|
||||
ensure!(
|
||||
*force || !config_file.exists(),
|
||||
"config file {config_file:?} already exists"
|
||||
);
|
||||
|
||||
config::Rosenpass::example_config().store(config_file)?;
|
||||
std::fs::write(config_file, config::EXAMPLE_CONFIG)?;
|
||||
}
|
||||
|
||||
// Deprecated - use gen-keys instead
|
||||
Keygen { args } => {
|
||||
Some(Keygen { args }) => {
|
||||
log::warn!("The 'keygen' command is deprecated. Please use the 'gen-keys' command instead.");
|
||||
|
||||
let mut public_key: Option<PathBuf> = None;
|
||||
@@ -288,12 +298,12 @@ impl CliArgs {
|
||||
generate_and_save_keypair(secret_key.unwrap(), public_key.unwrap())?;
|
||||
}
|
||||
|
||||
GenKeys {
|
||||
Some(GenKeys {
|
||||
config_file,
|
||||
public_key,
|
||||
secret_key,
|
||||
force,
|
||||
} => {
|
||||
}) => {
|
||||
// figure out where the key file is specified, in the config file or directly as flag?
|
||||
let (pkf, skf) = match (config_file, public_key, secret_key) {
|
||||
(Some(config_file), _, _) => {
|
||||
@@ -337,7 +347,7 @@ impl CliArgs {
|
||||
generate_and_save_keypair(skf, pkf)?;
|
||||
}
|
||||
|
||||
ExchangeConfig { config_file } => {
|
||||
Some(ExchangeConfig { config_file }) => {
|
||||
ensure!(
|
||||
config_file.exists(),
|
||||
"config file '{config_file:?}' does not exist"
|
||||
@@ -351,11 +361,11 @@ impl CliArgs {
|
||||
Self::event_loop(config, broker_interface, test_helpers)?;
|
||||
}
|
||||
|
||||
Exchange {
|
||||
Some(Exchange {
|
||||
first_arg,
|
||||
rest_of_args,
|
||||
config_file,
|
||||
} => {
|
||||
}) => {
|
||||
let mut rest_of_args = rest_of_args.clone();
|
||||
rest_of_args.insert(0, first_arg.clone());
|
||||
let args = rest_of_args;
|
||||
@@ -372,20 +382,22 @@ impl CliArgs {
|
||||
Self::event_loop(config, broker_interface, test_helpers)?;
|
||||
}
|
||||
|
||||
Validate { config_files } => {
|
||||
Some(Validate { config_files }) => {
|
||||
for file in config_files {
|
||||
match config::Rosenpass::load(file) {
|
||||
Ok(config) => {
|
||||
eprintln!("{file:?} is valid TOML and conforms to the expected schema");
|
||||
match config.validate() {
|
||||
Ok(_) => eprintln!("{file:?} has passed all logical checks"),
|
||||
Err(_) => eprintln!("{file:?} contains logical errors"),
|
||||
Err(err) => eprintln!("{file:?} contains logical errors: '{err}'"),
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("{file:?} is not valid: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&None => {} // calp print help if no command is given
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
//! ## TODO
|
||||
//! - support `~` in <https://github.com/rosenpass/rosenpass/issues/237>
|
||||
//! - provide tooling to create config file from shell <https://github.com/rosenpass/rosenpass/issues/247>
|
||||
|
||||
use crate::protocol::{SPk, SSk};
|
||||
use rosenpass_util::file::LoadValue;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs,
|
||||
@@ -207,23 +208,33 @@ impl Rosenpass {
|
||||
}
|
||||
|
||||
/// Validate a configuration
|
||||
///
|
||||
/// ## TODO
|
||||
/// - check that files do not just exist but are also readable
|
||||
/// - warn if neither out_key nor exchange_command of a peer is defined (v.i.)
|
||||
pub fn validate(&self) -> anyhow::Result<()> {
|
||||
if let Some(ref keypair) = self.keypair {
|
||||
// check the public key file exists
|
||||
ensure!(
|
||||
keypair.public_key.is_file(),
|
||||
"could not find public-key file {:?}: no such file",
|
||||
"could not find public-key file {:?}: no such file. Consider running `rosenpass gen-keys` to generate a new keypair.",
|
||||
keypair.public_key
|
||||
);
|
||||
|
||||
// check the public-key file is a valid key
|
||||
ensure!(
|
||||
SPk::load(&keypair.public_key).is_ok(),
|
||||
"could not load public-key file {:?}: invalid key",
|
||||
keypair.public_key
|
||||
);
|
||||
|
||||
// check the secret-key file exists
|
||||
ensure!(
|
||||
keypair.secret_key.is_file(),
|
||||
"could not find secret-key file {:?}: no such file",
|
||||
"could not find secret-key file {:?}: no such file. Consider running `rosenpass gen-keys` to generate a new keypair.",
|
||||
keypair.secret_key
|
||||
);
|
||||
|
||||
// check the secret-key file is a valid key
|
||||
ensure!(
|
||||
SSk::load(&keypair.secret_key).is_ok(),
|
||||
"could not load public-key file {:?}: invalid key",
|
||||
keypair.secret_key
|
||||
);
|
||||
}
|
||||
@@ -236,6 +247,13 @@ impl Rosenpass {
|
||||
peer.public_key
|
||||
);
|
||||
|
||||
// check peer's public-key file is a valid key
|
||||
ensure!(
|
||||
SPk::load(&peer.public_key).is_ok(),
|
||||
"peer {i} public-key file {:?} is invalid",
|
||||
peer.public_key
|
||||
);
|
||||
|
||||
// check endpoint is usable
|
||||
if let Some(addr) = peer.endpoint.as_ref() {
|
||||
ensure!(
|
||||
@@ -245,7 +263,22 @@ impl Rosenpass {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO warn if neither out_key nor exchange_command is defined
|
||||
// check if `key_out` or `device` and `peer` are defined
|
||||
if peer.key_out.is_none() {
|
||||
if let Some(wg) = &peer.wg {
|
||||
if wg.device.is_empty() || wg.peer.is_empty() {
|
||||
ensure!(
|
||||
false,
|
||||
"peer {i} has neither `key_out` nor valid wireguard config defined"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ensure!(
|
||||
false,
|
||||
"peer {i} has neither `key_out` nor valid wireguard config defined"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -491,38 +524,31 @@ impl Rosenpass {
|
||||
}
|
||||
}
|
||||
|
||||
impl Rosenpass {
|
||||
/// Generate an example configuration
|
||||
pub fn example_config() -> Self {
|
||||
let peer = RosenpassPeer {
|
||||
public_key: "/path/to/rp-peer-public-key".into(),
|
||||
endpoint: Some("my-peer.test:9999".into()),
|
||||
key_out: Some("/path/to/rp-key-out.txt".into()),
|
||||
pre_shared_key: Some("additional pre shared key".into()),
|
||||
wg: Some(WireGuard {
|
||||
device: "wirgeguard device e.g. wg0".into(),
|
||||
peer: "wireguard public key".into(),
|
||||
extra_params: vec!["passed to".into(), "wg set".into()],
|
||||
}),
|
||||
};
|
||||
|
||||
Self {
|
||||
keypair: Some(Keypair {
|
||||
public_key: "/path/to/rp-public-key".into(),
|
||||
secret_key: "/path/to/rp-secret-key".into(),
|
||||
}),
|
||||
peers: vec![peer],
|
||||
..Self::new(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Verbosity {
|
||||
fn default() -> Self {
|
||||
Self::Quiet
|
||||
}
|
||||
}
|
||||
|
||||
pub static EXAMPLE_CONFIG: &str = r###"public_key = "/path/to/rp-public-key"
|
||||
secret_key = "/path/to/rp-secret-key"
|
||||
listen = []
|
||||
verbosity = "Verbose"
|
||||
|
||||
[[peers]]
|
||||
# Commented out fields are optional
|
||||
public_key = "/path/to/rp-peer-public-key"
|
||||
endpoint = "127.0.0.1:9998"
|
||||
# pre_shared_key = "/path/to/preshared-key"
|
||||
|
||||
# Choose to store the key in a file via `key_out` or pass it to WireGuard by
|
||||
# defining `device` and `peer`. You may choose to do both.
|
||||
key_out = "/path/to/rp-key-out.txt" # path to store the key
|
||||
# device = "wg0" # WireGuard interface
|
||||
#peer = "RULdRAtUw7SFfVfGD..." # WireGuard public key
|
||||
# extra_params = [] # passed to WireGuard `wg set`
|
||||
"###;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
|
||||
@@ -1,13 +1,68 @@
|
||||
//! Pseudo Random Functions (PRFs) with a tree-like label scheme which
|
||||
//! ensures their uniqueness
|
||||
//! ensures their uniqueness.
|
||||
//!
|
||||
//! This ensures [domain separation](https://en.wikipedia.org/wiki/Domain_separation) is used
|
||||
//! across the Rosenpass protocol.
|
||||
//!
|
||||
//! There is a chart containing all hash domains used in Rosenpass in the
|
||||
//! [whitepaper](https://rosenpass.eu/whitepaper.pdf) ([/papers/whitepaper.md] in this repository).
|
||||
//!
|
||||
//! # Tutorial
|
||||
//!
|
||||
//! ```
|
||||
//! use rosenpass::{hash_domain, hash_domain_ns};
|
||||
//! use rosenpass::hash_domains::protocol;
|
||||
//!
|
||||
//! // Declaring a custom hash domain
|
||||
//! hash_domain_ns!(protocol, custom_domain, "my custom hash domain label");
|
||||
//!
|
||||
//! // Declaring a custom hashers
|
||||
//! hash_domain_ns!(custom_domain, hashers, "hashers");
|
||||
//! hash_domain_ns!(hashers, hasher1, "1");
|
||||
//! hash_domain_ns!(hashers, hasher2, "2");
|
||||
//!
|
||||
//! // Declaring specific domain separators
|
||||
//! hash_domain_ns!(custom_domain, domain_separators, "domain separators");
|
||||
//! hash_domain!(domain_separators, sep1, "1");
|
||||
//! hash_domain!(domain_separators, sep2, "2");
|
||||
//!
|
||||
//! // Generating values under hasher1 with both domain separators
|
||||
//! let h1 = hasher1()?.mix(b"some data")?.dup();
|
||||
//! let h1v1 = h1.mix(&sep1()?)?.mix(b"More data")?.into_value();
|
||||
//! let h1v2 = h1.mix(&sep2()?)?.mix(b"More data")?.into_value();
|
||||
//!
|
||||
//! // Generating values under hasher2 with both domain separators
|
||||
//! let h2 = hasher2()?.mix(b"some data")?.dup();
|
||||
//! let h2v1 = h2.mix(&sep1()?)?.mix(b"More data")?.into_value();
|
||||
//! let h2v2 = h2.mix(&sep2()?)?.mix(b"More data")?.into_value();
|
||||
//!
|
||||
//! // All of the domain separators are now different, random strings
|
||||
//! let values = [h1v1, h1v2, h2v1, h2v2];
|
||||
//! for i in 0..values.len() {
|
||||
//! for j in (i+1)..values.len() {
|
||||
//! assert_ne!(values[i], values[j]);
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! Ok::<(), anyhow::Error>(())
|
||||
//! ```
|
||||
|
||||
use anyhow::Result;
|
||||
use rosenpass_ciphers::{hash_domain::HashDomain, KEY_LEN};
|
||||
use rosenpass_ciphers::hash_domain::HashDomain;
|
||||
|
||||
/// Declare a hash function
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See the source file for details about how this is used concretely.
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general
|
||||
// TODO Use labels that can serve as identifiers
|
||||
#[macro_export]
|
||||
macro_rules! hash_domain_ns {
|
||||
($base:ident, $name:ident, $($lbl:expr),* ) => {
|
||||
pub fn $name() -> Result<HashDomain> {
|
||||
($(#[$($attrss:tt)*])* $base:ident, $name:ident, $($lbl:expr),+ ) => {
|
||||
$(#[$($attrss)*])*
|
||||
pub fn $name() -> ::anyhow::Result<::rosenpass_ciphers::hash_domain::HashDomain> {
|
||||
let t = $base()?;
|
||||
$( let t = t.mix($lbl.as_bytes())?; )*
|
||||
Ok(t)
|
||||
@@ -15,9 +70,18 @@ macro_rules! hash_domain_ns {
|
||||
}
|
||||
}
|
||||
|
||||
/// Declare a concrete hash value
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See the source file for details about how this is used concretely.
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general
|
||||
#[macro_export]
|
||||
macro_rules! hash_domain {
|
||||
($base:ident, $name:ident, $($lbl:expr),* ) => {
|
||||
pub fn $name() -> Result<[u8; KEY_LEN]> {
|
||||
($(#[$($attrss:tt)*])* $base:ident, $name:ident, $($lbl:expr),+ ) => {
|
||||
$(#[$($attrss)*])*
|
||||
pub fn $name() -> ::anyhow::Result<[u8; ::rosenpass_ciphers::KEY_LEN]> {
|
||||
let t = $base()?;
|
||||
$( let t = t.mix($lbl.as_bytes())?; )*
|
||||
Ok(t.into_value())
|
||||
@@ -25,24 +89,227 @@ macro_rules! hash_domain {
|
||||
}
|
||||
}
|
||||
|
||||
/// The hash domain containing the protocol string.
|
||||
///
|
||||
/// This serves as a global [domain separator](https://en.wikipedia.org/wiki/Domain_separation)
|
||||
/// used in various places in the rosenpass protocol.
|
||||
///
|
||||
/// This is generally used to create further hash-domains for specific purposes. See
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See the source file for details about how this is used concretely.
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general
|
||||
pub fn protocol() -> Result<HashDomain> {
|
||||
HashDomain::zero().mix("Rosenpass v1 mceliece460896 Kyber512 ChaChaPoly1305 BLAKE2s".as_bytes())
|
||||
}
|
||||
|
||||
hash_domain_ns!(protocol, mac, "mac");
|
||||
hash_domain_ns!(protocol, cookie, "cookie");
|
||||
hash_domain_ns!(protocol, cookie_value, "cookie-value");
|
||||
hash_domain_ns!(protocol, cookie_key, "cookie-key");
|
||||
hash_domain_ns!(protocol, peerid, "peer id");
|
||||
hash_domain_ns!(protocol, biscuit_ad, "biscuit additional data");
|
||||
hash_domain_ns!(protocol, ckinit, "chaining key init");
|
||||
hash_domain_ns!(protocol, _ckextract, "chaining key extract");
|
||||
hash_domain_ns!(
|
||||
/// Hash domain based on [protocol] for calculating [crate::msgs::Envelope::mac].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See the source of [crate::msgs::Envelope::seal] and [crate::msgs::Envelope::check_seal]
|
||||
/// to figure out how this is concretely used.
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
protocol, mac, "mac");
|
||||
hash_domain_ns!(
|
||||
/// Hash domain based on [protocol] involved in calculating [crate::msgs::Envelope::cookie].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See the source of [crate::msgs::Envelope::seal_cookie],
|
||||
/// [crate::protocol::CryptoServer::handle_msg_under_load], and
|
||||
/// [crate::protocol::CryptoServer::handle_cookie_reply]
|
||||
/// to figure out how this is concretely used.
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
protocol, cookie, "cookie");
|
||||
hash_domain_ns!(
|
||||
/// Hash domain based on [protocol] involved in calculating [crate::msgs::Envelope::cookie].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See the source of [crate::msgs::Envelope::seal_cookie],
|
||||
/// [crate::protocol::CryptoServer::handle_msg_under_load], and
|
||||
/// [crate::protocol::CryptoServer::handle_cookie_reply]
|
||||
/// to figure out how this is concretely used.
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
protocol, cookie_value, "cookie-value");
|
||||
hash_domain_ns!(
|
||||
/// Hash domain based on [protocol] involved in calculating [crate::msgs::Envelope::cookie].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See the source of [crate::msgs::Envelope::seal_cookie],
|
||||
/// [crate::protocol::CryptoServer::handle_msg_under_load], and
|
||||
/// [crate::protocol::CryptoServer::handle_cookie_reply]
|
||||
/// to figure out how this is concretely used.
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
protocol, cookie_key, "cookie-key");
|
||||
hash_domain_ns!(
|
||||
/// Hash domain based on [protocol] for calculating the peer id as transmitted (encrypted)
|
||||
/// in [crate::msgs::InitHello::pidic].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See the source of [crate::protocol::CryptoServer::pidm] and
|
||||
/// [crate::protocol::Peer::pidt]
|
||||
/// to figure out how this is concretely used.
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
protocol, peerid, "peer id");
|
||||
hash_domain_ns!(
|
||||
/// Hash domain based on [protocol] for calculating the additional data
|
||||
/// during [crate::msgs::Biscuit] encryption, storing the biscuit into
|
||||
/// [crate::msgs::RespHello::biscuit].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// To understand how the biscuit is used, it is best to read
|
||||
/// the code of [crate::protocol::HandshakeState::store_biscuit] and
|
||||
/// [crate::protocol::HandshakeState::load_biscuit]
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
protocol, biscuit_ad, "biscuit additional data");
|
||||
hash_domain_ns!(
|
||||
/// This hash domain begins our actual handshake procedure, initializing the
|
||||
/// chaining key [crate::protocol::HandshakeState::ck].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// To understand how the chaining key is used, study
|
||||
/// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init]
|
||||
/// and [crate::protocol::HandshakeState::mix].
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
protocol, ckinit, "chaining key init");
|
||||
hash_domain_ns!(
|
||||
/// Namespace for chaining key usage domain separators.
|
||||
///
|
||||
/// During the execution of the Rosenpass protocol, we use the chaining key for multiple
|
||||
/// purposes, so to make sure that we have unique value domains, we mix a domain separator
|
||||
/// into the chaining key before using it for any particular purpose.
|
||||
///
|
||||
/// We could use the full domain separation strings, but using a hash value here is nice
|
||||
/// because it does not lead to any constraints about domain separator format and we can
|
||||
/// even allow third parties to define their own separators by claiming a namespace.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// To understand how the chaining key is used, study
|
||||
/// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init]
|
||||
/// and [crate::protocol::HandshakeState::mix].
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
protocol, _ckextract, "chaining key extract");
|
||||
|
||||
hash_domain!(_ckextract, mix, "mix");
|
||||
hash_domain!(_ckextract, hs_enc, "handshake encryption");
|
||||
hash_domain!(_ckextract, ini_enc, "initiator handshake encryption");
|
||||
hash_domain!(_ckextract, res_enc, "responder handshake encryption");
|
||||
hash_domain!(
|
||||
/// Used to mix in further values into the chaining key during the handshake.
|
||||
///
|
||||
/// See [_ckextract].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// To understand how the chaining key is used, study
|
||||
/// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init]
|
||||
/// and [crate::protocol::HandshakeState::mix].
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
_ckextract, mix, "mix");
|
||||
hash_domain!(
|
||||
/// Chaining key domain separator for generating encryption keys that can
|
||||
/// encrypt parts of the handshake.
|
||||
///
|
||||
/// See [_ckextract].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Encryption of data during the handshake happens in
|
||||
/// [crate::protocol::HandshakeState::encrypt_and_mix] and decryption happens in
|
||||
/// [crate::protocol::HandshakeState::decrypt_and_mix]. See their source code
|
||||
/// for details.
|
||||
///
|
||||
/// To understand how the chaining key is used, study
|
||||
/// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init]
|
||||
/// and [crate::protocol::HandshakeState::mix].
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
_ckextract, hs_enc, "handshake encryption");
|
||||
hash_domain!(
|
||||
/// Chaining key domain separator for live data encryption.
|
||||
/// Live data encryption is only used to send confirmation of handshake
|
||||
/// done in [crate::msgs::EmptyData].
|
||||
///
|
||||
/// See [_ckextract].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// This domain separator finds use in [crate::protocol::HandshakeState::enter_live].
|
||||
///
|
||||
/// To understand how the chaining key is used, study
|
||||
/// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init]
|
||||
/// and [crate::protocol::HandshakeState::mix].
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
_ckextract, ini_enc, "initiator handshake encryption");
|
||||
hash_domain!(
|
||||
/// Chaining key domain separator for live data encryption.
|
||||
/// Live data encryption is only used to send confirmation of handshake
|
||||
/// done in [crate::msgs::EmptyData].
|
||||
///
|
||||
/// See [_ckextract].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// This domain separator finds use in [crate::protocol::HandshakeState::enter_live].
|
||||
/// Check out its source code!
|
||||
///
|
||||
/// To understand how the chaining key is used, study
|
||||
/// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init]
|
||||
/// and [crate::protocol::HandshakeState::mix].
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
_ckextract, res_enc, "responder handshake encryption");
|
||||
|
||||
hash_domain_ns!(_ckextract, _user, "user");
|
||||
hash_domain_ns!(_user, _rp, "rosenpass.eu");
|
||||
hash_domain!(_rp, osk, "wireguard psk");
|
||||
hash_domain_ns!(
|
||||
/// Chaining key domain separator for any usage specific purposes.
|
||||
///
|
||||
/// We do recommend that third parties base their specific domain separators
|
||||
/// on a internet domain and/or mix in much more specific information.
|
||||
///
|
||||
/// We only really use this to derive a output key for wireguard; see [osk].
|
||||
///
|
||||
/// See [_ckextract].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
_ckextract, _user, "user");
|
||||
hash_domain_ns!(
|
||||
/// Chaining key domain separator for any rosenpass specific purposes.
|
||||
///
|
||||
/// We only really use this to derive a output key for wireguard; see [osk].
|
||||
///
|
||||
/// See [_ckextract].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
_user, _rp, "rosenpass.eu");
|
||||
hash_domain!(
|
||||
/// Chaining key domain separator for deriving the key sent to WireGuard.
|
||||
///
|
||||
/// See [_ckextract].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// This domain separator finds use in [crate::protocol::CryptoServer::osk].
|
||||
/// Check out its source code!
|
||||
///
|
||||
/// See the [module](self) documentation on how to use the hash domains in general.
|
||||
_rp, osk, "wireguard psk");
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
//! This is the central rosenpass crate implementing the rosenpass protocol.
|
||||
//!
|
||||
//! - [crate::app_server] contains the business logic of rosenpass, handling networking
|
||||
//! - [crate::cli] contains the cli parsing logic and contains quite a bit of startup logic; the
|
||||
//! main function quickly hands over to [crate::cli::CliArgs::run] which contains quite a bit
|
||||
//! of our startup logic
|
||||
//! - [crate::config] has the code to parse and generate configuration files
|
||||
//! - [crate::hash_domains] lists the different hash function domains used in the Rosenpass
|
||||
//! protocol
|
||||
//! - [crate::msgs] provides declarations of the Rosenpass protocol network messages and facilities
|
||||
//! to parse those messages through the [::zerocopy] crate
|
||||
//! - [crate::protocol] this is where the bulk of our code lives; this module contains the actual
|
||||
//! cryptographic protocol logic
|
||||
//! - crate::api implements the Rosenpass unix socket API, if feature "experiment_api" is active
|
||||
|
||||
#[cfg(feature = "experiment_api")]
|
||||
pub mod api;
|
||||
pub mod app_server;
|
||||
@@ -7,14 +22,25 @@ pub mod hash_domains;
|
||||
pub mod msgs;
|
||||
pub mod protocol;
|
||||
|
||||
/// Error types used in diverse places across Rosenpass
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum RosenpassError {
|
||||
/// Usually indicates that parsing a struct through the
|
||||
/// [::zerocopy] crate failed
|
||||
#[error("buffer size mismatch")]
|
||||
BufferSizeMismatch,
|
||||
/// Mostly raised by the `TryFrom<u8>` implementation for [crate::msgs::MsgType]
|
||||
/// to indicate that a message type is not defined
|
||||
#[error("invalid message type")]
|
||||
InvalidMessageType(u8),
|
||||
InvalidMessageType(
|
||||
/// The message type that could not be parsed
|
||||
u8,
|
||||
),
|
||||
/// Raised by the `TryFrom<RawMsgType>` (crate::api::RawMsgType) implementation for crate::api::RequestMsgType
|
||||
/// and crate::api::RequestMsgType to indicate that a message type is not defined
|
||||
#[error("invalid API message type")]
|
||||
InvalidApiMessageType(u128),
|
||||
#[error("could not parse API message")]
|
||||
InvalidApiMessage,
|
||||
InvalidApiMessageType(
|
||||
/// The message type that could not be parsed
|
||||
u128,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,13 +1,56 @@
|
||||
//! For the main function
|
||||
|
||||
use clap::CommandFactory;
|
||||
use clap::Parser;
|
||||
use clap_mangen::roff::{roman, Roff};
|
||||
use log::error;
|
||||
use rosenpass::cli::CliArgs;
|
||||
use std::process::exit;
|
||||
|
||||
/// Printing custom man sections when generating the man page
|
||||
fn print_custom_man_section(section: &str, text: &str, file: &mut std::fs::File) {
|
||||
let mut roff = Roff::default();
|
||||
roff.control("SH", [section]);
|
||||
roff.text([roman(text)]);
|
||||
let _ = roff.to_writer(file);
|
||||
}
|
||||
|
||||
/// Catches errors, prints them through the logger, then exits
|
||||
///
|
||||
/// The bulk of the command line logic is handled inside [crate::cli::CliArgs::run].
|
||||
pub fn main() {
|
||||
// parse CLI arguments
|
||||
let args = CliArgs::parse();
|
||||
|
||||
if let Some(shell) = args.print_completions {
|
||||
let mut cmd = CliArgs::command();
|
||||
clap_complete::generate(shell, &mut cmd, "rosenpass", &mut std::io::stdout());
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(out_dir) = args.generate_manpage {
|
||||
std::fs::create_dir_all(&out_dir).expect("Failed to create man pages directory");
|
||||
|
||||
let cmd = CliArgs::command();
|
||||
let man = clap_mangen::Man::new(cmd.clone());
|
||||
let _ = clap_mangen::generate_to(cmd, &out_dir);
|
||||
|
||||
let file_path = out_dir.join("rosenpass.1");
|
||||
let mut file = std::fs::File::create(file_path).expect("Failed to create man page file");
|
||||
|
||||
let _ = man.render_title(&mut file);
|
||||
let _ = man.render_name_section(&mut file);
|
||||
let _ = man.render_synopsis_section(&mut file);
|
||||
let _ = man.render_subcommands_section(&mut file);
|
||||
let _ = man.render_options_section(&mut file);
|
||||
print_custom_man_section("EXIT STATUS", EXIT_STATUS_MAN, &mut file);
|
||||
print_custom_man_section("SEE ALSO", SEE_ALSO_MAN, &mut file);
|
||||
print_custom_man_section("STANDARDS", STANDARDS_MAN, &mut file);
|
||||
print_custom_man_section("AUTHORS", AUTHORS_MAN, &mut file);
|
||||
print_custom_man_section("BUGS", BUGS_MAN, &mut file);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
use rosenpass_secret_memory as SM;
|
||||
#[cfg(feature = "experiment_memfd_secret")]
|
||||
@@ -43,3 +86,27 @@ pub fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom main page section: Exit Status
|
||||
static EXIT_STATUS_MAN: &str = r"
|
||||
The rosenpass utility exits 0 on success, and >0 if an error occurs.";
|
||||
|
||||
/// Custom main page section: See also.
|
||||
static SEE_ALSO_MAN: &str = r"
|
||||
rp(1), wg(1)
|
||||
|
||||
Karolin Varner, Benjamin Lipp, Wanja Zaeske, and Lisa Schmidt, Rosenpass, https://rosenpass.eu/whitepaper.pdf, 2023.";
|
||||
|
||||
/// Custom main page section: Standards.
|
||||
static STANDARDS_MAN: &str = r"
|
||||
This tool is the reference implementation of the Rosenpass protocol, as
|
||||
specified within the whitepaper referenced above.";
|
||||
|
||||
/// Custom main page section: Authors.
|
||||
static AUTHORS_MAN: &str = r"
|
||||
Rosenpass was created by Karolin Varner, Benjamin Lipp, Wanja Zaeske, Marei
|
||||
Peischl, Stephan Ajuvo, and Lisa Schmidt.";
|
||||
|
||||
/// Custom main page section: Bugs.
|
||||
static BUGS_MAN: &str = r"
|
||||
The bugs are tracked at https://github.com/rosenpass/rosenpass/issues.";
|
||||
|
||||
@@ -9,20 +9,75 @@
|
||||
//! To achieve this we utilize the zerocopy library.
|
||||
//!
|
||||
use std::mem::size_of;
|
||||
use std::u8;
|
||||
use zerocopy::{AsBytes, FromBytes, FromZeroes};
|
||||
|
||||
use super::RosenpassError;
|
||||
use rosenpass_cipher_traits::Kem;
|
||||
use rosenpass_ciphers::kem::{EphemeralKem, StaticKem};
|
||||
use rosenpass_ciphers::{aead, xaead, KEY_LEN};
|
||||
pub const MSG_SIZE_LEN: usize = 1;
|
||||
pub const RESERVED_LEN: usize = 3;
|
||||
pub const MAC_SIZE: usize = 16;
|
||||
pub const COOKIE_SIZE: usize = 16;
|
||||
pub const SID_LEN: usize = 4;
|
||||
|
||||
/// Length of a session ID such as [InitHello::sidi]
|
||||
pub const SESSION_ID_LEN: usize = 4;
|
||||
/// Length of a biscuit ID; i.e. size of the value in [Biscuit::biscuit_no]
|
||||
pub const BISCUIT_ID_LEN: usize = 12;
|
||||
|
||||
/// TODO: Unused, remove!
|
||||
pub const WIRE_ENVELOPE_LEN: usize = 1 + 3 + 16 + 16; // TODO verify this
|
||||
|
||||
/// Size required to fit any message in binary form
|
||||
pub const MAX_MESSAGE_LEN: usize = 2500; // TODO fix this
|
||||
|
||||
/// length in bytes of an unencrypted Biscuit (plain text)
|
||||
pub const BISCUIT_PT_LEN: usize = size_of::<Biscuit>();
|
||||
|
||||
/// Length in bytes of an encrypted Biscuit (cipher text)
|
||||
pub const BISCUIT_CT_LEN: usize = BISCUIT_PT_LEN + xaead::NONCE_LEN + xaead::TAG_LEN;
|
||||
|
||||
/// Size of the field [Envelope::mac]
|
||||
pub const MAC_SIZE: usize = 16;
|
||||
/// Size of the field [Envelope::cookie]
|
||||
pub const COOKIE_SIZE: usize = MAC_SIZE;
|
||||
|
||||
/// Type of the mac field in [Envelope]
|
||||
pub type MsgEnvelopeMac = [u8; MAC_SIZE];
|
||||
|
||||
/// Type of the cookie field in [Envelope]
|
||||
pub type MsgEnvelopeCookie = [u8; COOKIE_SIZE];
|
||||
|
||||
/// Header and footer included in all our packages,
|
||||
/// including a type field.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass::msgs::{Envelope, InitHello};
|
||||
/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes};
|
||||
/// use memoffset::offset_of;
|
||||
///
|
||||
/// // Zero-initialization
|
||||
/// let mut ih = Envelope::<InitHello>::new_zeroed();
|
||||
///
|
||||
/// // Edit fields normally
|
||||
/// ih.mac[0] = 1;
|
||||
///
|
||||
/// // Edit as binary
|
||||
/// ih.as_bytes_mut()[offset_of!(Envelope<InitHello>, msg_type)] = 23;
|
||||
/// assert_eq!(ih.msg_type, 23);;
|
||||
///
|
||||
/// // Conversion to bytes
|
||||
/// let mut ih2 = ih.as_bytes().to_owned();
|
||||
///
|
||||
/// // Setting msg_type field, again
|
||||
/// ih2[0] = 42;
|
||||
///
|
||||
/// // Zerocopy parsing
|
||||
/// let ih3 = Ref::<&mut [u8], Envelope<InitHello>>::new(&mut ih2).unwrap();
|
||||
/// assert_ne!(ih.as_bytes(), ih3.as_bytes());
|
||||
/// assert_eq!(ih3.msg_type, 42);
|
||||
/// ```
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes, Clone)]
|
||||
pub struct Envelope<M: AsBytes + FromBytes> {
|
||||
/// [MsgType] of this message
|
||||
pub msg_type: u8,
|
||||
@@ -32,11 +87,45 @@ pub struct Envelope<M: AsBytes + FromBytes> {
|
||||
pub payload: M,
|
||||
/// Message Authentication Code (mac) over all bytes until (exclusive)
|
||||
/// `mac` itself
|
||||
pub mac: [u8; 16],
|
||||
pub mac: MsgEnvelopeMac,
|
||||
/// Currently unused, TODO: do something with this
|
||||
pub cookie: [u8; 16],
|
||||
pub cookie: MsgEnvelopeCookie,
|
||||
}
|
||||
|
||||
/// This is the first message sent by the initiator to the responder
|
||||
/// during the execution of the Rosenpass protocol.
|
||||
///
|
||||
/// When transmitted on the wire, this type will generally be wrapped into [Envelope].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Check out the code of [crate::protocol::CryptoServer::handle_initiation] (generation on
|
||||
/// iniatiator side) and [crate::protocol::CryptoServer::handle_init_hello] (processing on
|
||||
/// responder side) to understand how this is used.
|
||||
///
|
||||
/// [Envelope] contains some extra examples on how to use structures from the [::zerocopy] crate.
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass::msgs::{Envelope, InitHello};
|
||||
/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes};
|
||||
/// use memoffset::span_of;
|
||||
///
|
||||
/// // Zero initialization
|
||||
/// let mut ih = Envelope::<InitHello>::new_zeroed();
|
||||
///
|
||||
/// // Conversion to byte representation
|
||||
/// let ih = ih.as_bytes_mut();
|
||||
///
|
||||
/// // Set value on byte representation
|
||||
/// ih[span_of!(Envelope<InitHello>, payload)][span_of!(InitHello, sidi)]
|
||||
/// .copy_from_slice(&[1,2,3,4]);
|
||||
///
|
||||
/// // Conversion from bytes
|
||||
/// let ih = Ref::<&mut [u8], Envelope<InitHello>>::new(ih).unwrap();
|
||||
///
|
||||
/// // Check that write above on byte representation was effective
|
||||
/// assert_eq!(ih.payload.sidi, [1,2,3,4]);
|
||||
/// ```
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct InitHello {
|
||||
@@ -52,6 +141,40 @@ pub struct InitHello {
|
||||
pub auth: [u8; aead::TAG_LEN],
|
||||
}
|
||||
|
||||
/// This is the second message sent by the responder to the initiator
|
||||
/// during the execution of the Rosenpass protocol in response to [InitHello].
|
||||
///
|
||||
/// When transmitted on the wire, this type will generally be wrapped into [Envelope].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Check out the code of [crate::protocol::CryptoServer::handle_init_hello] (generation on
|
||||
/// responder side) and [crate::protocol::CryptoServer::handle_resp_hello] (processing on
|
||||
/// initiator side) to understand how this is used.
|
||||
///
|
||||
/// [Envelope] contains some extra examples on how to use structures from the [::zerocopy] crate.
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass::msgs::{Envelope, RespHello};
|
||||
/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes};
|
||||
/// use memoffset::span_of;
|
||||
///
|
||||
/// // Zero initialization
|
||||
/// let mut ih = Envelope::<RespHello>::new_zeroed();
|
||||
///
|
||||
/// // Conversion to byte representation
|
||||
/// let ih = ih.as_bytes_mut();
|
||||
///
|
||||
/// // Set value on byte representation
|
||||
/// ih[span_of!(Envelope<RespHello>, payload)][span_of!(RespHello, sidi)]
|
||||
/// .copy_from_slice(&[1,2,3,4]);
|
||||
///
|
||||
/// // Conversion from bytes
|
||||
/// let ih = Ref::<&mut [u8], Envelope<RespHello>>::new(ih).unwrap();
|
||||
///
|
||||
/// // Check that write above on byte representation was effective
|
||||
/// assert_eq!(ih.payload.sidi, [1,2,3,4]);
|
||||
/// ```
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct RespHello {
|
||||
@@ -69,8 +192,42 @@ pub struct RespHello {
|
||||
pub biscuit: [u8; BISCUIT_CT_LEN],
|
||||
}
|
||||
|
||||
/// This is the third message sent by the initiator to the responder
|
||||
/// during the execution of the Rosenpass protocol in response to [RespHello].
|
||||
///
|
||||
/// When transmitted on the wire, this type will generally be wrapped into [Envelope].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Check out the code of [crate::protocol::CryptoServer::handle_resp_hello] (generation on
|
||||
/// initiator side) and [crate::protocol::CryptoServer::handle_init_conf] (processing on
|
||||
/// responder side) to understand how this is used.
|
||||
///
|
||||
/// [Envelope] contains some extra examples on how to use structures from the [::zerocopy] crate.
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass::msgs::{Envelope, InitConf};
|
||||
/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes};
|
||||
/// use memoffset::span_of;
|
||||
///
|
||||
/// // Zero initialization
|
||||
/// let mut ih = Envelope::<InitConf>::new_zeroed();
|
||||
///
|
||||
/// // Conversion to byte representation
|
||||
/// let ih = ih.as_bytes_mut();
|
||||
///
|
||||
/// // Set value on byte representation
|
||||
/// ih[span_of!(Envelope<InitConf>, payload)][span_of!(InitConf, sidi)]
|
||||
/// .copy_from_slice(&[1,2,3,4]);
|
||||
///
|
||||
/// // Conversion from bytes
|
||||
/// let ih = Ref::<&mut [u8], Envelope<InitConf>>::new(ih).unwrap();
|
||||
///
|
||||
/// // Check that write above on byte representation was effective
|
||||
/// assert_eq!(ih.payload.sidi, [1,2,3,4]);
|
||||
/// ```
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes, Debug)]
|
||||
pub struct InitConf {
|
||||
/// Copied from InitHello
|
||||
pub sidi: [u8; 4],
|
||||
@@ -82,8 +239,53 @@ pub struct InitConf {
|
||||
pub auth: [u8; aead::TAG_LEN],
|
||||
}
|
||||
|
||||
/// This is the fourth message sent by the initiator to the responder
|
||||
/// during the execution of the Rosenpass protocol in response to [RespHello].
|
||||
///
|
||||
/// When transmitted on the wire, this type will generally be wrapped into [Envelope].
|
||||
///
|
||||
/// This message does not serve a cryptographic purpose; it just tells the initiator
|
||||
/// to stop package retransmission.
|
||||
///
|
||||
/// This message should really be called `RespConf`, but when we wrote the protocol,
|
||||
/// we initially designed the protocol we still though Rosenpass itself should do
|
||||
/// payload transmission at some point so `EmptyData` could have served as a more generic
|
||||
/// mechanism.
|
||||
///
|
||||
/// We might add payload transmission in the future again, but we will treat
|
||||
/// it as a protocol extension if we do.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Check out the code of [crate::protocol::CryptoServer::handle_init_conf] (generation on
|
||||
/// responder side) and [crate::protocol::CryptoServer::handle_resp_conf] (processing on
|
||||
/// initiator side) to understand how this is used.
|
||||
///
|
||||
/// [Envelope] contains some extra examples on how to use structures from the [::zerocopy] crate.
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass::msgs::{Envelope, EmptyData};
|
||||
/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes};
|
||||
/// use memoffset::span_of;
|
||||
///
|
||||
/// // Zero initialization
|
||||
/// let mut ih = Envelope::<EmptyData>::new_zeroed();
|
||||
///
|
||||
/// // Conversion to byte representation
|
||||
/// let ih = ih.as_bytes_mut();
|
||||
///
|
||||
/// // Set value on byte representation
|
||||
/// ih[span_of!(Envelope<EmptyData>, payload)][span_of!(EmptyData, sid)]
|
||||
/// .copy_from_slice(&[1,2,3,4]);
|
||||
///
|
||||
/// // Conversion from bytes
|
||||
/// let ih = Ref::<&mut [u8], Envelope<EmptyData>>::new(ih).unwrap();
|
||||
///
|
||||
/// // Check that write above on byte representation was effective
|
||||
/// assert_eq!(ih.payload.sid, [1,2,3,4]);
|
||||
/// ```
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes, Clone, Copy)]
|
||||
pub struct EmptyData {
|
||||
/// Copied from RespHello
|
||||
pub sid: [u8; 4],
|
||||
@@ -93,6 +295,22 @@ pub struct EmptyData {
|
||||
pub auth: [u8; aead::TAG_LEN],
|
||||
}
|
||||
|
||||
/// Cookie encrypted and sent to the initiator by the responder in [RespHello]
|
||||
/// and returned by the initiator in [InitConf].
|
||||
///
|
||||
/// The encryption key is randomly chosen by the responder and frequently regenerated.
|
||||
/// Using this biscuit value in the protocol allows us to make sure that the responder
|
||||
/// is mostly stateless until full initiator authentication is achieved, which is needed
|
||||
/// to prevent denial of service attacks. See the [whitepaper](https://rosenpass.eu/whitepaper.pdf)
|
||||
/// ([/papers/whitepaper.md] in this repository).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// To understand how the biscuit is used, it is best to read
|
||||
/// the code of [crate::protocol::HandshakeState::store_biscuit] and
|
||||
/// [crate::protocol::HandshakeState::load_biscuit]
|
||||
///
|
||||
/// [Envelope] and [InitHello] contain some extra examples on how to use structures from the [::zerocopy] crate.
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct Biscuit {
|
||||
@@ -104,12 +322,20 @@ pub struct Biscuit {
|
||||
pub ck: [u8; KEY_LEN],
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct DataMsg {
|
||||
pub dummy: [u8; 4],
|
||||
}
|
||||
|
||||
/// Specialized message for use in the cookie mechanism.
|
||||
///
|
||||
/// See the [whitepaper](https://rosenpass.eu/whitepaper.pdf) ([/papers/whitepaper.md] in this repository) for details.
|
||||
///
|
||||
/// Generally used together with [CookieReply] which brings this up to the size
|
||||
/// of [InitHello] to avoid amplification Denial of Service attacks.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// To understand how the biscuit is used, it is best to read
|
||||
/// the code of [crate::protocol::CryptoServer::handle_cookie_reply] and
|
||||
/// [crate::protocol::CryptoServer::handle_msg_under_load].
|
||||
///
|
||||
/// [Envelope] and [InitHello] contain some extra examples on how to use structures from the [::zerocopy] crate.
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct CookieReplyInner {
|
||||
@@ -123,6 +349,20 @@ pub struct CookieReplyInner {
|
||||
pub cookie_encrypted: [u8; xaead::NONCE_LEN + COOKIE_SIZE + xaead::TAG_LEN],
|
||||
}
|
||||
|
||||
/// Specialized message for use in the cookie mechanism.
|
||||
///
|
||||
/// This just brings [CookieReplyInner] up to the size
|
||||
/// of [InitHello] to avoid amplification Denial of Service attacks.
|
||||
///
|
||||
/// See the [whitepaper](https://rosenpass.eu/whitepaper.pdf) ([/papers/whitepaper.md] in this repository) for details.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// To understand how the biscuit is used, it is best to read
|
||||
/// the code of [crate::protocol::CryptoServer::handle_cookie_reply] and
|
||||
/// [crate::protocol::CryptoServer::handle_msg_under_load].
|
||||
///
|
||||
/// [Envelope] and [InitHello] contain some extra examples on how to use structures from the [::zerocopy] crate.
|
||||
#[repr(packed)]
|
||||
#[derive(AsBytes, FromBytes, FromZeroes)]
|
||||
pub struct CookieReply {
|
||||
@@ -130,33 +370,46 @@ pub struct CookieReply {
|
||||
pub padding: [u8; size_of::<Envelope<InitHello>>() - size_of::<CookieReplyInner>()],
|
||||
}
|
||||
|
||||
// Traits /////////////////////////////////////////////////////////////////////
|
||||
|
||||
pub trait WireMsg: std::fmt::Debug {
|
||||
const MSG_TYPE: MsgType;
|
||||
const MSG_TYPE_U8: u8 = Self::MSG_TYPE as u8;
|
||||
const BYTES: usize;
|
||||
}
|
||||
|
||||
// Constants //////////////////////////////////////////////////////////////////
|
||||
|
||||
pub const SESSION_ID_LEN: usize = 4;
|
||||
pub const BISCUIT_ID_LEN: usize = 12;
|
||||
|
||||
pub const WIRE_ENVELOPE_LEN: usize = 1 + 3 + 16 + 16; // TODO verify this
|
||||
|
||||
/// Size required to fit any message in binary form
|
||||
pub const MAX_MESSAGE_LEN: usize = 2500; // TODO fix this
|
||||
|
||||
/// Recognized message types
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass::msgs::MsgType;
|
||||
/// use rosenpass::msgs::MsgType as M;
|
||||
///
|
||||
/// let values = [M::InitHello, M::RespHello, M::InitConf, M::EmptyData, M::CookieReply];
|
||||
/// let values_u8 = values.map(|v| -> u8 { v.into() });
|
||||
///
|
||||
/// // Can be converted to and from u8 using [::std::convert::Into] or [::std::convert::From]
|
||||
/// for v in values.iter().copied() {
|
||||
/// let v_u8 : u8 = v.into();
|
||||
/// let v2 : MsgType = v_u8.try_into()?;
|
||||
/// assert_eq!(v, v2);
|
||||
/// }
|
||||
///
|
||||
/// // Converting an unsupported type produces an error
|
||||
/// let invalid_values = (u8::MIN..=u8::MAX)
|
||||
/// .filter(|v| !values_u8.contains(v));
|
||||
/// for v in invalid_values {
|
||||
/// let res : Result<MsgType, _> = v.try_into();
|
||||
/// assert!(res.is_err());
|
||||
/// }
|
||||
///
|
||||
/// Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
#[repr(u8)]
|
||||
#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
|
||||
pub enum MsgType {
|
||||
/// MsgType for [InitHello]
|
||||
InitHello = 0x81,
|
||||
/// MsgType for [RespHello]
|
||||
RespHello = 0x82,
|
||||
/// MsgType for [InitConf]
|
||||
InitConf = 0x83,
|
||||
/// MsgType for [EmptyData]
|
||||
EmptyData = 0x84,
|
||||
DataMsg = 0x85,
|
||||
/// MsgType for [CookieReply]
|
||||
CookieReply = 0x86,
|
||||
}
|
||||
|
||||
@@ -169,7 +422,6 @@ impl TryFrom<u8> for MsgType {
|
||||
0x82 => MsgType::RespHello,
|
||||
0x83 => MsgType::InitConf,
|
||||
0x84 => MsgType::EmptyData,
|
||||
0x85 => MsgType::DataMsg,
|
||||
0x86 => MsgType::CookieReply,
|
||||
_ => return Err(RosenpassError::InvalidMessageType(value)),
|
||||
})
|
||||
@@ -182,12 +434,6 @@ impl From<MsgType> for u8 {
|
||||
}
|
||||
}
|
||||
|
||||
/// length in bytes of an unencrypted Biscuit (plain text)
|
||||
pub const BISCUIT_PT_LEN: usize = size_of::<Biscuit>();
|
||||
|
||||
/// Length in bytes of an encrypted Biscuit (cipher text)
|
||||
pub const BISCUIT_CT_LEN: usize = BISCUIT_PT_LEN + xaead::NONCE_LEN + xaead::TAG_LEN;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_constants {
|
||||
use crate::msgs::{BISCUIT_CT_LEN, BISCUIT_PT_LEN};
|
||||
|
||||
@@ -1,3 +1,79 @@
|
||||
//! Module containing the cryptographic protocol implementation
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! The most important types in this module probably are [PollResult]
|
||||
//! & [CryptoServer]. Once a [CryptoServer] is created, the server is
|
||||
//! provided with new messages via the [CryptoServer::handle_msg] method.
|
||||
//! The [CryptoServer::poll] method can be used to let the server work, which
|
||||
//! will eventually yield a [PollResult]. Said [PollResult] contains
|
||||
//! prescriptive activities to be carried out. [CryptoServer::osk] can than
|
||||
//! be used to extract the shared key for two peers, once a key-exchange was
|
||||
//! successful.
|
||||
//!
|
||||
//! TODO explain briefly the role of epki
|
||||
//!
|
||||
//! # Example Handshake
|
||||
//!
|
||||
//! This example illustrates a minimal setup for a key-exchange between two
|
||||
//! [CryptoServer]s; this is what we use for some testing purposes but it is not
|
||||
//! what should be used in a real world application, as timing-based events
|
||||
//! are handled by [CryptoServer::poll].
|
||||
//!
|
||||
//! See [CryptoServer::poll] on how to use crypto server in polling mode for production usage.
|
||||
//!
|
||||
//! ```
|
||||
//! use std::ops::DerefMut;
|
||||
//! use rosenpass_secret_memory::policy::*;
|
||||
//! use rosenpass_cipher_traits::Kem;
|
||||
//! use rosenpass_ciphers::kem::StaticKem;
|
||||
//! use rosenpass::{
|
||||
//! protocol::{SSk, SPk, MsgBuf, PeerPtr, CryptoServer, SymKey},
|
||||
//! };
|
||||
//! # fn main() -> anyhow::Result<()> {
|
||||
//! // Set security policy for storing secrets
|
||||
//!
|
||||
//! secret_policy_try_use_memfd_secrets();
|
||||
//!
|
||||
//! // initialize secret and public key for peer a ...
|
||||
//! let (mut peer_a_sk, mut peer_a_pk) = (SSk::zero(), SPk::zero());
|
||||
//! StaticKem::keygen(peer_a_sk.secret_mut(), peer_a_pk.deref_mut())?;
|
||||
//!
|
||||
//! // ... and for peer b
|
||||
//! let (mut peer_b_sk, mut peer_b_pk) = (SSk::zero(), SPk::zero());
|
||||
//! StaticKem::keygen(peer_b_sk.secret_mut(), peer_b_pk.deref_mut())?;
|
||||
//!
|
||||
//! // initialize server and a pre-shared key
|
||||
//! let psk = SymKey::random();
|
||||
//! let mut a = CryptoServer::new(peer_a_sk, peer_a_pk.clone());
|
||||
//! let mut b = CryptoServer::new(peer_b_sk, peer_b_pk.clone());
|
||||
//!
|
||||
//! // introduce peers to each other
|
||||
//! a.add_peer(Some(psk.clone()), peer_b_pk)?;
|
||||
//! b.add_peer(Some(psk), peer_a_pk)?;
|
||||
//!
|
||||
//! // declare buffers for message exchange
|
||||
//! let (mut a_buf, mut b_buf) = (MsgBuf::zero(), MsgBuf::zero());
|
||||
//!
|
||||
//! // let a initiate a handshake
|
||||
//! let mut maybe_len = Some(a.initiate_handshake(PeerPtr(0), a_buf.as_mut_slice())?);
|
||||
//!
|
||||
//! // let a and b communicate
|
||||
//! while let Some(len) = maybe_len {
|
||||
//! maybe_len = b.handle_msg(&a_buf[..len], &mut b_buf[..])?.resp;
|
||||
//! std::mem::swap(&mut a, &mut b);
|
||||
//! std::mem::swap(&mut a_buf, &mut b_buf);
|
||||
//! }
|
||||
//!
|
||||
//! // all done! Extract the shared keys and ensure they are identical
|
||||
//! let a_key = a.osk(PeerPtr(0))?;
|
||||
//! let b_key = b.osk(PeerPtr(0))?;
|
||||
//! assert_eq!(a_key.secret(), b_key.secret(),
|
||||
//! "the key exchanged failed to establish a shared secret");
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
mod build_crypto_server;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod protocol;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ use rosenpass_util::{
|
||||
mio::WriteWithFileDescriptors,
|
||||
zerocopy::ZerocopySliceExt,
|
||||
};
|
||||
use rustix::fd::{AsFd, AsRawFd};
|
||||
use std::os::fd::{AsFd, AsRawFd};
|
||||
use tempfile::TempDir;
|
||||
use zerocopy::AsBytes;
|
||||
|
||||
@@ -33,8 +33,17 @@ struct KillChild(std::process::Child);
|
||||
|
||||
impl Drop for KillChild {
|
||||
fn drop(&mut self) {
|
||||
self.0.kill().discard_result();
|
||||
self.0.wait().discard_result()
|
||||
use rustix::process::{kill_process, Pid, Signal::Term};
|
||||
let pid = Pid::from_child(&self.0);
|
||||
// We seriously need to start handling signals with signalfd, our current signal handling
|
||||
// system is a bit broken; there is probably a few functions that just restart on EINTR
|
||||
// so the signal is absorbed
|
||||
loop {
|
||||
rustix::process::kill_process(pid, Term).discard_result();
|
||||
if self.0.try_wait().unwrap().is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +162,6 @@ fn api_integration_api_setup() -> anyhow::Result<()> {
|
||||
peer_b.config_file_path.to_str().context("")?,
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?,
|
||||
);
|
||||
|
||||
130
rosenpass/tests/app_server_example.rs
Normal file
130
rosenpass/tests/app_server_example.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
ops::DerefMut,
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
sync::mpsc,
|
||||
thread::{self, sleep},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::ensure;
|
||||
use rosenpass::{
|
||||
app_server::{ipv4_any_binding, ipv6_any_binding, AppServer, AppServerTest, MAX_B64_KEY_SIZE},
|
||||
protocol::{SPk, SSk, SymKey},
|
||||
};
|
||||
use rosenpass_cipher_traits::Kem;
|
||||
use rosenpass_ciphers::kem::StaticKem;
|
||||
use rosenpass_secret_memory::Secret;
|
||||
use rosenpass_util::{file::LoadValueB64, functional::run, mem::DiscardResultExt, result::OkExt};
|
||||
|
||||
#[test]
|
||||
fn key_exchange_with_app_server() -> anyhow::Result<()> {
|
||||
let tmpdir = tempfile::tempdir()?;
|
||||
let outfile_a = tmpdir.path().join("osk_a");
|
||||
let outfile_b = tmpdir.path().join("osk_b");
|
||||
|
||||
// Set security policy for storing secrets; choose the one that is faster for testing
|
||||
rosenpass_secret_memory::policy::secret_policy_use_only_malloc_secrets();
|
||||
|
||||
// Introduce the servers to each other
|
||||
let psk_a = SymKey::random();
|
||||
let psk_b = psk_a.clone();
|
||||
|
||||
let (tx_a, rx_b) = mpsc::sync_channel(1);
|
||||
let (tx_b, rx_a) = mpsc::sync_channel(1);
|
||||
|
||||
let (tx_term_a, rx_term_a) = mpsc::channel();
|
||||
let (tx_term_b, rx_term_b) = mpsc::channel();
|
||||
|
||||
let configs = [
|
||||
(false, outfile_a.clone(), psk_a, tx_a, rx_a, rx_term_a),
|
||||
(true, outfile_b.clone(), psk_b, tx_b, rx_b, rx_term_b),
|
||||
];
|
||||
|
||||
for (is_client, osk, psk, tx, rx, rx_term) in configs {
|
||||
thread::spawn(move || {
|
||||
run(move || -> anyhow::Result<()> {
|
||||
let mut srv = TestServer::new(rx_term)?;
|
||||
|
||||
tx.send((srv.loopback_port()?, srv.public_key()?.clone()))?;
|
||||
let (otr_port, otr_pk) = rx.recv()?;
|
||||
|
||||
let psk = Some(psk);
|
||||
let broker_peer = None;
|
||||
let pk = otr_pk;
|
||||
let outfile = Some(osk);
|
||||
let port = otr_port;
|
||||
let hostname = is_client.then(|| format!("[::1]:{port}"));
|
||||
srv.app_srv
|
||||
.add_peer(psk, pk, outfile, broker_peer, hostname)?;
|
||||
|
||||
srv.app_srv.event_loop()
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
// Busy wait for both keys to be exchanged
|
||||
let mut successful_exchange = false;
|
||||
for _ in 0..2000 {
|
||||
// 40s
|
||||
sleep(Duration::from_millis(20));
|
||||
run(|| -> anyhow::Result<()> {
|
||||
let osk_a = SymKey::load_b64::<MAX_B64_KEY_SIZE, _>(&outfile_a)?;
|
||||
let osk_b = SymKey::load_b64::<MAX_B64_KEY_SIZE, _>(&outfile_b)?;
|
||||
successful_exchange = rosenpass_constant_time::memcmp(osk_a.secret(), osk_b.secret());
|
||||
Ok(())
|
||||
})
|
||||
.discard_result();
|
||||
if successful_exchange {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the parties to terminate
|
||||
tx_term_a.send(())?;
|
||||
tx_term_b.send(())?;
|
||||
|
||||
assert!(
|
||||
successful_exchange,
|
||||
"Test did not complete successfully within the deadline"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct TestServer {
|
||||
app_srv: AppServer,
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
fn new(termination_queue: mpsc::Receiver<()>) -> anyhow::Result<Self> {
|
||||
let (mut sk, mut pk) = (SSk::zero(), SPk::zero());
|
||||
StaticKem::keygen(sk.secret_mut(), pk.deref_mut())?;
|
||||
|
||||
let keypair = Some((sk, pk));
|
||||
let addrs = vec![
|
||||
SocketAddr::from_str("[::1]:0")?, // Localhost, any port. For connecting to the test server.
|
||||
// ipv4_any_binding(), // any IPv4 interface
|
||||
// ipv6_any_binding(), // any IPv6 interface
|
||||
];
|
||||
let verbosity = rosenpass::config::Verbosity::Verbose;
|
||||
let test_helpers = Some(AppServerTest {
|
||||
enable_dos_permanently: false,
|
||||
termination_handler: Some(termination_queue),
|
||||
});
|
||||
|
||||
let app_srv = AppServer::new(keypair, addrs, verbosity, test_helpers)?;
|
||||
|
||||
Self { app_srv }.ok()
|
||||
}
|
||||
|
||||
fn loopback_port(&self) -> anyhow::Result<u16> {
|
||||
self.app_srv.sockets[0].local_addr()?.port().ok()
|
||||
}
|
||||
|
||||
fn public_key(&self) -> anyhow::Result<&SPk> {
|
||||
Ok(&self.app_srv.crypto_server()?.spkm)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::fs::File;
|
||||
use std::{
|
||||
fs,
|
||||
net::UdpSocket,
|
||||
@@ -5,9 +6,10 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
|
||||
use clap::Parser;
|
||||
use rosenpass::{app_server::AppServerTestBuilder, cli::CliArgs};
|
||||
use rosenpass::{app_server::AppServerTestBuilder, cli::CliArgs, config::EXAMPLE_CONFIG};
|
||||
use rosenpass_secret_memory::{Public, Secret};
|
||||
use rosenpass_wireguard_broker::{WireguardBrokerMio, WG_KEY_LEN, WG_PEER_LEN};
|
||||
use serial_test::serial;
|
||||
@@ -134,6 +136,46 @@ fn run_server_client_exchange(
|
||||
client_terminate.send(()).unwrap();
|
||||
}
|
||||
|
||||
// verify that EXAMPLE_CONFIG is correct
|
||||
#[test]
|
||||
fn check_example_config() {
|
||||
setup_tests();
|
||||
setup_logging();
|
||||
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let config_path = tmp_dir.path().join("config.toml");
|
||||
let mut config_file = File::create(config_path.to_owned()).unwrap();
|
||||
|
||||
config_file
|
||||
.write_all(
|
||||
EXAMPLE_CONFIG
|
||||
.replace("/path/to", tmp_dir.path().to_str().unwrap())
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = test_bin::get_test_bin(BIN)
|
||||
.args(["gen-keys"])
|
||||
.arg(&config_path)
|
||||
.output()
|
||||
.expect("EXAMPLE_CONFIG not valid");
|
||||
|
||||
fs::copy(
|
||||
tmp_dir.path().join("rp-public-key"),
|
||||
tmp_dir.path().join("rp-peer-public-key"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = test_bin::get_test_bin(BIN)
|
||||
.args(["validate"])
|
||||
.arg(&config_path)
|
||||
.output()
|
||||
.expect("EXAMPLE_CONFIG not valid");
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("has passed all logical checks"));
|
||||
}
|
||||
|
||||
// check that we can exchange keys
|
||||
#[test]
|
||||
#[serial]
|
||||
|
||||
99
rosenpass/tests/main-fn-generates-manpages.rs
Normal file
99
rosenpass/tests/main-fn-generates-manpages.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use rosenpass_util::functional::ApplyExt;
|
||||
|
||||
fn expect_section(manpage: &str, section: &str) -> anyhow::Result<()> {
|
||||
anyhow::ensure!(manpage.lines().any(|line| { line.starts_with(section) }));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expect_sections(manpage: &str, sections: &[&str]) -> anyhow::Result<()> {
|
||||
for section in sections.iter().copied() {
|
||||
expect_section(manpage, section)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expect_contents(manpage: &str, patterns: &[&str]) -> anyhow::Result<()> {
|
||||
for pat in patterns.iter().copied() {
|
||||
anyhow::ensure!(manpage.contains(pat))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn filter_backspace(str: &str) -> anyhow::Result<String> {
|
||||
let mut out = String::new();
|
||||
for chr in str.chars() {
|
||||
if chr == '\x08' {
|
||||
anyhow::ensure!(out.pop().is_some());
|
||||
} else {
|
||||
out.push(chr);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Spot tests about man page generation; these are by far not exhaustive.
|
||||
#[test]
|
||||
fn main_fn_generates_manpages() -> anyhow::Result<()> {
|
||||
let dir = tempfile::TempDir::with_prefix("rosenpass-test-main-fn-generates-mangapges")?;
|
||||
let cmd_out = test_bin::get_test_bin("rosenpass")
|
||||
.args(["--generate-manpage", dir.path().to_str().unwrap()])
|
||||
.output()?;
|
||||
assert!(cmd_out.status.success());
|
||||
|
||||
let expected_manpages = [
|
||||
"rosenpass.1",
|
||||
"rosenpass-exchange.1",
|
||||
"rosenpass-exchange-config.1",
|
||||
"rosenpass-gen-config.1",
|
||||
"rosenpass-gen-keys.1",
|
||||
"rosenpass-keygen.1",
|
||||
"rosenpass-validate.1",
|
||||
];
|
||||
|
||||
let man_texts: std::collections::HashMap<&str, String> = expected_manpages
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|name| (name, dir.path().join(name)))
|
||||
.map(|(name, path)| {
|
||||
let res = std::process::Command::new("man").arg(path).output()?;
|
||||
assert!(res.status.success());
|
||||
let body = res
|
||||
.stdout
|
||||
.apply(String::from_utf8)?
|
||||
.apply(|s| filter_backspace(&s))?;
|
||||
Ok((name, body))
|
||||
})
|
||||
.collect::<anyhow::Result<_>>()?;
|
||||
|
||||
for (name, body) in man_texts.iter() {
|
||||
expect_sections(body, &["NAME", "SYNOPSIS", "OPTIONS"])?;
|
||||
|
||||
if *name != "rosenpass.1" {
|
||||
expect_section(body, "DESCRIPTION")?;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let body = man_texts.get("rosenpass.1").unwrap();
|
||||
expect_sections(
|
||||
body,
|
||||
&["EXIT STATUS", "SEE ALSO", "STANDARDS", "AUTHORS", "BUGS"],
|
||||
)?;
|
||||
expect_contents(
|
||||
body,
|
||||
&[
|
||||
"[--log-level]",
|
||||
"rosenpass-exchange-config(1)",
|
||||
"Start Rosenpass key exchanges based on a configuration file",
|
||||
"https://rosenpass.eu/whitepaper.pdf",
|
||||
],
|
||||
)?;
|
||||
}
|
||||
|
||||
{
|
||||
let body = man_texts.get("rosenpass-exchange.1").unwrap();
|
||||
expect_contents(body, &["[-c|--config-file]", "PSK := preshared-key"])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
10
rosenpass/tests/main-fn-prints-errors.rs
Normal file
10
rosenpass/tests/main-fn-prints-errors.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
#[test]
|
||||
fn main_fn_prints_errors() -> anyhow::Result<()> {
|
||||
let out = test_bin::get_test_bin("rosenpass")
|
||||
.args(["exchange-config", "/"])
|
||||
.output()?;
|
||||
assert!(!out.status.success());
|
||||
assert!(String::from_utf8(out.stderr)?.contains("Is a directory (os error 21)"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
634
rosenpass/tests/poll_example.rs
Normal file
634
rosenpass/tests/poll_example.rs
Normal file
@@ -0,0 +1,634 @@
|
||||
/// This file contains a correct simulation of a two-party key exchange using Poll
|
||||
use std::{
|
||||
borrow::{Borrow, BorrowMut},
|
||||
collections::VecDeque,
|
||||
fmt::{Debug, Write},
|
||||
ops::{DerefMut, RangeBounds},
|
||||
};
|
||||
|
||||
use rand::distributions::uniform::SampleBorrow;
|
||||
use rosenpass_cipher_traits::Kem;
|
||||
use rosenpass_ciphers::kem::StaticKem;
|
||||
use rosenpass_util::result::OkExt;
|
||||
|
||||
use rosenpass::protocol::{
|
||||
testutils::time_travel_forward, CryptoServer, HostIdentification, MsgBuf, PeerPtr, PollResult,
|
||||
SPk, SSk, SymKey, Timing, UNENDING,
|
||||
};
|
||||
|
||||
// TODO: Most of the utility functions in here should probably be moved to
|
||||
// rosenpass::protocol::testutils;
|
||||
|
||||
#[test]
|
||||
fn test_successful_exchange_with_poll() -> anyhow::Result<()> {
|
||||
// Set security policy for storing secrets; choose the one that is faster for testing
|
||||
rosenpass_secret_memory::policy::secret_policy_use_only_malloc_secrets();
|
||||
|
||||
let mut sim = RosenpassSimulator::new()?;
|
||||
sim.poll_loop(150)?; // Poll 75 times
|
||||
let transcript = sim.transcript;
|
||||
|
||||
let completions: Vec<_> = transcript
|
||||
.iter()
|
||||
.filter(|elm| matches!(elm, (_, TranscriptEvent::CompletedExchange(_))))
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!completions.is_empty(),
|
||||
"\
|
||||
Should have performed a successful key exchanged!\n\
|
||||
Transcript: {transcript:?}\n\
|
||||
Completions: {completions:?}\
|
||||
"
|
||||
);
|
||||
assert!(
|
||||
completions[0].0 < 20.0,
|
||||
"\
|
||||
First key exchange should happen in under twenty seconds!\n\
|
||||
Transcript: {transcript:?}\n\
|
||||
Completions: {completions:?}\
|
||||
"
|
||||
);
|
||||
|
||||
assert!(
|
||||
completions.len() >= 3,
|
||||
"\
|
||||
Should have at least two renegotiations!\n\
|
||||
Transcript: {transcript:?}\n\
|
||||
Completions: {completions:?}\
|
||||
"
|
||||
);
|
||||
assert!(
|
||||
(110.0..175.0).contains(&completions[1].0),
|
||||
"\
|
||||
First renegotiation should happen in between two and three minutes!\n\
|
||||
Transcript: {transcript:?}\n\
|
||||
Completions: {completions:?}\
|
||||
"
|
||||
);
|
||||
assert!((110.0..175.0).contains(&(completions[2].0 - completions[1].0)), "\
|
||||
First renegotiation should happen in between two and three minutes after the first renegotiation!\n\
|
||||
Transcript: {transcript:?}\n\
|
||||
Completions: {completions:?}\
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_successful_exchange_under_packet_loss() -> anyhow::Result<()> {
|
||||
// Set security policy for storing secrets; choose the one that is faster for testing
|
||||
rosenpass_secret_memory::policy::secret_policy_use_only_malloc_secrets();
|
||||
|
||||
// Create the simulator
|
||||
let mut sim = RosenpassSimulator::new()?;
|
||||
|
||||
// Make sure the servers are set to under load condition
|
||||
sim.srv_a.under_load = true;
|
||||
sim.srv_b.under_load = false; // See Issue #539 -- https://github.com/rosenpass/rosenpass/issues/539
|
||||
|
||||
// Perform the key exchanges
|
||||
let mut pkg_counter = 0usize;
|
||||
for _ in 0..300 {
|
||||
let ev = sim.poll()?;
|
||||
if let TranscriptEvent::ServerEvent {
|
||||
source,
|
||||
event: ServerEvent::Transmit(_, _),
|
||||
} = ev
|
||||
{
|
||||
// Drop every fifth package
|
||||
if pkg_counter % 10 == 0 {
|
||||
source.drop_outgoing_packet(&mut sim);
|
||||
}
|
||||
|
||||
pkg_counter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let transcript = sim.transcript;
|
||||
let completions: Vec<_> = transcript
|
||||
.iter()
|
||||
.filter(|elm| matches!(elm, (_, TranscriptEvent::CompletedExchange(_))))
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!completions.is_empty(),
|
||||
"\
|
||||
Should have performed a successful key exchanged!\n\
|
||||
Transcript: {transcript:?}\n\
|
||||
Completions: {completions:?}\
|
||||
"
|
||||
);
|
||||
assert!(
|
||||
completions[0].0 < 10.0,
|
||||
"\
|
||||
First key exchange should happen in under twenty seconds!\n\
|
||||
Transcript: {transcript:?}\n\
|
||||
Completions: {completions:?}\
|
||||
"
|
||||
);
|
||||
|
||||
assert!(
|
||||
completions.len() >= 3,
|
||||
"\
|
||||
Should have at least two renegotiations!\n\
|
||||
Transcript: {transcript:?}\n\
|
||||
Completions: {completions:?}\
|
||||
"
|
||||
);
|
||||
assert!(
|
||||
(110.0..175.0).contains(&completions[1].0),
|
||||
"\
|
||||
First renegotiation should happen in between two and three minutes!\n\
|
||||
Transcript: {transcript:?}\n\
|
||||
Completions: {completions:?}\
|
||||
"
|
||||
);
|
||||
assert!((110.0..175.0).contains(&(completions[2].0 - completions[1].0)), "\
|
||||
First renegotiation should happen in between two and three minutes after the first renegotiation!\n\
|
||||
Transcript: {transcript:?}\n\
|
||||
Completions: {completions:?}\
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type MessageType = u8;
|
||||
|
||||
/// Lets record the events that are produced by Rosenpass
|
||||
#[derive(Debug)]
|
||||
#[allow(unused)]
|
||||
enum TranscriptEvent {
|
||||
Wait(Timing),
|
||||
ServerEvent {
|
||||
source: ServerPtr,
|
||||
event: ServerEvent,
|
||||
},
|
||||
CompletedExchange(SymKey),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(unused)]
|
||||
enum ServerEvent {
|
||||
DeleteKey,
|
||||
SendInitiationRequested,
|
||||
SendRetransmissionRequested,
|
||||
Exchanged(SymKey),
|
||||
DiscardInvalidMessage(anyhow::Error),
|
||||
Transmit(MessageType, SendMsgReason),
|
||||
Receive(Option<MessageType>),
|
||||
DroppedPackage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum SendMsgReason {
|
||||
Initiation,
|
||||
Response,
|
||||
Retransmission,
|
||||
}
|
||||
|
||||
impl TranscriptEvent {
|
||||
fn hibernate() -> Self {
|
||||
Self::Wait(UNENDING)
|
||||
}
|
||||
|
||||
fn begin_poll() -> Self {
|
||||
Self::hibernate()
|
||||
}
|
||||
|
||||
fn transmit(source: ServerPtr, buf: &[u8], reason: SendMsgReason) -> Self {
|
||||
assert!(!buf.is_empty());
|
||||
let msg_type = buf[0];
|
||||
ServerEvent::Transmit(msg_type, reason).into_transcript_event(source)
|
||||
}
|
||||
|
||||
fn receive(source: ServerPtr, buf: &[u8]) -> Self {
|
||||
let msg_type = (!buf.is_empty()).then(|| buf[0]);
|
||||
ServerEvent::Receive(msg_type).into_transcript_event(source)
|
||||
}
|
||||
|
||||
pub fn try_fold_with<F: FnOnce() -> anyhow::Result<TranscriptEvent>>(
|
||||
self,
|
||||
f: F,
|
||||
) -> anyhow::Result<TranscriptEvent> {
|
||||
let wait_time_a = match self {
|
||||
Self::Wait(wait_time_a) => wait_time_a,
|
||||
els => return (els).ok(),
|
||||
};
|
||||
|
||||
let wait_time_b = match f()? {
|
||||
Self::Wait(wait_time_b) => wait_time_b,
|
||||
els => return els.ok(),
|
||||
};
|
||||
|
||||
let min_wt = if wait_time_a <= wait_time_b {
|
||||
wait_time_a
|
||||
} else {
|
||||
wait_time_b
|
||||
};
|
||||
Self::Wait(min_wt).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerEvent {
|
||||
fn into_transcript_event(self, source: ServerPtr) -> TranscriptEvent {
|
||||
let event = self;
|
||||
TranscriptEvent::ServerEvent { source, event }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RosenpassSimulator {
|
||||
transcript: Vec<(Timing, TranscriptEvent)>,
|
||||
srv_a: SimulatorServer,
|
||||
srv_b: SimulatorServer,
|
||||
poll_focus: ServerPtr,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum UpcomingPollResult {
|
||||
IssueEvent(TranscriptEvent),
|
||||
SendMessage(Vec<u8>, TranscriptEvent),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SimulatorServer {
|
||||
/// We sometimes return multiple multiple events in one call,
|
||||
/// but [ServerPtr::poll] should return just one event per call
|
||||
upcoming_poll_results: VecDeque<UpcomingPollResult>,
|
||||
srv: CryptoServer,
|
||||
rx_queue: VecDeque<Vec<u8>>,
|
||||
other_peer: PeerPtr,
|
||||
under_load: bool,
|
||||
}
|
||||
|
||||
impl RosenpassSimulator {
|
||||
/// Set up the simulator
|
||||
fn new() -> anyhow::Result<Self> {
|
||||
// Set up the first server
|
||||
let (mut peer_a_sk, mut peer_a_pk) = (SSk::zero(), SPk::zero());
|
||||
StaticKem::keygen(peer_a_sk.secret_mut(), peer_a_pk.deref_mut())?;
|
||||
let mut srv_a = CryptoServer::new(peer_a_sk, peer_a_pk.clone());
|
||||
|
||||
// …and the second server.
|
||||
let (mut peer_b_sk, mut peer_b_pk) = (SSk::zero(), SPk::zero());
|
||||
StaticKem::keygen(peer_b_sk.secret_mut(), peer_b_pk.deref_mut())?;
|
||||
let mut srv_b = CryptoServer::new(peer_b_sk, peer_b_pk.clone());
|
||||
|
||||
// Generate a PSK and introduce the Peers to each other.
|
||||
let psk = SymKey::random();
|
||||
let peer_a = srv_a.add_peer(Some(psk.clone()), peer_b_pk)?;
|
||||
let peer_b = srv_b.add_peer(Some(psk), peer_a_pk)?;
|
||||
|
||||
// Set up the individual server data structures
|
||||
let srv_a = SimulatorServer::new(srv_a, peer_b);
|
||||
let srv_b = SimulatorServer::new(srv_b, peer_a);
|
||||
|
||||
// Initialize transcript and polling state
|
||||
let transcript = Vec::new();
|
||||
let poll_focus = ServerPtr::A;
|
||||
|
||||
// Construct the simulator itself
|
||||
Self {
|
||||
transcript,
|
||||
poll_focus,
|
||||
srv_a,
|
||||
srv_b,
|
||||
}
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Call [poll] a fixed number of times
|
||||
fn poll_loop(&mut self, times: u64) -> anyhow::Result<()> {
|
||||
for _ in 0..times {
|
||||
self.poll()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Every call to poll produces one [TranscriptEvent] and
|
||||
/// and implicitly adds it to [Self:::transcript]
|
||||
fn poll(&mut self) -> anyhow::Result<&TranscriptEvent> {
|
||||
let ev = TranscriptEvent::begin_poll()
|
||||
.try_fold_with(|| self.poll_focus.poll(self))?
|
||||
.try_fold_with(|| {
|
||||
self.poll_focus = self.poll_focus.other();
|
||||
self.poll_focus.poll(self)
|
||||
})?;
|
||||
|
||||
// Generate up a time stamp
|
||||
let now = self.srv_a.srv.timebase.now();
|
||||
|
||||
// Push the event onto the transcript
|
||||
self.transcript.push((now, ev));
|
||||
// We can unwrap; we just pushed the event ourselves
|
||||
let ev = self.transcript.last().unwrap().1.borrow();
|
||||
|
||||
// Time travel instead of waiting
|
||||
if let TranscriptEvent::Wait(secs) = ev {
|
||||
time_travel_forward(&mut self.srv_a.srv, *secs);
|
||||
time_travel_forward(&mut self.srv_b.srv, *secs);
|
||||
}
|
||||
|
||||
ev.ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl SimulatorServer {
|
||||
fn new(srv: CryptoServer, other_peer: PeerPtr) -> Self {
|
||||
let upcoming_poll_results = VecDeque::new();
|
||||
let rx_queue = VecDeque::new();
|
||||
let under_load = false;
|
||||
Self {
|
||||
upcoming_poll_results,
|
||||
srv,
|
||||
rx_queue,
|
||||
other_peer,
|
||||
under_load,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Straightforward way of accessing either of the two servers
|
||||
/// with associated data
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum ServerPtr {
|
||||
A,
|
||||
B,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ServerPtr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{:?}", self))
|
||||
}
|
||||
}
|
||||
|
||||
impl HostIdentification for ServerPtr {
|
||||
fn encode(&self) -> &[u8] {
|
||||
match *self {
|
||||
Self::A => b"ServerPtr::A",
|
||||
Self::B => b"ServerPtr::B",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerPtr {
|
||||
fn poll(self, sim: &mut RosenpassSimulator) -> anyhow::Result<TranscriptEvent> {
|
||||
TranscriptEvent::begin_poll()
|
||||
.try_fold_with(|| self.flush_upcoming_events(sim).ok())?
|
||||
.try_fold_with(|| self.poll_for_timed_events(sim))?
|
||||
.try_fold_with(|| self.process_incoming_messages(sim))
|
||||
}
|
||||
|
||||
/// Returns and applies the first upcoming event
|
||||
fn flush_upcoming_events(self, sim: &mut RosenpassSimulator) -> TranscriptEvent {
|
||||
use UpcomingPollResult as R;
|
||||
match self.get_mut(sim).upcoming_poll_results.pop_front() {
|
||||
None => TranscriptEvent::hibernate(),
|
||||
Some(R::IssueEvent(ev)) => ev,
|
||||
Some(R::SendMessage(msg, ev)) => {
|
||||
self.transmit(sim, msg);
|
||||
ev
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_for_timed_events(
|
||||
self,
|
||||
sim: &mut RosenpassSimulator,
|
||||
) -> anyhow::Result<TranscriptEvent> {
|
||||
use PollResult as P;
|
||||
use ServerEvent as SE;
|
||||
use TranscriptEvent as TE;
|
||||
|
||||
let other_peer = self.peer(sim);
|
||||
|
||||
// Check if there are events to process from poll()
|
||||
loop {
|
||||
match self.srv_mut(sim).poll()? {
|
||||
// Poll just told us to immediately call poll again
|
||||
P::Sleep(0.0) => continue,
|
||||
|
||||
// No event to handle immediately. We can now check to see if there are some
|
||||
// messages to be handled
|
||||
P::Sleep(wait_time) => {
|
||||
return TE::Wait(wait_time).ok();
|
||||
}
|
||||
|
||||
// Not deleting any keys in practice here, since we just push events to the
|
||||
// transcript
|
||||
P::DeleteKey(_) => {
|
||||
return SE::DeleteKey.into_transcript_event(self).ok();
|
||||
}
|
||||
|
||||
P::SendInitiation(_) => {
|
||||
self.enqueue_upcoming_poll_event(
|
||||
sim,
|
||||
SE::SendInitiationRequested.into_transcript_event(self),
|
||||
);
|
||||
|
||||
let mut buf = MsgBuf::zero();
|
||||
let len = self
|
||||
.srv_mut(sim)
|
||||
.initiate_handshake(other_peer, &mut buf[..])?;
|
||||
self.enqueue_upcoming_poll_transmission(
|
||||
sim,
|
||||
buf[..len].to_vec(),
|
||||
SendMsgReason::Initiation,
|
||||
);
|
||||
|
||||
return self.flush_upcoming_events(sim).ok(); // Just added them
|
||||
}
|
||||
|
||||
P::SendRetransmission(_) => {
|
||||
self.enqueue_upcoming_poll_event(
|
||||
sim,
|
||||
SE::SendRetransmissionRequested.into_transcript_event(self),
|
||||
);
|
||||
|
||||
let mut buf = MsgBuf::zero();
|
||||
let len = self
|
||||
.srv_mut(sim)
|
||||
.retransmit_handshake(other_peer, &mut buf[..])?;
|
||||
self.enqueue_upcoming_poll_transmission(
|
||||
sim,
|
||||
buf[..len].to_vec(),
|
||||
SendMsgReason::Retransmission,
|
||||
);
|
||||
|
||||
return self.flush_upcoming_events(sim).ok(); // Just added them
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn process_incoming_messages(
|
||||
self,
|
||||
sim: &mut RosenpassSimulator,
|
||||
) -> anyhow::Result<TranscriptEvent> {
|
||||
use ServerEvent as SE;
|
||||
use TranscriptEvent as TE;
|
||||
|
||||
// Check for a message or exit
|
||||
let rx_msg = match self.recv(sim) {
|
||||
None => return TE::hibernate().ok(),
|
||||
// Actually received a message
|
||||
Some(rx_msg) => rx_msg,
|
||||
};
|
||||
|
||||
// Add info that a message was received into the transcript
|
||||
self.enqueue_upcoming_poll_event(sim, TE::receive(self, rx_msg.borrow()));
|
||||
|
||||
// Let the crypto server handle the message now
|
||||
let mut tx_buf = MsgBuf::zero();
|
||||
let handle_msg_result = if self.get(sim).under_load {
|
||||
self.srv_mut(sim)
|
||||
.handle_msg_under_load(rx_msg.borrow(), tx_buf.borrow_mut(), &self)
|
||||
} else {
|
||||
self.srv_mut(sim)
|
||||
.handle_msg(rx_msg.borrow(), tx_buf.borrow_mut())
|
||||
};
|
||||
|
||||
// Handle bad messages
|
||||
let handle_msg_result = match handle_msg_result {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
self.enqueue_upcoming_poll_event(
|
||||
sim,
|
||||
SE::DiscardInvalidMessage(e).into_transcript_event(self),
|
||||
);
|
||||
return self.flush_upcoming_events(sim).ok(); // Just added them
|
||||
}
|
||||
};
|
||||
|
||||
// Successful key exchange; emit the appropriate event
|
||||
if handle_msg_result.exchanged_with.is_some() {
|
||||
self.enqueue_on_exchanged_events(sim)?;
|
||||
}
|
||||
|
||||
// Handle message responses
|
||||
if let Some(len) = handle_msg_result.resp {
|
||||
let resp = &tx_buf[..len];
|
||||
self.enqueue_upcoming_poll_transmission(sim, resp.to_vec(), SendMsgReason::Response);
|
||||
};
|
||||
|
||||
// Return the first of the events we just enqueued
|
||||
self.flush_upcoming_events(sim).ok()
|
||||
}
|
||||
|
||||
fn enqueue_on_exchanged_events(self, sim: &mut RosenpassSimulator) -> anyhow::Result<()> {
|
||||
use ServerEvent as SE;
|
||||
use TranscriptEvent as TE;
|
||||
|
||||
// Retrieve the key exchanged; this function will panic if the OSK is missing
|
||||
let osk = self.osk(sim).unwrap();
|
||||
|
||||
// Issue the `Exchanged`
|
||||
self.enqueue_upcoming_poll_event(
|
||||
sim,
|
||||
SE::Exchanged(osk.clone()).into_transcript_event(self),
|
||||
);
|
||||
|
||||
// Retrieve the other osk
|
||||
let other_osk = match self.other().try_osk(sim) {
|
||||
Some(other_osk) => other_osk,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// Issue the successful exchange event if the OSKs are equal;
|
||||
// be careful to use constant time comparison for things like this!
|
||||
if rosenpass_constant_time::memcmp(osk.secret(), other_osk.secret()) {
|
||||
self.enqueue_upcoming_poll_event(sim, TE::CompletedExchange(osk));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enqueue_upcoming_poll_event(self, sim: &mut RosenpassSimulator, ev: TranscriptEvent) {
|
||||
let upcoming = UpcomingPollResult::IssueEvent(ev);
|
||||
self.get_mut(sim).upcoming_poll_results.push_back(upcoming);
|
||||
}
|
||||
|
||||
fn enqueue_upcoming_poll_transmission(
|
||||
self,
|
||||
sim: &mut RosenpassSimulator,
|
||||
msg: Vec<u8>,
|
||||
reason: SendMsgReason,
|
||||
) {
|
||||
let ev = TranscriptEvent::transmit(self, msg.borrow(), reason);
|
||||
let upcoming = UpcomingPollResult::SendMessage(msg, ev);
|
||||
self.get_mut(sim).upcoming_poll_results.push_back(upcoming);
|
||||
}
|
||||
|
||||
fn try_osk(self, sim: &RosenpassSimulator) -> Option<SymKey> {
|
||||
let peer = self.peer(sim);
|
||||
let has_osk = peer.session().get(self.srv(sim)).is_some();
|
||||
|
||||
has_osk.then(|| {
|
||||
// We already checked whether the OSK is present; there should be no other errors
|
||||
self.osk(sim).unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
fn osk(self, sim: &RosenpassSimulator) -> anyhow::Result<SymKey> {
|
||||
self.srv(sim).osk(self.peer(sim))
|
||||
}
|
||||
|
||||
fn drop_outgoing_packet(self, sim: &mut RosenpassSimulator) -> Option<Vec<u8>> {
|
||||
let pkg = self.tx_queue_mut(sim).pop_front();
|
||||
self.enqueue_upcoming_poll_event(
|
||||
sim,
|
||||
ServerEvent::DroppedPackage.into_transcript_event(self),
|
||||
);
|
||||
pkg
|
||||
}
|
||||
|
||||
fn other(self) -> Self {
|
||||
match self {
|
||||
Self::A => Self::B,
|
||||
Self::B => Self::A,
|
||||
}
|
||||
}
|
||||
|
||||
fn get(self, sim: &RosenpassSimulator) -> &SimulatorServer {
|
||||
match self {
|
||||
ServerPtr::A => sim.srv_a.borrow(),
|
||||
ServerPtr::B => sim.srv_b.borrow(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_mut(self, sim: &mut RosenpassSimulator) -> &mut SimulatorServer {
|
||||
match self {
|
||||
ServerPtr::A => sim.srv_a.borrow_mut(),
|
||||
ServerPtr::B => sim.srv_b.borrow_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
fn srv(self, sim: &RosenpassSimulator) -> &CryptoServer {
|
||||
self.get(sim).srv.borrow()
|
||||
}
|
||||
|
||||
fn srv_mut(self, sim: &mut RosenpassSimulator) -> &mut CryptoServer {
|
||||
self.get_mut(sim).srv.borrow_mut()
|
||||
}
|
||||
|
||||
fn peer(self, sim: &RosenpassSimulator) -> PeerPtr {
|
||||
self.get(sim).other_peer
|
||||
}
|
||||
|
||||
fn recv(self, sim: &mut RosenpassSimulator) -> Option<Vec<u8>> {
|
||||
self.rx_queue_mut(sim).pop_front()
|
||||
}
|
||||
|
||||
fn transmit(self, sim: &mut RosenpassSimulator, msg: Vec<u8>) {
|
||||
self.tx_queue_mut(sim).push_back(msg);
|
||||
}
|
||||
|
||||
fn rx_queue_mut(self, sim: &mut RosenpassSimulator) -> &mut VecDeque<Vec<u8>> {
|
||||
self.get_mut(sim).rx_queue.borrow_mut()
|
||||
}
|
||||
|
||||
fn tx_queue_mut(self, sim: &mut RosenpassSimulator) -> &mut VecDeque<Vec<u8>> {
|
||||
self.other().rx_queue_mut(sim)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ repository = "https://github.com/rosenpass/rosenpass"
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
base64ct = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
x25519-dalek = { version = "2", features = ["static_secrets"] }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
@@ -24,10 +26,11 @@ rosenpass-wireguard-broker = { workspace = true }
|
||||
|
||||
tokio = { workspace = true }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||
ctrlc-async = "3.2"
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||
ctrlc-async = "3.2"
|
||||
genetlink = "0.2"
|
||||
rtnetlink = "0.14"
|
||||
netlink-packet-core = "0.7"
|
||||
|
||||
@@ -3,6 +3,11 @@ use std::{iter::Peekable, net::SocketAddr};
|
||||
|
||||
use crate::exchange::{ExchangeOptions, ExchangePeer};
|
||||
|
||||
/// The different commands supported by the `rp` binary.
|
||||
/// [GenKey](crate::cli::Command::GenKey), [PubKey](crate::cli::Command::PubKey),
|
||||
/// [Exchange](crate::cli::Command::Exchange) and
|
||||
/// [ExchangeConfig](crate::cli::Command::ExchangeConfig)
|
||||
/// contain information specific to the respective command.
|
||||
pub enum Command {
|
||||
GenKey {
|
||||
private_keys_dir: PathBuf,
|
||||
@@ -12,33 +17,57 @@ pub enum Command {
|
||||
public_keys_dir: PathBuf,
|
||||
},
|
||||
Exchange(ExchangeOptions),
|
||||
ExchangeConfig {
|
||||
config_file: PathBuf,
|
||||
},
|
||||
Help,
|
||||
}
|
||||
|
||||
/// The different command types supported by the `rp` binary.
|
||||
/// This enum is exclusively used in [fatal] and when calling [fatal] and is therefore
|
||||
/// limited to the command types that can fail. E.g., the help command can not fail and is therefore
|
||||
/// not part of the [CommandType]-enum.
|
||||
enum CommandType {
|
||||
GenKey,
|
||||
PubKey,
|
||||
Exchange,
|
||||
ExchangeConfig,
|
||||
}
|
||||
|
||||
/// This structure captures the result of parsing the arguments to the `rp` binary.
|
||||
/// A new [Cli] is created by calling [Cli::parse] with the appropriate arguments.
|
||||
#[derive(Default)]
|
||||
pub struct Cli {
|
||||
/// Whether the output should be verbose.
|
||||
pub verbose: bool,
|
||||
/// The command specified by the given arguments.
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
|
||||
/// Processes a fatal error when parsing cli arguments.
|
||||
/// It *always* returns an [Err(String)], where such that the contained [String] explains
|
||||
/// the parsing error, including the provided `note`.
|
||||
///
|
||||
/// # Generic Parameters
|
||||
/// the generic parameter `T` is given to make the [Result]-type compatible with the respective
|
||||
/// return type of the calling function.
|
||||
///
|
||||
fn fatal<T>(note: &str, command: Option<CommandType>) -> Result<T, String> {
|
||||
match command {
|
||||
Some(command) => match command {
|
||||
CommandType::GenKey => Err(format!("{}\nUsage: rp genkey PRIVATE_KEYS_DIR", note)),
|
||||
CommandType::PubKey => Err(format!("{}\nUsage: rp pubkey PRIVATE_KEYS_DIR PUBLIC_KEYS_DIR", note)),
|
||||
CommandType::Exchange => Err(format!("{}\nUsage: rp exchange PRIVATE_KEYS_DIR [dev <device>] [listen <ip>:<port>] [peer PUBLIC_KEYS_DIR [endpoint <ip>:<port>] [persistent-keepalive <interval>] [allowed-ips <ip1>/<cidr1>[,<ip2>/<cidr2>]...]]...", note)),
|
||||
CommandType::Exchange => Err(format!("{}\nUsage: rp exchange PRIVATE_KEYS_DIR [dev <device>] [ip <ip1>/<cidr1>] [listen <ip>:<port>] [peer PUBLIC_KEYS_DIR [endpoint <ip>:<port>] [persistent-keepalive <interval>] [allowed-ips <ip1>/<cidr1>[,<ip2>/<cidr2>]...]]...", note)),
|
||||
CommandType::ExchangeConfig => Err(format!("{}\nUsage: rp exchange-config <CONFIG_FILE>", note)),
|
||||
},
|
||||
None => Err(format!("{}\nUsage: rp [verbose] genkey|pubkey|exchange [ARGS]...", note)),
|
||||
None => Err(format!("{}\nUsage: rp [verbose] genkey|pubkey|exchange|exchange-config [ARGS]...", note)),
|
||||
}
|
||||
}
|
||||
|
||||
impl ExchangePeer {
|
||||
/// Parses peer parameters given to the `rp` binary in the context of an `exchange` operation.
|
||||
/// It returns a result with either [ExchangePeer] that contains the parameters of the peer
|
||||
/// or an error describing why the arguments could not be parsed.
|
||||
pub fn parse(args: &mut &mut Peekable<impl Iterator<Item = String>>) -> Result<Self, String> {
|
||||
let mut peer = ExchangePeer::default();
|
||||
|
||||
@@ -121,6 +150,9 @@ impl ExchangePeer {
|
||||
}
|
||||
|
||||
impl ExchangeOptions {
|
||||
/// Parses the arguments given to the `rp` binary *if the `exchange` operation is given*.
|
||||
/// It returns a result with either [ExchangeOptions] that contains the result of parsing the
|
||||
/// arguments or an error describing why the arguments could not be parsed.
|
||||
pub fn parse(mut args: &mut Peekable<impl Iterator<Item = String>>) -> Result<Self, String> {
|
||||
let mut options = ExchangeOptions::default();
|
||||
|
||||
@@ -144,6 +176,13 @@ impl ExchangeOptions {
|
||||
return fatal("dev option requires parameter", Some(CommandType::Exchange));
|
||||
}
|
||||
}
|
||||
"ip" => {
|
||||
if let Some(ip) = args.next() {
|
||||
options.ip = Some(ip);
|
||||
} else {
|
||||
return fatal("ip option requires parameter", Some(CommandType::Exchange));
|
||||
}
|
||||
}
|
||||
"listen" => {
|
||||
if let Some(addr) = args.next() {
|
||||
if let Ok(addr) = addr.parse::<SocketAddr>() {
|
||||
@@ -179,6 +218,9 @@ impl ExchangeOptions {
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
/// Parses the arguments given to the `rp` binary. It returns a result with either
|
||||
/// a [Cli] that contains the result of parsing the arguments or an error describing
|
||||
/// why the arguments could not be parsed.
|
||||
pub fn parse(mut args: Peekable<impl Iterator<Item = String>>) -> Result<Self, String> {
|
||||
let mut cli = Cli::default();
|
||||
|
||||
@@ -246,6 +288,21 @@ impl Cli {
|
||||
let options = ExchangeOptions::parse(&mut args)?;
|
||||
cli.command = Some(Command::Exchange(options));
|
||||
}
|
||||
"exchange-config" => {
|
||||
if cli.command.is_some() {
|
||||
return fatal("Too many commands supplied", None);
|
||||
}
|
||||
|
||||
if let Some(config_file) = args.next() {
|
||||
let config_file = PathBuf::from(config_file);
|
||||
cli.command = Some(Command::ExchangeConfig { config_file });
|
||||
} else {
|
||||
return fatal(
|
||||
"Required position argument: CONFIG_FILE",
|
||||
Some(CommandType::ExchangeConfig),
|
||||
);
|
||||
}
|
||||
}
|
||||
"help" => {
|
||||
cli.command = Some(Command::Help);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,47 @@
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
use anyhow::Error;
|
||||
use serde::Deserialize;
|
||||
use std::future::Future;
|
||||
use std::ops::DerefMut;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::{net::SocketAddr, path::PathBuf, process::Command};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use crate::key::WG_B64_LEN;
|
||||
|
||||
#[derive(Default)]
|
||||
/// Used to define a peer for the rosenpass connection that consists of
|
||||
/// a directory for storing public keys and optionally an IP address and port of the endpoint,
|
||||
/// for how long the connection should be kept alive and a list of allowed IPs for the peer.
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct ExchangePeer {
|
||||
/// Directory where public keys are stored
|
||||
pub public_keys_dir: PathBuf,
|
||||
/// The IP address of the endpoint
|
||||
pub endpoint: Option<SocketAddr>,
|
||||
/// For how long to keep the connection alive
|
||||
pub persistent_keepalive: Option<u32>,
|
||||
/// The IPs that are allowed for this peer.
|
||||
pub allowed_ips: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
/// Options for the exchange operation of the `rp` binary.
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct ExchangeOptions {
|
||||
/// Whether the cli output should be verbose.
|
||||
pub verbose: bool,
|
||||
/// path to the directory where private keys are stored.
|
||||
pub private_keys_dir: PathBuf,
|
||||
/// The link rosenpass should run as. If None is given [exchange] will use `"rosenpass0"`
|
||||
/// instead.
|
||||
pub dev: Option<String>,
|
||||
/// The IP-address rosenpass should run under.
|
||||
pub ip: Option<String>,
|
||||
/// The IP-address and port that the rosenpass [AppServer](rosenpass::app_server::AppServer)
|
||||
/// should use.
|
||||
pub listen: Option<SocketAddr>,
|
||||
/// Other peers a connection should be initialized to
|
||||
pub peers: Vec<ExchangePeer>,
|
||||
}
|
||||
|
||||
@@ -41,8 +64,11 @@ mod netlink {
|
||||
use netlink_packet_wireguard::nlas::WgDeviceAttrs;
|
||||
use rtnetlink::Handle;
|
||||
|
||||
/// Creates a netlink named `link_name` and changes the state to up. It returns the index
|
||||
/// of the interface in the list of interfaces as the result or an error if any of the
|
||||
/// operations of creating the link or changing its state to up fails.
|
||||
pub async fn link_create_and_up(rtnetlink: &Handle, link_name: String) -> Result<u32> {
|
||||
// add the link
|
||||
// Add the link, equivalent to `ip link add <link_name> type wireguard`.
|
||||
rtnetlink
|
||||
.link()
|
||||
.add()
|
||||
@@ -50,7 +76,8 @@ mod netlink {
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
// retrieve the link to be able to up it
|
||||
// Retrieve the link to be able to up it, equivalent to `ip link show` and then
|
||||
// using the link shown that is identified by `link_name`.
|
||||
let link = rtnetlink
|
||||
.link()
|
||||
.get()
|
||||
@@ -62,7 +89,7 @@ mod netlink {
|
||||
.0
|
||||
.unwrap()?;
|
||||
|
||||
// up the link
|
||||
// Up the link, equivalent to `ip link set dev <DEV> up`.
|
||||
rtnetlink
|
||||
.link()
|
||||
.set(link.header.index)
|
||||
@@ -73,12 +100,16 @@ mod netlink {
|
||||
Ok(link.header.index)
|
||||
}
|
||||
|
||||
/// Deletes a link using rtnetlink. The link is specified using its index in the list of links.
|
||||
pub async fn link_cleanup(rtnetlink: &Handle, index: u32) -> Result<()> {
|
||||
rtnetlink.link().del(index).execute().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a link using rtnetlink. The link is specified using its index in the list of links.
|
||||
/// In contrast to [link_cleanup], this function create a new socket connection to netlink and
|
||||
/// *ignores errors* that occur during deletion.
|
||||
pub async fn link_cleanup_standalone(index: u32) -> Result<()> {
|
||||
let (connection, rtnetlink, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(connection);
|
||||
@@ -104,7 +135,7 @@ mod netlink {
|
||||
use netlink_packet_generic::GenlMessage;
|
||||
use netlink_packet_wireguard::{Wireguard, WireguardCmd};
|
||||
|
||||
// Scope our `set` command to only the device of the specified index
|
||||
// Scope our `set` command to only the device of the specified index.
|
||||
attr.insert(0, WgDeviceAttrs::IfIndex(index));
|
||||
|
||||
// Construct the WireGuard-specific netlink packet
|
||||
@@ -113,12 +144,12 @@ mod netlink {
|
||||
nlas: attr,
|
||||
};
|
||||
|
||||
// Construct final message
|
||||
// Construct final message.
|
||||
let genl = GenlMessage::from_payload(wgc);
|
||||
let mut nlmsg = NetlinkMessage::from(genl);
|
||||
nlmsg.header.flags = NLM_F_REQUEST | NLM_F_ACK;
|
||||
|
||||
// Send and wait for the ACK or error
|
||||
// Send and wait for the ACK or error.
|
||||
let (res, _) = genetlink.request(nlmsg).await?.into_future().await;
|
||||
if let Some(res) = res {
|
||||
let res = res?;
|
||||
@@ -131,6 +162,38 @@ mod netlink {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for a list of cleanup handlers that can be used in an asynchronous context
|
||||
/// to clean up after the usage of rosenpass or if the `rp` binary is interrupted with ctrl+c
|
||||
/// or a `SIGINT` signal in general.
|
||||
#[derive(Clone)]
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
struct CleanupHandlers(
|
||||
Arc<::futures::lock::Mutex<Vec<Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>>>>,
|
||||
);
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
impl CleanupHandlers {
|
||||
/// Creates a new list of [CleanupHandlers].
|
||||
fn new() -> Self {
|
||||
CleanupHandlers(Arc::new(::futures::lock::Mutex::new(vec![])))
|
||||
}
|
||||
|
||||
/// Enqueues a new cleanup handler in the form of a [Future].
|
||||
async fn enqueue(&self, handler: Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>) {
|
||||
self.0.lock().await.push(Box::pin(handler))
|
||||
}
|
||||
|
||||
/// Runs all cleanup handlers. Following the documentation of [futures::future::try_join_all]:
|
||||
/// If any cleanup handler returns an error then all other cleanup handlers will be canceled and
|
||||
/// an error will be returned immediately. If all cleanup handlers complete successfully,
|
||||
/// however, then the returned future will succeed with a Vec of all the successful results.
|
||||
async fn run(self) -> Result<Vec<()>, Error> {
|
||||
futures::future::try_join_all(self.0.lock().await.deref_mut()).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up the rosenpass link and wireguard and configures both with the configuration specified by
|
||||
/// `options`.
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub async fn exchange(options: ExchangeOptions) -> Result<()> {
|
||||
use std::fs;
|
||||
@@ -151,16 +214,54 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> {
|
||||
let (connection, rtnetlink, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
let link_name = options.dev.unwrap_or("rosenpass0".to_string());
|
||||
let link_name = options.dev.clone().unwrap_or("rosenpass0".to_string());
|
||||
let link_index = netlink::link_create_and_up(&rtnetlink, link_name.clone()).await?;
|
||||
|
||||
// Set up a list of (initiallc empty) cleanup handlers that are to be run if
|
||||
// ctrl-c is hit or generally a `SIGINT` signal is received and always in the end.
|
||||
let cleanup_handlers = CleanupHandlers::new();
|
||||
let final_cleanup_handlers = (&cleanup_handlers).clone();
|
||||
|
||||
cleanup_handlers
|
||||
.enqueue(Box::pin(async move {
|
||||
netlink::link_cleanup_standalone(link_index).await
|
||||
}))
|
||||
.await;
|
||||
|
||||
ctrlc_async::set_async_handler(async move {
|
||||
netlink::link_cleanup_standalone(link_index)
|
||||
final_cleanup_handlers
|
||||
.run()
|
||||
.await
|
||||
.expect("Failed to clean up");
|
||||
})?;
|
||||
|
||||
// Deploy the classic wireguard private key
|
||||
// Run `ip address add <ip> dev <dev>` and enqueue `ip address del <ip> dev <dev>` as a cleanup.
|
||||
if let Some(ip) = options.ip {
|
||||
let dev = options.dev.clone().unwrap_or("rosenpass0".to_string());
|
||||
Command::new("ip")
|
||||
.arg("address")
|
||||
.arg("add")
|
||||
.arg(ip.clone())
|
||||
.arg("dev")
|
||||
.arg(dev.clone())
|
||||
.status()
|
||||
.expect("failed to configure ip");
|
||||
cleanup_handlers
|
||||
.enqueue(Box::pin(async move {
|
||||
Command::new("ip")
|
||||
.arg("address")
|
||||
.arg("del")
|
||||
.arg(ip)
|
||||
.arg("dev")
|
||||
.arg(dev)
|
||||
.status()
|
||||
.expect("failed to remove ip");
|
||||
Ok(())
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
|
||||
// Deploy the classic wireguard private key.
|
||||
let (connection, mut genetlink, _) = genetlink::new_connection()?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
@@ -181,6 +282,7 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> {
|
||||
|
||||
netlink::wg_set(&mut genetlink, link_index, attr).await?;
|
||||
|
||||
// set up the rosenpass AppServer
|
||||
let pqsk = options.private_keys_dir.join("pqsk");
|
||||
let pqpk = options.private_keys_dir.join("pqpk");
|
||||
|
||||
@@ -208,6 +310,7 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> {
|
||||
anyhow::Error::msg(format!("NativeUnixBrokerConfigBaseBuilderError: {:?}", e))
|
||||
}
|
||||
|
||||
// Configure everything per peer.
|
||||
for peer in options.peers {
|
||||
let wgpk = peer.public_keys_dir.join("wgpk");
|
||||
let pqpk = peer.public_keys_dir.join("pqpk");
|
||||
@@ -254,6 +357,30 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> {
|
||||
broker_peer,
|
||||
peer.endpoint.map(|x| x.to_string()),
|
||||
)?;
|
||||
|
||||
// Configure routes, equivalent to `ip route replace <allowed_ips> dev <dev>` and set up
|
||||
// the cleanup as `ip route del <allowed_ips>`.
|
||||
if let Some(allowed_ips) = peer.allowed_ips {
|
||||
Command::new("ip")
|
||||
.arg("route")
|
||||
.arg("replace")
|
||||
.arg(allowed_ips.clone())
|
||||
.arg("dev")
|
||||
.arg(options.dev.clone().unwrap_or("rosenpass0".to_string()))
|
||||
.status()
|
||||
.expect("failed to configure route");
|
||||
cleanup_handlers
|
||||
.enqueue(Box::pin(async move {
|
||||
Command::new("ip")
|
||||
.arg("route")
|
||||
.arg("del")
|
||||
.arg(allowed_ips)
|
||||
.status()
|
||||
.expect("failed to remove ip");
|
||||
Ok(())
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let out = srv.event_loop();
|
||||
@@ -263,7 +390,8 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> {
|
||||
match out {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
// Check if the returned error is actually EINTR, in which case, the run actually succeeded.
|
||||
// Check if the returned error is actually EINTR, in which case, the run actually
|
||||
// succeeded.
|
||||
let is_ok = if let Some(e) = e.root_cause().downcast_ref::<std::io::Error>() {
|
||||
matches!(e.kind(), std::io::ErrorKind::Interrupted)
|
||||
} else {
|
||||
|
||||
@@ -14,6 +14,7 @@ use rosenpass_cipher_traits::Kem;
|
||||
use rosenpass_ciphers::kem::StaticKem;
|
||||
use rosenpass_secret_memory::{file::StoreSecret as _, Public, Secret};
|
||||
|
||||
/// The length of wireguard keys as a length in base 64 encoding.
|
||||
pub const WG_B64_LEN: usize = 32 * 5 / 3;
|
||||
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
@@ -24,6 +25,14 @@ pub fn genkey(_: &Path) -> Result<()> {
|
||||
))
|
||||
}
|
||||
|
||||
/// Generates a new symmetric keys for wireguard and asymmetric keys for rosenpass
|
||||
/// in the provided `private_keys_dir`.
|
||||
///
|
||||
/// It checks whether the directory `private_keys_dir` points to exists and creates it otherwise.
|
||||
/// If it exists, it ensures that the permission is set to 0700 and aborts otherwise. If the
|
||||
/// directory is newly created, the appropriate permissions are set.
|
||||
///
|
||||
/// Already existing keys are not overwritten.
|
||||
#[cfg(target_family = "unix")]
|
||||
pub fn genkey(private_keys_dir: &Path) -> Result<()> {
|
||||
if private_keys_dir.exists() {
|
||||
@@ -70,6 +79,11 @@ pub fn genkey(private_keys_dir: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a new directory under `public_keys_dir` and stores the public keys for rosenpass and for
|
||||
/// wireguard that correspond to the private keys in `private_keys_dir` in `public_keys_dir`.
|
||||
///
|
||||
/// If `public_keys_dir` already exists, the wireguard private key or the rosenpass public key
|
||||
/// are not present in `private_keys_dir`, an error is returned.
|
||||
pub fn pubkey(private_keys_dir: &Path, public_keys_dir: &Path) -> Result<()> {
|
||||
if public_keys_dir.exists() {
|
||||
return Err(anyhow!("Directory {:?} already exists", public_keys_dir));
|
||||
@@ -90,9 +104,11 @@ pub fn pubkey(private_keys_dir: &Path, public_keys_dir: &Path) -> Result<()> {
|
||||
Public::from_slice(public.as_bytes())
|
||||
};
|
||||
|
||||
// Store the wireguard public key.
|
||||
wgpk.store_b64::<WG_B64_LEN, _>(public_wgpk)?;
|
||||
wgpk.zeroize();
|
||||
|
||||
// Copy the pq-public key to the public directory.
|
||||
fs::copy(private_pqpk, public_pqpk)?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::process::exit;
|
||||
use std::{fs, process::exit};
|
||||
|
||||
use cli::{Cli, Command};
|
||||
use exchange::exchange;
|
||||
@@ -36,6 +36,13 @@ async fn main() {
|
||||
options.verbose = cli.verbose;
|
||||
exchange(options).await
|
||||
}
|
||||
Command::ExchangeConfig { config_file } => {
|
||||
let s: String = fs::read_to_string(config_file).expect("cannot read config");
|
||||
let mut options: exchange::ExchangeOptions =
|
||||
toml::from_str::<exchange::ExchangeOptions>(&s).expect("cannot parse config");
|
||||
options.verbose = options.verbose || cli.verbose;
|
||||
exchange(options).await
|
||||
}
|
||||
Command::Help => {
|
||||
println!("Usage: rp [verbose] genkey|pubkey|exchange [ARGS]...");
|
||||
Ok(())
|
||||
|
||||
2
systemd/rosenpass.target
Normal file
2
systemd/rosenpass.target
Normal file
@@ -0,0 +1,2 @@
|
||||
[Unit]
|
||||
Description=Rosenpass target
|
||||
47
systemd/rosenpass@.service
Normal file
47
systemd/rosenpass@.service
Normal file
@@ -0,0 +1,47 @@
|
||||
[Unit]
|
||||
Description=Rosenpass key exchange for %I
|
||||
Documentation=man:rosenpass(1)
|
||||
Documentation=https://rosenpass.eu/docs
|
||||
|
||||
After=network-online.target nss-lookup.target sys-devices-virtual-net-%i.device
|
||||
Wants=network-online.target nss-lookup.target
|
||||
BindsTo=sys-devices-virtual-net-%i.device
|
||||
PartOf=rosenpass.target
|
||||
|
||||
[Service]
|
||||
ExecStart=rosenpass exchange-config /etc/rosenpass/%i.toml
|
||||
LoadCredential=pqsk:/etc/rosenpass/%i/pqsk
|
||||
|
||||
AmbientCapabilities=CAP_NET_ADMIN
|
||||
CapabilityBoundingSet=~CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER CAP_IPC_LOCK CAP_KILL CAP_LEASE CAP_LINUX_IMMUTABLE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_MKNOD CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_RAW CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_SYS_ADMIN CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYSLOG CAP_SYS_MODULE CAP_SYS_NICE CAP_SYS_RESOURCE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_RAWIO CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_WAKE_ALARM
|
||||
DynamicUser=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
PrivateDevices=true
|
||||
ProcSubset=pid
|
||||
ProtectClock=true
|
||||
ProtectControlGroups=true
|
||||
ProtectHome=true
|
||||
ProtectHostname=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectProc=noaccess
|
||||
RestrictAddressFamilies=AF_NETLINK AF_INET AF_INET6
|
||||
RestrictNamespaces=true
|
||||
RestrictRealtime=true
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=~@clock
|
||||
SystemCallFilter=~@cpu-emulation
|
||||
SystemCallFilter=~@debug
|
||||
SystemCallFilter=~@module
|
||||
SystemCallFilter=~@mount
|
||||
SystemCallFilter=~@obsolete
|
||||
SystemCallFilter=~@privileged
|
||||
SystemCallFilter=~@raw-io
|
||||
SystemCallFilter=~@reboot
|
||||
SystemCallFilter=~@swap
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
48
systemd/rp@.service
Normal file
48
systemd/rp@.service
Normal file
@@ -0,0 +1,48 @@
|
||||
[Unit]
|
||||
Description=Rosenpass key exchange for %I
|
||||
Documentation=man:rosenpass(1)
|
||||
Documentation=https://rosenpass.eu/docs
|
||||
|
||||
After=network-online.target nss-lookup.target
|
||||
Wants=network-online.target nss-lookup.target
|
||||
PartOf=rosenpass.target
|
||||
|
||||
[Service]
|
||||
ExecStart=rp exchange-config /etc/rosenpass/%i.toml
|
||||
LoadCredential=pqpk:/etc/rosenpass/%i/pqpk
|
||||
LoadCredential=pqsk:/etc/rosenpass/%i/pqsk
|
||||
LoadCredential=wgsk:/etc/rosenpass/%i/wgsk
|
||||
|
||||
AmbientCapabilities=CAP_NET_ADMIN
|
||||
CapabilityBoundingSet=~CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER CAP_IPC_LOCK CAP_KILL CAP_LEASE CAP_LINUX_IMMUTABLE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_MKNOD CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_RAW CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_SYS_ADMIN CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYSLOG CAP_SYS_MODULE CAP_SYS_NICE CAP_SYS_RESOURCE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_RAWIO CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_WAKE_ALARM
|
||||
DynamicUser=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
PrivateDevices=true
|
||||
ProcSubset=pid
|
||||
ProtectClock=true
|
||||
ProtectControlGroups=true
|
||||
ProtectHome=true
|
||||
ProtectHostname=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectProc=noaccess
|
||||
RestrictAddressFamilies=AF_NETLINK AF_INET AF_INET6
|
||||
RestrictNamespaces=true
|
||||
RestrictRealtime=true
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=~@clock
|
||||
SystemCallFilter=~@cpu-emulation
|
||||
SystemCallFilter=~@debug
|
||||
SystemCallFilter=~@module
|
||||
SystemCallFilter=~@mount
|
||||
SystemCallFilter=~@obsolete
|
||||
SystemCallFilter=~@privileged
|
||||
SystemCallFilter=~@raw-io
|
||||
SystemCallFilter=~@reboot
|
||||
SystemCallFilter=~@swap
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
183
tests/systemd/rosenpass.nix
Normal file
183
tests/systemd/rosenpass.nix
Normal file
@@ -0,0 +1,183 @@
|
||||
# This test is largely inspired from:
|
||||
# https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/rosenpass.nix
|
||||
# https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/wireguard/basic.nix
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
server = {
|
||||
ip4 = "192.168.0.1";
|
||||
ip6 = "fd00::1";
|
||||
wg = {
|
||||
ip4 = "10.23.42.1";
|
||||
ip6 = "fc00::1";
|
||||
public = "mQufmDFeQQuU/fIaB2hHgluhjjm1ypK4hJr1cW3WqAw=";
|
||||
secret = "4N5Y1dldqrpsbaEiY8O0XBUGUFf8vkvtBtm8AoOX7Eo=";
|
||||
listen = 10000;
|
||||
};
|
||||
};
|
||||
|
||||
client = {
|
||||
ip4 = "192.168.0.2";
|
||||
ip6 = "fd00::2";
|
||||
wg = {
|
||||
ip4 = "10.23.42.2";
|
||||
ip6 = "fc00::2";
|
||||
public = "Mb3GOlT7oS+F3JntVKiaD7SpHxLxNdtEmWz/9FMnRFU=";
|
||||
secret = "uC5dfGMv7Oxf5UDfdPkj6rZiRZT2dRWp5x8IQxrNcUE=";
|
||||
};
|
||||
};
|
||||
|
||||
server_config = {
|
||||
listen = [ "0.0.0.0:9999" ];
|
||||
public_key = "/etc/rosenpass/rp0/pqpk";
|
||||
secret_key = "/run/credentials/rosenpass@rp0.service/pqsk";
|
||||
verbosity = "Verbose";
|
||||
peers = [{
|
||||
device = "rp0";
|
||||
peer = client.wg.public;
|
||||
public_key = "/etc/rosenpass/rp0/peers/client/pqpk";
|
||||
}];
|
||||
};
|
||||
client_config = {
|
||||
listen = [ ];
|
||||
public_key = "/etc/rosenpass/rp0/pqpk";
|
||||
secret_key = "/run/credentials/rosenpass@rp0.service/pqsk";
|
||||
verbosity = "Verbose";
|
||||
peers = [{
|
||||
device = "rp0";
|
||||
peer = server.wg.public;
|
||||
public_key = "/etc/rosenpass/rp0/peers/server/pqpk";
|
||||
endpoint = "${server.ip4}:9999";
|
||||
}];
|
||||
};
|
||||
|
||||
config = pkgs.runCommand "config" { } ''
|
||||
mkdir -pv $out
|
||||
cp -v ${(pkgs.formats.toml {}).generate "rp0.toml" server_config} $out/server
|
||||
cp -v ${(pkgs.formats.toml {}).generate "rp0.toml" client_config} $out/client
|
||||
'';
|
||||
in
|
||||
{
|
||||
name = "rosenpass unit";
|
||||
|
||||
nodes =
|
||||
let
|
||||
shared = peer: { config, modulesPath, pkgs, ... }: {
|
||||
# Need to work around a problem in recent systemd changes.
|
||||
# It won't be necessary in other distros (for which the systemd file was designed), this is NixOS specific
|
||||
# https://github.com/NixOS/nixpkgs/issues/258371#issuecomment-1925672767
|
||||
# This can potentially be removed in future nixpkgs updates
|
||||
systemd.packages = [
|
||||
(pkgs.runCommand "rosenpass" { } ''
|
||||
mkdir -p $out/lib/systemd/system
|
||||
< ${pkgs.rosenpass}/lib/systemd/system/rosenpass.target > $out/lib/systemd/system/rosenpass.target
|
||||
< ${pkgs.rosenpass}/lib/systemd/system/rosenpass@.service \
|
||||
sed 's@^\(\[Service]\)$@\1\nEnvironment=PATH=${pkgs.wireguard-tools}/bin@' |
|
||||
sed 's@^ExecStartPre=envsubst @ExecStartPre='"${pkgs.envsubst}"'/bin/envsubst @' |
|
||||
sed 's@^ExecStart=rosenpass @ExecStart='"${pkgs.rosenpass}"'/bin/rosenpass @' > $out/lib/systemd/system/rosenpass@.service
|
||||
'')
|
||||
];
|
||||
networking.wireguard = {
|
||||
enable = true;
|
||||
interfaces.rp0 = {
|
||||
ips = [ "${peer.wg.ip4}/32" "${peer.wg.ip6}/128" ];
|
||||
privateKeyFile = "/etc/wireguard/wgsk";
|
||||
};
|
||||
};
|
||||
environment.etc."wireguard/wgsk".text = peer.wg.secret;
|
||||
networking.interfaces.eth1 = {
|
||||
ipv4.addresses = [{
|
||||
address = peer.ip4;
|
||||
prefixLength = 24;
|
||||
}];
|
||||
ipv6.addresses = [{
|
||||
address = peer.ip6;
|
||||
prefixLength = 64;
|
||||
}];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
server = {
|
||||
imports = [ (shared server) ];
|
||||
networking.firewall.allowedUDPPorts = [ 9999 server.wg.listen ];
|
||||
networking.wireguard.interfaces.rp0 = {
|
||||
listenPort = server.wg.listen;
|
||||
peers = [
|
||||
{
|
||||
allowedIPs = [ client.wg.ip4 client.wg.ip6 ];
|
||||
publicKey = client.wg.public;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
client = {
|
||||
imports = [ (shared client) ];
|
||||
networking.wireguard.interfaces.rp0 = {
|
||||
peers = [
|
||||
{
|
||||
allowedIPs = [ "10.23.42.0/24" "fc00::/64" ];
|
||||
publicKey = server.wg.public;
|
||||
endpoint = "${server.ip4}:${toString server.wg.listen}";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
testScript = { ... }: ''
|
||||
from os import system
|
||||
rosenpass = "${pkgs.rosenpass}/bin/rosenpass"
|
||||
|
||||
start_all()
|
||||
|
||||
for machine in [server, client]:
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
machine.wait_for_unit("network-online.target")
|
||||
|
||||
with subtest("Key, Config, and Service Setup"):
|
||||
for name, machine, remote in [("server", server, client), ("client", client, server)]:
|
||||
# generate all the keys
|
||||
system(f"{rosenpass} gen-keys --public-key {name}-pqpk --secret-key {name}-pqsk")
|
||||
|
||||
# copy private keys to our side
|
||||
machine.copy_from_host(f"{name}-pqsk", "/etc/rosenpass/rp0/pqsk")
|
||||
machine.copy_from_host(f"{name}-pqpk", "/etc/rosenpass/rp0/pqpk")
|
||||
|
||||
# copy public keys to other side
|
||||
remote.copy_from_host(f"{name}-pqpk", f"/etc/rosenpass/rp0/peers/{name}/pqpk")
|
||||
|
||||
machine.copy_from_host(f"${config}/{name}", "/etc/rosenpass/rp0.toml")
|
||||
|
||||
for machine in [server, client]:
|
||||
machine.wait_for_unit("wireguard-rp0.service")
|
||||
|
||||
with subtest("wg network test"):
|
||||
client.succeed("wg show all preshared-keys | grep none", timeout=5);
|
||||
client.succeed("ping -c5 ${server.wg.ip4}")
|
||||
server.succeed("ping -c5 ${client.wg.ip6}")
|
||||
|
||||
with subtest("Set up rosenpass"):
|
||||
for machine in [server, client]:
|
||||
machine.succeed("systemctl start rosenpass@rp0.service")
|
||||
|
||||
for machine in [server, client]:
|
||||
machine.wait_for_unit("rosenpass@rp0.service")
|
||||
|
||||
|
||||
with subtest("compare preshared keys"):
|
||||
client.wait_until_succeeds("wg show all preshared-keys | grep --invert-match none", timeout=5);
|
||||
server.wait_until_succeeds("wg show all preshared-keys | grep --invert-match none", timeout=5);
|
||||
|
||||
def get_psk(m):
|
||||
psk = m.succeed("wg show rp0 preshared-keys | awk '{print $2}'")
|
||||
psk = psk.strip()
|
||||
assert len(psk.split()) == 1, "Only one PSK"
|
||||
return psk
|
||||
|
||||
assert get_psk(client) == get_psk(server), "preshared keys need to match"
|
||||
|
||||
with subtest("rosenpass network test"):
|
||||
client.succeed("ping -c5 ${server.wg.ip4}")
|
||||
server.succeed("ping -c5 ${client.wg.ip6}")
|
||||
'';
|
||||
}
|
||||
139
tests/systemd/rp.nix
Normal file
139
tests/systemd/rp.nix
Normal file
@@ -0,0 +1,139 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
server = {
|
||||
ip4 = "192.168.0.1";
|
||||
ip6 = "fd00::1";
|
||||
wg = {
|
||||
ip6 = "fc00::1";
|
||||
listen = 10000;
|
||||
};
|
||||
};
|
||||
|
||||
client = {
|
||||
ip4 = "192.168.0.2";
|
||||
ip6 = "fd00::2";
|
||||
wg = {
|
||||
ip6 = "fc00::2";
|
||||
};
|
||||
};
|
||||
|
||||
server_config = {
|
||||
listen = "${server.ip4}:9999";
|
||||
private_keys_dir = "/run/credentials/rp@test-rp-device0.service";
|
||||
verbose = true;
|
||||
dev = "test-rp-device0";
|
||||
ip = "fc00::1/64";
|
||||
peers = [{
|
||||
public_keys_dir = "/etc/rosenpass/test-rp-device0/peers/client";
|
||||
allowed_ips = "fc00::2";
|
||||
}];
|
||||
};
|
||||
client_config = {
|
||||
private_keys_dir = "/run/credentials/rp@test-rp-device0.service";
|
||||
verbose = true;
|
||||
dev = "test-rp-device0";
|
||||
ip = "fc00::2/128";
|
||||
peers = [{
|
||||
public_keys_dir = "/etc/rosenpass/test-rp-device0/peers/server";
|
||||
endpoint = "${server.ip4}:9999";
|
||||
allowed_ips = "fc00::/64";
|
||||
}];
|
||||
};
|
||||
|
||||
config = pkgs.runCommand "config" { } ''
|
||||
mkdir -pv $out
|
||||
cp -v ${(pkgs.formats.toml {}).generate "test-rp-device0.toml" server_config} $out/server
|
||||
cp -v ${(pkgs.formats.toml {}).generate "test-rp-device0.toml" client_config} $out/client
|
||||
'';
|
||||
in
|
||||
{
|
||||
name = "rp systemd unit";
|
||||
|
||||
nodes =
|
||||
let
|
||||
shared = peer: { config, modulesPath, pkgs, ... }: {
|
||||
# Need to work around a problem in recent systemd changes.
|
||||
# It won't be necessary in other distros (for which the systemd file was designed), this is NixOS specific
|
||||
# https://github.com/NixOS/nixpkgs/issues/258371#issuecomment-1925672767
|
||||
# This can potentially be removed in future nixpkgs updates
|
||||
systemd.packages = [
|
||||
(pkgs.runCommand "rp@.service" { } ''
|
||||
mkdir -p $out/lib/systemd/system
|
||||
< ${pkgs.rosenpass}/lib/systemd/system/rosenpass.target > $out/lib/systemd/system/rosenpass.target
|
||||
< ${pkgs.rosenpass}/lib/systemd/system/rp@.service \
|
||||
sed 's@^\(\[Service]\)$@\1\nEnvironment=PATH=${pkgs.iproute2}/bin:${pkgs.wireguard-tools}/bin@' |
|
||||
sed 's@^ExecStartPre=envsubst @ExecStartPre='"${pkgs.envsubst}"'/bin/envsubst @' |
|
||||
sed 's@^ExecStart=rp @ExecStart='"${pkgs.rosenpass}"'/bin/rp @' > $out/lib/systemd/system/rp@.service
|
||||
'')
|
||||
];
|
||||
environment.systemPackages = [ pkgs.wireguard-tools ];
|
||||
networking.interfaces.eth1 = {
|
||||
ipv4.addresses = [{
|
||||
address = peer.ip4;
|
||||
prefixLength = 24;
|
||||
}];
|
||||
ipv6.addresses = [{
|
||||
address = peer.ip6;
|
||||
prefixLength = 64;
|
||||
}];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
server = {
|
||||
imports = [ (shared server) ];
|
||||
networking.firewall.allowedUDPPorts = [ 9999 server.wg.listen ];
|
||||
};
|
||||
client = {
|
||||
imports = [ (shared client) ];
|
||||
};
|
||||
};
|
||||
testScript = { ... }: ''
|
||||
from os import system
|
||||
rp = "${pkgs.rosenpass}/bin/rp"
|
||||
|
||||
start_all()
|
||||
|
||||
for machine in [server, client]:
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
machine.wait_for_unit("network-online.target")
|
||||
|
||||
with subtest("Key, Config, and Service Setup"):
|
||||
for name, machine, remote in [("server", server, client), ("client", client, server)]:
|
||||
# create all the keys
|
||||
system(f"{rp} genkey {name}-sk")
|
||||
system(f"{rp} pubkey {name}-sk {name}-pk")
|
||||
|
||||
# copy secret keys to our side
|
||||
for file in ["pqpk", "pqsk", "wgsk"]:
|
||||
machine.copy_from_host(f"{name}-sk/{file}", f"/etc/rosenpass/test-rp-device0/{file}")
|
||||
# copy public keys to other side
|
||||
for file in ["pqpk", "wgpk"]:
|
||||
remote.copy_from_host(f"{name}-pk/{file}", f"/etc/rosenpass/test-rp-device0/peers/{name}/{file}")
|
||||
|
||||
machine.copy_from_host(f"${config}/{name}", "/etc/rosenpass/test-rp-device0.toml")
|
||||
|
||||
for machine in [server, client]:
|
||||
machine.succeed("systemctl start rp@test-rp-device0.service")
|
||||
|
||||
for machine in [server, client]:
|
||||
machine.wait_for_unit("rp@test-rp-device0.service")
|
||||
|
||||
with subtest("compare preshared keys"):
|
||||
client.wait_until_succeeds("wg show all preshared-keys | grep --invert-match none", timeout=5);
|
||||
server.wait_until_succeeds("wg show all preshared-keys | grep --invert-match none", timeout=5);
|
||||
|
||||
def get_psk(m):
|
||||
psk = m.succeed("wg show test-rp-device0 preshared-keys | awk '{print $2}'")
|
||||
psk = psk.strip()
|
||||
assert len(psk.split()) == 1, "Only one PSK"
|
||||
return psk
|
||||
|
||||
assert get_psk(client) == get_psk(server), "preshared keys need to match"
|
||||
|
||||
with subtest("network test"):
|
||||
client.succeed("ping -c5 ${server.wg.ip6}")
|
||||
server.succeed("ping -c5 ${client.wg.ip6}")
|
||||
'';
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
#![warn(missing_docs)]
|
||||
#![recursion_limit = "256"]
|
||||
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
|
||||
|
||||
#[cfg(doctest)]
|
||||
|
||||
@@ -5,23 +5,70 @@ use crate::CondenseBeside;
|
||||
pub struct Beside<Val, Ret>(pub Val, pub Ret);
|
||||
|
||||
impl<Val, Ret> Beside<Val, Ret> {
|
||||
/// Get an immutable reference to the destination value
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use rosenpass_to::Beside;
|
||||
///
|
||||
/// let beside = Beside(1, 2);
|
||||
/// assert_eq!(beside.dest(), &1);
|
||||
/// ```
|
||||
pub fn dest(&self) -> &Val {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get an immutable reference to the return value
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use rosenpass_to::Beside;
|
||||
///
|
||||
/// let beside = Beside(1, 2);
|
||||
/// assert_eq!(beside.ret(), &2);
|
||||
/// ```
|
||||
pub fn ret(&self) -> &Ret {
|
||||
&self.1
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the destination value
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use rosenpass_to::Beside;
|
||||
///
|
||||
/// let mut beside = Beside(1, 2);
|
||||
/// *beside.dest_mut() = 3;
|
||||
/// assert_eq!(beside.dest(), &3);
|
||||
/// ```
|
||||
pub fn dest_mut(&mut self) -> &mut Val {
|
||||
&mut self.0
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the return value
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use rosenpass_to::Beside;
|
||||
///
|
||||
/// let mut beside = Beside(1, 2);
|
||||
/// *beside.ret_mut() = 3;
|
||||
/// assert_eq!(beside.ret(), &3);
|
||||
/// ```
|
||||
pub fn ret_mut(&mut self) -> &mut Ret {
|
||||
&mut self.1
|
||||
}
|
||||
|
||||
/// Perform beside condensation. See [CondenseBeside]
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use rosenpass_to::Beside;
|
||||
/// use rosenpass_to::CondenseBeside;
|
||||
///
|
||||
/// let beside = Beside(1, ());
|
||||
/// assert_eq!(beside.condense(), 1);
|
||||
/// ```
|
||||
pub fn condense(self) -> <Ret as CondenseBeside<Val>>::Condensed
|
||||
where
|
||||
Ret: CondenseBeside<Val>,
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
/// The function [Beside::condense()](crate::Beside::condense) is a shorthand for using the
|
||||
/// condense trait.
|
||||
pub trait CondenseBeside<Val> {
|
||||
/// The type that results from condensation.
|
||||
type Condensed;
|
||||
|
||||
/// Takes ownership of `self` and condenses it with the given value.
|
||||
fn condense(self, ret: Val) -> Self::Condensed;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/// Helper performing explicit unsized coercion.
|
||||
/// Used by the [to](crate::to()) function.
|
||||
pub trait DstCoercion<Dst: ?Sized> {
|
||||
/// Performs an explicit coercion to the destination type.
|
||||
fn coerce_dest(&mut self) -> &mut Dst;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use crate::{Beside, CondenseBeside};
|
||||
use std::borrow::BorrowMut;
|
||||
|
||||
// The To trait is the core of the to crate; most functions with destinations will either return
|
||||
// an object that is an instance of this trait or they will return `-> impl To<Destination,
|
||||
// Return_value`.
|
||||
//
|
||||
// A quick way to implement a function with destination is to use the
|
||||
// [with_destination(|param: &mut Type| ...)] higher order function.
|
||||
/// The To trait is the core of the to crate; most functions with destinations will either return
|
||||
/// an object that is an instance of this trait or they will return `-> impl To<Destination,
|
||||
/// Return_value`.
|
||||
///
|
||||
/// A quick way to implement a function with destination is to use the
|
||||
/// [with_destination(|param: &mut Type| ...)] higher order function.
|
||||
pub trait To<Dst: ?Sized, Ret>: Sized {
|
||||
/// Writes self to the destination `out` and returns a value of type `Ret`.
|
||||
///
|
||||
/// This is the core method that must be implemented by all types implementing `To`.
|
||||
fn to(self, out: &mut Dst) -> Ret;
|
||||
|
||||
/// Generate a destination on the fly with a lambda.
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
use crate::To;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// A struct that wraps a closure and implements the `To` trait
|
||||
///
|
||||
/// This allows passing closures that operate on a destination type `Dst`
|
||||
/// and return `Ret`.
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// * `Dst` - The destination type the closure operates on
|
||||
/// * `Ret` - The return type of the closure
|
||||
/// * `Fun` - The closure type that implements `FnOnce(&mut Dst) -> Ret`
|
||||
struct ToClosure<Dst, Ret, Fun>
|
||||
where
|
||||
Dst: ?Sized,
|
||||
Fun: FnOnce(&mut Dst) -> Ret,
|
||||
{
|
||||
/// The function to call.
|
||||
fun: Fun,
|
||||
/// Phantom data to hold the destination type
|
||||
_val: PhantomData<Box<Dst>>,
|
||||
}
|
||||
|
||||
/// Implementation of the `To` trait for ToClosure
|
||||
///
|
||||
/// This enables calling the wrapped closure with a destination reference.
|
||||
impl<Dst, Ret, Fun> To<Dst, Ret> for ToClosure<Dst, Ret, Fun>
|
||||
where
|
||||
Dst: ?Sized,
|
||||
Fun: FnOnce(&mut Dst) -> Ret,
|
||||
{
|
||||
/// Execute the wrapped closure with the given destination
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `out` - Mutable reference to the destination
|
||||
fn to(self, out: &mut Dst) -> Ret {
|
||||
(self.fun)(out)
|
||||
}
|
||||
@@ -22,6 +40,14 @@ where
|
||||
|
||||
/// Used to create a function with destination.
|
||||
///
|
||||
/// Creates a wrapper that implements the `To` trait for a closure that
|
||||
/// operates on a destination type.
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// * `Dst` - The destination type the closure operates on
|
||||
/// * `Ret` - The return type of the closure
|
||||
/// * `Fun` - The closure type that implements `FnOnce(&mut Dst) -> Ret`
|
||||
///
|
||||
/// See the tutorial in [readme.me]..
|
||||
pub fn with_destination<Dst, Ret, Fun>(fun: Fun) -> impl To<Dst, Ret>
|
||||
where
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
#[macro_export]
|
||||
/// A simple for loop to repeat a $body a number of times
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::repeat;
|
||||
/// let mut sum = 0;
|
||||
/// repeat!(10, {
|
||||
/// sum += 1;
|
||||
/// });
|
||||
/// assert_eq!(sum, 10);
|
||||
/// ```
|
||||
macro_rules! repeat {
|
||||
($times:expr, $body:expr) => {
|
||||
for _ in 0..($times) {
|
||||
@@ -12,6 +23,23 @@ macro_rules! repeat {
|
||||
|
||||
#[macro_export]
|
||||
/// Return unless the condition $cond is true, with return value $val, if given.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::return_unless;
|
||||
/// fn test_fn() -> i32 {
|
||||
/// return_unless!(true, 1);
|
||||
/// 0
|
||||
/// }
|
||||
/// assert_eq!(test_fn(), 0);
|
||||
|
||||
/// fn test_fn2() -> i32 {
|
||||
/// return_unless!(false, 1);
|
||||
/// 0
|
||||
/// }
|
||||
/// assert_eq!(test_fn2(), 1);
|
||||
/// ```
|
||||
macro_rules! return_unless {
|
||||
($cond:expr) => {
|
||||
if !($cond) {
|
||||
@@ -27,6 +55,23 @@ macro_rules! return_unless {
|
||||
|
||||
#[macro_export]
|
||||
/// Return if the condition $cond is true, with return value $val, if given.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::return_if;
|
||||
/// fn test_fn() -> i32 {
|
||||
/// return_if!(true, 1);
|
||||
/// 0
|
||||
/// }
|
||||
/// assert_eq!(test_fn(), 1);
|
||||
|
||||
/// fn test_fn2() -> i32 {
|
||||
/// return_if!(false, 1);
|
||||
/// 0
|
||||
/// }
|
||||
/// assert_eq!(test_fn2(), 0);
|
||||
/// ```
|
||||
macro_rules! return_if {
|
||||
($cond:expr) => {
|
||||
if $cond {
|
||||
@@ -42,6 +87,27 @@ macro_rules! return_if {
|
||||
|
||||
#[macro_export]
|
||||
/// Break unless the condition is true, from the loop with label $val, if given.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::break_if;
|
||||
/// let mut sum = 0;
|
||||
/// for i in 0..10 {
|
||||
/// break_if!(i == 5);
|
||||
/// sum += 1;
|
||||
/// }
|
||||
/// assert_eq!(sum, 5);
|
||||
|
||||
/// let mut sum = 0;
|
||||
/// 'one: for _ in 0..10 {
|
||||
/// for j in 0..20 {
|
||||
/// break_if!(j == 5, 'one);
|
||||
/// sum += 1;
|
||||
/// }
|
||||
/// }
|
||||
/// assert_eq!(sum, 5);
|
||||
/// ```
|
||||
macro_rules! break_if {
|
||||
($cond:expr) => {
|
||||
if $cond {
|
||||
@@ -57,6 +123,25 @@ macro_rules! break_if {
|
||||
|
||||
#[macro_export]
|
||||
/// Continue if the condition is true, in the loop with label $val, if given.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::continue_if;
|
||||
/// let mut sum = 0;
|
||||
/// for i in 0..10 {
|
||||
/// continue_if!(i == 5);
|
||||
/// sum += 1;
|
||||
/// }
|
||||
/// assert_eq!(sum, 9);
|
||||
|
||||
/// let mut sum = 0;
|
||||
/// 'one: for i in 0..10 {
|
||||
/// continue_if!(i == 5, 'one);
|
||||
/// sum += 1;
|
||||
/// }
|
||||
/// assert_eq!(sum, 9);
|
||||
/// ```
|
||||
macro_rules! continue_if {
|
||||
($cond:expr) => {
|
||||
if $cond {
|
||||
@@ -69,81 +154,3 @@ macro_rules! continue_if {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_repeat() {
|
||||
let mut sum = 0;
|
||||
repeat!(10, {
|
||||
sum += 1;
|
||||
});
|
||||
assert_eq!(sum, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_return_unless() {
|
||||
fn test_fn() -> i32 {
|
||||
return_unless!(true, 1);
|
||||
0
|
||||
}
|
||||
assert_eq!(test_fn(), 0);
|
||||
|
||||
fn test_fn2() -> i32 {
|
||||
return_unless!(false, 1);
|
||||
0
|
||||
}
|
||||
assert_eq!(test_fn2(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_return_if() {
|
||||
fn test_fn() -> i32 {
|
||||
return_if!(true, 1);
|
||||
0
|
||||
}
|
||||
assert_eq!(test_fn(), 1);
|
||||
|
||||
fn test_fn2() -> i32 {
|
||||
return_if!(false, 1);
|
||||
0
|
||||
}
|
||||
assert_eq!(test_fn2(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_break_if() {
|
||||
let mut sum = 0;
|
||||
for i in 0..10 {
|
||||
break_if!(i == 5);
|
||||
sum += 1;
|
||||
}
|
||||
assert_eq!(sum, 5);
|
||||
|
||||
let mut sum = 0;
|
||||
'one: for _ in 0..10 {
|
||||
for j in 0..20 {
|
||||
break_if!(j == 5, 'one);
|
||||
sum += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(sum, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_continue_if() {
|
||||
let mut sum = 0;
|
||||
for i in 0..10 {
|
||||
continue_if!(i == 5);
|
||||
sum += 1;
|
||||
}
|
||||
assert_eq!(sum, 9);
|
||||
|
||||
let mut sum = 0;
|
||||
'one: for i in 0..10 {
|
||||
continue_if!(i == 5, 'one);
|
||||
sum += 1;
|
||||
}
|
||||
assert_eq!(sum, 9);
|
||||
}
|
||||
}
|
||||
|
||||
232
util/src/fd.rs
232
util/src/fd.rs
@@ -1,17 +1,44 @@
|
||||
//! Utilities for working with file descriptors
|
||||
|
||||
use anyhow::bail;
|
||||
use rustix::{
|
||||
fd::{AsFd, BorrowedFd, FromRawFd, OwnedFd, RawFd},
|
||||
io::fcntl_dupfd_cloexec,
|
||||
};
|
||||
use rustix::io::fcntl_dupfd_cloexec;
|
||||
use std::os::fd::{AsFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
|
||||
|
||||
use crate::{mem::Forgetting, result::OkExt};
|
||||
|
||||
/// Prepare a file descriptor for use in Rust code.
|
||||
///
|
||||
|
||||
/// Checks if the file descriptor is valid and duplicates it to a new file descriptor.
|
||||
/// The old file descriptor is masked to avoid potential use after free (on file descriptor)
|
||||
/// in case the given file descriptor is still used somewhere
|
||||
///
|
||||
/// # Panic and safety
|
||||
///
|
||||
/// Will panic if the given file descriptor is negative of or larger than
|
||||
/// the file descriptor numbers permitted by the operating system.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::io::Write;
|
||||
/// use std::os::fd::{IntoRawFd, AsRawFd};
|
||||
/// use tempfile::tempdir;
|
||||
/// use rosenpass_util::fd::{claim_fd, FdIo};
|
||||
///
|
||||
/// // Open a file and turn it into a raw file descriptor
|
||||
/// let orig = tempfile::tempfile()?.into_raw_fd();
|
||||
///
|
||||
/// // Reclaim that file and ready it for reading
|
||||
/// let mut claimed = FdIo(claim_fd(orig)?);
|
||||
///
|
||||
/// // A different file descriptor is used
|
||||
/// assert!(orig.as_raw_fd() != claimed.0.as_raw_fd());
|
||||
///
|
||||
/// // Write some data
|
||||
/// claimed.write_all(b"Hello, World!")?;
|
||||
///
|
||||
/// Ok::<(), std::io::Error>(())
|
||||
/// ```
|
||||
pub fn claim_fd(fd: RawFd) -> rustix::io::Result<OwnedFd> {
|
||||
let new = clone_fd_cloexec(unsafe { BorrowedFd::borrow_raw(fd) })?;
|
||||
mask_fd(fd)?;
|
||||
@@ -22,7 +49,32 @@ pub fn claim_fd(fd: RawFd) -> rustix::io::Result<OwnedFd> {
|
||||
///
|
||||
/// Checks if the file descriptor is valid.
|
||||
///
|
||||
/// Unlike [claim_fd], this will reuse the same file descriptor identifier instead of masking it.
|
||||
/// Unlike [claim_fd], this will try to reuse the same file descriptor identifier instead of masking it.
|
||||
///
|
||||
/// # Panic and safety
|
||||
///
|
||||
/// Will panic if the given file descriptor is negative of or larger than
|
||||
/// the file descriptor numbers permitted by the operating system.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::io::Write;
|
||||
/// use std::os::fd::IntoRawFd;
|
||||
/// use tempfile::tempdir;
|
||||
/// use rosenpass_util::fd::{claim_fd_inplace, FdIo};
|
||||
///
|
||||
/// // Open a file and turn it into a raw file descriptor
|
||||
/// let fd = tempfile::tempfile()?.into_raw_fd();
|
||||
///
|
||||
/// // Reclaim that file and ready it for reading
|
||||
/// let mut fd = FdIo(claim_fd_inplace(fd)?);
|
||||
///
|
||||
/// // Write some data
|
||||
/// fd.write_all(b"Hello, World!")?;
|
||||
///
|
||||
/// Ok::<(), std::io::Error>(())
|
||||
/// ```
|
||||
pub fn claim_fd_inplace(fd: RawFd) -> rustix::io::Result<OwnedFd> {
|
||||
let mut new = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||
let tmp = clone_fd_cloexec(&new)?;
|
||||
@@ -30,6 +82,13 @@ pub fn claim_fd_inplace(fd: RawFd) -> rustix::io::Result<OwnedFd> {
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
/// Will close the given file descriptor and overwrite
|
||||
/// it with a masking file descriptor (see [open_nullfd]) to prevent accidental reuse.
|
||||
///
|
||||
/// # Panic and safety
|
||||
///
|
||||
/// Will panic if the given file descriptor is negative of or larger than
|
||||
/// the file descriptor numbers permitted by the operating system.
|
||||
pub fn mask_fd(fd: RawFd) -> rustix::io::Result<()> {
|
||||
// Safety: because the OwnedFd resulting from OwnedFd::from_raw_fd is wrapped in a Forgetting,
|
||||
// it never gets dropped, meaning that fd is never closed and thus outlives the OwnedFd
|
||||
@@ -37,11 +96,17 @@ pub fn mask_fd(fd: RawFd) -> rustix::io::Result<()> {
|
||||
clone_fd_to_cloexec(open_nullfd()?, &mut owned)
|
||||
}
|
||||
|
||||
/// Duplicate a file descriptor, setting the close on exec flag
|
||||
pub fn clone_fd_cloexec<Fd: AsFd>(fd: Fd) -> rustix::io::Result<OwnedFd> {
|
||||
const MINFD: RawFd = 3; // Avoid stdin, stdout, and stderr
|
||||
/// Avoid stdin, stdout, and stderr
|
||||
const MINFD: RawFd = 3;
|
||||
fcntl_dupfd_cloexec(fd, MINFD)
|
||||
}
|
||||
|
||||
/// Duplicate a file descriptor, setting the close on exec flag.
|
||||
///
|
||||
/// This is slightly different from [clone_fd_cloexec], as this function supports specifying an
|
||||
/// explicit destination file descriptor.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn clone_fd_to_cloexec<Fd: AsFd>(fd: Fd, new: &mut OwnedFd) -> rustix::io::Result<()> {
|
||||
use rustix::io::{dup3, DupFlags};
|
||||
@@ -49,6 +114,10 @@ pub fn clone_fd_to_cloexec<Fd: AsFd>(fd: Fd, new: &mut OwnedFd) -> rustix::io::R
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
/// Duplicate a file descriptor, setting the close on exec flag.
|
||||
///
|
||||
/// This is slightly different from [clone_fd_cloexec], as this function supports specifying an
|
||||
/// explicit destination file descriptor.
|
||||
pub fn clone_fd_to_cloexec<Fd: AsFd>(fd: Fd, new: &mut OwnedFd) -> rustix::io::Result<()> {
|
||||
use rustix::io::{dup2, fcntl_setfd, FdFlags};
|
||||
dup2(&fd, new)?;
|
||||
@@ -56,7 +125,21 @@ pub fn clone_fd_to_cloexec<Fd: AsFd>(fd: Fd, new: &mut OwnedFd) -> rustix::io::R
|
||||
}
|
||||
|
||||
/// Open a "blocked" file descriptor. I.e. a file descriptor that is neither meant for reading nor
|
||||
/// writing
|
||||
/// writing.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The behavior of the file descriptor when being written to or from is undefined.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::{fs::File, io::Write, os::fd::IntoRawFd};
|
||||
/// use rustix::fd::FromRawFd;
|
||||
/// use rosenpass_util::fd::open_nullfd;
|
||||
///
|
||||
/// let nullfd = open_nullfd().unwrap();
|
||||
/// ```
|
||||
pub fn open_nullfd() -> rustix::io::Result<OwnedFd> {
|
||||
use rustix::fs::{open, Mode, OFlags};
|
||||
// TODO: Add tests showing that this will throw errors on use
|
||||
@@ -64,8 +147,24 @@ pub fn open_nullfd() -> rustix::io::Result<OwnedFd> {
|
||||
}
|
||||
|
||||
/// Convert low level errors into std::io::Error
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::io::ErrorKind as EK;
|
||||
/// use rustix::io::Errno;
|
||||
/// use rosenpass_util::fd::IntoStdioErr;
|
||||
///
|
||||
/// let e = Errno::INTR.into_stdio_err();
|
||||
/// assert!(matches!(e.kind(), EK::Interrupted));
|
||||
///
|
||||
/// let r : rustix::io::Result<()> = Err(Errno::INTR);
|
||||
/// assert!(matches!(r, Err(e) if e.kind() == EK::Interrupted));
|
||||
/// ```
|
||||
pub trait IntoStdioErr {
|
||||
/// Target type produced (e.g. std::io:Error or std::io::Result depending on context
|
||||
type Target;
|
||||
/// Convert low level errors to
|
||||
fn into_stdio_err(self) -> Self::Target;
|
||||
}
|
||||
|
||||
@@ -86,6 +185,10 @@ impl<T> IntoStdioErr for rustix::io::Result<T> {
|
||||
}
|
||||
|
||||
/// Read and write directly from a file descriptor
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [claim_fd].
|
||||
pub struct FdIo<Fd: AsFd>(pub Fd);
|
||||
|
||||
impl<Fd: AsFd> std::io::Read for FdIo<Fd> {
|
||||
@@ -104,7 +207,17 @@ impl<Fd: AsFd> std::io::Write for FdIo<Fd> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helpers for accessing stat(2) information
|
||||
pub trait StatExt {
|
||||
/// Check if the file is a socket
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::fd::StatExt;
|
||||
/// assert!(rustix::fs::stat("/")?.is_socket() == false);
|
||||
/// Ok::<(), rustix::io::Errno>(())
|
||||
/// ````
|
||||
fn is_socket(&self) -> bool;
|
||||
}
|
||||
|
||||
@@ -116,8 +229,21 @@ impl StatExt for rustix::fs::Stat {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helpers for accessing stat(2) information on an open file descriptor
|
||||
pub trait TryStatExt {
|
||||
/// Error type returned by operations
|
||||
type Error;
|
||||
|
||||
/// Check if the file is a socket
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::fd::TryStatExt;
|
||||
/// let fd = rustix::fs::open("/", rustix::fs::OFlags::empty(), rustix::fs::Mode::empty())?;
|
||||
/// assert!(matches!(fd.is_socket(), Ok(false)));
|
||||
/// Ok::<(), rustix::io::Errno>(())
|
||||
/// ````
|
||||
fn is_socket(&self) -> Result<bool, Self::Error>;
|
||||
}
|
||||
|
||||
@@ -132,13 +258,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine the type of socket a file descriptor represents
|
||||
pub trait GetSocketType {
|
||||
/// Error type returned by operations in this trait
|
||||
type Error;
|
||||
/// Look up the socket; see [rustix::net::sockopt::get_socket_type]
|
||||
fn socket_type(&self) -> Result<rustix::net::SocketType, Self::Error>;
|
||||
/// Checks if the socket is a datagram socket
|
||||
fn is_datagram_socket(&self) -> Result<bool, Self::Error> {
|
||||
use rustix::net::SocketType;
|
||||
matches!(self.socket_type()?, SocketType::DGRAM).ok()
|
||||
}
|
||||
/// Checks if the socket is a stream socket
|
||||
fn is_stream_socket(&self) -> Result<bool, Self::Error> {
|
||||
Ok(self.socket_type()? == rustix::net::SocketType::STREAM)
|
||||
}
|
||||
@@ -155,13 +286,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Distinguish different socket address familys; e.g. IP and unix sockets
|
||||
#[cfg(target_os = "linux")]
|
||||
pub trait GetSocketDomain {
|
||||
/// Error type returned by operations in this trait
|
||||
type Error;
|
||||
/// Retrieve the socket domain (address family)
|
||||
fn socket_domain(&self) -> Result<rustix::net::AddressFamily, Self::Error>;
|
||||
/// Alias for [socket_domain]
|
||||
fn socket_address_family(&self) -> Result<rustix::net::AddressFamily, Self::Error> {
|
||||
self.socket_domain()
|
||||
}
|
||||
/// Check if the underlying socket is a unix domain socket
|
||||
fn is_unix_socket(&self) -> Result<bool, Self::Error> {
|
||||
Ok(self.socket_domain()? == rustix::net::AddressFamily::UNIX)
|
||||
}
|
||||
@@ -179,10 +315,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Distinguish different types of unix sockets
|
||||
#[cfg(target_os = "linux")]
|
||||
pub trait GetUnixSocketType {
|
||||
/// Error type returned by operations in this trait
|
||||
type Error;
|
||||
/// Check if the socket is a unix stream socket
|
||||
fn is_unix_stream_socket(&self) -> Result<bool, Self::Error>;
|
||||
/// Returns Ok(()) only if the underlying socket is a unix stream socket
|
||||
fn demand_unix_stream_socket(&self) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
@@ -210,14 +350,18 @@ where
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
/// Distinguish between different network socket protocols (e.g. tcp, udp)
|
||||
pub trait GetSocketProtocol {
|
||||
/// Retrieve the socket protocol
|
||||
fn socket_protocol(&self) -> Result<Option<rustix::net::Protocol>, rustix::io::Errno>;
|
||||
/// Check if the socket is a udp socket
|
||||
fn is_udp_socket(&self) -> Result<bool, rustix::io::Errno> {
|
||||
self.socket_protocol()?
|
||||
.map(|p| p == rustix::net::ipproto::UDP)
|
||||
.unwrap_or(false)
|
||||
.ok()
|
||||
}
|
||||
/// Return Ok(()) only if the socket is a udp socket
|
||||
fn demand_udp_socket(&self) -> anyhow::Result<()> {
|
||||
match self.socket_protocol() {
|
||||
Ok(Some(rustix::net::ipproto::UDP)) => Ok(()),
|
||||
@@ -243,58 +387,58 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs::{read_to_string, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::os::fd::IntoRawFd;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_claim_fd() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let path = tmp_dir.path().join("test");
|
||||
let file = File::create(path.clone()).unwrap();
|
||||
let fd: RawFd = file.into_raw_fd();
|
||||
let owned_fd = claim_fd(fd).unwrap();
|
||||
let mut file = unsafe { File::from_raw_fd(owned_fd.into_raw_fd()) };
|
||||
file.write_all(b"Hello, World!").unwrap();
|
||||
|
||||
let message = read_to_string(path).unwrap();
|
||||
assert_eq!(message, "Hello, World!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "fd != u32::MAX as RawFd")]
|
||||
fn test_claim_fd_invalid_neg() {
|
||||
let fd: RawFd = -1;
|
||||
let _ = claim_fd(fd);
|
||||
let _ = claim_fd(-1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "fd != u32::MAX as RawFd")]
|
||||
fn test_claim_fd_invalid_max() {
|
||||
let fd: RawFd = i64::MAX as RawFd;
|
||||
let _ = claim_fd(fd);
|
||||
let _ = claim_fd(i64::MAX as RawFd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_nullfd_write() {
|
||||
let nullfd = open_nullfd().unwrap();
|
||||
let mut file = unsafe { File::from_raw_fd(nullfd.into_raw_fd()) };
|
||||
let res = file.write_all(b"Hello, World!");
|
||||
assert!(res.is_err());
|
||||
assert_eq!(
|
||||
res.unwrap_err().to_string(),
|
||||
"Bad file descriptor (os error 9)"
|
||||
);
|
||||
#[should_panic]
|
||||
fn test_claim_fd_inplace_invalid_neg() {
|
||||
let _ = claim_fd_inplace(-1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_nullfd_read() {
|
||||
#[should_panic]
|
||||
fn test_claim_fd_inplace_invalid_max() {
|
||||
let _ = claim_fd_inplace(i64::MAX as RawFd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_mask_fd_invalid_neg() {
|
||||
let _ = mask_fd(-1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_mask_fd_invalid_max() {
|
||||
let _ = mask_fd(i64::MAX as RawFd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_nullfd() -> anyhow::Result<()> {
|
||||
let mut file = FdIo(open_nullfd()?);
|
||||
let mut buf = [0; 10];
|
||||
assert!(matches!(file.read(&mut buf), Ok(0) | Err(_)));
|
||||
assert!(matches!(file.write(&buf), Err(_)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nullfd_read_write() {
|
||||
let nullfd = open_nullfd().unwrap();
|
||||
let mut file = unsafe { File::from_raw_fd(nullfd.into_raw_fd()) };
|
||||
let mut buffer = [0; 10];
|
||||
let res = file.read_exact(&mut buffer);
|
||||
assert!(res.is_err());
|
||||
assert_eq!(res.unwrap_err().to_string(), "failed to fill whole buffer");
|
||||
let mut buf = vec![0u8; 16];
|
||||
assert_eq!(rustix::io::read(&nullfd, &mut buf).unwrap(), 0);
|
||||
assert!(rustix::io::write(&nullfd, b"test").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
269
util/src/file.rs
269
util/src/file.rs
@@ -1,15 +1,45 @@
|
||||
//! Helpers for working with files
|
||||
|
||||
use anyhow::ensure;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::{fs::OpenOptions, path::Path};
|
||||
|
||||
/// Level of secrecy applied for a file
|
||||
pub enum Visibility {
|
||||
/// The file might contain a public key
|
||||
Public,
|
||||
/// The file might contain a secret key
|
||||
Secret,
|
||||
}
|
||||
|
||||
/// Open a file writable
|
||||
/// Open a file writeably, truncating the file.
|
||||
///
|
||||
/// Sensible default permissions are chosen based on the value of `visibility`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::io::{Write, Read};
|
||||
/// use tempfile::tempdir;
|
||||
/// use rosenpass_util::file::{fopen_r, fopen_w, Visibility};
|
||||
///
|
||||
/// const CONTENTS : &[u8] = b"Hello World";
|
||||
///
|
||||
/// let dir = tempdir()?;
|
||||
/// let path = dir.path().join("secret_key");
|
||||
///
|
||||
/// let mut f = fopen_w(&path, Visibility::Secret)?;
|
||||
/// f.write_all(CONTENTS)?;
|
||||
///
|
||||
/// let mut f = fopen_r(&path)?;
|
||||
/// let mut b = Vec::new();
|
||||
/// f.read_to_end(&mut b)?;
|
||||
/// assert_eq!(CONTENTS, &b);
|
||||
///
|
||||
/// Ok::<(), std::io::Error>(())
|
||||
/// ```
|
||||
pub fn fopen_w<P: AsRef<Path>>(path: P, visibility: Visibility) -> std::io::Result<File> {
|
||||
let mut options = OpenOptions::new();
|
||||
options.create(true).write(true).read(false).truncate(true);
|
||||
@@ -19,7 +49,12 @@ pub fn fopen_w<P: AsRef<Path>>(path: P, visibility: Visibility) -> std::io::Resu
|
||||
};
|
||||
options.open(path)
|
||||
}
|
||||
/// Open a file readable
|
||||
|
||||
/// Open a file readably
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [fopen_w].
|
||||
pub fn fopen_r<P: AsRef<Path>>(path: P) -> std::io::Result<File> {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
@@ -29,9 +64,47 @@ pub fn fopen_r<P: AsRef<Path>>(path: P) -> std::io::Result<File> {
|
||||
.open(path)
|
||||
}
|
||||
|
||||
/// Extension trait for [std::io::Read] adding [read_slice_to_end]
|
||||
pub trait ReadSliceToEnd {
|
||||
/// Error type returned by functions in this trait
|
||||
type Error;
|
||||
|
||||
/// Read slice asserting that the length of the data to read is at most
|
||||
/// as long as the buffer to read into
|
||||
///
|
||||
/// Note that this *may* append data read to [buf] even if the function fails,
|
||||
/// so the caller should make no assumptions about the contents of the buffer
|
||||
/// after calling read_slice_to_end if the result is an error.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::file::ReadSliceToEnd;
|
||||
///
|
||||
/// const DATA : &[u8] = b"Hello World";
|
||||
///
|
||||
/// // It is OK if file and buffer are equally long
|
||||
/// let mut buf = vec![b' '; 11];
|
||||
/// let res = Clone::clone(&DATA).read_slice_to_end(&mut buf[..DATA.len()]);
|
||||
/// assert!(res.is_ok()); // Read is overlong
|
||||
/// assert_eq!(buf, DATA); // Finally, data was successfully read
|
||||
///
|
||||
/// // It is OK if the buffer is longer than the file
|
||||
/// let mut buf = vec![b' '; 16];
|
||||
/// let res = Clone::clone(&DATA).read_slice_to_end(&mut buf);
|
||||
/// assert!(matches!(res, Ok(11)));
|
||||
/// assert_eq!(buf, b"Hello World "); // Data was still read to the buffer!
|
||||
///
|
||||
/// // It is not OK if the buffer is shorter than the file
|
||||
/// let mut buf = vec![b' '; 5];
|
||||
/// let res = Clone::clone(&DATA).read_slice_to_end(&mut buf);
|
||||
/// assert!(res.is_err());
|
||||
///
|
||||
/// // THE BUFFER MAY STILL BE FILLED THOUGH, BUT THIS IS NOT GUARANTEED
|
||||
/// assert_eq!(buf, b"Hello"); // Data was still read to the buffer!
|
||||
///
|
||||
/// Ok::<(), std::io::Error>(())
|
||||
/// ```
|
||||
fn read_slice_to_end(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error>;
|
||||
}
|
||||
|
||||
@@ -53,9 +126,50 @@ impl<R: Read> ReadSliceToEnd for R {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for [std::io::Read] adding [read_exact_to_end]
|
||||
pub trait ReadExactToEnd {
|
||||
/// Error type returned by functions in this trait
|
||||
type Error;
|
||||
|
||||
/// Read slice asserting that the length of the data to be read
|
||||
/// and the buffer are exactly the same length.
|
||||
///
|
||||
/// Note that this *may* append data read to [buf] even if the function fails,
|
||||
/// so the caller should make no assumptions about the contents of the buffer
|
||||
/// after calling read_exact_to_end if the result is an error.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::file::ReadExactToEnd;
|
||||
///
|
||||
/// const DATA : &[u8] = b"Hello World";
|
||||
///
|
||||
/// // It is OK if file and buffer are equally long
|
||||
/// let mut buf = vec![b' '; 11];
|
||||
/// let res = Clone::clone(&DATA).read_exact_to_end(&mut buf[..DATA.len()]);
|
||||
/// assert!(res.is_ok()); // Read is overlong
|
||||
/// assert_eq!(buf, DATA); // Finally, data was successfully read
|
||||
///
|
||||
/// // It is not OK if the buffer is longer than the file
|
||||
/// let mut buf = vec![b' '; 16];
|
||||
/// let res = Clone::clone(&DATA).read_exact_to_end(&mut buf);
|
||||
/// assert!(res.is_err());
|
||||
///
|
||||
/// // THE BUFFER MAY STILL BE FILLED THOUGH, BUT THIS IS NOT GUARANTEED
|
||||
/// // The read implementation for &[u8] happens not to do this
|
||||
/// assert_eq!(buf, b" "); // Data was still read to the buffer!
|
||||
///
|
||||
/// // It is not OK if the buffer is shorter than the file
|
||||
/// let mut buf = vec![b' '; 5];
|
||||
/// let res = Clone::clone(&DATA).read_exact_to_end(&mut buf);
|
||||
/// assert!(res.is_err());
|
||||
///
|
||||
/// // THE BUFFER MAY STILL BE FILLED THOUGH, BUT THIS IS NOT GUARANTEED
|
||||
/// assert_eq!(buf, b"Hello"); // Data was still read to the buffer!
|
||||
///
|
||||
/// Ok::<(), std::io::Error>(())
|
||||
/// ```
|
||||
fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
@@ -70,51 +184,190 @@ impl<R: Read> ReadExactToEnd for R {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a value from a file
|
||||
pub trait LoadValue {
|
||||
/// Error type returned
|
||||
type Error;
|
||||
|
||||
/// Load a value from a file
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::path::Path;
|
||||
/// use std::io::Write;
|
||||
/// use tempfile::tempdir;
|
||||
/// use rosenpass_util::file::{fopen_r, fopen_w, LoadValue, ReadExactToEnd, StoreValue, Visibility};
|
||||
///
|
||||
/// #[derive(Debug, PartialEq, Eq)]
|
||||
/// struct MyInt(pub u32);
|
||||
///
|
||||
/// impl StoreValue for MyInt {
|
||||
/// type Error = std::io::Error;
|
||||
///
|
||||
/// fn store<P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error> {
|
||||
/// let mut f = fopen_w(path, Visibility::Public)?;
|
||||
/// f.write_all(&self.0.to_le_bytes())
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl LoadValue for MyInt {
|
||||
/// type Error = anyhow::Error;
|
||||
///
|
||||
/// fn load<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
|
||||
/// where
|
||||
/// Self: Sized,
|
||||
/// {
|
||||
/// let mut b = [0u8; 4];
|
||||
/// fopen_r(path)?.read_exact_to_end(&mut b)?;
|
||||
/// Ok(MyInt(u32::from_le_bytes(b)))
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let dir = tempdir()?;
|
||||
/// let path = dir.path().join("my_int");
|
||||
///
|
||||
/// let orig = MyInt(17);
|
||||
/// orig.store(&path)?;
|
||||
///
|
||||
/// let copy = MyInt::load(&path)?;
|
||||
/// assert_eq!(orig, copy);
|
||||
///
|
||||
/// Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
fn load<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
/// Load a value from a file encoded as base64
|
||||
pub trait LoadValueB64 {
|
||||
/// Error type returned
|
||||
type Error;
|
||||
|
||||
/// Load a value from a file encoded as base64
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::path::Path;
|
||||
/// use tempfile::tempdir;
|
||||
/// use rosenpass_util::b64::{b64_decode, b64_encode};
|
||||
/// use rosenpass_util::file::{
|
||||
/// fopen_r, fopen_w, LoadValueB64, ReadSliceToEnd, StoreValueB64, StoreValueB64Writer,
|
||||
/// Visibility,
|
||||
/// };
|
||||
///
|
||||
/// #[derive(Debug, PartialEq, Eq)]
|
||||
/// struct MyInt(pub u32);
|
||||
///
|
||||
/// impl StoreValueB64Writer for MyInt {
|
||||
/// type Error = anyhow::Error;
|
||||
///
|
||||
/// fn store_b64_writer<const F: usize, W: std::io::Write>(
|
||||
/// &self,
|
||||
/// mut writer: W,
|
||||
/// ) -> Result<(), Self::Error> {
|
||||
/// // Let me just point out while writing this example,
|
||||
/// // that this API is currently, entirely shit in terms of
|
||||
/// // how it deals with buffer lengths.
|
||||
/// let mut buf = [0u8; F];
|
||||
/// let b64 = b64_encode(&self.0.to_le_bytes(), &mut buf)?;
|
||||
/// writer.write_all(b64.as_bytes())?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl StoreValueB64 for MyInt {
|
||||
/// type Error = anyhow::Error;
|
||||
///
|
||||
/// fn store_b64<const F: usize, P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error>
|
||||
/// where
|
||||
/// Self: Sized,
|
||||
/// {
|
||||
/// // The buffer length (first generic arg) is kind of an upper bound
|
||||
/// self.store_b64_writer::<F, _>(fopen_w(path, Visibility::Public)?)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl LoadValueB64 for MyInt {
|
||||
/// type Error = anyhow::Error;
|
||||
///
|
||||
/// fn load_b64<const F: usize, P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
|
||||
/// where
|
||||
/// Self: Sized,
|
||||
/// {
|
||||
/// // The buffer length is kind of an upper bound
|
||||
/// let mut b64_buf = [0u8; F];
|
||||
/// let b64_len = fopen_r(path)?.read_slice_to_end(&mut b64_buf)?;
|
||||
/// let b64_dat = &b64_buf[..b64_len];
|
||||
///
|
||||
/// let mut buf = [0u8; 4];
|
||||
/// b64_decode(b64_dat, &mut buf)?;
|
||||
/// Ok(MyInt(u32::from_le_bytes(buf)))
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let dir = tempdir()?;
|
||||
/// let path = dir.path().join("my_int");
|
||||
///
|
||||
/// let orig = MyInt(17);
|
||||
/// orig.store_b64::<10, _>(&path)?;
|
||||
///
|
||||
/// let copy = MyInt::load_b64::<10, _>(&path)?;
|
||||
/// assert_eq!(orig, copy);
|
||||
///
|
||||
/// Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
fn load_b64<const F: usize, P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
/// Store a value encoded as base64 in a file.
|
||||
pub trait StoreValueB64 {
|
||||
/// Error type returned
|
||||
type Error;
|
||||
|
||||
/// Store a value encoded as base64 in a file.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [LoadValueB64::load_b64].
|
||||
fn store_b64<const F: usize, P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
/// Store a value encoded as base64 to a writable stream
|
||||
pub trait StoreValueB64Writer {
|
||||
/// Error type returned
|
||||
type Error;
|
||||
|
||||
/// Store a value encoded as base64 to a writable stream
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [LoadValueB64::load_b64].
|
||||
fn store_b64_writer<const F: usize, W: std::io::Write>(
|
||||
&self,
|
||||
writer: W,
|
||||
) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// Store a value in a file
|
||||
pub trait StoreValue {
|
||||
/// Error type returned
|
||||
type Error;
|
||||
|
||||
/// Store a value in a file
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [LoadValue::load].
|
||||
fn store<P: AsRef<Path>>(&self, path: P) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
pub trait DisplayValueB64 {
|
||||
type Error;
|
||||
|
||||
fn display_b64<'o>(&self, output: &'o mut [u8]) -> Result<&'o str, Self::Error>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,87 +1,260 @@
|
||||
pub fn mutating<T, F>(mut v: T, f: F) -> T
|
||||
//! Syntax sugar & helpers for a functional programming style and method chains
|
||||
|
||||
/// Mutate a value; mostly syntactic sugar
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::borrow::Borrow;
|
||||
/// use rosenpass_util::functional::{mutating, MutatingExt, sideeffect, SideffectExt, ApplyExt};
|
||||
/// use rosenpass_util::mem::DiscardResultExt;
|
||||
///
|
||||
/// // Say you have a function that takes a mutable reference
|
||||
/// fn replace<T: Copy + Eq>(slice: &mut [T], targ: T, by: T) {
|
||||
/// for val in slice.iter_mut() {
|
||||
/// if *val == targ {
|
||||
/// *val = by;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Or you have some action that you want to perform as a side effect
|
||||
/// fn count<T: Copy + Eq>(accumulator: &mut usize, slice: &[T], targ: T) {
|
||||
/// *accumulator += slice.iter()
|
||||
/// .filter(|e| *e == &targ)
|
||||
/// .count();
|
||||
/// }
|
||||
///
|
||||
/// // Lets say, you also have a function that actually modifies the value
|
||||
/// fn rot2<const N : usize>(slice: [u8; N]) -> [u8; N] {
|
||||
/// let it = slice.iter()
|
||||
/// .cycle()
|
||||
/// .skip(2)
|
||||
/// .take(N);
|
||||
///
|
||||
/// let mut ret = [0u8; N];
|
||||
/// for (no, elm) in it.enumerate() {
|
||||
/// ret[no] = *elm;
|
||||
/// }
|
||||
///
|
||||
/// ret
|
||||
/// }
|
||||
///
|
||||
/// // Then these function are kind of clunky to use in an expression;
|
||||
/// // it can be done, but the resulting code is a bit verbose
|
||||
/// let mut accu = 0;
|
||||
/// assert_eq!(b"llo_WorldHe", &{
|
||||
/// let mut buf = b"Hello World".to_owned();
|
||||
/// count(&mut accu, &buf, b'l');
|
||||
/// replace(&mut buf, b' ', b'_');
|
||||
/// rot2(buf)
|
||||
/// });
|
||||
/// assert_eq!(accu, 3);
|
||||
///
|
||||
/// // Instead you could use mutating for a slightly prettier syntax,
|
||||
/// // but this makes only sense if you want to apply a single action
|
||||
/// assert_eq!(b"Hello_World",
|
||||
/// &mutating(b"Hello World".to_owned(), |buf|
|
||||
/// replace(buf, b' ', b'_')));
|
||||
///
|
||||
/// // The same is the case for sideeffect()
|
||||
/// assert_eq!(b"Hello World",
|
||||
/// &sideeffect(b"Hello World".to_owned(), |buf|
|
||||
/// count(&mut accu, buf, b'l')));
|
||||
/// assert_eq!(accu, 6);
|
||||
///
|
||||
/// // Calling rot2 on its own is straightforward of course
|
||||
/// assert_eq!(b"llo WorldHe", &rot2(b"Hello World".to_owned()));
|
||||
///
|
||||
/// // These operations can be conveniently used in a method chain
|
||||
/// // by using the extension traits.
|
||||
/// //
|
||||
/// // This is also quite handy if you just need to
|
||||
/// // modify a value in a long method chain.
|
||||
/// //
|
||||
/// // Here apply() also comes in quite handy, because we can use it
|
||||
/// // to modify the value itself (turning it into a reference).
|
||||
/// assert_eq!(b"llo_WorldHe",
|
||||
/// b"Hello World"
|
||||
/// .to_owned()
|
||||
/// .sideeffect(|buf| count(&mut accu, buf, b'l'))
|
||||
/// .mutating(|buf| replace(buf, b' ', b'_'))
|
||||
/// .apply(rot2)
|
||||
/// .borrow() as &[u8]);
|
||||
/// assert_eq!(accu, 9);
|
||||
///
|
||||
/// // There is also the mutating_mut variant, which can operate on any mutable reference;
|
||||
/// // this is mainly useful in a method chain if you are dealing with a mutable reference.
|
||||
/// //
|
||||
/// // This example is quite artificial though.
|
||||
/// assert_eq!(b"llo_WorldHe",
|
||||
/// b"hello world"
|
||||
/// .to_owned()
|
||||
/// .mutating(|buf|
|
||||
/// // Can not use sideeffect_ref at the start, because it drops the mut reference
|
||||
/// // status
|
||||
/// buf.sideeffect_mut(|buf| count(&mut accu, buf, b'l'))
|
||||
/// .mutating_mut(|buf| replace(buf, b' ', b'_'))
|
||||
/// .mutating_mut(|buf| replace(buf, b'h', b'H'))
|
||||
/// .mutating_mut(|buf| replace(buf, b'w', b'W'))
|
||||
/// // Using rot2 is more complex now
|
||||
/// .mutating_mut(|buf| {
|
||||
/// *buf = rot2(*buf);
|
||||
/// })
|
||||
/// // Can use sideeffect_ref at the end, because we no longer need
|
||||
/// // the &mut reference
|
||||
/// .sideeffect_ref(|buf| count(&mut accu, *buf, b'l'))
|
||||
/// // And we can use apply to fix the return value – if we really want to go
|
||||
/// // crazy and avoid using a {} block
|
||||
/// .apply(|_| ())
|
||||
/// // [crate::mem::DiscardResult::discard_result] does the same job and it is more explicit.
|
||||
/// .discard_result())
|
||||
/// .borrow() as &[u8]);
|
||||
/// assert_eq!(accu, 15);
|
||||
/// ```
|
||||
pub fn mutating<T, F>(mut v: T, mut f: F) -> T
|
||||
where
|
||||
F: Fn(&mut T),
|
||||
F: FnMut(&mut T),
|
||||
{
|
||||
f(&mut v);
|
||||
v
|
||||
}
|
||||
|
||||
/// Mutating values on the fly in a method chain
|
||||
pub trait MutatingExt {
|
||||
/// Mutating values on the fly in a method chain (owning)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [mutating].
|
||||
fn mutating<F>(self, f: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Self);
|
||||
F: FnMut(&mut Self);
|
||||
|
||||
/// Mutating values on the fly in a method chain (non-owning)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [mutating].
|
||||
fn mutating_mut<F>(&mut self, f: F) -> &mut Self
|
||||
where
|
||||
F: Fn(&mut Self);
|
||||
F: FnMut(&mut Self);
|
||||
}
|
||||
|
||||
impl<T> MutatingExt for T {
|
||||
fn mutating<F>(self, f: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Self),
|
||||
F: FnMut(&mut Self),
|
||||
{
|
||||
mutating(self, f)
|
||||
}
|
||||
|
||||
fn mutating_mut<F>(&mut self, f: F) -> &mut Self
|
||||
fn mutating_mut<F>(&mut self, mut f: F) -> &mut Self
|
||||
where
|
||||
F: Fn(&mut Self),
|
||||
F: FnMut(&mut Self),
|
||||
{
|
||||
f(self);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sideeffect<T, F>(v: T, f: F) -> T
|
||||
/// Apply a sideeffect using some value in an expression
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [mutating].
|
||||
pub fn sideeffect<T, F>(v: T, mut f: F) -> T
|
||||
where
|
||||
F: Fn(&T),
|
||||
F: FnMut(&T),
|
||||
{
|
||||
f(&v);
|
||||
v
|
||||
}
|
||||
|
||||
/// Apply sideeffect on the fly in a method chain
|
||||
pub trait SideffectExt {
|
||||
/// Apply sideeffect on the fly in a method chain (owning)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [mutating].
|
||||
fn sideeffect<F>(self, f: F) -> Self
|
||||
where
|
||||
F: Fn(&Self);
|
||||
F: FnMut(&Self);
|
||||
/// Apply sideeffect on the fly in a method chain (immutable ref)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [mutating].
|
||||
fn sideeffect_ref<F>(&self, f: F) -> &Self
|
||||
where
|
||||
F: Fn(&Self);
|
||||
F: FnMut(&Self);
|
||||
/// Apply sideeffect on the fly in a method chain (mutable ref)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [mutating].
|
||||
fn sideeffect_mut<F>(&mut self, f: F) -> &mut Self
|
||||
where
|
||||
F: Fn(&Self);
|
||||
F: FnMut(&Self);
|
||||
}
|
||||
|
||||
impl<T> SideffectExt for T {
|
||||
fn sideeffect<F>(self, f: F) -> Self
|
||||
where
|
||||
F: Fn(&Self),
|
||||
F: FnMut(&Self),
|
||||
{
|
||||
sideeffect(self, f)
|
||||
}
|
||||
|
||||
fn sideeffect_ref<F>(&self, f: F) -> &Self
|
||||
fn sideeffect_ref<F>(&self, mut f: F) -> &Self
|
||||
where
|
||||
F: Fn(&Self),
|
||||
F: FnMut(&Self),
|
||||
{
|
||||
f(self);
|
||||
self
|
||||
}
|
||||
|
||||
fn sideeffect_mut<F>(&mut self, f: F) -> &mut Self
|
||||
fn sideeffect_mut<F>(&mut self, mut f: F) -> &mut Self
|
||||
where
|
||||
F: Fn(&Self),
|
||||
F: FnMut(&Self),
|
||||
{
|
||||
f(self);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Just run the function
|
||||
///
|
||||
/// This is occasionally useful; in particular, you can
|
||||
/// use it to control the meaning of the question mark operator.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::functional::run;
|
||||
///
|
||||
/// fn add_and_mul(a: Option<u32>, b: Option<u32>, c: anyhow::Result<u32>, d: anyhow::Result<u32>) -> u32 {
|
||||
/// run(|| -> anyhow::Result<u32> {
|
||||
/// let ab = run(|| Some(a? * b?)).unwrap_or(0);
|
||||
/// Ok(ab + c? + d?)
|
||||
/// }).unwrap()
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(98, add_and_mul(Some(10), Some(9), Ok(3), Ok(5)));
|
||||
/// assert_eq!(8, add_and_mul(None, Some(15), Ok(3), Ok(5)));
|
||||
/// ```
|
||||
pub fn run<R, F: FnOnce() -> R>(f: F) -> R {
|
||||
f()
|
||||
}
|
||||
|
||||
/// Apply a function to a value in a method chain
|
||||
pub trait ApplyExt: Sized {
|
||||
/// Apply a function to a value in a method chain
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [mutating].
|
||||
fn apply<R, F>(self, f: F) -> R
|
||||
where
|
||||
F: FnOnce(Self) -> R;
|
||||
|
||||
355
util/src/io.rs
355
util/src/io.rs
@@ -1,8 +1,262 @@
|
||||
//! Helpers for performing IO
|
||||
//!
|
||||
//! # IO Error handling helpers tutorial
|
||||
//!
|
||||
//! ```
|
||||
//! use std::io::ErrorKind as EK;
|
||||
//!
|
||||
//! // It can be a bit hard to use IO errors in match statements
|
||||
//!
|
||||
//! fn io_placeholder() -> std::io::Result<()> {
|
||||
//! Ok(())
|
||||
//! }
|
||||
//!
|
||||
//! loop {
|
||||
//! match io_placeholder() {
|
||||
//! Ok(()) => break,
|
||||
//! // All errors are unreachable; just here for demo purposes
|
||||
//! Err(e) if e.kind() == EK::Interrupted => continue,
|
||||
//! Err(e) if e.kind() == EK::WouldBlock => {
|
||||
//! panic!("This particular function is not designed to be used in nonblocking code!");
|
||||
//! }
|
||||
//! Err(e) => Err(e)?,
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // For this reason this module contains various helper functions to make
|
||||
//! // matching on error kinds a bit less repetitive. [IoResultKindHintExt::io_err_kind_hint]
|
||||
//! // provides the basic functionality for use mostly with std::io::Result
|
||||
//!
|
||||
//! use rosenpass_util::io::IoResultKindHintExt;
|
||||
//!
|
||||
//! loop {
|
||||
//! match io_placeholder().io_err_kind_hint() {
|
||||
//! Ok(()) => break,
|
||||
//! // All errors are unreachable; just here for demo purposes
|
||||
//! Err((_, EK::Interrupted)) => continue,
|
||||
//! Err((_, EK::WouldBlock)) => {
|
||||
//! // Unreachable, just here for explanation purposes
|
||||
//! panic!("This particular function is not designed to be used in nonblocking code!");
|
||||
//! }
|
||||
//! Err((e, _)) => Err(e)?,
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // The trait can be customized; firstly, you can use IoErrorKind
|
||||
//! // for error types that can be fully represented as std::io::ErrorKind
|
||||
//!
|
||||
//! use rosenpass_util::io::IoErrorKind;
|
||||
//!
|
||||
//! #[derive(thiserror::Error, Debug, PartialEq, Eq)]
|
||||
//! enum MyErrno {
|
||||
//! #[error("Got interrupted")]
|
||||
//! Interrupted,
|
||||
//! #[error("In nonblocking mode")]
|
||||
//! WouldBlock,
|
||||
//! }
|
||||
//!
|
||||
//! impl IoErrorKind for MyErrno {
|
||||
//! fn io_error_kind(&self) -> std::io::ErrorKind {
|
||||
//! use MyErrno as ME;
|
||||
//! match self {
|
||||
//! ME::Interrupted => EK::Interrupted,
|
||||
//! ME::WouldBlock => EK::WouldBlock,
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! EK::Interrupted,
|
||||
//! std::io::Error::new(EK::Interrupted, "artificially interrupted").io_error_kind()
|
||||
//! );
|
||||
//! assert_eq!(EK::Interrupted, MyErrno::Interrupted.io_error_kind());
|
||||
//! assert_eq!(EK::WouldBlock, MyErrno::WouldBlock.io_error_kind());
|
||||
//!
|
||||
//! // And when an error can not fully be represented as an std::io::ErrorKind,
|
||||
//! // you can still use [TryIoErrorKind]
|
||||
//!
|
||||
//! use rosenpass_util::io::TryIoErrorKind;
|
||||
//!
|
||||
//! #[derive(thiserror::Error, Debug, PartialEq, Eq)]
|
||||
//! enum MyErrnoOrBlue {
|
||||
//! #[error("Got interrupted")]
|
||||
//! Interrupted,
|
||||
//! #[error("In nonblocking mode")]
|
||||
//! WouldBlock,
|
||||
//! #[error("I am feeling blue")]
|
||||
//! FeelingBlue,
|
||||
//! }
|
||||
//!
|
||||
//! impl TryIoErrorKind for MyErrnoOrBlue {
|
||||
//! fn try_io_error_kind(&self) -> Option<std::io::ErrorKind> {
|
||||
//! use MyErrnoOrBlue as ME;
|
||||
//! match self {
|
||||
//! ME::Interrupted => Some(EK::Interrupted),
|
||||
//! ME::WouldBlock => Some(EK::WouldBlock),
|
||||
//! ME::FeelingBlue => None,
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! Some(EK::Interrupted),
|
||||
//! MyErrnoOrBlue::Interrupted.try_io_error_kind()
|
||||
//! );
|
||||
//! assert_eq!(
|
||||
//! Some(EK::WouldBlock),
|
||||
//! MyErrnoOrBlue::WouldBlock.try_io_error_kind()
|
||||
//! );
|
||||
//! assert_eq!(None, MyErrnoOrBlue::FeelingBlue.try_io_error_kind());
|
||||
//!
|
||||
//! // TryIoErrorKind is automatically implemented for all types that implement
|
||||
//! // IoErrorKind
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! Some(EK::Interrupted),
|
||||
//! std::io::Error::new(EK::Interrupted, "artificially interrupted").try_io_error_kind()
|
||||
//! );
|
||||
//! assert_eq!(
|
||||
//! Some(EK::Interrupted),
|
||||
//! MyErrno::Interrupted.try_io_error_kind()
|
||||
//! );
|
||||
//! assert_eq!(
|
||||
//! Some(EK::WouldBlock),
|
||||
//! MyErrno::WouldBlock.try_io_error_kind()
|
||||
//! );
|
||||
//!
|
||||
//! // By implementing IoErrorKind, we can automatically make use of IoResultKindHintExt<T>
|
||||
//! // with our custom error type
|
||||
//!
|
||||
//! //use rosenpass_util::io::IoResultKindHintExt;
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! Ok::<_, MyErrno>(42).io_err_kind_hint(),
|
||||
//! Ok(42));
|
||||
//! assert!(matches!(
|
||||
//! Err::<(), _>(std::io::Error::new(EK::Interrupted, "artificially interrupted")).io_err_kind_hint(),
|
||||
//! Err((err, EK::Interrupted)) if format!("{err:?}") == "Custom { kind: Interrupted, error: \"artificially interrupted\" }"));
|
||||
//! assert_eq!(
|
||||
//! Err::<(), _>(MyErrno::Interrupted).io_err_kind_hint(),
|
||||
//! Err((MyErrno::Interrupted, EK::Interrupted)));
|
||||
//!
|
||||
//! // Correspondingly, TryIoResultKindHintExt can be used for Results with Errors
|
||||
//! // that implement TryIoErrorKind
|
||||
//!
|
||||
//! use crate::rosenpass_util::io::TryIoResultKindHintExt;
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! Ok::<_, MyErrnoOrBlue>(42).try_io_err_kind_hint(),
|
||||
//! Ok(42));
|
||||
//! assert_eq!(
|
||||
//! Err::<(), _>(MyErrnoOrBlue::Interrupted).try_io_err_kind_hint(),
|
||||
//! Err((MyErrnoOrBlue::Interrupted, Some(EK::Interrupted))));
|
||||
//! assert_eq!(
|
||||
//! Err::<(), _>(MyErrnoOrBlue::FeelingBlue).try_io_err_kind_hint(),
|
||||
//! Err((MyErrnoOrBlue::FeelingBlue, None)));
|
||||
//!
|
||||
//! // SubstituteForIoErrorKindExt serves as a helper to handle specific ErrorKinds
|
||||
//! // using a method chaining style. It works on anything that implements TryIoErrorKind.
|
||||
//!
|
||||
//! use rosenpass_util::io::SubstituteForIoErrorKindExt;
|
||||
//!
|
||||
//! assert_eq!(Ok(42),
|
||||
//! Err(MyErrnoOrBlue::Interrupted)
|
||||
//! .substitute_for_ioerr_kind_with(EK::Interrupted, || 42));
|
||||
//!
|
||||
//! assert_eq!(Err(MyErrnoOrBlue::WouldBlock),
|
||||
//! Err(MyErrnoOrBlue::WouldBlock)
|
||||
//! .substitute_for_ioerr_kind_with(EK::Interrupted, || 42));
|
||||
//!
|
||||
//! // The other functions in SubstituteForIoErrorKindExt are mostly just wrappers,
|
||||
//! // getting the same job done with minor convenience
|
||||
//!
|
||||
//! // Plain Ok() value instead of function
|
||||
//! assert_eq!(Ok(42),
|
||||
//! Err(MyErrnoOrBlue::Interrupted)
|
||||
//! .substitute_for_ioerr_kind(EK::Interrupted, 42));
|
||||
//! assert_eq!(Err(MyErrnoOrBlue::WouldBlock),
|
||||
//! Err(MyErrnoOrBlue::WouldBlock)
|
||||
//! .substitute_for_ioerr_kind(EK::Interrupted, 42));
|
||||
//!
|
||||
//! // For specific errors
|
||||
//! assert_eq!(Ok(42),
|
||||
//! Err(MyErrnoOrBlue::Interrupted)
|
||||
//! .substitute_for_ioerr_interrupted_with(|| 42)
|
||||
//! .substitute_for_ioerr_wouldblock_with(|| 23));
|
||||
//! assert_eq!(Ok(23),
|
||||
//! Err(MyErrnoOrBlue::WouldBlock)
|
||||
//! .substitute_for_ioerr_interrupted_with(|| 42)
|
||||
//! .substitute_for_ioerr_wouldblock_with(|| 23));
|
||||
//! assert_eq!(Err(MyErrnoOrBlue::FeelingBlue),
|
||||
//! Err(MyErrnoOrBlue::FeelingBlue)
|
||||
//! .substitute_for_ioerr_interrupted_with(|| 42)
|
||||
//! .substitute_for_ioerr_wouldblock_with(|| 23));
|
||||
//!
|
||||
//! // And for specific errors without the function call
|
||||
//! assert_eq!(Ok(42),
|
||||
//! Err(MyErrnoOrBlue::Interrupted)
|
||||
//! .substitute_for_ioerr_interrupted(42)
|
||||
//! .substitute_for_ioerr_wouldblock(23));
|
||||
//! assert_eq!(Ok(23),
|
||||
//! Err(MyErrnoOrBlue::WouldBlock)
|
||||
//! .substitute_for_ioerr_interrupted(42)
|
||||
//! .substitute_for_ioerr_wouldblock(23));
|
||||
//! assert_eq!(Err(MyErrnoOrBlue::FeelingBlue),
|
||||
//! Err(MyErrnoOrBlue::FeelingBlue)
|
||||
//! .substitute_for_ioerr_interrupted(42)
|
||||
//! .substitute_for_ioerr_wouldblock(23));
|
||||
//!
|
||||
//! // handle_interrupted automates the process of handling ErrorKind::Interrupted
|
||||
//! // in cases where the action should simply be rerun; it can handle any error type
|
||||
//! // that implements TryIoErrorKind. It lets other errors and Ok(_) pass through.
|
||||
//!
|
||||
//! use rosenpass_util::io::handle_interrupted;
|
||||
//!
|
||||
//! let mut ctr = 0u32;
|
||||
//! let mut simulate_io = || -> Result<u32, MyErrnoOrBlue> {
|
||||
//! let r = match ctr % 6 {
|
||||
//! 1 => Ok(42),
|
||||
//! 3 => Err(MyErrnoOrBlue::FeelingBlue),
|
||||
//! 5 => Err(MyErrnoOrBlue::WouldBlock),
|
||||
//! _ => Err(MyErrnoOrBlue::Interrupted),
|
||||
//! };
|
||||
//! ctr += 1;
|
||||
//! r
|
||||
//! };
|
||||
//!
|
||||
//! assert_eq!(Ok(Some(42)), handle_interrupted(&mut simulate_io));
|
||||
//! assert_eq!(Err(MyErrnoOrBlue::FeelingBlue), handle_interrupted(&mut simulate_io));
|
||||
//! assert_eq!(Err(MyErrnoOrBlue::WouldBlock), handle_interrupted(&mut simulate_io));
|
||||
//! // never returns None
|
||||
//!
|
||||
//! // nonblocking_handle_io_errors performs the same job, except that
|
||||
//! // WouldBlock is substituted with Ok(None)
|
||||
//!
|
||||
//! use rosenpass_util::io::nonblocking_handle_io_errors;
|
||||
//!
|
||||
//! assert_eq!(Ok(Some(42)), nonblocking_handle_io_errors(&mut simulate_io));
|
||||
//! assert_eq!(Err(MyErrnoOrBlue::FeelingBlue), nonblocking_handle_io_errors(&mut simulate_io));
|
||||
//! assert_eq!(Ok(None), nonblocking_handle_io_errors(&mut simulate_io));
|
||||
//!
|
||||
//! Ok::<_, anyhow::Error>(())
|
||||
//! ```
|
||||
|
||||
use std::{borrow::Borrow, io};
|
||||
|
||||
use anyhow::ensure;
|
||||
use zerocopy::AsBytes;
|
||||
|
||||
/// Generic trait for accessing [std::io::Error::kind]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
pub trait IoErrorKind {
|
||||
/// Conversion to [std::io::Error::kind]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
fn io_error_kind(&self) -> io::ErrorKind;
|
||||
}
|
||||
|
||||
@@ -12,7 +266,17 @@ impl<T: Borrow<io::Error>> IoErrorKind for T {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic trait for accessing [std::io::Error::kind] where it may not be present
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
pub trait TryIoErrorKind {
|
||||
/// Conversion to [std::io::Error::kind] where it may not be present
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
fn try_io_error_kind(&self) -> Option<io::ErrorKind>;
|
||||
}
|
||||
|
||||
@@ -22,8 +286,19 @@ impl<T: IoErrorKind> TryIoErrorKind for T {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper for accessing [std::io::Error::kind] in Results
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
pub trait IoResultKindHintExt<T>: Sized {
|
||||
/// Error type including the ErrorKind hint
|
||||
type Error;
|
||||
/// Helper for accessing [std::io::Error::kind] in Results
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
fn io_err_kind_hint(self) -> Result<T, (Self::Error, io::ErrorKind)>;
|
||||
}
|
||||
|
||||
@@ -37,8 +312,19 @@ impl<T, E: IoErrorKind> IoResultKindHintExt<T> for Result<T, E> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper for accessing [std::io::Error::kind] in Results where it may not be present
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
pub trait TryIoResultKindHintExt<T>: Sized {
|
||||
/// Error type including the ErrorKind hint
|
||||
type Error;
|
||||
/// Helper for accessing [std::io::Error::kind] in Results where it may not be present
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
fn try_io_err_kind_hint(self) -> Result<T, (Self::Error, Option<io::ErrorKind>)>;
|
||||
}
|
||||
|
||||
@@ -52,17 +338,41 @@ impl<T, E: TryIoErrorKind> TryIoResultKindHintExt<T> for Result<T, E> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper for working with IO results using a method chaining style
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
pub trait SubstituteForIoErrorKindExt<T>: Sized {
|
||||
/// Error type produced by methods in this trait
|
||||
type Error;
|
||||
|
||||
/// Substitute errors with a certain [std::io::ErrorKind] by a value produced by a function
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
fn substitute_for_ioerr_kind_with<F: FnOnce() -> T>(
|
||||
self,
|
||||
kind: io::ErrorKind,
|
||||
f: F,
|
||||
) -> Result<T, Self::Error>;
|
||||
|
||||
/// Substitute errors with a certain [std::io::ErrorKind] by a value
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
fn substitute_for_ioerr_kind(self, kind: io::ErrorKind, v: T) -> Result<T, Self::Error> {
|
||||
self.substitute_for_ioerr_kind_with(kind, || v)
|
||||
}
|
||||
|
||||
/// Substitute errors with [std::io::ErrorKind] [std::io::ErrorKind::Interrupted] by a value
|
||||
/// produced by a function
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
fn substitute_for_ioerr_interrupted_with<F: FnOnce() -> T>(
|
||||
self,
|
||||
f: F,
|
||||
@@ -70,10 +380,21 @@ pub trait SubstituteForIoErrorKindExt<T>: Sized {
|
||||
self.substitute_for_ioerr_kind_with(io::ErrorKind::Interrupted, f)
|
||||
}
|
||||
|
||||
/// Substitute errors with [std::io::ErrorKind] [std::io::ErrorKind::Interrupted] by a value
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
fn substitute_for_ioerr_interrupted(self, v: T) -> Result<T, Self::Error> {
|
||||
self.substitute_for_ioerr_interrupted_with(|| v)
|
||||
}
|
||||
|
||||
/// Substitute errors with [std::io::ErrorKind] [std::io::ErrorKind::WouldBlock] by a value
|
||||
/// produced by a function
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
fn substitute_for_ioerr_wouldblock_with<F: FnOnce() -> T>(
|
||||
self,
|
||||
f: F,
|
||||
@@ -81,6 +402,11 @@ pub trait SubstituteForIoErrorKindExt<T>: Sized {
|
||||
self.substitute_for_ioerr_kind_with(io::ErrorKind::WouldBlock, f)
|
||||
}
|
||||
|
||||
/// Substitute errors with [std::io::ErrorKind] [std::io::ErrorKind::WouldBlock] by a value
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
fn substitute_for_ioerr_wouldblock(self, v: T) -> Result<T, Self::Error> {
|
||||
self.substitute_for_ioerr_wouldblock_with(|| v)
|
||||
}
|
||||
@@ -107,6 +433,10 @@ impl<T, E: TryIoErrorKind> SubstituteForIoErrorKindExt<T> for Result<T, E> {
|
||||
/// - If there is no error (i.e. on `Ok(r)`), the function will return `Ok(Some(r))`
|
||||
/// - `Interrupted` is handled internally, by retrying the IO operation
|
||||
/// - Other errors are returned as is
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
pub fn handle_interrupted<R, E, F>(mut iofn: F) -> Result<Option<R>, E>
|
||||
where
|
||||
E: TryIoErrorKind,
|
||||
@@ -128,6 +458,10 @@ where
|
||||
/// - `Interrupted` is handled internally, by retrying the IO operation
|
||||
/// - `WouldBlock` is handled by returning `Ok(None)`,
|
||||
/// - Other errors are returned as is
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tutorial in the module](self).
|
||||
pub fn nonblocking_handle_io_errors<R, E, F>(mut iofn: F) -> Result<Option<R>, E>
|
||||
where
|
||||
E: TryIoErrorKind,
|
||||
@@ -144,6 +478,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// [std:io::Read] extension trait for call with [nonblocking_handle_io_errors] applied
|
||||
pub trait ReadNonblockingWithBoringErrorsHandledExt {
|
||||
/// Convenience wrapper using [nonblocking_handle_io_errors] with [std::io::Read]
|
||||
fn read_nonblocking_with_boring_errors_handled(
|
||||
@@ -161,7 +496,27 @@ impl<T: io::Read> ReadNonblockingWithBoringErrorsHandledExt for T {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for [std::io::Read] providing the ability to read
|
||||
/// a buffer exactly
|
||||
pub trait ReadExt {
|
||||
/// Version of [std::io::Read::read_exact] that throws if there
|
||||
/// is extra data in the stream to be read
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::io::ReadExt;
|
||||
///
|
||||
/// let mut buf = [0u8; 4];
|
||||
///
|
||||
/// // Over or underlong buffer yields error
|
||||
/// assert!(b"12345".as_slice().read_exact_til_end(&mut buf).is_err());
|
||||
/// assert!(b"123".as_slice().read_exact_til_end(&mut buf).is_err());
|
||||
///
|
||||
/// // Buffer of precisely the right length leads to successful read
|
||||
/// assert!(b"1234".as_slice().read_exact_til_end(&mut buf).is_ok());
|
||||
/// assert_eq!(b"1234", &buf);
|
||||
/// ```
|
||||
fn read_exact_til_end(&mut self, buf: &mut [u8]) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
//! This module provides utilities for decoding length-prefixed messages from I/O streams.
|
||||
//!
|
||||
//! Messages are prefixed with an unsigned 64-bit little-endian length header, followed by the
|
||||
//! message payload. The [`decoder::LengthPrefixDecoder`] is a central component here, maintaining
|
||||
//! internal buffers and state for partial reads and boundary checks.
|
||||
//!
|
||||
//! The module defines errors to handle size mismatches, I/O issues, and boundary violations
|
||||
//! that may occur during decoding.
|
||||
//!
|
||||
//! The abstractions provided in this module enable safe and convenient reading
|
||||
//! of structured data from streams, including handling unexpected EOFs and ensuring messages
|
||||
//! fit within allocated buffers.
|
||||
|
||||
use std::{borrow::BorrowMut, cmp::min, io};
|
||||
|
||||
use thiserror::Error;
|
||||
@@ -8,16 +21,25 @@ use crate::{
|
||||
result::ensure_or,
|
||||
};
|
||||
|
||||
/// Size in bytes of the message header carrying length information.
|
||||
/// Currently, HEADER_SIZE is always 8 bytes and encodes a 64-bit little-endian number.
|
||||
pub const HEADER_SIZE: usize = std::mem::size_of::<u64>();
|
||||
|
||||
/// Error enum representing sanity check failures when accessing buffer regions.
|
||||
///
|
||||
/// This error is triggered when internal offsets point outside allowable regions.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SanityError {
|
||||
/// The given offset exceeded the read buffer bounds.
|
||||
#[error("Offset is out of read buffer bounds")]
|
||||
OutOfBufferBounds,
|
||||
|
||||
/// The given offset exceeded the message buffer bounds.
|
||||
#[error("Offset is out of message buffer bounds")]
|
||||
OutOfMessageBounds,
|
||||
}
|
||||
|
||||
/// Error indicating that the message size is larger than the available buffer space.
|
||||
#[derive(Error, Debug)]
|
||||
#[error("Message too large ({msg_size} bytes) for buffer ({buf_size} bytes)")]
|
||||
pub struct MessageTooLargeError {
|
||||
@@ -26,23 +48,52 @@ pub struct MessageTooLargeError {
|
||||
}
|
||||
|
||||
impl MessageTooLargeError {
|
||||
/// Creates a new `MessageTooLargeError` with the given message and buffer sizes.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use rosenpass_util::length_prefix_encoding::decoder::MessageTooLargeError;
|
||||
/// let err = MessageTooLargeError::new(1024, 512);
|
||||
/// assert_eq!(format!("{}", err), "Message too large (1024 bytes) for buffer (512 bytes)");
|
||||
/// ```
|
||||
pub fn new(msg_size: usize, buf_size: usize) -> Self {
|
||||
Self { msg_size, buf_size }
|
||||
}
|
||||
|
||||
/// Ensures the message fits within the given buffer.
|
||||
///
|
||||
/// Returns `Ok(())` if `msg_size <= buf_size`, otherwise returns a `MessageTooLargeError`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use rosenpass_util::length_prefix_encoding::decoder::MessageTooLargeError;
|
||||
/// let result = MessageTooLargeError::ensure(100, 200);
|
||||
/// assert!(result.is_ok());
|
||||
///
|
||||
/// let err = MessageTooLargeError::ensure(300, 200).unwrap_err();
|
||||
/// assert_eq!(format!("{}", err), "Message too large (300 bytes) for buffer (200 bytes)");
|
||||
/// ```
|
||||
pub fn ensure(msg_size: usize, buf_size: usize) -> Result<(), Self> {
|
||||
let err = MessageTooLargeError { msg_size, buf_size };
|
||||
ensure_or(msg_size <= buf_size, err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return type for `ReadFromIo` operations, containing the number of bytes read and an optional message slice.
|
||||
#[derive(Debug)]
|
||||
pub struct ReadFromIoReturn<'a> {
|
||||
/// Number of bytes read.
|
||||
pub bytes_read: usize,
|
||||
/// The complete message slice if fully read, otherwise `None`.
|
||||
pub message: Option<&'a mut [u8]>,
|
||||
}
|
||||
|
||||
impl<'a> ReadFromIoReturn<'a> {
|
||||
/// Creates a new `ReadFromIoReturn`.
|
||||
///
|
||||
/// Generally used internally to represent partial or complete read results.
|
||||
pub fn new(bytes_read: usize, message: Option<&'a mut [u8]>) -> Self {
|
||||
Self {
|
||||
bytes_read,
|
||||
@@ -51,10 +102,17 @@ impl<'a> ReadFromIoReturn<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that may occur when reading from an I/O source.
|
||||
///
|
||||
/// This enum wraps I/O errors and message-size errors, allowing higher-level logic to determine
|
||||
/// if the error is a fundamental I/O problem or a size mismatch issue.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ReadFromIoError {
|
||||
/// Error reading from the underlying I/O stream.
|
||||
#[error("Error reading from the underlying stream")]
|
||||
IoError(#[from] io::Error),
|
||||
|
||||
/// The message size exceeded the capacity of the available buffer.
|
||||
#[error("Message size out of buffer bounds")]
|
||||
MessageTooLargeError(#[from] MessageTooLargeError),
|
||||
}
|
||||
@@ -68,6 +126,30 @@ impl TryIoErrorKind for ReadFromIoError {
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoder for length-prefixed messages.
|
||||
///
|
||||
/// This decoder reads a 64-bit little-endian length prefix followed by the message payload.
|
||||
/// It maintains state so that partial reads from a non-blocking or streaming source can
|
||||
/// accumulate until a full message is available.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use std::io::Cursor;
|
||||
/// # use rosenpass_util::length_prefix_encoding::decoder::LengthPrefixDecoder;
|
||||
/// let data: Vec<u8> = {
|
||||
/// let mut buf = Vec::new();
|
||||
/// buf.extend_from_slice(&(5u64.to_le_bytes())); // message length = 5
|
||||
/// buf.extend_from_slice(b"hello");
|
||||
/// buf
|
||||
/// };
|
||||
///
|
||||
/// let mut decoder = LengthPrefixDecoder::new(vec![0; 64]);
|
||||
/// let mut cursor = Cursor::new(data);
|
||||
///
|
||||
/// let message = decoder.read_all_from_stdio(&mut cursor).expect("read failed");
|
||||
/// assert_eq!(message, b"hello");
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct LengthPrefixDecoder<Buf: BorrowMut<[u8]>> {
|
||||
header: [u8; HEADER_SIZE],
|
||||
@@ -76,25 +158,102 @@ pub struct LengthPrefixDecoder<Buf: BorrowMut<[u8]>> {
|
||||
}
|
||||
|
||||
impl<Buf: BorrowMut<[u8]>> LengthPrefixDecoder<Buf> {
|
||||
/// Creates a new `LengthPrefixDecoder` with the provided buffer.
|
||||
///
|
||||
/// The provided buffer must be large enough to hold the expected maximum message size.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use rosenpass_util::length_prefix_encoding::decoder::LengthPrefixDecoder;
|
||||
/// let decoder = LengthPrefixDecoder::new(vec![0; 1024]);
|
||||
/// assert_eq!(*decoder.bytes_read(), 0);
|
||||
/// ```
|
||||
pub fn new(buf: Buf) -> Self {
|
||||
let header = Default::default();
|
||||
let off = 0;
|
||||
Self { header, buf, off }
|
||||
}
|
||||
|
||||
/// Clears and zeroes all internal state.
|
||||
///
|
||||
/// This zeroizes the header and the buffer, as well as resets the offset to zero.
|
||||
pub fn clear(&mut self) {
|
||||
self.zeroize()
|
||||
}
|
||||
|
||||
/// Creates a decoder from parts.
|
||||
///
|
||||
/// Typically used for low-level reconstruction of a decoder state.
|
||||
pub fn from_parts(header: [u8; HEADER_SIZE], buf: Buf, off: usize) -> Self {
|
||||
Self { header, buf, off }
|
||||
}
|
||||
|
||||
/// Consumes the decoder and returns its internal parts.
|
||||
///
|
||||
/// Returns the header, the underlying buffer, and the current offset.
|
||||
pub fn into_parts(self) -> ([u8; HEADER_SIZE], Buf, usize) {
|
||||
let Self { header, buf, off } = self;
|
||||
(header, buf, off)
|
||||
}
|
||||
|
||||
/// Reads a complete message from the given reader.
|
||||
///
|
||||
/// Will retry on interrupts and fails if EOF is encountered prematurely. On success,
|
||||
/// returns a mutable slice of the fully read message.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Successful read
|
||||
/// ```
|
||||
/// # use std::io::Cursor;
|
||||
/// # use rosenpass_util::length_prefix_encoding::decoder::{LengthPrefixDecoder, ReadFromIoError, MessageTooLargeError};
|
||||
/// let mut data: Cursor<Vec<u8>> = {
|
||||
/// let mut buf = Vec::new();
|
||||
/// buf.extend_from_slice(&(3u64.to_le_bytes()));
|
||||
/// // The buffer can also be larger than the message size:
|
||||
/// // Here `cats` is 4 bytes and 1 byte longer than the message size defined in the header
|
||||
/// buf.extend_from_slice(b"cats");
|
||||
/// Cursor::new(buf)
|
||||
/// };
|
||||
/// let mut decoder = LengthPrefixDecoder::new(vec![0; 8]);
|
||||
/// let msg = decoder.read_all_from_stdio(&mut data).expect("read failed");
|
||||
/// assert_eq!(msg, b"cat");
|
||||
/// ```
|
||||
///
|
||||
/// ## MessageTooLargeError
|
||||
///
|
||||
/// Buffer of the `LengthPrefixDecoder` configured to be too small:
|
||||
/// ```
|
||||
/// # use std::io::Cursor;
|
||||
/// # use rosenpass_util::length_prefix_encoding::decoder::{LengthPrefixDecoder, ReadFromIoError, MessageTooLargeError};
|
||||
/// let mut data: Cursor<Vec<u8>> = {
|
||||
/// let mut buf = Vec::new();
|
||||
/// buf.extend_from_slice(&(7u64.to_le_bytes()));
|
||||
/// buf.extend_from_slice(b"giraffe");
|
||||
/// Cursor::new(buf)
|
||||
/// };
|
||||
/// // Buffer is too small, should be at least 7 bytes (defined in the header)
|
||||
/// let mut decoder = LengthPrefixDecoder::new(vec![0; 5]);
|
||||
/// let err = decoder.read_all_from_stdio(&mut data).expect_err("read should have failed");
|
||||
/// assert!(matches!(err, ReadFromIoError::MessageTooLargeError(_)));
|
||||
/// ```
|
||||
///
|
||||
/// ## IOError (EOF)
|
||||
/// ```
|
||||
/// # use std::io::Cursor;
|
||||
/// # use rosenpass_util::length_prefix_encoding::decoder::{LengthPrefixDecoder, ReadFromIoError, MessageTooLargeError};
|
||||
/// let mut data: Cursor<Vec<u8>> = {
|
||||
/// let mut buf = Vec::new();
|
||||
/// // Message size set to 10 bytes, but the message is only 7 bytes long
|
||||
/// buf.extend_from_slice(&(10u64.to_le_bytes()));
|
||||
/// buf.extend_from_slice(b"giraffe");
|
||||
/// Cursor::new(buf)
|
||||
/// };
|
||||
/// let mut decoder = LengthPrefixDecoder::new(vec![0; 10]);
|
||||
/// let err = decoder.read_all_from_stdio(&mut data).expect_err("read should have failed");
|
||||
/// assert!(matches!(err, ReadFromIoError::IoError(_)));
|
||||
/// ```
|
||||
pub fn read_all_from_stdio<R: io::Read>(
|
||||
&mut self,
|
||||
mut r: R,
|
||||
@@ -125,6 +284,19 @@ impl<Buf: BorrowMut<[u8]>> LengthPrefixDecoder<Buf> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to read from the given `Read` source into the decoder.
|
||||
///
|
||||
/// On success, returns how many bytes were read and a mutable slice of the complete message if fully available.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use std::io::Cursor;
|
||||
/// # use rosenpass_util::length_prefix_encoding::decoder::{LengthPrefixDecoder, ReadFromIoReturn};
|
||||
/// let mut data = Cursor::new([4u64.to_le_bytes().as_slice(), b"cats"].concat());
|
||||
/// let mut decoder = LengthPrefixDecoder::new(vec![0; 8]);
|
||||
/// decoder.read_from_stdio(&mut data).expect("read failed");
|
||||
/// ```
|
||||
pub fn read_from_stdio<R: io::Read>(
|
||||
&mut self,
|
||||
mut r: R,
|
||||
@@ -150,6 +322,12 @@ impl<Buf: BorrowMut<[u8]>> LengthPrefixDecoder<Buf> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the next slice of internal buffer that needs data.
|
||||
///
|
||||
/// If the header is not yet fully read, returns the remaining part of the header buffer.
|
||||
/// Otherwise, returns the remaining part of the message buffer if the message size is known.
|
||||
///
|
||||
/// If no more data is needed (message fully read), returns `Ok(None)`.
|
||||
pub fn next_slice_to_write_to(&mut self) -> Result<Option<&mut [u8]>, MessageTooLargeError> {
|
||||
fn some_if_nonempty(buf: &mut [u8]) -> Option<&mut [u8]> {
|
||||
match buf.is_empty() {
|
||||
@@ -172,6 +350,9 @@ impl<Buf: BorrowMut<[u8]>> LengthPrefixDecoder<Buf> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Advances the internal offset by `count` bytes.
|
||||
///
|
||||
/// This checks that the offset does not exceed buffer or message limits.
|
||||
pub fn advance(&mut self, count: usize) -> Result<(), SanityError> {
|
||||
let off = self.off + count;
|
||||
let msg_off = off.saturating_sub(HEADER_SIZE);
|
||||
@@ -189,6 +370,9 @@ impl<Buf: BorrowMut<[u8]>> LengthPrefixDecoder<Buf> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks that the allocated message buffer is large enough for the message length.
|
||||
///
|
||||
/// If the header is not fully read, this does nothing. If it is, ensures the buffer fits the message.
|
||||
pub fn ensure_sufficient_msg_buffer(&self) -> Result<(), MessageTooLargeError> {
|
||||
let buf_size = self.message_buffer().len();
|
||||
let msg_size = match self.get_header() {
|
||||
@@ -198,43 +382,64 @@ impl<Buf: BorrowMut<[u8]>> LengthPrefixDecoder<Buf> {
|
||||
MessageTooLargeError::ensure(msg_size, buf_size)
|
||||
}
|
||||
|
||||
/// Returns a reference to the header buffer.
|
||||
pub fn header_buffer(&self) -> &[u8] {
|
||||
&self.header[..]
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the header buffer.
|
||||
pub fn header_buffer_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.header[..]
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying message buffer.
|
||||
pub fn message_buffer(&self) -> &[u8] {
|
||||
self.buf.borrow()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the underlying message buffer.
|
||||
pub fn message_buffer_mut(&mut self) -> &mut [u8] {
|
||||
self.buf.borrow_mut()
|
||||
}
|
||||
|
||||
/// Returns a reference to the total number of bytes read so far.
|
||||
pub fn bytes_read(&self) -> &usize {
|
||||
&self.off
|
||||
}
|
||||
|
||||
/// Consumes the decoder and returns the underlying buffer.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::io::Cursor;
|
||||
/// # use rosenpass_util::length_prefix_encoding::decoder::{LengthPrefixDecoder, ReadFromIoReturn};
|
||||
/// let mut data = Cursor::new([4u64.to_le_bytes().as_slice(), b"cats"].concat());
|
||||
/// let mut decoder = LengthPrefixDecoder::new(vec![0; 8]);
|
||||
/// decoder.read_all_from_stdio(&mut data).expect("read failed");
|
||||
/// let buffer: Vec<u8> = decoder.into_message_buffer();
|
||||
/// assert_eq!(buffer, vec![99, 97, 116, 115, 0, 0, 0, 0]);
|
||||
/// ```
|
||||
pub fn into_message_buffer(self) -> Buf {
|
||||
let Self { buf, .. } = self;
|
||||
buf
|
||||
}
|
||||
|
||||
/// Returns the current offset into the header buffer.
|
||||
pub fn header_buffer_offset(&self) -> usize {
|
||||
min(self.off, HEADER_SIZE)
|
||||
}
|
||||
|
||||
/// Returns the current offset into the message buffer.
|
||||
pub fn message_buffer_offset(&self) -> usize {
|
||||
self.off.saturating_sub(HEADER_SIZE)
|
||||
}
|
||||
|
||||
/// Returns whether the header has been fully read.
|
||||
pub fn has_header(&self) -> bool {
|
||||
self.header_buffer_offset() == HEADER_SIZE
|
||||
}
|
||||
|
||||
/// Returns `true` if the entire message has been read, `false` otherwise.
|
||||
pub fn has_message(&self) -> Result<bool, MessageTooLargeError> {
|
||||
self.ensure_sufficient_msg_buffer()?;
|
||||
let msg_size = match self.get_header() {
|
||||
@@ -244,46 +449,55 @@ impl<Buf: BorrowMut<[u8]>> LengthPrefixDecoder<Buf> {
|
||||
Ok(self.message_buffer_avail().len() == msg_size)
|
||||
}
|
||||
|
||||
/// Returns the currently read portion of the header.
|
||||
pub fn header_buffer_avail(&self) -> &[u8] {
|
||||
let off = self.header_buffer_offset();
|
||||
&self.header_buffer()[..off]
|
||||
}
|
||||
|
||||
/// Returns a mutable slice of the currently read portion of the header.
|
||||
pub fn header_buffer_avail_mut(&mut self) -> &mut [u8] {
|
||||
let off = self.header_buffer_offset();
|
||||
&mut self.header_buffer_mut()[..off]
|
||||
}
|
||||
|
||||
/// Returns the remaining unread portion of the header.
|
||||
pub fn header_buffer_left(&self) -> &[u8] {
|
||||
let off = self.header_buffer_offset();
|
||||
&self.header_buffer()[off..]
|
||||
}
|
||||
|
||||
/// Returns a mutable slice of the remaining unread portion of the header.
|
||||
pub fn header_buffer_left_mut(&mut self) -> &mut [u8] {
|
||||
let off = self.header_buffer_offset();
|
||||
&mut self.header_buffer_mut()[off..]
|
||||
}
|
||||
|
||||
/// Returns the currently read portion of the message.
|
||||
pub fn message_buffer_avail(&self) -> &[u8] {
|
||||
let off = self.message_buffer_offset();
|
||||
&self.message_buffer()[..off]
|
||||
}
|
||||
|
||||
/// Returns a mutable slice of the currently read portion of the message.
|
||||
pub fn message_buffer_avail_mut(&mut self) -> &mut [u8] {
|
||||
let off = self.message_buffer_offset();
|
||||
&mut self.message_buffer_mut()[..off]
|
||||
}
|
||||
|
||||
/// Returns the remaining unread portion of the message buffer.
|
||||
pub fn message_buffer_left(&self) -> &[u8] {
|
||||
let off = self.message_buffer_offset();
|
||||
&self.message_buffer()[off..]
|
||||
}
|
||||
|
||||
/// Returns a mutable slice of the remaining unread portion of the message buffer.
|
||||
pub fn message_buffer_left_mut(&mut self) -> &mut [u8] {
|
||||
let off = self.message_buffer_offset();
|
||||
&mut self.message_buffer_mut()[off..]
|
||||
}
|
||||
|
||||
/// Returns the message size from the header if fully read.
|
||||
pub fn get_header(&self) -> Option<usize> {
|
||||
match self.header_buffer_offset() == HEADER_SIZE {
|
||||
false => None,
|
||||
@@ -291,19 +505,23 @@ impl<Buf: BorrowMut<[u8]>> LengthPrefixDecoder<Buf> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the message size if known (i.e., if the header is fully read).
|
||||
pub fn message_size(&self) -> Option<usize> {
|
||||
self.get_header()
|
||||
}
|
||||
|
||||
/// Returns the total size of the encoded message (header + payload) if known.
|
||||
pub fn encoded_message_bytes(&self) -> Option<usize> {
|
||||
self.message_size().map(|sz| sz + HEADER_SIZE)
|
||||
}
|
||||
|
||||
/// Returns the complete message fragment if the header is known and buffer is sufficient.
|
||||
pub fn message_fragment(&self) -> Result<Option<&[u8]>, MessageTooLargeError> {
|
||||
self.ensure_sufficient_msg_buffer()?;
|
||||
Ok(self.message_size().map(|sz| &self.message_buffer()[..sz]))
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the complete message fragment.
|
||||
pub fn message_fragment_mut(&mut self) -> Result<Option<&mut [u8]>, MessageTooLargeError> {
|
||||
self.ensure_sufficient_msg_buffer()?;
|
||||
Ok(self
|
||||
@@ -311,12 +529,14 @@ impl<Buf: BorrowMut<[u8]>> LengthPrefixDecoder<Buf> {
|
||||
.map(|sz| &mut self.message_buffer_mut()[..sz]))
|
||||
}
|
||||
|
||||
/// Returns the portion of the message fragment that has been filled so far.
|
||||
pub fn message_fragment_avail(&self) -> Result<Option<&[u8]>, MessageTooLargeError> {
|
||||
let off = self.message_buffer_avail().len();
|
||||
self.message_fragment()
|
||||
.map(|frag| frag.map(|frag| &frag[..off]))
|
||||
}
|
||||
|
||||
/// Returns a mutable portion of the message fragment that has been filled so far.
|
||||
pub fn message_fragment_avail_mut(
|
||||
&mut self,
|
||||
) -> Result<Option<&mut [u8]>, MessageTooLargeError> {
|
||||
@@ -325,24 +545,32 @@ impl<Buf: BorrowMut<[u8]>> LengthPrefixDecoder<Buf> {
|
||||
.map(|frag| frag.map(|frag| &mut frag[..off]))
|
||||
}
|
||||
|
||||
/// Returns the remaining portion of the message fragment that still needs to be read.
|
||||
pub fn message_fragment_left(&self) -> Result<Option<&[u8]>, MessageTooLargeError> {
|
||||
let off = self.message_buffer_avail().len();
|
||||
self.message_fragment()
|
||||
.map(|frag| frag.map(|frag| &frag[off..]))
|
||||
}
|
||||
|
||||
/// Returns a mutable slice of the remaining portion of the message fragment that still needs to be read.
|
||||
pub fn message_fragment_left_mut(&mut self) -> Result<Option<&mut [u8]>, MessageTooLargeError> {
|
||||
let off = self.message_buffer_avail().len();
|
||||
self.message_fragment_mut()
|
||||
.map(|frag| frag.map(|frag| &mut frag[off..]))
|
||||
}
|
||||
|
||||
/// If the entire message is available, returns a reference to it.
|
||||
///
|
||||
/// Otherwise returns `Ok(None)`.
|
||||
pub fn message(&self) -> Result<Option<&[u8]>, MessageTooLargeError> {
|
||||
let sz = self.message_size();
|
||||
self.message_fragment_avail()
|
||||
.map(|frag_opt| frag_opt.and_then(|frag| (frag.len() == sz?).then_some(frag)))
|
||||
}
|
||||
|
||||
/// If the entire message is available, returns a mutable reference to it.
|
||||
///
|
||||
/// Otherwise returns `Ok(None)`.
|
||||
pub fn message_mut(&mut self) -> Result<Option<&mut [u8]>, MessageTooLargeError> {
|
||||
let sz = self.message_size();
|
||||
self.message_fragment_avail_mut()
|
||||
@@ -357,3 +585,107 @@ impl<Buf: BorrowMut<[u8]>> Zeroize for LengthPrefixDecoder<Buf> {
|
||||
self.off.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_read_from_stdio() {
|
||||
use std::io::Cursor;
|
||||
let mut data = {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&(8u64.to_le_bytes()));
|
||||
buf.extend_from_slice(b"cats"); // provide only half of the message
|
||||
Cursor::new(buf)
|
||||
};
|
||||
|
||||
let mut decoder = LengthPrefixDecoder::new(vec![0; 9]);
|
||||
|
||||
fn loop_read(decoder: &mut LengthPrefixDecoder<Vec<u8>>, data: &mut Cursor<Vec<u8>>) {
|
||||
// Read until the buffer is fully read
|
||||
let data_len = data.get_ref().len();
|
||||
loop {
|
||||
let result: ReadFromIoReturn =
|
||||
decoder.read_from_stdio(&mut *data).expect("read failed");
|
||||
if data.position() as usize == data_len {
|
||||
// the entire data was read
|
||||
break;
|
||||
}
|
||||
assert!(result.message.is_none());
|
||||
assert!(result.bytes_read > 0); // at least 1 byte was read (or all data was read)
|
||||
}
|
||||
}
|
||||
|
||||
loop_read(&mut decoder, &mut data);
|
||||
|
||||
// INSERT HERE A TEST FOR EACH INTERNAL METHOD OF LengthPrefixDecoder (decoder)
|
||||
assert_eq!(decoder.message_size(), Some(8));
|
||||
|
||||
// Header-related assertions
|
||||
assert!(decoder.has_header());
|
||||
assert_eq!(decoder.has_message().ok(), Some(false));
|
||||
assert_eq!(decoder.header_buffer_offset(), HEADER_SIZE);
|
||||
assert_eq!(decoder.header_buffer_avail().len(), HEADER_SIZE);
|
||||
assert_eq!(decoder.header_buffer_left().len(), 0);
|
||||
{
|
||||
let header_buffer_mut: &mut [u8] = decoder.header_buffer_avail_mut();
|
||||
assert_eq!(header_buffer_mut, &[8, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let header_buffer_ref: &[u8] = decoder.header_buffer_avail();
|
||||
assert_eq!(header_buffer_ref, &[8, 0, 0, 0, 0, 0, 0, 0]);
|
||||
}
|
||||
assert_eq!(decoder.get_header(), Some(8));
|
||||
assert_eq!(decoder.message_size(), Some(8));
|
||||
assert_eq!(decoder.encoded_message_bytes(), Some(8 + HEADER_SIZE));
|
||||
|
||||
// Message-related assertions
|
||||
assert_eq!(*decoder.bytes_read(), 12);
|
||||
assert_eq!(decoder.message_buffer_offset(), 4); // "cats" is 4 bytes
|
||||
assert_eq!(decoder.message_buffer_avail(), b"cats");
|
||||
assert_eq!(decoder.message_buffer_avail_mut(), b"cats");
|
||||
assert_eq!(decoder.message_buffer_left().len(), 5); // buffer size is 9, 4 read -> 5 left
|
||||
assert_eq!(decoder.message_buffer_left_mut().len(), 5);
|
||||
assert!(!decoder.has_message().unwrap()); // not fully read
|
||||
|
||||
// Message fragment assertions
|
||||
let frag = decoder.message_fragment().unwrap().unwrap();
|
||||
assert_eq!(frag.len(), 8); // full message fragment slice (not fully filled)
|
||||
let frag_avail = decoder.message_fragment_avail().unwrap().unwrap();
|
||||
assert_eq!(frag_avail, b"cats"); // available portion matches what's read
|
||||
let frag_left = decoder.message_fragment_left().unwrap().unwrap();
|
||||
assert_eq!(frag_left.len(), 4); // 4 bytes remain to complete the message
|
||||
assert_eq!(decoder.message().unwrap(), None); // full message not yet available
|
||||
|
||||
// disassemble the decoder and reassemble it
|
||||
let (header, buf, off) = decoder.clone().into_parts();
|
||||
let mut decoder = LengthPrefixDecoder::from_parts(header, buf, off);
|
||||
|
||||
let mut data = Cursor::new(Vec::from(b"dogs"));
|
||||
loop_read(&mut decoder, &mut data);
|
||||
|
||||
// After providing the remaining "dogs" data, the message should now be fully available.
|
||||
assert!(decoder.has_message().unwrap());
|
||||
assert_eq!(decoder.message().unwrap().unwrap(), b"catsdogs");
|
||||
|
||||
// At this point:
|
||||
// - The entire message (8 bytes) plus the header (8 bytes for the length) should be accounted for.
|
||||
assert_eq!(
|
||||
decoder.message_fragment_avail().unwrap().unwrap(),
|
||||
b"catsdogs"
|
||||
);
|
||||
assert!(decoder.message_fragment_left().unwrap().unwrap().is_empty());
|
||||
|
||||
// The offsets and buffers should reflect that everything is read.
|
||||
assert_eq!(decoder.message_buffer_offset(), 8); // all 8 message bytes are now read
|
||||
assert_eq!(decoder.message_buffer_avail(), b"catsdogs");
|
||||
assert_eq!(decoder.message_buffer_left().len(), 1); // buffer size was 9, 8 read -> 1 left unused
|
||||
|
||||
// No more data needed to complete the message.
|
||||
assert!(decoder.next_slice_to_write_to().unwrap().is_none());
|
||||
|
||||
// clear the decoder
|
||||
decoder.clear();
|
||||
assert_eq!(decoder.buf, vec![0; 9]);
|
||||
assert_eq!(decoder.off, 0);
|
||||
assert_eq!(decoder.header, [0; HEADER_SIZE]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,46 +9,61 @@ use zeroize::Zeroize;
|
||||
|
||||
use crate::{io::IoResultKindHintExt, result::ensure_or};
|
||||
|
||||
/// Size of the length prefix header in bytes - equal to the size of a u64
|
||||
pub const HEADER_SIZE: usize = std::mem::size_of::<u64>();
|
||||
|
||||
#[derive(Error, Debug, Clone, Copy)]
|
||||
#[error("Write position is out of buffer bounds")]
|
||||
/// Error type indicating that a write position is beyond the boundaries of the allocated buffer
|
||||
pub struct PositionOutOfBufferBounds;
|
||||
|
||||
#[derive(Error, Debug, Clone, Copy)]
|
||||
#[error("Write position is out of message bounds")]
|
||||
/// Error type indicating that a write position is beyond the boundaries of the message
|
||||
pub struct PositionOutOfMessageBounds;
|
||||
|
||||
#[derive(Error, Debug, Clone, Copy)]
|
||||
#[error("Write position is out of header bounds")]
|
||||
/// Error type indicating that a write position is beyond the boundaries of the header
|
||||
pub struct PositionOutOfHeaderBounds;
|
||||
|
||||
#[derive(Error, Debug, Clone, Copy)]
|
||||
#[error("Message length is bigger than buffer length")]
|
||||
/// Error type indicating that the message length is larger than the available buffer space
|
||||
pub struct MessageTooLarge;
|
||||
|
||||
#[derive(Error, Debug, Clone, Copy)]
|
||||
/// Error type for message length sanity checks
|
||||
pub enum MessageLenSanityError {
|
||||
/// Error indicating position is beyond message boundaries
|
||||
#[error("{0:?}")]
|
||||
PositionOutOfMessageBounds(#[from] PositionOutOfMessageBounds),
|
||||
/// Error indicating message length exceeds buffer capacity
|
||||
#[error("{0:?}")]
|
||||
MessageTooLarge(#[from] MessageTooLarge),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone, Copy)]
|
||||
/// Error type for position bounds checking
|
||||
pub enum PositionSanityError {
|
||||
/// Error indicating position is beyond message boundaries
|
||||
#[error("{0:?}")]
|
||||
PositionOutOfMessageBounds(#[from] PositionOutOfMessageBounds),
|
||||
/// Error indicating position is beyond buffer boundaries
|
||||
#[error("{0:?}")]
|
||||
PositionOutOfBufferBounds(#[from] PositionOutOfBufferBounds),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone, Copy)]
|
||||
/// Error type combining all sanity check errors
|
||||
pub enum SanityError {
|
||||
/// Error indicating position is beyond message boundaries
|
||||
#[error("{0:?}")]
|
||||
PositionOutOfMessageBounds(#[from] PositionOutOfMessageBounds),
|
||||
/// Error indicating position is beyond buffer boundaries
|
||||
#[error("{0:?}")]
|
||||
PositionOutOfBufferBounds(#[from] PositionOutOfBufferBounds),
|
||||
/// Error indicating message length exceeds buffer capacity
|
||||
#[error("{0:?}")]
|
||||
MessageTooLarge(#[from] MessageTooLarge),
|
||||
}
|
||||
@@ -86,12 +101,16 @@ impl From<PositionSanityError> for SanityError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of a write operation on an IO stream
|
||||
pub struct WriteToIoReturn {
|
||||
/// Number of bytes successfully written in this operation
|
||||
pub bytes_written: usize,
|
||||
/// Whether the write operation has completed fully
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// Length-prefixed encoder that adds a length header to data before writing
|
||||
pub struct LengthPrefixEncoder<Buf: Borrow<[u8]>> {
|
||||
buf: Buf,
|
||||
header: [u8; HEADER_SIZE],
|
||||
@@ -99,6 +118,7 @@ pub struct LengthPrefixEncoder<Buf: Borrow<[u8]>> {
|
||||
}
|
||||
|
||||
impl<Buf: Borrow<[u8]>> LengthPrefixEncoder<Buf> {
|
||||
/// Creates a new encoder from a buffer
|
||||
pub fn from_buffer(buf: Buf) -> Self {
|
||||
let (header, pos) = ([0u8; HEADER_SIZE], 0);
|
||||
let mut r = Self { buf, header, pos };
|
||||
@@ -106,6 +126,7 @@ impl<Buf: Borrow<[u8]>> LengthPrefixEncoder<Buf> {
|
||||
r
|
||||
}
|
||||
|
||||
/// Creates a new encoder using the full buffer as a message
|
||||
pub fn from_message(msg: Buf) -> Self {
|
||||
let mut r = Self::from_buffer(msg);
|
||||
r.restart_write_with_new_message(r.buffer_bytes().len())
|
||||
@@ -113,23 +134,27 @@ impl<Buf: Borrow<[u8]>> LengthPrefixEncoder<Buf> {
|
||||
r
|
||||
}
|
||||
|
||||
/// Creates a new encoder using part of the buffer as a message
|
||||
pub fn from_short_message(msg: Buf, len: usize) -> Result<Self, MessageLenSanityError> {
|
||||
let mut r = Self::from_message(msg);
|
||||
r.set_message_len(len)?;
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Creates a new encoder from buffer, message length and write position
|
||||
pub fn from_parts(buf: Buf, len: usize, pos: usize) -> Result<Self, SanityError> {
|
||||
let mut r = Self::from_buffer(buf);
|
||||
r.set_msg_len_and_position(len, pos)?;
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Consumes the encoder and returns the underlying buffer
|
||||
pub fn into_buffer(self) -> Buf {
|
||||
let Self { buf, .. } = self;
|
||||
buf
|
||||
}
|
||||
|
||||
/// Consumes the encoder and returns buffer, message length and write position
|
||||
pub fn into_parts(self) -> (Buf, usize, usize) {
|
||||
let len = self.message_len();
|
||||
let pos = self.writing_position();
|
||||
@@ -137,11 +162,13 @@ impl<Buf: Borrow<[u8]>> LengthPrefixEncoder<Buf> {
|
||||
(buf, len, pos)
|
||||
}
|
||||
|
||||
/// Resets the encoder state
|
||||
pub fn clear(&mut self) {
|
||||
self.set_msg_len_and_position(0, 0).unwrap();
|
||||
self.set_message_offset(0).unwrap();
|
||||
}
|
||||
|
||||
/// Writes the full message to an IO writer, retrying on interrupts
|
||||
pub fn write_all_to_stdio<W: io::Write>(&mut self, mut w: W) -> io::Result<()> {
|
||||
use io::ErrorKind as K;
|
||||
loop {
|
||||
@@ -158,6 +185,7 @@ impl<Buf: Borrow<[u8]>> LengthPrefixEncoder<Buf> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the next chunk of data to an IO writer and returns number of bytes written and completion status
|
||||
pub fn write_to_stdio<W: io::Write>(&mut self, mut w: W) -> io::Result<WriteToIoReturn> {
|
||||
if self.exhausted() {
|
||||
return Ok(WriteToIoReturn {
|
||||
@@ -177,10 +205,12 @@ impl<Buf: Borrow<[u8]>> LengthPrefixEncoder<Buf> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Resets write position to start for restarting output
|
||||
pub fn restart_write(&mut self) {
|
||||
self.set_writing_position(0).unwrap()
|
||||
}
|
||||
|
||||
/// Resets write position to start and updates message length for restarting with new data
|
||||
pub fn restart_write_with_new_message(
|
||||
&mut self,
|
||||
len: usize,
|
||||
@@ -189,6 +219,7 @@ impl<Buf: Borrow<[u8]>> LengthPrefixEncoder<Buf> {
|
||||
.map_err(|e| e.try_into().unwrap())
|
||||
}
|
||||
|
||||
/// Returns the next unwritten slice of data to write from header or message
|
||||
pub fn next_slice_to_write(&self) -> &[u8] {
|
||||
let s = self.header_left();
|
||||
if !s.is_empty() {
|
||||
@@ -203,66 +234,82 @@ impl<Buf: Borrow<[u8]>> LengthPrefixEncoder<Buf> {
|
||||
&[]
|
||||
}
|
||||
|
||||
/// Returns true if all data including header and message has been written
|
||||
pub fn exhausted(&self) -> bool {
|
||||
self.next_slice_to_write().is_empty()
|
||||
}
|
||||
|
||||
/// Returns slice containing full message data
|
||||
pub fn message(&self) -> &[u8] {
|
||||
&self.buffer_bytes()[..self.message_len()]
|
||||
}
|
||||
|
||||
/// Returns slice containing written portion of length header
|
||||
pub fn header_written(&self) -> &[u8] {
|
||||
&self.header()[..self.header_offset()]
|
||||
}
|
||||
|
||||
/// Returns slice containing unwritten portion of length header
|
||||
pub fn header_left(&self) -> &[u8] {
|
||||
&self.header()[self.header_offset()..]
|
||||
}
|
||||
|
||||
/// Returns slice containing written portion of message data
|
||||
pub fn message_written(&self) -> &[u8] {
|
||||
&self.message()[..self.message_offset()]
|
||||
}
|
||||
|
||||
/// Returns slice containing unwritten portion of message data
|
||||
pub fn message_left(&self) -> &[u8] {
|
||||
&self.message()[self.message_offset()..]
|
||||
}
|
||||
|
||||
/// Returns reference to underlying buffer
|
||||
pub fn buf(&self) -> &Buf {
|
||||
&self.buf
|
||||
}
|
||||
|
||||
/// Returns slice view of underlying buffer bytes
|
||||
pub fn buffer_bytes(&self) -> &[u8] {
|
||||
self.buf().borrow()
|
||||
}
|
||||
|
||||
/// Decodes and returns length header value as u64
|
||||
pub fn decode_header(&self) -> u64 {
|
||||
u64::from_le_bytes(self.header)
|
||||
}
|
||||
|
||||
/// Returns slice containing raw length header bytes
|
||||
pub fn header(&self) -> &[u8; HEADER_SIZE] {
|
||||
&self.header
|
||||
}
|
||||
|
||||
/// Returns decoded message length from header
|
||||
pub fn message_len(&self) -> usize {
|
||||
self.decode_header() as usize
|
||||
}
|
||||
|
||||
/// Returns total encoded size including header and message bytes
|
||||
pub fn encoded_message_bytes(&self) -> usize {
|
||||
self.message_len() + HEADER_SIZE
|
||||
}
|
||||
|
||||
/// Returns current write position within header and message
|
||||
pub fn writing_position(&self) -> usize {
|
||||
self.pos
|
||||
}
|
||||
|
||||
/// Returns write offset within length header bytes
|
||||
pub fn header_offset(&self) -> usize {
|
||||
min(self.writing_position(), HEADER_SIZE)
|
||||
}
|
||||
|
||||
/// Returns write offset within message bytes
|
||||
pub fn message_offset(&self) -> usize {
|
||||
self.writing_position().saturating_sub(HEADER_SIZE)
|
||||
}
|
||||
|
||||
/// Sets new length header bytes with bounds checking
|
||||
pub fn set_header(&mut self, header: [u8; HEADER_SIZE]) -> Result<(), MessageLenSanityError> {
|
||||
self.offset_transaction(|t| {
|
||||
t.header = header;
|
||||
@@ -272,14 +319,17 @@ impl<Buf: Borrow<[u8]>> LengthPrefixEncoder<Buf> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Encodes and sets length header value with bounds checking
|
||||
pub fn encode_and_set_header(&mut self, header: u64) -> Result<(), MessageLenSanityError> {
|
||||
self.set_header(header.to_le_bytes())
|
||||
}
|
||||
|
||||
/// Sets message lengthwith bounds checking
|
||||
pub fn set_message_len(&mut self, len: usize) -> Result<(), MessageLenSanityError> {
|
||||
self.encode_and_set_header(len as u64)
|
||||
}
|
||||
|
||||
/// Sets write position with message and buffer bounds checking
|
||||
pub fn set_writing_position(&mut self, pos: usize) -> Result<(), PositionSanityError> {
|
||||
self.offset_transaction(|t| {
|
||||
t.pos = pos;
|
||||
@@ -289,20 +339,24 @@ impl<Buf: Borrow<[u8]>> LengthPrefixEncoder<Buf> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets write position within header bytes with bounds checking
|
||||
pub fn set_header_offset(&mut self, off: usize) -> Result<(), PositionOutOfHeaderBounds> {
|
||||
ensure_or(off <= HEADER_SIZE, PositionOutOfHeaderBounds)?;
|
||||
self.set_writing_position(off).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets write position within message bytes with bounds checking
|
||||
pub fn set_message_offset(&mut self, off: usize) -> Result<(), PositionSanityError> {
|
||||
self.set_writing_position(off + HEADER_SIZE)
|
||||
}
|
||||
|
||||
/// Advances write position by specified offset with bounds checking
|
||||
pub fn advance(&mut self, off: usize) -> Result<(), PositionSanityError> {
|
||||
self.set_writing_position(self.writing_position() + off)
|
||||
}
|
||||
|
||||
/// Sets message length and write position with bounds checking
|
||||
pub fn set_msg_len_and_position(&mut self, len: usize, pos: usize) -> Result<(), SanityError> {
|
||||
self.pos = 0;
|
||||
self.set_message_len(len)?;
|
||||
@@ -347,24 +401,29 @@ impl<Buf: Borrow<[u8]>> LengthPrefixEncoder<Buf> {
|
||||
}
|
||||
|
||||
impl<Buf: BorrowMut<[u8]>> LengthPrefixEncoder<Buf> {
|
||||
/// Gets a mutable reference to the underlying buffer
|
||||
pub fn buf_mut(&mut self) -> &mut Buf {
|
||||
&mut self.buf
|
||||
}
|
||||
|
||||
/// Gets the buffer as mutable bytes
|
||||
pub fn buffer_bytes_mut(&mut self) -> &mut [u8] {
|
||||
self.buf.borrow_mut()
|
||||
}
|
||||
|
||||
/// Gets a mutable reference to the message slice
|
||||
pub fn message_mut(&mut self) -> &mut [u8] {
|
||||
let off = self.message_len();
|
||||
&mut self.buffer_bytes_mut()[..off]
|
||||
}
|
||||
|
||||
/// Gets a mutable reference to the written portion of the message
|
||||
pub fn message_written_mut(&mut self) -> &mut [u8] {
|
||||
let off = self.message_offset();
|
||||
&mut self.message_mut()[..off]
|
||||
}
|
||||
|
||||
/// Gets a mutable reference to the unwritten portion of the message
|
||||
pub fn message_left_mut(&mut self) -> &mut [u8] {
|
||||
let off = self.message_offset();
|
||||
&mut self.message_mut()[off..]
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
/// Module that handles decoding functionality
|
||||
pub mod decoder;
|
||||
/// Module that handles encoding functionality
|
||||
pub mod encoder;
|
||||
|
||||
@@ -2,19 +2,37 @@
|
||||
#![warn(clippy::missing_docs_in_private_items)]
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
//! Core utility functions and types used across the codebase.
|
||||
|
||||
/// Base64 encoding and decoding functionality.
|
||||
pub mod b64;
|
||||
/// Build-time utilities and macros.
|
||||
pub mod build;
|
||||
/// Control flow abstractions and utilities.
|
||||
pub mod controlflow;
|
||||
/// File descriptor utilities.
|
||||
pub mod fd;
|
||||
/// File system operations and handling.
|
||||
pub mod file;
|
||||
/// Functional programming utilities.
|
||||
pub mod functional;
|
||||
/// Input/output operations.
|
||||
pub mod io;
|
||||
/// Length prefix encoding schemes implementation.
|
||||
pub mod length_prefix_encoding;
|
||||
/// Memory manipulation and allocation utilities.
|
||||
pub mod mem;
|
||||
/// MIO integration utilities.
|
||||
pub mod mio;
|
||||
/// Extended Option type functionality.
|
||||
pub mod option;
|
||||
/// Extended Result type functionality.
|
||||
pub mod result;
|
||||
/// Time and duration utilities.
|
||||
pub mod time;
|
||||
/// Type-level numbers and arithmetic.
|
||||
pub mod typenum;
|
||||
/// Zero-copy serialization utilities.
|
||||
pub mod zerocopy;
|
||||
/// Memory wiping utilities.
|
||||
pub mod zeroize;
|
||||
|
||||
@@ -22,6 +22,7 @@ macro_rules! cat {
|
||||
}
|
||||
|
||||
// TODO: consistent inout ordering
|
||||
/// Copy all bytes from `src` to `dst`. The lengths must match.
|
||||
pub fn cpy<T: BorrowMut<[u8]> + ?Sized, F: Borrow<[u8]> + ?Sized>(src: &F, dst: &mut T) {
|
||||
dst.borrow_mut().copy_from_slice(src.borrow());
|
||||
}
|
||||
@@ -41,11 +42,13 @@ pub struct Forgetting<T> {
|
||||
}
|
||||
|
||||
impl<T> Forgetting<T> {
|
||||
/// Creates a new `Forgetting<T>` instance containing the given value.
|
||||
pub fn new(value: T) -> Self {
|
||||
let value = Some(value);
|
||||
Self { value }
|
||||
}
|
||||
|
||||
/// Extracts and returns the contained value, consuming self.
|
||||
pub fn extract(mut self) -> T {
|
||||
let mut value = None;
|
||||
swap(&mut value, &mut self.value);
|
||||
@@ -93,7 +96,9 @@ impl<T> Drop for Forgetting<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait that provides a method to discard a value without explicitly handling its results.
|
||||
pub trait DiscardResultExt {
|
||||
/// Consumes and discards a value without doing anything with it.
|
||||
fn discard_result(self);
|
||||
}
|
||||
|
||||
@@ -101,7 +106,9 @@ impl<T> DiscardResultExt for T {
|
||||
fn discard_result(self) {}
|
||||
}
|
||||
|
||||
/// Trait that provides a method to explicitly forget values.
|
||||
pub trait ForgetExt {
|
||||
/// Consumes and forgets a value, preventing its destructor from running.
|
||||
fn forget(self);
|
||||
}
|
||||
|
||||
@@ -111,8 +118,11 @@ impl<T> ForgetExt for T {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait that provides methods for swapping values.
|
||||
pub trait SwapWithExt {
|
||||
/// Takes ownership of `other` and swaps its value with `self`, returning the original value.
|
||||
fn swap_with(&mut self, other: Self) -> Self;
|
||||
/// Swaps the values between `self` and `other` in place.
|
||||
fn swap_with_mut(&mut self, other: &mut Self);
|
||||
}
|
||||
|
||||
@@ -127,7 +137,9 @@ impl<T> SwapWithExt for T {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait that provides methods for swapping values with default values.
|
||||
pub trait SwapWithDefaultExt {
|
||||
/// Takes the current value and replaces it with the default value, returning the original.
|
||||
fn swap_with_default(&mut self) -> Self;
|
||||
}
|
||||
|
||||
@@ -137,6 +149,7 @@ impl<T: Default> SwapWithDefaultExt for T {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait that provides a method to explicitly move values.
|
||||
pub trait MoveExt {
|
||||
/// Deliberately move the value
|
||||
///
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
use mio::net::{UnixListener, UnixStream};
|
||||
use rustix::fd::{OwnedFd, RawFd};
|
||||
use std::os::fd::{OwnedFd, RawFd};
|
||||
|
||||
use crate::{
|
||||
fd::{claim_fd, claim_fd_inplace},
|
||||
result::OkExt,
|
||||
};
|
||||
|
||||
/// Module containing I/O interest flags for Unix operations
|
||||
pub mod interest {
|
||||
use mio::Interest;
|
||||
|
||||
/// Interest flag indicating readability
|
||||
pub const R: Interest = Interest::READABLE;
|
||||
|
||||
/// Interest flag indicating writability
|
||||
pub const W: Interest = Interest::WRITABLE;
|
||||
|
||||
/// Interest flag indicating both readability and writability
|
||||
pub const RW: Interest = R.add(W);
|
||||
}
|
||||
|
||||
/// Extension trait providing additional functionality for Unix listener
|
||||
pub trait UnixListenerExt: Sized {
|
||||
/// Creates a new Unix listener by claiming ownership of a raw file descriptor
|
||||
fn claim_fd(fd: RawFd) -> anyhow::Result<Self>;
|
||||
}
|
||||
|
||||
@@ -27,9 +36,15 @@ impl UnixListenerExt for UnixListener {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait providing additional functionality for Unix streams
|
||||
pub trait UnixStreamExt: Sized {
|
||||
/// Creates a new Unix stream from an owned file descriptor
|
||||
fn from_fd(fd: OwnedFd) -> anyhow::Result<Self>;
|
||||
|
||||
/// Claims ownership of a raw file descriptor and creates a new Unix stream
|
||||
fn claim_fd(fd: RawFd) -> anyhow::Result<Self>;
|
||||
|
||||
/// Claims ownership of a raw file descriptor in place and creates a new Unix stream
|
||||
fn claim_fd_inplace(fd: RawFd) -> anyhow::Result<Self>;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,15 @@ use std::{
|
||||
collections::VecDeque,
|
||||
io::Read,
|
||||
marker::PhantomData,
|
||||
os::fd::OwnedFd,
|
||||
os::fd::{FromRawFd, OwnedFd},
|
||||
};
|
||||
use uds::UnixStreamExt as FdPassingExt;
|
||||
|
||||
use crate::fd::{claim_fd_inplace, IntoStdioErr};
|
||||
|
||||
/// A wrapper around a socket that combines reading from the socket with tracking
|
||||
/// received file descriptors. Limits the maximum number of file descriptors that
|
||||
/// can be received in a single read operation via the `MAX_FDS` parameter.
|
||||
pub struct ReadWithFileDescriptors<const MAX_FDS: usize, Sock, BorrowSock, BorrowFds>
|
||||
where
|
||||
Sock: FdPassingExt,
|
||||
@@ -27,6 +30,8 @@ where
|
||||
BorrowSock: Borrow<Sock>,
|
||||
BorrowFds: BorrowMut<VecDeque<OwnedFd>>,
|
||||
{
|
||||
/// Creates a new `ReadWithFileDescriptors` by wrapping a socket and a file
|
||||
/// descriptor queue.
|
||||
pub fn new(socket: BorrowSock, fds: BorrowFds) -> Self {
|
||||
let _sock_dummy = PhantomData;
|
||||
Self {
|
||||
@@ -36,19 +41,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes the wrapper and returns the underlying socket and file
|
||||
/// descriptor queue.
|
||||
pub fn into_parts(self) -> (BorrowSock, BorrowFds) {
|
||||
let Self { socket, fds, .. } = self;
|
||||
(socket, fds)
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying socket.
|
||||
pub fn socket(&self) -> &Sock {
|
||||
self.socket.borrow()
|
||||
}
|
||||
|
||||
/// Returns a reference to the file descriptor queue.
|
||||
pub fn fds(&self) -> &VecDeque<OwnedFd> {
|
||||
self.fds.borrow()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the file descriptor queue.
|
||||
pub fn fds_mut(&mut self) -> &mut VecDeque<OwnedFd> {
|
||||
self.fds.borrow_mut()
|
||||
}
|
||||
@@ -61,6 +71,7 @@ where
|
||||
BorrowSock: BorrowMut<Sock>,
|
||||
BorrowFds: BorrowMut<VecDeque<OwnedFd>>,
|
||||
{
|
||||
/// Returns a mutable reference to the underlying socket.
|
||||
pub fn socket_mut(&mut self) -> &mut Sock {
|
||||
self.socket.borrow_mut()
|
||||
}
|
||||
@@ -115,7 +126,7 @@ where
|
||||
|
||||
// Close the remaining fds
|
||||
for fd in fd_iter {
|
||||
unsafe { rustix::io::close(*fd) };
|
||||
unsafe { drop(OwnedFd::from_raw_fd(*fd)) };
|
||||
}
|
||||
|
||||
claim_fd_result
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use rustix::fd::{AsFd, AsRawFd};
|
||||
use std::os::fd::{AsFd, AsRawFd};
|
||||
use std::{
|
||||
borrow::{Borrow, BorrowMut},
|
||||
cmp::min,
|
||||
@@ -10,6 +10,7 @@ use uds::UnixStreamExt as FdPassingExt;
|
||||
|
||||
use crate::{repeat, return_if};
|
||||
|
||||
/// A structure that facilitates writing data and file descriptors to a Unix domain socket
|
||||
pub struct WriteWithFileDescriptors<Sock, Fd, BorrowSock, BorrowFds>
|
||||
where
|
||||
Sock: FdPassingExt,
|
||||
@@ -30,6 +31,7 @@ where
|
||||
BorrowSock: Borrow<Sock>,
|
||||
BorrowFds: BorrowMut<VecDeque<Fd>>,
|
||||
{
|
||||
/// Creates a new `WriteWithFileDescriptors` instance with the given socket and file descriptor queue
|
||||
pub fn new(socket: BorrowSock, fds: BorrowFds) -> Self {
|
||||
let _sock_dummy = PhantomData;
|
||||
let _fd_dummy = PhantomData;
|
||||
@@ -41,19 +43,23 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes this instance and returns the underlying socket and file descriptor queue
|
||||
pub fn into_parts(self) -> (BorrowSock, BorrowFds) {
|
||||
let Self { socket, fds, .. } = self;
|
||||
(socket, fds)
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying socket
|
||||
pub fn socket(&self) -> &Sock {
|
||||
self.socket.borrow()
|
||||
}
|
||||
|
||||
/// Returns a reference to the file descriptor queue
|
||||
pub fn fds(&self) -> &VecDeque<Fd> {
|
||||
self.fds.borrow()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the file descriptor queue
|
||||
pub fn fds_mut(&mut self) -> &mut VecDeque<Fd> {
|
||||
self.fds.borrow_mut()
|
||||
}
|
||||
@@ -66,6 +72,7 @@ where
|
||||
BorrowSock: BorrowMut<Sock>,
|
||||
BorrowFds: BorrowMut<VecDeque<Fd>>,
|
||||
{
|
||||
/// Returns a mutable reference to the underlying socket
|
||||
pub fn socket_mut(&mut self) -> &mut Sock {
|
||||
self.socket.borrow_mut()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
/// A helper trait for turning any type value into `Some(value)`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::option::SomeExt;
|
||||
///
|
||||
/// let x = 42;
|
||||
/// let y = x.some();
|
||||
///
|
||||
/// assert_eq!(y, Some(42));
|
||||
/// ```
|
||||
pub trait SomeExt: Sized {
|
||||
/// Wraps the calling value in `Some()`.
|
||||
fn some(self) -> Option<Self> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ macro_rules! attempt {
|
||||
};
|
||||
}
|
||||
|
||||
/// Trait for the ok operation, which provides a way to convert a value into a Result
|
||||
pub trait OkExt<E>: Sized {
|
||||
/// Wraps a value in a Result::Ok variant
|
||||
fn ok(self) -> Result<Self, E>;
|
||||
}
|
||||
|
||||
@@ -25,6 +27,7 @@ impl<T, E> OkExt<E> for T {
|
||||
///
|
||||
/// Implementations must not panic.
|
||||
pub trait GuaranteedValue {
|
||||
/// The value type that will be returned by guaranteed()
|
||||
type Value;
|
||||
|
||||
/// Extract the contained value while being panic-safe, like .unwrap()
|
||||
@@ -35,7 +38,11 @@ pub trait GuaranteedValue {
|
||||
fn guaranteed(self) -> Self::Value;
|
||||
}
|
||||
|
||||
/// Extension trait for adding finally operation to types
|
||||
pub trait FinallyExt {
|
||||
/// Executes a closure with mutable access to self and returns self
|
||||
///
|
||||
/// The closure is guaranteed to be executed before returning.
|
||||
fn finally<F: FnOnce(&mut Self)>(self, f: F) -> Self;
|
||||
}
|
||||
|
||||
@@ -125,6 +132,18 @@ impl<T> GuaranteedValue for Guaranteed<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks a condition is true and returns an error if not.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rosenpass_util::result::ensure_or;
|
||||
/// let result = ensure_or(5 > 3, "not greater");
|
||||
/// assert!(result.is_ok());
|
||||
///
|
||||
/// let result = ensure_or(5 < 3, "not less");
|
||||
/// assert!(result.is_err());
|
||||
/// ```
|
||||
pub fn ensure_or<E>(b: bool, err: E) -> Result<(), E> {
|
||||
match b {
|
||||
true => Ok(()),
|
||||
@@ -132,6 +151,18 @@ pub fn ensure_or<E>(b: bool, err: E) -> Result<(), E> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates to an error if the condition is true.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rosenpass_util::result::bail_if;
|
||||
/// let result = bail_if(false, "not bailed");
|
||||
/// assert!(result.is_ok());
|
||||
///
|
||||
/// let result = bail_if(true, "bailed");
|
||||
/// assert!(result.is_err());
|
||||
/// ```
|
||||
pub fn bail_if<E>(b: bool, err: E) -> Result<(), E> {
|
||||
ensure_or(!b, err)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,19 @@ use std::time::Instant;
|
||||
/// This is a simple wrapper around `std::time::Instant` that provides a
|
||||
/// convenient way to get the seconds elapsed since the creation of the
|
||||
/// `Timebase` instance.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rosenpass_util::time::Timebase;
|
||||
///
|
||||
/// let timebase = Timebase::default();
|
||||
/// let now = timebase.now();
|
||||
/// assert!(now > 0.0);
|
||||
/// ```
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Timebase(Instant);
|
||||
pub struct Timebase(pub Instant);
|
||||
|
||||
impl Default for Timebase {
|
||||
// TODO: Implement new()?
|
||||
|
||||
@@ -16,6 +16,7 @@ macro_rules! typenum2const {
|
||||
|
||||
/// Trait implemented by constant integers to facilitate conversion to constant integers
|
||||
pub trait IntoConst<T> {
|
||||
/// The constant value after conversion
|
||||
const VALUE: T;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
//! This module provides utilities for working with zero-copy references
|
||||
//! and slices.
|
||||
//!
|
||||
//! It offers the following primary abstractions and traits:
|
||||
//!
|
||||
//! - [`RefMaker`](crate::zerocopy::RefMaker): A helper structure for safely
|
||||
//! creating `zerocopy::Ref` references from byte slices.
|
||||
//! - [`ZerocopyEmancipateExt`](crate::zerocopy::ZerocopyEmancipateExt):
|
||||
//! A trait to convert `Ref<B, T>` into a borrowed `Ref<&[u8], T>`.
|
||||
//! - [`ZerocopyEmancipateMutExt`](crate::zerocopy::ZerocopyEmancipateMutExt):
|
||||
//! A trait to convert `Ref<B, T>` into a borrowed mutable `Ref<&mut [u8], T>`.
|
||||
//! - [`ZerocopySliceExt`](crate::zerocopy::ZerocopySliceExt): Extension methods
|
||||
//! for parsing byte slices into zero-copy references.
|
||||
//! - [`ZerocopyMutSliceExt`](crate::zerocopy::ZerocopyMutSliceExt):
|
||||
//! Extension methods for parsing and zeroizing byte slices into zero-copy
|
||||
//! references.
|
||||
|
||||
mod ref_maker;
|
||||
mod zerocopy_ref_ext;
|
||||
mod zerocopy_slice_ext;
|
||||
|
||||
@@ -1,11 +1,44 @@
|
||||
use std::marker::PhantomData;
|
||||
//! A module providing the [`RefMaker`] type and its associated methods for constructing
|
||||
//! [`zerocopy::Ref`] references from byte buffers.
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use std::marker::PhantomData;
|
||||
use zerocopy::{ByteSlice, ByteSliceMut, Ref};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::zeroize::ZeroizedExt;
|
||||
|
||||
/// A convenience type for working with buffers and extracting [`zerocopy::Ref`]
|
||||
/// references.
|
||||
///
|
||||
/// `RefMaker` holds a buffer and a target type parameter `T`. Using `RefMaker`,
|
||||
/// you can validate that the provided buffer is large enough for `T` and then
|
||||
/// parse out a strongly-typed reference (`Ref`) to that data. It also provides
|
||||
/// methods for extracting prefixes and suffixes from the buffer.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use zerocopy::{AsBytes, FromBytes, FromZeroes, Ref};///
|
||||
/// # use rosenpass_util::zerocopy::RefMaker;
|
||||
///
|
||||
/// #[derive(FromBytes, FromZeroes, AsBytes)]
|
||||
/// #[repr(C)]
|
||||
/// struct Header {
|
||||
/// field1: u32,
|
||||
/// field2: u16,
|
||||
/// field3: u16,
|
||||
/// }
|
||||
/// #[repr(align(4))]
|
||||
/// struct AlignedBuf([u8; 8]);
|
||||
/// let bytes = AlignedBuf([0xAA, 0xBB, 0xCC, 0xDD,
|
||||
/// 0x00, 0x10, 0x20, 0x30]);
|
||||
/// let rm = RefMaker::<&[u8], Header>::new(&bytes.0);
|
||||
/// let header_ref: Ref<&[u8], Header> = rm.parse().unwrap();
|
||||
/// assert_eq!(header_ref.field1, 0xDDCCBBAA);
|
||||
/// assert_eq!(header_ref.field2, 0x1000);
|
||||
/// assert_eq!(header_ref.field3, 0x3020);
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct RefMaker<B: Sized, T> {
|
||||
buf: B,
|
||||
@@ -13,50 +46,161 @@ pub struct RefMaker<B: Sized, T> {
|
||||
}
|
||||
|
||||
impl<B, T> RefMaker<B, T> {
|
||||
/// Creates a new `RefMaker` with the given buffer.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rosenpass_util::zerocopy::RefMaker;
|
||||
/// let buffer = [0u8; 10];
|
||||
/// let rm: RefMaker<_, u32> = RefMaker::new(buffer);
|
||||
/// ```
|
||||
pub fn new(buf: B) -> Self {
|
||||
let _phantom_t = PhantomData;
|
||||
Self { buf, _phantom_t }
|
||||
}
|
||||
|
||||
/// Returns the size in bytes required by the target type `T`.
|
||||
/// This is currently defined as [`std::mem::size_of::<T>`] of `T`.
|
||||
pub const fn target_size() -> usize {
|
||||
std::mem::size_of::<T>()
|
||||
}
|
||||
|
||||
/// Consumes this `RefMaker` and returns the inner buffer.
|
||||
pub fn into_buf(self) -> B {
|
||||
self.buf
|
||||
}
|
||||
|
||||
/// Returns a reference to the inner buffer.
|
||||
pub fn buf(&self) -> &B {
|
||||
&self.buf
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the inner buffer.
|
||||
pub fn buf_mut(&mut self) -> &mut B {
|
||||
&mut self.buf
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: ByteSlice, T> RefMaker<B, T> {
|
||||
/// Parses the buffer into a [`zerocopy::Ref<B, T>`].
|
||||
///
|
||||
/// This will fail if the buffer is smaller than `size_of::<T>`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the buffer is undersized or if parsing fails.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use zerocopy::{AsBytes, FromBytes, FromZeroes, Ref};
|
||||
/// # use rosenpass_util::zerocopy::RefMaker;
|
||||
///
|
||||
/// #[derive(FromBytes, FromZeroes, AsBytes, Debug)]
|
||||
/// #[repr(C)]
|
||||
/// struct Data(u32);
|
||||
///
|
||||
/// let bytes: &[u8] = &[0x01, 0x00, 0x00, 0x00];
|
||||
/// let data_ref: Ref<&[u8], Data> = RefMaker::<_, Data>::new(bytes).parse().unwrap();
|
||||
/// assert_eq!(data_ref.0, 1);
|
||||
///
|
||||
/// // errors if buffer is undersized
|
||||
/// let bytes: &[u8] = &[0x01, 0x02, 0x03];
|
||||
/// let parse_error = RefMaker::<_, Data>::new(bytes).parse()
|
||||
/// .expect_err("Should error");
|
||||
/// assert_eq!(parse_error.to_string(),
|
||||
/// "Buffer is undersized at 3 bytes (need 4 bytes)!");
|
||||
///
|
||||
/// // errors if the byte buffer is misaligned
|
||||
/// let bytes = [1u8, 2, 3, 4, 5, 6, 7, 8];
|
||||
/// let parse_error = RefMaker::<_, Data>::new(&bytes[1..5]).parse()
|
||||
/// .expect_err("Should error");
|
||||
/// assert_eq!(parse_error.to_string(), "Parser error!");
|
||||
/// ```
|
||||
pub fn parse(self) -> anyhow::Result<Ref<B, T>> {
|
||||
self.ensure_fit()?;
|
||||
Ref::<B, T>::new(self.buf).context("Parser error!")
|
||||
}
|
||||
|
||||
/// Splits the internal buffer into a `RefMaker` containing a buffer with
|
||||
/// exactly `size_of::<T>()` bytes and the remaining tail of the previous
|
||||
/// internal buffer.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the buffer is undersized.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rosenpass_util::zerocopy::RefMaker;
|
||||
/// let bytes: &[u8] = &[1,2,3,4,5,6,7,8];
|
||||
/// let (prefix_rm, tail) = RefMaker::<_, u32>::new(bytes).from_prefix_with_tail().unwrap();
|
||||
/// assert_eq!(prefix_rm.bytes(), &[1,2,3,4]);
|
||||
/// assert_eq!(tail, &[5,6,7,8]);
|
||||
/// ```
|
||||
pub fn from_prefix_with_tail(self) -> anyhow::Result<(Self, B)> {
|
||||
self.ensure_fit()?;
|
||||
let (head, tail) = self.buf.split_at(Self::target_size());
|
||||
Ok((Self::new(head), tail))
|
||||
}
|
||||
|
||||
/// Splits the buffer into two `RefMaker`s, with the first containing the
|
||||
/// first `size_of::<T>()` bytes and the second containing the remaining
|
||||
/// tail buffer.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the buffer is undersized.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rosenpass_util::zerocopy::RefMaker;
|
||||
/// let bytes: &[u8] = &[1,2,3,4,5,6,7,8,9,10];
|
||||
/// let (prefix_rm, tail) = RefMaker::<_, u32>::new(bytes).split_prefix().unwrap();
|
||||
/// assert_eq!(prefix_rm.bytes(), &[1,2,3,4]);
|
||||
/// assert_eq!(tail.bytes(), &[5,6,7,8,9,10]);
|
||||
/// ```
|
||||
pub fn split_prefix(self) -> anyhow::Result<(Self, Self)> {
|
||||
self.ensure_fit()?;
|
||||
let (head, tail) = self.buf.split_at(Self::target_size());
|
||||
Ok((Self::new(head), Self::new(tail)))
|
||||
}
|
||||
|
||||
/// Returns a `RefMaker` containing only the first `size_of::<T>()` bytes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the buffer is undersized.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use rosenpass_util::zerocopy::RefMaker;
|
||||
/// let bytes: &[u8] = &[1,2,3,4,5,6,7,8,9,10];
|
||||
/// let prefix_rm = RefMaker::<_, u32>::new(bytes).from_prefix().unwrap();
|
||||
/// assert_eq!(prefix_rm.bytes(), &[1,2,3,4]);
|
||||
/// ```
|
||||
pub fn from_prefix(self) -> anyhow::Result<Self> {
|
||||
Ok(Self::from_prefix_with_tail(self)?.0)
|
||||
}
|
||||
|
||||
/// Splits the buffer into a `RefMaker` containing the last `size_of::<T>()`
|
||||
/// bytes as [RefMaker] and the preceding bytes as a buffer.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the buffer is undersized.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use rosenpass_util::zerocopy::RefMaker;
|
||||
/// let bytes: &[u8] = &[1,2,3,4,5,6,7,8,9,10];
|
||||
/// let (suffix_rm, head) = RefMaker::<_, u32>::new(bytes).from_suffix_with_head().unwrap();
|
||||
/// assert_eq!(suffix_rm.bytes(), &[7,8,9,10]);
|
||||
/// assert_eq!(head, &[1,2,3,4,5,6]);
|
||||
/// ```
|
||||
pub fn from_suffix_with_head(self) -> anyhow::Result<(Self, B)> {
|
||||
self.ensure_fit()?;
|
||||
let point = self.bytes().len() - Self::target_size();
|
||||
@@ -64,6 +208,22 @@ impl<B: ByteSlice, T> RefMaker<B, T> {
|
||||
Ok((Self::new(tail), head))
|
||||
}
|
||||
|
||||
/// Splits the buffer into two `RefMaker`s, with the second containing the
|
||||
/// last `size_of::<T>()` bytes, and the first containing the remaining
|
||||
/// preceding bytes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the buffer is undersized.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use rosenpass_util::zerocopy::RefMaker;
|
||||
/// let bytes: &[u8] = &[1,2,3,4,5,6,7,8,9,10];
|
||||
/// let (head, tail) = RefMaker::<_, u32>::new(bytes).split_suffix().unwrap();
|
||||
/// assert_eq!(head.bytes(), &[1,2,3,4,5,6]);
|
||||
/// assert_eq!(tail.bytes(), &[7,8,9,10]);
|
||||
/// ```
|
||||
pub fn split_suffix(self) -> anyhow::Result<(Self, Self)> {
|
||||
self.ensure_fit()?;
|
||||
let point = self.bytes().len() - Self::target_size();
|
||||
@@ -71,14 +231,46 @@ impl<B: ByteSlice, T> RefMaker<B, T> {
|
||||
Ok((Self::new(head), Self::new(tail)))
|
||||
}
|
||||
|
||||
/// Returns a `RefMaker` containing only the last `target_size()` bytes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the buffer is undersized.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use rosenpass_util::zerocopy::RefMaker;
|
||||
/// let bytes: &[u8] = &[1,2,3,4,5,6,7,8,9,10];
|
||||
/// let suffix_rm = RefMaker::<_, u32>::new(bytes).from_suffix().unwrap();
|
||||
/// assert_eq!(suffix_rm.bytes(), &[7,8,9,10]);
|
||||
/// ```
|
||||
pub fn from_suffix(self) -> anyhow::Result<Self> {
|
||||
Ok(Self::from_suffix_with_head(self)?.0)
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying bytes.
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
self.buf().deref()
|
||||
}
|
||||
|
||||
/// Ensures that the buffer is large enough to hold a `T`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the buffer is undersized.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rosenpass_util::zerocopy::RefMaker;
|
||||
/// let bytes: &[u8] = &[1,2,3,4,5,6,7,8,9,10];
|
||||
/// let rm = RefMaker::<_, u32>::new(bytes);
|
||||
/// rm.ensure_fit().unwrap();
|
||||
///
|
||||
/// let bytes: &[u8] = &[1,2,3];
|
||||
/// let rm = RefMaker::<_, u32>::new(bytes);
|
||||
/// assert!(rm.ensure_fit().is_err());
|
||||
/// ```
|
||||
pub fn ensure_fit(&self) -> anyhow::Result<()> {
|
||||
let have = self.bytes().len();
|
||||
let need = Self::target_size();
|
||||
@@ -91,10 +283,30 @@ impl<B: ByteSlice, T> RefMaker<B, T> {
|
||||
}
|
||||
|
||||
impl<B: ByteSliceMut, T> RefMaker<B, T> {
|
||||
/// Creates a zeroized reference of type `T` from the buffer.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the buffer is undersized.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use zerocopy::{AsBytes, FromBytes, FromZeroes, Ref}; ///
|
||||
/// # use rosenpass_util::zerocopy::RefMaker;
|
||||
/// #[derive(FromBytes, FromZeroes, AsBytes)]
|
||||
/// #[repr(C)]
|
||||
/// struct Data([u8; 4]);
|
||||
///
|
||||
/// let mut bytes = [0xFF; 4];
|
||||
/// let data_ref: Ref<&mut [u8], Data> = RefMaker::<_, Data>::new(&mut bytes[..]).make_zeroized().unwrap();
|
||||
/// assert_eq!(data_ref.0, [0,0,0,0]);
|
||||
/// ```
|
||||
pub fn make_zeroized(self) -> anyhow::Result<Ref<B, T>> {
|
||||
self.zeroized().parse()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the underlying bytes.
|
||||
pub fn bytes_mut(&mut self) -> &mut [u8] {
|
||||
self.buf_mut().deref_mut()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,64 @@
|
||||
//! Extension traits for converting `Ref<B, T>` into references backed by
|
||||
//! standard slices.
|
||||
|
||||
use zerocopy::{ByteSlice, ByteSliceMut, Ref};
|
||||
|
||||
/// A trait for converting a `Ref<B, T>` into a `Ref<&[u8], T>`.
|
||||
///
|
||||
/// This can be useful when you need a reference that is tied to a slice rather
|
||||
/// than the original buffer type `B`.
|
||||
///
|
||||
/// Note: This trait is implemented to [`Ref`] of byte slices (`&[u8]`).
|
||||
pub trait ZerocopyEmancipateExt<B, T> {
|
||||
/// Converts this reference into a reference backed by a plain byte slice.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use std::ops::Deref;
|
||||
/// # use zerocopy::{AsBytes, ByteSlice, FromBytes, FromZeroes, Ref};
|
||||
/// # use rosenpass_util::zerocopy::ZerocopyEmancipateExt;
|
||||
/// #[derive(FromBytes, FromZeroes, AsBytes)]
|
||||
/// #[repr(C)]
|
||||
/// struct Data(u32);
|
||||
/// #[repr(align(4))]
|
||||
/// struct AlignedBuf([u8; 4]);
|
||||
/// let bytes = AlignedBuf([0xAA, 0xBB, 0xCC, 0xDD]);
|
||||
/// let r = Ref::<&[u8], Data>::new(&bytes.0).unwrap();
|
||||
/// let emancipated: Ref<&[u8], Data> = r.emancipate(); // same data, but guaranteed &[u8] backing
|
||||
/// assert_eq!(emancipated.0, 0xDDCCBBAA);
|
||||
/// ```
|
||||
fn emancipate(&self) -> Ref<&[u8], T>;
|
||||
}
|
||||
|
||||
/// A trait for converting a `Ref<B, T>` into a mutable `Ref<&mut [u8], T>`.
|
||||
///
|
||||
/// Similar to [`ZerocopyEmancipateExt`], but for mutable references.
|
||||
///
|
||||
/// Note: this trait is implemented to [`Ref`] of mutable byte
|
||||
/// slices (`&mut [u8]`).
|
||||
pub trait ZerocopyEmancipateMutExt<B, T> {
|
||||
/// Converts this reference into a mutable reference backed by a plain
|
||||
/// mutable byte slice.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use zerocopy::{AsBytes, FromBytes, FromZeroes, Ref};
|
||||
/// # use rosenpass_util::zerocopy::{ZerocopyEmancipateMutExt};
|
||||
/// #[derive(FromBytes, FromZeroes, AsBytes)]
|
||||
/// #[repr(C)]
|
||||
/// struct Data(u32);
|
||||
/// #[repr(align(4))]
|
||||
/// struct AlignedBuf([u8; 4]);
|
||||
/// let mut bytes = AlignedBuf([0xAA, 0xBB, 0xCC, 0xDD]);
|
||||
/// let mut r = Ref::<&mut [u8], Data>::new(&mut bytes.0).unwrap();
|
||||
/// let mut emancipated: Ref<&mut [u8], Data> = r.emancipate_mut(); // same data, but guaranteed &[u8] backing
|
||||
/// assert_eq!(emancipated.0, 0xDDCCBBAA);
|
||||
/// emancipated.0 = 0x33221100;
|
||||
/// drop(emancipated);
|
||||
/// assert_eq!(bytes.0, [0x00, 0x11, 0x22, 0x33]);
|
||||
/// ```
|
||||
fn emancipate_mut(&mut self) -> Ref<&mut [u8], T>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,113 @@
|
||||
//! Extension traits for parsing slices into [`zerocopy::Ref`] values using the
|
||||
//! [`RefMaker`] abstraction.
|
||||
|
||||
use zerocopy::{ByteSlice, ByteSliceMut, Ref};
|
||||
|
||||
use super::RefMaker;
|
||||
|
||||
/// Extension trait for performing zero-copy parsing operations on byte slices.
|
||||
///
|
||||
/// This trait adds methods for creating [`Ref`] references from
|
||||
/// slices by using the [`RefMaker`] type internally.
|
||||
pub trait ZerocopySliceExt: Sized + ByteSlice {
|
||||
/// Creates a new `RefMaker` for the given slice.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use zerocopy::{AsBytes, FromBytes, FromZeroes};
|
||||
/// # use rosenpass_util::zerocopy::{RefMaker, ZerocopySliceExt};
|
||||
///
|
||||
/// #[derive(FromBytes, FromZeroes, AsBytes)]
|
||||
/// #[repr(C)]
|
||||
/// struct Data(u32);
|
||||
///
|
||||
/// let rm: RefMaker<&[u8], Data> = [3,0,0,0].zk_ref_maker();
|
||||
/// assert_eq!(rm.bytes(), &[3,0,0,0]);
|
||||
/// assert_eq!(rm.parse().unwrap().0, 3);
|
||||
/// ```
|
||||
fn zk_ref_maker<T>(self) -> RefMaker<Self, T> {
|
||||
RefMaker::<Self, T>::new(self)
|
||||
}
|
||||
|
||||
/// Parses the given slice into a zero-copy reference of the given type `T`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the slice is too small.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use zerocopy::{AsBytes, FromBytes, FromZeroes};
|
||||
/// # use rosenpass_util::zerocopy::ZerocopySliceExt;
|
||||
///
|
||||
/// #[derive(FromBytes, FromZeroes, AsBytes)]
|
||||
/// #[repr(C)]
|
||||
/// struct Data(u16, u16);
|
||||
/// #[repr(align(4))]
|
||||
/// struct AlignedBuf([u8; 4]);
|
||||
/// let bytes = AlignedBuf([0x01,0x02,0x03,0x04]);
|
||||
/// let data_ref = bytes.0.zk_parse::<Data>().unwrap();
|
||||
/// assert_eq!(data_ref.0, 0x0201);
|
||||
/// assert_eq!(data_ref.1, 0x0403);
|
||||
/// ```
|
||||
fn zk_parse<T>(self) -> anyhow::Result<Ref<Self, T>> {
|
||||
self.zk_ref_maker().parse()
|
||||
}
|
||||
|
||||
/// Parses a prefix of the slice into a zero-copy reference.
|
||||
///
|
||||
/// Uses only the first [`std::mem::size_of::<T>()`] bytes of `T`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the slice is too small.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use zerocopy::{AsBytes, FromBytes, FromZeroes};
|
||||
/// # use rosenpass_util::zerocopy::ZerocopySliceExt;
|
||||
/// #[derive(FromBytes, FromZeroes, AsBytes)]
|
||||
/// #[repr(C)]
|
||||
/// struct Header(u32);
|
||||
/// #[repr(align(4))]
|
||||
/// struct AlignedBuf([u8; 8]);
|
||||
/// let bytes = AlignedBuf([0xAA, 0xBB, 0xCC, 0xDD,
|
||||
/// 0x00, 0x10, 0x20, 0x30]);
|
||||
///
|
||||
/// let header_ref = bytes.0.zk_parse_prefix::<Header>().unwrap();
|
||||
/// assert_eq!(header_ref.0, 0xDDCCBBAA);
|
||||
/// ```
|
||||
fn zk_parse_prefix<T>(self) -> anyhow::Result<Ref<Self, T>> {
|
||||
self.zk_ref_maker().from_prefix()?.parse()
|
||||
}
|
||||
|
||||
/// Parses a suffix of the slice into a zero-copy reference.
|
||||
///
|
||||
/// Uses only the last [`std::mem::size_of::<T>()`] bytes of `T`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the slice is too small.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use zerocopy::{AsBytes, FromBytes, FromZeroes};
|
||||
/// # use rosenpass_util::zerocopy::ZerocopySliceExt;
|
||||
/// #[derive(FromBytes, FromZeroes, AsBytes)]
|
||||
/// #[repr(C)]
|
||||
/// struct Header(u32);
|
||||
/// #[repr(align(4))]
|
||||
/// struct AlignedBuf([u8; 8]);
|
||||
/// let bytes = AlignedBuf([0xAA, 0xBB, 0xCC, 0xDD,
|
||||
/// 0x00, 0x10, 0x20, 0x30]);
|
||||
///
|
||||
/// let header_ref = bytes.0.zk_parse_suffix::<Header>().unwrap();
|
||||
/// assert_eq!(header_ref.0, 0x30201000);
|
||||
/// ```
|
||||
fn zk_parse_suffix<T>(self) -> anyhow::Result<Ref<Self, T>> {
|
||||
self.zk_ref_maker().from_suffix()?.parse()
|
||||
}
|
||||
@@ -22,15 +115,90 @@ pub trait ZerocopySliceExt: Sized + ByteSlice {
|
||||
|
||||
impl<B: ByteSlice> ZerocopySliceExt for B {}
|
||||
|
||||
/// Extension trait for zero-copy parsing of mutable slices with zeroization
|
||||
/// capabilities.
|
||||
///
|
||||
/// Provides convenience methods to create zero-initialized references.
|
||||
pub trait ZerocopyMutSliceExt: ZerocopySliceExt + Sized + ByteSliceMut {
|
||||
/// Creates a new zeroized reference from the entire slice.
|
||||
///
|
||||
/// This zeroizes the slice first, then provides a `Ref`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the slice is too small.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use zerocopy::{AsBytes, FromBytes, FromZeroes};
|
||||
/// # use rosenpass_util::zerocopy::ZerocopyMutSliceExt;
|
||||
/// #[derive(FromBytes, FromZeroes, AsBytes)]
|
||||
/// #[repr(C)]
|
||||
/// struct Data([u8; 4]);
|
||||
/// #[repr(align(4))]
|
||||
/// struct AlignedBuf([u8; 4]);
|
||||
/// let mut bytes = AlignedBuf([0xFF; 4]);
|
||||
/// let data_ref = bytes.0.zk_zeroized::<Data>().unwrap();
|
||||
/// assert_eq!(data_ref.0, [0,0,0,0]);
|
||||
/// assert_eq!(bytes.0, [0, 0, 0, 0]);
|
||||
/// ```
|
||||
fn zk_zeroized<T>(self) -> anyhow::Result<Ref<Self, T>> {
|
||||
self.zk_ref_maker().make_zeroized()
|
||||
}
|
||||
|
||||
/// Creates a new zeroized reference from the prefix of the slice.
|
||||
///
|
||||
/// Zeroizes the first `target_size()` bytes of the slice, then returns a
|
||||
/// `Ref`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the slice is too small.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use zerocopy::{AsBytes, FromBytes, FromZeroes};
|
||||
/// # use rosenpass_util::zerocopy::ZerocopyMutSliceExt;
|
||||
/// #[derive(FromBytes, FromZeroes, AsBytes)]
|
||||
/// #[repr(C)]
|
||||
/// struct Data([u8; 4]);
|
||||
/// #[repr(align(4))]
|
||||
/// struct AlignedBuf([u8; 6]);
|
||||
/// let mut bytes = AlignedBuf([0xFF; 6]);
|
||||
/// let data_ref = bytes.0.zk_zeroized_from_prefix::<Data>().unwrap();
|
||||
/// assert_eq!(data_ref.0, [0,0,0,0]);
|
||||
/// assert_eq!(bytes.0, [0, 0, 0, 0, 0xFF, 0xFF]);
|
||||
/// ```
|
||||
fn zk_zeroized_from_prefix<T>(self) -> anyhow::Result<Ref<Self, T>> {
|
||||
self.zk_ref_maker().from_prefix()?.make_zeroized()
|
||||
}
|
||||
|
||||
/// Creates a new zeroized reference from the suffix of the slice.
|
||||
///
|
||||
/// Zeroizes the last `target_size()` bytes of the slice, then returns a
|
||||
/// `Ref`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the slice is too small.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use zerocopy::{AsBytes, FromBytes, FromZeroes};
|
||||
/// # use rosenpass_util::zerocopy::ZerocopyMutSliceExt;
|
||||
/// #[derive(FromBytes, FromZeroes, AsBytes)]
|
||||
/// #[repr(C)]
|
||||
/// struct Data([u8; 4]);
|
||||
/// #[repr(align(4))]
|
||||
/// struct AlignedBuf([u8; 6]);
|
||||
/// let mut bytes = AlignedBuf([0xFF; 6]);
|
||||
/// let data_ref = bytes.0.zk_zeroized_from_suffix::<Data>().unwrap();
|
||||
/// assert_eq!(data_ref.0, [0,0,0,0]);
|
||||
/// assert_eq!(bytes.0, [0xFF, 0xFF, 0, 0, 0, 0]);
|
||||
/// ```
|
||||
fn zk_zeroized_from_suffix<T>(self) -> anyhow::Result<Ref<Self, T>> {
|
||||
self.zk_ref_maker().from_suffix()?.make_zeroized()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// Extension trait providing a method for zeroizing a value and returning it
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use zeroize::Zeroize;
|
||||
/// use rosenpass_util::zeroize::ZeroizedExt;
|
||||
///
|
||||
/// let mut value = String::from("hello");
|
||||
/// value.zeroize();
|
||||
/// assert_eq!(value, "");
|
||||
///
|
||||
/// let value = String::from("hello").zeroized();
|
||||
/// assert_eq!(value, "");
|
||||
/// ```
|
||||
pub trait ZeroizedExt: Zeroize + Sized {
|
||||
/// Zeroizes the value in place and returns self
|
||||
fn zeroized(mut self) -> Self {
|
||||
self.zeroize();
|
||||
self
|
||||
|
||||
@@ -19,7 +19,7 @@ wireguard-uapi = { workspace = true }
|
||||
|
||||
# Socket handler only
|
||||
rosenpass-to = { workspace = true }
|
||||
tokio = { version = "1.40.0", features = ["sync", "full", "mio"] }
|
||||
tokio = { version = "1.42.0", features = ["sync", "full", "mio"] }
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
@@ -28,7 +28,7 @@ derive_builder = { workspace = true }
|
||||
postcard = { workspace = true }
|
||||
# Problem in CI, unknown reasons: dependency (libc) specified without providing a local path, Git repository, version, or workspace dependency to use
|
||||
# Maybe something about the combination of features and optional crates?
|
||||
rustix = { version = "0.38.37", optional = true }
|
||||
rustix = { version = "0.38.42", optional = true }
|
||||
libc = { version = "0.2", optional = true }
|
||||
|
||||
# Mio broker client
|
||||
|
||||
@@ -5,8 +5,8 @@ use rosenpass_to::{ops::copy_slice_least_src, To};
|
||||
use rosenpass_util::io::{IoResultKindHintExt, TryIoResultKindHintExt};
|
||||
use rosenpass_util::length_prefix_encoding::decoder::LengthPrefixDecoder;
|
||||
use rosenpass_util::length_prefix_encoding::encoder::LengthPrefixEncoder;
|
||||
use rustix::fd::AsFd;
|
||||
use std::borrow::{Borrow, BorrowMut};
|
||||
use std::os::fd::AsFd;
|
||||
|
||||
use crate::api::client::{
|
||||
BrokerClient, BrokerClientIo, BrokerClientPollResponseError, BrokerClientSetPskError,
|
||||
|
||||
Reference in New Issue
Block a user