tuwunel_api/client/admin/
register.rs1use 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
15pub(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: ®ister::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 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 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 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()); }
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}