libkeycard/
types.rs

1use crate::base::LKCError;
2use chrono::{prelude::*, Duration, NaiveDateTime};
3use hex;
4use lazy_static::lazy_static;
5use rand::prelude::*;
6use regex::Regex;
7use std::fmt;
8
9lazy_static! {
10    #[doc(hidden)]
11    pub static ref RANDOMID_PATTERN: regex::Regex =
12        Regex::new(r"^[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}$")
13        .unwrap();
14
15    #[doc(hidden)]
16    pub static ref USERID_PATTERN: regex::Regex =
17        Regex::new(r"^([\p{L}\p{M}\p{N}\-_]|\.[^.])+$")
18        .unwrap();
19
20    #[doc(hidden)]
21    pub static ref CONTROL_CHARS_PATTERN: regex::Regex =
22        Regex::new(r#"\p{C}"#)
23        .unwrap();
24
25    #[doc(hidden)]
26    pub static ref DOMAIN_PATTERN: regex::Regex =
27        Regex::new(r"^([a-zA-Z0-9\-]+)(\.[a-zA-Z0-9\-]+)*$")
28        .unwrap();
29
30}
31
32/// IDType identifies the type of RandomID used
33#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
34#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
35pub enum IDType {
36    WorkspaceID,
37    UserID,
38}
39
40/// The RandomID class is similar to v4 UUIDs. To obtain the maximum amount of entropy, all bits
41/// are random and no version information is stored in them. The only null value for the RandomID
42/// is all zeroes. Lastly, the only permissible format for the string version of the RandomID
43/// has all letters in lowercase and dashes are placed in the same places as for UUIDs.
44#[derive(Debug, PartialEq, PartialOrd, Clone)]
45#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
46pub struct RandomID {
47    data: String,
48}
49
50impl RandomID {
51    /// Creates a new populated RandomID
52    pub fn generate() -> RandomID {
53        let mut rdata: [u8; 16] = [0; 16];
54        rand::thread_rng().fill_bytes(&mut rdata[..]);
55        let out = RandomID {
56            data: format!(
57                "{}-{}-{}-{}-{}",
58                hex::encode(&rdata[0..4]),
59                hex::encode(&rdata[4..6]),
60                hex::encode(&rdata[6..8]),
61                hex::encode(&rdata[8..10]),
62                hex::encode(&rdata[10..])
63            ),
64        };
65
66        out
67    }
68
69    /// Creates a RandomID from an existing string and ensures that formatting is correct.
70    pub fn from(data: &str) -> Option<RandomID> {
71        if !RANDOMID_PATTERN.is_match(data) {
72            return None;
73        }
74
75        let mut out = RandomID {
76            data: String::from("00000000-0000-0000-0000-000000000000"),
77        };
78        out.data = data.to_lowercase();
79
80        Some(out)
81    }
82
83    /// Creates a RandomID from a Mensago UserID instance if compatible. All RandomIDs are valid
84    /// UserIDs, but not the other way around.
85    pub fn from_userid(uid: &UserID) -> Option<RandomID> {
86        match uid.get_type() {
87            IDType::UserID => None,
88            IDType::WorkspaceID => Some(RandomID {
89                data: uid.to_string(),
90            }),
91        }
92    }
93
94    /// Returns the RandomID as a string
95    pub fn as_string(&self) -> &str {
96        &self.data
97    }
98}
99
100impl fmt::Display for RandomID {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "{}", self.data)
103    }
104}
105
106/// A basic data type for housing Mensago user IDs. User IDs on the Mensago platform must be no
107/// more than 64 ASCII characters. These characters may be from the following: lowercase a-z,
108/// numbers, a dash, or an underscore. Periods may also be used so long as they are not consecutive.
109#[derive(Debug, PartialEq, PartialOrd, Clone)]
110#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
111pub struct UserID {
112    data: String,
113    idtype: IDType,
114}
115
116impl UserID {
117    /// Creates a UserID from an existing string. If it contains illegal characters, it will
118    /// return None. All capital letters will have their case squashed for compliance.
119    pub fn from(data: &str) -> Option<UserID> {
120        if data.len() > 64 || data.len() == 0 {
121            return None;
122        }
123
124        if !USERID_PATTERN.is_match(data) {
125            return None;
126        }
127
128        let mut out = UserID {
129            data: String::from(data),
130            idtype: IDType::UserID,
131        };
132        out.data = data.to_lowercase();
133
134        out.idtype = if RANDOMID_PATTERN.is_match(&out.data) {
135            IDType::WorkspaceID
136        } else {
137            IDType::UserID
138        };
139
140        Some(out)
141    }
142
143    /// Creates a UserID from a workspace ID
144    pub fn from_wid(wid: &RandomID) -> UserID {
145        UserID {
146            data: String::from(wid.as_string()),
147            idtype: IDType::WorkspaceID,
148        }
149    }
150
151    /// Returns the UserID as a string
152    pub fn as_string(&self) -> &str {
153        &self.data
154    }
155
156    /// Returns the type of ID (Workspace ID vs Mensago ID)
157    pub fn get_type(&self) -> IDType {
158        self.idtype
159    }
160}
161
162impl fmt::Display for UserID {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(f, "{}", self.data)
165    }
166}
167
168/// A basic data type for housing Internet domains.
169#[derive(Debug, PartialEq, PartialOrd, Clone)]
170#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
171pub struct Domain {
172    data: String,
173}
174
175impl Domain {
176    /// Creates a Domain from an existing string. If it contains illegal characters, it will
177    /// return None. All capital letters will have their case squashed. This type exists to ensure
178    /// that valid domains are used across the library
179    pub fn from(data: &str) -> Option<Domain> {
180        if !DOMAIN_PATTERN.is_match(data) {
181            return None;
182        }
183
184        let mut out = Domain {
185            data: String::from(data),
186        };
187        out.data = data.to_lowercase();
188
189        Some(out)
190    }
191
192    /// Returns the Domain as a string
193    pub fn as_string(&self) -> &str {
194        &self.data
195    }
196
197    /// Returns the parent domain unless doing so would return a top-level domain, i.e. '.com', in
198    /// which case it returns None.
199    pub fn parent(&self) -> Option<Domain> {
200        let domparts: Vec<&str> = self.data.split(".").collect();
201        if domparts.len() < 3 {
202            return None;
203        }
204
205        return Some(Domain {
206            data: String::from(&domparts[1..].join(".")),
207        });
208    }
209
210    /// Removes a level of subdomains from the item, working much like `parent()` except that the
211    /// current object is modified. LKCError::ErrOutOfRange is returned if the call would
212    /// result in the Domain object housing just a top-level domain, such as `com`.
213    pub fn pop(&mut self) -> Result<(), LKCError> {
214        let domparts: Vec<&str> = self.data.split(".").collect();
215        if domparts.len() < 3 {
216            return Err(LKCError::ErrOutOfRange);
217        }
218
219        self.data = String::from(&domparts[1..].join("."));
220        Ok(())
221    }
222
223    /// Adds a subdomain to the object
224    pub fn push(&mut self, subdom: &str) -> Result<(), LKCError> {
225        let mut domparts: Vec<&str> = self.data.split(".").collect();
226        let subdomparts: Vec<&str> = subdom.split(".").collect();
227
228        for part in subdomparts.iter().rev() {
229            domparts.insert(0, part);
230        }
231
232        self.data = String::from(&domparts.join("."));
233
234        Ok(())
235    }
236}
237
238impl fmt::Display for Domain {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        write!(f, "{}", self.data)
241    }
242}
243
244/// A basic data type representing a full Mensago address. It is used to ensure passing around
245/// valid data within the library.
246#[derive(Debug, PartialEq, PartialOrd, Clone)]
247#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
248pub struct MAddress {
249    pub uid: UserID,
250    pub domain: Domain,
251    address: String,
252}
253
254impl MAddress {
255    /// Creates a new MAddress from a string. If the string does not contain a valid Mensago
256    /// address, None will be returned.
257    pub fn from(data: &str) -> Option<MAddress> {
258        let parts = data.split("/").collect::<Vec<&str>>();
259
260        if parts.len() != 2 {
261            return None;
262        }
263
264        let out = MAddress {
265            uid: UserID::from(parts[0])?,
266            domain: Domain::from(parts[1])?,
267            address: format!("{}/{}", parts[0], parts[1]),
268        };
269
270        Some(out)
271    }
272
273    /// Creates an MAddress from an WAddress instance
274    pub fn from_waddress(waddr: &WAddress) -> MAddress {
275        let uid = UserID::from_wid(&waddr.wid);
276        MAddress {
277            address: format!("{}/{}", uid, waddr.domain),
278            uid,
279            domain: waddr.domain.clone(),
280        }
281    }
282
283    /// Creates an MAddress from its components
284    pub fn from_parts(uid: &UserID, domain: &Domain) -> MAddress {
285        MAddress {
286            uid: uid.clone(),
287            domain: domain.clone(),
288            address: format!("{}/{}", uid, domain),
289        }
290    }
291
292    /// Returns the MAddress as a string
293    pub fn as_string(&self) -> String {
294        format!("{}/{}", self.uid, self.domain)
295    }
296
297    /// Returns the UserID portion of the address
298    pub fn get_uid(&self) -> &UserID {
299        &self.uid
300    }
301
302    /// Returns the Domain portion of the address
303    pub fn get_domain(&self) -> &Domain {
304        &self.domain
305    }
306}
307
308impl fmt::Display for MAddress {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        write!(f, "{}/{}", self.uid, self.domain)
311    }
312}
313
314/// A basic data type representing a full Mensago address. It is used to ensure passing around
315/// valid data within the library.
316#[derive(Debug, PartialEq, PartialOrd, Clone)]
317#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
318pub struct WAddress {
319    wid: RandomID,
320    domain: Domain,
321
322    // We keep this extra copy around because converting the item to a full string is a very
323    // common operation
324    address: String,
325}
326
327impl WAddress {
328    /// Creates a new WAddress from a string. If the string does not contain a valid workspace
329    /// address, None will be returned.
330    pub fn from(data: &str) -> Option<WAddress> {
331        let parts = data.split("/").collect::<Vec<&str>>();
332
333        if parts.len() != 2 {
334            return None;
335        }
336
337        let out = WAddress {
338            wid: RandomID::from(parts[0])?,
339            domain: Domain::from(parts[1])?,
340            address: format!("{}/{}", parts[0], parts[1]),
341        };
342
343        Some(out)
344    }
345
346    /// Creates a WAddress from an MAddress instance if compatible. This is because all workspace
347    /// addresses are valid Mensago addresses, but not the other way around.
348    pub fn from_maddress(maddr: &MAddress) -> Option<WAddress> {
349        match maddr.uid.get_type() {
350            IDType::UserID => None,
351            IDType::WorkspaceID => Some(WAddress {
352                wid: RandomID::from_userid(&maddr.uid).unwrap(),
353                domain: maddr.domain.clone(),
354                address: format!("{}/{}", maddr.uid, maddr.domain),
355            }),
356        }
357    }
358
359    /// Creates a WAddress from its components
360    pub fn from_parts(wid: &RandomID, domain: &Domain) -> WAddress {
361        WAddress {
362            wid: wid.clone(),
363            domain: domain.clone(),
364            address: format!("{}/{}", wid, domain),
365        }
366    }
367
368    /// Returns the WAddress as a string
369    pub fn as_string(&self) -> String {
370        format!("{}/{}", self.wid, self.domain)
371    }
372
373    /// Returns the RandomID portion of the address
374    pub fn get_wid(&self) -> &RandomID {
375        &self.wid
376    }
377
378    /// Returns the Domain portion of the address
379    pub fn get_domain(&self) -> &Domain {
380        &self.domain
381    }
382}
383
384impl fmt::Display for WAddress {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        write!(f, "{}/{}", self.wid, self.domain)
387    }
388}
389
390/// A basic data type representing an Argon2id password hash. It is used to ensure passing around
391/// valid data within the library. This might someday be genericized, but for now it's fine.
392#[derive(Debug, PartialEq, PartialOrd, Clone)]
393#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
394pub struct ArgonHash {
395    hash: String,
396    hashtype: String,
397}
398
399impl ArgonHash {
400    /// Creates a new ArgonHash from the provided password
401    pub fn from(password: &str) -> ArgonHash {
402        ArgonHash {
403            hash: eznacl::hash_password(password, &eznacl::HashStrength::Basic),
404            hashtype: String::from("argon2id"),
405        }
406    }
407
408    /// Creates an ArgonHash object from a verified string
409    pub fn from_hashstr(passhash: &str) -> ArgonHash {
410        ArgonHash {
411            hash: String::from(passhash),
412            hashtype: String::from("argon2id"),
413        }
414    }
415
416    /// Returns the object's hash string
417    pub fn get_hash(&self) -> &str {
418        &self.hash
419    }
420
421    /// Returns the object's hash type
422    pub fn get_hashtype(&self) -> &str {
423        &self.hashtype
424    }
425}
426
427impl fmt::Display for ArgonHash {
428    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429        write!(f, "{}", self.hash)
430    }
431}
432
433/// A basic data type for timestamps as used on Mensago. The type is timezone-agnostic as the
434/// platform operates on UTC time only. It can be used to just store dates or dates with times
435#[derive(Debug, PartialEq, PartialOrd, Clone)]
436#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
437pub struct Timestamp {
438    datetime: NaiveDateTime,
439    data: String,
440}
441
442impl Timestamp {
443    /// Creates a new Timestamp object using the current date and time
444    pub fn new() -> Timestamp {
445        let utc: DateTime<Utc> = Utc::now();
446        let formatted = utc.format("%Y%m%dT%H%M%SZ");
447
448        Timestamp::from_str(formatted.to_string().as_str()).unwrap()
449    }
450
451    /// Creates a new Timestamp from a correctly-formatted string, which is YYYYMMDDTHHMMSSZ.
452    pub fn from_str(data: &str) -> Option<Timestamp> {
453        let datetime = match chrono::NaiveDateTime::parse_from_str(data, "%Y%m%dT%H%M%SZ") {
454            Ok(v) => v,
455            Err(_) => return None,
456        };
457
458        Some(Timestamp {
459            datetime,
460            data: String::from(data.to_uppercase()),
461        })
462    }
463
464    /// Creates a timestamp object from just a date, which internally is stored as midnight on
465    /// that date.
466    pub fn from_datestr(date: &str) -> Option<Self> {
467        let datetime = format!("{}T000000Z", date);
468        Timestamp::from_str(&datetime)
469    }
470
471    /// Returns the timestamp offset by the specified number of days
472    pub fn with_offset(&self, days: i64) -> Option<Timestamp> {
473        let offset_date = self.datetime.checked_add_signed(Duration::days(days))?;
474        Some(Timestamp {
475            datetime: offset_date,
476            data: offset_date.format("%Y%m%dT%H%M%SZ").to_string(),
477        })
478    }
479
480    /// Returns the Timestamp as a string with only the date in the format YYYYMMDD.
481    pub fn as_datestr(&self) -> &str {
482        &self.data[0..8]
483    }
484}
485
486impl fmt::Display for Timestamp {
487    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488        write!(f, "{}", self.data)
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use crate::*;
495
496    #[test]
497    fn test_randomid() {
498        let testid = RandomID::generate();
499
500        let strid = RandomID::from(testid.as_string());
501        assert_ne!(strid, None);
502    }
503
504    #[test]
505    fn test_userid() {
506        assert_ne!(UserID::from("valid_e-mail.123"), None);
507
508        match UserID::from("11111111-1111-1111-1111-111111111111") {
509            Some(v) => {
510                assert!(v.get_type() == IDType::WorkspaceID)
511            }
512            None => {
513                panic!("test_userid failed workspace ID assignment")
514            }
515        }
516
517        match UserID::from("Valid.but.needs_case-squashed") {
518            Some(v) => {
519                assert_eq!(v.as_string(), "valid.but.needs_case-squashed")
520            }
521            None => {
522                panic!("test_userid failed case-squashing check")
523            }
524        }
525
526        assert_eq!(UserID::from("invalid..number1"), None);
527        assert_eq!(UserID::from("invalid#2"), None);
528    }
529
530    #[test]
531    fn test_domain() {
532        // Test the basics
533
534        assert!(Domain::from("foo-bar").is_some());
535        assert!(Domain::from("foo-bar.baz.com").is_some());
536
537        match Domain::from("FOO.bar.com") {
538            Some(v) => {
539                assert_eq!(v.as_string(), "foo.bar.com")
540            }
541            None => {
542                panic!("test_domain failed case-squashing check")
543            }
544        }
545
546        assert!(Domain::from("a bad-id.com").is_none());
547        assert!(Domain::from("also_bad.org").is_none());
548
549        // parent() testing
550
551        assert!(Domain::from("foo-bar.baz.com").unwrap().parent().is_some());
552        assert!(Domain::from("baz.com").unwrap().parent().is_none());
553        assert_eq!(
554            Domain::from("foo-bar.baz.com")
555                .unwrap()
556                .parent()
557                .unwrap()
558                .to_string(),
559            "baz.com"
560        );
561
562        // push() and pop()
563
564        let mut d = Domain::from("baz.com").unwrap();
565        assert!(d.push("foo.bar").is_ok());
566        assert_eq!(d.to_string(), "foo.bar.baz.com");
567        assert!(d.pop().is_ok());
568        assert_eq!(d.to_string(), "bar.baz.com");
569        assert!(d.pop().is_ok());
570        assert_eq!(d.to_string(), "baz.com");
571        assert!(d.pop().is_err());
572    }
573
574    #[test]
575    fn test_maddress() {
576        assert_ne!(MAddress::from("cats4life/example.com"), None);
577        assert_ne!(
578            MAddress::from("5a56260b-aa5c-4013-9217-a78f094432c3/example.com"),
579            None
580        );
581
582        let waddr = WAddress::from("5a56260b-aa5c-4013-9217-a78f094432c3/example.com").unwrap();
583        assert_eq!(
584            MAddress::from_waddress(&waddr).to_string(),
585            "5a56260b-aa5c-4013-9217-a78f094432c3/example.com"
586        );
587
588        assert_eq!(MAddress::from("has spaces/example.com"), None);
589        assert_eq!(MAddress::from(r#"has_a_"/example.com"#), None);
590        assert_eq!(MAddress::from("\\not_allowed/example.com"), None);
591        assert_eq!(MAddress::from("/example.com"), None);
592        assert_eq!(
593            MAddress::from("5a56260b-aa5c-4013-9217-a78f094432c3/example.com/example.com"),
594            None
595        );
596        assert_eq!(MAddress::from("5a56260b-aa5c-4013-9217-a78f094432c3"), None);
597    }
598
599    #[test]
600    fn test_waddress() {
601        assert_ne!(
602            WAddress::from("5a56260b-aa5c-4013-9217-a78f094432c3/example.com"),
603            None
604        );
605        assert_eq!(WAddress::from("cats4life/example.com"), None);
606        assert_eq!(
607            WAddress::from_maddress(&MAddress::from("cats4life/example.com").unwrap()),
608            None
609        );
610        assert!(WAddress::from_maddress(
611            &MAddress::from("5a56260b-aa5c-4013-9217-a78f094432c3/example.com").unwrap()
612        )
613        .is_some());
614    }
615
616    #[test]
617    fn test_timestamp() {
618        assert_eq!(Timestamp::from_str("foobar"), None);
619
620        let ts = Timestamp::from_str("20220501T131011Z").unwrap();
621        assert_eq!(&ts.to_string(), "20220501T131011Z");
622        assert_eq!(ts.as_datestr(), "20220501");
623        assert_eq!(&ts.with_offset(1).unwrap().to_string(), "20220502T131011Z");
624
625        let ds = Timestamp::from_datestr("20220502").unwrap();
626        assert_eq!(ds.as_datestr(), "20220502");
627        assert_eq!(&ds.to_string(), "20220502T000000Z");
628    }
629}