Skip to content

Commit

Permalink
Add cable-localhost-tunnel feature.
Browse files Browse the repository at this point in the history
This makes the library connect to a tunnel server running at
`ws://localhost:8080`, rather than the proper tunnel server URL.
  • Loading branch information
micolous committed Mar 29, 2023
1 parent 37cd8d3 commit f3fa8df
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 40 deletions.
5 changes: 5 additions & 0 deletions webauthn-authenticator-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ bluetooth = ["btleplug"]

# caBLE / hybrid authenticator
cable = ["bluetooth", "hex", "tokio", "tokio-tungstenite", "qrcode"]

# Always connect to a caBLE tunnel server at ws://localhost:8080, rather than
# the true tunnel server URL. Only useful for testing.
cable-localhost-tunnel = []

nfc = ["pcsc"]
usb = ["hidapi"]
win10 = ["windows"]
Expand Down
12 changes: 4 additions & 8 deletions webauthn-authenticator-rs/src/cable/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use openssl::{
sign::Signer,
};
use std::mem::size_of;
use tokio_tungstenite::tungstenite::http::Uri;
use tokio_tungstenite::tungstenite::http::{uri::Builder, Uri};

use crate::{
cable::{btle::*, handshake::*, tunnel::get_domain, CableRequestType, Psk},
Expand Down Expand Up @@ -176,9 +176,7 @@ impl Discovery {
get_domain(domain_id)
.and_then(|domain| {
let tunnel_id = hex::encode_upper(self.derive_tunnel_id().ok()?);
Uri::builder()
.scheme("wss")
.authority(domain)
domain
.path_and_query(format!("/cable/new/{}", tunnel_id))
.build()
.ok()
Expand Down Expand Up @@ -339,7 +337,7 @@ impl Eid {
}

/// Gets the tunnel server domain for this [Eid].
fn get_domain(&self) -> Option<String> {
fn get_domain(&self) -> Option<Builder> {
get_domain(self.tunnel_server_id)
}

Expand All @@ -353,9 +351,7 @@ impl Eid {
let routing_id = hex::encode_upper(self.routing_id);
let tunnel_id = hex::encode_upper(tunnel_id);

Uri::builder()
.scheme("wss")
.authority(domain)
domain
.path_and_query(format!("/cable/connect/{}/{}", routing_id, tunnel_id))
.build()
.ok()
Expand Down
4 changes: 4 additions & 0 deletions webauthn-authenticator-rs/src/cable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
//! operated by Apple (`wss://cable.auth.com`) and Google
//! (`wss://cable.ua5v.com`), and an algorithm to generate tunnel server
//! domain names from a hash to allow for future expansion.
//!
//! You can also build the library with the `cable-localhost-tunnel` feature,
//! which causes it to always connect to `ws://localhost:8080` (over HTTP)
//! instead of the *proper* tunnel server over HTTPS.
//!
//! This module implements both the [initator][connect_cable_authenticator] and
//! [authenticator][share_cable_authenticator] side of caBLE, provided
Expand Down
92 changes: 60 additions & 32 deletions webauthn-authenticator-rs/src/cable/tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ use tokio_tungstenite::{
connect_async,
tungstenite::{client::IntoClientRequest, http::HeaderValue, Message},
};
use tokio_tungstenite::{tungstenite::http::Uri, MaybeTlsStream, WebSocketStream};
use tokio_tungstenite::{
tungstenite::http::{uri::Builder, Uri},
MaybeTlsStream, WebSocketStream,
};
use webauthn_rs_proto::AuthenticatorTransport;

use crate::{
Expand Down Expand Up @@ -59,37 +62,48 @@ const TUNNEL_SERVER_ID_OFFSET: usize = TUNNEL_SERVER_SALT.len() - 3;
const TUNNEL_SERVER_TLDS: [&str; 4] = [".com", ".org", ".net", ".info"];
const BASE32_CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";

#[cfg(feature = "cable-localhost-tunnel")]
const CABLE_LOCAL_TUNNEL: Option<&str> = Some("localhost:8080");
#[cfg(not(feature = "cable-localhost-tunnel"))]
const CABLE_LOCAL_TUNNEL: Option<&str> = None;

/// Decodes a `domain_id` into an actual domain name.
///
/// See Chromium's `tunnelserver::DecodeDomain`.
pub fn get_domain(domain_id: u16) -> Option<String> {
if domain_id < 256 {
return match ASSIGNED_DOMAINS.get(usize::from(domain_id)) {
Some(d) => Some(d.to_string()),
pub fn get_domain(domain_id: u16) -> Option<Builder> {
let domain = if domain_id < 256 {
match ASSIGNED_DOMAINS.get(usize::from(domain_id)) {
Some(d) => d.to_string(),
None => {
warn!("Invalid tunnel server ID {:04x}", domain_id);
None
return None;
}
};
}

let mut buf = TUNNEL_SERVER_SALT.to_vec();
buf[TUNNEL_SERVER_ID_OFFSET..TUNNEL_SERVER_ID_OFFSET + 2]
.copy_from_slice(&domain_id.to_le_bytes());
let digest = compute_sha256(&buf);
let mut result = u64::from_le_bytes(digest[..8].try_into().ok()?);

let tld = TUNNEL_SERVER_TLDS[(result & 3) as usize];
}
} else {
let mut buf = TUNNEL_SERVER_SALT.to_vec();
buf[TUNNEL_SERVER_ID_OFFSET..TUNNEL_SERVER_ID_OFFSET + 2]
.copy_from_slice(&domain_id.to_le_bytes());
let digest = compute_sha256(&buf);
let mut result = u64::from_le_bytes(digest[..8].try_into().ok()?);

let tld = TUNNEL_SERVER_TLDS[(result & 3) as usize];

let mut domain = String::from("cable.");
result >>= 2;
while result != 0 {
domain.push(char::from_u32(BASE32_CHARS[(result & 31) as usize].into())?);
result >>= 5;
}
domain.push_str(tld);
domain
};

let mut o = String::from("cable.");
result >>= 2;
while result != 0 {
o.push(char::from_u32(BASE32_CHARS[(result & 31) as usize].into())?);
result >>= 5;
if let Some(t) = CABLE_LOCAL_TUNNEL {
warn!("Using ws://{t} as caBLE tunnel server rather than wss://{domain}");
return Some(Uri::builder().scheme("ws").authority(t));
}
o.push_str(tld);

Some(o)
Some(Uri::builder().scheme("wss").authority(domain))
}

/// Websocket tunnel to a caBLE authenticator.
Expand Down Expand Up @@ -124,7 +138,7 @@ impl Tunnel {

trace!(?request);
let (stream, response) = connect_async(request).await.map_err(|e| {
error!("websocket error: {:?}", e);
error!("websocket error: {uri}: {e:?}");
WebauthnCError::Internal
})?;

Expand Down Expand Up @@ -459,26 +473,40 @@ mod test {

#[test]
fn check_known_tunnel_server_domains() {
assert_eq!(get_domain(0), Some(String::from("cable.ua5v.com")));
assert_eq!(get_domain(1), Some(String::from("cable.auth.com")));
assert_eq!(
get_domain(266),
Some(String::from("cable.wufkweyy3uaxb.com"))
get_domain(0).unwrap().path_and_query("/").build().unwrap(),
"wss://cable.ua5v.com/"
);
assert_eq!(
get_domain(1).unwrap().path_and_query("/").build().unwrap(),
"wss://cable.auth.com/"
);
assert_eq!(
get_domain(266)
.unwrap()
.path_and_query("/")
.build()
.unwrap(),
"wss://cable.wufkweyy3uaxb.com/"
);

assert_eq!(get_domain(255), None);
assert!(get_domain(255).is_none());

// 🦀 = \u{1f980}
assert_eq!(
get_domain(0xf980),
Some(String::from("cable.my4kstlhndi4c.net"))
get_domain(0xf980)
.unwrap()
.path_and_query("/")
.build()
.unwrap(),
"wss://cable.my4kstlhndi4c.net"
)
}

#[test]
fn check_all_hashed_tunnel_servers() {
for x in 256..u16::MAX {
assert_ne!(get_domain(x), None);
assert!(get_domain(x).is_some());
}
}
}
3 changes: 3 additions & 0 deletions webauthn-authenticator-rs/src/stubs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ pub mod tokio_tungstenite {
pub mod tungstenite {
pub mod http {
pub struct Uri {}
pub mod uri {
pub struct Builder {}
}
}
}
pub struct MaybeTlsStream<T> {}
Expand Down

0 comments on commit f3fa8df

Please sign in to comment.