cryptolens_yc/
lib.rs

1//! # Cryptolens Rust Client
2//!
3//! This crate provides helper function for managing and verifying license keys using cryptolens.
4//! It handles key activations, validations, and provides utilities for working with RSA keys for
5//! digital signatures.
6//!
7//! ## Examples
8//!
9//! ### Basic usage:
10//!
11//! ```
12//! use cryptolens_yc::{key_activate, KeyActivateArguments};
13//! // this is the example in original documentation
14//! let public_key = "<RSAKeyValue><Modulus>khbyu3/vAEBHi339fTuo2nUaQgSTBj0jvpt5xnLTTF35FLkGI+5Z3wiKfnvQiCLf+5s4r8JB/Uic/i6/iNjPMILlFeE0N6XZ+2pkgwRkfMOcx6eoewypTPUoPpzuAINJxJRpHym3V6ZJZ1UfYvzRcQBD/lBeAYrvhpCwukQMkGushKsOS6U+d+2C9ZNeP+U+uwuv/xu8YBCBAgGb8YdNojcGzM4SbCtwvJ0fuOfmCWZvUoiumfE4x7rAhp1pa9OEbUe0a5HL+1v7+JLBgkNZ7Z2biiHaM6za7GjHCXU8rojatEQER+MpgDuQV3ZPx8RKRdiJgPnz9ApBHFYDHLDzDw==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";
15//!
16//! let product_id = "3646";
17//! let key = "MPDWY-PQAOW-FKSCH-SGAAU";
18//! let token = "WyI0NjUiLCJBWTBGTlQwZm9WV0FyVnZzMEV1Mm9LOHJmRDZ1SjF0Vk52WTU0VzB2Il0=";
19//!
20//! let license_key = key_activate(
21//!     token,
22//!     KeyActivateArguments {
23//!         ProductId: product_id.parse().unwrap(),
24//!         Key: key.to_string(),
25//!         MachineCode: "289jf2afs3".to_string(),
26//!         ..Default::default()
27//!     },
28//! ).unwrap();
29//!
30//! match license_key.has_valid_signature(public_key) {
31//!     Ok(valid) => assert_eq!(valid, true),
32//!     Err(e) => panic!("Error: {}", e),
33//! }
34//!```
35//! ### Offline Validation Example
36//!```rust
37//! // get the license key from above code ...
38//!
39//! // save the license key to a file
40//! let path = "cached_license_key";
41//! save_license_key_to_file(&license_key, path)?;
42//!
43//! // you can also load the license key from a file
44//! let loaded_license_key = cryptolens_yc::load_license_key_from_file(path)?;
45//!
46//! // validate the loaded license key
47//! match loaded_license_key.has_valid_signature(public_key) {
48//!     Ok(valid) => assert_eq!(valid, true),
49//!     Err(e) => panic!("Error: {}", e),
50//! }
51//! ```
52//!
53
54use std::fs::File;
55use std::io::Write;
56
57use base64::prelude::*;
58use serde::{Deserialize, Serialize};
59
60/// Represents an RSA key value pair with modulus and exponent.
61#[derive(Serialize, Deserialize, Debug)]
62#[allow(non_snake_case)]
63struct RSAKeyValue {
64    Modulus: String,
65    Exponent: String,
66}
67
68/// Customer information associated with a license key.
69#[derive(Serialize, Deserialize, Debug)]
70#[allow(non_snake_case)]
71pub struct Customer {
72    pub Id: u64,
73    pub Name: String,
74    pub Email: String,
75    pub CompanyName: String,
76    pub Created: u64,
77}
78
79/// Represents data about an activation instance.
80#[derive(Serialize, Deserialize, Debug)]
81#[allow(non_snake_case)]
82pub struct ActivationData {
83    pub Mid: String,
84    pub IP: String,
85    pub Time: u64,
86}
87
88/// A data object that can store custom information.
89#[derive(Serialize, Deserialize, Debug)]
90#[allow(non_snake_case)]
91pub struct DataObject {
92    pub Id: u64,
93    pub Name: String,
94    pub StringValue: String,
95    pub IntValue: u64,
96}
97
98/// Arguments used to activate a product key.
99#[derive(Debug)]
100#[allow(non_snake_case)]
101pub struct KeyActivateArguments {
102    pub ProductId: u64,
103    pub Key: String,
104    pub MachineCode: String,
105    pub FriendlyName: String,
106    pub FieldsToReturn: u8,
107    pub SignMethod: u8,
108    pub FloatingTimeInterval: u64,
109    pub MaxOverdraft: u64,
110    pub Metadata: bool,
111    pub OSInfo: String,
112    pub ModelVersion: u8,
113    pub v: u8,
114}
115
116impl Default for KeyActivateArguments {
117    fn default() -> Self {
118        KeyActivateArguments {
119            ProductId: 0,
120            Key: "".to_string(),
121            MachineCode: "".to_string(),
122            FriendlyName: "".to_string(),
123            FieldsToReturn: 0,
124            SignMethod: 1,
125            FloatingTimeInterval: 0,
126            MaxOverdraft: 0,
127            Metadata: false,
128            OSInfo: "".to_string(),
129            ModelVersion: 1,
130            v: 1,
131        }
132    }
133}
134
135#[derive(Serialize, Deserialize, Debug)]
136#[allow(non_snake_case)]
137struct SerdeLicenseKey {
138    ProductId: u64,
139    Id: Option<u64>,
140    Key: Option<String>,
141    Created: u64,
142    Expires: u64,
143    Period: u64,
144    F1: bool,
145    F2: bool,
146    F3: bool,
147    F4: bool,
148    F5: bool,
149    F6: bool,
150    F7: bool,
151    F8: bool,
152    Notes: Option<String>,
153    Block: bool,
154    GlobalId: Option<u64>,
155    Customer: Option<Customer>,
156    ActivatedMachines: Vec<ActivationData>,
157    TrialActivation: bool,
158    MaxNoOfMachines: Option<u64>,
159    AllowedMachines: Option<String>,
160    DataObjects: Vec<DataObject>,
161    SignDate: u64,
162}
163
164/// Represents a license key in cryptolens format.
165#[derive(Serialize, Deserialize, Debug)]
166#[allow(non_snake_case)]
167pub struct LicenseKey {
168    pub ProductId: u64,
169    pub Id: Option<u64>,
170    pub Key: Option<String>,
171    pub Created: u64,
172    pub Expires: u64,
173    pub Period: u64,
174    pub F1: bool,
175    pub F2: bool,
176    pub F3: bool,
177    pub F4: bool,
178    pub F5: bool,
179    pub F6: bool,
180    pub F7: bool,
181    pub F8: bool,
182    pub Notes: Option<String>,
183    pub Block: bool,
184    pub GlobalId: Option<u64>,
185    pub Customer: Option<Customer>,
186    pub ActivatedMachines: Vec<ActivationData>,
187    pub TrialActivation: bool,
188    pub MaxNoOfMachines: Option<u64>,
189    pub AllowedMachines: Vec<String>,
190    pub DataObjects: Vec<DataObject>,
191    pub SignDate: u64,
192
193    license_key_bytes: Vec<u8>,
194    signature_bytes: Vec<u8>,
195}
196
197/// Represents the response from an activation request.
198#[derive(Serialize, Deserialize, Debug)]
199#[allow(non_snake_case)]
200pub struct ActivateResponse {
201    result: i8,
202    message: String,
203    licenseKey: String,
204    signature: Option<String>,
205}
206
207impl LicenseKey {
208    /// Constructs a `LicenseKey` from a JSON string containing activation response data.
209    ///
210    /// # Arguments
211    /// * `s` - A string slice that holds the JSON data to parse.
212    ///
213    /// # Errors
214    /// Returns an error if parsing fails or if base64 decoding is unsuccessful.
215    pub fn from_response_str(s: &str) -> anyhow::Result<LicenseKey> {
216        let activate_response: ActivateResponse = serde_json::from_str(s)?;
217
218        let license_key = activate_response.licenseKey;
219        let signature = activate_response.signature;
220
221        let license_key_bytes = BASE64_STANDARD.decode(license_key)?;
222        let signature_bytes = BASE64_STANDARD.decode(signature.unwrap())?;
223
224        let license_key_string = String::from_utf8(license_key_bytes.clone())?;
225        let serde_lk: SerdeLicenseKey = serde_json::from_str(&license_key_string)?;
226
227        Ok(LicenseKey {
228            ProductId: serde_lk.ProductId,
229            Id: serde_lk.Id,
230            Key: serde_lk.Key,
231            Created: serde_lk.Created,
232            Expires: serde_lk.Expires,
233            Period: serde_lk.Period,
234            F1: serde_lk.F1,
235            F2: serde_lk.F2,
236            F3: serde_lk.F3,
237            F4: serde_lk.F4,
238            F5: serde_lk.F5,
239            F6: serde_lk.F6,
240            F7: serde_lk.F7,
241            F8: serde_lk.F8,
242            Notes: serde_lk.Notes,
243            Block: serde_lk.Block,
244            GlobalId: serde_lk.GlobalId,
245            Customer: serde_lk.Customer,
246            ActivatedMachines: serde_lk.ActivatedMachines,
247            TrialActivation: serde_lk.TrialActivation,
248            MaxNoOfMachines: serde_lk.MaxNoOfMachines,
249            AllowedMachines: serde_lk
250                .AllowedMachines
251                .map(|s| s.split('\n').map(|x| x.to_string()).collect())
252                .unwrap_or_default(),
253            DataObjects: serde_lk.DataObjects,
254            SignDate: serde_lk.SignDate,
255
256            license_key_bytes,
257            signature_bytes,
258        })
259    }
260
261    /// Verifies the validity of the digital signature associated with this license key.
262    ///
263    /// # Arguments
264    /// * `public_key` - A string slice containing the public key in XML format used to verify the signature.
265    ///
266    /// # Returns
267    /// Returns `true` if the signature is valid, otherwise returns `false`.
268    ///
269    /// # Errors
270    /// Returns an error if any cryptographic operations fail during verification.
271    pub fn has_valid_signature(&self, public_key: &str) -> anyhow::Result<bool> {
272        let public_key: RSAKeyValue = serde_xml_rs::from_str(public_key)?;
273
274        let modulus_bytes = BASE64_STANDARD.decode(&public_key.Modulus)?;
275        let exponent_bytes = BASE64_STANDARD.decode(&public_key.Exponent)?;
276
277        let modulus = openssl::bn::BigNum::from_slice(&modulus_bytes)?;
278        let exponent = openssl::bn::BigNum::from_slice(&exponent_bytes)?;
279
280        let keypair = openssl::rsa::Rsa::from_public_components(modulus, exponent)?;
281        let keypair = openssl::pkey::PKey::from_rsa(keypair)?;
282
283        let mut verifier = openssl::sign::Verifier::new(openssl::hash::MessageDigest::sha256(), &keypair)?;
284
285        verifier.update(&self.license_key_bytes)?;
286        let valid = verifier.verify(&self.signature_bytes)?;
287        Ok(valid)
288    }
289}
290
291/// Activates a license key using the cryptolens API.
292///
293/// # Arguments
294/// * `token` - A string slice containing the token to use for the activation.
295/// * `args` - A `KeyActivateArguments` struct containing the arguments for the activation.
296///
297/// # Returns
298/// Returns a `LicenseKey` struct containing the activated license key.
299pub fn key_activate(token: &str, args: KeyActivateArguments) -> anyhow::Result<LicenseKey> {
300    // Create a new reqwest client (blocking)
301    let client = reqwest::blocking::Client::new();
302    let params = [
303        ("token", token),
304        ("ProductId", &args.ProductId.to_string()),
305        ("Key", &args.Key),
306        ("MachineCode", &args.MachineCode),
307        ("FriendlyName", &args.FriendlyName),
308        ("FieldsToReturn", &args.FieldsToReturn.to_string()),
309        ("SignMethod", &args.SignMethod.to_string()),
310        ("FloatingTimeInterval", &args.FloatingTimeInterval.to_string()),
311        ("MaxOverdraft", &args.MaxOverdraft.to_string()),
312        ("Metadata", &args.Metadata.to_string()),
313        ("OSInfo", &args.OSInfo),
314        ("ModelVersion", &args.ModelVersion.to_string()),
315        ("v", &args.v.to_string()),
316        ("Sign", "true"),
317    ];
318
319    let res = client
320        .post("https://app.cryptolens.io/api/key/Activate")
321        .form(&params)
322        .send()?;
323    let s = res.text()?;
324
325    // Check if result is an error, if so, return an error
326    let response: serde_json::Value = serde_json::from_str(&s)?;
327    if response["result"] != 0 {
328        return Err(anyhow::anyhow!(
329            "Error Info: result: {}, message: {}",
330            response["result"],
331            response["message"]
332        ));
333    }
334
335    // otherwise, return the license key
336    LicenseKey::from_response_str(&s)
337}
338
339/// Encodes and saves a license key to a file.
340///
341/// # Arguments
342/// * `license_key` - A reference to a `LicenseKey` struct to save.
343/// * `path` - A string slice containing the path to save the license key to.
344///
345/// # Errors
346/// Returns an error if the file cannot be created or written to.
347pub fn save_license_key_to_file(license_key: &LicenseKey, path: &str) -> anyhow::Result<()> {
348    let mut file = File::create(path)?;
349    let activate_response = ActivateResponse {
350        licenseKey: BASE64_STANDARD.encode(&license_key.license_key_bytes),
351        signature: Some(BASE64_STANDARD.encode(&license_key.signature_bytes)),
352        result: 0,
353        message: "".to_string(),
354    };
355    let s = serde_json::to_string(&activate_response)?;
356    file.write_all(s.as_bytes())?;
357    Ok(())
358}
359
360/// Loads a license key from a file.
361///
362/// # Arguments
363/// * `path` - A string slice containing the path to the file to load.
364///
365/// # Errors
366/// Returns an error if the file cannot be read or if the license key cannot be parsed.
367///
368/// # Returns
369/// Returns a `LicenseKey` struct containing the loaded license key.
370pub fn load_license_key_from_file(path: &str) -> anyhow::Result<LicenseKey> {
371    let activate_response_str = std::fs::read_to_string(path)?;
372    let license_key = LicenseKey::from_response_str(&activate_response_str)?;
373
374    Ok(license_key)
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[test]
382    fn test_key_activate() {
383        // this is the example in original documentation
384        let public_key = "<RSAKeyValue><Modulus>khbyu3/vAEBHi339fTuo2nUaQgSTBj0jvpt5xnLTTF35FLkGI+5Z3wiKfnvQiCLf+5s4r8JB/Uic/i6/iNjPMILlFeE0N6XZ+2pkgwRkfMOcx6eoewypTPUoPpzuAINJxJRpHym3V6ZJZ1UfYvzRcQBD/lBeAYrvhpCwukQMkGushKsOS6U+d+2C9ZNeP+U+uwuv/xu8YBCBAgGb8YdNojcGzM4SbCtwvJ0fuOfmCWZvUoiumfE4x7rAhp1pa9OEbUe0a5HL+1v7+JLBgkNZ7Z2biiHaM6za7GjHCXU8rojatEQER+MpgDuQV3ZPx8RKRdiJgPnz9ApBHFYDHLDzDw==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";
385
386        let product_id = "3646";
387        let key = "MPDWY-PQAOW-FKSCH-SGAAU";
388        let token = "WyI0NjUiLCJBWTBGTlQwZm9WV0FyVnZzMEV1Mm9LOHJmRDZ1SjF0Vk52WTU0VzB2Il0=";
389
390        let license_key = key_activate(
391            token,
392            KeyActivateArguments {
393                ProductId: product_id.parse().unwrap(),
394                Key: key.to_string(),
395                MachineCode: "289jf2afs3".to_string(),
396                ..Default::default()
397            },
398        )
399        .unwrap();
400
401        match license_key.has_valid_signature(public_key) {
402            Ok(valid) => assert!(valid, "Signature should be valid"),
403            Err(e) => panic!("Error: {}", e),
404        }
405    }
406
407    #[test]
408    fn test_save_license_key_to_file() {
409        let license_key = LicenseKey {
410            ProductId: 3646,
411            Id: Some(1),
412            Key: Some("XXXXX-XXXXX-XXXXX-XXXXX".to_string()),
413            Created: 1614556800,
414            Expires: 1614556800,
415            Period: 0,
416            F1: false,
417            F2: false,
418            F3: false,
419            F4: false,
420            F5: false,
421            F6: false,
422            F7: false,
423            F8: false,
424            Notes: None,
425            Block: false,
426            GlobalId: None,
427            Customer: None,
428            ActivatedMachines: vec![],
429            TrialActivation: false,
430            MaxNoOfMachines: None,
431            AllowedMachines: vec![],
432            DataObjects: vec![],
433            SignDate: 1614556800,
434            license_key_bytes: vec![1, 2, 3, 4, 5],
435            signature_bytes: vec![1, 2, 3, 4, 5],
436        };
437
438        let path = "test_license_key";
439        save_license_key_to_file(&license_key, path).unwrap();
440        assert!(std::path::Path::new(path).exists());
441
442        // cleanup
443        std::fs::remove_file(path).unwrap();
444    }
445
446    #[test]
447    fn test_load_license_key_from_file() {
448        let path = "fixtures/test_license_key";
449        let public_key = "<RSAKeyValue><Modulus>khbyu3/vAEBHi339fTuo2nUaQgSTBj0jvpt5xnLTTF35FLkGI+5Z3wiKfnvQiCLf+5s4r8JB/Uic/i6/iNjPMILlFeE0N6XZ+2pkgwRkfMOcx6eoewypTPUoPpzuAINJxJRpHym3V6ZJZ1UfYvzRcQBD/lBeAYrvhpCwukQMkGushKsOS6U+d+2C9ZNeP+U+uwuv/xu8YBCBAgGb8YdNojcGzM4SbCtwvJ0fuOfmCWZvUoiumfE4x7rAhp1pa9OEbUe0a5HL+1v7+JLBgkNZ7Z2biiHaM6za7GjHCXU8rojatEQER+MpgDuQV3ZPx8RKRdiJgPnz9ApBHFYDHLDzDw==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";
450
451        let license_key = load_license_key_from_file(path).unwrap();
452        assert!(license_key.ProductId == 3646);
453        assert!(license_key.Key == Some("MPDWY-PQAOW-FKSCH-SGAAU".to_string()));
454
455        assert!(license_key.has_valid_signature(public_key).unwrap());
456    }
457}