From c3e8b2b2a0e32b911657290368596d55f9001162 Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Thu, 30 Mar 2023 15:23:37 +0200 Subject: [PATCH 001/160] Improved cert generation from csr --- keysas_lib/src/lib.rs | 2 + keysas_lib/src/pki.rs | 401 +++++++++++++++++++++++++++++++----------- 2 files changed, 300 insertions(+), 103 deletions(-) diff --git a/keysas_lib/src/lib.rs b/keysas_lib/src/lib.rs index c449431..494d799 100644 --- a/keysas_lib/src/lib.rs +++ b/keysas_lib/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(is_some_and)] + use anyhow::Result; use regex::Regex; use sha2::{Digest, Sha256}; diff --git a/keysas_lib/src/pki.rs b/keysas_lib/src/pki.rs index e92152e..e3534af 100644 --- a/keysas_lib/src/pki.rs +++ b/keysas_lib/src/pki.rs @@ -23,25 +23,29 @@ #![warn(overflowing_literals)] #![warn(deprecated)] #![warn(unused_imports)] + use anyhow::{anyhow, Context}; use ed25519_dalek::Digest; use ed25519_dalek::Keypair; use ed25519_dalek::Sha512; use oqs::sig::Algorithm; -use oqs::sig::PublicKey; use oqs::sig::SecretKey; use oqs::sig::Sig; +use pkcs8::der::asn1::OctetString; use pkcs8::der::asn1::SetOfVec; +use pkcs8::der::oid::db::rfc5280; use pkcs8::pkcs5::pbes2; use pkcs8::EncryptedPrivateKeyInfo; use pkcs8::LineEnding; use pkcs8::PrivateKeyInfo; use rand_dl::RngCore; use rand_dl::rngs::OsRng; +use x509_cert::ext::Extension; use std::fs; use std::fs::File; use std::io::Write; use std::path::Path; +use std::str::FromStr; use std::time::Duration; use x509_cert::certificate::*; use x509_cert::der::asn1::BitString; @@ -126,16 +130,17 @@ use x509_cert::time::Validity; /// Structure containing informations to build the certificate #[derive(Debug)] pub struct CertificateFields { - pub org_name: String, - pub org_unit: String, - pub country: String, - pub validity: u32, + pub org_name: Option, + pub org_unit: Option, + pub country: Option, + pub common_name: Option, + pub validity: u32, } #[derive(Debug)] pub struct KeysasPQKey { pub private_key: SecretKey, - pub public_key: PublicKey, + pub public_key: oqs::sig::PublicKey, } #[derive(Debug)] @@ -149,36 +154,90 @@ pub struct HybridKeyPair { const DILITHIUM5_OID: &str = "1.3.6.1.4.1.2.267.7.8.7"; const ED25519_OID: &str = "1.3.101.112"; -/// Validate user input and construct a certificate fields structure that can be used -/// to build the certificates of the PKI. -/// The checks done are : -/// - Test if country is 2 letters long, if less return error, if more shorten it to the first two letters -/// - Test if validity can be converted to u32, if not generate error -/// - Test if sigAlgo is either ed25519 or ed448, if not defaut to ed25519 -pub fn validate_input_cert_fields<'a>( - org_name: &'a String, - org_unit: &'a String, - country: &'a String, - validity: &'a str, -) -> Result { - // Test if country is 2 letters long - let cn = match country.len() { - 0 | 1 => return Err(anyhow!("Failed to parse length field")), - 2 => country.to_string(), - _ => country[..2].to_string(), - }; - // Test if validity can be converted to u32 - let val = match validity.parse::() { - Ok(v) => v, - Err(_) => return Err(anyhow!("Failed to parse validity field")), - }; +impl CertificateFields { + /// Validate user input and construct a certificate fields structure that can be used + /// to build the certificates of the PKI. + /// The checks done are : + /// - Test if country is 2 letters long, if less return error, if more shorten it to the first two letters + /// - Test if validity can be converted to u32, if not generate error + /// - Test if sigAlgo is either ed25519 or ed448, if not defaut to ed25519 + fn validate_input_cert_fields<'a>( + org_name: &'a String, + org_unit: &'a String, + country: &'a String, + common_name: &'a String, + validity: &'a str, + ) -> Result { + // Test if country is 2 letters long + let cn = match country.len() { + 0 | 1 => return Err(anyhow!("Failed to parse length field")), + 2 => country.to_string(), + _ => country[..2].to_string(), + }; + // Test if validity can be converted to u32 + let val = match validity.parse::() { + Ok(v) => v, + Err(_) => return Err(anyhow!("Failed to parse validity field")), + }; - Ok(CertificateFields { - org_name: org_name.to_string(), - org_unit: org_unit.to_string(), - country: cn, - validity: val, - }) + Ok(CertificateFields { + org_name: Some(org_name.to_string()), + org_unit: Some(org_unit.to_string()), + country: Some(cn), + common_name: Some(common_name.to_string()), + validity: val, + }) + } + + /// Generate a distinghuished name from the input fields for the certificate + fn generate_dn(&self) -> Result { + let mut name = String::new(); + + // Add country name + if let Some(cn) = &self.country { + name.push_str("C="); + name.push_str(&cn); + name.push_str(","); + } + + // Add organisation name + if let Some(oa) = &self.org_name { + if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { + name.push_str(","); + } + name.push_str("O="); + name.push_str(&oa); + name.push_str(","); + } + + // Add organisational unit + if let Some(ou) = &self.org_unit { + if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { + name.push_str(","); + } + name.push_str("OU="); + name.push_str(&ou); + name.push_str(","); + } + + // Add common name + if let Some(co) = &self.common_name { + if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { + name.push_str(","); + } + name.push_str("CN="); + name.push_str(&co); + name.push_str(","); + } + + // Remove trailing ',' if there is one + if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { + name.pop(); + } + + let rdn = RdnSequence::from_str(&name)?; + Ok(rdn) + } } fn create_dir_if_not_exist(path: &String) -> Result<(), anyhow::Error> { @@ -214,15 +273,24 @@ pub fn create_pki_dir(pki_dir: &String) -> Result<(), anyhow::Error> { } fn construct_tbs_certificate( - infos: &CertificateFields, + issuer_infos: &CertificateFields, + subject_name: &RdnSequence, pub_value: &[u8], + serial: &[u8], algo_oid: &ObjectIdentifier, -) -> Result { - let dur = Duration::new((infos.validity * 60 * 60 * 24).into(), 0); - let issuer_name = RdnSequence::default(); - let subject_name = RdnSequence::default(); + is_app_cert: bool, + ) -> Result { + // Convert input validity from days to seconds + let dur = Duration::new((issuer_infos.validity * 60 * 60 * 24).into(), 0); + + // Create Distinguished Names for issuer and subject + let issuer_name = issuer_infos.generate_dn()?; + + // Convert the public key value to a bit string let pub_key = BitString::from_bytes(pub_value).with_context(|| "Failed get public key raw value")?; + + // Generate the public key information field let pub_key_info = SubjectPublicKeyInfo { algorithm: AlgorithmIdentifier { oid: *algo_oid, @@ -230,9 +298,40 @@ fn construct_tbs_certificate( }, subject_public_key: pub_key, }; + + // Create certificate extensions + let mut extensions: Vec = Vec::new(); + + // Authority Key Identifier + // According to RGS, this extension must be present and set to non critical + // for application certificate + if is_app_cert { + extensions.push(Extension { + extn_id: rfc5280::ID_CE_AUTHORITY_KEY_IDENTIFIER, + critical: false, + extn_value: OctetString::new(issuer_name.to_der()?)? + }); + } + + // Key usage + // According to RGS, must be set to critical + // Bit 0 is set to indicate digitalSignature + let ku_value: [u8; 2] = [1, 0]; + extensions.push(Extension { + extn_id: rfc5280::ID_CE_KEY_USAGE, + critical: true, + extn_value: OctetString::new(ku_value.to_vec())? + }); + + // Generate the TBS Certificate structure + // According to RGS: + // - Version is set to V3 + // - Issuer and subject are set with distinguished names + // - Unique Identifiers are not used + // - Extensions are set let tbs = TbsCertificate { version: Version::V3, - serial_number: SerialNumber::new(&[1]) + serial_number: SerialNumber::new(serial) .with_context(|| "Failed to generate serial number")?, signature: AlgorithmIdentifier { oid: *algo_oid, @@ -240,11 +339,11 @@ fn construct_tbs_certificate( }, issuer: issuer_name, validity: Validity::from_now(dur).with_context(|| "Failed to generate validity date")?, - subject: subject_name, + subject: subject_name.clone(), subject_public_key_info: pub_key_info, issuer_unique_id: None, subject_unique_id: None, - extensions: None, + extensions: Some(extensions), }; Ok(tbs) } @@ -261,12 +360,19 @@ pub fn generate_root_ed25519( let ed25519_oid = ObjectIdentifier::new(ED25519_OID).with_context(|| "Failed to generate OID")?; - let tbs = match construct_tbs_certificate(infos, &keypair.public.to_bytes(), &ed25519_oid) { - Ok(tbs) => tbs, - Err(e) => { - return Err(anyhow!("Failed to construct TBS certificate: {e}")); - } - }; + // Root ED25519 certificate as serial number 1 + let serial: [u8; 1] = [1]; + + // Build subject DN + let subject = infos.generate_dn()?; + + let tbs = construct_tbs_certificate( + infos, + &subject, + &keypair.public.to_bytes(), + &serial, + &ed25519_oid, + true)?; let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; let mut prehashed = Sha512::new(); @@ -290,14 +396,28 @@ pub fn generate_root_ed25519( pub fn generate_root_dilithium( infos: &CertificateFields, -) -> Result<(SecretKey, PublicKey, Certificate), anyhow::Error> { +) -> Result<(SecretKey, oqs::sig::PublicKey, Certificate), anyhow::Error> { // Create the root CA Dilithium key pair let pq_scheme = Sig::new(Algorithm::Dilithium5)?; let (pk, sk) = pq_scheme.keypair()?; // OID value for dilithium-sha512 from IBM's networking OID range let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; - let tbs = construct_tbs_certificate(infos, &pk.clone().into_vec(), &dilithium5_oid)?; + + // Root Dilithium5 certificate as serial number 2 + let serial: [u8; 1] = [2]; + + // Build subject DN + let subject = infos.generate_dn()?; + + let tbs = construct_tbs_certificate( + infos, + &subject, + &pk.clone().into_vec(), + &serial, + &dilithium5_oid, + true)?; + let content = tbs.to_der()?; let signature = pq_scheme.sign(&content, &sk)?; @@ -374,20 +494,18 @@ pub fn generate_root( pub fn generate_signed_keypair( ca_keys: &HybridKeyPair, - _subject_infos: &CertificateFields, + subject_name: &RdnSequence, pki_infos: &CertificateFields, + is_app_key: bool ) -> Result { - // Create the subject name for the certificate - let subject = RdnSequence::default(); - // Generate ED25519 key and certificate // Create the ED25519 keypair let mut csprng = OsRng {}; let kp_ed = Keypair::generate(&mut csprng); // Construct a CSR for the ED25519 key - let csr_ed = kp_ed.generate_csr(&subject)?; + let csr_ed = kp_ed.generate_csr(subject_name)?; // Generate a certificate from the CSR - let cert_ed = generate_cert_from_csr(ca_keys, &csr_ed, pki_infos)?; + let cert_ed = generate_cert_from_csr(ca_keys, &csr_ed, pki_infos, is_app_key)?; // Generate Dilithium key and certificate // Create the Dilithium key pair @@ -398,9 +516,9 @@ pub fn generate_signed_keypair( public_key: pk_dl }; // Construct a CSR for the Dilithium key - let csr_dl = kp_pq.generate_csr(&subject)?; + let csr_dl = kp_pq.generate_csr(subject_name)?; // Generate a certificate from the CSR - let cert_dl = generate_cert_from_csr(ca_keys, &csr_dl, pki_infos)?; + let cert_dl = generate_cert_from_csr(ca_keys, &csr_dl, pki_infos, is_app_key)?; // Construct hybrid key pair Ok(HybridKeyPair { @@ -418,74 +536,75 @@ fn generate_cert_from_csr( ca_keys: &HybridKeyPair, csr: &CertReq, pki_info: &CertificateFields, + is_app_cert: bool ) -> Result { // Extract and validate info in the CSR - //TODO: validate CSR authenticity - - let _subject = csr.info.subject.clone(); - //TODO: validate subject + let subject = csr.info.subject.clone(); - let pub_key = match csr.info.public_key.subject_public_key.as_bytes() { - Some(k) => { - //TODO: validate key - k - } - None => { - return Err(anyhow!("Invalid public key in CSR")); - } - }; + let pub_key = csr.info.public_key.subject_public_key + .as_bytes().ok_or(anyhow!("Subject public key missing"))?; let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; // Build the certificate - if let Ok(oid) = csr + if let Ok(_) = csr .info .public_key .algorithm .assert_algorithm_oid(ed25519_oid) { - // Build the certificate - let tbs = construct_tbs_certificate(pki_info, pub_key, &oid)?; + // Validate CSR authenticity + let key = ed25519_dalek::PublicKey::from_bytes(pub_key)?; + if let Err(_) = key.verify_strict( + &csr.info.to_der()?, + &ed25519_dalek::Signature::from_bytes(csr.signature.raw_bytes())?) { + return Err(anyhow!("Invalid CSR signature")); + } - let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; - let mut prehashed = Sha512::new(); - prehashed.update(content); - let signature = ca_keys - .classic - .sign_prehashed(prehashed, None) - .with_context(|| "Failed to sign certificate content")?; + // Generate serial number + let mut serial = [0u8; 20]; + OsRng.fill_bytes(&mut serial); - let cert = Certificate { - tbs_certificate: tbs, - signature_algorithm: AlgorithmIdentifier { - oid: dilithium5_oid, - parameters: None, - }, - signature: BitString::from_bytes(&signature.to_bytes())?, - }; + // Build the certificate + let cert = ca_keys.classic.generate_certificate( + pki_info, + &subject, + pub_key, + &serial, + is_app_cert)?; Ok(cert) - } else if let Ok(oid) = csr + } else if let Ok(_) = csr .info .public_key .algorithm .assert_algorithm_oid(dilithium5_oid) { - let tbs = construct_tbs_certificate(pki_info, pub_key, &oid)?; - let content = tbs.to_der()?; - + // Validate CSR authenticity + oqs::init(); let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let signature = pq_scheme.sign(&content, &ca_keys.pq.private_key)?; + if let Err(_) = pq_scheme.verify( + &csr.info.to_der()?, + &pq_scheme.signature_from_bytes(csr.signature.raw_bytes()) + .ok_or(anyhow!("Failed to create signature"))?, + &pq_scheme.public_key_from_bytes(pub_key) + .ok_or(anyhow!("Failed to create public key"))? + ) { + return Err(anyhow!("Invalid CSR signature")); + } - let cert = Certificate { - tbs_certificate: tbs, - signature_algorithm: AlgorithmIdentifier { - oid: dilithium5_oid, - parameters: None, - }, - signature: BitString::from_bytes(&signature.into_vec())?, - }; + // Generate serial number + let mut serial = [0u8; 20]; + OsRng.fill_bytes(&mut serial); + + // Build the certificate + let cert = ca_keys.pq.generate_certificate( + pki_info, + &subject, + pub_key, + &serial, + is_app_cert)?; Ok(cert) } else { @@ -551,6 +670,13 @@ pub trait KeysasKey { fn message_sign(&self, message: &[u8]) -> Result, anyhow::Error>; /// Verify the signature of a message fn message_verify(&self, message: &[u8], signature: &[u8]) -> Result; + /// Generate a certificate from a CSR and signed with the key + fn generate_certificate(&self, + ca_infos: &CertificateFields, + subject_infos: &RdnSequence, + subject_key: &[u8], + serial: &[u8], + is_app_cert: bool) -> Result; } // Implementing new methods on top of dalek Keypair @@ -662,6 +788,41 @@ impl KeysasKey for Keypair { // If no error has been returned then the signature is valid Ok(true) } + + fn generate_certificate(&self, + ca_infos: &CertificateFields, + subject_infos: &RdnSequence, + subject_key: &[u8], + serial: &[u8], + is_app_cert: bool) -> Result { + + let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; + + // Build the certificate + let tbs = construct_tbs_certificate( + ca_infos, + subject_infos, + subject_key, + serial, + &ed25519_oid, + is_app_cert)?; + + let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; + let mut prehashed = Sha512::new(); + prehashed.update(content); + let signature = self.sign_prehashed(prehashed, None) + .with_context(|| "Failed to sign certificate content")?; + + let cert = Certificate { + tbs_certificate: tbs, + signature_algorithm: AlgorithmIdentifier { + oid: ed25519_oid, + parameters: None, + }, + signature: BitString::from_bytes(&signature.to_bytes())?, + }; + Ok(cert) + } } impl KeysasKey for KeysasPQKey { @@ -797,4 +958,38 @@ impl KeysasKey for KeysasPQKey { // If no error then the signature is valid Ok(true) } + + fn generate_certificate(&self, + ca_infos: &CertificateFields, + subject_infos: &RdnSequence, + subject_key: &[u8], + serial: &[u8], + is_app_cert: bool) -> Result { + + let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; + + // Build the certificate + let tbs = construct_tbs_certificate( + ca_infos, + subject_infos, + subject_key, + serial, + &dilithium5_oid, + is_app_cert)?; + + let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; + + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let signature = pq_scheme.sign(&content, &self.private_key)?; + + let cert = Certificate { + tbs_certificate: tbs, + signature_algorithm: AlgorithmIdentifier { + oid: dilithium5_oid, + parameters: None, + }, + signature: BitString::from_bytes(&signature.into_vec())?, + }; + Ok(cert) + } } From 35f17935fe7e7d7bc243d4289e7ef9131047833d Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Thu, 30 Mar 2023 15:39:59 +0200 Subject: [PATCH 002/160] Documentation addition --- keysas_lib/src/pki.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/keysas_lib/src/pki.rs b/keysas_lib/src/pki.rs index e3534af..5e23c9c 100644 --- a/keysas_lib/src/pki.rs +++ b/keysas_lib/src/pki.rs @@ -272,6 +272,7 @@ pub fn create_pki_dir(pki_dir: &String) -> Result<(), anyhow::Error> { Ok(()) } +/// Construct a information field for a certificate fn construct_tbs_certificate( issuer_infos: &CertificateFields, subject_name: &RdnSequence, @@ -492,6 +493,7 @@ pub fn generate_root( Ok(hk) } +/// Generate a signed hybrid keypair (ED25519 and Dilithium5) pub fn generate_signed_keypair( ca_keys: &HybridKeyPair, subject_name: &RdnSequence, @@ -532,6 +534,10 @@ pub fn generate_signed_keypair( }) } +/// Generate a X509 certificate from a CSR and a CA keypair +/// is_app_cert is set to true if it is an application certificate, otherwise it +/// is considered to be a CA certificate +/// The certificate generated will always be for DigitalSignature fn generate_cert_from_csr( ca_keys: &HybridKeyPair, csr: &CertReq, @@ -625,6 +631,7 @@ fn store_keypair( OsRng.fill_bytes(&mut salt); let mut iv = [0u8; 16]; OsRng.fill_bytes(&mut iv); + // Use default parameters for scrypt let params = match pbes2::Parameters::scrypt_aes256cbc( pkcs8::pkcs5::scrypt::Params::recommended(), &salt, From ca54e17150db4ec736a5269191efcacc96568c0c Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Thu, 30 Mar 2023 16:53:53 +0200 Subject: [PATCH 003/160] Propagated API change to admin --- Cargo.toml | 1 + keysas-admin/src-tauri/src/main.rs | 54 ++++++++++++++++++++++++++--- keysas-admin/src-tauri/src/store.rs | 7 ++-- keysas_lib/src/pki.rs | 50 ++++++++++++++------------ 4 files changed, 83 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ad0501d..fcf8576 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,5 @@ members = [ "keysas-sign/", "keysas-fido/", "keysas_lib/", + "keysas-admin/src-tauri" ] diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index 10defd7..5fc2c2a 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -729,8 +729,12 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, validity: String, admin_pwd: String, pki_dir: String) -> Result { // Validate user inputs - let infos = match validate_input_cert_fields(&org_name, &org_unit, - &country, &validity) { + let infos = match CertificateFields::from_fields ( + Some(&org_name), + Some(&org_unit), + Some(&country), + None, + Some(&validity)) { Ok(i) => i, Err(_) => { log::error!("Failed to validate user input"); @@ -758,7 +762,27 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, }; // Generate keysas station intermediate CA key pair - let st_ca_keys = match generate_signed_keypair(&root_keys, &infos, &infos) { + let ca_infos = match CertificateFields::from_fields( + None, + None, + None, + Some("Station CA"), + None + ) { + Ok(i) => i, + Err(e) => { + log::error!("Failed to generate station CA name field: {e}"); + return Err(String::from("PKI error")); + } + }; + let ca_name = match ca_infos.generate_dn() { + Ok(n) => n, + Err(e) => { + log::error!("Failed to generate distinguished name for station CA: {e}"); + return Err(String::from("PKI error")); + } + }; + let st_ca_keys = match generate_signed_keypair(&root_keys, &ca_name, &infos, false) { Ok(kp) => kp, Err(e) => { log::error!("Failed to generate intermediate CA for station: {e}"); @@ -769,7 +793,29 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, // Save keys to directory // Generate USB signing key pair - let usb_keys = match generate_signed_keypair(&root_keys, &infos, &infos) { + let usb_infos = match CertificateFields::from_fields( + None, + None, + None, + Some("USB admin"), + None + ) { + Ok(i) => i, + Err(e) => { + log::error!("Failed to generate station CA name field: {e}"); + return Err(String::from("PKI error")); + } + }; + let usb_name = match usb_infos.generate_dn() { + Ok(n) => n, + Err(e) => { + log::error!("Failed to generate distinguished name for station CA: {e}"); + return Err(String::from("PKI error")); + } + }; + let subject_usb = infos.clone(); + let usb_keys = match generate_signed_keypair( + &root_keys, &usb_name, &infos, true) { Ok(kp) => kp, Err(e) => { log::error!("Failed to generate USB signing key pair: {e}"); diff --git a/keysas-admin/src-tauri/src/store.rs b/keysas-admin/src-tauri/src/store.rs index 6d8f24d..270c8cb 100644 --- a/keysas-admin/src-tauri/src/store.rs +++ b/keysas-admin/src-tauri/src/store.rs @@ -285,8 +285,11 @@ pub fn set_pki_config(pki_dir: &String, infos: &CertificateFields) -> Result<(), VALUES ('directory', '{}'), ('org_name', '{}'), \ ('org_unit', '{}'), ('country', '{}'), \ ('validity', '{}');", - pki_dir, &infos.org_name, &infos.org_unit, - &infos.country, &infos.validity); + pki_dir, + infos.org_name.as_ref().unwrap_or(&String::from("")).clone(), + infos.org_unit.as_ref().unwrap_or(&String::from("")), + infos.country.as_ref().unwrap_or(&String::from("")), + &infos.validity.unwrap_or(0)); log::debug!("Query: {}", query); connection.execute(query)?; return Ok(()); diff --git a/keysas_lib/src/pki.rs b/keysas_lib/src/pki.rs index 5e23c9c..dc1a52f 100644 --- a/keysas_lib/src/pki.rs +++ b/keysas_lib/src/pki.rs @@ -128,13 +128,13 @@ use x509_cert::time::Validity; // /// Structure containing informations to build the certificate -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct CertificateFields { pub org_name: Option, pub org_unit: Option, pub country: Option, pub common_name: Option, - pub validity: u32, + pub validity: Option, } #[derive(Debug)] @@ -161,36 +161,37 @@ impl CertificateFields { /// - Test if country is 2 letters long, if less return error, if more shorten it to the first two letters /// - Test if validity can be converted to u32, if not generate error /// - Test if sigAlgo is either ed25519 or ed448, if not defaut to ed25519 - fn validate_input_cert_fields<'a>( - org_name: &'a String, - org_unit: &'a String, - country: &'a String, - common_name: &'a String, - validity: &'a str, + pub fn from_fields<'a>( + org_name: Option<&'a str>, + org_unit: Option<&'a str>, + country: Option<&'a str>, + common_name: Option<&'a str>, + validity: Option<&'a str>, ) -> Result { // Test if country is 2 letters long - let cn = match country.len() { - 0 | 1 => return Err(anyhow!("Failed to parse length field")), - 2 => country.to_string(), - _ => country[..2].to_string(), - }; + let cn = country.map(|name| + match name.len() { + 0|1 => return Err(anyhow!("Invalid country name")), + 2 => Ok(name.to_string()), + _ => Ok(name[..2].to_string()) + } + ).transpose()?; + // Test if validity can be converted to u32 - let val = match validity.parse::() { - Ok(v) => v, - Err(_) => return Err(anyhow!("Failed to parse validity field")), - }; + let val = validity.map(|value| value.parse::()) + .transpose()?; Ok(CertificateFields { - org_name: Some(org_name.to_string()), - org_unit: Some(org_unit.to_string()), - country: Some(cn), - common_name: Some(common_name.to_string()), + org_name: org_name.map(|name| name.to_string()), + org_unit:org_unit.map(|name| name.to_string()), + country: cn, + common_name: common_name.map(|name| name.to_string()), validity: val, }) } /// Generate a distinghuished name from the input fields for the certificate - fn generate_dn(&self) -> Result { + pub fn generate_dn(&self) -> Result { let mut name = String::new(); // Add country name @@ -282,7 +283,10 @@ fn construct_tbs_certificate( is_app_cert: bool, ) -> Result { // Convert input validity from days to seconds - let dur = Duration::new((issuer_infos.validity * 60 * 60 * 24).into(), 0); + let dur = match issuer_infos.validity { + Some(value) => Duration::new((value * 60 * 60 * 24).into(), 0), + None => {return Err(anyhow!("Invalid validity value"));} + }; // Create Distinguished Names for issuer and subject let issuer_name = issuer_infos.generate_dn()?; From 44ee17b58a6334ad656cc3f5537e1c2add9d90d8 Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Fri, 31 Mar 2023 10:27:02 +0200 Subject: [PATCH 004/160] X509: PKI code refactoring (clippy, doc) PKI refactoring Clippy improvements --- keysas-admin/src-tauri/src/main.rs | 39 +- keysas-admin/src-tauri/src/store.rs | 2 +- keysas_lib/src/certificate_field.rs | 219 +++++++ keysas_lib/src/keysas_hybrid_keypair.rs | 271 ++++++++ keysas_lib/src/keysas_key.rs | 441 +++++++++++++ keysas_lib/src/lib.rs | 3 + keysas_lib/src/pki.rs | 818 +----------------------- keysas_lib/tests/pki_test.rs | 4 +- 8 files changed, 989 insertions(+), 808 deletions(-) create mode 100644 keysas_lib/src/certificate_field.rs create mode 100644 keysas_lib/src/keysas_hybrid_keypair.rs create mode 100644 keysas_lib/src/keysas_key.rs diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index 5fc2c2a..6d6f342 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -31,6 +31,8 @@ use crate::errors::*; use std::path::Path; use async_std::task; +use keysas_lib::certificate_field::CertificateFields; +use keysas_lib::keysas_hybrid_keypair::HybridKeyPair; use nom::bytes::complete::take_until; use nom::IResult; use sha2::{Digest, Sha256}; @@ -753,13 +755,23 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, } // Generate root key and save them in PKCS12 format - let root_keys = match generate_root(&infos, &pki_dir, &admin_pwd) { + let root_keys = match HybridKeyPair::generate_root(&infos, &pki_dir, &admin_pwd) { Ok(kp) => kp, Err(e) => { log::error!("Failed to generate PKI root keys: {e}"); return Err(String::from("PKI error")); } }; + + // Save keys + if let Err(e) = root_keys.save( + "root", + &Path::new(&(pki_dir.to_owned() + "/CA/root")), + &Path::new(&(pki_dir.to_owned() + "/CA/root")), + &admin_pwd) { + log::error!("Failed to save root key to disk: {e}"); + return Err(String::from("PKI error")); + } // Generate keysas station intermediate CA key pair let ca_infos = match CertificateFields::from_fields( @@ -782,15 +794,22 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, return Err(String::from("PKI error")); } }; - let st_ca_keys = match generate_signed_keypair(&root_keys, &ca_name, &infos, false) { + let st_ca_keys = match HybridKeyPair::generate_signed_keypair(&root_keys, &ca_name, &infos, false) { Ok(kp) => kp, Err(e) => { log::error!("Failed to generate intermediate CA for station: {e}"); return Err(String::from("PKI error")); } }; - - // Save keys to directory + // Save keys + if let Err(e) = st_ca_keys.save( + "st-ca", + &Path::new(&(pki_dir.to_owned() + "/CA/st")), + &Path::new(&(pki_dir.to_owned() + "/CA/st")), + &admin_pwd) { + log::error!("Failed to save station CA key to disk: {e}"); + return Err(String::from("PKI error")); + } // Generate USB signing key pair let usb_infos = match CertificateFields::from_fields( @@ -813,8 +832,7 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, return Err(String::from("PKI error")); } }; - let subject_usb = infos.clone(); - let usb_keys = match generate_signed_keypair( + let usb_keys = match HybridKeyPair::generate_signed_keypair( &root_keys, &usb_name, &infos, true) { Ok(kp) => kp, Err(e) => { @@ -822,6 +840,15 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, return Err(String::from("PKI error")); } }; + // Save keys + if let Err(e) = usb_keys.save( + "usb", + &Path::new(&(pki_dir.to_owned() + "/CA/usb")), + &Path::new(&(pki_dir.to_owned() + "/CA/usb")), + &admin_pwd) { + log::error!("Failed to save station CA key to disk: {e}"); + return Err(String::from("PKI error")); + } Ok(String::from("PKI created")) } diff --git a/keysas-admin/src-tauri/src/store.rs b/keysas-admin/src-tauri/src/store.rs index 270c8cb..dc07cdf 100644 --- a/keysas-admin/src-tauri/src/store.rs +++ b/keysas-admin/src-tauri/src/store.rs @@ -15,7 +15,7 @@ use anyhow::anyhow; use sqlite::Connection; use serde::Serialize; -use keysas_lib::pki::*; +use keysas_lib::certificate_field::CertificateFields; static STORE_HANDLE: Mutex> = Mutex::new(None); diff --git a/keysas_lib/src/certificate_field.rs b/keysas_lib/src/certificate_field.rs new file mode 100644 index 0000000..753124d --- /dev/null +++ b/keysas_lib/src/certificate_field.rs @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * The "keysas-lib". + * + * (C) Copyright 2019-2023 Stephane Neveu, Luc Bonnafoux + * + * This file contains various funtions + * for building the keysas_lib. + */ + + #![warn(unused_extern_crates)] + #![forbid(non_shorthand_field_patterns)] + #![warn(dead_code)] + #![warn(missing_debug_implementations)] + #![warn(missing_copy_implementations)] + #![warn(trivial_casts)] + #![warn(trivial_numeric_casts)] + #![warn(unused_extern_crates)] + #![warn(unused_import_braces)] #![warn(unused_qualifications)] + #![warn(variant_size_differences)] + #![forbid(private_in_public)] + #![warn(overflowing_literals)] + #![warn(deprecated)] + #![warn(unused_imports)] + + use anyhow::{anyhow, Context}; + use pkcs8::der::Encode; +use pkcs8::der::asn1::OctetString; + use pkcs8::der::oid::db::rfc5280; + use x509_cert::ext::Extension; + use std::str::FromStr; +use std::time::Duration; + use x509_cert::certificate::*; + use x509_cert::der::asn1::BitString; + use x509_cert::name::RdnSequence; + use x509_cert::serial_number::SerialNumber; + use x509_cert::spki::AlgorithmIdentifier; + use x509_cert::spki::ObjectIdentifier; + use x509_cert::spki::SubjectPublicKeyInfo; + use x509_cert::time::Validity; + +/// Structure containing informations to build the certificate +#[derive(Debug, Clone)] +pub struct CertificateFields { + pub org_name: Option, + pub org_unit: Option, + pub country: Option, + pub common_name: Option, + pub validity: Option, +} + +impl CertificateFields { + /// Validate user input and construct a certificate fields structure that can be used + /// to build the certificates of the PKI. + /// The checks done are : + /// - Test if country is 2 letters long, if less return error, if more shorten it to the first two letters + /// - Test if validity can be converted to u32, if not generate error + /// - Test if sigAlgo is either ed25519 or ed448, if not defaut to ed25519 + pub fn from_fields<'a>( + org_name: Option<&'a str>, + org_unit: Option<&'a str>, + country: Option<&'a str>, + common_name: Option<&'a str>, + validity: Option<&'a str>, + ) -> Result { + // Test if country is 2 letters long + let cn = country.map(|name| + match name.len() { + 0|1 => Err(anyhow!("Invalid country name")), + 2 => Ok(name.to_string()), + _ => Ok(name[..2].to_string()) + } + ).transpose()?; + + // Test if validity can be converted to u32 + let val = validity.map(|value| value.parse::()) + .transpose()?; + + Ok(CertificateFields { + org_name: org_name.map(|name| name.to_string()), + org_unit:org_unit.map(|name| name.to_string()), + country: cn, + common_name: common_name.map(|name| name.to_string()), + validity: val, + }) + } + + /// Generate a distinghuished name from the input fields for the certificate + pub fn generate_dn(&self) -> Result { + let mut name = String::new(); + + // Add country name + if let Some(cn) = &self.country { + name.push_str("C="); + name.push_str(cn); + name.push(','); + } + + // Add organisation name + if let Some(oa) = &self.org_name { + if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { + name.push(','); + } + name.push_str("O="); + name.push_str(oa); + name.push(','); + } + + // Add organisational unit + if let Some(ou) = &self.org_unit { + if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { + name.push(','); + } + name.push_str("OU="); + name.push_str(ou); + name.push(','); + } + + // Add common name + if let Some(co) = &self.common_name { + if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { + name.push(','); + } + name.push_str("CN="); + name.push_str(co); + name.push(','); + } + + // Remove trailing ',' if there is one + if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { + name.pop(); + } + + let rdn = RdnSequence::from_str(&name)?; + Ok(rdn) + } + + /// Construct a information field for a certificate using the issuer CertificateInfos + /// and the subject name and key + /// The serial number is supplied by the caller that must ensure its uniqueness + pub fn construct_tbs_certificate( + &self, + subject_name: &RdnSequence, + pub_value: &[u8], + serial: &[u8], + algo_oid: &ObjectIdentifier, + is_app_cert: bool, + ) -> Result { + // Convert input validity from days to seconds + let dur = match self.validity { + Some(value) => Duration::new((value * 60 * 60 * 24).into(), 0), + None => {return Err(anyhow!("Invalid validity value"));} + }; + + // Create Distinguished Names for issuer and subject + let issuer_name = self.generate_dn()?; + + // Convert the public key value to a bit string + let pub_key = + BitString::from_bytes(pub_value).with_context(|| "Failed get public key raw value")?; + + // Generate the public key information field + let pub_key_info = SubjectPublicKeyInfo { + algorithm: AlgorithmIdentifier { + oid: *algo_oid, + parameters: None, + }, + subject_public_key: pub_key, + }; + + // Create certificate extensions + let mut extensions: Vec = Vec::new(); + + // Authority Key Identifier + // According to RGS, this extension must be present and set to non critical + // for application certificate + if is_app_cert { + extensions.push(Extension { + extn_id: rfc5280::ID_CE_AUTHORITY_KEY_IDENTIFIER, + critical: false, + extn_value: OctetString::new(issuer_name.to_der()?)? + }); + } + + // Key usage + // According to RGS, must be set to critical + // Bit 0 is set to indicate digitalSignature + let ku_value: [u8; 2] = [1, 0]; + extensions.push(Extension { + extn_id: rfc5280::ID_CE_KEY_USAGE, + critical: true, + extn_value: OctetString::new(ku_value.to_vec())? + }); + + // Generate the TBS Certificate structure + // According to RGS: + // - Version is set to V3 + // - Issuer and subject are set with distinguished names + // - Unique Identifiers are not used + // - Extensions are set + let tbs = TbsCertificate { + version: Version::V3, + serial_number: SerialNumber::new(serial) + .with_context(|| "Failed to generate serial number")?, + signature: AlgorithmIdentifier { + oid: *algo_oid, + parameters: None, + }, + issuer: issuer_name, + validity: Validity::from_now(dur).with_context(|| "Failed to generate validity date")?, + subject: subject_name.clone(), + subject_public_key_info: pub_key_info, + issuer_unique_id: None, + subject_unique_id: None, + extensions: Some(extensions), + }; + Ok(tbs) + } +} \ No newline at end of file diff --git a/keysas_lib/src/keysas_hybrid_keypair.rs b/keysas_lib/src/keysas_hybrid_keypair.rs new file mode 100644 index 0000000..9c3f117 --- /dev/null +++ b/keysas_lib/src/keysas_hybrid_keypair.rs @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * The "keysas-lib". + * + * (C) Copyright 2019-2023 Stephane Neveu, Luc Bonnafoux + * + * This file contains various funtions + * for building the keysas_lib. + */ + + #![warn(unused_extern_crates)] + #![forbid(non_shorthand_field_patterns)] + #![warn(dead_code)] + #![warn(missing_debug_implementations)] + #![warn(missing_copy_implementations)] + #![warn(trivial_casts)] + #![warn(trivial_numeric_casts)] + #![warn(unused_extern_crates)] + #![warn(unused_import_braces)] + #![warn(unused_qualifications)] + #![warn(variant_size_differences)] + #![forbid(private_in_public)] + #![warn(overflowing_literals)] + #![warn(deprecated)] + #![warn(unused_imports)] + + use anyhow::Context; + use ed25519_dalek::Digest; + use ed25519_dalek::Keypair; + use ed25519_dalek::Sha512; + use oqs::sig::Algorithm; + use oqs::sig::SecretKey; + use oqs::sig::Sig; + use pkcs8::LineEnding; + use pkcs8::der::Encode; +use rand_dl::rngs::OsRng; + use std::fs::File; + use std::io::Write; + use std::path::Path; + use x509_cert::certificate::*; + use x509_cert::der::asn1::BitString; + use x509_cert::der::EncodePem; + use x509_cert::name::RdnSequence; + use x509_cert::spki::AlgorithmIdentifier; + use x509_cert::spki::ObjectIdentifier; + +use crate::keysas_key::KeysasPQKey; +use crate::keysas_key::KeysasKey; +use crate::certificate_field::CertificateFields; +use crate::pki::DILITHIUM5_OID; +use crate::pki::ED25519_OID; +use crate::pki::generate_cert_from_csr; + +/// Keysas `HybridKeyPair` +/// +/// Structure containing both a ED25519 and a Dilithium5 keypair +/// The structure also contains the associated certificates +#[derive(Debug)] +pub struct HybridKeyPair { + pub classic: Keypair, + pub classic_cert: Certificate, + pub pq: KeysasPQKey, + pub pq_cert: Certificate, +} + + /// Generate the root certificate of the PKI from a private key and information + /// fields + /// The function returns the certificate or an openssl error + fn generate_root_ed25519( + infos: &CertificateFields, + ) -> Result<(Keypair, Certificate), anyhow::Error> { + // Create the root CA Ed25519 key pair + let mut csprng = OsRng {}; + let keypair = Keypair::generate(&mut csprng); + let ed25519_oid = + ObjectIdentifier::new(ED25519_OID).with_context(|| "Failed to generate OID")?; + + // Root ED25519 certificate as serial number 1 + let serial: [u8; 1] = [1]; + + // Build subject DN + let subject = infos.generate_dn()?; + + let tbs = infos.construct_tbs_certificate( + &subject, + &keypair.public.to_bytes(), + &serial, + &ed25519_oid, + true)?; + + let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; + let mut prehashed = Sha512::new(); + prehashed.update(content); + let sig = keypair + .sign_prehashed(prehashed, None) + .with_context(|| "Failed to sign certificate content")?; + + let cert = Certificate { + tbs_certificate: tbs, + signature_algorithm: AlgorithmIdentifier { + oid: ed25519_oid, + parameters: None, + }, + signature: BitString::from_bytes(&sig.to_bytes()) + .with_context(|| "Failed to convert signature to bytes")?, + }; + + Ok((keypair, cert)) + } + + fn generate_root_dilithium( + infos: &CertificateFields, + ) -> Result<(SecretKey, oqs::sig::PublicKey, Certificate), anyhow::Error> { + // Create the root CA Dilithium key pair + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let (pk, sk) = pq_scheme.keypair()?; + + // OID value for dilithium-sha512 from IBM's networking OID range + let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; + + // Root Dilithium5 certificate as serial number 2 + let serial: [u8; 1] = [2]; + + // Build subject DN + let subject = infos.generate_dn()?; + + let tbs = infos.construct_tbs_certificate( + &subject, + &pk.clone().into_vec(), + &serial, + &dilithium5_oid, + true)?; + + let content = tbs.to_der()?; + + let signature = pq_scheme.sign(&content, &sk)?; + + let cert = Certificate { + tbs_certificate: tbs, + signature_algorithm: AlgorithmIdentifier { + oid: dilithium5_oid, + parameters: None, + }, + signature: BitString::from_bytes(&signature.into_vec())?, + }; + + Ok((sk, pk, cert)) + } + +impl HybridKeyPair { + /// Save the keypair to disk + /// The keys will be saved in DER encoded PKCS8 files at: keys_path/name-{cl|pq}.p8 + /// The certificates will be saved in PEM files at: certs_path/name-{cl|pq}.pem + /// pwd is used for encrypting the PKCS8 files + pub fn save(&self, name: &str, keys_path: &Path, _certs_path: &Path, pwd: &str) -> Result<(), anyhow::Error> { + // Save keys + let cl_key_path = keys_path.join(name.to_owned() + "-cl.p8"); + self.classic.save_keys(&cl_key_path, pwd)?; + + let pq_key_path = keys_path.join(name.to_owned() + "-pq.p8"); + self.pq.save_keys(&pq_key_path, pwd)?; + + // Save certificates + //let cl_cert_path = certs_path.join(name.to_owned() + "-cl.pem"); + //self.classic_cert.save_keys(&cl_cert_path, pwd)?; + + //let pq_cert_path = certs_path.join(name.to_owned() + "-pq.pem"); + //self.pq.save_keys(&pq_cert_path, pwd)?; + + Ok(()) + } + + /// Generate PKI root keys + pub fn generate_root( + infos: &CertificateFields, + pki_dir: &str, + pwd: &str, + ) -> Result { + // Generate root ED25519 key and certificate + let (kp_ed, cert_ed) = + generate_root_ed25519(infos).with_context(|| "ED25519 generation failed")?; + + // Generate root Dilithium key and certificate + let (sk_dl, pk_dl, cert_dl) = + generate_root_dilithium(infos).context("Dilithium generation failed")?; + + // Construct hybrid key pair + let hk = HybridKeyPair { + classic: kp_ed, + classic_cert: cert_ed, + pq: KeysasPQKey { + private_key: sk_dl, + public_key: pk_dl + }, + pq_cert: cert_dl, + }; + + // Save hybrid key pair to disk + hk.classic.save_keys( + Path::new(&(pki_dir.to_owned() + "/CA/root-priv-cl.p8")), + pwd) + .context("ED25519 storing failed")?; + + hk.pq.save_keys( + Path::new(&(pki_dir.to_owned() + "/CA/root-priv-pq.p8")), + pwd) + .context("Dilithium storing failed")?; + + // Save certificate pair to disk + let mut out_cl = File::create(pki_dir.to_owned() + "/CA/root-cert-cl.pem")?; + write!( + out_cl, + "{}", + hk.classic_cert + .to_pem(LineEnding::LF) + .context("ED25519 certificate to pem failed")? + )?; + + let mut out_pq = File::create(pki_dir.to_owned() + "/CA/root-cert-pq.pem")?; + write!( + out_pq, + "{}", + hk.pq_cert + .to_pem(LineEnding::LF) + .context("Dilithium certificate to pem failed")? + )?; + + Ok(hk) + } + + /// Generate a signed hybrid keypair (ED25519 and Dilithium5) + pub fn generate_signed_keypair( + ca_keys: &HybridKeyPair, + subject_name: &RdnSequence, + pki_infos: &CertificateFields, + is_app_key: bool + ) -> Result { + // Generate ED25519 key and certificate + // Create the ED25519 keypair + let mut csprng = OsRng {}; + let kp_ed = Keypair::generate(&mut csprng); + // Construct a CSR for the ED25519 key + let csr_ed = kp_ed.generate_csr(subject_name)?; + // Generate a certificate from the CSR + let cert_ed = generate_cert_from_csr(ca_keys, &csr_ed, pki_infos, is_app_key)?; + + // Generate Dilithium key and certificate + // Create the Dilithium key pair + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let (pk_dl, sk_dl) = pq_scheme.keypair()?; + let kp_pq = KeysasPQKey { + private_key: sk_dl, + public_key: pk_dl + }; + // Construct a CSR for the Dilithium key + let csr_dl = kp_pq.generate_csr(subject_name)?; + // Generate a certificate from the CSR + let cert_dl = generate_cert_from_csr(ca_keys, &csr_dl, pki_infos, is_app_key)?; + + // Construct hybrid key pair + Ok(HybridKeyPair { + classic: kp_ed, + classic_cert: cert_ed, + pq: KeysasPQKey { + private_key: kp_pq.private_key, + public_key: kp_pq.public_key + }, + pq_cert: cert_dl, + }) + } +} \ No newline at end of file diff --git a/keysas_lib/src/keysas_key.rs b/keysas_lib/src/keysas_key.rs new file mode 100644 index 0000000..6b5056f --- /dev/null +++ b/keysas_lib/src/keysas_key.rs @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * The "keysas-lib". + * + * (C) Copyright 2019-2023 Stephane Neveu, Luc Bonnafoux + * + * This file contains various funtions + * for building the keysas_lib. + */ + + #![warn(unused_extern_crates)] + #![forbid(non_shorthand_field_patterns)] + #![warn(dead_code)] + #![warn(missing_debug_implementations)] + #![warn(missing_copy_implementations)] + #![warn(trivial_casts)] + #![warn(trivial_numeric_casts)] + #![warn(unused_extern_crates)] + #![warn(unused_import_braces)] + #![warn(unused_qualifications)] + #![warn(variant_size_differences)] + #![forbid(private_in_public)] + #![warn(overflowing_literals)] + #![warn(deprecated)] + #![warn(unused_imports)] + +use anyhow::{anyhow, Context}; +use ed25519_dalek::Digest; +use ed25519_dalek::Keypair; +use ed25519_dalek::Sha512; +use oqs::sig::Algorithm; +use oqs::sig::SecretKey; +use oqs::sig::Sig; +use pkcs8::der::asn1::SetOfVec; +use pkcs8::EncryptedPrivateKeyInfo; +use pkcs8::PrivateKeyInfo; +use pkcs8::pkcs5::pbes2; +use std::fs; +use std::path::Path; +use x509_cert::certificate::*; +use x509_cert::der::asn1::BitString; +use x509_cert::der::Encode; +use x509_cert::name::RdnSequence; +use x509_cert::request::CertReq; +use x509_cert::request::CertReqInfo; +use x509_cert::spki::AlgorithmIdentifier; +use x509_cert::spki::ObjectIdentifier; +use x509_cert::spki::SubjectPublicKeyInfo; +use rand_dl::RngCore; +use rand_dl::rngs::OsRng; + +use crate::certificate_field::CertificateFields; +use crate::pki::ED25519_OID; +use crate::pki::DILITHIUM5_OID; + +#[derive(Debug)] +pub struct KeysasPQKey { + pub private_key: SecretKey, + pub public_key: oqs::sig::PublicKey, +} + +/// Store a keypair in a PKCS8 file with a password +fn store_keypair( + prk: &[u8], + pbk: &[u8], + oid: ObjectIdentifier, + pwd: &str, + path: &Path, +) -> Result<(), anyhow::Error> { + //Initialize key wrap function parameters + let mut salt = [0u8; 16]; + OsRng.fill_bytes(&mut salt); + let mut iv = [0u8; 16]; + OsRng.fill_bytes(&mut iv); + // Use default parameters for scrypt + let params = match pbes2::Parameters::scrypt_aes256cbc( + pkcs8::pkcs5::scrypt::Params::recommended(), + &salt, + &iv, + ) { + Ok(p) => p, + Err(e) => { + return Err(anyhow!("Failed to generate scrypt parameter: {e}")); + } + }; + + let pk_info = PrivateKeyInfo { + algorithm: pkcs8::AlgorithmIdentifierRef { + oid, + parameters: None, + }, + private_key: prk, + public_key: Some(pbk), + }; + + let pk_encrypted = match pk_info.encrypt_with_params(params, pwd) { + Ok(pk) => pk, + Err(e) => { + log::error!("Failed to encrypt private key: {e}"); + return Err(anyhow!("Failed to encrypt private key")); + } + }; + + pk_encrypted.write_der_file(path)?; + + Ok(()) +} + +/// Generic trait to abstract the main functions of the ED25519 and Dilthium keys +pub trait KeysasKey { + /// Load keypair from a DER encoded PKCS8 file protected with a password + fn load_keys(path: &Path, pwd: &str) -> Result; + /// Save keypair in a DER encoded PKCS8 file protected with a password + fn save_keys(&self, path: &Path, pwd: &str) -> Result<(), anyhow::Error>; + /// Generate a Certificate Signing Request for the keypair and with the subject name + fn generate_csr(&self, subject: &RdnSequence) -> Result; + /// Sign a message + fn message_sign(&self, message: &[u8]) -> Result, anyhow::Error>; + /// Verify the signature of a message + fn message_verify(&self, message: &[u8], signature: &[u8]) -> Result; + /// Generate a certificate from a CSR and signed with the key + fn generate_certificate(&self, + ca_infos: &CertificateFields, + subject_infos: &RdnSequence, + subject_key: &[u8], + serial: &[u8], + is_app_cert: bool) -> Result; +} + +// Implementing new methods on top of dalek Keypair +impl KeysasKey for Keypair { + fn load_keys(path: &Path, pwd: &str) -> Result { + // Load the pkcs8 from file + let cipher = fs::read(path)?; + + let enc_pk = match EncryptedPrivateKeyInfo::try_from(cipher.as_slice()) { + Ok(ep) => ep, + Err(e) => { + return Err(anyhow!("Failed to parse EncryptedPrivateKeyInfo: {e}")); + } + }; + let pk = match enc_pk.decrypt(pwd) { + Ok(p) => p, + Err(e) => { + return Err(anyhow!("Failed to decrypt document: {e}")); + } + }; + let decoded_pk: PrivateKeyInfo = match pk.decode_msg() { + Ok(parsed_pk) => parsed_pk, + Err(e) => { + return Err(anyhow!( + "Failed to decode asn.1 format for private key: {e}" + )); + } + }; + // ed25519 is only 32 bytes long + if decoded_pk.private_key.len() == 32 { + match ed25519_dalek::SecretKey::from_bytes(decoded_pk.private_key) { + Ok(secret_key) => { + Ok(Keypair { + public: (&(secret_key)).into(), + secret: secret_key, + }) + } + Err(e) => { + Err(anyhow!( + "Cannot parse private key CLASSIC/ed25519-dalek from pkcs#8: {e}" + )) + } + } + } else { + Err(anyhow!("Key is not 32 bytes long")) + } + } + + fn save_keys(&self, path: &Path, pwd: &str) -> Result<(), anyhow::Error> { + let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; + + store_keypair( + self.secret.as_bytes(), + self.public.as_bytes(), + ed25519_oid, + pwd, + path + ) + } + + fn generate_csr(&self, subject: &RdnSequence) -> Result { + let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; + + let pub_key = BitString::from_bytes(&self.public.to_bytes()) + .with_context(|| "Failed get public key raw value")?; + + let info = CertReqInfo { + version: x509_cert::request::Version::V1, + subject: subject.to_owned(), + public_key: SubjectPublicKeyInfo { + algorithm: AlgorithmIdentifier { + oid: ed25519_oid, + parameters: None, + }, + subject_public_key: pub_key, + }, + attributes: SetOfVec::new(), + }; + + let content = info.to_der().with_context(|| "Failed to convert to DER")?; + let mut prehashed = Sha512::new(); + prehashed.update(content); + let signature = self + .sign_prehashed(prehashed, None) + .with_context(|| "Failed to sign certificate content")?; + + let csr = CertReq { + info, + algorithm: AlgorithmIdentifier { + oid: ed25519_oid, + parameters: None, + }, + signature: BitString::from_bytes(&signature.to_bytes())?, + }; + + Ok(csr) + } + + fn message_sign(&self, message: &[u8]) -> Result, anyhow::Error> { + let mut prehashed = Sha512::new(); + prehashed.update(message); + let signature = self.sign_prehashed(prehashed, None)?; + Ok(signature.to_bytes().to_vec()) + } + + fn message_verify(&self, message: &[u8], signature: &[u8]) -> Result { + let sig = ed25519_dalek::Signature::from_bytes(signature)?; + self.verify(message, &sig)?; + // If no error has been returned then the signature is valid + Ok(true) + } + + fn generate_certificate(&self, + ca_infos: &CertificateFields, + subject_infos: &RdnSequence, + subject_key: &[u8], + serial: &[u8], + is_app_cert: bool) -> Result { + + let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; + + // Build the certificate + let tbs = ca_infos.construct_tbs_certificate( + subject_infos, + subject_key, + serial, + &ed25519_oid, + is_app_cert)?; + + let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; + let mut prehashed = Sha512::new(); + prehashed.update(content); + let signature = self.sign_prehashed(prehashed, None) + .with_context(|| "Failed to sign certificate content")?; + + let cert = Certificate { + tbs_certificate: tbs, + signature_algorithm: AlgorithmIdentifier { + oid: ed25519_oid, + parameters: None, + }, + signature: BitString::from_bytes(&signature.to_bytes())?, + }; + Ok(cert) + } +} + +impl KeysasKey for KeysasPQKey { + fn load_keys(path: &Path, pwd: &str) -> Result { + // Load the pkcs8 from file + let cipher = fs::read(path)?; + let enc_pk = match EncryptedPrivateKeyInfo::try_from(cipher.as_slice()) { + Ok(ep) => ep, + Err(e) => { + return Err(anyhow!("Failed to parse EncryptedPrivateKeyInfo: {e}")); + } + }; + let pk = match enc_pk.decrypt(pwd) { + Ok(p) => p, + Err(e) => { + return Err(anyhow!("Failed to decrypt document: {e}")); + } + }; + let decoded_pk: PrivateKeyInfo = match pk.decode_msg() { + Ok(parsed_pk) => parsed_pk, + Err(e) => { + return Err(anyhow!( + "Failed to decode asn.1 format for private key: {e}" + )); + } + }; + oqs::init(); + let scheme = match oqs::sig::Sig::new(oqs::sig::Algorithm::Dilithium5) { + Ok(scheme) => scheme, + Err(e) => { + return Err(anyhow!( + "OQS error: cannot initialize Dililthium5 scheme: {e}" + )) + } + }; + let tmp_pq_sk = match oqs::sig::Sig::secret_key_from_bytes(&scheme, decoded_pk.private_key) + { + Some(tmp_sig_sk) => tmp_sig_sk, + None => { + return Err(anyhow!( + "Cannot parse secret pq private key from decode value" + )); + } + }; + let secret_key = tmp_pq_sk.to_owned(); + match decoded_pk.public_key { + Some(public_key_u8) => { + let public_key = match oqs::sig::Sig::public_key_from_bytes(&scheme, public_key_u8) + { + Some(p) => p, + None => { + return Err(anyhow!("Cannot parse PQC public key from pkcs#8")); + } + }; + Ok(KeysasPQKey { + private_key: secret_key, + public_key: public_key.to_owned(), + }) + } + None => { + Err(anyhow!("No PQC public key found in pkcs#8 format")) + }, + } + } + + fn save_keys(&self, path: &Path, pwd: &str) -> Result<(), anyhow::Error> { + let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; + + store_keypair( + &self.private_key.clone().into_vec(), + &self.public_key.clone().into_vec(), + ed25519_oid, + pwd, + path + ) + } + + fn generate_csr(&self, subject: &RdnSequence) -> Result { + let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; + + let pub_key = BitString::from_bytes(&self.public_key.clone().into_vec()) + .with_context(|| "Failed get public key raw value")?; + + let info = CertReqInfo { + version: x509_cert::request::Version::V1, + subject: subject.to_owned(), + public_key: SubjectPublicKeyInfo { + algorithm: AlgorithmIdentifier { + oid: dilithium5_oid, + parameters: None, + }, + subject_public_key: pub_key, + }, + attributes: SetOfVec::new(), + }; + + let content = info.to_der().with_context(|| "Failed to convert to DER")?; + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let signature = pq_scheme.sign(&content, &self.private_key)?; + + let csr = CertReq { + info, + algorithm: AlgorithmIdentifier { + oid: dilithium5_oid, + parameters: None, + }, + signature: BitString::from_bytes(&signature.into_vec())?, + }; + + Ok(csr) + } + + fn message_sign(&self, message: &[u8]) -> Result, anyhow::Error> { + oqs::init(); + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let signature = pq_scheme.sign(message, &self.private_key)?; + Ok(signature.into_vec()) + } + + fn message_verify(&self, message: &[u8], signature: &[u8]) -> Result { + oqs::init(); + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let sig = match pq_scheme.signature_from_bytes(signature) { + Some(s) => s, + None => { + return Err(anyhow!("Invalid signature input")); + } + }; + pq_scheme.verify( + message, + sig, + &self.public_key)?; + // If no error then the signature is valid + Ok(true) + } + + fn generate_certificate(&self, + ca_infos: &CertificateFields, + subject_infos: &RdnSequence, + subject_key: &[u8], + serial: &[u8], + is_app_cert: bool) -> Result { + + let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; + + // Build the certificate + let tbs = ca_infos.construct_tbs_certificate( + subject_infos, + subject_key, + serial, + &dilithium5_oid, + is_app_cert)?; + + let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; + + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let signature = pq_scheme.sign(&content, &self.private_key)?; + + let cert = Certificate { + tbs_certificate: tbs, + signature_algorithm: AlgorithmIdentifier { + oid: dilithium5_oid, + parameters: None, + }, + signature: BitString::from_bytes(&signature.into_vec())?, + }; + Ok(cert) + } +} \ No newline at end of file diff --git a/keysas_lib/src/lib.rs b/keysas_lib/src/lib.rs index 494d799..45fdf29 100644 --- a/keysas_lib/src/lib.rs +++ b/keysas_lib/src/lib.rs @@ -12,6 +12,9 @@ use std::os::unix::io::AsRawFd; use std::path::PathBuf; pub mod pki; +pub mod keysas_key; +pub mod keysas_hybrid_keypair; +pub mod certificate_field; // Init logger pub fn init_logger() { diff --git a/keysas_lib/src/pki.rs b/keysas_lib/src/pki.rs index dc1a52f..f7b0d6c 100644 --- a/keysas_lib/src/pki.rs +++ b/keysas_lib/src/pki.rs @@ -24,41 +24,21 @@ #![warn(deprecated)] #![warn(unused_imports)] -use anyhow::{anyhow, Context}; -use ed25519_dalek::Digest; -use ed25519_dalek::Keypair; -use ed25519_dalek::Sha512; +use anyhow::anyhow; use oqs::sig::Algorithm; -use oqs::sig::SecretKey; use oqs::sig::Sig; -use pkcs8::der::asn1::OctetString; -use pkcs8::der::asn1::SetOfVec; -use pkcs8::der::oid::db::rfc5280; -use pkcs8::pkcs5::pbes2; -use pkcs8::EncryptedPrivateKeyInfo; -use pkcs8::LineEnding; -use pkcs8::PrivateKeyInfo; use rand_dl::RngCore; use rand_dl::rngs::OsRng; -use x509_cert::ext::Extension; use std::fs; -use std::fs::File; -use std::io::Write; use std::path::Path; -use std::str::FromStr; -use std::time::Duration; use x509_cert::certificate::*; -use x509_cert::der::asn1::BitString; use x509_cert::der::Encode; -use x509_cert::der::EncodePem; -use x509_cert::name::RdnSequence; use x509_cert::request::CertReq; -use x509_cert::request::CertReqInfo; -use x509_cert::serial_number::SerialNumber; -use x509_cert::spki::AlgorithmIdentifier; use x509_cert::spki::ObjectIdentifier; -use x509_cert::spki::SubjectPublicKeyInfo; -use x509_cert::time::Validity; + +use crate::certificate_field::CertificateFields; +use crate::keysas_hybrid_keypair::HybridKeyPair; +use crate::keysas_key::KeysasKey; // Profil des certificats // @@ -125,121 +105,9 @@ use x509_cert::time::Validity; // - Basic constraints => Critical, cA=False and pathLenConstraint=1 // - Key usage => Critical, digitalSignature // - Certificate policies => Not critical -// - -/// Structure containing informations to build the certificate -#[derive(Debug, Clone)] -pub struct CertificateFields { - pub org_name: Option, - pub org_unit: Option, - pub country: Option, - pub common_name: Option, - pub validity: Option, -} - -#[derive(Debug)] -pub struct KeysasPQKey { - pub private_key: SecretKey, - pub public_key: oqs::sig::PublicKey, -} - -#[derive(Debug)] -pub struct HybridKeyPair { - classic: Keypair, - classic_cert: Certificate, - pq: KeysasPQKey, - pq_cert: Certificate, -} - -const DILITHIUM5_OID: &str = "1.3.6.1.4.1.2.267.7.8.7"; -const ED25519_OID: &str = "1.3.101.112"; - -impl CertificateFields { - /// Validate user input and construct a certificate fields structure that can be used - /// to build the certificates of the PKI. - /// The checks done are : - /// - Test if country is 2 letters long, if less return error, if more shorten it to the first two letters - /// - Test if validity can be converted to u32, if not generate error - /// - Test if sigAlgo is either ed25519 or ed448, if not defaut to ed25519 - pub fn from_fields<'a>( - org_name: Option<&'a str>, - org_unit: Option<&'a str>, - country: Option<&'a str>, - common_name: Option<&'a str>, - validity: Option<&'a str>, - ) -> Result { - // Test if country is 2 letters long - let cn = country.map(|name| - match name.len() { - 0|1 => return Err(anyhow!("Invalid country name")), - 2 => Ok(name.to_string()), - _ => Ok(name[..2].to_string()) - } - ).transpose()?; - - // Test if validity can be converted to u32 - let val = validity.map(|value| value.parse::()) - .transpose()?; - - Ok(CertificateFields { - org_name: org_name.map(|name| name.to_string()), - org_unit:org_unit.map(|name| name.to_string()), - country: cn, - common_name: common_name.map(|name| name.to_string()), - validity: val, - }) - } - - /// Generate a distinghuished name from the input fields for the certificate - pub fn generate_dn(&self) -> Result { - let mut name = String::new(); - - // Add country name - if let Some(cn) = &self.country { - name.push_str("C="); - name.push_str(&cn); - name.push_str(","); - } - - // Add organisation name - if let Some(oa) = &self.org_name { - if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { - name.push_str(","); - } - name.push_str("O="); - name.push_str(&oa); - name.push_str(","); - } - - // Add organisational unit - if let Some(ou) = &self.org_unit { - if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { - name.push_str(","); - } - name.push_str("OU="); - name.push_str(&ou); - name.push_str(","); - } - - // Add common name - if let Some(co) = &self.common_name { - if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { - name.push_str(","); - } - name.push_str("CN="); - name.push_str(&co); - name.push_str(","); - } - // Remove trailing ',' if there is one - if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { - name.pop(); - } - - let rdn = RdnSequence::from_str(&name)?; - Ok(rdn) - } -} +pub const DILITHIUM5_OID: &str = "1.3.6.1.4.1.2.267.7.8.7"; +pub const ED25519_OID: &str = "1.3.101.112"; fn create_dir_if_not_exist(path: &String) -> Result<(), anyhow::Error> { if !Path::new(path).is_dir() { @@ -273,276 +141,11 @@ pub fn create_pki_dir(pki_dir: &String) -> Result<(), anyhow::Error> { Ok(()) } -/// Construct a information field for a certificate -fn construct_tbs_certificate( - issuer_infos: &CertificateFields, - subject_name: &RdnSequence, - pub_value: &[u8], - serial: &[u8], - algo_oid: &ObjectIdentifier, - is_app_cert: bool, - ) -> Result { - // Convert input validity from days to seconds - let dur = match issuer_infos.validity { - Some(value) => Duration::new((value * 60 * 60 * 24).into(), 0), - None => {return Err(anyhow!("Invalid validity value"));} - }; - - // Create Distinguished Names for issuer and subject - let issuer_name = issuer_infos.generate_dn()?; - - // Convert the public key value to a bit string - let pub_key = - BitString::from_bytes(pub_value).with_context(|| "Failed get public key raw value")?; - - // Generate the public key information field - let pub_key_info = SubjectPublicKeyInfo { - algorithm: AlgorithmIdentifier { - oid: *algo_oid, - parameters: None, - }, - subject_public_key: pub_key, - }; - - // Create certificate extensions - let mut extensions: Vec = Vec::new(); - - // Authority Key Identifier - // According to RGS, this extension must be present and set to non critical - // for application certificate - if is_app_cert { - extensions.push(Extension { - extn_id: rfc5280::ID_CE_AUTHORITY_KEY_IDENTIFIER, - critical: false, - extn_value: OctetString::new(issuer_name.to_der()?)? - }); - } - - // Key usage - // According to RGS, must be set to critical - // Bit 0 is set to indicate digitalSignature - let ku_value: [u8; 2] = [1, 0]; - extensions.push(Extension { - extn_id: rfc5280::ID_CE_KEY_USAGE, - critical: true, - extn_value: OctetString::new(ku_value.to_vec())? - }); - - // Generate the TBS Certificate structure - // According to RGS: - // - Version is set to V3 - // - Issuer and subject are set with distinguished names - // - Unique Identifiers are not used - // - Extensions are set - let tbs = TbsCertificate { - version: Version::V3, - serial_number: SerialNumber::new(serial) - .with_context(|| "Failed to generate serial number")?, - signature: AlgorithmIdentifier { - oid: *algo_oid, - parameters: None, - }, - issuer: issuer_name, - validity: Validity::from_now(dur).with_context(|| "Failed to generate validity date")?, - subject: subject_name.clone(), - subject_public_key_info: pub_key_info, - issuer_unique_id: None, - subject_unique_id: None, - extensions: Some(extensions), - }; - Ok(tbs) -} - -/// Generate the root certificate of the PKI from a private key and information -/// fields -/// The function returns the certificate or an openssl error -pub fn generate_root_ed25519( - infos: &CertificateFields, -) -> Result<(Keypair, Certificate), anyhow::Error> { - // Create the root CA Ed25519 key pair - let mut csprng = OsRng {}; - let keypair = Keypair::generate(&mut csprng); - let ed25519_oid = - ObjectIdentifier::new(ED25519_OID).with_context(|| "Failed to generate OID")?; - - // Root ED25519 certificate as serial number 1 - let serial: [u8; 1] = [1]; - - // Build subject DN - let subject = infos.generate_dn()?; - - let tbs = construct_tbs_certificate( - infos, - &subject, - &keypair.public.to_bytes(), - &serial, - &ed25519_oid, - true)?; - - let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; - let mut prehashed = Sha512::new(); - prehashed.update(content); - let sig = keypair - .sign_prehashed(prehashed, None) - .with_context(|| "Failed to sign certificate content")?; - - let cert = Certificate { - tbs_certificate: tbs, - signature_algorithm: AlgorithmIdentifier { - oid: ed25519_oid, - parameters: None, - }, - signature: BitString::from_bytes(&sig.to_bytes()) - .with_context(|| "Failed to convert signature to bytes")?, - }; - - Ok((keypair, cert)) -} - -pub fn generate_root_dilithium( - infos: &CertificateFields, -) -> Result<(SecretKey, oqs::sig::PublicKey, Certificate), anyhow::Error> { - // Create the root CA Dilithium key pair - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let (pk, sk) = pq_scheme.keypair()?; - - // OID value for dilithium-sha512 from IBM's networking OID range - let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; - - // Root Dilithium5 certificate as serial number 2 - let serial: [u8; 1] = [2]; - - // Build subject DN - let subject = infos.generate_dn()?; - - let tbs = construct_tbs_certificate( - infos, - &subject, - &pk.clone().into_vec(), - &serial, - &dilithium5_oid, - true)?; - - let content = tbs.to_der()?; - - let signature = pq_scheme.sign(&content, &sk)?; - - let cert = Certificate { - tbs_certificate: tbs, - signature_algorithm: AlgorithmIdentifier { - oid: dilithium5_oid, - parameters: None, - }, - signature: BitString::from_bytes(&signature.into_vec())?, - }; - - Ok((sk, pk, cert)) -} - -/// Generate PKI root keys -pub fn generate_root( - infos: &CertificateFields, - pki_dir: &str, - pwd: &str, -) -> Result { - // Generate root ED25519 key and certificate - let (kp_ed, cert_ed) = - generate_root_ed25519(infos).with_context(|| "ED25519 generation failed")?; - - // Generate root Dilithium key and certificate - let (sk_dl, pk_dl, cert_dl) = - generate_root_dilithium(infos).context("Dilithium generation failed")?; - - // Construct hybrid key pair - let hk = HybridKeyPair { - classic: kp_ed, - classic_cert: cert_ed, - pq: KeysasPQKey { - private_key: sk_dl, - public_key: pk_dl - }, - pq_cert: cert_dl, - }; - - // Save hybrid key pair to disk - hk.classic.save_keys( - Path::new(&(pki_dir.to_owned() + "/CA/root-priv-cl.p8")), - pwd) - .context("ED25519 storing failed")?; - - hk.pq.save_keys( - Path::new(&(pki_dir.to_owned() + "/CA/root-priv-pq.p8")), - pwd) - .context("Dilithium storing failed")?; - - // Save certificate pair to disk - let mut out_cl = File::create(pki_dir.to_owned() + "/CA/root-cert-cl.pem")?; - write!( - out_cl, - "{}", - hk.classic_cert - .to_pem(LineEnding::LF) - .context("ED25519 certificate to pem failed")? - )?; - - let mut out_pq = File::create(pki_dir.to_owned() + "/CA/root-cert-pq.pem")?; - write!( - out_pq, - "{}", - hk.pq_cert - .to_pem(LineEnding::LF) - .context("Dilithium certificate to pem failed")? - )?; - - Ok(hk) -} - -/// Generate a signed hybrid keypair (ED25519 and Dilithium5) -pub fn generate_signed_keypair( - ca_keys: &HybridKeyPair, - subject_name: &RdnSequence, - pki_infos: &CertificateFields, - is_app_key: bool -) -> Result { - // Generate ED25519 key and certificate - // Create the ED25519 keypair - let mut csprng = OsRng {}; - let kp_ed = Keypair::generate(&mut csprng); - // Construct a CSR for the ED25519 key - let csr_ed = kp_ed.generate_csr(subject_name)?; - // Generate a certificate from the CSR - let cert_ed = generate_cert_from_csr(ca_keys, &csr_ed, pki_infos, is_app_key)?; - - // Generate Dilithium key and certificate - // Create the Dilithium key pair - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let (pk_dl, sk_dl) = pq_scheme.keypair()?; - let kp_pq = KeysasPQKey { - private_key: sk_dl, - public_key: pk_dl - }; - // Construct a CSR for the Dilithium key - let csr_dl = kp_pq.generate_csr(subject_name)?; - // Generate a certificate from the CSR - let cert_dl = generate_cert_from_csr(ca_keys, &csr_dl, pki_infos, is_app_key)?; - - // Construct hybrid key pair - Ok(HybridKeyPair { - classic: kp_ed, - classic_cert: cert_ed, - pq: KeysasPQKey { - private_key: kp_pq.private_key, - public_key: kp_pq.public_key - }, - pq_cert: cert_dl, - }) -} - /// Generate a X509 certificate from a CSR and a CA keypair /// is_app_cert is set to true if it is an application certificate, otherwise it /// is considered to be a CA certificate /// The certificate generated will always be for DigitalSignature -fn generate_cert_from_csr( +pub fn generate_cert_from_csr( ca_keys: &HybridKeyPair, csr: &CertReq, pki_info: &CertificateFields, @@ -558,17 +161,17 @@ fn generate_cert_from_csr( let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; // Build the certificate - if let Ok(_) = csr + if csr .info .public_key .algorithm - .assert_algorithm_oid(ed25519_oid) + .assert_algorithm_oid(ed25519_oid).is_ok() { // Validate CSR authenticity let key = ed25519_dalek::PublicKey::from_bytes(pub_key)?; - if let Err(_) = key.verify_strict( + if key.verify_strict( &csr.info.to_der()?, - &ed25519_dalek::Signature::from_bytes(csr.signature.raw_bytes())?) { + &ed25519_dalek::Signature::from_bytes(csr.signature.raw_bytes())?).is_err() { return Err(anyhow!("Invalid CSR signature")); } @@ -585,22 +188,22 @@ fn generate_cert_from_csr( is_app_cert)?; Ok(cert) - } else if let Ok(_) = csr + } else if csr .info .public_key .algorithm - .assert_algorithm_oid(dilithium5_oid) + .assert_algorithm_oid(dilithium5_oid).is_ok() { // Validate CSR authenticity oqs::init(); let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - if let Err(_) = pq_scheme.verify( + if pq_scheme.verify( &csr.info.to_der()?, - &pq_scheme.signature_from_bytes(csr.signature.raw_bytes()) + pq_scheme.signature_from_bytes(csr.signature.raw_bytes()) .ok_or(anyhow!("Failed to create signature"))?, - &pq_scheme.public_key_from_bytes(pub_key) + pq_scheme.public_key_from_bytes(pub_key) .ok_or(anyhow!("Failed to create public key"))? - ) { + ).is_err() { return Err(anyhow!("Invalid CSR signature")); } @@ -620,387 +223,4 @@ fn generate_cert_from_csr( } else { return Err(anyhow!("Invalid algorithm OID")); } -} - -/// Store a keypair in a PKCS8 file with a password -fn store_keypair( - prk: &[u8], - pbk: &[u8], - oid: ObjectIdentifier, - pwd: &str, - path: &Path, -) -> Result<(), anyhow::Error> { - //Initialize key wrap function parameters - let mut salt = [0u8; 16]; - OsRng.fill_bytes(&mut salt); - let mut iv = [0u8; 16]; - OsRng.fill_bytes(&mut iv); - // Use default parameters for scrypt - let params = match pbes2::Parameters::scrypt_aes256cbc( - pkcs8::pkcs5::scrypt::Params::recommended(), - &salt, - &iv, - ) { - Ok(p) => p, - Err(e) => { - return Err(anyhow!("Failed to generate scrypt parameter: {e}")); - } - }; - - let pk_info = PrivateKeyInfo { - algorithm: pkcs8::AlgorithmIdentifierRef { - oid, - parameters: None, - }, - private_key: prk, - public_key: Some(pbk), - }; - - let pk_encrypted = match pk_info.encrypt_with_params(params, pwd) { - Ok(pk) => pk, - Err(e) => { - log::error!("Failed to encrypt private key: {e}"); - return Err(anyhow!("Failed to encrypt private key")); - } - }; - - pk_encrypted.write_der_file(path)?; - - Ok(()) -} - -/// Generic trait to abstract the main functions of the ED25519 and Dilthium keys -pub trait KeysasKey { - /// Load keypair from a DER encoded PKCS8 file protected with a password - fn load_keys(path: &Path, pwd: &str) -> Result; - /// Save keypair in a DER encoded PKCS8 file protected with a password - fn save_keys(&self, path: &Path, pwd: &str) -> Result<(), anyhow::Error>; - /// Generate a Certificate Signing Request for the keypair and with the subject name - fn generate_csr(&self, subject: &RdnSequence) -> Result; - /// Sign a message - fn message_sign(&self, message: &[u8]) -> Result, anyhow::Error>; - /// Verify the signature of a message - fn message_verify(&self, message: &[u8], signature: &[u8]) -> Result; - /// Generate a certificate from a CSR and signed with the key - fn generate_certificate(&self, - ca_infos: &CertificateFields, - subject_infos: &RdnSequence, - subject_key: &[u8], - serial: &[u8], - is_app_cert: bool) -> Result; -} - -// Implementing new methods on top of dalek Keypair -impl KeysasKey for Keypair { - fn load_keys(path: &Path, pwd: &str) -> Result { - // Load the pkcs8 from file - let cipher = fs::read(path)?; - - let enc_pk = match EncryptedPrivateKeyInfo::try_from(cipher.as_slice()) { - Ok(ep) => ep, - Err(e) => { - return Err(anyhow!("Failed to parse EncryptedPrivateKeyInfo: {e}")); - } - }; - let pk = match enc_pk.decrypt(pwd) { - Ok(p) => p, - Err(e) => { - return Err(anyhow!("Failed to decrypt document: {e}")); - } - }; - let decoded_pk: PrivateKeyInfo = match pk.decode_msg() { - Ok(parsed_pk) => parsed_pk, - Err(e) => { - return Err(anyhow!( - "Failed to decode asn.1 format for private key: {e}" - )); - } - }; - // ed25519 is only 32 bytes long - if decoded_pk.private_key.len() == 32 { - match ed25519_dalek::SecretKey::from_bytes(decoded_pk.private_key) { - Ok(secret_key) => { - return Ok(Keypair { - public: (&(secret_key)).into(), - secret: secret_key, - }); - } - Err(e) => { - return Err(anyhow!( - "Cannot parse private key CLASSIC/ed25519-dalek from pkcs#8: {e}" - )); - } - }; - } else { - return Err(anyhow!("Key is not 32 bytes long")); - } - } - - fn save_keys(&self, path: &Path, pwd: &str) -> Result<(), anyhow::Error> { - let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; - - store_keypair( - self.secret.as_bytes(), - self.public.as_bytes(), - ed25519_oid, - pwd, - path - ) - } - - fn generate_csr(&self, subject: &RdnSequence) -> Result { - let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; - - let pub_key = BitString::from_bytes(&self.public.to_bytes()) - .with_context(|| "Failed get public key raw value")?; - - let info = CertReqInfo { - version: x509_cert::request::Version::V1, - subject: subject.to_owned(), - public_key: SubjectPublicKeyInfo { - algorithm: AlgorithmIdentifier { - oid: ed25519_oid, - parameters: None, - }, - subject_public_key: pub_key, - }, - attributes: SetOfVec::new(), - }; - - let content = info.to_der().with_context(|| "Failed to convert to DER")?; - let mut prehashed = Sha512::new(); - prehashed.update(content); - let signature = self - .sign_prehashed(prehashed, None) - .with_context(|| "Failed to sign certificate content")?; - - let csr = CertReq { - info, - algorithm: AlgorithmIdentifier { - oid: ed25519_oid, - parameters: None, - }, - signature: BitString::from_bytes(&signature.to_bytes())?, - }; - - Ok(csr) - } - - fn message_sign(&self, message: &[u8]) -> Result, anyhow::Error> { - let mut prehashed = Sha512::new(); - prehashed.update(message); - let signature = self.sign_prehashed(prehashed, None)?; - Ok(signature.to_bytes().to_vec()) - } - - fn message_verify(&self, message: &[u8], signature: &[u8]) -> Result { - let sig = ed25519_dalek::Signature::from_bytes(signature)?; - self.verify(message, &sig)?; - // If no error has been returned then the signature is valid - Ok(true) - } - - fn generate_certificate(&self, - ca_infos: &CertificateFields, - subject_infos: &RdnSequence, - subject_key: &[u8], - serial: &[u8], - is_app_cert: bool) -> Result { - - let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; - - // Build the certificate - let tbs = construct_tbs_certificate( - ca_infos, - subject_infos, - subject_key, - serial, - &ed25519_oid, - is_app_cert)?; - - let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; - let mut prehashed = Sha512::new(); - prehashed.update(content); - let signature = self.sign_prehashed(prehashed, None) - .with_context(|| "Failed to sign certificate content")?; - - let cert = Certificate { - tbs_certificate: tbs, - signature_algorithm: AlgorithmIdentifier { - oid: ed25519_oid, - parameters: None, - }, - signature: BitString::from_bytes(&signature.to_bytes())?, - }; - Ok(cert) - } -} - -impl KeysasKey for KeysasPQKey { - fn load_keys(path: &Path, pwd: &str) -> Result { - // Load the pkcs8 from file - let cipher = fs::read(path)?; - let enc_pk = match EncryptedPrivateKeyInfo::try_from(cipher.as_slice()) { - Ok(ep) => ep, - Err(e) => { - return Err(anyhow!("Failed to parse EncryptedPrivateKeyInfo: {e}")); - } - }; - let pk = match enc_pk.decrypt(pwd) { - Ok(p) => p, - Err(e) => { - return Err(anyhow!("Failed to decrypt document: {e}")); - } - }; - let decoded_pk: PrivateKeyInfo = match pk.decode_msg() { - Ok(parsed_pk) => parsed_pk, - Err(e) => { - return Err(anyhow!( - "Failed to decode asn.1 format for private key: {e}" - )); - } - }; - oqs::init(); - let scheme = match oqs::sig::Sig::new(oqs::sig::Algorithm::Dilithium5) { - Ok(scheme) => scheme, - Err(e) => { - return Err(anyhow!( - "OQS error: cannot initialize Dililthium5 scheme: {e}" - )) - } - }; - let tmp_pq_sk = match oqs::sig::Sig::secret_key_from_bytes(&scheme, decoded_pk.private_key) - { - Some(tmp_sig_sk) => tmp_sig_sk, - None => { - return Err(anyhow!( - "Cannot parse secret pq private key from decode value" - )); - } - }; - let secret_key = tmp_pq_sk.to_owned(); - match decoded_pk.public_key { - Some(public_key_u8) => { - let public_key = match oqs::sig::Sig::public_key_from_bytes(&scheme, public_key_u8) - { - Some(p) => p, - None => { - return Err(anyhow!("Cannot parse PQC public key from pkcs#8")); - } - }; - return Ok(KeysasPQKey { - private_key: secret_key, - public_key: public_key.to_owned(), - }); - } - None => { - return Err(anyhow!("No PQC public key found in pkcs#8 format")); - }, - }; - } - - fn save_keys(&self, path: &Path, pwd: &str) -> Result<(), anyhow::Error> { - let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; - - store_keypair( - &self.private_key.clone().into_vec(), - &self.public_key.clone().into_vec(), - ed25519_oid, - pwd, - path - ) - } - - fn generate_csr(&self, subject: &RdnSequence) -> Result { - let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; - - let pub_key = BitString::from_bytes(&self.public_key.clone().into_vec()) - .with_context(|| "Failed get public key raw value")?; - - let info = CertReqInfo { - version: x509_cert::request::Version::V1, - subject: subject.to_owned(), - public_key: SubjectPublicKeyInfo { - algorithm: AlgorithmIdentifier { - oid: dilithium5_oid, - parameters: None, - }, - subject_public_key: pub_key, - }, - attributes: SetOfVec::new(), - }; - - let content = info.to_der().with_context(|| "Failed to convert to DER")?; - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let signature = pq_scheme.sign(&content, &self.private_key)?; - - let csr = CertReq { - info, - algorithm: AlgorithmIdentifier { - oid: dilithium5_oid, - parameters: None, - }, - signature: BitString::from_bytes(&signature.into_vec())?, - }; - - Ok(csr) - } - - fn message_sign(&self, message: &[u8]) -> Result, anyhow::Error> { - oqs::init(); - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let signature = pq_scheme.sign(message, &self.private_key)?; - Ok(signature.into_vec()) - } - - fn message_verify(&self, message: &[u8], signature: &[u8]) -> Result { - oqs::init(); - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let sig = match pq_scheme.signature_from_bytes(signature) { - Some(s) => s, - None => { - return Err(anyhow!("Invalid signature input")); - } - }; - pq_scheme.verify( - message, - sig, - &self.public_key)?; - // If no error then the signature is valid - Ok(true) - } - - fn generate_certificate(&self, - ca_infos: &CertificateFields, - subject_infos: &RdnSequence, - subject_key: &[u8], - serial: &[u8], - is_app_cert: bool) -> Result { - - let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; - - // Build the certificate - let tbs = construct_tbs_certificate( - ca_infos, - subject_infos, - subject_key, - serial, - &dilithium5_oid, - is_app_cert)?; - - let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; - - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let signature = pq_scheme.sign(&content, &self.private_key)?; - - let cert = Certificate { - tbs_certificate: tbs, - signature_algorithm: AlgorithmIdentifier { - oid: dilithium5_oid, - parameters: None, - }, - signature: BitString::from_bytes(&signature.into_vec())?, - }; - Ok(cert) - } -} +} \ No newline at end of file diff --git a/keysas_lib/tests/pki_test.rs b/keysas_lib/tests/pki_test.rs index 9ea40d9..1c919d4 100644 --- a/keysas_lib/tests/pki_test.rs +++ b/keysas_lib/tests/pki_test.rs @@ -2,8 +2,8 @@ use ed25519_dalek::Digest; use ed25519_dalek::Keypair; use ed25519_dalek::Sha512; use hex_literal::hex; -use keysas_lib::pki::KeysasKey; -use keysas_lib::pki::KeysasPQKey; +use keysas_lib::keysas_key::KeysasKey; +use keysas_lib::keysas_key::KeysasPQKey; use oqs::sig::Algorithm; use oqs::sig::Sig; use pkcs8::der::Any; From 30e27f56005a6a8ad52d57d0685a74d6dc3516b8 Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Fri, 31 Mar 2023 10:42:58 +0200 Subject: [PATCH 005/160] Addition of hybrid keypair loading --- keysas_lib/src/keysas_hybrid_keypair.rs | 49 +++++++++++++++++++++---- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/keysas_lib/src/keysas_hybrid_keypair.rs b/keysas_lib/src/keysas_hybrid_keypair.rs index 9c3f117..22e917d 100644 --- a/keysas_lib/src/keysas_hybrid_keypair.rs +++ b/keysas_lib/src/keysas_hybrid_keypair.rs @@ -32,9 +32,11 @@ use oqs::sig::SecretKey; use oqs::sig::Sig; use pkcs8::LineEnding; - use pkcs8::der::Encode; + use pkcs8::der::DecodePem; +use pkcs8::der::Encode; use rand_dl::rngs::OsRng; - use std::fs::File; + use std::fs; +use std::fs::File; use std::io::Write; use std::path::Path; use x509_cert::certificate::*; @@ -152,7 +154,7 @@ impl HybridKeyPair { /// The keys will be saved in DER encoded PKCS8 files at: keys_path/name-{cl|pq}.p8 /// The certificates will be saved in PEM files at: certs_path/name-{cl|pq}.pem /// pwd is used for encrypting the PKCS8 files - pub fn save(&self, name: &str, keys_path: &Path, _certs_path: &Path, pwd: &str) -> Result<(), anyhow::Error> { + pub fn save(&self, name: &str, keys_path: &Path, certs_path: &Path, pwd: &str) -> Result<(), anyhow::Error> { // Save keys let cl_key_path = keys_path.join(name.to_owned() + "-cl.p8"); self.classic.save_keys(&cl_key_path, pwd)?; @@ -161,15 +163,48 @@ impl HybridKeyPair { self.pq.save_keys(&pq_key_path, pwd)?; // Save certificates - //let cl_cert_path = certs_path.join(name.to_owned() + "-cl.pem"); - //self.classic_cert.save_keys(&cl_cert_path, pwd)?; + let cl_cert_path = certs_path.join(name.to_owned() + "-cl.pem"); + let cl_pem = self.classic_cert.to_pem(LineEnding::LF)?; + let mut cl_cert_file = File::create(cl_cert_path)?; + write!(cl_cert_file, "{}", cl_pem)?; - //let pq_cert_path = certs_path.join(name.to_owned() + "-pq.pem"); - //self.pq.save_keys(&pq_cert_path, pwd)?; + let pq_cert_path = certs_path.join(name.to_owned() + "-pq.pem"); + let pq_pem = self.pq_cert.to_pem(LineEnding::LF)?; + let mut pq_cert_file = File::create(pq_cert_path)?; + write!(pq_cert_file, "{}", pq_pem)?; Ok(()) } + /// Load the keypair from the disk + /// The keys will be loaded in DER encoded PKCS8 files from: keys_path/name-{cl|pq}.p8 + /// The certificates will be loaded in PEM files from: certs_path/name-{cl|pq}.pem + /// pwd is used for decrypting the PKCS8 files + pub fn load(name: &str, keys_path: &Path, certs_path: &Path, pwd: &str) -> Result { + // Load keys + let cl_key_path = keys_path.join(name.to_owned() + "-cl.p8"); + let cl_keys = Keypair::load_keys(&cl_key_path, pwd)?; + + let pq_key_path = keys_path.join(name.to_owned() + "-pq.p8"); + let pq_keys = KeysasPQKey::load_keys(&pq_key_path, pwd)?; + + // Load certificates + let cl_cert_path = certs_path.join(name.to_owned() + "-cl.pem"); + let cl_cert_pem = fs::read_to_string(cl_cert_path)?; + let cl_cert = Certificate::from_pem(cl_cert_pem)?; + + let pq_cert_path = certs_path.join(name.to_owned() + "-pq.pem"); + let pq_cert_pem = fs::read_to_string(pq_cert_path)?; + let pq_cert = Certificate::from_pem(pq_cert_pem)?; + + Ok(HybridKeyPair { + classic: cl_keys, + classic_cert: cl_cert, + pq: pq_keys, + pq_cert: pq_cert + }) + } + /// Generate PKI root keys pub fn generate_root( infos: &CertificateFields, From f792696cc9712daad031fb2d699497fe6d669d47 Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Fri, 31 Mar 2023 10:47:13 +0200 Subject: [PATCH 006/160] Passed cargo fmt --- keysas-admin/src-tauri/src/main.rs | 2 +- keysas_lib/src/certificate_field.rs | 113 ++++----- keysas_lib/src/keysas_hybrid_keypair.rs | 310 +++++++++++------------- keysas_lib/src/keysas_key.rs | 120 +++++---- keysas_lib/src/lib.rs | 6 +- keysas_lib/src/pki.rs | 92 ++++--- keysas_lib/tests/pki_test.rs | 45 ++-- 7 files changed, 347 insertions(+), 341 deletions(-) diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index 6d6f342..f734655 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -755,7 +755,7 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, } // Generate root key and save them in PKCS12 format - let root_keys = match HybridKeyPair::generate_root(&infos, &pki_dir, &admin_pwd) { + let root_keys = match HybridKeyPair::generate_root(&infos) { Ok(kp) => kp, Err(e) => { log::error!("Failed to generate PKI root keys: {e}"); diff --git a/keysas_lib/src/certificate_field.rs b/keysas_lib/src/certificate_field.rs index 753124d..0197389 100644 --- a/keysas_lib/src/certificate_field.rs +++ b/keysas_lib/src/certificate_field.rs @@ -8,45 +8,46 @@ * for building the keysas_lib. */ - #![warn(unused_extern_crates)] - #![forbid(non_shorthand_field_patterns)] - #![warn(dead_code)] - #![warn(missing_debug_implementations)] - #![warn(missing_copy_implementations)] - #![warn(trivial_casts)] - #![warn(trivial_numeric_casts)] - #![warn(unused_extern_crates)] - #![warn(unused_import_braces)] #![warn(unused_qualifications)] - #![warn(variant_size_differences)] - #![forbid(private_in_public)] - #![warn(overflowing_literals)] - #![warn(deprecated)] - #![warn(unused_imports)] - - use anyhow::{anyhow, Context}; - use pkcs8::der::Encode; +#![warn(unused_extern_crates)] +#![forbid(non_shorthand_field_patterns)] +#![warn(dead_code)] +#![warn(missing_debug_implementations)] +#![warn(missing_copy_implementations)] +#![warn(trivial_casts)] +#![warn(trivial_numeric_casts)] +#![warn(unused_extern_crates)] +#![warn(unused_import_braces)] +#![warn(unused_qualifications)] +#![warn(variant_size_differences)] +#![forbid(private_in_public)] +#![warn(overflowing_literals)] +#![warn(deprecated)] +#![warn(unused_imports)] + +use anyhow::{anyhow, Context}; use pkcs8::der::asn1::OctetString; - use pkcs8::der::oid::db::rfc5280; - use x509_cert::ext::Extension; - use std::str::FromStr; +use pkcs8::der::oid::db::rfc5280; +use pkcs8::der::Encode; +use std::str::FromStr; use std::time::Duration; - use x509_cert::certificate::*; - use x509_cert::der::asn1::BitString; - use x509_cert::name::RdnSequence; - use x509_cert::serial_number::SerialNumber; - use x509_cert::spki::AlgorithmIdentifier; - use x509_cert::spki::ObjectIdentifier; - use x509_cert::spki::SubjectPublicKeyInfo; - use x509_cert::time::Validity; +use x509_cert::certificate::*; +use x509_cert::der::asn1::BitString; +use x509_cert::ext::Extension; +use x509_cert::name::RdnSequence; +use x509_cert::serial_number::SerialNumber; +use x509_cert::spki::AlgorithmIdentifier; +use x509_cert::spki::ObjectIdentifier; +use x509_cert::spki::SubjectPublicKeyInfo; +use x509_cert::time::Validity; /// Structure containing informations to build the certificate #[derive(Debug, Clone)] pub struct CertificateFields { - pub org_name: Option, - pub org_unit: Option, - pub country: Option, + pub org_name: Option, + pub org_unit: Option, + pub country: Option, pub common_name: Option, - pub validity: Option, + pub validity: Option, } impl CertificateFields { @@ -64,21 +65,20 @@ impl CertificateFields { validity: Option<&'a str>, ) -> Result { // Test if country is 2 letters long - let cn = country.map(|name| - match name.len() { - 0|1 => Err(anyhow!("Invalid country name")), + let cn = country + .map(|name| match name.len() { + 0 | 1 => Err(anyhow!("Invalid country name")), 2 => Ok(name.to_string()), - _ => Ok(name[..2].to_string()) - } - ).transpose()?; + _ => Ok(name[..2].to_string()), + }) + .transpose()?; // Test if validity can be converted to u32 - let val = validity.map(|value| value.parse::()) - .transpose()?; + let val = validity.map(|value| value.parse::()).transpose()?; Ok(CertificateFields { org_name: org_name.map(|name| name.to_string()), - org_unit:org_unit.map(|name| name.to_string()), + org_unit: org_unit.map(|name| name.to_string()), country: cn, common_name: common_name.map(|name| name.to_string()), validity: val, @@ -145,20 +145,22 @@ impl CertificateFields { serial: &[u8], algo_oid: &ObjectIdentifier, is_app_cert: bool, - ) -> Result { + ) -> Result { // Convert input validity from days to seconds let dur = match self.validity { Some(value) => Duration::new((value * 60 * 60 * 24).into(), 0), - None => {return Err(anyhow!("Invalid validity value"));} + None => { + return Err(anyhow!("Invalid validity value")); + } }; - + // Create Distinguished Names for issuer and subject let issuer_name = self.generate_dn()?; - + // Convert the public key value to a bit string let pub_key = BitString::from_bytes(pub_value).with_context(|| "Failed get public key raw value")?; - + // Generate the public key information field let pub_key_info = SubjectPublicKeyInfo { algorithm: AlgorithmIdentifier { @@ -167,10 +169,10 @@ impl CertificateFields { }, subject_public_key: pub_key, }; - + // Create certificate extensions let mut extensions: Vec = Vec::new(); - + // Authority Key Identifier // According to RGS, this extension must be present and set to non critical // for application certificate @@ -178,10 +180,10 @@ impl CertificateFields { extensions.push(Extension { extn_id: rfc5280::ID_CE_AUTHORITY_KEY_IDENTIFIER, critical: false, - extn_value: OctetString::new(issuer_name.to_der()?)? + extn_value: OctetString::new(issuer_name.to_der()?)?, }); } - + // Key usage // According to RGS, must be set to critical // Bit 0 is set to indicate digitalSignature @@ -189,15 +191,15 @@ impl CertificateFields { extensions.push(Extension { extn_id: rfc5280::ID_CE_KEY_USAGE, critical: true, - extn_value: OctetString::new(ku_value.to_vec())? + extn_value: OctetString::new(ku_value.to_vec())?, }); - + // Generate the TBS Certificate structure // According to RGS: // - Version is set to V3 // - Issuer and subject are set with distinguished names // - Unique Identifiers are not used - // - Extensions are set + // - Extensions are set let tbs = TbsCertificate { version: Version::V3, serial_number: SerialNumber::new(serial) @@ -207,7 +209,8 @@ impl CertificateFields { parameters: None, }, issuer: issuer_name, - validity: Validity::from_now(dur).with_context(|| "Failed to generate validity date")?, + validity: Validity::from_now(dur) + .with_context(|| "Failed to generate validity date")?, subject: subject_name.clone(), subject_public_key_info: pub_key_info, issuer_unique_id: None, @@ -216,4 +219,4 @@ impl CertificateFields { }; Ok(tbs) } -} \ No newline at end of file +} diff --git a/keysas_lib/src/keysas_hybrid_keypair.rs b/keysas_lib/src/keysas_hybrid_keypair.rs index 22e917d..ad3bcb2 100644 --- a/keysas_lib/src/keysas_hybrid_keypair.rs +++ b/keysas_lib/src/keysas_hybrid_keypair.rs @@ -8,53 +8,53 @@ * for building the keysas_lib. */ - #![warn(unused_extern_crates)] - #![forbid(non_shorthand_field_patterns)] - #![warn(dead_code)] - #![warn(missing_debug_implementations)] - #![warn(missing_copy_implementations)] - #![warn(trivial_casts)] - #![warn(trivial_numeric_casts)] - #![warn(unused_extern_crates)] - #![warn(unused_import_braces)] - #![warn(unused_qualifications)] - #![warn(variant_size_differences)] - #![forbid(private_in_public)] - #![warn(overflowing_literals)] - #![warn(deprecated)] - #![warn(unused_imports)] - - use anyhow::Context; - use ed25519_dalek::Digest; - use ed25519_dalek::Keypair; - use ed25519_dalek::Sha512; - use oqs::sig::Algorithm; - use oqs::sig::SecretKey; - use oqs::sig::Sig; - use pkcs8::LineEnding; - use pkcs8::der::DecodePem; +#![warn(unused_extern_crates)] +#![forbid(non_shorthand_field_patterns)] +#![warn(dead_code)] +#![warn(missing_debug_implementations)] +#![warn(missing_copy_implementations)] +#![warn(trivial_casts)] +#![warn(trivial_numeric_casts)] +#![warn(unused_extern_crates)] +#![warn(unused_import_braces)] +#![warn(unused_qualifications)] +#![warn(variant_size_differences)] +#![forbid(private_in_public)] +#![warn(overflowing_literals)] +#![warn(deprecated)] +#![warn(unused_imports)] + +use anyhow::Context; +use ed25519_dalek::Digest; +use ed25519_dalek::Keypair; +use ed25519_dalek::Sha512; +use oqs::sig::Algorithm; +use oqs::sig::SecretKey; +use oqs::sig::Sig; +use pkcs8::der::DecodePem; use pkcs8::der::Encode; +use pkcs8::LineEnding; use rand_dl::rngs::OsRng; - use std::fs; +use std::fs; use std::fs::File; - use std::io::Write; - use std::path::Path; - use x509_cert::certificate::*; - use x509_cert::der::asn1::BitString; - use x509_cert::der::EncodePem; - use x509_cert::name::RdnSequence; - use x509_cert::spki::AlgorithmIdentifier; - use x509_cert::spki::ObjectIdentifier; +use std::io::Write; +use std::path::Path; +use x509_cert::certificate::*; +use x509_cert::der::asn1::BitString; +use x509_cert::der::EncodePem; +use x509_cert::name::RdnSequence; +use x509_cert::spki::AlgorithmIdentifier; +use x509_cert::spki::ObjectIdentifier; -use crate::keysas_key::KeysasPQKey; -use crate::keysas_key::KeysasKey; use crate::certificate_field::CertificateFields; +use crate::keysas_key::KeysasKey; +use crate::keysas_key::KeysasPQKey; +use crate::pki::generate_cert_from_csr; use crate::pki::DILITHIUM5_OID; use crate::pki::ED25519_OID; -use crate::pki::generate_cert_from_csr; /// Keysas `HybridKeyPair` -/// +/// /// Structure containing both a ED25519 and a Dilithium5 keypair /// The structure also contains the associated certificates #[derive(Debug)] @@ -65,103 +65,111 @@ pub struct HybridKeyPair { pub pq_cert: Certificate, } - /// Generate the root certificate of the PKI from a private key and information - /// fields - /// The function returns the certificate or an openssl error - fn generate_root_ed25519( - infos: &CertificateFields, - ) -> Result<(Keypair, Certificate), anyhow::Error> { - // Create the root CA Ed25519 key pair - let mut csprng = OsRng {}; - let keypair = Keypair::generate(&mut csprng); - let ed25519_oid = - ObjectIdentifier::new(ED25519_OID).with_context(|| "Failed to generate OID")?; - - // Root ED25519 certificate as serial number 1 - let serial: [u8; 1] = [1]; - - // Build subject DN - let subject = infos.generate_dn()?; - - let tbs = infos.construct_tbs_certificate( - &subject, - &keypair.public.to_bytes(), - &serial, - &ed25519_oid, - true)?; - - let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; - let mut prehashed = Sha512::new(); - prehashed.update(content); - let sig = keypair - .sign_prehashed(prehashed, None) - .with_context(|| "Failed to sign certificate content")?; - - let cert = Certificate { - tbs_certificate: tbs, - signature_algorithm: AlgorithmIdentifier { - oid: ed25519_oid, - parameters: None, - }, - signature: BitString::from_bytes(&sig.to_bytes()) - .with_context(|| "Failed to convert signature to bytes")?, - }; +/// Generate the root certificate of the PKI from a private key and information +/// fields +/// The function returns the certificate or an openssl error +fn generate_root_ed25519( + infos: &CertificateFields, +) -> Result<(Keypair, Certificate), anyhow::Error> { + // Create the root CA Ed25519 key pair + let mut csprng = OsRng {}; + let keypair = Keypair::generate(&mut csprng); + let ed25519_oid = + ObjectIdentifier::new(ED25519_OID).with_context(|| "Failed to generate OID")?; + + // Root ED25519 certificate as serial number 1 + let serial: [u8; 1] = [1]; + + // Build subject DN + let subject = infos.generate_dn()?; + + let tbs = infos.construct_tbs_certificate( + &subject, + &keypair.public.to_bytes(), + &serial, + &ed25519_oid, + true, + )?; + + let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; + let mut prehashed = Sha512::new(); + prehashed.update(content); + let sig = keypair + .sign_prehashed(prehashed, None) + .with_context(|| "Failed to sign certificate content")?; + + let cert = Certificate { + tbs_certificate: tbs, + signature_algorithm: AlgorithmIdentifier { + oid: ed25519_oid, + parameters: None, + }, + signature: BitString::from_bytes(&sig.to_bytes()) + .with_context(|| "Failed to convert signature to bytes")?, + }; + + Ok((keypair, cert)) +} - Ok((keypair, cert)) - } +fn generate_root_dilithium( + infos: &CertificateFields, +) -> Result<(SecretKey, oqs::sig::PublicKey, Certificate), anyhow::Error> { + // Create the root CA Dilithium key pair + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let (pk, sk) = pq_scheme.keypair()?; - fn generate_root_dilithium( - infos: &CertificateFields, - ) -> Result<(SecretKey, oqs::sig::PublicKey, Certificate), anyhow::Error> { - // Create the root CA Dilithium key pair - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let (pk, sk) = pq_scheme.keypair()?; + // OID value for dilithium-sha512 from IBM's networking OID range + let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; - // OID value for dilithium-sha512 from IBM's networking OID range - let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; + // Root Dilithium5 certificate as serial number 2 + let serial: [u8; 1] = [2]; - // Root Dilithium5 certificate as serial number 2 - let serial: [u8; 1] = [2]; + // Build subject DN + let subject = infos.generate_dn()?; - // Build subject DN - let subject = infos.generate_dn()?; + let tbs = infos.construct_tbs_certificate( + &subject, + &pk.clone().into_vec(), + &serial, + &dilithium5_oid, + true, + )?; - let tbs = infos.construct_tbs_certificate( - &subject, - &pk.clone().into_vec(), - &serial, - &dilithium5_oid, - true)?; + let content = tbs.to_der()?; - let content = tbs.to_der()?; + let signature = pq_scheme.sign(&content, &sk)?; - let signature = pq_scheme.sign(&content, &sk)?; - - let cert = Certificate { - tbs_certificate: tbs, - signature_algorithm: AlgorithmIdentifier { - oid: dilithium5_oid, - parameters: None, - }, - signature: BitString::from_bytes(&signature.into_vec())?, - }; + let cert = Certificate { + tbs_certificate: tbs, + signature_algorithm: AlgorithmIdentifier { + oid: dilithium5_oid, + parameters: None, + }, + signature: BitString::from_bytes(&signature.into_vec())?, + }; - Ok((sk, pk, cert)) - } + Ok((sk, pk, cert)) +} impl HybridKeyPair { /// Save the keypair to disk /// The keys will be saved in DER encoded PKCS8 files at: keys_path/name-{cl|pq}.p8 /// The certificates will be saved in PEM files at: certs_path/name-{cl|pq}.pem /// pwd is used for encrypting the PKCS8 files - pub fn save(&self, name: &str, keys_path: &Path, certs_path: &Path, pwd: &str) -> Result<(), anyhow::Error> { + pub fn save( + &self, + name: &str, + keys_path: &Path, + certs_path: &Path, + pwd: &str, + ) -> Result<(), anyhow::Error> { // Save keys let cl_key_path = keys_path.join(name.to_owned() + "-cl.p8"); self.classic.save_keys(&cl_key_path, pwd)?; let pq_key_path = keys_path.join(name.to_owned() + "-pq.p8"); self.pq.save_keys(&pq_key_path, pwd)?; - + // Save certificates let cl_cert_path = certs_path.join(name.to_owned() + "-cl.pem"); let cl_pem = self.classic_cert.to_pem(LineEnding::LF)?; @@ -180,37 +188,38 @@ impl HybridKeyPair { /// The keys will be loaded in DER encoded PKCS8 files from: keys_path/name-{cl|pq}.p8 /// The certificates will be loaded in PEM files from: certs_path/name-{cl|pq}.pem /// pwd is used for decrypting the PKCS8 files - pub fn load(name: &str, keys_path: &Path, certs_path: &Path, pwd: &str) -> Result { + pub fn load( + name: &str, + keys_path: &Path, + certs_path: &Path, + pwd: &str, + ) -> Result { // Load keys let cl_key_path = keys_path.join(name.to_owned() + "-cl.p8"); - let cl_keys = Keypair::load_keys(&cl_key_path, pwd)?; + let classic = Keypair::load_keys(&cl_key_path, pwd)?; let pq_key_path = keys_path.join(name.to_owned() + "-pq.p8"); - let pq_keys = KeysasPQKey::load_keys(&pq_key_path, pwd)?; + let pq = KeysasPQKey::load_keys(&pq_key_path, pwd)?; // Load certificates let cl_cert_path = certs_path.join(name.to_owned() + "-cl.pem"); let cl_cert_pem = fs::read_to_string(cl_cert_path)?; - let cl_cert = Certificate::from_pem(cl_cert_pem)?; + let classic_cert = Certificate::from_pem(cl_cert_pem)?; let pq_cert_path = certs_path.join(name.to_owned() + "-pq.pem"); let pq_cert_pem = fs::read_to_string(pq_cert_path)?; let pq_cert = Certificate::from_pem(pq_cert_pem)?; Ok(HybridKeyPair { - classic: cl_keys, - classic_cert: cl_cert, - pq: pq_keys, - pq_cert: pq_cert + classic, + classic_cert, + pq, + pq_cert, }) } /// Generate PKI root keys - pub fn generate_root( - infos: &CertificateFields, - pki_dir: &str, - pwd: &str, - ) -> Result { + pub fn generate_root(infos: &CertificateFields) -> Result { // Generate root ED25519 key and certificate let (kp_ed, cert_ed) = generate_root_ed25519(infos).with_context(|| "ED25519 generation failed")?; @@ -219,48 +228,15 @@ impl HybridKeyPair { let (sk_dl, pk_dl, cert_dl) = generate_root_dilithium(infos).context("Dilithium generation failed")?; - // Construct hybrid key pair - let hk = HybridKeyPair { + Ok(HybridKeyPair { classic: kp_ed, classic_cert: cert_ed, pq: KeysasPQKey { private_key: sk_dl, - public_key: pk_dl + public_key: pk_dl, }, pq_cert: cert_dl, - }; - - // Save hybrid key pair to disk - hk.classic.save_keys( - Path::new(&(pki_dir.to_owned() + "/CA/root-priv-cl.p8")), - pwd) - .context("ED25519 storing failed")?; - - hk.pq.save_keys( - Path::new(&(pki_dir.to_owned() + "/CA/root-priv-pq.p8")), - pwd) - .context("Dilithium storing failed")?; - - // Save certificate pair to disk - let mut out_cl = File::create(pki_dir.to_owned() + "/CA/root-cert-cl.pem")?; - write!( - out_cl, - "{}", - hk.classic_cert - .to_pem(LineEnding::LF) - .context("ED25519 certificate to pem failed")? - )?; - - let mut out_pq = File::create(pki_dir.to_owned() + "/CA/root-cert-pq.pem")?; - write!( - out_pq, - "{}", - hk.pq_cert - .to_pem(LineEnding::LF) - .context("Dilithium certificate to pem failed")? - )?; - - Ok(hk) + }) } /// Generate a signed hybrid keypair (ED25519 and Dilithium5) @@ -268,7 +244,7 @@ impl HybridKeyPair { ca_keys: &HybridKeyPair, subject_name: &RdnSequence, pki_infos: &CertificateFields, - is_app_key: bool + is_app_key: bool, ) -> Result { // Generate ED25519 key and certificate // Create the ED25519 keypair @@ -285,7 +261,7 @@ impl HybridKeyPair { let (pk_dl, sk_dl) = pq_scheme.keypair()?; let kp_pq = KeysasPQKey { private_key: sk_dl, - public_key: pk_dl + public_key: pk_dl, }; // Construct a CSR for the Dilithium key let csr_dl = kp_pq.generate_csr(subject_name)?; @@ -298,9 +274,9 @@ impl HybridKeyPair { classic_cert: cert_ed, pq: KeysasPQKey { private_key: kp_pq.private_key, - public_key: kp_pq.public_key + public_key: kp_pq.public_key, }, pq_cert: cert_dl, }) } -} \ No newline at end of file +} diff --git a/keysas_lib/src/keysas_key.rs b/keysas_lib/src/keysas_key.rs index 6b5056f..b739361 100644 --- a/keysas_lib/src/keysas_key.rs +++ b/keysas_lib/src/keysas_key.rs @@ -8,21 +8,21 @@ * for building the keysas_lib. */ - #![warn(unused_extern_crates)] - #![forbid(non_shorthand_field_patterns)] - #![warn(dead_code)] - #![warn(missing_debug_implementations)] - #![warn(missing_copy_implementations)] - #![warn(trivial_casts)] - #![warn(trivial_numeric_casts)] - #![warn(unused_extern_crates)] - #![warn(unused_import_braces)] - #![warn(unused_qualifications)] - #![warn(variant_size_differences)] - #![forbid(private_in_public)] - #![warn(overflowing_literals)] - #![warn(deprecated)] - #![warn(unused_imports)] +#![warn(unused_extern_crates)] +#![forbid(non_shorthand_field_patterns)] +#![warn(dead_code)] +#![warn(missing_debug_implementations)] +#![warn(missing_copy_implementations)] +#![warn(trivial_casts)] +#![warn(trivial_numeric_casts)] +#![warn(unused_extern_crates)] +#![warn(unused_import_braces)] +#![warn(unused_qualifications)] +#![warn(variant_size_differences)] +#![forbid(private_in_public)] +#![warn(overflowing_literals)] +#![warn(deprecated)] +#![warn(unused_imports)] use anyhow::{anyhow, Context}; use ed25519_dalek::Digest; @@ -32,9 +32,11 @@ use oqs::sig::Algorithm; use oqs::sig::SecretKey; use oqs::sig::Sig; use pkcs8::der::asn1::SetOfVec; +use pkcs8::pkcs5::pbes2; use pkcs8::EncryptedPrivateKeyInfo; use pkcs8::PrivateKeyInfo; -use pkcs8::pkcs5::pbes2; +use rand_dl::rngs::OsRng; +use rand_dl::RngCore; use std::fs; use std::path::Path; use x509_cert::certificate::*; @@ -46,12 +48,10 @@ use x509_cert::request::CertReqInfo; use x509_cert::spki::AlgorithmIdentifier; use x509_cert::spki::ObjectIdentifier; use x509_cert::spki::SubjectPublicKeyInfo; -use rand_dl::RngCore; -use rand_dl::rngs::OsRng; use crate::certificate_field::CertificateFields; -use crate::pki::ED25519_OID; use crate::pki::DILITHIUM5_OID; +use crate::pki::ED25519_OID; #[derive(Debug)] pub struct KeysasPQKey { @@ -119,12 +119,14 @@ pub trait KeysasKey { /// Verify the signature of a message fn message_verify(&self, message: &[u8], signature: &[u8]) -> Result; /// Generate a certificate from a CSR and signed with the key - fn generate_certificate(&self, - ca_infos: &CertificateFields, + fn generate_certificate( + &self, + ca_infos: &CertificateFields, subject_infos: &RdnSequence, subject_key: &[u8], serial: &[u8], - is_app_cert: bool) -> Result; + is_app_cert: bool, + ) -> Result; } // Implementing new methods on top of dalek Keypair @@ -156,17 +158,13 @@ impl KeysasKey for Keypair { // ed25519 is only 32 bytes long if decoded_pk.private_key.len() == 32 { match ed25519_dalek::SecretKey::from_bytes(decoded_pk.private_key) { - Ok(secret_key) => { - Ok(Keypair { - public: (&(secret_key)).into(), - secret: secret_key, - }) - } - Err(e) => { - Err(anyhow!( - "Cannot parse private key CLASSIC/ed25519-dalek from pkcs#8: {e}" - )) - } + Ok(secret_key) => Ok(Keypair { + public: (&(secret_key)).into(), + secret: secret_key, + }), + Err(e) => Err(anyhow!( + "Cannot parse private key CLASSIC/ed25519-dalek from pkcs#8: {e}" + )), } } else { Err(anyhow!("Key is not 32 bytes long")) @@ -178,10 +176,10 @@ impl KeysasKey for Keypair { store_keypair( self.secret.as_bytes(), - self.public.as_bytes(), + self.public.as_bytes(), ed25519_oid, pwd, - path + path, ) } @@ -237,13 +235,14 @@ impl KeysasKey for Keypair { Ok(true) } - fn generate_certificate(&self, - ca_infos: &CertificateFields, + fn generate_certificate( + &self, + ca_infos: &CertificateFields, subject_infos: &RdnSequence, subject_key: &[u8], serial: &[u8], - is_app_cert: bool) -> Result { - + is_app_cert: bool, + ) -> Result { let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; // Build the certificate @@ -252,12 +251,14 @@ impl KeysasKey for Keypair { subject_key, serial, &ed25519_oid, - is_app_cert)?; + is_app_cert, + )?; let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; let mut prehashed = Sha512::new(); prehashed.update(content); - let signature = self.sign_prehashed(prehashed, None) + let signature = self + .sign_prehashed(prehashed, None) .with_context(|| "Failed to sign certificate content")?; let cert = Certificate { @@ -329,9 +330,7 @@ impl KeysasKey for KeysasPQKey { public_key: public_key.to_owned(), }) } - None => { - Err(anyhow!("No PQC public key found in pkcs#8 format")) - }, + None => Err(anyhow!("No PQC public key found in pkcs#8 format")), } } @@ -340,10 +339,10 @@ impl KeysasKey for KeysasPQKey { store_keypair( &self.private_key.clone().into_vec(), - &self.public_key.clone().into_vec(), + &self.public_key.clone().into_vec(), ed25519_oid, pwd, - path + path, ) } @@ -352,7 +351,7 @@ impl KeysasKey for KeysasPQKey { let pub_key = BitString::from_bytes(&self.public_key.clone().into_vec()) .with_context(|| "Failed get public key raw value")?; - + let info = CertReqInfo { version: x509_cert::request::Version::V1, subject: subject.to_owned(), @@ -365,11 +364,11 @@ impl KeysasKey for KeysasPQKey { }, attributes: SetOfVec::new(), }; - + let content = info.to_der().with_context(|| "Failed to convert to DER")?; let pq_scheme = Sig::new(Algorithm::Dilithium5)?; let signature = pq_scheme.sign(&content, &self.private_key)?; - + let csr = CertReq { info, algorithm: AlgorithmIdentifier { @@ -378,7 +377,7 @@ impl KeysasKey for KeysasPQKey { }, signature: BitString::from_bytes(&signature.into_vec())?, }; - + Ok(csr) } @@ -398,21 +397,19 @@ impl KeysasKey for KeysasPQKey { return Err(anyhow!("Invalid signature input")); } }; - pq_scheme.verify( - message, - sig, - &self.public_key)?; + pq_scheme.verify(message, sig, &self.public_key)?; // If no error then the signature is valid Ok(true) } - fn generate_certificate(&self, - ca_infos: &CertificateFields, + fn generate_certificate( + &self, + ca_infos: &CertificateFields, subject_infos: &RdnSequence, subject_key: &[u8], serial: &[u8], - is_app_cert: bool) -> Result { - + is_app_cert: bool, + ) -> Result { let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; // Build the certificate @@ -421,10 +418,11 @@ impl KeysasKey for KeysasPQKey { subject_key, serial, &dilithium5_oid, - is_app_cert)?; + is_app_cert, + )?; let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; - + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; let signature = pq_scheme.sign(&content, &self.private_key)?; @@ -438,4 +436,4 @@ impl KeysasKey for KeysasPQKey { }; Ok(cert) } -} \ No newline at end of file +} diff --git a/keysas_lib/src/lib.rs b/keysas_lib/src/lib.rs index 45fdf29..898d1dd 100644 --- a/keysas_lib/src/lib.rs +++ b/keysas_lib/src/lib.rs @@ -11,10 +11,10 @@ use std::io::{BufReader, IoSlice, Read}; use std::os::unix::io::AsRawFd; use std::path::PathBuf; -pub mod pki; -pub mod keysas_key; -pub mod keysas_hybrid_keypair; pub mod certificate_field; +pub mod keysas_hybrid_keypair; +pub mod keysas_key; +pub mod pki; // Init logger pub fn init_logger() { diff --git a/keysas_lib/src/pki.rs b/keysas_lib/src/pki.rs index f7b0d6c..321dd26 100644 --- a/keysas_lib/src/pki.rs +++ b/keysas_lib/src/pki.rs @@ -8,27 +8,27 @@ * for building the keysas_lib. */ - #![warn(unused_extern_crates)] - #![forbid(non_shorthand_field_patterns)] - #![warn(dead_code)] - #![warn(missing_debug_implementations)] - #![warn(missing_copy_implementations)] - #![warn(trivial_casts)] - #![warn(trivial_numeric_casts)] - #![warn(unused_extern_crates)] - #![warn(unused_import_braces)] - #![warn(unused_qualifications)] - #![warn(variant_size_differences)] - #![forbid(private_in_public)] - #![warn(overflowing_literals)] - #![warn(deprecated)] - #![warn(unused_imports)] +#![warn(unused_extern_crates)] +#![forbid(non_shorthand_field_patterns)] +#![warn(dead_code)] +#![warn(missing_debug_implementations)] +#![warn(missing_copy_implementations)] +#![warn(trivial_casts)] +#![warn(trivial_numeric_casts)] +#![warn(unused_extern_crates)] +#![warn(unused_import_braces)] +#![warn(unused_qualifications)] +#![warn(variant_size_differences)] +#![forbid(private_in_public)] +#![warn(overflowing_literals)] +#![warn(deprecated)] +#![warn(unused_imports)] use anyhow::anyhow; use oqs::sig::Algorithm; use oqs::sig::Sig; -use rand_dl::RngCore; use rand_dl::rngs::OsRng; +use rand_dl::RngCore; use std::fs; use std::path::Path; use x509_cert::certificate::*; @@ -149,13 +149,17 @@ pub fn generate_cert_from_csr( ca_keys: &HybridKeyPair, csr: &CertReq, pki_info: &CertificateFields, - is_app_cert: bool + is_app_cert: bool, ) -> Result { // Extract and validate info in the CSR let subject = csr.info.subject.clone(); - let pub_key = csr.info.public_key.subject_public_key - .as_bytes().ok_or(anyhow!("Subject public key missing"))?; + let pub_key = csr + .info + .public_key + .subject_public_key + .as_bytes() + .ok_or(anyhow!("Subject public key missing"))?; let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; let ed25519_oid = ObjectIdentifier::new(ED25519_OID)?; @@ -165,13 +169,18 @@ pub fn generate_cert_from_csr( .info .public_key .algorithm - .assert_algorithm_oid(ed25519_oid).is_ok() + .assert_algorithm_oid(ed25519_oid) + .is_ok() { // Validate CSR authenticity let key = ed25519_dalek::PublicKey::from_bytes(pub_key)?; - if key.verify_strict( - &csr.info.to_der()?, - &ed25519_dalek::Signature::from_bytes(csr.signature.raw_bytes())?).is_err() { + if key + .verify_strict( + &csr.info.to_der()?, + &ed25519_dalek::Signature::from_bytes(csr.signature.raw_bytes())?, + ) + .is_err() + { return Err(anyhow!("Invalid CSR signature")); } @@ -185,25 +194,32 @@ pub fn generate_cert_from_csr( &subject, pub_key, &serial, - is_app_cert)?; + is_app_cert, + )?; Ok(cert) } else if csr .info .public_key .algorithm - .assert_algorithm_oid(dilithium5_oid).is_ok() + .assert_algorithm_oid(dilithium5_oid) + .is_ok() { // Validate CSR authenticity oqs::init(); let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - if pq_scheme.verify( - &csr.info.to_der()?, - pq_scheme.signature_from_bytes(csr.signature.raw_bytes()) - .ok_or(anyhow!("Failed to create signature"))?, - pq_scheme.public_key_from_bytes(pub_key) - .ok_or(anyhow!("Failed to create public key"))? - ).is_err() { + if pq_scheme + .verify( + &csr.info.to_der()?, + pq_scheme + .signature_from_bytes(csr.signature.raw_bytes()) + .ok_or(anyhow!("Failed to create signature"))?, + pq_scheme + .public_key_from_bytes(pub_key) + .ok_or(anyhow!("Failed to create public key"))?, + ) + .is_err() + { return Err(anyhow!("Invalid CSR signature")); } @@ -212,15 +228,13 @@ pub fn generate_cert_from_csr( OsRng.fill_bytes(&mut serial); // Build the certificate - let cert = ca_keys.pq.generate_certificate( - pki_info, - &subject, - pub_key, - &serial, - is_app_cert)?; + let cert = + ca_keys + .pq + .generate_certificate(pki_info, &subject, pub_key, &serial, is_app_cert)?; Ok(cert) } else { return Err(anyhow!("Invalid algorithm OID")); } -} \ No newline at end of file +} diff --git a/keysas_lib/tests/pki_test.rs b/keysas_lib/tests/pki_test.rs index 1c919d4..f1571ee 100644 --- a/keysas_lib/tests/pki_test.rs +++ b/keysas_lib/tests/pki_test.rs @@ -6,17 +6,17 @@ use keysas_lib::keysas_key::KeysasKey; use keysas_lib::keysas_key::KeysasPQKey; use oqs::sig::Algorithm; use oqs::sig::Sig; +use pkcs8::der::asn1::BitString; use pkcs8::der::Any; use pkcs8::der::Encode; -use pkcs8::der::asn1::BitString; use pkcs8::pkcs5::pbes2; +use pkcs8::spki::AlgorithmIdentifier; use pkcs8::EncryptedPrivateKeyInfo; use pkcs8::PrivateKeyInfo; -use pkcs8::spki::AlgorithmIdentifier; use rand_dl::rngs::OsRng; +use std::fs::read; use tempfile::NamedTempFile; use x509_cert::name::RdnSequence; -use std::fs::read; use x509_cert::spki::ObjectIdentifier; #[cfg(test)] @@ -188,13 +188,16 @@ fn test_generate_csr_ed25519() { prehashed.update(info); let signature = keypair.sign_prehashed(prehashed, None).unwrap(); - assert_eq!(csr.signature, BitString::from_bytes(&signature.to_bytes()).unwrap()); + assert_eq!( + csr.signature, + BitString::from_bytes(&signature.to_bytes()).unwrap() + ); // Test CSR signing algorithm let ed25519_oid = ObjectIdentifier::new("1.3.101.112").unwrap(); let ref_algo: AlgorithmIdentifier = AlgorithmIdentifier { oid: ed25519_oid, - parameters: None + parameters: None, }; assert_eq!(csr.algorithm, ref_algo); @@ -205,7 +208,10 @@ fn test_generate_csr_ed25519() { assert_eq!(csr.info.subject, subject); // Test CSR public key value - assert_eq!(csr.info.public_key.subject_public_key, BitString::from_bytes(&keypair.public.to_bytes()).unwrap()); + assert_eq!( + csr.info.public_key.subject_public_key, + BitString::from_bytes(&keypair.public.to_bytes()).unwrap() + ); assert_eq!(csr.info.public_key.algorithm, ref_algo); // Test CSR attributes @@ -221,7 +227,7 @@ fn test_generate_csr_dilithium5() { let (pk, sk) = pq_scheme.keypair().unwrap(); let keypair = KeysasPQKey { private_key: sk, - public_key: pk.clone() + public_key: pk.clone(), }; // Generate a CSR @@ -231,17 +237,20 @@ fn test_generate_csr_dilithium5() { // Test the CSR signature match pq_scheme.verify( &csr.info.to_der().unwrap(), - pq_scheme.signature_from_bytes(csr.signature.as_bytes().unwrap()).unwrap(), - &keypair.public_key) { + pq_scheme + .signature_from_bytes(csr.signature.as_bytes().unwrap()) + .unwrap(), + &keypair.public_key, + ) { Ok(_) => assert!(true), - Err(e) => assert!(false, "{}", e) + Err(e) => assert!(false, "{}", e), } // Test CSR signing algorithm let dilithium5_oid = ObjectIdentifier::new("1.3.6.1.4.1.2.267.7.8.7").unwrap(); let ref_algo: AlgorithmIdentifier = AlgorithmIdentifier { oid: dilithium5_oid, - parameters: None + parameters: None, }; assert_eq!(csr.algorithm, ref_algo); @@ -252,7 +261,10 @@ fn test_generate_csr_dilithium5() { assert_eq!(csr.info.subject, subject); // Test CSR public key value - assert_eq!(csr.info.public_key.subject_public_key, BitString::from_bytes(&keypair.public_key.clone().into_vec()).unwrap()); + assert_eq!( + csr.info.public_key.subject_public_key, + BitString::from_bytes(&keypair.public_key.clone().into_vec()).unwrap() + ); assert_eq!(csr.info.public_key.algorithm, ref_algo); // Test CSR attributes @@ -288,7 +300,7 @@ fn test_save_and_load_dilithium5() { let (pk, sk) = pq_scheme.keypair().unwrap(); let keypair = KeysasPQKey { private_key: sk, - public_key: pk.clone() + public_key: pk.clone(), }; // Store the key as DER in PKCS8 @@ -301,6 +313,9 @@ fn test_save_and_load_dilithium5() { // Load the keypair let loaded = KeysasPQKey::load_keys(&path, &String::from("Test")).unwrap(); - assert_eq!(loaded.private_key.into_vec(), keypair.private_key.into_vec()); + assert_eq!( + loaded.private_key.into_vec(), + keypair.private_key.into_vec() + ); assert_eq!(loaded.public_key.into_vec(), keypair.public_key.into_vec()); -} \ No newline at end of file +} From c0b5fbe4dfab0735af350d2cfea1a91e4368c114 Mon Sep 17 00:00:00 2001 From: lb-anssi <122874537+lb-anssi@users.noreply.github.com> Date: Fri, 31 Mar 2023 10:58:49 +0200 Subject: [PATCH 007/160] X509: Fix Github CI workflow Update rust.yml (#18) Fixed github workflow Github CI: added libsoup Github CI: changed libsoup version Github worflow: fixed typo in dependencies Github workflow: added javascript dependency Github workflow: added webkit dependency --- .github/workflows/rust.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3566de7..12dc8cc 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -24,7 +24,28 @@ jobs: - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} - run: | sudo apt-get update -yq - sudo apt -y install libyara-dev libyara8 wget make lsb-release software-properties-common libseccomp-dev pkg-config git bash libudev-dev cmake + sudo apt -y install \ + build-essential \ + curl \ + wget \ + libssl-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + libyara-dev \ + libyara8 \ + make \ + lsb-release \ + software-properties-common \ + libseccomp-dev \ + pkg-config \ + git \ + bash \ + libudev-dev \ + cmake \ + libsoup2.4-dev \ + libjavascriptcoregtk-4.0-dev \ + libwebkit2gtk-4.0-dev - run: sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" - name: Build run: cargo build --verbose From bc542f8df2e3a346ff1de5357d3f80e400384f61 Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Mon, 3 Apr 2023 14:16:53 +0200 Subject: [PATCH 008/160] X509: fix keysas-out use of PKI --- keysas-core/src/keysas-out/main.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/keysas-core/src/keysas-out/main.rs b/keysas-core/src/keysas-out/main.rs index 2348162..07c7766 100644 --- a/keysas-core/src/keysas-out/main.rs +++ b/keysas-core/src/keysas-out/main.rs @@ -28,11 +28,13 @@ use anyhow::{Context, Result}; use base64::{engine::general_purpose, Engine as _}; use clap::{crate_version, Arg, ArgAction, Command}; +use ed25519_dalek::Keypair; use ed25519_dalek::Signature as ECSignature; -use ed25519_dalek::{Keypair, Signer}; +use ed25519_dalek::Signer; use keysas_lib::append_ext; use keysas_lib::init_logger; -use keysas_lib::pki::{KeysasKey, KeysasPQKey}; +use keysas_lib::keysas_key::KeysasKey; +use keysas_lib::keysas_key::KeysasPQKey; use keysas_lib::sha256_digest; use landlock::{ path_beneath_rules, Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetError, @@ -256,16 +258,7 @@ fn parse_args() -> Configuration { /// This function retrieves the file descriptors and metadata from the messages fn parse_messages(messages: Messages, buffer: &[u8]) -> Vec { messages - .filter_map(|m| { - //Desencapsulate Result - match m { - Ok(ad) => Some(ad), - Err(e) => { - warn!("failed to get ancillary data: {:?}", e); - None - } - } - }) + .filter_map(|m| m.ok()) .filter_map(|ad| { // Filter AncillaryData to keep only ScmRights match ad { @@ -531,8 +524,8 @@ fn output_files(files: Vec, conf: &Configuration) -> Result<()> { && f.md.is_type_allowed && f.md.av_pass && !f.md.is_corrupted - - && (f.md.yara_pass || (!f.md.yara_pass && !conf.yara_clean)) + && f.md.yara_pass + || (!f.md.yara_pass && !conf.yara_clean) { // Output file let mut reader = BufReader::new(&file); From 5dc5d8f066b4e2ee0fd96d44c8e007aadd4cd635 Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Mon, 3 Apr 2023 15:14:41 +0200 Subject: [PATCH 009/160] Enrolment: Generate private keys on the station * Rework of private key generation in keysas-sign to use the utility functions in the PKI crate. * Added oqs initialisation to all function in PKI crate * Cleanup cargo manifest in keysas-sign --- keysas-sign/Cargo.toml | 12 +-- keysas-sign/src/main.rs | 139 ++++++++++++----------------------- keysas_lib/src/keysas_key.rs | 30 ++++++++ 3 files changed, 78 insertions(+), 103 deletions(-) diff --git a/keysas-sign/Cargo.toml b/keysas-sign/Cargo.toml index 3f097e4..a1c4f4e 100644 --- a/keysas-sign/Cargo.toml +++ b/keysas-sign/Cargo.toml @@ -6,18 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -mbrman = "0.5" clap = { version = "4", default-features = false, features = ["std", "cargo"] } -minisign = "0.7" anyhow = "1.0" -nom = "7" -libc = "0.2" -udev = "0.7" -openssl = "*" -openssl-sys = "*" -oqs = "0.7" ed25519-dalek = "1.0" -rand_dl = {package = "rand", version = "0.7"} pkcs8 = { version = "0.10", features = ["encryption", "pem"]} -hex-literal = "*" -x509-cert = "0.2" +keysas_lib = { path = "../keysas_lib" } \ No newline at end of file diff --git a/keysas-sign/src/main.rs b/keysas-sign/src/main.rs index 9b9fdec..6218eb5 100644 --- a/keysas-sign/src/main.rs +++ b/keysas-sign/src/main.rs @@ -7,25 +7,28 @@ * The code for keysas-sign binary. */ +//! Keysas-sign is a utility on the station that manage its private keys +//! It can be called on the command line and it offers the two functions +//! - generate_signing_keypair +//! This command is used to generate a new signing keypair on the station that +//! will be used to signed outgoing files and reports +//! - save_certificate +//! This command is used to load certificate on the station, it can be either: +//! - file: the certificate corresponds to the private signing key of the station +//! - usb: the certificate corresponds to the USB signing authority + pub use anyhow::{anyhow, Context, Result}; use clap::{crate_version, Arg, ArgAction, Command}; -use openssl::x509::X509; -use oqs::sig::PublicKey; -use oqs::sig::SecretKey; -use oqs::sig::Sig; -use pkcs8::pkcs5::pbes2; -use pkcs8::LineEnding; -use pkcs8::ObjectIdentifier; -use pkcs8::PrivateKeyInfo; +use keysas_lib::certificate_field::CertificateFields; +use pkcs8::der::EncodePem; use std::fs::File; use std::io::prelude::*; mod errors; -use std::str; -// Downgrade for ed25519-dalek use ed25519_dalek::Keypair; -use hex_literal::hex; -use oqs::sig::Algorithm; -use rand_dl::rngs::OsRng; +use keysas_lib::keysas_key::KeysasKey; +use keysas_lib::keysas_key::KeysasPQKey; +use std::path::Path; +use std::str; const FILE_PRIV_PATH: &str = "/etc/keysas/file-sign-priv.pem"; const FILE_CERT_PATH: &str = "/etc/keysas/file-sign-cert.pem"; @@ -33,6 +36,8 @@ const FILE_PRIV_PQ_PATH: &str = "/etc/keysas/file-sign-pq-priv.pem"; const FILE_CERT_PQ_PATH: &str = "/etc/keysas/file-sign-pq-cert.pem"; const USB_CERT_PATH: &str = "/etc/keysas/usb-ca-cert.pem"; +const KEY_PASSWD: &str = "Keysas007"; + /// Store command arguments struct Config { generate: bool, // True for the generate command @@ -42,11 +47,6 @@ struct Config { cert: String, // Certificate value } -enum KeyType { - CLASSIC, - PQ, -} - /// Parse command arguments /// The tool does only two function: /// - Generate a new file signing key @@ -119,88 +119,43 @@ fn command_args() -> Config { } } -/// Store a keypair in a PKCS8 file without password -fn store_keypair(prk: &[u8], pbk: &[u8], kind: KeyType, path: &String) -> Result<()> { - let params = match pbes2::Parameters::pbkdf2_sha256_aes256cbc( - 2048, - &hex!("79d982e70df91a88"), - &hex!("b2d02d78b2efd9dff694cf8e0af40925"), - ) { - Ok(p) => p, - Err(e) => { - return Err(anyhow!("Failed to generate pkcs5 parameters: {e}")); - } - }; - let (label, oid) = match kind { - KeyType::CLASSIC => ("ED25519", ObjectIdentifier::new("1.3.101.112").unwrap()), - KeyType::PQ => ( - "Dilithium5", - ObjectIdentifier::new("1.3.6.1.4.1.2.267.3").unwrap(), - ), - }; - - let pk_info = PrivateKeyInfo { - algorithm: pkcs8::AlgorithmIdentifierRef { - oid, - parameters: None, - }, - private_key: prk, - public_key: Some(pbk), - }; - // Caution here: Storing into pkcs8 format using an clear password to be able to write_pem_file - let pk_encrypted = pk_info.encrypt_with_params(params, "Keysas007").unwrap(); - pk_encrypted.write_pem_file(path, label, LineEnding::LF)?; - - Ok(()) -} - -fn gen_ed25519() -> Result { - let mut csprng = OsRng {}; - let keypair: Keypair = Keypair::generate(&mut csprng); - store_keypair( - &keypair.secret.to_bytes(), - &keypair.public.to_bytes(), - KeyType::CLASSIC, - &FILE_PRIV_PATH.to_string(), - )?; - Ok(keypair) -} - -fn gen_pq() -> Result<(PublicKey, SecretKey)> { - // Create the signing Dilithium L5 keypair - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let (pk, sk) = pq_scheme.keypair()?; - store_keypair( - &sk.as_ref(), - &pk.as_ref(), - KeyType::PQ, - &FILE_PRIV_PQ_PATH.to_string(), - )?; - Ok((pk, sk)) -} - /// Generate a new key and certification request /// The private key is saved to a new file at privkey_path /// The certificate request is a PEM-encoded PKCS#10 structure -fn generate_signing_keypair(config: &Config) -> Result<()> { +fn generate_signing_keypair(config: &Config) -> Result { // Generate the private keys - let _ec_key = gen_ed25519()?; - let _pq_key = gen_pq()?; + let ec_key = Keypair::generate_new()?; + let pq_key = KeysasPQKey::generate_new()?; + + // Save the keys + ec_key.save_keys(Path::new(FILE_PRIV_PATH), KEY_PASSWD)?; + pq_key.save_keys(Path::new(FILE_PRIV_PQ_PATH), KEY_PASSWD)?; + + let infos = CertificateFields::from_fields(None, None, None, Some(&config.name), None)?; + + let subject = infos.generate_dn()?; + // Build the csr now + let ec_csr = ec_key.generate_csr(&subject)?; + let pq_csr = pq_key.generate_csr(&subject)?; - Ok(()) + let mut hybrid_csr = String::new(); + // Add the ED25519 CSR + hybrid_csr.push_str(&ec_csr.to_pem(pkcs8::LineEnding::LF)?); + // Add the Dilithium5 CSR + hybrid_csr.push_str(&pq_csr.to_pem(pkcs8::LineEnding::LF)?); + + Ok(hybrid_csr) } fn save_certificate(cert_type: &String, cert: &String) -> Result<()> { if cert_type.eq("usb") { // Test if the certificate received is valid - X509::from_pem(cert.as_bytes())?; // Save it to a file let mut out = File::create(USB_CERT_PATH)?; out.write_all(cert.as_bytes())?; } else if cert_type.eq("file") { // Test if the certificate received is valid - X509::from_pem(cert.as_bytes())?; // Save it to a file let mut out = File::create(FILE_CERT_PATH)?; out.write_all(cert.as_bytes())?; @@ -213,18 +168,18 @@ fn save_certificate(cert_type: &String, cert: &String) -> Result<()> { fn main() -> Result<()> { let config = command_args(); - // Important load oqs: - oqs::init(); - if config.generate { - let req = match generate_signing_keypair(&config) { - Ok(r) => r, + // This command generate a new signing keypair for the station + // and generate a signing request for certificate creation by the admin + match generate_signing_keypair(&config) { + Ok(r) => { + // Return the CSR + println!("{}", r); + } Err(e) => { return Err(anyhow!("Failed to generate private key {e}")); } - }; - - println!("{req:?}"); + } } else if config.load { match save_certificate(&config.cert_type, &config.cert) { Ok(_) => println!("OK"), diff --git a/keysas_lib/src/keysas_key.rs b/keysas_lib/src/keysas_key.rs index b739361..762956b 100644 --- a/keysas_lib/src/keysas_key.rs +++ b/keysas_lib/src/keysas_key.rs @@ -108,6 +108,8 @@ fn store_keypair( /// Generic trait to abstract the main functions of the ED25519 and Dilthium keys pub trait KeysasKey { + /// Generate a new key pair + fn generate_new() -> Result; /// Load keypair from a DER encoded PKCS8 file protected with a password fn load_keys(path: &Path, pwd: &str) -> Result; /// Save keypair in a DER encoded PKCS8 file protected with a password @@ -131,6 +133,12 @@ pub trait KeysasKey { // Implementing new methods on top of dalek Keypair impl KeysasKey for Keypair { + fn generate_new() -> Result { + let mut csprng = OsRng {}; + let kp_ed = Keypair::generate(&mut csprng); + Ok(kp_ed) + } + fn load_keys(path: &Path, pwd: &str) -> Result { // Load the pkcs8 from file let cipher = fs::read(path)?; @@ -274,7 +282,23 @@ impl KeysasKey for Keypair { } impl KeysasKey for KeysasPQKey { + fn generate_new() -> Result { + // Important load oqs: + oqs::init(); + + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let (pk_dl, sk_dl) = pq_scheme.keypair()?; + let kp_pq = KeysasPQKey { + private_key: sk_dl, + public_key: pk_dl, + }; + Ok(kp_pq) + } + fn load_keys(path: &Path, pwd: &str) -> Result { + // Important load oqs: + oqs::init(); + // Load the pkcs8 from file let cipher = fs::read(path)?; let enc_pk = match EncryptedPrivateKeyInfo::try_from(cipher.as_slice()) { @@ -347,6 +371,9 @@ impl KeysasKey for KeysasPQKey { } fn generate_csr(&self, subject: &RdnSequence) -> Result { + // Important load oqs: + oqs::init(); + let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; let pub_key = BitString::from_bytes(&self.public_key.clone().into_vec()) @@ -410,6 +437,9 @@ impl KeysasKey for KeysasPQKey { serial: &[u8], is_app_cert: bool, ) -> Result { + // Important load oqs: + oqs::init(); + let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; // Build the certificate From 366a28beb8c73e90ebde934efb6ba80d0f67ef69 Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Mon, 3 Apr 2023 15:39:10 +0200 Subject: [PATCH 010/160] Enrolment: Added certificate saving to keysas-sign * Fixe of save function in keysas-sign * WIP - Addition of certificate validation in PKI --- keysas-sign/src/main.rs | 52 ++++++++++++++++++++--------- keysas_lib/src/certificate_field.rs | 12 +++++++ 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/keysas-sign/src/main.rs b/keysas-sign/src/main.rs index 6218eb5..e113d69 100644 --- a/keysas-sign/src/main.rs +++ b/keysas-sign/src/main.rs @@ -16,9 +16,26 @@ //! This command is used to load certificate on the station, it can be either: //! - file: the certificate corresponds to the private signing key of the station //! - usb: the certificate corresponds to the USB signing authority +#![feature(is_some_and)] +#![warn(unused_extern_crates)] +#![forbid(non_shorthand_field_patterns)] +#![warn(dead_code)] +#![warn(missing_debug_implementations)] +#![warn(missing_copy_implementations)] +#![warn(trivial_casts)] +#![warn(trivial_numeric_casts)] +#![warn(unused_extern_crates)] +#![warn(unused_import_braces)] +#![warn(unused_qualifications)] +#![warn(variant_size_differences)] +#![forbid(private_in_public)] +#![warn(overflowing_literals)] +#![warn(deprecated)] +#![warn(unused_imports)] pub use anyhow::{anyhow, Context, Result}; use clap::{crate_version, Arg, ArgAction, Command}; +use keysas_lib::certificate_field::validate_signing_certificate; use keysas_lib::certificate_field::CertificateFields; use pkcs8::der::EncodePem; use std::fs::File; @@ -30,11 +47,12 @@ use keysas_lib::keysas_key::KeysasPQKey; use std::path::Path; use std::str; -const FILE_PRIV_PATH: &str = "/etc/keysas/file-sign-priv.pem"; -const FILE_CERT_PATH: &str = "/etc/keysas/file-sign-cert.pem"; +const FILE_PRIV_CL_PATH: &str = "/etc/keysas/file-sign-cl-priv.pem"; +const FILE_CERT_CL_PATH: &str = "/etc/keysas/file-sign-cl-cert.pem"; const FILE_PRIV_PQ_PATH: &str = "/etc/keysas/file-sign-pq-priv.pem"; const FILE_CERT_PQ_PATH: &str = "/etc/keysas/file-sign-pq-cert.pem"; -const USB_CERT_PATH: &str = "/etc/keysas/usb-ca-cert.pem"; +const USB_CERT_CL_PATH: &str = "/etc/keysas/usb-ca-cl-cert.pem"; +const USB_CERT_PQ_PATH: &str = "/etc/keysas/usb-ca-pq-cert.pem"; const KEY_PASSWD: &str = "Keysas007"; @@ -95,7 +113,7 @@ fn command_args() -> Config { .short('t') .long("certtype") .value_name("certtype") - .help("[file|usb]: file is the station file signature certificate, usb is the CA certificate") + .help("[file-cl|file-pq|usb-cl|usb-pq]: file is the station file signature certificate, usb is the CA certificate") .default_value("") .action(clap::ArgAction::Set) ) @@ -128,7 +146,7 @@ fn generate_signing_keypair(config: &Config) -> Result { let pq_key = KeysasPQKey::generate_new()?; // Save the keys - ec_key.save_keys(Path::new(FILE_PRIV_PATH), KEY_PASSWD)?; + ec_key.save_keys(Path::new(FILE_PRIV_CL_PATH), KEY_PASSWD)?; pq_key.save_keys(Path::new(FILE_PRIV_PQ_PATH), KEY_PASSWD)?; let infos = CertificateFields::from_fields(None, None, None, Some(&config.name), None)?; @@ -148,19 +166,21 @@ fn generate_signing_keypair(config: &Config) -> Result { Ok(hybrid_csr) } -fn save_certificate(cert_type: &String, cert: &String) -> Result<()> { - if cert_type.eq("usb") { - // Test if the certificate received is valid - // Save it to a file - let mut out = File::create(USB_CERT_PATH)?; - out.write_all(cert.as_bytes())?; - } else if cert_type.eq("file") { - // Test if the certificate received is valid +/// Save a certificate on the station +fn save_certificate(cert_type: &str, cert: &str) -> Result<()> { + if validate_signing_certificate(cert).is_ok_and(|r| r) { + let path = match cert_type { + "usb-cl" => USB_CERT_CL_PATH, + "usb-pq" => USB_CERT_PQ_PATH, + "file-cl" => FILE_CERT_CL_PATH, + "file-pq" => FILE_CERT_PQ_PATH, + _ => { + return Err(anyhow!("Invalid certificate type")); + } + }; // Save it to a file - let mut out = File::create(FILE_CERT_PATH)?; + let mut out = File::create(path)?; out.write_all(cert.as_bytes())?; - } else { - return Err(anyhow!("Invalid certificate type")); } Ok(()) } diff --git a/keysas_lib/src/certificate_field.rs b/keysas_lib/src/certificate_field.rs index 0197389..7ebe489 100644 --- a/keysas_lib/src/certificate_field.rs +++ b/keysas_lib/src/certificate_field.rs @@ -25,6 +25,7 @@ #![warn(unused_imports)] use anyhow::{anyhow, Context}; +use pkcs8::der::DecodePem; use pkcs8::der::asn1::OctetString; use pkcs8::der::oid::db::rfc5280; use pkcs8::der::Encode; @@ -50,6 +51,17 @@ pub struct CertificateFields { pub validity: Option, } +/// Validate a Certificate received in PEM format +/// TODO: Check that the certificate is for signing and follows the allowed format +pub fn validate_signing_certificate(pem: &str) -> Result { + Certificate::from_pem(pem)?; + + //TODO: Validate certificate signature + //TODO: Validate certificate policy + + Ok(true) +} + impl CertificateFields { /// Validate user input and construct a certificate fields structure that can be used /// to build the certificates of the PKI. From 237f7bd77b837ce47b85f397746cbfb078a5ad46 Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Tue, 4 Apr 2023 13:56:53 +0200 Subject: [PATCH 011/160] Keysas-admin: added station enrolment * Implementation of station enrolment in admin * Fix generate signing keys in station * Fix Rdn generation * General code cleanup --- keysas-admin/src-tauri/src/errors.rs | 1 - keysas-admin/src-tauri/src/main.rs | 325 ++++++++++--------- keysas-admin/src-tauri/src/ssh_wrapper.rs | 25 +- keysas-admin/src-tauri/src/store.rs | 373 ++++++++++++---------- keysas-admin/src-tauri/src/utils.rs | 100 ++++++ keysas-backend/src/main.rs | 13 +- keysas-io/src/main.rs | 2 +- keysas-sign/Cargo.toml | 4 +- keysas-sign/src/keysas_sign_tests.rs | 43 +++ keysas-sign/src/main.rs | 26 +- keysas_lib/Cargo.toml | 3 +- keysas_lib/src/certificate_field.rs | 55 ++-- keysas_lib/src/keysas_key.rs | 6 +- keysas_lib/src/pki.rs | 34 -- keysas_lib/tests/pki_test.rs | 14 +- 15 files changed, 604 insertions(+), 420 deletions(-) delete mode 100644 keysas-admin/src-tauri/src/errors.rs create mode 100644 keysas-admin/src-tauri/src/utils.rs create mode 100644 keysas-sign/src/keysas_sign_tests.rs diff --git a/keysas-admin/src-tauri/src/errors.rs b/keysas-admin/src-tauri/src/errors.rs deleted file mode 100644 index 88a5c72..0000000 --- a/keysas-admin/src-tauri/src/errors.rs +++ /dev/null @@ -1 +0,0 @@ -pub use anyhow::{Context, Result}; diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index f734655..ebe0f0e 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -15,7 +15,6 @@ #![warn(missing_copy_implementations)] #![warn(trivial_casts)] #![warn(trivial_numeric_casts)] -#![warn(unstable_features)] #![warn(unused_extern_crates)] #![warn(unused_import_braces)] #![warn(unused_qualifications)] @@ -27,12 +26,13 @@ all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" )] +#![feature(str_split_remainder)] -use crate::errors::*; use std::path::Path; use async_std::task; use keysas_lib::certificate_field::CertificateFields; use keysas_lib::keysas_hybrid_keypair::HybridKeyPair; +use keysas_lib::pki::generate_cert_from_csr; use nom::bytes::complete::take_until; use nom::IResult; use sha2::{Digest, Sha256}; @@ -41,18 +41,61 @@ use tauri::command; use tauri::{CustomMenuItem, Menu, MenuItem, Submenu}; use tauri_plugin_store::PluginBuilder; use std::io::BufReader; +use anyhow::anyhow; +use std::fs; -mod errors; mod ssh_wrapper; use crate::ssh_wrapper::*; mod store; use crate::store::*; - -use keysas_lib::pki::*; +mod utils; +use crate::utils::*; // TODO: place constant paths in constants +const ST_CA_KEY_NAME: &str = "st-ca"; +const USB_CA_KEY_NAME: &str = "usb"; +const PKI_ROOT_KEY_NAME: &str = "root"; + +const _CA_DIR: &str = "/CA"; +const ST_CA_SUB_DIR: &str = "/CA/st"; +const USB_CA_SUB_DIR: &str = "/CA/usb"; +const PKI_ROOT_SUB_DIR: &str = "/CA/root"; +const _CRL_DIR: &str = "/CRL"; +const CERT_DIR: &str = "/CERT"; + +fn create_dir_if_not_exist(path: &String) -> Result<(), anyhow::Error> { + if !Path::new(path).is_dir() { + fs::create_dir(path)?; + } + Ok(()) +} + +/// Create the PKI directory hierachy as follows +/// pki_dir +/// |-- CA +/// | |--root +/// | |--st +/// | |--usb +/// |--CRL +/// |--CERT +fn create_pki_dir(pki_dir: &String) -> Result<(), anyhow::Error> { + // Test if the directory path is valid + if !Path::new(&pki_dir.trim()).is_dir() { + return Err(anyhow!("Invalid PKI directory path")); + } + + create_dir_if_not_exist(&(pki_dir.to_owned() + "/CA"))?; + create_dir_if_not_exist(&(pki_dir.to_owned() + "/CA/root"))?; + create_dir_if_not_exist(&(pki_dir.to_owned() + "/CA/st"))?; + create_dir_if_not_exist(&(pki_dir.to_owned() + "/CA/usb"))?; -fn main() -> Result<()> { + create_dir_if_not_exist(&(pki_dir.to_owned() + "/CRL"))?; + create_dir_if_not_exist(&(pki_dir.to_owned() + "/CERT"))?; + + Ok(()) +} + +fn main() -> Result<(), anyhow::Error> { // Initiliaze the logger simple_logger::init()?; @@ -66,7 +109,7 @@ fn main() -> Result<()> { } /// Creates a sha256 hash from a file -fn sha256_digest(password: &str) -> Result { +fn sha256_digest(password: &str) -> Result { let mut reader = BufReader::new(password.as_bytes()); let digest = { @@ -86,7 +129,7 @@ fn sha256_digest(password: &str) -> Result { static STORE_PATH: &str = ".keysas.dat"; -async fn init_tauri() -> Result<()> { +async fn init_tauri() -> Result<(), anyhow::Error> { let quit = CustomMenuItem::new("quit".to_string(), "Quit"); //let close = CustomMenuItem::new("close".to_string(), "Close"); let submenu = Submenu::new("Program", Menu::new().add_item(quit)); @@ -103,12 +146,11 @@ async fn init_tauri() -> Result<()> { Ok(()) }) .menu(menu) - .on_menu_event(|event| match event.menu_item_id() { - "quit" => { + .on_menu_event(|event| + if event.menu_item_id().eq("quit") { std::process::exit(0); } - _ => {} - }) + ) .invoke_handler(tauri::generate_handler![ reboot, update, @@ -153,11 +195,11 @@ async fn save_sshkeys(public: String, private: String) -> bool { fn get_sshkeys() -> Result<(String, String), String> { match get_ssh() { Ok((public, private)) => { - return Ok((public, private)); + Ok((public, private)) }, Err(e) => { log::error!("Failed to get ssh keys: {e}"); - return Err(String::from("Store error")); + Err(String::from("Store error")) } } } @@ -184,11 +226,11 @@ async fn save_station(name: String, ip: String) -> bool { fn get_station_ip(name: String) -> Result { match get_station_ip_by_name(&name) { Ok(res) => { - return Ok(res); + Ok(res) }, Err(e) => { log::error!("Failed to get station IP: {e}"); - return Err(String::from("Store error")); + Err(String::from("Store error")) } } } @@ -207,158 +249,161 @@ fn list_stations() -> Result { } }; log::debug!("Station list: {}", result); - return Ok(result); + Ok(result) }, Err(e) => { log::error!("Failed to get station IP: {e}"); - return Err(String::from("Store error")); + Err(String::from("Store error")) } } } /// This function initialize the keys in the station by /// 1. Generate a key pair for file signature on the station -/// 2. Set correct file attributes for the private key file -/// 3. Recover the public part of the key -/// 4. Generate a certificate for the public key -/// 5. Export the created certificate on the station -/// 6. Finally it loads the admin USB signing certificate on the station +/// 2. Recover the public part of the key +/// 3. Generate a certificate for the public key +/// 4. Export the created certificate on the station +/// 5. Finally it loads the admin USB signing certificate on the station #[command] -async fn init_keysas(ip: String, name: String, - ca_pwd: String, st_ca_file: String, usb_ca_file: String, - ) -> bool { - /* - let private_key = match get_ssh() { +fn init_keysas(ip: String, name: String, + ca_pwd: String + ) -> Result { + /* Get admin configuration from the store */ + // Get SSH key + let ssh_key = match get_ssh() { Ok((_, private)) => private, Err(e) => { log::error!("Failed to get private key: {e}"); - return false; + return Err(String::from("No SSH key")); } }; + + // Get path to PKI directory + let pki_dir = match get_pki_dir() { + Ok(dir) => dir, + Err(e) => { + log::error!("Failed to get PKI directory: {e}"); + return Err(String::from("Invalid PKI configuration")); + } + }; + + // Get PKI info + let pki_info = match get_pki_info() { + Ok(info) => info, + Err(e) => { + log::error!("Failed to get PKI informations: {e}"); + return Err(String::from("Invalid PKI configuration")); + } + }; + // Connect to the host - let mut session = match connect_key(&ip, &private_key) { + let mut session = match connect_key(&ip, &ssh_key) { Ok(tu) => tu, Err(e) => { log::error!("Failed to open ssh connection with station: {e}"); - return false; + return Err(String::from("Connection failed")); } }; // 1. Generate a key pair for file signature on the station - // 2. Set correct file attributes for the private key file - // 3. Recover the certification for the key - let command = format!("{}{}{}{}{}", - "sudo /usr/bin/keysas-sign --generate", - " --name ", name, - " && sudo /usr/bin/chmod 600 /etc/keysas/file-sign-priv.pem", - " && sudo /usr/bin/chattr +i /etc/keysas/file-sign-priv.pem"); - let cert_req = match session_exec(&mut session, &command) { - Ok(res) => { - match X509Req::from_pem(&res) { - Ok(req) => req, - Err(e) => { - log::error!("failed to convert command output: {:?}", e); - session.close(); - return false; - } - } - }, - Err(why) => { - log::error!("Error on send_command: {:?}", why); + // 2. Recover the CSR for the keys + let (csr_cl, csr_pq) = match cmd_generate_key_and_get_csr(&mut session, &name) { + Ok(csrs) => csrs, + Err(e) => { + log::error!("Failed to generate key on station and get CSR: {e}"); session.close(); - return false; + return Err(String::from("PKI error")); } }; - // 4. Generate a certificate from the request - // Load PKI admin key - let (root_key, _root_cert) = match load_pkcs12(&st_ca_file, &ca_pwd) { + + // 3. Generate a certificate from the request + // Load station CA keypair + let st_ca_keys = match HybridKeyPair::load( + ST_CA_KEY_NAME, Path::new(ST_CA_SUB_DIR), + Path::new(ST_CA_SUB_DIR), &ca_pwd) { Ok(k) => k, Err(e) => { - log::error!("Failed to load PKI private key: {e}"); + log::error!("Failed to load station CA key: {e}"); session.close(); - return false; + return Err(String::from("PKI error")); + } + }; + + // Load USB CA keypair + let usb_keys = match HybridKeyPair::load( + USB_CA_KEY_NAME, Path::new(USB_CA_SUB_DIR), + Path::new(USB_CA_SUB_DIR), &ca_pwd) { + Ok(k) => k, + Err(e) => { + log::error!("Failed to load station USB key: {e}"); + session.close(); + return Err(String::from("PKI error")); } }; // Generate certificate - let cert = match generate_cert_from_request(&cert_req, &root_key) { + let cert_cl = match generate_cert_from_csr(&st_ca_keys, &csr_cl, &pki_info, true) { Ok(c) => c, Err(e) => { log::error!("Failed to generate certificate from request: {e}"); session.close(); - return false; + return Err(String::from("PKI error")); } }; - // 5. Export the created certificate on the station - let output = match cert.to_pem() { - Ok(o) => { - match String::from_utf8(o) { - Ok(out) => out, - Err(e) => { - log::error!("Failed to convert certificate to string: {e}"); - session.close(); - return false; - } - } - }, + let cert_pq = match generate_cert_from_csr(&st_ca_keys, &csr_pq, &pki_info, true) { + Ok(c) => c, Err(e) => { - log::error!("Failed to convert certificate to PEM: {e}"); + log::error!("Failed to generate certificate from request: {e}"); session.close(); - return false; + return Err(String::from("PKI error")); } }; - - let command = format!("{}{}", - "sudo /usr/bin/keysas-sign --load --certtype file --cert ", - output); - if let Err(e) = session_exec(&mut session, &command) { + + // Save certificates + let path_cl = pki_dir.clone()+CERT_DIR+&name+"-cl.pem"; + if let Err(e) = save_certificate(&cert_cl, Path::new(&path_cl)) { + log::error!("Failed to save station certificate: {e}"); + session.close(); + return Err(String::from("PKI error")); + } + + let path_pq = pki_dir+CERT_DIR+&name+"-pq.pem"; + if let Err(e) = save_certificate(&cert_cl, Path::new(&path_pq)) { + log::error!("Failed to save station certificate: {e}"); + session.close(); + return Err(String::from("PKI error")); + } + + // 4. Export the created certificates on the station + if let Err(e) = send_cert_to_station(&mut session, &cert_cl, "file-cl") { log::error!("Failed to load certificate on the station: {e}"); session.close(); - return false; + return Err(String::from("Connection error")); } - // 6. Finally it loads the admin USB signing certificate - // Load USB CA cert - let (_usb_key, usb_cert) = match load_pkcs12(&usb_ca_file, &ca_pwd) { - Ok(k) => k, - Err(e) => { - log::error!("Failed to load USB CA certificate: {e}"); - session.close(); - return false; - } - }; + if let Err(e) = send_cert_to_station(&mut session, &cert_pq, "file-pq") { + log::error!("Failed to load certificate on the station: {e}"); + session.close(); + return Err(String::from("Connection error")); + } - let output = match usb_cert.to_pem() { - Ok(o) => { - match String::from_utf8(o) { - Ok(out) => out, - Err(e) => { - log::error!("Failed to convert USB certificate to string: {e}"); - session.close(); - return false; - } - } - }, - Err(e) => { - log::error!("Failed to convert USB certificate to PEM: {e}"); - session.close(); - return false; - } - }; - - let command = format!("{}{}", - "sudo /usr/bin/keysas-sign --load --certtype usb --cert ", - output); - if let Err(e) = session_exec(&mut session, &command) { - log::error!("Failed to load USB certificate on the station: {e}"); + // 5. Finally it loads the admin USB signing certificate + if let Err(e) = send_cert_to_station(&mut session, &usb_keys.classic_cert, "usb-cl") { + log::error!("Failed to load certificate on the station: {e}"); + session.close(); + return Err(String::from("Connection error")); + } + + if let Err(e) = send_cert_to_station(&mut session, &usb_keys.pq_cert, "usb-pq") { + log::error!("Failed to load certificate on the station: {e}"); session.close(); - return false; + return Err(String::from("Connection error")); } session.close(); - */ - true + + Ok(String::from("OK")) } #[command] @@ -480,7 +525,7 @@ async fn export_sshpubkey(ip: String) -> bool { } }; - match session_upload(&mut session, &public_key.trim().to_string(), + match session_upload(&mut session, public_key.trim(), &String::from("/home/keysas/.ssh/authorized_keys")) { Ok(_) => { log::info!("authorized_keys successfully s-copied !"); @@ -498,12 +543,12 @@ async fn export_sshpubkey(ip: String) -> bool { log::debug!("Command output: {}", String::from_utf8(res).unwrap()); log::info!("Password authentication has been disabled."); session.close(); - return true; + true }, Err(e) => { log::error!("Rust error on open_exec: {:?}", e); session.close(); - return false; + false } } } @@ -565,7 +610,7 @@ async fn sign_key(ip: String, password: String) -> bool { } }; - let password = sha256_digest(&password.trim()).unwrap(); + let password = sha256_digest(password.trim()).unwrap(); // Connect to the host let host = format!("{}{}", ip.trim(), ":22"); @@ -577,7 +622,7 @@ async fn sign_key(ip: String, password: String) -> bool { } }; - let command = format!("{}", "sudo /usr/bin/keysas-sign --watch"); + let command = "sudo /usr/bin/keysas-sign --watch".to_string(); let output = match session_exec(&mut session, &command) { Ok(out) => out, Err(e) => { @@ -591,7 +636,7 @@ async fn sign_key(ip: String, password: String) -> bool { let command = match String::from_utf8(output) { Ok(signme) => { let signme = signme.trim(); - let (command, _) = parser(&signme).unwrap(); + let (command, _) = parser(signme).unwrap(); let command = command.replace("YourSecretPassWord", password.trim()); let command = format!("{}{}{}", "sudo /usr/bin/", command, " --force"); log::debug!("{}", command); @@ -646,7 +691,7 @@ async fn revoke_key(ip: String) -> bool { } }; - let command = format!("{}", "sudo /usr/bin/keysas-sign --watch"); + let command = "sudo /usr/bin/keysas-sign --watch".to_string(); let stdout = match session_exec(&mut session, &command) { Ok(stdout) => stdout, Err(e) => { @@ -659,8 +704,8 @@ async fn revoke_key(ip: String) -> bool { let command = match String::from_utf8(stdout) { Ok(signme) => { let signme = signme.trim(); - let (command, _) = parser(&signme).unwrap(); - let (_, command) = parser_revoke(&command).unwrap(); + let (command, _) = parser(signme).unwrap(); + let (_, command) = parser_revoke(command).unwrap(); let command = format!( "{}{}{}", "sudo /usr/bin/", @@ -695,23 +740,13 @@ async fn revoke_key(ip: String) -> bool { #[command] fn validate_privatekey(public_key: String, private_key: String) -> bool { - if Path::new(&public_key.trim()).is_file() + Path::new(&public_key.trim()).is_file() && Path::new(&private_key.trim()).is_file() - { - true - } else { - false - } } #[command] fn validate_rootkey(root_key: String) -> bool { - if Path::new(&root_key.trim()).is_file() - { - true - } else { - false - } + Path::new(&root_key.trim()).is_file() } /// Generate a new PKI in an empty directory @@ -765,9 +800,9 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, // Save keys if let Err(e) = root_keys.save( - "root", - &Path::new(&(pki_dir.to_owned() + "/CA/root")), - &Path::new(&(pki_dir.to_owned() + "/CA/root")), + PKI_ROOT_KEY_NAME, + Path::new(&(pki_dir.to_owned() + PKI_ROOT_SUB_DIR)), + Path::new(&(pki_dir.to_owned() + PKI_ROOT_SUB_DIR)), &admin_pwd) { log::error!("Failed to save root key to disk: {e}"); return Err(String::from("PKI error")); @@ -803,9 +838,9 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, }; // Save keys if let Err(e) = st_ca_keys.save( - "st-ca", - &Path::new(&(pki_dir.to_owned() + "/CA/st")), - &Path::new(&(pki_dir.to_owned() + "/CA/st")), + ST_CA_KEY_NAME, + Path::new(&(pki_dir.to_owned() + ST_CA_SUB_DIR)), + Path::new(&(pki_dir.to_owned() + ST_CA_SUB_DIR)), &admin_pwd) { log::error!("Failed to save station CA key to disk: {e}"); return Err(String::from("PKI error")); @@ -842,9 +877,9 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, }; // Save keys if let Err(e) = usb_keys.save( - "usb", - &Path::new(&(pki_dir.to_owned() + "/CA/usb")), - &Path::new(&(pki_dir.to_owned() + "/CA/usb")), + USB_CA_KEY_NAME, + Path::new(&(pki_dir.to_owned() + USB_CA_SUB_DIR)), + Path::new(&(pki_dir + USB_CA_SUB_DIR)), &admin_pwd) { log::error!("Failed to save station CA key to disk: {e}"); return Err(String::from("PKI error")); diff --git a/keysas-admin/src-tauri/src/ssh_wrapper.rs b/keysas-admin/src-tauri/src/ssh_wrapper.rs index 5c30b8c..a233a9b 100644 --- a/keysas-admin/src-tauri/src/ssh_wrapper.rs +++ b/keysas-admin/src-tauri/src/ssh_wrapper.rs @@ -1,9 +1,9 @@ use std::error::Error; use std::net::TcpStream; -use ssh_rs::LocalSession; use ssh_rs::algorithm; use ssh_rs::ssh; +use ssh_rs::LocalSession; use ssh_rs::SshResult; const TIMEOUT: u64 = 60 * 1000; @@ -11,8 +11,10 @@ const USER: &str = "keysas"; const PASSWORD: &str = "Changeme007"; /// Create SSH connexion with RSA or ECC key -pub fn connect_key(ip: &String, private_key: &String) - -> Result, Box> { +pub fn connect_key( + ip: &str, + private_key: &str, +) -> Result, Box> { let host = format!("{}{}", ip.trim(), ":22"); let connector = ssh::create_session_without_default() .username(USER) @@ -33,7 +35,7 @@ pub fn connect_key(ip: &String, private_key: &String) } /// Create SSH connexion with password -pub fn connect_pwd(ip: &String) -> SshResult> { +pub fn connect_pwd(ip: &str) -> SshResult> { let host = format!("{}{}", ip.trim(), ":22"); let connector = ssh::create_session_without_default() .username(USER) @@ -53,15 +55,20 @@ pub fn connect_pwd(ip: &String) -> SshResult> { Ok(session) } -pub fn session_exec(session: &mut LocalSession, command: &String) - -> Result, Box> { +pub fn session_exec( + session: &mut LocalSession, + command: &str, +) -> Result, Box> { let channel = session.open_exec()?; let output = channel.send_command(command)?; Ok(output) } -pub fn session_upload(session: &mut LocalSession, path_l: &String, path_d: &String) - -> SshResult<()>{ +pub fn session_upload( + session: &mut LocalSession, + path_l: &str, + path_d: &str, +) -> SshResult<()> { let channel = session.open_scp()?; channel.upload(path_l, path_d) -} \ No newline at end of file +} diff --git a/keysas-admin/src-tauri/src/store.rs b/keysas-admin/src-tauri/src/store.rs index dc07cdf..8ff87d1 100644 --- a/keysas-admin/src-tauri/src/store.rs +++ b/keysas-admin/src-tauri/src/store.rs @@ -1,19 +1,19 @@ //! Handle the application data storage //! The application data is stored via sqlite in the file ".keysas.dat" -//! +//! //! Data is stored in three tables: -//! +//! //! SSH table (key: TEXT, value: TEXT) //! - name is either "public" or "private" //! - path is the path to the SSH key //! Station table (name: TEXT, ip: TEXT) //! CA table (key: TEXT, value: TEXT) -use std::{sync::Mutex, path::Path}; +use std::{path::Path, sync::Mutex}; use anyhow::anyhow; -use sqlite::Connection; use serde::Serialize; +use sqlite::Connection; use keysas_lib::certificate_field::CertificateFields; @@ -32,7 +32,7 @@ const GET_PRIVATE_QUERY: &str = "SELECT * FROM ssh_table WHERE name='private';"; #[derive(Debug, Serialize)] pub struct Station { name: String, - ip: String + ip: String, } /// Initialize the application store @@ -41,7 +41,7 @@ pub fn init_store(path: &str) -> Result<(), anyhow::Error> { match STORE_HANDLE.lock() { Err(e) => { return Err(anyhow!("Failed to get database lock: {e}")); - }, + } Ok(mut hdl) => { match hdl.as_ref() { Some(_) => return Ok(()), @@ -52,12 +52,12 @@ pub fn init_store(path: &str) -> Result<(), anyhow::Error> { match c.execute(CREATE_QUERY) { Ok(_) => { *hdl = Some(c); - }, + } Err(e) => { return Err(anyhow!("Failed to initialize database: {e}")) } } - }, + } Err(e) => { return Err(anyhow!("Failed to connect to the database: {e}")); } @@ -66,90 +66,68 @@ pub fn init_store(path: &str) -> Result<(), anyhow::Error> { } } } - return Ok(()) + Ok(()) } /// Return a tuple containing (path to public ssh key, path to private ssh key) pub fn get_ssh() -> Result<(String, String), anyhow::Error> { match STORE_HANDLE.lock() { Err(e) => { - return Err(anyhow!("Failed to get database lock: {e}")); - }, - Ok(hdl) => { - match hdl.as_ref() { - Some(connection) => { - let mut public = String::new(); - let mut private = String::new(); - - connection.iterate(GET_PUBLIC_QUERY, |pairs| { - for &(key, value) in pairs.iter() { - match key { - "path" => { - match value { - Some(p) => public.push_str(p), - None => () - } - }, - _ => () - } - } - true - })?; - connection.iterate(GET_PRIVATE_QUERY, |pairs| { - for &(key, value) in pairs.iter() { - match key { - "path" => { - match value { - Some(p) => private.push_str(p), - None => () - } - }, - _ => () - } - } - true - })?; - if (public.chars().count() > 0) - && (private.chars().count() > 0) { - log::debug!("Found: {}, {}", public, private); - return Ok((public, private)); - } else { - return Err(anyhow!("Failed to find station in database")); + Err(anyhow!("Failed to get database lock: {e}")) + } + Ok(hdl) => match hdl.as_ref() { + Some(connection) => { + let mut public = String::new(); + let mut private = String::new(); + + connection.iterate(GET_PUBLIC_QUERY, |pairs| { + for &(key, value) in pairs.iter() { + if key == "path" { if let Some(p) = value {public.push_str(p)}} } - }, - None => { - return Err(anyhow!("Store is not initialized")); + true + })?; + connection.iterate(GET_PRIVATE_QUERY, |pairs| { + for &(key, value) in pairs.iter() { + if key == "path" {if let Some(p) = value {private.push_str(p)}} + } + true + })?; + if (public.chars().count() > 0) && (private.chars().count() > 0) { + log::debug!("Found: {}, {}", public, private); + Ok((public, private)) + } else { + Err(anyhow!("Failed to find station in database")) } } - } + None => { + Err(anyhow!("Store is not initialized")) + } + }, } } /// Save the paths to the public and private SSH keys /// The function first checks that the path are valid files pub fn set_ssh(public: &String, private: &String) -> Result<(), anyhow::Error> { - if !Path::new(public.trim()).is_file() || - !Path::new(private.trim()).is_file() { + if !Path::new(public.trim()).is_file() || !Path::new(private.trim()).is_file() { return Err(anyhow!("Invalid paths")); } match STORE_HANDLE.lock() { Err(e) => { - return Err(anyhow!("Failed to get database lock: {e}")); - }, - Ok(hdl) => { - match hdl.as_ref() { - Some(connection) => { - let query = format!("REPLACE INTO ssh_table (name, path) VALUES ('public', '{}'), ('private', '{}');", + Err(anyhow!("Failed to get database lock: {e}")) + } + Ok(hdl) => match hdl.as_ref() { + Some(connection) => { + let query = format!("REPLACE INTO ssh_table (name, path) VALUES ('public', '{}'), ('private', '{}');", public, private); - connection.execute(query)?; - return Ok(()); - }, - None => { - return Err(anyhow!("Store is not initialized")); - } + connection.execute(query)?; + Ok(()) } - } + None => { + Err(anyhow!("Store is not initialized")) + } + }, } } @@ -158,22 +136,22 @@ pub fn set_ssh(public: &String, private: &String) -> Result<(), anyhow::Error> { pub fn set_station(name: &String, ip: &String) -> Result<(), anyhow::Error> { match STORE_HANDLE.lock() { Err(e) => { - return Err(anyhow!("Failed to get database lock: {e}")); - }, - Ok(hdl) => { - match hdl.as_ref() { - Some(connection) => { - let query = format!("REPLACE INTO station_table (name, ip) VALUES ('{}', '{}');", - name, ip); - log::debug!("Query: {}", query); - connection.execute(query)?; - return Ok(()); - }, - None => { - return Err(anyhow!("Store is not initialized")); - } - } + Err(anyhow!("Failed to get database lock: {e}")) } + Ok(hdl) => match hdl.as_ref() { + Some(connection) => { + let query = format!( + "REPLACE INTO station_table (name, ip) VALUES ('{}', '{}');", + name, ip + ); + log::debug!("Query: {}", query); + connection.execute(query)?; + Ok(()) + } + None => { + Err(anyhow!("Store is not initialized")) + } + }, } } @@ -183,43 +161,30 @@ pub fn set_station(name: &String, ip: &String) -> Result<(), anyhow::Error> { pub fn get_station_ip_by_name(name: &String) -> Result { match STORE_HANDLE.lock() { Err(e) => { - return Err(anyhow!("Failed to get database lock: {e}")); - }, - Ok(hdl) => { - match hdl.as_ref() { - Some(connection) => { - let query = format!("SELECT * FROM station_table WHERE name = '{}';", - name); - let mut result = String::new(); - log::debug!("Query: {}", query); - connection.iterate(query, |pairs| { - for &(key, value) in pairs.iter() { - match key { - "ip" => { - match value { - Some(ip) => { - result.push_str(ip); - }, - None => () - } - } - _ => () - } - } - true - })?; - if result.chars().count() > 0 { - log::debug!("Found: {}", result); - return Ok(result); - } else { - return Err(anyhow!("Failed to find station in database")); + Err(anyhow!("Failed to get database lock: {e}")) + } + Ok(hdl) => match hdl.as_ref() { + Some(connection) => { + let query = format!("SELECT * FROM station_table WHERE name = '{}';", name); + let mut result = String::new(); + log::debug!("Query: {}", query); + connection.iterate(query, |pairs| { + for &(key, value) in pairs.iter() { + if key == "ip" {if let Some(ip) = value {result.push_str(ip)}} } - }, - None => { - return Err(anyhow!("Store is not initialized")); + true + })?; + if result.chars().count() > 0 { + log::debug!("Found: {}", result); + Ok(result) + } else { + Err(anyhow!("Failed to find station in database")) } } - } + None => { + Err(anyhow!("Store is not initialized")) + } + }, } } @@ -228,46 +193,34 @@ pub fn get_station_ip_by_name(name: &String) -> Result { pub fn get_station_list() -> Result, anyhow::Error> { match STORE_HANDLE.lock() { Err(e) => { - return Err(anyhow!("Failed to get database lock: {e}")); - }, - Ok(hdl) => { - match hdl.as_ref() { - Some(connection) => { - let query = format!("SELECT * FROM station_table;"); - let mut result = Vec::new(); - connection.iterate(query, |pairs| { - let mut st = Station { - name: String::new(), - ip: String::new() - }; - for &(key, value) in pairs.iter() { - match key { - "name" => { - match value { - Some(n) => st.name.push_str(n), - None => () - } - }, - "ip" => { - match value { - Some(i) => st.ip.push_str(i), - None => () - } - } - _ => () - } + Err(anyhow!("Failed to get database lock: {e}")) + } + Ok(hdl) => match hdl.as_ref() { + Some(connection) => { + let query = "SELECT * FROM station_table;".to_string(); + let mut result = Vec::new(); + connection.iterate(query, |pairs| { + let mut st = Station { + name: String::new(), + ip: String::new(), + }; + for &(key, value) in pairs.iter() { + match key { + "name" => if let Some(n) = value {st.name.push_str(n)}, + "ip" => if let Some(i) = value {st.ip.push_str(i)}, + _ => (), } - result.push(st); - true - })?; - log::debug!("Found: {:?}", result); - return Ok(result); - }, - None => { - return Err(anyhow!("Store is not initialized")); - } + } + result.push(st); + true + })?; + log::debug!("Found: {:?}", result); + Ok(result) } - } + None => { + Err(anyhow!("Store is not initialized")) + } + }, } } @@ -276,28 +229,98 @@ pub fn get_station_list() -> Result, anyhow::Error> { pub fn set_pki_config(pki_dir: &String, infos: &CertificateFields) -> Result<(), anyhow::Error> { match STORE_HANDLE.lock() { Err(e) => { - return Err(anyhow!("Failed to get database lock: {e}")); - }, - Ok(hdl) => { - match hdl.as_ref() { - Some(connection) => { - let query = format!("REPLACE INTO ca_table (param, value) \ + Err(anyhow!("Failed to get database lock: {e}")) + } + Ok(hdl) => match hdl.as_ref() { + Some(connection) => { + let query = format!( + "REPLACE INTO ca_table (param, value) \ VALUES ('directory', '{}'), ('org_name', '{}'), \ ('org_unit', '{}'), ('country', '{}'), \ ('validity', '{}');", - pki_dir, - infos.org_name.as_ref().unwrap_or(&String::from("")).clone(), - infos.org_unit.as_ref().unwrap_or(&String::from("")), - infos.country.as_ref().unwrap_or(&String::from("")), - &infos.validity.unwrap_or(0)); - log::debug!("Query: {}", query); - connection.execute(query)?; - return Ok(()); - }, - None => { - return Err(anyhow!("Store is not initialized")); - } + pki_dir, + infos.org_name.as_ref().unwrap_or(&String::from("")).clone(), + infos.org_unit.as_ref().unwrap_or(&String::from("")), + infos.country.as_ref().unwrap_or(&String::from("")), + &infos.validity.unwrap_or(0) + ); + log::debug!("Query: {}", query); + connection.execute(query)?; + Ok(()) } + None => { + Err(anyhow!("Store is not initialized")) + } + }, + } +} + +pub fn get_pki_dir() -> Result { + match STORE_HANDLE.lock() { + Err(e) => { + Err(anyhow!("Failed to get database lock: {e}")) + } + Ok(hdl) => match hdl.as_ref() { + Some(connection) => { + let query = "SELECT directory FROM ca_table;".to_string(); + let mut result = String::new(); + connection.iterate(query, |pairs| { + for &(key, value) in pairs.iter() { + if key == "directory" { if let Some(dir) = value {result.push_str(dir)}} + } + true + })?; + log::debug!("Found: {:?}", result); + Ok(result) + } + None => { + Err(anyhow!("Store is not initialized")) + } + }, + } +} + +pub fn get_pki_info() -> Result { + match STORE_HANDLE.lock() { + Err(e) => { + Err(anyhow!("Failed to get database lock: {e}")) } + Ok(hdl) => match hdl.as_ref() { + Some(connection) => { + let query = "SELECT directory FROM ca_table;".to_string(); + let mut result = CertificateFields { + org_name: None, + org_unit: None, + country: None, + common_name: None, + validity: None + }; + connection.iterate(query, |pairs| { + for &(key, value) in pairs.iter() { + match key { + "org_name" => if let Some(val) = value {result.org_name = Some(val.to_string())}, + "org_unit" => if let Some(val) = value {result.org_unit = Some(val.to_string())}, + "country" => if let Some(val) = value {result.country = Some(val.to_string())}, + "validity" => if let Some(val) = value { + let num = match val.parse::() { + Ok(n) => n, + Err(_) => { + return true; + } + }; + result.validity = Some(num); + }, + _ => (), + } + } + true + })?; + log::debug!("Found: {:?}", result); + Ok(result) + } + None => { + Err(anyhow!("Store is not initialized")) + } + }, } } \ No newline at end of file diff --git a/keysas-admin/src-tauri/src/utils.rs b/keysas-admin/src-tauri/src/utils.rs new file mode 100644 index 0000000..d039b16 --- /dev/null +++ b/keysas-admin/src-tauri/src/utils.rs @@ -0,0 +1,100 @@ +use std::fs::File; +use std::io::Write; +use std::net::TcpStream; +use std::path::Path; +use pkcs8::der::EncodePem; +use ssh_rs::LocalSession; +use x509_cert::Certificate; +use x509_cert::request::CertReq; +use x509_cert::der::DecodePem; +use anyhow::anyhow; + +use crate::ssh_wrapper::session_exec; + +/// Wrapper function to triger a signing key generation on a station and +/// recover CSRs from it +pub fn cmd_generate_key_and_get_csr( + session: &mut LocalSession, + name: &str +) -> Result<(CertReq, CertReq), anyhow::Error> { + let command = format!("{}{}{}", + "sudo /usr/bin/keysas-sign --generate", + " --name ", name); + let cmd_res = match session_exec(session, &command) { + Ok(res) => res, + Err(why) => { + log::error!("Error on send_command: {:?}", why); + return Err(anyhow!("Connection failed")); + } + }; + + let cert_req = String::from_utf8(cmd_res)?; + + // Recover the CSR from the session command + let mut csrs = cert_req.split('|'); + let csr_cl = match csrs.next().and_then(|pem| + match CertReq::from_pem(pem) { + Ok(c) => Some(c), + Err(e) => { + log::error!("Failed to parse certification request: {e}"); + None + } + } + ) { + Some(csr) => csr, + None => { + return Err(anyhow!("Failed to parse certification request")); + } + }; + + let csr_pq = match csrs.remainder().and_then(|pem| + match CertReq::from_pem(pem) { + Ok(c) => Some(c), + Err(e) => { + log::error!("Failed to parse certification request: {e}"); + None + } + } + ) { + Some(csr) => csr, + None => { + return Err(anyhow!("Failed to parse certification request")); + } + }; + + Ok((csr_cl, csr_pq)) +} + +/// Utility function to load a certificate on the station +/// Kind: +/// - file-cl: certificate for ED25519 station files signing +/// - file-pq: certificate for Dilithium5 station files signing +/// - usb-cl: certificate for ED25519 USB signing +/// - usb-pq: certificate for Dilithium5 USB signing +pub fn send_cert_to_station( + session: &mut LocalSession, + cert: &Certificate, + kind: &str +) -> Result<(), anyhow::Error> { + let output = String::from_utf8(cert.to_pem(pkcs8::LineEnding::LF)?.into())?; + + let command = format!("{}{}{}{}", + "sudo /usr/bin/keysas-sign --load --certtype ", + kind, + " --cert ", + output); + + if let Err(e) = session_exec(session, &command) { + log::error!("Failed to load certificate on the station: {e}"); + return Err(anyhow!("Connection error")); + } + + Ok(()) +} + +pub fn save_certificate(cert: &Certificate, path: &Path) -> Result<(), anyhow::Error> { + let output = String::from_utf8(cert.to_pem(pkcs8::LineEnding::LF)?.into())?; + let mut file = File::create(path)?; + write!(file, "{}", output)?; + Ok(()) +} \ No newline at end of file diff --git a/keysas-backend/src/main.rs b/keysas-backend/src/main.rs index e1cebd4..971923d 100644 --- a/keysas-backend/src/main.rs +++ b/keysas-backend/src/main.rs @@ -163,15 +163,12 @@ fn get_ip() -> Result> { let mut ips = Vec::new(); let addrs = nix::ifaddrs::getifaddrs().unwrap(); for ifaddr in addrs { - match ifaddr.address { - Some(address) => { - let addr = address.to_string(); - let (_, ip) = parse_ip(&addr).unwrap(); - if ifaddr.interface_name == "eth0" && ip.parse::().is_ok() { - ips.push(ip.to_string()); - } + if let Some(address) = ifaddr.address { + let addr = address.to_string(); + let (_, ip) = parse_ip(&addr).unwrap(); + if ifaddr.interface_name == "eth0" && ip.parse::().is_ok() { + ips.push(ip.to_string()); } - None => {} } } Ok(ips) diff --git a/keysas-io/src/main.rs b/keysas-io/src/main.rs index be06cd6..1c80720 100644 --- a/keysas-io/src/main.rs +++ b/keysas-io/src/main.rs @@ -172,7 +172,7 @@ fn hmac_challenge() -> Option { match Store::new(cfg) { Ok(store) => match store.bucket::(Some("Keysas")) { Ok(enrolled_yubikeys) => { - enrolled_yubikeys.get(&hex_string).unwrap().map(|name| name) + enrolled_yubikeys.get(&hex_string).unwrap() } Err(why) => { println!("Error while accessing the Bucket: {why:?}"); diff --git a/keysas-sign/Cargo.toml b/keysas-sign/Cargo.toml index a1c4f4e..116480b 100644 --- a/keysas-sign/Cargo.toml +++ b/keysas-sign/Cargo.toml @@ -10,4 +10,6 @@ clap = { version = "4", default-features = false, features = ["std", "cargo"] } anyhow = "1.0" ed25519-dalek = "1.0" pkcs8 = { version = "0.10", features = ["encryption", "pem"]} -keysas_lib = { path = "../keysas_lib" } \ No newline at end of file +keysas_lib = { path = "../keysas_lib" } +tempfile = "3.4" +x509-cert = "0.2" \ No newline at end of file diff --git a/keysas-sign/src/keysas_sign_tests.rs b/keysas-sign/src/keysas_sign_tests.rs new file mode 100644 index 0000000..6ce5431 --- /dev/null +++ b/keysas-sign/src/keysas_sign_tests.rs @@ -0,0 +1,43 @@ +use ed25519_dalek::Keypair; +use keysas_lib::keysas_key::KeysasKey; +use keysas_lib::keysas_key::KeysasPQKey; +use tempfile::NamedTempFile; + +use crate::generate_signing_keypair; +use crate::Config; + +#[test] +fn test_generate_signing_keypair() { + // Generate temporay path to save the keys + + use pkcs8::der::DecodePem; + use x509_cert::request::CertReq; + let path_cl = NamedTempFile::new().unwrap().into_temp_path(); + let file_cl = path_cl.to_str().unwrap(); + let path_pq = NamedTempFile::new().unwrap().into_temp_path(); + let file_pq = path_pq.to_str().unwrap(); + + // Create mock Config + let config = Config { + generate: true, + load: false, + name: String::from("Keysas_station"), + cert_type: String::from(""), + cert: String::from(""), + }; + + // Generate the key and get the resulting CSRs + let csrs = generate_signing_keypair(&config, &file_cl, &file_pq, "Test").unwrap(); + println!("CSR: {:?}", csrs); + + // Test the private keys by loading them + Keypair::load_keys(&path_cl, "Test").unwrap(); + KeysasPQKey::load_keys(&path_pq, "Test").unwrap(); + + // Test the CSRs by reconstructing them from the function result + let mut csr = csrs.split('|'); + let csr_cl = csr.next().unwrap(); + let csr_pq = csr.remainder().unwrap(); + CertReq::from_pem(csr_cl).unwrap(); + CertReq::from_pem(csr_pq).unwrap(); +} diff --git a/keysas-sign/src/main.rs b/keysas-sign/src/main.rs index e113d69..8f27e43 100644 --- a/keysas-sign/src/main.rs +++ b/keysas-sign/src/main.rs @@ -32,6 +32,7 @@ #![warn(overflowing_literals)] #![warn(deprecated)] #![warn(unused_imports)] +#![feature(str_split_remainder)] pub use anyhow::{anyhow, Context, Result}; use clap::{crate_version, Arg, ArgAction, Command}; @@ -47,9 +48,12 @@ use keysas_lib::keysas_key::KeysasPQKey; use std::path::Path; use std::str; -const FILE_PRIV_CL_PATH: &str = "/etc/keysas/file-sign-cl-priv.pem"; +#[cfg(test)] +mod keysas_sign_tests; + +const FILE_PRIV_CL_PATH: &str = "/etc/keysas/file-sign-cl-priv.p8"; const FILE_CERT_CL_PATH: &str = "/etc/keysas/file-sign-cl-cert.pem"; -const FILE_PRIV_PQ_PATH: &str = "/etc/keysas/file-sign-pq-priv.pem"; +const FILE_PRIV_PQ_PATH: &str = "/etc/keysas/file-sign-pq-priv.p8"; const FILE_CERT_PQ_PATH: &str = "/etc/keysas/file-sign-pq-cert.pem"; const USB_CERT_CL_PATH: &str = "/etc/keysas/usb-ca-cl-cert.pem"; const USB_CERT_PQ_PATH: &str = "/etc/keysas/usb-ca-pq-cert.pem"; @@ -83,9 +87,6 @@ fn command_args() -> Config { .help("Generate a private for signing purpose (Default is false).") .default_value("false") .action(ArgAction::SetTrue) - //.requires("orgname") - //.requires("orgunit") - //.requires("country") .conflicts_with("load") ) .arg( @@ -140,14 +141,19 @@ fn command_args() -> Config { /// Generate a new key and certification request /// The private key is saved to a new file at privkey_path /// The certificate request is a PEM-encoded PKCS#10 structure -fn generate_signing_keypair(config: &Config) -> Result { +fn generate_signing_keypair( + config: &Config, + cl_path: &str, + pq_path: &str, + pwd: &str, +) -> Result { // Generate the private keys let ec_key = Keypair::generate_new()?; let pq_key = KeysasPQKey::generate_new()?; // Save the keys - ec_key.save_keys(Path::new(FILE_PRIV_CL_PATH), KEY_PASSWD)?; - pq_key.save_keys(Path::new(FILE_PRIV_PQ_PATH), KEY_PASSWD)?; + ec_key.save_keys(Path::new(cl_path), pwd)?; + pq_key.save_keys(Path::new(pq_path), pwd)?; let infos = CertificateFields::from_fields(None, None, None, Some(&config.name), None)?; @@ -160,6 +166,8 @@ fn generate_signing_keypair(config: &Config) -> Result { let mut hybrid_csr = String::new(); // Add the ED25519 CSR hybrid_csr.push_str(&ec_csr.to_pem(pkcs8::LineEnding::LF)?); + // Add a delimiter between the two CSR + hybrid_csr.push('|'); // Add the Dilithium5 CSR hybrid_csr.push_str(&pq_csr.to_pem(pkcs8::LineEnding::LF)?); @@ -191,7 +199,7 @@ fn main() -> Result<()> { if config.generate { // This command generate a new signing keypair for the station // and generate a signing request for certificate creation by the admin - match generate_signing_keypair(&config) { + match generate_signing_keypair(&config, FILE_PRIV_CL_PATH, FILE_PRIV_PQ_PATH, KEY_PASSWD) { Ok(r) => { // Return the CSR println!("{}", r); diff --git a/keysas_lib/Cargo.toml b/keysas_lib/Cargo.toml index d2c5508..84f4389 100644 --- a/keysas_lib/Cargo.toml +++ b/keysas_lib/Cargo.toml @@ -20,5 +20,6 @@ ed25519-dalek = "1" rand_dl = {package = "rand", version = "0.7"} rand_core = "0.6.4" oqs = "0.7" -hex-literal = "0.3" +hex-literal = "0.4" tempfile = "3.4" +der = "0.7" diff --git a/keysas_lib/src/certificate_field.rs b/keysas_lib/src/certificate_field.rs index 7ebe489..4f40d4a 100644 --- a/keysas_lib/src/certificate_field.rs +++ b/keysas_lib/src/certificate_field.rs @@ -25,16 +25,21 @@ #![warn(unused_imports)] use anyhow::{anyhow, Context}; -use pkcs8::der::DecodePem; +use der::asn1::SetOfVec; +use der::oid::db::rfc4519; +use der::Any; +use der::Tag; use pkcs8::der::asn1::OctetString; use pkcs8::der::oid::db::rfc5280; +use pkcs8::der::DecodePem; use pkcs8::der::Encode; -use std::str::FromStr; use std::time::Duration; +use x509_cert::attr::AttributeTypeAndValue; use x509_cert::certificate::*; use x509_cert::der::asn1::BitString; use x509_cert::ext::Extension; use x509_cert::name::RdnSequence; +use x509_cert::name::RelativeDistinguishedName; use x509_cert::serial_number::SerialNumber; use x509_cert::spki::AlgorithmIdentifier; use x509_cert::spki::ObjectIdentifier; @@ -99,51 +104,43 @@ impl CertificateFields { /// Generate a distinghuished name from the input fields for the certificate pub fn generate_dn(&self) -> Result { - let mut name = String::new(); + let mut rdn: SetOfVec = SetOfVec::new(); // Add country name if let Some(cn) = &self.country { - name.push_str("C="); - name.push_str(cn); - name.push(','); + rdn.add(AttributeTypeAndValue { + oid: rfc4519::C, + value: Any::new(Tag::PrintableString, cn.as_bytes())?, + })?; } // Add organisation name if let Some(oa) = &self.org_name { - if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { - name.push(','); - } - name.push_str("O="); - name.push_str(oa); - name.push(','); + rdn.add(AttributeTypeAndValue { + oid: rfc4519::O, + value: Any::new(Tag::PrintableString, oa.as_bytes())?, + })?; } // Add organisational unit if let Some(ou) = &self.org_unit { - if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { - name.push(','); - } - name.push_str("OU="); - name.push_str(ou); - name.push(','); + rdn.add(AttributeTypeAndValue { + oid: rfc4519::OU, + value: Any::new(Tag::PrintableString, ou.as_bytes())?, + })?; } // Add common name if let Some(co) = &self.common_name { - if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { - name.push(','); - } - name.push_str("CN="); - name.push_str(co); - name.push(','); + rdn.add(AttributeTypeAndValue { + oid: rfc4519::CN, + value: Any::new(Tag::PrintableString, co.as_bytes())?, + })?; } - // Remove trailing ',' if there is one - if name.chars().nth_back(0).is_some_and(|c| !c.eq(&',')) { - name.pop(); - } + let name = vec![RelativeDistinguishedName::from(rdn)]; - let rdn = RdnSequence::from_str(&name)?; + let rdn = RdnSequence::from(name); Ok(rdn) } diff --git a/keysas_lib/src/keysas_key.rs b/keysas_lib/src/keysas_key.rs index 762956b..463025c 100644 --- a/keysas_lib/src/keysas_key.rs +++ b/keysas_lib/src/keysas_key.rs @@ -136,7 +136,7 @@ impl KeysasKey for Keypair { fn generate_new() -> Result { let mut csprng = OsRng {}; let kp_ed = Keypair::generate(&mut csprng); - Ok(kp_ed) + Ok(kp_ed) } fn load_keys(path: &Path, pwd: &str) -> Result { @@ -292,13 +292,13 @@ impl KeysasKey for KeysasPQKey { private_key: sk_dl, public_key: pk_dl, }; - Ok(kp_pq) + Ok(kp_pq) } fn load_keys(path: &Path, pwd: &str) -> Result { // Important load oqs: oqs::init(); - + // Load the pkcs8 from file let cipher = fs::read(path)?; let enc_pk = match EncryptedPrivateKeyInfo::try_from(cipher.as_slice()) { diff --git a/keysas_lib/src/pki.rs b/keysas_lib/src/pki.rs index 321dd26..11c1e5b 100644 --- a/keysas_lib/src/pki.rs +++ b/keysas_lib/src/pki.rs @@ -29,8 +29,6 @@ use oqs::sig::Algorithm; use oqs::sig::Sig; use rand_dl::rngs::OsRng; use rand_dl::RngCore; -use std::fs; -use std::path::Path; use x509_cert::certificate::*; use x509_cert::der::Encode; use x509_cert::request::CertReq; @@ -109,38 +107,6 @@ use crate::keysas_key::KeysasKey; pub const DILITHIUM5_OID: &str = "1.3.6.1.4.1.2.267.7.8.7"; pub const ED25519_OID: &str = "1.3.101.112"; -fn create_dir_if_not_exist(path: &String) -> Result<(), anyhow::Error> { - if !Path::new(path).is_dir() { - fs::create_dir(path)?; - } - Ok(()) -} - -/// Create the PKI directory hierachy as follows -/// pki_dir -/// |-- CA -/// | |--root -/// | |--st -/// | |--usb -/// |--CRL -/// |--CERT -pub fn create_pki_dir(pki_dir: &String) -> Result<(), anyhow::Error> { - // Test if the directory path is valid - if !Path::new(&pki_dir.trim()).is_dir() { - return Err(anyhow!("Invalid PKI directory path")); - } - - create_dir_if_not_exist(&(pki_dir.to_owned() + "/CA"))?; - create_dir_if_not_exist(&(pki_dir.to_owned() + "/CA/root"))?; - create_dir_if_not_exist(&(pki_dir.to_owned() + "/CA/st"))?; - create_dir_if_not_exist(&(pki_dir.to_owned() + "/CA/usb"))?; - - create_dir_if_not_exist(&(pki_dir.to_owned() + "/CRL"))?; - create_dir_if_not_exist(&(pki_dir.to_owned() + "/CERT"))?; - - Ok(()) -} - /// Generate a X509 certificate from a CSR and a CA keypair /// is_app_cert is set to true if it is an application certificate, otherwise it /// is considered to be a CA certificate diff --git a/keysas_lib/tests/pki_test.rs b/keysas_lib/tests/pki_test.rs index f1571ee..49dfc89 100644 --- a/keysas_lib/tests/pki_test.rs +++ b/keysas_lib/tests/pki_test.rs @@ -60,10 +60,13 @@ fn test_pkcs8_create_and_decrypt_der() { let file = NamedTempFile::new().unwrap(); let path = file.into_temp_path(); + let salt = hex!("79d982e70df91a88"); + let iv = hex!("b2d02d78b2efd9dff694cf8e0af40925"); + let params = pbes2::Parameters::scrypt_aes256cbc( pkcs8::pkcs5::scrypt::Params::recommended(), - &hex!("79d982e70df91a88"), - &hex!("b2d02d78b2efd9dff694cf8e0af40925"), + &salt, + &iv, ) .unwrap(); @@ -122,10 +125,13 @@ fn test_pkcs8_create_and_decrypt_with_public_der() { let file = NamedTempFile::new().unwrap(); let path = file.into_temp_path(); + let salt = hex!("79d982e70df91a88"); + let iv = hex!("b2d02d78b2efd9dff694cf8e0af40925"); + let params = pbes2::Parameters::scrypt_aes256cbc( pkcs8::pkcs5::scrypt::Params::recommended(), - &hex!("79d982e70df91a88"), - &hex!("b2d02d78b2efd9dff694cf8e0af40925"), + &salt, + &iv, ) .unwrap(); From 25642269cf82178f44c1ab176e52971825a514c0 Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Tue, 4 Apr 2023 16:09:34 +0200 Subject: [PATCH 012/160] Addition of tauri dist directory --- .gitignore | 1 - keysas-admin/dist/index.html | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 keysas-admin/dist/index.html diff --git a/.gitignore b/.gitignore index 3334c32..d08675f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ keysas-frontend/dist keysas-frontend/.vscode keysas-admin/node_modules keysas-admin/src-tauri/target -keysas-admin/dist keysas-admin/.vscode keysas-admin/.keysas.dat keysas-admin/src-tauri/.keysas.dat diff --git a/keysas-admin/dist/index.html b/keysas-admin/dist/index.html new file mode 100644 index 0000000..76df1c8 --- /dev/null +++ b/keysas-admin/dist/index.html @@ -0,0 +1,15 @@ + + + + + + + Vite App + + + + +
+ + + \ No newline at end of file From 5e46d95bb219ddf44e210cab18d64e0e6949400e Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Wed, 5 Apr 2023 10:04:56 +0200 Subject: [PATCH 013/160] File signing: keysas-sign code cleanup and doc --- keysas-admin/src-tauri/src/main.rs | 179 ++++--- keysas-admin/src-tauri/src/ssh_wrapper.rs | 5 +- keysas-admin/src-tauri/src/store.rs | 128 ++--- keysas-admin/src-tauri/src/utils.rs | 60 ++- keysas-core/Cargo.toml | 1 - keysas-core/src/keysas-out/main.rs | 545 ++++++++++------------ keysas-io/src/main.rs | 4 +- keysas-sign/src/main.rs | 12 +- 8 files changed, 431 insertions(+), 503 deletions(-) diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index ebe0f0e..066b065 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -28,7 +28,7 @@ )] #![feature(str_split_remainder)] -use std::path::Path; +use anyhow::anyhow; use async_std::task; use keysas_lib::certificate_field::CertificateFields; use keysas_lib::keysas_hybrid_keypair::HybridKeyPair; @@ -36,13 +36,13 @@ use keysas_lib::pki::generate_cert_from_csr; use nom::bytes::complete::take_until; use nom::IResult; use sha2::{Digest, Sha256}; +use std::fs; +use std::io::BufReader; use std::io::Read; +use std::path::Path; use tauri::command; use tauri::{CustomMenuItem, Menu, MenuItem, Submenu}; use tauri_plugin_store::PluginBuilder; -use std::io::BufReader; -use anyhow::anyhow; -use std::fs; mod ssh_wrapper; use crate::ssh_wrapper::*; @@ -146,11 +146,11 @@ async fn init_tauri() -> Result<(), anyhow::Error> { Ok(()) }) .menu(menu) - .on_menu_event(|event| + .on_menu_event(|event| { if event.menu_item_id().eq("quit") { std::process::exit(0); } - ) + }) .invoke_handler(tauri::generate_handler![ reboot, update, @@ -194,9 +194,7 @@ async fn save_sshkeys(public: String, private: String) -> bool { #[command] fn get_sshkeys() -> Result<(String, String), String> { match get_ssh() { - Ok((public, private)) => { - Ok((public, private)) - }, + Ok((public, private)) => Ok((public, private)), Err(e) => { log::error!("Failed to get ssh keys: {e}"); Err(String::from("Store error")) @@ -225,9 +223,7 @@ async fn save_station(name: String, ip: String) -> bool { #[command] fn get_station_ip(name: String) -> Result { match get_station_ip_by_name(&name) { - Ok(res) => { - Ok(res) - }, + Ok(res) => Ok(res), Err(e) => { log::error!("Failed to get station IP: {e}"); Err(String::from("Store error")) @@ -250,7 +246,7 @@ fn list_stations() -> Result { }; log::debug!("Station list: {}", result); Ok(result) - }, + } Err(e) => { log::error!("Failed to get station IP: {e}"); Err(String::from("Store error")) @@ -265,9 +261,7 @@ fn list_stations() -> Result { /// 4. Export the created certificate on the station /// 5. Finally it loads the admin USB signing certificate on the station #[command] -fn init_keysas(ip: String, name: String, - ca_pwd: String - ) -> Result { +fn init_keysas(ip: String, name: String, ca_pwd: String) -> Result { /* Get admin configuration from the store */ // Get SSH key let ssh_key = match get_ssh() { @@ -275,7 +269,7 @@ fn init_keysas(ip: String, name: String, Err(e) => { log::error!("Failed to get private key: {e}"); return Err(String::from("No SSH key")); - } + } }; // Get path to PKI directory @@ -319,8 +313,11 @@ fn init_keysas(ip: String, name: String, // 3. Generate a certificate from the request // Load station CA keypair let st_ca_keys = match HybridKeyPair::load( - ST_CA_KEY_NAME, Path::new(ST_CA_SUB_DIR), - Path::new(ST_CA_SUB_DIR), &ca_pwd) { + ST_CA_KEY_NAME, + Path::new(ST_CA_SUB_DIR), + Path::new(ST_CA_SUB_DIR), + &ca_pwd, + ) { Ok(k) => k, Err(e) => { log::error!("Failed to load station CA key: {e}"); @@ -331,8 +328,11 @@ fn init_keysas(ip: String, name: String, // Load USB CA keypair let usb_keys = match HybridKeyPair::load( - USB_CA_KEY_NAME, Path::new(USB_CA_SUB_DIR), - Path::new(USB_CA_SUB_DIR), &ca_pwd) { + USB_CA_KEY_NAME, + Path::new(USB_CA_SUB_DIR), + Path::new(USB_CA_SUB_DIR), + &ca_pwd, + ) { Ok(k) => k, Err(e) => { log::error!("Failed to load station USB key: {e}"); @@ -361,14 +361,14 @@ fn init_keysas(ip: String, name: String, }; // Save certificates - let path_cl = pki_dir.clone()+CERT_DIR+&name+"-cl.pem"; + let path_cl = pki_dir.clone() + CERT_DIR + &name + "-cl.pem"; if let Err(e) = save_certificate(&cert_cl, Path::new(&path_cl)) { log::error!("Failed to save station certificate: {e}"); session.close(); return Err(String::from("PKI error")); } - let path_pq = pki_dir+CERT_DIR+&name+"-pq.pem"; + let path_pq = pki_dir + CERT_DIR + &name + "-pq.pem"; if let Err(e) = save_certificate(&cert_cl, Path::new(&path_pq)) { log::error!("Failed to save station certificate: {e}"); session.close(); @@ -413,7 +413,7 @@ async fn update(ip: String) -> bool { Err(e) => { log::error!("Failed to get private key: {e}"); return false; - } + } }; let host = format!("{}{}", ip.trim(), ":22"); @@ -437,7 +437,7 @@ async fn update(ip: String) -> bool { return false; } } - session.close(); + session.close(); true } @@ -448,7 +448,7 @@ async fn reboot(ip: String) -> bool { Err(e) => { log::error!("Failed to get private key: {e}"); return false; - } + } }; // Connect to the host let host = format!("{}{}", ip.trim(), ":22"); @@ -464,7 +464,7 @@ async fn reboot(ip: String) -> bool { match session_exec(&mut session, &String::from("sudo /bin/systemctl reboot")) { Ok(_) => { log::info!("Keysas station is rebooting !"); - }, + } Err(why) => { log::error!("Rust error on open_exec: {:?}", why); return false; @@ -481,7 +481,7 @@ async fn shutdown(ip: String) -> bool { Err(e) => { log::error!("Failed to get private key: {e}"); return false; - } + } }; // Connect to the host let mut session = match connect_key(&ip, &private_key) { @@ -495,7 +495,7 @@ async fn shutdown(ip: String) -> bool { Ok(_) => { log::info!("Keysas station is shutting down."); session.close(); - }, + } Err(why) => { log::error!("Rust error on open_exec: {:?}", why); session.close(); @@ -513,7 +513,7 @@ async fn export_sshpubkey(ip: String) -> bool { Err(e) => { log::error!("Failed to get private key: {e}"); return false; - } + } }; log::info!("Exporting public SSH key to {:?}", ip); // Connect to the host @@ -524,19 +524,22 @@ async fn export_sshpubkey(ip: String) -> bool { return false; } }; - - match session_upload(&mut session, public_key.trim(), - &String::from("/home/keysas/.ssh/authorized_keys")) { + + match session_upload( + &mut session, + public_key.trim(), + &String::from("/home/keysas/.ssh/authorized_keys"), + ) { Ok(_) => { log::info!("authorized_keys successfully s-copied !"); - }, + } Err(e) => { log::error!("Rust error on upload: {:?}", e); session.close(); - return false; + return false; } } - + // Once the SSH has been copied, disable password authentication match session_exec(&mut session, &String::from("sudo /usr/bin/sed -i \'s/.*PasswordAuthentication.*/PasswordAuthentication no/\' /etc/ssh/sshd_config && sudo /bin/systemctl restart sshd")) { Ok(res) => { @@ -567,7 +570,7 @@ fn is_alive(name: String) -> Result { Err(e) => { log::error!("Failed to get private key: {e}"); return Err(String::from("Store error")); - } + } }; let ip = match get_station_ip_by_name(&name) { @@ -589,7 +592,7 @@ fn is_alive(name: String) -> Result { match session_exec(&mut session, &String::from("/bin/systemctl status keysas")) { Ok(_) => { log::info!("Keysas is alive."); - }, + } Err(why) => { log::error!("Cannot execute command status: {:?}", why); session.close(); @@ -607,11 +610,11 @@ async fn sign_key(ip: String, password: String) -> bool { Err(e) => { log::error!("Failed to get private key: {e}"); return false; - } + } }; let password = sha256_digest(password.trim()).unwrap(); - + // Connect to the host let host = format!("{}{}", ip.trim(), ":22"); let mut session = match connect_key(&ip, &private_key) { @@ -641,7 +644,7 @@ async fn sign_key(ip: String, password: String) -> bool { let command = format!("{}{}{}", "sudo /usr/bin/", command, " --force"); log::debug!("{}", command); command - }, + } Err(why) => { log::error!("Rust error on session.open_exec: {:?}", why); session.close(); @@ -653,7 +656,7 @@ async fn sign_key(ip: String, password: String) -> bool { match session_exec(&mut session, &command) { Ok(_) => { log::info!("USB storage successfully signed !"); - }, + } Err(why) => { log::error!("Error while sign a USB storage: {:?}", why); session.close(); @@ -678,9 +681,9 @@ async fn revoke_key(ip: String) -> bool { Err(e) => { log::error!("Failed to get private key: {e}"); return false; - } + } }; - + // Connect to the host let host = format!("{}{}", ip.trim(), ":22"); let mut session = match connect_key(&ip, &private_key) { @@ -690,7 +693,7 @@ async fn revoke_key(ip: String) -> bool { return false; } }; - + let command = "sudo /usr/bin/keysas-sign --watch".to_string(); let stdout = match session_exec(&mut session, &command) { Ok(stdout) => stdout, @@ -700,21 +703,16 @@ async fn revoke_key(ip: String) -> bool { return false; } }; - + let command = match String::from_utf8(stdout) { Ok(signme) => { let signme = signme.trim(); let (command, _) = parser(signme).unwrap(); let (_, command) = parser_revoke(command).unwrap(); - let command = format!( - "{}{}{}", - "sudo /usr/bin/", - command.trim(), - " --revoke" - ); + let command = format!("{}{}{}", "sudo /usr/bin/", command.trim(), " --revoke"); log::debug!("{}", command); command - }, + } Err(e) => { log::error!("Error while revoking a USB storage: {:?}", e); session.close(); @@ -726,7 +724,7 @@ async fn revoke_key(ip: String) -> bool { match session_exec(&mut session, &command) { Ok(_) => { log::info!("USB storage successfully revoked !"); - }, + } Err(e) => { log::error!("Error while revoking a USB storage: {:?}", e); session.close(); @@ -740,8 +738,7 @@ async fn revoke_key(ip: String) -> bool { #[command] fn validate_privatekey(public_key: String, private_key: String) -> bool { - Path::new(&public_key.trim()).is_file() - && Path::new(&private_key.trim()).is_file() + Path::new(&public_key.trim()).is_file() && Path::new(&private_key.trim()).is_file() } #[command] @@ -750,7 +747,7 @@ fn validate_rootkey(root_key: String) -> bool { } /// Generate a new PKI in an empty directory -/// +/// /// # Arguments /// * `org_name` - String containing the organisation name of the PKI /// * `org_unit` - String containing the organisational unit of the PKI @@ -758,20 +755,26 @@ fn validate_rootkey(root_key: String) -> bool { /// * `validity` - String representation of the number of days of validity for PKI root keys /// * `admin_pwd` - String containing the PKI administrator password /// * `pki_dir` - String containing the path the PKI directory -/// +/// /// # Return /// Return a result containing an error message if any #[command] -fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, - validity: String, admin_pwd: String, - pki_dir: String) -> Result { +fn generate_pki_in_dir( + org_name: String, + org_unit: String, + country: String, + validity: String, + admin_pwd: String, + pki_dir: String, +) -> Result { // Validate user inputs - let infos = match CertificateFields::from_fields ( + let infos = match CertificateFields::from_fields( Some(&org_name), - Some(&org_unit), + Some(&org_unit), Some(&country), None, - Some(&validity)) { + Some(&validity), + ) { Ok(i) => i, Err(_) => { log::error!("Failed to validate user input"); @@ -803,19 +806,15 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, PKI_ROOT_KEY_NAME, Path::new(&(pki_dir.to_owned() + PKI_ROOT_SUB_DIR)), Path::new(&(pki_dir.to_owned() + PKI_ROOT_SUB_DIR)), - &admin_pwd) { + &admin_pwd, + ) { log::error!("Failed to save root key to disk: {e}"); return Err(String::from("PKI error")); } - + // Generate keysas station intermediate CA key pair - let ca_infos = match CertificateFields::from_fields( - None, - None, - None, - Some("Station CA"), - None - ) { + let ca_infos = match CertificateFields::from_fields(None, None, None, Some("Station CA"), None) + { Ok(i) => i, Err(e) => { log::error!("Failed to generate station CA name field: {e}"); @@ -829,31 +828,28 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, return Err(String::from("PKI error")); } }; - let st_ca_keys = match HybridKeyPair::generate_signed_keypair(&root_keys, &ca_name, &infos, false) { - Ok(kp) => kp, - Err(e) => { - log::error!("Failed to generate intermediate CA for station: {e}"); - return Err(String::from("PKI error")); - } - }; + let st_ca_keys = + match HybridKeyPair::generate_signed_keypair(&root_keys, &ca_name, &infos, false) { + Ok(kp) => kp, + Err(e) => { + log::error!("Failed to generate intermediate CA for station: {e}"); + return Err(String::from("PKI error")); + } + }; // Save keys if let Err(e) = st_ca_keys.save( ST_CA_KEY_NAME, Path::new(&(pki_dir.to_owned() + ST_CA_SUB_DIR)), Path::new(&(pki_dir.to_owned() + ST_CA_SUB_DIR)), - &admin_pwd) { + &admin_pwd, + ) { log::error!("Failed to save station CA key to disk: {e}"); return Err(String::from("PKI error")); } // Generate USB signing key pair - let usb_infos = match CertificateFields::from_fields( - None, - None, - None, - Some("USB admin"), - None - ) { + let usb_infos = match CertificateFields::from_fields(None, None, None, Some("USB admin"), None) + { Ok(i) => i, Err(e) => { log::error!("Failed to generate station CA name field: {e}"); @@ -867,8 +863,8 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, return Err(String::from("PKI error")); } }; - let usb_keys = match HybridKeyPair::generate_signed_keypair( - &root_keys, &usb_name, &infos, true) { + let usb_keys = match HybridKeyPair::generate_signed_keypair(&root_keys, &usb_name, &infos, true) + { Ok(kp) => kp, Err(e) => { log::error!("Failed to generate USB signing key pair: {e}"); @@ -880,7 +876,8 @@ fn generate_pki_in_dir(org_name: String, org_unit: String, country: String, USB_CA_KEY_NAME, Path::new(&(pki_dir.to_owned() + USB_CA_SUB_DIR)), Path::new(&(pki_dir + USB_CA_SUB_DIR)), - &admin_pwd) { + &admin_pwd, + ) { log::error!("Failed to save station CA key to disk: {e}"); return Err(String::from("PKI error")); } diff --git a/keysas-admin/src-tauri/src/ssh_wrapper.rs b/keysas-admin/src-tauri/src/ssh_wrapper.rs index a233a9b..54d40d0 100644 --- a/keysas-admin/src-tauri/src/ssh_wrapper.rs +++ b/keysas-admin/src-tauri/src/ssh_wrapper.rs @@ -11,10 +11,7 @@ const USER: &str = "keysas"; const PASSWORD: &str = "Changeme007"; /// Create SSH connexion with RSA or ECC key -pub fn connect_key( - ip: &str, - private_key: &str, -) -> Result, Box> { +pub fn connect_key(ip: &str, private_key: &str) -> Result, Box> { let host = format!("{}{}", ip.trim(), ":22"); let connector = ssh::create_session_without_default() .username(USER) diff --git a/keysas-admin/src-tauri/src/store.rs b/keysas-admin/src-tauri/src/store.rs index 8ff87d1..345f8ad 100644 --- a/keysas-admin/src-tauri/src/store.rs +++ b/keysas-admin/src-tauri/src/store.rs @@ -72,9 +72,7 @@ pub fn init_store(path: &str) -> Result<(), anyhow::Error> { /// Return a tuple containing (path to public ssh key, path to private ssh key) pub fn get_ssh() -> Result<(String, String), anyhow::Error> { match STORE_HANDLE.lock() { - Err(e) => { - Err(anyhow!("Failed to get database lock: {e}")) - } + Err(e) => Err(anyhow!("Failed to get database lock: {e}")), Ok(hdl) => match hdl.as_ref() { Some(connection) => { let mut public = String::new(); @@ -82,13 +80,21 @@ pub fn get_ssh() -> Result<(String, String), anyhow::Error> { connection.iterate(GET_PUBLIC_QUERY, |pairs| { for &(key, value) in pairs.iter() { - if key == "path" { if let Some(p) = value {public.push_str(p)}} + if key == "path" { + if let Some(p) = value { + public.push_str(p) + } + } } true })?; connection.iterate(GET_PRIVATE_QUERY, |pairs| { for &(key, value) in pairs.iter() { - if key == "path" {if let Some(p) = value {private.push_str(p)}} + if key == "path" { + if let Some(p) = value { + private.push_str(p) + } + } } true })?; @@ -99,9 +105,7 @@ pub fn get_ssh() -> Result<(String, String), anyhow::Error> { Err(anyhow!("Failed to find station in database")) } } - None => { - Err(anyhow!("Store is not initialized")) - } + None => Err(anyhow!("Store is not initialized")), }, } } @@ -114,9 +118,7 @@ pub fn set_ssh(public: &String, private: &String) -> Result<(), anyhow::Error> { } match STORE_HANDLE.lock() { - Err(e) => { - Err(anyhow!("Failed to get database lock: {e}")) - } + Err(e) => Err(anyhow!("Failed to get database lock: {e}")), Ok(hdl) => match hdl.as_ref() { Some(connection) => { let query = format!("REPLACE INTO ssh_table (name, path) VALUES ('public', '{}'), ('private', '{}');", @@ -124,9 +126,7 @@ pub fn set_ssh(public: &String, private: &String) -> Result<(), anyhow::Error> { connection.execute(query)?; Ok(()) } - None => { - Err(anyhow!("Store is not initialized")) - } + None => Err(anyhow!("Store is not initialized")), }, } } @@ -135,9 +135,7 @@ pub fn set_ssh(public: &String, private: &String) -> Result<(), anyhow::Error> { /// The function first checks that the path are valid files pub fn set_station(name: &String, ip: &String) -> Result<(), anyhow::Error> { match STORE_HANDLE.lock() { - Err(e) => { - Err(anyhow!("Failed to get database lock: {e}")) - } + Err(e) => Err(anyhow!("Failed to get database lock: {e}")), Ok(hdl) => match hdl.as_ref() { Some(connection) => { let query = format!( @@ -148,9 +146,7 @@ pub fn set_station(name: &String, ip: &String) -> Result<(), anyhow::Error> { connection.execute(query)?; Ok(()) } - None => { - Err(anyhow!("Store is not initialized")) - } + None => Err(anyhow!("Store is not initialized")), }, } } @@ -160,9 +156,7 @@ pub fn set_station(name: &String, ip: &String) -> Result<(), anyhow::Error> { /// the database pub fn get_station_ip_by_name(name: &String) -> Result { match STORE_HANDLE.lock() { - Err(e) => { - Err(anyhow!("Failed to get database lock: {e}")) - } + Err(e) => Err(anyhow!("Failed to get database lock: {e}")), Ok(hdl) => match hdl.as_ref() { Some(connection) => { let query = format!("SELECT * FROM station_table WHERE name = '{}';", name); @@ -170,7 +164,11 @@ pub fn get_station_ip_by_name(name: &String) -> Result { log::debug!("Query: {}", query); connection.iterate(query, |pairs| { for &(key, value) in pairs.iter() { - if key == "ip" {if let Some(ip) = value {result.push_str(ip)}} + if key == "ip" { + if let Some(ip) = value { + result.push_str(ip) + } + } } true })?; @@ -181,9 +179,7 @@ pub fn get_station_ip_by_name(name: &String) -> Result { Err(anyhow!("Failed to find station in database")) } } - None => { - Err(anyhow!("Store is not initialized")) - } + None => Err(anyhow!("Store is not initialized")), }, } } @@ -192,9 +188,7 @@ pub fn get_station_ip_by_name(name: &String) -> Result { /// Returns an error in case of trouble accessing the database pub fn get_station_list() -> Result, anyhow::Error> { match STORE_HANDLE.lock() { - Err(e) => { - Err(anyhow!("Failed to get database lock: {e}")) - } + Err(e) => Err(anyhow!("Failed to get database lock: {e}")), Ok(hdl) => match hdl.as_ref() { Some(connection) => { let query = "SELECT * FROM station_table;".to_string(); @@ -206,8 +200,16 @@ pub fn get_station_list() -> Result, anyhow::Error> { }; for &(key, value) in pairs.iter() { match key { - "name" => if let Some(n) = value {st.name.push_str(n)}, - "ip" => if let Some(i) = value {st.ip.push_str(i)}, + "name" => { + if let Some(n) = value { + st.name.push_str(n) + } + } + "ip" => { + if let Some(i) = value { + st.ip.push_str(i) + } + } _ => (), } } @@ -217,9 +219,7 @@ pub fn get_station_list() -> Result, anyhow::Error> { log::debug!("Found: {:?}", result); Ok(result) } - None => { - Err(anyhow!("Store is not initialized")) - } + None => Err(anyhow!("Store is not initialized")), }, } } @@ -228,9 +228,7 @@ pub fn get_station_list() -> Result, anyhow::Error> { /// Returns Ok or an Error pub fn set_pki_config(pki_dir: &String, infos: &CertificateFields) -> Result<(), anyhow::Error> { match STORE_HANDLE.lock() { - Err(e) => { - Err(anyhow!("Failed to get database lock: {e}")) - } + Err(e) => Err(anyhow!("Failed to get database lock: {e}")), Ok(hdl) => match hdl.as_ref() { Some(connection) => { let query = format!( @@ -248,43 +246,39 @@ pub fn set_pki_config(pki_dir: &String, infos: &CertificateFields) -> Result<(), connection.execute(query)?; Ok(()) } - None => { - Err(anyhow!("Store is not initialized")) - } + None => Err(anyhow!("Store is not initialized")), }, } } pub fn get_pki_dir() -> Result { match STORE_HANDLE.lock() { - Err(e) => { - Err(anyhow!("Failed to get database lock: {e}")) - } + Err(e) => Err(anyhow!("Failed to get database lock: {e}")), Ok(hdl) => match hdl.as_ref() { Some(connection) => { let query = "SELECT directory FROM ca_table;".to_string(); let mut result = String::new(); connection.iterate(query, |pairs| { for &(key, value) in pairs.iter() { - if key == "directory" { if let Some(dir) = value {result.push_str(dir)}} + if key == "directory" { + if let Some(dir) = value { + result.push_str(dir) + } + } } true })?; log::debug!("Found: {:?}", result); Ok(result) } - None => { - Err(anyhow!("Store is not initialized")) - } + None => Err(anyhow!("Store is not initialized")), }, } } pub fn get_pki_info() -> Result { match STORE_HANDLE.lock() { - Err(e) => { - Err(anyhow!("Failed to get database lock: {e}")) - } + Err(e) => Err(anyhow!("Failed to get database lock: {e}")), Ok(hdl) => match hdl.as_ref() { Some(connection) => { let query = "SELECT directory FROM ca_table;".to_string(); @@ -293,15 +287,28 @@ pub fn get_pki_info() -> Result { org_unit: None, country: None, common_name: None, - validity: None + validity: None, }; connection.iterate(query, |pairs| { for &(key, value) in pairs.iter() { match key { - "org_name" => if let Some(val) = value {result.org_name = Some(val.to_string())}, - "org_unit" => if let Some(val) = value {result.org_unit = Some(val.to_string())}, - "country" => if let Some(val) = value {result.country = Some(val.to_string())}, - "validity" => if let Some(val) = value { + "org_name" => { + if let Some(val) = value { + result.org_name = Some(val.to_string()) + } + } + "org_unit" => { + if let Some(val) = value { + result.org_unit = Some(val.to_string()) + } + } + "country" => { + if let Some(val) = value { + result.country = Some(val.to_string()) + } + } + "validity" => { + if let Some(val) = value { let num = match val.parse::() { Ok(n) => n, Err(_) => { @@ -309,7 +316,8 @@ pub fn get_pki_info() -> Result { } }; result.validity = Some(num); - }, + } + } _ => (), } } @@ -318,9 +326,7 @@ pub fn get_pki_info() -> Result { log::debug!("Found: {:?}", result); Ok(result) } - None => { - Err(anyhow!("Store is not initialized")) - } + None => Err(anyhow!("Store is not initialized")), }, } -} \ No newline at end of file +} diff --git a/keysas-admin/src-tauri/src/utils.rs b/keysas-admin/src-tauri/src/utils.rs index d039b16..d2ac19b 100644 --- a/keysas-admin/src-tauri/src/utils.rs +++ b/keysas-admin/src-tauri/src/utils.rs @@ -1,13 +1,13 @@ +use anyhow::anyhow; +use pkcs8::der::EncodePem; +use ssh_rs::LocalSession; use std::fs::File; use std::io::Write; use std::net::TcpStream; use std::path::Path; -use pkcs8::der::EncodePem; -use ssh_rs::LocalSession; -use x509_cert::Certificate; -use x509_cert::request::CertReq; use x509_cert::der::DecodePem; -use anyhow::anyhow; +use x509_cert::request::CertReq; +use x509_cert::Certificate; use crate::ssh_wrapper::session_exec; @@ -15,11 +15,12 @@ use crate::ssh_wrapper::session_exec; /// recover CSRs from it pub fn cmd_generate_key_and_get_csr( session: &mut LocalSession, - name: &str + name: &str, ) -> Result<(CertReq, CertReq), anyhow::Error> { - let command = format!("{}{}{}", - "sudo /usr/bin/keysas-sign --generate", - " --name ", name); + let command = format!( + "{}{}{}", + "sudo /usr/bin/keysas-sign --generate", " --name ", name + ); let cmd_res = match session_exec(session, &command) { Ok(res) => res, Err(why) => { @@ -32,30 +33,28 @@ pub fn cmd_generate_key_and_get_csr( // Recover the CSR from the session command let mut csrs = cert_req.split('|'); - let csr_cl = match csrs.next().and_then(|pem| - match CertReq::from_pem(pem) { - Ok(c) => Some(c), - Err(e) => { - log::error!("Failed to parse certification request: {e}"); - None - } + let csr_cl = match csrs.next().and_then(|pem| match CertReq::from_pem(pem) { + Ok(c) => Some(c), + Err(e) => { + log::error!("Failed to parse certification request: {e}"); + None } - ) { + }) { Some(csr) => csr, None => { return Err(anyhow!("Failed to parse certification request")); } }; - - let csr_pq = match csrs.remainder().and_then(|pem| - match CertReq::from_pem(pem) { + + let csr_pq = match csrs + .remainder() + .and_then(|pem| match CertReq::from_pem(pem) { Ok(c) => Some(c), Err(e) => { log::error!("Failed to parse certification request: {e}"); None } - } - ) { + }) { Some(csr) => csr, None => { return Err(anyhow!("Failed to parse certification request")); @@ -74,16 +73,15 @@ pub fn cmd_generate_key_and_get_csr( pub fn send_cert_to_station( session: &mut LocalSession, cert: &Certificate, - kind: &str + kind: &str, ) -> Result<(), anyhow::Error> { let output = String::from_utf8(cert.to_pem(pkcs8::LineEnding::LF)?.into())?; - - let command = format!("{}{}{}{}", - "sudo /usr/bin/keysas-sign --load --certtype ", - kind, - " --cert ", - output); - + + let command = format!( + "{}{}{}{}", + "sudo /usr/bin/keysas-sign --load --certtype ", kind, " --cert ", output + ); + if let Err(e) = session_exec(session, &command) { log::error!("Failed to load certificate on the station: {e}"); return Err(anyhow!("Connection error")); @@ -97,4 +95,4 @@ pub fn save_certificate(cert: &Certificate, path: &Path) -> Result<(), anyhow::E let mut file = File::create(path)?; write!(file, "{}", output)?; Ok(()) -} \ No newline at end of file +} diff --git a/keysas-core/Cargo.toml b/keysas-core/Cargo.toml index 6c8db40..bbbb849 100644 --- a/keysas-core/Cargo.toml +++ b/keysas-core/Cargo.toml @@ -24,7 +24,6 @@ itertools ="0.10" serde_json = "1.0" syscallz = "0.16" time = "0.3" -oqs = "0.7" base64 = "0.21" ed25519-dalek = "1.0" rand = "0.8" diff --git a/keysas-core/src/keysas-out/main.rs b/keysas-core/src/keysas-out/main.rs index 07c7766..dfc727f 100644 --- a/keysas-core/src/keysas-out/main.rs +++ b/keysas-core/src/keysas-out/main.rs @@ -8,6 +8,39 @@ * for building the keysas-out binary. */ +//! Output daemon +//! +//! Output the files from the processing pipeline into the temporary storage. +//! The daemon also generates a report for the file that contains the result from the checks. +//! +//! The report is JSON file containing the following structure: +//! ```json +//! { +//! "metadata": { +//! "name", // String: File name +//! "date", // String DD-MM-YYYY_HH-mm-SS-NN: Date of creation of the report +//! "file_type", // String: file type +//! "is_valid", // Boolean: true if all checks passed +//! "report": { +//! "yara", // String: yara detailed report +//! "av", // String: clamav detailed report +//! "type_allowed", // Boolean: false if forbidden type detected +//! "size", // u64: file size +//! "corrupted", // boolean: true if file integrity corruption detected +//! "toobig" // Boolean, true file size is too big +//! } +//! }, +//! "binding" : { +//! "file_digest", // String: base64 encoded SHA256 digest of the file +//! "metadata_digest", // String: base64 encoded SHA256 digest of the metadata +//! "station_certificate", // String: concatenation of the station signing certificates PEM +//! "report_signature", // String: base64 encoded concatenation of the ED25519 and Dilithium5 signatures +//! } +//! } +//! ``` +//! +//! The report is signed by the station. + #![feature(unix_socket_ancillary_data)] #![feature(tcp_quickack)] #![warn(unused_extern_crates)] @@ -25,16 +58,15 @@ #![warn(overflowing_literals)] #![warn(deprecated)] #![warn(unused_imports)] -use anyhow::{Context, Result}; +#![warn(missing_docs)] + +use anyhow::Result; use base64::{engine::general_purpose, Engine as _}; use clap::{crate_version, Arg, ArgAction, Command}; -use ed25519_dalek::Keypair; -use ed25519_dalek::Signature as ECSignature; -use ed25519_dalek::Signer; use keysas_lib::append_ext; use keysas_lib::init_logger; +use keysas_lib::keysas_hybrid_keypair::HybridKeyPair; use keysas_lib::keysas_key::KeysasKey; -use keysas_lib::keysas_key::KeysasPQKey; use keysas_lib::sha256_digest; use landlock::{ path_beneath_rules, Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetError, @@ -42,7 +74,7 @@ use landlock::{ }; use log::{error, info, warn}; use nix::unistd; -use oqs::sig::Signature; +use pkcs8::der::EncodePem; use sha2::Digest; use sha2::Sha256; use std::fs::File; @@ -61,75 +93,121 @@ use time::OffsetDateTime; #[macro_use] extern crate serde_derive; +/// Structure that holds a file metadata #[derive(Serialize, Deserialize, Debug, Clone)] struct FileMetadata { + /// Name of the file filename: String, + /// SHA256 digest of the file digest: String, + /// True if a file corruption as occured during processing is_digest_ok: bool, + /// True if the file is toobig is_toobig: bool, + /// Size of the file size: u64, + /// True if the file type is valid is_type_allowed: bool, + /// True if clamav tests pass av_pass: bool, + /// Detailed report of clamav if the test failed av_report: Vec, + /// True if yara tests pass yara_pass: bool, - yara_report: String, // this should be a vec + /// Detailed report of yara if the test failed + yara_report: String, + /// Timestamp of the file entering the station timestamp: String, + /// True if a file corruption occured during the processing is_corrupted: bool, + /// Type of the file file_type: String, } +/// Structure representing a file and its metadata in the daemon #[derive(Debug)] struct FileData { + /// File descriptor fd: i32, + /// Associated file metadata md: FileMetadata, } +/// Metadata object in the report. +/// The structure can be serialized to JSON. #[derive(Serialize, Deserialize, Clone)] struct MetaData { + /// Name of the file name: String, + /// Date of the report creation date: String, + /// Type of the file file_type: String, + /// True if the file is correct is_valid: bool, + /// Object containing the detailled [FileReport] report: FileReport, } +/// Signature binding the file and the report. +/// the structure can be serialized to JSON. #[derive(Serialize, Deserialize, Clone)] struct Bd { + /// SHA256 digest of the file encoded in base64 file_digest: String, + /// SHA256 digest of the [MetaData] associated to the file metadata_digest: String, + /// Station certificates: concatenation of its ED25519 and Dilithium5 signing certificates with a '|' delimiter station_certificate: String, - file_signature: String, + /// Report signature: concatenation of the ED25519 and Dilithium5 signatures in base64 + report_signature: String, } +/// Report that will be created for each file. +/// The structure can be serialized to JSON. #[derive(Serialize, Deserialize, Clone)] struct Report { + /// [MetaData] of the file analysis metadata: MetaData, + /// [Bd] binding of the file and the report with the station signature binding: Bd, } +/// Detailed report of the file checks. #[derive(Serialize, Deserialize, Clone)] struct FileReport { + /// Detailed report of the yara checks yara: String, + /// Detailed report of the clamav checks av: Vec, + /// True if the file type is allowed type_allowed: bool, + /// Size of the file size: u64, + /// True if a file corruption occured during the file processing corrupted: bool, + /// True if the file size is too big toobig: bool, } +/// Directory containing the station signing keys +const KEY_FILE_DIR: &str = "/etc/keysas"; +/// Password for the private signing keys PKCS#8 files +const KEY_PASSWD: &str = "Keysas007"; +/// Directory containing the station configuration +const CONFIG_DIRECTORY: &str = "/etc/keysas"; + /// Daemon configuration arguments struct Configuration { - socket_out: String, // Path for the socket with keysas-transit - sas_out: String, // Path to output directory + /// Path to the socket with keysas-transit + socket_out: String, + /// Path to the output directory + sas_out: String, + /// True if the file are allowed to pass even if yara failed yara_clean: bool, - signing_pq_cert: String, - signing_pq_key: String, - signing_cert: String, - signing_key: String, } -const CONFIG_DIRECTORY: &str = "/etc/keysas"; - +/// Setup the landlock sandboxing fn landlock_sandbox(sas_out: &String) -> Result<(), RulesetError> { let abi = ABI::V2; let status = Ruleset::new() @@ -190,38 +268,6 @@ fn parse_args() -> Configuration { .action(clap::ArgAction::SetTrue) .help("Remove the file if a Yara rule matched"), ) - .arg( - Arg::new("signing_pq_cert") - .short('p') - .long("signing_pq_cert") - .default_value("/etc/keysas/file-pq-sign.pem") - .action(clap::ArgAction::Set) - .help("Path to post-quantum signing PEM certificat (must be imported and signed)"), - ) - .arg( - Arg::new("signing_pq_key") - .short('s') - .long("signing_pq_key") - .default_value("/etc/keysas/file-sign-pq-priv.pem") - .action(clap::ArgAction::Set) - .help("Path to secret post-quantum signing key"), - ) - .arg( - Arg::new("signing_cert") - .short('z') - .long("signing_cert") - .default_value("/etc/keysas/file-sign.pem") - .action(clap::ArgAction::Set) - .help("Path to signing PEM certificat (must be imported and signed)"), - ) - .arg( - Arg::new("signing_key") - .short('w') - .long("signing_key") - .default_value("/etc/keysas/file-sign-priv.pem") - .action(clap::ArgAction::Set) - .help("Path to secret signing key"), - ) .arg( Arg::new("version") .short('v') @@ -236,26 +282,12 @@ fn parse_args() -> Configuration { socket_out: matches.get_one::("socket_out").unwrap().to_string(), sas_out: matches.get_one::("sas_out").unwrap().to_string(), yara_clean: matches.get_flag("yara_clean"), - signing_pq_cert: matches - .get_one::("signing_pq_cert") - .unwrap() - .to_string(), - signing_pq_key: matches - .get_one::("signing_pq_key") - .unwrap() - .to_string(), - signing_cert: matches - .get_one::("signing_cert") - .unwrap() - .to_string(), - signing_key: matches - .get_one::("signing_key") - .unwrap() - .to_string(), } } /// This function retrieves the file descriptors and metadata from the messages +/// messages contains the file descriptor +/// buffer contains the associated file metadata fn parse_messages(messages: Messages, buffer: &[u8]) -> Vec { messages .filter_map(|m| m.ok()) @@ -268,7 +300,7 @@ fn parse_messages(messages: Messages, buffer: &[u8]) -> Vec { }) .flatten() .filter_map(|fd| { - // Deserialize metadata + // Deserialize metadata into a [FileMetadata] struct match bincode::deserialize_from::<&[u8], FileMetadata>(buffer) { Ok(meta) => Some(FileData { fd, md: meta }), Err(e) => { @@ -280,243 +312,126 @@ fn parse_messages(messages: Messages, buffer: &[u8]) -> Vec { .collect() } -fn ec_sign( - file_digest: &String, - meta_digest: &String, - secret_key: &str, -) -> Result> { - // First let's sign both digests with ed25519, signing_key must have been saved to_bytes() - if Path::new(secret_key).exists() && Path::new(secret_key).is_file() { - let keypair_loaded = match Keypair::load_keys(Path::new(secret_key), "Keysas007") { - Ok(ed) => ed, - Err(e) => { - log::error!("load_keys: Cannot load ed25519 keys into struct: {e}"); - return Ok(None); - } - }; - // Prepare the String to sign and sign it - let concat = format!("{}-{}", file_digest, meta_digest); - let signature = keypair_loaded.sign(concat.as_bytes()); - Ok(Some(signature)) - } else { - log::warn!("No EC signature was created."); - Ok(None) +/// Wrapper around the report metadata creation +fn generate_report_metadata(f: &FileData) -> MetaData { + let timestamp = format!( + "{}-{}-{}_{}-{}-{}-{}", + OffsetDateTime::now_utc().day(), + OffsetDateTime::now_utc().month(), + OffsetDateTime::now_utc().year(), + OffsetDateTime::now_utc().hour(), + OffsetDateTime::now_utc().minute(), + OffsetDateTime::now_utc().second(), + OffsetDateTime::now_utc().nanosecond() + ); + + let new_file_report = FileReport { + yara: f.md.yara_report.clone(), + av: f.md.av_report.clone(), + type_allowed: f.md.is_type_allowed, + size: f.md.size, + corrupted: f.md.is_corrupted, + toobig: f.md.is_toobig, + }; + + MetaData { + name: f.md.filename.clone(), + date: timestamp, + file_type: f.md.file_type.clone(), + is_valid: f.md.av_pass + && f.md.yara_pass + && !f.md.is_toobig + && !f.md.is_corrupted + && f.md.is_digest_ok + && f.md.is_type_allowed, + report: new_file_report, } } -fn pq_sign( - file_digest: &String, - meta_digest: &String, - ec_signature: String, - secret_pq_key: &str, -) -> Result> { - // Check that secret key is on disk - if Path::new(secret_pq_key).exists() && Path::new(secret_pq_key).is_file() { - // Choosing Dilithium Level 5 - let scheme = oqs::sig::Sig::new(oqs::sig::Algorithm::Dilithium5) - .context("Unable to create new signature scheme")?; - let pq_loaded = match KeysasPQKey::load_keys(Path::new(secret_pq_key), "Keysas007") { - Ok(ed) => ed, - Err(e) => { - log::error!("load_keys: Cannot load ed25519 keys into struct: {e}"); - return Ok(None); - } - }; - // Concat both digest and previously created EC signature - let concat = format!("{}-{}-{}", file_digest, meta_digest, ec_signature); - // Get the final signature - let signature = scheme - .sign(concat.as_bytes(), &pq_loaded.private_key) - .context("Unable to create signature")?; - Ok(Some(signature)) - } else { - log::warn!("No PQ signature was created."); - Ok(None) - } +/// Bind the report to file by signing with ED25519 and Dilithium5 the concatenation +/// of the file digest and the report metadata digest. +/// The two signatures are concatenated (ED25519 first). +/// All the fields of the binding are encoded in base64 +fn bind_and_sign( + f: &FileData, + report_meta: &MetaData, + sign_keys: &HybridKeyPair, + sign_cert: &str, +) -> Result { + // Compute digest of report metadata + let json_string = serde_json::to_string_pretty(&report_meta)?; + + let mut hasher = Sha256::new(); + hasher.update(json_string.as_bytes()); + let result = hasher.finalize(); + + let meta_digest = format!("{result:x}"); + + // Sign the report and the file + let concat = format!("{}-{}", f.md.digest, meta_digest); + + let mut signature = String::new(); + + // Sign with ED25519 + let sign_cl = sign_keys.classic.message_sign(concat.as_bytes())?; + signature.push_str(&String::from_utf8(sign_cl)?); + + // Sign with Dilithium5 + let sign_pq = sign_keys.pq.message_sign(concat.as_bytes())?; + signature.push_str(&String::from_utf8(sign_pq)?); + + // Generate the final report + Ok(Report { + metadata: report_meta.clone(), + binding: Bd { + file_digest: general_purpose::STANDARD.encode(f.md.digest.clone()), + metadata_digest: general_purpose::STANDARD.encode(meta_digest), + station_certificate: sign_cert.to_string(), + report_signature: general_purpose::STANDARD.encode(signature), + }, + }) } /// This function output files and report received from transit /// The function first check the digest of the file received -fn output_files(files: Vec, conf: &Configuration) -> Result<()> { +fn output_files( + files: Vec, + conf: &Configuration, + sign_keys: &HybridKeyPair, + sign_cert: &str, +) -> Result<()> { for mut f in files { let file = unsafe { File::from_raw_fd(f.fd) }; // Position the cursor at the beginning of the file - match unistd::lseek(f.fd, 0, nix::unistd::Whence::SeekSet) { - Ok(_) => (), - Err(e) => { - error!("Unable to lseek on file descriptor: {e:?}, killing myself."); - process::exit(1); - } - } + unistd::lseek(f.fd, 0, nix::unistd::Whence::SeekSet)?; // Check digest - let digest = match sha256_digest(&file) { - Ok(d) => d, - Err(e) => { - error!( - "Failed to calculate digest for file {}: {e}, killing myself.", - f.md.filename - ); - process::exit(1); - } - }; + let digest = sha256_digest(&file)?; // Test if digest is correct if digest.ne(&f.md.digest) { warn!("Digest invalid for file {}", f.md.filename); f.md.is_digest_ok = false; } - // Always Write a report to json format + + // Generate a report + let report_meta = generate_report_metadata(&f); + + // Bind the report to the file and sign it + let new_report = bind_and_sign(&f, &report_meta, sign_keys, sign_cert)?; + + // Write the report to disk let mut path = PathBuf::new(); path.push(conf.sas_out.clone()); path.push(&f.md.filename); let path = append_ext("krp", path); - let mut report = match File::options() + let mut report = File::options() .read(true) .write(true) .create(true) - .open(&path) - { - Ok(f) => { - info!("Writing a report on path: {}", path.display()); - f - } - Err(e) => { - error!( - "Failed to create report for file {}: {e}, killing myself.", - f.md.filename - ); - process::exit(1); - } - }; - let timestamp = format!( - "{}-{}-{}_{}-{}-{}-{}", - OffsetDateTime::now_utc().day(), - OffsetDateTime::now_utc().month(), - OffsetDateTime::now_utc().year(), - OffsetDateTime::now_utc().hour(), - OffsetDateTime::now_utc().minute(), - OffsetDateTime::now_utc().second(), - OffsetDateTime::now_utc().nanosecond() - ); - - let new_file_report = FileReport { - yara: f.md.yara_report.clone(), - av: f.md.av_report.clone(), - type_allowed: f.md.is_type_allowed, - size: f.md.size, - corrupted: f.md.is_corrupted, - toobig: f.md.is_toobig, - }; - - let mut cert_file = Vec::new(); - // Get data from pem cert located in /etc/keysas - if Path::new(&conf.signing_pq_cert).exists() && Path::new(&conf.signing_pq_cert).is_file() { - cert_file = match std::fs::read(&conf.signing_pq_cert) { - Ok(cert_file) => cert_file, - Err(e) => { - error!("Cannot read certificate: {e}"); - continue; - } - }; - } + .open(&path)?; + let json_report = serde_json::to_string_pretty(&new_report)?; - let new_metadata = MetaData { - name: f.md.filename.clone(), - date: timestamp, - file_type: f.md.file_type.clone(), - is_valid: f.md.av_pass - && f.md.yara_pass - && !f.md.is_toobig - && !f.md.is_corrupted - && f.md.is_digest_ok - && f.md.is_type_allowed, - report: new_file_report, - }; - let json_string = serde_json::to_string_pretty(&new_metadata)?; - let mut hasher = Sha256::new(); - hasher.update(json_string.as_bytes()); - let result = hasher.finalize(); - let meta_digest = format!("{result:x}"); - match unistd::lseek(f.fd, 0, nix::unistd::Whence::SeekSet) { - Ok(_) => (), - Err(e) => { - error!("Unable to lseek on file descriptor: {e:?}, killing myself."); - process::exit(1); - } - } - //Check that file is safe and that private keys exist and that pem certs have been generated - let mut signature = String::new(); - if Path::new(&conf.signing_pq_key).is_file() - && new_metadata.is_valid - && Path::new(&conf.signing_pq_cert).is_file() - && Path::new(&conf.signing_key).is_file() - && Path::new(&conf.signing_cert).is_file() - { - let opt_ec_signature = match ec_sign(&f.md.digest, &meta_digest, &conf.signing_key) { - Ok(signature) => signature, - Err(e) => { - error!("Secret signing key is present but unable to sign (EC) on file descriptor: {e:?}, killing myself."); - process::exit(1); - } - }; - let ec_signature = match opt_ec_signature { - Some(signature) => general_purpose::STANDARD.encode(signature.as_ref()), - None => { - log::error!("Cannot get base64 encoded EC signature from bytes."); - String::new() - } - }; - - let opt_pq_signature = match pq_sign( - &f.md.digest, - &meta_digest, - ec_signature, - &conf.signing_pq_key, - ) { - Ok(signature) => signature, - Err(e) => { - error!("Secret signing key is present but unable to sign (PQ) on file descriptor: {e:?}, killing myself."); - process::exit(1); - } - }; - signature = match opt_pq_signature { - Some(signature) => general_purpose::STANDARD.encode(signature.as_ref()), - None => { - log::error!("Cannot get base64 encoded signature from bytes."); - String::new() - } - }; - } - - let new_bd = Bd { - file_digest: general_purpose::STANDARD.encode(f.md.digest.clone()), - metadata_digest: general_purpose::STANDARD.encode(meta_digest), - station_certificate: String::from_utf8(cert_file)?, - file_signature: signature, - }; - let new_report = Report { - metadata: new_metadata, - binding: new_bd, - }; - - let json_report = match serde_json::to_string_pretty(&new_report) { - Ok(j) => j, - Err(e) => { - error!("Cannot serialize MetaData struct to json for writing report: {e:?}, killing myself."); - process::exit(1); - } - }; - - match writeln!(report, "{}", json_report) { - Ok(_) => (), - Err(e) => { - error!( - "Failed to write report for file {}: {e}, killing myself.", - f.md.filename - ); - process::exit(1); - } - } + writeln!(report, "{}", json_report)?; // Test if the check passed, if yes write the file to sas_out if f.md.is_digest_ok @@ -534,38 +449,26 @@ fn output_files(files: Vec, conf: &Configuration) -> Result<()> { path.push(&conf.sas_out); path.push(&f.md.filename); - let output = match File::options().write(true).create(true).open(path) { - Ok(f) => f, - Err(e) => { - error!( - "Failed to create output file {}: {e}, killing myself.", - f.md.filename - ); - process::exit(1); - } - }; + let output = File::options().write(true).create(true).open(path)?; // Position the cursor at the beginning of the file - match unistd::lseek(f.fd, 0, nix::unistd::Whence::SeekSet) { - Ok(_) => (), - Err(e) => { - error!("Unable to lseek on file descriptor: {e:?}, killing myself."); - process::exit(1); - } - } + unistd::lseek(f.fd, 0, nix::unistd::Whence::SeekSet)?; let mut writer = BufWriter::new(output); - match io::copy(&mut reader, &mut writer) { - Ok(_) => (), - Err(e) => { - error!("Failed to output file {}, error {e}", f.md.filename); - } - } + io::copy(&mut reader, &mut writer)?; } + // Release the file in this daemon drop(file); } Ok(()) } +/// Main loop of the daemon +/// It starts by configuring the security (landlock and seccomp), it recovers the +/// station signing keys +/// Then it enters an infinite loops that: +/// 1. Get file descriptors and metadata from transit +/// 2. Parse them +/// 3. Create reports for each file and outputs them with the file to the output directory fn main() -> Result<()> { // TODO activate seccomp @@ -576,9 +479,43 @@ fn main() -> Result<()> { init_logger(); //Init Landlock - landlock_sandbox(&config.sas_out)?; + // Load station signing keys and certificate + let sign_keys = match HybridKeyPair::load( + "file-sign", + Path::new(KEY_FILE_DIR), + Path::new(KEY_FILE_DIR), + KEY_PASSWD, + ) { + Ok(k) => k, + Err(e) => { + error!("Failed to load station signing keys {e}"); + process::exit(1); + } + }; + + // Convert certificates to PEM string so that it can be placed in the reports + let mut sign_cert = String::new(); + let pem_cl = match sign_keys.classic_cert.to_pem(pkcs8::LineEnding::LF) { + Ok(p) => p, + Err(e) => { + error!("Failed to convert certificate to string {e}"); + process::exit(1); + } + }; + sign_cert.push_str(&pem_cl); + // Add a delimiter between the two certificates + sign_cert.push('|'); + let pem_pq = match sign_keys.pq_cert.to_pem(pkcs8::LineEnding::LF) { + Ok(p) => p, + Err(e) => { + error!("Failed to convert certificate to string {e}"); + process::exit(1); + } + }; + sign_cert.push_str(&pem_pq); + // Open socket with keysas-transit let addr_out = SocketAddr::from_abstract_name(&config.socket_out)?; let sock_out = match UnixStream::connect_addr(&addr_out) { @@ -596,9 +533,6 @@ fn main() -> Result<()> { let mut ancillary_buffer_in = [0; 128]; let mut ancillary_in = SocketAncillary::new(&mut ancillary_buffer_in[..]); - // Important: initialize liboqs - oqs::init(); - // Main loop // 1. receive file descriptor and metadata from transit // 2. Write file and report to output @@ -620,7 +554,6 @@ fn main() -> Result<()> { let files = parse_messages(ancillary_in.messages(), &buf_in); // Output file - - output_files(files, &config)?; + output_files(files, &config, &sign_keys, &sign_cert)?; } } diff --git a/keysas-io/src/main.rs b/keysas-io/src/main.rs index 1c80720..cf85951 100644 --- a/keysas-io/src/main.rs +++ b/keysas-io/src/main.rs @@ -171,9 +171,7 @@ fn hmac_challenge() -> Option { // Open the key/value store match Store::new(cfg) { Ok(store) => match store.bucket::(Some("Keysas")) { - Ok(enrolled_yubikeys) => { - enrolled_yubikeys.get(&hex_string).unwrap() - } + Ok(enrolled_yubikeys) => enrolled_yubikeys.get(&hex_string).unwrap(), Err(why) => { println!("Error while accessing the Bucket: {why:?}"); None diff --git a/keysas-sign/src/main.rs b/keysas-sign/src/main.rs index 8f27e43..8650f8e 100644 --- a/keysas-sign/src/main.rs +++ b/keysas-sign/src/main.rs @@ -51,12 +51,12 @@ use std::str; #[cfg(test)] mod keysas_sign_tests; -const FILE_PRIV_CL_PATH: &str = "/etc/keysas/file-sign-cl-priv.p8"; -const FILE_CERT_CL_PATH: &str = "/etc/keysas/file-sign-cl-cert.pem"; -const FILE_PRIV_PQ_PATH: &str = "/etc/keysas/file-sign-pq-priv.p8"; -const FILE_CERT_PQ_PATH: &str = "/etc/keysas/file-sign-pq-cert.pem"; -const USB_CERT_CL_PATH: &str = "/etc/keysas/usb-ca-cl-cert.pem"; -const USB_CERT_PQ_PATH: &str = "/etc/keysas/usb-ca-pq-cert.pem"; +const FILE_PRIV_CL_PATH: &str = "/etc/keysas/file-sign-cl.p8"; +const FILE_CERT_CL_PATH: &str = "/etc/keysas/file-sign-cl.pem"; +const FILE_PRIV_PQ_PATH: &str = "/etc/keysas/file-sign-pq.p8"; +const FILE_CERT_PQ_PATH: &str = "/etc/keysas/file-sign-pq.pem"; +const USB_CERT_CL_PATH: &str = "/etc/keysas/usb-ca-cl.pem"; +const USB_CERT_PQ_PATH: &str = "/etc/keysas/usb-ca-pq.pem"; const KEY_PASSWD: &str = "Keysas007"; From 8387d05928e3a09fb0ab0ee63fd9bec59dc5098a Mon Sep 17 00:00:00 2001 From: lb-anssi Date: Wed, 5 Apr 2023 16:29:52 +0200 Subject: [PATCH 014/160] File signing: added tests and doc --- keysas-core/Cargo.toml | 2 + keysas-core/src/keysas-out/main.rs | 124 +++++++++++++++++++++++++++-- 2 files changed, 121 insertions(+), 5 deletions(-) diff --git a/keysas-core/Cargo.toml b/keysas-core/Cargo.toml index bbbb849..6e546cf 100644 --- a/keysas-core/Cargo.toml +++ b/keysas-core/Cargo.toml @@ -28,6 +28,8 @@ base64 = "0.21" ed25519-dalek = "1.0" rand = "0.8" pkcs8 = {version = "0.10", features = ["encryption", "pem"] } +x509-cert = "0.2" +oqs = "0.7" [[bin]] diff --git a/keysas-core/src/keysas-out/main.rs b/keysas-core/src/keysas-out/main.rs index dfc727f..484f925 100644 --- a/keysas-core/src/keysas-out/main.rs +++ b/keysas-core/src/keysas-out/main.rs @@ -59,6 +59,7 @@ #![warn(deprecated)] #![warn(unused_imports)] #![warn(missing_docs)] +#![feature(str_split_remainder)] use anyhow::Result; use base64::{engine::general_purpose, Engine as _}; @@ -370,15 +371,13 @@ fn bind_and_sign( // Sign the report and the file let concat = format!("{}-{}", f.md.digest, meta_digest); - let mut signature = String::new(); + let mut signature = Vec::new(); // Sign with ED25519 - let sign_cl = sign_keys.classic.message_sign(concat.as_bytes())?; - signature.push_str(&String::from_utf8(sign_cl)?); + signature.append(&mut sign_keys.classic.message_sign(concat.as_bytes())?); // Sign with Dilithium5 - let sign_pq = sign_keys.pq.message_sign(concat.as_bytes())?; - signature.push_str(&String::from_utf8(sign_pq)?); + signature.append(&mut sign_keys.pq.message_sign(concat.as_bytes())?); // Generate the final report Ok(Report { @@ -557,3 +556,118 @@ fn main() -> Result<()> { output_files(files, &config, &sign_keys, &sign_cert)?; } } + +#[cfg(test)] +mod tests_out { + use base64::{engine::general_purpose, Engine}; + use ed25519_dalek::{self, Sha512, Digest}; + use keysas_lib::{keysas_hybrid_keypair::HybridKeyPair, certificate_field::CertificateFields}; + use oqs::sig::{Sig, Algorithm}; + use pkcs8::der::{EncodePem, DecodePem}; + use x509_cert::Certificate; + + use crate::{FileData, FileMetadata, generate_report_metadata, bind_and_sign}; + + #[test] + fn test_metadata_valid_file() { + // Generate dummy file data + let file_data = FileData { + fd: 2, + md: FileMetadata { + filename: "test.txt".to_string(), + digest: "00112233445566778899AABBCCDDEEFF".to_string(), + is_digest_ok: true, + is_toobig: false, + size: 42, + is_type_allowed: true, + av_pass: true, + av_report: Vec::new(), + yara_pass: true, + yara_report: "".to_string(), + timestamp: "timestamp".to_string(), + is_corrupted: false, + file_type: "txt".to_string() + } + }; + + // Generate report metadata + let meta = generate_report_metadata(&file_data); + + // Validate fields + assert_eq!(file_data.md.filename, meta.name); + assert_eq!(file_data.md.file_type, meta.file_type); + assert_eq!(meta.is_valid, true); + + } + + #[test] + fn test_bind_and_sign() { + // Generate temporary keys + let infos = CertificateFields::from_fields( + None, None, None, Some("Test_station"), Some("200") + ).unwrap(); + let sign_keys = HybridKeyPair::generate_root(&infos).unwrap(); + + let mut sign_cert = String::new(); + let pem_cl = sign_keys.classic_cert.to_pem(pkcs8::LineEnding::LF).unwrap(); + sign_cert.push_str(&pem_cl); + // Add a delimiter between the two certificates + sign_cert.push('|'); + let pem_pq = sign_keys.pq_cert.to_pem(pkcs8::LineEnding::LF).unwrap(); + sign_cert.push_str(&pem_pq); + + // Generate dummy file data + let file_data = FileData { + fd: 2, + md: FileMetadata { + filename: "test.txt".to_string(), + digest: "00112233445566778899AABBCCDDEEFF".to_string(), + is_digest_ok: true, + is_toobig: false, + size: 42, + is_type_allowed: true, + av_pass: true, + av_report: Vec::new(), + yara_pass: true, + yara_report: "".to_string(), + timestamp: "timestamp".to_string(), + is_corrupted: false, + file_type: "txt".to_string() + } + }; + + let meta = generate_report_metadata(&file_data); + + let report = bind_and_sign( + &file_data, &meta, &sign_keys, &sign_cert).unwrap(); + // Test the generated report + // Reconstruct the public keys from the binding certficates + let mut certs = report.binding.station_certificate.split('|'); + let cert_cl = Certificate::from_pem(certs.next().unwrap()).unwrap(); + let cert_pq = Certificate::from_pem(certs.remainder().unwrap()).unwrap(); + + let pub_cl = ed25519_dalek::PublicKey::from_bytes(cert_cl.tbs_certificate.subject_public_key_info.subject_public_key.raw_bytes()).unwrap(); + oqs::init(); + let pq_scheme = Sig::new(Algorithm::Dilithium5).unwrap(); + let pub_pq = pq_scheme.public_key_from_bytes(cert_pq.tbs_certificate.subject_public_key_info.subject_public_key.raw_bytes()).unwrap(); + + // Verify the signature of the report + let signature = general_purpose::STANDARD.decode(report.binding.report_signature).unwrap(); + let concat = format!("{}-{}", + String::from_utf8(general_purpose::STANDARD.decode(report.binding.file_digest).unwrap()).unwrap(), + String::from_utf8(general_purpose::STANDARD.decode(report.binding.metadata_digest).unwrap()).unwrap()); + + let mut prehashed = Sha512::new(); + prehashed.update(&concat); + assert_eq!(true, pub_cl.verify_prehashed( + prehashed, + None, + &ed25519_dalek::Signature::from_bytes(&signature[0..ed25519_dalek::SIGNATURE_LENGTH]).unwrap()) + .is_ok()); + + assert_eq!(true, pq_scheme.verify( + concat.as_bytes(), + pq_scheme.signature_from_bytes(&signature[ed25519_dalek::SIGNATURE_LENGTH..]).unwrap(), + pub_pq).is_ok()); + } +} \ No newline at end of file From a4ac69ee6043f8910520f0e0772b87453c765357 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 6 Apr 2023 11:05:54 +0200 Subject: [PATCH 015/160] Cleaning up the code --- keysas-io/Cargo.toml | 13 +-- keysas-io/src/main.rs | 200 ++++++++++++++++++++++-------------------- 2 files changed, 113 insertions(+), 100 deletions(-) diff --git a/keysas-io/Cargo.toml b/keysas-io/Cargo.toml index 71f6726..4d8c9ac 100644 --- a/keysas-io/Cargo.toml +++ b/keysas-io/Cargo.toml @@ -7,15 +7,15 @@ edition = "2021" [dependencies] -tungstenite = "*" -serde = "*" -serde_json = "*" -serde_derive = "*" +tungstenite = "0.18" +serde = "1.0" +serde_json = "1.0" +serde_derive = "1.0" log = "0.4" -anyhow = "*" +anyhow = "1.0" udev = "0.7" regex = "1.7" -libc = "*" +libc = "0.2" nom = "7" minisign = "0.7" clap = { version = "4", default-features = false, features = ["std", "cargo"] } @@ -31,6 +31,7 @@ yubico_manager = "0.9" walkdir = "2.3" landlock = "0.2" flexi_logger = "0.25" +keysas_lib = { path = "../keysas_lib" } [dev-dependencies] criterion = "0.4" diff --git a/keysas-io/src/main.rs b/keysas-io/src/main.rs index cf85951..e9d9afe 100644 --- a/keysas-io/src/main.rs +++ b/keysas-io/src/main.rs @@ -15,6 +15,8 @@ extern crate udev; use anyhow::anyhow; use clap::{crate_version, Arg, Command as Clap_Command}; +use keysas_lib::init_logger; +use log::{debug, error, info, warn}; use regex::Regex; use std::fs::{self, create_dir_all}; use std::path::PathBuf; @@ -122,19 +124,29 @@ impl StrExt for str { } } +// Const because we do not want them to be modifyied +const TMP_DIR: &str = "/var/local/tmp/"; +const SAS_IN: &str = "/var/local/in/"; +const SAS_OUT: &str = "/var/local/out/"; +const LOCK: &str = "/var/local/in/.lock"; +const FIDO_DB: &str = "/etc/keysas/fido_db"; +const VAR_LOCK_DIR: &str = "/var/lock/keysas/"; +const WORKING_IN_FILE: &str = "/var/lock/keysas/keysas-in"; +const WORKING_OUT_FILE: &str = "/var/lock/keysas/keysas-out"; + fn list_yubikey() -> Vec { let mut yubi = Yubico::new(); let mut yubikey_vector = Vec::new(); if let Ok(device) = yubi.find_yubikey() { - //println!( + //info!( // "Vendor ID: {:?} Product ID {:?}", // device.vendor_id, device.product_id //); let concat = format!("{:?}/{:?}", device.vendor_id, device.product_id); yubikey_vector.push(concat); } else { - println!("Fido2: Yubikey not present !"); + debug!("Fido2: Yubikey not present."); } yubikey_vector @@ -142,12 +154,12 @@ fn list_yubikey() -> Vec { fn hmac_challenge() -> Option { // TODO: Must be improved to manage all cases - if Path::new("/etc/keysas/yubikey_db").is_dir() { + if Path::new(FIDO_DB).is_dir() { let mut yubi = Yubico::new(); if let Ok(device) = yubi.find_yubikey() { - println!( - "Yubico found: Vendor ID is {:?}, Product ID is {:?}", + info!( + "New Yubico found: Vendor ID is {:?}, Product ID is {:?}", device.vendor_id, device.product_id ); @@ -166,34 +178,34 @@ fn hmac_challenge() -> Option { let v: &[u8] = hmac_result.deref(); let hex_string = hex::encode(v); - let cfg = kvConfig::new("/etc/keysas/yubikey_db"); + let cfg = kvConfig::new(FIDO_DB); // Open the key/value store match Store::new(cfg) { Ok(store) => match store.bucket::(Some("Keysas")) { Ok(enrolled_yubikeys) => enrolled_yubikeys.get(&hex_string).unwrap(), Err(why) => { - println!("Error while accessing the Bucket: {why:?}"); + error!("Error while accessing the Bucket: {why:?}"); None } }, Err(why) => { - println!("Error while accessing the store: {why:?}"); + error!("Error while accessing the store: {why:?}"); None } } } Err(why) => { - println!("Error while performing hmac challenge {why:?}"); + error!("Error while performing hmac challenge {why:?}"); None } } } else { - println!("Yubikey not found, please insert a Yubikey."); + warn!("Yubikey not found, please insert a Yubikey."); None } } else { - println!("Error: Database /etc/keysas/yubikey_db wasn't found."); + error!("Error: Database Fido database wasn't found."); None } } @@ -217,7 +229,7 @@ fn get_signature(device: &str) -> Result { let signature = str::from_utf8(signature)?; // match parser(&bufstr) { // Ok(signature) => { - // println!("{}",signature); + // info!("{}",signature); // signature}, // Err(err) => err.to_string(), // }; @@ -232,10 +244,10 @@ fn is_signed( id_revision: &str, id_serial: &str, ) -> Result { - println!("Checking signature for device: {device}"); + debug!("Checking signature for device: {device}"); match get_signature(device.remove_last()) { Ok(signature) => { - //println!("Read signature from key: {:?}", signature); + debug!("Read signature from key: {:?}", signature); let pubkey_path = pubkey_path; let pk_box_str = fs::read_to_string(pubkey_path)?; @@ -251,7 +263,7 @@ fn is_signed( //println!("{data}"); let data_reader = Cursor::new(&data); let verified = minisign::verify(&pk, &signature_box, data_reader, true, false, false); - println!("USB device is signed: {verified:?}"); + info!("USB device is signed: {verified:?}"); match verified { Ok(()) => Ok(true), Err(_) => Ok(false), @@ -264,7 +276,7 @@ fn is_signed( fn copy_device_in(device: &Path) -> Result<()> { let dir = tempfile::tempdir()?; let mount_point = dir.path(); - println!("Unsigned USB device {device:?} will be mounted on path: {mount_point:?}"); + info!("Unsigned USB device {device:?} will be mounted on path: {mount_point:?}"); let supported = SupportedFilesystems::new()?; let mount_result = Mount::builder() .fstype(FilesystemType::from(&supported)) @@ -273,18 +285,18 @@ fn copy_device_in(device: &Path) -> Result<()> { match mount_result { Ok(mount) => { // Copying file to the mounted device. - println!("Unsigned device is mounted on: {mount_point:?}"); + info!("Unsigned device is mounted on: {mount_point:?}"); copy_files_in(&mount_point.to_path_buf())?; // Make the mount temporary, so that it will be unmounted on drop. let _mount = mount.into_unmount_drop(UnmountFlags::DETACH); } Err(why) => { - eprintln!("Failed to mount unsigned device: {why}"); + error!("Failed to mount unsigned device: {why}"); let reg = Regex::new(r"/tmp/\.tmp.*")?; for mount in MountIter::new()? { let mnt = mount.as_ref().unwrap().dest.to_str().unwrap(); if reg.is_match(mnt) { - println!("Will umount: {mnt}"); + debug!("Will umount: {mnt}"); } } } @@ -295,7 +307,7 @@ fn copy_device_in(device: &Path) -> Result<()> { fn move_device_out(device: &Path) -> Result { let dir = tempfile::tempdir()?; let mount_point = dir.path(); - println!("Signed USB device {device:?} will be mounted on path: {mount_point:?}"); + info!("Signed USB device {device:?} will be mounted on path: {mount_point:?}"); let supported = SupportedFilesystems::new()?; let mount_result = Mount::builder() .fstype(FilesystemType::from(&supported)) @@ -304,48 +316,44 @@ fn move_device_out(device: &Path) -> Result { match mount_result { Ok(mount) => { // Moving files to the mounted device. - println!("Temporary out mount point: {mount_point:?}"); + info!("Temporary out mount point for signed key: {mount_point:?}"); move_files_out(&mount_point.to_path_buf())?; // Make the mount temporary, so that it will be unmounted on drop. let _mount = mount.into_unmount_drop(UnmountFlags::DETACH); } Err(why) => { - eprintln!("Failed to mount device: {why}"); + error!("Failed to mount signed device: {why}"); } } Ok(mount_point.to_path_buf()) } fn copy_files_in(mount_point: &PathBuf) -> Result<()> { - File::create("/var/local/in/.lock")?; + File::create(LOCK)?; thread::scope(|s| { for e in WalkDir::new(mount_point).into_iter().filter_map(|e| e.ok()) { if e.metadata().expect("Cannot get metadata for file.").is_file() { s.spawn(move |_| { - println!("New entry path found: {}.", e.path().display()); + debug!("New entry path found: {}.", e.path().display()); let path_to_read = e.path().to_str().unwrap(); - //let entry_str = e.path().display(); - //Replacing any ? in case conversion failed - //let path_to_read = - // format!("{}{}{}", &mount_point.to_string_lossy(), "/", &entry_str); let entry = e.file_name().to_string_lossy(); let entry_cleaned = str::replace(&entry, "?", "-"); let path_to_write = format!( "{}{}", - "/var/local/in/", + SAS_IN, diacritics::remove_diacritics(&entry_cleaned) ); let path_to_tmp = format!( "{}{}", - "/var/local/tmp/", + TMP_DIR, diacritics::remove_diacritics(&entry_cleaned) ); // Create a tmp dir to be able to rename files later - let tmp = Path::new("/var/local/tmp/"); + let tmp = Path::new(TMP_DIR); if !tmp.exists() && !tmp.is_dir() { match fs::create_dir(tmp) { - Ok(_)=> println!("Creating tmp directory for writing incoming files !"), - Err(e) => println!("Cannot create tmp directory: {e:?}"), + Ok(_)=> info!("Creating tmp directory for writing incoming files !"), + Err(e) => error!("Cannot create tmp directory: {e:?}"), } } match fs::metadata(path_to_read) { @@ -353,19 +361,19 @@ fn copy_files_in(mount_point: &PathBuf) -> Result<()> { if Path::new(&path_to_read).exists() && !mtdata.is_dir() { match fs::copy(path_to_read, &path_to_tmp) { Ok(_) => { - println!("File {path_to_read} copied to {path_to_tmp}."); - if fs::rename(&path_to_tmp, path_to_write).is_ok() { println!("File {} moved to sas-in.", &path_to_tmp) } + info!("File {path_to_read} copied to {path_to_tmp}."); + if fs::rename(&path_to_tmp, path_to_write).is_ok() { info!("File {} moved to sas-in.", &path_to_tmp) } }, Err(e) => { - println!( + error!( "Error while copying file {path_to_read}: {e:?}" ); let mut report = format!("{}{}", path_to_write, ".failed"); match File::create(&report) { - Ok(_) => println!("io-error report file created."), + Ok(_) => warn!("io-error report file created."), Err(why) => { - eprintln!( + error!( "Failed to create io-error report {report:?}: {why}" ); } @@ -374,21 +382,21 @@ fn copy_files_in(mount_point: &PathBuf) -> Result<()> { report, "Error while copying file: {e:?}" ) { - Ok(_) => println!("io-error report file created."), + Ok(_) => info!("io-error report file updated."), Err(why) => { - eprintln!( + error!( "Failed to write into io-error report {report:?}: {why}" ); } } match unmount(mount_point, UnmountFlags::DETACH) { Ok(()) => { - println!( + debug!( "Early removing mount point: {mount_point:?}" ) } Err(why) => { - eprintln!( + error!( "Failed to unmount {mount_point:?}: {why}" ); } @@ -397,7 +405,7 @@ fn copy_files_in(mount_point: &PathBuf) -> Result<()> { } } } - Err(why) => eprintln!( + Err(why) => error!( "Thread error: Cannot get metadata for file {path_to_read:?}: {why:?}. Terminating thread..." ), }; @@ -406,18 +414,18 @@ fn copy_files_in(mount_point: &PathBuf) -> Result<()> { } }) .expect("Cannot scope threads !"); - println!("Incoming files copied, unlocking."); - if Path::new("/var/local/in/.lock").exists() { - fs::remove_file("/var/local/in/.lock")?; + info!("Incoming files copied sucessfully, unlocking."); + if Path::new(LOCK).exists() { + fs::remove_file(LOCK)?; } Ok(()) } fn move_files_out(mount_point: &PathBuf) -> Result<()> { - let dir = fs::read_dir("/var/local/out/")?; + let dir = fs::read_dir(SAS_OUT)?; for entry in dir { let entry = entry?; - println!("New entry found: {:?}.", entry.file_name()); + debug!("New entry found: {:?}.", entry.file_name()); let path_to_write = format!( "{}{}{}", @@ -427,67 +435,72 @@ fn move_files_out(mount_point: &PathBuf) -> Result<()> { ); let path_to_read = format!( "{}{}", - "/var/local/out/", + SAS_OUT, entry.file_name().to_string_lossy().into_owned() ); if !fs::metadata(&path_to_read)?.is_dir() { match fs::copy(&path_to_read, path_to_write) { - Ok(_) => println!("Copying file: {path_to_read} to signed device."), + Ok(_) => info!("Copying file: {path_to_read} to signed device."), Err(e) => { - println!("Error while copying file to signed device {path_to_read}: {e:?}"); + error!("Error while copying file to signed device {path_to_read}: {e:?}"); match unmount(mount_point, UnmountFlags::DETACH) { - Ok(()) => println!("Early removing mount point: {mount_point:?}"), + Ok(()) => debug!("Early removing mount point: {mount_point:?}"), Err(why) => { - eprintln!("Failed to unmount {mount_point:?}: {why}"); + error!("Failed to unmount {mount_point:?}: {why}"); } } } } fs::remove_file(&path_to_read)?; - println!("Removing file: {path_to_read}."); + info!("Removing file: {path_to_read}."); } } - println!("Moving files to out device done."); + info!("Moving files to outgoing device done."); Ok(()) } + +// Function done for keysas-backend daemon +// Keysas-backend shows the final user +// if the station is busy or not. +// Simple files are created and are watched +// as I do not want any communications +// between these daemons. fn busy_in() -> Result<(), anyhow::Error> { - if !Path::new("/var/lock/keysas").exists() { - create_dir_all("/var/lock/keysas")?; - } else if Path::new("/var/lock/keysas/keysas-out").exists() { - fs::remove_file("/var/lock/keysas/keysas-out")?; - } else if Path::new("/var/lock/keysas/keysas-transit").exists() { - fs::remove_file("/var/lock/keysas/keysas-transit")?; - } else if !Path::new("/var/lock/keysas/keysas-in").exists() { - File::create("/var/lock/keysas/keysas-in")?; + if !Path::new(VAR_LOCK_DIR).exists() { + create_dir_all(VAR_LOCK_DIR)?; + } else if Path::new(WORKING_OUT_FILE).exists() { + fs::remove_file(WORKING_OUT_FILE)?; + } else if !Path::new(WORKING_IN_FILE).exists() { + File::create(WORKING_IN_FILE)?; } else { + debug!("No WORKING_FILES was found.") } Ok(()) } fn busy_out() -> Result<(), anyhow::Error> { - if !Path::new("/var/lock/keysas").exists() { - create_dir_all("/var/lock/keysas")?; - } else if Path::new("/var/lock/keysas/keysas-in").exists() { - fs::remove_file("/var/lock/keysas/keysas-in")?; - } else if Path::new("/var/lock/keysas/keysas-transit").exists() { - fs::remove_file("/var/lock/keysas/keysas-transit")?; - } else if !Path::new("/var/lock/keysas/keysas-out").exists() { - File::create("/var/lock/keysas/keysas-out")?; + if !Path::new(VAR_LOCK_DIR).exists() { + create_dir_all(VAR_LOCK_DIR)?; + } else if Path::new(WORKING_IN_FILE).exists() { + fs::remove_file(WORKING_IN_FILE)?; + } else if !Path::new(WORKING_OUT_FILE).exists() { + File::create(WORKING_OUT_FILE)?; } else { + debug!("No WORKING_FILES was found.") } Ok(()) } fn ready_in() -> Result<(), anyhow::Error> { - if Path::new("/var/lock/keysas/keysas-in").exists() { - fs::remove_file("/var/lock/keysas/keysas-in")?; + if Path::new(WORKING_IN_FILE).exists() { + fs::remove_file(WORKING_IN_FILE)?; } Ok(()) } fn ready_out() -> Result<(), anyhow::Error> { - if Path::new("/var/lock/keysas/keysas-out").exists() { - fs::remove_file("/var/lock/keysas/keysas-out")?; + if Path::new(WORKING_OUT_FILE).exists() { + fs::remove_file(WORKING_OUT_FILE)?; } Ok(()) } @@ -557,12 +570,13 @@ fn main() -> Result<()> { .parse::() .context("Cannot convert YUBIKEY value string into boolean !")?; + init_logger(); let server = TcpListener::bind("127.0.0.1:3013")?; for stream in server.incoming() { let pubkey_path = Arc::clone(&pubkey_path); spawn(move || -> Result<()> { let callback = |_req: &Request, response: Response| { - println!("keysas-io: Received a new websocket handshake."); + info!("keysas-io: Received a new websocket handshake."); Ok(response) }; let mut websocket = accept_hdr(stream?, callback)?; @@ -603,7 +617,7 @@ fn main() -> Result<()> { }; if result < 0 { - println!("Error: ppoll error, result is < 0."); + error!("Error: ppoll error, result is < 0."); } let event = match socket.iter().next() { @@ -614,7 +628,7 @@ fn main() -> Result<()> { } }; - //println!("Event: {:?}", event.event_type()); + debug!("Event type is: {:?}", event.event_type()); if event.action() == Some(OsStr::new("add")) && event.property_value( OsStr::new("DEVTYPE") @@ -677,7 +691,7 @@ fn main() -> Result<()> { // //println!("{:?} = {:?}", property.name(), property.value()); //} // } - println!("New USB device found: {}", device.to_string_lossy()); + info!("New USB device found: {}", device.to_string_lossy()); let product = format!( "{}/{}/{}", id_vendor_id.to_string_lossy(), @@ -713,7 +727,7 @@ fn main() -> Result<()> { Ok(value) => { //Invalid Signature if !value { - println!("Device signature is not valid !"); + info!("Device signature is not valid !"); let keys_in_iter: Vec = keys_in.clone().into_iter().collect(); if !keys_in_iter.contains(&product) { @@ -734,27 +748,27 @@ fn main() -> Result<()> { if yubikey { match hmac_challenge() { Some(name) => { - println!( + info!( "HMAC challenge successfull for user: {name} !" ); copy_device_in(Path::new(&device))?; - println!("Unsigned USB device done."); + info!("Unsigned USB device done."); ready_in()?; } None => { - println!("No user found during HMAC challenge !"); + warn!("No user found during HMAC challenge !"); ready_in()?; } }; } else { copy_device_in(Path::new(&device))?; - println!("Unsigned USB device done."); + info!("Unsigned USB device done."); ready_in()?; } } //Signature ok so this is a out device } else if value { - println!("USB device is signed..."); + info!("USB device is signed."); let keys_out_iter: Vec = keys_out.clone().into_iter().collect(); if !keys_out_iter.contains(&product) { @@ -775,7 +789,7 @@ fn main() -> Result<()> { .write_message(Message::Text(serialized)) .expect("bunbun"); move_device_out(Path::new(&device))?; - println!("Signed USB device done."); + info!("Signed USB device done."); ready_out()?; } } else { @@ -783,7 +797,7 @@ fn main() -> Result<()> { keys_undef.clone().into_iter().collect(); if !keys_undef_iter.contains(&product) { keys_undef.push(product); - println!("Undefined USB device."); + warn!("Undefined USB device."); let yubi: Yubistruct = Yubistruct { active: yubikey, yubikeys: list_yubikey(), @@ -800,7 +814,7 @@ fn main() -> Result<()> { } } Err(e) => { - println!("USB device never signed: {e:?}"); + info!("USB device never signed: {e:?}"); let keys_in_iter: Vec = keys_in.clone().into_iter().collect(); if !keys_in_iter.contains(&product) { // busy_in ? @@ -821,18 +835,16 @@ fn main() -> Result<()> { if yubikey { match hmac_challenge() { Some(name) => { - println!( - "HMAC challenge successfull for user: {name} !" - ); + info!("HMAC challenge successfull for user: {name} !"); copy_device_in(Path::new(&device))?; - println!("Unsigned USB device done."); + info!("Unsigned USB device done."); ready_in()?; } - None => println!("No user found during HMAC challenge !"), + None => warn!("No user found during HMAC challenge !"), }; } else { copy_device_in(Path::new(&device))?; - println!("Unsigned USB device done."); + info!("Unsigned USB device done."); ready_in()?; } } From a93a44efe3ac478a72e0b8a9f5d6fd91583a5b1d Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Tue, 11 Apr 2023 15:49:09 +0200 Subject: [PATCH 016/160] Add SDPX headers + remove feature --- keysas_lib/src/lib.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/keysas_lib/src/lib.rs b/keysas_lib/src/lib.rs index 898d1dd..68c3280 100644 --- a/keysas_lib/src/lib.rs +++ b/keysas_lib/src/lib.rs @@ -1,5 +1,12 @@ -#![feature(is_some_and)] - +// SPDX-License-Identifier: GPL-3.0-only +/* + * The "keysas-lib". + * + * (C) Copyright 2019-2023 Stephane Neveu, Luc Bonnafoux + * + * This file contains various funtions + * for building the keysas_lib. + */ use anyhow::Result; use regex::Regex; use sha2::{Digest, Sha256}; From 17b6a57e17badc9e5a964be4b4cf1e848030f29b Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Tue, 11 Apr 2023 15:49:54 +0200 Subject: [PATCH 017/160] Format --- keysas-core/src/keysas-out/main.rs | 116 ++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 35 deletions(-) diff --git a/keysas-core/src/keysas-out/main.rs b/keysas-core/src/keysas-out/main.rs index 484f925..f4e0a3e 100644 --- a/keysas-core/src/keysas-out/main.rs +++ b/keysas-core/src/keysas-out/main.rs @@ -560,13 +560,13 @@ fn main() -> Result<()> { #[cfg(test)] mod tests_out { use base64::{engine::general_purpose, Engine}; - use ed25519_dalek::{self, Sha512, Digest}; - use keysas_lib::{keysas_hybrid_keypair::HybridKeyPair, certificate_field::CertificateFields}; - use oqs::sig::{Sig, Algorithm}; - use pkcs8::der::{EncodePem, DecodePem}; + use ed25519_dalek::{self, Digest, Sha512}; + use keysas_lib::{certificate_field::CertificateFields, keysas_hybrid_keypair::HybridKeyPair}; + use oqs::sig::{Algorithm, Sig}; + use pkcs8::der::{DecodePem, EncodePem}; use x509_cert::Certificate; - use crate::{FileData, FileMetadata, generate_report_metadata, bind_and_sign}; + use crate::{bind_and_sign, generate_report_metadata, FileData, FileMetadata}; #[test] fn test_metadata_valid_file() { @@ -586,8 +586,8 @@ mod tests_out { yara_report: "".to_string(), timestamp: "timestamp".to_string(), is_corrupted: false, - file_type: "txt".to_string() - } + file_type: "txt".to_string(), + }, }; // Generate report metadata @@ -597,19 +597,21 @@ mod tests_out { assert_eq!(file_data.md.filename, meta.name); assert_eq!(file_data.md.file_type, meta.file_type); assert_eq!(meta.is_valid, true); - } #[test] fn test_bind_and_sign() { // Generate temporary keys - let infos = CertificateFields::from_fields( - None, None, None, Some("Test_station"), Some("200") - ).unwrap(); + let infos = + CertificateFields::from_fields(None, None, None, Some("Test_station"), Some("200")) + .unwrap(); let sign_keys = HybridKeyPair::generate_root(&infos).unwrap(); let mut sign_cert = String::new(); - let pem_cl = sign_keys.classic_cert.to_pem(pkcs8::LineEnding::LF).unwrap(); + let pem_cl = sign_keys + .classic_cert + .to_pem(pkcs8::LineEnding::LF) + .unwrap(); sign_cert.push_str(&pem_cl); // Add a delimiter between the two certificates sign_cert.push('|'); @@ -632,42 +634,86 @@ mod tests_out { yara_report: "".to_string(), timestamp: "timestamp".to_string(), is_corrupted: false, - file_type: "txt".to_string() - } + file_type: "txt".to_string(), + }, }; let meta = generate_report_metadata(&file_data); - let report = bind_and_sign( - &file_data, &meta, &sign_keys, &sign_cert).unwrap(); + let report = bind_and_sign(&file_data, &meta, &sign_keys, &sign_cert).unwrap(); // Test the generated report - // Reconstruct the public keys from the binding certficates + // Reconstruct the public keys from the binding certificates let mut certs = report.binding.station_certificate.split('|'); let cert_cl = Certificate::from_pem(certs.next().unwrap()).unwrap(); let cert_pq = Certificate::from_pem(certs.remainder().unwrap()).unwrap(); - let pub_cl = ed25519_dalek::PublicKey::from_bytes(cert_cl.tbs_certificate.subject_public_key_info.subject_public_key.raw_bytes()).unwrap(); + let pub_cl = ed25519_dalek::PublicKey::from_bytes( + cert_cl + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + ) + .unwrap(); oqs::init(); let pq_scheme = Sig::new(Algorithm::Dilithium5).unwrap(); - let pub_pq = pq_scheme.public_key_from_bytes(cert_pq.tbs_certificate.subject_public_key_info.subject_public_key.raw_bytes()).unwrap(); - + let pub_pq = pq_scheme + .public_key_from_bytes( + cert_pq + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + ) + .unwrap(); + // Verify the signature of the report - let signature = general_purpose::STANDARD.decode(report.binding.report_signature).unwrap(); - let concat = format!("{}-{}", - String::from_utf8(general_purpose::STANDARD.decode(report.binding.file_digest).unwrap()).unwrap(), - String::from_utf8(general_purpose::STANDARD.decode(report.binding.metadata_digest).unwrap()).unwrap()); + let signature = general_purpose::STANDARD + .decode(report.binding.report_signature) + .unwrap(); + let concat = format!( + "{}-{}", + String::from_utf8( + general_purpose::STANDARD + .decode(report.binding.file_digest) + .unwrap() + ) + .unwrap(), + String::from_utf8( + general_purpose::STANDARD + .decode(report.binding.metadata_digest) + .unwrap() + ) + .unwrap() + ); let mut prehashed = Sha512::new(); prehashed.update(&concat); - assert_eq!(true, pub_cl.verify_prehashed( - prehashed, - None, - &ed25519_dalek::Signature::from_bytes(&signature[0..ed25519_dalek::SIGNATURE_LENGTH]).unwrap()) - .is_ok()); - - assert_eq!(true, pq_scheme.verify( - concat.as_bytes(), - pq_scheme.signature_from_bytes(&signature[ed25519_dalek::SIGNATURE_LENGTH..]).unwrap(), - pub_pq).is_ok()); + assert_eq!( + true, + pub_cl + .verify_prehashed( + prehashed, + None, + &ed25519_dalek::Signature::from_bytes( + &signature[0..ed25519_dalek::SIGNATURE_LENGTH] + ) + .unwrap() + ) + .is_ok() + ); + + assert_eq!( + true, + pq_scheme + .verify( + concat.as_bytes(), + pq_scheme + .signature_from_bytes(&signature[ed25519_dalek::SIGNATURE_LENGTH..]) + .unwrap(), + pub_pq + ) + .is_ok() + ); } -} \ No newline at end of file +} From 6e7bf89bbac140a499d77ce94b753d7ba70ce5dc Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Tue, 11 Apr 2023 15:50:32 +0200 Subject: [PATCH 018/160] Validate hybrid signature from certs --- keysas-io/Cargo.toml | 4 +- keysas-io/src/main.rs | 151 +++++++++++++++++++++-------------- keysas_lib/src/keysas_key.rs | 95 ++++++++++++++++++++++ 3 files changed, 189 insertions(+), 61 deletions(-) diff --git a/keysas-io/Cargo.toml b/keysas-io/Cargo.toml index 4d8c9ac..6e55f21 100644 --- a/keysas-io/Cargo.toml +++ b/keysas-io/Cargo.toml @@ -16,8 +16,6 @@ anyhow = "1.0" udev = "0.7" regex = "1.7" libc = "0.2" -nom = "7" -minisign = "0.7" clap = { version = "4", default-features = false, features = ["std", "cargo"] } crossbeam-utils = "0.8" nix = "0.26" @@ -32,6 +30,8 @@ walkdir = "2.3" landlock = "0.2" flexi_logger = "0.25" keysas_lib = { path = "../keysas_lib" } +oqs ="0.7" +ed25519-dalek = "1.0" [dev-dependencies] criterion = "0.4" diff --git a/keysas-io/src/main.rs b/keysas-io/src/main.rs index e9d9afe..c9e3d54 100644 --- a/keysas-io/src/main.rs +++ b/keysas-io/src/main.rs @@ -8,6 +8,7 @@ */ #![feature(atomic_from_mut)] +#![feature(str_split_remainder)] extern crate libc; extern crate regex; @@ -15,7 +16,6 @@ extern crate udev; use anyhow::anyhow; use clap::{crate_version, Arg, Command as Clap_Command}; -use keysas_lib::init_logger; use log::{debug, error, info, warn}; use regex::Regex; use std::fs::{self, create_dir_all}; @@ -41,18 +41,17 @@ extern crate serde_derive; use crate::errors::*; use crossbeam_utils::thread; +use ed25519_dalek::Signature as SignatureDalek; +use keysas_lib::init_logger; +use keysas_lib::keysas_key::PublicKeys; +use keysas_lib::keysas_key::{KeysasHybridPubKeys, KeysasHybridSignature}; use kv::Config as kvConfig; use kv::*; use libc::{c_int, c_short, c_ulong, c_void}; -use minisign::PublicKeyBox; -use minisign::SignatureBox; -use nom::bytes::complete::take; -use nom::error::Error; -use nom::number::complete::be_u32; +use oqs::sig::{Algorithm, Sig}; use proc_mounts::MountIter; use std::fmt::Write; use std::fs::File; -use std::io::Cursor; use std::io::Read; use std::io::Seek; use std::io::SeekFrom; @@ -189,6 +188,7 @@ fn hmac_challenge() -> Option { None } }, + Err(why) => { error!("Error while accessing the store: {why:?}"); None @@ -210,66 +210,85 @@ fn hmac_challenge() -> Option { } } -fn get_signature(device: &str) -> Result { +fn get_signature(device: &str) -> Result { let offset = 512; let mut f = File::options() .read(true) .open(device) .context("Cannot open the USB device to verify the signature.")?; - let mut buf = vec![0u8; 2048]; + let mut buf = Vec::new(); f.seek(SeekFrom::Start(offset))?; f.read_exact(&mut buf)?; - - let (i, len) = be_u32::<&[u8], nom::error::Error<&[u8]>>(&buf).map_err(|err| { - err.map(|err| Error::new(String::from_utf8(err.input.to_vec()), err.code)) - })?; - let (_, signature) = take::>(len)(i).map_err(|err| { - err.map(|err| Error::new(String::from_utf8(err.input.to_vec()), err.code)) - })?; - let signature = str::from_utf8(signature)?; - // match parser(&bufstr) { - // Ok(signature) => { - // info!("{}",signature); - // signature}, - // Err(err) => err.to_string(), - // }; - Ok(signature.to_string()) + let buf_str = String::from_utf8(buf)?; + + let mut signatures = buf_str.split('|'); + //TODO: handle these unwrap + let s_cl = signatures.next().unwrap(); + let s_pq = signatures.remainder().unwrap(); + let sig_dalek = SignatureDalek::from_bytes(s_cl.as_bytes()) + .context("Cannot parse classic signature from bytes")?; + oqs::init(); + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let sig_pq = match pq_scheme.signature_from_bytes(s_pq.as_bytes()) { + Some(sig) => sig, + None => return Err(anyhow!("Cannot parse PQ signature from bytes")), + }; + Ok(KeysasHybridSignature { + classic: sig_dalek, + pq: sig_pq.to_owned(), + }) } fn is_signed( device: &str, - pubkey_path: &str, + ca_cert_cl: &str, + ca_cert_pq: &str, id_vendor_id: &str, id_model_id: &str, id_revision: &str, id_serial: &str, ) -> Result { debug!("Checking signature for device: {device}"); - match get_signature(device.remove_last()) { + //Getting both pubkeys for certs + let opt_pubkeys = match KeysasHybridPubKeys::get_pubkeys_from_certs(ca_cert_cl, ca_cert_pq) { + Ok(o) => o, + Err(e) => { + error!("Cannot get pubkeys from certs: {e}"); + return Ok(false); + } + }; + let pubkeys = match opt_pubkeys { + Some(p) => p, + None => { + error!("No pubkeys found in certificates, cannot build KeysasHybridPubKeys"); + return Ok(false); + } + }; + + //Let's read the hybrid signature from the device + let signatures = match get_signature(device.remove_last()) { Ok(signature) => { - debug!("Read signature from key: {:?}", signature); - let pubkey_path = pubkey_path; - - let pk_box_str = fs::read_to_string(pubkey_path)?; - let signature_box = SignatureBox::from_string(&signature)?; - // Load the public key from the string. - let pk_box = PublicKeyBox::from_string(&pk_box_str)?; - let pk = pk_box.into_public_key()?; - // And verify the data. - let data = format!( - "{}/{}/{}/{}/{}", - id_vendor_id, id_model_id, id_revision, id_serial, "out" - ); - //println!("{data}"); - let data_reader = Cursor::new(&data); - let verified = minisign::verify(&pk, &signature_box, data_reader, true, false, false); - info!("USB device is signed: {verified:?}"); - match verified { - Ok(()) => Ok(true), - Err(_) => Ok(false), - } + info!("Reading signature from device: {:?}", device.remove_last()); + signature + } + Err(e) => { + error!("Cannot parse signature on the device: {e}"); + return Ok(false); + } + }; + let data = format!( + "{}/{}/{}/{}/{}", + id_vendor_id, id_model_id, id_revision, id_serial, "out" + ); + match KeysasHybridPubKeys::verify_key_signatures(data.as_bytes(), signatures, pubkeys) { + Ok(_) => { + info!("USB device is signed"); + return Ok(true); + } + Err(e) => { + info!("Signatures are not matching on USB device: {e}"); + return Ok(false); } - Err(_) => Ok(false), } } @@ -369,7 +388,7 @@ fn copy_files_in(mount_point: &PathBuf) -> Result<()> { "Error while copying file {path_to_read}: {e:?}" ); let mut report = - format!("{}{}", path_to_write, ".failed"); + format!("{}{}", path_to_write, ".ioerror"); match File::create(&report) { Ok(_) => warn!("io-error report file created."), Err(why) => { @@ -543,13 +562,22 @@ fn main() -> Result<()> { .author("Stephane N") .about("keysas-io for USB devices verification.") .arg( - Arg::new("pubkey") + Arg::new("ca-cert-cl") + .short('c') + .long("classiccacert") + .value_name("/etc/keysas/usb-ca-cl.pem") + .value_parser(clap::value_parser!(String)) + .default_value("/etc/keysas/usb-ca-cl.pem") + .help("The path to Classic CA certificate (Default is /etc/keysas/usb-ca-cl.pem)."), + ) + .arg( + Arg::new("ca-cert-pq") .short('p') - .long("pubkey") - .value_name("/path/to/public.pub") + .long("pqcacert") + .value_name("/etc/keysas/usb-ca-pq.pem") .value_parser(clap::value_parser!(String)) - .default_value("/etc/keysas/keysas.pub") - .help("The path to public key (Default is /etc/keysas/keysas.pub)."), + .default_value("/etc/keysas/usb-ca-pq.pem") + .help("The path to post-quantum CA certificate (Default is /etc/keysas/usb-ca-pq.pem)."), ) .arg( Arg::new("yubikey") @@ -562,9 +590,12 @@ fn main() -> Result<()> { ) .get_matches(); - let pubkey = matches.get_one::("pubkey").unwrap(); - let pubkey_path = pubkey.to_string(); - let pubkey_path = Arc::new(pubkey_path); + let ca_cert_cl = matches.get_one::("ca-cert-cl").unwrap(); + let ca_cert_cl = ca_cert_cl.to_string(); + let ca_cert_cl = Arc::new(ca_cert_cl); + let ca_cert_pq = matches.get_one::("ca-cert-pq").unwrap(); + let ca_cert_pq = ca_cert_pq.to_string(); + let ca_cert_pq = Arc::new(ca_cert_pq); let yubikey = matches.get_one::("yubikey").unwrap(); let yubikey = yubikey .parse::() @@ -573,7 +604,8 @@ fn main() -> Result<()> { init_logger(); let server = TcpListener::bind("127.0.0.1:3013")?; for stream in server.incoming() { - let pubkey_path = Arc::clone(&pubkey_path); + let ca_cert_cl = Arc::clone(&ca_cert_cl); + let ca_cert_pq = Arc::clone(&ca_cert_pq); spawn(move || -> Result<()> { let callback = |_req: &Request, response: Response| { info!("keysas-io: Received a new websocket handshake."); @@ -717,7 +749,8 @@ fn main() -> Result<()> { let signed = is_signed( device, - &pubkey_path, + &ca_cert_cl, + &ca_cert_pq, id_vendor_id, id_model_id, id_revision, diff --git a/keysas_lib/src/keysas_key.rs b/keysas_lib/src/keysas_key.rs index 463025c..0eae410 100644 --- a/keysas_lib/src/keysas_key.rs +++ b/keysas_lib/src/keysas_key.rs @@ -25,12 +25,18 @@ #![warn(unused_imports)] use anyhow::{anyhow, Context}; +use der::DecodePem; use ed25519_dalek::Digest; use ed25519_dalek::Keypair; +use ed25519_dalek::PublicKey; use ed25519_dalek::Sha512; +use ed25519_dalek::Signature as SignatureDalek; +use ed25519_dalek::Verifier; use oqs::sig::Algorithm; +use oqs::sig::PublicKey as PqPublicKey; use oqs::sig::SecretKey; use oqs::sig::Sig; +use oqs::sig::Signature as SignatureOqs; use pkcs8::der::asn1::SetOfVec; use pkcs8::pkcs5::pbes2; use pkcs8::EncryptedPrivateKeyInfo; @@ -38,6 +44,8 @@ use pkcs8::PrivateKeyInfo; use rand_dl::rngs::OsRng; use rand_dl::RngCore; use std::fs; +use std::fs::File; +use std::io::Read; use std::path::Path; use x509_cert::certificate::*; use x509_cert::der::asn1::BitString; @@ -59,6 +67,93 @@ pub struct KeysasPQKey { pub public_key: oqs::sig::PublicKey, } +#[derive(Debug)] +pub struct KeysasHybridPubKeys { + pub classic: PublicKey, + pub pq: PqPublicKey, +} + +#[derive(Debug)] +pub struct KeysasHybridSignature { + pub classic: SignatureDalek, + pub pq: SignatureOqs, +} +pub trait PublicKeys { + fn get_pubkeys_from_certs( + path_cl: &str, + path_pq: &str, + ) -> Result, anyhow::Error>; + fn verify_key_signatures( + message: &[u8], + signatures: KeysasHybridSignature, + pubkeys: KeysasHybridPubKeys, + ) -> Result<(), anyhow::Error>; +} + +impl PublicKeys for KeysasHybridPubKeys { + fn get_pubkeys_from_certs( + cert_cl: &str, + cert_pq: &str, + ) -> Result, anyhow::Error> { + let mut cert_cl = File::open(cert_cl) + .context("Cannot open Classic PEM certificate to get the public key")?; + let mut cert_cl_bytes = Vec::new(); + cert_cl + .read_to_end(&mut cert_cl_bytes) + .context("Cannot read Classic certificate file.")?; + let mut cert_pq = File::open(cert_pq) + .context("Cannot open Dilithium PEM certificate to get the public key")?; + let mut cert_pq_bytes = Vec::new(); + cert_pq + .read_to_end(&mut cert_pq_bytes) + .context("Cannot read Dilithium certificate file")?; + let cert_cl = Certificate::from_pem(cert_cl_bytes)?; + let cert_pq = Certificate::from_pem(cert_pq_bytes)?; + + let pub_cl = ed25519_dalek::PublicKey::from_bytes( + cert_cl + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + )?; + oqs::init(); + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let pub_pq = match pq_scheme.public_key_from_bytes( + cert_pq + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + ) { + Some(pk) => pk, + None => return Ok(None), + }; + + Ok(Some(KeysasHybridPubKeys { + classic: pub_cl, + pq: pub_pq.to_owned(), + })) + } + fn verify_key_signatures( + message: &[u8], + signatures: KeysasHybridSignature, + pubkeys: KeysasHybridPubKeys, + ) -> Result<(), anyhow::Error> { + pubkeys + .classic + .verify(message, &signatures.classic) + .context("Invalid Ed25519 signature")?; + oqs::init(); + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + pq_scheme + .verify(message, &signatures.pq, &pubkeys.pq) + .context("Invalid Dilithium5 signature")?; + // If no error has been returned then the signature is valid + Ok(()) + } +} + /// Store a keypair in a PKCS8 file with a password fn store_keypair( prk: &[u8], From 25035218ae1e08a4190797c25dd5f5a36b749f05 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Wed, 12 Apr 2023 08:24:28 +0200 Subject: [PATCH 019/160] oqs: use only dilithium feature --- keysas-admin/src-tauri/Cargo.toml | 2 +- keysas-core/Cargo.toml | 2 +- keysas-io/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/keysas-admin/src-tauri/Cargo.toml b/keysas-admin/src-tauri/Cargo.toml index caa8c00..f6b78c0 100644 --- a/keysas-admin/src-tauri/Cargo.toml +++ b/keysas-admin/src-tauri/Cargo.toml @@ -34,7 +34,7 @@ arbitrary = "1.3" ed25519-dalek = "1" rand_dl = {package = "rand", version = "0.7"} rand_core = "0.6.4" -oqs = "0.7" +oqs = { version = "0.7", default-features = false, features = ["dilithium"] } hex-literal = "0.3" keysas_lib = { path = "../../keysas_lib" } diff --git a/keysas-core/Cargo.toml b/keysas-core/Cargo.toml index 6e546cf..5d44dcb 100644 --- a/keysas-core/Cargo.toml +++ b/keysas-core/Cargo.toml @@ -29,7 +29,7 @@ ed25519-dalek = "1.0" rand = "0.8" pkcs8 = {version = "0.10", features = ["encryption", "pem"] } x509-cert = "0.2" -oqs = "0.7" +oqs = { version = "0.7", default-features = false, features = ["dilithium"] } [[bin]] diff --git a/keysas-io/Cargo.toml b/keysas-io/Cargo.toml index 6e55f21..f5417b5 100644 --- a/keysas-io/Cargo.toml +++ b/keysas-io/Cargo.toml @@ -30,7 +30,7 @@ walkdir = "2.3" landlock = "0.2" flexi_logger = "0.25" keysas_lib = { path = "../keysas_lib" } -oqs ="0.7" +oqs = { version = "0.7", default-features = false, features =["dilithium"] } ed25519-dalek = "1.0" [dev-dependencies] From 3b5c67aa23c652093ea9e34659b6dacbd21c9b0d Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Wed, 12 Apr 2023 08:25:07 +0200 Subject: [PATCH 020/160] Remove minisign crate --- keysas-io/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/keysas-io/src/main.rs b/keysas-io/src/main.rs index c9e3d54..d80de58 100644 --- a/keysas-io/src/main.rs +++ b/keysas-io/src/main.rs @@ -30,7 +30,6 @@ use tungstenite::{ use udev::Event; use walkdir::WalkDir; -extern crate minisign; extern crate proc_mounts; extern crate serde; extern crate serde_json; From fe5f8c755c9847276eda92e743f2f99e4e1ef4f7 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Wed, 12 Apr 2023 08:25:38 +0200 Subject: [PATCH 021/160] Feature became stable --- keysas-sign/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/keysas-sign/src/main.rs b/keysas-sign/src/main.rs index 8650f8e..d943c68 100644 --- a/keysas-sign/src/main.rs +++ b/keysas-sign/src/main.rs @@ -16,7 +16,6 @@ //! This command is used to load certificate on the station, it can be either: //! - file: the certificate corresponds to the private signing key of the station //! - usb: the certificate corresponds to the USB signing authority -#![feature(is_some_and)] #![warn(unused_extern_crates)] #![forbid(non_shorthand_field_patterns)] #![warn(dead_code)] From 30dde7935ec8ee004e675d485469dfc972ddb5c1 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Wed, 12 Apr 2023 13:38:50 +0200 Subject: [PATCH 022/160] Add usb signing functions --- keysas-admin/src-tauri/src/usb_sign.rs | 297 +++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 keysas-admin/src-tauri/src/usb_sign.rs diff --git a/keysas-admin/src-tauri/src/usb_sign.rs b/keysas-admin/src-tauri/src/usb_sign.rs new file mode 100644 index 0000000..6a3cf03 --- /dev/null +++ b/keysas-admin/src-tauri/src/usb_sign.rs @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * The "keysas-sign". + * + * (C) Copyright 2019-2023 Stephane Neveu + * + * The code for keysas-sign binary. + */ + + use anyhow::anyhow; + use clap::{crate_version, Arg, ArgAction, Command}; + use nom::bytes::complete::take; + use nom::number::complete::be_u32; + use std::fs::remove_file; + use std::fs::File; + use std::io; + use std::io::prelude::*; + use std::io::SeekFrom; + use std::path::Path; + extern crate minisign; + use std::io::Cursor; + mod errors; + use crate::errors::*; + use std::fs; + extern crate libc; + extern crate udev; + use libc::{c_int, c_short, c_ulong, c_void}; + use nom::error::Error; + use std::ffi::OsStr; + use std::io::Read; + use std::os::unix::io::AsRawFd; + use std::process; + use std::process::Command as Cmd; + use std::ptr; + use std::str; + use std::thread; + use std::time::Duration; + + #[repr(C)] + struct pollfd { + fd: c_int, + events: c_short, + revents: c_short, + } + + #[repr(C)] + struct sigset_t { + __private: c_void, + } + + #[allow(non_camel_case_types)] + type nfds_t = c_ulong; + + const POLLIN: c_short = 0x0001; + + extern "C" { + fn ppoll( + fds: *mut pollfd, + nfds: nfds_t, + timeout_ts: *mut libc::timespec, + sigmask: *const sigset_t, + ) -> c_int; + } + + trait StrExt { + fn remove_last(&self) -> &str; + } + + impl StrExt for str { + fn remove_last(&self) -> &str { + match self.char_indices().next_back() { + Some((i, _)) => &self[..i], + None => self, + } + } + } + + // Remove the partition number and return the device + // TODO: manage if nb partition >= 10 + fn rm_last(value: &str) -> &str { + let chars = value.chars(); + let mut tmp = chars.clone(); + match chars.last() { + Some(last) => { + if last.is_numeric() { + tmp.next_back(); + return tmp.as_str(); + } else { + return tmp.as_str(); + } + } + None => value, + } + } + + fn signme( + vendor: &str, + model: &str, + revision: &str, + serial: &str, + direction: &str, + privkey_path: &str, + password: &str, + ) -> Result { + let sk_box_str = fs::read_to_string(privkey_path)?; + let sk_box = SecretKeyBox::from_string(&sk_box_str)?; + + // and the box can be opened using the password to reveal the original secret key: + let sk = sk_box.into_secret_key(Some(password.to_string()))?; + + // Now, we can use the secret key to sign anything. + let data = format!("{}/{}/{}/{}/{}", vendor, model, revision, serial, direction); + let data_reader = Cursor::new(&data); + let signature_box = minisign::sign( + None, + &sk, + data_reader, + Some(&data), + Some("Signature from Keysas secret"), + )?; + + // Converting the signature box to a string in order to save it is easy. + Ok(signature_box.into_string()) + } + + fn watch() -> Result { + let socket = udev::MonitorBuilder::new()? + //.match_subsystem_devtype("usb", "usb_device")? + .match_subsystem("block")? + .listen()?; + + let mut fds = vec![pollfd { + fd: socket.as_raw_fd(), + events: POLLIN, + revents: 0, + }]; + log::debug!("Watching... you can plug your device in !"); + + loop { + let result = unsafe { + ppoll( + (&mut fds[..]).as_mut_ptr(), + fds.len() as nfds_t, + ptr::null_mut(), + ptr::null(), + ) + }; + + if result < 0 { + println!("Error: result is < 0."); + } + + let event = match socket.iter().next() { + Some(evt) => evt, + None => { + thread::sleep(Duration::from_millis(5)); + continue; + } + }; + + for _property in event.properties() { + if event.action() == Some(OsStr::new("add")) + && event.property_value( + OsStr::new("DEVTYPE") + .to_str() + .ok_or_else(|| anyhow!("Cannot convert DEVTYPE to str."))?, + ) == Some(OsStr::new("partition")) + { + let dev = event.device(); + let device = dev.devnode().unwrap(); + let dev = &device.to_string_lossy(); + let device = rm_last(dev); + + let vendor = event + .property_value( + OsStr::new("ID_VENDOR_ID") + .to_str() + .ok_or_else(|| anyhow!("Cannot convert ID_VENDOR_ID to str."))?, + ) + .ok_or_else(|| anyhow!("Cannot get ID_VENDOR_ID from event."))?; + let model = event + .property_value( + OsStr::new("ID_MODEL_ID") + .to_str() + .ok_or_else(|| anyhow!("Cannot convert ID_MODEL_ID to str."))?, + ) + .ok_or_else(|| anyhow!("Cannot get ID_MODEL_ID from event."))?; + let revision = event + .property_value( + OsStr::new("ID_REVISION") + .to_str() + .ok_or_else(|| anyhow!("Cannot convert ID_REVISION to str."))?, + ) + .ok_or_else(|| anyhow!("Cannot get ID_REVISION from event."))?; + let serial = event + .property_value( + OsStr::new("ID_SERIAL") + .to_str() + .ok_or_else(|| anyhow!("Cannot convert ID_SERIAL to str."))?, + ) + .ok_or_else(|| anyhow!("Cannot get ID_SERIAL from event."))?; + println!( + "Found key: Vendor: {}, Model: {}, Revision: {}, Serial: {}", + vendor.to_string_lossy(), + model.to_string_lossy(), + revision.to_string_lossy(), + serial.to_string_lossy() + ); + println!( + "To sign your new USB-OUT key, type the following with your own password:" + ); + //println!("keysas-sign --device={} --sign --password=YourSecretPassWord --vendorid={} --modelid={} --revision={} --serial={}", device ,vendor.to_string_lossy() ,model.to_string_lossy(), revision.to_string_lossy(), serial.to_string_lossy()); + let information = format!("New USB device found: Vendor ID: {}, Model ID: {}, Revision: {}, Serial number: {}", vendor.to_string_lossy() ,model.to_string_lossy(), revision.to_string_lossy(), serial.to_string_lossy()); + Ok(information) + } + } + } + } + + fn sign_usb( + device: &str, + vendor: &str, + model: &str, + revision: &str, + serial: &str, + direction: &str, + privkey_path: &str, + password: &str, + ) -> Result<()> { + log::debug!("Let's start signing the new out-key !"); + let mut f = File::options() + .write(true) + .read(true) + .open(device) + .context("Cannot open device for signing.")?; + + let ss = 512; + let mut mbr = mbrman::MBR::new_from(&mut f, ss as u32, [0x00, 0x0A, 0x0B, 0x0C]) + .context("Could not make a partition table")?; + let sectors = mbr + .get_maximum_partition_size() + .context("No more space available in the USB device")?; + + let starting_lba_i32 = 4096; + let starting_lba = starting_lba_i32 as u32; + + mbr[1] = mbrman::MBRPartitionEntry { + boot: mbrman::BOOT_INACTIVE, // boot flag + first_chs: mbrman::CHS::empty(), // first CHS address (only useful for old computers) + sys: 0x0c, // fat32+ LBA filesystem + last_chs: mbrman::CHS::empty(), // last CHS address (only useful for old computers) + starting_lba, // the sector where the partition starts + sectors, // the number of sectors in that partition + }; + + // actually writes the new partition Entry: + mbr.write_into(&mut f) + .context("Could not write MBR to disk")?; + + //Let's write behind the magic number now + let offset = 512; + let attrs = signme( + vendor, + model, + revision, + serial, + direction, + privkey_path, + password, + )?; + let size_u32 = attrs.len() as u32; + f.seek(SeekFrom::Start(offset))?; + f.write_all(&size_u32.to_be_bytes())?; + f.write_all(attrs.as_bytes())?; + log::info!("USB device is now signed successfully."); + Ok(()) + } + + fn revoke_usb(device: &str) -> Result<()> { + println!("Let's start signing the new out-key !"); + let mut f = File::options() + .write(true) + .read(true) + .open(device) + .context("Cannot open device for revoking.")?; + + //Let's write behind the magic number now + let offset = 512; + let blank: String = String::from("0000000"); + let size_u32 = blank.len() as u32; + f.seek(SeekFrom::Start(offset))?; + f.write_all(&size_u32.to_be_bytes())?; + f.write_all(blank.as_bytes())?; + log::info!("USB device is now signed successfully."); + Ok(()) + } + \ No newline at end of file From e909853c5e208bc8b2ac4ae7867a9cc532ecd25e Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Wed, 12 Apr 2023 13:39:50 +0200 Subject: [PATCH 023/160] Remove tauri plugin store --- keysas-admin/src-tauri/src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index 066b065..608d070 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -42,7 +42,6 @@ use std::io::Read; use std::path::Path; use tauri::command; use tauri::{CustomMenuItem, Menu, MenuItem, Submenu}; -use tauri_plugin_store::PluginBuilder; mod ssh_wrapper; use crate::ssh_wrapper::*; @@ -138,7 +137,6 @@ async fn init_tauri() -> Result<(), anyhow::Error> { //.add_item(CustomMenuItem::new("hide", "Hide")) .add_submenu(submenu); tauri::Builder::default() - .plugin(PluginBuilder::default().build()) .setup(|_app| { if let Err(e) = init_store(STORE_PATH) { return Err(e.into()); From a1a7389e7fc888dae8c4afce46056478ef93b725 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Wed, 12 Apr 2023 13:40:46 +0200 Subject: [PATCH 024/160] Add libs for signing device --- keysas-admin/src-tauri/Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keysas-admin/src-tauri/Cargo.toml b/keysas-admin/src-tauri/Cargo.toml index f6b78c0..7c3eeca 100644 --- a/keysas-admin/src-tauri/Cargo.toml +++ b/keysas-admin/src-tauri/Cargo.toml @@ -19,7 +19,6 @@ serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tauri = { version = "1.2.3", features = ["api-all", "updater"] } ssh-rs = "0.3.2" -tauri-plugin-store = { git="https://github.com/r3dlight/tauri-plugin-store", branch="dev" } anyhow = { version = "1.0", features = ["backtrace"] } async-std = "1.12" nom = "7.1" @@ -36,6 +35,9 @@ rand_dl = {package = "rand", version = "0.7"} rand_core = "0.6.4" oqs = { version = "0.7", default-features = false, features = ["dilithium"] } hex-literal = "0.3" +libc = "0.2" +udev = "0.7" +mbrman = "0.5" keysas_lib = { path = "../../keysas_lib" } [features] From f80fc50120d2d582c528fa0c61e1276d3656e178 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 13 Apr 2023 09:25:10 +0200 Subject: [PATCH 025/160] Start modifying keysas-admin to locally sign usb devices --- .gitignore | 1 + keysas-admin/src-tauri/Cargo.toml | 1 + keysas-admin/src-tauri/src/main.rs | 6 +- keysas-admin/src-tauri/src/usb_sign.rs | 558 +++++++++--------- .../src/components/DisplaySigningConfig.vue | 4 + keysas-admin/src/components/SigningKeys.vue | 19 +- keysas-admin/src/views/ManageView.vue | 4 +- keysas-admin/src/views/config.vue | 4 +- 8 files changed, 293 insertions(+), 304 deletions(-) diff --git a/.gitignore b/.gitignore index d08675f..bf8b345 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ keysas-admin/src-tauri/target keysas-admin/.vscode keysas-admin/.keysas.dat keysas-admin/src-tauri/.keysas.dat +.vscode diff --git a/keysas-admin/src-tauri/Cargo.toml b/keysas-admin/src-tauri/Cargo.toml index 7c3eeca..a5525ca 100644 --- a/keysas-admin/src-tauri/Cargo.toml +++ b/keysas-admin/src-tauri/Cargo.toml @@ -38,6 +38,7 @@ hex-literal = "0.3" libc = "0.2" udev = "0.7" mbrman = "0.5" +base64 = "0.21" keysas_lib = { path = "../../keysas_lib" } [features] diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index 608d070..3b586b0 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -7,7 +7,7 @@ * This file contains the main function. */ -#![forbid(unsafe_code)] +//#![forbid(unsafe_code)] #![warn(unused_extern_crates)] #![forbid(non_shorthand_field_patterns)] #![warn(dead_code)] @@ -49,6 +49,8 @@ mod store; use crate::store::*; mod utils; use crate::utils::*; +mod usb_sign; +use crate::usb_sign::*; // TODO: place constant paths in constants const ST_CA_KEY_NAME: &str = "st-ca"; @@ -601,6 +603,7 @@ fn is_alive(name: String) -> Result { Ok(true) } +// TODO: to be modified to work locally #[command] async fn sign_key(ip: String, password: String) -> bool { let private_key = match get_ssh() { @@ -672,6 +675,7 @@ fn parser_revoke(s: &str) -> IResult<&str, &str> { take_until("--sign")(s) } +// TODO: to be modified to work locally #[command] async fn revoke_key(ip: String) -> bool { let private_key = match get_ssh() { diff --git a/keysas-admin/src-tauri/src/usb_sign.rs b/keysas-admin/src-tauri/src/usb_sign.rs index 6a3cf03..9776a49 100644 --- a/keysas-admin/src-tauri/src/usb_sign.rs +++ b/keysas-admin/src-tauri/src/usb_sign.rs @@ -7,291 +7,273 @@ * The code for keysas-sign binary. */ - use anyhow::anyhow; - use clap::{crate_version, Arg, ArgAction, Command}; - use nom::bytes::complete::take; - use nom::number::complete::be_u32; - use std::fs::remove_file; - use std::fs::File; - use std::io; - use std::io::prelude::*; - use std::io::SeekFrom; - use std::path::Path; - extern crate minisign; - use std::io::Cursor; - mod errors; - use crate::errors::*; - use std::fs; - extern crate libc; - extern crate udev; - use libc::{c_int, c_short, c_ulong, c_void}; - use nom::error::Error; - use std::ffi::OsStr; - use std::io::Read; - use std::os::unix::io::AsRawFd; - use std::process; - use std::process::Command as Cmd; - use std::ptr; - use std::str; - use std::thread; - use std::time::Duration; - - #[repr(C)] - struct pollfd { - fd: c_int, - events: c_short, - revents: c_short, - } - - #[repr(C)] - struct sigset_t { - __private: c_void, - } - - #[allow(non_camel_case_types)] - type nfds_t = c_ulong; - - const POLLIN: c_short = 0x0001; - - extern "C" { - fn ppoll( - fds: *mut pollfd, - nfds: nfds_t, - timeout_ts: *mut libc::timespec, - sigmask: *const sigset_t, - ) -> c_int; - } - - trait StrExt { - fn remove_last(&self) -> &str; - } - - impl StrExt for str { - fn remove_last(&self) -> &str { - match self.char_indices().next_back() { - Some((i, _)) => &self[..i], - None => self, - } - } - } - - // Remove the partition number and return the device - // TODO: manage if nb partition >= 10 - fn rm_last(value: &str) -> &str { - let chars = value.chars(); - let mut tmp = chars.clone(); - match chars.last() { - Some(last) => { - if last.is_numeric() { - tmp.next_back(); - return tmp.as_str(); - } else { - return tmp.as_str(); - } - } - None => value, - } - } - - fn signme( - vendor: &str, - model: &str, - revision: &str, - serial: &str, - direction: &str, - privkey_path: &str, - password: &str, - ) -> Result { - let sk_box_str = fs::read_to_string(privkey_path)?; - let sk_box = SecretKeyBox::from_string(&sk_box_str)?; - - // and the box can be opened using the password to reveal the original secret key: - let sk = sk_box.into_secret_key(Some(password.to_string()))?; - - // Now, we can use the secret key to sign anything. - let data = format!("{}/{}/{}/{}/{}", vendor, model, revision, serial, direction); - let data_reader = Cursor::new(&data); - let signature_box = minisign::sign( - None, - &sk, - data_reader, - Some(&data), - Some("Signature from Keysas secret"), - )?; - - // Converting the signature box to a string in order to save it is easy. - Ok(signature_box.into_string()) - } - - fn watch() -> Result { - let socket = udev::MonitorBuilder::new()? - //.match_subsystem_devtype("usb", "usb_device")? - .match_subsystem("block")? - .listen()?; - - let mut fds = vec![pollfd { - fd: socket.as_raw_fd(), - events: POLLIN, - revents: 0, - }]; - log::debug!("Watching... you can plug your device in !"); - - loop { - let result = unsafe { - ppoll( - (&mut fds[..]).as_mut_ptr(), - fds.len() as nfds_t, - ptr::null_mut(), - ptr::null(), - ) - }; - - if result < 0 { - println!("Error: result is < 0."); - } - - let event = match socket.iter().next() { - Some(evt) => evt, - None => { - thread::sleep(Duration::from_millis(5)); - continue; - } - }; - - for _property in event.properties() { - if event.action() == Some(OsStr::new("add")) - && event.property_value( - OsStr::new("DEVTYPE") - .to_str() - .ok_or_else(|| anyhow!("Cannot convert DEVTYPE to str."))?, - ) == Some(OsStr::new("partition")) - { - let dev = event.device(); - let device = dev.devnode().unwrap(); - let dev = &device.to_string_lossy(); - let device = rm_last(dev); - - let vendor = event - .property_value( - OsStr::new("ID_VENDOR_ID") - .to_str() - .ok_or_else(|| anyhow!("Cannot convert ID_VENDOR_ID to str."))?, - ) - .ok_or_else(|| anyhow!("Cannot get ID_VENDOR_ID from event."))?; - let model = event - .property_value( - OsStr::new("ID_MODEL_ID") - .to_str() - .ok_or_else(|| anyhow!("Cannot convert ID_MODEL_ID to str."))?, - ) - .ok_or_else(|| anyhow!("Cannot get ID_MODEL_ID from event."))?; - let revision = event - .property_value( - OsStr::new("ID_REVISION") - .to_str() - .ok_or_else(|| anyhow!("Cannot convert ID_REVISION to str."))?, - ) - .ok_or_else(|| anyhow!("Cannot get ID_REVISION from event."))?; - let serial = event - .property_value( - OsStr::new("ID_SERIAL") - .to_str() - .ok_or_else(|| anyhow!("Cannot convert ID_SERIAL to str."))?, - ) - .ok_or_else(|| anyhow!("Cannot get ID_SERIAL from event."))?; - println!( - "Found key: Vendor: {}, Model: {}, Revision: {}, Serial: {}", - vendor.to_string_lossy(), - model.to_string_lossy(), - revision.to_string_lossy(), - serial.to_string_lossy() - ); - println!( - "To sign your new USB-OUT key, type the following with your own password:" - ); - //println!("keysas-sign --device={} --sign --password=YourSecretPassWord --vendorid={} --modelid={} --revision={} --serial={}", device ,vendor.to_string_lossy() ,model.to_string_lossy(), revision.to_string_lossy(), serial.to_string_lossy()); - let information = format!("New USB device found: Vendor ID: {}, Model ID: {}, Revision: {}, Serial number: {}", vendor.to_string_lossy() ,model.to_string_lossy(), revision.to_string_lossy(), serial.to_string_lossy()); - Ok(information) - } - } - } - } - - fn sign_usb( - device: &str, - vendor: &str, - model: &str, - revision: &str, - serial: &str, - direction: &str, - privkey_path: &str, - password: &str, - ) -> Result<()> { - log::debug!("Let's start signing the new out-key !"); - let mut f = File::options() - .write(true) - .read(true) - .open(device) - .context("Cannot open device for signing.")?; - - let ss = 512; - let mut mbr = mbrman::MBR::new_from(&mut f, ss as u32, [0x00, 0x0A, 0x0B, 0x0C]) - .context("Could not make a partition table")?; - let sectors = mbr - .get_maximum_partition_size() - .context("No more space available in the USB device")?; - - let starting_lba_i32 = 4096; - let starting_lba = starting_lba_i32 as u32; - - mbr[1] = mbrman::MBRPartitionEntry { - boot: mbrman::BOOT_INACTIVE, // boot flag - first_chs: mbrman::CHS::empty(), // first CHS address (only useful for old computers) - sys: 0x0c, // fat32+ LBA filesystem - last_chs: mbrman::CHS::empty(), // last CHS address (only useful for old computers) - starting_lba, // the sector where the partition starts - sectors, // the number of sectors in that partition - }; - - // actually writes the new partition Entry: - mbr.write_into(&mut f) - .context("Could not write MBR to disk")?; - - //Let's write behind the magic number now - let offset = 512; - let attrs = signme( - vendor, - model, - revision, - serial, - direction, - privkey_path, - password, - )?; - let size_u32 = attrs.len() as u32; - f.seek(SeekFrom::Start(offset))?; - f.write_all(&size_u32.to_be_bytes())?; - f.write_all(attrs.as_bytes())?; - log::info!("USB device is now signed successfully."); - Ok(()) - } - - fn revoke_usb(device: &str) -> Result<()> { - println!("Let's start signing the new out-key !"); - let mut f = File::options() - .write(true) - .read(true) - .open(device) - .context("Cannot open device for revoking.")?; - - //Let's write behind the magic number now - let offset = 512; - let blank: String = String::from("0000000"); - let size_u32 = blank.len() as u32; - f.seek(SeekFrom::Start(offset))?; - f.write_all(&size_u32.to_be_bytes())?; - f.write_all(blank.as_bytes())?; - log::info!("USB device is now signed successfully."); - Ok(()) - } - \ No newline at end of file +use anyhow::{anyhow, Context, Result}; +use std::fs::File; +use std::io::prelude::*; +use std::io::SeekFrom; +use std::path::Path; +//extern crate udev; +use base64::{engine::general_purpose, Engine as _}; +use ed25519_dalek::Keypair; +use keysas_lib::keysas_key::KeysasKey; +use keysas_lib::keysas_key::KeysasPQKey; +use libc::{c_int, c_short, c_ulong, c_void}; +use std::ffi::OsStr; +use std::os::unix::io::AsRawFd; +use std::ptr; +use std::str; +use std::thread; +use std::time::Duration; + +#[repr(C)] +#[cfg(target_os = "linux")] +struct pollfd { + fd: c_int, + events: c_short, + revents: c_short, +} + +#[repr(C)] +#[cfg(target_os = "linux")] +struct sigset_t { + __private: c_void, +} + +#[allow(non_camel_case_types)] +#[cfg(target_os = "linux")] +type nfds_t = c_ulong; +#[cfg(target_os = "linux")] +const POLLIN: c_short = 0x0001; + +#[cfg(target_os = "linux")] +extern "C" { + fn ppoll( + fds: *mut pollfd, + nfds: nfds_t, + timeout_ts: *mut libc::timespec, + sigmask: *const sigset_t, + ) -> c_int; +} + +trait StrExt { + fn remove_last(&self) -> &str; +} + +impl StrExt for str { + fn remove_last(&self) -> &str { + match self.char_indices().next_back() { + Some((i, _)) => &self[..i], + None => self, + } + } +} + +// Remove the partition number and return the device +// TODO: manage if nb partition >= 10 +fn rm_last(value: &str) -> &str { + let chars = value.chars(); + let mut tmp = chars.clone(); + match chars.last() { + Some(last) => { + if last.is_numeric() { + tmp.next_back(); + return tmp.as_str(); + } else { + return tmp.as_str(); + } + } + None => value, + } +} + +/// Construct an hybrid signature from +#[cfg(target_os = "linux")] +fn sign_device( + vendor: &str, + model: &str, + revision: &str, + serial: &str, + direction: &str, + path_cl: &Path, + path_pq: &Path, + password: &str, +) -> Result { + //use nom::AsBytes; + let data = format!("{}/{}/{}/{}/{}", vendor, model, revision, serial, direction); + // Test the private keys by loading them + let classic_struct = Keypair::load_keys(path_cl, password)?; + let pq_pub_struct = KeysasPQKey::load_keys(path_pq, password)?; + let classic_sig = classic_struct.message_sign(data.as_bytes())?; + let pq_sig = pq_pub_struct.message_sign(data.as_bytes())?; + let hybrid_sig = format!( + "{}-{}", + general_purpose::STANDARD.encode(classic_sig.as_slice()), + general_purpose::STANDARD.encode(pq_sig.as_slice()) + ); + Ok(hybrid_sig) +} + +#[cfg(target_os = "linux")] +fn watch_new_usb() -> Result { + let socket = udev::MonitorBuilder::new()? + //.match_subsystem_devtype("usb", "usb_device")? + .match_subsystem("block")? + .listen()?; + + let mut fds = vec![pollfd { + fd: socket.as_raw_fd(), + events: POLLIN, + revents: 0, + }]; + log::debug!("Watching... you can plug your device in !"); + + loop { + let result = unsafe { + ppoll( + (&mut fds[..]).as_mut_ptr(), + fds.len() as nfds_t, + ptr::null_mut(), + ptr::null(), + ) + }; + + if result < 0 { + println!("Error: result is < 0."); + } + + let event = match socket.iter().next() { + Some(evt) => evt, + None => { + thread::sleep(Duration::from_millis(5)); + continue; + } + }; + + for _property in event.properties() { + if event.action() == Some(OsStr::new("add")) + && event.property_value( + OsStr::new("DEVTYPE") + .to_str() + .ok_or_else(|| anyhow!("Cannot convert DEVTYPE to str."))?, + ) == Some(OsStr::new("partition")) + { + let dev = event.device(); + let device = dev.devnode().unwrap(); + let dev = &device.to_string_lossy(); + let device = rm_last(dev); + + let vendor = event + .property_value( + OsStr::new("ID_VENDOR_ID") + .to_str() + .ok_or_else(|| anyhow!("Cannot convert ID_VENDOR_ID to str."))?, + ) + .ok_or_else(|| anyhow!("Cannot get ID_VENDOR_ID from event."))?; + let model = event + .property_value( + OsStr::new("ID_MODEL_ID") + .to_str() + .ok_or_else(|| anyhow!("Cannot convert ID_MODEL_ID to str."))?, + ) + .ok_or_else(|| anyhow!("Cannot get ID_MODEL_ID from event."))?; + let revision = event + .property_value( + OsStr::new("ID_REVISION") + .to_str() + .ok_or_else(|| anyhow!("Cannot convert ID_REVISION to str."))?, + ) + .ok_or_else(|| anyhow!("Cannot get ID_REVISION from event."))?; + let serial = event + .property_value( + OsStr::new("ID_SERIAL") + .to_str() + .ok_or_else(|| anyhow!("Cannot convert ID_SERIAL to str."))?, + ) + .ok_or_else(|| anyhow!("Cannot get ID_SERIAL from event."))?; + log::info!( + "Found new USB device : Device: {}, Vendor: {}, Model: {}, Revision: {}, Serial: {}", + device, + vendor.to_string_lossy(), + model.to_string_lossy(), + revision.to_string_lossy(), + serial.to_string_lossy() + ); + let information = format!("New USB device found: Vendor ID: {}, Model ID: {}, Revision: {}, Serial number: {}", vendor.to_string_lossy() ,model.to_string_lossy(), revision.to_string_lossy(), serial.to_string_lossy()); + return Ok(information); + } + } + } +} + +fn sign_usb( + device: &str, + vendor: &str, + model: &str, + revision: &str, + serial: &str, + direction: &str, + path_cl: &Path, + path_pq: &Path, + password: &str, +) -> Result<()> { + log::debug!("Let's start signing the new out-key !"); + let mut f = File::options() + .write(true) + .read(true) + .open(device) + .context("Cannot open device for signing.")?; + + let ss = 512; + let mut mbr = mbrman::MBR::new_from(&mut f, ss as u32, [0x00, 0x0A, 0x0B, 0x0C]) + .context("Could not make a partition table")?; + let sectors = mbr + .get_maximum_partition_size() + .context("No more space available in the USB device")?; + + let starting_lba_i32 = 4096; + let starting_lba = starting_lba_i32; + + mbr[1] = mbrman::MBRPartitionEntry { + boot: mbrman::BOOT_INACTIVE, // boot flag + first_chs: mbrman::CHS::empty(), // first CHS address (only useful for old computers) + sys: 0x0c, // fat32+ LBA filesystem + last_chs: mbrman::CHS::empty(), // last CHS address (only useful for old computers) + starting_lba, // the sector where the partition starts + sectors, // the number of sectors in that partition + }; + + // actually writes the new partition Entry: + mbr.write_into(&mut f) + .context("Could not write MBR to disk")?; + + //Let's write behind the magic number now + let offset = 512; + let attrs = sign_device( + vendor, model, revision, serial, direction, path_cl, path_pq, password, + )?; + let size_u32 = attrs.len() as u32; + f.seek(SeekFrom::Start(offset))?; + f.write_all(&size_u32.to_be_bytes())?; + f.write_all(attrs.as_bytes())?; + log::info!("USB device is now signed successfully."); + Ok(()) +} + +fn revoke_device(device: &str) -> Result<()> { + log::debug!("Let's revoke the device."); + let mut f = File::options() + .write(true) + .read(true) + .open(device) + .context("Cannot open device for revoking.")?; + + //Let's write behind the magic number now + let offset = 512; + let blank: String = String::from("0000000"); + let size_u32 = blank.len() as u32; + f.seek(SeekFrom::Start(offset))?; + f.write_all(&size_u32.to_be_bytes())?; + f.write_all(blank.as_bytes())?; + log::info!("USB device is now signed successfully."); + Ok(()) +} diff --git a/keysas-admin/src/components/DisplaySigningConfig.vue b/keysas-admin/src/components/DisplaySigningConfig.vue index 99833bf..980ed93 100644 --- a/keysas-admin/src/components/DisplaySigningConfig.vue +++ b/keysas-admin/src/components/DisplaySigningConfig.vue @@ -134,4 +134,8 @@ button { color: rgb(158, 161, 163); font-size: 1em; } +h3 { + margin: 45px 0 0; + color: #fff; +} diff --git a/keysas-admin/src/components/SigningKeys.vue b/keysas-admin/src/components/SigningKeys.vue index fcc2055..80c8859 100644 --- a/keysas-admin/src/components/SigningKeys.vue +++ b/keysas-admin/src/components/SigningKeys.vue @@ -1,7 +1,8 @@ - + +
- \ No newline at end of file + diff --git a/keysas-admin/package-lock.json b/keysas-admin/package-lock.json index 5049d50..8d18de4 100644 --- a/keysas-admin/package-lock.json +++ b/keysas-admin/package-lock.json @@ -1,12 +1,12 @@ { "name": "keysas-admin", - "version": "0.1.2", - "lockfileVersion": 2, + "version": "0.2.0", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "keysas-admin", - "version": "0.1.2", + "version": "0.2.0", "dependencies": { "@tauri-apps/api": "^1.2.0", "@tauri-apps/cli": "^1.2.2", @@ -30,9 +30,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", - "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -41,9 +41,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.12.tgz", - "integrity": "sha512-CTWgMJtpCyCltrvipZrrcjjRu+rzm6pf9V8muCsJqtKujR3kPmU4ffbckvugNNaRmhxAF1ZI3J+0FUIFLFg8KA==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.17.tgz", + "integrity": "sha512-E6VAZwN7diCa3labs0GYvhEPL2M94WLF8A+czO8hfjREXxba8Ng7nM5VxV+9ihNXIY1iQO1XxUU4P7hbqbICxg==", "cpu": [ "arm" ], @@ -57,9 +57,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.12.tgz", - "integrity": "sha512-0LacmiIW+X0/LOLMZqYtZ7d4uY9fxYABAYhSSOu+OGQVBqH4N5eIYgkT7bBFnR4Nm3qo6qS3RpHKVrDASqj/uQ==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.17.tgz", + "integrity": "sha512-jaJ5IlmaDLFPNttv0ofcwy/cfeY4bh/n705Tgh+eLObbGtQBK3EPAu+CzL95JVE4nFAliyrnEu0d32Q5foavqg==", "cpu": [ "arm64" ], @@ -73,9 +73,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.12.tgz", - "integrity": "sha512-sS5CR3XBKQXYpSGMM28VuiUnbX83Z+aWPZzClW+OB2JquKqxoiwdqucJ5qvXS8pM6Up3RtJfDnRQZkz3en2z5g==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.17.tgz", + "integrity": "sha512-446zpfJ3nioMC7ASvJB1pszHVskkw4u/9Eu8s5yvvsSDTzYh4p4ZIRj0DznSl3FBF0Z/mZfrKXTtt0QCoFmoHA==", "cpu": [ "x64" ], @@ -89,9 +89,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.12.tgz", - "integrity": "sha512-Dpe5hOAQiQRH20YkFAg+wOpcd4PEuXud+aGgKBQa/VriPJA8zuVlgCOSTwna1CgYl05lf6o5els4dtuyk1qJxQ==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.17.tgz", + "integrity": "sha512-m/gwyiBwH3jqfUabtq3GH31otL/0sE0l34XKpSIqR7NjQ/XHQ3lpmQHLHbG8AHTGCw8Ao059GvV08MS0bhFIJQ==", "cpu": [ "arm64" ], @@ -105,9 +105,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.12.tgz", - "integrity": "sha512-ApGRA6X5txIcxV0095X4e4KKv87HAEXfuDRcGTniDWUUN+qPia8sl/BqG/0IomytQWajnUn4C7TOwHduk/FXBQ==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.17.tgz", + "integrity": "sha512-4utIrsX9IykrqYaXR8ob9Ha2hAY2qLc6ohJ8c0CN1DR8yWeMrTgYFjgdeQ9LIoTOfLetXjuCu5TRPHT9yKYJVg==", "cpu": [ "x64" ], @@ -121,9 +121,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.12.tgz", - "integrity": "sha512-AMdK2gA9EU83ccXCWS1B/KcWYZCj4P3vDofZZkl/F/sBv/fphi2oUqUTox/g5GMcIxk8CF1CVYTC82+iBSyiUg==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.17.tgz", + "integrity": "sha512-4PxjQII/9ppOrpEwzQ1b0pXCsFLqy77i0GaHodrmzH9zq2/NEhHMAMJkJ635Ns4fyJPFOlHMz4AsklIyRqFZWA==", "cpu": [ "arm64" ], @@ -137,9 +137,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.12.tgz", - "integrity": "sha512-KUKB9w8G/xaAbD39t6gnRBuhQ8vIYYlxGT2I+mT6UGRnCGRr1+ePFIGBQmf5V16nxylgUuuWVW1zU2ktKkf6WQ==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.17.tgz", + "integrity": "sha512-lQRS+4sW5S3P1sv0z2Ym807qMDfkmdhUYX30GRBURtLTrJOPDpoU0kI6pVz1hz3U0+YQ0tXGS9YWveQjUewAJw==", "cpu": [ "x64" ], @@ -153,9 +153,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.12.tgz", - "integrity": "sha512-vhDdIv6z4eL0FJyNVfdr3C/vdd/Wc6h1683GJsFoJzfKb92dU/v88FhWdigg0i6+3TsbSDeWbsPUXb4dif2abg==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.17.tgz", + "integrity": "sha512-biDs7bjGdOdcmIk6xU426VgdRUpGg39Yz6sT9Xp23aq+IEHDb/u5cbmu/pAANpDB4rZpY/2USPhCA+w9t3roQg==", "cpu": [ "arm" ], @@ -169,9 +169,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.12.tgz", - "integrity": "sha512-29HXMLpLklDfmw7T2buGqq3HImSUaZ1ArmrPOMaNiZZQptOSZs32SQtOHEl8xWX5vfdwZqrBfNf8Te4nArVzKQ==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.17.tgz", + "integrity": "sha512-2+pwLx0whKY1/Vqt8lyzStyda1v0qjJ5INWIe+d8+1onqQxHLLi3yr5bAa4gvbzhZqBztifYEu8hh1La5+7sUw==", "cpu": [ "arm64" ], @@ -185,9 +185,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.12.tgz", - "integrity": "sha512-JFDuNDTTfgD1LJg7wHA42o2uAO/9VzHYK0leAVnCQE/FdMB599YMH73ux+nS0xGr79pv/BK+hrmdRin3iLgQjg==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.17.tgz", + "integrity": "sha512-IBTTv8X60dYo6P2t23sSUYym8fGfMAiuv7PzJ+0LcdAndZRzvke+wTVxJeCq4WgjppkOpndL04gMZIFvwoU34Q==", "cpu": [ "ia32" ], @@ -201,9 +201,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.12.tgz", - "integrity": "sha512-xTGzVPqm6WKfCC0iuj1fryIWr1NWEM8DMhAIo+4rFgUtwy/lfHl+Obvus4oddzRDbBetLLmojfVZGmt/g/g+Rw==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.17.tgz", + "integrity": "sha512-WVMBtcDpATjaGfWfp6u9dANIqmU9r37SY8wgAivuKmgKHE+bWSuv0qXEFt/p3qXQYxJIGXQQv6hHcm7iWhWjiw==", "cpu": [ "loong64" ], @@ -217,9 +217,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.12.tgz", - "integrity": "sha512-zI1cNgHa3Gol+vPYjIYHzKhU6qMyOQrvZ82REr5Fv7rlh5PG6SkkuCoH7IryPqR+BK2c/7oISGsvPJPGnO2bHQ==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.17.tgz", + "integrity": "sha512-2kYCGh8589ZYnY031FgMLy0kmE4VoGdvfJkxLdxP4HJvWNXpyLhjOvxVsYjYZ6awqY4bgLR9tpdYyStgZZhi2A==", "cpu": [ "mips64el" ], @@ -233,9 +233,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.12.tgz", - "integrity": "sha512-/C8OFXExoMmvTDIOAM54AhtmmuDHKoedUd0Otpfw3+AuuVGemA1nQK99oN909uZbLEU6Bi+7JheFMG3xGfZluQ==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.17.tgz", + "integrity": "sha512-KIdG5jdAEeAKogfyMTcszRxy3OPbZhq0PPsW4iKKcdlbk3YE4miKznxV2YOSmiK/hfOZ+lqHri3v8eecT2ATwQ==", "cpu": [ "ppc64" ], @@ -249,9 +249,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.12.tgz", - "integrity": "sha512-qeouyyc8kAGV6Ni6Isz8hUsKMr00EHgVwUKWNp1r4l88fHEoNTDB8mmestvykW6MrstoGI7g2EAsgr0nxmuGYg==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.17.tgz", + "integrity": "sha512-Cj6uWLBR5LWhcD/2Lkfg2NrkVsNb2sFM5aVEfumKB2vYetkA/9Uyc1jVoxLZ0a38sUhFk4JOVKH0aVdPbjZQeA==", "cpu": [ "riscv64" ], @@ -265,9 +265,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.12.tgz", - "integrity": "sha512-s9AyI/5vz1U4NNqnacEGFElqwnHusWa81pskAf8JNDM2eb6b2E6PpBmT8RzeZv6/TxE6/TADn2g9bb0jOUmXwQ==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.17.tgz", + "integrity": "sha512-lK+SffWIr0XsFf7E0srBjhpkdFVJf3HEgXCwzkm69kNbRar8MhezFpkIwpk0qo2IOQL4JE4mJPJI8AbRPLbuOQ==", "cpu": [ "s390x" ], @@ -281,9 +281,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.12.tgz", - "integrity": "sha512-e8YA7GQGLWhvakBecLptUiKxOk4E/EPtSckS1i0MGYctW8ouvNUoh7xnU15PGO2jz7BYl8q1R6g0gE5HFtzpqQ==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.17.tgz", + "integrity": "sha512-XcSGTQcWFQS2jx3lZtQi7cQmDYLrpLRyz1Ns1DzZCtn898cWfm5Icx/DEWNcTU+T+tyPV89RQtDnI7qL2PObPg==", "cpu": [ "x64" ], @@ -297,9 +297,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.12.tgz", - "integrity": "sha512-z2+kUxmOqBS+6SRVd57iOLIHE8oGOoEnGVAmwjm2aENSP35HPS+5cK+FL1l+rhrsJOFIPrNHqDUNechpuG96Sg==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.17.tgz", + "integrity": "sha512-RNLCDmLP5kCWAJR+ItLM3cHxzXRTe4N00TQyQiimq+lyqVqZWGPAvcyfUBM0isE79eEZhIuGN09rAz8EL5KdLA==", "cpu": [ "x64" ], @@ -313,9 +313,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.12.tgz", - "integrity": "sha512-PAonw4LqIybwn2/vJujhbg1N9W2W8lw9RtXIvvZoyzoA/4rA4CpiuahVbASmQohiytRsixbNoIOUSjRygKXpyA==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.17.tgz", + "integrity": "sha512-PAXswI5+cQq3Pann7FNdcpSUrhrql3wKjj3gVkmuz6OHhqqYxKvi6GgRBoaHjaG22HV/ZZEgF9TlS+9ftHVigA==", "cpu": [ "x64" ], @@ -329,9 +329,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.12.tgz", - "integrity": "sha512-+wr1tkt1RERi+Zi/iQtkzmMH4nS8+7UIRxjcyRz7lur84wCkAITT50Olq/HiT4JN2X2bjtlOV6vt7ptW5Gw60Q==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.17.tgz", + "integrity": "sha512-V63egsWKnx/4V0FMYkr9NXWrKTB5qFftKGKuZKFIrAkO/7EWLFnbBZNM1CvJ6Sis+XBdPws2YQSHF1Gqf1oj/Q==", "cpu": [ "x64" ], @@ -345,9 +345,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.12.tgz", - "integrity": "sha512-XEjeUSHmjsAOJk8+pXJu9pFY2O5KKQbHXZWQylJzQuIBeiGrpMeq9sTVrHefHxMOyxUgoKQTcaTS+VK/K5SviA==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.17.tgz", + "integrity": "sha512-YtUXLdVnd6YBSYlZODjWzH+KzbaubV0YVd6UxSfoFfa5PtNJNaW+1i+Hcmjpg2nEe0YXUCNF5bkKy1NnBv1y7Q==", "cpu": [ "arm64" ], @@ -361,9 +361,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.12.tgz", - "integrity": "sha512-eRKPM7e0IecUAUYr2alW7JGDejrFJXmpjt4MlfonmQ5Rz9HWpKFGCjuuIRgKO7W9C/CWVFXdJ2GjddsBXqQI4A==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.17.tgz", + "integrity": "sha512-yczSLRbDdReCO74Yfc5tKG0izzm+lPMYyO1fFTcn0QNwnKmc3K+HdxZWLGKg4pZVte7XVgcFku7TIZNbWEJdeQ==", "cpu": [ "ia32" ], @@ -377,9 +377,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.12.tgz", - "integrity": "sha512-iPYKN78t3op2+erv2frW568j1q0RpqX6JOLZ7oPPaAV1VaF7dDstOrNw37PVOYoTWE11pV4A1XUitpdEFNIsPg==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.17.tgz", + "integrity": "sha512-FNZw7H3aqhF9OyRQbDDnzUApDXfC1N6fgBhkqEO2jvYCJ+DxMTfZVqg3AX0R1khg1wHTBRD5SdcibSJ+XF6bFg==", "cpu": [ "x64" ], @@ -392,16 +392,40 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", + "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", - "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", + "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", - "globals": "^13.15.0", + "espree": "^9.5.1", + "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -415,10 +439,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/js": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz", + "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.7", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", - "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -484,9 +517,9 @@ } }, "node_modules/@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", + "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==", "peer": true, "funding": { "type": "opencollective", @@ -514,9 +547,9 @@ } }, "node_modules/@tauri-apps/cli": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.2.2.tgz", - "integrity": "sha512-D8zib3A0vWCvPPSyYLxww/OdDlVcY7fpcDVBH6qUvheOjj2aCyU7H9AYMRBwpgCfz8zY5+vomee+laLeB0H13w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.2.3.tgz", + "integrity": "sha512-erxtXuPhMEGJPBtnhPILD4AjuT81GZsraqpFvXAmEJZ2p8P6t7MVBifCL8LznRknznM3jn90D3M8RNBP3wcXTw==", "bin": { "tauri": "tauri.js" }, @@ -528,21 +561,21 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "1.2.2", - "@tauri-apps/cli-darwin-x64": "1.2.2", - "@tauri-apps/cli-linux-arm-gnueabihf": "1.2.2", - "@tauri-apps/cli-linux-arm64-gnu": "1.2.2", - "@tauri-apps/cli-linux-arm64-musl": "1.2.2", - "@tauri-apps/cli-linux-x64-gnu": "1.2.2", - "@tauri-apps/cli-linux-x64-musl": "1.2.2", - "@tauri-apps/cli-win32-ia32-msvc": "1.2.2", - "@tauri-apps/cli-win32-x64-msvc": "1.2.2" + "@tauri-apps/cli-darwin-arm64": "1.2.3", + "@tauri-apps/cli-darwin-x64": "1.2.3", + "@tauri-apps/cli-linux-arm-gnueabihf": "1.2.3", + "@tauri-apps/cli-linux-arm64-gnu": "1.2.3", + "@tauri-apps/cli-linux-arm64-musl": "1.2.3", + "@tauri-apps/cli-linux-x64-gnu": "1.2.3", + "@tauri-apps/cli-linux-x64-musl": "1.2.3", + "@tauri-apps/cli-win32-ia32-msvc": "1.2.3", + "@tauri-apps/cli-win32-x64-msvc": "1.2.3" } }, "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.2.2.tgz", - "integrity": "sha512-W+Cp2weUMlvmGkRJeUjypbz9Lpl6o98xkgKAtobZSum5SNwpsBQfawJTESakNoD+FXyVg/snIk5sRdHge+tAaA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-phJN3fN8FtZZwqXg08bcxfq1+X1JSDglLvRxOxB7VWPq+O5SuB8uLyssjJsu+PIhyZZnIhTGdjhzLSFhSXfLsw==", "cpu": [ "arm64" ], @@ -555,9 +588,9 @@ } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.2.2.tgz", - "integrity": "sha512-vmVAqt+ECH2d6cbcGJ7ddcCAZgmKe5xmxlL5r4xoaphu7OqU4gnv4VFURYkVltOfwzIFQVOPVSqwYyIDToCYNQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.2.3.tgz", + "integrity": "sha512-jFZ/y6z8z6v4yliIbXKBXA7BJgtZVMsITmEXSuD6s5+eCOpDhQxbRkr6CA+FFfr+/r96rWSDSgDenDQuSvPAKw==", "cpu": [ "x64" ], @@ -570,9 +603,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.2.2.tgz", - "integrity": "sha512-yYTdQurgi4QZR8z+fANjl522jdQz/VtesFpw+C/A0+zXg7tiRjicsywBDdPsvNzCqFeGKKkmTR+Lny5qxhGaeQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-C7h5vqAwXzY0kRGSU00Fj8PudiDWFCiQqqUNI1N+fhCILrzWZB9TPBwdx33ZfXKt/U4+emdIoo/N34v3TiAOmQ==", "cpu": [ "arm" ], @@ -585,9 +618,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.2.2.tgz", - "integrity": "sha512-ZSOVT6Eq1ay2+27B8KfA0MnpO7KYzONU6TjenH7DNcQki6eWGG5JoNu8QQ9Mdn3dAzY0XBP9i1ZHQOFu4iPtEg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-buf1c8sdkuUzVDkGPQpyUdAIIdn5r0UgXU6+H5fGPq/Xzt5K69JzXaeo6fHsZEZghbV0hOK+taKV4J0m30UUMQ==", "cpu": [ "arm64" ], @@ -600,9 +633,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.2.2.tgz", - "integrity": "sha512-/NHSkqNQ+Pr4PshvyD1CeNFaPCaCpe1OeuAQgVi0rboSecC9fXN96G5dQbSBoxOUcCo6f8aTVE7zkZ4WchFVog==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-x88wPS9W5xAyk392vc4uNHcKBBvCp0wf4H9JFMF9OBwB7vfd59LbQCFcPSu8f0BI7bPrOsyHqspWHuFL8ojQEA==", "cpu": [ "arm64" ], @@ -615,9 +648,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.2.2.tgz", - "integrity": "sha512-4YTmfPuyvlHsvCkATDMwhklfuQm3HKxYXv/IOW9H0ra6pS9efVhrFYIC9Vfv6XaKN85Vnn/FYTEGMJLwCxZw2Q==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-ZMz1jxEVe0B4/7NJnlPHmwmSIuwiD6ViXKs8F+OWWz2Y4jn5TGxWKFg7DLx5OwQTRvEIZxxT7lXHi5CuTNAxKg==", "cpu": [ "x64" ], @@ -630,9 +663,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.2.2.tgz", - "integrity": "sha512-wr46tbscwFuCcA931R+ItOiUTT0djMmgKLd1HFCmFF82V9BKE2reIjr6O9l0NCXCo2WeD4pe3jA/Pt1dxDu+JA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-B/az59EjJhdbZDzawEVox0LQu2ZHCZlk8rJf85AMIktIUoAZPFbwyiUv7/zjzA/sY6Nb58OSJgaPL2/IBy7E0A==", "cpu": [ "x64" ], @@ -645,9 +678,9 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.2.2.tgz", - "integrity": "sha512-6VmbVJOWUZJK5/JKhb3mNFKrKGfq0KV7lJGumfN95WJgkHeyL61p8bZit+o6ZgUGUhrOabkAawhDkrRY+ZQhIw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.2.3.tgz", + "integrity": "sha512-ypdO1OdC5ugNJAKO2m3sb1nsd+0TSvMS9Tr5qN/ZSMvtSduaNwrcZ3D7G/iOIanrqu/Nl8t3LYlgPZGBKlw7Ng==", "cpu": [ "ia32" ], @@ -660,9 +693,9 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.2.2.tgz", - "integrity": "sha512-YRPJguJma+zSuRZpFoSZqls6+laggG1vqG0FPQWQTi+ywATgMpai2b2RZnffDlpHKp9mt4V/s2dtqOy6bpGZHg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-CsbHQ+XhnV/2csOBBDVfH16cdK00gNyNYUW68isedmqcn8j+s0e9cQ1xXIqi+Hue3awp8g3ImYN5KPepf3UExw==", "cpu": [ "x64" ], @@ -675,9 +708,9 @@ } }, "node_modules/@vitejs/plugin-vue": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz", - "integrity": "sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.1.0.tgz", + "integrity": "sha512-++9JOAFdcXI3lyer9UKUV4rfoQ3T1RN8yDqoCLar86s0xQct5yblxAE+yWgRnU5/0FOlVCpTZpYSBV/bGWrSrQ==", "dev": true, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -688,36 +721,36 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.45.tgz", - "integrity": "sha512-rcMj7H+PYe5wBV3iYeUgbCglC+pbpN8hBLTJvRiK2eKQiWqu+fG9F+8sW99JdL4LQi7Re178UOxn09puSXvn4A==", + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.47.tgz", + "integrity": "sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==", "dependencies": { "@babel/parser": "^7.16.4", - "@vue/shared": "3.2.45", + "@vue/shared": "3.2.47", "estree-walker": "^2.0.2", "source-map": "^0.6.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.45.tgz", - "integrity": "sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==", + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz", + "integrity": "sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==", "dependencies": { - "@vue/compiler-core": "3.2.45", - "@vue/shared": "3.2.45" + "@vue/compiler-core": "3.2.47", + "@vue/shared": "3.2.47" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.45.tgz", - "integrity": "sha512-1jXDuWah1ggsnSAOGsec8cFjT/K6TMZ0sPL3o3d84Ft2AYZi2jWJgRMjw4iaK0rBfA89L5gw427H4n1RZQBu6Q==", + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz", + "integrity": "sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==", "dependencies": { "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.45", - "@vue/compiler-dom": "3.2.45", - "@vue/compiler-ssr": "3.2.45", - "@vue/reactivity-transform": "3.2.45", - "@vue/shared": "3.2.45", + "@vue/compiler-core": "3.2.47", + "@vue/compiler-dom": "3.2.47", + "@vue/compiler-ssr": "3.2.47", + "@vue/reactivity-transform": "3.2.47", + "@vue/shared": "3.2.47", "estree-walker": "^2.0.2", "magic-string": "^0.25.7", "postcss": "^8.1.10", @@ -725,23 +758,23 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.45.tgz", - "integrity": "sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==", + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz", + "integrity": "sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==", "dependencies": { - "@vue/compiler-dom": "3.2.45", - "@vue/shared": "3.2.45" + "@vue/compiler-dom": "3.2.47", + "@vue/shared": "3.2.47" } }, "node_modules/@vue/devtools-api": { - "version": "6.4.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.4.5.tgz", - "integrity": "sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ==" + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", + "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" }, "node_modules/@vue/eslint-config-prettier": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-7.0.0.tgz", - "integrity": "sha512-/CTc6ML3Wta1tCe1gUeO0EYnVXfo3nJXsIhZ8WJr3sov+cGASr6yuiibJTL6lmIBm7GobopToOuB3B6AWyV0Iw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-7.1.0.tgz", + "integrity": "sha512-Pv/lVr0bAzSIHLd9iz0KnvAr4GKyCEl+h52bc4e5yWuDVtLgFwycF7nrbWTAQAS+FU6q1geVd07lc6EWfJiWKQ==", "dev": true, "dependencies": { "eslint-config-prettier": "^8.3.0", @@ -753,65 +786,65 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.45.tgz", - "integrity": "sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==", + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.47.tgz", + "integrity": "sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==", "dependencies": { - "@vue/shared": "3.2.45" + "@vue/shared": "3.2.47" } }, "node_modules/@vue/reactivity-transform": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.45.tgz", - "integrity": "sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==", + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz", + "integrity": "sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==", "dependencies": { "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.45", - "@vue/shared": "3.2.45", + "@vue/compiler-core": "3.2.47", + "@vue/shared": "3.2.47", "estree-walker": "^2.0.2", "magic-string": "^0.25.7" } }, "node_modules/@vue/runtime-core": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.45.tgz", - "integrity": "sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==", + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.47.tgz", + "integrity": "sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==", "dependencies": { - "@vue/reactivity": "3.2.45", - "@vue/shared": "3.2.45" + "@vue/reactivity": "3.2.47", + "@vue/shared": "3.2.47" } }, "node_modules/@vue/runtime-dom": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.45.tgz", - "integrity": "sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==", + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz", + "integrity": "sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==", "dependencies": { - "@vue/runtime-core": "3.2.45", - "@vue/shared": "3.2.45", + "@vue/runtime-core": "3.2.47", + "@vue/shared": "3.2.47", "csstype": "^2.6.8" } }, "node_modules/@vue/server-renderer": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.45.tgz", - "integrity": "sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==", + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.47.tgz", + "integrity": "sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==", "dependencies": { - "@vue/compiler-ssr": "3.2.45", - "@vue/shared": "3.2.45" + "@vue/compiler-ssr": "3.2.47", + "@vue/shared": "3.2.47" }, "peerDependencies": { - "vue": "3.2.45" + "vue": "3.2.47" } }, "node_modules/@vue/shared": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.45.tgz", - "integrity": "sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==" + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz", + "integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==" }, "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -931,9 +964,9 @@ } }, "node_modules/bootstrap-icons": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.2.tgz", - "integrity": "sha512-PTPYadRn1AMGr+QTSxe4ZCc+Wzv9DGZxbi3lNse/dajqV31n2/wl/7NX78ZpkvFgRNmH4ogdIQPQmxAfhEV6nA==" + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.4.tgz", + "integrity": "sha512-eI3HyIUmpGKRiRv15FCZccV+2sreGE2NnmH8mtxV/nPOzQVu0sPEj8HhF1MwjJ31IhjF0rgMvtYOX5VqIzcb/A==" }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -1109,9 +1142,9 @@ } }, "node_modules/esbuild": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.12.tgz", - "integrity": "sha512-eq5KcuXajf2OmivCl4e89AD3j8fbV+UTE9vczEzq5haA07U9oOTzBWlh3+6ZdjJR7Rz2QfWZ2uxZyhZxBgJ4+g==", + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.17.tgz", + "integrity": "sha512-/jUywtAymR8jR4qsa2RujlAF7Krpt5VWi72Q2yuLD4e/hvtNcFQ0I1j8m/bxq238pf3/0KO5yuXNpuLx8BE1KA==", "dev": true, "hasInstallScript": true, "bin": { @@ -1121,28 +1154,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.16.12", - "@esbuild/android-arm64": "0.16.12", - "@esbuild/android-x64": "0.16.12", - "@esbuild/darwin-arm64": "0.16.12", - "@esbuild/darwin-x64": "0.16.12", - "@esbuild/freebsd-arm64": "0.16.12", - "@esbuild/freebsd-x64": "0.16.12", - "@esbuild/linux-arm": "0.16.12", - "@esbuild/linux-arm64": "0.16.12", - "@esbuild/linux-ia32": "0.16.12", - "@esbuild/linux-loong64": "0.16.12", - "@esbuild/linux-mips64el": "0.16.12", - "@esbuild/linux-ppc64": "0.16.12", - "@esbuild/linux-riscv64": "0.16.12", - "@esbuild/linux-s390x": "0.16.12", - "@esbuild/linux-x64": "0.16.12", - "@esbuild/netbsd-x64": "0.16.12", - "@esbuild/openbsd-x64": "0.16.12", - "@esbuild/sunos-x64": "0.16.12", - "@esbuild/win32-arm64": "0.16.12", - "@esbuild/win32-ia32": "0.16.12", - "@esbuild/win32-x64": "0.16.12" + "@esbuild/android-arm": "0.17.17", + "@esbuild/android-arm64": "0.17.17", + "@esbuild/android-x64": "0.17.17", + "@esbuild/darwin-arm64": "0.17.17", + "@esbuild/darwin-x64": "0.17.17", + "@esbuild/freebsd-arm64": "0.17.17", + "@esbuild/freebsd-x64": "0.17.17", + "@esbuild/linux-arm": "0.17.17", + "@esbuild/linux-arm64": "0.17.17", + "@esbuild/linux-ia32": "0.17.17", + "@esbuild/linux-loong64": "0.17.17", + "@esbuild/linux-mips64el": "0.17.17", + "@esbuild/linux-ppc64": "0.17.17", + "@esbuild/linux-riscv64": "0.17.17", + "@esbuild/linux-s390x": "0.17.17", + "@esbuild/linux-x64": "0.17.17", + "@esbuild/netbsd-x64": "0.17.17", + "@esbuild/openbsd-x64": "0.17.17", + "@esbuild/sunos-x64": "0.17.17", + "@esbuild/win32-arm64": "0.17.17", + "@esbuild/win32-ia32": "0.17.17", + "@esbuild/win32-x64": "0.17.17" } }, "node_modules/escape-string-regexp": { @@ -1158,13 +1191,16 @@ } }, "node_modules/eslint": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz", - "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz", + "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.3.3", - "@humanwhocodes/config-array": "^0.11.6", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.2", + "@eslint/js": "8.38.0", + "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", @@ -1174,16 +1210,15 @@ "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "eslint-visitor-keys": "^3.4.0", + "espree": "^9.5.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.15.0", + "globals": "^13.19.0", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", @@ -1198,7 +1233,6 @@ "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", - "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" @@ -1214,9 +1248,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", - "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -1247,12 +1281,12 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.8.0.tgz", - "integrity": "sha512-E/AXwcTzunyzM83C2QqDHxepMzvI2y6x+mmeYHbVDQlKFqmKYvRrhaVixEeeG27uI44p9oKDFiyCRw4XxgtfHA==", + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.11.0.tgz", + "integrity": "sha512-bBCJAZnkBV7ATH4Z1E7CvN3nmtS4H7QUU3UBxPdo8WohRU+yHjnQRALpTbxMVcz0e4Mx3IyxIdP5HYODMxK9cQ==", "dev": true, "dependencies": { - "eslint-utils": "^3.0.0", + "@eslint-community/eslint-utils": "^4.3.0", "natural-compare": "^1.4.0", "nth-check": "^2.0.1", "postcss-selector-parser": "^6.0.9", @@ -1268,9 +1302,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -1278,53 +1312,32 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", + "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", "dev": true, "dependencies": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1334,9 +1347,9 @@ } }, "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -1405,9 +1418,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", - "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -1529,9 +1542,9 @@ } }, "node_modules/globals": { - "version": "13.19.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", - "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1571,18 +1584,18 @@ } }, "node_modules/ignore": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", - "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true, "engines": { "node": ">= 4" } }, "node_modules/immutable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", - "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==" }, "node_modules/import-fresh": { "version": "3.3.0", @@ -1637,9 +1650,9 @@ } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -1691,9 +1704,9 @@ "dev": true }, "node_modules/js-sdsl": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", - "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", "dev": true, "funding": { "type": "opencollective", @@ -1803,9 +1816,15 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -1957,9 +1976,9 @@ } }, "node_modules/postcss": { - "version": "8.4.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", - "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", + "version": "8.4.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.22.tgz", + "integrity": "sha512-XseknLAfRHzVWjCEtdviapiBtfLdgyzExD50Rg2ePaucEesyh8Wv4VPdW0nbyDa1ydbrAxV19jvMT4+LFmcNUA==", "funding": [ { "type": "opencollective", @@ -1968,10 +1987,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -2002,9 +2025,9 @@ } }, "node_modules/prettier": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz", - "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", "dev": true, "bin": { "prettier": "bin-prettier.js" @@ -2029,9 +2052,9 @@ } }, "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true, "engines": { "node": ">=6" @@ -2068,25 +2091,13 @@ "node": ">=8.10.0" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -2132,9 +2143,9 @@ } }, "node_modules/rollup": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.9.0.tgz", - "integrity": "sha512-nGGylpmblyjTpF4lEUPgmOw6OVxRvnI6Iuuh6Lz4O/X66cVOX1XJSsqP1YamxQ+mPuFE7qJxLFDSCk8rNv5dDw==", + "version": "3.20.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.6.tgz", + "integrity": "sha512-2yEB3nQXp/tBQDN0hJScJQheXdvU2wFhh6ld7K/aiZ1vYcak6N/BKjY1QrU6BvO2JWYS8bEs14FRaxXosxy2zw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -2171,9 +2182,9 @@ } }, "node_modules/sass": { - "version": "1.56.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz", - "integrity": "sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==", + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.0.tgz", + "integrity": "sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg==", "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -2183,13 +2194,13 @@ "sass": "sass.js" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2363,15 +2374,15 @@ "dev": true }, "node_modules/vite": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz", - "integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-PcNtT5HeDxb3QaSqFYkEum8f5sCVe0R3WK20qxgIvNBZPXU/Obxs/+ubBMeE7nLWeCo2LDzv+8hRYSlcaSehig==", "dev": true, "dependencies": { - "esbuild": "^0.16.3", - "postcss": "^8.4.20", + "esbuild": "^0.17.5", + "postcss": "^8.4.21", "resolve": "^1.22.1", - "rollup": "^3.7.0" + "rollup": "^3.18.0" }, "bin": { "vite": "bin/vite.js" @@ -2412,26 +2423,26 @@ } }, "node_modules/vue": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.45.tgz", - "integrity": "sha512-9Nx/Mg2b2xWlXykmCwiTUCWHbWIj53bnkizBxKai1g61f2Xit700A1ljowpTIM11e3uipOeiPcSqnmBg6gyiaA==", + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.47.tgz", + "integrity": "sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==", "dependencies": { - "@vue/compiler-dom": "3.2.45", - "@vue/compiler-sfc": "3.2.45", - "@vue/runtime-dom": "3.2.45", - "@vue/server-renderer": "3.2.45", - "@vue/shared": "3.2.45" + "@vue/compiler-dom": "3.2.47", + "@vue/compiler-sfc": "3.2.47", + "@vue/runtime-dom": "3.2.47", + "@vue/server-renderer": "3.2.47", + "@vue/shared": "3.2.47" } }, "node_modules/vue-awesome-sidebar": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/vue-awesome-sidebar/-/vue-awesome-sidebar-1.2.1.tgz", - "integrity": "sha512-R8kzF0dtGJhFgbbbp6NDj6XYCriX6dw/vlDJQUE6+rjRZnQLCU+0SSZlKAMoocYLtvcKGsw5FSoXraUarfCXfg==" + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/vue-awesome-sidebar/-/vue-awesome-sidebar-1.2.3.tgz", + "integrity": "sha512-A/2UOMZnzcMvin9rObqqEDcs10i3fiANqpTGGFDWoynzpx1PQ+yco7CmMwY78AEZbGAUx8KSf8qUeRMHmbXqHA==" }, "node_modules/vue-eslint-parser": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz", - "integrity": "sha512-NGn/iQy8/Wb7RrRa4aRkokyCZfOUWk19OP5HP6JEozQFX5AoS/t+Z0ZN7FY4LlmWc4FNI922V7cvX28zctN8dQ==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.1.1.tgz", + "integrity": "sha512-C2aI/r85Q6tYcz4dpgvrs4wH/MqVrRAVIdpYedrxnATDHHkb+TroeRcDpKWGZCx/OcECMWfz7tVwQ8e+Opy6rA==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -2523,1628 +2534,5 @@ "url": "https://github.com/sponsors/sindresorhus" } } - }, - "dependencies": { - "@babel/parser": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", - "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==" - }, - "@esbuild/android-arm": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.12.tgz", - "integrity": "sha512-CTWgMJtpCyCltrvipZrrcjjRu+rzm6pf9V8muCsJqtKujR3kPmU4ffbckvugNNaRmhxAF1ZI3J+0FUIFLFg8KA==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.12.tgz", - "integrity": "sha512-0LacmiIW+X0/LOLMZqYtZ7d4uY9fxYABAYhSSOu+OGQVBqH4N5eIYgkT7bBFnR4Nm3qo6qS3RpHKVrDASqj/uQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.12.tgz", - "integrity": "sha512-sS5CR3XBKQXYpSGMM28VuiUnbX83Z+aWPZzClW+OB2JquKqxoiwdqucJ5qvXS8pM6Up3RtJfDnRQZkz3en2z5g==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.12.tgz", - "integrity": "sha512-Dpe5hOAQiQRH20YkFAg+wOpcd4PEuXud+aGgKBQa/VriPJA8zuVlgCOSTwna1CgYl05lf6o5els4dtuyk1qJxQ==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.12.tgz", - "integrity": "sha512-ApGRA6X5txIcxV0095X4e4KKv87HAEXfuDRcGTniDWUUN+qPia8sl/BqG/0IomytQWajnUn4C7TOwHduk/FXBQ==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.12.tgz", - "integrity": "sha512-AMdK2gA9EU83ccXCWS1B/KcWYZCj4P3vDofZZkl/F/sBv/fphi2oUqUTox/g5GMcIxk8CF1CVYTC82+iBSyiUg==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.12.tgz", - "integrity": "sha512-KUKB9w8G/xaAbD39t6gnRBuhQ8vIYYlxGT2I+mT6UGRnCGRr1+ePFIGBQmf5V16nxylgUuuWVW1zU2ktKkf6WQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.12.tgz", - "integrity": "sha512-vhDdIv6z4eL0FJyNVfdr3C/vdd/Wc6h1683GJsFoJzfKb92dU/v88FhWdigg0i6+3TsbSDeWbsPUXb4dif2abg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.12.tgz", - "integrity": "sha512-29HXMLpLklDfmw7T2buGqq3HImSUaZ1ArmrPOMaNiZZQptOSZs32SQtOHEl8xWX5vfdwZqrBfNf8Te4nArVzKQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.12.tgz", - "integrity": "sha512-JFDuNDTTfgD1LJg7wHA42o2uAO/9VzHYK0leAVnCQE/FdMB599YMH73ux+nS0xGr79pv/BK+hrmdRin3iLgQjg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.12.tgz", - "integrity": "sha512-xTGzVPqm6WKfCC0iuj1fryIWr1NWEM8DMhAIo+4rFgUtwy/lfHl+Obvus4oddzRDbBetLLmojfVZGmt/g/g+Rw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.12.tgz", - "integrity": "sha512-zI1cNgHa3Gol+vPYjIYHzKhU6qMyOQrvZ82REr5Fv7rlh5PG6SkkuCoH7IryPqR+BK2c/7oISGsvPJPGnO2bHQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.12.tgz", - "integrity": "sha512-/C8OFXExoMmvTDIOAM54AhtmmuDHKoedUd0Otpfw3+AuuVGemA1nQK99oN909uZbLEU6Bi+7JheFMG3xGfZluQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.12.tgz", - "integrity": "sha512-qeouyyc8kAGV6Ni6Isz8hUsKMr00EHgVwUKWNp1r4l88fHEoNTDB8mmestvykW6MrstoGI7g2EAsgr0nxmuGYg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.12.tgz", - "integrity": "sha512-s9AyI/5vz1U4NNqnacEGFElqwnHusWa81pskAf8JNDM2eb6b2E6PpBmT8RzeZv6/TxE6/TADn2g9bb0jOUmXwQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.12.tgz", - "integrity": "sha512-e8YA7GQGLWhvakBecLptUiKxOk4E/EPtSckS1i0MGYctW8ouvNUoh7xnU15PGO2jz7BYl8q1R6g0gE5HFtzpqQ==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.12.tgz", - "integrity": "sha512-z2+kUxmOqBS+6SRVd57iOLIHE8oGOoEnGVAmwjm2aENSP35HPS+5cK+FL1l+rhrsJOFIPrNHqDUNechpuG96Sg==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.12.tgz", - "integrity": "sha512-PAonw4LqIybwn2/vJujhbg1N9W2W8lw9RtXIvvZoyzoA/4rA4CpiuahVbASmQohiytRsixbNoIOUSjRygKXpyA==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.12.tgz", - "integrity": "sha512-+wr1tkt1RERi+Zi/iQtkzmMH4nS8+7UIRxjcyRz7lur84wCkAITT50Olq/HiT4JN2X2bjtlOV6vt7ptW5Gw60Q==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.12.tgz", - "integrity": "sha512-XEjeUSHmjsAOJk8+pXJu9pFY2O5KKQbHXZWQylJzQuIBeiGrpMeq9sTVrHefHxMOyxUgoKQTcaTS+VK/K5SviA==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.12.tgz", - "integrity": "sha512-eRKPM7e0IecUAUYr2alW7JGDejrFJXmpjt4MlfonmQ5Rz9HWpKFGCjuuIRgKO7W9C/CWVFXdJ2GjddsBXqQI4A==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.12.tgz", - "integrity": "sha512-iPYKN78t3op2+erv2frW568j1q0RpqX6JOLZ7oPPaAV1VaF7dDstOrNw37PVOYoTWE11pV4A1XUitpdEFNIsPg==", - "dev": true, - "optional": true - }, - "@eslint/eslintrc": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", - "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.4.0", - "globals": "^13.15.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@humanwhocodes/config-array": { - "version": "0.11.7", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", - "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", - "peer": true - }, - "@rushstack/eslint-patch": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", - "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==", - "dev": true - }, - "@tauri-apps/api": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.2.0.tgz", - "integrity": "sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==" - }, - "@tauri-apps/cli": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.2.2.tgz", - "integrity": "sha512-D8zib3A0vWCvPPSyYLxww/OdDlVcY7fpcDVBH6qUvheOjj2aCyU7H9AYMRBwpgCfz8zY5+vomee+laLeB0H13w==", - "requires": { - "@tauri-apps/cli-darwin-arm64": "1.2.2", - "@tauri-apps/cli-darwin-x64": "1.2.2", - "@tauri-apps/cli-linux-arm-gnueabihf": "1.2.2", - "@tauri-apps/cli-linux-arm64-gnu": "1.2.2", - "@tauri-apps/cli-linux-arm64-musl": "1.2.2", - "@tauri-apps/cli-linux-x64-gnu": "1.2.2", - "@tauri-apps/cli-linux-x64-musl": "1.2.2", - "@tauri-apps/cli-win32-ia32-msvc": "1.2.2", - "@tauri-apps/cli-win32-x64-msvc": "1.2.2" - } - }, - "@tauri-apps/cli-darwin-arm64": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.2.2.tgz", - "integrity": "sha512-W+Cp2weUMlvmGkRJeUjypbz9Lpl6o98xkgKAtobZSum5SNwpsBQfawJTESakNoD+FXyVg/snIk5sRdHge+tAaA==", - "optional": true - }, - "@tauri-apps/cli-darwin-x64": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.2.2.tgz", - "integrity": "sha512-vmVAqt+ECH2d6cbcGJ7ddcCAZgmKe5xmxlL5r4xoaphu7OqU4gnv4VFURYkVltOfwzIFQVOPVSqwYyIDToCYNQ==", - "optional": true - }, - "@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.2.2.tgz", - "integrity": "sha512-yYTdQurgi4QZR8z+fANjl522jdQz/VtesFpw+C/A0+zXg7tiRjicsywBDdPsvNzCqFeGKKkmTR+Lny5qxhGaeQ==", - "optional": true - }, - "@tauri-apps/cli-linux-arm64-gnu": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.2.2.tgz", - "integrity": "sha512-ZSOVT6Eq1ay2+27B8KfA0MnpO7KYzONU6TjenH7DNcQki6eWGG5JoNu8QQ9Mdn3dAzY0XBP9i1ZHQOFu4iPtEg==", - "optional": true - }, - "@tauri-apps/cli-linux-arm64-musl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.2.2.tgz", - "integrity": "sha512-/NHSkqNQ+Pr4PshvyD1CeNFaPCaCpe1OeuAQgVi0rboSecC9fXN96G5dQbSBoxOUcCo6f8aTVE7zkZ4WchFVog==", - "optional": true - }, - "@tauri-apps/cli-linux-x64-gnu": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.2.2.tgz", - "integrity": "sha512-4YTmfPuyvlHsvCkATDMwhklfuQm3HKxYXv/IOW9H0ra6pS9efVhrFYIC9Vfv6XaKN85Vnn/FYTEGMJLwCxZw2Q==", - "optional": true - }, - "@tauri-apps/cli-linux-x64-musl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.2.2.tgz", - "integrity": "sha512-wr46tbscwFuCcA931R+ItOiUTT0djMmgKLd1HFCmFF82V9BKE2reIjr6O9l0NCXCo2WeD4pe3jA/Pt1dxDu+JA==", - "optional": true - }, - "@tauri-apps/cli-win32-ia32-msvc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.2.2.tgz", - "integrity": "sha512-6VmbVJOWUZJK5/JKhb3mNFKrKGfq0KV7lJGumfN95WJgkHeyL61p8bZit+o6ZgUGUhrOabkAawhDkrRY+ZQhIw==", - "optional": true - }, - "@tauri-apps/cli-win32-x64-msvc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.2.2.tgz", - "integrity": "sha512-YRPJguJma+zSuRZpFoSZqls6+laggG1vqG0FPQWQTi+ywATgMpai2b2RZnffDlpHKp9mt4V/s2dtqOy6bpGZHg==", - "optional": true - }, - "@vitejs/plugin-vue": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz", - "integrity": "sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==", - "dev": true, - "requires": {} - }, - "@vue/compiler-core": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.45.tgz", - "integrity": "sha512-rcMj7H+PYe5wBV3iYeUgbCglC+pbpN8hBLTJvRiK2eKQiWqu+fG9F+8sW99JdL4LQi7Re178UOxn09puSXvn4A==", - "requires": { - "@babel/parser": "^7.16.4", - "@vue/shared": "3.2.45", - "estree-walker": "^2.0.2", - "source-map": "^0.6.1" - } - }, - "@vue/compiler-dom": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.45.tgz", - "integrity": "sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==", - "requires": { - "@vue/compiler-core": "3.2.45", - "@vue/shared": "3.2.45" - } - }, - "@vue/compiler-sfc": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.45.tgz", - "integrity": "sha512-1jXDuWah1ggsnSAOGsec8cFjT/K6TMZ0sPL3o3d84Ft2AYZi2jWJgRMjw4iaK0rBfA89L5gw427H4n1RZQBu6Q==", - "requires": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.45", - "@vue/compiler-dom": "3.2.45", - "@vue/compiler-ssr": "3.2.45", - "@vue/reactivity-transform": "3.2.45", - "@vue/shared": "3.2.45", - "estree-walker": "^2.0.2", - "magic-string": "^0.25.7", - "postcss": "^8.1.10", - "source-map": "^0.6.1" - } - }, - "@vue/compiler-ssr": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.45.tgz", - "integrity": "sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==", - "requires": { - "@vue/compiler-dom": "3.2.45", - "@vue/shared": "3.2.45" - } - }, - "@vue/devtools-api": { - "version": "6.4.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.4.5.tgz", - "integrity": "sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ==" - }, - "@vue/eslint-config-prettier": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-7.0.0.tgz", - "integrity": "sha512-/CTc6ML3Wta1tCe1gUeO0EYnVXfo3nJXsIhZ8WJr3sov+cGASr6yuiibJTL6lmIBm7GobopToOuB3B6AWyV0Iw==", - "dev": true, - "requires": { - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-prettier": "^4.0.0" - } - }, - "@vue/reactivity": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.45.tgz", - "integrity": "sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==", - "requires": { - "@vue/shared": "3.2.45" - } - }, - "@vue/reactivity-transform": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.45.tgz", - "integrity": "sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==", - "requires": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.45", - "@vue/shared": "3.2.45", - "estree-walker": "^2.0.2", - "magic-string": "^0.25.7" - } - }, - "@vue/runtime-core": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.45.tgz", - "integrity": "sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==", - "requires": { - "@vue/reactivity": "3.2.45", - "@vue/shared": "3.2.45" - } - }, - "@vue/runtime-dom": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.45.tgz", - "integrity": "sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==", - "requires": { - "@vue/runtime-core": "3.2.45", - "@vue/shared": "3.2.45", - "csstype": "^2.6.8" - } - }, - "@vue/server-renderer": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.45.tgz", - "integrity": "sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==", - "requires": { - "@vue/compiler-ssr": "3.2.45", - "@vue/shared": "3.2.45" - } - }, - "@vue/shared": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.45.tgz", - "integrity": "sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==" - }, - "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "animate.css": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz", - "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==" - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "bootstrap": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz", - "integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==", - "requires": {} - }, - "bootstrap-icons": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.2.tgz", - "integrity": "sha512-PTPYadRn1AMGr+QTSxe4ZCc+Wzv9DGZxbi3lNse/dajqV31n2/wl/7NX78ZpkvFgRNmH4ogdIQPQmxAfhEV6nA==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "csstype": { - "version": "2.6.21", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", - "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "esbuild": { - "version": "0.16.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.12.tgz", - "integrity": "sha512-eq5KcuXajf2OmivCl4e89AD3j8fbV+UTE9vczEzq5haA07U9oOTzBWlh3+6ZdjJR7Rz2QfWZ2uxZyhZxBgJ4+g==", - "dev": true, - "requires": { - "@esbuild/android-arm": "0.16.12", - "@esbuild/android-arm64": "0.16.12", - "@esbuild/android-x64": "0.16.12", - "@esbuild/darwin-arm64": "0.16.12", - "@esbuild/darwin-x64": "0.16.12", - "@esbuild/freebsd-arm64": "0.16.12", - "@esbuild/freebsd-x64": "0.16.12", - "@esbuild/linux-arm": "0.16.12", - "@esbuild/linux-arm64": "0.16.12", - "@esbuild/linux-ia32": "0.16.12", - "@esbuild/linux-loong64": "0.16.12", - "@esbuild/linux-mips64el": "0.16.12", - "@esbuild/linux-ppc64": "0.16.12", - "@esbuild/linux-riscv64": "0.16.12", - "@esbuild/linux-s390x": "0.16.12", - "@esbuild/linux-x64": "0.16.12", - "@esbuild/netbsd-x64": "0.16.12", - "@esbuild/openbsd-x64": "0.16.12", - "@esbuild/sunos-x64": "0.16.12", - "@esbuild/win32-arm64": "0.16.12", - "@esbuild/win32-ia32": "0.16.12", - "@esbuild/win32-x64": "0.16.12" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz", - "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==", - "dev": true, - "requires": { - "@eslint/eslintrc": "^1.3.3", - "@humanwhocodes/config-array": "^0.11.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.15.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", - "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-plugin-vue": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.8.0.tgz", - "integrity": "sha512-E/AXwcTzunyzM83C2QqDHxepMzvI2y6x+mmeYHbVDQlKFqmKYvRrhaVixEeeG27uI44p9oKDFiyCRw4XxgtfHA==", - "dev": true, - "requires": { - "eslint-utils": "^3.0.0", - "natural-compare": "^1.4.0", - "nth-check": "^2.0.1", - "postcss-selector-parser": "^6.0.9", - "semver": "^7.3.5", - "vue-eslint-parser": "^9.0.1", - "xml-name-validator": "^4.0.0" - } - }, - "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - } - } - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true - }, - "espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" - } - }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "fastq": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", - "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.19.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", - "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "ignore": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", - "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", - "dev": true - }, - "immutable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", - "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==" - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "js-sdsl": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", - "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "requires": { - "sourcemap-codec": "^1.4.8" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "requires": { - "boolbase": "^1.0.0" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" - }, - "postcss": { - "version": "8.4.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", - "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", - "requires": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "prettier": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz", - "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "requires": { - "picomatch": "^2.2.1" - } - }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "rollup": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.9.0.tgz", - "integrity": "sha512-nGGylpmblyjTpF4lEUPgmOw6OVxRvnI6Iuuh6Lz4O/X66cVOX1XJSsqP1YamxQ+mPuFE7qJxLFDSCk8rNv5dDw==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "sass": { - "version": "1.56.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz", - "integrity": "sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==", - "requires": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - } - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" - }, - "sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "tauri-plugin-store-api": { - "version": "git+ssh://git@github.com/r3dlight/tauri-plugin-store.git#13183ef1030aa1977dc3ddbb475af3a1bf34b00f", - "from": "tauri-plugin-store-api@github:r3dlight/tauri-plugin-store#dev", - "requires": { - "@tauri-apps/api": "1.2.0", - "tslib": "2.4.0" - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "vite": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz", - "integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==", - "dev": true, - "requires": { - "esbuild": "^0.16.3", - "fsevents": "~2.3.2", - "postcss": "^8.4.20", - "resolve": "^1.22.1", - "rollup": "^3.7.0" - } - }, - "vue": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.45.tgz", - "integrity": "sha512-9Nx/Mg2b2xWlXykmCwiTUCWHbWIj53bnkizBxKai1g61f2Xit700A1ljowpTIM11e3uipOeiPcSqnmBg6gyiaA==", - "requires": { - "@vue/compiler-dom": "3.2.45", - "@vue/compiler-sfc": "3.2.45", - "@vue/runtime-dom": "3.2.45", - "@vue/server-renderer": "3.2.45", - "@vue/shared": "3.2.45" - } - }, - "vue-awesome-sidebar": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/vue-awesome-sidebar/-/vue-awesome-sidebar-1.2.1.tgz", - "integrity": "sha512-R8kzF0dtGJhFgbbbp6NDj6XYCriX6dw/vlDJQUE6+rjRZnQLCU+0SSZlKAMoocYLtvcKGsw5FSoXraUarfCXfg==" - }, - "vue-eslint-parser": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz", - "integrity": "sha512-NGn/iQy8/Wb7RrRa4aRkokyCZfOUWk19OP5HP6JEozQFX5AoS/t+Z0ZN7FY4LlmWc4FNI922V7cvX28zctN8dQ==", - "dev": true, - "requires": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" - } - }, - "vue-router": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz", - "integrity": "sha512-DYWYwsG6xNPmLq/FmZn8Ip+qrhFEzA14EI12MsMgVxvHFDYvlr4NXpVF5hrRH1wVcDP8fGi5F4rxuJSl8/r+EQ==", - "requires": { - "@vue/devtools-api": "^6.4.5" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - } } } diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index 7d818df..3b868f3 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -561,7 +561,7 @@ async fn export_sshpubkey(ip: String) -> bool { #[command] fn is_alive(name: String) -> Result { if name.chars().count() == 0 { - log::warn!("Name must not be empty"); + log::warn!(" is_alive: Name must not be empty"); return Ok(false); } @@ -606,6 +606,16 @@ fn is_alive(name: String) -> Result { // TODO: to be modified to work locally #[command] async fn sign_key(password: String) -> bool { + let device = match watch_new_usb(){ + Ok(dev) => { + log::debug!("{dev}"); + dev + }, + Err(e) => { + log::error!("Error while looking for new USB device: {e}"); + return false; + } + }; true } @@ -681,12 +691,12 @@ async fn revoke_key(ip: String) -> bool { } #[command] -fn validate_privatekey(public_key: String, private_key: String) -> bool { +async fn validate_privatekey(public_key: String, private_key: String) -> bool { Path::new(&public_key.trim()).is_file() && Path::new(&private_key.trim()).is_file() } #[command] -fn validate_rootkey(root_key: String) -> bool { +async fn validate_rootkey(root_key: String) -> bool { Path::new(&root_key.trim()).is_file() } @@ -703,7 +713,7 @@ fn validate_rootkey(root_key: String) -> bool { /// # Return /// Return a result containing an error message if any #[command] -fn generate_pki_in_dir( +async fn generate_pki_in_dir( org_name: String, org_unit: String, country: String, diff --git a/keysas-admin/src-tauri/src/usb_sign.rs b/keysas-admin/src-tauri/src/usb_sign.rs index 9776a49..3fdac4f 100644 --- a/keysas-admin/src-tauri/src/usb_sign.rs +++ b/keysas-admin/src-tauri/src/usb_sign.rs @@ -12,7 +12,6 @@ use std::fs::File; use std::io::prelude::*; use std::io::SeekFrom; use std::path::Path; -//extern crate udev; use base64::{engine::general_purpose, Engine as _}; use ed25519_dalek::Keypair; use keysas_lib::keysas_key::KeysasKey; @@ -86,7 +85,7 @@ fn rm_last(value: &str) -> &str { } } -/// Construct an hybrid signature from +/// Construct an hybrid signature from firmware information #[cfg(target_os = "linux")] fn sign_device( vendor: &str, @@ -114,7 +113,7 @@ fn sign_device( } #[cfg(target_os = "linux")] -fn watch_new_usb() -> Result { +pub fn watch_new_usb() -> Result { let socket = udev::MonitorBuilder::new()? //.match_subsystem_devtype("usb", "usb_device")? .match_subsystem("block")? @@ -190,7 +189,7 @@ fn watch_new_usb() -> Result { .ok_or_else(|| anyhow!("Cannot convert ID_SERIAL to str."))?, ) .ok_or_else(|| anyhow!("Cannot get ID_SERIAL from event."))?; - log::info!( + log::debug!( "Found new USB device : Device: {}, Vendor: {}, Model: {}, Revision: {}, Serial: {}", device, vendor.to_string_lossy(), diff --git a/keysas-admin/src/App.vue b/keysas-admin/src/App.vue index 6eee7c5..1caeae1 100644 --- a/keysas-admin/src/App.vue +++ b/keysas-admin/src/App.vue @@ -86,6 +86,12 @@ body { background-image: linear-gradient(to right, #7a7979 0%, rgb(41, 40, 40) 100%); } +h1 { + font-weight: bold; + color: #fff; + font-size: 26px; +} + nav { //padding: 10px; a { diff --git a/keysas-admin/src/components/DisplaySigningConfig.vue b/keysas-admin/src/components/DisplaySigningConfig.vue index 980ed93..8074cd7 100644 --- a/keysas-admin/src/components/DisplaySigningConfig.vue +++ b/keysas-admin/src/components/DisplaySigningConfig.vue @@ -2,22 +2,12 @@
Help

- You must provide the application a dedicated root - Certificate Authority keypair to setup the PKI. - ED25519 or ED448 keys are accepted in PKCS#12 format.
- To generate this new keypair on your local machine, open a terminal and - enter to following commands:
-
-
- # Generate private key for the root CA
- > openssl genpkey -algorithm ed25519 -out root-priv.pem
- # Generate corresponding certificate
- > openssl req -new x509 -nodes -days 3650 -key root-priv.pem -out root-cert.pem
- # Generate PKCS#12 file with the two keys
- > openssl pkcs12 -export -out root-store.p12 -inkey root-priv.pem -in root-cert.pem
- # Clean private key file
- > rm ./root-priv.pem -
+ If you are configuring Keysas-admin for the first time, click on Create a new PKI. + Then, provide all the requested information to allow us to create a new PKI for you.
+ When done, you will be able to start signing new outgoing USB devices.
+
+ If you have already created a PKI and you want to restore it, choose Load from local PKI +

diff --git a/keysas-admin/src/components/RevokeDevice.vue b/keysas-admin/src/components/RevokeDevice.vue index 15e3c53..0bdc8ab 100644 --- a/keysas-admin/src/components/RevokeDevice.vue +++ b/keysas-admin/src/components/RevokeDevice.vue @@ -1,11 +1,32 @@ + \ No newline at end of file diff --git a/keysas-admin/src/components/SignKey.vue b/keysas-admin/src/components/SignKey.vue index eb38a35..3135aa8 100644 --- a/keysas-admin/src/components/SignKey.vue +++ b/keysas-admin/src/components/SignKey.vue @@ -19,7 +19,13 @@ HELP

Enter your signing password and plug the new device within 30 - seconds to sign it. + seconds to sign it. Before signing your first device: +
  • On GNU/Linux:
  • +
      +
    • Create a new file /etc/udev/rules.d/71-keysas
    • +
    • Copy and paste this in the new file:
      SUBSYSTEMS=="usb", MODE="0660", TAG+="uaccess"
    • +
    • Open a terminal and execute this:
      udevadm trigger && udevadm control --reload
    • +
    diff --git a/keysas-admin/src/router/index.js b/keysas-admin/src/router/index.js index 6da98cd..d6c1863 100644 --- a/keysas-admin/src/router/index.js +++ b/keysas-admin/src/router/index.js @@ -21,7 +21,8 @@ const routes = [ // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. - component: () => import(/* webpackChunkName: "about" */ '../views/ManageView.vue') + component: () => import(/* webpackChunkName: "about" */ '../views/ManageView.vue'), + props: true }, { path: '/config', From 517f0a854f85387b3160415a3f8f0217a69f87f9 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 27 Apr 2023 14:09:08 +0200 Subject: [PATCH 036/160] Add check for PKI password length --- keysas-admin/src/components/SigningKeys.vue | 28 ++++++++++++--------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/keysas-admin/src/components/SigningKeys.vue b/keysas-admin/src/components/SigningKeys.vue index 576ca6a..2a43c31 100644 --- a/keysas-admin/src/components/SigningKeys.vue +++ b/keysas-admin/src/components/SigningKeys.vue @@ -83,7 +83,7 @@ -
    {{ keysError }} +
    {{ passwordError }}


    @@ -129,7 +129,8 @@ export default { waiting: false, showLoadPKIForm: false, showRootKeyForm: false, - showPkiDirForm: false + showPkiDirForm: false, + passwordError: '', } }, @@ -153,22 +154,25 @@ export default { console.log('Root CA form submission'); }, async submit() { - this.waiting = true; await this.submitPKIDirForm(); }, async submitPKIDirForm() { console.log('PKI Dir form submission'); - await invoke('generate_pki_in_dir', { - orgName: this.orgName, - orgUnit: this.orgUnit, - country: this.country, - validity: this.validity, - adminPwd: this.adminPwd, - pkiDir: this.pkiDir - - }) + this.passwordError = this.adminPwd.length > 7 ? + '' : "Password should have been created with at least 8 chars"; + if (!this.passwordError) { + this.waiting = true; + await invoke('generate_pki_in_dir', { + orgName: this.orgName, + orgUnit: this.orgUnit, + country: this.country, + validity: this.validity, + adminPwd: this.adminPwd, + pkiDir: this.pkiDir + }) .then((res) => this.pkiGenerated()) .catch((error) => console.error(error)); + } else {console.log("Password incorrect lengh !")} }, async pkiGenerated(){ this.waiting = false; From 7e0830ba3db3d6a2c3edbc30172e12780c7fd4ea Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 27 Apr 2023 14:49:00 +0200 Subject: [PATCH 037/160] Rename few componants --- .../src/components/{SignKey.vue => SignDevice.vue} | 2 +- keysas-admin/src/router/index.js | 2 +- keysas-admin/src/views/{config.vue => AdminView.vue} | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) rename keysas-admin/src/components/{SignKey.vue => SignDevice.vue} (99%) rename keysas-admin/src/views/{config.vue => AdminView.vue} (95%) diff --git a/keysas-admin/src/components/SignKey.vue b/keysas-admin/src/components/SignDevice.vue similarity index 99% rename from keysas-admin/src/components/SignKey.vue rename to keysas-admin/src/components/SignDevice.vue index 3135aa8..cf02b76 100644 --- a/keysas-admin/src/components/SignKey.vue +++ b/keysas-admin/src/components/SignDevice.vue @@ -50,7 +50,7 @@ import { invoke } from "@tauri-apps/api"; import 'animate.css'; export default { - name: 'SignKey', + name: 'SignDevice', props: { signUsbStatus: Boolean, }, diff --git a/keysas-admin/src/router/index.js b/keysas-admin/src/router/index.js index d6c1863..0521667 100644 --- a/keysas-admin/src/router/index.js +++ b/keysas-admin/src/router/index.js @@ -30,7 +30,7 @@ const routes = [ // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. - component: () => import(/* webpackChunkName: "about" */ '../views/config.vue') + component: () => import(/* webpackChunkName: "about" */ '../views/AdminView.vue') }, { path: '/about', diff --git a/keysas-admin/src/views/config.vue b/keysas-admin/src/views/AdminView.vue similarity index 95% rename from keysas-admin/src/views/config.vue rename to keysas-admin/src/views/AdminView.vue index c9a2c9c..4964340 100644 --- a/keysas-admin/src/views/config.vue +++ b/keysas-admin/src/views/AdminView.vue @@ -22,7 +22,7 @@

    USB device signing

    - +
    @@ -58,14 +58,14 @@ import SSHKeys from '../components/SSHKeys.vue' import DisplaySSHConfig from '../components/DisplaySSHConfig.vue' import SigningKeys from '../components/SigningKeys.vue' import DisplaySigningConfig from '../components/DisplaySigningConfig.vue' -import SignKey from '../components/SignKey.vue' +import SignDevice from '../components/SignDevice.vue' import RevokeDevice from '../components/RevokeDevice.vue' export default { - name: 'AddView', + name: 'AdminView', components: { NavBar, - SignKey, + SignDevice, SSHKeys, DisplaySSHConfig, SigningKeys, From cee3e27fa3d9be427c99a066b9e45e7e32057b43 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 27 Apr 2023 14:55:16 +0200 Subject: [PATCH 038/160] Bump version --- keysas-admin/src/components/DisplaySSHConfig.vue | 2 +- keysas-admin/src/views/AboutView.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/keysas-admin/src/components/DisplaySSHConfig.vue b/keysas-admin/src/components/DisplaySSHConfig.vue index 98566e0..bd35446 100644 --- a/keysas-admin/src/components/DisplaySSHConfig.vue +++ b/keysas-admin/src/components/DisplaySSHConfig.vue @@ -123,7 +123,7 @@ button { .tip { max-width: 1000px; text-align: left; - margin: 30px auto; + margin: 40px auto; background: white; padding: 40px; border-radius: 10px; diff --git a/keysas-admin/src/views/AboutView.vue b/keysas-admin/src/views/AboutView.vue index 050c897..581dd54 100644 --- a/keysas-admin/src/views/AboutView.vue +++ b/keysas-admin/src/views/AboutView.vue @@ -23,7 +23,7 @@ export default { From e7b9a400740c3ac081c066fba93f49f086f69235 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 27 Apr 2023 15:05:08 +0200 Subject: [PATCH 039/160] Check that country is < 2 chars long --- keysas-admin/src/components/SigningKeys.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/keysas-admin/src/components/SigningKeys.vue b/keysas-admin/src/components/SigningKeys.vue index 2a43c31..e16417e 100644 --- a/keysas-admin/src/components/SigningKeys.vue +++ b/keysas-admin/src/components/SigningKeys.vue @@ -74,6 +74,7 @@ +
    {{ countryError }}
    @@ -131,6 +132,7 @@ export default { showRootKeyForm: false, showPkiDirForm: false, passwordError: '', + countryError: '', } }, @@ -160,7 +162,9 @@ export default { console.log('PKI Dir form submission'); this.passwordError = this.adminPwd.length > 7 ? '' : "Password should have been created with at least 8 chars"; - if (!this.passwordError) { + this.countryError = this.country.length < 3 ? + '' : "Country must be 2 chars long"; + if (!this.passwordError && !this.countryError) { this.waiting = true; await invoke('generate_pki_in_dir', { orgName: this.orgName, From 592c2d2e6925b4f2f7b589284c04b98fc13ac4d4 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 27 Apr 2023 15:05:19 +0200 Subject: [PATCH 040/160] Bump version --- keysas-admin/package-lock.json | 2 +- keysas-admin/package.json | 2 +- keysas-admin/src-tauri/Cargo.toml | 2 +- keysas-admin/src-tauri/tauri.conf.json | 2 +- keysas-admin/src/views/HomeView.vue | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/keysas-admin/package-lock.json b/keysas-admin/package-lock.json index 8d18de4..b3fa08e 100644 --- a/keysas-admin/package-lock.json +++ b/keysas-admin/package-lock.json @@ -1,6 +1,6 @@ { "name": "keysas-admin", - "version": "0.2.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/keysas-admin/package.json b/keysas-admin/package.json index a7a1378..f77b252 100644 --- a/keysas-admin/package.json +++ b/keysas-admin/package.json @@ -1,6 +1,6 @@ { "name": "keysas-admin", - "version": "0.2.0", + "version": "2.0.0", "scripts": { "dev": "vite", "build": "vite build", diff --git a/keysas-admin/src-tauri/Cargo.toml b/keysas-admin/src-tauri/Cargo.toml index bfbc059..fe7f3c7 100644 --- a/keysas-admin/src-tauri/Cargo.toml +++ b/keysas-admin/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keysas-admin" -version = "0.2.0" +version = "2.0.0" description = "Keysas stations administration application" authors = ["Stephane N", "Luc Bonnafoux"] license = "GPL-3.0" diff --git a/keysas-admin/src-tauri/tauri.conf.json b/keysas-admin/src-tauri/tauri.conf.json index 9e0f7bb..3f3c723 100644 --- a/keysas-admin/src-tauri/tauri.conf.json +++ b/keysas-admin/src-tauri/tauri.conf.json @@ -7,7 +7,7 @@ }, "package": { "productName": "keysas-admin", - "version": "0.1.2" + "version": "2.0.0" }, "tauri": { "allowlist": { diff --git a/keysas-admin/src/views/HomeView.vue b/keysas-admin/src/views/HomeView.vue index 5ed859e..c548584 100644 --- a/keysas-admin/src/views/HomeView.vue +++ b/keysas-admin/src/views/HomeView.vue @@ -4,7 +4,7 @@

    Welcome to Keysas-admin !
    This application allows you to manage your Keysas stations.
    You can register news Keysas stations, update them, sign USB devices and much more.



    - QUICK START +

    QUICK START


    • SSH configuration
    • From 736d993bd2803ed2ac7cd9cb9ad76db96b6674c7 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Fri, 28 Apr 2023 10:23:14 +0200 Subject: [PATCH 041/160] Improve UX --- keysas-admin/src/views/HomeView.vue | 4 ++-- keysas-admin/src/views/ManageView.vue | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/keysas-admin/src/views/HomeView.vue b/keysas-admin/src/views/HomeView.vue index c548584..6555ff3 100644 --- a/keysas-admin/src/views/HomeView.vue +++ b/keysas-admin/src/views/HomeView.vue @@ -10,14 +10,14 @@
    • SSH configuration
      • First, start by creating a ED25519 private key and the associated public key on your - computer ;
        + computer
        This keypair should only be dedicated to the administration of your Keysas stations. To do so, open a terminal and use the following command:
      • > ssh-keygen -m PEM -t ed25519 -f mykey

        -
      • Then, set the path of both keys in the "Admin configuration/SSH configuration" menu ;
      • +
      • Then, set the path of both keys in the "Admin configuration->SSH configuration" menu

    • Generate a IKPQPKI
    • diff --git a/keysas-admin/src/views/ManageView.vue b/keysas-admin/src/views/ManageView.vue index b82d57a..e01f26e 100644 --- a/keysas-admin/src/views/ManageView.vue +++ b/keysas-admin/src/views/ManageView.vue @@ -84,7 +84,7 @@
      +

      Done !

      @@ -355,7 +353,7 @@ export default { console.log("sign_usb_status: " + this.sign_usb_status); } else { - console.log('CreateKeypair not called!') + console.log('SignUSB not called!') } }, isalive() { From 7757736d89065c030d2390037c4593a243ffd8f4 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Fri, 28 Apr 2023 10:51:35 +0200 Subject: [PATCH 042/160] Improve HELP for adding a station --- keysas-admin/src/components/DisplayKeysas.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keysas-admin/src/components/DisplayKeysas.vue b/keysas-admin/src/components/DisplayKeysas.vue index 47ae825..b92ef12 100644 --- a/keysas-admin/src/components/DisplayKeysas.vue +++ b/keysas-admin/src/components/DisplayKeysas.vue @@ -4,7 +4,7 @@
      Add here the Keysas stations you want to manage with this application.
      You can find the IP address in the Help menu of your Keysas station + class="bi bi-emoji-wink">
      Do not not forget to enroll it when done !

    From 9c8d39e5011692bd41ca6ed6c848106249df2a19 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Fri, 28 Apr 2023 10:57:49 +0200 Subject: [PATCH 043/160] Rename componant AddForm into AddStation --- keysas-admin/src/components/{AddForm.vue => AddStation.vue} | 2 +- keysas-admin/src/views/AddView.vue | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename keysas-admin/src/components/{AddForm.vue => AddStation.vue} (99%) diff --git a/keysas-admin/src/components/AddForm.vue b/keysas-admin/src/components/AddStation.vue similarity index 99% rename from keysas-admin/src/components/AddForm.vue rename to keysas-admin/src/components/AddStation.vue index 9765b3f..6416586 100644 --- a/keysas-admin/src/components/AddForm.vue +++ b/keysas-admin/src/components/AddStation.vue @@ -23,7 +23,7 @@ import { invoke } from "@tauri-apps/api"; export default { - name: 'AddForm', + name: 'AddStation', props: { //message: String, }, diff --git a/keysas-admin/src/views/AddView.vue b/keysas-admin/src/views/AddView.vue index bbb6795..4df4165 100644 --- a/keysas-admin/src/views/AddView.vue +++ b/keysas-admin/src/views/AddView.vue @@ -6,7 +6,7 @@
    - +
    @@ -19,7 +19,7 @@ "use strict"; import NavBar from '../components/NavBar.vue' -import AddForm from '../components/AddForm.vue' +import AddStation from '../components/AddStation.vue' import DisplayKeysas from '../components/DisplayKeysas.vue' @@ -27,7 +27,7 @@ export default { name: 'AddView', components: { NavBar, - AddForm, + AddStation, DisplayKeysas }, computed: { From 3fb45db7f1d672cb516e44cf0cb9bff3189926a1 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Fri, 28 Apr 2023 11:17:15 +0200 Subject: [PATCH 044/160] Fox is_alive to be async --- keysas-admin/src-tauri/src/main.rs | 2 +- keysas-admin/src/components/GenKeypair.vue | 48 ---------------------- keysas-admin/src/views/ManageView.vue | 13 ++---- 3 files changed, 4 insertions(+), 59 deletions(-) delete mode 100644 keysas-admin/src/components/GenKeypair.vue diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index 96f446c..0393395 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -558,7 +558,7 @@ async fn export_sshpubkey(ip: String) -> bool { /// This command test if a given station is connected or not /// The function returns a boolean indicating the station status or an error #[command] -fn is_alive(name: String) -> Result { +async fn is_alive(name: String) -> Result { if name.chars().count() == 0 { log::warn!(" is_alive: Name must not be empty"); return Ok(false); diff --git a/keysas-admin/src/components/GenKeypair.vue b/keysas-admin/src/components/GenKeypair.vue deleted file mode 100644 index ade8026..0000000 --- a/keysas-admin/src/components/GenKeypair.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - diff --git a/keysas-admin/src/views/ManageView.vue b/keysas-admin/src/views/ManageView.vue index e01f26e..f07da4e 100644 --- a/keysas-admin/src/views/ManageView.vue +++ b/keysas-admin/src/views/ManageView.vue @@ -134,7 +134,6 @@
    - @@ -152,9 +151,7 @@ + + diff --git a/keysas-usbfilter/tray-app/package-lock.json b/keysas-usbfilter/tray-app/package-lock.json new file mode 100644 index 0000000..d124d65 --- /dev/null +++ b/keysas-usbfilter/tray-app/package-lock.json @@ -0,0 +1,1053 @@ +{ + "name": "keysas-minifilter", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "keysas-minifilter", + "version": "0.0.0", + "dependencies": { + "@tauri-apps/api": "^1.2.0", + "vue": "^3.2.45" + }, + "devDependencies": { + "@tauri-apps/cli": "^1.2.3", + "@types/node": "^18.7.10", + "@vitejs/plugin-vue": "^4.0.0", + "typescript": "^4.9.5", + "vite": "^4.2.1", + "vue-tsc": "^1.0.11" + } + }, + "node_modules/@babel/parser": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.17.tgz", + "integrity": "sha512-E6VAZwN7diCa3labs0GYvhEPL2M94WLF8A+czO8hfjREXxba8Ng7nM5VxV+9ihNXIY1iQO1XxUU4P7hbqbICxg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.17.tgz", + "integrity": "sha512-jaJ5IlmaDLFPNttv0ofcwy/cfeY4bh/n705Tgh+eLObbGtQBK3EPAu+CzL95JVE4nFAliyrnEu0d32Q5foavqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.17.tgz", + "integrity": "sha512-446zpfJ3nioMC7ASvJB1pszHVskkw4u/9Eu8s5yvvsSDTzYh4p4ZIRj0DznSl3FBF0Z/mZfrKXTtt0QCoFmoHA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.17.tgz", + "integrity": "sha512-m/gwyiBwH3jqfUabtq3GH31otL/0sE0l34XKpSIqR7NjQ/XHQ3lpmQHLHbG8AHTGCw8Ao059GvV08MS0bhFIJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.17.tgz", + "integrity": "sha512-4utIrsX9IykrqYaXR8ob9Ha2hAY2qLc6ohJ8c0CN1DR8yWeMrTgYFjgdeQ9LIoTOfLetXjuCu5TRPHT9yKYJVg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.17.tgz", + "integrity": "sha512-4PxjQII/9ppOrpEwzQ1b0pXCsFLqy77i0GaHodrmzH9zq2/NEhHMAMJkJ635Ns4fyJPFOlHMz4AsklIyRqFZWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.17.tgz", + "integrity": "sha512-lQRS+4sW5S3P1sv0z2Ym807qMDfkmdhUYX30GRBURtLTrJOPDpoU0kI6pVz1hz3U0+YQ0tXGS9YWveQjUewAJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.17.tgz", + "integrity": "sha512-biDs7bjGdOdcmIk6xU426VgdRUpGg39Yz6sT9Xp23aq+IEHDb/u5cbmu/pAANpDB4rZpY/2USPhCA+w9t3roQg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.17.tgz", + "integrity": "sha512-2+pwLx0whKY1/Vqt8lyzStyda1v0qjJ5INWIe+d8+1onqQxHLLi3yr5bAa4gvbzhZqBztifYEu8hh1La5+7sUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.17.tgz", + "integrity": "sha512-IBTTv8X60dYo6P2t23sSUYym8fGfMAiuv7PzJ+0LcdAndZRzvke+wTVxJeCq4WgjppkOpndL04gMZIFvwoU34Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.17.tgz", + "integrity": "sha512-WVMBtcDpATjaGfWfp6u9dANIqmU9r37SY8wgAivuKmgKHE+bWSuv0qXEFt/p3qXQYxJIGXQQv6hHcm7iWhWjiw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.17.tgz", + "integrity": "sha512-2kYCGh8589ZYnY031FgMLy0kmE4VoGdvfJkxLdxP4HJvWNXpyLhjOvxVsYjYZ6awqY4bgLR9tpdYyStgZZhi2A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.17.tgz", + "integrity": "sha512-KIdG5jdAEeAKogfyMTcszRxy3OPbZhq0PPsW4iKKcdlbk3YE4miKznxV2YOSmiK/hfOZ+lqHri3v8eecT2ATwQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.17.tgz", + "integrity": "sha512-Cj6uWLBR5LWhcD/2Lkfg2NrkVsNb2sFM5aVEfumKB2vYetkA/9Uyc1jVoxLZ0a38sUhFk4JOVKH0aVdPbjZQeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.17.tgz", + "integrity": "sha512-lK+SffWIr0XsFf7E0srBjhpkdFVJf3HEgXCwzkm69kNbRar8MhezFpkIwpk0qo2IOQL4JE4mJPJI8AbRPLbuOQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.17.tgz", + "integrity": "sha512-XcSGTQcWFQS2jx3lZtQi7cQmDYLrpLRyz1Ns1DzZCtn898cWfm5Icx/DEWNcTU+T+tyPV89RQtDnI7qL2PObPg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.17.tgz", + "integrity": "sha512-RNLCDmLP5kCWAJR+ItLM3cHxzXRTe4N00TQyQiimq+lyqVqZWGPAvcyfUBM0isE79eEZhIuGN09rAz8EL5KdLA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.17.tgz", + "integrity": "sha512-PAXswI5+cQq3Pann7FNdcpSUrhrql3wKjj3gVkmuz6OHhqqYxKvi6GgRBoaHjaG22HV/ZZEgF9TlS+9ftHVigA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.17.tgz", + "integrity": "sha512-V63egsWKnx/4V0FMYkr9NXWrKTB5qFftKGKuZKFIrAkO/7EWLFnbBZNM1CvJ6Sis+XBdPws2YQSHF1Gqf1oj/Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.17.tgz", + "integrity": "sha512-YtUXLdVnd6YBSYlZODjWzH+KzbaubV0YVd6UxSfoFfa5PtNJNaW+1i+Hcmjpg2nEe0YXUCNF5bkKy1NnBv1y7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.17.tgz", + "integrity": "sha512-yczSLRbDdReCO74Yfc5tKG0izzm+lPMYyO1fFTcn0QNwnKmc3K+HdxZWLGKg4pZVte7XVgcFku7TIZNbWEJdeQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.17.tgz", + "integrity": "sha512-FNZw7H3aqhF9OyRQbDDnzUApDXfC1N6fgBhkqEO2jvYCJ+DxMTfZVqg3AX0R1khg1wHTBRD5SdcibSJ+XF6bFg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@tauri-apps/api": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.2.0.tgz", + "integrity": "sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==", + "engines": { + "node": ">= 14.6.0", + "npm": ">= 6.6.0", + "yarn": ">= 1.19.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.2.3.tgz", + "integrity": "sha512-erxtXuPhMEGJPBtnhPILD4AjuT81GZsraqpFvXAmEJZ2p8P6t7MVBifCL8LznRknznM3jn90D3M8RNBP3wcXTw==", + "dev": true, + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "1.2.3", + "@tauri-apps/cli-darwin-x64": "1.2.3", + "@tauri-apps/cli-linux-arm-gnueabihf": "1.2.3", + "@tauri-apps/cli-linux-arm64-gnu": "1.2.3", + "@tauri-apps/cli-linux-arm64-musl": "1.2.3", + "@tauri-apps/cli-linux-x64-gnu": "1.2.3", + "@tauri-apps/cli-linux-x64-musl": "1.2.3", + "@tauri-apps/cli-win32-ia32-msvc": "1.2.3", + "@tauri-apps/cli-win32-x64-msvc": "1.2.3" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-phJN3fN8FtZZwqXg08bcxfq1+X1JSDglLvRxOxB7VWPq+O5SuB8uLyssjJsu+PIhyZZnIhTGdjhzLSFhSXfLsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.2.3.tgz", + "integrity": "sha512-jFZ/y6z8z6v4yliIbXKBXA7BJgtZVMsITmEXSuD6s5+eCOpDhQxbRkr6CA+FFfr+/r96rWSDSgDenDQuSvPAKw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-C7h5vqAwXzY0kRGSU00Fj8PudiDWFCiQqqUNI1N+fhCILrzWZB9TPBwdx33ZfXKt/U4+emdIoo/N34v3TiAOmQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-buf1c8sdkuUzVDkGPQpyUdAIIdn5r0UgXU6+H5fGPq/Xzt5K69JzXaeo6fHsZEZghbV0hOK+taKV4J0m30UUMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-x88wPS9W5xAyk392vc4uNHcKBBvCp0wf4H9JFMF9OBwB7vfd59LbQCFcPSu8f0BI7bPrOsyHqspWHuFL8ojQEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-ZMz1jxEVe0B4/7NJnlPHmwmSIuwiD6ViXKs8F+OWWz2Y4jn5TGxWKFg7DLx5OwQTRvEIZxxT7lXHi5CuTNAxKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-B/az59EjJhdbZDzawEVox0LQu2ZHCZlk8rJf85AMIktIUoAZPFbwyiUv7/zjzA/sY6Nb58OSJgaPL2/IBy7E0A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.2.3.tgz", + "integrity": "sha512-ypdO1OdC5ugNJAKO2m3sb1nsd+0TSvMS9Tr5qN/ZSMvtSduaNwrcZ3D7G/iOIanrqu/Nl8t3LYlgPZGBKlw7Ng==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-CsbHQ+XhnV/2csOBBDVfH16cdK00gNyNYUW68isedmqcn8j+s0e9cQ1xXIqi+Hue3awp8g3ImYN5KPepf3UExw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/node": { + "version": "18.15.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.12.tgz", + "integrity": "sha512-Wha1UwsB3CYdqUm2PPzh/1gujGCNtWVUYF0mB00fJFoR4gTyWTDPjSm+zBF787Ahw8vSGgBja90MkgFwvB86Dg==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.1.0.tgz", + "integrity": "sha512-++9JOAFdcXI3lyer9UKUV4rfoQ3T1RN8yDqoCLar86s0xQct5yblxAE+yWgRnU5/0FOlVCpTZpYSBV/bGWrSrQ==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "1.3.0-alpha.0", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.3.0-alpha.0.tgz", + "integrity": "sha512-W3uMzecHPcbwddPu4SJpUcPakRBK/y/BP+U0U6NiPpUX1tONLC4yCawt+QBJqtgJ+sfD6ztf5PyvPL3hQRqfOA==", + "dev": true, + "dependencies": { + "@volar/source-map": "1.3.0-alpha.0" + } + }, + "node_modules/@volar/source-map": { + "version": "1.3.0-alpha.0", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.3.0-alpha.0.tgz", + "integrity": "sha512-jSdizxWFvDTvkPYZnO6ew3sBZUnS0abKCbuopkc0JrIlFbznWC/fPH3iPFIMS8/IIkRxq1Jh9VVG60SmtsdaMQ==", + "dev": true, + "dependencies": { + "muggle-string": "^0.2.2" + } + }, + "node_modules/@volar/typescript": { + "version": "1.3.0-alpha.0", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.3.0-alpha.0.tgz", + "integrity": "sha512-5UItyW2cdH2mBLu4RrECRNJRgtvvzKrSCn2y3v/D61QwIDkGx4aeil6x8RFuUL5TFtV6QvVHXnsOHxNgd+sCow==", + "dev": true, + "dependencies": { + "@volar/language-core": "1.3.0-alpha.0" + } + }, + "node_modules/@volar/vue-language-core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@volar/vue-language-core/-/vue-language-core-1.2.0.tgz", + "integrity": "sha512-w7yEiaITh2WzKe6u8ZdeLKCUz43wdmY/OqAmsB/PGDvvhTcVhCJ6f0W/RprZL1IhqH8wALoWiwEh/Wer7ZviMQ==", + "dev": true, + "dependencies": { + "@volar/language-core": "1.3.0-alpha.0", + "@volar/source-map": "1.3.0-alpha.0", + "@vue/compiler-dom": "^3.2.47", + "@vue/compiler-sfc": "^3.2.47", + "@vue/reactivity": "^3.2.47", + "@vue/shared": "^3.2.47", + "minimatch": "^6.1.6", + "muggle-string": "^0.2.2", + "vue-template-compiler": "^2.7.14" + } + }, + "node_modules/@volar/vue-typescript": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@volar/vue-typescript/-/vue-typescript-1.2.0.tgz", + "integrity": "sha512-zjmRi9y3J1EkG+pfuHp8IbHmibihrKK485cfzsHjiuvJMGrpkWvlO5WVEk8oslMxxeGC5XwBFE9AOlvh378EPA==", + "dev": true, + "dependencies": { + "@volar/typescript": "1.3.0-alpha.0", + "@volar/vue-language-core": "1.2.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.47.tgz", + "integrity": "sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==", + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/shared": "3.2.47", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz", + "integrity": "sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==", + "dependencies": { + "@vue/compiler-core": "3.2.47", + "@vue/shared": "3.2.47" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz", + "integrity": "sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==", + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.47", + "@vue/compiler-dom": "3.2.47", + "@vue/compiler-ssr": "3.2.47", + "@vue/reactivity-transform": "3.2.47", + "@vue/shared": "3.2.47", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7", + "postcss": "^8.1.10", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz", + "integrity": "sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==", + "dependencies": { + "@vue/compiler-dom": "3.2.47", + "@vue/shared": "3.2.47" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.47.tgz", + "integrity": "sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==", + "dependencies": { + "@vue/shared": "3.2.47" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz", + "integrity": "sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==", + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.47", + "@vue/shared": "3.2.47", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.47.tgz", + "integrity": "sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==", + "dependencies": { + "@vue/reactivity": "3.2.47", + "@vue/shared": "3.2.47" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz", + "integrity": "sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==", + "dependencies": { + "@vue/runtime-core": "3.2.47", + "@vue/shared": "3.2.47", + "csstype": "^2.6.8" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.47.tgz", + "integrity": "sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==", + "dependencies": { + "@vue/compiler-ssr": "3.2.47", + "@vue/shared": "3.2.47" + }, + "peerDependencies": { + "vue": "3.2.47" + } + }, + "node_modules/@vue/shared": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz", + "integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.17.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.17.tgz", + "integrity": "sha512-/jUywtAymR8jR4qsa2RujlAF7Krpt5VWi72Q2yuLD4e/hvtNcFQ0I1j8m/bxq238pf3/0KO5yuXNpuLx8BE1KA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.17", + "@esbuild/android-arm64": "0.17.17", + "@esbuild/android-x64": "0.17.17", + "@esbuild/darwin-arm64": "0.17.17", + "@esbuild/darwin-x64": "0.17.17", + "@esbuild/freebsd-arm64": "0.17.17", + "@esbuild/freebsd-x64": "0.17.17", + "@esbuild/linux-arm": "0.17.17", + "@esbuild/linux-arm64": "0.17.17", + "@esbuild/linux-ia32": "0.17.17", + "@esbuild/linux-loong64": "0.17.17", + "@esbuild/linux-mips64el": "0.17.17", + "@esbuild/linux-ppc64": "0.17.17", + "@esbuild/linux-riscv64": "0.17.17", + "@esbuild/linux-s390x": "0.17.17", + "@esbuild/linux-x64": "0.17.17", + "@esbuild/netbsd-x64": "0.17.17", + "@esbuild/openbsd-x64": "0.17.17", + "@esbuild/sunos-x64": "0.17.17", + "@esbuild/win32-arm64": "0.17.17", + "@esbuild/win32-ia32": "0.17.17", + "@esbuild/win32-x64": "0.17.17" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/minimatch": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", + "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/muggle-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.2.2.tgz", + "integrity": "sha512-YVE1mIJ4VpUMqZObFndk9CJu6DBJR/GB13p3tXuNbwD4XExaI5EOuRl6BHeIDxIqXZVxSfAC+y6U1Z/IxCfKUg==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/postcss": { + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "3.20.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.6.tgz", + "integrity": "sha512-2yEB3nQXp/tBQDN0hJScJQheXdvU2wFhh6ld7K/aiZ1vYcak6N/BKjY1QrU6BvO2JWYS8bEs14FRaxXosxy2zw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-JTGFgDh3dVxeGBpuQX04Up+JZmuG6wu9414Ei36vQzaEruY/M4K0AgwtuB2b4HaBgB7R8l+LHxjB0jcgz4d2qQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.21", + "rollup": "^3.20.2" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.47.tgz", + "integrity": "sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==", + "dependencies": { + "@vue/compiler-dom": "3.2.47", + "@vue/compiler-sfc": "3.2.47", + "@vue/runtime-dom": "3.2.47", + "@vue/server-renderer": "3.2.47", + "@vue/shared": "3.2.47" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", + "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.2.0.tgz", + "integrity": "sha512-rIlzqdrhyPYyLG9zxsVRa+JEseeS9s8F2BbVVVWRRsTZvJO2BbhLEb2HW3MY+DFma0378tnIqs+vfTzbcQtRFw==", + "dev": true, + "dependencies": { + "@volar/vue-language-core": "1.2.0", + "@volar/vue-typescript": "1.2.0" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + } + } +} diff --git a/keysas-usbfilter/tray-app/package.json b/keysas-usbfilter/tray-app/package.json new file mode 100644 index 0000000..fec4e4a --- /dev/null +++ b/keysas-usbfilter/tray-app/package.json @@ -0,0 +1,24 @@ +{ + "name": "keysas-minifilter", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "vue": "^3.2.45", + "@tauri-apps/api": "^1.2.0" + }, + "devDependencies": { + "@types/node": "^18.7.10", + "@vitejs/plugin-vue": "^4.0.0", + "typescript": "^4.9.5", + "vite": "^4.2.1", + "vue-tsc": "^1.0.11", + "@tauri-apps/cli": "^1.2.3" + } +} diff --git a/keysas-usbfilter/tray-app/public/tauri.svg b/keysas-usbfilter/tray-app/public/tauri.svg new file mode 100644 index 0000000..31b62c9 --- /dev/null +++ b/keysas-usbfilter/tray-app/public/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/keysas-usbfilter/tray-app/public/vite.svg b/keysas-usbfilter/tray-app/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/keysas-usbfilter/tray-app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/keysas-usbfilter/tray-app/src-tauri/.gitignore b/keysas-usbfilter/tray-app/src-tauri/.gitignore new file mode 100644 index 0000000..f4dfb82 --- /dev/null +++ b/keysas-usbfilter/tray-app/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + diff --git a/keysas-usbfilter/tray-app/src-tauri/Cargo.toml b/keysas-usbfilter/tray-app/src-tauri/Cargo.toml new file mode 100644 index 0000000..70d636d --- /dev/null +++ b/keysas-usbfilter/tray-app/src-tauri/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "keysas-usbfilter-trayapp" +version = "0.0.0" +description = "Keysas USB firewall tray application" +authors = ["Luc Bonnafoux", "Stephane N"] +license = "GPL-3.0" +repository = "" +default-run = "keysas-usbfilter-trayapp" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tauri-build = { version = "1.2", features = [] } + +[dependencies] +tauri = { version = "1.2", features = ["shell-open"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +anyhow = "1.0" +simple_logger = "4.1" +log = "0.4" + +[features] +# by default Tauri runs in production mode +# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL +default = [ "custom-protocol" ] +# this feature is used for production builds or when `devPath` points to the filesystem +# DO NOT REMOVE!! +custom-protocol = ["tauri/custom-protocol"] diff --git a/keysas-usbfilter/tray-app/src-tauri/build.rs b/keysas-usbfilter/tray-app/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/keysas-usbfilter/tray-app/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/128x128.png b/keysas-usbfilter/tray-app/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..6be5e50e9b9ae84d9e2ee433f32ef446495eaf3b GIT binary patch literal 3512 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 literal 0 HcmV?d00001 diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/128x128@2x.png b/keysas-usbfilter/tray-app/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e81becee571e96f76aa5667f9324c05e5e7a4479 GIT binary patch literal 7012 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 Sh@|Ye~|0>}wMPLT8jm@Y!H33Sz}5aFI6 zM9Lzqz|;A*0sGs=2A1uU!1nk2dGF7knQwr99SAFen)x(eCO;F8y2C~0FD1YxRTPcy zPWVxkUYmeuz}Tv?7&Fe-!UE{)ZW)Mb;H)^#eHDv$`dkZGguJz@^MA!ZNGAUqt{|0H zpZ7Ch9S`q5!>R%}>}62!+(T^evyO+ImSo2wpu)su4^3nw5(%)KD%gbSev^*HZZ&3( z#&c@Z0gH|}Ck)w6fh0&NBJ62ib%R}(3@$VFl*_#l2W$wQ-~4RmZZAt5O*^2Q5}Xr8Hy@c`#pM?kc?hFWxRXr*mUfUCXf4ka5DD~ zat6d85COB05l#(P9*cQZ3EC8fVdS~?&vN#rce(aF9@xp80O2{{FBvU+{X>Hoh;xI` z{$e^Nw1y*VbO8wv`8|-m?NwNaKGTGaF{P^JLB^DbOYWIbn%eT`*!^C1H36=O8Z-M> zkD~88ry`eSo`tEBN4>w7OWZwUzlh{WM1m8R6zepqGcGMaV7vWY9b?K4b6~|HVG)ec wi>I@ws#sZo7or4_*4M>7;p5{nr2pZ?Uu4>Krr0kU)&Kwi07*qoM6N<$f)&@lf&c&j literal 0 HcmV?d00001 diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/Square107x107Logo.png b/keysas-usbfilter/tray-app/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0ca4f27198838968bd60ed7d371bfa23496b7fe5 GIT binary patch literal 2863 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 Y2S8W#vM)6=T>t<807*qoM6N<$f*y@n<^TWy literal 0 HcmV?d00001 diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/Square284x284Logo.png b/keysas-usbfilter/tray-app/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c021d2ba76619c08969ab688db3b27f29257aa6f GIT binary patch literal 7737 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 literal 0 HcmV?d00001 diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/Square30x30Logo.png b/keysas-usbfilter/tray-app/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..621970023096ed9f494ba18ace15421a45cd65fa GIT binary patch literal 903 zcmV;219<$2P)2 z+CUKPMqaqGiH;zb!R4$B-WXS^YzQr=@UH>k4?*L)&R=zYjBrZenKdc9|JlS$SO*RJ zKt8FSTDAdk1g_WPAO!p^V!AuL;Lm;uQyV;zKq)J3i(;q*;k+pD%f3eltU`PYdy9(k0&%` zuWAPcV6|-y?|?7O1W!KSK}pbk8#~!|FA@(VJkt^V@0lio{afoAeo*f&$W2s6${5!1eKvAGD2$GZwSB98L2ZVS- zKn8ENRkZ*sb!@QugOrQNK3(sy1v%J#m|rpB+h|Nkqa3FRT>74xSs{#&saU2Lf!_Iq zKmuKAESh`gs!fneGWn+nf}l?7jE$HW!Af&vE5=G!QU)U2v&HLIBGXKk4nQx{hsHjL zLPMAo5=*uInFbq7(aa`Y2VX5wCmaeqvECOFv)a>0t>ZaEb*cJccER=BB?KFZhV$c^ znL*l8x*UYZv4WK|j?~Jt6~~F%{pk~z5A*>^M`?r5m9@RJ_x|uEtX(6Vk@Y()MVto* z93wr)%3m%|#OZ~srm>zF(JvDuTq*@;d&^>_BJm5hOU`3FjG70L#Vzv9I?`<7$T@

    jU?lMi@tgxr7CqX_r3uw^y4tVU3Pm0sw;|1WSUO%?=bG`*Kmz6u4{#ti;T7AWIBAEh!(Y zz>O01&#X?Ds@L)Sb{CkG#Yz4$3o d@96)?#cz^xWoA}>B$xmI002ovPDHLkV1l3&k#zt7 literal 0 HcmV?d00001 diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/Square310x310Logo.png b/keysas-usbfilter/tray-app/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f9bc04839491e66c07b16ab03743c0c53b4109cc GIT binary patch literal 8591 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 JPDHLkV1lm|RXG3v literal 0 HcmV?d00001 diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/Square71x71Logo.png b/keysas-usbfilter/tray-app/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..63440d7984936a9caa89275928d8dce97e4d033b GIT binary patch literal 2011 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∓t$dRJGIadE?vc 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 Kx8Vjl{{H|h@<;Lj literal 0 HcmV?d00001 diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/icon.ico b/keysas-usbfilter/tray-app/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3636e4b22ba65db9061cd60a77b02c92022dfd6 GIT binary patch literal 86642 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 AKmY&$ literal 0 HcmV?d00001 diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/icon.png b/keysas-usbfilter/tray-app/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e1cd2619e0b5ec089cbba5ec7b03ddf2b1dfceb6 GIT binary patch literal 14183 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

    5l#v3A#xI3*z~1~fvo9Q(N^(==!|_FZ z*duZ=+M1~)8E|otX8KNZlr?qels#x_1Xq@9IIw~@9uAREJVH)Xw^}UclF6327}E42 zT)E&?U%TK?(+K7%R!`H5oX0i)4Qn5??Iw3p5J~6_u+aWehY{DSn}3V2p$bgjnAu?o)v@iC254fXeMv50$9YrpU`N?u@QIWs)T?SP|fa}(|9 zqAX+!7`cx=4)cCBg5h~pu(?@9`)aCr#oyz$ld=#RFxYCNZCZls@4v2~*e-t6PEVvV z&bbK3b3wt(Coc!ufAbXXC<**#HQ%J9k`New6iG<5RjtO4XVO?dCvwxD{kJ#tfQr(X zg^NTwF-FwAeS_{V4bfel8l`~NbfrTR2s!G>WduFWxH(t~aK4q=6rEE^$+Uox>gJO2 z{L<;6Q6nHa5#ZEM>H58not!)z(6*_=^~8}jWf*IG$AUKVWOZ4?)GfF z+BM#*wKKmLFD7E~W3U!$IVm$k_k1f&Kz6WV8@55P?r~bcg-Za-!rvW?ns&)KOGT2~ zlkAyqhQj=P$Eg3w#K~}zH@J5bo-BfHjInKSz$@?+Z)NPD4pHj^_Qxmi`UqoTy=`sV zLVxrXGuBr=QRm|}wg75yetQQK4fY3#P_~J}zEfPnb2C4Wo!E(d*(cA;b?7$g2in<( zPn)ghX}nzJPmb6(3Dpeg_GW~Hc}Lt=lgsSZz z!5QXyz7KaR;D`3Ee}d`af{H>WWZ|Io1QI3~4Ll_`g1(cRnhLK73Ro)7zPCd={1W2x zRp%Xlvv4>!<2@}$hz|!V{T}_eHx2xkLl^hQoZTCnsjCl|W_@5Fx2(+j0ogy&Y+;L- z<)G$*CiN7hOm^s!{U>1F7U=iNk{+u~dAC!eDz%=|glFW0jEZU1&o(G_c#wTxUjnG} z#cg3>jEpUi#Mlq@t?Msg_#geK^Lx@DyHWf7=AS5vVyM7YOjvUVCfcpVR<(+5!H?9- zySI6s>o3m&*zr||=wcPGyBkQV`EWJl@bH8qobjOp+sXL*)=&yX)8aAbf~tGv?a2SN zu^Ddo-z?DWk9h9Yz#5p^NU#x~wYSd?H@w@!2Gb4G)6-utEMV~~M85Br5ff(v5O1|T z zIR`9v=XXbK8N1BZV|h34+~1u1oJ_h>7aS*^LOi zS?hm+ec#1L<6bZ!Oc9OG-gV_V$j{5(O1RZD9`g%{h;v>0d zWiz)=`n67_-$k!Qp(dKW6m@Xi_CesKg~LL=e5V3#YN>;l#X) zHz6W=*ucpXy35@nx1)e|M-IcA>?RmWa)fP$3;*?-yraubd*HgRmAxty2ChoMmOJ(z zJKCPRl#%}U=5It0RrpPM-!VH}hd=~)Dgrd$Xa{xl7m@&qyV;7{bKiJt1}0(zWG;nM z*1KXcyD)ss@$q)hg31UNhb@0?Nl9`#klSY~0mVw;&b=%QK~s8IFXc!F5p^a~%zWmV zZJtPB8R=a#DYTy5Z)F|d(vv8Le0cDUfp(A=+8=zftD?-zNk522{i7(|otj9m+yuVX+hY6rRUn6cGGIp1ZdbJid*Uj}>|6O+%M$p(Q32+w2=sfwN14nBnms&GWQT;bYy>aG9 zPr6Cd#uA1P#}T@__%bE|_zq$$Uq0D;)oI(51NepuZw_VsS}Wm3fO?65Ghs-L5Y7GJ zLIb!-G_V};j1QOoJGZuU!{_^uLL^q?67ac`_1g7Ci)<1m$~^foc2@Oz_+n^`6C*Q) z4T02iPh}_YT5x8sN4uk?9(*=IfB@7nLJx4m+z4*1%olhnL{b0QQ?J_k&g=uRR#T@ck<>fO@F?_=pHVa@D;b*RSyCu;(cPAe?GFc~o>pnJbs_ zl1l-I8t{|mTecYcs@j1uvW09EKFp82PJS04Fs+8ys-MS8Kj%a0`K9hOFsr?0KT05_ z-qPfC|ADFn6bo)#`5S)^%6XKt9>$%BPRiU2ACnI78LtlM!3Y|@WCuRmwTvdeR}e|O zoQ_8f>>i3%vce(s;hDMjqMi|dq)o^x#NC#}_V3i1xARk!cH>NLtnx*VG91+hRXb2i z(8Rh(carI}sY2CavhN=3-`7;QH(11wQh zP;d43IbKw1Bs8TPtY$TgJe$}bJ6dRQH}XAxtwrzArUe%5#s*>t*c4ri%riv3((Aa}(}jAR@Z4(p z-St<0$zye=znm-re+QT%YgT0lPQW`C`>bnml$OKpIUb_K)Ln?HtlN7&D? zce9gBWPlhOdWJU%Z$Rp)g}T_;Q-S+@A>VbkYDi-}Xb&x8WhB@;QZD`|oq&vvW6`i`65b&(uy+Zt<<-oGX}plTUIr!V9THGPYbgYYYZ zj~5jMhZ@h}sNarolPDj80vQqXKK3UV90%jX`t-X^Z2HIP%yZi7SW7I*uG-UA1 zVuRN1Z-#@F^j8(GI^$^4?DPv4;ZtL1WdyjrQq$d>ItF4s&Rdc;l6asHjkJ2YfANQ0tp93~R_WJ6W;!Fw6 z`_&T%lm@4jAACAX+oQ?1G)|xS;NylhQw_dgg=$xgY#$BUy?y&%#DFTBJ}oo*y`*WW zh0BBTF|O=ILcEXiIx*WvX?<#QHH=ot+7rnLLWDsQ6n9`7(>}SUD$c_hy|u87|2ehz z!$4Gq)@1SaVZOOIr){?PUr#i=QZXpTP4SE^_HdZ615YT-Mxq zaU=o9m|f2%zQ!`{{bY$e6hmX3)`!B|4Epd^b@RK%3s?=p?RQz&wO;j-(5P1kck$wd zSJ&DfjKN$?vegNGkE)ftChzIhc-&J&UP~)iQS{5IgFrWb(-TpP389q}c`g5_UKr}* zTV`e40XXe8`o2v{SM^gaF{tN~vs1oYEH0ZIG<2|4fWlpe;{Q7v2eV4MT?@pAC#FQ} z1#v^nMVh9F(f8xk1twtl9n%~9=PhY~kse$*zeza6>Y~mucCA-aK#_m8kW$;ho}k)d zef)!x)+xig;L+^Zn@-hLjJ|=MGQgJO48Zh|BVx3qjQpD~&keYzu08*c`6L77$Odq^)ySMSKo~EG>7qO4) zGQ)1PUpjB%VxfNDiDf4Ro1o$&^7Z)mNLab|_7)vaPv5!^CHt3vXwv#|+`R07+H52% zKo%nK#80s-o)YZj?*ITk+}k^g+myi0bp#KfHwslIGiuDjs~yxHx&gptDVWHG=70&V zJ8Io-FR9z~W&kLF(n_>c?3f)cYo6``BMI)wm3jZFbPN8=?HR1B%7>HqNtp?ns~LRX z9I^(_-#Wqs4rYIAzyB*x_rTr;$D0IjmOVaIb*f!eRcm`A$QFiU*E+iYVy(ww*D#+G z4HPQp`u-fa`BDzB*4ZfjHvM8IMi!3!Rv9Ifk3a)bnSGPt_|HayKxwKr8EiZp4ENUM z53~}@bJhH>Z+4qaz_de#z`Nk~-Xj#@`R5upr+J$E_E78H>WPHkEn!|F-Wx92_)~gF z2)F3pQ^!@nTj?i4U^t|f_WD0c>fxtBtXMyIl3x(VyD-sm2;X&fx~*6;rc?rV_gch` zyN$kU`>}KvO#R2AS=Jr7_3Ipox2Z@^{e^GbkT-DuOD$?@^P~b?+CL`B%(rGrZX(XK zB;huyA)r%y72y_VVMa0v_3;!uONHw zoRni;$j1Ra@!^urL#n@$>-xC*WIGo_R5kih{`Gxs4?X65^Z|d%#zxiVbe&$7!wqpB z&Gqq9c!_(*Qp%}ybz$e$eNfD%25@W1%^-Lv!No&Q7eO-*_+I+nyzFbkExed7(pohd zFcaui&L7DXAzjue3 zAncEwaY=bSyTKAntX{Y``Td(kG^niT%yilzTza@SJ?iu5#t=xpcNrHq;5&!j8s6Oy zetM@f_AI0nlI6oafRq+dpX=eD9JgvAw&63Y9DJu}eMQtm%uMgk3K#)+7{ZlVy3fxP zBR(sz&2{V9I!pzKO(qAsz>_xVOOyl^XwC?y4S(8G3sSSj#eFOS0}q)SBw@cO2`27r ze(`We&e5WW?y7A~hhHz4;n*9u=1}rRDJ6V7K~!v*_peughtWU0tpa}h8`F4r1z?lD zN3U_T4#UQb{975_<1b`0`)vi|=5-7rGUbFJ>TCOS;$2XR!cZ|m1HXl4PvaWzU#)Av zV^0!NYg2Yd5~CSM9#DJGNkF{Ab335tD*S3or#<1O%fW*o?Xu^@CP<*c{YpDF|k?t^m$uBbp4Lwi@Baxp9=Mc*(~xK6`g z=hKP^8aedgD#a7mFY}l#Mq+QAZERu0OuxWZS1ULRxwAufv^C?3d%-W=%KJC3-uH}o z1oZPfArJj~@24Pyk@?>uWUms4%sf^D0npR@uxOruAu#d#f3rWINyCbv1WuszHEAz& z=?qL;EJ^}GJt`ml*Cb64NCM3D_Z;&ll82@1V*Vfr;x~{CbpuZ_w~aAeS^5l>0R?!d zOUu`UqI4T!6aN@F4>pDmc_^2GLMq=H1kArrC$v-S;Ly(W+)6v}=fJXt#Kw?r z<4BNZ)kbJ5nvgPW^BF=39{nSI5a0dBXlGZnU!2@8@uC@|B?9ISkRZ)P@>eoY*k`i{ zpIdaL3~cVlGz+YqmT|aE=C-@QkuSOE`e&o-2a`_m#D7^@wTL-hCp^eggtg@r#Kl1# zw4tC;ko=KFA>wgkGS=z*cj@L-#$`K*B|(33f}w1JKLmw^yYL(j>aO0cuko3}1W8{o zrx%w0qh*SnV6qR)#I-k`UGfwvg=!lp*Y)<$?(s5G;XptR`oXMthRorcd&W&C2| z!^L@skGCA-~}Ka^T8SSo0nynP|RU!FKm;e3uRh%sH=JP2(kzg*8>fg z*#_C9z>d<_M#%~*0rduNj`qqMZAAIrbkJN$h+hkbG|IT8OK{Ug*BfV7`67$&?LOS3 zhT3Rfp==4iG-;np#jrT<8R%UC;K~puSgdfHC=_ot5?)jrFH>g5KAHEmwtQHkiiyN6B2g)XX%#m5#`fPyR!RI z5M2-E&!BSvrD+Em(}f*VFd%7AUmA0^Xux{c6R@kes6AJzJ& z$cFLCdjgU*hhG=2ehpu4QV4{1_1}3xN*GT943{@|4Thv)b7D;}$=^aWh^Br?N?865 ze}23(;yHT?oU)V+g#unK^kTnu+&VG#yu?!i1ZS zX#zTt$Y09M-=Rc6Iuhe|Ob~eU*%@fPZN~VrOx>t^1`Q%}NUp)J0DC-ery?iN=fNtg zq7es_@hL>?<+(aOv@b@GpD7&pcXKau3j!2~_)QD3BkTSIY|}(3XJQ?06)6p4G;-;}Y@)~&+B4D(Q#kj~nC@K=65{rb~5fQ?27_$O{UA`h=+ zk-SJ^m5V?CHa5hGtTxIb(OyI-KI(h=_sPXWD{u)Jfy&f{MB0%pYWZKL>oHzz7diuV z|7}09KDCW$bxeIded}%F(v~XTCr-r)5uOjh(AFjgg#6KCwXCfpXOq1yFS3^Z6P|1A z<+TjRjM)9!)l+*g$=V9-@u+q_sGjk)=&553xTvh7zFfhz|Ai$yQkNtPN!M4%ED^8g zosuJv=Y%Lz8R20ju_!X6`D String { + format!("Hello, {}! You've been greeted from Rust!", name) +} + +fn main() -> Result<(), anyhow::Error> { + // Launch the tauri application + init_tauri()?; + + Ok(()) +} + +// Initialize the tauri application as a system tray app +fn init_tauri() -> Result<(), anyhow::Error> { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![greet]) + .run(tauri::generate_context!())?; + Ok(()) +} \ No newline at end of file diff --git a/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json b/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json new file mode 100644 index 0000000..752fe6d --- /dev/null +++ b/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json @@ -0,0 +1,49 @@ +{ + "build": { + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build", + "devPath": "http://localhost:1420", + "distDir": "../dist", + "withGlobalTauri": false + }, + "package": { + "productName": "keysas-minifilter", + "version": "0.0.0" + }, + "tauri": { + "allowlist": { + "all": false, + "shell": { + "all": false, + "open": true + } + }, + "bundle": { + "active": true, + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "identifier": "com.tauri.dev", + "targets": "all" + }, + "security": { + "csp": null + }, + "updater": { + "active": false + }, + "windows": [ + { + "fullscreen": false, + "resizable": true, + "title": "keysas-minifilter", + "width": 800, + "height": 600 + } + ] + } +} diff --git a/keysas-usbfilter/tray-app/src/App.vue b/keysas-usbfilter/tray-app/src/App.vue new file mode 100644 index 0000000..aa87d0a --- /dev/null +++ b/keysas-usbfilter/tray-app/src/App.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/keysas-usbfilter/tray-app/src/assets/vue.svg b/keysas-usbfilter/tray-app/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/keysas-usbfilter/tray-app/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/keysas-usbfilter/tray-app/src/components/Greet.vue b/keysas-usbfilter/tray-app/src/components/Greet.vue new file mode 100644 index 0000000..289373b --- /dev/null +++ b/keysas-usbfilter/tray-app/src/components/Greet.vue @@ -0,0 +1,21 @@ + + + diff --git a/keysas-usbfilter/tray-app/src/main.ts b/keysas-usbfilter/tray-app/src/main.ts new file mode 100644 index 0000000..5eea099 --- /dev/null +++ b/keysas-usbfilter/tray-app/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from "vue"; +import "./styles.css"; +import App from "./App.vue"; + +createApp(App).mount("#app"); diff --git a/keysas-usbfilter/tray-app/src/styles.css b/keysas-usbfilter/tray-app/src/styles.css new file mode 100644 index 0000000..f7de85b --- /dev/null +++ b/keysas-usbfilter/tray-app/src/styles.css @@ -0,0 +1,109 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color: #0f0f0f; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +.container { + margin: 0; + padding-top: 10vh; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: 0.75s; +} + +.logo.tauri:hover { + filter: drop-shadow(0 0 2em #24c8db); +} + +.row { + display: flex; + justify-content: center; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +h1 { + text-align: center; +} + +input, +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + color: #0f0f0f; + background-color: #ffffff; + transition: border-color 0.25s; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); +} + +button { + cursor: pointer; +} + +button:hover { + border-color: #396cd8; +} +button:active { + border-color: #396cd8; + background-color: #e8e8e8; +} + +input, +button { + outline: none; +} + +#greet-input { + margin-right: 5px; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6f6f6; + background-color: #2f2f2f; + } + + a:hover { + color: #24c8db; + } + + input, + button { + color: #ffffff; + background-color: #0f0f0f98; + } + button:active { + background-color: #0f0f0f69; + } +} diff --git a/keysas-usbfilter/tray-app/src/vite-env.d.ts b/keysas-usbfilter/tray-app/src/vite-env.d.ts new file mode 100644 index 0000000..fc81239 --- /dev/null +++ b/keysas-usbfilter/tray-app/src/vite-env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/keysas-usbfilter/tray-app/tsconfig.json b/keysas-usbfilter/tray-app/tsconfig.json new file mode 100644 index 0000000..d4aefa2 --- /dev/null +++ b/keysas-usbfilter/tray-app/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM"], + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/keysas-usbfilter/tray-app/tsconfig.node.json b/keysas-usbfilter/tray-app/tsconfig.node.json new file mode 100644 index 0000000..440c4d8 --- /dev/null +++ b/keysas-usbfilter/tray-app/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["tray-app/vite.config.ts"] +} diff --git a/keysas-usbfilter/tray-app/vite.config.ts b/keysas-usbfilter/tray-app/vite.config.ts new file mode 100644 index 0000000..a21643d --- /dev/null +++ b/keysas-usbfilter/tray-app/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +// https://vitejs.dev/config/ +export default defineConfig(async () => ({ + plugins: [vue()], + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // prevent vite from obscuring rust errors + clearScreen: false, + // tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + }, + // to make use of `TAURI_DEBUG` and other env variables + // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand + envPrefix: ["VITE_", "TAURI_"], + build: { + // Tauri supports es2021 + target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13", + // don't minify for debug builds + minify: !process.env.TAURI_DEBUG ? "esbuild" : false, + // produce sourcemaps for debug builds + sourcemap: !!process.env.TAURI_DEBUG, + }, +})); diff --git a/keysas_lib/Cargo.toml b/keysas_lib/Cargo.toml index 29a47fd..98793ae 100644 --- a/keysas_lib/Cargo.toml +++ b/keysas_lib/Cargo.toml @@ -19,8 +19,11 @@ arbitrary = "1.3" ed25519-dalek = "1" rand_dl = {package = "rand", version = "0.7"} rand_core = "0.6.4" -oqs = { version = "0.7", default-features = false, features = ["std", "dilithium"] } hex-literal = "0.4" tempfile = "3.4" der = "0.7" -serde = "1.0" + +[dependencies.oqs] +version = "0.7" +default-features = false +features = ["dilithium"] From 8f88a2aa23364842ff39c8fcff00af5682d76a00 Mon Sep 17 00:00:00 2001 From: Luc Date: Tue, 25 Apr 2023 10:55:31 +0200 Subject: [PATCH 051/160] Improved context management --- .../daemon/src/windows_driver_interface.rs | 2 + .../minifilter/keysasCommunication.c | 5 + keysas-usbfilter/minifilter/keysasDriver.c | 17 +- keysas-usbfilter/minifilter/keysasDriver.h | 1 + keysas-usbfilter/minifilter/keysasFile.c | 981 ++++++++++-------- keysas-usbfilter/minifilter/keysasFile.h | 7 + keysas-usbfilter/minifilter/keysasInstance.c | 173 ++- keysas-usbfilter/minifilter/keysasInstance.h | 7 + 8 files changed, 697 insertions(+), 496 deletions(-) diff --git a/keysas-usbfilter/daemon/src/windows_driver_interface.rs b/keysas-usbfilter/daemon/src/windows_driver_interface.rs index d1e49f7..0fee273 100644 --- a/keysas-usbfilter/daemon/src/windows_driver_interface.rs +++ b/keysas-usbfilter/daemon/src/windows_driver_interface.rs @@ -125,6 +125,8 @@ impl WindowsDriverInterface { ) .unwrap(); } + + println!("Sent response"); } }); Ok(()) diff --git a/keysas-usbfilter/minifilter/keysasCommunication.c b/keysas-usbfilter/minifilter/keysasCommunication.c index 3b919d5..f65f763 100644 --- a/keysas-usbfilter/minifilter/keysasCommunication.c +++ b/keysas-usbfilter/minifilter/keysasCommunication.c @@ -30,6 +30,11 @@ Module Name: // Name of the port used to communicate with user space const PWSTR KeysasPortName = L"\\KeysasPort"; +#ifdef ALLOC_PRAGMA +#pragma alloc_text(PAGE, KeysasPortConnect) +#pragma alloc_text(PAGE, KeysasPortDisconnect) +#endif + NTSTATUS KeysasInitPort( diff --git a/keysas-usbfilter/minifilter/keysasDriver.c b/keysas-usbfilter/minifilter/keysasDriver.c index b38c1b7..203f117 100644 --- a/keysas-usbfilter/minifilter/keysasDriver.c +++ b/keysas-usbfilter/minifilter/keysasDriver.c @@ -63,12 +63,6 @@ KfUnload( _In_ FLT_FILTER_UNLOAD_FLAGS Flags ); -VOID -KfContextCleanup( - _In_ PFLT_CONTEXT Context, - _In_ FLT_CONTEXT_TYPE ContextType -); - // // Assign text sections for each routine. // @@ -76,11 +70,6 @@ KfContextCleanup( #ifdef ALLOC_PRAGMA #pragma alloc_text(INIT, DriverEntry) #pragma alloc_text(PAGE, KfUnload) -#pragma alloc_text(PAGE, KfContextCleanup) -#pragma alloc_text(PAGE, KfInstanceQueryTeardown) -#pragma alloc_text(PAGE, KfInstanceSetup) -#pragma alloc_text(PAGE, KfInstanceTeardownStart) -#pragma alloc_text(PAGE, KfInstanceTeardownComplete) #endif // @@ -237,10 +226,14 @@ Return Value: KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfUnload: Entered\n")); FltCloseCommunicationPort(KeysasData.ServerPort); - KeysasData.ServerPort = NULL; + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfUnload: Closed server port\n")); FltUnregisterFilter(KeysasData.Filter); KeysasData.Filter = NULL; + KeysasData.ServerPort = NULL; + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfUnload: Done\n")); return STATUS_SUCCESS; } \ No newline at end of file diff --git a/keysas-usbfilter/minifilter/keysasDriver.h b/keysas-usbfilter/minifilter/keysasDriver.h index 9a7332c..b81f76e 100644 --- a/keysas-usbfilter/minifilter/keysasDriver.h +++ b/keysas-usbfilter/minifilter/keysasDriver.h @@ -54,6 +54,7 @@ typedef enum _KEYSAS_AUTHORIZATION { AUTH_PENDING, // Authorization request pending AUTH_BLOCK, // Access is blocked AUTH_ALLOW_READ, // Access is allowed in read mode + AUTH_ALLOW_WARNING, // Access is allowed but with a warning to the user AUTH_ALLOW_ALL // Access is allowed for all operations } KEYSAS_AUTHORIZATION; diff --git a/keysas-usbfilter/minifilter/keysasFile.c b/keysas-usbfilter/minifilter/keysasFile.c index 97a2118..b774e98 100644 --- a/keysas-usbfilter/minifilter/keysasFile.c +++ b/keysas-usbfilter/minifilter/keysasFile.c @@ -1,459 +1,524 @@ -/*++ - -Copyright (c) 2023 Luc Bonnafoux - -Module Name: - - keysasFile.c - -Abstract: - - Contains the callback to handle transactions on file access - -Environment: - - Kernel mode - ---*/ - -#include "keysasFile.h" - -#include -#include -#include -#include -#include -#include - -#include "keysasDriver.h" -#include "keysasCommunication.h" -#include "keysasUtils.h" - -VOID -KfFileContextCleanup( - _In_ PFLT_CONTEXT Context, - _In_ FLT_CONTEXT_TYPE ContextType -) -/*++ -Routine Description: - This routine is called to cleanup the ressource allocated with the context -Arguments: - Context - Pointer to the context - ContextType - Type of context received -Return Value: ---*/ -{ - PKEYSAS_FILE_CTX fileContext; - - PAGED_CODE(); - - switch (ContextType) { - case FLT_FILE_CONTEXT: - fileContext = (PKEYSAS_FILE_CTX)Context; - if (fileContext->Resource != NULL) { - ExDeleteResourceLite(fileContext->Resource); - ExFreePoolWithTag(fileContext->Resource, KEYSAS_MEMORY_TAG); - } - fileContext->Authorization = AUTH_UNKNOWN; - break; - default: - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfContextCleanup: Unsupport context type\n")); - break; - } -} - -NTSTATUS -FindFileContext( - _In_ PFLT_CALLBACK_DATA Data, - _Outptr_ PKEYSAS_FILE_CTX *FileContext, - _Out_opt_ PBOOLEAN ContextCreated -) -/*++ - -Routine Description: - - Find an existing file context or create one if there is none - -Arguments: - - Data - Pointer to the filter callbackData that is passed to us. - - FileContext - Pointer to the File context - - ContextCreated - Set to TRUE if the context has been created during the call - -Return Value: - - The return value is the status of the operation. - ---*/ -{ - NTSTATUS status = STATUS_SUCCESS; - PKEYSAS_FILE_CTX fileContext = NULL; - PKEYSAS_FILE_CTX oldFileContext = NULL; - - PAGED_CODE(); - - // Initialize output paramters - *FileContext = NULL; - if (NULL != ContextCreated) { - *ContextCreated = FALSE; - } - else { - // ContextCreated must point to valid memory - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindFileContext: Invalid input\n")); - return STATUS_UNSUCCESSFUL; - } - - // Try to find an existing file context - status = FltGetFileContext( - Data->Iopb->TargetInstance, - Data->Iopb->TargetFileObject, - &fileContext - ); - - // If the call fail because the context does not exist, create a new one - if (!NT_SUCCESS(status) - && (STATUS_NOT_FOUND == status)) { - - status = FltAllocateContext( - KeysasData.Filter, - FLT_FILE_CONTEXT, - KEYSAS_FILE_CTX_SIZE, - PagedPool, - &fileContext - ); - if (!NT_SUCCESS(status)) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindFileContext: FltAllocateContext failed with status: 0x%x\n", - status)); - return status; - } - - // Initialize the context - // Set all the fields to 0 => Authorization = UNKNOWN - RtlZeroMemory(fileContext, KEYSAS_FILE_CTX_SIZE); - fileContext->Resource = ExAllocatePoolZero( - NonPagedPool, - sizeof(ERESOURCE), - KEYSAS_MEMORY_TAG - ); - if (NULL == fileContext->Resource) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindFileContext: ExAllocatePoolZero failed with status: 0x%x\n", - status)); - FltReleaseContext(fileContext); - return STATUS_INSUFFICIENT_RESOURCES; - } - ExInitializeResourceLite(fileContext->Resource); - - // Attach the context to the file - status = FltSetFileContext( - Data->Iopb->TargetInstance, - Data->Iopb->TargetFileObject, - FLT_SET_CONTEXT_KEEP_IF_EXISTS, - fileContext, - &oldFileContext - ); - - if (!NT_SUCCESS(status)) { - FltReleaseContext(fileContext); - - if (STATUS_FLT_CONTEXT_ALREADY_DEFINED != status) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindFileContext: FltSetFileContext failed with status: 0x%x\n", - status)); - FltReleaseContext(fileContext); - return status; - } - - // A context already exists - fileContext = oldFileContext; - status = STATUS_SUCCESS; - } - else { - // Successful creation of a new file context - *ContextCreated = TRUE; - } - } - - *FileContext = fileContext; - - return status; -} - -FLT_PREOP_CALLBACK_STATUS -KfPreCreateHandler( - _Inout_ PFLT_CALLBACK_DATA Data, - _In_ PCFLT_RELATED_OBJECTS FltObjects, - _Flt_CompletionContext_Outptr_ PVOID* CompletionContext -) -/*++ - -Routine Description: - - This is non-pageable because it could be called on the paging path - -Arguments: - - Data - Pointer to the filter callbackData that is passed to us. - - FltObjects - Pointer to the FLT_RELATED_OBJECTS data structure containing - opaque handles to this filter, instance, its associated volume and - file object. - - CompletionContext - The context for the completion routine for this - operation. - -Return Value: - - The return value is the status of the operation. - ---*/ -{ - NTSTATUS status = STATUS_SUCCESS; - NTSTATUS result = FLT_PREOP_SUCCESS_WITH_CALLBACK; - PFLT_FILE_NAME_INFORMATION nameInfo = NULL; - UNREFERENCED_PARAMETER(FltObjects); - UNREFERENCED_PARAMETER(CompletionContext); - - PAGED_CODE(); - - // Check if the file is of interest - status = FltGetFileNameInformation( - Data, - FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, - &nameInfo - ); - if (!NT_SUCCESS(status)) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPreCreateHandler: FltGetFileNameInformation failed with status: 0x%x\n", - status)); - return FLT_POSTOP_FINISHED_PROCESSING; - } - - FltParseFileNameInformation(nameInfo); - - if (0 == nameInfo->FinalComponent.Length) { - // Not a file but a directory - // No need to intercept POST operation - result = FLT_PREOP_SUCCESS_NO_CALLBACK; - } - - FltReleaseFileNameInformation(nameInfo); - - return result; -} - -FLT_POSTOP_CALLBACK_STATUS -KfPostCreateHandler( - _Inout_ PFLT_CALLBACK_DATA Data, - _In_ PCFLT_RELATED_OBJECTS FltObjects, - _In_opt_ PVOID CompletionContext, - _In_ FLT_POST_OPERATION_FLAGS Flags -) -/*++ - -Routine Description: - - Post create callback. - File scanning must be done after the create has gone to the file system in order to read the file. - - Test if the file is a Keysas report. If not send check its validity before allowing to open it. - - This is non-pageable because it may be called at DPC level. - -Arguments: - - Data - Pointer to the filter callbackData that is passed to us. - - FltObjects - Pointer to the FLT_RELATED_OBJECTS data structure containing - opaque handles to this filter, instance, its associated volume and - file object. - - CompletionContext - The completion context set in the pre-operation routine. - - Flags - Denotes whether the completion is successful or is being drained. - -Return Value: - - The return value is the status of the operation. - ---*/ -{ - NTSTATUS status = STATUS_SUCCESS; - PFLT_FILE_NAME_INFORMATION nameInfo = NULL; - BOOLEAN safeToOpen = TRUE; - PKEYSAS_FILE_CTX fileContext = NULL; - BOOLEAN contextCreated = FALSE; - - UNREFERENCED_PARAMETER(FltObjects); - UNREFERENCED_PARAMETER(CompletionContext); - UNREFERENCED_PARAMETER(Flags); - - PAGED_CODE(); - - // If the create is failing, don't bother with it - if (!NT_SUCCESS(Data->IoStatus.Status) || - (STATUS_REPARSE == Data->IoStatus.Status)) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Failing create call\n")); - return FLT_POSTOP_FINISHED_PROCESSING; - } - - // Check if the file is of interest - status = FltGetFileNameInformation( - Data, - FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, - &nameInfo - ); - if (!NT_SUCCESS(status)) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: FltGetFileNameInformation failed with status: 0x%x\n", - status)); - return FLT_POSTOP_FINISHED_PROCESSING; - } - - FltParseFileNameInformation(nameInfo); - - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: File Name:%wZ, Extension: %wZ, Volume: %wZ\n", - nameInfo->Name, - nameInfo->Extension, - nameInfo->Volume)); - - FltReleaseFileNameInformation(nameInfo); - - // Find or create File context - status = FindFileContext(Data, &fileContext, &contextCreated); - if (!NT_SUCCESS(status)) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: FindFileContext failed with status: 0x%x\n", - status)); - return FLT_POSTOP_FINISHED_PROCESSING; - } - // Acquire lock on File context - // By default acquire in shared mode only to read the authorization state - // If the authorization state is unknown then try to acquire the lock in write mode to scan the file - AcquireResourceRead(fileContext->Resource); - - if (AUTH_UNKNOWN == fileContext->Authorization) { - // The authorization status is not known for this file - // Get a write lock - ReleaseResource(fileContext->Resource); - AcquireResourceWrite(fileContext->Resource); - // Test the authorization again as it can have been preempted - if (AUTH_UNKNOWN == fileContext->Authorization) { - fileContext->Authorization = AUTH_PENDING; - // Send the file to further analysis in user space - (VOID)KeysasScanFileInUserMode( - &nameInfo->Name, - &safeToOpen - ); - - // TODO: by default allow all files - fileContext->Authorization = AUTH_ALLOW_READ; - } - } - - switch (fileContext->Authorization) { - case AUTH_BLOCK: - // Block the transaction - Data->IoStatus.Status = STATUS_ACCESS_DENIED; - Data->IoStatus.Information = 0; - break; - case AUTH_PENDING: - case AUTH_UNKNOWN: - // These states should not happen - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Error inconsistent file authorization\n")); - default: - // Unless the file is explicitely blocked do nothing and allow the transaction - break; - } - - ReleaseResource(fileContext->Resource); - - return FLT_POSTOP_FINISHED_PROCESSING; -} - -NTSTATUS -KeysasScanFileInUserMode( - _In_ PUNICODE_STRING FileName, - _Out_ PBOOLEAN SafeToOpen -) -/*++ -Routine Description: - This routine is called to send a request up to user mode to scan a given - file and tell our caller whether it's safe to open this file. - Note that if the scan fails, we set SafeToOpen to TRUE. The scan may fail - because the service hasn't started, or perhaps because this create/cleanup - is for a directory, and there's no data to read & scan. - If we failed creates when the service isn't running, there'd be a - bootstrapping problem -- how would we ever load the .exe for the service? -Arguments: - FileName - Name of the file. It should be NORMALIZED thus the complete path is given - SafeToOpen - Set to FALSE if the file is scanned successfully and it contains - foul language. -Return Value: - The status of the operation, hopefully STATUS_SUCCESS. The common failure - status will probably be STATUS_INSUFFICIENT_RESOURCES. ---*/ - -{ - NTSTATUS status = STATUS_SUCCESS; - PKEYSAS_DRIVER_REQUEST request = NULL; - ULONG replyLength = 0; - - // Set default authorization to true - *SafeToOpen = TRUE; - - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: Entered\n")); - - if (NULL == KeysasData.ClientPort) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: Invalid client port\n")); - return status; - } - - if (FileName->Length > (KEYSAS_REQUEST_BUFFER_SIZE - 1)) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: File name too long\n")); - } - - // Allocate request buffer - request = ExAllocatePoolZero( - NonPagedPool, - sizeof(KEYSAS_DRIVER_REQUEST), - KEYSAS_MEMORY_TAG - ); - - if (NULL == request) { - status = STATUS_INSUFFICIENT_RESOURCES; - goto end; - } - - // Copy the name of the file in the request - status = RtlStringCbCopyUnicodeString(request->Content, KEYSAS_REQUEST_BUFFER_SIZE * sizeof(WCHAR), FileName); - if (STATUS_SUCCESS != status) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: Failed to convert UNICODE_STRING\n")); - goto end; - } - - replyLength = sizeof(request); - - // Send request to userspace - status = FltSendMessage( - KeysasData.Filter, - &KeysasData.ClientPort, - request, - sizeof(request->Content), - request, - &replyLength, - NULL - ); - - if (STATUS_SUCCESS == status) { - *SafeToOpen = ((PKEYSAS_REPLY)request)->Result; - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: Result: %p\n", SafeToOpen)); - } - else { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: Failed to send request to userspace\n")); - } - -end: - if (NULL != request) { - ExFreePoolWithTag(request, KEYSAS_MEMORY_TAG); - } - - return status; +/*++ + +Copyright (c) 2023 Luc Bonnafoux + +Module Name: + + keysasFile.c + +Abstract: + + Contains the callback to handle transactions on file access + +Environment: + + Kernel mode + +--*/ + +#include "keysasFile.h" + +#include +#include +#include +#include +#include +#include + +#include "keysasDriver.h" +#include "keysasCommunication.h" +#include "keysasUtils.h" +#include "keysasInstance.h" + +#ifdef ALLOC_PRAGMA +#pragma alloc_text(PAGE, KfFileContextCleanup) +#pragma alloc_text(PAGE, FindFileContext) +#pragma alloc_text(PAGE, KfPostCreateHandler) +#pragma alloc_text(PAGE, KfPreCreateHandler) +#endif + +VOID +KfFileContextCleanup( + _In_ PFLT_CONTEXT Context, + _In_ FLT_CONTEXT_TYPE ContextType +) +/*++ +Routine Description: + This routine is called to cleanup the ressource allocated with the context +Arguments: + Context - Pointer to the context + ContextType - Type of context received +Return Value: +--*/ +{ + PKEYSAS_FILE_CTX fileContext; + + PAGED_CODE(); + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfFileContextCleanup: Entered\n")); + + switch (ContextType) { + case FLT_FILE_CONTEXT: + fileContext = (PKEYSAS_FILE_CTX)Context; + if (fileContext->Resource != NULL) { + ExDeleteResourceLite(fileContext->Resource); + ExFreePoolWithTag(fileContext->Resource, KEYSAS_MEMORY_TAG); + } + fileContext->Authorization = AUTH_UNKNOWN; + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfFileContextCleanup: Cleaned context\n")); + break; + default: + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfFileContextCleanup: Unsupport context type\n")); + break; + } +} + +NTSTATUS +FindFileContext( + _In_ PFLT_CALLBACK_DATA Data, + _Outptr_ PKEYSAS_FILE_CTX *FileContext, + _Out_opt_ PBOOLEAN ContextCreated +) +/*++ + +Routine Description: + + Find an existing file context or create one if there is none + +Arguments: + + Data - Pointer to the filter callbackData that is passed to us. + + FileContext - Pointer to the File context + + ContextCreated - Set to TRUE if the context has been created during the call + +Return Value: + + The return value is the status of the operation. + +--*/ +{ + NTSTATUS status = STATUS_SUCCESS; + PKEYSAS_FILE_CTX fileContext = NULL; + PKEYSAS_FILE_CTX oldFileContext = NULL; + + PAGED_CODE(); + + // Initialize output paramters + *FileContext = NULL; + if (NULL != ContextCreated) { + *ContextCreated = FALSE; + } + else { + // ContextCreated must point to valid memory + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindFileContext: Invalid input\n")); + return STATUS_UNSUCCESSFUL; + } + + // Try to find an existing file context + status = FltGetFileContext( + Data->Iopb->TargetInstance, + Data->Iopb->TargetFileObject, + &fileContext + ); + + // If the call fail because the context does not exist, create a new one + if (!NT_SUCCESS(status) + && (STATUS_NOT_FOUND == status)) { + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindFileContext: File context not found\n")); + status = FltAllocateContext( + KeysasData.Filter, + FLT_FILE_CONTEXT, + KEYSAS_FILE_CTX_SIZE, + PagedPool, + &fileContext + ); + if (!NT_SUCCESS(status)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindFileContext: FltAllocateContext failed with status: %0x8x\n", + status)); + return status; + } + + // Initialize the context + // Set all the fields to 0 => Authorization = UNKNOWN + RtlZeroMemory(fileContext, KEYSAS_FILE_CTX_SIZE); + fileContext->Resource = ExAllocatePoolZero( + NonPagedPool, + sizeof(ERESOURCE), + KEYSAS_MEMORY_TAG + ); + if (NULL == fileContext->Resource) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindFileContext: ExAllocatePoolZero failed with status: %0x8x\n", + status)); + FltReleaseContext(fileContext); + return STATUS_INSUFFICIENT_RESOURCES; + } + ExInitializeResourceLite(fileContext->Resource); + + // Attach the context to the file + status = FltSetFileContext( + Data->Iopb->TargetInstance, + Data->Iopb->TargetFileObject, + FLT_SET_CONTEXT_KEEP_IF_EXISTS, + fileContext, + &oldFileContext + ); + + if (!NT_SUCCESS(status)) { + FltReleaseContext(fileContext); + + if (STATUS_FLT_CONTEXT_ALREADY_DEFINED != status) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindFileContext: FltSetFileContext failed with status: %0x8x\n", + status)); + return status; + } + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindFileContext: File context already defined\n")); + // A context already exists + fileContext = oldFileContext; + status = STATUS_SUCCESS; + } + else { + // Successful creation of a new file context + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindFileContext: Created a new file context\n")); + *ContextCreated = TRUE; + } + } + + *FileContext = fileContext; + + return status; +} + +FLT_PREOP_CALLBACK_STATUS +KfPreCreateHandler( + _Inout_ PFLT_CALLBACK_DATA Data, + _In_ PCFLT_RELATED_OBJECTS FltObjects, + _Flt_CompletionContext_Outptr_ PVOID* CompletionContext +) +/*++ + +Routine Description: + + This is non-pageable because it could be called on the paging path + +Arguments: + + Data - Pointer to the filter callbackData that is passed to us. + + FltObjects - Pointer to the FLT_RELATED_OBJECTS data structure containing + opaque handles to this filter, instance, its associated volume and + file object. + + CompletionContext - The context for the completion routine for this + operation. + +Return Value: + + The return value is the status of the operation. + +--*/ +{ + NTSTATUS status = STATUS_SUCCESS; + NTSTATUS result = FLT_PREOP_SUCCESS_WITH_CALLBACK; + PFLT_FILE_NAME_INFORMATION nameInfo = NULL; + PKEYSAS_INSTANCE_CTX instanceContext = NULL; + + UNREFERENCED_PARAMETER(FltObjects); + UNREFERENCED_PARAMETER(CompletionContext); + + PAGED_CODE(); + + // Get the instance context + // If the instance is blocked, reject all calls + status = FltGetInstanceContext( + Data->Iopb->TargetInstance, + &instanceContext + ); + if (!NT_SUCCESS(status)) { + // There should always be a context for an instance to which the filter is attached + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPreCreateHandler: FltGetInstanceContext failed with status: %0x8x\n", + status)); + return FLT_PREOP_SUCCESS_NO_CALLBACK; + } + + // Get a read lock on the instance context + AcquireResourceRead(instanceContext->Resource); + switch (instanceContext->Authorization) + { + case AUTH_BLOCK: + // Block all calls + Data->IoStatus.Status = STATUS_ACCESS_DENIED; + Data->IoStatus.Information = 0; + ReleaseResource(instanceContext->Resource); + return FLT_PREOP_COMPLETE; + break; + case AUTH_ALLOW_ALL: + // Allow all call without verification + ReleaseResource(instanceContext->Resource); + return FLT_PREOP_SUCCESS_NO_CALLBACK; + break; + case AUTH_UNKNOWN: + case AUTH_PENDING: + // These two states should not happen + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPreCreateHandler: Invalid instance authorization state\n")); + default: + break; + } + ReleaseResource(instanceContext->Resource); + FltReleaseContext(instanceContext); + + // Check if the file is of interest + status = FltGetFileNameInformation( + Data, + FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, + &nameInfo + ); + if (!NT_SUCCESS(status)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPreCreateHandler: FltGetFileNameInformation failed with status: %0x8x\n", + status)); + return FLT_PREOP_SUCCESS_NO_CALLBACK; + } + + FltParseFileNameInformation(nameInfo); + + if (0 == nameInfo->FinalComponent.Length) { + // Not a file but a directory + // No need to intercept POST operation + result = FLT_PREOP_SUCCESS_NO_CALLBACK; + } + + FltReleaseFileNameInformation(nameInfo); + + return result; +} + +FLT_POSTOP_CALLBACK_STATUS +KfPostCreateHandler( + _Inout_ PFLT_CALLBACK_DATA Data, + _In_ PCFLT_RELATED_OBJECTS FltObjects, + _In_opt_ PVOID CompletionContext, + _In_ FLT_POST_OPERATION_FLAGS Flags +) +/*++ + +Routine Description: + + Post create callback. + File scanning must be done after the create has gone to the file system in order to read the file. + + Test if the file is a Keysas report. If not send check its validity before allowing to open it. + + This is non-pageable because it may be called at DPC level. + +Arguments: + + Data - Pointer to the filter callbackData that is passed to us. + + FltObjects - Pointer to the FLT_RELATED_OBJECTS data structure containing + opaque handles to this filter, instance, its associated volume and + file object. + + CompletionContext - The completion context set in the pre-operation routine. + + Flags - Denotes whether the completion is successful or is being drained. + +Return Value: + + The return value is the status of the operation. + +--*/ +{ + NTSTATUS status = STATUS_SUCCESS; + PFLT_FILE_NAME_INFORMATION nameInfo = NULL; + BOOLEAN safeToOpen = TRUE; + PKEYSAS_FILE_CTX fileContext = NULL; + BOOLEAN contextCreated = FALSE; + + UNREFERENCED_PARAMETER(FltObjects); + UNREFERENCED_PARAMETER(CompletionContext); + UNREFERENCED_PARAMETER(Flags); + + PAGED_CODE(); + + // If the create is failing, don't bother with it + if (!NT_SUCCESS(Data->IoStatus.Status) || + (STATUS_REPARSE == Data->IoStatus.Status)) { + return FLT_POSTOP_FINISHED_PROCESSING; + } + + // Check if the file is of interest + status = FltGetFileNameInformation( + Data, + FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, + &nameInfo + ); + if (!NT_SUCCESS(status)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: FltGetFileNameInformation failed with status: %0x8x\n", + status)); + return FLT_POSTOP_FINISHED_PROCESSING; + } + + FltParseFileNameInformation(nameInfo); + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: File Name:%wZ, Extension: %wZ, Volume: %wZ\n", + nameInfo->Name, + nameInfo->Extension, + nameInfo->Volume)); + + FltReleaseFileNameInformation(nameInfo); + + // Find or create File context + status = FindFileContext(Data, &fileContext, &contextCreated); + if (!NT_SUCCESS(status)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: FindFileContext failed with status: %0x8x\n", + status)); + return FLT_POSTOP_FINISHED_PROCESSING; + } + // Acquire lock on File context + // By default acquire in shared mode only to read the authorization state + // If the authorization state is unknown then try to acquire the lock in write mode to scan the file + AcquireResourceRead(fileContext->Resource); + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Acquired ressource in read mode\n")); + + if (AUTH_UNKNOWN == fileContext->Authorization) { + // The authorization status is not known for this file + // Get a write lock + ReleaseResource(fileContext->Resource); + AcquireResourceWrite(fileContext->Resource); + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Acquired ressource in write mode\n")); + // Test the authorization again as it can have been preempted + if (AUTH_UNKNOWN == fileContext->Authorization) { + fileContext->Authorization = AUTH_PENDING; + // Send the file to further analysis in user space + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Send request to userspace\n")); + (VOID)KeysasScanFileInUserMode( + &nameInfo->Name, + &safeToOpen + ); + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Received authorization from userspace\n")); + + // TODO: by default allow all files + if (TRUE == safeToOpen) { + fileContext->Authorization = AUTH_ALLOW_READ; + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: File authorization ALLOW\n")); + } + else { + fileContext->Authorization = AUTH_BLOCK; + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: File authorization BLOCK\n")); + } + } + } + + switch (fileContext->Authorization) { + case AUTH_BLOCK: + // Block the transaction + Data->IoStatus.Status = STATUS_ACCESS_DENIED; + Data->IoStatus.Information = 0; + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: File blocked\n")); + break; + case AUTH_PENDING: + case AUTH_UNKNOWN: + // These states should not happen + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Error inconsistent file authorization\n")); + default: + // Unless the file is explicitely blocked do nothing and allow the transaction + break; + } + + ReleaseResource(fileContext->Resource); + FltReleaseContext(fileContext); + + return FLT_POSTOP_FINISHED_PROCESSING; +} + +NTSTATUS +KeysasScanFileInUserMode( + _In_ PUNICODE_STRING FileName, + _Out_ PBOOLEAN SafeToOpen +) +/*++ +Routine Description: + This routine is called to send a request up to user mode to scan a given + file and tell our caller whether it's safe to open this file. + Note that if the scan fails, we set SafeToOpen to TRUE. The scan may fail + because the service hasn't started, or perhaps because this create/cleanup + is for a directory, and there's no data to read & scan. + If we failed creates when the service isn't running, there'd be a + bootstrapping problem -- how would we ever load the .exe for the service? +Arguments: + FileName - Name of the file. It should be NORMALIZED thus the complete path is given + SafeToOpen - Set to FALSE if the file is scanned successfully and it contains + foul language. +Return Value: + The status of the operation, hopefully STATUS_SUCCESS. The common failure + status will probably be STATUS_INSUFFICIENT_RESOURCES. +--*/ + +{ + NTSTATUS status = STATUS_SUCCESS; + PKEYSAS_DRIVER_REQUEST request = NULL; + ULONG replyLength = 0; + + // Set default authorization to true + *SafeToOpen = TRUE; + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: Entered\n")); + + if (NULL == KeysasData.ClientPort) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: Invalid client port\n")); + return status; + } + + if (FileName->Length > (KEYSAS_REQUEST_BUFFER_SIZE - 1)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: File name too long\n")); + } + + // Allocate request buffer + request = ExAllocatePoolZero( + NonPagedPool, + sizeof(KEYSAS_DRIVER_REQUEST), + KEYSAS_MEMORY_TAG + ); + + if (NULL == request) { + status = STATUS_INSUFFICIENT_RESOURCES; + goto end; + } + + // Copy the name of the file in the request + status = RtlStringCbCopyUnicodeString(request->Content, KEYSAS_REQUEST_BUFFER_SIZE * sizeof(WCHAR), FileName); + if (STATUS_SUCCESS != status) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: Failed to convert UNICODE_STRING\n")); + goto end; + } + + replyLength = sizeof(request); + + // Send request to userspace + status = FltSendMessage( + KeysasData.Filter, + &KeysasData.ClientPort, + request, + sizeof(request->Content), + request, + &replyLength, + NULL + ); + + if (STATUS_SUCCESS == status) { + *SafeToOpen = ((PKEYSAS_REPLY)request)->Result; + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: Received result\n")); + } + else { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: Failed to send request to userspace\n")); + } + +end: + if (NULL != request) { + ExFreePoolWithTag(request, KEYSAS_MEMORY_TAG); + } + + return status; } \ No newline at end of file diff --git a/keysas-usbfilter/minifilter/keysasFile.h b/keysas-usbfilter/minifilter/keysasFile.h index cdd9d07..cf7060a 100644 --- a/keysas-usbfilter/minifilter/keysasFile.h +++ b/keysas-usbfilter/minifilter/keysasFile.h @@ -60,4 +60,11 @@ KfPostCreateHandler( _In_ FLT_POST_OPERATION_FLAGS Flags ); +NTSTATUS +FindFileContext( + _In_ PFLT_CALLBACK_DATA Data, + _Outptr_ PKEYSAS_FILE_CTX* FileContext, + _Out_opt_ PBOOLEAN ContextCreated +); + #endif \ No newline at end of file diff --git a/keysas-usbfilter/minifilter/keysasInstance.c b/keysas-usbfilter/minifilter/keysasInstance.c index 8cd9746..3fa9580 100644 --- a/keysas-usbfilter/minifilter/keysasInstance.c +++ b/keysas-usbfilter/minifilter/keysasInstance.c @@ -25,6 +25,134 @@ Module Name: #include #include +#include "keysasUtils.h" + +#ifdef ALLOC_PRAGMA +#pragma alloc_text(PAGE, KfInstanceContextCleanup) +#pragma alloc_text(PAGE, KfInstanceQueryTeardown) +#pragma alloc_text(PAGE, KfInstanceSetup) +#pragma alloc_text(PAGE, KfInstanceTeardownComplete) +#pragma alloc_text(PAGE, KfInstanceTeardownStart) +#pragma alloc_text(PAGE, FindInstanceContext) +#endif + +NTSTATUS +FindInstanceContext( + _In_ PFLT_INSTANCE Instance, + _Outptr_ PKEYSAS_INSTANCE_CTX* InstanceContext, + _Out_opt_ PBOOLEAN ContextCreated +) +/*++ + +Routine Description: + + Find an existing instance context or create one if there is none + +Arguments: + + Instance - Pointer to the instance + + InstanceContext - Pointer to the Instance context + + ContextCreated - Set to TRUE if the context has been created during the call + +Return Value: + + The return value is the status of the operation. + +--*/ +{ + NTSTATUS status = STATUS_SUCCESS; + PKEYSAS_INSTANCE_CTX instanceContext = NULL; + PKEYSAS_INSTANCE_CTX oldInstanceContext = NULL; + + PAGED_CODE(); + + // Initialize output paramters + *InstanceContext = NULL; + if (NULL != ContextCreated) { + *ContextCreated = FALSE; + } + else { + // ContextCreated must point to valid memory + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindInstanceContext: Invalid input\n")); + return STATUS_UNSUCCESSFUL; + } + + // Try to find an existing instance context + status = FltGetInstanceContext( + Instance, + &instanceContext + ); + + // If the call fail because the context does not exist, create a new one + if (!NT_SUCCESS(status) + && (STATUS_NOT_FOUND == status)) { + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindInstanceContext: Instance context not found\n")); + status = FltAllocateContext( + KeysasData.Filter, + FLT_INSTANCE_CONTEXT, + KEYSAS_INSTANCE_CTX_SIZE, + PagedPool, + &instanceContext + ); + if (!NT_SUCCESS(status)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindInstanceContext: FltAllocateContext failed with status: %0x8x\n", + status)); + return status; + } + + // Initialize the context + // Set all the fields to 0 => Authorization = UNKNOWN + RtlZeroMemory(instanceContext, KEYSAS_INSTANCE_CTX_SIZE); + instanceContext->Resource = ExAllocatePoolZero( + NonPagedPool, + sizeof(ERESOURCE), + KEYSAS_MEMORY_TAG + ); + if (NULL == instanceContext->Resource) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindInstanceContext: ExAllocatePoolZero failed with status: %0x8x\n", + status)); + FltReleaseContext(instanceContext); + return STATUS_INSUFFICIENT_RESOURCES; + } + ExInitializeResourceLite(instanceContext->Resource); + + // Attach the context to the file + status = FltSetInstanceContext( + Instance, + FLT_SET_CONTEXT_KEEP_IF_EXISTS, + instanceContext, + &oldInstanceContext + ); + + if (!NT_SUCCESS(status)) { + FltReleaseContext(instanceContext); + + if (STATUS_FLT_CONTEXT_ALREADY_DEFINED != status) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindInstanceContext: FltSetInstanceContext failed with status: %0x8x\n", + status)); + return status; + } + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindInstanceContext: Instance context already defined\n")); + // A context already exists + instanceContext = oldInstanceContext; + status = STATUS_SUCCESS; + } + else { + // Successful creation of a new file context + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!FindInstanceContext: Created a new instance context\n")); + *ContextCreated = TRUE; + } + } + + *InstanceContext = instanceContext; + + return status; +} + NTSTATUS KfInstanceSetup( _In_ PCFLT_RELATED_OBJECTS FltObjects, @@ -73,6 +201,7 @@ Return Value: STORAGE_DESCRIPTOR_HEADER HeaderDescriptor = { 0 }; ULONG SizeNeeded, RetLength, OutputLength, SizeRequired; PKEYSAS_INSTANCE_CTX instanceContext = NULL; + BOOLEAN instanceCreated = FALSE; // Print debug info on the call context KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: Entered\n")); @@ -85,7 +214,7 @@ Return Value: // Open the volume to get information on it status = FltOpenVolume(FltObjects->Instance, &FsVolumeHandle, &FsFileObject); if (!NT_SUCCESS(status)) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: FltOpenVolume failed with status = 0x%x\n", status)); + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: FltOpenVolume failed with status = %0x8x\n", status)); goto end; } @@ -111,7 +240,7 @@ Return Value: &RetLength); if (!NT_SUCCESS(status)) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: FltDeviceIoControlFile failed with status = 0x%x\n", status)); + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: FltDeviceIoControlFile failed with status = %0x8x\n", status)); goto end; } @@ -140,7 +269,7 @@ Return Value: ); if (!NT_SUCCESS(status)) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: FltDeviceIoControlFile failed with status = 0x%x\n", status)); + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: FltDeviceIoControlFile failed with status = %0x8x\n", status)); goto end; } @@ -149,36 +278,25 @@ Return Value: KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: USB descriptor found attach\n")); status = STATUS_SUCCESS; - // Create a context for the instance - status = FltAllocateContext( - FltObjects->Filter, - FLT_INSTANCE_CONTEXT, - KEYSAS_INSTANCE_CTX_SIZE, - NonPagedPool, - &instanceContext + status = FindInstanceContext( + FltObjects->Instance, + &instanceContext, + &instanceCreated ); + if (!NT_SUCCESS(status)) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: FltAllocateContext failed with status = 0x%x\n", status)); + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: FindInstanceContext failed with status = %0x8x\n", status)); status = STATUS_FLT_DO_NOT_ATTACH; goto end; } - instanceContext->Authorization = AUTH_UNKNOWN; - instanceContext->Resource = NULL; - - status = FltSetInstanceContext( - FltObjects->Instance, - FLT_SET_CONTEXT_KEEP_IF_EXISTS, - instanceContext, - NULL - ); - if (!NT_SUCCESS(status)) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: FltSetInstanceContext failed with status = 0x%x\n", status)); - status = STATUS_FLT_DO_NOT_ATTACH; - goto end; + // TODO: default set instance to ALLOW + AcquireResourceWrite(instanceContext->Resource); + instanceContext->Authorization = AUTH_ALLOW_WARNING; + ReleaseResource(instanceContext->Resource); - } + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: Instance context attached\n")); } else { KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: Not a USB device do not attach\n")); @@ -337,6 +455,8 @@ Return Value: PAGED_CODE(); + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceContextCleanup: Entered\n")); + switch (ContextType) { case FLT_INSTANCE_CONTEXT: instanceContext = (PKEYSAS_INSTANCE_CTX)Context; @@ -345,9 +465,10 @@ Return Value: ExFreePoolWithTag(instanceContext->Resource, KEYSAS_MEMORY_TAG); } instanceContext->Authorization = AUTH_UNKNOWN; + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceContextCleanup: Cleaned instance context\n")); break; default: - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfContextCleanup: Unsupport context type\n")); + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceContextCleanup: Unsupport context type\n")); break; } } \ No newline at end of file diff --git a/keysas-usbfilter/minifilter/keysasInstance.h b/keysas-usbfilter/minifilter/keysasInstance.h index 5f19723..98497ce 100644 --- a/keysas-usbfilter/minifilter/keysasInstance.h +++ b/keysas-usbfilter/minifilter/keysasInstance.h @@ -66,4 +66,11 @@ KfInstanceContextCleanup( _In_ FLT_CONTEXT_TYPE ContextType ); +NTSTATUS +FindInstanceContext( + _In_ PFLT_INSTANCE Instance, + _Outptr_ PKEYSAS_INSTANCE_CTX* InstanceContext, + _Out_opt_ PBOOLEAN ContextCreated +); + #endif _H_KEYSAS_INSTANCE_ \ No newline at end of file From 5490c8d1c9dd103cf0c90e1a565e610b674cc810 Mon Sep 17 00:00:00 2001 From: Luc Date: Tue, 25 Apr 2023 15:54:50 +0200 Subject: [PATCH 052/160] Implemented file validation by the user --- keysas-usbfilter/daemon/Cargo.toml | 3 +- keysas-usbfilter/daemon/src/main.rs | 6 +- .../daemon/src/windows_driver_interface.rs | 95 +++++++++----- .../minifilter/KeysasDriver.vcxproj | 1 + .../minifilter/KeysasDriver.vcxproj.filters | 3 + keysas-usbfilter/minifilter/keysasFile.c | 124 ++++++++++++++---- keysas-usbfilter/minifilter/keysasFile.h | 2 + keysas-usbfilter/minifilter/keysasUtils.c | 58 ++++++++ keysas-usbfilter/minifilter/keysasUtils.h | 35 +---- 9 files changed, 238 insertions(+), 89 deletions(-) create mode 100644 keysas-usbfilter/minifilter/keysasUtils.c diff --git a/keysas-usbfilter/daemon/Cargo.toml b/keysas-usbfilter/daemon/Cargo.toml index 60654b6..c8fd0d0 100644 --- a/keysas-usbfilter/daemon/Cargo.toml +++ b/keysas-usbfilter/daemon/Cargo.toml @@ -24,5 +24,6 @@ features = [ "Win32_Security", "Win32_Storage_InstallableFileSystems", "Win32_System_IO", - "Win32_System_Threading" + "Win32_System_Threading", + "Win32_UI_WindowsAndMessaging" ] \ No newline at end of file diff --git a/keysas-usbfilter/daemon/src/main.rs b/keysas-usbfilter/daemon/src/main.rs index 248b605..1da9bbc 100644 --- a/keysas-usbfilter/daemon/src/main.rs +++ b/keysas-usbfilter/daemon/src/main.rs @@ -25,6 +25,8 @@ #![warn(deprecated)] #![warn(unused_imports)] +#![feature(vec_into_raw_parts)] + pub mod driver_interface; pub mod windows_driver_interface; @@ -45,12 +47,10 @@ fn main() -> Result<(), anyhow::Error> { log::info!("Driver interface OK"); loop {} - - Ok(()) } // Initialize the driver interface and register the callbacks fn init_driver_interface() -> Result<(), anyhow::Error> { init_driver_com()?; Ok(()) -} \ No newline at end of file +} diff --git a/keysas-usbfilter/daemon/src/windows_driver_interface.rs b/keysas-usbfilter/daemon/src/windows_driver_interface.rs index 0fee273..22560ab 100644 --- a/keysas-usbfilter/daemon/src/windows_driver_interface.rs +++ b/keysas-usbfilter/daemon/src/windows_driver_interface.rs @@ -28,19 +28,30 @@ use anyhow::anyhow; use std::mem::size_of; use std::thread; -use wchar::wchar_t; use widestring::U16CString; -use windows::core::PCWSTR; -use windows::Win32::Foundation::{BOOLEAN, HANDLE, STATUS_SUCCESS, CloseHandle}; +use windows::core::{PCWSTR, PCSTR}; +use windows::Win32::Foundation::{CloseHandle, BOOLEAN, HANDLE, STATUS_SUCCESS}; use windows::Win32::Storage::InstallableFileSystems::{ FilterConnectCommunicationPort, FilterGetMessage, FilterReplyMessage, FILTER_MESSAGE_HEADER, FILTER_REPLY_HEADER, }; +use windows::Win32::UI::WindowsAndMessaging::*; +use windows::s; + +// Operation code for the request to userland +#[derive(Debug)] +enum KeysasFilterOperation { + ScanFile = 0, // Validate the signature of the file and the report + UserAllowFile, // Ask user to allow the file + UserAllowAllUsb, // Ask user to allow complete access the USB drive + UserAllowUsbWithWarning // Ask user to allow access to USB drive with warning on file opening +} #[derive(Debug)] #[repr(C)] struct DriverMessage { header: FILTER_MESSAGE_HEADER, + operation: KeysasFilterOperation, path: [u16; 1024], } @@ -80,50 +91,70 @@ impl WindowsDriverInterface { Ok(Self { handle }) } - + /// Start listening to the drivers' requests and register a callback to handle them - /// + /// /// # Arguments - /// - /// * `cb` - Callback to handle the driver requests + /// + /// * `cb` - Callback to handle the driver requests pub fn start_driver_com(&self, _cb: fn() -> ()) -> Result<(), anyhow::Error> { let handle = self.handle.clone(); - thread::spawn(move || { + thread::spawn(move || -> Result<(), anyhow::Error> { + let request_size = u32::try_from(size_of::())?; + let reply_size = u32::try_from(size_of::())? + + u32::try_from(size_of::())?; + loop { let mut message = DriverMessage { header: FILTER_MESSAGE_HEADER::default(), + operation: KeysasFilterOperation::ScanFile, path: [0; 1024], }; - + unsafe { - FilterGetMessage( - handle, - &mut message.header, - u32::try_from(size_of::()).unwrap(), - None, - ) - .unwrap(); + FilterGetMessage(handle, &mut message.header, request_size, None)?; } - + println!("{:?}", message); - println!("Path: {:?}", String::from_utf16(&message.path).unwrap()); - - let mut reply = UserReply { - header: FILTER_REPLY_HEADER::default(), - file_safe: BOOLEAN::from(true), + println!("Path: {:?}", String::from_utf16(&message.path)?); + + let file_path = String::from_utf16(&message.path)?; + let filename = format!("Allow file: {:?}", file_path.trim_matches(char::from(0))); + let (name_ptr, _, _) = filename.into_raw_parts(); + let mut authorization_status = MESSAGEBOX_RESULT::default(); + unsafe { + authorization_status = MessageBoxA( + None, + PCSTR::from_raw(name_ptr), + s!("Keysas USB Filter"), + MB_YESNO | MB_ICONWARNING | MB_SYSTEMMODAL + ); + } + + let mut reply = match authorization_status { + IDYES => UserReply { + header: FILTER_REPLY_HEADER::default(), + file_safe: BOOLEAN::from(true), + }, + IDNO => UserReply { + header: FILTER_REPLY_HEADER::default(), + file_safe: BOOLEAN::from(false), + }, + _ => { + println!("Unknown Authorization: {:?}", authorization_status); + // By default block the access to the file + UserReply { + header: FILTER_REPLY_HEADER::default(), + file_safe: BOOLEAN::from(false), + } + } }; - + reply.header.MessageId = message.header.MessageId; reply.header.Status = STATUS_SUCCESS; - + unsafe { - FilterReplyMessage( - handle, - &reply.header, - u32::try_from(size_of::()).unwrap() - + u32::try_from(size_of::()).unwrap(), - ) - .unwrap(); + FilterReplyMessage(handle, &reply.header, reply_size)?; } println!("Sent response"); @@ -138,4 +169,4 @@ impl WindowsDriverInterface { CloseHandle::(self.handle); } } -} \ No newline at end of file +} diff --git a/keysas-usbfilter/minifilter/KeysasDriver.vcxproj b/keysas-usbfilter/minifilter/KeysasDriver.vcxproj index 2bc5d0d..92555db 100644 --- a/keysas-usbfilter/minifilter/KeysasDriver.vcxproj +++ b/keysas-usbfilter/minifilter/KeysasDriver.vcxproj @@ -114,6 +114,7 @@ + diff --git a/keysas-usbfilter/minifilter/KeysasDriver.vcxproj.filters b/keysas-usbfilter/minifilter/KeysasDriver.vcxproj.filters index 277b149..ece9cf0 100644 --- a/keysas-usbfilter/minifilter/KeysasDriver.vcxproj.filters +++ b/keysas-usbfilter/minifilter/KeysasDriver.vcxproj.filters @@ -36,6 +36,9 @@ Source Files + + Source Files + diff --git a/keysas-usbfilter/minifilter/keysasFile.c b/keysas-usbfilter/minifilter/keysasFile.c index b774e98..4e8f5d7 100644 --- a/keysas-usbfilter/minifilter/keysasFile.c +++ b/keysas-usbfilter/minifilter/keysasFile.c @@ -244,7 +244,10 @@ Return Value: } // Get a read lock on the instance context - AcquireResourceRead(instanceContext->Resource); + if (!AcquireResourceRead(instanceContext->Resource)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Failed to acquire ressource in read mode\n")); + } + switch (instanceContext->Authorization) { case AUTH_BLOCK: @@ -335,6 +338,8 @@ Return Value: BOOLEAN safeToOpen = TRUE; PKEYSAS_FILE_CTX fileContext = NULL; BOOLEAN contextCreated = FALSE; + KEYSAS_FILTER_OPERATION operation = SCAN_FILE; + PKEYSAS_INSTANCE_CTX instanceContext = NULL; UNREFERENCED_PARAMETER(FltObjects); UNREFERENCED_PARAMETER(CompletionContext); @@ -345,7 +350,8 @@ Return Value: // If the create is failing, don't bother with it if (!NT_SUCCESS(Data->IoStatus.Status) || (STATUS_REPARSE == Data->IoStatus.Status)) { - return FLT_POSTOP_FINISHED_PROCESSING; + status = FLT_POSTOP_FINISHED_PROCESSING; + goto cleanup; } // Check if the file is of interest @@ -357,7 +363,8 @@ Return Value: if (!NT_SUCCESS(status)) { KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: FltGetFileNameInformation failed with status: %0x8x\n", status)); - return FLT_POSTOP_FINISHED_PROCESSING; + status = FLT_POSTOP_FINISHED_PROCESSING; + goto cleanup; } FltParseFileNameInformation(nameInfo); @@ -367,46 +374,96 @@ Return Value: nameInfo->Extension, nameInfo->Volume)); - FltReleaseFileNameInformation(nameInfo); - // Find or create File context status = FindFileContext(Data, &fileContext, &contextCreated); if (!NT_SUCCESS(status)) { KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: FindFileContext failed with status: %0x8x\n", status)); - return FLT_POSTOP_FINISHED_PROCESSING; + status = FLT_POSTOP_FINISHED_PROCESSING; + goto cleanup; } // Acquire lock on File context // By default acquire in shared mode only to read the authorization state // If the authorization state is unknown then try to acquire the lock in write mode to scan the file - AcquireResourceRead(fileContext->Resource); + if (!AcquireResourceRead(fileContext->Resource)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Failed to acquire ressource in read mode\n")); + } KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Acquired ressource in read mode\n")); if (AUTH_UNKNOWN == fileContext->Authorization) { // The authorization status is not known for this file // Get a write lock ReleaseResource(fileContext->Resource); - AcquireResourceWrite(fileContext->Resource); + if (!AcquireResourceWrite(fileContext->Resource)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Failed to acquire ressource in write mode\n")); + } KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Acquired ressource in write mode\n")); // Test the authorization again as it can have been preempted if (AUTH_UNKNOWN == fileContext->Authorization) { fileContext->Authorization = AUTH_PENDING; - // Send the file to further analysis in user space - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Send request to userspace\n")); - (VOID)KeysasScanFileInUserMode( - &nameInfo->Name, - &safeToOpen + + // Try to acquire the instance context as the file authorization will depend on the instance status + status = FindInstanceContext( + Data->Iopb->TargetInstance, + &instanceContext, + &contextCreated ); - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Received authorization from userspace\n")); + if (!NT_SUCCESS(status)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: FindInstanceContext failed with status: %0x8x\n", + status)); + status = FLT_POSTOP_FINISHED_PROCESSING; + goto cleanup; + } - // TODO: by default allow all files - if (TRUE == safeToOpen) { - fileContext->Authorization = AUTH_ALLOW_READ; - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: File authorization ALLOW\n")); + // Get read access to the instance state + if (!AcquireResourceRead(instanceContext->Resource)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Failed to acquire ressource in read mode\n")); } - else { + + // Set the scan operation depending on the instance status + operation = SCAN_FILE; + switch (instanceContext->Authorization) { + case AUTH_BLOCK: + // Set the file to block mode + ReleaseResource(instanceContext->Resource); fileContext->Authorization = AUTH_BLOCK; - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: File authorization BLOCK\n")); + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Instance blocked, File authorization BLOCK\n")); + break; + case AUTH_ALLOW_WARNING: + // In this case, ask for the user authorization + operation = USER_ALLOW_FILE; + case AUTH_ALLOW_READ: + // Ask the userspace to scan the file + ReleaseResource(instanceContext->Resource); + // Send the file to further analysis in user space + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Send request to userspace\n")); + (VOID)KeysasScanFileInUserMode( + &nameInfo->Name, + operation, + &safeToOpen + ); + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Received authorization from userspace\n")); + // Allow or block the file depending on the userspace result + if (TRUE == safeToOpen) { + fileContext->Authorization = AUTH_ALLOW_READ; + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: File authorization ALLOW\n")); + } + else { + fileContext->Authorization = AUTH_BLOCK; + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: File authorization BLOCK\n")); + } + break; + case AUTH_ALLOW_ALL: + // Set the file to allow mode + ReleaseResource(instanceContext->Resource); + fileContext->Authorization = AUTH_ALLOW_READ; + break; + default: + // Should not happen, log and set file to blocking + ReleaseResource(instanceContext->Resource); + fileContext->Authorization = AUTH_BLOCK; + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Incoherent instance state, File authorization BLOCK\n")); + break; } } } @@ -427,15 +484,34 @@ Return Value: break; } - ReleaseResource(fileContext->Resource); - FltReleaseContext(fileContext); + status = FLT_POSTOP_FINISHED_PROCESSING; + +cleanup: + if (NULL != nameInfo) { + FltReleaseFileNameInformation(nameInfo); + } - return FLT_POSTOP_FINISHED_PROCESSING; + if (NULL != fileContext) { + if (NULL != fileContext->Resource) { + ReleaseResource(fileContext->Resource); + } + FltReleaseContext(fileContext); + } + + if (NULL != instanceContext) { + if (NULL != instanceContext->Resource) { + ReleaseResource(instanceContext->Resource); + } + FltReleaseContext(instanceContext); + } + + return status; } NTSTATUS KeysasScanFileInUserMode( _In_ PUNICODE_STRING FileName, + _In_ KEYSAS_FILTER_OPERATION Operation, _Out_ PBOOLEAN SafeToOpen ) /*++ @@ -449,6 +525,7 @@ Routine Description: bootstrapping problem -- how would we ever load the .exe for the service? Arguments: FileName - Name of the file. It should be NORMALIZED thus the complete path is given + Operation - Operation code for the user app SafeToOpen - Set to FALSE if the file is scanned successfully and it contains foul language. Return Value: @@ -493,6 +570,7 @@ Return Value: KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanFileInUserMode: Failed to convert UNICODE_STRING\n")); goto end; } + request->Operation = Operation; replyLength = sizeof(request); diff --git a/keysas-usbfilter/minifilter/keysasFile.h b/keysas-usbfilter/minifilter/keysasFile.h index cf7060a..557ef74 100644 --- a/keysas-usbfilter/minifilter/keysasFile.h +++ b/keysas-usbfilter/minifilter/keysasFile.h @@ -22,6 +22,7 @@ Module Name: #define _H_KEYSAS_FILE_ #include "keysasDriver.h" +#include "keysasCommunication.h" typedef struct _KEYSAS_FILE_CTX { // Authorization state of the file @@ -42,6 +43,7 @@ KfFileContextCleanup( NTSTATUS KeysasScanFileInUserMode( _In_ PUNICODE_STRING FileName, + _In_ KEYSAS_FILTER_OPERATION Operation, _Out_ PBOOLEAN SafeToOpen ); diff --git a/keysas-usbfilter/minifilter/keysasUtils.c b/keysas-usbfilter/minifilter/keysasUtils.c new file mode 100644 index 0000000..7457eb2 --- /dev/null +++ b/keysas-usbfilter/minifilter/keysasUtils.c @@ -0,0 +1,58 @@ +#include "keysasUtils.h" + +#ifdef ALLOC_PRAGMA +#pragma alloc_text(PAGE, AcquireResourceWrite) +#pragma alloc_text(PAGE, AcquireResourceRead) +#pragma alloc_text(PAGE, ReleaseResource) +#endif + +BOOLEAN +_Acquires_lock_(_Global_critical_region_) +_IRQL_requires_max_(APC_LEVEL) +AcquireResourceWrite( + _Inout_ _Requires_lock_not_held_(*_Curr_) _Acquires_exclusive_lock_(*_Curr_) PERESOURCE Resource +) +{ + PAGED_CODE(); + if ((KeGetCurrentIrql() <= APC_LEVEL) + && (ExIsResourceAcquiredExclusiveLite(Resource) + || !ExIsResourceAcquiredSharedLite(Resource))) { + KeEnterCriticalRegion(); + (VOID)ExAcquireResourceExclusiveLite(Resource, TRUE); + return TRUE; + } + return FALSE; +} + +BOOLEAN +_Acquires_lock_(_Global_critical_region_) +_IRQL_requires_max_(APC_LEVEL) +AcquireResourceRead( + _Inout_ _Requires_lock_not_held_(*_Curr_) _Acquires_shared_lock_(*_Curr_) PERESOURCE Resource +) +{ + PAGED_CODE(); + if (KeGetCurrentIrql() <= APC_LEVEL) { + KeEnterCriticalRegion(); + (VOID)ExAcquireResourceSharedLite(Resource, TRUE); + return TRUE; + } + return FALSE; +} + +VOID +_Releases_lock_(_Global_critical_region_) +_Requires_lock_held_(_Global_critical_region_) +_IRQL_requires_max_(APC_LEVEL) +ReleaseResource( + _Inout_ _Requires_lock_held_(*_Curr_) _Releases_lock_(*_Curr_) PERESOURCE Resource +) +{ + PAGED_CODE(); + if ((KeGetCurrentIrql() <= APC_LEVEL) + && (ExIsResourceAcquiredExclusiveLite(Resource) + || ExIsResourceAcquiredSharedLite(Resource))) { + ExReleaseResourceLite(Resource); + KeLeaveCriticalRegion(); + } +} \ No newline at end of file diff --git a/keysas-usbfilter/minifilter/keysasUtils.h b/keysas-usbfilter/minifilter/keysasUtils.h index 78eff30..22c8443 100644 --- a/keysas-usbfilter/minifilter/keysasUtils.h +++ b/keysas-usbfilter/minifilter/keysasUtils.h @@ -27,54 +27,29 @@ Module Name: // Acquire ressource in write mode // Wait for the lock if not available -FORCEINLINE -VOID +BOOLEAN _Acquires_lock_(_Global_critical_region_) _IRQL_requires_max_(APC_LEVEL) AcquireResourceWrite( _Inout_ _Requires_lock_not_held_(*_Curr_) _Acquires_exclusive_lock_(*_Curr_) PERESOURCE Resource -) -{ - FLT_ASSERT(KeGetCurrentIrql() <= APC_LEVEL); - FLT_ASSERT(ExIsResourceAcquiredExclusiveLite(Resource) - || !ExIsResourceAcquiredSharedLite(Resource)); - - KeEnterCriticalRegion(); - (VOID)ExAcquireResourceExclusiveLite(Resource, TRUE); -} +); // Acquire ressource in read mode // Wait for the lock if not available -FORCEINLINE -VOID +BOOLEAN _Acquires_lock_(_Global_critical_region_) _IRQL_requires_max_(APC_LEVEL) AcquireResourceRead( _Inout_ _Requires_lock_not_held_(*_Curr_) _Acquires_shared_lock_(*_Curr_) PERESOURCE Resource -) -{ - FLT_ASSERT(KeGetCurrentIrql() <= APC_LEVEL); - - KeEnterCriticalRegion(); - (VOID)ExAcquireResourceSharedLite(Resource, TRUE); -} +); // Realease a lock -FORCEINLINE VOID _Releases_lock_(_Global_critical_region_) _Requires_lock_held_(_Global_critical_region_) _IRQL_requires_max_(APC_LEVEL) ReleaseResource( _Inout_ _Requires_lock_held_(*_Curr_) _Releases_lock_(*_Curr_) PERESOURCE Resource -) -{ - FLT_ASSERT(KeGetCurrentIrql() <= APC_LEVEL); - FLT_ASSERT(ExIsResourceAcquiredExclusiveLite(Resource) - || ExIsResourceAcquiredSharedLite(Resource)); - - ExReleaseResourceLite(Resource); - KeLeaveCriticalRegion(); -} +); #endif // _H_KEYSAS_UTILS_ \ No newline at end of file From dac8a985e0d6f0115d73f4b104573a8d1c06d678 Mon Sep 17 00:00:00 2001 From: Luc Date: Tue, 25 Apr 2023 16:33:25 +0200 Subject: [PATCH 053/160] Improved list of create call filtered --- keysas-usbfilter/minifilter/keysasFile.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/keysas-usbfilter/minifilter/keysasFile.c b/keysas-usbfilter/minifilter/keysasFile.c index 4e8f5d7..f592fa2 100644 --- a/keysas-usbfilter/minifilter/keysasFile.c +++ b/keysas-usbfilter/minifilter/keysasFile.c @@ -226,10 +226,23 @@ Return Value: PKEYSAS_INSTANCE_CTX instanceContext = NULL; UNREFERENCED_PARAMETER(FltObjects); - UNREFERENCED_PARAMETER(CompletionContext); + UNREFERENCED_PARAMETER(CompletionContext = NULL); PAGED_CODE(); + // Allow call from our userspace application + if (IoThreadToProcess(Data->Thread) == KeysasData.UserProcess) { + return FLT_PREOP_SUCCESS_NO_CALLBACK; + } + + // Don't filter call to directories + if (FlagOn(Data->Iopb->Parameters.Create.Options, FILE_DIRECTORY_FILE)) { + return FLT_PREOP_SUCCESS_NO_CALLBACK; + } + if (FlagOn(Data->Iopb->OperationFlags, SL_OPEN_TARGET_DIRECTORY)) { + return FLT_PREOP_SUCCESS_NO_CALLBACK; + } + // Get the instance context // If the instance is blocked, reject all calls status = FltGetInstanceContext( From 007e20874e30d8d56b4c54dfd8c0e00c918c85c5 Mon Sep 17 00:00:00 2001 From: Luc Date: Thu, 27 Apr 2023 16:01:12 +0200 Subject: [PATCH 054/160] Started file validation --- keysas-usbfilter/daemon/Cargo.toml | 13 +- keysas-usbfilter/daemon/src/main.rs | 6 +- .../daemon/src/windows_driver_interface.rs | 509 ++++++++++++++++-- .../minifilter/keysasCommunication.h | 1 + keysas-usbfilter/minifilter/keysasFile.c | 15 +- keysas-usbfilter/minifilter/keysasInstance.c | 120 ++++- keysas-usbfilter/minifilter/keysasInstance.h | 8 + 7 files changed, 615 insertions(+), 57 deletions(-) diff --git a/keysas-usbfilter/daemon/Cargo.toml b/keysas-usbfilter/daemon/Cargo.toml index c8fd0d0..16a6963 100644 --- a/keysas-usbfilter/daemon/Cargo.toml +++ b/keysas-usbfilter/daemon/Cargo.toml @@ -16,6 +16,15 @@ anyhow = "1.0" simple_logger = "4.1" log = "0.4" wchar = "0.11" +mbrman = "0.5" +libc = "0.2" +serde_derive = "1.0" +serde = "1.0" +serde_json = "1.0" +x509-cert = "0.2" +ed25519-dalek = "1.0" +base64 = "0.21" +pkcs8 = {version = "0.10", features = ["encryption", "pem"] } [dependencies.windows] version = "0.48.0" @@ -24,6 +33,8 @@ features = [ "Win32_Security", "Win32_Storage_InstallableFileSystems", "Win32_System_IO", + "Win32_System_Ioctl", "Win32_System_Threading", - "Win32_UI_WindowsAndMessaging" + "Win32_UI_WindowsAndMessaging", + "Win32_Storage_FileSystem" ] \ No newline at end of file diff --git a/keysas-usbfilter/daemon/src/main.rs b/keysas-usbfilter/daemon/src/main.rs index 1da9bbc..12166ae 100644 --- a/keysas-usbfilter/daemon/src/main.rs +++ b/keysas-usbfilter/daemon/src/main.rs @@ -24,8 +24,9 @@ #![warn(overflowing_literals)] #![warn(deprecated)] #![warn(unused_imports)] - #![feature(vec_into_raw_parts)] +#![feature(is_some_and)] +#![feature(str_split_remainder)] pub mod driver_interface; pub mod windows_driver_interface; @@ -34,6 +35,9 @@ use crate::driver_interface::init_driver_com; use anyhow::anyhow; +#[macro_use] +extern crate serde_derive; + fn main() -> Result<(), anyhow::Error> { // Initialize the logger simple_logger::init()?; diff --git a/keysas-usbfilter/daemon/src/windows_driver_interface.rs b/keysas-usbfilter/daemon/src/windows_driver_interface.rs index 22560ab..dbe9170 100644 --- a/keysas-usbfilter/daemon/src/windows_driver_interface.rs +++ b/keysas-usbfilter/daemon/src/windows_driver_interface.rs @@ -26,47 +26,126 @@ #![warn(unused_imports)] use anyhow::anyhow; +use libc::c_void; +use std::ffi::OsStr; +use std::fs::File; +use std::io::BufReader; +use std::io::Read; use std::mem::size_of; +use std::path::PathBuf; +use std::path::{Component, Path}; use std::thread; use widestring::U16CString; -use windows::core::{PCWSTR, PCSTR}; -use windows::Win32::Foundation::{CloseHandle, BOOLEAN, HANDLE, STATUS_SUCCESS}; +use windows::core::{PCSTR, PCWSTR}; +use windows::s; +use windows::Win32::Foundation::{ + CloseHandle, GetLastError, BOOL, BOOLEAN, HANDLE, STATUS_SUCCESS, STATUS_UNSUCCESSFUL, +}; +use windows::Win32::Storage::FileSystem::{ + CreateFileA, ReadFile, SetFilePointer, FILE_ATTRIBUTE_NORMAL, FILE_BEGIN, + FILE_FLAGS_AND_ATTRIBUTES, FILE_FLAG_BACKUP_SEMANTICS, FILE_SHARE_READ, FILE_SHARE_WRITE, + IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, OPEN_EXISTING, +}; use windows::Win32::Storage::InstallableFileSystems::{ FilterConnectCommunicationPort, FilterGetMessage, FilterReplyMessage, FILTER_MESSAGE_HEADER, FILTER_REPLY_HEADER, }; +use windows::Win32::System::Ioctl::VOLUME_DISK_EXTENTS; +use windows::Win32::System::IO::DeviceIoControl; use windows::Win32::UI::WindowsAndMessaging::*; -use windows::s; +use base64::{engine::general_purpose, Engine as _}; +use x509_cert::Certificate; +use ed25519_dalek::{self, Digest, Sha512}; +use pkcs8::der::{DecodePem, EncodePem}; +//use oqs::sig::{Algorithm, Sig}; // Operation code for the request to userland #[derive(Debug)] enum KeysasFilterOperation { - ScanFile = 0, // Validate the signature of the file and the report - UserAllowFile, // Ask user to allow the file - UserAllowAllUsb, // Ask user to allow complete access the USB drive - UserAllowUsbWithWarning // Ask user to allow access to USB drive with warning on file opening + ScanFile = 0, // Validate the signature of the file and the report + UserAllowFile, // Ask user to allow the file + ScanUsb, // Ask to validate the USB drive signature + UserAllowAllUsb, // Ask user to allow complete access the USB drive + UserAllowUsbWithWarning, // Ask user to allow access to USB drive with warning on file opening } #[derive(Debug)] #[repr(C)] -struct DriverMessage { +struct DriverRequest { header: FILTER_MESSAGE_HEADER, operation: KeysasFilterOperation, - path: [u16; 1024], + content: [u16; 1024], } #[derive(Debug)] #[repr(C)] struct UserReply { header: FILTER_REPLY_HEADER, - file_safe: BOOLEAN, + result: BOOLEAN, } -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct WindowsDriverInterface { handle: HANDLE, } +/// Metadata object in the report. +/// The structure can be serialized to JSON. +#[derive(Debug, Serialize, Deserialize, Clone)] +struct MetaData { + /// Name of the file + name: String, + /// Date of the report creation + date: String, + /// Type of the file + file_type: String, + /// True if the file is correct + is_valid: bool, + /// Object containing the detailled [FileReport] + report: FileReport, +} + +/// Signature binding the file and the report. +/// the structure can be serialized to JSON. +#[derive(Debug, Serialize, Deserialize, Clone)] +struct Bd { + /// SHA256 digest of the file encoded in base64 + file_digest: String, + /// SHA256 digest of the [MetaData] associated to the file + metadata_digest: String, + /// Station certificates: concatenation of its ED25519 and Dilithium5 signing certificates with a '|' delimiter + station_certificate: String, + /// Report signature: concatenation of the ED25519 and Dilithium5 signatures in base64 + report_signature: String, +} + +/// Report that will be created for each file. +/// The structure can be serialized to JSON. +#[derive(Debug, Serialize, Deserialize, Clone)] +struct Report { + /// [MetaData] of the file analysis + metadata: MetaData, + /// [Bd] binding of the file and the report with the station signature + binding: Bd, +} + +/// Detailed report of the file checks. +#[derive(Debug, Serialize, Deserialize, Clone)] +struct FileReport { + /// Detailed report of the yara checks + yara: String, + /// Detailed report of the clamav checks + av: Vec, + /// True if the file type is allowed + type_allowed: bool, + /// Size of the file + size: u64, + /// True if a file corruption occured during the file processing + corrupted: bool, + /// True if the file size is too big + toobig: bool, +} + /// Name of the communication port with the driver const DRIVER_COM_PORT: &str = "\\KeysasPort"; @@ -100,61 +179,79 @@ impl WindowsDriverInterface { pub fn start_driver_com(&self, _cb: fn() -> ()) -> Result<(), anyhow::Error> { let handle = self.handle.clone(); thread::spawn(move || -> Result<(), anyhow::Error> { - let request_size = u32::try_from(size_of::())?; + let request_size = u32::try_from(size_of::())?; let reply_size = u32::try_from(size_of::())? + u32::try_from(size_of::())?; loop { - let mut message = DriverMessage { + // Wait for a request from the driver + let mut request = DriverRequest { header: FILTER_MESSAGE_HEADER::default(), operation: KeysasFilterOperation::ScanFile, - path: [0; 1024], + content: [0; 1024], }; unsafe { - FilterGetMessage(handle, &mut message.header, request_size, None)?; - } - - println!("{:?}", message); - println!("Path: {:?}", String::from_utf16(&message.path)?); - - let file_path = String::from_utf16(&message.path)?; - let filename = format!("Allow file: {:?}", file_path.trim_matches(char::from(0))); - let (name_ptr, _, _) = filename.into_raw_parts(); - let mut authorization_status = MESSAGEBOX_RESULT::default(); - unsafe { - authorization_status = MessageBoxA( - None, - PCSTR::from_raw(name_ptr), - s!("Keysas USB Filter"), - MB_YESNO | MB_ICONWARNING | MB_SYSTEMMODAL - ); + if let Err(_) = + FilterGetMessage(handle, &mut request.header, request_size, None) + { + println!("Failed to get message from driver"); + continue; + } } - let mut reply = match authorization_status { - IDYES => UserReply { - header: FILTER_REPLY_HEADER::default(), - file_safe: BOOLEAN::from(true), - }, - IDNO => UserReply { - header: FILTER_REPLY_HEADER::default(), - file_safe: BOOLEAN::from(false), + // Convert the request to Rust String + let content = match String::from_utf16(&request.content) { + Ok(c) => c, + Err(_) => { + println!("Failed to convert request to string"); + // Send error response to driver + let reply = UserReply { + header: FILTER_REPLY_HEADER { + MessageId: request.header.MessageId, + Status: STATUS_UNSUCCESSFUL, }, - _ => { - println!("Unknown Authorization: {:?}", authorization_status); - // By default block the access to the file - UserReply { - header: FILTER_REPLY_HEADER::default(), - file_safe: BOOLEAN::from(false), + result: BOOLEAN::from(false), + }; + unsafe { + if let Err(_) = FilterReplyMessage(handle, &reply.header, reply_size) { + println!("Failed to send response to driver"); + } } + continue; } }; - reply.header.MessageId = message.header.MessageId; - reply.header.Status = STATUS_SUCCESS; + // Dispatch the request + let result = match request.operation { + KeysasFilterOperation::ScanFile | KeysasFilterOperation::UserAllowFile => { + match authorize_file(request.operation, &content) { + Ok(true) => true, + _ => false, + } + } + KeysasFilterOperation::ScanUsb => true /*match authorize_usb(&content) { + Ok(true) => true, + _ => false, + }*/, + KeysasFilterOperation::UserAllowAllUsb => true, + KeysasFilterOperation::UserAllowUsbWithWarning => true, + }; + + // Prepare the response and send it + let reply = UserReply { + header: FILTER_REPLY_HEADER { + MessageId: request.header.MessageId, + Status: STATUS_SUCCESS, + }, + result: BOOLEAN::from(result), + }; unsafe { - FilterReplyMessage(handle, &reply.header, reply_size)?; + if let Err(_) = FilterReplyMessage(handle, &reply.header, reply_size) { + println!("Failed to send response to driver"); + continue; + } } println!("Sent response"); @@ -170,3 +267,319 @@ impl WindowsDriverInterface { } } } + +fn authorize_usb(content: &str) -> Result { + println!("Received USB scan request: {:?}", content); + let mut device = HANDLE::default(); + let mut buffer: [u8; 4096] = [0; 4096]; + let mut byte_read: u32 = 0; + + // Open the device on the first sector + unsafe { + device = match CreateFileA( + s!("\\\\.\\D:"), + 1179785u32, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + None, + ) { + Ok(d) => d, + Err(_) => { + println!("Failed to open device"); + let err = GetLastError(); + println!("Error: {:?}", err.to_hresult().message().to_string_lossy()); + return Err(anyhow!("Failed to open device")); + } + } + } + + if device.is_invalid() { + println!("Invalid device handle"); + return Err(anyhow!("Invalid device handle")); + } + + let mut vde = VOLUME_DISK_EXTENTS::default(); + let mut dw: u32 = 0; + let mut res = BOOL::from(false); + + unsafe { + res = DeviceIoControl( + device, + IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, + None, + 0, + Some(&mut vde as *mut _ as *mut c_void), + u32::try_from(size_of::())?, + Some(&mut dw), + None, + ); + } + + let mut drive_path = String::from("\\\\.\\PhysicalDrive"); + drive_path.push_str(&vde.Extents[0].DiskNumber.to_string()); + + println!("Physical Drive path: {:?}", drive_path); + + let drive_str = PCSTR::from_raw(drive_path.as_ptr() as *const u8); + unsafe { + println!("Physical Drive path windows: {:?}", drive_str.to_string()?); + } + + let mut handle_usb = HANDLE::default(); + unsafe { + handle_usb = match CreateFileA( + drive_str, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + None, + ) { + Ok(d) => d, + Err(_) => { + println!("Failed to open usb"); + let err = GetLastError(); + println!("Error: {:?}", err.to_hresult().message().to_string_lossy()); + return Err(anyhow!("Failed to open usb")); + } + } + } + + if handle_usb.is_invalid() { + println!("Invalid device usb handle"); + return Err(anyhow!("Invalid device usb handle")); + } + + // Move the file pointer after the MBR table (512B) + // and read the signature content + let mut read = BOOL::from(false); + unsafe { + //SetFilePointer(device, 512, None, FILE_BEGIN); + read = ReadFile( + handle_usb, + Some(buffer.as_mut_ptr() as *mut c_void), + 4096, + Some(&mut byte_read), + None, + ); + } + + if read.as_bool() { + println!("Device content: {:?}", buffer); + } else { + println!("Failed to read device content"); + unsafe { + let err = GetLastError(); + println!("Error: {:?}", err.to_hresult().message().to_string_lossy()); + } + } + + Ok(true) +} + +fn authorize_file(op: KeysasFilterOperation, content: &str) -> Result { + let file_path = Path::new(content.trim_matches(char::from(0))); + + // Try to get the parent directory + let mut components = file_path.components(); + println!("Path components: {:?}", components); + + // First component is the Root Directory + // If the second directory is "System Volume Information" then it is internal to windows, skip it + loop { + let c = components.next(); + if c == Some(Component::RootDir) { + break; + } + } + + if components.next() == Some(Component::Normal(OsStr::new("System Volume Information"))) { + return Ok(true); + } + + // Skip the directories + if file_path.metadata()?.is_dir() { + return Ok(true); + } + + // Try to validate the file from the station report + match validate_file(file_path) { + Ok(true) => { + return Ok(true); + } + _ => { + println!("File not validated by station"); + } + } + + // If the validation fails, ask the user authorization + return user_authorize_file(file_path); +} + +fn validate_file(path: &Path) -> Result { + // Test if the file is a station report + if path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("krp")) + { + // If yes validate it alone + return validate_report_alone(path); + } + + // If not try to find the corresponding report + // It should be in the same directory with the same name + '.krp' + let report_path = path.to_path_buf().join(".krp"); + match File::open(report_path.as_path()) { + Ok(report) => { + // If a corresponding report is found then validate both the file and the report + return validate_file_and_report(path, report_path.as_path()); + } + Err(_) => { + // There is no corresponding report for validating the file + return Ok(false); + } + } +} + +fn parse_report(report_path: &Path) -> Result { + let report_content = match std::fs::read_to_string(report_path) { + Ok(ct) => ct, + Err(_) => { + println!("Failed to read report content"); + return Err(anyhow!("Failed to read report content")); + } + }; + let report: Report = serde_json::from_str(report_content.as_str())?; + + println!("Report: {:?}", report); + + let mut certs = report.binding.station_certificate.split('|'); + // TODO: remove unwraps + let cert_cl = Certificate::from_pem(certs.next().unwrap())?; + let cert_pq = Certificate::from_pem(certs.remainder().unwrap())?; + + let pub_cl = ed25519_dalek::PublicKey::from_bytes( + cert_cl + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + )?; +/* + oqs::init(); + let pq_scheme = Sig::new(Algorithm::Dilithium5).unwrap(); + let pub_pq = pq_scheme + .public_key_from_bytes( + cert_pq + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + ) + .unwrap(); +*/ + // Verify the signature of the report + let signature = general_purpose::STANDARD + .decode(&report.binding.report_signature)?; + let concat = format!( + "{}-{}", + String::from_utf8( + general_purpose::STANDARD + .decode(&report.binding.file_digest)? + )?, + String::from_utf8( + general_purpose::STANDARD + .decode(&report.binding.metadata_digest)? + )? + ); + + let mut prehashed = Sha512::new(); + prehashed.update(&concat); + /* + assert_eq!( + true, + pub_cl + .verify_prehashed( + prehashed, + None, + &ed25519_dalek::Signature::from_bytes( + &signature[0..ed25519_dalek::SIGNATURE_LENGTH] + ) + .unwrap() + ) + .is_ok() + ); + + assert_eq!( + true, + pq_scheme + .verify( + concat.as_bytes(), + pq_scheme + .signature_from_bytes(&signature[ed25519_dalek::SIGNATURE_LENGTH..]) + .unwrap(), + pub_pq + ) + .is_ok() + ); + */ + Ok(report) +} + +fn validate_report_alone(report_path: &Path) -> Result { + let report = match parse_report(report_path) { + Ok(rp) => rp, + Err(e) => { + println!("Failed to parse report"); + return Ok(false); + } + }; + Ok(true) +} + +fn validate_file_and_report(file: &Path, report_path: &Path) -> Result { + let report = match parse_report(report_path) { + Ok(rp) => rp, + Err(e) => { + println!("Failed to parse report"); + return Ok(false); + } + }; + + // Compute hash of the file + Ok(true) +} + +fn user_authorize_file(path: &Path) -> Result { + // Find authorization status for the file + let mut authorization_status = MESSAGEBOX_RESULT::default(); + let auth_request = format!("Allow file: {:?}", path.as_os_str()); + let (auth_request_ptr, _, _) = auth_request.into_raw_parts(); + + unsafe { + authorization_status = MessageBoxA( + None, + PCSTR::from_raw(auth_request_ptr), + s!("Keysas USB Filter"), + MB_YESNO | MB_ICONWARNING | MB_SYSTEMMODAL, + ); + } + + match authorization_status { + IDYES => { + return Ok(true); + } + IDNO => { + return Ok(false); + } + _ => { + return Err(anyhow!(format!( + "Unknown Authorization: {:?}", + authorization_status + ))); + } + }; +} diff --git a/keysas-usbfilter/minifilter/keysasCommunication.h b/keysas-usbfilter/minifilter/keysasCommunication.h index 3a8fe36..352e648 100644 --- a/keysas-usbfilter/minifilter/keysasCommunication.h +++ b/keysas-usbfilter/minifilter/keysasCommunication.h @@ -32,6 +32,7 @@ Module Name: typedef enum _KEYSAS_FILTER_OPERATION { SCAN_FILE = 0, // Validate the signature of the file and the report USER_ALLOW_FILE, // Ask user to allow the file + SCAN_USB, // Ask to validate the USB drive signature USER_ALLOW_USB_ALL, // Ask user to allow complete access the USB drive USER_ALLOW_USB_WITH_WARNING // Ask user to allow access to USB drive with warning on file opening } KEYSAS_FILTER_OPERATION; diff --git a/keysas-usbfilter/minifilter/keysasFile.c b/keysas-usbfilter/minifilter/keysasFile.c index f592fa2..7c6bf74 100644 --- a/keysas-usbfilter/minifilter/keysasFile.c +++ b/keysas-usbfilter/minifilter/keysasFile.c @@ -353,6 +353,7 @@ Return Value: BOOLEAN contextCreated = FALSE; KEYSAS_FILTER_OPERATION operation = SCAN_FILE; PKEYSAS_INSTANCE_CTX instanceContext = NULL; + POBJECT_NAME_INFORMATION msFileName = NULL; UNREFERENCED_PARAMETER(FltObjects); UNREFERENCED_PARAMETER(CompletionContext); @@ -400,8 +401,9 @@ Return Value: // If the authorization state is unknown then try to acquire the lock in write mode to scan the file if (!AcquireResourceRead(fileContext->Resource)) { KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Failed to acquire ressource in read mode\n")); + status = FLT_POSTOP_FINISHED_PROCESSING; + goto cleanup; } - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Acquired ressource in read mode\n")); if (AUTH_UNKNOWN == fileContext->Authorization) { // The authorization status is not known for this file @@ -409,8 +411,9 @@ Return Value: ReleaseResource(fileContext->Resource); if (!AcquireResourceWrite(fileContext->Resource)) { KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Failed to acquire ressource in write mode\n")); + status = FLT_POSTOP_FINISHED_PROCESSING; + goto cleanup; } - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Acquired ressource in write mode\n")); // Test the authorization again as it can have been preempted if (AUTH_UNKNOWN == fileContext->Authorization) { fileContext->Authorization = AUTH_PENDING; @@ -450,8 +453,9 @@ Return Value: ReleaseResource(instanceContext->Resource); // Send the file to further analysis in user space KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPostCreateHandler: Send request to userspace\n")); + IoQueryFileDosDeviceName(FltObjects->FileObject, &msFileName); (VOID)KeysasScanFileInUserMode( - &nameInfo->Name, + &msFileName->Name, operation, &safeToOpen ); @@ -539,8 +543,7 @@ Routine Description: Arguments: FileName - Name of the file. It should be NORMALIZED thus the complete path is given Operation - Operation code for the user app - SafeToOpen - Set to FALSE if the file is scanned successfully and it contains - foul language. + SafeToOpen - Set to TRUE if the file is valid Return Value: The status of the operation, hopefully STATUS_SUCCESS. The common failure status will probably be STATUS_INSUFFICIENT_RESOURCES. @@ -592,7 +595,7 @@ Return Value: KeysasData.Filter, &KeysasData.ClientPort, request, - sizeof(request->Content), + FileName->Length+sizeof(KEYSAS_FILTER_OPERATION), request, &replyLength, NULL diff --git a/keysas-usbfilter/minifilter/keysasInstance.c b/keysas-usbfilter/minifilter/keysasInstance.c index 3fa9580..a39cb39 100644 --- a/keysas-usbfilter/minifilter/keysasInstance.c +++ b/keysas-usbfilter/minifilter/keysasInstance.c @@ -34,6 +34,7 @@ Module Name: #pragma alloc_text(PAGE, KfInstanceTeardownComplete) #pragma alloc_text(PAGE, KfInstanceTeardownStart) #pragma alloc_text(PAGE, FindInstanceContext) +#pragma alloc_text(PAGE, KeysasScanInstanceInUserMode) #endif NTSTATUS @@ -153,6 +154,93 @@ Return Value: return status; } +NTSTATUS +KeysasScanInstanceInUserMode( + _In_ PUNICODE_STRING InstanceName, + _In_ KEYSAS_FILTER_OPERATION Operation, + _Out_ PBOOLEAN SafeToOpen +) +/*++ +Routine Description: + This routine is called to send a request up to user mode to scan a given + instance and tell our caller whether it's safe to open it. +Arguments: + FileName - Name of the file. It should be NORMALIZED thus the complete path is given + Operation - Operation code for the user app + SafeToOpen - Set to TRUE if the instance is valid +Return Value: + The status of the operation, hopefully STATUS_SUCCESS. The common failure + status will probably be STATUS_INSUFFICIENT_RESOURCES. +--*/ + +{ + NTSTATUS status = STATUS_SUCCESS; + PKEYSAS_DRIVER_REQUEST request = NULL; + ULONG replyLength = 0; + + // Set default authorization to true + *SafeToOpen = TRUE; + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanInstanceInUserMode: Entered\n")); + + if (NULL == KeysasData.ClientPort) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanInstanceInUserMode: Invalid client port\n")); + return status; + } + + if (InstanceName->Length > (KEYSAS_REQUEST_BUFFER_SIZE - 1)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanInstanceInUserMode: File name too long\n")); + } + + // Allocate request buffer + request = ExAllocatePoolZero( + NonPagedPool, + sizeof(KEYSAS_DRIVER_REQUEST), + KEYSAS_MEMORY_TAG + ); + + if (NULL == request) { + status = STATUS_INSUFFICIENT_RESOURCES; + goto end; + } + + // Copy the name of the file in the request + status = RtlStringCbCopyUnicodeString(request->Content, KEYSAS_REQUEST_BUFFER_SIZE * sizeof(WCHAR), InstanceName); + if (STATUS_SUCCESS != status) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanInstanceInUserMode: Failed to convert UNICODE_STRING\n")); + goto end; + } + request->Operation = Operation; + + replyLength = sizeof(request); + + // Send request to userspace + status = FltSendMessage( + KeysasData.Filter, + &KeysasData.ClientPort, + request, + sizeof(request->Content), + request, + &replyLength, + NULL + ); + + if (STATUS_SUCCESS == status) { + *SafeToOpen = ((PKEYSAS_REPLY)request)->Result; + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanInstanceInUserMode: Received result\n")); + } + else { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KeysasScanInstanceInUserMode: Failed to send request to userspace\n")); + } + +end: + if (NULL != request) { + ExFreePoolWithTag(request, KEYSAS_MEMORY_TAG); + } + + return status; +} + NTSTATUS KfInstanceSetup( _In_ PCFLT_RELATED_OBJECTS FltObjects, @@ -202,6 +290,9 @@ Return Value: ULONG SizeNeeded, RetLength, OutputLength, SizeRequired; PKEYSAS_INSTANCE_CTX instanceContext = NULL; BOOLEAN instanceCreated = FALSE; + wchar_t nameBuffer[512] = { 0 }; + UNICODE_STRING volumeName = { 0, sizeof(nameBuffer)-sizeof(wchar_t), nameBuffer}; + BOOLEAN instanceValid = TRUE; // Print debug info on the call context KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: Entered\n")); @@ -273,6 +364,8 @@ Return Value: goto end; } + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: Storage property size = %lu\n", RetLength)); + pStorageDesciptor = (PSTORAGE_ADAPTER_DESCRIPTOR)buffer; if (pStorageDesciptor->BusType == BusTypeUsb) { KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: USB descriptor found attach\n")); @@ -288,11 +381,33 @@ Return Value: KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: FindInstanceContext failed with status = %0x8x\n", status)); status = STATUS_FLT_DO_NOT_ATTACH; goto end; - } // TODO: default set instance to ALLOW AcquireResourceWrite(instanceContext->Resource); + + status = FltGetVolumeName(FltObjects->Volume, &volumeName, NULL); + + if (!NT_SUCCESS(status)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: FltGetVolumeName failed with status = %0x8x\n", status)); + status = STATUS_FLT_DO_NOT_ATTACH; + goto end; + } + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: Volume name = %wZ\n", volumeName)); + + status = KeysasScanInstanceInUserMode( + &volumeName, + SCAN_USB, + &instanceValid + ); + + if (!NT_SUCCESS(status)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: KeysasScanInstanceInUserMode failed with status = %0x8x\n", status)); + status = STATUS_FLT_DO_NOT_ATTACH; + goto end; + } + instanceContext->Authorization = AUTH_ALLOW_WARNING; ReleaseResource(instanceContext->Resource); @@ -324,6 +439,9 @@ Return Value: } if (instanceContext != NULL) { + if (NULL != instanceContext->Resource) { + ReleaseResource(instanceContext->Resource); + } FltReleaseContext(instanceContext); instanceContext = NULL; } diff --git a/keysas-usbfilter/minifilter/keysasInstance.h b/keysas-usbfilter/minifilter/keysasInstance.h index 98497ce..d4a9f9e 100644 --- a/keysas-usbfilter/minifilter/keysasInstance.h +++ b/keysas-usbfilter/minifilter/keysasInstance.h @@ -22,6 +22,7 @@ Module Name: #define _H_KEYSAS_INSTANCE_ #include "keysasDriver.h" +#include "keysasCommunication.h" // Instance context data structure typedef struct _KEYSAS_INSTANCE_CTX { @@ -73,4 +74,11 @@ FindInstanceContext( _Out_opt_ PBOOLEAN ContextCreated ); +NTSTATUS +KeysasScanInstanceInUserMode( + _In_ PUNICODE_STRING FileName, + _In_ KEYSAS_FILTER_OPERATION Operation, + _Out_ PBOOLEAN SafeToOpen +); + #endif _H_KEYSAS_INSTANCE_ \ No newline at end of file From 9bb22d0e761b9804aa758a092e1fe96ff94407bd Mon Sep 17 00:00:00 2001 From: Luc Date: Fri, 28 Apr 2023 09:59:23 +0200 Subject: [PATCH 055/160] Run analysis on driver and initialized tray app --- .gitignore | 8 ++- keysas-usbfilter/daemon/Cargo.toml | 7 ++- .../daemon/src/windows_driver_interface.rs | 6 +- .../minifilter/KeysasDriver.vcxproj | 3 + keysas-usbfilter/minifilter/keysasFile.c | 5 +- keysas-usbfilter/minifilter/keysasInstance.c | 20 +++++-- .../tray-app/src-tauri/Cargo.toml | 2 +- .../tray-app/src-tauri/src/main.rs | 55 ++++++++++++++++++- .../tray-app/src-tauri/tauri.conf.json | 13 ++--- 9 files changed, 97 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 9aa63c1..0cb4aad 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ keysas-admin/src-tauri/.keysas.dat .vs/ x64/ x86/ +sdv/ +minifilterx64/ .vscode/c_cpp_properties.json # Logs logs @@ -43,4 +45,8 @@ dist-ssr *.ntvs* *.njsproj *.sln -*.sw? \ No newline at end of file +*.sw? +keysas-usbfilter/minifilter/runsdvui.cmd +keysas-usbfilter/minifilter/SDV-default.xml +keysas-usbfilter/minifilter/sdv-user.sdv +keysas-usbfilter/minifilter/smvstats.txt diff --git a/keysas-usbfilter/daemon/Cargo.toml b/keysas-usbfilter/daemon/Cargo.toml index 16a6963..3f52f0b 100644 --- a/keysas-usbfilter/daemon/Cargo.toml +++ b/keysas-usbfilter/daemon/Cargo.toml @@ -37,4 +37,9 @@ features = [ "Win32_System_Threading", "Win32_UI_WindowsAndMessaging", "Win32_Storage_FileSystem" -] \ No newline at end of file +] + +[dependencies.oqs] +version = "0.7" +default-features = false +features = ["dilithium"] \ No newline at end of file diff --git a/keysas-usbfilter/daemon/src/windows_driver_interface.rs b/keysas-usbfilter/daemon/src/windows_driver_interface.rs index dbe9170..d163b91 100644 --- a/keysas-usbfilter/daemon/src/windows_driver_interface.rs +++ b/keysas-usbfilter/daemon/src/windows_driver_interface.rs @@ -57,7 +57,7 @@ use base64::{engine::general_purpose, Engine as _}; use x509_cert::Certificate; use ed25519_dalek::{self, Digest, Sha512}; use pkcs8::der::{DecodePem, EncodePem}; -//use oqs::sig::{Algorithm, Sig}; +use oqs::sig::{Algorithm, Sig}; // Operation code for the request to userland #[derive(Debug)] @@ -468,7 +468,7 @@ fn parse_report(report_path: &Path) -> Result { .subject_public_key .raw_bytes(), )?; -/* + oqs::init(); let pq_scheme = Sig::new(Algorithm::Dilithium5).unwrap(); let pub_pq = pq_scheme @@ -480,7 +480,7 @@ fn parse_report(report_path: &Path) -> Result { .raw_bytes(), ) .unwrap(); -*/ + // Verify the signature of the report let signature = general_purpose::STANDARD .decode(&report.binding.report_signature)?; diff --git a/keysas-usbfilter/minifilter/KeysasDriver.vcxproj b/keysas-usbfilter/minifilter/KeysasDriver.vcxproj index 92555db..aa806d1 100644 --- a/keysas-usbfilter/minifilter/KeysasDriver.vcxproj +++ b/keysas-usbfilter/minifilter/KeysasDriver.vcxproj @@ -92,6 +92,9 @@ sha256 + + $(DDK_LIB_PATH)fltMgr.lib;%(AdditionalDependencies) + diff --git a/keysas-usbfilter/minifilter/keysasFile.c b/keysas-usbfilter/minifilter/keysasFile.c index 7c6bf74..7999701 100644 --- a/keysas-usbfilter/minifilter/keysasFile.c +++ b/keysas-usbfilter/minifilter/keysasFile.c @@ -35,6 +35,7 @@ Module Name: #pragma alloc_text(PAGE, FindFileContext) #pragma alloc_text(PAGE, KfPostCreateHandler) #pragma alloc_text(PAGE, KfPreCreateHandler) +#pragma alloc_text(PAGE, KeysasScanFileInUserMode) #endif VOID @@ -554,6 +555,8 @@ Return Value: PKEYSAS_DRIVER_REQUEST request = NULL; ULONG replyLength = 0; + PAGED_CODE(); + // Set default authorization to true *SafeToOpen = TRUE; @@ -588,7 +591,7 @@ Return Value: } request->Operation = Operation; - replyLength = sizeof(request); + replyLength = sizeof(*request); // Send request to userspace status = FltSendMessage( diff --git a/keysas-usbfilter/minifilter/keysasInstance.c b/keysas-usbfilter/minifilter/keysasInstance.c index a39cb39..83d2875 100644 --- a/keysas-usbfilter/minifilter/keysasInstance.c +++ b/keysas-usbfilter/minifilter/keysasInstance.c @@ -178,6 +178,8 @@ Return Value: PKEYSAS_DRIVER_REQUEST request = NULL; ULONG replyLength = 0; + PAGED_CODE(); + // Set default authorization to true *SafeToOpen = TRUE; @@ -212,7 +214,7 @@ Return Value: } request->Operation = Operation; - replyLength = sizeof(request); + replyLength = sizeof(*request); // Send request to userspace status = FltSendMessage( @@ -274,6 +276,7 @@ Return Value: { UNREFERENCED_PARAMETER(Flags); UNREFERENCED_PARAMETER(VolumeFilesystemType); + UNREFERENCED_PARAMETER(VolumeDeviceType); PAGED_CODE(); @@ -312,7 +315,10 @@ Return Value: // Create a query and get the storage descriptor header SizeNeeded = max(sizeof(STORAGE_DESCRIPTOR_HEADER), sizeof(STORAGE_PROPERTY_QUERY)); OutputBuffer = (PSTORAGE_PROPERTY_QUERY)ExAllocatePool2(POOL_FLAG_NON_PAGED, SizeNeeded, KEYSAS_MEMORY_TAG); - ASSERT(OutputBuffer != NULL); + if (NULL == OutputBuffer) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: ExAllocatePool2 failed\n")); + goto end; + } RtlZeroMemory(OutputBuffer, SizeNeeded); @@ -337,11 +343,17 @@ Return Value: // Get the header size and update the query with the correct size OutputLength = HeaderDescriptor.Size; - ASSERT(OutputLength >= sizeof(STORAGE_DESCRIPTOR_HEADER)); + if (OutputLength < sizeof(STORAGE_DESCRIPTOR_HEADER)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: Invalid output length\n")); + goto end; + } SizeRequired = max(OutputLength, sizeof(STORAGE_PROPERTY_QUERY)); buffer = (PSTORAGE_PROPERTY_QUERY)ExAllocatePool2(POOL_FLAG_NON_PAGED, SizeRequired, KEYSAS_MEMORY_TAG); - ASSERT(buffer != NULL); + if (NULL == buffer) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfInstanceSetup: ExAllocatePool2 failed\n")); + goto end; + } RtlZeroMemory(buffer, SizeRequired); pQuery = (PSTORAGE_PROPERTY_QUERY)buffer; diff --git a/keysas-usbfilter/tray-app/src-tauri/Cargo.toml b/keysas-usbfilter/tray-app/src-tauri/Cargo.toml index 70d636d..8ec6bbc 100644 --- a/keysas-usbfilter/tray-app/src-tauri/Cargo.toml +++ b/keysas-usbfilter/tray-app/src-tauri/Cargo.toml @@ -14,7 +14,7 @@ edition = "2021" tauri-build = { version = "1.2", features = [] } [dependencies] -tauri = { version = "1.2", features = ["shell-open"] } +tauri = { version = "1.2", features = ["shell-open", "system-tray"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" anyhow = "1.0" diff --git a/keysas-usbfilter/tray-app/src-tauri/src/main.rs b/keysas-usbfilter/tray-app/src-tauri/src/main.rs index 01d9702..19e96e7 100644 --- a/keysas-usbfilter/tray-app/src-tauri/src/main.rs +++ b/keysas-usbfilter/tray-app/src-tauri/src/main.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* + * * (C) Copyright 2019-2023 Luc Bonnafoux, Stephane Neveu * */ @@ -24,6 +25,8 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use tauri::{SystemTray, CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem, SystemTrayEvent}; + // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command #[tauri::command] fn greet(name: &str) -> String { @@ -39,8 +42,56 @@ fn main() -> Result<(), anyhow::Error> { // Initialize the tauri application as a system tray app fn init_tauri() -> Result<(), anyhow::Error> { + let quit = CustomMenuItem::new("quit".to_string(), "Quit"); + let hide = CustomMenuItem::new("hide".to_string(), "Hide"); + let tray_menu = SystemTrayMenu::new() + .add_item(quit) + .add_native_item(SystemTrayMenuItem::Separator) + .add_item(hide); + let tray = SystemTray::new().with_menu(tray_menu); + tauri::Builder::default() - .invoke_handler(tauri::generate_handler![greet]) - .run(tauri::generate_context!())?; + .system_tray(tray) + .on_system_tray_event(|app, event| match event { + SystemTrayEvent::LeftClick { + position: _, + size: _, + .. + } => { + println!("Left click event"); + } + SystemTrayEvent::RightClick { + position: _, + size: _, + .. + } => { + println!("Right click event"); + } + SystemTrayEvent::DoubleClick { + position: _, + size: _, + .. + } => { + println!("Double click event"); + } + SystemTrayEvent::MenuItemClick { + id, + .. + } => { + match id.as_str() { + "quit" => { + println!("Quit selected"); + }, + "hide" => { + println!("Hide selected"); + }, + _ => {} + } + } + _ => {} + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); + Ok(()) } \ No newline at end of file diff --git a/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json b/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json index 752fe6d..88edc94 100644 --- a/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json +++ b/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json @@ -36,14 +36,9 @@ "updater": { "active": false }, - "windows": [ - { - "fullscreen": false, - "resizable": true, - "title": "keysas-minifilter", - "width": 800, - "height": 600 - } - ] + "systemTray": { + "iconPath": "icons/icon.png", + "iconAsTemplate": true + } } } From b798828ae27eeaa78b80ec42985e1bb5be7d7231 Mon Sep 17 00:00:00 2001 From: Luc Date: Tue, 2 May 2023 10:04:45 +0200 Subject: [PATCH 056/160] Continued file verification --- keysas-admin/src-tauri/Cargo.toml | 2 +- keysas-core/Cargo.toml | 8 +- keysas-core/src/keysas-out/main.rs | 332 +----------- keysas-usbfilter/daemon/Cargo.toml | 15 +- keysas-usbfilter/daemon/src/main.rs | 3 - .../daemon/src/windows_driver_interface.rs | 211 ++------ keysas_lib/Cargo.toml | 7 +- keysas_lib/src/certificate_field.rs | 85 ++- keysas_lib/src/file_report.rs | 510 ++++++++++++++++++ keysas_lib/src/lib.rs | 21 +- 10 files changed, 651 insertions(+), 543 deletions(-) create mode 100644 keysas_lib/src/file_report.rs diff --git a/keysas-admin/src-tauri/Cargo.toml b/keysas-admin/src-tauri/Cargo.toml index a02a520..fe672ff 100644 --- a/keysas-admin/src-tauri/Cargo.toml +++ b/keysas-admin/src-tauri/Cargo.toml @@ -43,7 +43,7 @@ keysas_lib = { path = "../../keysas_lib" } [dependencies.oqs] version = "0.7" default-features = false -features = ["dilithium"] +features = ["std", "dilithium"] [features] # by default Tauri runs in production mode diff --git a/keysas-core/Cargo.toml b/keysas-core/Cargo.toml index 82320ae..80099bf 100644 --- a/keysas-core/Cargo.toml +++ b/keysas-core/Cargo.toml @@ -3,8 +3,6 @@ name = "keysas-core" version = "2.0.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] anyhow = "1.0" bincode ="1.3" @@ -18,17 +16,17 @@ log = "0.4" regex = "1.7" infer = "0.13" clamav-tcp = "0.2" -yara = "0.17" -landlock = "0.2" itertools ="0.10" serde_json = "1.0" -syscallz = "0.16" time = "0.3" base64 = "0.21" ed25519-dalek = "1.0" rand = "0.8" pkcs8 = {version = "0.10", features = ["encryption", "pem"] } x509-cert = "0.2" +landlock = "0.2" +syscallz = "0.16" +yara = "0.17" [dependencies.oqs] version = "0.7" diff --git a/keysas-core/src/keysas-out/main.rs b/keysas-core/src/keysas-out/main.rs index bda90be..17fa361 100644 --- a/keysas-core/src/keysas-out/main.rs +++ b/keysas-core/src/keysas-out/main.rs @@ -69,6 +69,9 @@ use keysas_lib::init_logger; use keysas_lib::keysas_hybrid_keypair::HybridKeyPair; use keysas_lib::keysas_key::KeysasKey; use keysas_lib::sha256_digest; +use keysas_lib::file_report::FileMetadata; +use keysas_lib::file_report::generate_report_metadata; +use keysas_lib::file_report::bind_and_sign; use landlock::{ path_beneath_rules, Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetError, RulesetStatus, ABI, @@ -94,103 +97,15 @@ use time::OffsetDateTime; #[macro_use] extern crate serde_derive; -/// Structure that holds a file metadata -#[derive(Serialize, Deserialize, Debug, Clone)] -struct FileMetadata { - /// Name of the file - filename: String, - /// SHA256 digest of the file - digest: String, - /// True if a file corruption as occured during processing - is_digest_ok: bool, - /// True if the file is toobig - is_toobig: bool, - /// Size of the file - size: u64, - /// True if the file type is valid - is_type_allowed: bool, - /// True if clamav tests pass - av_pass: bool, - /// Detailed report of clamav if the test failed - av_report: Vec, - /// True if yara tests pass - yara_pass: bool, - /// Detailed report of yara if the test failed - yara_report: String, - /// Timestamp of the file entering the station - timestamp: String, - /// True if a file corruption occured during the processing - is_corrupted: bool, - /// Type of the file - file_type: String, -} - /// Structure representing a file and its metadata in the daemon #[derive(Debug)] -struct FileData { +pub struct FileData { /// File descriptor fd: i32, /// Associated file metadata md: FileMetadata, } -/// Metadata object in the report. -/// The structure can be serialized to JSON. -#[derive(Serialize, Deserialize, Clone)] -struct MetaData { - /// Name of the file - name: String, - /// Date of the report creation - date: String, - /// Type of the file - file_type: String, - /// True if the file is correct - is_valid: bool, - /// Object containing the detailled [FileReport] - report: FileReport, -} - -/// Signature binding the file and the report. -/// the structure can be serialized to JSON. -#[derive(Serialize, Deserialize, Clone)] -struct Bd { - /// SHA256 digest of the file encoded in base64 - file_digest: String, - /// SHA256 digest of the [MetaData] associated to the file - metadata_digest: String, - /// Station certificates: concatenation of its ED25519 and Dilithium5 signing certificates with a '|' delimiter - station_certificate: String, - /// Report signature: concatenation of the ED25519 and Dilithium5 signatures in base64 - report_signature: String, -} - -/// Report that will be created for each file. -/// The structure can be serialized to JSON. -#[derive(Serialize, Deserialize, Clone)] -struct Report { - /// [MetaData] of the file analysis - metadata: MetaData, - /// [Bd] binding of the file and the report with the station signature - binding: Bd, -} - -/// Detailed report of the file checks. -#[derive(Serialize, Deserialize, Clone)] -struct FileReport { - /// Detailed report of the yara checks - yara: String, - /// Detailed report of the clamav checks - av: Vec, - /// True if the file type is allowed - type_allowed: bool, - /// Size of the file - size: u64, - /// True if a file corruption occured during the file processing - corrupted: bool, - /// True if the file size is too big - toobig: bool, -} - /// Directory containing the station signing keys const KEY_FILE_DIR: &str = "/etc/keysas"; /// Password for the private signing keys PKCS#8 files @@ -313,84 +228,6 @@ fn parse_messages(messages: Messages, buffer: &[u8]) -> Vec { .collect() } -/// Wrapper around the report metadata creation -fn generate_report_metadata(f: &FileData) -> MetaData { - let timestamp = format!( - "{}-{}-{}_{}-{}-{}-{}", - OffsetDateTime::now_utc().day(), - OffsetDateTime::now_utc().month(), - OffsetDateTime::now_utc().year(), - OffsetDateTime::now_utc().hour(), - OffsetDateTime::now_utc().minute(), - OffsetDateTime::now_utc().second(), - OffsetDateTime::now_utc().nanosecond() - ); - - let new_file_report = FileReport { - yara: f.md.yara_report.clone(), - av: f.md.av_report.clone(), - type_allowed: f.md.is_type_allowed, - size: f.md.size, - corrupted: f.md.is_corrupted, - toobig: f.md.is_toobig, - }; - - MetaData { - name: f.md.filename.clone(), - date: timestamp, - file_type: f.md.file_type.clone(), - is_valid: f.md.av_pass - && f.md.yara_pass - && !f.md.is_toobig - && !f.md.is_corrupted - && f.md.is_digest_ok - && f.md.is_type_allowed, - report: new_file_report, - } -} - -/// Bind the report to file by signing with ED25519 and Dilithium5 the concatenation -/// of the file digest and the report metadata digest. -/// The two signatures are concatenated (ED25519 first). -/// All the fields of the binding are encoded in base64 -fn bind_and_sign( - f: &FileData, - report_meta: &MetaData, - sign_keys: &HybridKeyPair, - sign_cert: &str, -) -> Result { - // Compute digest of report metadata - let json_string = serde_json::to_string(&report_meta)?; - - let mut hasher = Sha256::new(); - hasher.update(json_string.as_bytes()); - let result = hasher.finalize(); - - let meta_digest = format!("{result:x}"); - - // Sign the report and the file - let concat = format!("{}-{}", f.md.digest, meta_digest); - - let mut signature = Vec::new(); - - // Sign with ED25519 - signature.append(&mut sign_keys.classic.message_sign(concat.as_bytes())?); - - // Sign with Dilithium5 - signature.append(&mut sign_keys.pq.message_sign(concat.as_bytes())?); - - // Generate the final report - Ok(Report { - metadata: report_meta.clone(), - binding: Bd { - file_digest: general_purpose::STANDARD.encode(f.md.digest.clone()), - metadata_digest: general_purpose::STANDARD.encode(meta_digest), - station_certificate: sign_cert.to_string(), - report_signature: general_purpose::STANDARD.encode(signature), - }, - }) -} - /// This function output files and report received from transit /// The function first check the digest of the file received fn output_files( @@ -556,164 +393,3 @@ fn main() -> Result<()> { output_files(files, &config, &sign_keys, &sign_cert)?; } } - -#[cfg(test)] -mod tests_out { - use base64::{engine::general_purpose, Engine}; - use ed25519_dalek::{self, Digest, Sha512}; - use keysas_lib::{certificate_field::CertificateFields, keysas_hybrid_keypair::HybridKeyPair}; - use oqs::sig::{Algorithm, Sig}; - use pkcs8::der::{DecodePem, EncodePem}; - use x509_cert::Certificate; - - use crate::{bind_and_sign, generate_report_metadata, FileData, FileMetadata}; - - #[test] - fn test_metadata_valid_file() { - // Generate dummy file data - let file_data = FileData { - fd: 2, - md: FileMetadata { - filename: "test.txt".to_string(), - digest: "00112233445566778899AABBCCDDEEFF".to_string(), - is_digest_ok: true, - is_toobig: false, - size: 42, - is_type_allowed: true, - av_pass: true, - av_report: Vec::new(), - yara_pass: true, - yara_report: "".to_string(), - timestamp: "timestamp".to_string(), - is_corrupted: false, - file_type: "txt".to_string(), - }, - }; - - // Generate report metadata - let meta = generate_report_metadata(&file_data); - - // Validate fields - assert_eq!(file_data.md.filename, meta.name); - assert_eq!(file_data.md.file_type, meta.file_type); - assert_eq!(meta.is_valid, true); - } - - #[test] - fn test_bind_and_sign() { - // Generate temporary keys - let infos = - CertificateFields::from_fields(None, None, None, Some("Test_station"), Some("200")) - .unwrap(); - let sign_keys = HybridKeyPair::generate_root(&infos).unwrap(); - - let mut sign_cert = String::new(); - let pem_cl = sign_keys - .classic_cert - .to_pem(pkcs8::LineEnding::LF) - .unwrap(); - sign_cert.push_str(&pem_cl); - // Add a delimiter between the two certificates - sign_cert.push('|'); - let pem_pq = sign_keys.pq_cert.to_pem(pkcs8::LineEnding::LF).unwrap(); - sign_cert.push_str(&pem_pq); - - // Generate dummy file data - let file_data = FileData { - fd: 2, - md: FileMetadata { - filename: "test.txt".to_string(), - digest: "00112233445566778899AABBCCDDEEFF".to_string(), - is_digest_ok: true, - is_toobig: false, - size: 42, - is_type_allowed: true, - av_pass: true, - av_report: Vec::new(), - yara_pass: true, - yara_report: "".to_string(), - timestamp: "timestamp".to_string(), - is_corrupted: false, - file_type: "txt".to_string(), - }, - }; - - let meta = generate_report_metadata(&file_data); - - let report = bind_and_sign(&file_data, &meta, &sign_keys, &sign_cert).unwrap(); - // Test the generated report - // Reconstruct the public keys from the binding certificates - let mut certs = report.binding.station_certificate.split('|'); - let cert_cl = Certificate::from_pem(certs.next().unwrap()).unwrap(); - let cert_pq = Certificate::from_pem(certs.remainder().unwrap()).unwrap(); - - let pub_cl = ed25519_dalek::PublicKey::from_bytes( - cert_cl - .tbs_certificate - .subject_public_key_info - .subject_public_key - .raw_bytes(), - ) - .unwrap(); - oqs::init(); - let pq_scheme = Sig::new(Algorithm::Dilithium5).unwrap(); - let pub_pq = pq_scheme - .public_key_from_bytes( - cert_pq - .tbs_certificate - .subject_public_key_info - .subject_public_key - .raw_bytes(), - ) - .unwrap(); - - // Verify the signature of the report - let signature = general_purpose::STANDARD - .decode(report.binding.report_signature) - .unwrap(); - let concat = format!( - "{}-{}", - String::from_utf8( - general_purpose::STANDARD - .decode(report.binding.file_digest) - .unwrap() - ) - .unwrap(), - String::from_utf8( - general_purpose::STANDARD - .decode(report.binding.metadata_digest) - .unwrap() - ) - .unwrap() - ); - - let mut prehashed = Sha512::new(); - prehashed.update(&concat); - assert_eq!( - true, - pub_cl - .verify_prehashed( - prehashed, - None, - &ed25519_dalek::Signature::from_bytes( - &signature[0..ed25519_dalek::SIGNATURE_LENGTH] - ) - .unwrap() - ) - .is_ok() - ); - - assert_eq!( - true, - pq_scheme - .verify( - concat.as_bytes(), - pq_scheme - .signature_from_bytes(&signature[ed25519_dalek::SIGNATURE_LENGTH..]) - .unwrap(), - pub_pq - ) - .is_ok() - ); - } -} diff --git a/keysas-usbfilter/daemon/Cargo.toml b/keysas-usbfilter/daemon/Cargo.toml index 3f52f0b..aaf3d78 100644 --- a/keysas-usbfilter/daemon/Cargo.toml +++ b/keysas-usbfilter/daemon/Cargo.toml @@ -18,13 +18,7 @@ log = "0.4" wchar = "0.11" mbrman = "0.5" libc = "0.2" -serde_derive = "1.0" -serde = "1.0" -serde_json = "1.0" -x509-cert = "0.2" -ed25519-dalek = "1.0" -base64 = "0.21" -pkcs8 = {version = "0.10", features = ["encryption", "pem"] } +keysas_lib = { path = "../../keysas_lib" } [dependencies.windows] version = "0.48.0" @@ -37,9 +31,4 @@ features = [ "Win32_System_Threading", "Win32_UI_WindowsAndMessaging", "Win32_Storage_FileSystem" -] - -[dependencies.oqs] -version = "0.7" -default-features = false -features = ["dilithium"] \ No newline at end of file +] \ No newline at end of file diff --git a/keysas-usbfilter/daemon/src/main.rs b/keysas-usbfilter/daemon/src/main.rs index 12166ae..9ecd1e0 100644 --- a/keysas-usbfilter/daemon/src/main.rs +++ b/keysas-usbfilter/daemon/src/main.rs @@ -35,9 +35,6 @@ use crate::driver_interface::init_driver_com; use anyhow::anyhow; -#[macro_use] -extern crate serde_derive; - fn main() -> Result<(), anyhow::Error> { // Initialize the logger simple_logger::init()?; diff --git a/keysas-usbfilter/daemon/src/windows_driver_interface.rs b/keysas-usbfilter/daemon/src/windows_driver_interface.rs index d163b91..d780af2 100644 --- a/keysas-usbfilter/daemon/src/windows_driver_interface.rs +++ b/keysas-usbfilter/daemon/src/windows_driver_interface.rs @@ -27,7 +27,6 @@ use anyhow::anyhow; use libc::c_void; -use std::ffi::OsStr; use std::fs::File; use std::io::BufReader; use std::io::Read; @@ -35,6 +34,8 @@ use std::mem::size_of; use std::path::PathBuf; use std::path::{Component, Path}; use std::thread; +use std::os::windows::ffi::OsStrExt; +use std::ffi::{OsStr, OsString}; use widestring::U16CString; use windows::core::{PCSTR, PCWSTR}; use windows::s; @@ -53,11 +54,8 @@ use windows::Win32::Storage::InstallableFileSystems::{ use windows::Win32::System::Ioctl::VOLUME_DISK_EXTENTS; use windows::Win32::System::IO::DeviceIoControl; use windows::Win32::UI::WindowsAndMessaging::*; -use base64::{engine::general_purpose, Engine as _}; -use x509_cert::Certificate; -use ed25519_dalek::{self, Digest, Sha512}; -use pkcs8::der::{DecodePem, EncodePem}; -use oqs::sig::{Algorithm, Sig}; + +use keysas_lib::file_report::parse_report; // Operation code for the request to userland #[derive(Debug)] @@ -89,63 +87,6 @@ pub struct WindowsDriverInterface { handle: HANDLE, } -/// Metadata object in the report. -/// The structure can be serialized to JSON. -#[derive(Debug, Serialize, Deserialize, Clone)] -struct MetaData { - /// Name of the file - name: String, - /// Date of the report creation - date: String, - /// Type of the file - file_type: String, - /// True if the file is correct - is_valid: bool, - /// Object containing the detailled [FileReport] - report: FileReport, -} - -/// Signature binding the file and the report. -/// the structure can be serialized to JSON. -#[derive(Debug, Serialize, Deserialize, Clone)] -struct Bd { - /// SHA256 digest of the file encoded in base64 - file_digest: String, - /// SHA256 digest of the [MetaData] associated to the file - metadata_digest: String, - /// Station certificates: concatenation of its ED25519 and Dilithium5 signing certificates with a '|' delimiter - station_certificate: String, - /// Report signature: concatenation of the ED25519 and Dilithium5 signatures in base64 - report_signature: String, -} - -/// Report that will be created for each file. -/// The structure can be serialized to JSON. -#[derive(Debug, Serialize, Deserialize, Clone)] -struct Report { - /// [MetaData] of the file analysis - metadata: MetaData, - /// [Bd] binding of the file and the report with the station signature - binding: Bd, -} - -/// Detailed report of the file checks. -#[derive(Debug, Serialize, Deserialize, Clone)] -struct FileReport { - /// Detailed report of the yara checks - yara: String, - /// Detailed report of the clamav checks - av: Vec, - /// True if the file type is allowed - type_allowed: bool, - /// Size of the file - size: u64, - /// True if a file corruption occured during the file processing - corrupted: bool, - /// True if the file size is too big - toobig: bool, -} - /// Name of the communication port with the driver const DRIVER_COM_PORT: &str = "\\KeysasPort"; @@ -381,7 +322,7 @@ fn authorize_usb(content: &str) -> Result { } fn authorize_file(op: KeysasFilterOperation, content: &str) -> Result { - let file_path = Path::new(content.trim_matches(char::from(0))); + let mut file_path = Path::new(content.trim_matches(char::from(0))); // Try to get the parent directory let mut components = file_path.components(); @@ -391,7 +332,7 @@ fn authorize_file(op: KeysasFilterOperation, content: &str) -> Result Result Result { // Test if the file is a station report - if path + if Path::new(path) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("krp")) { // If yes validate it alone - return validate_report_alone(path); + if let Err(e) = parse_report(Path::new(path), None, None, None) { + println!("Failed to parse report: {e}"); + } + return Ok(true); } // If not try to find the corresponding report // It should be in the same directory with the same name + '.krp' - let report_path = path.to_path_buf().join(".krp"); - match File::open(report_path.as_path()) { - Ok(report) => { + let mut path_report = PathBuf::from(path); + match path_report.extension() { + Some(ext) => { + let mut ext = ext.to_os_string(); + ext.push(".krp"); + path_report.set_extension(ext); + }, + _ => { + path_report.set_extension(".krp"); + } + } + match path_report.is_file() { + true => { // If a corresponding report is found then validate both the file and the report - return validate_file_and_report(path, report_path.as_path()); + if let Err(e) = parse_report(path_report.as_path(), Some(path), None, None) { + println!("Failed to parse file and report: {e}"); + } + return Ok(true); } - Err(_) => { + false => { // There is no corresponding report for validating the file + println!("No report found at {:?}", path_report); return Ok(false); } } } -fn parse_report(report_path: &Path) -> Result { - let report_content = match std::fs::read_to_string(report_path) { - Ok(ct) => ct, - Err(_) => { - println!("Failed to read report content"); - return Err(anyhow!("Failed to read report content")); - } - }; - let report: Report = serde_json::from_str(report_content.as_str())?; - - println!("Report: {:?}", report); - - let mut certs = report.binding.station_certificate.split('|'); - // TODO: remove unwraps - let cert_cl = Certificate::from_pem(certs.next().unwrap())?; - let cert_pq = Certificate::from_pem(certs.remainder().unwrap())?; - - let pub_cl = ed25519_dalek::PublicKey::from_bytes( - cert_cl - .tbs_certificate - .subject_public_key_info - .subject_public_key - .raw_bytes(), - )?; - - oqs::init(); - let pq_scheme = Sig::new(Algorithm::Dilithium5).unwrap(); - let pub_pq = pq_scheme - .public_key_from_bytes( - cert_pq - .tbs_certificate - .subject_public_key_info - .subject_public_key - .raw_bytes(), - ) - .unwrap(); - - // Verify the signature of the report - let signature = general_purpose::STANDARD - .decode(&report.binding.report_signature)?; - let concat = format!( - "{}-{}", - String::from_utf8( - general_purpose::STANDARD - .decode(&report.binding.file_digest)? - )?, - String::from_utf8( - general_purpose::STANDARD - .decode(&report.binding.metadata_digest)? - )? - ); - - let mut prehashed = Sha512::new(); - prehashed.update(&concat); - /* - assert_eq!( - true, - pub_cl - .verify_prehashed( - prehashed, - None, - &ed25519_dalek::Signature::from_bytes( - &signature[0..ed25519_dalek::SIGNATURE_LENGTH] - ) - .unwrap() - ) - .is_ok() - ); - - assert_eq!( - true, - pq_scheme - .verify( - concat.as_bytes(), - pq_scheme - .signature_from_bytes(&signature[ed25519_dalek::SIGNATURE_LENGTH..]) - .unwrap(), - pub_pq - ) - .is_ok() - ); - */ - Ok(report) -} - -fn validate_report_alone(report_path: &Path) -> Result { - let report = match parse_report(report_path) { - Ok(rp) => rp, - Err(e) => { - println!("Failed to parse report"); - return Ok(false); - } - }; - Ok(true) -} - -fn validate_file_and_report(file: &Path, report_path: &Path) -> Result { - let report = match parse_report(report_path) { - Ok(rp) => rp, - Err(e) => { - println!("Failed to parse report"); - return Ok(false); - } - }; - - // Compute hash of the file - Ok(true) -} - fn user_authorize_file(path: &Path) -> Result { // Find authorization status for the file let mut authorization_status = MESSAGEBOX_RESULT::default(); diff --git a/keysas_lib/Cargo.toml b/keysas_lib/Cargo.toml index 98793ae..8de40e8 100644 --- a/keysas_lib/Cargo.toml +++ b/keysas_lib/Cargo.toml @@ -22,8 +22,13 @@ rand_core = "0.6.4" hex-literal = "0.4" tempfile = "3.4" der = "0.7" +serde_derive = "1.0" +serde = "1.0" +serde_json = "1.0" +time = "0.3" +base64 = "0.21" [dependencies.oqs] version = "0.7" default-features = false -features = ["dilithium"] +features = ["std", "dilithium"] diff --git a/keysas_lib/src/certificate_field.rs b/keysas_lib/src/certificate_field.rs index c8e5691..38e75d9 100644 --- a/keysas_lib/src/certificate_field.rs +++ b/keysas_lib/src/certificate_field.rs @@ -29,6 +29,8 @@ use der::asn1::SetOfVec; use der::oid::db::rfc4519; use der::Any; use der::Tag; +use ed25519_dalek::Verifier; +use oqs::sig::{Algorithm, Sig}; use pkcs8::der::asn1::OctetString; use pkcs8::der::oid::db::rfc5280; use pkcs8::der::DecodePem; @@ -47,6 +49,9 @@ use x509_cert::spki::ObjectIdentifier; use x509_cert::spki::SubjectPublicKeyInfo; use x509_cert::time::Validity; +use crate::pki::DILITHIUM5_OID; +use crate::pki::ED25519_OID; + /// Structure containing informations to build the certificate #[derive(Debug, Clone, Serialize)] pub struct CertificateFields { @@ -58,14 +63,82 @@ pub struct CertificateFields { } /// Validate a Certificate received in PEM format -/// TODO: Check that the certificate is for signing and follows the allowed format -pub fn validate_signing_certificate(pem: &str) -> Result { - Certificate::from_pem(pem)?; +/// Check that +/// - it can be parsed into a X509 certificate +/// - it is used for signing +/// - if there is a ca_cert supplied, it signed by the ca +/// +/// # Arguments +/// +/// * `pem` - Certificate in PEM format +/// * `ca_cert` - CA certificate either ED25519 or Dilithium +pub fn validate_signing_certificate( + pem: &str, + ca_cert: Option<&Certificate>, +) -> Result { + // Parse the certificate + let cert = Certificate::from_pem(pem)?; + + // If there is a CA, validate the certificate signature + if let Some(ca) = ca_cert { + match std::str::from_utf8( + ca.tbs_certificate + .subject_public_key_info + .algorithm + .oid + .as_bytes(), + )? { + ED25519_OID => { + // Extract the CA public key + let ca_key = ed25519_dalek::PublicKey::from_bytes( + cert.tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + )?; + + // Verify the certificate signature + let sig = ed25519_dalek::Signature::from_bytes( + cert.signature + .as_bytes() + .ok_or_else(|| anyhow!("Signature field is empty"))?, + )?; + ca_key.verify(&cert.tbs_certificate.to_der()?, &sig)?; + // If the signature is invalid an error is thrown + } + DILITHIUM5_OID => { + // Initialize liboqs + oqs::init(); + + // Extract the CA public key + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let ca_key = pq_scheme + .public_key_from_bytes( + cert.tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + ) + .ok_or_else(|| anyhow!("Invalid Dilithium key"))?; - //TODO: Validate certificate signature - //TODO: Validate certificate policy + // Verify the certificate signature + let sig = pq_scheme + .signature_from_bytes( + cert.signature + .as_bytes() + .ok_or_else(|| anyhow!("Signature field is empty"))?, + ) + .ok_or_else(|| anyhow!("Failed to parse signature field"))?; + pq_scheme.verify(&cert.tbs_certificate.to_der()?, sig, ca_key)?; + // If the signature is invalid an error is thrown + } + _ => { + return Err(anyhow!("Signature algorithm not supported")); + } + } + } - Ok(true) + Ok(cert) } impl CertificateFields { diff --git a/keysas_lib/src/file_report.rs b/keysas_lib/src/file_report.rs new file mode 100644 index 0000000..994038a --- /dev/null +++ b/keysas_lib/src/file_report.rs @@ -0,0 +1,510 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * + * (C) Copyright 2019-2023 Stephane Neveu, Luc Bonnafoux + * + * This file contains various funtions for building and validating file report + */ + +//! The report is JSON file containing the following structure: +//! ```json +//! { +//! "metadata": { +//! "name", // String: File name +//! "date", // String DD-MM-YYYY_HH-mm-SS-NN: Date of creation of the report +//! "file_type", // String: file type +//! "is_valid", // Boolean: true if all checks passed +//! "report": { +//! "yara", // String: yara detailed report +//! "av", // String: clamav detailed report +//! "type_allowed", // Boolean: false if forbidden type detected +//! "size", // u64: file size +//! "corrupted", // boolean: true if file integrity corruption detected +//! "toobig" // Boolean, true file size is too big +//! } +//! }, +//! "binding" : { +//! "file_digest", // String: base64 encoded SHA256 digest of the file +//! "metadata_digest", // String: base64 encoded SHA256 digest of the metadata +//! "station_certificate", // String: concatenation of the station signing certificates PEM +//! "report_signature", // String: base64 encoded concatenation of the ED25519 and Dilithium5 signatures +//! } +//! } +//! ``` +//! +//! The report is signed by the station and validated by the usb firewall +//! + +use crate::keysas_key::KeysasKey; +use crate::sha256_digest; +use crate::{ + certificate_field::validate_signing_certificate, keysas_hybrid_keypair::HybridKeyPair, +}; +use anyhow::anyhow; +use base64::{engine::general_purpose, Engine as _}; +use ed25519_dalek::{self, Verifier}; +use oqs::sig::{Algorithm, Sig}; +use sha2::Sha256; +use std::fs::File; +use std::path::Path; +use time::OffsetDateTime; +use x509_cert::Certificate; + +/// Metadata object in the report. +/// The structure can be serialized to JSON. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MetaData { + /// Name of the file + pub name: String, + /// Date of the report creation + pub date: String, + /// Type of the file + pub file_type: String, + /// True if the file is correct + pub is_valid: bool, + /// Object containing the detailled [FileReport] + pub report: FileReport, +} + +/// Signature binding the file and the report. +/// the structure can be serialized to JSON. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Bd { + /// SHA256 digest of the file encoded in base64 + pub file_digest: String, + /// SHA256 digest of the [MetaData] associated to the file + pub metadata_digest: String, + /// Station certificates: concatenation of its ED25519 and Dilithium5 signing certificates with a '|' delimiter + pub station_certificate: String, + /// Report signature: concatenation of the ED25519 and Dilithium5 signatures in base64 + pub report_signature: String, +} + +/// Report that will be created for each file. +/// The structure can be serialized to JSON. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Report { + /// [MetaData] of the file analysis + pub metadata: MetaData, + /// [Bd] binding of the file and the report with the station signature + pub binding: Bd, +} + +/// Detailed report of the file checks. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FileReport { + /// Detailed report of the yara checks + pub yara: String, + /// Detailed report of the clamav checks + pub av: Vec, + /// True if the file type is allowed + pub type_allowed: bool, + /// Size of the file + pub size: u64, + /// True if a file corruption occured during the file processing + pub corrupted: bool, + /// True if the file size is too big + pub toobig: bool, +} + +/// Structure that holds a file metadata +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FileMetadata { + /// Name of the file + pub filename: String, + /// SHA256 digest of the file + pub digest: String, + /// True if a file corruption as occured during processing + pub is_digest_ok: bool, + /// True if the file is toobig + pub is_toobig: bool, + /// Size of the file + pub size: u64, + /// True if the file type is valid + pub is_type_allowed: bool, + /// True if clamav tests pass + pub av_pass: bool, + /// Detailed report of clamav if the test failed + pub av_report: Vec, + /// True if yara tests pass + pub yara_pass: bool, + /// Detailed report of yara if the test failed + pub yara_report: String, + /// Timestamp of the file entering the station + pub timestamp: String, + /// True if a file corruption occured during the processing + pub is_corrupted: bool, + /// Type of the file + pub file_type: String, +} + +/// Wrapper around the report metadata creation +/// +/// # Arguments +/// +/// * `f` - File metadata received from keysas transit +pub fn generate_report_metadata(f: &FileMetadata) -> MetaData { + let timestamp = format!( + "{}-{}-{}_{}-{}-{}-{}", + OffsetDateTime::now_utc().day(), + OffsetDateTime::now_utc().month(), + OffsetDateTime::now_utc().year(), + OffsetDateTime::now_utc().hour(), + OffsetDateTime::now_utc().minute(), + OffsetDateTime::now_utc().second(), + OffsetDateTime::now_utc().nanosecond() + ); + + let new_file_report = FileReport { + yara: f.yara_report.clone(), + av: f.av_report.clone(), + type_allowed: f.is_type_allowed, + size: f.size, + corrupted: f.is_corrupted, + toobig: f.is_toobig, + }; + + MetaData { + name: f.filename.clone(), + date: timestamp, + file_type: f.file_type.clone(), + is_valid: f.av_pass + && f.yara_pass + && !f.is_toobig + && !f.is_corrupted + && f.is_digest_ok + && f.is_type_allowed, + report: new_file_report, + } +} + +/// Bind the report to the file by signing with ED25519 and Dilithium5 the concatenation +/// of the file digest and the report metadata digest. +/// The two signatures are concatenated (ED25519 first). +/// All the fields of the binding are encoded in base64 +/// +/// # Arguments +/// +/// * `f` - Metadata from the file analysis, it is used to get the file digest +/// * `report_meta` - Report metadata that will be included in the json file +/// * `sign_keys` - Hybrid key pair to sign the report +/// * `sign_cert` - Hybrid key pair certificate that will be included in the report +pub fn bind_and_sign( + f: &FileMetadata, + report_meta: &MetaData, + sign_keys: &HybridKeyPair, + sign_cert: &str, +) -> Result { + // Compute digest of report metadata + let json_string = serde_json::to_string(&report_meta)?; + + let mut meta_digest = String::new(); + + { + // Import Trait digest localy to avoid collisation with Trait defined in ed25519_dalek + use sha2::Digest; + let mut hasher = Sha256::new(); + hasher.update(json_string.as_bytes()); + let result = hasher.finalize(); + meta_digest.push_str(&format!("{result:x}")); + } + + // Sign the report and the file + let concat = format!("{}-{}", f.digest, meta_digest); + + let mut signature = Vec::new(); + + // Sign with ED25519 + signature.append(&mut sign_keys.classic.message_sign(concat.as_bytes())?); + + // Sign with Dilithium5 + signature.append(&mut sign_keys.pq.message_sign(concat.as_bytes())?); + + // Generate the final report + Ok(Report { + metadata: report_meta.clone(), + binding: Bd { + file_digest: general_purpose::STANDARD.encode(f.digest.clone()), + metadata_digest: general_purpose::STANDARD.encode(meta_digest), + station_certificate: sign_cert.to_string(), + report_signature: general_purpose::STANDARD.encode(signature), + }, + }) +} + +/// Parse a json file and try to extract a valid report from it +/// The function returns an error if the file is invalid or if the report contained is invalid +/// If there are CA certificate available, use them to validate the certificates in the report +/// +/// # Arguments +/// +/// * `report_path` - Path to the file containing the report +/// * `file_path` - Path to the file linked to the report +/// * `ca_cert_cl` - ED25519 certificate of the authority, used to validate the certificate in the report +/// * `ca_cert_pq` - Dilithium certificate of the authority +pub fn parse_report( + report_path: &Path, + file_path: Option<&Path>, + ca_cert_cl: Option<&Certificate>, + ca_cert_pq: Option<&Certificate>, +) -> Result { + // Open the report + let report_content = match std::fs::read_to_string(report_path) { + Ok(ct) => ct, + Err(_) => { + return Err(anyhow!("Failed to read report content")); + } + }; + + // Parse the json and coerce it into a Report structure + let report: Report = serde_json::from_str(report_content.as_str())?; + + // If the report is linked to a file, test that there is a path to it supplied + if report.metadata.name.ne("") && file_path == None { + return Err(anyhow!("No file supplied with the report")); + } + + // Extracts the certificate within the report and validate them + let mut certs = report.binding.station_certificate.split('|'); + let cert_cl = validate_signing_certificate( + certs.next().ok_or(anyhow!("No ED25519 certificate"))?, + ca_cert_cl, + )?; + let cert_pq = validate_signing_certificate( + certs + .remainder() + .ok_or(anyhow!("No Dilithium certificate"))?, + ca_cert_pq, + )?; + + // Reference Validation + // and create the signature message from the digest of the metadata and the linked file if there is one + let mut message = String::new(); + { + // Import Trait digest localy to avoid collisation with Trait defined in ed25519_dalek + use sha2::Digest; + + // Compute the digest of the file + if let Some(f) = file_path { + let file_digest = sha256_digest(&File::open(f)?)?; + // Validate that it corresponds to the reference in the binding + if general_purpose::STANDARD.encode(&file_digest) != report.binding.file_digest { + return Err(anyhow!("File reference is invalid")); + } + message.push_str(&file_digest); + } + + // Add delimiter between the two digests + message.push('-'); + + // Compute digest of the report metadata section + let mut hasher = Sha256::new(); + println!("Meta content: {:?}", report.metadata); + hasher.update(serde_json::to_string_pretty(&report.metadata)?); + let meta_digest: String = format!("{:X}", hasher.finalize()); + // Validate that it corresponds to the reference in the binding + println!("Report meta: {:?}", report.binding.metadata_digest); + println!("Meta raw: {:?}", general_purpose::STANDARD.encode(&meta_digest)); + if general_purpose::STANDARD.encode(&meta_digest) != report.binding.metadata_digest { + return Err(anyhow!("Metadata reference is invalid")); + } + message.push_str(&meta_digest); + } + + // Signature validation + let signature = general_purpose::STANDARD.decode(&report.binding.report_signature)?; + + // Validate the signature with ED25519 + let pub_cl = ed25519_dalek::PublicKey::from_bytes( + cert_cl + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + )?; + let sig_cl = + ed25519_dalek::Signature::from_bytes(&signature[0..ed25519_dalek::SIGNATURE_LENGTH])?; + pub_cl.verify(message.as_bytes(), &sig_cl)?; + // If the signature is invalid, an error is thrown + + // Validate the signature with Dilithium + oqs::init(); + let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let pub_pq = pq_scheme + .public_key_from_bytes( + cert_pq + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + ) + .ok_or_else(|| anyhow!("Failed to extract Dilithium public key"))?; + let sig_pq = pq_scheme + .signature_from_bytes(&signature[ed25519_dalek::SIGNATURE_LENGTH..]) + .ok_or_else(|| anyhow!("Failed to parse signature field"))?; + pq_scheme.verify(message.as_bytes(), sig_pq, pub_pq)?; + // If the signature is invalid an error is thrown + + Ok(report) +} + +#[cfg(test)] +mod tests_out { + use base64::{engine::general_purpose, Engine}; + use ed25519_dalek::{self, Digest, Sha512}; + use keysas_lib::{certificate_field::CertificateFields, keysas_hybrid_keypair::HybridKeyPair}; + use oqs::sig::{Algorithm, Sig}; + use pkcs8::der::{DecodePem, EncodePem}; + use x509_cert::Certificate; + + use crate::{bind_and_sign, generate_report_metadata, FileData, FileMetadata}; + + #[test] + fn test_metadata_valid_file() { + // Generate dummy file data + let file_data = FileData { + fd: 2, + md: FileMetadata { + filename: "test.txt".to_string(), + digest: "00112233445566778899AABBCCDDEEFF".to_string(), + is_digest_ok: true, + is_toobig: false, + size: 42, + is_type_allowed: true, + av_pass: true, + av_report: Vec::new(), + yara_pass: true, + yara_report: "".to_string(), + timestamp: "timestamp".to_string(), + is_corrupted: false, + file_type: "txt".to_string(), + }, + }; + + // Generate report metadata + let meta = generate_report_metadata(&file_data); + + // Validate fields + assert_eq!(file_data.md.filename, meta.name); + assert_eq!(file_data.md.file_type, meta.file_type); + assert_eq!(meta.is_valid, true); + } + + #[test] + fn test_bind_and_sign() { + // Generate temporary keys + let infos = + CertificateFields::from_fields(None, None, None, Some("Test_station"), Some("200")) + .unwrap(); + let sign_keys = HybridKeyPair::generate_root(&infos).unwrap(); + + let mut sign_cert = String::new(); + let pem_cl = sign_keys + .classic_cert + .to_pem(pkcs8::LineEnding::LF) + .unwrap(); + sign_cert.push_str(&pem_cl); + // Add a delimiter between the two certificates + sign_cert.push('|'); + let pem_pq = sign_keys.pq_cert.to_pem(pkcs8::LineEnding::LF).unwrap(); + sign_cert.push_str(&pem_pq); + + // Generate dummy file data + let file_data = FileData { + fd: 2, + md: FileMetadata { + filename: "test.txt".to_string(), + digest: "00112233445566778899AABBCCDDEEFF".to_string(), + is_digest_ok: true, + is_toobig: false, + size: 42, + is_type_allowed: true, + av_pass: true, + av_report: Vec::new(), + yara_pass: true, + yara_report: "".to_string(), + timestamp: "timestamp".to_string(), + is_corrupted: false, + file_type: "txt".to_string(), + }, + }; + + let meta = generate_report_metadata(&file_data); + + let report = bind_and_sign(&file_data, &meta, &sign_keys, &sign_cert).unwrap(); + // Test the generated report + // Reconstruct the public keys from the binding certficates + let mut certs = report.binding.station_certificate.split('|'); + let cert_cl = Certificate::from_pem(certs.next().unwrap()).unwrap(); + let cert_pq = Certificate::from_pem(certs.remainder().unwrap()).unwrap(); + + let pub_cl = ed25519_dalek::PublicKey::from_bytes( + cert_cl + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + ) + .unwrap(); + oqs::init(); + let pq_scheme = Sig::new(Algorithm::Dilithium5).unwrap(); + let pub_pq = pq_scheme + .public_key_from_bytes( + cert_pq + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + ) + .unwrap(); + + // Verify the signature of the report + let signature = general_purpose::STANDARD + .decode(report.binding.report_signature) + .unwrap(); + let concat = format!( + "{}-{}", + String::from_utf8( + general_purpose::STANDARD + .decode(report.binding.file_digest) + .unwrap() + ) + .unwrap(), + String::from_utf8( + general_purpose::STANDARD + .decode(report.binding.metadata_digest) + .unwrap() + ) + .unwrap() + ); + + let mut prehashed = Sha512::new(); + prehashed.update(&concat); + assert_eq!( + true, + pub_cl + .verify_prehashed( + prehashed, + None, + &ed25519_dalek::Signature::from_bytes( + &signature[0..ed25519_dalek::SIGNATURE_LENGTH] + ) + .unwrap() + ) + .is_ok() + ); + + assert_eq!( + true, + pq_scheme + .verify( + concat.as_bytes(), + pq_scheme + .signature_from_bytes(&signature[ed25519_dalek::SIGNATURE_LENGTH..]) + .unwrap(), + pub_pq + ) + .is_ok() + ); + } +} diff --git a/keysas_lib/src/lib.rs b/keysas_lib/src/lib.rs index 68c3280..7052f2a 100644 --- a/keysas_lib/src/lib.rs +++ b/keysas_lib/src/lib.rs @@ -1,12 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * The "keysas-lib". * * (C) Copyright 2019-2023 Stephane Neveu, Luc Bonnafoux * - * This file contains various funtions - * for building the keysas_lib. + * This file contains various utility functions */ + +//! This module contains utility functions for the rest of Keysas + +#![feature(str_split_remainder)] use anyhow::Result; use regex::Regex; use sha2::{Digest, Sha256}; @@ -14,11 +16,19 @@ use std::env; use std::ffi::{OsStr, OsString}; use std::fs; use std::fs::File; -use std::io::{BufReader, IoSlice, Read}; -use std::os::unix::io::AsRawFd; +use std::io::{BufReader, Read}; use std::path::PathBuf; +#[cfg(target_os = "linux")] +use std::io::IoSlice; +#[cfg(target_os = "linux")] +use std::os::unix::io::AsRawFd; + +#[macro_use] +extern crate serde_derive; + pub mod certificate_field; +pub mod file_report; pub mod keysas_hybrid_keypair; pub mod keysas_key; pub mod pki; @@ -109,6 +119,7 @@ pub fn list_files(directory: &str) -> Result> { Ok(names) } +#[cfg(target_os = "linux")] pub fn convert_ioslice<'a>( files: &'a Vec, input: &'a Vec>, From c390f21c2a6bc649a52776437067fee9918505aa Mon Sep 17 00:00:00 2001 From: Luc Date: Wed, 3 May 2023 15:31:36 +0200 Subject: [PATCH 057/160] Validated file reports --- .../daemon/src/windows_driver_interface.rs | 5 +- keysas_lib/src/file_report.rs | 69 ++++++++----------- keysas_lib/src/keysas_key.rs | 5 +- 3 files changed, 34 insertions(+), 45 deletions(-) diff --git a/keysas-usbfilter/daemon/src/windows_driver_interface.rs b/keysas-usbfilter/daemon/src/windows_driver_interface.rs index d780af2..e732539 100644 --- a/keysas-usbfilter/daemon/src/windows_driver_interface.rs +++ b/keysas-usbfilter/daemon/src/windows_driver_interface.rs @@ -194,8 +194,6 @@ impl WindowsDriverInterface { continue; } } - - println!("Sent response"); } }); Ok(()) @@ -326,7 +324,6 @@ fn authorize_file(op: KeysasFilterOperation, content: &str) -> Result Result { // If yes validate it alone if let Err(e) = parse_report(Path::new(path), None, None, None) { println!("Failed to parse report: {e}"); + return Ok(false); } return Ok(true); } @@ -391,6 +389,7 @@ fn validate_file(path: &Path) -> Result { // If a corresponding report is found then validate both the file and the report if let Err(e) = parse_report(path_report.as_path(), Some(path), None, None) { println!("Failed to parse file and report: {e}"); + return Ok(false); } return Ok(true); } diff --git a/keysas_lib/src/file_report.rs b/keysas_lib/src/file_report.rs index 994038a..65f104e 100644 --- a/keysas_lib/src/file_report.rs +++ b/keysas_lib/src/file_report.rs @@ -299,12 +299,9 @@ pub fn parse_report( // Compute digest of the report metadata section let mut hasher = Sha256::new(); - println!("Meta content: {:?}", report.metadata); - hasher.update(serde_json::to_string_pretty(&report.metadata)?); - let meta_digest: String = format!("{:X}", hasher.finalize()); + hasher.update(serde_json::to_string(&report.metadata)?.as_bytes()); + let meta_digest: String = format!("{:x}", hasher.finalize()); // Validate that it corresponds to the reference in the binding - println!("Report meta: {:?}", report.binding.metadata_digest); - println!("Meta raw: {:?}", general_purpose::STANDARD.encode(&meta_digest)); if general_purpose::STANDARD.encode(&meta_digest) != report.binding.metadata_digest { return Err(anyhow!("Metadata reference is invalid")); } @@ -362,23 +359,20 @@ mod tests_out { #[test] fn test_metadata_valid_file() { // Generate dummy file data - let file_data = FileData { - fd: 2, - md: FileMetadata { - filename: "test.txt".to_string(), - digest: "00112233445566778899AABBCCDDEEFF".to_string(), - is_digest_ok: true, - is_toobig: false, - size: 42, - is_type_allowed: true, - av_pass: true, - av_report: Vec::new(), - yara_pass: true, - yara_report: "".to_string(), - timestamp: "timestamp".to_string(), - is_corrupted: false, - file_type: "txt".to_string(), - }, + let file_data = FileMetadata { + filename: "test.txt".to_string(), + digest: "00112233445566778899AABBCCDDEEFF".to_string(), + is_digest_ok: true, + is_toobig: false, + size: 42, + is_type_allowed: true, + av_pass: true, + av_report: Vec::new(), + yara_pass: true, + yara_report: "".to_string(), + timestamp: "timestamp".to_string(), + is_corrupted: false, + file_type: "txt".to_string(), }; // Generate report metadata @@ -410,23 +404,20 @@ mod tests_out { sign_cert.push_str(&pem_pq); // Generate dummy file data - let file_data = FileData { - fd: 2, - md: FileMetadata { - filename: "test.txt".to_string(), - digest: "00112233445566778899AABBCCDDEEFF".to_string(), - is_digest_ok: true, - is_toobig: false, - size: 42, - is_type_allowed: true, - av_pass: true, - av_report: Vec::new(), - yara_pass: true, - yara_report: "".to_string(), - timestamp: "timestamp".to_string(), - is_corrupted: false, - file_type: "txt".to_string(), - }, + let file_data = FileMetadata { + filename: "test.txt".to_string(), + digest: "00112233445566778899AABBCCDDEEFF".to_string(), + is_digest_ok: true, + is_toobig: false, + size: 42, + is_type_allowed: true, + av_pass: true, + av_report: Vec::new(), + yara_pass: true, + yara_report: "".to_string(), + timestamp: "timestamp".to_string(), + is_corrupted: false, + file_type: "txt".to_string(), }; let meta = generate_report_metadata(&file_data); diff --git a/keysas_lib/src/keysas_key.rs b/keysas_lib/src/keysas_key.rs index 45a922f..2775ddc 100644 --- a/keysas_lib/src/keysas_key.rs +++ b/keysas_lib/src/keysas_key.rs @@ -32,6 +32,7 @@ use ed25519_dalek::PublicKey; use ed25519_dalek::Sha512; use ed25519_dalek::Signature as SignatureDalek; use ed25519_dalek::Verifier; +use ed25519_dalek::Signer; use oqs::sig::Algorithm; use oqs::sig::PublicKey as PqPublicKey; use oqs::sig::SecretKey; @@ -325,9 +326,7 @@ impl KeysasKey for Keypair { } fn message_sign(&self, message: &[u8]) -> Result, anyhow::Error> { - let mut prehashed = Sha512::new(); - prehashed.update(message); - let signature = self.sign_prehashed(prehashed, None)?; + let signature = self.sign(message); Ok(signature.to_bytes().to_vec()) } From ff33e922c6ba68864cd3dfcaa74f0dc46a6c586d Mon Sep 17 00:00:00 2001 From: Luc Date: Wed, 3 May 2023 15:32:09 +0200 Subject: [PATCH 058/160] Added first window to tray app --- img/logo-keysas-short.png | Bin 0 -> 19019 bytes keysas-usbfilter/tray-app/index.html | 4 +- keysas-usbfilter/tray-app/package-lock.json | 45 ++++++++ keysas-usbfilter/tray-app/package.json | 10 +- .../tray-app/public/logo-keysas-short-16.png | Bin 0 -> 6775 bytes .../tray-app/public/logo-keysas-short-256.png | Bin 0 -> 24397 bytes .../tray-app/public/logo-keysas-short-32.png | Bin 0 -> 8333 bytes .../tray-app/public/logo-keysas-short-48.png | Bin 0 -> 12553 bytes .../tray-app/public/logo-keysas-short.ico | Bin 0 -> 21072 bytes keysas-usbfilter/tray-app/public/tauri.svg | 6 -- keysas-usbfilter/tray-app/public/vite.svg | 1 - .../tray-app/src-tauri/Cargo.toml | 8 ++ .../tray-app/src-tauri/icons/128x128.png | Bin 3512 -> 0 bytes .../tray-app/src-tauri/icons/128x128@2x.png | Bin 7012 -> 0 bytes .../tray-app/src-tauri/icons/32x32.png | Bin 974 -> 0 bytes .../src-tauri/icons/Square107x107Logo.png | Bin 2863 -> 0 bytes .../src-tauri/icons/Square142x142Logo.png | Bin 3858 -> 0 bytes .../src-tauri/icons/Square150x150Logo.png | Bin 3966 -> 0 bytes .../src-tauri/icons/Square284x284Logo.png | Bin 7737 -> 0 bytes .../src-tauri/icons/Square30x30Logo.png | Bin 903 -> 0 bytes .../src-tauri/icons/Square310x310Logo.png | Bin 8591 -> 0 bytes .../src-tauri/icons/Square44x44Logo.png | Bin 1299 -> 0 bytes .../src-tauri/icons/Square71x71Logo.png | Bin 2011 -> 0 bytes .../src-tauri/icons/Square89x89Logo.png | Bin 2468 -> 0 bytes .../tray-app/src-tauri/icons/StoreLogo.png | Bin 1523 -> 0 bytes .../tray-app/src-tauri/icons/icon.icns | Bin 98451 -> 0 bytes .../tray-app/src-tauri/icons/icon.ico | Bin 86642 -> 0 bytes .../tray-app/src-tauri/icons/icon.png | Bin 14183 -> 0 bytes .../src-tauri/icons/logo-keysas-short-16.png | Bin 0 -> 6775 bytes .../src-tauri/icons/logo-keysas-short-256.png | Bin 0 -> 24397 bytes .../src-tauri/icons/logo-keysas-short-32.png | Bin 0 -> 8333 bytes .../src-tauri/icons/logo-keysas-short-48.png | Bin 0 -> 12553 bytes .../src-tauri/icons/logo-keysas-short.ico | Bin 0 -> 21072 bytes .../src-tauri/icons/logo-keysas-short.png | Bin 0 -> 19019 bytes .../tray-app/src-tauri/src/app_controler.rs | 52 +++++++++ .../tray-app/src-tauri/src/filter_store.rs | 102 ++++++++++++++++++ .../tray-app/src-tauri/src/main.rs | 75 +++++++++---- .../tray-app/src-tauri/src/service_if.rs | 70 ++++++++++++ .../tray-app/src-tauri/tauri.conf.json | 27 +++-- keysas-usbfilter/tray-app/src/App.vue | 49 ++++++--- keysas-usbfilter/tray-app/src/USBDetails.vue | 44 ++++++++ .../tray-app/src/components/Greet.vue | 21 ---- keysas-usbfilter/tray-app/src/styles.css | 4 + keysas-usbfilter/tray-app/src/usb_details.ts | 5 + keysas-usbfilter/tray-app/usb_details.html | 14 +++ 45 files changed, 458 insertions(+), 79 deletions(-) create mode 100644 img/logo-keysas-short.png create mode 100644 keysas-usbfilter/tray-app/public/logo-keysas-short-16.png create mode 100644 keysas-usbfilter/tray-app/public/logo-keysas-short-256.png create mode 100644 keysas-usbfilter/tray-app/public/logo-keysas-short-32.png create mode 100644 keysas-usbfilter/tray-app/public/logo-keysas-short-48.png create mode 100644 keysas-usbfilter/tray-app/public/logo-keysas-short.ico delete mode 100644 keysas-usbfilter/tray-app/public/tauri.svg delete mode 100644 keysas-usbfilter/tray-app/public/vite.svg delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/128x128.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/128x128@2x.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/32x32.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/Square107x107Logo.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/Square142x142Logo.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/Square150x150Logo.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/Square284x284Logo.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/Square30x30Logo.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/Square310x310Logo.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/Square44x44Logo.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/Square71x71Logo.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/Square89x89Logo.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/StoreLogo.png delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/icon.icns delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/icon.ico delete mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/icon.png create mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short-16.png create mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short-256.png create mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short-32.png create mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short-48.png create mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short.ico create mode 100644 keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short.png create mode 100644 keysas-usbfilter/tray-app/src-tauri/src/app_controler.rs create mode 100644 keysas-usbfilter/tray-app/src-tauri/src/filter_store.rs create mode 100644 keysas-usbfilter/tray-app/src-tauri/src/service_if.rs create mode 100644 keysas-usbfilter/tray-app/src/USBDetails.vue delete mode 100644 keysas-usbfilter/tray-app/src/components/Greet.vue create mode 100644 keysas-usbfilter/tray-app/src/usb_details.ts create mode 100644 keysas-usbfilter/tray-app/usb_details.html diff --git a/img/logo-keysas-short.png b/img/logo-keysas-short.png new file mode 100644 index 0000000000000000000000000000000000000000..b34da2af6936b6e45119391230f8d09fe8ea0a53 GIT binary patch literal 19019 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

    Ab;!7 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 NlU9~0e`_50e*wV=XSV - + - Tauri + Vue + TS + Keysas USB Firewall diff --git a/keysas-usbfilter/tray-app/package-lock.json b/keysas-usbfilter/tray-app/package-lock.json index d124d65..97f6872 100644 --- a/keysas-usbfilter/tray-app/package-lock.json +++ b/keysas-usbfilter/tray-app/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.0", "dependencies": { "@tauri-apps/api": "^1.2.0", + "bootstrap": "^5.2.3", + "bootstrap-icons": "^1.10.5", "vue": "^3.2.45" }, "devDependencies": { @@ -383,6 +385,16 @@ "node": ">=12" } }, + "node_modules/@popperjs/core": { + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", + "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@tauri-apps/api": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.2.0.tgz", @@ -749,6 +761,39 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bootstrap": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz", + "integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.6" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.5.tgz", + "integrity": "sha512-oSX26F37V7QV7NCE53PPEL45d7EGXmBgHG3pDpZvcRaKVzWMqIRL9wcqJUyEha1esFtM3NJzvmxFXDxjJYD0jQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ] + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", diff --git a/keysas-usbfilter/tray-app/package.json b/keysas-usbfilter/tray-app/package.json index fec4e4a..a3164db 100644 --- a/keysas-usbfilter/tray-app/package.json +++ b/keysas-usbfilter/tray-app/package.json @@ -10,15 +10,17 @@ "tauri": "tauri" }, "dependencies": { - "vue": "^3.2.45", - "@tauri-apps/api": "^1.2.0" + "@tauri-apps/api": "^1.2.0", + "bootstrap": "^5.2.3", + "bootstrap-icons": "^1.10.5", + "vue": "^3.2.45" }, "devDependencies": { + "@tauri-apps/cli": "^1.2.3", "@types/node": "^18.7.10", "@vitejs/plugin-vue": "^4.0.0", "typescript": "^4.9.5", "vite": "^4.2.1", - "vue-tsc": "^1.0.11", - "@tauri-apps/cli": "^1.2.3" + "vue-tsc": "^1.0.11" } } diff --git a/keysas-usbfilter/tray-app/public/logo-keysas-short-16.png b/keysas-usbfilter/tray-app/public/logo-keysas-short-16.png new file mode 100644 index 0000000000000000000000000000000000000000..d231af37f2b12186ee5cc902903e6ffc874bd8c9 GIT binary patch literal 6775 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 ZCq3TrO*V1g!1iw(xQ4!Zm8xyn{{n*~GnN1V literal 0 HcmV?d00001 diff --git a/keysas-usbfilter/tray-app/public/logo-keysas-short.ico b/keysas-usbfilter/tray-app/public/logo-keysas-short.ico new file mode 100644 index 0000000000000000000000000000000000000000..52c5eff065a6b03e0984877a04ba187ed233c9b6 GIT binary patch literal 21072 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 - - - - - diff --git a/keysas-usbfilter/tray-app/public/vite.svg b/keysas-usbfilter/tray-app/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/keysas-usbfilter/tray-app/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/keysas-usbfilter/tray-app/src-tauri/Cargo.toml b/keysas-usbfilter/tray-app/src-tauri/Cargo.toml index 8ec6bbc..6ecfef2 100644 --- a/keysas-usbfilter/tray-app/src-tauri/Cargo.toml +++ b/keysas-usbfilter/tray-app/src-tauri/Cargo.toml @@ -15,11 +15,19 @@ tauri-build = { version = "1.2", features = [] } [dependencies] tauri = { version = "1.2", features = ["shell-open", "system-tray"] } +tauri-plugin-positioner = {version = "1.0", features = ["system-tray"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" anyhow = "1.0" simple_logger = "4.1" log = "0.4" +tokio = {version = "1.28", features = ["net", "time"] } + +[dependencies.windows-sys] +version = "0.48.0" +features = [ + "Win32_Foundation", +] [features] # by default Tauri runs in production mode diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/128x128.png b/keysas-usbfilter/tray-app/src-tauri/icons/128x128.png deleted file mode 100644 index 6be5e50e9b9ae84d9e2ee433f32ef446495eaf3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3512 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 diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/128x128@2x.png b/keysas-usbfilter/tray-app/src-tauri/icons/128x128@2x.png deleted file mode 100644 index e81becee571e96f76aa5667f9324c05e5e7a4479..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7012 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 Sh@|Ye~|0>}wMPLT8jm@Y!H33Sz}5aFI6 zM9Lzqz|;A*0sGs=2A1uU!1nk2dGF7knQwr99SAFen)x(eCO;F8y2C~0FD1YxRTPcy zPWVxkUYmeuz}Tv?7&Fe-!UE{)ZW)Mb;H)^#eHDv$`dkZGguJz@^MA!ZNGAUqt{|0H zpZ7Ch9S`q5!>R%}>}62!+(T^evyO+ImSo2wpu)su4^3nw5(%)KD%gbSev^*HZZ&3( z#&c@Z0gH|}Ck)w6fh0&NBJ62ib%R}(3@$VFl*_#l2W$wQ-~4RmZZAt5O*^2Q5}Xr8Hy@c`#pM?kc?hFWxRXr*mUfUCXf4ka5DD~ zat6d85COB05l#(P9*cQZ3EC8fVdS~?&vN#rce(aF9@xp80O2{{FBvU+{X>Hoh;xI` z{$e^Nw1y*VbO8wv`8|-m?NwNaKGTGaF{P^JLB^DbOYWIbn%eT`*!^C1H36=O8Z-M> zkD~88ry`eSo`tEBN4>w7OWZwUzlh{WM1m8R6zepqGcGMaV7vWY9b?K4b6~|HVG)ec wi>I@ws#sZo7or4_*4M>7;p5{nr2pZ?Uu4>Krr0kU)&Kwi07*qoM6N<$f)&@lf&c&j diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/Square107x107Logo.png b/keysas-usbfilter/tray-app/src-tauri/icons/Square107x107Logo.png deleted file mode 100644 index 0ca4f27198838968bd60ed7d371bfa23496b7fe5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2863 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 Y2S8W#vM)6=T>t<807*qoM6N<$f*y@n<^TWy diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/Square284x284Logo.png b/keysas-usbfilter/tray-app/src-tauri/icons/Square284x284Logo.png deleted file mode 100644 index c021d2ba76619c08969ab688db3b27f29257aa6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7737 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 diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/Square30x30Logo.png b/keysas-usbfilter/tray-app/src-tauri/icons/Square30x30Logo.png deleted file mode 100644 index 621970023096ed9f494ba18ace15421a45cd65fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 903 zcmV;219<$2P)2 z+CUKPMqaqGiH;zb!R4$B-WXS^YzQr=@UH>k4?*L)&R=zYjBrZenKdc9|JlS$SO*RJ zKt8FSTDAdk1g_WPAO!p^V!AuL;Lm;uQyV;zKq)J3i(;q*;k+pD%f3eltU`PYdy9(k0&%` zuWAPcV6|-y?|?7O1W!KSK}pbk8#~!|FA@(VJkt^V@0lio{afoAeo*f&$W2s6${5!1eKvAGD2$GZwSB98L2ZVS- zKn8ENRkZ*sb!@QugOrQNK3(sy1v%J#m|rpB+h|Nkqa3FRT>74xSs{#&saU2Lf!_Iq zKmuKAESh`gs!fneGWn+nf}l?7jE$HW!Af&vE5=G!QU)U2v&HLIBGXKk4nQx{hsHjL zLPMAo5=*uInFbq7(aa`Y2VX5wCmaeqvECOFv)a>0t>ZaEb*cJccER=BB?KFZhV$c^ znL*l8x*UYZv4WK|j?~Jt6~~F%{pk~z5A*>^M`?r5m9@RJ_x|uEtX(6Vk@Y()MVto* z93wr)%3m%|#OZ~srm>zF(JvDuTq*@;d&^>_BJm5hOU`3FjG70L#Vzv9I?`<7$T@

    jU?lMi@tgxr7CqX_r3uw^y4tVU3Pm0sw;|1WSUO%?=bG`*Kmz6u4{#ti;T7AWIBAEh!(Y zz>O01&#X?Ds@L)Sb{CkG#Yz4$3o d@96)?#cz^xWoA}>B$xmI002ovPDHLkV1l3&k#zt7 diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/Square310x310Logo.png b/keysas-usbfilter/tray-app/src-tauri/icons/Square310x310Logo.png deleted file mode 100644 index f9bc04839491e66c07b16ab03743c0c53b4109cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8591 zcmbtahc}$h_twIy(GxYgAVgi!!xDs*)f2s!wX2s9Bo-?nB+*%-1*_LxM2i}|mu0o+ zU80NN=kxs+esj*8_ssL&Gk4CMdGGr?_s$21o+dQ~D+K`o0kyW4x&Z+JA@IKrAiYI) znp%o(ALO1|uY3pyC>j3igaqjs_isT$9|KJ_g7P8ut=j>Kvnp7XfS~FVJ7pZI}8ladf{o!;c zm1(K;-KkdRXO-n=L1P0pQv0P`U(b2~9nEJ=@_rst-RE_UCEIhCS6ZC{wgP%L=ch&T zC*gow@BgnRJVg7H?|jR*KU64`|5#Jg~WpHZ+L{j}|Li4|snUleLlZI)ZeC zOI^*wECuanft|Cy7L!avUqb|s`zkL-uUniu+&?`PC1In=Ea{>DZXXUSFYUIYtR83C zra$`5(dV9>JAOL}$hJclnH&JSKk%j1Hve%5+nA;Kpc0mQn*Ti~f?BK;JrIBAa$eE+ z@j#pupdkvqx*TZ}?&Ia-L_V0(F#w!2UsUGF^sb*3d{2s?9{L8Tb?6NZ_#{1)7Mm{N zhK+vn?p+Kqf?CgLD02|sP;&<{&SF;h@qwL~*dr1)_9B3E&BtHsceG7qR>%PL;B> zB_F)S$_$6{RbkQlTRg>ezn)f360DC+Y})U`pU@+ouf%$!z|czk5$U9&=5D1k8>Jvm zAv8|7*o77+9P1kQH1BKXo5q-&tu8K{F#3rez}W20aldEBAFYju9G9-dBUkeXND0x! zyV>gDE&8^GTdUO{!K}&NM%s2J;s^f9_oGeJ|Fmy7BDN)+Cjb5J4?!4mbx|T{?NjrxhJ61zx;_vPzEwo7$v&}AL|(FD9o-n zI99cr^aZ_<$bIbA$(l#CNSf84z*f@X7@<^}6y_GHC z9`IfYQ0F(;5Tl!7`I`mtDcjDlKrNQ2=tt20CZ~N+;vby{Nn|&UPE*%!3g<^Rx@(Il zm^fJ}vYu87Q3Lrh?tJXkI8z&Xqy;_Tm@FgYgS};gCyNHdZ%!PIoQNyiP^02Z=J_HZi(^*)}oDJjS!}u4hms?hy7s-Cg?{7h*k= zn=>J?uK9a1;W;kqefG`vB~#EvTZOx(984*jwL$_7jb1Il6iHqj58c{WT<%KXgF?-W z2OhfkK-uw}*Sig_5$VBCZ6C76@O`0FFk_^~b5(YTM9g;K0(-~|`1KW`GJG0c%wav> zv%7*>v1?Qs4IKOAU57cw78`YXOi|IIq<;oVnDAb-P|yk%s68#6T!5H+%|Fh`6lFs> zP!=A>vl8)VAck!0mHn_9wzT5TT8^^#@UBn;X42=E~h@Jd7nVf^qZr65Sp_-rT;j z|Bb`c$Hafo$r7p?HW?gShdf2TYRk4(H8;P-jt1r1-8O(dV#`Nf@Sp7Ts+P0 z1=YjoOaZ2{Sx8kRZIfBY7Q2LJ7<~|(heip|2=-M2Qg$-1%elQ!+RqJ$kNp{xj#iQ!xdt&U}`4h~bXnikM-7RQ+db4QFj$M*0Q( z=6?L;m)xt5u5Yi%bC@ft4gbDV)83>p1_%Q`y|#Z=jA5pJL1%|tHJzpr3i|KkAc6j| zcKS*x-w&RW)-zg@P7w&Z=Z}{7i0?X^`!h#xCkMBoHoN24bl*iw-fEwl+Ej*y4l$U5 zOsmW4+>ixG+JEoiicM8u z{p*QtFrRQulAI=Z>PM>Ce;!sgJG+`9ExIa$=kKD06*FQ&$ehjhGqz~>{E^Lm=?j7l+D#JLlMa0&Se}V*n)qA0`sy&k1DlFLiKVB)AbADG0~~puma1DHs7_NN}_R>+cpikj+ZS+X+C)7 zVxY6LU{AuPUebgMh-2;b!|S^nN*wsabFz%{4w1cay)>fRuhJUuSWQ}3S)qf`a!ixM zQs1maTy)8X_jBSuJ}_CU7dW8wPn*_ltka^fjVn_#GjCim9Jb0dnN-&y8f*@93?xn% z_+znuyU?&s#V?r;{2$7`n05S@8Y~&KF$1X*nwp)1$Bth5yT{K&90C(uCH~Crpr(yN z`o7zm@V=^IYA1?~-|ZSaZ<*qT%CRTy1zyKV8^{kMZ48~feHul}UUw)8s-E^f&_XvK z%_pX3Qm+viH6%4@gzhH!Xoi+#asO$3n|M!J+2mz*$q%l9hq9CouPuiBR(O>YV3?`5 zSMxGTIoLmY@mD((7mg(yHBLA43{IyhG_Jh(!=9aM{j}Mqm2IBvOirget~WJeLbl=g z_BX7*{rRl0D#S&Ubs3?)WDn2nKK99(lbEYJ9KMCAWI6Xaj$uQ(#T9;_H?Je_VhBTi znPgNdj0;+W0tAxUkmW8Ud?T>PDc6=ke>l3g&Z?ig9#kGii0|AEAhZ}A&M zhJ?P0J*r82tj%HsBkc7Yzb`d>xuquI=>J8BjBt!7P^e;{3rBiW=gNhzrc}Imcq%3| zG@>#^nIN`7o(VquCx0}AMwK_+R3UCF5w*J_nBs7Wh^D4N{d0Yzoldki;v=1UiuJgf zS){!BhxB??`yf_bl^}uLW>(Ppqw5z*0G2K-2&tkp!G_4sH?$yb?~$Q$H2msdd`6w4&pX{8p*8W z7M-lhF{$Du3+Ylvyy0b=gdG4Y6%XmxJ!J$X`ixw?+=2zY3%5}qp3$&Dk-Wfwvxz2{ z(#Zx;Q?6#YKNub=gxIedHW7&Jkyvi#h z=Bo>uB!l>JcKaG25qp-Ri(>m-*iTPlCO}9bnD2K9sOx-rc zbIZQ=2)07go5G&MU-Pm1(rEJDbv!^FOU3!%7bIw5{I3cNFqbo0HOv}4@QEq8Z#(!b zrPHiN4P{G-DtEjBJtCIoQOhJVRF|GT({~r#Gyq^;=JLgH_0v$N z%U7R$Cd6{wRO00o7Qq^CRjWD1l#;WOq{~)^x46584tj;Q3mBl*RWheFamkPxl?^ky z!>vq|VV!XVEA%Fp>)IkDA@z=E$Dou@G4@V$z@D+S4#vc4d$;EAUVr8{hNw$iVVXvVC%+nWM zKVP_sgP``51Vri6`Lhy5hnO%FKo-O^xeBM(GR=pVdwb^7!mTQ!NPIB~c^4vZ9+@78 zY$LNeP?|Tae0jluNw@cj@wDfmgt1B29nE8&Q!BjSRc&Xh=I?o=|5E9aU0qS}+DNW- z-Q!_j>0t*J$b_O&%}Y0}0SzaP^$q4{CQ;X2s*1?s2{9eZ_=SUwrY7LUx8uYFGZJ$c z2m)#n0KFL0d4g=CCJY~Fn32Qyd+6Ju>160zkKE+-LzgbV!R#n@@k3 z5`OG@emYkvyTNkQkvyBznrWQ?Icf+6JFYx6lE*oOE2QzoaX(bsGdcy=o^mfCrCgN& zwd6%(Ml?!yp?m>7g88w;`dj5LNAT~R0*Iu20LJIbyBg~$Sfu3M6ij09i`)u5*?KwZ zH_*w_$Im}i;bnYaSg_=`-#tZ$oM`VlEb5jifY8*jl;4pTc_HC-%74kcd4oERH#u$$ zLyY~YE*D##e)ywc`Un(|4;t+w#ZMe@%us%R%FR7tqjgJVl)ss;zK}R5GUDIB%}Fe_ zfnrVRpyE_mGq;3;4q^wbikJN1qEfGL$gp1vL$Pjj`yWV>SbG&Ok~cH08ImZmBa`Xu za*69RmPGf7>LR0wo4!gJ%)c(OsEjP1k{p7z<`E##bT$p~97w1~yOA(X&D0I~nmmWJ zgTB;Es`go*@hxQH=KZ+sbkOb3qB}{DG?A#-@Rp`QITSPsyu)<_^`4<1q|&a0merrB zUYY&q+g1Fml+zZ+FR5Ml_Q))Y0Ld?5J49o&K+S>H?dtwO?j8G;O4WKXb;74qT77s= z65z81Ui>#=s6xe*1i%($1r#=0X##)LMsYu+N?=0>2n@`nA8Is^8Ryyc*NCTZ3f4x8 zJ)|-o6?f4Gn2E(GhZj?6;8)Y6sVW^QkiFEZawFdS;1rFlu)j8qf9;&bw8nn`sQ@-w z2pUxlyD7BV1etmJ>e+84;bIwSDjPKGzE&=Cv*jGtOaWfi;HCR?%0eV&DLti6gT zo{_4;pbM@135?7^UXTZ_7GqG;6JHJQczK=O=j+~aJExu8DCf}h>teRM9}T5O=4Y5v z28WydXtdPSx`fn%Ic?oRy#%9^Ii<$+XbFfi<`P^dB0- zDYRg8Z<^a4)Wl5<2JPS6(lpXGQq#z9x=QsbD?y zxoOtH@m`%JzBaJw=*lQ%X@Djo{buiNl!T~3j) zGUGh;(=u1Qq`Q8L*EML+rvv-kqNa~7;)YG&H=2FPu#j`U!OqFm(z`Gx{%M+}3(n0XU!oB>& z>N0%})PC_3P(K!dPil}y-0j=nVD6%W^2KR(ZkfeD?nkFi^<)~A+ zUqt%8f81vhi}7!b*xY?uM%ii2(W`$?lLID}&x7*&mHvqx^&FmUpN{s9_`p^@a=%|cF#|YANVICIMT%?io8XlzMB7u zOlLz(ZSOwyYg=#j%7%rCg2x0UB4!D75>&3>AB4sFa-3}|^gttoer??X9$z%KaHy1T z5vbaYm)||e_+pvr)C&>cp0BhH;GWtS>4Nqz6_Ff>scg!i)Ry(IX<4ze+DAv9xzW0_ zhTmY$7y52)BJHx*T|E}*Wn(7uBT}2Mpn{(x>t(hOoCS|@ABSIPj0^HRSjFprp4Wsx_qMo>R$QHPmoCMe&Jc&=Wcuceio+`ZQL=SiCr&b9pj7&fx+qO-6Ts331~VhMamuyQ@#6snW-yuSjRv&q05A;Mb_z&|xk6l5 z{o~`0sSLUz7VK(!i~t~@-No$9y%bKhJ>MXYqT&V*;LYq|9T_ptXvw8XQO&I`bKw&7 zt9^r!k3E+ZXEfgSVEW#~qSwI@F?+##vHd1uRg)UN&OGDBPc{VuocbE0-_n#stZo<0fFgZYb6bUqI zab!gC2{LXCKo6VM%YNvP(H)eczGSn)uaITZztR+?Jv|hj(OgC`?b-b*d{HCtczCOR z`V;2DRyU@7vr)LLAb^pIZ5~WRDHYv7+m7ye7ExdY@R!IE{K3EwM(O=`5cKuQWNd}KWuu8W z=!%PNAP;PF_U`RAVsK}l7|)V=f zF(-ewaf3|VGC9lCY9AlyWJ{YoBl)GOufnV)DH*@-7n<|0<`xPr6t{wl^>!)X#LL}} z-m44?nz&nH$o0B@=6P)FD_n~o_$M^Te&||J$Ipq4XwCCTnMhO_$(SBo)x73sm$l_D zH(=PMtk-|)eDK*>vM|}f*Hj1H5ZUnIVsBMt6`8)1IBriRwNiNE`>FhD?J+Lek-*a6 znQ&dnV}C1wj0*8I=8I8`4>YF2qe%W&T}bC5zQz{2e~MW@=55!#m(=F80k@j9r3o|~ zs3}tHIzEZ*J^AnG_v_lvAn`=8(Hudn9hrNm>ElejQLTL(EncKVlDwK4rZo*-gG|hi zIHWhO>ig%9&R(60h^B0Dx^8cnj%T2la=C%(upE6`DB7s-SE8v{{jy!JeL;~LbPAotrW{D%$&V-(1RlqPIW88iKMmhDV23GudMR(% zg6r!9(q5}GNnISBKGNPW#eUKTt*2)Ds6Nvk{=8+73`cMItBGz=V+Tzsv39T3m4)`= zzE1y|XP%8(f~Y{l%P<&)g}E1Rd0W3L$QHUY5U7LqMwj*hyf-@Hv#ffPchCy+0h}aH z6k0F#W8RQ>k|&_>aKx7}4w&4{>P1Y^zbOVf4Vc0ndH_mOfdrnFfgJ6RZ!3}~2g(;wzyAy)r!Qsc zpe;rPb__Y`02<^seV-${o1n$qhywV#kY1Qs_v(0}py&g``$B~b=&652dRYs#FboDmB8#tnYzQ_*^+gGi)d9$pUCHs=Yh(mUQiGoCdx*cs%nQxkY7i0{N z%ULUVd|kdTHYWT((JtL1nN67B3ur2_sBG|=Z8w2C9Ik%xodqDCgN1+otb0gXG*#&? z`f;0DLnyi!-efCsC&K*6ExYT9GDoSYVVHIK!@_LRu zy-BktNmRh9t1FBQN=)@^twC?AQH5(x(R+|hPT*l>;ZC0!s=wt$V5uTiQ!CutSFNvK@S|*s|&sn1wz9#z%$o1c7X&?I>g} zeS9Hhk)}n>xj)lxLk#RE8AtRx1?mX4Ir*_Nv-|p!hl6yQc9^-r=%X%yC)o-P`sccKAHm${4R4(y=z*n)P9IuXE z23YI&)FS7`ad%Bs^_*wOTaok!4X$i>hRDfQpjWoth!n{3P-$zz&w#IMn>%BDMONbw z9S(qWs|yb5@b?o=4~6H_EG`e~a#`Y&9To<~A1^D`tu(AGo*Bw1<%6rV(Xp}nUPa(8 zfjQ+d*seRHrc4#G0=v(JA zXzoSb!F%jE-$!TxceFZ5*qf9S%1Lo8V2oPls9blxY z&bN;{x%7SskKWdY?3j%lZRkm&hf=*=akbhk(v-fcl^nFk?Q7ikBQgelc2(j6wr5IQ zq0&wmJ#vs*>8!Tj)3PZVkj{&}r)9O{?Uc$8Fw-5=Q+blWE;{9&D_*??-IJIEN`W$=~J3n>(DxK~SH)77}VK5s%PoI(c zI1Mb4(`4EEGp4c>Btn9xb70YOVtrBa*GcIMwTk`WC*ejjWg5P_k*|Kx&}P!Yexm*A z3Dv+2W^jbcr`DMd%g9V|ET~*rHKd0-8z6H6smjbnP~Uk%!+IwvEP9V|Ok1}?+5jU`?BGe1>gHDD=@3GHyJKq)}Q_JxJk&qHbBiKF9ldd6)_6rL6 zf<6|j`3A2&Wz{tNnt>)gmpPg;a1 zEy)}|*T@nh0Q-Y)Nq30ye(u+yJ=W~*?aSfoGYKMUJ%mk6rwz?esQFBcz8E2x@X0+A za|bhX^A&rK8}Xmr1BRJVMQff?Il))AoXVR1ha4A<#{@PGol8)Vchm1;I-@Q{MNHq; zI~=)iiJ#3U8?>>}QhU$$G?i$b{!>e-3gNc5Rm;`&74)c6!W{QHHiQ|IDLf`B<__FJ z57;o$!k8ewCJC;185mn%VIC{C&mt}7D+!BW0ZL{OmMt8v52`f&EX|dE&{{8Mo5Jvd zZ8@2(C9b+!L@$57Uudfjd`RwfaD{sraE7l44*c0#a5MUkn()8N5&yr&d8J}TlB+X4 Riu&JN+8TQ58XP)}x#CqR3GU7ujt6U06NkcaF#4@P;6 zg@bZ};3_9&yplTI19+v8Mj(OnwBG|iLr>2~tLN*U0l3FKA`tKifx~K%-ioWQbJ4Wt zup{;uEl`-HCB6J4UTeI=lB1pbS+5&V5B2~zto0QXd0oBj!vI*r9^2mD^_ma zbPsQw;Wsb;XeE;1LSl%&Wv=rEGsHxyM4~Z1S4Om&o|*9BuTHP<-k%`^yqg<_ck9O1 zXB7bKE5mDLh$Da(Q3o1bhYUK*Q7tSyUa-L)*SP&WPFVI68aEteN)1~XS5rk>-nSzB z?e(nWFZ>}UR5Z6%%eLuE@fGZVjf6R}OR`vs{D2e{1Cm8PfUzdoT=8TwPFe=G#Ks&p z7rv#E6@UZpvv=j`qe`OoE?Y;mlwp>uQ%FX1lL@djcIgr3RPey-D$XqD(b2{t!G(nK z^=g&R^Q7M5BTVsQXj?F}gj036ax=Z8=ypOwqv>&FV}p_ftG;3u8C(_)H_2X`5*%HH zEO_Ys1p7v`%CRO7(s~JPO89Ww2tNQKKX6aJbCYa&V;(GmHj1Fg8*X}18Nn8y;zFA? zwwY7YO`pTUs6!;N#PcLGu5{wPe~AK%(wzR|;k9!{q%F`9<&teu1w>S;Bz1f#(Pd~; zLRALCU;LHm0L^n?vSA456X`~x-(|_3(E@5ox3}r|w1kC1*m?YYZ09nmm_FZmuB$_# zk{v%y>m^Tdy90z-*!iA8Ha^SqoV$&AN=gVf{Js3@&#zS*=V95VC*dZ|_X01eJuHPj z&t)6guurq})cOc3)yB9D8i{uP!Kq4`zV|eWQlf~CDCb*JYct+SEPZQGxqjV25jnSM zi$-ZODVp9Fbu$QxA0GVsB6CBO0b0Vcous}uq5ufZZ8bLCugAyzK0RM+`mi$2GJiv9 zeodu0bcZ0&_8$Dx%o9Ow{K3RFpuA9F*>v9=AC(~^QdPo4KdOtgn7R1!95RCBkF*!g z*JLGxVL=XTJcJ&;bovwyD>{oJ9UPpxCuKKnE zx(p0Ic;-AliYQ8n8m9ty9dh4Qt01R>kA73vm+XbG+$bNs;p)ye4it3y2wdq9p-6wE zlxVgiS?NEEF{KCPA@m?0M%80hRL1X|AV(KFZsa^L(M{^rz0 zfLvUvu~gv$st_YIao`u;jrUnd_I6dZ?ln-nefudZ-97H1;6JET9r9*AF){!E002ov JPDHLkV1lm|RXG3v diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/Square71x71Logo.png b/keysas-usbfilter/tray-app/src-tauri/icons/Square71x71Logo.png deleted file mode 100644 index 63440d7984936a9caa89275928d8dce97e4d033b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2011 zcmV<12PF83P) zNQT)H*aaHEvPo@cmXa#lOYSVWlpR1nAeK#0OX|;=*_qi5z??aA=FFLM-4Sq2kUOhO z__7Kf+yUXO;t~3LY3h_?kg^Ly_=vx^#d`M`3g*hiK~ZY3AT~jwFz3ZcM?f3JYN1%a z6(!V_i6eLKHt^>r*a)I0z_0NJhQk($6o5l!E{?JkPrSxoeQ-;Fqc_D`_YF8=rsANr zG)LA_971eEG~9CGYBLi@?p9m)@)Tx607JQ+*Ue@kj-@a(D+T!4#k)I>|5h&OqgB`h z?c4$tE)KfVHvW8WK2f$Y7BwM~AJbeyzOSy~m#(8wbuiN%36#mj3KfSHV@MPU&upJC z26nV0*ffeHL`yvW^BH8IFmcq)d*U$Vl;hFt@(S`@2NOr}7Sd+Fp?rbjZ-XVpiL+ZJ zVf=)*k4NU-1sB(fAHUA1R4M)eyT=i=ZEY{1xRDA;0LLFcXEjsGBO-LlIJ_9C(9GAXuL zTaWXYBX?I{f^r>rHH*sm()GzY;)y_KC4pG$l!1wRaq#9`i86Kr+wt%Lp<83lq@x7B zc+~kD7&vz;-52pYhf9^cUJaN~#g4OG2QA=;{?W`wITJf(pw%Y67s?G_QcOUGi6G6& zes8BV2#>7foT{<4uXDpmrPUS?Y#N*Dc@w_-L=?H*HrkF$d z3#j0$2Sp3K2%hvFtymS9Sa)qEdq;w&zs&Xs0O0ycQ zotoD}7%D-MawgdX3vAu0raMUP)Mv~{MWbR(S_xv|QUu#_sO6A2bqlWvmiXwRRCa(P zrkd;tCrIm!27Jr$U`;uIDWY{FbGBTGA*OV zaq5*ndh8t-G|j7}W|J`FP8pl}HkPBUggH&DxJAlnPY$8scRI#6B;VhC88^|5Yw+Yw zFCZhin_c2;@Q?8%idU?`0AtcEb2~yxj9bROOps?20l^aI_TFE9(tF{z-yMMgA%zc2 z&=P-y{B&LH&tZx4DR**bcD>1&f?pVFQJX093q$1Y1bU|txk2hWkd(uZoI-_?$%A_< zj9#-AT7##pEbqV(?3jbINuVFV+y(4ETyBH8=ZjV&T43g4Od410WtYMbY;mOUw5}mR zm}em*yjgmZBrt*Rwfgs$&57DLxX0`84J8Wpfr?mqW>@9Q`v=b@3@>-;s2ay^AGb|G z<6sHfKvDhCp|(Ve;bzEcvl3O;*J%g4%2fpH=m(LF-ZdyZU1QbHsqFQSE-uy)Xaxb* zSL{BCOVmU2;8(hf{{5BA37-zT*~-HPxP<1#!&DztK74BQf4R+BWyl2;uM4NAH38ll z)?^!My^IQCPqXx!6D!LZt!(O(KGg{Rd}Pcg?FQ!DagHC3ltZvYG*|f@ACA5 z(y$gMwjP<7kBkLc{{3_A^=#U;p=LeX-Jli8g)Q4S zGsR5xg_uRQNQ?m0(5Dd4a{mz+l&#zm6l9G~=l9G~=k}HOSD-3Se z=jhwnuK|Cl<(>yq#FY^_60{B#=L!9<4oE+T!cL+`@6H3nF8HuR!uOycre0(cw+R)s zrXgw)9=+XH;QO7tEq!W5CUINfkhlOY*hZ-ijQkgQi9K~92bSxob%4Nfvqh88H~~nx4}GW7*L4jK^Py8nIo~x?+DryN$BTbk-|idT*N-e1Rex&uYxV8 zs;+vp|9Rr`zilkh+9til7D(?B%R(0-awITYu&enHvQ*rlq~fJXBoGMhV~fOV=|9Sz zk1j^!w~cK|E}ELFSzIe&R%qSO0o{x1yR+jkFgySCIvN*o&;lgREZ5PMw8rCoZ%QaX64C6^AXjaDf@M)O$fvw-Xm4 zt^`?V3UU)UuwtamC!Smc9uo<@k+`s;bllrS^0Va7iZ6r1vL1bPqV(2-93i1s$!T_D z7tto2#+s{;0~f3~jCJXYVqMD{n-L>?PJ6{s>>3BCj-7BZCXma<7nLp7)5N-2qp=YV z=uVqAdF{DaGK9W%ej3I74qbe*Ru1bXZOmb3#=x4dbdQe->(6ixLJ_>E)#QNzWXYcvW6ai{SG;$nFpf0nwv+(Nj!yGQQA zUjKFVWcY)R=mSTSED7eq+Po4|hgBUmOg zkxAe-S?M+cy74QOzJD{YBEl8BjD+U{A(=!MwcUdbDtM-|mVC1Zx*)wlldbxix&h}~ zRB>33<*kdnuy;t-t6PvK<3wNI%9No1-|!#7YMWLcVAWl)1%p7~kc$3Nj$`HYL?M?0 zHxgEOAjF!;?1ND$Ef*2drN7=hd~o}v;4!>O3aweAlzARE_O}LilNFK4f?FK>YAxny zg2e4Vs4e$@uZb#ffkjd|RPYdw(%@GhA!(do1fM}jYLPj~0OjZkyfM7?RV?ngr&#W7 zX>~NBj1Qz>{1lVP2ySYTM{2Z|9H#MIhAaKWJF8x!k$U$IIvSxxdzUT<8vqS)N*xyF z<7b`?NEKahvOxm3lGd@nhY#*Zd~YHoV28eSq9K;?>@rv3-WZouE6y`|u9yYXY%m~Q z2&dzR6|@f*?FxME>BG)S>h6kG4^pWuFu>SduoXjcxYq42)?UC>ppv++c&4o~W06%- zxJK2rAr7q$?q!9R6{DG}V2niO%37i?c3{JM_^St3fp9J_9t7h%(n#c) zI1GAp+(Mf4lE_tjdT?hR1hBxA)FjuQ$)d=r+mM2As#CFx(5bUnnd%h#WNL!Or=6fg zSrK0}ErG))U%UPO@26l$bbO7cO7#j^KK@~2RzxhaN)kiZv!lDBr6utA>3wGtgs`~5 z;JIkJAKSK$3X4VN4Jr2bC=;11U)JbUFc&34T41-n8HlSr*&jTr9Zr1O!FrERIr{b1 zDBgBKiUUj9Yo+yH4%aLS%;Y-+{sXhe$40FlMCA&W3q&RhZuYEasfCVd9na1V$R~po zrGm42x@cZVTpyFZk|kE=HRcDjk$NCS2_`F5;_C^+w2TC1x+ucV%B0sb2s$ib9Bd_un1t9}B+W_q;KcXHeqea5`f}#vwDo;9E(yh-Bp~2o zJ1Nz{OB2MFJe;k@UUh{iN*35uR)R_oo=Nz~RRkam&4m)cMMec9L)|06# z%}rAOmFG@q1~y+tYxV$h!wE+OQ_4x7-z({de9*XF4mQVf1=dWz@46 zg>a{{Gg}lEOcsz*-|DxY^8T0`EjT4#cz?KFJsuq;l?ZHMe4HWCWw13vwc$OS_n<(= z7R%@GcvBwlB_<_VQ;ah{M0~}k_$Mx4Ylb1a6!{cSN^b4;TaLmf6tUFtWatK_6f^cE&b_un2M|G?W_mkF9Cw)GzMsK>bTBr9#h4x_TJ_mxiyvpcx z(mHY#ojg0~sYK?TnQqBW;=&w+W((Hou&^&4;V9REo74rO)9W*EFf?P;`-M{5ebqtk(uz+ljul8XxR$4c;uCf zPh2p%Y@JJ++Klp_Aoy&xO%M?I;pL*n#;l6Wme+33E;?q zyB_qeHy|InYJ`nx5}3)GqQV0000N?3#xh7$lMzK8K=2xV( zktZjJ6YWNPc&1V{V~9QO?wPSoe)&new!5c$`gL_xy=nl)7-I|@5S|!RE;#(*f`XTT z%IP$>fC3K!xWbiM1xA1;A;OEF0;RS9X&Hz~*wF&SQ}Ba5Cgs6^7&#F-f3wB^@9@_t z$O^=xK?#kFNN9x|9p)QaAUVyy&=;T|sk zwhJjSG?B<3unKw-yl^_;g;(&W>UnIOJn!-fHn`t4%wEFf+A*ZS@I>Cf;p0RlP0s;G zB{}b{#5u}^5^sk1l@se~@i8l=@tL8BbQW-^>Dl6){24N!b39M@YXN#!DArs_8n0j& zM7tPYQf3l@aMuHp1$({Ify*S_r11k239S(w1##jdA;7!m4npDq;V}$oy{{vu+pySJ z7!XWki(gQUJMkz$=Y@S<+E!0v+E`2_>}$m~UZ zH-FM*u>cn2AtPR2G@Z6;pKvrONJx2ntwR0z zRj_HCj7Ti`&d}?{ep{75CX38{XcpSwS0fTBLDmIK(TCzoZBGDy#h(QWQWFtNkn+nc z&HE=LXekQxj*eiAG$2mDRQ&_=D~l7fDuh%-goKX<5(vBP$9+U0P%XB-$mzC<2akVu51 zlgo=P^}d5VpZt~UrEfh*fsW{#ruW6=u)(J*o0#lK5~p_(u+}HZ7D4Ej2dH+vxAPuk zL~0d~!_BUM7$E@bSgVhSZvgbx+-!}b>xJ1=HNqeWHC(*PWG$B@<*gR+F<6baDgVwY z3MJd;Z`$GcZY<7KAOo00fqkhzNfPWOjkQ{Ykla{Ht-kb~(Ya?X8wdH@_Mdzl%kqzZ zH=W3;i3t573JATCF@-e*3E{UlQc00xdQv0{%aqOD$H~cY*mkN_V=|LcnYGw~mV|^{ zf^A3vJCRrjL^8*6MBLD}Gnr?%FSLCfE3nEXos98pqB4$55+y*To%Hp^?@m0=^o#># zlQcSOJ&^DqC59_?JGhygkor0+MRoPyBssdv=ttOB9g>F{=5yuOz}46V&w& zb7%Z<1{okpGn%*@BeMw&Uq4`weLC;GC04vZCMN~FHmn!ET^;!t{M z=&o?zkssvFyM5mj+0|(Jpy#B&oYVj^Dir- z2+^5u8u=)#@r}uT;vy4YOh@+p>sMuNwv2% zV`mX&0RVvA!ra6W0KlhHFaTpb9S)*@kxmy`T9_C*N9S!&S!d3=xyV1=_B!lXe$8uc z4wlWdGBTItapnO_-~O!KZO(TF#Q%JBHz8%{(mp%(X-@^}N}rvXgUL=pRL&DHONu#q z=N>0>n3?2~bOw~i);4&Vbbp*ioNJh{Q z^{t-yi7pEDX@5PJcJJx`oBm&qgRyWqHl9?otN8zKrYldLFZ{vuVZqFLDRE$SXzz8+ z@Z4e4E$W;7_(v|EXWtPgpLRY(eIGQCA8W`Y+ZxyO+`n*B=^SS!S3 ze^OWD4-VhhKv(Vu4+$}MnFC)x7$JteaQkTLyX@uv?dYPeY{I$qjAF*c%sFvCSwQ7- z%icb+?_HtyMC3tBvEs#*#zmbCd?WU{M?7|MH|E8rZaO|N=_VhFk-o7~yyd80-)7hnVq7j=Ji?5o%544B;xp(Il zD4w~0H%NP@9N^1~Hmqi>Mkif3$ zN8x|bQoAK`TG~0&clT#-we#K~5@e#%+rGB9eV)-BFXKB(Tz2Io)n3>GnB$F3v5tW` z8sSMz>th~{D=9)1}@ z3g$b{MPBt85o0-CAhXGWnu%96nSq_!!>dM6Z61vr*vR%JO&-ZifMrDoj4;$^+Bk>_ zgtz2FLYQ~tq%)_nGT@`%;&>@pbXLkilx*L(EVPoLIZgxt7ft{8#}2srLc`t><74cj zLYW0qw_fncrc;SJmq*R2t2!8A335z1LZO7=yX%j+p33^l0*fmE)u7mbg~GS9>(^S< zLxwp{4_e4NxopE5 z@qSLnC_{#M=03^OtsiUfLYir2{~(^DZMi@aDJu!+c#I~eAU=I~@eL%%-H$<~>4lQ( zme&uomBhF~MKsd-wLS#(Auidp;L zZ&i91s%QbjT^}~C9u8Xx@D!H!CCET>pi8dQnRuNH1zEHWuOtt!omv8RNJ5bG?sHsr zY{y?=G1&VP>rIEy7h8y7P~R8*ICI7;;Lz@bc(q@{5061B_sr>0K1Y<0W_n<&L~O0o z)*(c9fb^*uh;gVU7X>CT1b`24+s-US6sb}4;u+=);K7Q4rVH-w_du4g%7>y-8A&MQ zK3z11aI|^hGqv>-!zS@=11M7f$D2|2?ECU^KOo0&(9H1+L9}qv%mjeAw3|1_SiVsr zeznoRzDe)c8bHlb=Y2@|=`$myj4cOXnKMGnIA##Z3o6+(l}uKrQkPMEF~r&ehk}UT zP4AzRK6xMl17v+2O0O$23so@@fGBR+LUoX~xGdso5mAmwrx;hpDqB>jSy}-xV+kul zT8e(2u-I;{_=JES^HFqm#KALpKnAbidEYtK<8QHiGcjFpx6aC2_rs)M7ysSc2@uP~ z6q!i6nQEkE0(W$IMi?kOD?OH-?$_XhU>*g>X=|PlBJx%Y-XjIahvVcB!&bsy%uvNm|R z>WU=ew>1fBz9g6IYamY=P&NEiTS>iiUh4eLUHIXv2}dw`dpY9&gQXEd@jy!$Q8UB zWf84B$mI~9iKbWMn~qwWD-gN9p`tRN$&0eSu$|5=E%oD&`wg|fkMe$l2d;#GHJ~{H zW&DJKHxHq|9^}hGo|rQ&9l^abfmLLBvPK=J#fr>Pb{n*`4khuSaETk;WKo7{CN9kd zT}VYZ%lCt#gO`#Ljt@O+;t|gQezuQgiCMOWq&uU#0e&*%?bmILDS$j+dC8Li`L!R&qAAKU}BIAVS$Nx9FlJFikZx>c`}s2 zVK*hspd>D|sVPfK74)Mo)`4I)9EG8v$Ked|HJV)gK(07!n7q9y4VL;hI@4HMVZqr( zUyP!1ICF=ZptFF==07PHPjeiz5e|dmI9_kaj#WM(XQN$s8UGanPoz&jF!Cp;KCWXh z1@_~$_)2|oF1kI)hodgM49#QM4}#n9pB*??r+?)+-TQ+tmoDtFtWu>;w<$UH0FgH;7! zcsVH^X-pprYF-u;6XR+C@t~Kl44D;%tcoi`mS9($r7Ln?iWi~;U8&q2*Ne|!xQ>y5 zx6wag2iz=aD;IdsWdQ2)FbK|wdbb8&m*PZyt2rdmHk05_p?uBMOBm=KMHmOKF^`z7Z5-3p{$M4_ur;(#Ocd}y++ZQ&{JRn zaq#l3a$LwPsbh9brsIMdnHxhumm5CkqT?V6Q?$j&bI!%K5dy>>l=lVgi0h|e1UkVPBMS#ma zEO5mpN%d`TF3_2ZOX|WJb`KFgHh>BE1qNzPj?jV>n_#}Qo|$6dWQbaA&;caCYsfrE zWh$5Vwar2So_P@8;_MenKXKT0DvY9iF-~w+#EHod906>8TaZ zp-XeI4mL>wqsWX7tO+A20KDSAX3RmlFZe@;+46U{aTjVbX?j!}28uKRw`?T(b2Ee` z0qu>s;f0bcy|M|9A%U`Jo&*`*$b;WhGt{;SmijF>;C;166~mQJ!pyk0nLw~E6YcBE zy=`wIozk85vy*lr3X1@dK9)in6GU&)w*)@%{DYxC-H^!Qc=@pKPNR0H0AX8YFB@jG z73q1?a9}%%J3;MyS37Y*!Ru{%owFDk3Xyj zboWC*D&VF%VkV+d{L35=;2>qCck=Bed(x3dYft`xFdj*mhO2fdxLZ1m!55j`Z}Lj5 zQXjow9$N!ap$84O#jBVnZxfg#hdkJps~EKj!!B$GtEw5-28X4^d&!|Dh>t>zMe$Zc zBzIUi0c*p4P$|4pBAC&SIdDHbU`2Ery7EezKq`EIIgTlGA9bmmp7w5WU2M zXtJoL;bTvR^|#hLXb!cR^2buLl4ii8EFhKb>}9b~a+l-m!FcR18=vN%`W^d6wawFz zCVWBL5e}o<^!MarxwfXaX28bTXP2)A?w-3-4{7W%s6)0sBNyZC>mQajDQ-n$UW@8 zGN~^sJM7A0t^~3W)W|wD_$>5T2Tu3wM{OP?!#hQ+$+c~&%oT6ZLzx&;W=Qf|@RoLf zXg})Tg$agG`jUT$YZJZ!Baiu#?7$lF^|yTd*}LlH*rM0*FL;mwTjw_3c*{YiY8LP| z)5Jlz+wEiW=Fvm(+U|lkdwwk;+K(bB+Lt?M&EPglIdNyVz}l{?!SO@ik1aQ=@+7D7 ziTO)8-cLfB@w0cEsz;_$P_0~P^%1szhrb11kfucUYk>-zqXsy{BOVlOwTIZ~A4im_ z8TfnUhpnkaGG@RkS+Bc&6VE2r*8hF^R5BxrdBzha0%ayag_#M^g!_{LI2HOIy+mGE z+Ulv}cZ7F-E^F^#Y13qKExjZ+ABkxEJHB_&8v0Z8#lW=D)nA%t{Ebfp^B-6SB#|O3R^59ZCTO!P&AY>oa?!7 zD$FkQEb%l*t;zz4@S08fBL(^|kzb?^@^|01mzQ@31sJ=Ro0kdK59ibIO8~tp9pxc* zc`StCY-Fg&`L6J6je;4$a~4D}{frxJ7M0EvFRDr~?=D6cTme2Whm8X6W&Y`z&X0e8 zuQs6Nx5lrB21m4AGDy~z9trvSNoA^N`GCTn3Rr`VJ+dW2Hp1t1V!=|{bSd&>P`lk< zK#OCon%R5~zAy4H2lyoTwS~(XEWfrA>2sNqV9jK2YlG0exC@4dcFyTG}CRhl(axm;Lc=h`A4kf(C}TIO5mO0yhI?6kmh zf_ggNIX>)F+-P2W;c$T8{*=FVopYv0tu@pVrZ#iwcrpsvad0W+4V&pz;9ncg04%i8 z%m?tpI7S(sCY@ec+A$JaL=fFyZ$Gv+l(*@XoB0G>Oyh|>LKqAT+sAXWgeqnjI{3sR- zf=!3t4b^R#kaNJUGQIK+`IFZ!7G!D=X@c>#l!+|M-8gC(dom9Vn@&Dx+!o}8Dv6;7 z@4H8Ju*IOSM?!NABD}n4{bFmBaN@vCNdEk$Nvq-ma-?u~4?wz}NCUjMlGvqkU= zjf$N5{O4T0g!1VJtN_!2*D%OHfh&(;C;1(%j0)Om?gz{mKPv*i8BG$IwW3UsllWI? zGq)9NK~M7xDq>5J+D*}6y95O-nPdRKWB?b zNiqCmyZ+q;Mwl401lrb?VM(RTg-Mb#q|TGFT5%B-=oPRA{Maf1&OssO)5SO_6C;)> z5V~mw+SG+fv~~Gn(-i7^t3g?s=qrrPZRMzq z&ZAS{*PcNor9gbgpaZ#`awtL?Ebufah~uM$Y~hoL8I8f!PCC-9Ix2qU$wKc$d0tvV z2On+N6c8}vx%CW8cpi^cL|nw<8E$t&Rhfa)z+)8JRt1(N*!7~=CO^iY^hTFkrtkIH zmp=gCFH3jJS@I;9Bq4{Zk6VAJ9rF$*>RmT45JY<_e^>dnW10BxLa8j!_@@F_uRdK} z5c=)g2@7~W%GZK%kG-&Iha~HW_Wtg|6sr2Ds6Et&=ad!71lVeJ%L(u#=n^7sE&|QR zeB88NX|+(-cwU>l1}BmZJYFP7aflH>-A z_)6R2=HUn~2+P3Xis$wIF0SxGDQ{k6O=`0--P%NQkEswzvIz8@i1izJ)Q5q2#yN)Y zpz-Nmf3oXP&Qtx|S3cR?mgTc$z)Is}0T}Kj2iMN32_sEu((Y($w)K`BI5wy$O0zXo;XiJD|Csl;V34Nw^ElH5_8Nxnd+RjgHFf-P{9(&Phu3T~{r;tU zXBaiuTU-XzeRH<7{&aPCvAg+7yq`AZYm0Z?DaVQxLuf17^-aZzWM-9DJn`}XAPwJkW}`h1>=Y!b3V1NjJFdQM9}kdX?c}CzPA>i% zHY3I|8Tn3y3rJvh%tHBaNsC3JI)Q|#QTdIMQKpYKakLjL0fzl1oe!m!@6=D7Tk`B) z&c4DVBmsG_@S7$xJ^VZFr~Ic7>)1JwaUO7!>$uo5JILO6OXN!qgVEhMSzJ*1xgYwE zVz#>_hL5H&xlKe)@tR*u@Nkp%#S*h$9r>2|;r}@HUOm*|M0!)+G`!E4f2}$q`YZ0z z)EPvPBH}aqvin(B(h9EK_A2>>KXMsa1&{7=t9{+EeW2tu9WygGb%I19^{op9AONea ziKyPZ6L5S^>jbnz|GiD_fWsrbun&owBFq^{n4UKa{h3MANBH*!ButdqLWf$$pw3p8 ztipSA3l1Cf_D0AA%TKG5*~7S+IF;}BGgS)R8QoXnqFbulp8Y95Ti)sIl6)_78r1?oucV`U3Q^C9t|(vKK>J`Ye?JaQpJD<+kmN;!}DP3l-{?v3zS2cZDTS zwwn1~@g1oz@EFFm|5#+=La9j&*F-kGN|)riiO;=5CNXWhsz-lST6^j=@y8N9gJ(sV zt+}9s@9AErw3A-Iy2G&@^E<=gw+u_naLl#4!!L}Gug-Lpof(j{ME=Jj?4swEwyD{ADCg3-iaB5P>Y~;}Vy5zan1F67h_$Qu1 z#R&g`SeTS=58cz->-G?DnZ9ZsWm7!S9id`i+p4Q6!CEZQq@SO?8M(p(MbSznz= zb^;Ch{~irL=x|i7zIO2yS^L*8vS4L@kxQ@j>Lm``<}!N|$n+`QcB!4v5$wcppkLCb zDVCY^)<#?XwRsZ#E+zge1kOP=QzqWH_>W^gp4c?n*E21t>T3bS+WvZ_nWn$rz!~-C zR^Pv-(fL@Byb#~`UH3vk5#XVHJisdM$(k<@W_e%CXN(z&&0|S1xSGWj&~y#Q>CSK+ z#d$k}1&x}~`qwCE`cH4ZhaUX~ql0OG`7(vHR|xfk8mt~?A&2Zx`YR7 zASkZm!UTjis3`|Au;GdkJ0>P-b;|dd@fN2417bhFMj5Xqt)yeTs>c!NAz-NC%*sz=37pn zjpwpSnyVKNJc{|-Z>xasRQYDqrwa!&_O^>BQf9b;FHNtW`LAo50@d^t&xhmjQZL6V z?n}5a7e1DKu5lntaAd$J{U;3>jqxdM*!~RV8X~HFLFG=W>3lUhz^MEb`M9_IH7ai3 zV$BR25jOL@PKLdU`e;TOJIlnK->)L+ClU8axg+ApsU~LQVA73?Ib#NF_o)iatHyx) zOI13iZ+$PItG0?C9Z#5};hfAb`_8Tm$(SDQ<?&)>k?a$RAO}R^keyZq&NYIn>EDLMoa2w2{4A33MoE-4$ z>(7BYyDVjdGQEPQF#WH_1AX)*23nWWTkBN`x%w>suY~>Q5T`V@d!?-00L$0?EZ~~z zX`QiQ5zDSI$M~mHp_z-tMdB9|qNSnd0W^XDU?*9__J8+Sr^5mIyk z>igxoZIxYl5h?JPjR`;2Y**%+&OZ`oX_!25nc5_ zWqf`D`1+3C%@}n7Oa3)rYicKi)%=>`6AL_lJ=ah_-FZ=wfnboHJ}ubdBL{Hon=NNr zgghzMkJp}h)~!1h!=t83rE*1m_PC_|ms zMbMpHTlplB4)Qg-=3RB#ZV+3I^;tkHx8>_of`YQ@)9KOvPb)+)ocdacxQH;Y-U%q1{pT`mF}!^Sm!F{T zMNM{8l&1_o2X3>^duDS9n7+MIvtbuo_Da9QQp9?k=?GUC6Qgl7ERyN1zt?C0B~?otAHaok5)tpAtf1}Y%Wo1ilAv3 zHf6kyQ%m=rXq;3RuBCN#43c>ek+Dq;Tf*MUpkff1Ki5;5hq3n3O5Vt^-r1`e0Wz$C zN|NQ7m0nd>`mVB+CE7weftn|L6z0^imuyY{J-D*_H&$pzD`&>E@1wrFO)O*)?xP~h zR%=Xv2Wb+rFNucBCF1w$X4gt*;~yC>cRC0oCyJ^66niBKAUC+EG=`J756l^kcQqv| zTk>d8dmV>;*f`RwkirK*Y;5rh#sV%Sw87ta0m|Judi-($*^m9gn#ezVTLdnj+*wQ` zsLy2ykxGMa%vvr7WI3JO9XraKXJ)_Gvh8`%NX?dM#El_;KWO-3;%aDqj~piAn$ko6 z*0Xmm$jdt_U4zj}s(`XIA16s5vgQ47vmDi1iXRBXs7+XW^KdA8&8fh4Hc10M`>09A z@lhlwOF(kk=w%BeD+N&u@g0LZC>NRuqkl4+%f*ITZAMKumobbNO`#2-Ql-$2dGC!7 zqwnO>3~TuZjfp=NS25`F+&yFDFbzWx@J(@6h6TFWEyk} zKB%>ULs3`Zhl$HR$Dc!DQ+HLOF9bZqM|B>9hfKj+Q>c2M_2xIMLh-yx+{a?GTNiizz9@eB*%{cWuExBF^$A2$vVZ-)B8pzq3EWb+YNY-VmLMHyUW*Sn7h>N_#uvjenHEF*)iK{`% z$D60Kq4puaM!UghbC(?Odgv#xOyN;0Wc99U&{U47&GX2YHcCSyR>}7IGYbKTW6B&? zig(}LHKm&K=!%3K@JhCDfD^c(WhF0vK@WT#_5MbE`K`aTMzWHYOc|#QHK>hq-Fqmm z5-{iAaR13!CvS*4AU1iu-;leMPp8JpRRW^=b2TNCLq4`^TNAbcgKPM?rd#j`{Ot$b z&ej<>jT&tpFgnWrm~T`~+Jx&F&}dDSJ~SV7wtN4AjMlr`1j8_F|dJz&N{b^-`TVF!9d3T<<(yxAoj>LXOj>bP<{b;q} zUNkk{VPtxI)Lb0kMjgd3a9rLVRe4X_wUjVH*0FCnNub41YL~Gq%6O{Nd;XC6F%{`_ z6pCFQZG)f4`VeaCKK2w2t5N7_msvl!CWeY3R!P?-9j zpT2PDzd$~iNxr2UDi%FAzLRCFtY2<6krVm`B2a?^>6?aYHP@gcsqz7k!xYArVH_VgC>Zx}~MP zCQ|MJtlznXm1abo7r{ct?Qm9FBV~9cptEpnLLPY*!}cmpP8xijUKI=v|NE}s@n>bp zsI_w`*rXj+aoly046r5F&P7sz=%~55u*-I=AJ%&uWGT0tfYh%!59^gO31m6f&XvOS zQ-1_mW3>EJ^oqtnp`}H{HOb5p-Q^Fuh3(tlL5o3G%9mA<*0G!G7p=uX{+i!J-hSg@ zDQX?QCBQ<{n4@4~f9?Bp_{=^iTw|0u@G1_s3Y6F4Bl5uD{2w{eOfWPd+gxBX$J`3wv26J#dmTwghWu+(UZxYz|qWh8SSot&ghzr zz#%NHC&XeJH2uN#Z6|X)8x{hIGTA6Kg!x3{|9N$9i|Bzgn2k*&FAuTlsPun(_8#4{ ze4)Sb^+oPtVZhjl8#XzLq(o&`oVi-*WaZPp40-8S_~V2L8fxtcW1qh5-U8qLOnZ|2 zi@rZlyDJNn8!9RF_9mH(><|-SU<&ODt4-nvd3)AF?`RQ)91T}x1ei05f&b}FM)^r0 zHC9en8O@F9Iy|^%-+r9_NF$wVF11f^5_VibTBr&}Z!@*v3CBvYZY^oA0YcYnu)@%IWk~|X;AkadOz8qKS4$w)O@iey1SS6 z{2;N1_SUv%897yOBcq%jwBw!|b2l)jCzAK0-aRK=;q|3{32!ipXRTZc88;mbj_$g# zg$`XRmbt^)qeGqV^F1ngtht{$yWO!4Ac2q^fy}Wh{0J-mW^;!2tuytq zr%WCjlAr@bS<6amJPd#^`ijIL)?(SdzA*w{o&kG+c}!DM7}2Seq?yitV&JIvmH89x zyKhjHr-{&w;j}mS&1@q5W*45ek{&I ze@rD0Dy>*0A+Ba(=y75(qbl6JUUJ|mwLm^=7bT~6AIKv_D{0}+*yg0p$#XS|ALr*x zp#S!^WTz0S2^Oiobqp_(Fj+hH(W2edojf`R7bs<@q2*-R;D6ymf6IYv7EVR4I!kaN z;60LIC=N65PO~8H>iGFUL^Wk;#&p5ZoH=PCj3ex+5J%%83=na+P#RQrrLn_0mCgIG zep#0X2vdpouBgbCHyC~FwOf4<;PUPa5=6STrSG65iAEJoIqF%ejp1X34C`bG{_&{J zmXm*p8x2f15EQZEm1O5&6;HYlMQ0i3WT%Ebobu7#enTz=H~Lu+8fAb3vjtbW00s5e z&S&q5$hxksEB!q4ig4Z)bXsRD^-cbJb;dX~ik*Up(}cCHe!li~RHZcTxnhw^?vcuE ze^+N08d$lQ*fjk=l2Nh@;`@eSt>NS5UyjyzMfCs3HjW~B! zgn~cQSMC40s9s;0;Abfob5jq=--`#g{mvKPNJ=Ya`W%K{11nZtyK7oB`Bztf-rSe{ zdN#R3m1$|7c$U@mI%h)L#R+ePQ^m&*$zD4K%>3bFyTiK19-*6=ZiZIgV>_sQ>fbn& zc3)9CD3uT4jP|ZhWdbfMbX#^@RJG>?73TE$|74KYZ`8Uiz=zKDcxAR0hY4jnlf11{ z6~AT2*(i&aB5DQI&t$!nT~hZ-UTH}l04AA|5+q^0mB3T6X?{wR7>JNV2WXp1W#9cN zKkA2d{(?9uQAl+A6R5M83d&Y7fZqPkrPjf%lW6=+xpP(7^`mkuk#tpo8x6gqd%Iy5 zX>%*QiG7@-$0UUa2_rO4WXs-|j|0}2Um>RLQD*_!>>Km30OB^l%cWHMWDLA>wS_aE zqH~_R3ixCZ3qd>L*P&rbjQ67pm(3G+DdX|iye^q^{fe=GoBnqyyz6|sa~0gwdSPrn z1}q1jF=*abzDjiy%_uYnoc8+5Zc2w?T&a`gQkJZL`(@-3R<<2?WjW}rnubM-cfV~{ zJ7uA(!S-dKSmb$924jT7XKck`^TjSvMJF3f+|$1!4pMp( z5TqK`p6kE(vXQ4T0U^Q=5Z|KBQa4)-Zj6MYt52G&x2Lf?cj*kZv~wv|4fL@NQRbB@ zj^kFh_9@J%8Urv(bnQPD*m8Srkq2A{d#hNNE``)p!327*^Zz#m1D?3yUh7X1xtVUv zOUOZ^wMVf`56VgEFCS^ln0&)%H&2!kAImd+6mz9S7%dsm?~ADN@+JRbNH1{GGU$vm zL1b?pcko4ixrdCvQ+pMK39cgzqMBTh5EIjv&i)ngL)ke8fA_jZ*F5=mV|~Xaw9NmS zM^F)#pmIe`aNHCG5tYNvxUZ0Pd#CcDqBLSCb1I;jnInV$*2CfElY7%yK^TxHF#e7! z1SG@F7}nXzBg*A4C7mIoEHB%{NKH<~hHVHeH~bT__Id7%cu<~MSy7bc zIf%!Kusf$@1II1(+oJ4*-js?Nl@AVOMFy3u!f_Lh-=W>x*KYS@gSWJnLjJSCg!O4i z^KYtBdXjK~5SH=ckN<8ToF4^Igo<=kNKWsz)RCOAekd6)lbHC9!3#>OA_138hbK%# z-TC4kC%gK*Y}9dJ(PZGBKhrUjUdd&ilqkx*Qyo($^k@eT7?^PO27O&|9#2P$OfUX( zgmP!vU;bnJC83aM@~kv26J5H&nb>Bbug6pEcZ1iOnQI(8`N6;3wiu{`KLg(>H^((f z0SC$RmO8$N>4y1PK=4COvP*#OCO_Io3t1m7zF4grt1BN({?H7HN^?Px#TPC z?*9EhbTTMn>NwWt%q%3xitA>2swz9#s{2x!#t2XQRPR;D21kGXup+;i@k!n;r@&CE z<%11aKZWCyGQj(6P#UBje<*g_uQ=^dXHN=bwITf*aAXO?+f)n`iGviv_wgf~EKX5e8f~ zAA5?N106ul*}n(4+`uN4K=3z?QoDvFpqu^-B3|J8e5S7P>SmsaTa=+($ z!}aD~U-}c^;IZ`5+7^`>I;-e>>oJf=f+mqQhlfwV8DvSWrv?}NZ~iJd$7PFj*eOw= zC&3POKj69%jP`;yjPE=~w%g`$Lo-nvgP4BN3=@X)mFz5}`E^@*q9Vf0gK(b*63hw) zy5T9n$V}&(v*qx$DTefDFw+onfVR^S-O6|F6pi1Is460D+~<+g(8K-bck)#*27~0L zeNQnXs?bOY?@VtXP~x;JVJmiE0ZAgBItP%<5AVQp1sQIDB!}odo2BPR{nVC3GC^;D zUKQB*wr+eZVWZqqV@#7^1=~0rDDWehRNeM*J|D&2t|6d#?sc+-XDi6Q4@C+dZALQg z#G(ym)d%Qqk&@ui$L&@1j4lnSseTdSa zvU~wCPnSwaCw4k`yN2IT zBSnV79VjVFIEbySMCv|k8U9w*vaPhq{~_do*4Ff(o$4itfVAb&RM)7P*^F+Hkm_-o zu0sBDq!Cw=W@4;uB%KlHwh$5<15Yivk@8}=q@YD*8V5{>4v|f}>kE89lx=2sT0Qv1 z)XCVzF75MNN03?&h$q2fME;Nsx7dVQaE_!k$NJfE@lOjvDt>N%MG|*Tx|n$)Z;k&T zBFV|y$25t!(MY$^7hRsM1Q&^*X%OY!DmI6VI{F^J-nZ?EN4mZWYz{21W5MX=u5)f% zm;f(Q?ES*tciL~7Asgk~6G z?CP&|0Q|u)yV?lt%jC^qIHfDb?th4g-x}Y z%?_`t(BtbeX~%QO$%;2`q4Qfkma}2L3tRZmH;z8-C63sZc}04=`JrK}vLNkd>DzQ0 zWI~A?mz*;6K#H2-ovkM8sfs3fTp}@%I$r*g?kVDk`X;>1+gM^iAE#BXFUEpU$+O9bR%+Bqpn?y>SThir1IrSu>+Za#iq}r z<#yAvQ*blz95tQJH$XKK7U9Kky{I*!hqCM--Nx!#%C85wZ;Ehoc-}&_#7* zCSVO8ZO87J04Z;v|LHP>b$|*?pw+&!83|uYEXtSbm;P?&Y%4#o9@gccgq0;)FiRod zGsUq{ykrs5QZxIZ_yE-nM9=rG+?1`}(fx0pf|1629^qJF!X(on%CguA? zI{@b`TtX=6g%Iui4!UO*PzBStp28NJA&-!8YmldoB#nM=aCFI5wv-rojZ%|FI{}}C z(Qn+zTtcE-=`a9!_TitvQUpuUt4+)DsD{sKtVAgtj4Sota|JP!`Xo@o%#JYQ|fhF}`C~i4E?}#Jtozy71v#2_Wj6F(2sSsG|IV`;k20GkH4$r%FPDc2^s*RO*dQ z3)Vd?j?I#PhM$$V1eMSe7q^`h6`h?VZ}s3*Fz_|OLO%RhZq43L`*?CZLrDoH1yRv# z_8QYMiY}VMTtX2FR!>?=Mj;1se9h|;X(cz$JpGE?YNx$i9aMRZots!FH%B*e zuH0vazPhW;ZhuQ!C{-ggjXRa=|?dd5MV@w^TN8(G?gS<7m--hntMV>I0oB-R#Ntnje5q>wZ zW12sW7(_P>LPDQ_HVvlbSn9@v(FR}P=_D+DfBOE$%m)$oXskIP56;n8(gfX)TdSXV z)Q0-e_vYKwVeAKAuN-cr0Hcg&2z7Lf!xeAPCmG3H*U(CEA|A52%z$RC&Y}Xo*+j5+D$SZuXTle}At6Iq0)Hj?P zj@zVPChfb%W^XewKbn1SJ6~q54xU}R9}tgy0XVMva@@(t7|}nXO0bAEUEYGC7@@}5 z5@o#xpm&Z1?(1Q}nCS6z84l#YQEBG%@M|db+cnM&wn|{8IRgeM(F9iS6*|Yotweo+ zb_Ig1Wf=1eD7kN)d}X+&gB{SPq04?6|BoqY9OaUS>S|7p%C2Jn``UfO?dVunXso3Q z!Xfcl{};KZ%+T~3*U?u5XQ;^3>Ukp^7cF_>i*# ztEDvpum(vb%Ohnzqk`v-lU?AK1zd5&PgVoG@nv}bN$0M5iKZTEeI}+e9{(XjKBdKj zbkyFkTYb%b+t1#NU|S8I5@%ABw$ENUeL@p_EgNi}r*~$LRVlF|wm^n+&d^E8`M1Kv z$WJoJq&eJO@SR2mX>VAVJ;Phj5ybgNFzQ?{H2Hz7Mm4RQF8}Za`JrZQP!;5zQ0Qf1 zTSX;fKrcFvEA)AvWjR24ME8OM@{T_{U!YWF4i=9(|4HD-+^JcK-}Ti}$Fw=7-M&4> zW`S!&?Pa>8av2NfA1EI$-ae&Yv{lj1ziYAs1kO2Nl6}PBE6(maNRA*V1354dzmNfX z4PLQixbypzmBnj&{e`d22d%}b&3Wrk-wRzd-FcCIry|`u>MWzhP2Rj5i1KrT7s_C5 zbV^06sMcmf~Ji@3@nbaKD& zF~)V3ll?ItCy7lb1Hd<=yNh`_`2RK(cj&)Zc#tZ#KhQ(||RqzUg(<(23MmKkS1J2|4A zz-Ny+JuS3UsKRCWugL<(sHN%Ozv??9`#w+Md#^h|)#D$%mz^xCX$~%?Eeu>y!9A}} zu#!|b_UobCJXANREwbRo|57RUujCe*;J$9&v)}9uN~Nkd|JKgnbYRL?#AbEsuh&%q zR= zdPR)!Ifl3SKl?~{`VZ8Dzz>bT^+G`W=cd7#AYegyCY|{H%$27So!f~M73y&W$ja5< zNBbt|;psoRuB%7H(y~{Q?~aFqFStZx-ChfPFY=MlD8ehu+{}kGD=Anr_9C9_}mZbDxdyh}o2(oEq$ z`0IR=aW>v(yrdI+#|dSS7;!!Nr|s6Dzrw8KdURNQOq`bgR~(pbr*|)zG$=7uCLT-E zJZd&bpzjL3xS5Z-RatN{nZFiap0oDoT2SP&)XxIP{y&^GQfxb0anI-U2HI63sC}0) z2xu5Q2Il|fpM+<%Wz+ELt+aFElUlF#KPiAOx4AwfzxFnZj)i{OjJMY+q_&;8Cunk3 z(^&HJuyLPYu*+Jj+FXhC@uxvmwUGPxGaala$lC|)Gx*do2Kj>Wa`L-Xk~i5FP9ArQ z-}#sLQxP5LYdmp;|N8Yxb4Q1FtmtcZ&yP*j5jC}*q93dxnQcT14(s82k`3W*JhbE# zK!Blf_?usrChT@!L&!;NM7LJ8Yoc03#g;g>QSry7>zcAF(drpm7^q4Jmu$PV!BovZ z<6$q@_P+KfRMK%?nxQVN{O`qpi!4fjm683BL=c-N2`~lSfdZ^xDSbdCc3BJiX< z@4oJqS4$63s20@stG!JAq~*hmen7nN0BwIUXkmIJkgIx+RaR71y8Er^y*?eai2kQ{ zVn;1s9u4+2g-VP;fFF9HH%WUX_j|V5b36-@>1s5+F?_>TI-T?|_IP_x6PDQd%t<_y zQZbnsB)c?(F%xeH1Zt%s0)a-u5#_fa*EAr)gHGyWh@h2-k)%80ukAheP#T*ElO>eU zk8d^LFOj;sYP&yqZEDm7fqqDj7T7`T-8zNZzW)xJXoZG7GTJdH1mW6go9_qdesxh~ zgev?l@!A`6CVSR;-nKd0;FqGINnbtcjB;C7<=mCeXlHkT9yRg2;QN7OLK~EVH{dX0 zt1ae@EaNAYcqU3`!~l%)-5P4Ez~A?^7s)W9ERF~Fw{j#Y+MwM??jmR{z}H^3U^wIF zmEwy)C(zq5Y`_>*nUf~NH0qi0GhIP0T8R)<1_>Lcl0>#rJJr`x%$*>qW%93U!8otjT*PpcP|Z@)s!8=)!2Ni_dcW`fMp_Ewgv|0@ zNNS`s+Da|rk-0vF>+P|eS?*2HiS#Fgn-mxb&k-6Cen*jYcAlx*?O>le)}biTSzWH~ ztcI~}B``m+(k*H0t-U5C2&OXuzBTi}x8_#g{(LiM|M5?MOrJK3r^N&Q9*~k!yC`v> z@3C1C`Jc4herExy{<>6P2)~1LXE^=eip55=N!U~LvMnS_4@~?fDhv(M)_3B!d$fXw)()N$V^R3@X zl>Gba-_vjwL51$;wm-|IdJ${9f)97Lk^IzzS7su0e44w#AGPOVzCa-hs{pw{Uz0@Uddaj+U4aM-U^XN5iZ9KIqSai`x*bxu8v#*XpxHrK}b9*A*? zn{(@?7}luAtSXoDhn?p_rUSC@@%<@wNn9K95fR1=gZn8P882%A7RtL) z`-gd(*&D{ap|4h;27ZDZbsje82Z7skFCuF)nU)y-1YCsuP_cM6{&<-+a_4J#a@|bI z$E#njrYlJGFn01Ptp9O+y}nQ)olkM6UiPP#cvAOZ$?Jolnj}_`93_7kTDwnPZwD(5qYhz%M__z=3c7p-oDCs9fj_$hpRa(>GPwGiddP#z>uvLuFV0lq`cx~}>kt5oo3Yg_sPhx~{MYyh zcR1N{QUi4LHqlbnA2H{^1Fzqds!1c78vhHx24PO%3)$qb zWz2LjI6dZBB1Z{Ckec4zzK`0GZ`M5)=u;hyKEbmO43CvIh$6G${`J6gO{I#9<9qHA z{ihzXJbp{@d_W^&v2he+_i!Ii|40A6oe(3*Elvq=IV1{8rIl+n7R>IN#skD%V22~1 zj46>Cw`r_(*GZB?Y6Id3_Hk-iT!r`s5);oNX74q3`%-8X1ZB6L&S29uc6EC0GWJre z0tK&+vdLhc18%?+JMv-_x>*W0O3828!lRs#P62^T)yOtQx z(o!T@h-e=X$bR7s+Q=4cdw7!b{^aPannj*RIV@rm^{ViqUtixZF{=_5<u%oFUn&Hh~ zqsk+#0zvj!1svpX^1)a?D&;S8oNhTg%!vn_s#&T=q5QAHoyUIm8P%7-nG$95&mDs% z$(qR0PaaqoS|H{9@09S0a}~My{wx}sNWdOg|KeGY2|R%CVt_Em4EZ`_RWl=2a(u2k zWIx3{E*$Vw7u;ay4r=*m`nCS^}fR<@5yet_-q?Zr{+U9(x&*(3R7*@p^Uf9O<<4&Q3ekMI) z9usDi0q=0ftG?c|_PkiVN23(S@6yeTD_62a7i_-y$U&PKKQ4)uq|Jom zTC7$DbeNea8HscnWPuaP;@5!{fIBYbAz$n4#A+^Io5hv; z(xT7`lUwNKoy(o95Q}30)g{v`GVGqjGyPNQ#f9^~4%sqmb&=_O#IRD!s35Vk>W_H# zX*46AL2V{HEAf2oliNKU9}7~C{Ovu`0AIsj2E6Q_q9d;z7{97t&?CR?!19HRd*ZIr zJ~>tWItaXzLRzr+68rZN$WwT#B-(DlX!mel*@-(|H`{ylDi~37L-$77Jz)cixESn> zs1-m#9Ni0zj$k&o8)zNi?xE<&{5HNTMhm!}U!mTw8bG0bBD)MC{pJSI2&A+1Nk-TQ z#6@;|pTQ1%z9YxP1p+3Wr_{bSBVtd}GTf&U%zHO)UPXHgm`iRMM493Wrxp*2im)zH z81DfE)c((QF`r*+Wh8Ch(2c|i$!6RT(Czq zu8=H{3x8oJ8lV5&{lSZa#t}FddcZfWr&bSxeK~8*<>Kq++eZ}xLSSa0@ z3l}=-gjPoiw}n+qDugEpgI|I*70IT2K=|vn&6RwxMt#9%(BDAZlWbk98IU+y zMUnWNX2IcX)& zc&1%-TS3dXj%80r7`df7Ha22mdfrxc^R_ZTAa;S#VPS0Yzl}h8hJ?DI;6)*$R;6(aMfz3JXc!g?S19$&8ze9y>lZ|2mof=g%}`&tnDg$b<)>M3z0ym_>d%);=fo1((=9()zr8428+H9m zc<$E)X^x&5c)IVul9ZwVML1S?js7^II2b)*35xID`$#>yRb3vCRtHyQ!U^5uleo}X zvTQnZ>dDVIy-m-z%2@o12~g`t{sV%*%6N+ouyN%$A`R+UWol9eA{OC?R@D`e6SNtj z5eyqHjRLJdgAhN`;?E)sJ?YqoAT~b0by~rA+PB%`zB*in#QAn3A?l0R2Kd!CX7QIR zPd)am`|=Z<9EsYU(Ge`(f?TrE8#=f=8J0pB7rIy_yJXOX@*S22*4xNQK!2%xxtg z9E!{SykzLH-}d^R%w+IriY>?yyFzb$gv$F~_zY?T29CzX8w#(+J^NNh7ORQt&eOpa zBSaxW4273ti#@{fHcN1p2^|A=ks)XIkND|=1)}k$W9SopPj*11y0Ylh>MwQBaG4kP zEwX%*QZ12mO!oV673_8(5Zqj>M>t!ortIm|A!0c@8qBSfXm3o+{B_Zi`#EQK!XB;p z>a3;>ShU7DE|_g01PeulY069?E)*Y{;1Bagq2`m|jDEfot`OlGAIt5ab)^p{$v7EQ zn5owf7k11m+W-F5f`iXiOYDQX*B?T0O8~fmS9nYR7|RDDJ%}ng!S=~hQ7i`yf>&`r zq=!zhUdLA)4_%Z9DO)}!fdIS^l&9^RmJa!B7TkranE0|Otpqdcpy)|0U_*W|?JuI5 zeQJ04yY*tVQ!2s;`}FZEr*G~P5~y!FgaLK_=tEKDPn{r}xRl)uWNeAsIf&G*7C#OP zHUt+Gqn^p5BCrfcBO*W>Q;7uWR}n~5HVRqyuL&00AB9NZA7CTgf5w87AX+wGBXd$kaqonyujdwJ68^5Y6nxMI|VibBFA(>?5(ta@PHR$>R&Y zN)I6NS7l$kim$ndZu*gDg#H&3k#=DkmBRQ$O%)a4ZT2%-)Db1fZ+hx>V?=*FYI_Ex zh#3ZMfs=MAE>eQoiuiuoJBB)}HTUnbftI`&A9PC_fE+9!=qte6nG4FGl?#m=s6XDL zl$YCaa10HRrd>d%amfso3ftJddoub_LPBluw%*BLtBn%y?16BWbvbSPczr6Rq`w3k zdC1n&5=#f-7utFa!pj2vGpXPu5MuslW=VaN9vC z-s-8VTR#@f{;Hu%3URwz{SJ%@0WyC$^|qy5&pX2>1(yQc8*-^}e5~z+fc*TgUK+{! zs?3(OMYu;5dh8gna3K03utKV8DcQyKl|a;LEXfD_!DH@|SR#2~LqO-=18E?tu?2;v zPokCa*ea<%dpxG`qlgQ$YA@h$Fn*#c0{-zD`S7wou$Y=5Lh4V8oRW6;XYV@vZG{T$ z;{m@J!8xsTgRt51X#O?#Dc^#cs7^E?Od*`7fGj?XnbMQj#bB(;_baDR9K0 z4){TdX2yjCM;VW`zHAY(hDPMZ?@gcOnU;l4xH#&y@ve2dY@nF=n{l z^%)KDP%G%RcyO_%!yd3!YpB3M!^E$YFMmv-{zR=^%_c^-%^NhqKRJ<(<6LqL1)|i% zK;xj)Rk#T)C{-Z%S(5W{3aLLOmw9BRiW(5mJ`etm|2jITtp&SU%poM;5v>fvsUzVZ{TGUJg4XWXNEKTVfw?lMi``4?MbNSbvo{aGNUJMl{=3= z?LjeU?l0llH!uDOM(h{z(bk~l_nAtoPtC)ae(z{w!CqKap3mttzK0UF|MEc2B$}s~ zCm(EVteE!3zv3(_BY%(jj-96UVeO8(dCmsT{m;Ro{Q$!O_ulNUs)KeWH3M3rz4e!K zu-VBgF_0j~IY=EX>H)>lZy5avB$oEiXj$jCG&;C98<(fJV$H+%lVAS3zI{CMhcLJi z*cW~!C_m%Me(GsRLa3WW&gTiHy$Vu{>B@|Z-R zpeLDv7MMu8_c3?S;V8gx=+j9=|WJ zRbr%c^vSOlVnfm#^ZTy&PAgfd*Q0&vC+Rr7?Tr~l$N*GAQ^QH*w=JPTnlL^&lU5b^ zCHv-u-O9Ucr}miy5cyFIc7Hz$5?)^L9B@~=wI*eF%&yJ&J83D#@OOm^?+srA*X{Rr zvWG3@Mv9nS9kcUnOP}_;Y6=a}Jco|YEF}r3W$uA{(m>|il75&;nt-SWG``-BXH8=8 zM0vI@bZ;a54OY@j?W>~3be)a=GL+gEiwDbg`z!yAvHneE6`l4UkEk!n4yl<8~>7${x8VM{Es)Fv2Nd($msw2>I+OrUnZw z7*t}@lW`SdOszQSjL|nEpUuChj9L_T`^pAngNB^FzgXIWp7Nz}0xXeeu$tiPhD@v| z;q+h^wPybB<);V11C+S?DkEV!AK&Pxzv^Y;uMGRTT6F(?{%B+flUW=8@6AumUi-hw znak@V3V$E;1pFEaM)`+NW`LZ-{SVoVrnlwez()aS%b19Y071C~TLwR*!U!_k*T;kE+cO|4DOxj?|g{P&w}SH+_rcxv!(puZ@wYh06FCJJY`b@P{Zdpr#MhjS!-4(%73a> zqPPGA$ex!4_q5R9B_53sExPw_ra6&T*Y_-7o?x*?aUv9uv?&W)&e*b+z zS<|SRP~F zZ59uJ&H^q1|L<(AWv=XTqzqq^Wf^~SQa<=ll+biw>qnkR2cT!koCLN4VF?7&Zh%b0 zn!vzk9eHq9zp3_W?hB`SOtpPxsqDb+TA}-xWcr5V@oV;mcwAe9)Y9R#V|fh?fUiUd zWGKUZ$u4;9MS`W~7Iu32p@i1Q@^i07gZ(|Fs?!bd z(mMQE`?gXI1Nc-&le`V{Q%$$+_aZB=1S&_}T^<`~ui-U|-|X^FN=swMyjO%#}N}zg2IA$^RDucRT|&b zbzUmwp!XK#!FBv2qoy9YL}s4hY4 z*a^PJ=e2)CD-Lp{aTBsrL5^^-j;LmAKZR z?oTYt*I6;V2<^o~=CbC^-|=Wo1CW(E#((*A6#JKjFi~oj^IhQ@P6uYxQ~uUpl6UxAZ(QpOtDT(`+_;ROwFUWFfsheObHnMXy~PMv|a{G9F4pZdg?p zu0)y1$rj0ArJ)t3%IJnK+Us@S#yaV5z45%09m_ouRQ}6;p&^f6iIE6q109NM6Lzi) zEgyZ^oUD6@?f_H1laJ$1vU$spAb+9jPDPJ}k*(|3FFzAiyd^m1E)|TDVGykss$bVd zc~|piKtuY{fpVUZdHqMF`5}M3gT6JEQ+S=zPs&j>j^}Fve+Do5bmmfO+i0X0*L{)C zY!H}^xnzlN-vT(mfw^N0U9%Bw@n}*nE#&PXZsyvHQd!?6cc3V(_@QUu?z%Gb(iG`Z zWarEr>PqOd)%|5ZIs;4~*oC;H5kCy+>$776xugWCQFN6^3(jp024>jGPLu`))!fnD zc?}{nR}QQICrW#5sRHTau;y;LTV500-v0`3Z)KxDcshdY&MjTRZ@-~);yI1rD;j$= zM1F_}d%*+%pL$S9d9<|XbAJ!J_b+ZF<-ENees+}~U~9$VC*Q1u*z=!f_+Ilex9^VA zq9<#7|1#8erE{upJ6&sLaB)_|U9C9cBxS<^bsR_I`eLq(`O2-D+X}%y3U1mh)jm%B zdj-+{h+Bi+jFeN${q=TW;jrM(eXgdTV^{1!6{89(2HevbFOQCPPXg*wIZ*ddKR(fm zi{c??t&DgFj|wgR*kT435yE2=;_K=^toY__<*EjT0pvc4aT7A0>&5zxLIc5GyQ7<5 z3@cEm98?6%-e0?SP?8*K_KD_s0XRI2Ml_BP?~^;nTfO&A7dc6ayQC@bs4ev0{qu*( z6xHcKgK)}~3#8!18}{A6rjMT}P6R@$IA>(7T}-bwzgL?W5g?L{G$LHAsIf)YPZn&( zoNs@Rq+o^*PkZ*+_D9^CZCjRtj2&Jh#&-`U1!hfwW$y8yYhOlN#KZYv?h|e9D>69z zg%)u@dH6ST1~?B)B63kbjEE`iDMUK)YlQA-!MikC=q-ug!}85yTfHoR+Q2|`drBR= z!4}g`rTVh?asbkD>kt;fWIAZNRc#+mOvC}Swb((nUkGSejLt-tQY2FRf&gW3hxWP% zdfsJQZ3ySK*x_Tyn@GQwr;PjyYO9vRX+RcU({~X>o;@_gs^mBI&e?Bj7q{+?F}-Vh zayWRDDHHS61|Yx0=>X+&JADZ+0))BHgx@cgp6@Z?_orkhPG|##M?a>eK+j(S3>ZtcC8%07 z6ks8J-KRVXIBUKsjE3SjTJwD?m@q>(t?36rF5n&(klb~Wc|`B0Gs_Bul{6^W1QstA z5O^b7Yj4|di5D&wiEd)Idn(0NI0#5W%nP9EGV{wSxyG*cgZV#qQRk|gHk8fWWR2Tx z(4&nfl}A}RNl<7Sp_dQk-^$+l7o2b50(0+Bw-!o#ddb9|#%bPhECJ>{!oh3^OV4-a zdhl{C%Lg@|JeOOg{waMC&jBN^Fuy9?sPoZ=Ke)xn$1jmi7vBrN_9bFU3&96@yUL9o zCM*h`bS;6m&XGI_Y>EUp4~51{GZnDvTgtWW)V=Lv&1sX&SppW>dmh9+Ck`KDZzL^o z;@m|*IT_l9=H|j6wo!p67em$#4EFoe@O$5cwFI)rk8$;BU=k&8$@LpGUk8a`6`)d3TCMTeG8gmmD$uCb9$Gy5DFlA?~l^Kq#A~2UcY*?3MB^I zKHFQ2dGC-uHZT$?Bn1+7=?n!OxzR>gGlRa`5{qFE9>3D=D_5zA-)C7|D`c}75{(D9 zAr6+bC*-1oE?s2k4V%w&!WiAwzJfIFV0>9i+*0I^4}lJ&#)AXZZJ;5?3kVMK~CF{{!p{+R!+M zw*}l}&?3;;<2>i5wJSGY&UdxZd|R&0!gFI>i9~_NR(rTzmRpSm|LYt}zxr&>Q z=8F07pSbbqW?q9A-hKprw)5X3)px+nzt7vf#jYYU5@Fa8!-1G>#t)QVWy+lNq`_h+ z__CzZ%o7^Of8K}XM_J*bV0MRjJ5AzwrMy5qKTHf`iAY3}H}#Di?o~iR+#Ll94U>|@ zuV?_wib>{Y#4&ZC@^(w~h`w@f&Liarf*VvxPCyIntAom(WbXe>2cq=jTPUXQEpWL# zY?lRJy$dMU$deD>A*}PnVH;)EQ)y7o z&0TtKW!}k(1?O%F#aU11kz;?@pqx%0UDYs*aQ0s@U6wRJ)Gz@M9UXDgM3LP%_v2&{ z3*H(tDG-%_-ZA_rOrFd+^7d4kgLWw1RL$GYDcj*IWo-Z`FlWoVKaQgiIKgeHO>+IdXzf1r{QvUb1XzqpoNl8~!h*73Qei|>A1!G2B z&58g-%b4yGE%6^-jWWZt()|ysCxzK9wwLL%4jNKUJ)dn{(z9q~%n%y|rG6U+>99fW z$Ur#F=}Hk+8Bc>p^(ddJsA_-v08RA}18eus8jde$t8)t6IKeMHAS65i>TeYINJyyP=Qz=oMo$RvQmioDWmw>`Iox+iz^D5TI#bJ}2#|@zmEx$0i4L(4{p;PI14_SaJo28kuAP13v2}dVda>khHlqiA?wK7faj#saDOpoXGU)I1yS}7T~66-=pyoy$bZ! zU9xXoFYMtxQj5hjORK7E#;t@5uTJuyRywXIp+IXkCsId{>wt@>iewnxlm8aFy=Zao ztI@d8fCh~?BC`Ua($T=+ng~>MIGrdGuXRZBmFlw-EUET4aL&yCf*i=$^tXEw&pnV8 zAqm?ne=^CASfSi20$g&`Ml2mq)Ku^KWO$-y#CU?+?t_g!s#Gx`QdWOnyE@23m5#^l zi2dPXC%w^R+40X?%EqIvanwlF^5_Q>y-&4;<^8D+U+g5~WMFC@{Ji{;=Lrg_W>*Wn zY|mbzjiPl9(~D%e_}}!~DiR~q1jLSpWtb`%Xlsh_4bp%fIZXiP(S_sxMNG9I{ERNx zWwwXcUVsd>^b@jlTJ5Lnp_{{yt;zluuLnNGeDIlEAbTMDS;0@9@(R2d4Ni060S}Zs zD@fsih=IZp5WpC*$aQXd(QQ3$4>xm%;&%ZTdP3fa%$uGlMi)3^u6+_rVW+r8wwEed zF*39T{HOdel6e+u#2;g>{B~{LraZay0w-qm9o*2n zDZuGw|7zo@ErUjDeuLhxXy0F#<6~V}s8O5c<@69*_7CG}3sqt_Qg0E=e>x+${OP(@ zz;0Wr#;29i^&tlKAQR-c)P+$E4(q>xk-Cpa?7n|4D}VkX_Xu_=@N-fnRN)oyQCK0nc8-+@9mh)HINvEKQ@Dee%n#5X{y7WzU>aOc`+#C=C~#vlPdZ zfGh}I)P1_HM~J;n+PBZ2I9a_9TEcF>X7tdrTkCDR|3#p3ddnrrJfPGPupgS+(Y+vq zxYZt|lX~S*k^7hn*PUO9Gfo2-|b%Jg#n$GZbN6gib5Y@xS<);SBbFTeAc`8(V`BjUGOp1X!-ry zeBmr`?6QzToGMZADai3UgoIb~1XKdCT*N9nppRnPk9|UABp#VZ6!p`>mUWn@gdi`v zy}acVF_7m2bL+=0YL;E?TzqY}vrPhA&9Y1ig*^odnYF^t-ti_k&D{Sj1Fg^<7#3)b zESbEA&?fb-719hQ9z1Jxhtfq8WU@|2_C``4S7a9-QIcUA_WvI!xiP z0TlJ0KlX0_Yi(XC3}s;H73%lL!&ZG00H6}*W1U20u(@!=q;=^AbMCLr$}bUVBfKzCigzOcuz$7 zMbMB9@-cb%{N56U656{%Pq}o2B|H3#-F^3%p5}pzKuEG+yaujSCii6~qaFv|>L*AF zWNc(@CYYxh#2N6hEBd0y%a6rPxT$T^WX*tS({mQ@&vjC4E(?KZB$QQ2vrDOzfs@?gS z|6s3n>t_+Tz#A)i)_)CZ+b$pu%DmJN#k_!0*<*%_>o6jxfS|MKK^Sc)mVUwWpTIeB zT#?%l{-K~<=x11>umN0n#xGYQ&xoerE4nob({OuQ=9s}eP7et6#ZpBudt)iUd6%Ni zC4U&?89?SdQ%AmKldfDY&Um=kFS-Qt{nPf&D=h?vR4`KqqzHX@>t@eUFNl{YGFlqn zbO2!|Z-jhwoZH?zVY3eFrj+FI% z_&4B%)A?UTU786=b^&$7$-_%{E3{jKL;H>oNuyDis2UmMYj@CH1c!TpzPbScOv}K* zyOu&xjEO$Miaho!+^GNkDH{q%<|fKIQHIW6t`aMluH@!j@bR>EJi1q{$I5BA$ ze_i|Cy3HUm#n73O;!aPw@wZ?u5fmG;hl*9SFC7m` z1F*thhd-aRJVgYiMf)dlK@y8@2qL~Ph1qBlo02~omqy}N*@!3RZ={DR;y}NjLjsdS z#AIXq)C(zVTc2C%UgEgg{2H5SbvC8KhLYU2``zAl(WbUCl|UwjP_ODSa7^`8J38)X zxGieK9=Jv0xfZ{B>xwyT2wGKo=7;Q**&q%i3UJnZH-kES;p9 zf&|z4X@Ng8zubOW8id**OumB~5qPQ>@AqH;ay0qjf!?`_O=`v8^+!jh*3yCv5bDG* zd3k%4qzt}Z6HTlpZwJ_M0Yrg^HysWK!?K|!rOlWu&Wy>c%uOlQmdzoLTht$DH`^+=O4at{QJF0 z3QxC1F=hIATO@fzcC|*&$(b{!f~4&$VTKKT5+5tL$b+oH3g{xzOo!3>Ul!aquvs4tLHde{_Y|G14JLMc z`j~fxAj(k40tmte1bbfXa{ky(Z1w7eNfdkHFUpz3)PmLYfE4>YIs{br3zPTnEL8Sp zT({%}q-$+FlH>+jGh{f4E3;^io(4A%Qal_f-!&fC=9l)l+g$ulF!ps&K!R29(=@^g4;$viy=1rREA4L&pQ)_Sz=pRueKf5vKIpzI#G3(+KQoYv+}R zoO^7RQ?C#Qtipt&ShKV%1R;a`OrF>~da0aNhN6-TeRw*15QcClLq@V7S|H{}V`68k zZ)ujOSf8ZG5uFhD8g;t_nkuqLq*D}|oAO_WxM-lkSm4wOUYa)6hCvvtp4^i_dt<*T zE1cjTWZ|fF_Dn!r(wX0?9uN>$wC}Qpv^8~4g7z-+EahSD8-44KAVo4t*(kD{fpcui zO;iW=RR;?nK;Yj$pVTM%d9DoCa&kBbl}_teSMav}W`t?cGDwB&X50-$EsKut2QLk| zeSnCHMIHxO-R^H*QhWET!~I)07<}Z{(N>V!%z3PYSEj%IYZ{cD=d84VhSu2sEtSZl zd2=m={f4US5|vrzqi+x)F2~cwg5TuAvN@IZ-DEmS&5dki)A{TUzXMKHrb1MRbo4e)qDZ-Ujws`^>>h%Li72g?}St zWN}>guD#q1EJ4TDn--#lX@?RgwC}E*CGyM|X9={+)<{mAzR3TKQPfT61fu^R(obhT2T>lb>IVRQx_v35jmP)@*)IjGvLHl5QrPa-=`L;#2)U;c}dX8Msu zJ8{ZMYFq(*{+j~us?rGy3aCTMgeN4fpJ(*I7sZhM+v4{i&)Q$H!9M(I&jVlL+Tp@| zjeV5;c%RbYDBzbAzSYJ0E-5I@F~2inATdiS=q*|@f#%c`+$HB9>7(Ur*8S(M8SqA! z5T#lZUgq>C62qTYUP@}k>am9!fFH19D1YisTe9CPQgd!{AtbqjaRXvv=lS&#szC@c z37cKY@q~yLMHwKyM399I)Ut|QvW*Az4HSnWa@avmDY++P% zQfw;B3y5yl0Y7%FA@o)1`G3`IUWH8-_EiQE`f-6yCj28D+j00Z92lIjT5xSGiyjM7A-zSFiP zs0|!F|MGDHJPBJS5lL0ASE8dxXa ze_Z_Y@a^fWdhjh711DyDQ7e@^}Q6`8SNsFsTy4EAxJQLmg zk^y|4A*dA^;xaNY)}S#Ertbyaq&p>7hf}PBe#dA|m4&_ddYh}NJiFzg>z~JmvGrR& zm8VVj!Gl4TWi;uJ!A0PgWQs=kW>4aHt-*Ls>2&}SE(m*J-)3hM-zI+qfw}_i%!l07 z?%S!RC`4Td9_SQ8O_=? zbK0}hFnT_DwqZY}jHbjmO9#z83}Tx;bX&kv7o>s0=EIXs(cgjGL*KTWvd?E@x*L}1 zApWdQ0jB}?@KY+u3W3kZ|E*D6L?v7EkzkKKA;lZtZw;}>CzaU+tpy9F0bd!ut$^Gp z?w0<^PrfUz-F-Y!q&bq`c2k70dQ!wfpDYgF!BAxKBp!?l7$cU#qe5f3V+~3lvEV^` z8Ndo$(h#inLH}xG!D^aI?pn|!TQ_x|gYOS8dHiqv7&*KE6tOSxiuW}Gi6acLoRN-Z z8lT&(c>We-=(0dlfL`SSWGH=G<>k<=Y8tg*nbTi<@vM4a0H<8Q${7bwO zVR1_(W(wS?^Ua4f1NU?1tX}4{-@pb>%E09 z?4GLBno1x)G#3`m76yEHTke3!1PFm7LN%dGs}d47sZu zXfMHfI;aBOZPk#zfV4CT=cd1B7gj6^xMb|v&j zqt_cMqT?$JhaKG~hd8p`?yXzi^cv@|co4Ow%OHLcOis&^a<#{G)&Jp|C`5eT$zN&J**XgdULX`71&!z_+1lhBDu-jb|$$f8wj*SFGYHy zO5~0*dDY!3O$SD^tK{vasb#nIoF#0Oa=0C(i1sqS5zf19p2hs|V)Tqeli1|ecD|kX zhMh?d#PxT80q!Z>q%*Qr@@&KWC*S-4U^*%S&V)wF#z;xwH5 zm6C*;YFugmee3hrp#ER=Y9FlP7O=`QTm;V@imQi{+?W7y1{BN!RHCaBenhS$!iY*R zL3dt{x)g^KxgXM%$VTxU@4Qpz{-8P$`AL4$d-MGRe z$$YCni`_}Y2DfojabVd&l20aK+$vSR;pSH7V>tpX8OfphK-e zAkYwa&U2Ri8XzIij&Vgdn;*^8Z=Oaghlz_6Io83R&|MoshWIXXOmc`m@@mTv| z{tF&!L4cyq{pe?>pbmR^cYTjg*S`p}5T43eT^1B!>LMlUUcR@T&`Gv~I$^+n_0xwE z{hIpK|9ejUtwnCuQMPt`;{Vs-IH4_y68`3I=WLVr?ud}YH`e?+L((rc?kMQi)eS#u zK!m=%Sp^w{)LXu)BLBxpWK|1z?8gTqx#edLH1^9H0KRj4uJI&9TbR?aehM`#F<^=F zzB6O72yzvsH7&xWo^tJjksN{oKOQkX89hyIJox-w@qxi#P)T;x8y3g!DI$=A&)z+r zd@oaQ7alSX0&f^nli&ljpjLZnQ20qsG0)u#>W_I5(LrgjVMhU_rzoz`FL{tEQ@qG18{N)f7D_kb4w(z#r$S>px^*54H(; zEfV#uH;?6KCCA6=*KgY_HP2^L)eXIcT4zqIw-{+A+p=f^C#P#{cC{dq2h*M6 zk=36LA3Xtl!$Fcf*?~a#Da?R?dW-N?0$(2z3W84&TPW+&(~}f460!?(OSlWLkjU17 zSXxlWQ#U(*JqRPDkU52*3A^rg+3uqCH#9LHPJDRJ?6$)cE`Uy&3T01!>QJnvT0vBOOsA8i3hOPD^FN6TZ_|pT5}BeM zO7?QzYAllc;o(E~Yz5z)#Y=G&E}B-!qqDPWYLkqh{w$D<0zTSb`K7Dx1cKne?}atK6|5;>OhOR`5yS8A+}>} zEBLaXnagQ~vxg@oX4U;}p22^M0cO`1<5{^U#tQmwEPZeW`Dn5blAr^UIM?IF6Y>>s zd(WE`Kwpw&uirEVnukbzU1Ru3!cc2)f0?zrs&_mK`?Y%J>G_09I0phW4S$EL1rrhr zKu3C1r1#b?UW@Rny&-EW%Ho}YM;6D9>+$l7QgJ_CxLt%{xAqo3B=WxvT8VI9O3S#NmIm@zo%jAjvK7UnoJsW#=CqA<+4Q_HM@g zcg>=I8|k`e2{f-fzAR=(qtslxf9WH`(Ug^Xs!VQX>-`#-T&Tk=VLNSAVq?mMQtRWJrLiGh%3pv2tN1x+B^eZo>K}y0nEDrpoD?emVgZ@nZbWudE zYvxSq6_}@N^$}a*-_CSvC^1gg)os9-?m8t-Wpp-P?@gB{jk&OCN!|0HuUGMO#Wd=) zl)D^9+I=al!1!JFAFg@Nxi-CSy3Dt%|60DKs0NT~dp(XAGfDpl>Rd`UwL2JO;6ek1Hk z8z5p^z%4}yO9eh@`Q|>$I(7)71|GT1z$Z*9V9ZafIe!OboXlkzIu68JhzeoNp$ZpkFr%Yu6p~o!y?W@tWEoJ)NV}}3I5|Z@>`MmAiMpI(&N9t;iCTjCpd}v6? zfh>iyv@~05enLrjQRLhN^iccIvn=7`_)i|hKb@yXho=AG1|&<37%S<>Q&|>L&Eb_l z+?mzW1n0?}DqmTho)!A;KOH_r!knIa1kr9^j#Byjo+N*XRmtYJ$Q$<%^HUmyXrOw< zkQA$Euo2{X^;yrU(FQgY=jk-Cu*ZLs4wH;$c5~#w8GwJqSb5w{5LBe3q1zFa*1GIH zS5<71>Xz)DLjr7QF)@*Lb$l^z?#8PO^Z?=}j6zm^(*h>6WvsZ9*{(3$OHf)XX)2m7 zzblq_lNPo4ro zAK*s+Zm@0*f9tHYqKoM8;!3VldojDN^antT#svI6ELeFmq=xXh|K)MCb-+0UjUo(9 zsW>vC4`(%)A{MLpZR8)X8qt#*Bi4scv)rX@Kt;Lk=`~bhrW)82^%NG7eNn+LTKI92 zhk06#xJad7x!^MJ^8$?&N0g&vb1r1OD8POs`rrYbs1bAFiO$d_e&c2Q5VzZ49Q(jx zGc+nZh^w{&`Sk;p&u{_f1=J`Y`>wFLG-OImWL4ew+PB4*P0y#u(Oh9&dp=4XZd2(2foF(XxX3xqs9f@knQs&zKkj z1NK3MsofZXpeIT}(qOS$ARFGJ_quvIQ~i1Qw^z8Ac!rQy?}#dW`{ct}VCA~#OkMYz z22_11H}E=@-0@q|I(rh7WKx)D3;XdMlCl(!9tkq{7sYrq!yWDwG4nDCEfSKzm%bD4 z0pIjdE1&LO=iNq%mF6nxeq>HAF1!dbHP%%CONVU!A4z8!*W~-Z{cAyYBNC%Kr9l`7 zN|yqPASkGGm((^&LK>vMAR!$pO0yA4N|)qBx|Oc&zu$d7-;=#|y*@jy&w0Gx2hy|J zg+YnhtWm!|L28Cy>iFuw0sJ-4a9zrk5Ab=XEnQA<=-z|!-GN!Fy-(-7@CEV;8ysls zaHZ3=p%$WtK~AZOOLYQ2RfEbaBDSc;L42j*YUH#aQ@Se}J8_MFxSkjt*NZ2Ghdd3` zwL9gHq+%MCJ07Cg+w_Agw7$iG%uJR!2<)|ytV|Dgtc5p~b}h(FOlm*;i2 zfqJ*h|9)}obDBBfq1(!rERkQcjow?EK84c;uidMSbBQz9#GC& zGQg~exk#>+xygW9@MbZHU}HL0h=dZ}16gT#q_g7$Nw2NCtNWUg9ba3@y`uj?hs=YK z!-WSP4B*OeAkM9SQybZ93SdUaN% z%r1Ero1h0*CvyC`4-pO91I=YnvWb&}wRw;>pcHe@$0rP*0pff6O)^WM-+{UA^#=_p z%zCEHOm{X4Y^D6ahYp_zeTC2g3qg%WcZdk9VrERqpG)$BuVOuC*be;y5zy1h7O_8F zU*g3~?jy+!tFFbFc8HSY3An2FNqk*J@{XW6$eK^P(zz2+JQ}Ye(asAMReWy+jd?o- z9CL$IK2~+t`eH6A<$7c(4UBv83hU}t3dk!;++W#recUDDG0@SzU-H(?;W^nX1A_2pB!YyQfn5O0HXU?Ai-S>I_tU>p?!?axT7Q+1T2d8-B0>dk= zrRzID{`i504IOO}4J73(0#1v~`c}eSd(hjAKUH*m26GH~!*0(!X`ZxvcAY$Yw`~u1 zW;UGtw;}D_Q`7(a;!b-j9}(gPUQ=xUqbGLUl`A_ubJy|A6HfsT!Sh>b#(d;MbgcVF z0X5UbE)}QIAa&+kO@34!1aJ9REt+c^(XH>w40t>e{ zh3II+i&XwjWr(OB8LJ*(-x*%1pN2kY#iBS3%$Ef6tJ>Ua$l}NmTvCW6*)@T)#WyY z9828`APGn6=Nt!_rxYeHGgJvmcmLfNbLCS@-=kIWA4ZftMMIT03z#zH1CU&n6b)#U zQx1_+ej{6{Fz7OG{RpS)!?7&W#KJwPD*e41+;Q@v9^=)S-2&rhbtvfCZ`GS_=W1bWz2=s20_!`IyN|gPI4@;0-YBtX}hG0IBo*&o0U+geHE` z2gW!h-zwy|oq$|twGjqfy33>T%(zSmo1%IxJM_M#7i+$2<>oO<*($v9=lVGL`0~0y z?gvBEZj{q^R4AL%s3Wkq#RXrc2OTi7YT`?jfgqAez~Y@KtT6%1+nV&1LV{dFi)5iV z(HA(+YGzW~rs$;86r(o?3qV-!I)l`13xEw};YXpM!+?Rc+fKK*V>u&Z^tG5h849da zSxPhh>b8=fH0bM*TpqRj`ZZ(gy>B!F>y>{U^qr}9(!5~V#I{}k?+-k=<_%$iDAr_X0evi?6a-Jf zEnDJNGaR+}I4MpiupgSDnCwot>j`~o{vc9&lZ;Tj`-;OJYL`ppG+vlS#F9F)rXmLx zHN0N*IYrC5jS9ZNpp=OUB(SdqwRET^-HuA`(-c~z6zUTJiWd?N4pWjDqnT`$Ng#dDD|AmF<#-JJctQd&sn);}W&I zzv=r=oQuJuMp<$el_|AfYrD76RjLZye-iY3p_{OBU3?*sA-@8XN(ajPj^H?(Bf z|I#jrSMSg8H0xLMw_#C0*zd0ug^#KD{n05xV% zh4?^mHLUeF*5_(5VC}=#T^D5B$;aSy(#=VmIupOV7PFAvfiL?tlXW=ElDLz#eSb8O z*3$x9-m>~^36XLP{I|V+)8r)G_i|r3wZ?j86oZ$^QwlYKOkAsPiRCJHt)@?n#S0LOQGw5I* z@#7#WfF09efr*EKY+#c4g*LT_z3U|dw%VT_WA7=Dj+X7q5VO3bFJb*pm1O2C(PVgcmfPDdVWJjDV$yc3k9cQV2 zC*fuL3;*gH45`{~5W5f2e?RhW*DW{FMYuDL2=cVG5XgEZ57Ip9deIOVNSH2BJHqTC zY(J=X3)~M5c`^=QNe;7bCk?2O{jA6l{l#}W<%@8?twju`8}-`=5y>e2IO4?ICtSV( ze>Ugt=lJr;ao495Uhimg3=<9?p(tvrNfPsfF~zPL79XU1rMi>U&e-!w=D4%lFBk4O*i5^B50bTGh1s{jlGe#mJtloXQ9tzlh z9Oo&^DcKZ~2@%Ys$H;dghbimrHFD4lLNtbSkv=B0)ZQ&9_QMA$a5G^TnQvw(8x~Z? z^bnl<3za&&a3PpiXLzjpb?)|*1r63r^E8lJEdB>z#0%2h=yvEhDCgXCBvFk6HdqzG zQmcM8rhrP*hWPoJG{ry^cCT_t=$9OoL`WVn&Be~C)< zKz0Gf-Z2&SIyOpnD}P_vI6bC z{fT-Y$Y$joZ&-9|fqq!wkkYe4b&){& zOwn3TMAwkARyJY@tP85P9@mxuBJ8gcrH!F>F(d#b+4WbN8JcXq5(e30WG7XW?6xGf zAD9MtZh=0njvC3B=ijGP2CTOSlRQdekmsCPP$`E(VY+Io-xeB{{}!!)-z2(Ku;`UJlj%!rejaKBvVx;GH#b;=OR6iM$YK~#T>A0hS1&02vT zh`zg~10N#fid;RcO2rLDJ9!QFOn%LLiT~k!&!^;d5k&(tkKHa;bMYIRwEUM+N3&Nu1SGg|B zgAIY|b3!=UGm|iMt5zip0cSNRbLT=BH+j)q$c{|(jSnA|043k7=O%flY5s4HiMIWd z#OCDG*z=HV8x|xqUC@#|GTWS6T1Euy4W)e3^o@O+@cH;3?Qg5c6IYRx*Z~x6g4WEN zpXqhuGOzW(n;xmQ>HUT%A>l0Z^VcWNa46haz0xM-2CWt}Se-1RAP)J>zedVI&(rl2~k(yz(i$+`BGc8!yh>{)Y* z{@1H){16*Ih7S4Z)@UAtx^NX5(`oIEA8ZEejjS0w^JIW2#8&xFB|JSFANJDNv+c=W z$2c?l0<>QBSI^avwM%=U7Pw<2%JsYhb>d5QjY0=*uq0i(=(i8FF;`v7L)Xj|rRBDJ z2hEK+A-!ipN1}C)T-5O|EbGvlri;fOwJgBh*IftuPxD^T_|oFFdyv5%wUNnA#OWac z+tlUbv21m?krvClMEIH!l@Xb0sYC8E-nU$nuoxb1ln7@WElW8s2Yk#&e$@<`eyE?& zTv(CJCve@9Ib_B@?=v!&Ey??FBdg-VN4ia(|Ff%tPJsaC07NI%f~YO#S5RLW(U<_s ziogpz*0;h8QBoEOd&muTPoTMtybNQ_NLD!De#y?X8`S~)Hx+$d7d!aGQyG*-8c35z zj1fg-DIWG43;w6})8GY|>Ft3JH8POjxE~0UU}4f(ZqudXV=(NSdH;MWnQEqJxeJUA z`}bvXj<6aQDZu^FThlvVzeUixrQ@|Xhy`T7K}Xf@(}9DZ%_2_2(swNVR+y3(4n7m@ zPv|3Ezxd(4O}d-+9^90rnPFa6LL6Ix5H)_os6PK8@e=MQWcpXS*pnqhzSwuKuT=Rw zg#r~nUHOr|wd2H=IiQf#E}tN(We990h;1Zo>)YeCk!3BofXbl?UTW#DZ)zv;dg-X^d znFMq4OLmsr{u}!O^E}Qf#L`{&>;>pk5 z?%P|+Fmc|_zr6A30eSQ$6>sdGtW4qTe#O16ZK(_n;H_RflYcV$dmKo;UpV+)L5sen zrS?NC@l#@j_JjE{w?xF=+XD2Ps?b;I1^BFjV*|6=p2dKYks4gCy?DiyQ+8oFSzm%g zJLdSy<4iQcC3^NPtH%`)jt&{o;!xH@X8c_;&J()jfjpl}7LTm(fw^csWE2}q-~kne zpUtZW`?Rl_X5TShds^^1_nlXfI>JF3%cA|D0dT75N;eR%&2Hw+CJCl?CT`$BJ-gl? zy#DQZ?vPT-q|^=&tw_D*fv@iddsV;|*1J%T9w0k8(!!Ieg-C_V9}XHs&R$TUs&XwV zVyUaQeXs?PvLK{sBP39U>}~(tWQr%Pz+wNdjf%?+#Nyg{lHj?@xYtBxAI(5^Ov#2Z z5KuslVFQt$9(&0vBkz^P8RYna^TXbk*|gY~-opnz9?Nliqy>tNuijJeuf#@D z#P(Zi{-j5Je8`o)zFBSKS+Xw}iJ}kBdt=h-b1S1Psvl%L-Vtx}b;H42{YKFIfT1X9V7uF0cz)bX_u(6k7o+LgZ+JyfPv-)qVq?G+(@Gqe$fRj-$Isgdt0($ki* z#+(AnR?>E*anFjf9BzB_7L$#B3|l_$H{HLGjJguu^r3_9=m-t}WW0R)yhSWJ^Y&B0A1UNNA9%^x;`zrNcNtP}`okeYvDTe%AtN9iM8!oFgN1 zOk=^FIUDo~J_{i{Ze<&nuW@^`X6z#mjh->6w+boVComV#56&3j%cv!$g$ox4Ua88^ z?Mh^-YuJ|0B%fnz8Th>#Sc)%1W~>{Xs0EgS>o=x2(!>&LPf7`K6Pw=kWqLr_AVyie z?}I1}!_7RpNRwRfMcHoDgW-7_XUN3)972O3U!nO)nv8}fo0u>Xao8lZZku9_>zfk0 z+F_F?A64NSs<@1kU6zz1E*h!HP^F6*-e`HX!MeTYb!0O*3jjvVo=swD0~=U!UQn9FT+wco`(e*rUU_=XL1wgBz;jX z!cULPArfE{<`fc8`*{)Ca^~8;Hq0vTj-TMD4@UAETXYU$eI=m}^K$vm&g`PmO&RePNoZSytkDB=$G$q|qG^`lKX z_<}Hh8muWqQ4qryXWnP3(zcvZZ1@^e!%3rT<8D0}vTU`l6^CNW)U1+kEXX3e*xR-5 zoPWVXD?x_+EzN=}C|f(w0py<#ITsW1HJ9ahX;MK3CEm%1t3W?4&MOg6&b@9mkdj$S z6)DC}bApV~A z1kFNC3fYsXr)TQBAvzO~O|J^)|AeGQs9uZz+>s33JRP{1_`7-Z%K9$LCsrvz>U4?Q z+fc;{Gf!ij*l=ku{A*(X*RLR0%UOrqX$xgevF5%wYJ=0A6zP*yWZaX-R8n@SX_M2v|}J-z9jtC4i^5b_)NcnZEhXu zqqr34ig21yMuy?u8nPAfc4jh)?d@BqHR|tGX5Kx%6nv8uQ?zP;KyJQiqA`W+3Y(;v z!L7-n8VrSRVQp}V8ZcUDtk6)L?V$4eF!@bq(n)Rbw2n^2Aif|K5F_p44kMpC|1>|+ zL)m=%b!P=<(2K4-olpJ&yUdm7l3JvB7xD2b^CjKJ#Z8Z;o`A5F%h;Ns4ew#CHnuDr zE-XG8@Hh%_vHH5)J6=2N*C+h+t0~)DUvI59_!wH?@DE56zIeJ_R)vdZoa|%(f`}60NB3&}%)o;%NSy36ife_#X3$idmPEtKOX9i;E$e$^#@5BI%IaSguZNe8$l zmNd-D(UuW4B_j%OfW>CxsgLB6cNAjdjn}zJI+*l6JWflw>Arc(pM@_sU{5Vz3xt&x zAZrMMu{bHcu}l+O-v2X{CfY1!;Jj0_;tp?Oq}_pFb+>tRB&7*iLMN0nCv7~z-@e;y z_9vZZqQdy{+D)sP8KkOq;Ie)`xhI0I)h_&pYVwV6aK@5 zw@@z4mY)!sx0;a5Z+p~!z;=F)P&_v7M;#FfnQ;KSy`{{LAv{GCo>)MXwI*<)AkWSD zhjF{f;%UeDw>-J}`Tcu1=l^imy-u6mXMrj&@+VJv!?tRu0fxvX*SK@=rlJ*XDcEEH z{*SniuJ`Q{;wl2oK@*Hk)Jpj;Z)4Z>aZe=Reiz#+q`{%UoVxVhg|&x{h%!gRK=CGE zf<6$0A)zjGHdDcR+6GZS&7KHRKUM0i!GzKvi-a^8;`#ArAE6}PGX9r}Sp3cgl})pw7uuJ}N; z(S1W7pFA+_DwG`Gl5Jxx(L78Lv=|0iGr9$$kz}Uv+z85l-}cc}O34%#lK0-&jy&fD zqF!}f2Ko_D+!&ZvZ}?v#Qf%#Z{Yvj8Kz-i*X(&>N%X9AZ5q`pJU04}B-E1-Gx5EH9 zAi;{_CBH3BtEEjA)p|=A-V^ir&aFw^3X>=irv9W>P?1a?`7=U2kux$b0&Fh8sLkU$ zY{gX7z$8T+woTu+S8xt>kSdoR<1> z=w_>UDxiI(z^;!8;qx{t1*_E$eJO|T$Nub9EP`MX3gUZ`^mK$r%RxLWjZ#5$_Ynmh= z>SFIIoe1A7))(Xq9QZq91IiU`y6G}3ZxicnE<5E(*n>&JI; zL-3_Zwo1rfZ>|i>?`0<%BBeA)8M2HLA{fz#7i>K-BN(nit9;5OFAl+jb*8hu$fbi& zu>X|bU~sG?T#Ga&-&5w7v$xYrEuTR<60tD4-;X~pM-4UCca_bjF8AHeA9H@^X#3$0 z>`bXaS`4X=p~gu1(Yw+Ze>$nT-6#se*x%s=R`SG}0PicOg7_|B(9oj~&$!Ac*keRH zeoCpObUSzGoP8;zj@AfVrWKKxqxjWcn`9--%Sb62YMe#Rw?{QE!ymqX^z^WiD#QY| zJVH$+9+xokGN%d0RkL5L2Z%8CtRb~10PKhpAf)8U=kcQ)A>Zd1i#}^-}Ia1ejZWCbn5)a6gk}q8b0{j0Adjsox zyD+1wG2FKbL5^}ve)viV^jxV7KFk&nv0>G*Bm#%1c{gj! z-U3fa4zGqia-kU7f*e*Z`=(QZx#6X#-)FLJY=y?kg{mkqqXXsY&k3JDW0Jj2D*pOC zYIxrnxF-1?zs5!;&3*WC(xqu6#wuZAQ_m=bTikwo(uP*NdhS^N=STXI(}6Aa z+~`XuM%WBP;UI-wO3jY3BN*8Vl6ZmH=EDE^kstKnOe-bZ!0x4lp>nk)f<^|Y3KpSU zRVJDb6_!R4>MfadG;`$+IFKNYw>KJ;S^88>BS%?+)#>Bt5#W%70}i-q8>A!~BT4@m zkOS%k)mXm;KGFbY*Rc0Z-|IQ_(=3-(pS$_;OBEGi_z=~xY63Z8_TDDFj4(qwhh2qK zv3Yu&thF!?@ssOpL9KUrS88ofxmvV2pcGL-#I#ROVsw%(m`9ptNlBMIaL-yU%T_Q8 ze`=*IKts~e{*Ya^g#mRz%3UAR7t&lCQzQ9UnS$AOHc(17;ue0LX%A(J{7< zwTz%z(!+TkjY7Sj5tGFQo0GWtm#({NzwqwS=Jb$c!F^Jx-zddu`oq~Pj)0elnM$Ni!;$*ilgiz&K?;5gF+|^$WPwqz^a?Fq( zb~@rF8TrYSGI~`>6PXZJe_22dC6XC^tbXJcDeOc_2TTQNta{%xE z<2SXs^OM`|WuV2U=?{n3{FRcB&_kvz&X`Emv0!~80i_Jz&B9kju`~wZy90=Ml)3_4 zlTYCu743;e?+V=hMGEXorE$>%0bY^gA~>Og(ek=h2Dtg5u=qqwJNMU5&H}XggBiC> z<$Rl|(XaGxC%2n;VCi4{Y>nLW8iIGqUIo`qnvax6?>8p!+p}IfIdM(!k(xmo zTwnr_!&!ORfg0SF+)qF7stCl}{v9A@XR_YV7eRi35F_3FM;6nwD7Q^z!bm5KNu%00 zp1InGigK+BJ~w%~jJE0I5@GEc zKvq8scdK@?yh)_>3IhSVgv@=bBsU~QgVtSO)lw$I>4enM7TsP9SlY7O9vRJ(B{|>q z;7L#OI|bjL=Sy(2E)6Tj1G4>XtTs=}#p@k- zA|Dccm?d7r|HVXN92d7}kXJ;m1VYCg$d#6&!^}rh=FIn|C6;WG4BB0D`c6Gd*M1*) zd<*!O%vP8J&MKu(9nl6H|6_ zC?*}pf0ept-7lCZ`$3;2=(dne)=}10-RA10ozh%i!WK-XKkS<0Aa$V1rj9hSGcO-B(aSdo;KV|MT zl-z|^Y1n*VdTT%<1FaPYMr(!@dTSi3Rpy7c{;vQM+LE76XA$Fzv8OmU%|LQ_v;_q} z0G9rKD$d7tEoMd{^E2S9Eu@)r5!ZyvYVyzG@x+BczO|jIIcpCqi3{|8anHY2{OhAN zZNL!^GB;qws_iip21(3`_5DFyw@Ju~+UF3Ra1_&xf`7c4wCLLAS~l|Kte0->`4Faz zA{0qf=6-*r(afz)?fnt~%8OGRqG@~~3-?rthreY2clm2E4~6c}C|-JN|jMknCo=7QW7@4{p*|roO!ULXk;>XxLSdqH$XH(!R zpJH*J5X+h{=avvG4&snDGby&dvsbBGY$rEx!QwUBvVX`h_a)d(cusyf@afLbM$v8g zGxuZ~%_lKO_O-i8#1>3%prgK4TEw0t8agCd%G?l}6TFfo#u|Zq(v2S!gIYgbqgaxE zF&gxZA_}awFt_(0Lk~GuI}X}xPPDWE!woeZYc4+(jt$Iqb&6Tiu`^i`54L`1jr7JFPi~HF(6e&`l`p)0FvfU3$ z`mm#yU346d5hfe`8jKL({GI_uTqkyKr}{K<=>`+R5s#(He&cIj$EngWs@sEjjkX~2L(zWWozIC z5oZp405Rh6NkA-UetD74AERquC`_D@eJJAYs6dZILEaiM*Hrf)X_B1Ix!~yR2^arV zY>Ng1x{P|lUdM{eiUHabo z(N3|4S4rL1kN6a&TB5!Ja45l9m`fZ;0216p4-pe`y_4brA0-er{7CkCePohtuQpXG z`j0NK&%^pHA`P}R?Z%~keq5ve9~K;Qgb!S++YB$SO{lm4y(RAxkCL~zz;6@r}NL-h=zrP4$q|v zwk18!lf9JyG|*C~fVeo3`rFrc2F2As25_CeM6_Hy`zi>UO>C@yI_n>lyh)re^b*cF z{l3Ayc)8phFpW;44^nX6Q{+3!o>-G1&LPmWx1^MUX*;wz%I}^dG}o$ z&^&cd_S0sfFX#d3p-+?SXc-HkiuO$s;(F6zO%%Mljjvm3<*t=z?YeBH_Ri~gn{ckd zm;B^L<*>vnEKp*KywXNx<~@&yeUghJ^~b~koTs@~(Wi1VUd~GuY;!6blwTgrdQLa` zU_SU8@Z&=m8xbZ2U}M_+vZC-K=6UWXj>C8MbnSphTEIEP8-qeKYk6Ax!YrTez6*<+ zUgnBWckLe0kOYL8U`l{@Br-U0KVlH9Ee?`p0FNy{{I9vC2tDs%p0*sCBJ%8VdFpbn zu>?+=5$>ObR5UeX`{&VvY-`QhVX>Q0))9n(RY^|&4l$@dAc~rlc--rb`d=;em;+j` zn|$iOqbrgxSI7LI!zTTooHq2DuT|e|Hn}F=P?E=zmbI$w?_~0dUPV2vbZzyt=FDOr z`7BIVVhY64M!Ho_0d{7z*`&JhO7|&7iLOJV$25HZSc5dG=yOkwwDsD=4ls z2m#|B-QhuGdES+tCdD2WLr!ySPaZVB%ua?bc+oOI^q{*gtw{DdoYNidAY1l{HuTp^ zoA1wSLmqzFMxXxKJ?KMyy>86~{w-{yx2WujXnEQ`y7|pLhYUT&#{~hMLVY*W|3RCU zXQQ6vZgd1bsCah1U260&?hio%=+}j=bxDKd=RIX73K7;r`urZdV$#%qUb`bO_e#O$ z*l*A@`?;w0;l>|~+P{048DpCVDS**o-o)$C&u9ySsv=Si=sCNz-MX(Mc_f*}Fbh1l zNgcBZ4P<{yg#YPG67r~~BHuYxbtXfi&<20_y)XsQ^wCh9&`eDS{Mp&zCZ|2QEi}04 zF^)FP5&?UW&6d`pj+^UgcqBw~&(5mCPA)AkRnb(I-%8qREBE_jz-?G+X3T$&NTB+5 zQ!S9``x}dZ4--hK7oOiCnMI_HzB=}K<`ZE`i1bYHfS9k{HqkWaJ~w}yqTrT)*i8F} zwScbBxi<_E>h$BxLZAI{*@LFwz|~E@5E2En6KYb3=@-$T&`s$w3VtU$Dh-N9eobrt zy{?-dvX+n|?Xu{cly4FxhdrOw0ba4QUbFm$##mkux;ttvTV(-%CJ+3W06d)!+aE51 zYwZIbK}WCZ*@(=5LMj$kBKMZAMksjZhQM10fay>$BP2m%r(oG0Z*#&DWAgjTm&dp} z!>do78#Kz1yt`3EB;p^{tyT2KZKR*Sk&8tRpqIL7h0*s^Ak{|Y=2H4QC+!nbO*dEEU7MHW{ao^S*R)5Gol6aXEaV}4X3*iT4%i)(-V zS$Y67><0tN@^*T9(j@Tg^rPMq_-CsBzEgQJf`%1aWP#}@r_JEGdiBPEku`kt=-p&O zUA-K|iUpBw)lv&l&;tqI*0}(zdV6UPuw?(@GV}%}l2_~fJp}!es@rF>h}r+m08O>U z68=!byd7tpep$6lR)wp*FQo*JDfnY~v*)mO4{unvIV!<=MiVm*77|mxgDqZ`Ss?fC z(%{>Cn?TvNyO&lf2ny{)k9cH3__x^m*(juE5dTySA%(qzsrX(dp!r*$qKHYBmBAOR zBXBmalhhm+ALA=s8?Gb{oPaS^!8#Q1IHWq)u_IB4>H`*^&-dX!C`EsIiXu>Fz66H^ z=3tyCGPI4ikh{IM^Y|?rMU*O{31^UcHG}Ocn~Mw2b4;!RBd-{>7UYNJ2BUG76-x-V ze|5M`MAgdROqBhwp_Gyx;rzCKZU5onbx3ed7VW>J$S6Nofgbue_QNwbDZaMhUnIe( z!uFfR#`&~APgBSJ*2Xe|YyYsH1y3BqheZJbgk|td2T3fqXZ6bqugEEQE4;pW?!w6cLB_H*X(9bp9gZpRbKRBWnwxD*75uS z@aF#tk!DPdLXp>qRStK0PZC3T zI(gqYvF8m)kq1K$4qC7fIzAY<`gno+np>-%_@6TBK|Ix8eF(Ny-?(^@{=-o!bfx zA5+iwn9r|@Ewe#Ms0AoZ+ZS9k+W+lB8!h5z_dlFpik#=6C!M5s%g9f2O3@=FaVnJZ z;d7^I9i>$vgnh!@5hrN07U;epM(M{Zc2$ahFOzhkb;n*!To$MXw_su1k(oJDu6Y%vUg&x6zL#=%xy!rh{ZffstJF$4=-^o7_ zt}l&yyhmu0wAsqDUQ(J75_&+{%;Z#?LOTr_)j=(WZM_*Z#e4KmpEPDqmvN0+KfVxj zDBSRRos=Z?+PgQf2Gb72oqkzgmu3VNW&k#&C`D~4hj%=L?j-#ioVH=2(;8jX@7WRV(G;K~803`U!5VI!CDpnl(; zQNDbVfi7A4n5JL5_(c}guWmF}_c{<3CQwPPBdC{eyO)}nm`?}RCBYVShr^o?6Zuh> zTy=L>ES7s!*z8b!76R9^TN_EFUs@dH$T@`u1 zQfJh%yvXNv@_prT3@tIfJV=wN-3-i#O;ZkQNczg~V`vZ?poOVyT z@B|$I9YlFtv}tSbE@K3>wt7qZbFI9hD_r0V)9nAEBFJHhaiDR&C^+ z#1Co!VZha`dGN02i-NuRk)U_k|A8M-vI>xP&I&5`-(IuRGO?Bn%)ierR8EqLojdzh z*XV$uE6X{f6ym&z%#ga4t_!LVsSA4Bt*`n-KU%_!)0-~g`P|vKtNLG7thBI{YYq|| zFfNgi1Ky$@$M|x(vV-Ssyht?kpt#fS2a{*&l_r_$-o2Xo)2`+C0b{O*9(lNg)*z$I z(9Qw~V@_`La#&4YfuzkAi93Q0quTUL`EKIic={Hhog;9jtHr7N_GGBt%QlO{cAD)R z!SO@R)i)Kf4~sI>dBmaDJ{u&&-fVLlL0}UzWTRve@1712DGj}TTa6>cL4R>s;HP{= zN`9JeI&(e%moTZz-+*{f6Hu!%CEPi*x;UfbMIIpDr*I{E)#3|^BgUq}&HFwe^ufpE z1hL|I6-_&D%j9jQ&!#S=%-t=4GPlSt&BUeLI5j&9z-^Pf$Y3g@oG-%=wXl}1F0coS z5ir#iw6BB2kmmW-IqhG5*xCL}F=GwM<%YeoytK5ntsv}b8VW};{JiETcdZhnNG2Cg zaLs2UYmHaul-M6igY>vYbietG(cHDVj8L3Ax3)?7}s2<8efC(}XKwA+YY zY5yrwKbRM*WAcL@U+3jm5L14oAlT#u61eG*A3oq~Z^RE(OcX>)fL;3si^*9xrLjIe$ne%Qt@F^FAe=lCu!_9PY#mWJC}A7)n+vHP{326XQ1HY~6&m`avZEj5ToawpCN&jh5VXTq8g3HVRJ~b4CTZSyg*%NArf;@Q3FW zwd)h~%(vfNE$dedN-lk3oOvh(h$I&#f>oIy^pcQweR-f4%xz=AgrO5G^hRQIncxJq<+9iGV#xvw|!;mSdXq1Ngs-g4MxY;)jlxu6i`3jzb~%Ux_~3U zFPfY?6r3-ZlSFCYoFEXE_L#)yg~qT@3@U~Ac!qkd=%q7I?Im$!A|p`9@(Q+v7a2^#YJ9>(|5L4)y3 zsK?k1vaOq+8h-wA_p}4M{95Nt=%saS1lC`K$U6HOpt||>CGyLAyx+(J?WbfI)l5L; zD9M5v(_!`m7JzP+DlxIRW+RiWw?t0JPg3b(!Zn_rmbslHVmp_wCtQkjzkV|XRx5?p zynJ}j)>LN(1$VT-IemaDg(*szdM7>uQtk|(13uU7k3EVpvcAK+h4j|V8})2v zVWFcHY^R0@=_XH~uwB-{IPSV|*dAo6J8z7~;9avfSUQ|}q<)AVK`Z_`Kbvxe!P=G- zRJS233u-PeFE{v&i?r#%?&_D=eF87kGB@u>P$%?V^z-ZdQ@B zjHF4XYnUu4J61|~wB$oV=q?YWqW~Zni>}}~#gF$ts~^QyrN7y!%C$%3ge%6|*whcZ zx-NTltAPFeS#xtKVWX1g)b^)man+G`=)$q|<&V?@K3m^-*X|UmFLMaP5oK1B$IsW3 z7JmQtH}x`CAAbz;H(+Z~9@8EJ+r$V9wEna(6B`ViDH9k9`Qs64v{I$8u76u1O$bfmaAc5@HRNM02*m3qK+Z#!jUj-+ph^d3946*9#npeMS zaGiE#Bw0EP-kEo$9tcI#gPe)-00n2h9#q(8!$B=>tKTE#&eXy{?&&|L|J{`JM0_bB zIli8t-D4QhhPJ#zc=LgF^jdPJJsXej%#Nd9ZeEl8xm)l{Cpm3>gL{p>Co_iDB*PZm zLE3D}Z+97Rc|Gl?fSEWe0gUe98%`wUNmg=52@7QgEIZ^3jLieKl4XG-N62pED-8yV z{?lo9pS{4F5`D|-@yY^qQ$Of{CjcW)ptm5 z2h=ll&P~vQmle{26nl(}XUkf1^z6R**gh}_O~srrW6t;`fhIh`Y}YQ^`#l=(cELro zQ~rj#E+%K;Y<8A0c_Ynh^T(WD#9iwi>-DV;92EQgem*PfW^yZB|xYr-!!>*_p zXbpvBBAz%XBiHfVa&TS%Snv-Py08x-#kwVEqM0C{-BIBZ00TINUQ4jHkt+K6JPAqX zZ^rXIpJcr4`V{)jO@UB5UQ}a~SP9XTghJocwtOKHW^zA?1%`-KSwmd>*Cgq{(ZjOiJCSO8UISl?a(#~eG$wd#$0}@eKfA1-eg@l zg+6(aC7Mz@$D|-Yey&@~S5JX)N=Hg_IDC)Rqrxi_gj^|6PgKG8>9FsLt61O?_|HOy zNFsbP?->JI2{Bg9{Axls>4*#yS*Rt#BCidfyxBXO;o(N6BSpEjs;=b>t0O{XF~ayv zy6d`-v`V*Tu9$^uG;pp)4x}KH!J{pAEcHb}pY!L}d4Rtj(`4r&!$%}jt@{L-zAsOx z6=dQcyoDnLNPHYQfczt!aV$p`?u+D3^i&gEZrm>3x$e{gn_)wTbMZHj!LP88!3Xj$ z7`WoPR=qy!el-Vk8=4Fj4ln94MG^H&H4y@UTM=qwAghfek5)FEt3pJfTQLY@M{~wv z%DgG&qx(3`hbS^bg_(q!?rdx57KIxUq$<|8Ap$=1IkXDo@W1-9N=zCa)>E8$0L@yz zad~<$0?-f(3j)WcD67AFL0f#1O6aladUh#F(Dm^_nHxgsHHLjOehgy2a-<0kh$W?5 z0FtHV7+L`m{}ag*BFx#|-r2Ly9kK%m73=fmO#G+5 zCnX=kT7II!G>(~xjCtT#kaBNYWadIAo2No0@4-OnyhSij z>sBC_06#1n+UyeH#0MSuNwgYD7NJiuC2aR$zQZlDR4?U8D{@z#QS13hENCzd#SCJeiMIk8>JeK_rD zSsH5$xOqV!3kvGf9}8#Sw1)-gAqFtF>|w)Fqz5h*QIQ!tBVoO?WwD{YqzIqUU&t1X;&=2art+rx)&vCE2=JJ!zmpYJKF>L>Y#U z1_Ri8egG40%mt~YFo7kFNTyCE1rfczd@Mq<_Xph9UdN$+l&|vM`NX4FMQ!X$Q{0!$ zqj{w?m{lB^5mNWk&P=dSqGm;j1H~wfRokZ3#F!Hg$@~yOD*Z5_0&MpFIAUJ05_zTF zN}$HbCyLb{C{^$PG;0Vy4mzkcbDtbd5giCd@mK-7gujk|??I?wxl#GTmG-xN136HO zyL))A6p)}>1u32cjrjTG#!s?xHh^Z8=IyAl6W==bLZuT%O*hob9ZX2^_pz_tjWXX#qw`a2m>f zsCu3(K`x(1qp8t0-g}DHPP!G#M${~Vd|>;{7u`y6^AOWn6=pzMC<6@OKVr}y=f>ed zxx66Xe+T4rG##^_OJk+W6_~r6&_IZ&IZ@MIGmVfrF@cr;KaS4B5z7C8=X&Yk;w-sAQD zddF8#Ac9svaRQyO93g^qe=y?kYTvn*7~b_StmWKt>1OzC!l}n;T&H>X^V1D`eiizV z>I*biIQTK~V@~JLI+QkD1GiD6PnoqCJgtFYAdXb~8~2Ja@MByDxc?W#i(?9Zp>4M2 zS0Wnd%YCuhM;Cv`yV3TXQQIrVS+*F!(7|-eqTs^0g2>~MT=J8ex$%4CHunR-fwy(Y zONsVAw&qTg<2fdmn}tQcux+U^uk0Z+{avTuO6_&5=!lJa#Y+yulgdh(vAkn{|Beej zgxzDstYg;Bn5Mpa*MqW4;vBxSdIpinVTto~pXTCPB{Lm`KohZF?DoBrxhSXqx|N21 z7ied4!fk>hfs&90_G+(;o|l_c8R_g>MLNie1oV*={`A(Y1Hp@rnC^uLi67TNfXaON z6*749(&TSA;E(4|RJ2gqDMT8xq<|ZtXX$_h8$wnnU;Zh$)d|nEpHgkh)Jkh6x;ABq zx+!R(wbOlfWI!$YM`PMUA8yzH?gcFnDSwCOS`<7~@Qu5a4<(pNOqaFq)TGV8>CSDU z1;csYlTWH&Wq!0wx>q24c+?axm1en$ZA--7dAoSu>qtym)M6OP1_ z1@8Gim}lV_aAn+3R^ZdHOMQ&}y_K^2ppKaRhc3!)^B`=knxT9F8@8X2x6;?FMj744 z!erc9pOnLu0A-?TRk~5>jo^=EZiTQR?w6{&nHSM@uv>FIWuV3@;Y}glxUP#Nh-%AY zm{MQ11AI4?l{hh^$~a-AVfG{ci5QTvY$ihycnBr-$={1ZEW7g*9y|nRhahL*{i*Pc z5Qn|)Tg6!IxzKOQ)b6=2-((2F!f$iii(zvnq#%-IkN=Z1<(EEb#7|S`+fF(s_7hyG#DFNNi75i8b~TXJK=Gk7oTGQJ6|#`01-^TQ|1SJdu~_}yI4jePm# z2wHsqttIC)vXUh$Tn*~7n-4!R5yolK)Io^YYi*3Ievn_s!?Xn#TWOve(;Ztx&iEFd z<5dZJjyRFtUNMZbI>io`JYGp|uEF{p$b!s!5d2m2MY&JU&&{dux-mB&0^zSh1i>=xoc-syAu@(>n0=F-s!ug3u%8$`ws&4~ZJkVgM|sH!{x9E~uh| zt=PJ$z)eagC3M7gpz6<>hradaBAyb(R9-tS<>UHkEvy`nnAb{@rZRYmbv$zCopTfk zRKo%Z?l;$SDZ!%!xQGb-gA0R@nH(7Bg3`GrSAapXn#RtlI*08MxN3TN;jm~qt*hnaQigf{pDoQZ=(($%)p&jzf zNE$Y_eQIWMO6h3bpq<7L$1_N$hcxwAp+fyQdHJBq)2;s&%23S(5m@cjweHIdy&@`1 z8zm7na#a!7r!E*lh&E2!gz>(m)>wgbp!QD+6*2fVWV=C43DC_uvl=Ff@OHYr^Flu1 ztTSGaCIoBp6cHjTwkDnOGH$%2sNn)i#r^ca^ScgOm*k#qAGjeEi-d1$%sg#8f1zvk ztKLQ6J3tHtTKZQC^Ip*UkLz{+LOXj&E=~|~q46Qap>-LC?JLW`))ya$g&X^%_lHdL ziyL+=mo6XHT6{R0w`3vs6HsaraGs_+P7 z^Fa&DK%I0ecRZI zMNS5ew1?P;W-%PBi~t4oxKe%y~e33da&Qq9wcu z5ytax$wLFUD_YGDfosMSaV3A!82&BE0CkQ)xNt(0(huDOXUW%xth_Rj4ZwfbW`_YA{B^_&{eq& zWA;ks$kJ+t)SE#*K>0(P4xNk)f3r8pM_bl}`EBO#0$?bEVbgCct+4s6Csx}%=)-cSe)BXAH(Tg%G$14aH24p7wb|>roZIj?sI{Q_l@nm!`2)>`0ZONBx=~>g87+-IsTS+RnXV zwxWA*gG6Ih`+Ecp#-tZVj*EB6f@%KY7NW!T~?rNKDOi)lnoy$po78TN#~ve1}vSNmXw{eklr z3f1!Bqs;&&RR~t>IES=G4kYakbyht=10MC1ojRc>z=n%ap7gqkYcb%&&6xp%FZbKF zZypVuJ=}87sJo_cvW1KP3jdVRgt55(f~#!VY$7Z}oJUWPTZ#AZRTMtvZTY&5KCCZk3j>O6HrfQ6$%T$lXR0lLGLNPxIf zl@!P`8Eyn3-?9+5BxQwlD%YI06G35Dx@mtvqZ7zQ0KeDfW9r@rHwvKssOG%Xjj(q* zrEOrLKeeUVC}7%1XNx5(}A8VZXb6OwtDVd-n+)4omHbJ2%Ik05WK zvgljoo}p+EOh_X+Jq~f$e-SIRlnrsnj6)}&5ttbpJtBpRa)*Q}%qtcmul@9ZTJ^wt zYWK5Kryc>LbF>&amEQpUNocT}>*MWiCQq>!9J(b^uuW~Va@3pJV~HJHW@eE<(B%9k z!`ZkS^fl9F;7idf01hevsMmW?!*+culdd5Z!sNl~;{()Wj-&ft#$0g>51;hm2Ae0o z&*RgURNwQc!ciaAOPG#+>k^|8wIMpHAkVq`yDQx}3r^udd9}f@O8@0#IEdkdI@{T_ zLfuP8D?xQd5@5BZxxGU&6A89$O=qykf+ivGr&mbKFW+svO{hCwNrf=Jgit-O5XM?C zKM7_^oTohmcRO+@0-E?~3p?`F7oRPQ?Zq9rQ+gg+-6=3ZUp+3F${l{aOsQeH^1CZ| z=Q+DPdR+c68*ulH?cK<9KPSTB^)ir8i1oFWD(9jSZScomXHk{k3wLUlu(%3CG>Wuh zr*qnQe(u<%=^x>n%IfHTuRw!3XY*{mERz`c)({adjHYgv0!U9}HuKH;1LhdC)nT8% zSSi8X0CjLh`*HgiOQvII%UMzgax<>e7#YwlOA{VtwNwVrBhlL8gqQpkPU;gw^`nqS zu7-$y%M1i?$N~=uzyFo>y1;*KpAnz54Q?d`$4SoX2jT>XuBog*WycQc5j`MEbc5P+ z#pz^F=f<$N%Q8RfZ8J3NcYn#EprVK9Cern5eE)Q2T!yqohwvzWq66FfpB$84MI)g- zaOR(OR|>K1YaXOjkHB|bF9p=qFk&nwl(mDgfpy)-01A$+Tfsp;h^q6OJ!J^9hnu=U z8m%h}MYjA}Izj;mmU@1ut6;7Od` zk8T?5sTM{T)E)ZB0A}#Em|@s*Pgja*T#Nu4Say|I@eopx7vB~^PNC}HDEC5g2@63| zuvJ&VqJTGRAD-1*7Glx@u$nM!%hztc;?3IRaRVwaEKh-{*!*=7f-`I>2iMUpK1Xpl zWtkt2(Usf3T)CyyeD%ZLsb>9g+mLM`W4t6rE68dn0G!rCteVjbYB|0;e!v)fLPLVHN8K`rYSCJ)$Bi^wZnLTPMQn1=}&)OEsy}Lmb zs@^c0L#j0=-oD8J6#lin-em*iU>0%K`(PIOiWw9W&pOCtKtLHW2e4dWha!t8EJY7jf%h^%Rb3I?5)1rEfxo;7r!VDv z;2t%$N5v-OT2ua(RW+szJj7D|{0?%zydFSWN1UA9Ho;d~Bp2Z}Zwuv+bb=)cFubJ< zFrl~4Zmg_z2grK9p8vq|eeF8sZ)q71X@R<(iN)?21A!eQ$>XsaV~iT-pW>Qb2%8W# z*Z^bYwdV7g&$zHvT+fyiPv>DT(Mh{dIyyx6D|%h%vtl}4m3ziaA8(*T7#Yb|W`Q5V zXI`F^Da1WTwE|=}U%V_6>%hiY;w68undu$^T`Ad+-IR&IWg}xyKy(JL#`Obd7MJ_; zjqUrR!`{qAf*`h%#wOjB7tVY;OjEVd#PF7%4E8q88YjyY+V=PNM-$ZW&snO>+xvl> z<6ZS&>$rHJ07ZK1>4pfo9)HMfLQ`q~hLaCj$_(x7aQHO#Q;TV&+`z4>WI4uK0Q9(f z)P9^+^y7^!Q8o!z@4q* zwDG>At^n9T&{Z}XK@mE;>O@5w#*c2Er@}2%TIRpExmMo6^nZ&FvJu`pO81KIDU+4K zh(WxcmzXh-WtHUU8oZ6Es`IK>f#^+970G?tPoZwtTEcP}==-!LT(omw)niHL49Ag7 z#zwK}Q)g&7YZ}!0lgRN3qp#{6WVH$j9D-x%gv>GNb_y)i8(Q9^oQzMUe9}{?w?= zL+I}&?rn?JA$tifgz6Y|#I-5a3|1n{Z3OM_jLN%u-M8+vlsXR%<4q!m$QtfvB5JIXY*eo`izE!c^ z-oX`zKfsWtGKS|Np}whxXPXgE4CoOI1%Sg=8N$!w;m@0liGf@M=Px3rH8F=pzfLtp zaXcYt`WYF{0=71#(^@jnc7WdM-D3=l@0MV5V&*&kjjGGA!m_xEe)0kDs^Al}19snj zUk(!_WTxhJs~P=Z1?MR^KarVxN1Z`gK7a0A(RDu01_(&3y7C3~@Z}ySZE0V;61?eq z$At3dTT|o@lrRIPTBji-0!x3g-ReN(7i-dnppk40rW(Qtt+1U?ZFr2C08!UO=}&jTk#&>+ zbvA5`r9qAv_p6+r|I&*>gG>J3B93w0wnz3if1Um~zzD5Nq5LFz<{$VNemcVm-t+=8 z2jr<0&JVatzPOtZc3WgqI5l+Ct%&QclU2FIlX`%I-!&I#IEOqjuRmy&ZxL*MJNWC^ zgEDXB?!4U+K`A1Qe%vXUb}aja2G69VM&)b45Xdr617` zR_mE@LW4h}2fDY^dut;|@hCgsrkBHxo3kc$vyvZEbWqF`uOW}lkXt4QCTK8igxG^I z7oZrGUO{M(2N1NEUKm0$SpBDaFncUK`ki9^kMhXXHDj5$3()pA$+SPXsqs#UL1a6V z8VjAI&n|*9`!R<7neNW>KWCu>d3_2U+9I0j`L|~V4442$uov_9gOU^1fT~XQmjXCf z{!J_iJ6}?G+WK>Ic|whvq7_>!*FIVJdy_#F)j9^u7)X}pRK!>?6Ju_Yi@JnNVOC)4 zmC%AM#h9}mDZkL6_!Ogf&!5!wl~9%6w1F!?;V5+>4UlH}V@8LD6aMb7Xe`j-1k*+U zVA8ycvUuS`?T}_RzCahB>68Tx$tT>rj6Ay)U_j9@!ocG<)hY_Res-4}?Jz}bucpwC ziLhnG#}wZPWX`U=7sc$PQ-3U7A^vN%E()HNHwEkcHyq@>PrC∓t$dRJGIadE?vc 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 Kx8Vjl{{H|h@<;Lj diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/icon.ico b/keysas-usbfilter/tray-app/src-tauri/icons/icon.ico deleted file mode 100644 index b3636e4b22ba65db9061cd60a77b02c92022dfd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86642 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 AKmY&$ diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/icon.png b/keysas-usbfilter/tray-app/src-tauri/icons/icon.png deleted file mode 100644 index e1cd2619e0b5ec089cbba5ec7b03ddf2b1dfceb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14183 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

    5l#v3A#xI3*z~1~fvo9Q(N^(==!|_FZ z*duZ=+M1~)8E|otX8KNZlr?qels#x_1Xq@9IIw~@9uAREJVH)Xw^}UclF6327}E42 zT)E&?U%TK?(+K7%R!`H5oX0i)4Qn5??Iw3p5J~6_u+aWehY{DSn}3V2p$bgjnAu?o)v@iC254fXeMv50$9YrpU`N?u@QIWs)T?SP|fa}(|9 zqAX+!7`cx=4)cCBg5h~pu(?@9`)aCr#oyz$ld=#RFxYCNZCZls@4v2~*e-t6PEVvV z&bbK3b3wt(Coc!ufAbXXC<**#HQ%J9k`New6iG<5RjtO4XVO?dCvwxD{kJ#tfQr(X zg^NTwF-FwAeS_{V4bfel8l`~NbfrTR2s!G>WduFWxH(t~aK4q=6rEE^$+Uox>gJO2 z{L<;6Q6nHa5#ZEM>H58not!)z(6*_=^~8}jWf*IG$AUKVWOZ4?)GfF z+BM#*wKKmLFD7E~W3U!$IVm$k_k1f&Kz6WV8@55P?r~bcg-Za-!rvW?ns&)KOGT2~ zlkAyqhQj=P$Eg3w#K~}zH@J5bo-BfHjInKSz$@?+Z)NPD4pHj^_Qxmi`UqoTy=`sV zLVxrXGuBr=QRm|}wg75yetQQK4fY3#P_~J}zEfPnb2C4Wo!E(d*(cA;b?7$g2in<( zPn)ghX}nzJPmb6(3Dpeg_GW~Hc}Lt=lgsSZz z!5QXyz7KaR;D`3Ee}d`af{H>WWZ|Io1QI3~4Ll_`g1(cRnhLK73Ro)7zPCd={1W2x zRp%Xlvv4>!<2@}$hz|!V{T}_eHx2xkLl^hQoZTCnsjCl|W_@5Fx2(+j0ogy&Y+;L- z<)G$*CiN7hOm^s!{U>1F7U=iNk{+u~dAC!eDz%=|glFW0jEZU1&o(G_c#wTxUjnG} z#cg3>jEpUi#Mlq@t?Msg_#geK^Lx@DyHWf7=AS5vVyM7YOjvUVCfcpVR<(+5!H?9- zySI6s>o3m&*zr||=wcPGyBkQV`EWJl@bH8qobjOp+sXL*)=&yX)8aAbf~tGv?a2SN zu^Ddo-z?DWk9h9Yz#5p^NU#x~wYSd?H@w@!2Gb4G)6-utEMV~~M85Br5ff(v5O1|T z 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 ZCq3TrO*V1g!1iw(xQ4!Zm8xyn{{n*~GnN1V literal 0 HcmV?d00001 diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short.ico b/keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short.ico new file mode 100644 index 0000000000000000000000000000000000000000..52c5eff065a6b03e0984877a04ba187ed233c9b6 GIT binary patch literal 21072 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

    Ab;!7 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 NlU9~0e`_50e*wV=XSV AppControler { + let mut ctrl = AppControler { + store: FilterStore::init_store(), + }; + + // Create a default USB device for the tests + let usb = USBDevice { + name: String::from("Kingston USB"), + path: String::from("D:"), + authorization: KeysasAuthorization::AllowedRead, + files: Vec::new(), + }; + + ctrl.store.add_device(&usb); + + ctrl + } +} diff --git a/keysas-usbfilter/tray-app/src-tauri/src/filter_store.rs b/keysas-usbfilter/tray-app/src-tauri/src/filter_store.rs new file mode 100644 index 0000000..6f7ff36 --- /dev/null +++ b/keysas-usbfilter/tray-app/src-tauri/src/filter_store.rs @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * + * (C) Copyright 2019-2023 Luc Bonnafoux, Stephane Neveu + * + */ + +//! Data store for Keysas filter application + +#![warn(unused_extern_crates)] +#![forbid(non_shorthand_field_patterns)] +#![warn(dead_code)] +#![warn(missing_debug_implementations)] +#![warn(missing_copy_implementations)] +#![warn(trivial_casts)] +#![warn(trivial_numeric_casts)] +#![warn(unused_extern_crates)] +#![warn(unused_import_braces)] +#![warn(unused_qualifications)] +#![warn(variant_size_differences)] +#![forbid(private_in_public)] +#![warn(overflowing_literals)] +#![warn(deprecated)] +#![warn(unused_imports)] + +use anyhow::anyhow; + +#[derive(Debug, Clone)] +pub enum KeysasAuthorization { + Blocked, + AllowedRead, + AllowedRwWarning, + AllowedAll, +} + +#[derive(Debug, Clone)] +pub struct FileAuth { + pub path: String, + pub authorization: KeysasAuthorization, +} + +#[derive(Debug, Clone)] +pub struct USBDevice { + pub name: String, + pub path: String, + pub authorization: KeysasAuthorization, + pub files: Vec, +} + +#[derive(Debug, Clone)] +pub struct FilterStore { + pub devices: Vec, +} + +impl FilterStore { + pub fn init_store() -> FilterStore { + FilterStore { + devices: Vec::new(), + } + } + + pub fn add_device(&mut self, device: &USBDevice) { + self.devices.push(device.clone()); + } + + pub fn remove_device(&mut self, device_name: &str) -> Result<(), anyhow::Error> { + Ok(()) + } + + pub fn get_devices(&self) -> &[USBDevice] { + &self.devices + } + + pub fn set_device_auth( + &mut self, + device_name: &str, + auth: KeysasAuthorization, + ) -> Result<(), anyhow::Error> { + Ok(()) + } + + pub fn add_file(&mut self, device_name: &str, file: &FileAuth) -> Result<(), anyhow::Error> { + Ok(()) + } + + pub fn remove_file(&mut self, device_name: &str, file_name: &str) -> Result<(), anyhow::Error> { + Ok(()) + } + + pub fn get_files(&self, device_name: &str) -> Result<&[FileAuth], anyhow::Error> { + Err(anyhow!("Not implemented")) + } + + pub fn set_file_auth( + &mut self, + device_name: &str, + file_name: &str, + auth: KeysasAuthorization, + ) -> Result<(), anyhow::Error> { + Ok(()) + } +} diff --git a/keysas-usbfilter/tray-app/src-tauri/src/main.rs b/keysas-usbfilter/tray-app/src-tauri/src/main.rs index 19e96e7..521a6f1 100644 --- a/keysas-usbfilter/tray-app/src-tauri/src/main.rs +++ b/keysas-usbfilter/tray-app/src-tauri/src/main.rs @@ -25,12 +25,30 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use tauri::{SystemTray, CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem, SystemTrayEvent}; +//mod service_if; +mod app_controler; +mod filter_store; -// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command +use tauri::{ + App, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, +}; +use tauri_plugin_positioner::{Position, WindowExt}; + +use crate::app_controler::AppControler; + +/// Command call to open the USB device window +/// +/// # Arguments +/// +/// * 'app' - Handle to the tauri app, supplied by tauri +/// * 'name' - Name of the USB device selected, supplied by the frontend #[tauri::command] -fn greet(name: &str) -> String { - format!("Hello, {}! You've been greeted from Rust!", name) +fn show_usb_device(app: tauri::AppHandle, name: &str) { + tauri::WindowBuilder::new( + &app, + "usbDetails", + tauri::WindowUrl::App("usb_details.html".into()) + ).build().unwrap(); } fn main() -> Result<(), anyhow::Error> { @@ -50,7 +68,10 @@ fn init_tauri() -> Result<(), anyhow::Error> { .add_item(hide); let tray = SystemTray::new().with_menu(tray_menu); - tauri::Builder::default() + let app = tauri::Builder::default() + .plugin(tauri_plugin_positioner::init()) + .manage(AppControler::init()) + .invoke_handler(tauri::generate_handler![show_usb_device]) .system_tray(tray) .on_system_tray_event(|app, event| match event { SystemTrayEvent::LeftClick { @@ -59,6 +80,17 @@ fn init_tauri() -> Result<(), anyhow::Error> { .. } => { println!("Left click event"); + let window = app.get_window("main").unwrap(); + match window.is_visible() { + Ok(false) => { + window.move_window(Position::BottomRight); + window.show(); + } + Ok(true) => { + window.hide(); + } + _ => {} + } } SystemTrayEvent::RightClick { position: _, @@ -74,24 +106,25 @@ fn init_tauri() -> Result<(), anyhow::Error> { } => { println!("Double click event"); } - SystemTrayEvent::MenuItemClick { - id, - .. - } => { - match id.as_str() { - "quit" => { - println!("Quit selected"); - }, - "hide" => { - println!("Hide selected"); - }, - _ => {} + SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { + "quit" => { + println!("Quit selected"); } - } + "hide" => { + println!("Hide selected"); + } + _ => {} + }, _ => {} }) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .build(tauri::generate_context!())?; + + app.run(|_app_handle, event| match event { + tauri::RunEvent::ExitRequested { api, .. } => { + api.prevent_exit(); + } + _ => {} + }); Ok(()) -} \ No newline at end of file +} diff --git a/keysas-usbfilter/tray-app/src-tauri/src/service_if.rs b/keysas-usbfilter/tray-app/src-tauri/src/service_if.rs new file mode 100644 index 0000000..d7564fc --- /dev/null +++ b/keysas-usbfilter/tray-app/src-tauri/src/service_if.rs @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * + * (C) Copyright 2019-2023 Luc Bonnafoux, Stephane Neveu + * + */ + +//! Connection to Keysas Windows service + +#![warn(unused_extern_crates)] +#![forbid(non_shorthand_field_patterns)] +#![warn(dead_code)] +#![warn(missing_debug_implementations)] +#![warn(missing_copy_implementations)] +#![warn(trivial_casts)] +#![warn(trivial_numeric_casts)] +#![warn(unused_extern_crates)] +#![warn(unused_import_braces)] +#![warn(unused_qualifications)] +#![warn(variant_size_differences)] +#![forbid(private_in_public)] +#![warn(overflowing_literals)] +#![warn(deprecated)] +#![warn(unused_imports)] + +use std::time::Duration; +use std::io; +use std::thread; +use tokio::net::windows::named_pipe::ClientOptions; +use tokio::time; +use windows_sys::Win32::Foundation::ERROR_PIPE_BUSY; +use anyhow::anyhow; + +use crate::filter_store::FilterStore; + +const SERVICE_PIPE: &str = r"\\.\pipe\keysas-service"; + +/// Initialize the pipe with Keysas Service and start a thread to monitor it +pub fn init_service_if(store: &FilterStore) -> Result<(), anyhow::Error> { + // Initialize the client socket + let client = loop { + match ClientOptions::new().open(SERVICE_PIPE) { + Ok(client) => break client, + Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY as i32) => (), + Err(e) => return Err(anyhow!("Failed to open client socket")), + } + time::sleep(Duration::from_millis(50)); + }; + + // Start the listening thread + tokio::task::spawn(async { + let mut msg = vec![0;1024]; + + loop { + client.readable().await; + + match client.try_read(&mut msg) { + Ok(n) => { + msg.truncate(n); + println!("Message: {:?}", msg); + break; + } + Err(e) if e.kind() == io::ErrorKind::WouldBlock => {continue;} + Err(e) => {return;} + } + } + }); + + Ok(()) +} \ No newline at end of file diff --git a/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json b/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json index 88edc94..52c31db 100644 --- a/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json +++ b/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json @@ -21,14 +21,15 @@ "bundle": { "active": true, "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" + "icons/logo-keysas-short.ico", + "icons/logo-keysas-short-16.png", + "icons/logo-keysas-short-32.png", + "icons/logo-keysas-short-48.png", + "icons/logo-keysas-short-256.png" ], - "identifier": "com.tauri.dev", - "targets": "all" + "identifier": "com.keysas.usbfirewall", + "targets": "all", + "category": "Utility" }, "security": { "csp": null @@ -37,8 +38,16 @@ "active": false }, "systemTray": { - "iconPath": "icons/icon.png", + "iconPath": "./icons/logo-keysas-short.ico", "iconAsTemplate": true - } + }, + "windows": [ + { + "label": "main", + "title": "Keysas USB Firewall", + "url": "index.html", + "visible": false + } + ] } } diff --git a/keysas-usbfilter/tray-app/src/App.vue b/keysas-usbfilter/tray-app/src/App.vue index aa87d0a..def61bc 100644 --- a/keysas-usbfilter/tray-app/src/App.vue +++ b/keysas-usbfilter/tray-app/src/App.vue @@ -1,25 +1,44 @@ diff --git a/keysas-usbfilter/tray-app/src/USBDetails.vue b/keysas-usbfilter/tray-app/src/USBDetails.vue new file mode 100644 index 0000000..70128a9 --- /dev/null +++ b/keysas-usbfilter/tray-app/src/USBDetails.vue @@ -0,0 +1,44 @@ + + + + + \ No newline at end of file diff --git a/keysas-usbfilter/tray-app/src/components/Greet.vue b/keysas-usbfilter/tray-app/src/components/Greet.vue deleted file mode 100644 index 289373b..0000000 --- a/keysas-usbfilter/tray-app/src/components/Greet.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/keysas-usbfilter/tray-app/src/styles.css b/keysas-usbfilter/tray-app/src/styles.css index f7de85b..b20c2a5 100644 --- a/keysas-usbfilter/tray-app/src/styles.css +++ b/keysas-usbfilter/tray-app/src/styles.css @@ -106,4 +106,8 @@ button { button:active { background-color: #0f0f0f69; } + + container, row { + background-color: #0f0f0f69; + } } diff --git a/keysas-usbfilter/tray-app/src/usb_details.ts b/keysas-usbfilter/tray-app/src/usb_details.ts new file mode 100644 index 0000000..3f96bca --- /dev/null +++ b/keysas-usbfilter/tray-app/src/usb_details.ts @@ -0,0 +1,5 @@ +import { createApp } from "vue"; +import "./styles.css"; +import UsbApp from "./USBDetails.vue"; + +createApp(UsbApp).mount("#usb_details"); diff --git a/keysas-usbfilter/tray-app/usb_details.html b/keysas-usbfilter/tray-app/usb_details.html new file mode 100644 index 0000000..52e2b3e --- /dev/null +++ b/keysas-usbfilter/tray-app/usb_details.html @@ -0,0 +1,14 @@ + + + + + + + Keysas USB Firewall + + + +

    + + + \ No newline at end of file From 083565a54a7bbdb2e98bca3d6f14b95663ed4424 Mon Sep 17 00:00:00 2001 From: Luc Date: Thu, 4 May 2023 09:03:20 +0200 Subject: [PATCH 059/160] Cleaned driver and service code --- .../daemon/src/driver_interface.rs | 3 +- keysas-usbfilter/daemon/src/main.rs | 7 +- .../daemon/src/windows_driver_interface.rs | 196 ++++++++++++------ keysas-usbfilter/minifilter/keysasFile.c | 35 +--- 4 files changed, 143 insertions(+), 98 deletions(-) diff --git a/keysas-usbfilter/daemon/src/driver_interface.rs b/keysas-usbfilter/daemon/src/driver_interface.rs index b632b58..2dffc9a 100644 --- a/keysas-usbfilter/daemon/src/driver_interface.rs +++ b/keysas-usbfilter/daemon/src/driver_interface.rs @@ -14,7 +14,6 @@ #![warn(dead_code)] #![warn(missing_debug_implementations)] #![warn(missing_copy_implementations)] -#![warn(trivial_casts)] #![warn(trivial_numeric_casts)] #![warn(unused_extern_crates)] #![warn(unused_import_braces)] @@ -43,6 +42,6 @@ pub fn init_driver_com() -> Result { Ok(driver_interface) } else { log::error!("OS not supported"); - return Err(anyhow!("Failed to open driver interface: OS not supported")); + Err(anyhow!("Failed to open driver interface: OS not supported")) } } diff --git a/keysas-usbfilter/daemon/src/main.rs b/keysas-usbfilter/daemon/src/main.rs index 9ecd1e0..570c04b 100644 --- a/keysas-usbfilter/daemon/src/main.rs +++ b/keysas-usbfilter/daemon/src/main.rs @@ -14,7 +14,6 @@ #![warn(dead_code)] #![warn(missing_debug_implementations)] #![warn(missing_copy_implementations)] -#![warn(trivial_casts)] #![warn(trivial_numeric_casts)] #![warn(unused_extern_crates)] #![warn(unused_import_braces)] @@ -24,8 +23,8 @@ #![warn(overflowing_literals)] #![warn(deprecated)] #![warn(unused_imports)] + #![feature(vec_into_raw_parts)] -#![feature(is_some_and)] #![feature(str_split_remainder)] pub mod driver_interface; @@ -47,7 +46,9 @@ fn main() -> Result<(), anyhow::Error> { log::info!("Driver interface OK"); - loop {} + loop { + std::thread::sleep(std::time::Duration::from_secs(10)); + } } // Initialize the driver interface and register the callbacks diff --git a/keysas-usbfilter/daemon/src/windows_driver_interface.rs b/keysas-usbfilter/daemon/src/windows_driver_interface.rs index e732539..0c4d698 100644 --- a/keysas-usbfilter/daemon/src/windows_driver_interface.rs +++ b/keysas-usbfilter/daemon/src/windows_driver_interface.rs @@ -14,7 +14,6 @@ #![warn(dead_code)] #![warn(missing_debug_implementations)] #![warn(missing_copy_implementations)] -#![warn(trivial_casts)] #![warn(trivial_numeric_casts)] #![warn(unused_extern_crates)] #![warn(unused_import_braces)] @@ -27,24 +26,19 @@ use anyhow::anyhow; use libc::c_void; -use std::fs::File; -use std::io::BufReader; -use std::io::Read; use std::mem::size_of; use std::path::PathBuf; use std::path::{Component, Path}; use std::thread; -use std::os::windows::ffi::OsStrExt; -use std::ffi::{OsStr, OsString}; +use std::ffi::OsStr; use widestring::U16CString; use windows::core::{PCSTR, PCWSTR}; use windows::s; use windows::Win32::Foundation::{ - CloseHandle, GetLastError, BOOL, BOOLEAN, HANDLE, STATUS_SUCCESS, STATUS_UNSUCCESSFUL, + CloseHandle, GetLastError, BOOLEAN, HANDLE, STATUS_SUCCESS, STATUS_UNSUCCESSFUL, }; use windows::Win32::Storage::FileSystem::{ - CreateFileA, ReadFile, SetFilePointer, FILE_ATTRIBUTE_NORMAL, FILE_BEGIN, - FILE_FLAGS_AND_ATTRIBUTES, FILE_FLAG_BACKUP_SEMANTICS, FILE_SHARE_READ, FILE_SHARE_WRITE, + CreateFileA, ReadFile, FILE_FLAG_BACKUP_SEMANTICS, FILE_SHARE_READ, FILE_SHARE_WRITE, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, OPEN_EXISTING, }; use windows::Win32::Storage::InstallableFileSystems::{ @@ -57,33 +51,47 @@ use windows::Win32::UI::WindowsAndMessaging::*; use keysas_lib::file_report::parse_report; -// Operation code for the request to userland +/// Operation code for the request to userland #[derive(Debug)] enum KeysasFilterOperation { - ScanFile = 0, // Validate the signature of the file and the report - UserAllowFile, // Ask user to allow the file - ScanUsb, // Ask to validate the USB drive signature - UserAllowAllUsb, // Ask user to allow complete access the USB drive - UserAllowUsbWithWarning, // Ask user to allow access to USB drive with warning on file opening + /// Validate the signature of the file and the report + ScanFile = 0, + /// Ask user to allow the file + UserAllowFile, + /// Ask to validate the USB drive signature + ScanUsb, + /// Ask user to allow complete access the USB drive + UserAllowAllUsb, + /// Ask user to allow access to USB drive with warning on file opening + UserAllowUsbWithWarning, } +/// Format of a request from the driver to the service scanner #[derive(Debug)] #[repr(C)] struct DriverRequest { + /// Header of the request managed by Windows header: FILTER_MESSAGE_HEADER, + /// Operation code defined in [KeysasFilterOperation] operation: KeysasFilterOperation, + /// Buffer with the content of the operation content: [u16; 1024], } +/// Format of a reply to the driver #[derive(Debug)] #[repr(C)] struct UserReply { + /// Header of the message, managed by Windows header: FILTER_REPLY_HEADER, + /// Result of the request result: BOOLEAN, } +/// Handle to the driver interface #[derive(Debug, Copy, Clone)] pub struct WindowsDriverInterface { + /// Handle to the communication port handle: HANDLE, } @@ -97,17 +105,15 @@ impl WindowsDriverInterface { // Open communication canal with the driver let com_port_name = U16CString::from_str(DRIVER_COM_PORT).unwrap().into_raw(); - let handle; - - unsafe { - handle = match FilterConnectCommunicationPort(PCWSTR(com_port_name), 0, None, 0, None) { + let handle = unsafe { + match FilterConnectCommunicationPort(PCWSTR(com_port_name), 0, None, 0, None) { Ok(h) => h, Err(e) => { log::error!("Connection to minifilter failed: {e}"); return Err(anyhow!("Connection to minifilter failed: {e}")); } - }; - } + } + }; Ok(Self { handle }) } @@ -118,7 +124,7 @@ impl WindowsDriverInterface { /// /// * `cb` - Callback to handle the driver requests pub fn start_driver_com(&self, _cb: fn() -> ()) -> Result<(), anyhow::Error> { - let handle = self.handle.clone(); + let handle = self.handle; thread::spawn(move || -> Result<(), anyhow::Error> { let request_size = u32::try_from(size_of::())?; let reply_size = u32::try_from(size_of::())? @@ -133,8 +139,7 @@ impl WindowsDriverInterface { }; unsafe { - if let Err(_) = - FilterGetMessage(handle, &mut request.header, request_size, None) + if FilterGetMessage(handle, &mut request.header, request_size, None).is_err() { println!("Failed to get message from driver"); continue; @@ -155,7 +160,7 @@ impl WindowsDriverInterface { result: BOOLEAN::from(false), }; unsafe { - if let Err(_) = FilterReplyMessage(handle, &reply.header, reply_size) { + if FilterReplyMessage(handle, &reply.header, reply_size).is_err() { println!("Failed to send response to driver"); } } @@ -166,10 +171,7 @@ impl WindowsDriverInterface { // Dispatch the request let result = match request.operation { KeysasFilterOperation::ScanFile | KeysasFilterOperation::UserAllowFile => { - match authorize_file(request.operation, &content) { - Ok(true) => true, - _ => false, - } + matches!(authorize_file(request.operation, &content), Ok(true)) } KeysasFilterOperation::ScanUsb => true /*match authorize_usb(&content) { Ok(true) => true, @@ -189,7 +191,7 @@ impl WindowsDriverInterface { }; unsafe { - if let Err(_) = FilterReplyMessage(handle, &reply.header, reply_size) { + if FilterReplyMessage(handle, &reply.header, reply_size).is_err() { println!("Failed to send response to driver"); continue; } @@ -207,15 +209,20 @@ impl WindowsDriverInterface { } } +/// Check a USB device to allow it not +/// Return Ok(true) or Ok(false) according to the authorization +/// +/// # Arguments +/// +/// * 'content' - Content of the request from the driver fn authorize_usb(content: &str) -> Result { println!("Received USB scan request: {:?}", content); - let mut device = HANDLE::default(); let mut buffer: [u8; 4096] = [0; 4096]; let mut byte_read: u32 = 0; // Open the device on the first sector - unsafe { - device = match CreateFileA( + let device = unsafe { + match CreateFileA( s!("\\\\.\\D:"), 1179785u32, FILE_SHARE_READ | FILE_SHARE_WRITE, @@ -232,7 +239,7 @@ fn authorize_usb(content: &str) -> Result { return Err(anyhow!("Failed to open device")); } } - } + }; if device.is_invalid() { println!("Invalid device handle"); @@ -241,10 +248,8 @@ fn authorize_usb(content: &str) -> Result { let mut vde = VOLUME_DISK_EXTENTS::default(); let mut dw: u32 = 0; - let mut res = BOOL::from(false); - - unsafe { - res = DeviceIoControl( + match unsafe { + DeviceIoControl( device, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, None, @@ -253,7 +258,14 @@ fn authorize_usb(content: &str) -> Result { u32::try_from(size_of::())?, Some(&mut dw), None, - ); + ).as_bool() + } { + true => (), + false => { + println!("Failed to query device"); + return Err(anyhow!("Failed to query device")); + + } } let mut drive_path = String::from("\\\\.\\PhysicalDrive"); @@ -266,9 +278,8 @@ fn authorize_usb(content: &str) -> Result { println!("Physical Drive path windows: {:?}", drive_str.to_string()?); } - let mut handle_usb = HANDLE::default(); - unsafe { - handle_usb = match CreateFileA( + let handle_usb = unsafe { + match CreateFileA( drive_str, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, @@ -285,7 +296,7 @@ fn authorize_usb(content: &str) -> Result { return Err(anyhow!("Failed to open usb")); } } - } + }; if handle_usb.is_invalid() { println!("Invalid device usb handle"); @@ -294,17 +305,16 @@ fn authorize_usb(content: &str) -> Result { // Move the file pointer after the MBR table (512B) // and read the signature content - let mut read = BOOL::from(false); - unsafe { + let read = unsafe { //SetFilePointer(device, 512, None, FILE_BEGIN); - read = ReadFile( + ReadFile( handle_usb, Some(buffer.as_mut_ptr() as *mut c_void), 4096, Some(&mut byte_read), None, - ); - } + ) + }; if read.as_bool() { println!("Device content: {:?}", buffer); @@ -319,8 +329,19 @@ fn authorize_usb(content: &str) -> Result { Ok(true) } -fn authorize_file(op: KeysasFilterOperation, content: &str) -> Result { - let mut file_path = Path::new(content.trim_matches(char::from(0))); +/// Decide to authorize a file +/// Start by whitelisting file that belongs to Windows and remove directories +/// Then try to validate it with a station report +/// Finaly if it fails ask the user to validate it manualy +/// +/// USB_op will be used to apply a device wide filter policy +/// +/// # Arguments +/// +/// * 'usb_op' - Device wide filtering policy +/// * 'content' - Content of the driver request, it contains the path to the file +fn authorize_file(_usb_op: KeysasFilterOperation, content: &str) -> Result { + let file_path = Path::new(content.trim_matches(char::from(0))); // Try to get the parent directory let mut components = file_path.components(); @@ -329,7 +350,7 @@ fn authorize_file(op: KeysasFilterOperation, content: &str) -> Result Result Result { // Test if the file is a station report if Path::new(path) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("krp")) { - // If yes validate it alone - if let Err(e) = parse_report(Path::new(path), None, None, None) { - println!("Failed to parse report: {e}"); - return Ok(false); + // Try to find the corresponding file + let mut file_path = path.to_path_buf(); + // file_path.file_name should not be None at this point + file_path.set_extension(""); + + match file_path.is_file() { + true => { + // If it exists, validate both + match parse_report(Path::new(path), Some(&file_path), None, None) { + Ok(_) => return Ok(true), + Err(e) => { + println!("Failed to parse report: {e}"); + return Ok(false); + } + } + }, + false => { + // If no corresponding file validate it alone + match parse_report(Path::new(path), None, None, None) { + Ok(_) => return Ok(true), + Err(e) => { + println!("Failed to parse report: {e}"); + return Ok(false); + } + } + } } - return Ok(true); } // If not try to find the corresponding report @@ -391,43 +444,48 @@ fn validate_file(path: &Path) -> Result { println!("Failed to parse file and report: {e}"); return Ok(false); } - return Ok(true); + Ok(true) } false => { // There is no corresponding report for validating the file println!("No report found at {:?}", path_report); - return Ok(false); + Ok(false) } } } +/// Spawn a dialog box to ask the user to validate a file or not +/// Return Ok(true) or Ok(false) accordingly +/// +/// # Arguments +/// +/// * 'path' - Path to the file fn user_authorize_file(path: &Path) -> Result { // Find authorization status for the file - let mut authorization_status = MESSAGEBOX_RESULT::default(); let auth_request = format!("Allow file: {:?}", path.as_os_str()); let (auth_request_ptr, _, _) = auth_request.into_raw_parts(); - unsafe { - authorization_status = MessageBoxA( + let authorization_status = unsafe { + MessageBoxA( None, PCSTR::from_raw(auth_request_ptr), s!("Keysas USB Filter"), MB_YESNO | MB_ICONWARNING | MB_SYSTEMMODAL, - ); - } + ) + }; match authorization_status { IDYES => { - return Ok(true); + Ok(true) } IDNO => { - return Ok(false); + Ok(false) } _ => { - return Err(anyhow!(format!( + Err(anyhow!(format!( "Unknown Authorization: {:?}", authorization_status - ))); + ))) } - }; + } } diff --git a/keysas-usbfilter/minifilter/keysasFile.c b/keysas-usbfilter/minifilter/keysasFile.c index 7999701..d10a555 100644 --- a/keysas-usbfilter/minifilter/keysasFile.c +++ b/keysas-usbfilter/minifilter/keysasFile.c @@ -223,8 +223,8 @@ Return Value: { NTSTATUS status = STATUS_SUCCESS; NTSTATUS result = FLT_PREOP_SUCCESS_WITH_CALLBACK; - PFLT_FILE_NAME_INFORMATION nameInfo = NULL; PKEYSAS_INSTANCE_CTX instanceContext = NULL; + BOOLEAN isDirectory = FALSE; UNREFERENCED_PARAMETER(FltObjects); UNREFERENCED_PARAMETER(CompletionContext = NULL); @@ -236,7 +236,16 @@ Return Value: return FLT_PREOP_SUCCESS_NO_CALLBACK; } - // Don't filter call to directories + // Don't filter call to directories or volumes + status = FltIsDirectory(Data->Iopb->TargetFileObject, FltObjects->Instance, &isDirectory); + + if (((Data->Iopb->TargetFileObject->Flags & FO_VOLUME_OPEN) == TRUE) || + ((Data->Iopb->TargetFileObject->FileName.Length == 0) && (Data->Iopb->TargetFileObject->RelatedFileObject == NULL)) || + isDirectory) + { + return FLT_PREOP_SUCCESS_NO_CALLBACK; + } + if (FlagOn(Data->Iopb->Parameters.Create.Options, FILE_DIRECTORY_FILE)) { return FLT_PREOP_SUCCESS_NO_CALLBACK; } @@ -286,28 +295,6 @@ Return Value: ReleaseResource(instanceContext->Resource); FltReleaseContext(instanceContext); - // Check if the file is of interest - status = FltGetFileNameInformation( - Data, - FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, - &nameInfo - ); - if (!NT_SUCCESS(status)) { - KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas!KfPreCreateHandler: FltGetFileNameInformation failed with status: %0x8x\n", - status)); - return FLT_PREOP_SUCCESS_NO_CALLBACK; - } - - FltParseFileNameInformation(nameInfo); - - if (0 == nameInfo->FinalComponent.Length) { - // Not a file but a directory - // No need to intercept POST operation - result = FLT_PREOP_SUCCESS_NO_CALLBACK; - } - - FltReleaseFileNameInformation(nameInfo); - return result; } From d687434eda7ee45e5077b07d55c8260836009be7 Mon Sep 17 00:00:00 2001 From: Luc Date: Tue, 9 May 2023 08:11:42 +0200 Subject: [PATCH 060/160] Update windows GUI --- .gitignore | 1 + keysas-admin/src-tauri/Cargo.toml | 10 +- keysas-backend/Cargo.toml | 8 +- keysas-core/Cargo.toml | 4 +- keysas-io/Cargo.toml | 10 +- keysas-sign/Cargo.toml | 4 +- keysas-usbfilter/tray-app/package-lock.json | 1302 ++++++++++++++++- keysas-usbfilter/tray-app/package.json | 7 +- .../tray-app/src-tauri/Cargo.toml | 3 +- .../tray-app/src-tauri/src/app_controler.rs | 2 - .../tray-app/src-tauri/src/main.rs | 121 +- .../tray-app/src-tauri/tauri.conf.json | 8 +- keysas-usbfilter/tray-app/src/App.vue | 133 +- keysas-usbfilter/tray-app/src/USBDetails.vue | 44 - keysas-usbfilter/tray-app/src/usb_details.ts | 5 - keysas-usbfilter/tray-app/usb_details.html | 14 - keysas_lib/Cargo.toml | 8 +- 17 files changed, 1454 insertions(+), 230 deletions(-) delete mode 100644 keysas-usbfilter/tray-app/src/USBDetails.vue delete mode 100644 keysas-usbfilter/tray-app/src/usb_details.ts delete mode 100644 keysas-usbfilter/tray-app/usb_details.html diff --git a/.gitignore b/.gitignore index 0cb4aad..497d71b 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ keysas-usbfilter/minifilter/runsdvui.cmd keysas-usbfilter/minifilter/SDV-default.xml keysas-usbfilter/minifilter/sdv-user.sdv keysas-usbfilter/minifilter/smvstats.txt +keysas-usbfilter/tray-app/package-lock.json diff --git a/keysas-admin/src-tauri/Cargo.toml b/keysas-admin/src-tauri/Cargo.toml index fe672ff..8accb99 100644 --- a/keysas-admin/src-tauri/Cargo.toml +++ b/keysas-admin/src-tauri/Cargo.toml @@ -12,18 +12,18 @@ rust-version = "1.57" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] -tauri-build = { version = "1.2.1", features = [] } +tauri-build = { version = "1.3.0", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.2.3", features = ["api-all", "updater"] } +tauri = { version = "1.2.5", features = ["api-all", "updater"] } ssh-rs = "0.3.2" anyhow = { version = "1.0", features = ["backtrace"] } async-std = "1.12" nom = "7.1" sha2 ="0.10" -regex="1.7" +regex="1.8" sqlite = "0.30" simple_logger = "4.1" log = "0.4" @@ -31,9 +31,9 @@ pkcs8 = {version = "0.10", features = ["encryption", "pem"] } x509-cert = "0.2" arbitrary = "1.3" ed25519-dalek = "1" -rand_dl = {package = "rand", version = "0.7"} +rand_dl = {package = "rand", version = "0.8"} rand_core = "0.6.4" -hex-literal = "0.3" +hex-literal = "0.4" libc = "0.2" udev = "0.7" mbrman = "0.5" diff --git a/keysas-backend/Cargo.toml b/keysas-backend/Cargo.toml index c1575b0..aa588a4 100644 --- a/keysas-backend/Cargo.toml +++ b/keysas-backend/Cargo.toml @@ -7,22 +7,22 @@ edition = "2021" [dependencies] -tungstenite = "0.18" +tungstenite = "0.19" serde = "1.0" serde_json = "1.0" serde_derive = "1.0" log = "0.4" anyhow = "1.0" udev = "0.7" -regex = "1.7" +regex = "1.8" libc = "0.2" nom = "7" minisign = "0.7" clap = { version = "4", default-features = false, features = ["std", "cargo"] } crossbeam-utils = "0.8" nix = "0.26" -tempfile = "3.3" -diacritics = "0.1" +tempfile = "3.5" +diacritics = "0.2" sys-mount = "2.0" proc-mounts = "0.3" hex = "0.4" diff --git a/keysas-core/Cargo.toml b/keysas-core/Cargo.toml index 80099bf..bc0990f 100644 --- a/keysas-core/Cargo.toml +++ b/keysas-core/Cargo.toml @@ -13,7 +13,7 @@ nix = "0.26" keysas_lib = { path = "../keysas_lib" } clap = { version = "4", default-features = false, features = ["std", "cargo"] } log = "0.4" -regex = "1.7" +regex = "1.8" infer = "0.13" clamav-tcp = "0.2" itertools ="0.10" @@ -26,7 +26,7 @@ pkcs8 = {version = "0.10", features = ["encryption", "pem"] } x509-cert = "0.2" landlock = "0.2" syscallz = "0.16" -yara = "0.17" +yara = "0.19" [dependencies.oqs] version = "0.7" diff --git a/keysas-io/Cargo.toml b/keysas-io/Cargo.toml index f5417b5..7f7369d 100644 --- a/keysas-io/Cargo.toml +++ b/keysas-io/Cargo.toml @@ -14,13 +14,15 @@ serde_derive = "1.0" log = "0.4" anyhow = "1.0" udev = "0.7" -regex = "1.7" -libc = "0.2" +regex = "1.8" +libc = "*" +nom = "7" +minisign = "0.7" clap = { version = "4", default-features = false, features = ["std", "cargo"] } crossbeam-utils = "0.8" nix = "0.26" -tempfile = "3.3" -diacritics = "0.1" +tempfile = "3.5" +diacritics = "0.2" sys-mount = "2.0" proc-mounts = "0.3" hex = "0.4" diff --git a/keysas-sign/Cargo.toml b/keysas-sign/Cargo.toml index 116480b..9cc1f65 100644 --- a/keysas-sign/Cargo.toml +++ b/keysas-sign/Cargo.toml @@ -11,5 +11,5 @@ anyhow = "1.0" ed25519-dalek = "1.0" pkcs8 = { version = "0.10", features = ["encryption", "pem"]} keysas_lib = { path = "../keysas_lib" } -tempfile = "3.4" -x509-cert = "0.2" \ No newline at end of file +tempfile = "3.5" +x509-cert = "0.2" diff --git a/keysas-usbfilter/tray-app/package-lock.json b/keysas-usbfilter/tray-app/package-lock.json index 97f6872..ac52dde 100644 --- a/keysas-usbfilter/tray-app/package-lock.json +++ b/keysas-usbfilter/tray-app/package-lock.json @@ -8,13 +8,14 @@ "name": "keysas-minifilter", "version": "0.0.0", "dependencies": { - "@tauri-apps/api": "^1.2.0", + "@tauri-apps/api": "^1.3.0", "bootstrap": "^5.2.3", "bootstrap-icons": "^1.10.5", - "vue": "^3.2.45" + "vue": "^3.2.45", + "vue-cli-plugin-tauri": "^1.0.0" }, "devDependencies": { - "@tauri-apps/cli": "^1.2.3", + "@tauri-apps/cli": "^1.3.0", "@types/node": "^18.7.10", "@vitejs/plugin-vue": "^4.0.0", "typescript": "^4.9.5", @@ -22,6 +23,51 @@ "vue-tsc": "^1.0.11" } }, + "node_modules/@achrinza/node-ipc": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@achrinza/node-ipc/-/node-ipc-9.2.2.tgz", + "integrity": "sha512-b90U39dx0cU6emsOvy5hxU4ApNXnE3+Tuo8XQZfiKTGelDwpMwBVgBP7QX6dGTcJgu/miyJuNJ/2naFBliNWEw==", + "dependencies": { + "@node-ipc/js-queue": "2.0.3", + "event-pubsub": "4.3.0", + "js-message": "1.0.7" + }, + "engines": { + "node": "8 || 10 || 12 || 14 || 16 || 17" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", @@ -385,6 +431,56 @@ "node": ">=12" } }, + "node_modules/@hapi/address": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", + "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==", + "deprecated": "Moved to 'npm install @sideway/address'" + }, + "node_modules/@hapi/bourne": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", + "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==", + "deprecated": "This version has been deprecated and is no longer supported or maintained" + }, + "node_modules/@hapi/hoek": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", + "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==", + "deprecated": "This version has been deprecated and is no longer supported or maintained" + }, + "node_modules/@hapi/joi": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", + "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", + "deprecated": "Switch to 'npm install joi'", + "dependencies": { + "@hapi/address": "2.x.x", + "@hapi/bourne": "1.x.x", + "@hapi/hoek": "8.x.x", + "@hapi/topo": "3.x.x" + } + }, + "node_modules/@hapi/topo": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", + "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "deprecated": "This version has been deprecated and is no longer supported or maintained", + "dependencies": { + "@hapi/hoek": "^8.3.0" + } + }, + "node_modules/@node-ipc/js-queue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@node-ipc/js-queue/-/js-queue-2.0.3.tgz", + "integrity": "sha512-fL1wpr8hhD5gT2dA1qifeVaoDFlQR5es8tFuKqjHX+kdOtdNHnxkVZbtIrR2rxnMFvehkjaZRNV2H/gPXlb0hw==", + "dependencies": { + "easy-stack": "1.0.1" + }, + "engines": { + "node": ">=1.0.0" + } + }, "node_modules/@popperjs/core": { "version": "2.11.7", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", @@ -396,9 +492,9 @@ } }, "node_modules/@tauri-apps/api": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.2.0.tgz", - "integrity": "sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.3.0.tgz", + "integrity": "sha512-AH+3FonkKZNtfRtGrObY38PrzEj4d+1emCbwNGu0V2ENbXjlLHMZQlUh+Bhu/CRmjaIwZMGJ3yFvWaZZgTHoog==", "engines": { "node": ">= 14.6.0", "npm": ">= 6.6.0", @@ -410,10 +506,9 @@ } }, "node_modules/@tauri-apps/cli": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.2.3.tgz", - "integrity": "sha512-erxtXuPhMEGJPBtnhPILD4AjuT81GZsraqpFvXAmEJZ2p8P6t7MVBifCL8LznRknznM3jn90D3M8RNBP3wcXTw==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.3.0.tgz", + "integrity": "sha512-H65YQQkE6SBTQ+KlqTmzx7oCL/2p36v2jPFVNHBhZ5EN7g0VLYmImh9TFcB/QsO2aT+sVlRZSmTpL3R0Iiu8pA==", "bin": { "tauri": "tauri.js" }, @@ -425,25 +520,24 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "1.2.3", - "@tauri-apps/cli-darwin-x64": "1.2.3", - "@tauri-apps/cli-linux-arm-gnueabihf": "1.2.3", - "@tauri-apps/cli-linux-arm64-gnu": "1.2.3", - "@tauri-apps/cli-linux-arm64-musl": "1.2.3", - "@tauri-apps/cli-linux-x64-gnu": "1.2.3", - "@tauri-apps/cli-linux-x64-musl": "1.2.3", - "@tauri-apps/cli-win32-ia32-msvc": "1.2.3", - "@tauri-apps/cli-win32-x64-msvc": "1.2.3" + "@tauri-apps/cli-darwin-arm64": "1.3.0", + "@tauri-apps/cli-darwin-x64": "1.3.0", + "@tauri-apps/cli-linux-arm-gnueabihf": "1.3.0", + "@tauri-apps/cli-linux-arm64-gnu": "1.3.0", + "@tauri-apps/cli-linux-arm64-musl": "1.3.0", + "@tauri-apps/cli-linux-x64-gnu": "1.3.0", + "@tauri-apps/cli-linux-x64-musl": "1.3.0", + "@tauri-apps/cli-win32-ia32-msvc": "1.3.0", + "@tauri-apps/cli-win32-x64-msvc": "1.3.0" } }, "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-phJN3fN8FtZZwqXg08bcxfq1+X1JSDglLvRxOxB7VWPq+O5SuB8uLyssjJsu+PIhyZZnIhTGdjhzLSFhSXfLsw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-uuhx3/LaqFyHkoGOnOltBLKWGOzC6WzdXu+/Qv3NmNnyQWkY7O34z5V0oP6ibfuiOBZufKjOuBR+8YAIR8Qh9Q==", "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -453,13 +547,12 @@ } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.2.3.tgz", - "integrity": "sha512-jFZ/y6z8z6v4yliIbXKBXA7BJgtZVMsITmEXSuD6s5+eCOpDhQxbRkr6CA+FFfr+/r96rWSDSgDenDQuSvPAKw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.3.0.tgz", + "integrity": "sha512-fj0VXHMDvb/H1CjaS/JoYd7xcourxndJn1IyM4afYbpXibT/fpmM6uZflDI6rRa220NfnBtQvy+asgwC9wuyLA==", "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -469,13 +562,12 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.2.3.tgz", - "integrity": "sha512-C7h5vqAwXzY0kRGSU00Fj8PudiDWFCiQqqUNI1N+fhCILrzWZB9TPBwdx33ZfXKt/U4+emdIoo/N34v3TiAOmQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.3.0.tgz", + "integrity": "sha512-f80DmFPnH5ZskG61KIlAyMVk9YkrTq0XM2uiQjOo5gToIdJidSwhPQVeBLv+7UxhqaRBx082Dg2fOkWlO3LiOQ==", "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -485,13 +577,12 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.2.3.tgz", - "integrity": "sha512-buf1c8sdkuUzVDkGPQpyUdAIIdn5r0UgXU6+H5fGPq/Xzt5K69JzXaeo6fHsZEZghbV0hOK+taKV4J0m30UUMQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.3.0.tgz", + "integrity": "sha512-s6/OByuGoppoUSnOXv/b6Oe6cVFk2w/KHs19aJJpo9ov/dUAA1w9wXlXu2l6sOFGsu/plaVomF2cw3iAQmaUCQ==", "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -501,13 +592,12 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.2.3.tgz", - "integrity": "sha512-x88wPS9W5xAyk392vc4uNHcKBBvCp0wf4H9JFMF9OBwB7vfd59LbQCFcPSu8f0BI7bPrOsyHqspWHuFL8ojQEA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.3.0.tgz", + "integrity": "sha512-yZfZAW4BG92cynL/D4wdrwBAl2oekRwiZnU5CM8k5yncalVEL0tyzuxQjjqbqrtDcw0rdkoBWrhFd+EB89vQaQ==", "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -517,13 +607,12 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.2.3.tgz", - "integrity": "sha512-ZMz1jxEVe0B4/7NJnlPHmwmSIuwiD6ViXKs8F+OWWz2Y4jn5TGxWKFg7DLx5OwQTRvEIZxxT7lXHi5CuTNAxKg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.3.0.tgz", + "integrity": "sha512-K3KRWSGKh7DTBr/ZKgWzeNX1Vdgx1ZBlUJXsm72R0Hb+93fDEp3TWgiwVkxqecB4aNWJhJsDcvRHuxw1G8xPlA==", "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -533,13 +622,12 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.2.3.tgz", - "integrity": "sha512-B/az59EjJhdbZDzawEVox0LQu2ZHCZlk8rJf85AMIktIUoAZPFbwyiUv7/zjzA/sY6Nb58OSJgaPL2/IBy7E0A==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.3.0.tgz", + "integrity": "sha512-99bVHqL1EtF7oESrlmEb5BWJsMNQ2ha70gesZhaVO2qI9Vg089XvrFZWC+aGiUsXNFrOw270+D9DKn03xO5+Zg==", "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -549,13 +637,12 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.2.3.tgz", - "integrity": "sha512-ypdO1OdC5ugNJAKO2m3sb1nsd+0TSvMS9Tr5qN/ZSMvtSduaNwrcZ3D7G/iOIanrqu/Nl8t3LYlgPZGBKlw7Ng==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.3.0.tgz", + "integrity": "sha512-ckBUTqXXdnCiYyf2xvxiuqiKZurg7ET++f6yzfvYa+gofd5dagQJkGLlkIg2pJ2c8mhEG1Cfk1vxWPqqGfN2GQ==", "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -565,13 +652,12 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.2.3.tgz", - "integrity": "sha512-CsbHQ+XhnV/2csOBBDVfH16cdK00gNyNYUW68isedmqcn8j+s0e9cQ1xXIqi+Hue3awp8g3ImYN5KPepf3UExw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.3.0.tgz", + "integrity": "sha512-vQ4wqRE0aziyRxgHIOLudGuxx4wETvFnmMvDBaNJRRrZQPlkOKnRxrvj1rNnI1845BdzSbDF4p7JDcFzToAfXA==", "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -586,6 +672,11 @@ "integrity": "sha512-Wha1UwsB3CYdqUm2PPzh/1gujGCNtWVUYF0mB00fJFoR4gTyWTDPjSm+zBF787Ahw8vSGgBja90MkgFwvB86Dg==", "dev": true }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" + }, "node_modules/@vitejs/plugin-vue": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.1.0.tgz", @@ -653,6 +744,25 @@ "@volar/vue-language-core": "1.2.0" } }, + "node_modules/@vue/cli-shared-utils": { + "version": "4.5.19", + "resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.19.tgz", + "integrity": "sha512-JYpdsrC/d9elerKxbEUtmSSU6QRM60rirVubOewECHkBHj+tLNznWq/EhCjswywtePyLaMUK25eTqnTSZlEE+g==", + "dependencies": { + "@achrinza/node-ipc": "9.2.2", + "@hapi/joi": "^15.0.1", + "chalk": "^2.4.2", + "execa": "^1.0.0", + "launch-editor": "^2.2.1", + "lru-cache": "^5.1.1", + "open": "^6.3.0", + "ora": "^3.4.0", + "read-pkg": "^5.1.1", + "request": "^2.88.2", + "semver": "^6.1.0", + "strip-ansi": "^6.0.0" + } + }, "node_modules/@vue/compiler-core": { "version": "3.2.47", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.47.tgz", @@ -755,12 +865,88 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz", "integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==" }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/bootstrap": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz", @@ -803,17 +989,180 @@ "balanced-match": "^1.0.0" } }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", + "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/csstype": { "version": "2.6.21", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", "dev": true }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/easy-stack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz", + "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.17.17", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.17.tgz", @@ -851,11 +1200,88 @@ "@esbuild/win32-x64": "0.17.17" } }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, + "node_modules/event-pubsub": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -870,6 +1296,70 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -879,6 +1369,157 @@ "he": "bin/he" } }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "node_modules/js-message": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", + "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -887,6 +1528,33 @@ "sourcemap-codec": "^1.4.8" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", @@ -925,6 +1593,157 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "dependencies": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -957,6 +1776,109 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/rollup": { "version": "3.20.6", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.6.tgz", @@ -973,6 +1895,70 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -995,6 +1981,135 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==" + }, + "node_modules/sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "engines": { + "node": ">=8" + } + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -1008,6 +2123,45 @@ "node": ">=4.2.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/vite": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.0.tgz", @@ -1068,6 +2222,15 @@ "@vue/shared": "3.2.47" } }, + "node_modules/vue-cli-plugin-tauri": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vue-cli-plugin-tauri/-/vue-cli-plugin-tauri-1.0.0.tgz", + "integrity": "sha512-OGTvXOoFeIpwq87/kSxTyZH9uPc573GyJU06s7/1+xUWfCqh1zMhPdDzoV3TKwOr6OreyakqPiltDrYaMIT4rQ==", + "dependencies": { + "@tauri-apps/cli": "^1.0.3", + "@vue/cli-shared-utils": "^4.1.1" + } + }, "node_modules/vue-template-compiler": { "version": "2.7.14", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", @@ -1093,6 +2256,35 @@ "peerDependencies": { "typescript": "*" } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } } diff --git a/keysas-usbfilter/tray-app/package.json b/keysas-usbfilter/tray-app/package.json index a3164db..da477a4 100644 --- a/keysas-usbfilter/tray-app/package.json +++ b/keysas-usbfilter/tray-app/package.json @@ -10,13 +10,14 @@ "tauri": "tauri" }, "dependencies": { - "@tauri-apps/api": "^1.2.0", + "@tauri-apps/api": "^1.3.0", "bootstrap": "^5.2.3", "bootstrap-icons": "^1.10.5", - "vue": "^3.2.45" + "vue": "^3.2.45", + "vue-cli-plugin-tauri": "^1.0.0" }, "devDependencies": { - "@tauri-apps/cli": "^1.2.3", + "@tauri-apps/cli": "^1.3.0", "@types/node": "^18.7.10", "@vitejs/plugin-vue": "^4.0.0", "typescript": "^4.9.5", diff --git a/keysas-usbfilter/tray-app/src-tauri/Cargo.toml b/keysas-usbfilter/tray-app/src-tauri/Cargo.toml index 6ecfef2..a6bf3c2 100644 --- a/keysas-usbfilter/tray-app/src-tauri/Cargo.toml +++ b/keysas-usbfilter/tray-app/src-tauri/Cargo.toml @@ -14,11 +14,12 @@ edition = "2021" tauri-build = { version = "1.2", features = [] } [dependencies] -tauri = { version = "1.2", features = ["shell-open", "system-tray"] } +tauri = { version = "1.2.5", features = ["notification-all", "shell-open", "system-tray"] } tauri-plugin-positioner = {version = "1.0", features = ["system-tray"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" anyhow = "1.0" +# Bumped down simple logger version to avoid conflict with time dependency of tauri simple_logger = "4.1" log = "0.4" tokio = {version = "1.28", features = ["net", "time"] } diff --git a/keysas-usbfilter/tray-app/src-tauri/src/app_controler.rs b/keysas-usbfilter/tray-app/src-tauri/src/app_controler.rs index bcbbdbd..591f3b2 100644 --- a/keysas-usbfilter/tray-app/src-tauri/src/app_controler.rs +++ b/keysas-usbfilter/tray-app/src-tauri/src/app_controler.rs @@ -25,8 +25,6 @@ use crate::filter_store::{FilterStore, KeysasAuthorization, USBDevice}; -use tauri::App; - pub struct AppControler { pub store: FilterStore } diff --git a/keysas-usbfilter/tray-app/src-tauri/src/main.rs b/keysas-usbfilter/tray-app/src-tauri/src/main.rs index 521a6f1..d2108bb 100644 --- a/keysas-usbfilter/tray-app/src-tauri/src/main.rs +++ b/keysas-usbfilter/tray-app/src-tauri/src/main.rs @@ -30,93 +30,51 @@ mod app_controler; mod filter_store; use tauri::{ - App, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, + AppHandle, Manager, SystemTray, SystemTrayEvent, State }; use tauri_plugin_positioner::{Position, WindowExt}; use crate::app_controler::AppControler; -/// Command call to open the USB device window -/// -/// # Arguments -/// -/// * 'app' - Handle to the tauri app, supplied by tauri -/// * 'name' - Name of the USB device selected, supplied by the frontend -#[tauri::command] -fn show_usb_device(app: tauri::AppHandle, name: &str) { - tauri::WindowBuilder::new( - &app, - "usbDetails", - tauri::WindowUrl::App("usb_details.html".into()) - ).build().unwrap(); +use anyhow::anyhow; + +/// Payload for the init event sent to the usb_details window +#[derive(Clone, serde::Serialize)] +struct InitPayload { + /// Name of the USB device + usb_name: String } fn main() -> Result<(), anyhow::Error> { + // Initialize the logger + simple_logger::init()?; + // Launch the tauri application init_tauri()?; Ok(()) } -// Initialize the tauri application as a system tray app +/// Initialize the tauri application as a system tray app fn init_tauri() -> Result<(), anyhow::Error> { - let quit = CustomMenuItem::new("quit".to_string(), "Quit"); - let hide = CustomMenuItem::new("hide".to_string(), "Hide"); - let tray_menu = SystemTrayMenu::new() - .add_item(quit) - .add_native_item(SystemTrayMenuItem::Separator) - .add_item(hide); - let tray = SystemTray::new().with_menu(tray_menu); - let app = tauri::Builder::default() .plugin(tauri_plugin_positioner::init()) .manage(AppControler::init()) - .invoke_handler(tauri::generate_handler![show_usb_device]) - .system_tray(tray) + .system_tray(SystemTray::new()) .on_system_tray_event(|app, event| match event { SystemTrayEvent::LeftClick { position: _, size: _, .. } => { - println!("Left click event"); - let window = app.get_window("main").unwrap(); - match window.is_visible() { - Ok(false) => { - window.move_window(Position::BottomRight); - window.show(); - } - Ok(true) => { - window.hide(); - } - _ => {} + if let Err(e) = open_usb_view(app) { + log::error!("Failed to open main view: {e}"); + app.exit(1); } } - SystemTrayEvent::RightClick { - position: _, - size: _, - .. - } => { - println!("Right click event"); - } - SystemTrayEvent::DoubleClick { - position: _, - size: _, - .. - } => { - println!("Double click event"); - } - SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { - "quit" => { - println!("Quit selected"); - } - "hide" => { - println!("Hide selected"); - } - _ => {} - }, _ => {} }) + .invoke_handler(tauri::generate_handler![get_file_list]) .build(tauri::generate_context!())?; app.run(|_app_handle, event| match event { @@ -128,3 +86,48 @@ fn init_tauri() -> Result<(), anyhow::Error> { Ok(()) } + +/// Toggle the USB view when the tray icon is clicked +/// +/// # Arguments +/// +/// * 'app' - The tauri application +fn open_usb_view(app: &AppHandle) -> Result<(), anyhow::Error> { + // Get the window + match app.get_window("main") { + Some(w) => { + // If the window exists, toggle its visibility + match w.is_visible()? { + false => { + w.move_window(Position::BottomRight)?; + w.set_focus()?; + w.show()?; + } + true => { + w.hide()?; + } + } + }, + None => { + // If the window does not exists, create a new one + let w = tauri::WindowBuilder::new( + app, + "main", + tauri::WindowUrl::App("index.html".into()) + ).build()?; + w.move_window(Position::BottomRight)?; + w.set_decorations(false)?; + w.set_focus()?; + } + }; + + Ok(()) +} + +#[tauri::command] +fn get_file_list(usb_name: String, app_ctrl: State) -> Result { + match app_ctrl.store.get_files(&usb_name) { + Ok(files) => Ok(String::from("Found files")), + Err(e) => Err(String::from("Failed to get files")) + } +} \ No newline at end of file diff --git a/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json b/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json index 52c31db..047f14c 100644 --- a/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json +++ b/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json @@ -12,6 +12,9 @@ }, "tauri": { "allowlist": { + "notification": { + "all": true + }, "all": false, "shell": { "all": false, @@ -46,7 +49,10 @@ "label": "main", "title": "Keysas USB Firewall", "url": "index.html", - "visible": false + "visible": false, + "decorations": false, + "width": 600, + "height": 400 } ] } diff --git a/keysas-usbfilter/tray-app/src/App.vue b/keysas-usbfilter/tray-app/src/App.vue index def61bc..61bb4e9 100644 --- a/keysas-usbfilter/tray-app/src/App.vue +++ b/keysas-usbfilter/tray-app/src/App.vue @@ -2,43 +2,126 @@ import "bootstrap/dist/css/bootstrap.min.css" import "bootstrap" import 'bootstrap-icons/font/bootstrap-icons.css' - -import {invoke} from "@tauri-apps/api" - -let usb_list: {name: string, path: string}[] = [ - {"name": "Kingstong USB", "path": "D:"} -]; - -function showUsbDevice(name: string) { - invoke('show_usb_device', {name: name}); -} + + diff --git a/keysas-usbfilter/tray-app/src/USBDetails.vue b/keysas-usbfilter/tray-app/src/USBDetails.vue deleted file mode 100644 index 70128a9..0000000 --- a/keysas-usbfilter/tray-app/src/USBDetails.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - - - \ No newline at end of file diff --git a/keysas-usbfilter/tray-app/src/usb_details.ts b/keysas-usbfilter/tray-app/src/usb_details.ts deleted file mode 100644 index 3f96bca..0000000 --- a/keysas-usbfilter/tray-app/src/usb_details.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createApp } from "vue"; -import "./styles.css"; -import UsbApp from "./USBDetails.vue"; - -createApp(UsbApp).mount("#usb_details"); diff --git a/keysas-usbfilter/tray-app/usb_details.html b/keysas-usbfilter/tray-app/usb_details.html deleted file mode 100644 index 52e2b3e..0000000 --- a/keysas-usbfilter/tray-app/usb_details.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - Keysas USB Firewall - - - -
    - - - \ No newline at end of file diff --git a/keysas_lib/Cargo.toml b/keysas_lib/Cargo.toml index 8de40e8..34a0206 100644 --- a/keysas_lib/Cargo.toml +++ b/keysas_lib/Cargo.toml @@ -8,19 +8,19 @@ edition = "2021" [dependencies] sha2 = "0.10" anyhow = "1.0" -clap = "4.1" +clap = "4.2" walkdir = "2.3" -regex ="1.7.1" +regex ="1.8.1" simple_logger = "4.1" log = "0.4" pkcs8 = {version = "0.10", default-features = false, features = ["encryption", "pem"] } x509-cert = "0.2" arbitrary = "1.3" ed25519-dalek = "1" -rand_dl = {package = "rand", version = "0.7"} +rand_dl = {package = "rand", version = "0.8"} rand_core = "0.6.4" hex-literal = "0.4" -tempfile = "3.4" +tempfile = "3.5" der = "0.7" serde_derive = "1.0" serde = "1.0" From 7c1a90d794a2e259a72492fcdb2c1333a0025683 Mon Sep 17 00:00:00 2001 From: Luc Date: Tue, 9 May 2023 09:26:37 +0200 Subject: [PATCH 061/160] Fixed dalek version --- keysas_lib/Cargo.toml | 2 +- keysas_lib/src/certificate_field.rs | 1 - keysas_lib/src/keysas_key.rs | 2 ++ 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/keysas_lib/Cargo.toml b/keysas_lib/Cargo.toml index 34a0206..cf4bdde 100644 --- a/keysas_lib/Cargo.toml +++ b/keysas_lib/Cargo.toml @@ -17,7 +17,7 @@ pkcs8 = {version = "0.10", default-features = false, features = ["encryption", " x509-cert = "0.2" arbitrary = "1.3" ed25519-dalek = "1" -rand_dl = {package = "rand", version = "0.8"} +rand_dl = {package = "rand", version = "0.7"} rand_core = "0.6.4" hex-literal = "0.4" tempfile = "3.5" diff --git a/keysas_lib/src/certificate_field.rs b/keysas_lib/src/certificate_field.rs index 38e75d9..5b30fd8 100644 --- a/keysas_lib/src/certificate_field.rs +++ b/keysas_lib/src/certificate_field.rs @@ -35,7 +35,6 @@ use pkcs8::der::asn1::OctetString; use pkcs8::der::oid::db::rfc5280; use pkcs8::der::DecodePem; use pkcs8::der::Encode; -use serde::Serialize; use std::time::Duration; use x509_cert::attr::AttributeTypeAndValue; use x509_cert::certificate::*; diff --git a/keysas_lib/src/keysas_key.rs b/keysas_lib/src/keysas_key.rs index 2775ddc..a86ef2f 100644 --- a/keysas_lib/src/keysas_key.rs +++ b/keysas_lib/src/keysas_key.rs @@ -33,6 +33,8 @@ use ed25519_dalek::Sha512; use ed25519_dalek::Signature as SignatureDalek; use ed25519_dalek::Verifier; use ed25519_dalek::Signer; +use ed25519_dalek::Signature as SignatureDalek; +use ed25519_dalek::Verifier; use oqs::sig::Algorithm; use oqs::sig::PublicKey as PqPublicKey; use oqs::sig::SecretKey; From 3d35edfd1179d657d9de87e0e1a394e5b5c3ba37 Mon Sep 17 00:00:00 2001 From: Luc Date: Tue, 9 May 2023 09:36:04 +0200 Subject: [PATCH 062/160] Update keysas-lib dependencies versions --- keysas_lib/Cargo.toml | 4 ++-- keysas_lib/src/keysas_key.rs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/keysas_lib/Cargo.toml b/keysas_lib/Cargo.toml index cf4bdde..af531b3 100644 --- a/keysas_lib/Cargo.toml +++ b/keysas_lib/Cargo.toml @@ -10,7 +10,7 @@ sha2 = "0.10" anyhow = "1.0" clap = "4.2" walkdir = "2.3" -regex ="1.8.1" +regex ="1.8" simple_logger = "4.1" log = "0.4" pkcs8 = {version = "0.10", default-features = false, features = ["encryption", "pem"] } @@ -18,7 +18,7 @@ x509-cert = "0.2" arbitrary = "1.3" ed25519-dalek = "1" rand_dl = {package = "rand", version = "0.7"} -rand_core = "0.6.4" +rand_core = "0.6" hex-literal = "0.4" tempfile = "3.5" der = "0.7" diff --git a/keysas_lib/src/keysas_key.rs b/keysas_lib/src/keysas_key.rs index a86ef2f..2775ddc 100644 --- a/keysas_lib/src/keysas_key.rs +++ b/keysas_lib/src/keysas_key.rs @@ -33,8 +33,6 @@ use ed25519_dalek::Sha512; use ed25519_dalek::Signature as SignatureDalek; use ed25519_dalek::Verifier; use ed25519_dalek::Signer; -use ed25519_dalek::Signature as SignatureDalek; -use ed25519_dalek::Verifier; use oqs::sig::Algorithm; use oqs::sig::PublicKey as PqPublicKey; use oqs::sig::SecretKey; From 11f8a81d17b7349beb15bce2ba9e9cb590058311 Mon Sep 17 00:00:00 2001 From: Luc Date: Tue, 9 May 2023 09:41:24 +0200 Subject: [PATCH 063/160] Renamed USB firewall folder --- .../daemon/Cargo.toml | 0 .../daemon/src/driver_interface.rs | 0 .../daemon/src/main.rs | 0 .../daemon/src/windows_driver_interface.rs | 0 .../minifilter/KeysasDriver.inf | 0 .../minifilter/KeysasDriver.vcxproj | 0 .../minifilter/KeysasDriver.vcxproj.filters | 0 .../minifilter/keysasCommunication.c | 0 .../minifilter/keysasCommunication.h | 0 .../minifilter/keysasDriver.c | 0 .../minifilter/keysasDriver.h | 0 .../minifilter/keysasFile.c | 0 .../minifilter/keysasFile.h | 0 .../minifilter/keysasInstance.c | 0 .../minifilter/keysasInstance.h | 0 .../minifilter/keysasUtils.c | 0 .../minifilter/keysasUtils.h | 0 .../tray-app/README.md | 0 .../tray-app/index.html | 0 .../tray-app/package-lock.json | 0 .../tray-app/package.json | 0 .../tray-app/public/logo-keysas-short-16.png | Bin .../tray-app/public/logo-keysas-short-256.png | Bin .../tray-app/public/logo-keysas-short-32.png | Bin .../tray-app/public/logo-keysas-short-48.png | Bin .../tray-app/public/logo-keysas-short.ico | Bin .../tray-app/src-tauri/.gitignore | 0 .../tray-app/src-tauri/Cargo.toml | 0 .../tray-app/src-tauri/build.rs | 0 .../src-tauri/icons/logo-keysas-short-16.png | Bin .../src-tauri/icons/logo-keysas-short-256.png | Bin .../src-tauri/icons/logo-keysas-short-32.png | Bin .../src-tauri/icons/logo-keysas-short-48.png | Bin .../tray-app/src-tauri/icons/logo-keysas-short.ico | Bin .../tray-app/src-tauri/icons/logo-keysas-short.png | Bin .../tray-app/src-tauri/src/app_controler.rs | 0 .../tray-app/src-tauri/src/filter_store.rs | 0 .../tray-app/src-tauri/src/main.rs | 0 .../tray-app/src-tauri/src/service_if.rs | 0 .../tray-app/src-tauri/tauri.conf.json | 0 .../tray-app/src/App.vue | 0 .../tray-app/src/assets/vue.svg | 0 .../tray-app/src/main.ts | 0 .../tray-app/src/styles.css | 0 .../tray-app/src/vite-env.d.ts | 0 .../tray-app/tsconfig.json | 0 .../tray-app/tsconfig.node.json | 0 .../tray-app/vite.config.ts | 0 48 files changed, 0 insertions(+), 0 deletions(-) rename {keysas-usbfilter => keysas-firewall}/daemon/Cargo.toml (100%) rename {keysas-usbfilter => keysas-firewall}/daemon/src/driver_interface.rs (100%) rename {keysas-usbfilter => keysas-firewall}/daemon/src/main.rs (100%) rename {keysas-usbfilter => keysas-firewall}/daemon/src/windows_driver_interface.rs (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/KeysasDriver.inf (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/KeysasDriver.vcxproj (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/KeysasDriver.vcxproj.filters (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/keysasCommunication.c (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/keysasCommunication.h (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/keysasDriver.c (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/keysasDriver.h (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/keysasFile.c (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/keysasFile.h (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/keysasInstance.c (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/keysasInstance.h (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/keysasUtils.c (100%) rename {keysas-usbfilter => keysas-firewall}/minifilter/keysasUtils.h (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/README.md (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/index.html (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/package-lock.json (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/package.json (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/public/logo-keysas-short-16.png (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/public/logo-keysas-short-256.png (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/public/logo-keysas-short-32.png (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/public/logo-keysas-short-48.png (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/public/logo-keysas-short.ico (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/.gitignore (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/Cargo.toml (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/build.rs (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/icons/logo-keysas-short-16.png (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/icons/logo-keysas-short-256.png (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/icons/logo-keysas-short-32.png (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/icons/logo-keysas-short-48.png (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/icons/logo-keysas-short.ico (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/icons/logo-keysas-short.png (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/src/app_controler.rs (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/src/filter_store.rs (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/src/main.rs (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/src/service_if.rs (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src-tauri/tauri.conf.json (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src/App.vue (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src/assets/vue.svg (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src/main.ts (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src/styles.css (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/src/vite-env.d.ts (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/tsconfig.json (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/tsconfig.node.json (100%) rename {keysas-usbfilter => keysas-firewall}/tray-app/vite.config.ts (100%) diff --git a/keysas-usbfilter/daemon/Cargo.toml b/keysas-firewall/daemon/Cargo.toml similarity index 100% rename from keysas-usbfilter/daemon/Cargo.toml rename to keysas-firewall/daemon/Cargo.toml diff --git a/keysas-usbfilter/daemon/src/driver_interface.rs b/keysas-firewall/daemon/src/driver_interface.rs similarity index 100% rename from keysas-usbfilter/daemon/src/driver_interface.rs rename to keysas-firewall/daemon/src/driver_interface.rs diff --git a/keysas-usbfilter/daemon/src/main.rs b/keysas-firewall/daemon/src/main.rs similarity index 100% rename from keysas-usbfilter/daemon/src/main.rs rename to keysas-firewall/daemon/src/main.rs diff --git a/keysas-usbfilter/daemon/src/windows_driver_interface.rs b/keysas-firewall/daemon/src/windows_driver_interface.rs similarity index 100% rename from keysas-usbfilter/daemon/src/windows_driver_interface.rs rename to keysas-firewall/daemon/src/windows_driver_interface.rs diff --git a/keysas-usbfilter/minifilter/KeysasDriver.inf b/keysas-firewall/minifilter/KeysasDriver.inf similarity index 100% rename from keysas-usbfilter/minifilter/KeysasDriver.inf rename to keysas-firewall/minifilter/KeysasDriver.inf diff --git a/keysas-usbfilter/minifilter/KeysasDriver.vcxproj b/keysas-firewall/minifilter/KeysasDriver.vcxproj similarity index 100% rename from keysas-usbfilter/minifilter/KeysasDriver.vcxproj rename to keysas-firewall/minifilter/KeysasDriver.vcxproj diff --git a/keysas-usbfilter/minifilter/KeysasDriver.vcxproj.filters b/keysas-firewall/minifilter/KeysasDriver.vcxproj.filters similarity index 100% rename from keysas-usbfilter/minifilter/KeysasDriver.vcxproj.filters rename to keysas-firewall/minifilter/KeysasDriver.vcxproj.filters diff --git a/keysas-usbfilter/minifilter/keysasCommunication.c b/keysas-firewall/minifilter/keysasCommunication.c similarity index 100% rename from keysas-usbfilter/minifilter/keysasCommunication.c rename to keysas-firewall/minifilter/keysasCommunication.c diff --git a/keysas-usbfilter/minifilter/keysasCommunication.h b/keysas-firewall/minifilter/keysasCommunication.h similarity index 100% rename from keysas-usbfilter/minifilter/keysasCommunication.h rename to keysas-firewall/minifilter/keysasCommunication.h diff --git a/keysas-usbfilter/minifilter/keysasDriver.c b/keysas-firewall/minifilter/keysasDriver.c similarity index 100% rename from keysas-usbfilter/minifilter/keysasDriver.c rename to keysas-firewall/minifilter/keysasDriver.c diff --git a/keysas-usbfilter/minifilter/keysasDriver.h b/keysas-firewall/minifilter/keysasDriver.h similarity index 100% rename from keysas-usbfilter/minifilter/keysasDriver.h rename to keysas-firewall/minifilter/keysasDriver.h diff --git a/keysas-usbfilter/minifilter/keysasFile.c b/keysas-firewall/minifilter/keysasFile.c similarity index 100% rename from keysas-usbfilter/minifilter/keysasFile.c rename to keysas-firewall/minifilter/keysasFile.c diff --git a/keysas-usbfilter/minifilter/keysasFile.h b/keysas-firewall/minifilter/keysasFile.h similarity index 100% rename from keysas-usbfilter/minifilter/keysasFile.h rename to keysas-firewall/minifilter/keysasFile.h diff --git a/keysas-usbfilter/minifilter/keysasInstance.c b/keysas-firewall/minifilter/keysasInstance.c similarity index 100% rename from keysas-usbfilter/minifilter/keysasInstance.c rename to keysas-firewall/minifilter/keysasInstance.c diff --git a/keysas-usbfilter/minifilter/keysasInstance.h b/keysas-firewall/minifilter/keysasInstance.h similarity index 100% rename from keysas-usbfilter/minifilter/keysasInstance.h rename to keysas-firewall/minifilter/keysasInstance.h diff --git a/keysas-usbfilter/minifilter/keysasUtils.c b/keysas-firewall/minifilter/keysasUtils.c similarity index 100% rename from keysas-usbfilter/minifilter/keysasUtils.c rename to keysas-firewall/minifilter/keysasUtils.c diff --git a/keysas-usbfilter/minifilter/keysasUtils.h b/keysas-firewall/minifilter/keysasUtils.h similarity index 100% rename from keysas-usbfilter/minifilter/keysasUtils.h rename to keysas-firewall/minifilter/keysasUtils.h diff --git a/keysas-usbfilter/tray-app/README.md b/keysas-firewall/tray-app/README.md similarity index 100% rename from keysas-usbfilter/tray-app/README.md rename to keysas-firewall/tray-app/README.md diff --git a/keysas-usbfilter/tray-app/index.html b/keysas-firewall/tray-app/index.html similarity index 100% rename from keysas-usbfilter/tray-app/index.html rename to keysas-firewall/tray-app/index.html diff --git a/keysas-usbfilter/tray-app/package-lock.json b/keysas-firewall/tray-app/package-lock.json similarity index 100% rename from keysas-usbfilter/tray-app/package-lock.json rename to keysas-firewall/tray-app/package-lock.json diff --git a/keysas-usbfilter/tray-app/package.json b/keysas-firewall/tray-app/package.json similarity index 100% rename from keysas-usbfilter/tray-app/package.json rename to keysas-firewall/tray-app/package.json diff --git a/keysas-usbfilter/tray-app/public/logo-keysas-short-16.png b/keysas-firewall/tray-app/public/logo-keysas-short-16.png similarity index 100% rename from keysas-usbfilter/tray-app/public/logo-keysas-short-16.png rename to keysas-firewall/tray-app/public/logo-keysas-short-16.png diff --git a/keysas-usbfilter/tray-app/public/logo-keysas-short-256.png b/keysas-firewall/tray-app/public/logo-keysas-short-256.png similarity index 100% rename from keysas-usbfilter/tray-app/public/logo-keysas-short-256.png rename to keysas-firewall/tray-app/public/logo-keysas-short-256.png diff --git a/keysas-usbfilter/tray-app/public/logo-keysas-short-32.png b/keysas-firewall/tray-app/public/logo-keysas-short-32.png similarity index 100% rename from keysas-usbfilter/tray-app/public/logo-keysas-short-32.png rename to keysas-firewall/tray-app/public/logo-keysas-short-32.png diff --git a/keysas-usbfilter/tray-app/public/logo-keysas-short-48.png b/keysas-firewall/tray-app/public/logo-keysas-short-48.png similarity index 100% rename from keysas-usbfilter/tray-app/public/logo-keysas-short-48.png rename to keysas-firewall/tray-app/public/logo-keysas-short-48.png diff --git a/keysas-usbfilter/tray-app/public/logo-keysas-short.ico b/keysas-firewall/tray-app/public/logo-keysas-short.ico similarity index 100% rename from keysas-usbfilter/tray-app/public/logo-keysas-short.ico rename to keysas-firewall/tray-app/public/logo-keysas-short.ico diff --git a/keysas-usbfilter/tray-app/src-tauri/.gitignore b/keysas-firewall/tray-app/src-tauri/.gitignore similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/.gitignore rename to keysas-firewall/tray-app/src-tauri/.gitignore diff --git a/keysas-usbfilter/tray-app/src-tauri/Cargo.toml b/keysas-firewall/tray-app/src-tauri/Cargo.toml similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/Cargo.toml rename to keysas-firewall/tray-app/src-tauri/Cargo.toml diff --git a/keysas-usbfilter/tray-app/src-tauri/build.rs b/keysas-firewall/tray-app/src-tauri/build.rs similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/build.rs rename to keysas-firewall/tray-app/src-tauri/build.rs diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short-16.png b/keysas-firewall/tray-app/src-tauri/icons/logo-keysas-short-16.png similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short-16.png rename to keysas-firewall/tray-app/src-tauri/icons/logo-keysas-short-16.png diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short-256.png b/keysas-firewall/tray-app/src-tauri/icons/logo-keysas-short-256.png similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short-256.png rename to keysas-firewall/tray-app/src-tauri/icons/logo-keysas-short-256.png diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short-32.png b/keysas-firewall/tray-app/src-tauri/icons/logo-keysas-short-32.png similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short-32.png rename to keysas-firewall/tray-app/src-tauri/icons/logo-keysas-short-32.png diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short-48.png b/keysas-firewall/tray-app/src-tauri/icons/logo-keysas-short-48.png similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short-48.png rename to keysas-firewall/tray-app/src-tauri/icons/logo-keysas-short-48.png diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short.ico b/keysas-firewall/tray-app/src-tauri/icons/logo-keysas-short.ico similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short.ico rename to keysas-firewall/tray-app/src-tauri/icons/logo-keysas-short.ico diff --git a/keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short.png b/keysas-firewall/tray-app/src-tauri/icons/logo-keysas-short.png similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/icons/logo-keysas-short.png rename to keysas-firewall/tray-app/src-tauri/icons/logo-keysas-short.png diff --git a/keysas-usbfilter/tray-app/src-tauri/src/app_controler.rs b/keysas-firewall/tray-app/src-tauri/src/app_controler.rs similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/src/app_controler.rs rename to keysas-firewall/tray-app/src-tauri/src/app_controler.rs diff --git a/keysas-usbfilter/tray-app/src-tauri/src/filter_store.rs b/keysas-firewall/tray-app/src-tauri/src/filter_store.rs similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/src/filter_store.rs rename to keysas-firewall/tray-app/src-tauri/src/filter_store.rs diff --git a/keysas-usbfilter/tray-app/src-tauri/src/main.rs b/keysas-firewall/tray-app/src-tauri/src/main.rs similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/src/main.rs rename to keysas-firewall/tray-app/src-tauri/src/main.rs diff --git a/keysas-usbfilter/tray-app/src-tauri/src/service_if.rs b/keysas-firewall/tray-app/src-tauri/src/service_if.rs similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/src/service_if.rs rename to keysas-firewall/tray-app/src-tauri/src/service_if.rs diff --git a/keysas-usbfilter/tray-app/src-tauri/tauri.conf.json b/keysas-firewall/tray-app/src-tauri/tauri.conf.json similarity index 100% rename from keysas-usbfilter/tray-app/src-tauri/tauri.conf.json rename to keysas-firewall/tray-app/src-tauri/tauri.conf.json diff --git a/keysas-usbfilter/tray-app/src/App.vue b/keysas-firewall/tray-app/src/App.vue similarity index 100% rename from keysas-usbfilter/tray-app/src/App.vue rename to keysas-firewall/tray-app/src/App.vue diff --git a/keysas-usbfilter/tray-app/src/assets/vue.svg b/keysas-firewall/tray-app/src/assets/vue.svg similarity index 100% rename from keysas-usbfilter/tray-app/src/assets/vue.svg rename to keysas-firewall/tray-app/src/assets/vue.svg diff --git a/keysas-usbfilter/tray-app/src/main.ts b/keysas-firewall/tray-app/src/main.ts similarity index 100% rename from keysas-usbfilter/tray-app/src/main.ts rename to keysas-firewall/tray-app/src/main.ts diff --git a/keysas-usbfilter/tray-app/src/styles.css b/keysas-firewall/tray-app/src/styles.css similarity index 100% rename from keysas-usbfilter/tray-app/src/styles.css rename to keysas-firewall/tray-app/src/styles.css diff --git a/keysas-usbfilter/tray-app/src/vite-env.d.ts b/keysas-firewall/tray-app/src/vite-env.d.ts similarity index 100% rename from keysas-usbfilter/tray-app/src/vite-env.d.ts rename to keysas-firewall/tray-app/src/vite-env.d.ts diff --git a/keysas-usbfilter/tray-app/tsconfig.json b/keysas-firewall/tray-app/tsconfig.json similarity index 100% rename from keysas-usbfilter/tray-app/tsconfig.json rename to keysas-firewall/tray-app/tsconfig.json diff --git a/keysas-usbfilter/tray-app/tsconfig.node.json b/keysas-firewall/tray-app/tsconfig.node.json similarity index 100% rename from keysas-usbfilter/tray-app/tsconfig.node.json rename to keysas-firewall/tray-app/tsconfig.node.json diff --git a/keysas-usbfilter/tray-app/vite.config.ts b/keysas-firewall/tray-app/vite.config.ts similarity index 100% rename from keysas-usbfilter/tray-app/vite.config.ts rename to keysas-firewall/tray-app/vite.config.ts From 8ff603ac128942f68cfd44da3e7ff23b6d41cb30 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Tue, 9 May 2023 10:17:36 +0200 Subject: [PATCH 064/160] Fix function signatures + tests --- keysas-core/src/keysas-out/main.rs | 12 ++---------- keysas-sign/src/main.rs | 2 +- keysas_lib/src/file_report.rs | 8 ++++---- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/keysas-core/src/keysas-out/main.rs b/keysas-core/src/keysas-out/main.rs index 17fa361..18862b5 100644 --- a/keysas-core/src/keysas-out/main.rs +++ b/keysas-core/src/keysas-out/main.rs @@ -62,12 +62,10 @@ #![feature(str_split_remainder)] use anyhow::Result; -use base64::{engine::general_purpose, Engine as _}; use clap::{crate_version, Arg, ArgAction, Command}; use keysas_lib::append_ext; use keysas_lib::init_logger; use keysas_lib::keysas_hybrid_keypair::HybridKeyPair; -use keysas_lib::keysas_key::KeysasKey; use keysas_lib::sha256_digest; use keysas_lib::file_report::FileMetadata; use keysas_lib::file_report::generate_report_metadata; @@ -79,8 +77,6 @@ use landlock::{ use log::{error, info, warn}; use nix::unistd; use pkcs8::der::EncodePem; -use sha2::Digest; -use sha2::Sha256; use std::fs::File; use std::io; use std::io::BufReader; @@ -92,10 +88,6 @@ use std::path::Path; use std::path::PathBuf; use std::process; use std::str; -use time::OffsetDateTime; - -#[macro_use] -extern crate serde_derive; /// Structure representing a file and its metadata in the daemon #[derive(Debug)] @@ -250,10 +242,10 @@ fn output_files( } // Generate a report - let report_meta = generate_report_metadata(&f); + let report_meta = generate_report_metadata(&f.md); // Bind the report to the file and sign it - let new_report = bind_and_sign(&f, &report_meta, sign_keys, sign_cert)?; + let new_report = bind_and_sign(&f.md, &report_meta, sign_keys, sign_cert)?; // Write the report to disk let mut path = PathBuf::new(); diff --git a/keysas-sign/src/main.rs b/keysas-sign/src/main.rs index d943c68..af46744 100644 --- a/keysas-sign/src/main.rs +++ b/keysas-sign/src/main.rs @@ -175,7 +175,7 @@ fn generate_signing_keypair( /// Save a certificate on the station fn save_certificate(cert_type: &str, cert: &str) -> Result<()> { - if validate_signing_certificate(cert).is_ok_and(|r| r) { + if validate_signing_certificate(cert, None).is_ok() { let path = match cert_type { "usb-cl" => USB_CERT_CL_PATH, "usb-pq" => USB_CERT_PQ_PATH, diff --git a/keysas_lib/src/file_report.rs b/keysas_lib/src/file_report.rs index 65f104e..8be0235 100644 --- a/keysas_lib/src/file_report.rs +++ b/keysas_lib/src/file_report.rs @@ -349,12 +349,12 @@ pub fn parse_report( mod tests_out { use base64::{engine::general_purpose, Engine}; use ed25519_dalek::{self, Digest, Sha512}; - use keysas_lib::{certificate_field::CertificateFields, keysas_hybrid_keypair::HybridKeyPair}; + use crate::{certificate_field::CertificateFields, keysas_hybrid_keypair::HybridKeyPair}; use oqs::sig::{Algorithm, Sig}; use pkcs8::der::{DecodePem, EncodePem}; use x509_cert::Certificate; - use crate::{bind_and_sign, generate_report_metadata, FileData, FileMetadata}; + use crate::file_report::{bind_and_sign, generate_report_metadata, FileMetadata}; #[test] fn test_metadata_valid_file() { @@ -379,8 +379,8 @@ mod tests_out { let meta = generate_report_metadata(&file_data); // Validate fields - assert_eq!(file_data.md.filename, meta.name); - assert_eq!(file_data.md.file_type, meta.file_type); + assert_eq!(file_data.filename, meta.name); + assert_eq!(file_data.file_type, meta.file_type); assert_eq!(meta.is_valid, true); } From cd1041ea2e8d6c3508a28f1598beb7613ce348b3 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Tue, 9 May 2023 13:31:58 +0200 Subject: [PATCH 065/160] BEGIN: Fixing frontend for reading reports --- keysas-frontend/package-lock.json | 554 +++++++----------- keysas-frontend/package.json | 2 +- keysas-frontend/src/App.vue | 4 +- keysas-frontend/src/components/AppGuichet.vue | 47 +- keysas-frontend/src/locales/en.json | 2 +- keysas-frontend/src/locales/fr.json | 2 +- 6 files changed, 267 insertions(+), 344 deletions(-) diff --git a/keysas-frontend/package-lock.json b/keysas-frontend/package-lock.json index e571778..a8ecfa7 100644 --- a/keysas-frontend/package-lock.json +++ b/keysas-frontend/package-lock.json @@ -18,7 +18,7 @@ "@rollup/plugin-alias": "^4.0.2", "@vitejs/plugin-vue": "^4.0.0", "sass": "^1.56.2", - "vite": "^4.0.0" + "vite": "^4.3.5" } }, "node_modules/@babel/parser": { @@ -33,9 +33,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.4.tgz", - "integrity": "sha512-rZzb7r22m20S1S7ufIc6DC6W659yxoOrl7sKP1nCYhuvUlnCFHVSbATG4keGUtV8rDz11sRRDbWkvQZpzPaHiw==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", + "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==", "cpu": [ "arm" ], @@ -49,9 +49,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.4.tgz", - "integrity": "sha512-VPuTzXFm/m2fcGfN6CiwZTlLzxrKsWbPkG7ArRFpuxyaHUm/XFHQPD4xNwZT6uUmpIHhnSjcaCmcla8COzmZ5Q==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz", + "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==", "cpu": [ "arm64" ], @@ -65,9 +65,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.4.tgz", - "integrity": "sha512-MW+B2O++BkcOfMWmuHXB15/l1i7wXhJFqbJhp82IBOais8RBEQv2vQz/jHrDEHaY2X0QY7Wfw86SBL2PbVOr0g==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz", + "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==", "cpu": [ "x64" ], @@ -81,9 +81,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.4.tgz", - "integrity": "sha512-a28X1O//aOfxwJVZVs7ZfM8Tyih2Za4nKJrBwW5Wm4yKsnwBy9aiS/xwpxiiTRttw3EaTg4Srerhcm6z0bu9Wg==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz", + "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==", "cpu": [ "arm64" ], @@ -97,9 +97,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.4.tgz", - "integrity": "sha512-e3doCr6Ecfwd7VzlaQqEPrnbvvPjE9uoTpxG5pyLzr2rI2NMjDHmvY1E5EO81O/e9TUOLLkXA5m6T8lfjK9yAA==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz", + "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==", "cpu": [ "x64" ], @@ -113,9 +113,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.4.tgz", - "integrity": "sha512-Oup3G/QxBgvvqnXWrBed7xxkFNwAwJVHZcklWyQt7YCAL5bfUkaa6FVWnR78rNQiM8MqqLiT6ZTZSdUFuVIg1w==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz", + "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==", "cpu": [ "arm64" ], @@ -129,9 +129,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.4.tgz", - "integrity": "sha512-vAP+eYOxlN/Bpo/TZmzEQapNS8W1njECrqkTpNgvXskkkJC2AwOXwZWai/Kc2vEFZUXQttx6UJbj9grqjD/+9Q==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz", + "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==", "cpu": [ "x64" ], @@ -145,9 +145,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.4.tgz", - "integrity": "sha512-A47ZmtpIPyERxkSvIv+zLd6kNIOtJH03XA0Hy7jaceRDdQaQVGSDt4mZqpWqJYgDk9rg96aglbF6kCRvPGDSUA==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz", + "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==", "cpu": [ "arm" ], @@ -161,9 +161,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.4.tgz", - "integrity": "sha512-2zXoBhv4r5pZiyjBKrOdFP4CXOChxXiYD50LRUU+65DkdS5niPFHbboKZd/c81l0ezpw7AQnHeoCy5hFrzzs4g==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz", + "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==", "cpu": [ "arm64" ], @@ -177,9 +177,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.4.tgz", - "integrity": "sha512-uxdSrpe9wFhz4yBwt2kl2TxS/NWEINYBUFIxQtaEVtglm1eECvsj1vEKI0KX2k2wCe17zDdQ3v+jVxfwVfvvjw==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz", + "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==", "cpu": [ "ia32" ], @@ -193,9 +193,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.4.tgz", - "integrity": "sha512-peDrrUuxbZ9Jw+DwLCh/9xmZAk0p0K1iY5d2IcwmnN+B87xw7kujOkig6ZRcZqgrXgeRGurRHn0ENMAjjD5DEg==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz", + "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==", "cpu": [ "loong64" ], @@ -209,9 +209,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.4.tgz", - "integrity": "sha512-sD9EEUoGtVhFjjsauWjflZklTNr57KdQ6xfloO4yH1u7vNQlOfAlhEzbyBKfgbJlW7rwXYBdl5/NcZ+Mg2XhQA==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz", + "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==", "cpu": [ "mips64el" ], @@ -225,9 +225,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.4.tgz", - "integrity": "sha512-X1HSqHUX9D+d0l6/nIh4ZZJ94eQky8d8z6yxAptpZE3FxCWYWvTDd9X9ST84MGZEJx04VYUD/AGgciddwO0b8g==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz", + "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==", "cpu": [ "ppc64" ], @@ -241,9 +241,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.4.tgz", - "integrity": "sha512-97ANpzyNp0GTXCt6SRdIx1ngwncpkV/z453ZuxbnBROCJ5p/55UjhbaG23UdHj88fGWLKPFtMoU4CBacz4j9FA==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz", + "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==", "cpu": [ "riscv64" ], @@ -257,9 +257,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.4.tgz", - "integrity": "sha512-pUvPQLPmbEeJRPjP0DYTC1vjHyhrnCklQmCGYbipkep+oyfTn7GTBJXoPodR7ZS5upmEyc8lzAkn2o29wD786A==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz", + "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==", "cpu": [ "s390x" ], @@ -273,9 +273,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.4.tgz", - "integrity": "sha512-N55Q0mJs3Sl8+utPRPBrL6NLYZKBCLLx0bme/+RbjvMforTGGzFvsRl4xLTZMUBFC1poDzBEPTEu5nxizQ9Nlw==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz", + "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==", "cpu": [ "x64" ], @@ -289,9 +289,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.4.tgz", - "integrity": "sha512-LHSJLit8jCObEQNYkgsDYBh2JrJT53oJO2HVdkSYLa6+zuLJh0lAr06brXIkljrlI+N7NNW1IAXGn/6IZPi3YQ==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz", + "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==", "cpu": [ "x64" ], @@ -305,9 +305,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.4.tgz", - "integrity": "sha512-nLgdc6tWEhcCFg/WVFaUxHcPK3AP/bh+KEwKtl69Ay5IBqUwKDaq/6Xk0E+fh/FGjnLwqFSsarsbPHeKM8t8Sw==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz", + "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==", "cpu": [ "x64" ], @@ -321,9 +321,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.4.tgz", - "integrity": "sha512-08SluG24GjPO3tXKk95/85n9kpyZtXCVwURR2i4myhrOfi3jspClV0xQQ0W0PYWHioJj+LejFMt41q+PG3mlAQ==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz", + "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==", "cpu": [ "x64" ], @@ -337,9 +337,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.4.tgz", - "integrity": "sha512-yYiRDQcqLYQSvNQcBKN7XogbrSvBE45FEQdH8fuXPl7cngzkCvpsG2H9Uey39IjQ6gqqc+Q4VXYHsQcKW0OMjQ==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz", + "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==", "cpu": [ "arm64" ], @@ -353,9 +353,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.4.tgz", - "integrity": "sha512-5rabnGIqexekYkh9zXG5waotq8mrdlRoBqAktjx2W3kb0zsI83mdCwrcAeKYirnUaTGztR5TxXcXmQrEzny83w==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz", + "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==", "cpu": [ "ia32" ], @@ -369,9 +369,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.4.tgz", - "integrity": "sha512-sN/I8FMPtmtT2Yw+Dly8Ur5vQ5a/RmC8hW7jO9PtPSQUPkowxWpcUZnqOggU7VwyT3Xkj6vcXWd3V/qTXwultQ==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz", + "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==", "cpu": [ "x64" ], @@ -835,9 +835,9 @@ } }, "node_modules/esbuild": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.4.tgz", - "integrity": "sha512-qQrPMQpPTWf8jHugLWHoGqZjApyx3OEm76dlTXobHwh/EBbavbRdjXdYi/GWr43GyN0sfpap14GPkb05NH3ROA==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz", + "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==", "dev": true, "hasInstallScript": true, "bin": { @@ -847,28 +847,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.16.4", - "@esbuild/android-arm64": "0.16.4", - "@esbuild/android-x64": "0.16.4", - "@esbuild/darwin-arm64": "0.16.4", - "@esbuild/darwin-x64": "0.16.4", - "@esbuild/freebsd-arm64": "0.16.4", - "@esbuild/freebsd-x64": "0.16.4", - "@esbuild/linux-arm": "0.16.4", - "@esbuild/linux-arm64": "0.16.4", - "@esbuild/linux-ia32": "0.16.4", - "@esbuild/linux-loong64": "0.16.4", - "@esbuild/linux-mips64el": "0.16.4", - "@esbuild/linux-ppc64": "0.16.4", - "@esbuild/linux-riscv64": "0.16.4", - "@esbuild/linux-s390x": "0.16.4", - "@esbuild/linux-x64": "0.16.4", - "@esbuild/netbsd-x64": "0.16.4", - "@esbuild/openbsd-x64": "0.16.4", - "@esbuild/sunos-x64": "0.16.4", - "@esbuild/win32-arm64": "0.16.4", - "@esbuild/win32-ia32": "0.16.4", - "@esbuild/win32-x64": "0.16.4" + "@esbuild/android-arm": "0.17.18", + "@esbuild/android-arm64": "0.17.18", + "@esbuild/android-x64": "0.17.18", + "@esbuild/darwin-arm64": "0.17.18", + "@esbuild/darwin-x64": "0.17.18", + "@esbuild/freebsd-arm64": "0.17.18", + "@esbuild/freebsd-x64": "0.17.18", + "@esbuild/linux-arm": "0.17.18", + "@esbuild/linux-arm64": "0.17.18", + "@esbuild/linux-ia32": "0.17.18", + "@esbuild/linux-loong64": "0.17.18", + "@esbuild/linux-mips64el": "0.17.18", + "@esbuild/linux-ppc64": "0.17.18", + "@esbuild/linux-riscv64": "0.17.18", + "@esbuild/linux-s390x": "0.17.18", + "@esbuild/linux-x64": "0.17.18", + "@esbuild/netbsd-x64": "0.17.18", + "@esbuild/openbsd-x64": "0.17.18", + "@esbuild/sunos-x64": "0.17.18", + "@esbuild/win32-arm64": "0.17.18", + "@esbuild/win32-ia32": "0.17.18", + "@esbuild/win32-x64": "0.17.18" } }, "node_modules/eslint-utils": { @@ -958,12 +958,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -975,18 +969,6 @@ "node": ">= 6" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/immutable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", @@ -1004,18 +986,6 @@ "node": ">=8" } }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1119,9 +1089,15 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -1137,12 +1113,6 @@ "node": ">=0.10.0" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, "node_modules/pathe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.0.0.tgz", @@ -1165,9 +1135,9 @@ } }, "node_modules/postcss": { - "version": "8.4.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", - "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", "funding": [ { "type": "opencollective", @@ -1176,10 +1146,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -1217,23 +1191,6 @@ "node": ">=8.10.0" } }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -1244,9 +1201,9 @@ } }, "node_modules/rollup": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.7.3.tgz", - "integrity": "sha512-7e68MQbAWCX6mI4/0lG1WHd+NdNAlVamg0Zkd+8LZ/oXojligdGnCNyHlzXqXCZObyjs5FRc3AH0b17iJESGIQ==", + "version": "3.21.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.5.tgz", + "integrity": "sha512-a4NTKS4u9PusbUJcfF4IMxuqjFzjm6ifj76P54a7cKnvVzJaG12BLVR+hgU2YDGHzyMMQNxLAZWuALsn8q2oQg==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -1340,18 +1297,6 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1386,15 +1331,14 @@ } }, "node_modules/vite": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.0.tgz", - "integrity": "sha512-ynad+4kYs8Jcnn8J7SacS9vAbk7eMy0xWg6E7bAhS1s79TK+D7tVFGXVZ55S7RNLRROU1rxoKlvZ/qjaB41DGA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", + "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", "dev": true, "dependencies": { - "esbuild": "^0.16.3", - "postcss": "^8.4.19", - "resolve": "^1.22.1", - "rollup": "^3.7.0" + "esbuild": "^0.17.5", + "postcss": "^8.4.23", + "rollup": "^3.21.0" }, "bin": { "vite": "bin/vite.js" @@ -1504,156 +1448,156 @@ "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==" }, "@esbuild/android-arm": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.4.tgz", - "integrity": "sha512-rZzb7r22m20S1S7ufIc6DC6W659yxoOrl7sKP1nCYhuvUlnCFHVSbATG4keGUtV8rDz11sRRDbWkvQZpzPaHiw==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", + "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.4.tgz", - "integrity": "sha512-VPuTzXFm/m2fcGfN6CiwZTlLzxrKsWbPkG7ArRFpuxyaHUm/XFHQPD4xNwZT6uUmpIHhnSjcaCmcla8COzmZ5Q==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz", + "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.4.tgz", - "integrity": "sha512-MW+B2O++BkcOfMWmuHXB15/l1i7wXhJFqbJhp82IBOais8RBEQv2vQz/jHrDEHaY2X0QY7Wfw86SBL2PbVOr0g==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz", + "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.4.tgz", - "integrity": "sha512-a28X1O//aOfxwJVZVs7ZfM8Tyih2Za4nKJrBwW5Wm4yKsnwBy9aiS/xwpxiiTRttw3EaTg4Srerhcm6z0bu9Wg==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz", + "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.4.tgz", - "integrity": "sha512-e3doCr6Ecfwd7VzlaQqEPrnbvvPjE9uoTpxG5pyLzr2rI2NMjDHmvY1E5EO81O/e9TUOLLkXA5m6T8lfjK9yAA==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz", + "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.4.tgz", - "integrity": "sha512-Oup3G/QxBgvvqnXWrBed7xxkFNwAwJVHZcklWyQt7YCAL5bfUkaa6FVWnR78rNQiM8MqqLiT6ZTZSdUFuVIg1w==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz", + "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.4.tgz", - "integrity": "sha512-vAP+eYOxlN/Bpo/TZmzEQapNS8W1njECrqkTpNgvXskkkJC2AwOXwZWai/Kc2vEFZUXQttx6UJbj9grqjD/+9Q==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz", + "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.4.tgz", - "integrity": "sha512-A47ZmtpIPyERxkSvIv+zLd6kNIOtJH03XA0Hy7jaceRDdQaQVGSDt4mZqpWqJYgDk9rg96aglbF6kCRvPGDSUA==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz", + "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.4.tgz", - "integrity": "sha512-2zXoBhv4r5pZiyjBKrOdFP4CXOChxXiYD50LRUU+65DkdS5niPFHbboKZd/c81l0ezpw7AQnHeoCy5hFrzzs4g==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz", + "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.4.tgz", - "integrity": "sha512-uxdSrpe9wFhz4yBwt2kl2TxS/NWEINYBUFIxQtaEVtglm1eECvsj1vEKI0KX2k2wCe17zDdQ3v+jVxfwVfvvjw==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz", + "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.4.tgz", - "integrity": "sha512-peDrrUuxbZ9Jw+DwLCh/9xmZAk0p0K1iY5d2IcwmnN+B87xw7kujOkig6ZRcZqgrXgeRGurRHn0ENMAjjD5DEg==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz", + "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.4.tgz", - "integrity": "sha512-sD9EEUoGtVhFjjsauWjflZklTNr57KdQ6xfloO4yH1u7vNQlOfAlhEzbyBKfgbJlW7rwXYBdl5/NcZ+Mg2XhQA==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz", + "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.4.tgz", - "integrity": "sha512-X1HSqHUX9D+d0l6/nIh4ZZJ94eQky8d8z6yxAptpZE3FxCWYWvTDd9X9ST84MGZEJx04VYUD/AGgciddwO0b8g==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz", + "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.4.tgz", - "integrity": "sha512-97ANpzyNp0GTXCt6SRdIx1ngwncpkV/z453ZuxbnBROCJ5p/55UjhbaG23UdHj88fGWLKPFtMoU4CBacz4j9FA==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz", + "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.4.tgz", - "integrity": "sha512-pUvPQLPmbEeJRPjP0DYTC1vjHyhrnCklQmCGYbipkep+oyfTn7GTBJXoPodR7ZS5upmEyc8lzAkn2o29wD786A==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz", + "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.4.tgz", - "integrity": "sha512-N55Q0mJs3Sl8+utPRPBrL6NLYZKBCLLx0bme/+RbjvMforTGGzFvsRl4xLTZMUBFC1poDzBEPTEu5nxizQ9Nlw==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz", + "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.4.tgz", - "integrity": "sha512-LHSJLit8jCObEQNYkgsDYBh2JrJT53oJO2HVdkSYLa6+zuLJh0lAr06brXIkljrlI+N7NNW1IAXGn/6IZPi3YQ==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz", + "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.4.tgz", - "integrity": "sha512-nLgdc6tWEhcCFg/WVFaUxHcPK3AP/bh+KEwKtl69Ay5IBqUwKDaq/6Xk0E+fh/FGjnLwqFSsarsbPHeKM8t8Sw==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz", + "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.4.tgz", - "integrity": "sha512-08SluG24GjPO3tXKk95/85n9kpyZtXCVwURR2i4myhrOfi3jspClV0xQQ0W0PYWHioJj+LejFMt41q+PG3mlAQ==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz", + "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.4.tgz", - "integrity": "sha512-yYiRDQcqLYQSvNQcBKN7XogbrSvBE45FEQdH8fuXPl7cngzkCvpsG2H9Uey39IjQ6gqqc+Q4VXYHsQcKW0OMjQ==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz", + "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.4.tgz", - "integrity": "sha512-5rabnGIqexekYkh9zXG5waotq8mrdlRoBqAktjx2W3kb0zsI83mdCwrcAeKYirnUaTGztR5TxXcXmQrEzny83w==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz", + "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.4.tgz", - "integrity": "sha512-sN/I8FMPtmtT2Yw+Dly8Ur5vQ5a/RmC8hW7jO9PtPSQUPkowxWpcUZnqOggU7VwyT3Xkj6vcXWd3V/qTXwultQ==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz", + "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==", "dev": true, "optional": true }, @@ -1979,33 +1923,33 @@ } }, "esbuild": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.4.tgz", - "integrity": "sha512-qQrPMQpPTWf8jHugLWHoGqZjApyx3OEm76dlTXobHwh/EBbavbRdjXdYi/GWr43GyN0sfpap14GPkb05NH3ROA==", + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz", + "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==", "dev": true, "requires": { - "@esbuild/android-arm": "0.16.4", - "@esbuild/android-arm64": "0.16.4", - "@esbuild/android-x64": "0.16.4", - "@esbuild/darwin-arm64": "0.16.4", - "@esbuild/darwin-x64": "0.16.4", - "@esbuild/freebsd-arm64": "0.16.4", - "@esbuild/freebsd-x64": "0.16.4", - "@esbuild/linux-arm": "0.16.4", - "@esbuild/linux-arm64": "0.16.4", - "@esbuild/linux-ia32": "0.16.4", - "@esbuild/linux-loong64": "0.16.4", - "@esbuild/linux-mips64el": "0.16.4", - "@esbuild/linux-ppc64": "0.16.4", - "@esbuild/linux-riscv64": "0.16.4", - "@esbuild/linux-s390x": "0.16.4", - "@esbuild/linux-x64": "0.16.4", - "@esbuild/netbsd-x64": "0.16.4", - "@esbuild/openbsd-x64": "0.16.4", - "@esbuild/sunos-x64": "0.16.4", - "@esbuild/win32-arm64": "0.16.4", - "@esbuild/win32-ia32": "0.16.4", - "@esbuild/win32-x64": "0.16.4" + "@esbuild/android-arm": "0.17.18", + "@esbuild/android-arm64": "0.17.18", + "@esbuild/android-x64": "0.17.18", + "@esbuild/darwin-arm64": "0.17.18", + "@esbuild/darwin-x64": "0.17.18", + "@esbuild/freebsd-arm64": "0.17.18", + "@esbuild/freebsd-x64": "0.17.18", + "@esbuild/linux-arm": "0.17.18", + "@esbuild/linux-arm64": "0.17.18", + "@esbuild/linux-ia32": "0.17.18", + "@esbuild/linux-loong64": "0.17.18", + "@esbuild/linux-mips64el": "0.17.18", + "@esbuild/linux-ppc64": "0.17.18", + "@esbuild/linux-riscv64": "0.17.18", + "@esbuild/linux-s390x": "0.17.18", + "@esbuild/linux-x64": "0.17.18", + "@esbuild/netbsd-x64": "0.17.18", + "@esbuild/openbsd-x64": "0.17.18", + "@esbuild/sunos-x64": "0.17.18", + "@esbuild/win32-arm64": "0.17.18", + "@esbuild/win32-ia32": "0.17.18", + "@esbuild/win32-x64": "0.17.18" } }, "eslint-utils": { @@ -2070,12 +2014,6 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "optional": true }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, "glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -2084,15 +2022,6 @@ "is-glob": "^4.0.1" } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, "immutable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", @@ -2107,15 +2036,6 @@ "binary-extensions": "^2.0.0" } }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2192,21 +2112,15 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, "pathe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.0.0.tgz", @@ -2223,11 +2137,11 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, "postcss": { - "version": "8.4.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", - "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } @@ -2245,26 +2159,15 @@ "picomatch": "^2.2.1" } }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, "rollup": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.7.3.tgz", - "integrity": "sha512-7e68MQbAWCX6mI4/0lG1WHd+NdNAlVamg0Zkd+8LZ/oXojligdGnCNyHlzXqXCZObyjs5FRc3AH0b17iJESGIQ==", + "version": "3.21.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.5.tgz", + "integrity": "sha512-a4NTKS4u9PusbUJcfF4IMxuqjFzjm6ifj76P54a7cKnvVzJaG12BLVR+hgU2YDGHzyMMQNxLAZWuALsn8q2oQg==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -2315,12 +2218,6 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2348,16 +2245,15 @@ } }, "vite": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.0.tgz", - "integrity": "sha512-ynad+4kYs8Jcnn8J7SacS9vAbk7eMy0xWg6E7bAhS1s79TK+D7tVFGXVZ55S7RNLRROU1rxoKlvZ/qjaB41DGA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", + "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", "dev": true, "requires": { - "esbuild": "^0.16.3", + "esbuild": "^0.17.5", "fsevents": "~2.3.2", - "postcss": "^8.4.19", - "resolve": "^1.22.1", - "rollup": "^3.7.0" + "postcss": "^8.4.23", + "rollup": "^3.21.0" } }, "vue": { diff --git a/keysas-frontend/package.json b/keysas-frontend/package.json index 28f2719..1a36afd 100644 --- a/keysas-frontend/package.json +++ b/keysas-frontend/package.json @@ -19,6 +19,6 @@ "@rollup/plugin-alias": "^4.0.2", "@vitejs/plugin-vue": "^4.0.0", "sass": "^1.56.2", - "vite": "^4.0.0" + "vite": "^4.3.5" } } diff --git a/keysas-frontend/src/App.vue b/keysas-frontend/src/App.vue index 393a8b2..88d13e4 100644 --- a/keysas-frontend/src/App.vue +++ b/keysas-frontend/src/App.vue @@ -49,9 +49,9 @@ - + - + diff --git a/keysas-frontend/src/components/AppGuichet.vue b/keysas-frontend/src/components/AppGuichet.vue index 2b02664..32033d2 100644 --- a/keysas-frontend/src/components/AppGuichet.vue +++ b/keysas-frontend/src/components/AppGuichet.vue @@ -133,7 +133,7 @@ export default { '.forbidden': 'guichet_OUT.files.error.reason.forbidden', '.yara': 'guichet_OUT.files.error.reason.yara', '.toobig': 'guichet_OUT.files.error.reason.toobig', - '.failed': 'guichet_OUT.files.error.reason.failed', + '.ioerror': 'guichet_OUT.files.error.reason.ioerror', }; if(this.type === 'IN') { @@ -145,13 +145,13 @@ export default { } val.forEach(element => { - let failed = element.endsWith('.failed'); - let slicedElement = failed ? element.substring(0, element.indexOf('.failed')) : element; + let failed = element.endsWith('.ioerror'); + let slicedElement = failed ? element.substring(0, element.indexOf('.ioerror')) : element; if(!this.listInBackup.map(x => x.filename).includes(slicedElement)) { this.listInBackup.push({ filename: slicedElement, - error: failed ? errorsMessages['.failed'] : null + error: failed ? errorsMessages['.ioerror'] : null }); } }); @@ -169,14 +169,41 @@ export default { return; } - // handling Yara case - if (element.endsWith('.yara')) { - let slicedElement = element.substring(0, element.indexOf('.yara')); + // handling Keysas reports + if (element.endsWith('.krp')) { + let slicedElement = element.substring(0, element.indexOf('.krp')); let list = val.includes(slicedElement) ? this.listOutOK : this.listOutError; if (!list.map(x => x.filename).includes(slicedElement)) { - list.push({ - filename: slicedElement, - error: errorsMessages['.yara'] + fetch(element) + .then(response => response.json()) + .then(data =>{ + if (data.metadata.report.yara === true) { + list.push({ + filename: slicedElement, + error: errorsMessages['.yara'] + }); + } + if (data.metadata.report.av === true) { + list.push({ + filename: slicedElement, + error: errorsMessages['.antivirus'] + }); + } + if (data.metadata.report.type_allowed === false) { + list.push({ + filename: slicedElement, + error: errorsMessages['.forbidden'] + }); + } + if (data.metadata.report.toobig === true) { + list.push({ + filename: slicedElement, + error: errorsMessages['.toobig'] + }); + } + }) + .catch(error => { + console.log('An unknown error appened'); }); } return; diff --git a/keysas-frontend/src/locales/en.json b/keysas-frontend/src/locales/en.json index 4bf66c2..fea9681 100644 --- a/keysas-frontend/src/locales/en.json +++ b/keysas-frontend/src/locales/en.json @@ -37,7 +37,7 @@ "forbidden": "extension forbidden", "yara": "potential virus detected", "toobig": "file too big", - "failed": "I/O error", + "ioerror": "I/O error", "unknown": "unknown error" } } diff --git a/keysas-frontend/src/locales/fr.json b/keysas-frontend/src/locales/fr.json index 768d369..c399886 100644 --- a/keysas-frontend/src/locales/fr.json +++ b/keysas-frontend/src/locales/fr.json @@ -38,7 +38,7 @@ "forbidden": "extension interdite", "yara": "virus potentiel détecté", "toobig": "fichier trop gros", - "failed": "erreur de transfert", + "ioerror": "erreur de transfert", "unknown": "erreur inconnue" } } From 0c2ef0e7b0ca64dfd8ff668c47cd6f1d260a85bb Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Wed, 10 May 2023 09:03:58 +0200 Subject: [PATCH 066/160] Fix bind_and_sign() tests and set the signature optional if keys are not found --- keysas-admin/src-tauri/src/utils.rs | 1 + keysas-core/src/keysas-out/main.rs | 54 +++++++++++++++-------------- keysas_lib/src/file_report.rs | 29 ++++++++-------- keysas_lib/src/keysas_key.rs | 2 +- 4 files changed, 45 insertions(+), 41 deletions(-) diff --git a/keysas-admin/src-tauri/src/utils.rs b/keysas-admin/src-tauri/src/utils.rs index d2ac19b..c5f6bb5 100644 --- a/keysas-admin/src-tauri/src/utils.rs +++ b/keysas-admin/src-tauri/src/utils.rs @@ -30,6 +30,7 @@ pub fn cmd_generate_key_and_get_csr( }; let cert_req = String::from_utf8(cmd_res)?; + log::debug!("{cert_req:?}"); // Recover the CSR from the session command let mut csrs = cert_req.split('|'); diff --git a/keysas-core/src/keysas-out/main.rs b/keysas-core/src/keysas-out/main.rs index 18862b5..2f057d0 100644 --- a/keysas-core/src/keysas-out/main.rs +++ b/keysas-core/src/keysas-out/main.rs @@ -64,12 +64,12 @@ use anyhow::Result; use clap::{crate_version, Arg, ArgAction, Command}; use keysas_lib::append_ext; +use keysas_lib::file_report::bind_and_sign; +use keysas_lib::file_report::generate_report_metadata; +use keysas_lib::file_report::FileMetadata; use keysas_lib::init_logger; use keysas_lib::keysas_hybrid_keypair::HybridKeyPair; use keysas_lib::sha256_digest; -use keysas_lib::file_report::FileMetadata; -use keysas_lib::file_report::generate_report_metadata; -use keysas_lib::file_report::bind_and_sign; use landlock::{ path_beneath_rules, Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetError, RulesetStatus, ABI, @@ -225,7 +225,7 @@ fn parse_messages(messages: Messages, buffer: &[u8]) -> Vec { fn output_files( files: Vec, conf: &Configuration, - sign_keys: &HybridKeyPair, + sign_keys: Option<&HybridKeyPair>, sign_cert: &str, ) -> Result<()> { for mut f in files { @@ -316,33 +316,35 @@ fn main() -> Result<()> { Path::new(KEY_FILE_DIR), KEY_PASSWD, ) { - Ok(k) => k, + Ok(k) => Some(k), Err(e) => { - error!("Failed to load station signing keys {e}"); - process::exit(1); + warn!("Failed to load station signing keys {e}"); + None } }; // Convert certificates to PEM string so that it can be placed in the reports let mut sign_cert = String::new(); - let pem_cl = match sign_keys.classic_cert.to_pem(pkcs8::LineEnding::LF) { - Ok(p) => p, - Err(e) => { - error!("Failed to convert certificate to string {e}"); - process::exit(1); - } - }; - sign_cert.push_str(&pem_cl); - // Add a delimiter between the two certificates - sign_cert.push('|'); - let pem_pq = match sign_keys.pq_cert.to_pem(pkcs8::LineEnding::LF) { - Ok(p) => p, - Err(e) => { - error!("Failed to convert certificate to string {e}"); - process::exit(1); - } - }; - sign_cert.push_str(&pem_pq); + if let Some(ref keys) = sign_keys { + let pem_cl = match keys.classic_cert.to_pem(pkcs8::LineEnding::LF) { + Ok(p) => p, + Err(e) => { + error!("Failed to convert certificate to string {e}"); + process::exit(1); + } + }; + sign_cert.push_str(&pem_cl); + // Add a delimiter between the two certificates + sign_cert.push('|'); + let pem_pq = match keys.pq_cert.to_pem(pkcs8::LineEnding::LF) { + Ok(p) => p, + Err(e) => { + error!("Failed to convert certificate to string {e}"); + process::exit(1); + } + }; + sign_cert.push_str(&pem_pq); + } // Open socket with keysas-transit let addr_out = SocketAddr::from_abstract_name(&config.socket_out)?; @@ -382,6 +384,6 @@ fn main() -> Result<()> { let files = parse_messages(ancillary_in.messages(), &buf_in); // Output file - output_files(files, &config, &sign_keys, &sign_cert)?; + output_files(files, &config, sign_keys.as_ref(), &sign_cert)?; } } diff --git a/keysas_lib/src/file_report.rs b/keysas_lib/src/file_report.rs index 8be0235..0b48032 100644 --- a/keysas_lib/src/file_report.rs +++ b/keysas_lib/src/file_report.rs @@ -192,7 +192,7 @@ pub fn generate_report_metadata(f: &FileMetadata) -> MetaData { pub fn bind_and_sign( f: &FileMetadata, report_meta: &MetaData, - sign_keys: &HybridKeyPair, + sign_keys: Option<&HybridKeyPair>, sign_cert: &str, ) -> Result { // Compute digest of report metadata @@ -214,11 +214,16 @@ pub fn bind_and_sign( let mut signature = Vec::new(); - // Sign with ED25519 - signature.append(&mut sign_keys.classic.message_sign(concat.as_bytes())?); + match sign_keys { + Some(keys) => { + // Sign with ED25519 + signature.append(&mut keys.classic.message_sign(concat.as_bytes())?); - // Sign with Dilithium5 - signature.append(&mut sign_keys.pq.message_sign(concat.as_bytes())?); + // Sign with Dilithium5 + signature.append(&mut keys.pq.message_sign(concat.as_bytes())?); + } + None => (), + } // Generate the final report Ok(Report { @@ -347,9 +352,9 @@ pub fn parse_report( #[cfg(test)] mod tests_out { - use base64::{engine::general_purpose, Engine}; - use ed25519_dalek::{self, Digest, Sha512}; use crate::{certificate_field::CertificateFields, keysas_hybrid_keypair::HybridKeyPair}; + use base64::{engine::general_purpose, Engine}; + use ed25519_dalek::{self, Verifier}; use oqs::sig::{Algorithm, Sig}; use pkcs8::der::{DecodePem, EncodePem}; use x509_cert::Certificate; @@ -422,7 +427,7 @@ mod tests_out { let meta = generate_report_metadata(&file_data); - let report = bind_and_sign(&file_data, &meta, &sign_keys, &sign_cert).unwrap(); + let report = bind_and_sign(&file_data, &meta, Some(&sign_keys), &sign_cert).unwrap(); // Test the generated report // Reconstruct the public keys from the binding certficates let mut certs = report.binding.station_certificate.split('|'); @@ -468,15 +473,11 @@ mod tests_out { ) .unwrap() ); - - let mut prehashed = Sha512::new(); - prehashed.update(&concat); assert_eq!( true, pub_cl - .verify_prehashed( - prehashed, - None, + .verify( + concat.as_bytes(), &ed25519_dalek::Signature::from_bytes( &signature[0..ed25519_dalek::SIGNATURE_LENGTH] ) diff --git a/keysas_lib/src/keysas_key.rs b/keysas_lib/src/keysas_key.rs index 2775ddc..414a88e 100644 --- a/keysas_lib/src/keysas_key.rs +++ b/keysas_lib/src/keysas_key.rs @@ -31,8 +31,8 @@ use ed25519_dalek::Keypair; use ed25519_dalek::PublicKey; use ed25519_dalek::Sha512; use ed25519_dalek::Signature as SignatureDalek; -use ed25519_dalek::Verifier; use ed25519_dalek::Signer; +use ed25519_dalek::Verifier; use oqs::sig::Algorithm; use oqs::sig::PublicKey as PqPublicKey; use oqs::sig::SecretKey; From 89d970b970e60452444aa99bff501fad1508f3fc Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 11 May 2023 11:09:40 +0200 Subject: [PATCH 067/160] Fix const paths bugs --- keysas-admin/src-tauri/src/main.rs | 13 ++++++--- keysas-admin/src-tauri/src/utils.rs | 39 +++++++++++++++++++------ keysas_lib/src/keysas_hybrid_keypair.rs | 24 ++++++++++++--- keysas_lib/src/keysas_key.rs | 2 ++ 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index 0393395..9c210a5 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -55,11 +55,11 @@ const USB_CA_KEY_NAME: &str = "usb"; const PKI_ROOT_KEY_NAME: &str = "root"; const _CA_DIR: &str = "/CA"; -const ST_CA_SUB_DIR: &str = "/CA/st"; -const USB_CA_SUB_DIR: &str = "/CA/usb"; +const ST_CA_SUB_DIR: &str = "./CA/st"; +const USB_CA_SUB_DIR: &str = "./CA/usb"; const PKI_ROOT_SUB_DIR: &str = "/CA/root"; const _CRL_DIR: &str = "/CRL"; -const CERT_DIR: &str = "/CERT"; +const CERT_DIR: &str = "/CERT/"; fn create_dir_if_not_exist(path: &String) -> Result<(), anyhow::Error> { if !Path::new(path).is_dir() { @@ -310,6 +310,7 @@ fn init_keysas(ip: String, name: String, ca_pwd: String) -> Result k, @@ -325,6 +326,7 @@ fn init_keysas(ip: String, name: String, ca_pwd: String) -> Result k, @@ -356,6 +358,7 @@ fn init_keysas(ip: String, name: String, ca_pwd: String) -> Result Result Some(c), Err(e) => { - log::error!("Failed to parse certification request: {e}"); + log::error!("Failed to parse classic certification request (1): {e}"); None } }) { Some(csr) => csr, None => { - return Err(anyhow!("Failed to parse certification request")); + return Err(anyhow!("Failed to parse classic certification request (2)")); } }; - let csr_pq = match csrs - .remainder() - .and_then(|pem| match CertReq::from_pem(pem) { + let csr_pq = match csrs.remainder().and_then(|pem| { + match CertReq::from_pem(pem.trim_end_matches("\n\n")) { Ok(c) => Some(c), Err(e) => { - log::error!("Failed to parse certification request: {e}"); + log::debug!("{pem:?}"); + log::error!("Failed to parse PQC certification request (1): {e}"); None } - }) { + } + }) { Some(csr) => csr, None => { - return Err(anyhow!("Failed to parse certification request")); + return Err(anyhow!("Failed to parse PQC certification request (2)")); } }; @@ -80,7 +81,10 @@ pub fn send_cert_to_station( let command = format!( "{}{}{}{}", - "sudo /usr/bin/keysas-sign --load --certtype ", kind, " --cert ", output + "sudo /usr/bin/keysas-sign --load --certtype ", + kind, + " --cert=", + "\"".to_owned() + &output + "\"", ); if let Err(e) = session_exec(session, &command) { @@ -88,6 +92,23 @@ pub fn send_cert_to_station( return Err(anyhow!("Connection error")); } + let command = format!( + "{}", + "sudo /bin/chown keysas-out:keysas-out /etc/keysas/file-sign-cl.p8 /etc/keysas/file-sign-cl.pem /etc/keysas/file-sign-pq.p8 /etc/keysas/file-sign-pq.pem /etc/keysas/usb-ca-cl.pem /etc/keysas/usb-ca-pq.pem", + ); + + if let Err(e) = session_exec(session, &command) { + log::error!("Failed to chown files: {e}"); + return Err(anyhow!("Connection error")); + } + + let command = format!("{}", "sudo /bin/systemctl restart keysas-out",); + + if let Err(e) = session_exec(session, &command) { + log::error!("Failed to restart Keysas: {e}"); + return Err(anyhow!("Connection error")); + } + Ok(()) } diff --git a/keysas_lib/src/keysas_hybrid_keypair.rs b/keysas_lib/src/keysas_hybrid_keypair.rs index 0fda66f..b79c8be 100644 --- a/keysas_lib/src/keysas_hybrid_keypair.rs +++ b/keysas_lib/src/keysas_hybrid_keypair.rs @@ -49,6 +49,7 @@ use x509_cert::spki::ObjectIdentifier; use crate::certificate_field::CertificateFields; use crate::keysas_key::KeysasKey; use crate::keysas_key::KeysasPQKey; +use crate::pki; use crate::pki::generate_cert_from_csr; use crate::pki::DILITHIUM5_OID; use crate::pki::ED25519_OID; @@ -192,21 +193,36 @@ impl HybridKeyPair { name: &str, keys_path: &Path, certs_path: &Path, + pki_dir: &Path, pwd: &str, ) -> Result { // Load keys - let cl_key_path = keys_path.join(name.to_owned() + "-cl.p8"); + log::debug!("PKI dir: {pki_dir:?}"); + + let keys_dir = pki_dir.join(keys_path); + log::debug!("Keys dir: {keys_dir:?}"); + + let cl_key_path = keys_dir.join(name.to_owned() + "-cl.p8"); + log::debug!("Classic: {cl_key_path:?}"); + let classic = Keypair::load_keys(&cl_key_path, pwd)?; + let pq_key_path = keys_dir.join(name.to_owned() + "-pq.p8"); + log::debug!("PQ: {pq_key_path:?}"); - let pq_key_path = keys_path.join(name.to_owned() + "-pq.p8"); let pq = KeysasPQKey::load_keys(&pq_key_path, pwd)?; // Load certificates - let cl_cert_path = certs_path.join(name.to_owned() + "-cl.pem"); + let certs_dir = pki_dir.join(certs_path); + + let cl_cert_path = certs_dir.join(name.to_owned() + "-cl.pem"); + log::debug!("cl_cert_path: {cl_cert_path:?}"); + let cl_cert_pem = fs::read_to_string(cl_cert_path)?; let classic_cert = Certificate::from_pem(cl_cert_pem)?; - let pq_cert_path = certs_path.join(name.to_owned() + "-pq.pem"); + let pq_cert_path = certs_dir.join(name.to_owned() + "-pq.pem"); + log::debug!("pq_cert_path: {pq_cert_path:?}"); + let pq_cert_pem = fs::read_to_string(pq_cert_path)?; let pq_cert = Certificate::from_pem(pq_cert_pem)?; diff --git a/keysas_lib/src/keysas_key.rs b/keysas_lib/src/keysas_key.rs index 414a88e..4e72fcc 100644 --- a/keysas_lib/src/keysas_key.rs +++ b/keysas_lib/src/keysas_key.rs @@ -395,6 +395,8 @@ impl KeysasKey for KeysasPQKey { // Load the pkcs8 from file let cipher = fs::read(path)?; + log::debug!("Read done: {path:?}"); + let enc_pk = match EncryptedPrivateKeyInfo::try_from(cipher.as_slice()) { Ok(ep) => ep, Err(e) => { From 127cb10acb990c629be9e8ea2d314f6d3bc96c2e Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Fri, 12 May 2023 13:09:06 +0200 Subject: [PATCH 068/160] handle ws upgrade --- keysas-backend/src/main.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/keysas-backend/src/main.rs b/keysas-backend/src/main.rs index 971923d..b8df9cd 100644 --- a/keysas-backend/src/main.rs +++ b/keysas-backend/src/main.rs @@ -10,6 +10,7 @@ use std::{net::TcpListener, path::Path, thread::spawn}; use anyhow::Result; +use http::header::HeaderValue; use landlock::{ path_beneath_rules, Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetError, RulesetStatus, ABI, @@ -178,22 +179,27 @@ fn main() -> Result<()> { landlock_sandbox()?; let server = TcpListener::bind("127.0.0.1:3012")?; for stream in server.incoming() { + println!("keysas-backend: Received a new websocket handshake."); + let stream = match stream { + Ok(s) => s, + Err(e) => { + eprintln!("Failed to accept client connection: {}", e); + continue; + } + }; spawn(move || -> Result<()> { let callback = |_req: &Request, mut response: Response| { - println!("keysas-backend: Received a new websocket handshake."); - //println!("The request's path is: {}", req.uri().path()); - //println!("The request's headers are:"); - //for (ref header, _value) in req.headers() { - // println!("* {}", header); - //} - - // Let's add an additional header to our response to the client. - let headers = response.headers_mut(); - headers.append("Keysas-backend", "true".parse().unwrap()); - + log::info!("keysas-backend: Received a new websocket handshake."); + //let headers = response.headers_mut(); + //headers.append("KeysasBackend", "true".parse().unwrap()); + response.headers_mut().append( + "Sec-WebSocket-Protocol", + HeaderValue::from_static("websocket"), + ); + //println!("Response: {response:?}"); Ok(response) }; - let mut websocket = accept_hdr(stream?, callback)?; + let mut websocket = accept_hdr(stream, callback)?; loop { let files_in = list_files("/var/local/in"); From d638556ec8180b9cb0b77bf0708dbb14e1988424 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Fri, 12 May 2023 13:10:46 +0200 Subject: [PATCH 069/160] handle ws upgrade --- keysas-backend/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keysas-backend/Cargo.toml b/keysas-backend/Cargo.toml index aa588a4..0a2aa79 100644 --- a/keysas-backend/Cargo.toml +++ b/keysas-backend/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] -tungstenite = "0.19" +tungstenite = "0.18" serde = "1.0" serde_json = "1.0" serde_derive = "1.0" @@ -31,6 +31,7 @@ yubico_manager = "0.9" walkdir = "2.3" landlock = "0.2" flexi_logger = "0.25" +http = "0.2" [dev-dependencies] criterion = "0.4" From 8283030f741e555674d29afa30dbf555aad9c51d Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Mon, 15 May 2023 12:40:15 +0200 Subject: [PATCH 070/160] Fix paths for PKI creation --- keysas-admin/src-tauri/src/main.rs | 6 ++++-- keysas_lib/src/keysas_hybrid_keypair.rs | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index 9c210a5..5e3163f 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -55,8 +55,8 @@ const USB_CA_KEY_NAME: &str = "usb"; const PKI_ROOT_KEY_NAME: &str = "root"; const _CA_DIR: &str = "/CA"; -const ST_CA_SUB_DIR: &str = "./CA/st"; -const USB_CA_SUB_DIR: &str = "./CA/usb"; +const ST_CA_SUB_DIR: &str = "/./CA/st"; +const USB_CA_SUB_DIR: &str = "/./CA/usb"; const PKI_ROOT_SUB_DIR: &str = "/CA/root"; const _CRL_DIR: &str = "/CRL"; const CERT_DIR: &str = "/CERT/"; @@ -801,6 +801,7 @@ async fn generate_pki_in_dir( } }; // Save keys + log::debug!("{:?}", Path::new(&(pki_dir.to_owned() + ST_CA_SUB_DIR))); if let Err(e) = st_ca_keys.save( ST_CA_KEY_NAME, Path::new(&(pki_dir.to_owned() + ST_CA_SUB_DIR)), @@ -836,6 +837,7 @@ async fn generate_pki_in_dir( } }; // Save keys + log::debug!("{:?}", Path::new(&(pki_dir.to_owned() + "/" + USB_CA_SUB_DIR + "/"))); if let Err(e) = usb_keys.save( USB_CA_KEY_NAME, Path::new(&(pki_dir.to_owned() + USB_CA_SUB_DIR)), diff --git a/keysas_lib/src/keysas_hybrid_keypair.rs b/keysas_lib/src/keysas_hybrid_keypair.rs index b79c8be..77b74e1 100644 --- a/keysas_lib/src/keysas_hybrid_keypair.rs +++ b/keysas_lib/src/keysas_hybrid_keypair.rs @@ -166,19 +166,23 @@ impl HybridKeyPair { ) -> Result<(), anyhow::Error> { // Save keys let cl_key_path = keys_path.join(name.to_owned() + "-cl.p8"); + log::debug!("cl_key_path: {:?}", cl_key_path); self.classic.save_keys(&cl_key_path, pwd)?; let pq_key_path = keys_path.join(name.to_owned() + "-pq.p8"); + log::debug!("pq_key_path: {:?}", pq_key_path); self.pq.save_keys(&pq_key_path, pwd)?; // Save certificates let cl_cert_path = certs_path.join(name.to_owned() + "-cl.pem"); let cl_pem = self.classic_cert.to_pem(LineEnding::LF)?; + log::debug!("cl_cert_path: {:?}", cl_cert_path); let mut cl_cert_file = File::create(cl_cert_path)?; write!(cl_cert_file, "{}", cl_pem)?; let pq_cert_path = certs_path.join(name.to_owned() + "-pq.pem"); let pq_pem = self.pq_cert.to_pem(LineEnding::LF)?; + log::debug!("pq_cert_path: {:?}", pq_cert_path); let mut pq_cert_file = File::create(pq_cert_path)?; write!(pq_cert_file, "{}", pq_pem)?; From decaaf06dc8bae962b362378e8eb65ed54ac7277 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Mon, 22 May 2023 09:10:42 +0200 Subject: [PATCH 071/160] Update some logs + stay async as much as possible --- keysas-frontend/src/App.vue | 16 ++++++++-------- keysas_lib/src/keysas_hybrid_keypair.rs | 1 - 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/keysas-frontend/src/App.vue b/keysas-frontend/src/App.vue index 88d13e4..2d7d677 100644 --- a/keysas-frontend/src/App.vue +++ b/keysas-frontend/src/App.vue @@ -79,7 +79,7 @@ export default { }, data() { return { - debug: (process.env.NODE_ENV === 'development'), + debug: (process.env.NODE_ENV === 'production'), appStarted: false, StatusIn: undefined, StatusTransit: undefined, @@ -113,11 +113,11 @@ export default { this.$refs.GuichetIn.clearAllLists(); this.$refs.GuichetOut.clearAllLists(); }, - wsUdev() { - this.connection_udev = new WebSocket("ws://127.0.0.1:3013/socket"); + async wsUdev() { + this.connection_udev = await new WebSocket("ws://127.0.0.1:3013"); this.connection_udev.onopen = function (event) { + console.log("Successfully connected to websocket server keysas-io"); console.log(event); - console.log("Successfully connected to websocket server udev"); }; var self = this; this.connection_udev.onmessage = function (event) { @@ -143,11 +143,11 @@ export default { }; } }, - wsBackend() { - this.connection_backend = new WebSocket("ws://127.0.0.1:3012/socket"); + async wsBackend() { + this.connection_backend = await new WebSocket("ws://127.0.0.1:3012"); this.connection_backend.onopen = function (event) { + console.log("Successfully connected to websocket server keysas-backend"); console.log(event); - console.log("Successfully connected to websocket server backend"); }; var self = this; this.connection_backend.onmessage = function (event) { @@ -200,7 +200,7 @@ export default { this.StatusIn = true; this.StatusTransit = true; this.StatusOut = true; - }, 2000); + }, 5000); // <<< For Dev Only <<< } diff --git a/keysas_lib/src/keysas_hybrid_keypair.rs b/keysas_lib/src/keysas_hybrid_keypair.rs index 77b74e1..27595aa 100644 --- a/keysas_lib/src/keysas_hybrid_keypair.rs +++ b/keysas_lib/src/keysas_hybrid_keypair.rs @@ -49,7 +49,6 @@ use x509_cert::spki::ObjectIdentifier; use crate::certificate_field::CertificateFields; use crate::keysas_key::KeysasKey; use crate::keysas_key::KeysasPQKey; -use crate::pki; use crate::pki::generate_cert_from_csr; use crate::pki::DILITHIUM5_OID; use crate::pki::ED25519_OID; From 2d76b9e54e4edaa9430a7cf458ccb2fbcb60c7ed Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Mon, 22 May 2023 12:52:08 +0200 Subject: [PATCH 072/160] Fix GNU/Linux paths for key generation --- keysas_lib/src/keysas_hybrid_keypair.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keysas_lib/src/keysas_hybrid_keypair.rs b/keysas_lib/src/keysas_hybrid_keypair.rs index 27595aa..0a17fa9 100644 --- a/keysas_lib/src/keysas_hybrid_keypair.rs +++ b/keysas_lib/src/keysas_hybrid_keypair.rs @@ -202,7 +202,7 @@ impl HybridKeyPair { // Load keys log::debug!("PKI dir: {pki_dir:?}"); - let keys_dir = pki_dir.join(keys_path); + let keys_dir = pki_dir.join(".".to_owned() + &keys_path.to_string_lossy()); log::debug!("Keys dir: {keys_dir:?}"); let cl_key_path = keys_dir.join(name.to_owned() + "-cl.p8"); @@ -215,7 +215,7 @@ impl HybridKeyPair { let pq = KeysasPQKey::load_keys(&pq_key_path, pwd)?; // Load certificates - let certs_dir = pki_dir.join(certs_path); + let certs_dir = pki_dir.join(".".to_owned() + &certs_path.to_string_lossy()); let cl_cert_path = certs_dir.join(name.to_owned() + "-cl.pem"); log::debug!("cl_cert_path: {cl_cert_path:?}"); From 0e42eef38e759a1a989a96c5aae3610f9828a7d6 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Mon, 22 May 2023 12:52:36 +0200 Subject: [PATCH 073/160] Fix stations online status --- keysas-admin/src/views/ManageView.vue | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/keysas-admin/src/views/ManageView.vue b/keysas-admin/src/views/ManageView.vue index f07da4e..796855f 100644 --- a/keysas-admin/src/views/ManageView.vue +++ b/keysas-admin/src/views/ManageView.vue @@ -63,16 +63,16 @@
  • IP: {{ current_ip }}
  • -
  • +
  • Status: Online
  • -
  • +
  • Status: Offline
  • + v-if="KeysasAlive != false && KeysasAlive != true"> Status: Unknown
  • @@ -115,8 +115,7 @@

    Warning!
    - This action will sign the CSRs automatically generated at first boot - on this Keysas station. + This action will create private keys and CSRs on this Keysas station. Then, it will sign the CSRs with the PKi. This may be long !
    @@ -126,8 +125,11 @@
    {{ passwordError }}
    + +

    Processing...

    +

    Done !

    +



    -

    Done !

    @@ -196,6 +198,7 @@ export default { ShowPasswordSign: false, reboot_status: undefined, update_status: undefined, + init_status: undefined, shutdown_status: undefined, export_ssh_status: undefined, create_keypair_status: undefined, @@ -324,10 +327,8 @@ export default { async onSubmitInit() { await this.getKeysasIP(this.current_keysas); this.confirmed = await confirm('This action cannot be reverted. Are you sure?', { title: 'Ready to initialize this Keysas', type: 'warning' }); - var password = prompt("Enter PKI password"); - if (this.confirmed == true) { - //TODO - this.update_status = await init(this.current_ip, this.current_keysas,password); + if (this.confirmed === true) { + this.init_status = await init(this.current_ip, this.current_keysas, this.password); this.confirmed = false; } else { this.ShowUpdateKeysas = false; From 2b256b5f104ed454f09ef84d8cf6d4028e6850c4 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Mon, 22 May 2023 13:24:36 +0200 Subject: [PATCH 074/160] UX: color update on homepage + remove useless method --- keysas-admin/src/views/AboutView.vue | 5 +---- keysas-admin/src/views/HomeView.vue | 12 +++++------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/keysas-admin/src/views/AboutView.vue b/keysas-admin/src/views/AboutView.vue index 581dd54..e05073b 100644 --- a/keysas-admin/src/views/AboutView.vue +++ b/keysas-admin/src/views/AboutView.vue @@ -13,10 +13,6 @@ export default { return { } }, - - methods: { - - } } @@ -24,6 +20,7 @@ export default { diff --git a/keysas-admin/src/views/HomeView.vue b/keysas-admin/src/views/HomeView.vue index 6555ff3..099f4a6 100644 --- a/keysas-admin/src/views/HomeView.vue +++ b/keysas-admin/src/views/HomeView.vue @@ -7,7 +7,7 @@

    QUICK START


      -
    • SSH configuration
    • +
    • SSH configuration
      • First, start by creating a ED25519 private key and the associated public key on your computer
        @@ -20,7 +20,7 @@
      • Then, set the path of both keys in the "Admin configuration->SSH configuration" menu

      -
    • Generate a IKPQPKI
    • +
    • Generate a IKPQPKI
      • To be able to sign your outgoing USB devices and to enroll new Keysas stations, you need to create a IKPQPKI
        (Incredible Keysas (Hybrid) Post-Quantum Public Key Infrastucture) 😁
      • @@ -29,24 +29,22 @@
      • Be patient, this may take a while ☕

      -
    • Enroll you Keysas stations
    • +
    • Enroll you Keysas stations
      • You can now start adding new keysas stations in the "Add a new Keysas" menu providing a name and an IP address
      • When done, export the public key by clicking the "Export SSH pubkey" button for each station added

      -
    • Sign your outgoing USB keys
    • +
    • Sign your outgoing USB keys
      • You can now start signing at least one USB device in "Admin configuration->USB Signing"
      • Type the password provided during your IKPQPKI creation and plug the USB key
      • Be patient, this may take a while ☕

      -
    • You're now ready to go !
    • +
    • You're now ready to go !
    -

    Please visit
    keysas.fr to learn - more ! From 18c9c68ba1a88004fe1560134498ca28e9f1c881 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Tue, 23 May 2023 14:11:00 +0200 Subject: [PATCH 075/160] Set some functions in async mode --- keysas-admin/src-tauri/src/main.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index 5e3163f..2c17acb 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -174,7 +174,7 @@ async fn save_sshkeys(public: String, private: String) -> bool { /// The first returned value is a boolean indicating if an error occured during /// the execution (true: result is ok, false: error) #[command] -fn get_sshkeys() -> Result<(String, String), String> { +async fn get_sshkeys() -> Result<(String, String), String> { match get_ssh() { Ok((public, private)) => Ok((public, private)), Err(e) => { @@ -215,7 +215,7 @@ async fn remove_station(name: String) -> bool { /// The returned value contains a boolean indicating if an error occured during /// the execution (true: result is ok, false: error) #[command] -fn get_station_ip(name: String) -> Result { +async fn get_station_ip(name: String) -> Result { match get_station_ip_by_name(&name) { Ok(res) => Ok(res), Err(e) => { @@ -228,7 +228,7 @@ fn get_station_ip(name: String) -> Result { /// This functions returns a list of all the station name and IP address stored /// The list is a JSON of the form "[{name, ip}]" #[command] -fn list_stations() -> Result { +async fn list_stations() -> Result { match get_station_list() { Ok(res) => { let result = match serde_json::to_string(&res) { @@ -255,7 +255,7 @@ fn list_stations() -> Result { /// 4. Export the created certificate on the station /// 5. Finally it loads the admin USB signing certificate on the station #[command] -fn init_keysas(ip: String, name: String, ca_pwd: String) -> Result { +async fn init_keysas(ip: String, name: String, ca_pwd: String) -> Result { /* Get admin configuration from the store */ // Get SSH key let ssh_key = match get_ssh() { @@ -402,7 +402,7 @@ fn init_keysas(ip: String, name: String, ca_pwd: String) -> Result Date: Tue, 23 May 2023 14:12:13 +0200 Subject: [PATCH 076/160] Print some feedback to final user while creating a new PKI --- keysas-admin/src/views/ManageView.vue | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/keysas-admin/src/views/ManageView.vue b/keysas-admin/src/views/ManageView.vue index 796855f..d311fe2 100644 --- a/keysas-admin/src/views/ManageView.vue +++ b/keysas-admin/src/views/ManageView.vue @@ -125,11 +125,11 @@
    {{ passwordError }}
    - -

    Processing...

    -

    Done !

    -



    +

    Processing

    +

    Done !

    + +
    @@ -327,11 +327,16 @@ export default { async onSubmitInit() { await this.getKeysasIP(this.current_keysas); this.confirmed = await confirm('This action cannot be reverted. Are you sure?', { title: 'Ready to initialize this Keysas', type: 'warning' }); + //console.log("confirmed1:" + this.confirmed); if (this.confirmed === true) { + //console.log("confirmed2:" + this.confirmed); this.init_status = await init(this.current_ip, this.current_keysas, this.password); - this.confirmed = false; + //console.log("init_status:" + this.init_status); + //console.log("global:" + (this.confirmed === true && this.init_status == 'true' )); + // Set this.confirmed = true for 5s showing the success message + this.ShowTwoSec(); } else { - this.ShowUpdateKeysas = false; + this.confirmed = false; } }, async onSubmitSign() { @@ -362,6 +367,12 @@ export default { console.error(error); this.KeysasAlive = false; }) + }, + ShowTwoSec() { + this.confirmed = true; + setTimeout(() => { + this.confirmed = false + }, 5000) } }, beforeUnmount() { From 3767c9791f2d79b6c49904fcbe590ea9cb11e623 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Tue, 23 May 2023 14:26:07 +0200 Subject: [PATCH 077/160] Clear the password as soon as used --- keysas-admin/src/views/ManageView.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/keysas-admin/src/views/ManageView.vue b/keysas-admin/src/views/ManageView.vue index d311fe2..5db59d1 100644 --- a/keysas-admin/src/views/ManageView.vue +++ b/keysas-admin/src/views/ManageView.vue @@ -331,11 +331,13 @@ export default { if (this.confirmed === true) { //console.log("confirmed2:" + this.confirmed); this.init_status = await init(this.current_ip, this.current_keysas, this.password); + this.password = undefined; //console.log("init_status:" + this.init_status); //console.log("global:" + (this.confirmed === true && this.init_status == 'true' )); // Set this.confirmed = true for 5s showing the success message this.ShowTwoSec(); } else { + this.password = undefined; this.confirmed = false; } }, From 2f26eb971d1ae438fe1b85bee7594f2d0497da27 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 25 May 2023 13:05:44 +0200 Subject: [PATCH 078/160] Downgrade time crate to match tauri pinning :( --- keysas_lib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keysas_lib/Cargo.toml b/keysas_lib/Cargo.toml index af531b3..4ab11fd 100644 --- a/keysas_lib/Cargo.toml +++ b/keysas_lib/Cargo.toml @@ -11,7 +11,7 @@ anyhow = "1.0" clap = "4.2" walkdir = "2.3" regex ="1.8" -simple_logger = "4.1" +simple_logger = "3" log = "0.4" pkcs8 = {version = "0.10", default-features = false, features = ["encryption", "pem"] } x509-cert = "0.2" From d504fecfe537948fc93fcd102cfc5b84967ac001 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 25 May 2023 13:06:33 +0200 Subject: [PATCH 079/160] Update deprecated method add to insert --- keysas_lib/src/certificate_field.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/keysas_lib/src/certificate_field.rs b/keysas_lib/src/certificate_field.rs index 5b30fd8..e227bb0 100644 --- a/keysas_lib/src/certificate_field.rs +++ b/keysas_lib/src/certificate_field.rs @@ -181,7 +181,7 @@ impl CertificateFields { // Add country name if let Some(cn) = &self.country { - rdn.add(AttributeTypeAndValue { + rdn.insert(AttributeTypeAndValue { oid: rfc4519::C, value: Any::new(Tag::PrintableString, cn.as_bytes())?, })?; @@ -189,7 +189,7 @@ impl CertificateFields { // Add organisation name if let Some(oa) = &self.org_name { - rdn.add(AttributeTypeAndValue { + rdn.insert(AttributeTypeAndValue { oid: rfc4519::O, value: Any::new(Tag::PrintableString, oa.as_bytes())?, })?; @@ -197,7 +197,7 @@ impl CertificateFields { // Add organisational unit if let Some(ou) = &self.org_unit { - rdn.add(AttributeTypeAndValue { + rdn.insert(AttributeTypeAndValue { oid: rfc4519::OU, value: Any::new(Tag::PrintableString, ou.as_bytes())?, })?; @@ -205,7 +205,7 @@ impl CertificateFields { // Add common name if let Some(co) = &self.common_name { - rdn.add(AttributeTypeAndValue { + rdn.insert(AttributeTypeAndValue { oid: rfc4519::CN, value: Any::new(Tag::PrintableString, co.as_bytes())?, })?; From dfdc155a9ef4bf7a8d54d5f77f93f532f051a0e5 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 25 May 2023 13:07:05 +0200 Subject: [PATCH 080/160] Add tests file --- keysas-core/src/keysas-in/tests.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 keysas-core/src/keysas-in/tests.rs diff --git a/keysas-core/src/keysas-in/tests.rs b/keysas-core/src/keysas-in/tests.rs new file mode 100644 index 0000000..7accbd7 --- /dev/null +++ b/keysas-core/src/keysas-in/tests.rs @@ -0,0 +1,19 @@ +#[cfg(test)] +mod tests { + use crate::is_corrupted; + use tempfile::tempdir; + use std::fs::File; + + #[test] + fn test_is_corrupted() { + let dir = tempdir().unwrap(); + let file = dir.path().join("file.txt"); + File::create(&file).unwrap(); + assert_eq!(false, is_corrupted(file)); + let file = dir.path().join("file.txt"); + let file_corrupted = dir.path().join("file.txt.ioerror"); + File::create(&file).unwrap(); + File::create(&file_corrupted).unwrap(); + assert_eq!(true, is_corrupted(file)); + } +} From 341352af1598f03bbe8f8cdfb128d3a8e9d1ddf8 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 25 May 2023 13:07:40 +0200 Subject: [PATCH 081/160] Import mod tests --- keysas-core/src/keysas-in/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keysas-core/src/keysas-in/main.rs b/keysas-core/src/keysas-in/main.rs index 1674dc5..95b167d 100644 --- a/keysas-core/src/keysas-in/main.rs +++ b/keysas-core/src/keysas-in/main.rs @@ -48,6 +48,7 @@ use std::process; use std::thread as main_thread; use std::time::Duration; use time::OffsetDateTime; +mod tests; #[macro_use] extern crate serde_derive; @@ -194,7 +195,7 @@ fn send_files(files: &[String], stream: &UnixStream, sas_in: &String) -> Result< base_path }) .filter_map(|f| { - // FD is opened in read-only mode + // FD is opened in read-write mode let fh = match File::open(&f) { Ok(f) => f, Err(e) => { From e209ac95e99a1901b022066614f9965e62bc3521 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 25 May 2023 13:08:32 +0200 Subject: [PATCH 082/160] Add new empty arg to match signature --- keysas-core/src/keysas-out/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/keysas-core/src/keysas-out/main.rs b/keysas-core/src/keysas-out/main.rs index 2f057d0..1b41aa1 100644 --- a/keysas-core/src/keysas-out/main.rs +++ b/keysas-core/src/keysas-out/main.rs @@ -314,6 +314,7 @@ fn main() -> Result<()> { "file-sign", Path::new(KEY_FILE_DIR), Path::new(KEY_FILE_DIR), + Path::new("."), KEY_PASSWD, ) { Ok(k) => Some(k), From 137ad2791e47cfad9bb776fdb2855ece190c3e6f Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 25 May 2023 13:09:06 +0200 Subject: [PATCH 083/160] Add dev-dependencies + update yara --- keysas-core/Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/keysas-core/Cargo.toml b/keysas-core/Cargo.toml index bc0990f..5d00667 100644 --- a/keysas-core/Cargo.toml +++ b/keysas-core/Cargo.toml @@ -26,13 +26,16 @@ pkcs8 = {version = "0.10", features = ["encryption", "pem"] } x509-cert = "0.2" landlock = "0.2" syscallz = "0.16" -yara = "0.19" +yara = "0.20" [dependencies.oqs] version = "0.7" default-features = false features = ["dilithium"] +[dev-dependencies] +tempfile = "3.5" + [[bin]] name = "keysas-in" path = "src/keysas-in/main.rs" From f780c2652a33b508f301117cb085ea9050871f4a Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 25 May 2023 13:49:57 +0200 Subject: [PATCH 084/160] Downgrade logger crate to match tauri pinning :( --- keysas-admin/src-tauri/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keysas-admin/src-tauri/Cargo.toml b/keysas-admin/src-tauri/Cargo.toml index 8accb99..bea44ce 100644 --- a/keysas-admin/src-tauri/Cargo.toml +++ b/keysas-admin/src-tauri/Cargo.toml @@ -25,7 +25,7 @@ nom = "7.1" sha2 ="0.10" regex="1.8" sqlite = "0.30" -simple_logger = "4.1" +simple_logger = "3" log = "0.4" pkcs8 = {version = "0.10", features = ["encryption", "pem"] } x509-cert = "0.2" From 85cb1037898de6da93439c78a77a7bd84f2e1b82 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 25 May 2023 13:50:59 +0200 Subject: [PATCH 085/160] Change const paths for windows --- keysas-admin/src-tauri/src/main.rs | 24 +++++++++++++++++++++++- keysas-admin/src/views/ManageView.vue | 1 - 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index 2c17acb..298e8d7 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -49,18 +49,40 @@ use crate::utils::*; mod usb_sign; use crate::usb_sign::*; -// TODO: place constant paths in constants +// Key names won't change const ST_CA_KEY_NAME: &str = "st-ca"; const USB_CA_KEY_NAME: &str = "usb"; const PKI_ROOT_KEY_NAME: &str = "root"; +// Define PKI paths for GNU/Linux +#[cfg(target_os = "linux")] const _CA_DIR: &str = "/CA"; +#[cfg(target_os = "linux")] const ST_CA_SUB_DIR: &str = "/./CA/st"; +#[cfg(target_os = "linux")] const USB_CA_SUB_DIR: &str = "/./CA/usb"; +#[cfg(target_os = "linux")] const PKI_ROOT_SUB_DIR: &str = "/CA/root"; +#[cfg(target_os = "linux")] const _CRL_DIR: &str = "/CRL"; +#[cfg(target_os = "linux")] const CERT_DIR: &str = "/CERT/"; +// Define PKI paths for windows +// TODO: test it on windows ! +#[cfg(target_os = "windows")] +const _CA_DIR: &str = "\\CA"; +#[cfg(target_os = "windows")] +const ST_CA_SUB_DIR: &str = "\\.\\CA\\st"; +#[cfg(target_os = "windows")] +const USB_CA_SUB_DIR: &str = "\\.\\CA\\usb"; +#[cfg(target_os = "windows")] +const PKI_ROOT_SUB_DIR: &str = "\\CA\\root"; +#[cfg(target_os = "windows")] +const _CRL_DIR: &str = "\\CRL"; +#[cfg(target_os = "windows")] +const CERT_DIR: &str = "\\CERT\\"; + fn create_dir_if_not_exist(path: &String) -> Result<(), anyhow::Error> { if !Path::new(path).is_dir() { fs::create_dir(path)?; diff --git a/keysas-admin/src/views/ManageView.vue b/keysas-admin/src/views/ManageView.vue index 5db59d1..8bcd716 100644 --- a/keysas-admin/src/views/ManageView.vue +++ b/keysas-admin/src/views/ManageView.vue @@ -177,7 +177,6 @@ export default { ExportSSH, }, computed: { - }, data() { return { From 95c86442b925fd7a7563dcea9da78726ae9ff1ea Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Thu, 25 May 2023 13:59:26 +0200 Subject: [PATCH 086/160] Bump tungstenite crate --- keysas-backend/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keysas-backend/Cargo.toml b/keysas-backend/Cargo.toml index 0a2aa79..2776dcb 100644 --- a/keysas-backend/Cargo.toml +++ b/keysas-backend/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] -tungstenite = "0.18" +tungstenite = "0.19" serde = "1.0" serde_json = "1.0" serde_derive = "1.0" From ddfae93646b7701e731b038be135b1f12fb644d3 Mon Sep 17 00:00:00 2001 From: r3dlight <5929582+r3dlight@users.noreply.github.com> Date: Fri, 2 Jun 2023 13:29:25 +0200 Subject: [PATCH 087/160] Tests fix (#25) * Fix bind_and_sign() tests and set the signature optional if keys are not found * Fix const paths bugs * handle ws upgrade * handle ws upgrade * Fix paths for PKI creation * Update some logs + stay async as much as possible * Fix GNU/Linux paths for key generation * Fix stations online status * UX: color update on homepage + remove useless method * Set some functions in async mode * Print some feedback to final user while creating a new PKI * Clear the password as soon as used * Downgrade time crate to match tauri pinning :( * Update deprecated method add to insert * Add tests file * Import mod tests * Add new empty arg to match signature * Add dev-dependencies + update yara * Downgrade logger crate to match tauri pinning :( * Change const paths for windows * Bump tungstenite crate * Bump criterion * Update windows pzaths and clean useless fn * cargo fmt * Add delete PKI functionnality * Handle errors when cannot parse signature * Install sudoers config for keysas-admin * Update install/uninstall scripts with sudoers config * Restart keysas instead of keysas-out (socket broken) * Remove useless import * remove old diode paths * Fix some clippy warnings * Update log message * Add base64 + bump some crates * Get the signature size and decode both signatures * Add some debug logs * Bump simple logger crate * Clean up useless fn --- keysas-admin/src-tauri/Cargo.toml | 5 +- keysas-admin/src-tauri/src/main.rs | 106 +++++------------- keysas-admin/src-tauri/src/store.rs | 16 +++ keysas-admin/src-tauri/src/usb_sign.rs | 7 +- keysas-admin/src-tauri/src/utils.rs | 2 +- .../src/components/DisplaySigningConfig.vue | 12 +- keysas-backend/Cargo.toml | 3 +- keysas-backend/src/main.rs | 56 +++------ keysas-core/debian/keysas-sudoconfig | 11 ++ keysas-core/sh/install.sh | 21 ++-- keysas-core/sh/uninstall.sh | 1 + keysas-core/src/keysas-in/tests.rs | 2 +- keysas-io/Cargo.toml | 6 +- keysas-io/src/main.rs | 46 ++++++-- 14 files changed, 146 insertions(+), 148 deletions(-) create mode 100644 keysas-core/debian/keysas-sudoconfig diff --git a/keysas-admin/src-tauri/Cargo.toml b/keysas-admin/src-tauri/Cargo.toml index bea44ce..843e231 100644 --- a/keysas-admin/src-tauri/Cargo.toml +++ b/keysas-admin/src-tauri/Cargo.toml @@ -4,7 +4,7 @@ version = "2.0.0" description = "Keysas stations administration application" authors = ["Stephane N", "Luc Bonnafoux"] license = "GPL-3.0" -repository = "" +repository = "https://github.com/r3dlight/keysas" default-run = "keysas-admin" edition = "2021" rust-version = "1.57" @@ -21,11 +21,10 @@ tauri = { version = "1.2.5", features = ["api-all", "updater"] } ssh-rs = "0.3.2" anyhow = { version = "1.0", features = ["backtrace"] } async-std = "1.12" -nom = "7.1" sha2 ="0.10" regex="1.8" sqlite = "0.30" -simple_logger = "3" +simple_logger = "4" log = "0.4" pkcs8 = {version = "0.10", features = ["encryption", "pem"] } x509-cert = "0.2" diff --git a/keysas-admin/src-tauri/src/main.rs b/keysas-admin/src-tauri/src/main.rs index 298e8d7..d5db5c8 100644 --- a/keysas-admin/src-tauri/src/main.rs +++ b/keysas-admin/src-tauri/src/main.rs @@ -33,8 +33,6 @@ use async_std::task; use keysas_lib::certificate_field::CertificateFields; use keysas_lib::keysas_hybrid_keypair::HybridKeyPair; use keysas_lib::pki::generate_cert_from_csr; -use nom::bytes::complete::take_until; -use nom::IResult; use std::fs; use std::path::Path; use tauri::command; @@ -98,6 +96,7 @@ fn create_dir_if_not_exist(path: &String) -> Result<(), anyhow::Error> { /// | |--usb /// |--CRL /// |--CERT +#[cfg(target_os = "linux")] fn create_pki_dir(pki_dir: &String) -> Result<(), anyhow::Error> { // Test if the directory path is valid if !Path::new(&pki_dir.trim()).is_dir() { @@ -108,10 +107,24 @@ fn create_pki_dir(pki_dir: &String) -> Result<(), anyhow::Error> { create_dir_if_not_exist(&(pki_dir.to_owned() + "/CA/root"))?; create_dir_if_not_exist(&(pki_dir.to_owned() + "/CA/st"))?; create_dir_if_not_exist(&(pki_dir.to_owned() + "/CA/usb"))?; - create_dir_if_not_exist(&(pki_dir.to_owned() + "/CRL"))?; create_dir_if_not_exist(&(pki_dir.to_owned() + "/CERT"))?; + Ok(()) +} + +#[cfg(target_os = "windows")] +fn create_pki_dir(pki_dir: &String) -> Result<(), anyhow::Error> { + // Test if the directory path is valid + if !Path::new(&pki_dir.trim()).is_dir() { + return Err(anyhow!("Invalid PKI directory path")); + } + create_dir_if_not_exist(&(pki_dir.to_owned() + "\\CA"))?; + create_dir_if_not_exist(&(pki_dir.to_owned() + "\\CA\\root"))?; + create_dir_if_not_exist(&(pki_dir.to_owned() + "\\CA\\st"))?; + create_dir_if_not_exist(&(pki_dir.to_owned() + "\\CA\\usb"))?; + create_dir_if_not_exist(&(pki_dir.to_owned() + "\\CRL"))?; + create_dir_if_not_exist(&(pki_dir.to_owned() + "\\CERT"))?; Ok(()) } @@ -159,7 +172,6 @@ async fn init_tauri() -> Result<(), anyhow::Error> { export_sshpubkey, is_alive, sign_key, - revoke_key, validate_privatekey, validate_rootkey, generate_pki_in_dir, @@ -172,6 +184,7 @@ async fn init_tauri() -> Result<(), anyhow::Error> { get_pki_config, get_pki_path, revoke_usb, + del_pki, ]) .run(tauri::generate_context!())?; Ok(()) @@ -206,6 +219,20 @@ async fn get_sshkeys() -> Result<(String, String), String> { } } +/// This functions drop the ca_table from the DB to remove the PKI configuration +/// The first returned value is a boolean indicating if an error occured during +/// the execution (true: result is ok, false: error) +#[command] +async fn del_pki() -> Result<(), String> { + match drop_pki().await { + Ok(()) => Ok(()), + Err(e) => { + log::error!("Failed to get ssh keys: {e}"); + Err(String::from("Store error")) + } + } +} + /// This function saves a station configuration in the database /// If a station already exists with the same name, it is replaced /// The returned value contains a boolean indicating if an error occured during @@ -651,77 +678,6 @@ async fn sign_key(password: String) -> bool { true } -fn parser(s: &str) -> IResult<&str, &str> { - take_until("keysas-sign")(s) -} - -fn parser_revoke(s: &str) -> IResult<&str, &str> { - take_until("--sign")(s) -} - -// TODO: to be modified to work locally -#[command] -async fn revoke_key(ip: String) -> bool { - let private_key = match get_ssh() { - Ok((_, private)) => private, - Err(e) => { - log::error!("Failed to get private key: {e}"); - return false; - } - }; - - // Connect to the host - let host = format!("{}{}", ip.trim(), ":22"); - let mut session = match connect_key(&ip, &private_key) { - Ok(s) => s, - Err(e) => { - log::error!("Failed to open ssh connection with station: {e}"); - return false; - } - }; - - let command = "sudo /usr/bin/keysas-sign --watch".to_string(); - let stdout = match session_exec(&mut session, &command) { - Ok(stdout) => stdout, - Err(e) => { - log::error!("Error while revoking a USB storage: {:?}", e); - session.close(); - return false; - } - }; - - let command = match String::from_utf8(stdout) { - Ok(signme) => { - let signme = signme.trim(); - let (command, _) = parser(signme).unwrap(); - let (_, command) = parser_revoke(command).unwrap(); - let command = format!("{}{}{}", "sudo /usr/bin/", command.trim(), " --revoke"); - log::debug!("{}", command); - command - } - Err(e) => { - log::error!("Error while revoking a USB storage: {:?}", e); - session.close(); - return false; - } - }; - - log::debug!("Going to revoke a USB device on keysas: {}", host); - match session_exec(&mut session, &command) { - Ok(_) => { - log::info!("USB storage successfully revoked !"); - } - Err(e) => { - log::error!("Error while revoking a USB storage: {:?}", e); - session.close(); - return false; - } - } - - session.close(); - true -} - #[command] async fn validate_privatekey(public_key: String, private_key: String) -> bool { Path::new(&public_key.trim()).is_file() && Path::new(&private_key.trim()).is_file() diff --git a/keysas-admin/src-tauri/src/store.rs b/keysas-admin/src-tauri/src/store.rs index 3281358..0f12aea 100644 --- a/keysas-admin/src-tauri/src/store.rs +++ b/keysas-admin/src-tauri/src/store.rs @@ -167,6 +167,22 @@ pub fn delete_station(name: &String) -> Result<(), anyhow::Error> { } } +/// Drop the current PKI +pub async fn drop_pki() -> Result<(), anyhow::Error> { + match STORE_HANDLE.lock() { + Err(e) => Err(anyhow!("Failed to get database lock: {e}")), + Ok(hdl) => match hdl.as_ref() { + Some(connection) => { + let query = format!("DROP TABLE ca_table; CREATE TABLE IF NOT EXISTS ca_table (name TEXT PRIMARY KEY, directory TEXT, org_name TEXT, org_unit TEXT, country TEXT, validity TEXT);"); + log::debug!("Query: {}", query); + connection.execute(query)?; + Ok(()) + } + None => Err(anyhow!("Store is not initialized")), + }, + } +} + /// Get the station IP address by name /// Returns an error if the station does not exist or in case of trouble accessing /// the database diff --git a/keysas-admin/src-tauri/src/usb_sign.rs b/keysas-admin/src-tauri/src/usb_sign.rs index c1fc3a2..21dd7a0 100644 --- a/keysas-admin/src-tauri/src/usb_sign.rs +++ b/keysas-admin/src-tauri/src/usb_sign.rs @@ -107,10 +107,11 @@ fn sign_device( let classic_sig = classic_struct.message_sign(data.as_bytes())?; let pq_sig = pq_pub_struct.message_sign(data.as_bytes())?; let hybrid_sig = format!( - "{}-{}", + "{}|{}", general_purpose::STANDARD.encode(classic_sig.as_slice()), general_purpose::STANDARD.encode(pq_sig.as_slice()) ); + log::debug!("{}", hybrid_sig); Ok(hybrid_sig) } @@ -235,8 +236,7 @@ pub fn sign_usb( .get_maximum_partition_size() .context("No more space available in the USB device")?; - let starting_lba_i32 = 4096; - let starting_lba = starting_lba_i32; + let starting_lba = 8192; mbr[1] = mbrman::MBRPartitionEntry { boot: mbrman::BOOT_INACTIVE, // boot flag @@ -270,6 +270,7 @@ pub fn sign_usb( vendor, model, revision, serial, direction, path_cl, path_pq, password, )?; let size_u32 = attrs.len() as u32; + log::info!("Signature size is {}", size_u32); f.seek(SeekFrom::Start(offset))?; f.write_all(&size_u32.to_be_bytes())?; f.write_all(attrs.as_bytes())?; diff --git a/keysas-admin/src-tauri/src/utils.rs b/keysas-admin/src-tauri/src/utils.rs index 4eebc5b..cd4fd07 100644 --- a/keysas-admin/src-tauri/src/utils.rs +++ b/keysas-admin/src-tauri/src/utils.rs @@ -102,7 +102,7 @@ pub fn send_cert_to_station( return Err(anyhow!("Connection error")); } - let command = format!("{}", "sudo /bin/systemctl restart keysas-out",); + let command = format!("{}", "sudo /bin/systemctl restart keysas",); if let Err(e) = session_exec(session, &command) { log::error!("Failed to restart Keysas: {e}"); diff --git a/keysas-admin/src/components/DisplaySigningConfig.vue b/keysas-admin/src/components/DisplaySigningConfig.vue index 807c357..1bc49e5 100644 --- a/keysas-admin/src/components/DisplaySigningConfig.vue +++ b/keysas-admin/src/components/DisplaySigningConfig.vue @@ -36,6 +36,7 @@ }} +
    @@ -79,7 +80,16 @@ export default { this.pkiPath = dir; }) .catch((error) => console.error(error)); - } + }, + async removePKI(keysas) { + this.confirmed = await confirm('This cannot be reverted, please confirm', { title: 'Remove this PKI ?', type: 'warning' }); + if (this.confirmed == true) { + await invoke('del_pki', {}) + .then((res) => console.log("PKI deleted")) + .catch((error) => console.error(error)); + this.confirmed = false; + } + }, } } diff --git a/keysas-backend/Cargo.toml b/keysas-backend/Cargo.toml index 2776dcb..d33ab1a 100644 --- a/keysas-backend/Cargo.toml +++ b/keysas-backend/Cargo.toml @@ -17,7 +17,6 @@ udev = "0.7" regex = "1.8" libc = "0.2" nom = "7" -minisign = "0.7" clap = { version = "4", default-features = false, features = ["std", "cargo"] } crossbeam-utils = "0.8" nix = "0.26" @@ -34,7 +33,7 @@ flexi_logger = "0.25" http = "0.2" [dev-dependencies] -criterion = "0.4" +criterion = "0.5" env_logger = "0.10" input_buffer = "0.5" socket2 = "0.5" diff --git a/keysas-backend/src/main.rs b/keysas-backend/src/main.rs index b8df9cd..e935b02 100644 --- a/keysas-backend/src/main.rs +++ b/keysas-backend/src/main.rs @@ -124,11 +124,8 @@ pub fn daemon_status() -> Result<[bool; 3]> { .expect("failed to get status for keysas-in"); let status_in = String::from_utf8_lossy(&output.stdout); let re = Regex::new(r"Active: active")?; - if re.is_match(&status_in) { - state[0] = true; - } else { - state[0] = false; - } + state[0] = re.is_match(&status_in); + let output = Command::new("systemctl") .arg("status") .arg("keysas-transit.service") @@ -136,11 +133,8 @@ pub fn daemon_status() -> Result<[bool; 3]> { .expect("failed to get status for keysas-transit"); let status_in = String::from_utf8_lossy(&output.stdout); let re = Regex::new(r"Active: active")?; - if re.is_match(&status_in) { - state[1] = true; - } else { - state[1] = false; - } + state[1] = re.is_match(&status_in); + let output = Command::new("systemctl") .arg("status") .arg("keysas-out.service") @@ -148,11 +142,8 @@ pub fn daemon_status() -> Result<[bool; 3]> { .expect("failed to get status for keysas-out"); let status_in = String::from_utf8_lossy(&output.stdout); let re = Regex::new(r"Active: active")?; - if re.is_match(&status_in) { - state[2] = true; - } else { - state[2] = false; - } + state[2] = re.is_match(&status_in); + Ok(state) } @@ -162,11 +153,12 @@ fn parse_ip(s: &str) -> IResult<&str, &str> { fn get_ip() -> Result> { let mut ips = Vec::new(); - let addrs = nix::ifaddrs::getifaddrs().unwrap(); + let addrs = nix::ifaddrs::getifaddrs()?; for ifaddr in addrs { if let Some(address) = ifaddr.address { let addr = address.to_string(); let (_, ip) = parse_ip(&addr).unwrap(); + //TODO: should be fixed to match other eth names if ifaddr.interface_name == "eth0" && ip.parse::().is_ok() { ips.push(ip.to_string()); } @@ -205,31 +197,16 @@ fn main() -> Result<()> { let files_in = list_files("/var/local/in"); let files_out = list_files("/var/local/out"); - let mut diode_in = PathBuf::new(); - diode_in.push("/run/diode-in"); - let mut diode_out = PathBuf::new(); - diode_out.push("/run/diode-out/"); - let is_empty_in = diode_in.read_dir()?.next().is_none(); let mut fs_in = PathBuf::new(); fs_in.push("/var/local/in"); let is_empty_fs_in = fs_in.read_dir()?.next().is_none(); - let is_empty_out = diode_out.read_dir()?.next().is_none(); - let mut fs_transit = PathBuf::new(); - fs_transit.push("/var/local/transit"); - let is_empty_fs_transit = fs_transit.read_dir()?.next().is_none(); - - let working_in = !is_empty_in - || Path::new("/var/lock/keysas/keysas-in").exists() - || !is_empty_fs_in; + let working_in = + Path::new("/var/lock/keysas/keysas-in").exists() || !is_empty_fs_in; - let working_out = !is_empty_out - || Path::new("/var/lock/keysas/keysas-out").exists() - || !is_empty_fs_transit; + let working_out = Path::new("/var/lock/keysas/keysas-out").exists(); - let working_transit = !(is_empty_out - && is_empty_in - && !Path::new("/var/lock/keysas/keysas-transit").exists()); + let working_transit = Path::new("/var/lock/keysas/keysas-transit").exists(); let health: Daemons = Daemons { status_in: daemon_status()?[0], @@ -251,13 +228,6 @@ fn main() -> Result<()> { if !Path::new("/usr/share/keysas/neversigned").exists() { has_signed = true; } - let mut keypair_ok = false; - - if Path::new("/etc/keysas/keysas.priv").exists() - && Path::new("/etc/keysas/keysas.pub").exists() - { - keypair_ok = true; - } let orders = GlobalStatus { health, @@ -265,7 +235,7 @@ fn main() -> Result<()> { guichettransit: working_transit, guichetout: guichet_state_out, has_signed_once: has_signed, - keypair_generated: keypair_ok, + keypair_generated: true, ip: get_ip()?, }; diff --git a/keysas-core/debian/keysas-sudoconfig b/keysas-core/debian/keysas-sudoconfig new file mode 100644 index 0000000..e75a309 --- /dev/null +++ b/keysas-core/debian/keysas-sudoconfig @@ -0,0 +1,11 @@ +keysas ALL=(root) EXEC,NOPASSWD: /usr/bin/keysas-sign +keysas ALL=(root) EXEC,NOPASSWD: /usr/bin/keysas-manage-yubikey +keysas ALL=(root) EXEC,NOPASSWD: /bin/systemctl reboot +keysas ALL=(root) EXEC,NOPASSWD: /bin/systemctl poweroff +keysas ALL=(root) EXEC,NOPASSWD: /usr/bin/apt update +keysas ALL=(root) EXEC,NOPASSWD: /usr/bin/apt -y dist-upgrade +keysas ALL=(root) EXEC,NOPASSWD: /usr/bin/keysas-sign +keysas ALL=(root) EXEC,NOPASSWD: /usr/bin/sed -i s/.*PasswordAuthentication.*/PasswordAuthentication no/ /etc/ssh/sshd_config +keysas ALL=(root) EXEC,NOPASSWD: /bin/systemctl restart sshd +keysas ALL=(root) EXEC,NOPASSWD: /bin/systemctl restart keysas +keysas ALL=(root) EXEC,NOPASSWD: /bin/chown keysas-out\:keysas-out /etc/keysas/file-sign-cl.p8 /etc/keysas/file-sign-cl.pem /etc/keysas/file-sign-pq.p8 /etc/keysas/file-sign-pq.pem /etc/keysas/usb-ca-cl.pem /etc/keysas/usb-ca-pq.pem \ No newline at end of file diff --git a/keysas-core/sh/install.sh b/keysas-core/sh/install.sh index 37d5ac2..535b5b6 100755 --- a/keysas-core/sh/install.sh +++ b/keysas-core/sh/install.sh @@ -42,24 +42,24 @@ add_users() { # Install ELF binaries in /usr/bin/. install_bin() { if [ -d "/usr/bin" ]; then - if [ -f "bin/keysas-in" ]; then - install -v -o $U_KEYSAS_IN -g $U_KEYSAS_IN -m 0500 bin/keysas-in /usr/bin/ + if [ -f "target/release/keysas-in" ]; then + install -v -o $U_KEYSAS_IN -g $U_KEYSAS_IN -m 0500 target/release/keysas-in /usr/bin/ else - echo "Binary ./bin/keysas-in cannot be found !" + echo "Binary ./target/release/keysas-in cannot be found !" fi fi if [ -d "/usr/bin" ]; then - if [ -f "bin/keysas-transit" ]; then - install -v -o $U_KEYSAS_TRANSIT -g $U_KEYSAS_TRANSIT -m 0500 bin/keysas-transit /usr/bin/ + if [ -f "target/release/keysas-transit" ]; then + install -v -o $U_KEYSAS_TRANSIT -g $U_KEYSAS_TRANSIT -m 0500 target/release/keysas-transit /usr/bin/ else - echo "Binary ./bin/keysas-transit cannot be found !" + echo "Binary ./target/release/keysas-transit cannot be found !" fi fi if [ -d "/usr/bin" ]; then - if [ -f "bin/keysas-out" ]; then - install -v -o $U_KEYSAS_OUT -g $U_KEYSAS_OUT -m 0500 bin/keysas-out /usr/bin/ + if [ -f "target/release/keysas-out" ]; then + install -v -o $U_KEYSAS_OUT -g $U_KEYSAS_OUT -m 0500 target/release/keysas-out /usr/bin/ else - echo "Binary ./bin/keysas-out cannot be found !" + echo "Binary ./target/release/keysas-out cannot be found !" fi fi } @@ -108,6 +108,9 @@ install_config() { install -v -o $U_KEYSAS_TRANSIT -g $U_KEYSAS_TRANSIT -m 0600 debian/keysas-transit.default /etc/keysas/keysas-transit.conf install -v -o $U_KEYSAS_OUT -g $U_KEYSAS_OUT -m 0600 debian/keysas-out.default /etc/keysas/keysas-out.conf fi + if [ -d "/etc/sudoers.d"]; then + install -v -o root -g root -m 0644 debian/keysas-sudoconfig /etc/sudoers.d/010_keysas + fi } # Install apparmor profiles. diff --git a/keysas-core/sh/uninstall.sh b/keysas-core/sh/uninstall.sh index e42acc4..256b914 100755 --- a/keysas-core/sh/uninstall.sh +++ b/keysas-core/sh/uninstall.sh @@ -33,6 +33,7 @@ main() { /etc/apparmor.d/usr.bin.keysas-out /etc/apparmor.d/local/usr.sbin.clamd /etc/systemd/system/clamav-daemon.socket + /etc/sudoers.d/010_keysas " dirs=" diff --git a/keysas-core/src/keysas-in/tests.rs b/keysas-core/src/keysas-in/tests.rs index 7accbd7..24a1b28 100644 --- a/keysas-core/src/keysas-in/tests.rs +++ b/keysas-core/src/keysas-in/tests.rs @@ -1,8 +1,8 @@ #[cfg(test)] mod tests { use crate::is_corrupted; - use tempfile::tempdir; use std::fs::File; + use tempfile::tempdir; #[test] fn test_is_corrupted() { diff --git a/keysas-io/Cargo.toml b/keysas-io/Cargo.toml index 7f7369d..cb5581f 100644 --- a/keysas-io/Cargo.toml +++ b/keysas-io/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] -tungstenite = "0.18" +tungstenite = "0.19" serde = "1.0" serde_json = "1.0" serde_derive = "1.0" @@ -17,7 +17,6 @@ udev = "0.7" regex = "1.8" libc = "*" nom = "7" -minisign = "0.7" clap = { version = "4", default-features = false, features = ["std", "cargo"] } crossbeam-utils = "0.8" nix = "0.26" @@ -34,9 +33,10 @@ flexi_logger = "0.25" keysas_lib = { path = "../keysas_lib" } oqs = { version = "0.7", default-features = false, features =["dilithium"] } ed25519-dalek = "1.0" +base64 = "0.21" [dev-dependencies] -criterion = "0.4" +criterion = "0.5" env_logger = "0.10" input_buffer = "0.5" socket2 = "0.5" diff --git a/keysas-io/src/main.rs b/keysas-io/src/main.rs index d80de58..644cf64 100644 --- a/keysas-io/src/main.rs +++ b/keysas-io/src/main.rs @@ -15,6 +15,7 @@ extern crate regex; extern crate udev; use anyhow::anyhow; +use base64::{engine::general_purpose, Engine as _}; use clap::{crate_version, Arg, Command as Clap_Command}; use log::{debug, error, info, warn}; use regex::Regex; @@ -215,20 +216,51 @@ fn get_signature(device: &str) -> Result { .read(true) .open(device) .context("Cannot open the USB device to verify the signature.")?; - let mut buf = Vec::new(); + // Seeking for hybrid signature + let mut buf = [0u8; 4]; f.seek(SeekFrom::Start(offset))?; f.read_exact(&mut buf)?; - let buf_str = String::from_utf8(buf)?; + let signature_size = u32::from_be_bytes(buf); + // Size must not be > 7684 bytes LBA-MBR (8196-512) + if signature_size > 7684 as u32 { + return Err(anyhow!("Invalid length for signature")); + } + // Now read the signature size only + let mut buffer = vec![0u8; signature_size.try_into()?]; + log::debug!("Allocated buffer size for signature is {}", buf.len()); + f.read_exact(&mut buffer)?; + let buf_str = String::from_utf8(buffer.to_vec())?; let mut signatures = buf_str.split('|'); - //TODO: handle these unwrap - let s_cl = signatures.next().unwrap(); - let s_pq = signatures.remainder().unwrap(); - let sig_dalek = SignatureDalek::from_bytes(s_cl.as_bytes()) + let s_cl = match signatures.next() { + Some(cl) => general_purpose::STANDARD.decode(cl)?, + None => return Err(anyhow!("Cannot parse Classic signature from USB device")), + }; + + let s_cl_decoded = match general_purpose::STANDARD.decode(s_cl) { + Ok(cl) => cl, + Err(e) => { + return Err(anyhow!( + "Cannot decode base64 Classic signature from bytes: {e}" + )) + } + }; + + let s_pq = match signatures.remainder() { + Some(pq) => pq, + None => return Err(anyhow!("Cannot parse PQ signature from USB device")), + }; + + let s_pq_decoded = match general_purpose::STANDARD.decode(s_pq) { + Ok(pq) => pq, + Err(e) => return Err(anyhow!("Cannot decode base64 PQ signature from bytes: {e}")), + }; + + let sig_dalek = SignatureDalek::from_bytes(&s_cl_decoded) .context("Cannot parse classic signature from bytes")?; oqs::init(); let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let sig_pq = match pq_scheme.signature_from_bytes(s_pq.as_bytes()) { + let sig_pq = match pq_scheme.signature_from_bytes(&s_pq_decoded) { Some(sig) => sig, None => return Err(anyhow!("Cannot parse PQ signature from bytes")), }; From 5bce889a85effbbba4e6ea91c9a8c287825d84f6 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Fri, 2 Jun 2023 13:37:54 +0200 Subject: [PATCH 088/160] Fix: remove unwanted b64 decode --- keysas-io/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keysas-io/src/main.rs b/keysas-io/src/main.rs index 644cf64..705bac8 100644 --- a/keysas-io/src/main.rs +++ b/keysas-io/src/main.rs @@ -233,7 +233,7 @@ fn get_signature(device: &str) -> Result { let mut signatures = buf_str.split('|'); let s_cl = match signatures.next() { - Some(cl) => general_purpose::STANDARD.decode(cl)?, + Some(cl) => cl, None => return Err(anyhow!("Cannot parse Classic signature from USB device")), }; From 8f63739d3f7a148f66a6bc98a5924a3b73c1a9c8 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Fri, 2 Jun 2023 14:47:07 +0200 Subject: [PATCH 089/160] Handle bad password error in UX --- keysas-admin/src/utils/utils.js | 3 ++- keysas-admin/src/views/ManageView.vue | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/keysas-admin/src/utils/utils.js b/keysas-admin/src/utils/utils.js index 73cfdab..c41a69e 100644 --- a/keysas-admin/src/utils/utils.js +++ b/keysas-admin/src/utils/utils.js @@ -83,7 +83,8 @@ export async function init(ip, name, caPwd, return res; } catch(e) { console.log(e) - return Promise.reject(e); + //return Promise.reject(e); + return false; } } diff --git a/keysas-admin/src/views/ManageView.vue b/keysas-admin/src/views/ManageView.vue index 8bcd716..4f9200e 100644 --- a/keysas-admin/src/views/ManageView.vue +++ b/keysas-admin/src/views/ManageView.vue @@ -128,6 +128,7 @@

    Processing

    Done !

    +

    PKI error !


    @@ -325,7 +326,7 @@ export default { */ async onSubmitInit() { await this.getKeysasIP(this.current_keysas); - this.confirmed = await confirm('This action cannot be reverted. Are you sure?', { title: 'Ready to initialize this Keysas', type: 'warning' }); + this.confirmed = await confirm('Are you sure ?', { title: 'Ready to initialize this Keysas', type: 'warning' }); //console.log("confirmed1:" + this.confirmed); if (this.confirmed === true) { //console.log("confirmed2:" + this.confirmed); From 0f51fa75ae2fb4967ad6913d31b909c955c2e777 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Fri, 2 Jun 2023 15:10:35 +0200 Subject: [PATCH 090/160] Update installation process for Tauri --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f79015d..2b77a05 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Files are passed between daemons as raw file descriptors and using abstract sock - Keysas-fido: Manage Yubikeys 5 enrollment - Keysas-backend: Create a websocket server to send different json values to the keysas-frontend - Keysas-frontend: Readonly VueJS3 Frontend for the final user - - Keysas-admin: Desktop application for managing several Keysas stations (Tauri + VueJS3). It also provides a PKI to sign USB outgoing devices, sign certificat signing reqests (csr) from Keysas stations. + - Keysas-admin: Desktop application for managing several Keysas stations (Tauri + VueJS). It also provides an hybrid post-quantum PKI to sign USB outgoing devices, sign certificat signing reqests (csr) from Keysas stations. ## Installation @@ -50,15 +50,17 @@ On Debian stable: echo "deb http://deb.debian.org/debian bullseye-backports main contrib non-free" > /etc/apt/sources.list.d/backports.list apt-get update -yq apt -qy -t bullseye-backports install libyara-dev libyara9 -apt-get install -y wget cmake make lsb-release software-properties-common libseccomp-dev clamav-daemon clamav-freshclam pkg-config git bash libudev-dev +apt -qy install -y wget cmake make lsb-release software-properties-common libseccomp-dev clamav-daemon clamav-freshclam pkg-config git bash libudev-dev libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly -y git clone --depth=1 https://github.com/r3dlight/keysas && cd keysas +rustup toolchain install nightly-2023-05-20 --force +rustup default nightly-2023-05-20 make help make build make install ``` ## User documentation -User documentation can be found here : [https://keysas.fr](https://keysas.fr) +User documentation (deprecated for now) can be found here : [https://keysas.fr](https://keysas.fr) From 5319a1f5b3747736370d8a96999b52337da0b558 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Mon, 5 Jun 2023 10:58:55 +0200 Subject: [PATCH 091/160] UX: add some verbosity for admin --- keysas-admin/src/components/SignDevice.vue | 2 +- keysas-admin/src/views/HomeView.vue | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/keysas-admin/src/components/SignDevice.vue b/keysas-admin/src/components/SignDevice.vue index cf02b76..3d08e75 100644 --- a/keysas-admin/src/components/SignDevice.vue +++ b/keysas-admin/src/components/SignDevice.vue @@ -35,7 +35,7 @@ Error while signing the new device !
    - Please plug a new USB device...
    + Please plug a new USB device and wait
    diff --git a/keysas-admin/src/views/HomeView.vue b/keysas-admin/src/views/HomeView.vue index 099f4a6..0d5fe51 100644 --- a/keysas-admin/src/views/HomeView.vue +++ b/keysas-admin/src/views/HomeView.vue @@ -33,7 +33,9 @@
    • You can now start adding new keysas stations in the "Add a new Keysas" menu providing a name and an IP address
    • -
    • When done, export the public key by clicking the "Export SSH pubkey" button for each station added
    • +
    • When done, export the public SSH key by clicking the "Export SSH pubkey" button for each station added
    • +
    • In the menu, go to "Manage your registred stations", click on more, provide your IKPQPKI password then click on "Enroll"
    • +
    • Be patient, this may take some time ☕

  • Sign your outgoing USB keys
  • From 3dd04e2a45d2b4b7cdc5cd132cdcd087d483c6f3 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Mon, 5 Jun 2023 12:43:22 +0200 Subject: [PATCH 092/160] Update readme --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2b77a05..0768f70 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ Keysas -# USB virus cleaning station (WIP) - -Warning: This is only a work in progress for now. +# USB virus cleaning station # Main features - Retrieve untrusted files from USB (via keysas-io) or over the network @@ -62,5 +60,5 @@ make install ``` ## User documentation -User documentation (deprecated for now) can be found here : [https://keysas.fr](https://keysas.fr) +User documentation (outdated for now) can be found here : [https://keysas.fr](https://keysas.fr) From 6d0f6998dd9e45f6579c3c1ea3f64cf53046491f Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Mon, 12 Jun 2023 10:40:13 +0200 Subject: [PATCH 093/160] Remove std feature for Windows --- keysas_lib/Cargo.toml | 4 +- keysas_lib/src/certificate_field.rs | 10 +++- keysas_lib/src/file_report.rs | 11 ++-- keysas_lib/src/keysas_hybrid_keypair.rs | 26 ++++++++-- keysas_lib/src/keysas_key.rs | 67 +++++++++++++++++++------ keysas_lib/src/pki.rs | 5 +- 6 files changed, 95 insertions(+), 28 deletions(-) diff --git a/keysas_lib/Cargo.toml b/keysas_lib/Cargo.toml index 4ab11fd..938fe2e 100644 --- a/keysas_lib/Cargo.toml +++ b/keysas_lib/Cargo.toml @@ -11,7 +11,7 @@ anyhow = "1.0" clap = "4.2" walkdir = "2.3" regex ="1.8" -simple_logger = "3" +simple_logger = "4" log = "0.4" pkcs8 = {version = "0.10", default-features = false, features = ["encryption", "pem"] } x509-cert = "0.2" @@ -31,4 +31,4 @@ base64 = "0.21" [dependencies.oqs] version = "0.7" default-features = false -features = ["std", "dilithium"] +features = ["dilithium"] diff --git a/keysas_lib/src/certificate_field.rs b/keysas_lib/src/certificate_field.rs index e227bb0..8877aab 100644 --- a/keysas_lib/src/certificate_field.rs +++ b/keysas_lib/src/certificate_field.rs @@ -110,7 +110,10 @@ pub fn validate_signing_certificate( oqs::init(); // Extract the CA public key - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let pq_scheme = match Sig::new(Algorithm::Dilithium5) { + Ok(pq_s) => pq_s, + Err(e) => return Err(anyhow!("Cannot construct new Dilithium algorithm: {e}")), + }; let ca_key = pq_scheme .public_key_from_bytes( cert.tbs_certificate @@ -128,7 +131,10 @@ pub fn validate_signing_certificate( .ok_or_else(|| anyhow!("Signature field is empty"))?, ) .ok_or_else(|| anyhow!("Failed to parse signature field"))?; - pq_scheme.verify(&cert.tbs_certificate.to_der()?, sig, ca_key)?; + match pq_scheme.verify(&cert.tbs_certificate.to_der()?, sig, ca_key) { + Ok(_) => log::info!("Certificate is verified"), + Err(e) => return Err(anyhow!("Certificate is not verified: {e}")), + } // If the signature is invalid an error is thrown } _ => { diff --git a/keysas_lib/src/file_report.rs b/keysas_lib/src/file_report.rs index 0b48032..380a19e 100644 --- a/keysas_lib/src/file_report.rs +++ b/keysas_lib/src/file_report.rs @@ -331,7 +331,10 @@ pub fn parse_report( // Validate the signature with Dilithium oqs::init(); - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let pq_scheme = match Sig::new(Algorithm::Dilithium5) { + Ok(pq_s) => pq_s, + Err(e) => return Err(anyhow!("Cannot construct new Dilithium algorithm: {e}")), + }; let pub_pq = pq_scheme .public_key_from_bytes( cert_pq @@ -344,9 +347,11 @@ pub fn parse_report( let sig_pq = pq_scheme .signature_from_bytes(&signature[ed25519_dalek::SIGNATURE_LENGTH..]) .ok_or_else(|| anyhow!("Failed to parse signature field"))?; - pq_scheme.verify(message.as_bytes(), sig_pq, pub_pq)?; + match pq_scheme.verify(message.as_bytes(), sig_pq, pub_pq) { + Ok(_) => log::info!("Dilithium scheme is now verified"), + Err(e) => return Err(anyhow!("Dilithium scheme is not verified: {e}")), + } // If the signature is invalid an error is thrown - Ok(report) } diff --git a/keysas_lib/src/keysas_hybrid_keypair.rs b/keysas_lib/src/keysas_hybrid_keypair.rs index 0a17fa9..d29895d 100644 --- a/keysas_lib/src/keysas_hybrid_keypair.rs +++ b/keysas_lib/src/keysas_hybrid_keypair.rs @@ -24,6 +24,7 @@ #![warn(deprecated)] #![warn(unused_imports)] +use anyhow::anyhow; use anyhow::Context; use ed25519_dalek::Digest; use ed25519_dalek::Keypair; @@ -115,8 +116,14 @@ fn generate_root_dilithium( infos: &CertificateFields, ) -> Result<(SecretKey, oqs::sig::PublicKey, Certificate), anyhow::Error> { // Create the root CA Dilithium key pair - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let (pk, sk) = pq_scheme.keypair()?; + let pq_scheme = match Sig::new(Algorithm::Dilithium5) { + Ok(pq_s) => pq_s, + Err(e) => return Err(anyhow!("Cannot construct new Dilithium algorithm: {e}")), + }; + let (pk, sk) = match pq_scheme.keypair() { + Ok((public, secret)) => (public, secret), + Err(e) => return Err(anyhow!("Cannot generate new Dilithium keypair: {e}")), + }; // OID value for dilithium-sha512 from IBM's networking OID range let dilithium5_oid = ObjectIdentifier::new(DILITHIUM5_OID)?; @@ -137,7 +144,10 @@ fn generate_root_dilithium( let content = tbs.to_der()?; - let signature = pq_scheme.sign(&content, &sk)?; + let signature = match pq_scheme.sign(&content, &sk) { + Ok(sig) => sig, + Err(e) => return Err(anyhow!("Cannot sign message: {e}")), + }; let cert = Certificate { tbs_certificate: tbs, @@ -276,8 +286,14 @@ impl HybridKeyPair { // Generate Dilithium key and certificate // Create the Dilithium key pair - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let (pk_dl, sk_dl) = pq_scheme.keypair()?; + let pq_scheme = match Sig::new(Algorithm::Dilithium5) { + Ok(pq_s) => pq_s, + Err(e) => return Err(anyhow!("Cannot construct new Dilithium algorithm: {e}")), + }; + let (pk_dl, sk_dl) = match pq_scheme.keypair() { + Ok((public, secret)) => (public, secret), + Err(e) => return Err(anyhow!("Cannot generate new Dilithium keypair: {e}")), + }; let kp_pq = KeysasPQKey { private_key: sk_dl, public_key: pk_dl, diff --git a/keysas_lib/src/keysas_key.rs b/keysas_lib/src/keysas_key.rs index 4e72fcc..eb93e5b 100644 --- a/keysas_lib/src/keysas_key.rs +++ b/keysas_lib/src/keysas_key.rs @@ -119,7 +119,10 @@ impl PublicKeys for KeysasHybridPubKeys { .raw_bytes(), )?; oqs::init(); - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let pq_scheme = match Sig::new(Algorithm::Dilithium5) { + Ok(pq_s) => pq_s, + Err(e) => return Err(anyhow!("Cannot construct Dilithium algorithm: {e}")), + }; let pub_pq = match pq_scheme.public_key_from_bytes( cert_pq .tbs_certificate @@ -146,10 +149,14 @@ impl PublicKeys for KeysasHybridPubKeys { .verify(message, &signatures.classic) .context("Invalid Ed25519 signature")?; oqs::init(); - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - pq_scheme - .verify(message, &signatures.pq, &pubkeys.pq) - .context("Invalid Dilithium5 signature")?; + let pq_scheme = match Sig::new(Algorithm::Dilithium5) { + Ok(pq_s) => pq_s, + Err(e) => return Err(anyhow!("Cannot construct new Dilithium algorithm: {e}")), + }; + match pq_scheme.verify(message, &signatures.pq, &pubkeys.pq) { + Ok(_) => log::info!("Dilithium scheme is verified"), + Err(e) => return Err(anyhow!("Dilithium scheme is not verified: {e}")), + }; // If no error has been returned then the signature is valid Ok(()) } @@ -380,8 +387,14 @@ impl KeysasKey for KeysasPQKey { // Important load oqs: oqs::init(); - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let (pk_dl, sk_dl) = pq_scheme.keypair()?; + let pq_scheme = match Sig::new(Algorithm::Dilithium5) { + Ok(pq_s) => pq_s, + Err(e) => return Err(anyhow!("Cannot construct new Dilithium algorithm: {e}")), + }; + let (pk_dl, sk_dl) = match pq_scheme.keypair() { + Ok((public, secret)) => (public, secret), + Err(e) => return Err(anyhow!("Cannot generate new Dilithium keypair: {e}")), + }; let kp_pq = KeysasPQKey { private_key: sk_dl, public_key: pk_dl, @@ -489,8 +502,14 @@ impl KeysasKey for KeysasPQKey { }; let content = info.to_der().with_context(|| "Failed to convert to DER")?; - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let signature = pq_scheme.sign(&content, &self.private_key)?; + let pq_scheme = match Sig::new(Algorithm::Dilithium5) { + Ok(pq_s) => pq_s, + Err(e) => return Err(anyhow!("Cannot construct new Dilithium algorithm: {e}")), + }; + let signature = match pq_scheme.sign(&content, &self.private_key) { + Ok(sig) => sig, + Err(e) => return Err(anyhow!("Cannot sign message: {e}")), + }; let csr = CertReq { info, @@ -506,21 +525,33 @@ impl KeysasKey for KeysasPQKey { fn message_sign(&self, message: &[u8]) -> Result, anyhow::Error> { oqs::init(); - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let signature = pq_scheme.sign(message, &self.private_key)?; + let pq_scheme = match Sig::new(Algorithm::Dilithium5) { + Ok(pq_s) => pq_s, + Err(e) => return Err(anyhow!("Cannot construct new Dilithium algorithm: {e}")), + }; + let signature = match pq_scheme.sign(message, &self.private_key) { + Ok(sig) => sig, + Err(e) => return Err(anyhow!("Cannot sign message: {e}")), + }; Ok(signature.into_vec()) } fn message_verify(&self, message: &[u8], signature: &[u8]) -> Result { oqs::init(); - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let pq_scheme = match Sig::new(Algorithm::Dilithium5) { + Ok(pq_s) => pq_s, + Err(e) => return Err(anyhow!("Cannot construct new Dilithium algorithm: {e}")), + }; let sig = match pq_scheme.signature_from_bytes(signature) { Some(s) => s, None => { return Err(anyhow!("Invalid signature input")); } }; - pq_scheme.verify(message, sig, &self.public_key)?; + match pq_scheme.verify(message, sig, &self.public_key) { + Ok(_) => log::info!("Dilithium scheme verified"), + Err(e) => return Err(anyhow!("Dilithium scheme not verified: {e}")), + } // If no error then the signature is valid Ok(true) } @@ -549,8 +580,14 @@ impl KeysasKey for KeysasPQKey { let content = tbs.to_der().with_context(|| "Failed to convert to DER")?; - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; - let signature = pq_scheme.sign(&content, &self.private_key)?; + let pq_scheme = match Sig::new(Algorithm::Dilithium5) { + Ok(pq_s) => pq_s, + Err(e) => return Err(anyhow!("Cannot construct new Dilithium algorithm: {e}")), + }; + let signature = match pq_scheme.sign(&content, &self.private_key) { + Ok(sig) => sig, + Err(e) => return Err(anyhow!("Cannot sign message: {e}")), + }; let cert = Certificate { tbs_certificate: tbs, diff --git a/keysas_lib/src/pki.rs b/keysas_lib/src/pki.rs index fd391c0..17e0c6c 100644 --- a/keysas_lib/src/pki.rs +++ b/keysas_lib/src/pki.rs @@ -187,7 +187,10 @@ pub fn generate_cert_from_csr( { // Validate CSR authenticity oqs::init(); - let pq_scheme = Sig::new(Algorithm::Dilithium5)?; + let pq_scheme = match Sig::new(Algorithm::Dilithium5) { + Ok(pq_s) => pq_s, + Err(e) => return Err(anyhow!("Cannot construct new Dilithium algorithm: {e}")), + }; if pq_scheme .verify( &csr.info.to_der()?, From 6dedef311b7acf2402e585fa3e43372a6ff2a772 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Tue, 13 Jun 2023 07:17:09 +0200 Subject: [PATCH 094/160] Rename component to keysas --- .../src/components/{AppGuichet.vue => AppKeysas.vue} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename keysas-frontend/src/components/{AppGuichet.vue => AppKeysas.vue} (99%) diff --git a/keysas-frontend/src/components/AppGuichet.vue b/keysas-frontend/src/components/AppKeysas.vue similarity index 99% rename from keysas-frontend/src/components/AppGuichet.vue rename to keysas-frontend/src/components/AppKeysas.vue index 32033d2..11518bf 100644 --- a/keysas-frontend/src/components/AppGuichet.vue +++ b/keysas-frontend/src/components/AppKeysas.vue @@ -97,7 +97,7 @@ @@ -64,7 +54,7 @@ import 'bootstrap-icons/font/bootstrap-icons.css' import {invoke} from "@tauri-apps/api" enum AuthorizationMode { - Blocked = 1, + Blocked = 0, Allowed_Read, Allowed_RW } @@ -76,8 +66,10 @@ declare interface UsbDevice { } declare interface File { + device: string, + id: number[], path: string, - allowed: boolean + authorization: AuthorizationMode } export default { @@ -93,27 +85,58 @@ export default { usb_device: {} as UsbDevice, } }, - mounted() { + async mounted() { this.usb_list.push({ name: "Kingston USB", - path: "D:/", + path: "D:", authorization: AuthorizationMode.Allowed_RW }); + + await listen('file_update', (event) => { + this.refreshFileList(event.payload as string); + }); }, methods: { - showUsbDevice(usb_device: UsbDevice) { + async refreshFileList(device_path: string) { + if (device_path === this.usb_device.path) { + invoke('get_file_list', {devicePath: device_path}) + .then((result) => { + const file_list = JSON.parse(result as string); + file_list.forEach((file: File) => { + if (!this.file_list.some(f => f.path === file.path)) { + this.file_list.push(file); + } + }); + }) + .catch((error) => console.error(error)); + } + }, + async showUsbDevice(usb_device: UsbDevice) { // Set the selected device this.usb_device = usb_device; // Fetch the file list from the backend - invoke('get_file_list') - .then((result) => console.log(result)) - .catch((error) => console.error(error)); + this.refreshFileList(usb_device.path); // Display the details window this.showUsbList = false; this.showUsbDetails = true; }, + async toggleFileAuth(file: File, new_mode: AuthorizationMode) { + let auth = 0; // Blocked + if (new_mode == AuthorizationMode.Allowed_Read) { + auth = 1; + } else if (new_mode == AuthorizationMode.Allowed_RW) { + auth = 2; + } + console.log("New authorization"); + invoke('toggle_file_auth', {device: file.device, id: file.id, path: file.path, newAuth: auth}) + .then((result) => { + console.log("New authorization result OK"); + file.authorization = new_mode; + }) + .catch((error) => alert("Toggle file authorization failed")); + }, backToUsbList() { this.showUsbDetails = false; this.showUsbList = true; @@ -124,4 +147,48 @@ export default { diff --git a/keysas-firewall/usbfilter/USBFilterDriver.c b/keysas-firewall/usbfilter/USBFilterDriver.c new file mode 100644 index 0000000..8aff8fd --- /dev/null +++ b/keysas-firewall/usbfilter/USBFilterDriver.c @@ -0,0 +1,517 @@ +/*++ + +Copyright (c) 2023 Luc Bonnafoux + +Module Name: + + USBFilterDriver.c + +Abstract: + + This filter monitors USB device connections. + +Environment: + + Kernel mode + +--*/ + +#include +#include + +/*--------------------------------------- +- +- Global definitions +- +-----------------------------------------*/ +#define KEYSAS_USBFILTER_POOL_TAG 'FUeK' + + +/*--------------------------------------- +- +- Function declarations +- +-----------------------------------------*/ + +NTSTATUS +DriverEntry( + _In_ PDRIVER_OBJECT DriverObject, + _In_ PUNICODE_STRING RegistryPath +); + +NTSTATUS +KUFDeviceAddEvt( + _In_ WDFDRIVER Driver, + _Inout_ PWDFDEVICE_INIT DeviceInit +); + +VOID +KUFEvtDeviceControl( + _In_ WDFQUEUE Queue, + _In_ WDFREQUEST Request, + _In_ size_t OutputBufferLength, + _In_ size_t InputBufferLength, + _In_ ULONG IoControlCode +); + +NTSTATUS +KUFPnpQueryDeviceCallback( + IN WDFDEVICE Device, + IN PIRP Irp +); + +NTSTATUS +KUFGetDeviceInfo( + _In_ PDEVICE_OBJECT Device, + _In_ BUS_QUERY_ID_TYPE Type, + _Outptr_opt_ PWCHAR* Information +); + +NTSTATUS +KUFIsUsbHub( + _In_ PDEVICE_OBJECT Device, + _Out_ PBOOLEAN IsHub +); + +IO_COMPLETION_ROUTINE KUFDeviceRelationsPostProcessing; + +#ifdef ALLOC_PRAGMA +#pragma alloc_text (INIT, DriverEntry) +#pragma alloc_text (PAGE, KUFDeviceAddEvt) +#pragma alloc_text (PAGE, KUFEvtDeviceControl) +#pragma alloc_text (PAGE, KUFPnpQueryDeviceCallback) +#pragma alloc_text (PAGE, KUFGetDeviceInfo) +#pragma alloc_text (PAGE, KUFIsUsbHub) +#endif + +/*--------------------------------------- +- +- Function implementations +- +-----------------------------------------*/ + +NTSTATUS +KUFGetDeviceInfo( + _In_ PDEVICE_OBJECT Device, + _In_ BUS_QUERY_ID_TYPE Type, + _Outptr_opt_ PWCHAR * Information +) +/*++ +Routine Description: + This routine is called to get information on a PDO + +Arguments: + Device - Pointer to the target device + Type - Type of information requested + Information - Output pointer for the information. It is allocated by the function + +Return Value: + Return STATUS_SUCCESS + +IRQL: + Must be called at PASSIVE_LEVEL +--*/ +{ + NTSTATUS result = STATUS_UNSUCCESSFUL; + KEVENT ke; + IO_STATUS_BLOCK ios = { 0 }; + PIRP irp = NULL; + PIO_STACK_LOCATION stack = NULL; + NTSTATUS nts = STATUS_UNSUCCESSFUL; + size_t bufferLength = 0; + + // Test inputs provided + if (NULL == Device || NULL == Information) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFGetDeviceInfo: Invalid inputs\n")); + goto cleanup; + } + + KeInitializeEvent(&ke, NotificationEvent, FALSE); + + irp = IoBuildSynchronousFsdRequest( + IRP_MJ_PNP, + Device, + NULL, + 0, + NULL, + &ke, + &ios + ); + + if (NULL == irp) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFGetDeviceInfo: Failed to allocate IRP\n")); + goto cleanup; + } + + irp->IoStatus.Status = STATUS_NOT_SUPPORTED; + + stack = IoGetNextIrpStackLocation(irp); + + if (NULL == stack) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFGetDeviceInfo: Failed to get stack location\n")); + goto cleanup; + } + + stack->MinorFunction = IRP_MN_QUERY_ID; + stack->Parameters.QueryId.IdType = Type; + + nts = IoCallDriver(Device, irp); + + if (STATUS_PENDING == nts) { + KeWaitForSingleObject(&ke, Executive, KernelMode, FALSE, NULL); + } + + if (NT_SUCCESS(nts)) { + bufferLength = (wcslen((WCHAR*)ios.Information)+1) * sizeof(WCHAR); + *Information = (PWCHAR)ExAllocatePool2(NonPagedPool, bufferLength, KEYSAS_USBFILTER_POOL_TAG); + if (NULL != *Information) { + RtlCopyMemory(*Information, (PWCHAR) ios.Information, bufferLength - 2); + result = STATUS_SUCCESS; + } + else { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFGetDeviceInfo: Failed to allocate output buffer\n")); + } + } + +cleanup: + + return result; +} + +NTSTATUS +KUFIsUsbHub( + _In_ PDEVICE_OBJECT Device, + _Out_ PBOOLEAN IsHub +) +/*++ +Routine Description: + This routine test if a physical device is a USB root Hub + The decision is made on the Device ID. For USB root hubs it starts with: + "USB\ROOT_HUB', "NUSB3\ROOT_HUB" or "IUSB3\ROOT_HUB" + TODO - Verify the exhaustivity of the list + +Arguments: + Device - Pointer to the device to test + IsHub - Boolean containing the result of the test + +Return: + Returns STATUS_SUCCESS if no error occured. + +IRQL: + Must be called at PASSIVE_LEVEL +--*/ +{ + NTSTATUS result = STATUS_UNSUCCESSFUL; + PWCHAR deviceId = NULL; + + // Test inputs + if (NULL == Device || NULL == IsHub) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFIsUsbHub: Invalid inputs\n")); + goto cleanup; + } + // Set default to FALSE + *IsHub = FALSE; + + // Get DeviceID + if (STATUS_SUCCESS != KUFGetDeviceInfo( + Device, + BusQueryDeviceID, + &deviceId + )) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFIsUsbHub: Failed to get Device ID\n")); + goto cleanup; + } + + // Compare with reference strings + if (!wcsncmp(deviceId, L"USB\\ROOT_HUB", 13) + || !wcsncmp(deviceId, L"NUSB3\\ROOT_HUB", 15) + || !wcsncmp(deviceId, L"IUSB3\\ROOT_HUB", 15)) { + // It is a USB Hub + *IsHub = TRUE; + } + + result = STATUS_SUCCESS; + +cleanup: + if (NULL != deviceId) { + ExFreePoolWithTag(deviceId, KEYSAS_USBFILTER_POOL_TAG); + } + + return result; +} + +NTSTATUS +DriverEntry( + _In_ PDRIVER_OBJECT DriverObject, + _In_ PUNICODE_STRING RegistryPath +) +/*++ +Routine Description: + This is the initialization routine for this Keysas driver. + +Arguments: + DriverObject - Pointer to driver object created by the system to + represent this driver. + RegistryPath - Unicode string identifying where the parameters for this + driver are located in the registry. + +Return Value: + Returns STATUS_SUCCESS. + +IRQL: + Routine called at PASSIVE_LEVEL in system thread context. +--*/ +{ + WDF_DRIVER_CONFIG config = { 0 }; + + NTSTATUS status = STATUS_SUCCESS; + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!DriverEntry: Entered\n")); + + WDF_DRIVER_CONFIG_INIT(&config, KUFDeviceAddEvt); + + status = WdfDriverCreate( + DriverObject, + RegistryPath, + WDF_NO_OBJECT_ATTRIBUTES, + &config, + WDF_NO_HANDLE + ); + + if (!NT_SUCCESS(status)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!DriverEntry: WdfDriverCreate failed with status: %0x8x\n", + status)); + } + + status = STATUS_SUCCESS; + + return status; +} + +NTSTATUS +KUFDeviceAddEvt( + _In_ WDFDRIVER Driver, + _Inout_ PWDFDEVICE_INIT DeviceInit +) +/*++ +Routine Description: + Called by the system when a new device is found + +Arguments: + Driver - Pointer to our driver + DeviceInit - New device initialization structure + +Return Value: + Returns STATUS_SUCCESS. + +IRQL: + Routine called at PASSIVE_LEVEL in system thread context. +--*/ +{ + NTSTATUS status = STATUS_SUCCESS; + WDFDEVICE wdfDevice = { 0 }; + WDF_IO_QUEUE_CONFIG ioQueueConfig = { 0 }; + WDF_OBJECT_ATTRIBUTES wdfObjectAttr = { 0 }; + UCHAR minorFunctions = 0; + + UNREFERENCED_PARAMETER(Driver); + + PAGED_CODE(); + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFDeviceAddEvt: Entered\n")); + + // Set the new instance as a filter + WdfDeviceInitSetDeviceType(DeviceInit, FILE_DEVICE_BUS_EXTENDER); + WdfFdoInitSetFilter(DeviceInit); + + minorFunctions = IRP_MN_QUERY_DEVICE_RELATIONS; + status = WdfDeviceInitAssignWdmIrpPreprocessCallback( + DeviceInit, + KUFPnpQueryDeviceCallback, + IRP_MJ_PNP, + &minorFunctions, + 1 + ); + + if (!NT_SUCCESS(status)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFDeviceAddEvt: WdfDeviceInitAssignWdmIrpPreprocessCallback failed with status: %0x8x\n", + status)); + goto cleanup; + } + + WDF_OBJECT_ATTRIBUTES_INIT(&wdfObjectAttr); + + // Create the new instance for the device + status = WdfDeviceCreate( + &DeviceInit, + &wdfObjectAttr, + &wdfDevice + ); + + if (!NT_SUCCESS(status)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFDeviceAddEvt: WdfDeviceCreate failed with status: %0x8x\n", + status)); + goto cleanup; + } + + // Create a queue to handle the requests + WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE( + &ioQueueConfig, + WdfIoQueueDispatchParallel + ); + + ioQueueConfig.EvtIoDeviceControl = KUFEvtDeviceControl; + + status = WdfIoQueueCreate( + wdfDevice, + &ioQueueConfig, + WDF_NO_OBJECT_ATTRIBUTES, + WDF_NO_HANDLE + ); + + if (!NT_SUCCESS(status)) { + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFDeviceAddEvt: WdfIoQueueCreate failed with status: %0x8x\n", + status)); + goto cleanup; + } + + status = STATUS_SUCCESS; + +cleanup: + + return status; +} + +VOID +KUFEvtDeviceControl( + _In_ WDFQUEUE Queue, + _In_ WDFREQUEST Request, + _In_ size_t OutputBufferLength, + _In_ size_t InputBufferLength, + _In_ ULONG IoControlCode +) +/*++ +Routine Description: + Handler for I/O request to the device + +Arguments: + Queue - Pointer to the framework queue + Request - Pointer to the request + OutputBufferLength - Length, in bytes, of the request's output buffer + InputBufferLength - Length, in bytes, of the request's input buffer + IoControlCode - IOCTL associated with the request + +Return Value: + Returns STATUS_SUCCESS. + +IRQL: + Can be called at IRQL <= DISPATCH_LEVEL +--*/ +{ + WDF_REQUEST_SEND_OPTIONS sendOpts = { 0 }; + WDFDEVICE wdfDevice = NULL; + NTSTATUS status; + + UNREFERENCED_PARAMETER(OutputBufferLength); + UNREFERENCED_PARAMETER(InputBufferLength); + UNREFERENCED_PARAMETER(IoControlCode); + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFEvtDeviceControl: Entered\n")); + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFEvtDeviceControl: Request 0x%p - IoControlCode 0x%p\n", + Request, IoControlCode)); + + WDF_REQUEST_SEND_OPTIONS_INIT(&sendOpts, WDF_REQUEST_SEND_OPTION_SEND_AND_FORGET); + + wdfDevice = WdfIoQueueGetDevice(Queue); + + if (!WdfRequestSend(Request, WdfDeviceGetIoTarget(wdfDevice), &sendOpts)) { + status = WdfRequestGetStatus(Request); + WdfRequestComplete(Request, STATUS_SUCCESS); + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFEvtDeviceControl: WdfRequestSend failed\n")); + } + +} + +_Use_decl_annotations_ +NTSTATUS +KUFDeviceRelationsPostProcessing( + PDEVICE_OBJECT Device, + PIRP Irp, + PVOID Context +) +/*++ +Routine Description: + Completion routine called to filter device relations list provided by USB hubs to the PNP Manager + +Arguments: + Device - Pointer to the device object + Irp - Pointer to the IRP for the current request + Context - Additional data passed in the pre process stage, not used +--*/ +{ + NTSTATUS result = STATUS_SUCCESS; + + UNREFERENCED_PARAMETER(Context); + UNREFERENCED_PARAMETER(Device); + UNREFERENCED_PARAMETER(Irp); + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFDeviceRelationsPostProcessing: Entered\n")); + + return result; +} + +NTSTATUS +KUFPnpQueryDeviceCallback( + IN WDFDEVICE Device, + IN PIRP Irp +) +/*++ +Routine Description: + This routine is called when a IRP_MN_QUERY_DEVICE_RELATIONS is received + +Arguments: + Device - Pointer to the device object for this device + Irp - Pointer to the IRP for the current request + +Return Value: + The function value is the final status of the call + +IRQL: + Is called at the IRQL of the IRP calling thread +--*/ +{ + NTSTATUS status = STATUS_SUCCESS; + PIO_STACK_LOCATION irpStack = NULL; + + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Keysas - USBFilter!KUFPnpQueryDeviceCallback: Entered\n")); + irpStack = IoGetCurrentIrpStackLocation(Irp); + + if (NULL != irpStack + && BusRelations == irpStack->Parameters.QueryDeviceRelations.Type) { + // Register a callback to filter the list of devices returned by the hub + IoCopyCurrentIrpStackLocationToNext(Irp); + + IoSetCompletionRoutine( + Irp, + KUFDeviceRelationsPostProcessing, + NULL, // No context + TRUE, // Call on successful IRP + FALSE, // Don't invoke on error + FALSE // Dont't invoke on canceled IRP + ); + } + else { + // No callback for post processing the IRP + // Return the IRP to the framework + IoSkipCurrentIrpStackLocation(Irp); + } + + status = WdfDeviceWdmDispatchPreprocessedIrp( + Device, + Irp + ); + + return status; +} \ No newline at end of file diff --git a/keysas-firewall/usbfilter/usbfilter.inf b/keysas-firewall/usbfilter/usbfilter.inf new file mode 100644 index 0000000..dfee57f --- /dev/null +++ b/keysas-firewall/usbfilter/usbfilter.inf @@ -0,0 +1,47 @@ +; +; usbfilter.inf +; + +[Version] +Signature="$WINDOWS NT$" +Class = USB +ClassGuid = {36fc9e60-c465-11cf-8056-444553540000} +Provider=%ManufacturerName% +CatalogFile=usbfilter.cat +DriverVer = 05/10/2023,16.21.56.700 +PnpLockdown=0 + +[DefaultInstall.NTamd64] +CopyFiles=@usbfilter.sys +Addreg = usbfilter.AddReg + +[DestinationDirs] +DefaultDestDir = 12 + +[usbfilter.AddReg] +HKLM, System\CurrentControlSet\Control\Class\{36fc9e60-c465-11cf-8056-444553540000}, UpperFilters, 0x00010008, usbfilter + +[DefaultInstall.NTamd64.Services] +AddService = usbfilter, , usbfilter.Service.Install + +[usbfilter.Service.Install] +DisplayName = "usbfilter" +Description = "Keysas USB filter" +ServiceBinary = %12%\usbfilter.sys +ServiceType = 1 +StartType = 0 +ErrorControl = 1 + +[SourceDisksFiles] +usbfilter.sys=1 + +[SourceDisksNames] +1 = %DiskId1% + +[Strings] +; SPSVCINST_ASSOCSERVICE= 0x00000002 +ManufacturerName="Keysas" +DiskName = "usbfilter Installation Disk" +usbfilter.DeviceDesc = "usbfilter Device" +usbfilter.SVCDESC = "usbfilter Service" +DiskId1 = "USB upper filter driver" \ No newline at end of file diff --git a/keysas-firewall/usbfilter/usbfilter.vcxproj b/keysas-firewall/usbfilter/usbfilter.vcxproj new file mode 100644 index 0000000..dcb5e08 --- /dev/null +++ b/keysas-firewall/usbfilter/usbfilter.vcxproj @@ -0,0 +1,127 @@ + + + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + {A1988889-1DC2-4E8A-B301-3094926F732E} + {1bc93793-694f-48fe-9372-81e2b05556fd} + v4.5 + 12.0 + Debug + x64 + usbfilter + + + + Windows10 + true + WindowsKernelModeDriver10.0 + Driver + KMDF + Universal + false + + + Windows10 + false + WindowsKernelModeDriver10.0 + Driver + KMDF + Universal + false + + + Windows10 + true + WindowsKernelModeDriver10.0 + Driver + KMDF + Universal + + + Windows10 + false + WindowsKernelModeDriver10.0 + Driver + KMDF + Universal + + + + + + + + + + + DbgengKernelDebugger + C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\km\x86;C:\Program Files (x86)\Windows Kits\10\Lib\wdf\kmdf\x86\1.33;$(LibraryPath) + + + DbgengKernelDebugger + C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\km\x86;C:\Program Files (x86)\Windows Kits\10\Lib\wdf\kmdf\x86\1.33;$(LibraryPath) + + + DbgengKernelDebugger + + + DbgengKernelDebugger + + + + sha256 + + + $(KMDF_INC_PATH)$(KMDF_VER_PATH);C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\km;%(AdditionalIncludeDirectories) + _AMD64_ + + + + + sha256 + + + $(KMDF_INC_PATH)$(KMDF_VER_PATH);C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\km;%(AdditionalIncludeDirectories) + _AMD64_ + + + + + sha256 + + + + + sha256 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/keysas-firewall/usbfilter/usbfilter.vcxproj.filters b/keysas-firewall/usbfilter/usbfilter.vcxproj.filters new file mode 100644 index 0000000..4c9c4a6 --- /dev/null +++ b/keysas-firewall/usbfilter/usbfilter.vcxproj.filters @@ -0,0 +1,31 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {8E41214B-6785-4CFE-B992-037D68949A14} + inf;inv;inx;mof;mc; + + + + + Driver Files + + + + + Source Files + + + \ No newline at end of file diff --git a/keysas-firewall/usbfilter/usbfilter.vcxproj.user b/keysas-firewall/usbfilter/usbfilter.vcxproj.user new file mode 100644 index 0000000..fa0bdd3 --- /dev/null +++ b/keysas-firewall/usbfilter/usbfilter.vcxproj.user @@ -0,0 +1,31 @@ + + + + True + None + + + C:\Program Files (x86)\Windows Kits\10\Testing\Tests\Utilities\DefaultDriverPackageInstallationTask.dll + TestVM + TestVM + + Microsoft.DriverKit.DefaultDriverPackageInstallationClass.PerformDefaultDriverPackageInstallation + + FvOn + true + + + True + None + + + C:\Program Files (x86)\Windows Kits\10\Testing\Tests\Utilities\DefaultDriverPackageInstallationTask.dll + TestVM + TestVM + + Microsoft.DriverKit.DefaultDriverPackageInstallationClass.PerformDefaultDriverPackageInstallation + + FvOn + true + + \ No newline at end of file From cfcf7cb0eb5a00ddaf8992dc246e73c8a1ffb125 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Mon, 31 Jul 2023 10:41:44 +0200 Subject: [PATCH 157/160] Update url --- documentation/user_documentation/raspberry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/user_documentation/raspberry.rst b/documentation/user_documentation/raspberry.rst index f48fee6..96737a6 100644 --- a/documentation/user_documentation/raspberry.rst +++ b/documentation/user_documentation/raspberry.rst @@ -22,7 +22,7 @@ The code is entirely written in Rust, sandboxed, and follows the principle of le Download ========= - `keysas-sd-v2.0 `_ (`sha256 `_) -- `keysas-admin-v2.0 (GNU/Linux) `_ (`sha256 `_) +- `keysas-admin-v2.0 (GNU/Linux) `_ (`sha256 `_) The downloaded image will automatically resize according to the size of your MicroSD card. To copy the **Keysas** station image to your SD card: From 63f54dcaa5a05000e569ec1715f7c4dab35077e3 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Mon, 31 Jul 2023 11:25:22 +0200 Subject: [PATCH 158/160] Fix some typo + styling --- documentation/user_documentation/conf.py | 4 ++-- .../user_documentation/installation.rst | 4 ++-- .../user_documentation/windows_firewall.rst | 21 +++++++++---------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/documentation/user_documentation/conf.py b/documentation/user_documentation/conf.py index 79b4cab..ecd5188 100644 --- a/documentation/user_documentation/conf.py +++ b/documentation/user_documentation/conf.py @@ -24,9 +24,9 @@ author = 'Stephane N' # The short X.Y version -version = '2.0' +version = '2.1' # The full version, including alpha/beta/rc tags -release = 'v2.0' +release = 'v2.1' # -- General configuration --------------------------------------------------- diff --git a/documentation/user_documentation/installation.rst b/documentation/user_documentation/installation.rst index 192a2e4..d784b2d 100644 --- a/documentation/user_documentation/installation.rst +++ b/documentation/user_documentation/installation.rst @@ -38,8 +38,8 @@ Getting **Keysas** ------------------- A pre-compiled **Keysas** binary is at your -disposal, you can choose and download a specific version of **Keysas** -using the :ref:`download section `. +disposal. We recommend to use the latest version here: +https://github.com/r3dlight/keysas/tags Download the following files of lastest stable version. * keysas-vx.y.z.zip diff --git a/documentation/user_documentation/windows_firewall.rst b/documentation/user_documentation/windows_firewall.rst index 2973e07..f232871 100644 --- a/documentation/user_documentation/windows_firewall.rst +++ b/documentation/user_documentation/windows_firewall.rst @@ -2,13 +2,12 @@ Windows USB firewall ******************** -Keysas system also includes a **USB firewall** for Windows in order to check that: -- USB stick plugged on user laptop have been checked by a Keysas station +**Keysas** system also includes a **USB firewall** for Windows in order to check that: +- USB stick plugged on user laptop have been checked by a Keysas station; - Files on the USB stick have been validated by the station. .. warning:: - -**USB firewall** has for now only been tested on Windows 10 laptop in debug mode. + **USB firewall** has only been tested on Windows 10 laptop in debug mode for now. Architecture ============ @@ -19,7 +18,7 @@ The firewall is composed of four elements: - A USB bus filter driver - A minifilter (driver to filter system calls towards the filesystem) - In userspace - - A daemon that supervises the two drivers and checks files and reports based on the system security policy + - A daemon supervising the two drivers and checks files and reports based on the system security policy - A tray application to allow the end user to control the security settings Security Policy configuration @@ -28,16 +27,16 @@ Security Policy configuration System security policy is configured from a TOML file at the base of the Daemon directory. The policy is configured with: -- 'disable_unsigned_usb': if set to 'true' unsigned usb devices are allowed. No checks are performed on files on these devices. -- 'allow_user_usb_authorization': if set to 'true' grant the user the ability to manually allow unsigned USB devices. No checks are performed on files on these devices. -- 'allow_user_file_read': if set to 'true' grant the user the ability to manually allow read access to an unsigned file. -- 'allow_user_file_write': if set to 'true' grant the user the ability to manually allow write access to file on a USB device. 'allow_user_file_read' must also be set to true. +- 'disable_unsigned_usb': if set to 'true', unsigned usb devices are allowed. No checks are performed on files on these devices. +- 'allow_user_usb_authorization': if set to 'true', grant the user the ability to manually allow unsigned USB devices. No checks are performed on files on these devices. +- 'allow_user_file_read': if set to 'true', grant the user the ability to manually allow read access to an unsigned file. +- 'allow_user_file_write': if set to 'true', grant the user the ability to manually allow write access to file on a USB device. 'allow_user_file_read' must also be set to true. -If parameters are missing from the configuration file they are considered to be set to 'false'. +If parameters are missing from the configuration file, they are considered to be set to 'false'. CA certificates must be provided to the daemon. The path to the pem files is given as arguments to the command line. -The comple command line is +The complete command line is ```bash ./keysas-usbfilter-daemon.exe -config -ca_cl -ca_pq From 30e3ee2c48650685ab11fd409d5c804ca9f38163 Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Mon, 31 Jul 2023 11:25:52 +0200 Subject: [PATCH 159/160] Bump version to 2.1 --- keysas-admin/src-tauri/Cargo.toml | 2 +- keysas-backend/Cargo.toml | 2 +- keysas-core/Cargo.toml | 2 +- keysas-frontend/package-lock.json | 4 ++-- keysas-frontend/package.json | 2 +- keysas-io/Cargo.toml | 2 +- keysas-sign/Cargo.toml | 2 +- keysas_lib/Cargo.toml | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/keysas-admin/src-tauri/Cargo.toml b/keysas-admin/src-tauri/Cargo.toml index 843e231..63c15c5 100644 --- a/keysas-admin/src-tauri/Cargo.toml +++ b/keysas-admin/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keysas-admin" -version = "2.0.0" +version = "2.1.0" description = "Keysas stations administration application" authors = ["Stephane N", "Luc Bonnafoux"] license = "GPL-3.0" diff --git a/keysas-backend/Cargo.toml b/keysas-backend/Cargo.toml index d33ab1a..33b36b7 100644 --- a/keysas-backend/Cargo.toml +++ b/keysas-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keysas-backend" -version = "2.0.0" +version = "2.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/keysas-core/Cargo.toml b/keysas-core/Cargo.toml index 5d00667..9ccb694 100644 --- a/keysas-core/Cargo.toml +++ b/keysas-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keysas-core" -version = "2.0.0" +version = "2.1.0" edition = "2021" [dependencies] diff --git a/keysas-frontend/package-lock.json b/keysas-frontend/package-lock.json index 7823ac7..8b1c464 100644 --- a/keysas-frontend/package-lock.json +++ b/keysas-frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "keysas-frontend", - "version": "0.1.0", + "version": "2.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "keysas-frontend", - "version": "0.1.0", + "version": "2.1.0", "dependencies": { "@intlify/unplugin-vue-i18n": "^0.8.1", "animate.css": "^4.1.1", diff --git a/keysas-frontend/package.json b/keysas-frontend/package.json index 93d80c7..fb0c6cf 100644 --- a/keysas-frontend/package.json +++ b/keysas-frontend/package.json @@ -1,7 +1,7 @@ { "name": "keysas-frontend", "private": true, - "version": "0.1.0", + "version": "2.1.0", "type": "module", "scripts": { "dev": "vite", diff --git a/keysas-io/Cargo.toml b/keysas-io/Cargo.toml index cb5581f..5ab61b5 100644 --- a/keysas-io/Cargo.toml +++ b/keysas-io/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keysas-io" -version = "2.0.0" +version = "2.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/keysas-sign/Cargo.toml b/keysas-sign/Cargo.toml index 9cc1f65..06e1cb1 100644 --- a/keysas-sign/Cargo.toml +++ b/keysas-sign/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keysas-sign" -version = "1.2.0" +version = "2.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/keysas_lib/Cargo.toml b/keysas_lib/Cargo.toml index 938fe2e..c5cddf6 100644 --- a/keysas_lib/Cargo.toml +++ b/keysas_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keysas_lib" -version = "2.0.0" +version = "2.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 0fc7a4192ef0c8bc571a40c6207ae117ffac6b5b Mon Sep 17 00:00:00 2001 From: Stephane Neveu Date: Mon, 31 Jul 2023 11:27:43 +0200 Subject: [PATCH 160/160] Specifying the support for Debian 12 only --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b71937..441aca1 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Files are passed between daemons as raw file descriptors and using abstract sock ## Installation -On Debian stable (Bookwoom): +On Debian stable (Bookwoom only): ```bash apt -qy install -y libyara-dev libyara9 wget cmake make lsb-release software-properties-common libseccomp-dev clamav-daemon clamav-freshclam pkg-config git bash libudev-dev libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev