Skip to main content

tuwunel_api_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::{
10	profile::check_member_displayname,
11	users::{Register, device::generate_refresh_token},
12};
13
14use crate::{ClientIp, Ruma};
15
16type HmacSha1 = Hmac<Sha1>;
17
18/// # `POST /_synapse/admin/v1/register`
19///
20/// Out-of-band account creation authenticated by HMAC over the homeserver's
21/// registration shared secret. Bypasses UIAA. Mirrors Synapse's endpoint of
22/// the same name.
23pub(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	// Checked before anything is stored, so a refused ID or name leaves the user ID free.
54	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: &register::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	// Reference vectors from Python's hmac.new(SECRET, digestmod=sha1) over
200	// NONCE\0USER\0PASS\0(admin|notadmin)[\0user_type].
201	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		// the notadmin MAC must not validate when admin=true and vice versa.
239		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		// omitting user_type from the request must not validate the with-user_type MAC.
246		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()); // odd length
258	}
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}