keygen_rs/
machine_file.rs

1use base64::{engine::general_purpose, Engine};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{
7    certificate::{
8        validate_certificate_meta, Certificate, CertificateFileAttributes, CertificateFileMeta,
9    },
10    config::get_config,
11    decryptor::Decryptor,
12    errors::Error,
13    license::License,
14    machine::{Machine, MachineAttributes},
15    verifier::Verifier,
16    KeygenResponseData,
17};
18
19#[derive(Debug, Serialize, Deserialize)]
20pub struct MachineFileDataset {
21    pub license: License,
22    pub machine: Machine,
23    pub issued: DateTime<Utc>,
24    pub expiry: DateTime<Utc>,
25    pub ttl: i32,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct MachineFile {
30    pub id: String,
31    pub certificate: String,
32    pub issued: DateTime<Utc>,
33    pub expiry: DateTime<Utc>,
34    pub ttl: i32,
35}
36
37impl Into<MachineFile> for CertificateFileAttributes {
38    fn into(self) -> MachineFile {
39        MachineFile {
40            id: "".into(),
41            certificate: self.certificate,
42            issued: self.issued,
43            expiry: self.expiry,
44            ttl: self.ttl,
45        }
46    }
47}
48
49impl MachineFile {
50    pub(crate) fn from(data: KeygenResponseData<CertificateFileAttributes>) -> MachineFile {
51        MachineFile {
52            id: data.id,
53            ..data.attributes.into()
54        }
55    }
56
57    pub fn from_cert(key: &str, content: &str) -> Result<MachineFile, Error> {
58        let dataset = Self::_decrypt(key, content)?;
59        Ok(MachineFile {
60            id: dataset.machine.id.clone(),
61            certificate: content.to_string(),
62            issued: dataset.issued,
63            expiry: dataset.expiry,
64            ttl: dataset.ttl,
65        })
66    }
67
68    pub fn verify(&self) -> Result<(), Error> {
69        self.validate_ttl()?;
70
71        let config = get_config();
72
73        if let Some(public_key) = config.public_key {
74            let verifier = Verifier::new(public_key);
75            verifier.verify_machine_file(self)
76        } else {
77            Err(Error::PublicKeyMissing)
78        }
79    }
80
81    pub fn validate_ttl(&self) -> Result<(), Error> {
82        let now = Utc::now();
83        if now > self.expiry {
84            let dataset = self.decrypt("").unwrap_or_else(|_| {
85                use std::collections::HashMap;
86                MachineFileDataset {
87                    license: License::from(crate::KeygenResponseData {
88                        id: "".to_string(),
89                        r#type: "licenses".to_string(),
90                        attributes: crate::license::LicenseAttributes {
91                            name: None,
92                            key: "".to_string(),
93                            expiry: None,
94                            status: Some("".to_string()),
95                            uses: Some(0),
96                            max_machines: None,
97                            max_cores: None,
98                            max_uses: None,
99                            max_processes: None,
100                            max_users: None,
101                            protected: None,
102                            suspended: None,
103                            permissions: None,
104                            metadata: HashMap::new(),
105                        },
106                        relationships: crate::KeygenRelationships::default(),
107                    }),
108                    machine: Machine::from(crate::KeygenResponseData {
109                        id: "".to_string(),
110                        r#type: "machines".to_string(),
111                        attributes: crate::machine::MachineAttributes {
112                            fingerprint: "".to_string(),
113                            name: None,
114                            platform: None,
115                            hostname: None,
116                            ip: None,
117                            cores: None,
118                            metadata: None,
119                            require_heartbeat: false,
120                            heartbeat_status: "".to_string(),
121                            heartbeat_duration: None,
122                            created: Utc::now(),
123                            updated: Utc::now(),
124                        },
125                        relationships: crate::KeygenRelationships::default(),
126                    }),
127                    issued: self.issued,
128                    expiry: self.expiry,
129                    ttl: self.ttl,
130                }
131            });
132            Err(Error::MachineFileExpired(dataset))
133        } else {
134            Ok(())
135        }
136    }
137
138    pub fn decrypt(&self, key: &str) -> Result<MachineFileDataset, Error> {
139        Self::_decrypt(key, &self.certificate)
140    }
141
142    pub fn certificate(&self) -> Result<Certificate, Error> {
143        Self::_certificate(self.certificate.clone())
144    }
145
146    fn _decrypt(key: &str, content: &str) -> Result<MachineFileDataset, Error> {
147        let cert = Self::_certificate(content.to_string())?;
148
149        match cert.alg.as_str() {
150            "aes-256-gcm+rsa-pss-sha256" | "aes-256-gcm+rsa-sha256" => {
151                return Err(Error::LicenseFileNotSupported(cert.alg.clone()));
152            }
153            "aes-256-gcm+ed25519" => {}
154            _ => return Err(Error::LicenseFileNotEncrypted),
155        }
156
157        let decryptor = Decryptor::new(key.to_string());
158        let data = decryptor.decrypt_certificate(&cert)?;
159        let dataset: Value =
160            serde_json::from_slice(&data).map_err(|e| Error::MachineFileInvalid(e.to_string()))?;
161
162        let meta: CertificateFileMeta = serde_json::from_value(dataset["meta"].clone())
163            .map_err(|e| Error::LicenseFileInvalid(e.to_string()))?;
164
165        // Find type = "licenses" element in dataset["included"] array
166        let license_data = dataset["included"]
167            .as_array()
168            .ok_or(Error::MachineFileInvalid(
169                "Included data is not an array".into(),
170            ))?
171            .iter()
172            .find(|v| v["type"] == "licenses")
173            .ok_or(Error::MachineFileInvalid(
174                "No license data found in included data".into(),
175            ))?;
176        let license = License::from(serde_json::from_value(license_data.clone())?);
177
178        let machine_data: KeygenResponseData<MachineAttributes> =
179            serde_json::from_value(dataset["data"].clone())
180                .map_err(|e| Error::MachineFileInvalid(e.to_string()))?;
181        let machine = Machine::from(machine_data);
182
183        let dataset = MachineFileDataset {
184            license,
185            machine,
186            issued: meta.issued,
187            expiry: meta.expiry,
188            ttl: meta.ttl,
189        };
190
191        if let Err(err) = validate_certificate_meta(&meta) {
192            match err {
193                Error::CerificateFileExpired => Err(Error::MachineFileExpired(dataset)),
194                _ => Err(err),
195            }
196        } else {
197            Ok(dataset)
198        }
199    }
200
201    fn _certificate(certificate: String) -> Result<Certificate, Error> {
202        let payload = certificate.trim();
203        let payload = payload
204            .strip_prefix("-----BEGIN MACHINE FILE-----")
205            .and_then(|s| s.strip_suffix("-----END MACHINE FILE-----"))
206            .ok_or(Error::MachineFileInvalid(
207                "Invalid machine file format".into(),
208            ))?
209            .trim()
210            .replace("\n", "");
211
212        let decoded = general_purpose::STANDARD
213            .decode(payload)
214            .map_err(|e| Error::MachineFileInvalid(e.to_string()))?;
215
216        let cert: Certificate = serde_json::from_slice(&decoded)
217            .map_err(|e| Error::MachineFileInvalid(e.to_string()))?;
218
219        Ok(cert)
220    }
221}