reachable_node/server/
mod.rs

1// SPDX-FileCopyrightText: 2023—2025 eaon <eaon@posteo.net>
2// SPDX-License-Identifier: EUPL-1.2
3
4use axum::{body::Body, http::HeaderValue, response::Response};
5use hyper::{StatusCode, header};
6use include_dir::{Dir, include_dir};
7use rand_core::CryptoRngCore;
8
9use reach_aliases::*;
10use reach_core::{AuditAuthenticationAssurance, RandomFromRng, error, storage::*, wire::*};
11use reach_signatures::Verifier;
12
13pub mod memory;
14use memory::Sealable;
15
16pub mod net;
17use net::SessionState;
18
19pub const DIST_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../reaching-app/dist");
20
21pub fn from_included_dir<A>(directory: &'static Dir<'_>, path: A) -> Response
22where
23    A: AsRef<str>,
24{
25    let content_type = mime_guess::from_path(path.as_ref()).first_or_octet_stream();
26    let path = path.as_ref().trim_start_matches('/');
27
28    match directory.get_file(path) {
29        None => Response::builder()
30            .status(StatusCode::NOT_FOUND)
31            .body(Body::empty())
32            .unwrap(),
33        Some(file) => Response::builder()
34            .status(StatusCode::OK)
35            .header(
36                header::CONTENT_TYPE,
37                HeaderValue::from_str(content_type.essence_str()).unwrap(),
38            )
39            .body(Body::from(file.contents()))
40            .unwrap(),
41    }
42}
43
44#[macro_export]
45#[doc(hidden)]
46macro_rules! _included {
47    ($included_dir:tt, $included_path:tt) => {
48        || async { reachable_node::server::from_included_dir(&$included_dir, $included_path) }
49    };
50
51    ($included_dir:tt) => {
52        |axum::extract::Path(path): axum::extract::Path<String>| async {
53            reachable_node::server::from_included_dir(&$included_dir, path)
54        }
55    };
56}
57
58#[doc(inline)]
59pub use _included as included;
60
61impl Sealable<SealedEnvelopeId> for EnvelopeId {}
62impl Sealable<SealedMessageVaultId> for MessageVaultId {}
63
64type AddableEnvelopeMaterial<'a> = (
65    SealedEnvelopeId,
66    Vec<(BlindedPublicKey, OneSix)>,
67    Envelope,
68    &'a MessageVaultId,
69);
70
71pub fn assign_envelope_id(envelope_seed: EnvelopeSeed, id: EnvelopeId) -> Envelope {
72    let EnvelopeSeed {
73        blinded_public_keys: _,
74        credential_vaults,
75        nonce,
76        message_vault_passport_ciphertext,
77    } = envelope_seed;
78
79    Envelope {
80        id,
81        credential_vaults,
82        nonce,
83        message_vault_passport_ciphertext,
84    }
85}
86
87pub fn assign_message_vault_id(
88    message_vault_seed: MessageVaultSeed,
89    id: MessageVaultId,
90) -> MessageVault {
91    MessageVault {
92        id,
93        message_ciphertext: message_vault_seed.message_ciphertext,
94    }
95}
96
97pub fn update_reaching_salts(salts: &mut Salts, csprng: &mut impl CryptoRngCore) {
98    salts.reaching_previous = salts.reaching_current;
99    salts.reaching_current = ThreeTwo::random_from_rng(csprng);
100}
101
102pub fn process_envelope_seed<'a>(
103    envelope_seed: EnvelopeSeed,
104    message_vault_id: &'a MessageVaultId,
105    shared_public_keys: &'_ SharedPublicKeys,
106    salts: &'_ Salts,
107    csprng: &mut impl CryptoRngCore,
108) -> Result<AddableEnvelopeMaterial<'a>, error::CryptError> {
109    let envelope_id = EnvelopeId::random_from_rng(csprng);
110    let sealed_envelope_id = envelope_id.seal(shared_public_keys, salts, csprng)?;
111    let blinded_public_key_removal_token_pairs = envelope_seed
112        .blinded_public_keys
113        .clone()
114        .into_iter()
115        .map(|blinded_public_key| (blinded_public_key, OneSix::random_from_rng(csprng)))
116        .collect();
117    let envelope = assign_envelope_id(envelope_seed, envelope_id);
118
119    Ok((
120        sealed_envelope_id,
121        blinded_public_key_removal_token_pairs,
122        envelope,
123        message_vault_id,
124    ))
125}
126
127type AddableMessageVaultMaterial = (SealedMessageVaultId, MessageVault);
128
129pub fn process_message_vault_seed(
130    message_vault_seed: MessageVaultSeed,
131    shared_public_keys: &SharedPublicKeys,
132    salts: &Salts,
133    csprng: &mut impl CryptoRngCore,
134) -> Result<AddableMessageVaultMaterial, error::CryptError> {
135    let message_vault_id = MessageVaultId::random_from_rng(csprng);
136    let sealed_message_vault_id = message_vault_id.seal(shared_public_keys, salts, csprng)?;
137
138    Ok((
139        sealed_message_vault_id,
140        assign_message_vault_id(message_vault_seed, message_vault_id),
141    ))
142}
143
144pub trait SessionAuthentication {
145    const AUTHENTICATED_SESSION_STATE: SessionState;
146
147    fn attempt_session_authentication(
148        &self,
149        authentication_assurance: &AuthenticationAssurance,
150        authentication_challenge: &AuthenticationChallenge,
151    ) -> SessionState
152    where
153        Self: Verifier + AuditAuthenticationAssurance,
154    {
155        if self.audit_authentication_assurance(authentication_assurance, authentication_challenge) {
156            return Self::AUTHENTICATED_SESSION_STATE;
157        }
158
159        SessionState::Ready
160    }
161}
162
163impl SessionAuthentication for ReachableVerifyingKeys {
164    const AUTHENTICATED_SESSION_STATE: SessionState = SessionState::AuthenticatedReachable;
165}
166
167impl SessionAuthentication for AttestantAuthenticationKeys {
168    const AUTHENTICATED_SESSION_STATE: SessionState = SessionState::AuthenticatedAttestant;
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    use ecdh_omr::Decoy;
176    use reach_harness::seeded_rng;
177
178    #[test]
179    fn salts_update() {
180        let mut seeded_rng = seeded_rng(0);
181        let mut salts = Salts::random_decoy(&mut seeded_rng);
182        let expiring_secret_keys_salt = salts.reaching_current;
183        update_reaching_salts(&mut salts, &mut seeded_rng);
184
185        assert_eq!(expiring_secret_keys_salt, salts.reaching_previous);
186        assert_ne!(expiring_secret_keys_salt, salts.reaching_current);
187    }
188}