1use std::result::Result as StdResult;
2
3use axum::extract::State;
4use hmac::{Hmac, Mac};
5use ruma::{OwnedUserId, UserId};
6use sha1::Sha1;
7use synapse_admin_api::register_users::shared_secret_register as register;
8use tuwunel_core::{Err, Result, err};
9use tuwunel_service::{
10 profile::check_member_displayname,
11 users::{Register, device::generate_refresh_token},
12};
13
14use crate::{ClientIp, Ruma};
15
16type HmacSha1 = Hmac<Sha1>;
17
18pub(crate) async fn admin_register_route(
24 State(services): State<crate::State>,
25 ClientIp(client): ClientIp,
26 body: Ruma<register::v1::Request>,
27) -> Result<register::v1::Response> {
28 let Some(shared_secret) = services.admin.register_shared_secret() else {
29 return Err!(Request(Unknown("Shared-secret registration is not enabled")));
30 };
31
32 if !services.admin.consume_register_nonce(&body.nonce) {
33 return Err!(Request(InvalidParam("Unrecognised or expired nonce")));
34 }
35
36 check_field("Username", &body.username)?;
37 check_field("Password", &body.password)?;
38
39 verify_mac(&shared_secret, &body)
40 .map_err(|()| err!(Request(Forbidden("HMAC check failed"))))?;
41
42 let user_id = resolve_local_user_id(services, &body.username)?;
43
44 if services.users.exists(&user_id).await {
45 return Err!(Request(UserInUse("User ID is not available")));
46 }
47
48 let device_id = body
49 .device_id
50 .as_deref()
51 .filter(|_| !body.inhibit_login);
52
53 check_member_displayname(body.displayname.as_deref())?;
55 services
56 .users
57 .check_device_id(&user_id, device_id)
58 .await?;
59
60 services
61 .users
62 .full_register(Register {
63 user_id: Some(&user_id),
64 password: Some(&body.password),
65 displayname: body.displayname.as_deref(),
66 grant_first_user_admin: false,
67 ..Default::default()
68 })
69 .await?;
70
71 if body.admin {
72 services.admin.make_user_admin(&user_id).await?;
73 }
74
75 let home_server = services.globals.server_name().to_owned();
76
77 if body.inhibit_login {
78 return Ok(register::v1::Response::new(user_id, home_server));
79 }
80
81 let (access_token, expires_in) = services
82 .users
83 .generate_access_token(body.refresh_token);
84
85 let refresh_token = expires_in.is_some().then(generate_refresh_token);
86
87 let device_id = services
88 .users
89 .create_device(
90 &user_id,
91 device_id,
92 (Some(&access_token), expires_in),
93 refresh_token.as_deref(),
94 body.initial_device_display_name.as_deref(),
95 Some(client),
96 )
97 .await?;
98
99 Ok(register::v1::Response {
100 user_id,
101 home_server,
102 access_token: Some(access_token),
103 device_id: Some(device_id),
104 refresh_token,
105 expires_in,
106 })
107}
108
109fn check_field(label: &str, value: &str) -> Result<()> {
110 if value.is_empty() {
111 return Err!(Request(InvalidParam("{label} must not be empty")));
112 }
113
114 if value.len() > 512 {
115 return Err!(Request(InvalidParam("{label} must not exceed 512 bytes")));
116 }
117
118 if value.as_bytes().contains(&0) {
119 return Err!(Request(InvalidParam("{label} must not contain a null byte")));
120 }
121
122 Ok(())
123}
124
125fn verify_mac(secret: &str, req: ®ister::v1::Request) -> StdResult<(), ()> {
126 let admin = if req.admin {
127 b"admin".as_slice()
128 } else {
129 b"notadmin".as_slice()
130 };
131
132 let parts = [req.nonce.as_bytes(), req.username.as_bytes(), req.password.as_bytes(), admin]
133 .into_iter()
134 .chain(req.user_type.as_deref().map(str::as_bytes));
135
136 let mut mac = HmacSha1::new_from_slice(secret.as_bytes()).expect("HMAC accepts any key size");
137 for (i, part) in parts.enumerate() {
138 if i > 0 {
139 mac.update(b"\0");
140 }
141 mac.update(part);
142 }
143
144 let expected = decode_hex(&req.mac).ok_or(())?;
145
146 mac.verify_slice(&expected).map_err(|_| ())
147}
148
149fn decode_hex(s: &str) -> Option<Vec<u8>> {
150 s.len().is_multiple_of(2).then_some(())?;
151
152 s.as_bytes()
153 .as_chunks::<2>()
154 .0
155 .iter()
156 .map(|c| Some((hex_nibble(c[0])? << 4) | hex_nibble(c[1])?))
157 .collect()
158}
159
160const fn hex_nibble(b: u8) -> Option<u8> {
161 match b {
162 | b'0'..=b'9' => Some(b.wrapping_sub(b'0')),
163 | b'a'..=b'f' => Some(b.wrapping_sub(b'a').wrapping_add(10)),
164 | b'A'..=b'F' => Some(b.wrapping_sub(b'A').wrapping_add(10)),
165 | _ => None,
166 }
167}
168
169fn resolve_local_user_id(services: crate::State, username: &str) -> Result<OwnedUserId> {
170 let server_name = services.globals.server_name();
171 let username = username.to_lowercase();
172
173 let user_id = match username.starts_with('@') {
174 | true => UserId::parse(&username),
175 | false => UserId::parse_with_server_name(&username, server_name),
176 }
177 .map_err(|_| err!(Request(InvalidParam("Invalid user id"))))?;
178
179 user_id.validate_strict().map_err(|_| {
180 err!(Request(InvalidUsername("Username contains disallowed characters or spaces")))
181 })?;
182
183 if user_id.server_name() != server_name {
184 return Err!(Request(InvalidParam("User is not local to this server")));
185 }
186
187 Ok(user_id)
188}
189
190#[cfg(test)]
191mod tests {
192 use super::{decode_hex, register, verify_mac};
193
194 const SECRET: &str = "shared-secret-12345";
195 const NONCE: &str = "thenonce0123456789abcdef";
196 const USER: &str = "alice";
197 const PASS: &str = "p4ssw0rd!";
198
199 const MAC_NOTADMIN: &str = "44e0ec50d52aaa4029731dfcfe7e22123fa4c53e";
202 const MAC_ADMIN: &str = "7774d6962a728b48ca7cd41e99a5149a93ff1ec5";
203 const MAC_ADMIN_BOT: &str = "10f12d86c7210410ee777edadf655033b8f36008";
204
205 fn req(admin: bool, user_type: Option<&str>, mac: &str) -> register::v1::Request {
206 register::v1::Request {
207 nonce: NONCE.into(),
208 username: USER.into(),
209 displayname: None,
210 password: PASS.into(),
211 admin,
212 user_type: user_type.map(Into::into),
213 mac: mac.into(),
214 inhibit_login: false,
215 refresh_token: false,
216 device_id: None,
217 initial_device_display_name: None,
218 }
219 }
220
221 #[test]
222 fn verify_mac_accepts_notadmin_reference_vector() {
223 assert!(verify_mac(SECRET, &req(false, None, MAC_NOTADMIN)).is_ok());
224 }
225
226 #[test]
227 fn verify_mac_accepts_admin_reference_vector() {
228 assert!(verify_mac(SECRET, &req(true, None, MAC_ADMIN)).is_ok());
229 }
230
231 #[test]
232 fn verify_mac_accepts_admin_with_user_type() {
233 assert!(verify_mac(SECRET, &req(true, Some("bot"), MAC_ADMIN_BOT)).is_ok());
234 }
235
236 #[test]
237 fn verify_mac_admin_flag_is_part_of_the_mac() {
238 assert!(verify_mac(SECRET, &req(true, None, MAC_NOTADMIN)).is_err());
240 assert!(verify_mac(SECRET, &req(false, None, MAC_ADMIN)).is_err());
241 }
242
243 #[test]
244 fn verify_mac_user_type_is_part_of_the_mac() {
245 assert!(verify_mac(SECRET, &req(true, None, MAC_ADMIN_BOT)).is_err());
247 }
248
249 #[test]
250 fn verify_mac_rejects_wrong_secret() {
251 assert!(verify_mac("other-secret", &req(false, None, MAC_NOTADMIN)).is_err());
252 }
253
254 #[test]
255 fn verify_mac_rejects_malformed_hex() {
256 assert!(verify_mac(SECRET, &req(false, None, "not-hex")).is_err());
257 assert!(verify_mac(SECRET, &req(false, None, "abc")).is_err()); }
259
260 #[test]
261 fn verify_mac_accepts_uppercase_hex() {
262 assert!(verify_mac(SECRET, &req(false, None, &MAC_NOTADMIN.to_uppercase())).is_ok());
263 }
264
265 #[test]
266 fn decode_hex_roundtrips() {
267 assert_eq!(decode_hex("00ff10ab"), Some(vec![0x00, 0xFF, 0x10, 0xAB]));
268 assert_eq!(decode_hex(""), Some(vec![]));
269 assert_eq!(decode_hex("0"), None);
270 assert_eq!(decode_hex("0g"), None);
271 }
272}