Skip to main content

tuwunel_api/client/admin/
register.rs

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