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 },
111 relationships: crate::KeygenRelationships::default(),
112 }),
113 machine: Machine::from(crate::KeygenResponseData {
114 id: "".to_string(),
115 r#type: "machines".to_string(),
116 attributes: crate::machine::MachineAttributes {
117 fingerprint: "".to_string(),
118 name: None,
119 platform: None,
120 hostname: None,
121 ip: None,
122 cores: None,
123 metadata: None,
124 require_heartbeat: false,
125 heartbeat_status: "".to_string(),
126 heartbeat_duration: None,
127 created: Utc::now(),
128 updated: Utc::now(),
129 },
130 relationships: crate::KeygenRelationships::default(),
131 }),
132 issued: self.issued,
133 expiry: self.expiry,
134 ttl: self.ttl,
135 included: None,
136 }
137 });
138 Err(Error::MachineFileExpired(Box::new(dataset)))
139 } else {
140 Ok(())
141 }
142 }
143
144 pub fn decrypt(&self, key: &str) -> Result<MachineFileDataset, Error> {
145 Self::_decrypt(key, &self.certificate)
146 }
147
148 pub fn certificate(&self) -> Result<Certificate, Error> {
149 Self::_certificate(self.certificate.clone())
150 }
151
152 fn _decrypt(key: &str, content: &str) -> Result<MachineFileDataset, Error> {
153 let cert = Self::_certificate(content.to_string())?;
154
155 match cert.alg.as_str() {
156 "aes-256-gcm+rsa-pss-sha256" | "aes-256-gcm+rsa-sha256" => {
157 return Err(Error::LicenseFileNotSupported(cert.alg.clone()));
158 }
159 "aes-256-gcm+ed25519" => {}
160 _ => return Err(Error::LicenseFileNotEncrypted),
161 }
162
163 let decryptor = Decryptor::new(key.to_string());
164 let data = decryptor.decrypt_certificate(&cert)?;
165 let dataset: Value =
166 serde_json::from_slice(&data).map_err(|e| Error::MachineFileInvalid(e.to_string()))?;
167
168 let meta: CertificateFileMeta = serde_json::from_value(dataset["meta"].clone())
169 .map_err(|e| Error::LicenseFileInvalid(e.to_string()))?;
170
171 let included_array = dataset["included"]
173 .as_array()
174 .ok_or(Error::MachineFileInvalid(
175 "Included data is not an array".into(),
176 ))?;
177
178 let license_data = included_array
180 .iter()
181 .find(|v| v["type"] == "licenses")
182 .ok_or(Error::MachineFileInvalid(
183 "No license data found in included data".into(),
184 ))?;
185 let license = License::from(serde_json::from_value(license_data.clone())?);
186
187 let included = if included_array.len() > 1 {
189 Some(IncludedResources::parse_from_json(&Value::Array(
190 included_array.clone(),
191 ))?)
192 } else {
193 None
194 };
195
196 let machine_data: KeygenResponseData<MachineAttributes> =
197 serde_json::from_value(dataset["data"].clone())
198 .map_err(|e| Error::MachineFileInvalid(e.to_string()))?;
199 let machine = Machine::from(machine_data);
200
201 let dataset = MachineFileDataset {
202 license,
203 machine,
204 issued: meta.issued,
205 expiry: meta.expiry,
206 ttl: meta.ttl,
207 included,
208 };
209
210 if let Err(err) = validate_certificate_meta(&meta) {
211 match err {
212 Error::CertificateFileExpired => Err(Error::MachineFileExpired(Box::new(dataset))),
213 _ => Err(err),
214 }
215 } else {
216 Ok(dataset)
217 }
218 }
219
220 fn _certificate(certificate: String) -> Result<Certificate, Error> {
221 let payload = certificate.trim();
222 let payload = payload
223 .strip_prefix("-----BEGIN MACHINE FILE-----")
224 .and_then(|s| s.strip_suffix("-----END MACHINE FILE-----"))
225 .ok_or(Error::MachineFileInvalid(
226 "Invalid machine file format".into(),
227 ))?
228 .trim()
229 .replace("\n", "");
230
231 let decoded = general_purpose::STANDARD
232 .decode(payload)
233 .map_err(|e| Error::MachineFileInvalid(e.to_string()))?;
234
235 let cert: Certificate = serde_json::from_slice(&decoded)
236 .map_err(|e| Error::MachineFileInvalid(e.to_string()))?;
237
238 Ok(cert)
239 }
240
241 pub fn entitlements(&self, key: &str) -> Result<Vec<Entitlement>, Error> {
244 let dataset = self.decrypt(key)?;
245 Ok(dataset.offline_entitlements().unwrap_or(&vec![]).clone())
246 }
247
248 pub fn components(&self, key: &str) -> Result<Vec<Component>, Error> {
251 let dataset = self.decrypt(key)?;
252 Ok(dataset.offline_components().unwrap_or(&vec![]).clone())
253 }
254
255 pub fn groups(&self, key: &str) -> Result<Vec<Group>, Error> {
258 let dataset = self.decrypt(key)?;
259 Ok(dataset.offline_groups().unwrap_or(&vec![]).clone())
260 }
261}
262
263impl MachineFileDataset {
264 pub fn offline_entitlements(&self) -> Option<&Vec<Entitlement>> {
266 self.included.as_ref().map(|inc| &inc.entitlements)
267 }
268
269 pub fn offline_components(&self) -> Option<&Vec<Component>> {
271 self.included.as_ref().map(|inc| &inc.components)
272 }
273
274 pub fn offline_groups(&self) -> Option<&Vec<Group>> {
276 self.included.as_ref().map(|inc| &inc.groups)
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::machine::MachineCheckoutOpts;
284 use serde_json::json;
285
286 #[test]
287 fn test_machine_file_included_resources_parsing() {
288 let included_json = json!([
290 {
291 "type": "licenses",
292 "id": "lic1",
293 "attributes": {
294 "name": "Test License",
295 "key": "test-key",
296 "expiry": null,
297 "status": "active",
298 "uses": 0,
299 "maxMachines": 5,
300 "maxCores": null,
301 "maxUses": null,
302 "maxProcesses": null,
303 "maxUsers": null,
304 "protected": false,
305 "suspended": false,
306 "permissions": null,
307 "metadata": {}
308 },
309 "relationships": {
310 "account": {"data": {"type": "accounts", "id": "acc1"}}
311 }
312 },
313 {
314 "type": "entitlements",
315 "id": "ent1",
316 "attributes": {
317 "name": "Feature A",
318 "code": "feature-a",
319 "metadata": {},
320 "created": "2023-01-01T00:00:00Z",
321 "updated": "2023-01-01T00:00:00Z"
322 },
323 "relationships": {
324 "account": {"data": {"type": "accounts", "id": "acc1"}}
325 }
326 },
327 {
328 "type": "components",
329 "id": "comp1",
330 "attributes": {
331 "fingerprint": "component-fingerprint",
332 "name": "CPU Component"
333 }
334 }
335 ]);
336
337 let result = IncludedResources::parse_from_json(&included_json);
338 assert!(result.is_ok());
339
340 let included = result.unwrap();
341 assert_eq!(included.entitlements.len(), 1);
342 assert_eq!(included.entitlements[0].code, "feature-a");
343 assert_eq!(included.entitlements[0].name, Some("Feature A".to_string()));
344
345 assert_eq!(included.components.len(), 1);
346 assert_eq!(included.components[0].id, "comp1");
347 assert_eq!(included.components[0].fingerprint, "component-fingerprint");
348 assert_eq!(included.components[0].name, "CPU Component");
349 }
350
351 #[test]
352 fn test_machine_checkout_opts_with_ttl() {
353 let opts = MachineCheckoutOpts::with_ttl(7200);
354
355 assert_eq!(opts.ttl, Some(7200));
356 assert!(opts.include.is_none());
357 }
358
359 #[test]
360 fn test_machine_checkout_opts_with_include() {
361 let include_vec = vec!["license.entitlements".to_string(), "components".to_string()];
362 let opts = MachineCheckoutOpts::with_include(include_vec);
363
364 assert!(opts.include.is_some());
365 let includes = opts.include.unwrap();
366 assert!(includes.contains(&"license.entitlements".to_string()));
367 assert!(includes.contains(&"components".to_string()));
368 assert_eq!(includes.len(), 2);
369 assert!(opts.ttl.is_none());
370 }
371
372 #[test]
373 fn test_machine_checkout_opts_new() {
374 let opts = MachineCheckoutOpts::new();
375
376 assert!(opts.ttl.is_none());
377 assert!(opts.include.is_none());
378 }
379}