├── integration-testing ├── .gitignore ├── reticulum_config │ ├── .gitignore │ └── config ├── run-test-server.sh ├── setup-tests.sh └── server.py ├── reticulum-rs ├── .gitignore ├── src │ ├── constants.rs │ ├── random.rs │ ├── persistence │ │ ├── mod.rs │ │ ├── in_memory.rs │ │ └── destination.rs │ ├── lib.rs │ ├── identity.rs │ └── packet.rs ├── bin │ └── integration_tests.rs └── Cargo.toml ├── rp2040-reticulum ├── .gitignore ├── memory.x ├── .cargo │ └── config.toml ├── build.rs ├── Cargo.toml └── src │ └── main.rs ├── fernet-rs ├── .gitignore ├── tests │ ├── verify.json │ ├── generate.json │ └── invalid.json ├── .github │ ├── dependabot.yml │ └── workflows │ │ └── ci.yml ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── README.md ├── LICENSE └── src │ └── lib.rs ├── .gitmodules ├── LICENSE.MIT ├── README.md └── LICENSE.Apache /integration-testing/.gitignore: -------------------------------------------------------------------------------- 1 | test-venv 2 | -------------------------------------------------------------------------------- /reticulum-rs/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /integration-testing/reticulum_config/.gitignore: -------------------------------------------------------------------------------- 1 | storage/ -------------------------------------------------------------------------------- /rp2040-reticulum/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /reticulum-rs/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const MTU: u32 = 500; 2 | -------------------------------------------------------------------------------- /fernet-rs/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target 3 | **/*.rs.bk 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /integration-testing/run-test-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 -m venv test-venv 4 | source test-venv/bin/activate 5 | 6 | python server.py -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "integration-testing/Reticulum"] 2 | path = integration-testing/Reticulum 3 | url = https://github.com/markqvist/Reticulum.git 4 | -------------------------------------------------------------------------------- /rp2040-reticulum/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 3 | FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 4 | RAM : ORIGIN = 0x20000000, LENGTH = 256K 5 | } -------------------------------------------------------------------------------- /rp2040-reticulum/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 2 | runner = "probe-rs run --chip RP2040" 3 | 4 | [build] 5 | target = "thumbv6m-none-eabi" 6 | 7 | [env] 8 | DEFMT_LOG = "debug" -------------------------------------------------------------------------------- /integration-testing/setup-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is used to partially automate setup for the integration tests. Run it from this directory. 4 | 5 | rm -r test-venv 6 | python3 -m venv test-venv 7 | source test-venv/bin/activate 8 | pip install -e ./Reticulum -------------------------------------------------------------------------------- /fernet-rs/tests/verify.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "token": "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==", 4 | "now": "1985-10-26T01:20:01-07:00", 5 | "ttl_sec": 60, 6 | "src": "hello", 7 | "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /fernet-rs/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | allow: 8 | # Also update indirect dependencies 9 | - dependency-type: all 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /fernet-rs/tests/generate.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "token": "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==", 4 | "now": "1985-10-26T01:20:00-07:00", 5 | "iv": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], 6 | "src": "hello", 7 | "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /fernet-rs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | -------------------------------------------------------------------------------- /rp2040-reticulum/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::File; 3 | use std::io::Write; 4 | use std::path::PathBuf; 5 | 6 | fn main() { 7 | // Put `memory.x` in our output directory and ensure it's 8 | // on the linker search path. 9 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 10 | File::create(out.join("memory.x")) 11 | .unwrap() 12 | .write_all(include_bytes!("memory.x")) 13 | .unwrap(); 14 | println!("cargo:rustc-link-search={}", out.display()); 15 | 16 | // By default, Cargo will re-run a build script whenever 17 | // any file in the project changes. By specifying `memory.x` 18 | // here, we ensure the build script is only re-run when 19 | // `memory.x` is changed. 20 | println!("cargo:rerun-if-changed=memory.x"); 21 | 22 | println!("cargo:rustc-link-arg-bins=--nmagic"); 23 | println!("cargo:rustc-link-arg-bins=-Tlink.x"); 24 | println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); 25 | println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); 26 | } 27 | -------------------------------------------------------------------------------- /reticulum-rs/src/random.rs: -------------------------------------------------------------------------------- 1 | use defmt::trace; 2 | use rand::{RngCore, SeedableRng}; 3 | use rand_chacha::ChaCha20Rng; 4 | 5 | // lazy_static! { 6 | // pub(super) static ref RNG: Mutex> = 7 | // Mutex::new(None); 8 | // } 9 | 10 | #[cfg(feature = "embassy")] 11 | pub(super) static RNG: embassy_sync::mutex::Mutex< 12 | embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, 13 | Option, 14 | > = embassy_sync::mutex::Mutex::new(None); 15 | #[cfg(feature = "tokio")] 16 | lazy_static::lazy_static! { 17 | pub(crate) static ref RNG: tokio::sync::Mutex> = tokio::sync::Mutex::new(None); 18 | } 19 | 20 | pub(crate) async fn random_bytes(bytes: &mut [u8]) { 21 | let mut rng = RNG.lock().await; 22 | let rng = rng.as_mut().unwrap(); 23 | rng.fill_bytes(bytes); 24 | trace!("random_bytes {:?}", bytes); 25 | } 26 | 27 | pub async fn init_from_seed(seed: [u8; 32]) { 28 | let mut rng = RNG.lock().await; 29 | *rng = Some(ChaCha20Rng::from_seed(seed)); 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | Copyright 2023 Ellen Poe 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the “Software”), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reticulum-rs 2 | 3 | Partial rewrite of Reticulum in Rust for `no_std` targets. Not ready for use. Work has stopped on this project in favor of the [Liminality](https://github.com/ellenhp/liminality) reference implementation, which provides trustworthy and comprehensible security properties by using the noise protocol framework, and strong (but not very comprehensible) privacy properties. This project is probably a good starting point if someone else wants to continue work in this space, but it's not polished and probably has bugs. 4 | 5 | ## Roadmap 6 | 7 | Anything with a checkmark here is both implemented and tested. 8 | 9 | - [x] Cryptographic primitives 10 | - [x] Wire format (de)serialization 11 | - [x] Announce packet generation 12 | - [x] Event loop boilerplate 13 | - [ ] Path responses 14 | - [ ] Maintain a routing table 15 | - [ ] Link establishment 16 | - [ ] Test a basic channel end to end 17 | - [ ] Overhaul virtual network system to test disrupted networks. 18 | - [ ] Forward messages if appropriate 19 | - [ ] Test a DTN channel end to end 20 | - [ ] Groups 21 | - [ ] Cleartext messages 22 | - [ ] Other stuff? There are a lot of message types and they probably all do something. 23 | - [ ] Integration tests 24 | 25 | ## Licensing 26 | 27 | Dual license under Apache 2.0 and MIT. 28 | -------------------------------------------------------------------------------- /fernet-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fernet" 3 | version = "0.2.1" 4 | authors = ["Alex Gaynor ", "Ben Bangert "] 5 | description = "An implementation of fernet in Rust." 6 | repository = "https://github.com/mozilla-services/fernet-rs/" 7 | homepage = "https://github.com/mozilla-services/fernet-rs/" 8 | license = "MPL-2.0" 9 | readme = "README.md" 10 | edition = "2018" 11 | 12 | 13 | [badges] 14 | travis-ci = { repository = "mozilla-services/fernet-rs" } 15 | 16 | [features] 17 | fernet_danger_timestamps = [] 18 | 19 | [package.metadata.docs.rs] 20 | features = ["fernet_danger_timestamps"] 21 | 22 | [dependencies] 23 | base64 = { version = "0.21", default-features = false, features = ["alloc"] } 24 | byteorder = { version = "1", default-features = false } 25 | getrandom = { version = "0.2", default-features = false, features = ["custom"] } 26 | zeroize = { version = "1.0", features = ["zeroize_derive"] } 27 | aes = { version = "0.8", default-features = false } 28 | cbc = { version = "0.1", features = ["alloc"] } 29 | hmac = { version = "0.12", default-features = false } 30 | sha2 = { version = "0.10", default-features = false } 31 | subtle = { version = "2.4", default-features = false } 32 | 33 | 34 | [dev-dependencies] 35 | time = { version = "0.3", features = ["parsing"] } 36 | serde = "1.0" 37 | serde_derive = "1.0" 38 | serde_json = "1.0" 39 | -------------------------------------------------------------------------------- /reticulum-rs/bin/integration_tests.rs: -------------------------------------------------------------------------------- 1 | // use core::{sync::Arc, time::Duration}; 2 | 3 | // use reticulum_rs::{ 4 | // identity::Identity, 5 | // interface::{udp::UdpInterface, Interface}, 6 | // persistence::{ 7 | // in_memory::{InMemoryDestinationStore, InMemoryMessageStore}, 8 | // DestinationStore, 9 | // }, 10 | // Reticulum, 11 | // }; 12 | // use smol::{block_on, lock::Mutex, Timer}; 13 | 14 | // fn main() { 15 | // env_logger::init(); 16 | // block_on(async { 17 | // let interfaces: Vec> = vec![Arc::new( 18 | // UdpInterface::new( 19 | // "127.0.0.1:44243".parse().unwrap(), 20 | // "127.0.0.1:44242".parse().unwrap(), 21 | // ) 22 | // .await, 23 | // )]; 24 | // let destination_store = Arc::new(Mutex::new(Box::new(InMemoryDestinationStore::new()))); 25 | // let message_store = Arc::new(Mutex::new(Box::new(InMemoryMessageStore::new()))); 26 | 27 | // let node = Reticulum::new(interfaces, destination_store.clone(), message_store).unwrap(); 28 | // node.register_destination_prefix("reticulum-rs".to_string(), vec![]) 29 | // .await 30 | // .unwrap(); 31 | 32 | // { 33 | // let mut destination_store = destination_store.lock().await; 34 | // destination_store 35 | // .as_mut() 36 | // .builder("reticulum-rs") 37 | // .build_single(&Identity::new_local(), destination_store.as_mut()) 38 | // .await 39 | // .unwrap(); 40 | // } 41 | // loop { 42 | // let peer_destinations = node.get_peer_destinations().await.unwrap(); 43 | // println!("Peer destinations: {:?}", peer_destinations); 44 | // node.force_announce_all_local().await.unwrap(); 45 | // Timer::after(Duration::from_secs(1)).await; 46 | // } 47 | // }); 48 | // } 49 | fn main() {} 50 | -------------------------------------------------------------------------------- /fernet-rs/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [pull_request, push] 2 | 3 | name: CI 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | RUST: 11 | - stable 12 | - beta 13 | - nightly 14 | features: 15 | - "--features default" 16 | - "--no-default-features --features rustcrypto" 17 | steps: 18 | - uses: actions/checkout@v2.3.4 19 | - uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: ${{ matrix.RUST }} 23 | override: true 24 | components: rustfmt, clippy 25 | 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | command: build 29 | args: ${{ matrix.features }} 30 | 31 | - uses: actions-rs/cargo@v1 32 | with: 33 | command: test 34 | 35 | - uses: actions-rs/cargo@v1 36 | with: 37 | command: test 38 | args: ${{ matrix.features }} 39 | 40 | - uses: actions-rs/cargo@v1 41 | with: 42 | command: fmt 43 | args: --all -- --check 44 | 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: clippy 48 | args: -- -D warnings 49 | 50 | - uses: actions-rs/audit-check@v1 51 | with: 52 | token: ${{ secrets.GITHUB_TOKEN }} 53 | -------------------------------------------------------------------------------- /fernet-rs/README.md: -------------------------------------------------------------------------------- 1 | fernet-rs 2 | ========= 3 | 4 | [![dependency status](https://deps.rs/repo/github/mozilla-services/fernet-rs/status.svg)](https://deps.rs/repo/github/mozilla-services/fernet-rs) 5 | 6 | An implementation of [fernet](https://github.com/fernet/spec) in Rust. 7 | 8 | What is Fernet? 9 | --------------- 10 | 11 | Fernet is a small library to help you encrypt parcels of data with optional expiry times. It's 12 | great for tokens or exchanging small strings or blobs of data. Fernet is designed to be easy 13 | to use, combining cryptographic primitives in a way that is hard to get wrong, prevents tampering 14 | and gives you confidence that the token is legitimate. You should consider this if you need: 15 | 16 | * Time limited authentication tokens in URLs or authorisation headers 17 | * To send small blobs of encrypted data between two points with a static key 18 | * Simple encryption of secrets to store to disk that can be read later 19 | * Many more ... 20 | 21 | Great! How do I start? 22 | ---------------------- 23 | 24 | Add fernet to your Cargo.toml: 25 | 26 | [dependencies] 27 | fernet = "0.1" 28 | 29 | And then have a look at our [API documentation] online, or run "cargo doc --open" in your 30 | project. 31 | 32 | [API documentation online]: https://docs.rs/fernet 33 | 34 | Testing Token Expiry 35 | -------------------- 36 | 37 | By default fernet wraps operations in an attempt to be safe - you should never be able to 38 | "hold it incorrectly". But we understand that sometimes you need to be able to do some 39 | more complicated operations. 40 | 41 | The major example of this is having your application test how it handles tokens that 42 | have expired past their ttl. 43 | 44 | To support this, we allow you to pass in timestamps to the `encrypt_at_time` and 45 | `decrypt_at_time` functions, but these are behind a feature gate. To activate these 46 | api's you need to add the following to Cargo.toml 47 | 48 | [dependencies] 49 | fernet = { version = "0.1", features = ["fernet_danger_timestamps"] } 50 | 51 | 52 | -------------------------------------------------------------------------------- /reticulum-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reticulum-rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "integration_tests" 8 | path = "bin/integration_tests.rs" 9 | 10 | [dependencies] 11 | async-trait = "0.1.73" 12 | base64 = { version = "0.21", default-features = false, features = ["alloc"] } 13 | ed25519-dalek = { version = "2.0", default-features = false, features = ["rand_core"] } 14 | fernet = { path = "../fernet-rs" } 15 | hex = { version = "0.4", default-features = false, features = ["alloc"] } 16 | hkdf = "0.12.3" 17 | defmt = "0.3" 18 | getrandom = { version = "0.2", default-features = false, features = ["custom"] } 19 | packed_struct = { version = "0.10.1", default-features = false } 20 | rand = { version = "0.8.5", default-features = false, features = ["rand_chacha"] } 21 | sha2 = { version = "0.10", default-features = false } 22 | x25519-dalek = { version = "2.0", features = ["reusable_secrets", "static_secrets"] } 23 | embassy-executor = { git = "https://github.com/embassy-rs/embassy", optional = true, features = ["nightly", "arch-cortex-m", "executor-thread", "executor-interrupt", "defmt", "integrated-timers"] } 24 | embassy-sync = { git = "https://github.com/embassy-rs/embassy", optional = true, features = ["defmt"] } 25 | embassy-futures = { git = "https://github.com/embassy-rs/embassy", optional = true } 26 | embassy-time = { git = "https://github.com/embassy-rs/embassy", optional = true, features = ["nightly", "unstable-traits", "defmt", "defmt-timestamp-uptime"] } 27 | tokio = { version = "1", optional = true, features = ["full"] } 28 | rand_chacha = { version = "0.3.1", default-features = false } 29 | lazy_static = { version = "1.4.0", optional = true } 30 | 31 | [dev-dependencies] 32 | crossbeam = "0.8.2" 33 | env_logger = "0.10.0" 34 | smol = "^1.2" 35 | tokio = { version = "1", features = ["full"] } 36 | tokio-test = "0.4.3" 37 | rand = { version = "0.8.5" } 38 | 39 | [features] 40 | default = ["interfaces", "stores", "tokio", "std"] 41 | embassy = ["dep:embassy-sync", "dep:embassy-futures", "dep:embassy-time", "dep:embassy-executor"] 42 | tokio = ["dep:tokio", "dep:lazy_static"] 43 | interfaces = [] 44 | stores = [] 45 | std = [] 46 | -------------------------------------------------------------------------------- /fernet-rs/tests/invalid.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "desc": "incorrect mac", 4 | "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykQUFBQUFBQUFBQQ==", 5 | "now": "1985-10-26T01:20:01-07:00", 6 | "ttl_sec": 60, 7 | "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" 8 | }, 9 | { 10 | "desc": "too short", 11 | "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPA==", 12 | "now": "1985-10-26T01:20:01-07:00", 13 | "ttl_sec": 60, 14 | "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" 15 | }, 16 | { 17 | "desc": "invalid base64", 18 | "token": "%%%%%%%%%%%%%AECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykRtfsH-p1YsUD2Q==", 19 | "now": "1985-10-26T01:20:01-07:00", 20 | "ttl_sec": 60, 21 | "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" 22 | }, 23 | { 24 | "desc": "payload size not multiple of block size", 25 | "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPOm73QeoCk9uGib28Xe5vz6oxq5nmxbx_v7mrfyudzUm", 26 | "now": "1985-10-26T01:20:01-07:00", 27 | "ttl_sec": 60, 28 | "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" 29 | }, 30 | { 31 | "desc": "payload padding error", 32 | "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0ODz4LEpdELGQAad7aNEHbf-JkLPIpuiYRLQ3RtXatOYREu2FWke6CnJNYIbkuKNqOhw==", 33 | "now": "1985-10-26T01:20:01-07:00", 34 | "ttl_sec": 60, 35 | "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" 36 | }, 37 | { 38 | "desc": "far-future TS (unacceptable clock skew)", 39 | "token": "gAAAAAAdwStRAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAnja1xKYyhd-Y6mSkTOyTGJmw2Xc2a6kBd-iX9b_qXQcw==", 40 | "now": "1985-10-26T01:20:01-07:00", 41 | "ttl_sec": 60, 42 | "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" 43 | }, 44 | { 45 | "desc": "expired TTL", 46 | "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykRtfsH-p1YsUD2Q==", 47 | "now": "1985-10-26T01:21:31-07:00", 48 | "ttl_sec": 60, 49 | "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" 50 | }, 51 | { 52 | "desc": "incorrect IV (causes padding error)", 53 | "token": "gAAAAAAdwJ6xBQECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAkLhFLHpGtDBRLRTZeUfWgHSv49TF2AUEZ1TIvcZjK1zQ==", 54 | "now": "1985-10-26T01:20:01-07:00", 55 | "ttl_sec": 60, 56 | "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /rp2040-reticulum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rp2040-reticulum" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | 9 | [dependencies] 10 | reticulum-rs = { path = "../reticulum-rs", default-features = false, features = ["embassy"] } 11 | 12 | embassy-embedded-hal = { git = "https://github.com/embassy-rs/embassy", features = ["defmt"] } 13 | embassy-sync = { git = "https://github.com/embassy-rs/embassy", features = ["defmt"] } 14 | embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = ["nightly", "arch-cortex-m", "executor-thread", "executor-interrupt", "defmt", "integrated-timers"] } 15 | embassy-time = { git = "https://github.com/embassy-rs/embassy", features = ["nightly", "unstable-traits", "defmt", "defmt-timestamp-uptime"] } 16 | embassy-rp = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "unstable-traits", "nightly", "unstable-pac", "time-driver", "critical-section-impl"] } 17 | embassy-usb = { git = "https://github.com/embassy-rs/embassy", features = ["defmt"] } 18 | embassy-futures = { git = "https://github.com/embassy-rs/embassy" } 19 | embassy-usb-logger = { git = "https://github.com/embassy-rs/embassy" } 20 | #embassy-lora = { git = "https://github.com/embassy-rs/embassy", features = ["time", "defmt"] } 21 | lora-phy = { version = "1" } 22 | 23 | defmt = "0.3" 24 | defmt-rtt = "0.4" 25 | fixed = "1.23.1" 26 | fixed-macro = "1.2" 27 | 28 | cortex-m = { version = "0.7.6", features = ["inline-asm"] } 29 | cortex-m-rt = "0.7.0" 30 | panic-probe = { version = "0.3", features = ["print-defmt"] } 31 | futures = { version = "0.3.17", default-features = false, features = ["async-await", "cfg-target-has-atomic", "unstable"] } 32 | embedded-graphics = "0.7.1" 33 | display-interface = "0.4.1" 34 | byte-slice-cast = { version = "1.2.0", default-features = false } 35 | heapless = "0.7.15" 36 | usbd-hid = "0.6.1" 37 | getrandom = { version = "0.2", default-features = false, features = ["custom"] } 38 | 39 | embedded-hal-1 = { package = "embedded-hal", version = "=1.0.0-rc.1" } 40 | embedded-hal-async = "1.0.0-rc.1" 41 | embedded-hal-bus = { version = "0.1.0-rc.1", features = ["async"] } 42 | embedded-io-async = { version = "0.5.0", features = ["defmt-03"] } 43 | embedded-storage = { version = "0.3" } 44 | static_cell = { version = "1.1", features = ["nightly"]} 45 | log = "0.4" 46 | pio-proc = "0.2" 47 | pio = "0.2.1" 48 | rand = { version = "0.8.5", default-features = false } 49 | embedded-alloc = "0.5.0" 50 | 51 | [profile.release] 52 | 53 | [patch.crates-io] 54 | lora-phy = { git = "https://github.com/embassy-rs/lora-phy", rev = "1323eccc1c470d4259f95f4f315d1be830d572a3"} 55 | -------------------------------------------------------------------------------- /rp2040-reticulum/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(type_alias_impl_trait)] 4 | extern crate alloc; 5 | 6 | use alloc::{boxed::Box, string::ToString}; 7 | use defmt::*; 8 | use embassy_executor::Spawner; 9 | use embassy_rp::gpio; 10 | use embassy_sync::channel; 11 | use embassy_time::{Duration, Timer}; 12 | use embedded_alloc::Heap; 13 | use getrandom::register_custom_getrandom; 14 | use gpio::{Level, Output}; 15 | use reticulum_rs::{ 16 | identity::Identity, 17 | persistence::{in_memory::InMemoryReticulumStore, ReticulumStore}, 18 | Reticulum, 19 | }; 20 | use {defmt_rtt as _, panic_probe as _}; 21 | 22 | #[global_allocator] 23 | static HEAP: Heap = Heap::empty(); 24 | 25 | pub fn danger_zero_random(buf: &mut [u8]) -> Result<(), getrandom::Error> { 26 | for i in buf.iter_mut() { 27 | *i = 0; 28 | } 29 | Ok(()) 30 | } 31 | 32 | register_custom_getrandom!(danger_zero_random); 33 | 34 | #[embassy_executor::main] 35 | async fn main(spawner: Spawner) { 36 | { 37 | use core::mem::MaybeUninit; 38 | const HEAP_SIZE: usize = 1024 * 150; 39 | static mut HEAP_MEM: [MaybeUninit; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE]; 40 | unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) } 41 | } 42 | reticulum_rs::random::init_from_seed([0; 32]).await; 43 | 44 | let p = embassy_rp::init(Default::default()); 45 | let mut led = Output::new(p.PIN_25, Level::Low); 46 | 47 | let store1: Box = Box::new(InMemoryReticulumStore::new()); 48 | let store2: Box = Box::new(InMemoryReticulumStore::new()); 49 | 50 | store1 51 | .register_destination_name("app".to_string(), [].to_vec()) 52 | .await 53 | .unwrap(); 54 | store2 55 | .register_destination_name("app".to_string(), [].to_vec()) 56 | .await 57 | .unwrap(); 58 | 59 | let reticulum1 = Reticulum::new(&store1).unwrap(); 60 | let reticulum2 = Reticulum::new(&store2).unwrap(); 61 | 62 | let destination1 = store1 63 | .destination_builder("app") 64 | .build_single(&Identity::new_local().await, &store1) 65 | .await 66 | .unwrap(); 67 | 68 | let mut announce_packets = 0; 69 | for packet in reticulum1.announce_local_destinations().await.unwrap() { 70 | announce_packets += 1; 71 | reticulum2.process_packet(packet).await.unwrap(); 72 | } 73 | 74 | loop { 75 | info!( 76 | "annoucnes: {}, destinations: {:?}, {:?}", 77 | announce_packets, 78 | store1.get_all_destinations().await.unwrap().len(), 79 | store2.get_all_destinations().await.unwrap().len(), 80 | ); 81 | info!("led on!"); 82 | led.set_high(); 83 | Timer::after(Duration::from_secs(1)).await; 84 | 85 | info!("led off!"); 86 | led.set_low(); 87 | Timer::after(Duration::from_secs(1)).await; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /reticulum-rs/src/persistence/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod destination; 2 | pub mod in_memory; 3 | 4 | use core::time::Duration; 5 | 6 | use alloc::{boxed::Box, string::String, vec::Vec}; 7 | use async_trait::async_trait; 8 | 9 | use crate::{ 10 | identity::Identity, 11 | packet::{AnnouncePacket, Packet}, 12 | NameHash, TruncatedHash, 13 | }; 14 | 15 | use self::destination::{Destination, DestinationBuilder}; 16 | 17 | #[derive(Debug)] 18 | pub enum PersistenceError { 19 | Unspecified(String), 20 | } 21 | 22 | #[async_trait] 23 | pub trait ReticulumStore: Send + Sync + 'static { 24 | async fn poll_inbox(&self, destination_hash: &TruncatedHash) -> Option; 25 | async fn next_inbox(&self, destination_hash: &TruncatedHash) -> Option; 26 | 27 | async fn register_destination_name( 28 | &self, 29 | app_name: String, 30 | aspects: Vec, 31 | ) -> Result<(), PersistenceError>; 32 | async fn get_destination_names(&self) -> Result)>, PersistenceError>; 33 | async fn register_local_destination( 34 | &self, 35 | destination: &Destination, 36 | ) -> Result<(), PersistenceError>; 37 | async fn get_local_destinations(&self) -> Result, PersistenceError>; 38 | async fn get_peer_destinations(&self) -> Result, PersistenceError>; 39 | fn destination_builder(&self, app_name: &str) -> DestinationBuilder; 40 | async fn resolve_destination( 41 | &self, 42 | hash: &NameHash, 43 | identity: &Identity, 44 | ) -> Option; 45 | async fn get_all_destinations(&self) -> Result, PersistenceError>; 46 | async fn get_destinations_by_identity_handle( 47 | &self, 48 | handle: &TruncatedHash, 49 | ) -> Result, PersistenceError>; 50 | async fn get_destinations_by_name( 51 | &self, 52 | name: &str, 53 | ) -> Result, PersistenceError>; 54 | 55 | async fn add_destination(&self, destination: Destination) -> Result<(), PersistenceError>; 56 | async fn get_destination( 57 | &self, 58 | hash: &NameHash, 59 | ) -> Result, PersistenceError>; 60 | async fn remove_destination(&self, destination: &Destination) -> Result<(), PersistenceError>; 61 | } 62 | 63 | #[derive(Clone)] 64 | pub struct AnnounceTableEntry { 65 | #[cfg(feature = "embassy")] 66 | _received_time: embassy_time::Instant, 67 | #[cfg(feature = "tokio")] 68 | _received_time: tokio::time::Instant, 69 | _retransmit_timeout: Duration, 70 | _retries: u8, 71 | _received_from: Option, 72 | _destination: Destination, 73 | _packet: AnnouncePacket, 74 | _local_rebroadcasts: u8, 75 | _block_rebroadcasts: bool, 76 | // _attached_interface: Option, 77 | } 78 | 79 | #[async_trait] 80 | pub trait MessageStore: Send + Sync + Sized + 'static { 81 | // fn sender( 82 | // &mut self, 83 | // destination_hash: &TruncatedHash, 84 | // ) -> Option>; 85 | } 86 | -------------------------------------------------------------------------------- /integration-testing/reticulum_config/config: -------------------------------------------------------------------------------- 1 | # This is the default Reticulum config file. 2 | # You should probably edit it to include any additional, 3 | # interfaces and settings you might need. 4 | 5 | # Only the most basic options are included in this default 6 | # configuration. To see a more verbose, and much longer, 7 | # configuration example, you can run the command: 8 | # rnsd --exampleconfig 9 | 10 | 11 | [reticulum] 12 | 13 | # If you enable Transport, your system will route traffic 14 | # for other peers, pass announces and serve path requests. 15 | # This should only be done for systems that are suited to 16 | # act as transport nodes, ie. if they are stationary and 17 | # always-on. This directive is optional and can be removed 18 | # for brevity. 19 | 20 | enable_transport = False 21 | 22 | 23 | # By default, the first program to launch the Reticulum 24 | # Network Stack will create a shared instance, that other 25 | # programs can communicate with. Only the shared instance 26 | # opens all the configured interfaces directly, and other 27 | # local programs communicate with the shared instance over 28 | # a local socket. This is completely transparent to the 29 | # user, and should generally be turned on. This directive 30 | # is optional and can be removed for brevity. 31 | 32 | share_instance = Yes 33 | 34 | 35 | # If you want to run multiple *different* shared instances 36 | # on the same system, you will need to specify different 37 | # shared instance ports for each. The defaults are given 38 | # below, and again, these options can be left out if you 39 | # don't need them. 40 | 41 | shared_instance_port = 37428 42 | instance_control_port = 37429 43 | 44 | 45 | # You can configure Reticulum to panic and forcibly close 46 | # if an unrecoverable interface error occurs, such as the 47 | # hardware device for an interface disappearing. This is 48 | # an optional directive, and can be left out for brevity. 49 | # This behaviour is disabled by default. 50 | 51 | panic_on_interface_error = No 52 | 53 | 54 | [logging] 55 | # Valid log levels are 0 through 7: 56 | # 0: Log only critical information 57 | # 1: Log errors and lower log levels 58 | # 2: Log warnings and lower log levels 59 | # 3: Log notices and lower log levels 60 | # 4: Log info and lower (this is the default) 61 | # 5: Verbose logging 62 | # 6: Debug logging 63 | # 7: Extreme logging 64 | 65 | loglevel = 6 66 | 67 | 68 | # The interfaces section defines the physical and virtual 69 | # interfaces Reticulum will use to communicate on. This 70 | # section will contain examples for a variety of interface 71 | # types. You can modify these or use them as a basis for 72 | # your own config, or simply remove the unused ones. 73 | 74 | [interfaces] 75 | 76 | # This interface enables communication with other 77 | # link-local Reticulum nodes over UDP. It does not 78 | # need any functional IP infrastructure like routers 79 | # or DHCP servers, but will require that at least link- 80 | # local IPv6 is enabled in your operating system, which 81 | # should be enabled by default in almost any OS. See 82 | # the Reticulum Manual for more configuration options. 83 | 84 | [[UDP Interface]] 85 | type = UDPInterface 86 | interface_enabled = True 87 | 88 | listen_ip = 127.0.0.1 89 | listen_port = 44242 90 | forward_ip = 127.0.0.1 91 | forward_port = 44243 -------------------------------------------------------------------------------- /integration-testing/server.py: -------------------------------------------------------------------------------- 1 | ########################################################## 2 | # This RNS example demonstrates setting up announce # 3 | # callbacks, which will let an application receive a # 4 | # notification when an announce relevant for it arrives # 5 | ########################################################## 6 | 7 | import random 8 | import RNS 9 | 10 | # Let's define an app name. We'll use this for all 11 | # destinations we create. Since this basic example 12 | # is part of a range of example utilities, we'll put 13 | # them all within the app namespace "example_utilities" 14 | APP_NAME = "reticulum-rs" 15 | 16 | # We initialise two lists of strings to use as app_data 17 | fruits = ["Peach", "Quince", "Date", "Tangerine", "Pomelo", "Carambola", "Grape"] 18 | noble_gases = ["Helium", "Neon", "Argon", "Krypton", "Xenon", "Radon", "Oganesson"] 19 | 20 | # This initialisation is executed when the program is started 21 | def program_setup(configpath): 22 | # We must first initialise Reticulum 23 | reticulum = RNS.Reticulum(configpath) 24 | 25 | # Randomly create a new identity for our example 26 | identity = RNS.Identity() 27 | 28 | # Using the identity we just created, we create two destinations 29 | # in the "example_utilities.announcesample" application space. 30 | # 31 | # Destinations are endpoints in Reticulum, that can be addressed 32 | # and communicated with. Destinations can also announce their 33 | # existence, which will let the network know they are reachable 34 | # and autoomatically create paths to them, from anywhere else 35 | # in the network. 36 | destination_1 = RNS.Destination( 37 | identity, 38 | RNS.Destination.IN, 39 | RNS.Destination.SINGLE, 40 | APP_NAME, 41 | ) 42 | 43 | destination_2 = RNS.Destination( 44 | identity, 45 | RNS.Destination.IN, 46 | RNS.Destination.SINGLE, 47 | APP_NAME, 48 | "announcesample", 49 | "noble_gases" 50 | ) 51 | 52 | # We configure the destinations to automatically prove all 53 | # packets adressed to it. By doing this, RNS will automatically 54 | # generate a proof for each incoming packet and transmit it 55 | # back to the sender of that packet. This will let anyone that 56 | # tries to communicate with the destination know whether their 57 | # communication was received correctly. 58 | destination_1.set_proof_strategy(RNS.Destination.PROVE_ALL) 59 | destination_2.set_proof_strategy(RNS.Destination.PROVE_ALL) 60 | 61 | announce_handler = ExampleAnnounceHandler() 62 | 63 | # We register the announce handler with Reticulum 64 | RNS.Transport.register_announce_handler(announce_handler) 65 | 66 | # Everything's ready! 67 | # Let's hand over control to the announce loop 68 | announceLoop(destination_1, destination_2) 69 | 70 | 71 | def announceLoop(destination_1, destination_2): 72 | # Let the user know that everything is ready 73 | RNS.log("Announce example running, hit enter to manually send an announce (Ctrl-C to quit)") 74 | 75 | # We enter a loop that runs until the users exits. 76 | # If the user hits enter, we will announce our server 77 | # destination on the network, which will let clients 78 | # know how to create messages directed towards it. 79 | while True: 80 | entered = input() 81 | 82 | # Randomly select a fruit 83 | fruit = fruits[random.randint(0,len(fruits)-1)] 84 | 85 | # Send the announce including the app data 86 | destination_1.announce() 87 | RNS.log( 88 | "Sent announce from "+ 89 | RNS.prettyhexrep(destination_1.hash)+ 90 | " ("+destination_1.name+")" 91 | ) 92 | 93 | # Randomly select a noble gas 94 | noble_gas = noble_gases[random.randint(0,len(noble_gases)-1)] 95 | 96 | # Send the announce including the app data 97 | destination_2.announce() 98 | RNS.log( 99 | "Sent announce from "+ 100 | RNS.prettyhexrep(destination_2.hash)+ 101 | " ("+destination_2.name+")" 102 | ) 103 | 104 | # We will need to define an announce handler class that 105 | # Reticulum can message when an announce arrives. 106 | class ExampleAnnounceHandler: 107 | # The initialisation method takes the optional 108 | # aspect_filter argument. If aspect_filter is set to 109 | # None, all announces will be passed to the instance. 110 | # If only some announces are wanted, it can be set to 111 | # an aspect string. 112 | def __init__(self, aspect_filter=None): 113 | self.aspect_filter = aspect_filter 114 | 115 | # This method will be called by Reticulums Transport 116 | # system when an announce arrives that matches the 117 | # configured aspect filter. Filters must be specific, 118 | # and cannot use wildcards. 119 | def received_announce(self, destination_hash, announced_identity, app_data): 120 | RNS.log( 121 | "Received an announce from "+ 122 | RNS.prettyhexrep(destination_hash) 123 | ) 124 | 125 | if app_data: 126 | RNS.log( 127 | "The announce contained the following app data: "+ 128 | app_data.decode("utf-8") 129 | ) 130 | 131 | ########################################################## 132 | #### Program Startup ##################################### 133 | ########################################################## 134 | 135 | # This part of the program gets run at startup, 136 | # and parses input from the user, and then starts 137 | # the desired program mode. 138 | if __name__ == "__main__": 139 | try: 140 | program_setup("./reticulum_config") 141 | 142 | except KeyboardInterrupt: 143 | print("") 144 | exit() -------------------------------------------------------------------------------- /reticulum-rs/src/persistence/in_memory.rs: -------------------------------------------------------------------------------- 1 | use alloc::boxed::Box; 2 | use alloc::format; 3 | use alloc::{string::String, vec::Vec}; 4 | use async_trait::async_trait; 5 | use defmt::{trace, warn}; 6 | 7 | use crate::identity::Identity; 8 | use crate::NameHash; 9 | use crate::{identity::IdentityCommon, packet::Packet, TruncatedHash}; 10 | 11 | use super::destination::DestinationBuilder; 12 | use super::ReticulumStore; 13 | use super::{destination::Destination, PersistenceError}; 14 | 15 | pub struct InMemoryReticulumStore { 16 | #[cfg(feature = "embassy")] 17 | destination_names: embassy_sync::mutex::Mutex< 18 | embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, 19 | Vec<(String, Vec)>, 20 | >, 21 | #[cfg(feature = "tokio")] 22 | destination_names: tokio::sync::Mutex)>>, 23 | #[cfg(feature = "embassy")] 24 | destinations: embassy_sync::mutex::Mutex< 25 | embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, 26 | Vec<(String, Destination)>, 27 | >, 28 | #[cfg(feature = "tokio")] 29 | destinations: tokio::sync::Mutex>, 30 | #[cfg(feature = "embassy")] 31 | messages: embassy_sync::mutex::Mutex< 32 | embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, 33 | Vec<( 34 | TruncatedHash, 35 | embassy_sync::channel::Channel< 36 | embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, 37 | Packet, 38 | 1, 39 | >, 40 | )>, 41 | >, 42 | #[cfg(feature = "tokio")] 43 | messages: tokio::sync::Mutex< 44 | Vec<( 45 | TruncatedHash, 46 | ( 47 | tokio::sync::mpsc::Sender, 48 | tokio::sync::Mutex>, 49 | ), 50 | )>, 51 | >, 52 | } 53 | 54 | impl InMemoryReticulumStore { 55 | pub fn new() -> InMemoryReticulumStore { 56 | InMemoryReticulumStore { 57 | #[cfg(feature = "embassy")] 58 | messages: embassy_sync::mutex::Mutex::new(Vec::new()), 59 | #[cfg(feature = "tokio")] 60 | messages: tokio::sync::Mutex::new(Vec::new()), 61 | #[cfg(feature = "embassy")] 62 | destination_names: embassy_sync::mutex::Mutex::new(Vec::new()), 63 | #[cfg(feature = "tokio")] 64 | destination_names: tokio::sync::Mutex::new(Vec::new()), 65 | #[cfg(feature = "embassy")] 66 | destinations: embassy_sync::mutex::Mutex::new(Vec::new()), 67 | #[cfg(feature = "tokio")] 68 | destinations: tokio::sync::Mutex::new(Vec::new()), 69 | } 70 | } 71 | } 72 | 73 | #[async_trait] 74 | impl ReticulumStore for InMemoryReticulumStore { 75 | async fn register_destination_name( 76 | &self, 77 | app_name: String, 78 | aspects: Vec, 79 | ) -> Result<(), PersistenceError> { 80 | self.destination_names 81 | .lock() 82 | .await 83 | .push((app_name, aspects)); 84 | Ok(()) 85 | } 86 | 87 | async fn register_local_destination( 88 | &self, 89 | destination: &Destination, 90 | ) -> Result<(), PersistenceError> { 91 | self.destinations 92 | .lock() 93 | .await 94 | .push((destination.full_name(), destination.clone())); 95 | Ok(()) 96 | } 97 | 98 | async fn get_destination_names(&self) -> Result)>, PersistenceError> { 99 | Ok(self.destination_names.lock().await.clone()) 100 | } 101 | 102 | async fn get_all_destinations(&self) -> Result, PersistenceError> { 103 | let mut all_destinations = Vec::new(); 104 | for (_, destination) in self.destinations.lock().await.iter() { 105 | all_destinations.push(destination.clone()); 106 | } 107 | Ok(all_destinations) 108 | } 109 | 110 | async fn get_destinations_by_identity_handle( 111 | &self, 112 | handle: &TruncatedHash, 113 | ) -> Result, PersistenceError> { 114 | let mut matching_destinations = Vec::new(); 115 | for (_, destination) in self.destinations.lock().await.iter() { 116 | if let Some(identity) = destination.identity() { 117 | if &identity.handle() == handle { 118 | matching_destinations.push(destination.clone()); 119 | } 120 | } 121 | } 122 | Ok(matching_destinations) 123 | } 124 | 125 | async fn add_destination(&self, destination: Destination) -> Result<(), PersistenceError> { 126 | let name = destination.full_name(); 127 | if !self 128 | .destinations 129 | .lock() 130 | .await 131 | .iter() 132 | .any(|(key, _)| key == &name) 133 | { 134 | self.destinations 135 | .lock() 136 | .await 137 | .push((destination.full_name(), destination.clone())); 138 | } else { 139 | trace!("destination already exists"); 140 | } 141 | Ok(()) 142 | } 143 | 144 | async fn remove_destination(&self, destination: &Destination) -> Result<(), PersistenceError> { 145 | let mut destinations = self.destinations.lock().await; 146 | *destinations = destinations 147 | .iter() 148 | .filter(|(_, existing_destination)| { 149 | existing_destination.address_hash() != destination.address_hash() 150 | }) 151 | .cloned() 152 | .collect(); 153 | Ok(()) 154 | } 155 | async fn poll_inbox(&self, destination_hash: &TruncatedHash) -> Option { 156 | #[cfg(feature = "embassy")] 157 | { 158 | let messages = self.messages.lock().await; 159 | let (_hash, channel) = messages 160 | .iter() 161 | .filter(|(hash, _)| hash == destination_hash) 162 | .next()?; 163 | match channel.try_receive() { 164 | Ok(packet) => Some(packet), 165 | // TODO: Do these errors mean we need to do something? 166 | Err(_) => None, 167 | } 168 | } 169 | #[cfg(feature = "tokio")] 170 | { 171 | let mut messages = self.messages.lock().await; 172 | let (_, (_sender, receiver)) = messages 173 | .iter_mut() 174 | .filter(|(hash, _)| hash == destination_hash) 175 | .next()?; 176 | let retval = match receiver.lock().await.try_recv() { 177 | Ok(packet) => Some(packet), 178 | Err(_) => None, 179 | }; 180 | return retval; 181 | } 182 | } 183 | 184 | async fn next_inbox(&self, destination_hash: &TruncatedHash) -> Option { 185 | #[cfg(feature = "embassy")] 186 | { 187 | let messages = self.messages.lock().await; 188 | let (_hash, channel) = messages 189 | .iter() 190 | .filter(|(hash, _)| hash == destination_hash) 191 | .next()?; 192 | return Some(channel.receive().await); 193 | } 194 | #[cfg(feature = "tokio")] 195 | { 196 | let mut messages = self.messages.lock().await; 197 | let (_, (_sender, receiver)) = messages 198 | .iter_mut() 199 | .filter(|(hash, _)| hash == destination_hash) 200 | .next()?; 201 | let retval = match receiver.lock().await.try_recv() { 202 | Ok(packet) => Some(packet), 203 | Err(_) => None, 204 | }; 205 | return retval; 206 | } 207 | } 208 | 209 | async fn get_local_destinations(&self) -> Result, PersistenceError> { 210 | let all_destinations = self.get_all_destinations().await?; 211 | let mut local_destinations = Vec::new(); 212 | for destination in all_destinations { 213 | if let Some(Identity::Local(_)) = destination.identity() { 214 | local_destinations.push(destination); 215 | } 216 | } 217 | Ok(local_destinations) 218 | } 219 | async fn get_peer_destinations(&self) -> Result, PersistenceError> { 220 | let all_destinations = self.get_all_destinations().await?; 221 | let mut peer_destinations = Vec::new(); 222 | for destination in all_destinations { 223 | if let Some(Identity::Peer(_)) = destination.identity() { 224 | peer_destinations.push(destination); 225 | } 226 | } 227 | Ok(peer_destinations) 228 | } 229 | fn destination_builder(&self, app_name: &str) -> DestinationBuilder { 230 | Destination::builder(app_name) 231 | } 232 | async fn resolve_destination( 233 | &self, 234 | hash: &NameHash, 235 | identity: &Identity, 236 | ) -> Option { 237 | if let Ok(Some(destination)) = self.get_destination(hash).await { 238 | if destination.name_hash() == *hash { 239 | return Some(destination); 240 | } 241 | } 242 | let destination_names = if let Ok(names) = self.get_destination_names().await { 243 | names 244 | } else { 245 | warn!("error getting destination names"); 246 | return None; 247 | }; 248 | for (app_name, aspects) in destination_names { 249 | let mut builder = Destination::builder(app_name.as_str()); 250 | for aspect in aspects { 251 | builder = builder.aspect(aspect.as_str()); 252 | } 253 | let destination = if let Ok(destination) = 254 | builder.build_single(identity, self).await.map_err(|err| { 255 | PersistenceError::Unspecified(format!("error building destination: {:?}", err)) 256 | }) { 257 | destination 258 | } else { 259 | warn!("error building destination"); 260 | return None; 261 | }; 262 | if destination.name_hash() == *hash { 263 | self.add_destination(destination.clone()).await.unwrap(); 264 | return Some(destination); 265 | } 266 | } 267 | None 268 | } 269 | 270 | async fn get_destinations_by_name( 271 | &self, 272 | name: &str, 273 | ) -> Result, PersistenceError> { 274 | let all_destinations = self.get_all_destinations().await?; 275 | let mut matching_destinations = Vec::new(); 276 | for destination in all_destinations { 277 | if destination.full_name() == name { 278 | matching_destinations.push(destination); 279 | } 280 | } 281 | Ok(matching_destinations) 282 | } 283 | 284 | async fn get_destination( 285 | &self, 286 | hash: &NameHash, 287 | ) -> Result, PersistenceError> { 288 | let all_destinations = self.get_all_destinations().await?; 289 | for existing_destination in all_destinations { 290 | if &existing_destination.name_hash() == hash { 291 | return Ok(Some(existing_destination)); 292 | } 293 | } 294 | Ok(None) 295 | } 296 | } 297 | 298 | impl AsRef for InMemoryReticulumStore { 299 | fn as_ref(&self) -> &dyn ReticulumStore { 300 | self 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /LICENSE.Apache: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /reticulum-rs/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![feature(async_closure)] 3 | #![feature(error_in_core)] 4 | #![feature(type_alias_impl_trait)] 5 | 6 | extern crate alloc; 7 | 8 | use core::error::Error; 9 | 10 | use alloc::{boxed::Box, string::String, vec::Vec}; 11 | use defmt::{debug, trace}; 12 | pub use fernet; 13 | use identity::Identity; 14 | use packet::{AnnouncePacket, Packet, PacketContextType, PacketError, WirePacket}; 15 | use persistence::{destination::Destination, PersistenceError, ReticulumStore}; 16 | 17 | use crate::packet::PacketHeaderVariable; 18 | 19 | pub mod constants; 20 | pub mod identity; 21 | pub mod packet; 22 | pub mod persistence; 23 | pub mod random; 24 | 25 | #[derive(Debug)] 26 | pub enum ReticulumError { 27 | Persistence(PersistenceError), 28 | Transport(TransportError), 29 | Unspecified(Box), 30 | } 31 | 32 | #[derive(Debug)] 33 | pub enum TransportError { 34 | Thread(Box), 35 | Persistence(PersistenceError), 36 | Packet(PacketError), 37 | Unspecified(Box), 38 | } 39 | 40 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 41 | pub struct TruncatedHash([u8; 16]); 42 | 43 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 44 | pub struct NameHash([u8; 10]); 45 | 46 | pub enum PacketChannelData { 47 | Packet(WirePacket), 48 | Close, 49 | } 50 | 51 | #[cfg(feature = "embassy")] 52 | pub type PacketSender = embassy_sync::channel::Sender< 53 | 'static, 54 | embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, 55 | PacketChannelData, 56 | 1, 57 | >; 58 | 59 | #[cfg(feature = "embassy")] 60 | pub type PacketReceiver = embassy_sync::channel::Receiver< 61 | 'static, 62 | embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, 63 | PacketChannelData, 64 | 1, 65 | >; 66 | 67 | #[cfg(feature = "tokio")] 68 | pub type PacketSender = tokio::sync::mpsc::Sender; 69 | 70 | #[cfg(feature = "tokio")] 71 | pub type PacketReceiver = tokio::sync::mpsc::Receiver; 72 | 73 | pub struct Reticulum<'a> { 74 | reticulum_store: &'a Box, 75 | } 76 | 77 | impl<'a> Reticulum<'a> { 78 | pub fn new( 79 | reticulum_store: &'a Box, 80 | ) -> Result, ReticulumError> { 81 | return Ok(Reticulum { reticulum_store }); 82 | } 83 | 84 | pub async fn get_known_destinations(&self) -> Vec { 85 | self.reticulum_store.get_all_destinations().await.unwrap() 86 | } 87 | 88 | pub async fn get_local_destinations(&self) -> Vec { 89 | let all_destinations = self.reticulum_store.get_all_destinations().await.unwrap(); 90 | let mut local_destinations = Vec::new(); 91 | for destination in all_destinations { 92 | if let Some(Identity::Local(_)) = destination.identity() { 93 | local_destinations.push(destination); 94 | } 95 | } 96 | local_destinations 97 | } 98 | 99 | pub async fn get_peer_destinations(&self) -> Result, PersistenceError> { 100 | let all_destinations = self.reticulum_store.get_all_destinations().await.unwrap(); 101 | let mut peer_destinations = Vec::new(); 102 | for destination in all_destinations { 103 | if let Some(Identity::Peer(_)) = destination.identity() { 104 | peer_destinations.push(destination); 105 | } 106 | } 107 | Ok(peer_destinations) 108 | } 109 | 110 | pub async fn register_destination_prefix( 111 | &self, 112 | app_name: String, 113 | aspects: Vec, 114 | ) -> Result<(), PersistenceError> { 115 | self.reticulum_store 116 | .register_destination_name(app_name, aspects) 117 | .await 118 | } 119 | 120 | pub async fn poll_inbox(&self, destination: &TruncatedHash) -> Option { 121 | self.reticulum_store.poll_inbox(destination).await 122 | } 123 | 124 | pub async fn next_inbox(&self, destination: &TruncatedHash) -> Option { 125 | self.reticulum_store.next_inbox(destination).await 126 | } 127 | 128 | pub async fn announce_local_destinations(&self) -> Result, TransportError> { 129 | let mut packets = Vec::new(); 130 | let destinations = self 131 | .reticulum_store 132 | .get_all_destinations() 133 | .await 134 | .map_err(|err| TransportError::Persistence(err))?; 135 | for destination in destinations { 136 | match destination.identity() { 137 | Some(Identity::Local(_)) => {} 138 | _ => continue, 139 | } 140 | 141 | // Packet response doesn't make sense here if the intention is to force an announce. 142 | let packet = AnnouncePacket::new(destination, PacketContextType::None, Vec::new()) 143 | .await 144 | .map_err(|err| TransportError::Packet(err))?; 145 | 146 | packets.push(packet.wire_packet().clone()); 147 | } 148 | Ok(packets) 149 | } 150 | 151 | async fn maybe_process_announce( 152 | semantic_packet: &Packet, 153 | reticulum_store: &Box, 154 | ) { 155 | match &semantic_packet { 156 | crate::packet::Packet::Announce(announce_packet) => { 157 | // Add to the identity store and announce table. 158 | let identity = announce_packet.identity(); 159 | trace!("Resolving destination"); 160 | let resolved_destination = { 161 | let resolved_destination = reticulum_store 162 | .resolve_destination(&announce_packet.destination_name_hash(), identity); 163 | resolved_destination.await 164 | }; 165 | if let Some(destination) = resolved_destination { 166 | match reticulum_store.add_destination(destination).await { 167 | Ok(_) => { 168 | trace!("Added destination to store"); 169 | } 170 | Err(_err) => { 171 | debug!("Failed to add destination to store"); 172 | } 173 | } 174 | } else { 175 | trace!("Failed to resolve destination"); 176 | } 177 | } 178 | crate::packet::Packet::Other(_) => todo!(), 179 | } 180 | } 181 | 182 | pub async fn process_packet(&self, packet: WirePacket) -> Result<(), ()> { 183 | // trace!("Common: {:?}", packet.header().header_common()); 184 | match packet.header().header_variable() { 185 | PacketHeaderVariable::LrProof(_hash) => { 186 | // trace!("Received LR proof: {:?}", hash); 187 | } 188 | PacketHeaderVariable::Header1(_header1) => { 189 | // trace!("Received header1: {:?}", header1); 190 | } 191 | PacketHeaderVariable::Header2(_header2) => { 192 | // trace!("Received header2: {:?}", header2); 193 | } 194 | } 195 | let semantic_packet = match packet.into_semantic_packet() { 196 | Ok(semantic_packet) => { 197 | trace!("Converted packet to semantic packet"); 198 | semantic_packet 199 | } 200 | Err(_err) => { 201 | debug!("Failed to convert packet to semantic packet"); 202 | return Ok(()); 203 | } 204 | }; 205 | // trace!("Semantic packet: {:?}", semantic_packet); 206 | Self::maybe_process_announce(&semantic_packet, &self.reticulum_store).await; 207 | if let Some(destination) = semantic_packet.destination(&self.reticulum_store).await { 208 | if let Some(Identity::Local(_)) = destination.identity() { 209 | trace!("Destination is local"); 210 | // if let Some(sender) = message_store 211 | // .lock() 212 | // .await 213 | // .as_mut() 214 | // .sender(&destination.address_hash()) 215 | // { 216 | // match sender.try_send(semantic_packet) { 217 | // Ok(_) => {} 218 | // Err(err) => { 219 | // debug!("Failed to send packet to inbox"); 220 | // } 221 | // } 222 | // } else { 223 | // debug!("No sender found for local destination"); 224 | // } 225 | } else { 226 | trace!("Destination is not local"); 227 | } 228 | } else { 229 | trace!("No destination found for packet"); 230 | } 231 | Ok(()) 232 | } 233 | } 234 | 235 | #[cfg(test)] 236 | mod test { 237 | #[cfg(test)] 238 | extern crate std; 239 | #[cfg(feature = "tokio")] 240 | extern crate tokio; 241 | 242 | use crate::{ 243 | identity::Identity, 244 | persistence::{ 245 | destination::Destination, in_memory::InMemoryReticulumStore, ReticulumStore, 246 | }, 247 | Reticulum, 248 | }; 249 | use alloc::{boxed::Box, string::ToString, vec::Vec}; 250 | use rand::Rng; 251 | use rand_chacha::rand_core::OsRng; 252 | 253 | pub(crate) fn init_test() { 254 | let _ = env_logger::try_init(); 255 | 256 | tokio_test::block_on(async { 257 | let mut seed = [0; 32]; 258 | OsRng.fill(&mut seed); 259 | crate::random::init_from_seed(seed).await; 260 | }); 261 | } 262 | 263 | async fn create_node<'a>(store: &'a Box) -> Reticulum<'a> { 264 | store 265 | .register_destination_name("app".to_string(), Vec::new()) 266 | .await 267 | .unwrap(); 268 | 269 | let node = Reticulum::new(&store).unwrap(); 270 | node 271 | } 272 | 273 | async fn setup_node<'a>(node: &'a Reticulum<'a>) -> Destination { 274 | let builder = node.reticulum_store.destination_builder("app"); 275 | let destination = builder 276 | .build_single(&Identity::new_local().await, &node.reticulum_store) 277 | .await 278 | .unwrap(); 279 | destination 280 | } 281 | 282 | #[test] 283 | fn test_announce() { 284 | init_test(); 285 | tokio_test::block_on(async { 286 | let store1: Box = Box::new(InMemoryReticulumStore::new()); 287 | let store2: Box = Box::new(InMemoryReticulumStore::new()); 288 | let reticulum1 = create_node(&store1).await; 289 | let reticulum2 = create_node(&store2).await; 290 | let node1 = &reticulum1; 291 | let node2 = &reticulum2; 292 | let _destination1 = setup_node(&node1).await; 293 | // let destination2 = setup_node(&node2).await; 294 | 295 | assert_eq!(node2.get_known_destinations().await.len(), 0); 296 | let packets = node1.announce_local_destinations().await.unwrap(); 297 | assert_eq!(packets.len(), 1); 298 | for packet in packets { 299 | node2.process_packet(packet).await.unwrap(); 300 | } 301 | tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; 302 | assert_eq!(node2.get_known_destinations().await.len(), 1); 303 | }) 304 | } 305 | 306 | #[test] 307 | fn test_announce_bidirectional() { 308 | init_test(); 309 | tokio_test::block_on(async { 310 | let store1: Box = Box::new(InMemoryReticulumStore::new()); 311 | let store2: Box = Box::new(InMemoryReticulumStore::new()); 312 | let reticulum1 = create_node(&store1).await; 313 | let reticulum2 = create_node(&store2).await; 314 | let node1 = &reticulum1; 315 | let node2 = &reticulum2; 316 | let _destination1 = setup_node(&node1).await; 317 | let _destination2 = setup_node(&node2).await; 318 | 319 | assert_eq!(node1.get_local_destinations().await.len(), 1); 320 | assert_eq!(node2.get_local_destinations().await.len(), 1); 321 | for packet in node1.announce_local_destinations().await.unwrap() { 322 | node2.process_packet(packet).await.unwrap(); 323 | } 324 | for packet in node2.announce_local_destinations().await.unwrap() { 325 | node1.process_packet(packet).await.unwrap(); 326 | } 327 | tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; 328 | assert_eq!(node1.get_known_destinations().await.len(), 2); 329 | assert_eq!(node2.get_known_destinations().await.len(), 2); 330 | }) 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /reticulum-rs/src/persistence/destination.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | extern crate std; 3 | 4 | use alloc::{ 5 | string::{String, ToString}, 6 | vec::Vec, 7 | }; 8 | use defmt::debug; 9 | use sha2::{Digest, Sha256}; 10 | 11 | use crate::{ 12 | identity::{Identity, IdentityCommon}, 13 | packet::{DestinationType, PacketError}, 14 | persistence::ReticulumStore, 15 | NameHash, TruncatedHash, 16 | }; 17 | 18 | #[derive(Debug, PartialEq)] 19 | pub enum DestinationError { 20 | EmptyAppName, 21 | DotInAppName, 22 | EmptyAspect, 23 | DotInAspect, 24 | } 25 | 26 | #[derive(Debug, Clone)] 27 | pub struct Destination { 28 | app_name: String, 29 | aspects: Vec, 30 | inner: DestinationInner, 31 | } 32 | 33 | #[derive(Debug, Clone)] 34 | enum DestinationInner { 35 | Single(SingleDestination), 36 | Group(GroupDestination), 37 | Plain(PlainDestination), 38 | } 39 | 40 | impl Destination { 41 | async fn new>( 42 | app_name: String, 43 | aspects: Vec, 44 | inner: DestinationInner, 45 | store: R, 46 | ) -> Result { 47 | if app_name.is_empty() { 48 | return Err(DestinationError::EmptyAppName); 49 | } 50 | if app_name.contains('.') { 51 | return Err(DestinationError::DotInAppName); 52 | } 53 | for aspect in &aspects { 54 | if aspect.is_empty() { 55 | return Err(DestinationError::EmptyAspect); 56 | } 57 | if aspect.contains('.') { 58 | return Err(DestinationError::DotInAspect); 59 | } 60 | } 61 | let dest = Destination { 62 | app_name, 63 | aspects, 64 | inner, 65 | }; 66 | if match dest.identity() { 67 | Some(Identity::Local(_)) => true, 68 | _ => false, 69 | } { 70 | if let Err(_err) = store.as_ref().register_local_destination(&dest).await { 71 | debug!("failed to register local destination"); 72 | } 73 | } else { 74 | if let Err(_err) = store.as_ref().add_destination(dest.clone()).await { 75 | debug!("failed to register non-local destination"); 76 | } 77 | } 78 | Ok(dest) 79 | } 80 | 81 | pub(crate) fn builder(app_name: &str) -> DestinationBuilder { 82 | DestinationBuilder::new(app_name.to_string()) 83 | } 84 | 85 | pub fn app_name(&self) -> &str { 86 | &self.app_name 87 | } 88 | 89 | pub fn aspects(&self) -> &[String] { 90 | &self.aspects 91 | } 92 | 93 | pub fn identity(&self) -> Option<&Identity> { 94 | match &self.inner { 95 | DestinationInner::Single(single) => Some(&single.identity), 96 | _ => None, 97 | } 98 | } 99 | 100 | /// Returns the full name of the destination according to the Reticulum spec. 101 | pub fn full_name(&self) -> String { 102 | let mut aspects = self.aspects.clone(); 103 | // For single destinations the spec requires that we include the identity hash as an aspect. 104 | if let DestinationInner::Single(single) = &self.inner { 105 | aspects.push(single.identity.hex_hash()) 106 | } 107 | [&[self.app_name.clone()], aspects.as_slice()] 108 | .concat() 109 | .join(".") 110 | } 111 | 112 | /// Returns the truncated hash of this destination according to the Reticulum spec. 113 | pub fn address_hash(&self) -> TruncatedHash { 114 | let mut hasher = Sha256::new(); 115 | hasher.update(self.name_hash().0); 116 | if let Some(identity) = self.identity() { 117 | hasher.update(identity.truncated_hash()); 118 | } 119 | TruncatedHash( 120 | hasher.finalize()[..16] 121 | .try_into() 122 | .expect("slice operation must produce 16 bytes"), 123 | ) 124 | } 125 | 126 | /// Returns the name hash of this destination according to the Reticulum spec. 127 | pub fn name_hash(&self) -> NameHash { 128 | let name = self.full_name(); 129 | let mut hasher = Sha256::new(); 130 | hasher.update(name.as_bytes()); 131 | NameHash( 132 | hasher.finalize()[..10] 133 | .try_into() 134 | .expect("slice operation must produce 16 bytes"), 135 | ) 136 | } 137 | 138 | /// Returns the hex representation of the truncated hash of this destination according to the Reticulum spec. 139 | pub fn hex_hash(&self) -> String { 140 | hex::encode(self.address_hash().0) 141 | } 142 | 143 | pub fn destination_type(&self) -> DestinationType { 144 | match self.inner { 145 | DestinationInner::Single(_) => DestinationType::Single, 146 | DestinationInner::Group(_) => DestinationType::Group, 147 | DestinationInner::Plain(_) => DestinationType::Plain, 148 | } 149 | } 150 | 151 | pub async fn encrypt(&self, payload: Vec) -> Result, PacketError> { 152 | match &self.inner { 153 | DestinationInner::Single(single) => single 154 | .identity 155 | .encrypt_for(&payload) 156 | .await 157 | .map_err(|err| PacketError::CryptoError(err)), 158 | DestinationInner::Group(_) => { 159 | todo!("implement group destination encryption") 160 | } 161 | DestinationInner::Plain(_) => Ok(payload), 162 | } 163 | } 164 | } 165 | 166 | #[derive(Debug, Clone)] 167 | pub struct SingleDestination { 168 | identity: Identity, 169 | } 170 | 171 | #[derive(Debug, Clone)] 172 | pub struct GroupDestination {} 173 | 174 | #[derive(Debug, Clone)] 175 | pub struct PlainDestination {} 176 | 177 | pub struct DestinationBuilder { 178 | app_name: String, 179 | aspects: Vec, 180 | } 181 | 182 | impl DestinationBuilder { 183 | fn new(app_name: String) -> DestinationBuilder { 184 | DestinationBuilder { 185 | app_name, 186 | aspects: Vec::new(), 187 | } 188 | } 189 | 190 | pub fn aspect(mut self, aspect: &str) -> DestinationBuilder { 191 | self.aspects.push(aspect.to_string()); 192 | self 193 | } 194 | 195 | #[cfg(test)] 196 | pub async fn build_single>( 197 | self, 198 | identity: &Identity, 199 | store: R, 200 | ) -> Result { 201 | Destination::new( 202 | self.app_name, 203 | self.aspects, 204 | DestinationInner::Single(SingleDestination { 205 | identity: identity.clone(), 206 | }), 207 | store, 208 | ) 209 | .await 210 | } 211 | 212 | #[cfg(not(test))] 213 | pub async fn build_single>( 214 | self, 215 | identity: &Identity, 216 | store: R, 217 | ) -> Result { 218 | Destination::new( 219 | self.app_name, 220 | self.aspects, 221 | DestinationInner::Single(SingleDestination { 222 | identity: identity.clone(), 223 | }), 224 | store, 225 | ) 226 | .await 227 | } 228 | 229 | pub async fn build_group>( 230 | self, 231 | store: R, 232 | ) -> Result { 233 | Destination::new( 234 | self.app_name, 235 | self.aspects, 236 | DestinationInner::Group(GroupDestination {}), 237 | store, 238 | ) 239 | .await 240 | } 241 | 242 | pub async fn build_plain>( 243 | self, 244 | store: R, 245 | ) -> Result { 246 | Destination::new( 247 | self.app_name, 248 | self.aspects, 249 | DestinationInner::Plain(PlainDestination {}), 250 | store, 251 | ) 252 | .await 253 | } 254 | } 255 | 256 | #[cfg(test)] 257 | mod tests { 258 | use alloc::{boxed::Box, format}; 259 | 260 | use crate::{ 261 | identity::IdentityCommon, persistence::in_memory::InMemoryReticulumStore, test::init_test, 262 | }; 263 | 264 | use super::*; 265 | 266 | #[test] 267 | fn test_full_name_single() { 268 | init_test(); 269 | tokio_test::block_on(async { 270 | let store: Box = Box::new(InMemoryReticulumStore::new()); 271 | let identity = Identity::new_local().await; 272 | let hex_hash = identity.hex_hash(); 273 | let destination = Destination::builder("app") 274 | .aspect("aspect1") 275 | .aspect("aspect2") 276 | .build_single(&identity, &store) 277 | .await; 278 | assert!(destination.is_ok()); 279 | let destination = destination.unwrap(); 280 | assert_eq!( 281 | destination.full_name(), 282 | format!("app.aspect1.aspect2.{}", hex_hash) 283 | ); 284 | }); 285 | } 286 | 287 | #[test] 288 | fn test_full_name_group() { 289 | init_test(); 290 | tokio_test::block_on(async { 291 | let store: Box = Box::new(InMemoryReticulumStore::new()); 292 | let destination = Destination::builder("app") 293 | .aspect("aspect1") 294 | .aspect("aspect2") 295 | .build_group(&store) 296 | .await; 297 | assert!(destination.is_ok()); 298 | let destination = destination.unwrap(); 299 | assert_eq!(destination.full_name(), "app.aspect1.aspect2"); 300 | }); 301 | } 302 | 303 | #[test] 304 | fn test_full_name_plain() { 305 | init_test(); 306 | tokio_test::block_on(async { 307 | let store: Box = Box::new(InMemoryReticulumStore::new()); 308 | let destination = Destination::builder("app") 309 | .aspect("aspect1") 310 | .aspect("aspect2") 311 | .build_plain(&store) 312 | .await; 313 | assert!(destination.is_ok()); 314 | let destination = destination.unwrap(); 315 | assert_eq!(destination.full_name(), "app.aspect1.aspect2"); 316 | }); 317 | } 318 | 319 | #[test] 320 | fn test_hex_hash_single() { 321 | init_test(); 322 | tokio_test::block_on(async { 323 | let store: Box = Box::new(InMemoryReticulumStore::new()); 324 | let identity = Identity::new_local().await; 325 | let hex_hash = identity.hex_hash(); 326 | let destination = Destination::builder("app") 327 | .aspect("aspect1") 328 | .aspect("aspect2") 329 | .build_single(&identity, &store) 330 | .await; 331 | assert!(destination.is_ok()); 332 | let destination = destination.unwrap(); 333 | assert_eq!( 334 | destination.full_name(), 335 | format!("app.aspect1.aspect2.{}", hex_hash) 336 | ); 337 | let mut hasher = Sha256::new(); 338 | hasher.update(destination.name_hash().0); 339 | hasher.update(identity.truncated_hash()); 340 | let hash = hasher.finalize(); 341 | assert_eq!(destination.address_hash().0, &hash[..16]); 342 | assert_eq!(destination.hex_hash(), hex::encode(&hash[..16])); 343 | }); 344 | } 345 | 346 | #[test] 347 | fn test_dot_in_app_name() { 348 | init_test(); 349 | tokio_test::block_on(async { 350 | let store: Box = Box::new(InMemoryReticulumStore::new()); 351 | let identity = Identity::new_local().await; 352 | let destination = Destination::builder("app.name") 353 | .aspect("aspect") 354 | .build_single(&identity, &store) 355 | .await; 356 | assert!(destination.is_err()); 357 | let err = destination.unwrap_err(); 358 | assert_eq!(err, DestinationError::DotInAppName); 359 | }); 360 | } 361 | 362 | #[test] 363 | fn test_dot_in_aspect() { 364 | init_test(); 365 | tokio_test::block_on(async { 366 | let store: Box = Box::new(InMemoryReticulumStore::new()); 367 | let identity = Identity::new_local().await; 368 | let destination = Destination::builder("app") 369 | .aspect("aspect.name") 370 | .build_single(&identity, &store) 371 | .await; 372 | assert!(destination.is_err()); 373 | let err = destination.unwrap_err(); 374 | assert_eq!(err, DestinationError::DotInAspect); 375 | }); 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /reticulum-rs/src/identity.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | 3 | use alloc::{boxed::Box, string::String, vec::Vec}; 4 | use async_trait::async_trait; 5 | use base64::Engine; 6 | use ed25519_dalek::{Signer, Verifier}; 7 | use hkdf::Hkdf; 8 | use sha2::{Digest, Sha256}; 9 | use x25519_dalek::PublicKey; 10 | 11 | use crate::{ 12 | packet::SignedMessage, 13 | random::{random_bytes, RNG}, 14 | TruncatedHash, 15 | }; 16 | 17 | #[derive(Debug, PartialEq)] 18 | pub enum CryptoError { 19 | InvalidKey, 20 | EncryptFailed, 21 | DecryptFailed, 22 | InvalidSignature, 23 | } 24 | 25 | #[async_trait] 26 | pub trait IdentityCommon { 27 | async fn encrypt_for(&self, message: &[u8]) -> Result, CryptoError>; 28 | fn verify_from(&self, message: Box) -> Result<(), CryptoError>; 29 | fn truncated_hash(&self) -> [u8; 16]; 30 | fn hex_hash(&self) -> String { 31 | let hash = self.truncated_hash(); 32 | hex::encode(hash) 33 | } 34 | fn handle(&self) -> TruncatedHash { 35 | TruncatedHash(self.truncated_hash()) 36 | } 37 | fn wire_repr(&self) -> [u8; 64]; 38 | } 39 | 40 | pub trait LocalIdentity { 41 | fn decrypt(&self, message: &[u8]) -> Result, CryptoError>; 42 | fn sign(&self, message: &[u8]) -> Result, CryptoError>; 43 | } 44 | 45 | #[derive(Debug, Clone)] 46 | pub struct PeerIdentityInner { 47 | identity_key: x25519_dalek::PublicKey, 48 | sign_key: ed25519_dalek::VerifyingKey, 49 | } 50 | 51 | #[async_trait] 52 | impl IdentityCommon for PeerIdentityInner { 53 | async fn encrypt_for(&self, message: &[u8]) -> Result, CryptoError> { 54 | let mut rng_guard = RNG.lock().await; 55 | let rng = rng_guard.as_mut().unwrap(); 56 | let ephemeral_key = x25519_dalek::EphemeralSecret::random_from_rng(rng); 57 | let ephemeral_pubkey = PublicKey::from(&ephemeral_key); 58 | let shared_secret = ephemeral_key.diffie_hellman(&self.identity_key); 59 | 60 | let salt = self.truncated_hash(); 61 | let ikm = shared_secret.as_bytes(); 62 | let hkdf = Hkdf::::new(Some(&salt), ikm); 63 | let mut okm = [0u8; 32]; 64 | hkdf.expand(&[], &mut okm).unwrap(); 65 | 66 | let base64_key = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(okm); 67 | if let Some(fernet_key) = fernet::Fernet::new(&base64_key) { 68 | let message = fernet_key.encrypt(message); 69 | if let Ok(bytes) = base64::engine::general_purpose::URL_SAFE.decode(message) { 70 | let ephemeral_pubkey = ephemeral_pubkey.as_bytes(); 71 | assert_eq!(ephemeral_pubkey.len(), 32); 72 | Ok([ephemeral_pubkey, bytes.as_slice()].concat()) 73 | } else { 74 | Err(CryptoError::EncryptFailed) 75 | } 76 | } else { 77 | Err(CryptoError::InvalidKey) 78 | } 79 | } 80 | 81 | fn verify_from(&self, message: Box) -> Result<(), CryptoError> { 82 | let signature = message.signature(); 83 | let signed_data = message.signed_data(); 84 | if let Ok(signature_fixed_len) = signature[0..64].try_into() { 85 | let signature = ed25519_dalek::Signature::from_bytes(signature_fixed_len); 86 | if self.sign_key.verify(signed_data, &signature).is_ok() { 87 | Ok(()) 88 | } else { 89 | Err(CryptoError::InvalidSignature) 90 | } 91 | } else { 92 | Err(CryptoError::InvalidSignature) 93 | } 94 | } 95 | 96 | fn truncated_hash(&self) -> [u8; 16] { 97 | let mut hasher = Sha256::new(); 98 | hasher.update(self.wire_repr()); 99 | let hash = hasher.finalize(); 100 | let mut truncated_hash = [0u8; 16]; 101 | truncated_hash.copy_from_slice(&hash[0..16]); 102 | truncated_hash 103 | } 104 | 105 | fn wire_repr(&self) -> [u8; 64] { 106 | let mut public_keys = [0u8; 64]; 107 | let identity_key = *self.identity_key.as_bytes(); 108 | let sign_key = self.sign_key.to_bytes(); 109 | public_keys[0..32].copy_from_slice(&identity_key); 110 | public_keys[32..64].copy_from_slice(&sign_key); 111 | public_keys 112 | } 113 | } 114 | 115 | #[derive(Clone)] 116 | pub struct LocalIdentityInner { 117 | public_keys: PeerIdentityInner, 118 | private_key: x25519_dalek::StaticSecret, 119 | private_sign_key: ed25519_dalek::SigningKey, 120 | } 121 | 122 | impl Debug for LocalIdentityInner { 123 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 124 | f.debug_struct("LocalIdentityInner") 125 | .field("public_keys", &self.public_keys) 126 | .finish() 127 | } 128 | } 129 | 130 | #[async_trait] 131 | impl IdentityCommon for LocalIdentityInner { 132 | async fn encrypt_for(&self, message: &[u8]) -> Result, CryptoError> { 133 | self.public_keys.encrypt_for(message).await 134 | } 135 | 136 | fn verify_from(&self, message: Box) -> Result<(), CryptoError> { 137 | self.public_keys.verify_from(message) 138 | } 139 | 140 | fn truncated_hash(&self) -> [u8; 16] { 141 | self.public_keys.truncated_hash() 142 | } 143 | 144 | fn wire_repr(&self) -> [u8; 64] { 145 | self.public_keys.wire_repr() 146 | } 147 | } 148 | 149 | impl LocalIdentity for LocalIdentityInner { 150 | fn decrypt(&self, message_encoded: &[u8]) -> Result, CryptoError> { 151 | let message_pubkey: &[u8; 32] = message_encoded[0..32].try_into().unwrap(); 152 | let message_ciphertext = &message_encoded[32..]; 153 | 154 | let other_pubkey = x25519_dalek::PublicKey::from(message_pubkey.clone()); 155 | let shared_secret = self.private_key.diffie_hellman(&other_pubkey); 156 | 157 | let salt = self.truncated_hash(); 158 | let ikm = shared_secret.as_bytes(); 159 | let hkdf = Hkdf::::new(Some(&salt), ikm); 160 | let mut okm = [0u8; 32]; 161 | hkdf.expand(&[], &mut okm).unwrap(); 162 | 163 | let base64_key = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(okm); 164 | let message_ciphertext_base64 = 165 | base64::engine::general_purpose::URL_SAFE.encode(message_ciphertext); 166 | if let Some(fernet_key) = fernet::Fernet::new(&base64_key) { 167 | let message_cleartext = fernet_key.decrypt(&message_ciphertext_base64).unwrap(); 168 | Ok(message_cleartext) 169 | } else { 170 | Err(CryptoError::InvalidKey) 171 | } 172 | } 173 | 174 | fn sign(&self, message: &[u8]) -> Result, CryptoError> { 175 | let signature = self.private_sign_key.sign(message); 176 | Ok(signature.to_bytes().to_vec()) 177 | } 178 | } 179 | 180 | #[derive(Debug, Clone)] 181 | pub enum Identity { 182 | Local(LocalIdentityInner), 183 | Peer(PeerIdentityInner), 184 | } 185 | 186 | #[async_trait] 187 | impl IdentityCommon for Identity { 188 | async fn encrypt_for(&self, message: &[u8]) -> Result, CryptoError> { 189 | match self { 190 | Identity::Local(local_identity) => local_identity.encrypt_for(message).await, 191 | Identity::Peer(peer_identity) => peer_identity.encrypt_for(message).await, 192 | } 193 | } 194 | 195 | fn verify_from(&self, message: Box) -> Result<(), CryptoError> { 196 | match self { 197 | Identity::Local(local_identity) => local_identity.verify_from(message), 198 | Identity::Peer(peer_identity) => peer_identity.verify_from(message), 199 | } 200 | } 201 | 202 | fn truncated_hash(&self) -> [u8; 16] { 203 | match self { 204 | Identity::Local(local_identity) => local_identity.truncated_hash(), 205 | Identity::Peer(peer_identity) => peer_identity.truncated_hash(), 206 | } 207 | } 208 | 209 | fn wire_repr(&self) -> [u8; 64] { 210 | match self { 211 | Identity::Local(local_identity) => local_identity.wire_repr(), 212 | Identity::Peer(peer_identity) => peer_identity.wire_repr(), 213 | } 214 | } 215 | } 216 | 217 | impl Identity { 218 | pub async fn new_local() -> Identity { 219 | let mut key = [0u8; 32]; 220 | random_bytes(&mut key).await; 221 | let private_key = x25519_dalek::StaticSecret::from(key); 222 | let public_key = x25519_dalek::PublicKey::from(&private_key); 223 | 224 | random_bytes(&mut key).await; 225 | let private_sign_key = ed25519_dalek::SigningKey::from_bytes(&key); 226 | let public_sign_key = private_sign_key.verifying_key(); 227 | let peer_identity = PeerIdentityInner { 228 | identity_key: public_key, 229 | sign_key: public_sign_key, 230 | }; 231 | let local_identity = LocalIdentityInner { 232 | public_keys: peer_identity, 233 | private_key, 234 | private_sign_key, 235 | }; 236 | Identity::Local(local_identity) 237 | } 238 | 239 | pub fn from_wire_repr(wire_repr: &[u8]) -> Result { 240 | if wire_repr.len() != 64 { 241 | return Err(CryptoError::InvalidKey); 242 | } 243 | let identity_key_bytes: [u8; 32] = wire_repr[0..32] 244 | .try_into() 245 | .expect("Slice must yield 32 bytes"); 246 | let identity_key = x25519_dalek::PublicKey::from(identity_key_bytes); 247 | let sign_key_bytes: [u8; 32] = wire_repr[32..64] 248 | .try_into() 249 | .expect("Slice must yield 32 bytes"); 250 | let sign_key = ed25519_dalek::VerifyingKey::from_bytes(&sign_key_bytes) 251 | .map_err(|_| CryptoError::InvalidKey)?; 252 | let peer_identity = PeerIdentityInner { 253 | identity_key, 254 | sign_key, 255 | }; 256 | Ok(Identity::Peer(peer_identity)) 257 | } 258 | 259 | pub fn is_local(&self) -> bool { 260 | match self { 261 | Identity::Local(_) => true, 262 | _ => false, 263 | } 264 | } 265 | 266 | pub fn full_hash(&self) -> [u8; 32] { 267 | let mut hasher = Sha256::new(); 268 | hasher.update(self.wire_repr()); 269 | let hash = hasher.finalize(); 270 | let mut full_hash = [0u8; 32]; 271 | full_hash.copy_from_slice(&hash[0..32]); 272 | full_hash 273 | } 274 | } 275 | 276 | #[cfg(test)] 277 | mod test { 278 | use alloc::{boxed::Box, vec::Vec}; 279 | 280 | use crate::{packet::SignedMessage, test::init_test}; 281 | 282 | use super::{IdentityCommon, LocalIdentity}; 283 | 284 | struct TestSignedMessage { 285 | signed_data: Vec, 286 | signature: Vec, 287 | } 288 | 289 | impl SignedMessage for TestSignedMessage { 290 | fn signed_data(&self) -> &[u8] { 291 | &self.signed_data 292 | } 293 | 294 | fn signature(&self) -> &[u8] { 295 | &self.signature 296 | } 297 | } 298 | 299 | #[test] 300 | fn create_identity() { 301 | init_test(); 302 | tokio_test::block_on(async { 303 | let identity = super::Identity::new_local().await; 304 | match identity { 305 | super::Identity::Local(_) => (), 306 | _ => panic!("Expected local identity"), 307 | } 308 | }) 309 | } 310 | 311 | #[test] 312 | fn encrypt_decrypt_local() { 313 | init_test(); 314 | tokio_test::block_on(async { 315 | let identity = super::Identity::new_local().await; 316 | let message = b"Hello, world!"; 317 | let encrypted = identity.encrypt_for(message).await.unwrap(); 318 | match identity { 319 | super::Identity::Local(identity) => { 320 | let decrypted = identity.decrypt(&encrypted).unwrap(); 321 | assert_eq!(message, decrypted.as_slice()); 322 | } 323 | _ => panic!("Expected local identity"), 324 | } 325 | }); 326 | } 327 | 328 | #[test] 329 | fn sign_verify_local() { 330 | init_test(); 331 | tokio_test::block_on(async { 332 | let identity = super::Identity::new_local().await; 333 | let message = b"Hello, world!"; 334 | let signature = match &identity { 335 | super::Identity::Local(identity) => identity.sign(message).unwrap(), 336 | _ => panic!("Expected local identity"), 337 | }; 338 | let _ = identity 339 | .verify_from(Box::new(TestSignedMessage { 340 | signed_data: message.to_vec(), 341 | signature: signature.to_vec(), 342 | })) 343 | .unwrap(); 344 | }); 345 | } 346 | 347 | #[test] 348 | fn sign_verify_local_tampering() { 349 | init_test(); 350 | tokio_test::block_on(async { 351 | let identity = super::Identity::new_local().await; 352 | let message = b"Hello, world!"; 353 | let mut signature = match &identity { 354 | super::Identity::Local(identity) => identity.sign(message).unwrap(), 355 | _ => panic!("Expected local identity"), 356 | }; 357 | signature[0] = signature[0].wrapping_add(128); 358 | assert!(identity 359 | .verify_from(Box::new(TestSignedMessage { 360 | signed_data: message.to_vec(), 361 | signature: signature.to_vec(), 362 | })) 363 | .is_err()); 364 | }); 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /fernet-rs/LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /fernet-rs/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![feature(error_in_core)] 3 | 4 | //! Fernet provides symmetric-authenticated-encryption with an API that makes 5 | //! misusing it difficult. It is based on a public specification and there are 6 | //! interoperable implementations in Rust, Python, Ruby, Go, and Clojure. 7 | 8 | //! # Example 9 | //! ```rust 10 | //! // Store `key` somewhere safe! 11 | //! let key = fernet::Fernet::generate_key(); 12 | //! let fernet = fernet::Fernet::new(&key).unwrap(); 13 | //! let plaintext = b"my top secret message!"; 14 | //! let ciphertext = fernet.encrypt(plaintext); 15 | //! let decrypted_plaintext = fernet.decrypt(&ciphertext); 16 | //! assert_eq!(decrypted_plaintext.unwrap(), plaintext); 17 | // ``` 18 | 19 | extern crate alloc; 20 | 21 | use core::convert::TryInto; 22 | use core::fmt::Display; 23 | use core::{error::Error, fmt}; 24 | 25 | use alloc::{string::String, vec::Vec}; 26 | use base64::Engine; 27 | use byteorder::{BigEndian, ByteOrder}; 28 | use zeroize::Zeroize; 29 | 30 | use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; 31 | use hmac::Mac; 32 | use sha2::Sha256; 33 | 34 | const MAX_CLOCK_SKEW: u64 = 60; 35 | 36 | // Automatically zero out the contents of the memory when the struct is drop'd. 37 | #[derive(Clone, Zeroize)] 38 | #[zeroize(drop)] 39 | pub struct Fernet { 40 | encryption_key: [u8; 16], 41 | signing_key: [u8; 16], 42 | } 43 | 44 | /// This error is returned when fernet cannot decrypt the ciphertext for any 45 | /// reason. It could be an expired token, incorrect key or other failure. If 46 | /// you recieve this error, you should consider the fernet token provided as 47 | /// invalid. 48 | #[derive(Debug, PartialEq, Eq)] 49 | pub struct DecryptionError; 50 | 51 | impl Error for DecryptionError {} 52 | 53 | impl Display for DecryptionError { 54 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 55 | write!(f, "Fernet decryption error") 56 | } 57 | } 58 | 59 | #[derive(Clone)] 60 | pub struct MultiFernet { 61 | fernets: Vec, 62 | } 63 | 64 | /// `MultiFernet` encapsulates the encrypt operation with the first `Fernet` 65 | /// instance and decryption with the `Fernet` instances provided in order 66 | /// until successful decryption or a `DecryptionError`. 67 | impl MultiFernet { 68 | pub fn new(keys: Vec) -> MultiFernet { 69 | assert!(!keys.is_empty(), "keys must not be empty"); 70 | MultiFernet { fernets: keys } 71 | } 72 | 73 | /// Encrypts data with the first `Fernet` instance. Returns a value 74 | /// (which is base64-encoded) that can be passed to `MultiFernet::decrypt`. 75 | pub fn encrypt(&self, data: &[u8]) -> String { 76 | self.fernets[0].encrypt(data) 77 | } 78 | 79 | /// Decrypts a ciphertext, using the `Fernet` instances provided. Returns 80 | /// either `Ok(plaintext)` if decryption is successful or 81 | /// `Err(DecryptionError)` if no decryption was possible across the set of 82 | /// fernet keys. 83 | pub fn decrypt(&self, token: &str) -> Result, DecryptionError> { 84 | for fernet in self.fernets.iter() { 85 | let res = fernet.decrypt(token); 86 | if res.is_ok() { 87 | return res; 88 | } 89 | } 90 | 91 | Err(DecryptionError) 92 | } 93 | } 94 | 95 | /// Token split into parts before decryption. 96 | struct ParsedToken { 97 | /// message is the whole token except for the HMAC 98 | message: Vec, 99 | /// 128 bit IV 100 | iv: [u8; 16], 101 | /// Ciphertext (part of message) 102 | ciphertext: Vec, 103 | /// 256 bit HMAC 104 | hmac: [u8; 32], 105 | } 106 | 107 | /// `Fernet` encapsulates encrypt and decrypt operations for a particular symmetric key. 108 | impl Fernet { 109 | /// Returns a new fernet instance with the provided key. The key should be 110 | /// 32-bytes, url-safe base64-encoded. Generating keys with `Fernet::generate_key` 111 | /// is recommended. DO NOT USE A HUMAN READABLE PASSWORD AS A KEY. Returns 112 | /// `None` if the key is not 32-bytes base64 encoded. 113 | pub fn new(key: &str) -> Option { 114 | let key = b64_decode_url(key).ok()?; 115 | if key.len() != 32 { 116 | return None; 117 | } 118 | 119 | let mut signing_key: [u8; 16] = Default::default(); 120 | signing_key.copy_from_slice(&key[..16]); 121 | let mut encryption_key: [u8; 16] = Default::default(); 122 | encryption_key.copy_from_slice(&key[16..]); 123 | 124 | Some(Fernet { 125 | signing_key, 126 | encryption_key, 127 | }) 128 | } 129 | 130 | /// Generates a new, random, key. Can be safely passed to `Fernet::new()`. 131 | /// Store this somewhere safe! 132 | pub fn generate_key() -> String { 133 | let mut key: [u8; 32] = Default::default(); 134 | getrandom::getrandom(&mut key).expect("Error in getrandom"); 135 | crate::b64_encode_url(&key.to_vec()) 136 | } 137 | 138 | /// Encrypts data into a token. Returns a value (which is base64-encoded) that can be 139 | /// passed to `Fernet::decrypt` for decryption and verification.. 140 | pub fn encrypt(&self, data: &[u8]) -> String { 141 | let current_time = 0; 142 | self._encrypt_at_time(data, current_time) 143 | } 144 | 145 | /// Encrypts data with the current_time. Returns a value (which is base64-encoded) that can be 146 | /// passed to `Fernet::decrypt`. 147 | /// 148 | /// This function has the capacity to be used incorrectly or insecurely due to 149 | /// to the "current_time" parameter. current_time must be the systems `time::SystemTime::now()` 150 | /// with `duraction_since(time::UNIX_EPOCH)` as seconds. 151 | /// 152 | /// The motivation for a function like this is for your application to be able to test 153 | /// ttl expiry of tokens in your API. This allows you to pass in mock time data to assert 154 | /// correct behaviour of your application. Care should be taken to ensure you always pass in 155 | /// correct current_time values for deployments. 156 | #[inline] 157 | #[cfg(feature = "fernet_danger_timestamps")] 158 | pub fn encrypt_at_time(&self, data: &[u8], current_time: u64) -> String { 159 | self._encrypt_at_time(data, current_time) 160 | } 161 | 162 | fn _encrypt_at_time(&self, data: &[u8], current_time: u64) -> String { 163 | let mut iv: [u8; 16] = Default::default(); 164 | getrandom::getrandom(&mut iv).expect("Error in getrandom"); 165 | self._encrypt_from_parts(data, current_time, &iv) 166 | } 167 | 168 | fn _encrypt_from_parts(&self, data: &[u8], current_time: u64, iv: &[u8]) -> String { 169 | let ciphertext = cbc::Encryptor::::new_from_slices(&self.encryption_key, iv) 170 | .unwrap() 171 | .encrypt_padded_vec_mut::(data); 172 | 173 | let mut result = [0x80].to_vec(); 174 | result.extend_from_slice(¤t_time.to_be_bytes()); 175 | result.extend_from_slice(iv); 176 | result.extend_from_slice(&ciphertext); 177 | 178 | let mut hmac_signer = hmac::Hmac::::new_from_slice(&self.signing_key) 179 | .expect("Signing key has unexpected size"); 180 | hmac_signer.update(&result); 181 | 182 | result.extend_from_slice(&hmac_signer.finalize().into_bytes()); 183 | crate::b64_encode_url(&result) 184 | } 185 | 186 | /// Decrypts a ciphertext. Returns either `Ok(plaintext)` if decryption is 187 | /// successful or `Err(DecryptionError)` if there are any errors. Errors could 188 | /// include incorrect key or tampering with the data. 189 | pub fn decrypt(&self, token: &str) -> Result, DecryptionError> { 190 | let current_time = 0; 191 | self._decrypt_at_time(token, None, current_time) 192 | } 193 | 194 | /// Decrypts a ciphertext with a time-to-live. Returns either `Ok(plaintext)` 195 | /// if decryption is successful or `Err(DecryptionError)` if there are any errors. 196 | /// Note if the token timestamp + ttl > current time, then this will also yield a 197 | /// DecryptionError. The ttl is measured in seconds. This is a relative time, not 198 | /// the absolute time of expiry. IE you would use 60 as a ttl_secs if you wanted 199 | /// tokens to be considered invalid after that time. 200 | pub fn decrypt_with_ttl(&self, token: &str, ttl_secs: u64) -> Result, DecryptionError> { 201 | let current_time = 0; 202 | self._decrypt_at_time(token, Some(ttl_secs), current_time) 203 | } 204 | 205 | /// Decrypt a ciphertext with a time-to-live, and the current time. 206 | /// Returns either `Ok(plaintext)` if decryption is 207 | /// successful or `Err(DecryptionError)` if there are any errors. 208 | /// 209 | /// This function has the capacity to be used incorrectly or insecurely due to 210 | /// to the "current_time" parameter. current_time must be the systems time::SystemTime::now() 211 | /// with duraction_since(time::UNIX_EPOCH) as seconds. 212 | /// 213 | /// The motivation for a function like this is for your application to be able to test 214 | /// ttl expiry of tokens in your API. This allows you to pass in mock time data to assert 215 | /// correct behaviour of your application. Care should be taken to ensure you always pass in 216 | /// correct current_time values for deployments. 217 | #[inline] 218 | #[cfg(feature = "fernet_danger_timestamps")] 219 | pub fn decrypt_at_time( 220 | &self, 221 | token: &str, 222 | ttl: Option, 223 | current_time: u64, 224 | ) -> Result, DecryptionError> { 225 | self._decrypt_at_time(token, ttl, current_time) 226 | } 227 | 228 | fn _decrypt_at_time( 229 | &self, 230 | token: &str, 231 | ttl: Option, 232 | current_time: u64, 233 | ) -> Result, DecryptionError> { 234 | let parsed = Self::_decrypt_parse(token, ttl, current_time)?; 235 | 236 | let mut hmac_signer = hmac::Hmac::::new_from_slice(&self.signing_key) 237 | .expect("Signing key has unexpected size"); 238 | hmac_signer.update(&parsed.message); 239 | 240 | let expected_hmac = hmac_signer.finalize().into_bytes(); 241 | 242 | use subtle::ConstantTimeEq; 243 | let hmac_matches: bool = parsed.hmac.ct_eq(&expected_hmac).into(); 244 | if !hmac_matches { 245 | return Err(DecryptionError); 246 | } 247 | 248 | let plaintext = 249 | cbc::Decryptor::::new_from_slices(&self.encryption_key, &parsed.iv) 250 | .unwrap() 251 | .decrypt_padded_vec_mut::(&parsed.ciphertext) 252 | .map_err(|_| DecryptionError)?; 253 | 254 | Ok(plaintext) 255 | } 256 | 257 | /// Parse the base64-encoded token into parts, verify timestamp TTL if given 258 | fn _decrypt_parse( 259 | token: &str, 260 | ttl: Option, 261 | current_time: u64, 262 | ) -> Result { 263 | let data = match b64_decode_url(token) { 264 | Ok(data) => data, 265 | Err(_) => return Err(DecryptionError), 266 | }; 267 | 268 | match data[0] { 269 | 0x80 => {} 270 | _ => return Err(DecryptionError), 271 | } 272 | 273 | let timestamp = BigEndian::read_u64(&data[1..9]); 274 | 275 | if let Some(ttl) = ttl { 276 | if timestamp + ttl < current_time { 277 | return Err(DecryptionError); 278 | } 279 | } 280 | 281 | if current_time + MAX_CLOCK_SKEW < timestamp { 282 | return Err(DecryptionError); 283 | } 284 | 285 | let iv: [u8; 16] = data[9..25].try_into().map_err(|_| DecryptionError)?; 286 | 287 | let rest = &data[25..]; 288 | if rest.len() < 32 { 289 | return Err(DecryptionError); 290 | } 291 | let ciphertext = rest[..rest.len() - 32].to_vec(); 292 | 293 | let hmac = data[data.len() - 32..] 294 | .try_into() 295 | .map_err(|_| DecryptionError)?; 296 | 297 | let message = data[..data.len() - 32].to_vec(); 298 | Ok(ParsedToken { 299 | message, 300 | iv, 301 | ciphertext, 302 | hmac, 303 | }) 304 | } 305 | } 306 | 307 | #[cfg(test)] 308 | mod tests { 309 | extern crate std; 310 | 311 | use super::{DecryptionError, Fernet, MultiFernet}; 312 | use alloc::vec::Vec; 313 | use serde_derive::Deserialize; 314 | use std::collections::HashSet; 315 | use time::format_description::well_known::Rfc3339; 316 | use time::OffsetDateTime; 317 | 318 | #[derive(Deserialize)] 319 | struct GenerateVector<'a> { 320 | token: &'a str, 321 | now: &'a str, 322 | iv: Vec, 323 | src: &'a str, 324 | secret: &'a str, 325 | } 326 | 327 | #[derive(Deserialize)] 328 | struct VerifyVector<'a> { 329 | token: &'a str, 330 | now: &'a str, 331 | ttl_sec: u64, 332 | src: &'a str, 333 | secret: &'a str, 334 | } 335 | 336 | #[derive(Deserialize, Debug)] 337 | struct InvalidVector<'a> { 338 | token: &'a str, 339 | now: &'a str, 340 | ttl_sec: u64, 341 | secret: &'a str, 342 | } 343 | 344 | #[test] 345 | fn test_generate_vectors() { 346 | let vectors: Vec = 347 | serde_json::from_str(include_str!("../tests/generate.json")).unwrap(); 348 | 349 | for v in vectors { 350 | let f = Fernet::new(v.secret).unwrap(); 351 | let token = f._encrypt_from_parts( 352 | v.src.as_bytes(), 353 | OffsetDateTime::parse(v.now, &Rfc3339) 354 | .unwrap() 355 | .unix_timestamp() as u64, 356 | &v.iv, 357 | ); 358 | assert_eq!(token, v.token); 359 | } 360 | } 361 | 362 | #[test] 363 | fn test_verify_vectors() { 364 | let vectors: Vec = 365 | serde_json::from_str(include_str!("../tests/verify.json")).unwrap(); 366 | 367 | for v in vectors { 368 | let f = Fernet::new(v.secret).unwrap(); 369 | let decrypted = f._decrypt_at_time( 370 | v.token, 371 | Some(v.ttl_sec), 372 | OffsetDateTime::parse(v.now, &Rfc3339) 373 | .unwrap() 374 | .unix_timestamp() as u64, 375 | ); 376 | assert_eq!(decrypted, Ok(v.src.as_bytes().to_vec())); 377 | } 378 | } 379 | 380 | #[test] 381 | fn test_invalid_vectors() { 382 | let vectors: Vec = 383 | serde_json::from_str(include_str!("../tests/invalid.json")).unwrap(); 384 | 385 | for v in vectors { 386 | let f = Fernet::new(v.secret).unwrap(); 387 | let decrypted = f._decrypt_at_time( 388 | v.token, 389 | Some(v.ttl_sec), 390 | OffsetDateTime::parse(v.now, &Rfc3339) 391 | .unwrap() 392 | .unix_timestamp() as u64, 393 | ); 394 | assert_eq!(decrypted, Err(DecryptionError)); 395 | } 396 | } 397 | 398 | #[test] 399 | fn test_invalid() { 400 | let f = Fernet::new(&super::b64_encode_url(&[0; 32].to_vec())).unwrap(); 401 | 402 | // Invalid version byte 403 | assert_eq!( 404 | f.decrypt(&crate::b64_encode_url(&b"\x81".to_vec())), 405 | Err(DecryptionError) 406 | ); 407 | // Timestamp too short 408 | assert_eq!( 409 | f.decrypt(&super::b64_encode_url(&b"\x80\x00\x00\x00".to_vec())), 410 | Err(DecryptionError) 411 | ); 412 | // Invalid base64 413 | assert_eq!(f.decrypt("\x00"), Err(DecryptionError)); 414 | } 415 | 416 | #[test] 417 | fn test_roundtrips() { 418 | let f = Fernet::new(&super::b64_encode_url(&[0; 32].to_vec())).unwrap(); 419 | 420 | for val in [b"".to_vec(), b"Abc".to_vec(), b"\x00\xFF\x00\x00".to_vec()].iter() { 421 | assert_eq!(f.decrypt(&f.encrypt(val)), Ok(val.clone())); 422 | } 423 | } 424 | 425 | #[test] 426 | fn test_new_errors() { 427 | assert!(Fernet::new("axxx").is_none()); 428 | assert!(Fernet::new(&super::b64_encode_url(&[0, 33].to_vec())).is_none()); 429 | assert!(Fernet::new(&super::b64_encode_url(&[0, 31].to_vec())).is_none()); 430 | } 431 | 432 | #[test] 433 | fn test_generate_key() { 434 | let mut keys = HashSet::new(); 435 | for _ in 0..1024 { 436 | keys.insert(Fernet::generate_key()); 437 | } 438 | assert_eq!(keys.len(), 1024); 439 | } 440 | 441 | #[test] 442 | fn test_generate_key_roundtrips() { 443 | let k = Fernet::generate_key(); 444 | let f1 = Fernet::new(&k).unwrap(); 445 | let f2 = Fernet::new(&k).unwrap(); 446 | 447 | for val in [b"".to_vec(), b"Abc".to_vec(), b"\x00\xFF\x00\x00".to_vec()].iter() { 448 | assert_eq!(f1.decrypt(&f2.encrypt(val)), Ok(val.clone())); 449 | assert_eq!(f2.decrypt(&f1.encrypt(val)), Ok(val.clone())); 450 | } 451 | } 452 | 453 | #[test] 454 | fn test_multi_encrypt() { 455 | let key1 = Fernet::generate_key(); 456 | let key2 = Fernet::generate_key(); 457 | let f1 = Fernet::new(&key1).unwrap(); 458 | let f2 = Fernet::new(&key2).unwrap(); 459 | let f = 460 | MultiFernet::new([Fernet::new(&key1).unwrap(), Fernet::new(&key2).unwrap()].to_vec()); 461 | assert_eq!(f1.decrypt(&f.encrypt(b"abc")).unwrap(), b"abc".to_vec()); 462 | assert_eq!(f2.decrypt(&f.encrypt(b"abc")), Err(DecryptionError)); 463 | } 464 | 465 | #[test] 466 | fn test_multi_decrypt() { 467 | let key1 = Fernet::generate_key(); 468 | let key2 = Fernet::generate_key(); 469 | let f1 = Fernet::new(&key1).unwrap(); 470 | let f2 = Fernet::new(&key2).unwrap(); 471 | let f = 472 | MultiFernet::new([Fernet::new(&key1).unwrap(), Fernet::new(&key2).unwrap()].to_vec()); 473 | assert_eq!(f.decrypt(&f1.encrypt(b"abc")).unwrap(), b"abc".to_vec()); 474 | assert_eq!(f.decrypt(&f2.encrypt(b"abc")).unwrap(), b"abc".to_vec()); 475 | assert_eq!(f.decrypt("\x00"), Err(DecryptionError)); 476 | } 477 | 478 | #[test] 479 | #[should_panic] 480 | fn test_multi_no_fernets() { 481 | MultiFernet::new(Vec::new()); 482 | } 483 | 484 | #[test] 485 | fn test_multi_roundtrips() { 486 | let f = MultiFernet::new([Fernet::new(&Fernet::generate_key()).unwrap()].to_vec()); 487 | 488 | for val in [b"".to_vec(), b"Abc".to_vec(), b"\x00\xFF\x00\x00".to_vec()].iter() { 489 | assert_eq!(f.decrypt(&f.encrypt(val)), Ok(val.clone())); 490 | } 491 | } 492 | } 493 | 494 | /// base64 had a habit of changing this a fair bit, so isolating these functions 495 | /// to reduce future code changes. 496 | /// 497 | pub(crate) fn b64_decode_url(input: &str) -> Result, base64::DecodeError> { 498 | base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(input.trim_end_matches('=')) 499 | } 500 | 501 | pub(crate) fn b64_encode_url(input: &Vec) -> String { 502 | base64::engine::general_purpose::URL_SAFE.encode(input) 503 | } 504 | -------------------------------------------------------------------------------- /reticulum-rs/src/packet.rs: -------------------------------------------------------------------------------- 1 | use core::error::Error; 2 | 3 | use alloc::format; 4 | use alloc::string::{String, ToString}; 5 | use alloc::{boxed::Box, vec::Vec}; 6 | use defmt::warn; 7 | use packed_struct::prelude::{PackedStruct, PrimitiveEnum}; 8 | 9 | use crate::random::random_bytes; 10 | use crate::{ 11 | identity::{CryptoError, Identity, IdentityCommon, LocalIdentity}, 12 | persistence::{destination::Destination, ReticulumStore}, 13 | NameHash, TruncatedHash, 14 | }; 15 | 16 | pub trait SignedMessage { 17 | fn signed_data(&self) -> &[u8]; 18 | fn signature(&self) -> &[u8]; 19 | } 20 | 21 | pub trait EncryptedMessage { 22 | fn public_key(&self) -> &[u8]; 23 | fn encrypted_data(&self) -> &[u8]; 24 | } 25 | 26 | #[derive(Debug)] 27 | pub enum PacketError { 28 | AnnounceDestinationNotSingle, 29 | CryptoError(CryptoError), 30 | PackingError(packed_struct::PackingError), 31 | Unspecified(Box), 32 | Unknown(String), 33 | } 34 | 35 | #[derive(Clone, PartialEq, PackedStruct)] 36 | #[packed_struct(bit_numbering = "msb0")] 37 | pub struct PacketHeaderCommon { 38 | // First byte 39 | #[packed_field(bits = "1", ty = "enum")] 40 | header_type: HeaderType, 41 | #[packed_field(bits = "2..=3", ty = "enum")] 42 | transport_type: TransportType, 43 | #[packed_field(bits = "4..=5", ty = "enum")] 44 | destination_type: DestinationType, 45 | #[packed_field(bits = "6..=7", ty = "enum")] 46 | packet_type: PacketType, 47 | // Second byte: The number of hops this packet has taken. 48 | hops: u8, 49 | } 50 | 51 | impl PacketHeaderCommon { 52 | pub fn header_type(&self) -> HeaderType { 53 | self.header_type 54 | } 55 | pub fn transport_type(&self) -> TransportType { 56 | self.transport_type 57 | } 58 | pub fn destination_type(&self) -> DestinationType { 59 | self.destination_type 60 | } 61 | pub fn packet_type(&self) -> PacketType { 62 | self.packet_type 63 | } 64 | pub fn hops(&self) -> u8 { 65 | self.hops 66 | } 67 | } 68 | 69 | #[derive(Clone, PartialEq, PackedStruct)] 70 | #[packed_struct(bit_numbering = "msb0")] 71 | 72 | pub struct PacketHeader1 { 73 | // Destination's truncated hash. 74 | destination_hash: [u8; 16], 75 | // Packet context type. 76 | #[packed_field(bytes = "16", ty = "enum")] 77 | context_type: PacketContextType, 78 | } 79 | 80 | impl PacketHeader1 { 81 | pub fn destination_hash(&self) -> TruncatedHash { 82 | TruncatedHash(self.destination_hash) 83 | } 84 | pub fn context_type(&self) -> PacketContextType { 85 | self.context_type 86 | } 87 | } 88 | 89 | #[derive(Clone, PartialEq, PackedStruct)] 90 | #[packed_struct(bit_numbering = "msb0")] 91 | pub struct PacketHeader2 { 92 | // Transport's truncated hash. 93 | transport_id: [u8; 16], 94 | // Destination's truncated hash. 95 | destination_hash: [u8; 16], 96 | // Packet context type. 97 | #[packed_field(bytes = "32", ty = "enum")] 98 | context_type: PacketContextType, 99 | } 100 | 101 | impl PacketHeader2 { 102 | pub fn transport_id(&self) -> TruncatedHash { 103 | TruncatedHash(self.transport_id) 104 | } 105 | pub fn destination_hash(&self) -> TruncatedHash { 106 | TruncatedHash(self.destination_hash) 107 | } 108 | pub fn context_type(&self) -> PacketContextType { 109 | self.context_type 110 | } 111 | } 112 | 113 | #[derive(PrimitiveEnum, Debug, Copy, Clone, PartialEq)] 114 | pub enum HeaderType { 115 | Header1 = 0, 116 | Header2 = 1, 117 | } 118 | 119 | #[derive(PrimitiveEnum, Debug, Copy, Clone, PartialEq)] 120 | pub enum TransportType { 121 | Broadcast = 0, 122 | Transport = 1, 123 | Relay = 2, 124 | Tunnel = 3, 125 | } 126 | 127 | #[derive(PrimitiveEnum, Debug, Copy, Clone, PartialEq)] 128 | pub enum DestinationType { 129 | Single = 0, 130 | Group = 1, 131 | Plain = 2, 132 | Link = 3, 133 | } 134 | 135 | #[derive(PrimitiveEnum, Debug, Copy, Clone, PartialEq)] 136 | pub enum PacketType { 137 | Data = 0, 138 | Announce = 1, 139 | LinkRequest = 2, 140 | Proof = 3, 141 | } 142 | 143 | #[derive(PrimitiveEnum, Debug, Copy, Clone, PartialEq)] 144 | pub enum PacketContextType { 145 | None = 0x00, 146 | Resource = 0x01, 147 | ResourceAdv = 0x02, 148 | ResourceReq = 0x03, 149 | ResourceHmu = 0x04, 150 | ResourcePrf = 0x05, 151 | ResourceIcl = 0x06, 152 | ResourceRcl = 0x07, 153 | CacheRequest = 0x08, 154 | Request = 0x09, 155 | Response = 0x0A, 156 | PathResponse = 0x0B, 157 | Command = 0x0C, 158 | CommandStatus = 0x0D, 159 | Channel = 0x0E, 160 | Keepalive = 0xFA, 161 | LinkIdentify = 0xFB, 162 | LinkClose = 0xFC, 163 | LinkProof = 0xFD, 164 | LRRTT = 0xFE, 165 | LRProof = 0xFF, 166 | } 167 | 168 | #[derive(Clone, PartialEq)] 169 | pub enum PacketHeaderVariable { 170 | LrProof(TruncatedHash), 171 | Header1(PacketHeader1), 172 | Header2(PacketHeader2), 173 | } 174 | 175 | #[derive(Clone, PartialEq)] 176 | pub struct PacketHeader { 177 | header_common: PacketHeaderCommon, 178 | header_variable: PacketHeaderVariable, 179 | } 180 | 181 | impl PacketHeader { 182 | pub fn header_common(&self) -> &PacketHeaderCommon { 183 | &self.header_common 184 | } 185 | pub fn header_variable(&self) -> &PacketHeaderVariable { 186 | &self.header_variable 187 | } 188 | } 189 | 190 | #[derive(Clone, PartialEq)] 191 | pub struct WirePacket { 192 | header_common: PacketHeaderCommon, 193 | header: PacketHeader, 194 | payload: Vec, 195 | } 196 | 197 | impl WirePacket { 198 | pub fn new_lrproof( 199 | packet_type: PacketType, 200 | transport_type: TransportType, 201 | destination_link_hash: TruncatedHash, 202 | payload: Vec, 203 | ) -> WirePacket { 204 | let header_common = PacketHeaderCommon { 205 | header_type: HeaderType::Header2, 206 | transport_type, 207 | destination_type: DestinationType::Link, 208 | packet_type, 209 | hops: 0, 210 | }; 211 | let header_variable = PacketHeaderVariable::LrProof(destination_link_hash); 212 | let header = PacketHeader { 213 | header_common: header_common.clone(), 214 | header_variable, 215 | }; 216 | debug_assert!(!Self::should_encrypt_payload(&header_common, &header)); 217 | WirePacket { 218 | header_common: header_common.clone(), 219 | header, 220 | payload, 221 | } 222 | } 223 | 224 | pub async fn new_without_transport( 225 | packet_type: PacketType, 226 | context_type: PacketContextType, 227 | transport_type: TransportType, 228 | destination: &Destination, 229 | payload: Vec, 230 | ) -> Result { 231 | let header_common = PacketHeaderCommon { 232 | header_type: HeaderType::Header1, 233 | transport_type, 234 | destination_type: destination.destination_type(), 235 | packet_type, 236 | hops: 0, 237 | }; 238 | let header_variable = PacketHeaderVariable::Header1(PacketHeader1 { 239 | destination_hash: destination.address_hash().0, 240 | context_type, 241 | }); 242 | let header = PacketHeader { 243 | header_common: header_common.clone(), 244 | header_variable, 245 | }; 246 | let payload = if Self::should_encrypt_payload(&header_common, &header) { 247 | destination.encrypt(payload).await? 248 | } else { 249 | payload 250 | }; 251 | Ok(WirePacket { 252 | header_common: header_common.clone(), 253 | header, 254 | payload, 255 | }) 256 | } 257 | 258 | pub async fn new_with_transport( 259 | packet_type: PacketType, 260 | context_type: PacketContextType, 261 | transport_type: TransportType, 262 | transport_id: &Destination, // maybe an identity here? 263 | destination: &Destination, 264 | payload: Vec, 265 | ) -> Result { 266 | let header_common = PacketHeaderCommon { 267 | header_type: HeaderType::Header2, 268 | transport_type, 269 | destination_type: destination.destination_type(), 270 | packet_type, 271 | hops: 0, 272 | }; 273 | let header_variable = PacketHeaderVariable::Header2(PacketHeader2 { 274 | transport_id: transport_id.address_hash().0, 275 | destination_hash: destination.address_hash().0, 276 | context_type, 277 | }); 278 | let header = PacketHeader { 279 | header_common: header_common.clone(), 280 | header_variable, 281 | }; 282 | let payload = if Self::should_encrypt_payload(&header_common, &header) { 283 | destination.encrypt(payload).await? 284 | } else { 285 | payload 286 | }; 287 | Ok(WirePacket { 288 | header_common: header_common.clone(), 289 | header, 290 | payload, 291 | }) 292 | } 293 | 294 | pub fn pack(&self) -> Result, PacketError> { 295 | let mut packed = Vec::new(); 296 | packed.extend( 297 | self.header_common 298 | .pack() 299 | .map_err(|err| PacketError::PackingError(err))?, 300 | ); 301 | 302 | match &self.header.header_variable { 303 | PacketHeaderVariable::LrProof(destination_link_hash) => { 304 | packed.extend(destination_link_hash.0); 305 | } 306 | PacketHeaderVariable::Header1(header1) => { 307 | packed.extend( 308 | header1 309 | .pack() 310 | .map_err(|err| PacketError::PackingError(err))?, 311 | ); 312 | } 313 | PacketHeaderVariable::Header2(header2) => { 314 | packed.extend( 315 | header2 316 | .pack() 317 | .map_err(|err| PacketError::PackingError(err))?, 318 | ); 319 | } 320 | } 321 | packed.extend(self.payload.clone()); 322 | Ok(packed) 323 | } 324 | 325 | pub fn unpack(raw: &[u8]) -> Result { 326 | let header_common = PacketHeaderCommon::unpack(&[raw[0], raw[1]]) 327 | .map_err(|err| PacketError::PackingError(err))?; 328 | let (header_variable, payload) = match header_common.header_type { 329 | HeaderType::Header1 => { 330 | let raw_header: &[u8; 17] = raw[2..19] 331 | .try_into() 332 | .map_err(|_err| PacketError::Unknown("Try from slice failed".to_string()))?; 333 | let header1 = PacketHeader1::unpack(raw_header) 334 | .map_err(|err| PacketError::PackingError(err))?; 335 | (PacketHeaderVariable::Header1(header1), &raw[19..]) 336 | } 337 | HeaderType::Header2 => { 338 | let raw_header: &[u8; 33] = raw[2..35] 339 | .try_into() 340 | .map_err(|_err| PacketError::Unknown("Try from slice failed".to_string()))?; 341 | 342 | let header2 = PacketHeader2::unpack(raw_header) 343 | .map_err(|err| PacketError::PackingError(err))?; 344 | (PacketHeaderVariable::Header2(header2), &raw[35..]) 345 | } 346 | }; 347 | let header = PacketHeader { 348 | header_common: header_common.clone(), 349 | header_variable, 350 | }; 351 | 352 | Ok(WirePacket { 353 | header_common: header_common.clone(), 354 | header, 355 | payload: payload.to_vec(), 356 | }) 357 | } 358 | 359 | pub fn header(&self) -> &PacketHeader { 360 | &self.header 361 | } 362 | 363 | pub fn into_semantic_packet(self) -> Result { 364 | if self.header.header_common.packet_type == PacketType::Announce { 365 | let expected_size: usize = 64 // Public keys 366 | + 10 // Destination name hash 367 | + 10 // Random hash 368 | + 64; // Signature 369 | if self.payload.len() < expected_size { 370 | return Err(PacketError::Unknown(format!( 371 | "Announce packet incorrect length: {}", 372 | self.payload.len() 373 | ))); 374 | } 375 | let identity = Identity::from_wire_repr(&self.payload[0..64]) 376 | .map_err(|err| PacketError::CryptoError(err))?; 377 | let destination_name_hash = NameHash( 378 | self.payload[64..74] 379 | .try_into() 380 | .map_err(|_err| PacketError::Unknown("Try from slice failed".to_string()))?, 381 | ); 382 | let random_hash = NameHash( 383 | self.payload[74..84] 384 | .try_into() 385 | .map_err(|_err| PacketError::Unknown("Try from slice failed".to_string()))?, 386 | ); 387 | let signature = self.payload[84..148] 388 | .try_into() 389 | .map_err(|_err| PacketError::Unknown("Try from slice failed".to_string()))?; 390 | let app_data = self.payload[148..].to_vec(); 391 | 392 | warn!("TODO: Verify signature"); 393 | 394 | return Ok(Packet::Announce(AnnouncePacket { 395 | identity, 396 | destination_name_hash, 397 | random_hash, 398 | signature, 399 | app_data, 400 | wire_packet: self, 401 | })); 402 | } 403 | return Ok(Packet::Other(self)); 404 | } 405 | 406 | fn should_encrypt_payload(header_common: &PacketHeaderCommon, header: &PacketHeader) -> bool { 407 | match &header.header_variable { 408 | PacketHeaderVariable::LrProof(_destination_link_hash) => { 409 | return false; 410 | } 411 | PacketHeaderVariable::Header1(header1) => { 412 | let packet_type = header_common.packet_type; 413 | let context_type = header1.context_type; 414 | let destination_type = header_common.destination_type; 415 | if packet_type == PacketType::Announce { 416 | return false; 417 | } 418 | if packet_type == PacketType::LinkRequest { 419 | return false; 420 | } 421 | if packet_type == PacketType::Proof 422 | && context_type == PacketContextType::ResourcePrf 423 | { 424 | return false; 425 | } 426 | if packet_type == PacketType::Proof && destination_type == DestinationType::Link { 427 | return false; 428 | } 429 | if context_type == PacketContextType::Resource { 430 | return false; 431 | } 432 | if context_type == PacketContextType::Keepalive { 433 | return false; 434 | } 435 | if context_type == PacketContextType::CacheRequest { 436 | return false; 437 | } 438 | return true; 439 | } 440 | PacketHeaderVariable::Header2(_header2) => { 441 | let packet_type = header_common.packet_type; 442 | if packet_type == PacketType::Announce { 443 | return false; 444 | } 445 | return true; 446 | } 447 | } 448 | } 449 | } 450 | 451 | #[derive(Clone)] 452 | pub struct AnnouncePacket { 453 | identity: Identity, 454 | destination_name_hash: NameHash, 455 | random_hash: NameHash, 456 | signature: [u8; 64], 457 | #[allow(dead_code)] 458 | app_data: Vec, 459 | wire_packet: WirePacket, 460 | } 461 | 462 | impl AnnouncePacket { 463 | pub async fn new( 464 | destination: Destination, 465 | path_context: PacketContextType, 466 | app_data: Vec, 467 | ) -> Result { 468 | if path_context != PacketContextType::None 469 | && path_context != PacketContextType::PathResponse 470 | { 471 | return Err(PacketError::Unknown( 472 | "Announce packet path context not supported".to_string(), 473 | )); 474 | } 475 | let identity = destination 476 | .identity() 477 | .ok_or(PacketError::AnnounceDestinationNotSingle)?; 478 | let destination_name_hash = destination.name_hash(); 479 | let mut random_hash_bytes = [0u8; 10]; 480 | random_bytes(&mut random_hash_bytes).await; 481 | let random_hash = NameHash(random_hash_bytes); // TODO: Include time? That's what the reference implementation does. 482 | let mut signature_material = [0u8; 164].to_vec(); 483 | signature_material[0..16].copy_from_slice(&destination.address_hash().0); 484 | signature_material[16..80].copy_from_slice(&identity.wire_repr()); 485 | signature_material[80..90].copy_from_slice(&destination_name_hash.0); 486 | signature_material[90..100].copy_from_slice(&random_hash.0); 487 | if let Identity::Local(local) = identity { 488 | let signature = local 489 | .sign(&signature_material[0..100]) 490 | .map_err(|err| PacketError::CryptoError(err))?; 491 | signature_material[100..164].copy_from_slice(&signature); 492 | } else { 493 | return Err(PacketError::Unknown( 494 | "Announce packet identity not local".to_string(), 495 | )); 496 | } 497 | signature_material.extend(&app_data); 498 | let wire_packet = WirePacket::new_without_transport( 499 | PacketType::Announce, 500 | path_context, 501 | TransportType::Broadcast, 502 | &destination, 503 | signature_material[16..].to_vec(), 504 | ) 505 | .await?; 506 | Ok(AnnouncePacket { 507 | identity: identity.clone(), 508 | destination_name_hash: destination_name_hash, 509 | random_hash, 510 | signature: signature_material[100..164] 511 | .try_into() 512 | .map_err(|_err| PacketError::Unknown("Try from slice failed".to_string()))?, 513 | app_data, 514 | wire_packet, 515 | }) 516 | } 517 | pub fn identity(&self) -> &Identity { 518 | &self.identity 519 | } 520 | pub fn destination_name_hash(&self) -> &NameHash { 521 | &self.destination_name_hash 522 | } 523 | pub fn random_hash(&self) -> &NameHash { 524 | &self.random_hash 525 | } 526 | pub fn signature(&self) -> &[u8] { 527 | &self.signature 528 | } 529 | pub fn wire_packet(&self) -> &WirePacket { 530 | &self.wire_packet 531 | } 532 | } 533 | 534 | #[derive(Clone)] 535 | pub enum Packet { 536 | Announce(AnnouncePacket), 537 | Other(WirePacket), 538 | } 539 | 540 | impl Packet { 541 | pub fn wire_packet(&self) -> &WirePacket { 542 | match self { 543 | Packet::Announce(announce) => announce.wire_packet(), 544 | Packet::Other(other) => other, 545 | } 546 | } 547 | 548 | pub(crate) async fn destination( 549 | &self, 550 | reticulum_store: &Box, 551 | ) -> Option { 552 | match self { 553 | Packet::Announce(announce) => { 554 | reticulum_store 555 | .resolve_destination(announce.destination_name_hash(), announce.identity()) 556 | .await 557 | } 558 | Packet::Other(_other) => None, 559 | } 560 | } 561 | } 562 | 563 | #[cfg(test)] 564 | mod test { 565 | 566 | use alloc::{boxed::Box, sync::Arc, vec::Vec}; 567 | use smol::lock::Mutex; 568 | 569 | use crate::{ 570 | identity::{Identity, IdentityCommon, LocalIdentity}, 571 | packet::{ 572 | AnnouncePacket, Packet, PacketContextType, PacketType, TransportType, WirePacket, 573 | }, 574 | persistence::{destination::Destination, in_memory::InMemoryReticulumStore}, 575 | test::init_test, 576 | }; 577 | 578 | #[test] 579 | fn test_packet() { 580 | init_test(); 581 | tokio_test::block_on(async move { 582 | let store = Arc::new(Mutex::new(Box::new(InMemoryReticulumStore::new()))); 583 | let receiver = Identity::new_local().await; 584 | let destination = Destination::builder("app") 585 | .build_single(&receiver, store.lock().await.as_ref()) 586 | .await 587 | .unwrap(); 588 | let packet = WirePacket::new_without_transport( 589 | PacketType::Data, 590 | PacketContextType::None, 591 | TransportType::Transport, 592 | &destination, 593 | [0; 16].to_vec(), 594 | ) 595 | .await 596 | .unwrap(); 597 | let packed = packet.pack().unwrap(); 598 | let unpacked = WirePacket::unpack(&packed).unwrap(); 599 | // assert_eq!(packet, unpacked); 600 | let decrypted = if let Identity::Local(local) = receiver { 601 | local.decrypt(&unpacked.payload).unwrap() 602 | } else { 603 | panic!("not a local identity"); 604 | }; 605 | assert_eq!([0; 16].to_vec(), decrypted); 606 | }); 607 | } 608 | 609 | #[test] 610 | fn test_create_and_parse_announce_packet() { 611 | init_test(); 612 | tokio_test::block_on(async move { 613 | let store = Arc::new(Mutex::new(Box::new(InMemoryReticulumStore::new()))); 614 | let identity = Identity::new_local().await; 615 | let destination = Destination::builder("app") 616 | .build_single(&identity, store.lock().await.as_mut()) 617 | .await 618 | .unwrap(); 619 | let packet = 620 | AnnouncePacket::new(destination.clone(), PacketContextType::None, Vec::new()) 621 | .await 622 | .unwrap(); 623 | let wire_packet = packet.wire_packet(); 624 | let packed = wire_packet.pack().unwrap(); 625 | let unpacked = WirePacket::unpack(&packed).unwrap(); 626 | // assert_eq!(wire_packet, &unpacked); 627 | let semantic = unpacked.into_semantic_packet().unwrap(); 628 | if let Packet::Announce(announce) = semantic { 629 | assert_eq!(announce.identity().wire_repr(), identity.wire_repr()); 630 | assert_eq!(announce.destination_name_hash(), &destination.name_hash()); 631 | assert_eq!(announce.random_hash(), packet.random_hash()); 632 | assert_eq!(announce.signature(), packet.signature()); 633 | } else { 634 | panic!("not an announce packet"); 635 | } 636 | }); 637 | } 638 | } 639 | --------------------------------------------------------------------------------