libkeycard 0.1.12

A digital certificate library for the Mensago platform
Documentation
use eznacl::*;
use std::collections::HashMap;

use crate::base::*;
use crate::entry::*;

/// `parse_entries()` is a generic helper function which reads keycard data and returns a Vector
/// of entries. The data for each entry is validated and it ensures that types aren't mixed, but
/// no other validation is performed. This call is mostly for reading card data received from
/// servers, which can be an entire keycard, part of one, or just one entry.
pub fn parse_entries(data: &str) -> Result<Vec<Entry>, LKCError> {
    let mut out = Vec::<Entry>::new();
    let mut card_type = String::from("");
    let mut accumulator = Vec::<&str>::new();
    let mut line_index: usize = 1;

    for line in data.split("\r\n") {
        let trimmed = line.trim();
        if trimmed.len() == 0 {
            line_index += 1;
            continue;
        }

        if trimmed == "----- BEGIN ENTRY -----" {
            accumulator.clear();
        } else if line == "----- END ENTRY -----" {
            let entry = match &*card_type {
                "User" | "Organization" => Entry::from(&accumulator.join("\r\n"))?,
                _ => return Err(LKCError::ErrInvalidKeycard),
            };

            out.push(entry);
        } else {
            let parts = trimmed.splitn(2, ":").collect::<Vec<&str>>();
            if parts.len() != 2 {
                return Err(LKCError::ErrBadFieldValue(String::from(trimmed)));
            }

            let field_name = match parts.get(0) {
                Some(v) => v.clone(),
                None => {
                    return Err(LKCError::ErrBadFieldValue(String::from(format!(
                        "Invalid line {}",
                        line_index
                    ))))
                }
            };

            if field_name == "Type" {
                if card_type.len() > 0 {
                    if card_type != parts[1] {
                        return Err(LKCError::ErrBadFieldValue(String::from(
                            "entry type does not match keycard type",
                        )));
                    }
                } else {
                    card_type = String::from(parts[1]);
                }
            }

            accumulator.push(trimmed);
        }

        line_index += 1;
    }

    Ok(out)
}

/// A Keycard object is a collection of entries tied together in an authenticated blockchain. It
/// consists of the root entry for the entity all the way through the current entry.
#[derive(Debug)]
#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Keycard {
    _type: EntryType,
    pub entries: Vec<Entry>,
}

impl Keycard {
    /// Creates a new Keycard of the specified type
    pub fn new(t: EntryType) -> Keycard {
        Keycard {
            _type: t,
            entries: Vec::<Entry>::new(),
        }
    }

    /// Creates a new keycard entry chain from text.
    pub fn from(data: &str) -> Result<Keycard, LKCError> {
        let mut cardtype = EntryType::None;
        let mut entries = parse_entries(data)?;
        if entries.len() > 0 {
            match entries[0].get_field("Index")?.as_str() {
                "Organization" => cardtype = EntryType::Organization,
                "User" => cardtype = EntryType::User,
                _ => return Err(LKCError::ErrBadFieldValue(String::from("Type"))),
            }
        }

        let mut out = Keycard::new(cardtype);
        out.entries.append(&mut entries);

        Ok(out)
    }

    /// Finds the entry in a keycard which has the requested hash
    pub fn find(&self, hash: &CryptoString) -> Result<&Entry, LKCError> {
        for i in 0..self.entries.len() {
            if self.entries[i].get_authstr("Hash")? == hash {
                return Ok(&self.entries[i]);
            }
        }

        Err(LKCError::ErrNotFound)
    }

    /// Returns the last entry of the keycard
    pub fn get_current(&self) -> Option<&Entry> {
        let entrycount = self.entries.len();
        if entrycount == 0 {
            return None;
        }

        self.entries.get(&entrycount - 1)
    }

    /// Returns a mutable reference to the last entry of the keycard
    pub fn get_current_mut(&mut self) -> Option<&mut Entry> {
        let entrycount = self.entries.len();
        if entrycount == 0 {
            return None;
        }

        self.entries.get_mut(&entrycount - 1)
    }

    /// Returns the type of entries stored in the keycard
    pub fn get_type(&self) -> EntryType {
        self._type
    }

    /// Returns a string containing the owner of the keycard, which will be a domain for an
    /// organization and a workspace address for a user. This call will fail if the card has no
    /// entries or if the current entry does not have the required fields, i.e. Domain or
    /// Domain + Workspace-ID.
    pub fn get_owner(&self) -> Result<String, LKCError> {
        let current = match self.get_current() {
            Some(v) => v,
            None => return Err(LKCError::ErrEmptyData),
        };

        current.get_owner()
    }

    /// Returns the entire keycard chain as text
    pub fn get_text(&self) -> Result<String, LKCError> {
        let mut parts = Vec::<String>::new();

        for i in 0..self.entries.len() {
            parts.push(format!(
                "----- BEGIN ENTRY -----\r\n\
				{}\
				----- END ENTRY -----\r\n",
                self.entries[i].get_full_text("")?
            ));
        }

        Ok(parts.join(""))
    }

    /// Creates a new Entry object in the keycard. Organization keycards are complete and compliant
    /// when chain() returns. User keycards will require cross_sign() and user_sign() to be called
    /// before it is complete.
    ///
    /// This method returns a HashMap which contains the newly-generated keys associated with the
    /// new keycard entry. The fields returned will depend on the keycard type.
    ///
    /// Organization keycards will return the fields `primary.public`, `primary.private`,
    /// `encryption.public`, and `encryption.private`. The secondary signing keypair is not returned
    /// because the signing pair passed to the method becomes the secondary signing keypair after
    /// this call completes.
    ///
    /// User keycards will return the fields `crsigning.public`, `crsigning.private`,
    /// `crencryption.public`, `crencryption.private`, `signing.public`, `signing.private`,
    /// `encryption.public`, and `encryption.private`.
    pub fn chain(
        &mut self,
        spair: &SigningPair,
        expires: u16,
    ) -> Result<HashMap<&'static str, CryptoString>, LKCError> {
        let (newentry, keys) = {
            let entry = match self.get_current() {
                Some(v) => v,
                None => return Err(LKCError::ErrEmptyData),
            };

            match entry.get_field("Type")?.as_str() {
                "Organization" | "User" => (),
                _ => return Err(LKCError::ErrInvalidKeycard),
            }

            entry.chain(spair, expires)?
        };

        self.entries.push(newentry);

        Ok(keys)
    }

    /// This convenience method applies only to user keycards and is used to set the organization's
    /// signature for the current entry.
    pub fn cross_sign(&mut self, signing_pair: &SigningPair) -> Result<(), LKCError> {
        let entrycount = self.entries.len();
        if entrycount == 0 {
            return Err(LKCError::ErrEmptyData);
        }

        let current = self.get_current_mut().unwrap();
        if current.get_field("Type")? != "User" {
            return Err(LKCError::ErrTypeMismatch);
        }

        current.sign("Organization-Signature", &signing_pair)
    }

    /// This convenience method applies only to user keycards and is used to add the final user
    /// signature and generate the hash for the entry. Once this has been applied, the current
    /// entry for the card should be compliant and should pass verification.
    pub fn user_sign(
        &mut self,
        hash_algorithm: &str,
        signing_pair: &SigningPair,
    ) -> Result<(), LKCError> {
        let entrycount = self.entries.len();
        if entrycount == 0 {
            return Err(LKCError::ErrEmptyData);
        }

        let current = self.get_current_mut().unwrap();
        if current.get_field("Type")? != "User" {
            return Err(LKCError::ErrTypeMismatch);
        }

        current.hash(hash_algorithm)?;
        current.sign("User-Signature", &signing_pair)
    }

    /// Verifies the keycard's complete chain of entries
    pub fn verify(&self) -> Result<(), LKCError> {
        match self.entries.len() {
            0 => return Err(LKCError::ErrEmptyData),
            1 => return Ok(()),
            _ => (),
        }

        for i in 0..self.entries.len() - 1 {
            self.entries[i + 1].verify_chain(&self.entries[i])?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use crate::*;

    #[test]
    pub fn test_parse_entries() -> Result<(), LKCError> {
        let testname = "test_parse_entries";

        let card_data = "----- BEGIN ENTRY -----\r\n\
		Type:Organization\r\n\
		Index:1\r\n\
		Name:Example, Inc.\r\n\
		Domain:example.com\r\n\
		Contact-Admin:c590b44c-798d-4055-8d72-725a7942f3f6/example.com\r\n\
		Language:en\r\n\
		Primary-Verification-Key:ED25519:r#r*RiXIN-0n)BzP3bv`LA&t4LFEQNF0Q@$N~RF*\r\n\
		Encryption-Key:CURVE25519:SNhj2K`hgBd8>G>lW$!pXiM7S-B!Fbd9jT2&{{Az\r\n\
		Time-To-Live:14\r\n\
		Expires:20230818\r\n\
		Timestamp:20220818T172640Z\r\n\
		Hash:BLAKE3-256:ocU4XayQkNEHh-zHevRuX;YJmKp4AD2eo_R|9I31\r\n\
		Organization-Signature:ED25519:Rlusb?3WRvd95Hc<aYat$GH2AszxNVvF8Hly&eYyqys0on&vx=tCAKbe~!owEi5HQnTafpEdoJ*F&`TZ\r\n\
		----- END ENTRY -----\r\n\
		----- BEGIN ENTRY -----\r\n\
		Type:Organization\r\n\
		Index:2\r\n\
		Name:Example, Inc.\r\n\
		Domain:example.com\r\n\
		Contact-Admin:c590b44c-798d-4055-8d72-725a7942f3f6/example.com\r\n\
		Language:en\r\n\
		Primary-Verification-Key:ED25519:f!7Asqr9w7v=fsX@<?_s*}Btn>WfrgOZk?M)YOGr\r\n\
		Secondary-Verification-Key:ED25519:r#r*RiXIN-0n)BzP3bv`LA&t4LFEQNF0Q@$N~RF*\r\n\
		Encryption-Key:CURVE25519:Yoj)=FNDj>sc0U^JCypwu=W1~c!C`1^xlul{|GT`\r\n\
		Time-To-Live:14\r\n\
		Expires:20230818\r\n\
		Timestamp:20220818T172640Z\r\n\
		Custody-Signature:ED25519:N603IF=MJ-Se<!g+%3rx{mUlOp7}XqIwE<SGEhG~R;@(1gzM|V7lcXw++%NPGuS2zS@yRLpSxmc0oW-I\r\n\
		Previous-Hash:BLAKE3-256:ocU4XayQkNEHh-zHevRuX;YJmKp4AD2eo_R|9I31\r\n\
		Hash:BLAKE3-256:vg65eVmF~r#^zG*gwF2XIEl3*l;J>aB*iLGkN?m6\r\n\
		Organization-Signature:ED25519:^O)N7oeF9Af)7fS{Kde_3hPcne{CLfmm3f{%w1`08xp9Df_v9Fc~zCe<k~-$_yzNA<I3*5&J_0<UlNE5\r\n\
		----- END ENTRY -----\r\n";

        let mut out = Keycard::new(EntryType::Organization);
        let mut entries = match parse_entries(card_data) {
            Ok(v) => v,
            Err(e) => {
                return Err(LKCError::ErrProgramException(format!(
                    "{}: failed to parse entries: {}",
                    testname,
                    e.to_string()
                )))
            }
        };
        out.entries.append(&mut entries);

        Ok(())
    }
}