diff --git a/Cargo.lock b/Cargo.lock index aec8c14da..2019d80ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3248,6 +3248,7 @@ dependencies = [ name = "mas-data-model" version = "0.12.0" dependencies = [ + "base64ct", "chrono", "crc", "mas-iana", diff --git a/Cargo.toml b/Cargo.toml index a19d9f881..e2c37d489 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,10 @@ version = "0.7.9" version = "0.9.6" features = ["cookie-private", "cookie-key-expansion", "typed-header"] +# Constant-time base64 +[workspace.dependencies.base64ct] +version = "1.6.0" + # Bytes [workspace.dependencies.bytes] version = "1.9.0" diff --git a/crates/data-model/Cargo.toml b/crates/data-model/Cargo.toml index c9f4a3a50..ee49a6c05 100644 --- a/crates/data-model/Cargo.toml +++ b/crates/data-model/Cargo.toml @@ -12,6 +12,7 @@ publish = false workspace = true [dependencies] +base64ct.workspace = true chrono.workspace = true thiserror.workspace = true serde.workspace = true diff --git a/crates/data-model/src/tokens.rs b/crates/data-model/src/tokens.rs index 9e94f7171..44fe8cc48 100644 --- a/crates/data-model/src/tokens.rs +++ b/crates/data-model/src/tokens.rs @@ -4,6 +4,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +use base64ct::{Base64Url, Encoding}; use chrono::{DateTime, Utc}; use crc::{Crc, CRC_32_ISO_HDLC}; use mas_iana::oauth::OAuthTokenTypeHint; @@ -294,7 +295,7 @@ impl TokenType { pub fn check(token: &str) -> Result { // these are legacy tokens imported from Synapse // we don't do any validation on them and continue as is - if token.starts_with("syt_") { + if token.starts_with("syt_") || is_likely_synapse_macaroon(token) { return Ok(TokenType::CompatAccessToken); } if token.starts_with("syr_") { @@ -344,6 +345,20 @@ impl PartialEq for TokenType { } } +/// Returns true if and only if a token looks like it may be a macaroon. +/// +/// Macaroons are a standard for tokens that support attenuation. +/// Synapse used them for old sessions and for guest sessions. +/// +/// We won't bother to decode them fully, but we can check to see if the first +/// constraint is the `location` constraint. +fn is_likely_synapse_macaroon(token: &str) -> bool { + let Ok(decoded) = Base64Url::decode_vec(token) else { + return false; + }; + decoded.get(4..13) == Some(b"location ") +} + const NUM: [u8; 62] = *b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; fn base62_encode(mut num: u32) -> String { @@ -420,6 +435,25 @@ mod tests { ); } + #[test] + fn test_is_likely_synapse_macaroon() { + // This is just the prefix of a Synapse macaroon, but it's enough to make the + // sniffing work + assert!(is_likely_synapse_macaroon( + "MDAxYmxvY2F0aW9uIGxpYnJlcHVzaC5uZXQKMDAx" + )); + + // Whilst this is a macaroon, it's not a Synapse macaroon + assert!(! is_likely_synapse_macaroon("MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVzZWQgb3VyIHNlY3JldCBrZXkKMDAyZnNpZ25hdHVyZSDj2eApCFJsTAA5rhURQRXZf91ovyujebNCqvD2F9BVLwo")); + + // None of these are macaroons + assert!(!is_likely_synapse_macaroon( + "eyJARTOhearotnaeisahtoarsnhiasra.arsohenaor.oarnsteao" + )); + assert!(!is_likely_synapse_macaroon("....")); + assert!(!is_likely_synapse_macaroon("aaa")); + } + #[test] fn test_generate_and_check() { const COUNT: usize = 500; // Generate 500 of each token type diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index 334ad6246..a2a129c03 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -68,7 +68,7 @@ pbkdf2 = { version = "0.12.2", features = [ zeroize = "1.8.1" # Various data types and utilities -base64ct = "1.6.0" +base64ct.workspace = true camino.workspace = true chrono.workspace = true elliptic-curve.workspace = true