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 component::Component,
11 decryptor::Decryptor,
12 entitlement::Entitlement,
13 errors::Error,
14 group::Group,
15 license::License,
16 license_file::IncludedResources,
17 machine::{Machine, MachineAttributes},
18 verifier::Verifier,
19 KeygenResponseData,
20};
21
22#[derive(Debug, Serialize, Deserialize)]
23pub struct MachineFileDataset {
24 pub license: License,
25 pub machine: Machine,
26 pub issued: DateTime<Utc>,
27 pub expiry: DateTime<Utc>,
28 pub ttl: i32,
29 #[serde(default)]
30 pub included: Option<IncludedResources>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct MachineFile {
35 pub id: String,
36 pub certificate: String,
37 pub issued: DateTime<Utc>,
38 pub expiry: DateTime<Utc>,
39 pub ttl: i32,
40}
41
42impl From<CertificateFileAttributes> for MachineFile {
43 fn from(val: CertificateFileAttributes) -> Self {
44 MachineFile {
45 id: "".into(),
46 certificate: val.certificate,
47 issued: val.issued,
48 expiry: val.expiry,
49 ttl: val.ttl,
50 }
51 }
52}
53
54impl MachineFile {
55 pub(crate) fn from(data: KeygenResponseData<CertificateFileAttributes>) -> MachineFile {
56 MachineFile {
57 id: data.id,
58 ..data.attributes.into()
59 }
60 }
61
62 pub fn from_cert(key: &str, content: &str) -> Result<MachineFile, Error> {
63 let dataset = Self::_decrypt(key, content)?;
64 Ok(MachineFile {
65 id: dataset.machine.id.clone(),
66 certificate: content.to_string(),
67 issued: dataset.issued,
68 expiry: dataset.expiry,
69 ttl: dataset.ttl,
70 })
71 }
72
73 pub fn verify(&self) -> Result<(), Error> {
74 self.validate_ttl()?;
75
76 let config = crate::config::get_config()?;
77
78 if let Some(public_key) = &config.public_key {
79 let verifier = Verifier::new(public_key.clone());
80 verifier.verify_machine_file(self)
81 } else {
82 Err(Error::PublicKeyMissing)
83 }
84 }
85
86 pub fn validate_ttl(&self) -> Result<(), Error> {
87 let now = Utc::now();
88 if now > self.expiry {
89 let dataset = self.decrypt("").unwrap_or_else(|_| {
90 use std::collections::HashMap;
91 MachineFileDataset {
92 license: License::from(crate::KeygenResponseData {
93 id: "".to_string(),
94 r#type: "licenses".to_string(),
95 attributes: crate::license::LicenseAttributes {
96 name: None,
97 key: "".to_string(),
98 expiry: None,
99 status: Some("".to_string()),
100 uses: Some(0),
101 max_machines: None,
102 max_cores: None,
103 max_uses: None,
104 max_processes: None,
105 max_users: None,
106 protected: None,
107 suspended: None,
108 permissions: None,
109 metadata: HashMap::new(),
110 ..Default::default()
111 },
112 relationships: crate::KeygenRelationships::default(),
113 }),
114 machine: Machine::from(crate::KeygenResponseData {
115 id: "".to_string(),
116 r#type: "machines".to_string(),
117 attributes: crate::machine::MachineAttributes {
118 fingerprint: "".to_string(),
119 name: None,
120 platform: None,
121 hostname: None,
122 ip: None,
123 cores: None,
124 metadata: None,
125 require_heartbeat: false,
126 heartbeat_status: "".to_string(),
127 heartbeat_duration: None,
128 created: Utc::now(),
129 updated: Utc::now(),
130 },
131 relationships: crate::KeygenRelationships::default(),
132 }),
133 issued: self.issued,
134 expiry: self.expiry,
135 ttl: self.ttl,
136 included: None,
137 }
138 });
139 Err(Error::MachineFileExpired(Box::new(dataset)))
140 } else {
141 Ok(())
142 }
143 }
144
145 pub fn decrypt(&self, key: &str) -> Result<MachineFileDataset, Error> {
146 Self::_decrypt(key, &self.certificate)
147 }
148
149 pub fn certificate(&self) -> Result<Certificate, Error> {
150 Self::_certificate(self.certificate.clone())
151 }
152
153 fn _decrypt(key: &str, content: &str) -> Result<MachineFileDataset, Error> {
154 let cert = Self::_certificate(content.to_string())?;
155
156 match cert.alg.as_str() {
157 "aes-256-gcm+rsa-pss-sha256" | "aes-256-gcm+rsa-sha256" => {
158 return Err(Error::LicenseFileNotSupported(cert.alg.clone()));
159 }
160 "aes-256-gcm+ed25519" => {}
161 _ => return Err(Error::LicenseFileNotEncrypted),
162 }
163
164 let decryptor = Decryptor::new(key.to_string());
165 let data = decryptor.decrypt_certificate(&cert)?;
166 let dataset: Value =
167 serde_json::from_slice(&data).map_err(|e| Error::MachineFileInvalid(e.to_string()))?;
168
169 let meta: CertificateFileMeta = serde_json::from_value(dataset["meta"].clone())
170 .map_err(|e| Error::LicenseFileInvalid(e.to_string()))?;
171
172 let included_array = dataset["included"]
174 .as_array()
175 .ok_or(Error::MachineFileInvalid(
176 "Included data is not an array".into(),
177 ))?;
178
179 let license_data = included_array
181 .iter()
182 .find(|v| v["type"] == "licenses")
183 .ok_or(Error::MachineFileInvalid(
184 "No license data found in included data".into(),
185 ))?;
186 let license = License::from(serde_json::from_value(license_data.clone())?);
187
188 let included = if included_array.len() > 1 {
190 Some(IncludedResources::parse_from_json(&Value::Array(
191 included_array.clone(),
192 ))?)
193 } else {
194 None
195 };
196
197 let machine_data: KeygenResponseData<MachineAttributes> =
198 serde_json::from_value(dataset["data"].clone())
199 .map_err(|e| Error::MachineFileInvalid(e.to_string()))?;
200 let machine = Machine::from(machine_data);
201
202 let dataset = MachineFileDataset {
203 license,
204 machine,
205 issued: meta.issued,
206 expiry: meta.expiry,
207 ttl: meta.ttl,
208 included,
209 };
210
211 if let Err(err) = validate_certificate_meta(&meta) {
212 match err {
213 Error::CertificateFileExpired => Err(Error::MachineFileExpired(Box::new(dataset))),
214 _ => Err(err),
215 }
216 } else {
217 Ok(dataset)
218 }
219 }
220
221 fn _certificate(certificate: String) -> Result<Certificate, Error> {
222 let payload = certificate.trim();
223 let payload = payload
224 .strip_prefix("-----BEGIN MACHINE FILE-----")
225 .and_then(|s| s.strip_suffix("-----END MACHINE FILE-----"))
226 .ok_or(Error::MachineFileInvalid(
227 "Invalid machine file format".into(),
228 ))?
229 .trim()
230 .replace("\n", "");
231
232 let decoded = general_purpose::STANDARD
233 .decode(payload)
234 .map_err(|e| Error::MachineFileInvalid(e.to_string()))?;
235
236 let cert: Certificate = serde_json::from_slice(&decoded)
237 .map_err(|e| Error::MachineFileInvalid(e.to_string()))?;
238
239 Ok(cert)
240 }
241
242 pub fn entitlements(&self, key: &str) -> Result<Vec<Entitlement>, Error> {
245 let dataset = self.decrypt(key)?;
246 Ok(dataset.offline_entitlements().unwrap_or(&vec![]).clone())
247 }
248
249 pub fn components(&self, key: &str) -> Result<Vec<Component>, Error> {
252 let dataset = self.decrypt(key)?;
253 Ok(dataset.offline_components().unwrap_or(&vec![]).clone())
254 }
255
256 pub fn groups(&self, key: &str) -> Result<Vec<Group>, Error> {
259 let dataset = self.decrypt(key)?;
260 Ok(dataset.offline_groups().unwrap_or(&vec![]).clone())
261 }
262}
263
264impl MachineFileDataset {
265 pub fn offline_entitlements(&self) -> Option<&Vec<Entitlement>> {
267 self.included.as_ref().map(|inc| &inc.entitlements)
268 }
269
270 pub fn offline_components(&self) -> Option<&Vec<Component>> {
272 self.included.as_ref().map(|inc| &inc.components)
273 }
274
275 pub fn offline_groups(&self) -> Option<&Vec<Group>> {
277 self.included.as_ref().map(|inc| &inc.groups)
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::machine::MachineCheckoutOpts;
285 use serde_json::json;
286
287 #[test]
288 fn test_machine_file_included_resources_parsing() {
289 let included_json = json!([
291 {
292 "type": "licenses",
293 "id": "lic1",
294 "attributes": {
295 "name": "Test License",
296 "key": "test-key",
297 "expiry": null,
298 "status": "active",
299 "uses": 0,
300 "maxMachines": 5,
301 "maxCores": null,
302 "maxUses": null,
303 "maxProcesses": null,
304 "maxUsers": null,
305 "protected": false,
306 "suspended": false,
307 "permissions": null,
308 "metadata": {}
309 },
310 "relationships": {
311 "account": {"data": {"type": "accounts", "id": "acc1"}}
312 }
313 },
314 {
315 "type": "entitlements",
316 "id": "ent1",
317 "attributes": {
318 "name": "Feature A",
319 "code": "feature-a",
320 "metadata": {},
321 "created": "2023-01-01T00:00:00Z",
322 "updated": "2023-01-01T00:00:00Z"
323 },
324 "relationships": {
325 "account": {"data": {"type": "accounts", "id": "acc1"}}
326 }
327 },
328 {
329 "type": "components",
330 "id": "comp1",
331 "attributes": {
332 "fingerprint": "component-fingerprint",
333 "name": "CPU Component"
334 }
335 }
336 ]);
337
338 let result = IncludedResources::parse_from_json(&included_json);
339 assert!(result.is_ok());
340
341 let included = result.unwrap();
342 assert_eq!(included.entitlements.len(), 1);
343 assert_eq!(included.entitlements[0].code, "feature-a");
344 assert_eq!(included.entitlements[0].name, Some("Feature A".to_string()));
345
346 assert_eq!(included.components.len(), 1);
347 assert_eq!(included.components[0].id, "comp1");
348 assert_eq!(included.components[0].fingerprint, "component-fingerprint");
349 assert_eq!(included.components[0].name, "CPU Component");
350 }
351
352 #[test]
353 fn test_machine_checkout_opts_with_ttl() {
354 let opts = MachineCheckoutOpts::with_ttl(7200);
355
356 assert_eq!(opts.ttl, Some(7200));
357 assert!(opts.include.is_none());
358 }
359
360 #[test]
361 fn test_machine_checkout_opts_with_include() {
362 let include_vec = vec!["license.entitlements".to_string(), "components".to_string()];
363 let opts = MachineCheckoutOpts::with_include(include_vec);
364
365 assert!(opts.include.is_some());
366 let includes = opts.include.unwrap();
367 assert!(includes.contains(&"license.entitlements".to_string()));
368 assert!(includes.contains(&"components".to_string()));
369 assert_eq!(includes.len(), 2);
370 assert!(opts.ttl.is_none());
371 }
372
373 #[test]
374 fn test_machine_checkout_opts_new() {
375 let opts = MachineCheckoutOpts::new();
376
377 assert!(opts.ttl.is_none());
378 assert!(opts.include.is_none());
379 }
380}