geph4_protocol/binder/
protocol.rs

1use arrayref::array_ref;
2use async_trait::async_trait;
3use bytes::Bytes;
4use chacha20poly1305::{
5    aead::{Aead, NewAead},
6    ChaCha20Poly1305, Nonce,
7};
8use nanorpc::{nanorpc_derive, JrpcRequest, JrpcResponse};
9use serde::{Deserialize, Serialize};
10use serde_with::serde_as;
11use smol_str::SmolStr;
12use std::{
13    collections::BTreeMap,
14    net::SocketAddr,
15    time::{SystemTime, UNIX_EPOCH},
16};
17use stdcode::StdcodeSerializeExt;
18use thiserror::Error;
19use tmelcrypt::{Ed25519PK, Ed25519SK};
20
21const PUBKEY_AUTH_COOKIE: &[u8; 32] = b"gephauth001---------------------";
22
23/// Verifies a signature for the pubkey authentication method used by the client/binder protocol.
24pub fn verify_pk_auth(pk: Ed25519PK, unix_secs: u64, sig: &[u8]) -> bool {
25    let now = SystemTime::now()
26        .duration_since(UNIX_EPOCH)
27        .unwrap()
28        .as_secs();
29    if now.abs_diff(unix_secs) > 600 {
30        return false;
31    }
32    pk.verify(
33        blake3::keyed_hash(PUBKEY_AUTH_COOKIE, &unix_secs.to_be_bytes()).as_bytes(),
34        sig,
35    )
36}
37
38/// Encrypts a message, "box-style", to a destination diffie-hellman public key.
39pub fn box_encrypt(
40    plain: &[u8],
41    my_sk: x25519_dalek::StaticSecret,
42    their_pk: x25519_dalek::PublicKey,
43) -> Bytes {
44    let my_pk = x25519_dalek::PublicKey::from(&my_sk);
45    let shared_secret = my_sk.diffie_hellman(&their_pk);
46    // we key to *their pk* to ensure that our message, reflected back, cannot be misinterpreted as a valid message addressed to us (e.g. as a response)
47    let key = blake3::keyed_hash(
48        blake3::hash(their_pk.as_bytes()).as_bytes(),
49        shared_secret.as_bytes(),
50    );
51    let cipher = ChaCha20Poly1305::new(key.as_bytes().into());
52    let ciphertext = cipher
53        .encrypt(Nonce::from_slice(&[0u8; 12]), plain)
54        .unwrap();
55    let mut pk_and_ctext = Vec::with_capacity(ciphertext.len() + 32);
56    pk_and_ctext.extend_from_slice(my_pk.as_bytes());
57    pk_and_ctext.extend_from_slice(&ciphertext);
58    pk_and_ctext.into()
59}
60
61/// Decrypts a message, "box-style", given our diffie-hellman secret key. Returns both the other side's public key and the plaintext.
62pub fn box_decrypt(
63    ctext: &[u8],
64    my_sk: x25519_dalek::StaticSecret,
65) -> Result<(Bytes, x25519_dalek::PublicKey), BoxDecryptError> {
66    if ctext.len() < 32 {
67        return Err(BoxDecryptError::BadFormat);
68    }
69    let their_pk = x25519_dalek::PublicKey::from(*array_ref![ctext, 0, 32]);
70    let shared_secret = my_sk.diffie_hellman(&their_pk);
71    // we use *our pk* as the key
72    let my_pk = x25519_dalek::PublicKey::from(&my_sk);
73    let key = blake3::keyed_hash(
74        blake3::hash(my_pk.as_bytes()).as_bytes(),
75        shared_secret.as_bytes(),
76    );
77    let cipher = ChaCha20Poly1305::new(key.as_bytes().into());
78    let plain = cipher
79        .decrypt(Nonce::from_slice(&[0u8; 12]), &ctext[32..])
80        .map_err(|_| BoxDecryptError::DecryptionFailed)?;
81    Ok((plain.into(), their_pk))
82}
83
84/// Authentication error
85#[derive(Error, Debug, Clone, Copy, Serialize, Deserialize)]
86pub enum BoxDecryptError {
87    #[error("decryption failed")]
88    DecryptionFailed,
89    #[error("badly formatted message")]
90    BadFormat,
91}
92
93#[derive(Error, Debug, Clone, Copy, Serialize, Deserialize)]
94pub enum RpcError {
95    #[error("error retreiving bootstrap routes")]
96    BootstrapFailed,
97    #[error("error connecting to melnode")]
98    ConnectFailed,
99    #[error("error communicating with melnode")]
100    CommFailed,
101}
102
103#[nanorpc_derive]
104#[async_trait]
105pub trait BinderProtocol {
106    /// Authenticates a 24-hour-long session for a user.
107    /// NOTE: This is a legacy method, and will be deprecated later.
108    async fn authenticate(&self, auth_req: AuthRequest) -> Result<AuthResponse, AuthError>;
109
110    /// Authenticates a 24-hour-long session for a user.
111    async fn authenticate_v2(&self, auth_req: AuthRequestV2) -> Result<AuthResponseV2, AuthError>;
112
113    /// Retrieves a login url
114    async fn get_login_url(&self, credentials: Credentials) -> Result<String, AuthError>;
115
116    /// Validates a blind signature token, applying rate-limiting as appropriate
117    async fn validate(&self, token: BlindToken) -> bool;
118
119    /// Obtains a unique captcha, for user registry.
120    async fn get_captcha(&self) -> Result<Captcha, MiscFatalError>;
121
122    /// Registers a new user.
123    /// NOTE: This is a legacy method, and will be deprecated later.
124    async fn register_user(
125        &self,
126        username: SmolStr,
127        password: SmolStr,
128        captcha_id: SmolStr,
129        captcha_soln: SmolStr,
130    ) -> Result<(), RegisterError>;
131
132    /// Registers a new user.
133    async fn register_user_v2(
134        &self,
135        credentials: Credentials,
136        captcha_id: SmolStr,
137        captcha_soln: SmolStr,
138    ) -> Result<(), RegisterError>;
139
140    /// Deletes a user.
141    /// NOTE: This is a legacy method, and will be deprecated later.
142    async fn delete_user(&self, username: SmolStr, password: SmolStr) -> Result<(), AuthError>;
143
144    /// Deletes a user.
145    async fn delete_user_v2(&self, credentials: Credentials) -> Result<(), AuthError>;
146
147    /// Adds a bridge route.
148    async fn add_bridge_route(&self, descriptor: BridgeDescriptor) -> Result<(), MiscFatalError>;
149
150    /// Obtains the master summary of the network state.
151    async fn get_summary(&self) -> MasterSummary;
152
153    /// Obtains a list of bridges.
154    async fn get_bridges(&self, token: BlindToken, exit: SmolStr) -> Vec<BridgeDescriptor>;
155
156    /// Obtains a list of bridges, limited to sosistab2.
157    async fn get_bridges_v2(&self, token: BlindToken, exit: SmolStr) -> Vec<BridgeDescriptor>;
158
159    /// Obtains the Mizaru long-term key.
160    async fn get_mizaru_pk(&self, level: Level) -> mizaru::PublicKey;
161
162    /// Obtains a Mizaru epoch key.
163    async fn get_mizaru_epoch_key(&self, level: Level, epoch: u16) -> rsa::RSAPublicKey;
164
165    /// Obtains recent announcements, as a string containing an RSS feed.
166    async fn get_announcements(&self) -> String;
167
168    /// Reverse proxies requests to melnode
169    async fn reverse_proxy_melnode(&self, req: JrpcRequest) -> Result<JrpcResponse, RpcError>;
170
171    /// Adds a specific metric datapoint to the db
172    async fn add_metric(&self, session: i64, data: serde_json::Value)
173        -> Result<(), MiscFatalError>;
174
175    /// Retrieves user info
176    async fn get_user_info(&self, auth_req: Credentials) -> Result<UserInfoV2, AuthError>;
177}
178
179/// Authentication request
180#[serde_as]
181#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
182pub struct AuthRequest {
183    pub username: SmolStr,
184    pub password: SmolStr,
185    pub level: Level,
186    pub epoch: u16,
187    #[serde_as(as = "serde_with::base64::Base64")]
188    pub blinded_digest: Bytes,
189}
190
191/// Authentication request generic over authentication type
192#[serde_as]
193#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
194pub struct AuthRequestV2 {
195    pub credentials: Credentials,
196    pub level: Level,
197    pub epoch: u16,
198    #[serde_as(as = "serde_with::base64::Base64")]
199    pub blinded_digest: Bytes,
200}
201
202/// The different authentications methods available in AuthRequestV2
203#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
204pub enum Credentials {
205    Password {
206        username: SmolStr,
207        password: SmolStr,
208    },
209    Signature {
210        pubkey: Ed25519PK,
211        unix_secs: u64,
212        signature: Vec<u8>,
213    },
214}
215
216impl Credentials {
217    /// Signs a new keypair credential, valid for the next 10 minutes, given a secret key.
218    pub fn new_keypair(my_sk: &Ed25519SK) -> Self {
219        let unix_secs = SystemTime::now()
220            .duration_since(UNIX_EPOCH)
221            .unwrap()
222            .as_secs();
223        let to_sign = blake3::keyed_hash(PUBKEY_AUTH_COOKIE, &unix_secs.to_be_bytes());
224        Credentials::Signature {
225            pubkey: my_sk.to_public(),
226            unix_secs,
227            signature: my_sk.sign(to_sign.as_bytes()),
228        }
229    }
230}
231
232/// Authentication response
233#[serde_as]
234#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
235pub struct AuthResponse {
236    pub user_info: UserInfo,
237    #[serde_as(as = "serde_with::base64::Base64")]
238    pub blind_signature_bincode: Bytes,
239}
240
241/// Authentication response v2
242#[serde_as]
243#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
244pub struct AuthResponseV2 {
245    pub user_info: UserInfoV2,
246    #[serde_as(as = "serde_with::base64::Base64")]
247    pub blind_signature_bincode: Bytes,
248}
249
250/// Authentication error
251#[derive(Error, Debug, Clone, Serialize, Deserialize)]
252pub enum AuthError {
253    #[error("invalid credentials")]
254    InvalidCredentials,
255    #[error("too many requests")]
256    TooManyRequests,
257    #[error("level wrong")]
258    WrongLevel,
259    #[error("other error: {0}")]
260    Other(SmolStr),
261}
262
263/// Registration error
264#[derive(Error, Debug, Clone, Serialize, Deserialize)]
265pub enum RegisterError {
266    #[error("duplicate credentials")]
267    DuplicateCredentials,
268    #[error("other error: {0}")]
269    Other(SmolStr),
270}
271
272/// Information for a particular user
273#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
274pub struct UserInfo {
275    pub userid: i32,
276    pub username: SmolStr,
277    pub subscription: Option<SubscriptionInfo>,
278}
279
280/// Information for a particular user v2
281#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
282pub struct UserInfoV2 {
283    pub userid: i32,
284    pub subscription: Option<SubscriptionInfo>,
285}
286
287/// Information about a user's subscription
288#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
289pub struct SubscriptionInfo {
290    pub level: Level,
291    pub expires_unix: i64,
292}
293
294#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug, Hash)]
295pub enum Level {
296    Free,
297    Plus,
298}
299
300/// A "blind token" that is either valid or not.
301#[serde_as]
302#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, Hash)]
303pub struct BlindToken {
304    pub level: Level,
305    #[serde_as(as = "serde_with::base64::Base64")]
306    pub unblinded_digest: Bytes,
307    #[serde_as(as = "serde_with::base64::Base64")]
308    pub unblinded_signature_bincode: Bytes,
309
310    #[serde(default)]
311    pub version: Option<SmolStr>,
312}
313
314/// A captcha.
315#[serde_as]
316#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, Hash)]
317pub struct Captcha {
318    pub captcha_id: SmolStr,
319    #[serde_as(as = "serde_with::base64::Base64")]
320    pub png_data: Bytes,
321}
322
323/// A miscellaneous, "dynamically typed" fatal error
324#[derive(Error, Debug, Clone, Serialize, Deserialize)]
325pub enum MiscFatalError {
326    #[error("database error: {0}")]
327    Database(SmolStr),
328    #[error("backend network error: {0}")]
329    BadNet(SmolStr),
330    #[error("authentication error: {0}")]
331    Auth(AuthError),
332}
333
334/// Bridge descriptor
335#[serde_as]
336#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
337pub struct BridgeDescriptor {
338    pub is_direct: bool,
339    pub protocol: SmolStr,
340    pub endpoint: SocketAddr,
341    #[serde(rename = "sosistab_key")]
342    pub cookie: Bytes,
343    pub exit_hostname: SmolStr,
344    pub alloc_group: SmolStr,
345    pub update_time: u64,
346    pub exit_signature: Bytes,
347}
348
349/// Exit descriptor
350#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
351pub struct ExitDescriptor {
352    pub hostname: SmolStr,
353    pub signing_key: ed25519_dalek::PublicKey,
354    pub country_code: SmolStr,
355    pub city_code: SmolStr,
356    pub direct_routes: Vec<BridgeDescriptor>,
357    #[serde(rename = "legacy_direct_sosistab_pk")]
358    pub sosistab_e2e_pk: x25519_dalek::PublicKey,
359    pub allowed_levels: Vec<Level>,
360    pub load: f64,
361}
362
363/// Master summary.
364#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
365pub struct MasterSummary {
366    pub exits: Vec<ExitDescriptor>,
367    pub bad_countries: Vec<SmolStr>,
368}
369
370impl MasterSummary {
371    /// Gets a hash of the [`MasterSummary`].
372    /// This clears out dynamically changing fields like `load` and `direct_route` in each exit descriptor before hashing.
373    pub fn clean_hash(&self) -> blake3::Hash {
374        let mut exit_tree: BTreeMap<String, (Vec<u8>, Vec<u8>)> = BTreeMap::new();
375
376        for exit in &self.exits {
377            exit_tree.insert(
378                exit.hostname.clone().into(),
379                (
380                    exit.signing_key.as_bytes().to_vec(),
381                    exit.sosistab_e2e_pk.as_bytes().to_vec(),
382                ),
383            );
384        }
385
386        blake3::hash(&exit_tree.stdcode())
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn box_encryption() {
396        let test_string = b"hello world";
397        let alice_sk = x25519_dalek::StaticSecret::new(rand::thread_rng());
398        let alice_pk = x25519_dalek::PublicKey::from(&alice_sk);
399        let bob_sk = x25519_dalek::StaticSecret::new(rand::thread_rng());
400        let bob_pk = x25519_dalek::PublicKey::from(&bob_sk);
401        let encrypted = box_encrypt(test_string, alice_sk, bob_pk);
402        let (decrypted, purported_alice_pk) = box_decrypt(&encrypted, bob_sk).unwrap();
403        assert_eq!(test_string, &decrypted[..]);
404        assert_eq!(purported_alice_pk, alice_pk);
405    }
406
407    #[test]
408    fn pk_auth() {
409        let sk = Ed25519SK::generate();
410        let cred = Credentials::new_keypair(&sk);
411        match cred {
412            Credentials::Password { .. } => todo!(),
413            Credentials::Signature {
414                pubkey,
415                unix_secs,
416                signature,
417            } => {
418                assert!(verify_pk_auth(pubkey, unix_secs, &signature));
419            }
420        }
421    }
422}