1use core::{fmt::Debug, hash::Hash};
2use std::{
3 borrow::Cow,
4 time::{Duration, SystemTime},
5};
6
7use crate::{BaseUtils, Cluster, RandomBytes, WalletAccount, WalletBaseError, WalletBaseResult};
8
9#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct SignInInput<'wa> {
15 domain: Option<Cow<'wa, str>>,
18 address: Option<Cow<'wa, str>>,
22 statement: Option<Cow<'wa, str>>,
26 uri: Option<Cow<'wa, str>>,
30 version: Option<Cow<'wa, str>>,
33 chain_id: Option<Cow<'wa, str>>,
38 nonce: Option<Cow<'wa, str>>,
42 issued_at: Option<Cow<'wa, str>>,
48 expiration_time: Option<Cow<'wa, str>>,
52 not_before: Option<Cow<'wa, str>>,
56 request_id: Option<Cow<'wa, str>>,
63 resources: Cow<'wa, [Cow<'wa, str>]>,
70}
71
72impl<'wa> SignInInput<'_> {
73 pub fn new() -> Self {
75 Self::default()
76 }
77
78 pub fn set_domain(&mut self, domain: &str) -> &mut Self {
81 self.domain.replace(Cow::Owned(domain.to_string()));
82
83 self
84 }
85
86 pub fn set_address(&'_ mut self, address: &str) -> WalletBaseResult<'_, &'_ mut Self> {
91 let mut buffer = [0u8; 32];
92 let buffer_written_len = bs58::decode(address).onto(&mut buffer).or(Err(
93 WalletBaseError::InvalidBase58Address(Cow::Owned(address.to_string())),
94 ))?;
95
96 if buffer_written_len != 32 {
97 return Err(WalletBaseError::InvalidEd25519PublicKeyLen(
98 buffer_written_len as u8,
99 ));
100 }
101
102 self.address.replace(Cow::Owned(address.to_string()));
103
104 Ok(self)
105 }
106 pub fn set_statement(&mut self, statement: &str) -> &mut Self {
109 self.statement.replace(Cow::Owned(statement.to_string()));
110
111 self
112 }
113
114 pub fn set_uri(&mut self, uri: &str) -> &mut Self {
119 self.uri.replace(Cow::Owned(uri.to_string()));
120
121 self
122 }
123
124 pub fn set_version(&mut self, version: &str) -> &mut Self {
127 self.version.replace(Cow::Owned(version.to_string()));
128
129 self
130 }
131
132 pub fn set_chain_id(&mut self, cluster: impl Cluster) -> &mut Self {
136 self.chain_id
137 .replace(Cow::Owned(cluster.chain().to_string()));
138
139 self
140 }
141
142 pub fn set_nonce(&mut self) -> &mut Self {
146 let random_bytes = RandomBytes::<32>::generate();
147
148 self.nonce
149 .replace(Cow::Owned(blake3::hash(random_bytes.expose()).to_string()));
150
151 self
152 }
153
154 pub fn set_custom_nonce(&'_ mut self, nonce: &str) -> WalletBaseResult<'_, &'_ mut Self> {
158 let nonce_length = nonce.len();
159 if nonce_length < 8 {
160 return Err(WalletBaseError::NonceMustBeAtLeast8Characters(
161 nonce_length as u8,
162 ));
163 }
164
165 self.nonce.replace(Cow::Owned(nonce.to_string()));
166
167 Ok(self)
168 }
169
170 pub fn set_issued_at(&mut self, time: SystemTime) -> &mut Self {
176 self.issued_at.replace(Cow::Owned(
177 humantime::format_rfc3339_millis(time).to_string(),
178 ));
179
180 self
181 }
182
183 pub fn set_expiration_time_millis(
186 &'_ mut self,
187 now: SystemTime,
188 expiration_time_milliseconds: u64,
189 ) -> WalletBaseResult<'_, &'_ mut Self> {
190 let duration = Duration::from_millis(expiration_time_milliseconds);
191
192 self.set_expiry_internal(now, duration)
193 }
194
195 pub fn set_expiration_time_seconds(
198 &'_ mut self,
199 now: SystemTime,
200 expiration_time_seconds: u64,
201 ) -> WalletBaseResult<'_, &'_ mut Self> {
202 let duration = Duration::from_secs(expiration_time_seconds);
203
204 self.set_expiry_internal(now, duration)
205 }
206
207 fn set_expiry_internal(
208 &'_ mut self,
209 now: SystemTime,
210 duration: Duration,
211 ) -> WalletBaseResult<'_, &'_ mut Self> {
212 let expiry_time = if let Some(issued_time) = self.issued_at.as_ref() {
213 let issued_time = humantime::parse_rfc3339(issued_time).or(Err(
214 WalletBaseError::InvalidISO8601Timestamp(issued_time.clone()),
215 ))?;
216 issued_time
217 .checked_add(duration)
218 .ok_or(WalletBaseError::SystemTimeCheckedAddOverflow)?
219 } else {
220 now
221 };
222
223 self.set_expiration_time(now, expiry_time)
224 }
225
226 pub fn set_expiration_time(
230 &'_ mut self,
231 now: SystemTime,
232 expiration_time: SystemTime,
233 ) -> WalletBaseResult<'_, &'_ mut Self> {
234 if let Some(issued_at) = self.issued_at.as_ref() {
235 let issued_at = humantime::parse_rfc3339(issued_at).or(Err(
236 WalletBaseError::InvalidISO8601Timestamp(issued_at.clone()),
237 ))?;
238
239 if issued_at > expiration_time {
240 let issued = BaseUtils::to_iso860(issued_at).to_string();
241 let expiry = BaseUtils::to_iso860(expiration_time).to_string();
242
243 return Err(WalletBaseError::ExpiryTimeEarlierThanIssuedTime {
244 issued: issued.into(),
245 expiry: expiry.into(),
246 });
247 }
248 }
249
250 if now > expiration_time {
251 let now = BaseUtils::to_iso860(now).to_string();
252 let expiry = BaseUtils::to_iso860(expiration_time).to_string();
253 return Err(WalletBaseError::ExpirationTimeIsInThePast {
254 now: now.into(),
255 expiry: expiry.into(),
256 });
257 }
258
259 self.expiration_time.replace(Cow::Owned(
260 humantime::format_rfc3339_millis(expiration_time).to_string(),
261 ));
262
263 Ok(self)
264 }
265
266 fn set_not_before_internal(
267 &'_ mut self,
268 now: SystemTime,
269 duration: Duration,
270 ) -> WalletBaseResult<'_, &'_ mut Self> {
271 let not_before = if let Some(issued_time) = self.issued_at.as_ref() {
272 let issued_time = humantime::parse_rfc3339(issued_time).or(Err(
273 WalletBaseError::InvalidISO8601Timestamp(issued_time.clone()),
274 ))?;
275
276 issued_time
277 .checked_add(duration)
278 .ok_or(WalletBaseError::SystemTimeCheckedAddOverflow)?
279 } else {
280 now
281 };
282
283 self.set_not_before_time(now, not_before)
284 }
285
286 pub fn set_not_before_time_millis(
289 &'_ mut self,
290 now: SystemTime,
291 expiration_time_milliseconds: u64,
292 ) -> WalletBaseResult<'_, &'_ mut Self> {
293 let duration = Duration::from_millis(expiration_time_milliseconds);
294
295 self.set_not_before_internal(now, duration)
296 }
297
298 pub fn set_not_before_time_seconds(
301 &'_ mut self,
302 now: SystemTime,
303 expiration_time_seconds: u64,
304 ) -> WalletBaseResult<'_, &'_ mut Self> {
305 let duration = Duration::from_secs(expiration_time_seconds);
306
307 self.set_not_before_internal(now, duration)
308 }
309
310 pub fn set_not_before_time(
315 &'_ mut self,
316 now: SystemTime,
317 not_before: SystemTime,
318 ) -> WalletBaseResult<'_, &'_ mut Self> {
319 if let Some(issued_at) = self.issued_at.as_ref() {
320 let issued_at = humantime::parse_rfc3339(issued_at).or(Err(
321 WalletBaseError::InvalidISO8601Timestamp(issued_at.clone()),
322 ))?;
323
324 if issued_at > not_before {
325 let issued = BaseUtils::to_iso860(issued_at).to_string();
326 let not_before = BaseUtils::to_iso860(not_before).to_string();
327 return Err(WalletBaseError::NotBeforeTimeEarlierThanIssuedTime {
328 issued_at: issued.into(),
329 not_before: not_before.into(),
330 });
331 }
332 }
333
334 if now > not_before {
335 let now = BaseUtils::to_iso860(now).to_string();
336 let not_before = BaseUtils::to_iso860(not_before).to_string();
337
338 return Err(WalletBaseError::NotBeforeTimeIsInThePast {
339 now: now.into(),
340 not_before: not_before.into(),
341 });
342 }
343
344 if let Some(expiration_time) = self.expiration_time.as_ref() {
345 let expiration_time = humantime::parse_rfc3339(expiration_time).or(Err(
346 WalletBaseError::InvalidISO8601Timestamp(expiration_time.clone()),
347 ))?;
348
349 if not_before > expiration_time {
350 let expiry = BaseUtils::to_iso860(expiration_time).to_string();
351 let not_before = BaseUtils::to_iso860(not_before).to_string();
352 return Err(WalletBaseError::NotBeforeTimeLaterThanExpirationTime {
353 not_before: not_before.into(),
354 expiry: expiry.into(),
355 });
356 }
357 }
358
359 self.not_before.replace(Cow::Owned(
360 humantime::format_rfc3339_millis(not_before).to_string(),
361 ));
362
363 Ok(self)
364 }
365
366 pub fn parser(input: &'wa str) -> WalletBaseResult<'wa, SignInInput<'wa>> {
368 let mut signin_input = SignInInput::default();
369
370 input
371 .split_once(" ")
372 .map(|(left, _right)| signin_input.domain.replace(left.trim().into()));
373
374 let split_colon = |value: &str| -> Option<Cow<'_, str>> {
375 value
376 .split_once(":")
377 .map(|(_left, right)| Cow::Owned(right.trim().to_string()))
378 };
379
380 let split_colon_system_time = |value: &str| -> WalletBaseResult<Option<Cow<'_, str>>> {
381 value
382 .split_once(":")
383 .map(|(_left, right)| {
384 humantime::parse_rfc3339(right.trim()).or(Err(
385 WalletBaseError::InvalidISO8601Timestamp(right.to_string().into()),
386 ))?;
387 Ok(Cow::Owned(right.to_string()))
388 })
389 .transpose()
390 };
391
392 input
393 .split("\n")
394 .enumerate()
395 .try_for_each(|(index, input)| {
396 if index == 1 {
397 signin_input.address.replace(input.trim().into());
398 }
399
400 if index == 3 {
401 signin_input.statement.replace(input.trim().into());
402 }
403
404 if input.contains("URI") {
405 signin_input.uri = split_colon(input);
406 }
407
408 if input.contains("Version") {
409 signin_input.version = split_colon(input);
410 }
411
412 if input.contains("Chain ID") {
413 if let Some((_left, right)) = input.split_once(":") {
414 let cluster = right.trim().into();
415
416 signin_input.chain_id.replace(cluster);
417 }
418 }
419 if input.contains("Nonce") {
420 signin_input.nonce = split_colon(input);
421 }
422
423 if input.contains("Issued At") {
424 signin_input.issued_at = split_colon_system_time(input)?;
425 }
426
427 if input.contains("Expiration") {
428 signin_input.expiration_time = split_colon_system_time(input)?;
429 }
430
431 if input.contains("Not Before") {
432 signin_input.not_before = split_colon_system_time(input)?;
433 }
434
435 if input.contains("Request ID") {
436 signin_input.request_id = split_colon(input);
437 }
438
439 if input.starts_with("-") {
440 if let Some(value) = input.split("-").nth(1) {
441 signin_input
442 .resources
443 .to_mut()
444 .push(Cow::Owned(value.trim().to_string()));
445 }
446 }
447
448 Ok::<(), WalletBaseError>(())
449 })?;
450
451 Ok(signin_input)
452 }
453
454 pub fn check_eq(&'_ self, other: &'_ Self) -> WalletBaseResult<'_, ()> {
457 if self.eq(other) {
458 Ok(())
459 } else {
460 Err(WalletBaseError::MessageResponseMismatch)
461 }
462 }
463
464 pub fn set_request_id(&mut self, id: &str) -> &mut Self {
471 self.request_id.replace(Cow::Owned(id.into()));
472
473 self
474 }
475
476 pub fn add_resource(&mut self, resource: &str) -> &mut Self {
481 self.resources
482 .to_mut()
483 .push(Cow::Owned(resource.to_string()));
484
485 self
486 }
487
488 pub fn add_resources(&mut self, resources: &[&str]) -> &mut Self {
490 resources.iter().for_each(|resource| {
491 self.resources
492 .to_mut()
493 .push(Cow::Owned(resource.to_string()))
494 });
495
496 self
497 }
498
499 pub fn domain(&self) -> Option<&str> {
501 self.domain.as_deref()
502 }
503
504 pub fn address(&self) -> Option<&str> {
506 self.address.as_deref()
507 }
508
509 pub fn statement(&self) -> Option<&str> {
511 self.statement.as_deref()
512 }
513
514 pub fn uri(&self) -> Option<&str> {
516 self.uri.as_deref()
517 }
518
519 pub fn version(&self) -> Option<&str> {
521 self.version.as_deref()
522 }
523
524 pub fn chain_id(&self) -> Option<&str> {
526 self.chain_id.as_deref()
527 }
528
529 pub fn nonce(&self) -> Option<&str> {
531 self.nonce.as_deref()
532 }
533
534 pub fn issued_at(&self) -> Option<&Cow<'_, str>> {
536 self.issued_at.as_ref()
537 }
538
539 pub fn expiration_time(&self) -> Option<&Cow<'_, str>> {
541 self.expiration_time.as_ref()
542 }
543
544 pub fn not_before(&self) -> Option<&Cow<'_, str>> {
546 self.not_before.as_ref()
547 }
548
549 pub fn issued_at_system_time(&self) -> Option<SystemTime> {
551 self.issued_at
552 .as_ref()
553 .map(|value| humantime::parse_rfc3339(value).ok())?
554 }
555
556 pub fn expiration_time_system_time(&self) -> Option<SystemTime> {
558 self.expiration_time
559 .as_ref()
560 .map(|value| humantime::parse_rfc3339(value).ok())?
561 }
562
563 pub fn not_before_system_time(&self) -> Option<SystemTime> {
565 self.not_before
566 .as_ref()
567 .map(|value| humantime::parse_rfc3339(value).ok())?
568 }
569
570 pub fn request_id(&self) -> Option<&str> {
572 self.request_id.as_deref()
573 }
574
575 pub fn resources(&'wa self) -> &'wa [Cow<'wa, str>] {
577 &self.resources
578 }
579}
580
581#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
583pub struct SignInOutput<T: WalletAccount + Debug + Clone + Hash + Ord + PartialEq + Eq + Default> {
584 pub account: T,
586 pub message: String,
588 pub signature: [u8; 64],
591 pub public_key: [u8; 32],
594}
595
596#[cfg(test)]
597#[cfg(target_arch = "wasm32")]
598mod signin_input_sanity_checks {
599 use super::*;
600
601 #[test]
602 fn set_issued_at() {
603 let mut signin_input = SigninInput::default();
604
605 assert!(signin_input.issued_at().is_none());
606
607 signin_input.set_issued_at().unwrap();
608
609 assert!(signin_input.issued_at.unwrap() > SystemTime::UNIX_EPOCH)
610 }
611
612 #[test]
613 fn set_expiration_time() {
614 let mut signin_input = SigninInput::default();
615
616 let now = SigninInput::time_now().unwrap();
617
618 let past_time = now.checked_sub(Duration::from_secs(300)).unwrap();
619 assert_eq!(
620 Some(WalletError::ExpirationTimeIsInThePast),
621 signin_input.set_expiration_time(past_time).err()
622 );
623
624 signin_input.set_issued_at().unwrap();
625 assert_eq!(
626 Some(WalletError::ExpiryTimeEarlierThanIssuedTime),
627 signin_input.set_expiration_time(past_time).err()
628 );
629
630 let valid_expiry = now.checked_add(Duration::from_secs(300)).unwrap();
631 assert!(signin_input.set_expiration_time(valid_expiry).is_ok());
632
633 assert!(signin_input.issued_at.unwrap() > SystemTime::UNIX_EPOCH);
634
635 assert!(signin_input.set_expiration_time_millis(4000).is_ok());
636 assert!(signin_input.set_expiration_time_seconds(4).is_ok());
637 }
638
639 #[test]
640 fn set_not_before_time() {
641 let mut signin_input = SigninInput::default();
642
643 let now = SigninInput::time_now().unwrap();
644
645 let past_time = now.checked_sub(Duration::from_secs(300)).unwrap();
646 assert_eq!(
647 Some(WalletError::NotBeforeTimeIsInThePast),
648 signin_input.set_not_before_time(past_time).err()
649 );
650
651 signin_input.set_issued_at().unwrap();
652 let future_time = now.checked_sub(Duration::from_secs(3000000)).unwrap();
653 assert_eq!(
654 Some(WalletError::NotBeforeTimeEarlierThanIssuedTime),
655 signin_input.set_not_before_time(future_time).err()
656 );
657
658 signin_input.set_issued_at().unwrap();
659 let future_time = SigninInput::time_now()
660 .unwrap()
661 .checked_add(Duration::from_secs(30000))
662 .unwrap();
663 signin_input.set_expiration_time(future_time).unwrap();
664 let future_time = now.checked_add(Duration::from_secs(3000000)).unwrap();
665 assert_eq!(
666 Some(WalletError::NotBeforeTimeLaterThanExpirationTime),
667 signin_input.set_not_before_time(future_time).err()
668 );
669
670 let valid_expiry = now.checked_add(Duration::from_secs(300)).unwrap();
671 assert!(signin_input.set_not_before_time(valid_expiry).is_ok());
672
673 assert!(signin_input.issued_at.unwrap() > SystemTime::UNIX_EPOCH);
674
675 assert!(signin_input.set_not_before_time_millis(4000).is_ok());
676 assert!(signin_input.set_not_before_time_seconds(4).is_ok());
677 }
678}