1use crate::{
4 decode_public_key, decode_signature, encode_newline_data,
5 error::SqrlError,
6 get_or_error, parse_newline_data, parse_query_data,
7 server_response::{ServerResponse, TIFValue},
8 ProtocolVersion, Result, SqrlUrl, PROTOCOL_VERSIONS,
9};
10use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
11use ed25519_dalek::{Signature, VerifyingKey};
12use std::{collections::HashMap, convert::TryFrom, fmt, result, str::FromStr};
13
14const CLIENT_PARAMETERS_KEY: &str = "client";
16const SERVER_DATA_KEY: &str = "server";
17const IDENTITY_SIGNATURE_KEY: &str = "ids";
18const PREVIOUS_IDENTITY_SIGNATURE_KEY: &str = "pids";
19const UNLOCK_REQUEST_SIGNATURE_KEY: &str = "urs";
20
21const PROTOCOL_VERSION_KEY: &str = "ver";
23const COMMAND_KEY: &str = "cmd";
24const IDENTITY_KEY_KEY: &str = "idk";
25const OPTIONS_KEY: &str = "opt";
26const BUTTON_KEY: &str = "btn";
27const PREVIOUS_IDENTITY_KEY_KEY: &str = "pidk";
28const INDEX_SECRET_KEY: &str = "ins";
29const PREVIOUS_INDEX_SECRET_KEY: &str = "pins";
30const SERVER_UNLOCK_KEY_KEY: &str = "suk";
31const VERIFY_UNLOCK_KEY_KEY: &str = "vuk";
32
33pub struct ClientRequest {
35 pub client_params: ClientParameters,
37 pub server_data: ServerData,
39 pub identity_signature: Signature,
41 pub previous_identity_signature: Option<Signature>,
43 pub unlock_request_signature: Option<String>,
45}
46
47impl ClientRequest {
48 pub fn new(
50 client_params: ClientParameters,
51 server_data: ServerData,
52 identity_signature: Signature,
53 ) -> Self {
54 ClientRequest {
55 client_params,
56 server_data,
57 identity_signature,
58 previous_identity_signature: None,
59 unlock_request_signature: None,
60 }
61 }
62
63 pub fn from_query_string(query_string: &str) -> Result<Self> {
65 let map = parse_query_data(query_string)?;
66 let client_parameters_string = get_or_error(
67 &map,
68 CLIENT_PARAMETERS_KEY,
69 "Invalid client request: No client parameters",
70 )?;
71 let client_params = ClientParameters::from_base64(&client_parameters_string)?;
72 let server_string = get_or_error(
73 &map,
74 SERVER_DATA_KEY,
75 "Invalid client request: No server value",
76 )?;
77 let server_data = ServerData::from_base64(&server_string)?;
78 let ids_string = get_or_error(
79 &map,
80 IDENTITY_SIGNATURE_KEY,
81 "Invalid client request: No ids value",
82 )?;
83 let identity_signature = decode_signature(&ids_string)?;
84 let previous_identity_signature = match map.get(PREVIOUS_IDENTITY_SIGNATURE_KEY) {
85 Some(x) => Some(decode_signature(x)?),
86 None => None,
87 };
88
89 let unlock_request_signature = map.get(UNLOCK_REQUEST_SIGNATURE_KEY).map(|x| x.to_string());
90
91 Ok(ClientRequest {
92 client_params,
93 server_data,
94 identity_signature,
95 previous_identity_signature,
96 unlock_request_signature,
97 })
98 }
99
100 pub fn to_query_string(&self) -> String {
102 let mut result = format!(
103 "{}={}",
104 CLIENT_PARAMETERS_KEY,
105 self.client_params.to_base64()
106 );
107 result += &format!("&{}={}", SERVER_DATA_KEY, self.server_data);
108 result += &format!(
109 "&{}={}",
110 IDENTITY_SIGNATURE_KEY,
111 BASE64_URL_SAFE_NO_PAD.encode(self.identity_signature.to_bytes())
112 );
113
114 if let Some(pids) = &self.previous_identity_signature {
115 result += &format!(
116 "&{}={}",
117 PREVIOUS_IDENTITY_SIGNATURE_KEY,
118 BASE64_URL_SAFE_NO_PAD.encode(pids.to_bytes())
119 );
120 }
121 if let Some(urs) = &self.unlock_request_signature {
122 result += &format!(
123 "&{}={}",
124 UNLOCK_REQUEST_SIGNATURE_KEY,
125 BASE64_URL_SAFE_NO_PAD.encode(urs)
126 );
127 }
128
129 result
130 }
131
132 pub fn get_signed_string(&self) -> String {
134 format!(
135 "{}{}",
136 self.client_params.to_base64(),
137 &self.server_data.to_base64()
138 )
139 }
140
141 pub fn validate(&self) -> Result<()> {
143 self.client_params.validate()?;
144
145 if self.previous_identity_signature.is_some()
147 && self.client_params.previous_identity_key.is_none()
148 {
149 return Err(SqrlError::new(
150 "Previous identity signature set, but no previous identity key set".to_owned(),
151 ));
152 } else if self.previous_identity_signature.is_none()
153 && self.client_params.previous_identity_key.is_some()
154 {
155 return Err(SqrlError::new(
156 "Previous identity key set, but no previous identity signature".to_owned(),
157 ));
158 }
159
160 if (self.client_params.command == ClientCommand::Enable
162 || self.client_params.command == ClientCommand::Remove)
163 && self.unlock_request_signature.is_none()
164 {
165 return Err(SqrlError::new(
166 "When attempting to enable identity, unlock request signature (urs) must be set"
167 .to_owned(),
168 ));
169 }
170
171 match &self.server_data {
172 ServerData::ServerResponse {
173 server_response, ..
174 } if !server_response
175 .transaction_indication_flags
176 .contains(&TIFValue::CurrentIdMatch) =>
177 {
178 if self.client_params.server_unlock_key.is_none() {
179 return Err(SqrlError::new("If attempting to re-enable identity (cmd=enable), must include server unlock key (suk)".to_owned()));
180 } else if self.client_params.verify_unlock_key.is_none() {
181 return Err(SqrlError::new("If attempting to re-enable identity (cmd=enable), must include verify unlock key (vuk)".to_owned()));
182 }
183 }
184 _ => (),
185 }
186
187 Ok(())
188 }
189}
190
191#[derive(Debug, PartialEq)]
193pub struct ClientParameters {
194 pub protocol_version: ProtocolVersion,
196 pub command: ClientCommand,
198 pub identity_key: VerifyingKey,
200 pub options: Option<Vec<ClientOption>>,
202 pub button: Option<u8>,
204 pub previous_identity_key: Option<VerifyingKey>,
206 pub index_secret: Option<String>,
208 pub previous_index_secret: Option<String>,
210 pub server_unlock_key: Option<String>,
212 pub verify_unlock_key: Option<String>,
214}
215
216impl ClientParameters {
217 pub fn new(command: ClientCommand, identity_key: VerifyingKey) -> ClientParameters {
219 ClientParameters {
220 protocol_version: ProtocolVersion::new(PROTOCOL_VERSIONS).unwrap(),
221 command,
222 identity_key,
223 options: None,
224 button: None,
225 previous_identity_key: None,
226 index_secret: None,
227 previous_index_secret: None,
228 server_unlock_key: None,
229 verify_unlock_key: None,
230 }
231 }
232
233 pub fn from_base64(base64_string: &str) -> Result<Self> {
235 let query_string = String::from_utf8(BASE64_URL_SAFE_NO_PAD.decode(base64_string)?)?;
236 Self::from_str(&query_string)
237 }
238
239 pub fn to_base64(&self) -> String {
241 BASE64_URL_SAFE_NO_PAD.encode(self.to_string().as_bytes())
242 }
243
244 pub fn validate(&self) -> Result<()> {
246 Ok(())
247 }
248}
249
250impl fmt::Display for ClientParameters {
251 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
252 let mut map = HashMap::<&str, &str>::new();
253 let protocol = self.protocol_version.to_string();
254 map.insert(PROTOCOL_VERSION_KEY, &protocol);
255 let command = self.command.to_string();
256 map.insert(COMMAND_KEY, &command);
257
258 let identity_key = BASE64_URL_SAFE_NO_PAD.encode(self.identity_key.as_bytes());
259 map.insert(IDENTITY_KEY_KEY, &identity_key);
260
261 let options_string: String;
262 if let Some(options) = &self.options {
263 options_string = ClientOption::to_option_string(options);
264 map.insert(OPTIONS_KEY, &options_string);
265 }
266 let button_string: String;
267 if let Some(button) = &self.button {
268 button_string = button.to_string();
269 map.insert(BUTTON_KEY, &button_string);
270 }
271 let previous_identity_key_string: String;
272 if let Some(previous_identity_key) = &self.previous_identity_key {
273 previous_identity_key_string =
274 BASE64_URL_SAFE_NO_PAD.encode(previous_identity_key.as_bytes());
275 map.insert(PREVIOUS_IDENTITY_KEY_KEY, &previous_identity_key_string);
276 }
277 if let Some(index_secret) = &self.index_secret {
278 map.insert(INDEX_SECRET_KEY, index_secret);
279 }
280 if let Some(previous_index_secret) = &self.previous_index_secret {
281 map.insert(PREVIOUS_INDEX_SECRET_KEY, previous_index_secret);
282 }
283 if let Some(server_unlock_key) = &self.server_unlock_key {
284 map.insert(SERVER_UNLOCK_KEY_KEY, server_unlock_key);
285 }
286 if let Some(verify_unlock_key) = &self.verify_unlock_key {
287 map.insert(VERIFY_UNLOCK_KEY_KEY, verify_unlock_key);
288 }
289
290 write!(f, "{}", &encode_newline_data(&map))
291 }
292}
293
294impl FromStr for ClientParameters {
295 type Err = SqrlError;
296
297 fn from_str(s: &str) -> result::Result<Self, Self::Err> {
298 let map = parse_newline_data(s)?;
299 let ver_string = get_or_error(
301 &map,
302 PROTOCOL_VERSION_KEY,
303 "Invalid client request: No version number",
304 )?;
305 let protocol_version = ProtocolVersion::new(&ver_string)?;
306
307 let cmd_string = get_or_error(&map, COMMAND_KEY, "Invalid client request: No cmd value")?;
308 let command = ClientCommand::from(cmd_string);
309 let idk_string = get_or_error(
310 &map,
311 IDENTITY_KEY_KEY,
312 "Invalid client request: No idk value",
313 )?;
314 let identity_key = decode_public_key(&idk_string)?;
315
316 let button = match map.get(BUTTON_KEY) {
317 Some(s) => match s.parse::<u8>() {
318 Ok(b) => Some(b),
319 Err(_) => {
320 return Err(SqrlError::new(format!(
321 "Invalid client request: Unable to parse btn {}",
322 s
323 )))
324 }
325 },
326 None => None,
327 };
328
329 let previous_identity_key = match map.get(PREVIOUS_IDENTITY_KEY_KEY) {
330 Some(x) => Some(decode_public_key(x)?),
331 None => None,
332 };
333
334 let options = match map.get(OPTIONS_KEY) {
335 Some(x) => Some(ClientOption::from_option_string(x)?),
336 None => None,
337 };
338
339 let index_secret = map.get(INDEX_SECRET_KEY).map(|x| x.to_string());
340 let previous_index_secret = map.get(PREVIOUS_INDEX_SECRET_KEY).map(|x| x.to_string());
341 let server_unlock_key = map.get(SERVER_UNLOCK_KEY_KEY).map(|x| x.to_string());
342 let verify_unlock_key = map.get(VERIFY_UNLOCK_KEY_KEY).map(|x| x.to_string());
343
344 Ok(ClientParameters {
345 protocol_version,
346 command,
347 identity_key,
348 options,
349 button,
350 previous_identity_key,
351 index_secret,
352 previous_index_secret,
353 server_unlock_key,
354 verify_unlock_key,
355 })
356 }
357}
358
359#[derive(Debug, PartialEq)]
361pub enum ClientCommand {
362 Query,
364 Ident,
366 Disable,
368 Enable,
370 Remove,
372}
373
374impl fmt::Display for ClientCommand {
375 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
376 match self {
377 ClientCommand::Query => write!(f, "query"),
378 ClientCommand::Ident => write!(f, "ident"),
379 ClientCommand::Disable => write!(f, "disable"),
380 ClientCommand::Enable => write!(f, "enable"),
381 ClientCommand::Remove => write!(f, "remove"),
382 }
383 }
384}
385
386impl From<String> for ClientCommand {
387 fn from(value: String) -> Self {
388 match value.as_str() {
389 "query" => ClientCommand::Query,
390 "ident" => ClientCommand::Ident,
391 "disable" => ClientCommand::Disable,
392 "enable" => ClientCommand::Enable,
393 "remove" => ClientCommand::Remove,
394 _ => panic!("Not this!"),
395 }
396 }
397}
398
399#[derive(Debug, PartialEq)]
401pub enum ClientOption {
402 NoIPTest,
405 SQRLOnly,
407 Hardlock,
410 ClientProvidedSession,
413 ServerUnlockKey,
416}
417
418impl ClientOption {
419 fn from_option_string(opt: &str) -> Result<Vec<Self>> {
420 let mut options: Vec<ClientOption> = Vec::new();
421 for option in opt.split('~') {
422 options.push(ClientOption::try_from(option)?)
423 }
424
425 Ok(options)
426 }
427
428 fn to_option_string(opt: &Vec<Self>) -> String {
429 let mut options = "".to_owned();
430 for option in opt {
431 if options.is_empty() {
432 options += &format!("{}", option);
433 } else {
434 options += &format!("~{}", option);
435 }
436 }
437
438 options
439 }
440}
441
442impl fmt::Display for ClientOption {
443 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444 match self {
445 ClientOption::NoIPTest => write!(f, "noiptest"),
446 ClientOption::SQRLOnly => write!(f, "sqrlonly"),
447 ClientOption::Hardlock => write!(f, "hardlock"),
448 ClientOption::ClientProvidedSession => write!(f, "cps"),
449 ClientOption::ServerUnlockKey => write!(f, "suk"),
450 }
451 }
452}
453
454impl TryFrom<&str> for ClientOption {
455 type Error = SqrlError;
456
457 fn try_from(value: &str) -> Result<Self> {
458 match value {
459 "noiptest" => Ok(ClientOption::NoIPTest),
460 "sqrlonly" => Ok(ClientOption::SQRLOnly),
461 "hardlock" => Ok(ClientOption::Hardlock),
462 "cps" => Ok(ClientOption::ClientProvidedSession),
463 "suk" => Ok(ClientOption::ServerUnlockKey),
464 _ => Err(SqrlError::new(format!("Invalid client option {}", value))),
465 }
466 }
467}
468
469#[derive(Debug, PartialEq)]
472pub enum ServerData {
473 Url {
476 url: SqrlUrl,
478 },
479 ServerResponse {
482 server_response: ServerResponse,
484 original_response: String,
486 },
487}
488
489impl ServerData {
490 pub fn from_base64(base64_string: &str) -> Result<Self> {
492 let data = String::from_utf8(BASE64_URL_SAFE_NO_PAD.decode(base64_string)?)?;
493 if let Ok(parsed) = SqrlUrl::parse(&data) {
494 return Ok(ServerData::Url { url: parsed });
495 }
496
497 match ServerResponse::from_str(&data) {
498 Ok(server_response) => Ok(ServerData::ServerResponse {
499 server_response,
500 original_response: base64_string.to_owned(),
501 }),
502 Err(_) => Err(SqrlError::new(format!("Invalid server data: {}", &data))),
503 }
504 }
505
506 pub fn to_base64(&self) -> String {
508 match self {
509 ServerData::Url { url } => BASE64_URL_SAFE_NO_PAD.encode(url.to_string().as_bytes()),
510 ServerData::ServerResponse {
511 original_response, ..
512 } => original_response.clone(),
513 }
514 }
515}
516
517impl fmt::Display for ServerData {
518 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
519 match self {
520 ServerData::Url { url } => {
521 write!(f, "{}", url)
522 }
523 ServerData::ServerResponse {
524 original_response, ..
525 } => {
526 write!(f, "{}", &original_response)
527 }
528 }
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 const TEST_CLIENT_REQUEST: &str = "client=dmVyPTENCmNtZD1xdWVyeQ0KaWRrPWlnZ2N1X2UtdFdxM3NvZ2FhMmFBRENzeFJaRUQ5b245SDcxNlRBeVBSMHcNCnBpZGs9RTZRczJnWDdXLVB3aTlZM0tBbWJrdVlqTFNXWEN0S3lCY3ltV2xvSEF1bw0Kb3B0PWNwc35zdWsNCg&server=c3FybDovL3Nxcmwuc3RldmUuY29tL2NsaS5zcXJsP3g9MSZudXQ9ZTd3ZTZ3Q3RvU3hsJmNhbj1hSFIwY0hNNkx5OXNiMk5oYkdodmMzUXZaR1Z0Ynk1MFpYTjA&ids=hcXWTPx3EgP9R_AjtoCIrie_YgZxVD72nd5_pjMOnhUEYmhdjLUYs3jjcJT_GQuzNKXyAwY1ns1R6QJn1YKzCA";
537 const TEST_CLIENT_PARAMS: &str = "dmVyPTENCmNtZD1xdWVyeQ0KaWRrPWlnZ2N1X2UtdFdxM3NvZ2FhMmFBRENzeFJaRUQ5b245SDcxNlRBeVBSMHcNCnBpZGs9RTZRczJnWDdXLVB3aTlZM0tBbWJrdVlqTFNXWEN0S3lCY3ltV2xvSEF1bw0Kb3B0PWNwc35zdWsNCg";
538 const TEST_SERVER_RESPONSE: &str = "dmVyPTENCm51dD0xV005bGZGMVNULXoNCnRpZj01DQpxcnk9L2NsaS5zcXJsP251dD0xV005bGZGMVNULXoNCnN1az1CTUZEbTdiUGxzUW9qdUpzb0RUdmxTMU1jbndnU2N2a3RGODR2TGpzY0drDQo";
539 const TEST_SQRL_URL: &str = "c3FybDovL3Rlc3R1cmwuY29t";
540 const TEST_INVALID_URL: &str = "aHR0cHM6Ly9nb29nbGUuY29t";
541
542 #[test]
543 fn client_request_validate_example() {
544 ClientRequest::from_query_string(TEST_CLIENT_REQUEST).unwrap();
545 }
546
547 #[test]
548 fn client_parameters_encode_decode() {
549 let mut params = ClientParameters::new(
550 ClientCommand::Query,
551 decode_public_key("iggcu_e-tWq3sogaa2aADCsxRZED9on9H716TAyPR0w").unwrap(),
552 );
553 params.previous_identity_key =
554 Some(decode_public_key("E6Qs2gX7W-Pwi9Y3KAmbkuYjLSWXCtKyBcymWloHAuo").unwrap());
555 params.options = Some(vec![
556 ClientOption::ClientProvidedSession,
557 ClientOption::ServerUnlockKey,
558 ]);
559
560 let decoded = ClientParameters::from_base64(¶ms.to_base64()).unwrap();
561 assert_eq!(params, decoded);
562 }
563
564 #[test]
565 fn client_parameters_decode_example() {
566 let client_parameters = ClientParameters::from_base64(TEST_CLIENT_PARAMS).unwrap();
567
568 assert_eq!(client_parameters.protocol_version.to_string(), "1");
569 assert_eq!(client_parameters.command, ClientCommand::Query);
570 assert_eq!(
571 BASE64_URL_SAFE_NO_PAD.encode(client_parameters.identity_key.as_bytes()),
572 "iggcu_e-tWq3sogaa2aADCsxRZED9on9H716TAyPR0w"
573 );
574 match &client_parameters.previous_identity_key {
575 Some(s) => assert_eq!(
576 BASE64_URL_SAFE_NO_PAD.encode(s.as_bytes()),
577 "E6Qs2gX7W-Pwi9Y3KAmbkuYjLSWXCtKyBcymWloHAuo"
578 ),
579 None => panic!(),
580 }
581 match &client_parameters.options {
582 Some(s) => assert_eq!(
583 s,
584 &vec![
585 ClientOption::ClientProvidedSession,
586 ClientOption::ServerUnlockKey
587 ]
588 ),
589 None => panic!(),
590 }
591 }
592
593 #[test]
594 fn server_data_parse_sqrl_url() {
595 let data = ServerData::from_base64(TEST_SQRL_URL).unwrap();
596 match data {
597 ServerData::Url { url } => assert_eq!(url.to_string(), "sqrl://testurl.com"),
598 ServerData::ServerResponse { .. } => {
599 panic!("Did not expect a ServerResponse");
600 }
601 };
602 }
603
604 #[test]
605 fn server_data_parse_nonsqrl_url() {
606 let result = ServerData::from_base64(TEST_INVALID_URL);
607 if result.is_ok() {
608 panic!("Got back a real result");
609 }
610 }
611
612 #[test]
613 fn server_data_parse_server_data() {
614 let data = ServerData::from_base64(TEST_SERVER_RESPONSE).unwrap();
615 match data {
616 ServerData::Url { url: _ } => panic!("Did not expect a url"),
617 ServerData::ServerResponse {
618 server_response,
619 original_response,
620 ..
621 } => {
622 assert_eq!(server_response.nut, "1WM9lfF1ST-z");
623 assert_eq!(original_response, TEST_SERVER_RESPONSE);
624 }
625 };
626 }
627}