konnektoren_core/certificates/
certificate_data.rs

1use super::keypair_from_static_str;
2use crate::certificates::error::{CertificateError, Result};
3use crate::challenges::PerformanceRecord;
4use base64::{Engine as _, engine::general_purpose};
5use chrono::{DateTime, Utc};
6use ed25519_dalek::{Signature, Signer, Verifier, ed25519::SignatureBytes};
7use serde::{Deserialize, Serialize};
8use sha2::Digest;
9use sha2::Sha256;
10
11#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Hash, Eq, Default)]
12pub struct CertificateData {
13    pub game_path_name: String,
14    pub total_challenges: usize,
15    pub solved_challenges: usize,
16    pub performance_percentage: u8,
17    pub profile_name: String,
18    pub date: DateTime<Utc>,
19    pub signature: Option<Vec<u8>>,
20}
21
22impl CertificateData {
23    pub fn new(
24        game_path_name: String,
25        total_challenges: usize,
26        solved_challenges: usize,
27        player_name: String,
28        date: DateTime<Utc>,
29    ) -> Self {
30        let performance_percentage =
31            ((solved_challenges as f64 / total_challenges as f64) * 100.0) as u8;
32        CertificateData {
33            game_path_name,
34            total_challenges,
35            solved_challenges,
36            performance_percentage,
37            profile_name: player_name,
38            date,
39            signature: None,
40        }
41    }
42
43    pub fn new_data_copy(&self) -> Self {
44        CertificateData {
45            game_path_name: self.game_path_name.clone(),
46            total_challenges: self.total_challenges,
47            solved_challenges: self.solved_challenges,
48            performance_percentage: self.performance_percentage,
49            profile_name: self.profile_name.clone(),
50            date: self.date,
51            signature: None,
52        }
53    }
54
55    pub fn to_sha256(&self) -> Vec<u8> {
56        let data = self.to_base64();
57
58        let mut sha256 = Sha256::new();
59        sha256.update(data);
60        format!("{:X}", sha256.finalize()).into_bytes()
61    }
62
63    pub fn to_base64(&self) -> String {
64        let mut buf = Vec::new();
65
66        self.serialize(&mut rmp_serde::Serializer::new(&mut buf))
67            .map_err(|e| {
68                CertificateError::SerializationError(format!(
69                    "Failed to serialize certificate data to msgpack: {}",
70                    e
71                ))
72            })
73            .expect("Failed to serialize certificate data to msgpack.");
74
75        general_purpose::STANDARD.encode(buf).to_string()
76    }
77
78    pub fn from_base64(encoded: &str) -> Result<Self> {
79        let decoded = general_purpose::STANDARD
80            .decode(encoded)
81            .map_err(|_| CertificateError::DecodingError)?;
82
83        rmp_serde::from_slice(&decoded).map_err(|e| {
84            CertificateError::DeserializationError(format!(
85                "Failed to deserialize certificate data: {}",
86                e
87            ))
88        })
89    }
90
91    pub fn create_signature(&mut self) -> Result<()> {
92        let (signing_key, _) = keypair_from_static_str();
93        let certificate_data_copy = CertificateData::new_data_copy(self);
94
95        let serialized = serde_cbor::to_vec(&certificate_data_copy).map_err(|e| {
96            CertificateError::SerializationError(format!(
97                "Failed to serialize certificate data: {}",
98                e
99            ))
100        })?;
101
102        let signature: Signature = signing_key.sign(&serialized);
103        self.signature = Some(signature.to_bytes().to_vec());
104
105        Ok(())
106    }
107
108    pub fn verify(&self) -> Result<bool> {
109        if self.signature.is_none() {
110            return Ok(false);
111        }
112
113        let (_, verifying_key) = keypair_from_static_str();
114        let certificate_data_copy = CertificateData::new_data_copy(self);
115
116        let serialized = serde_cbor::to_vec(&certificate_data_copy).map_err(|e| {
117            CertificateError::SerializationError(format!(
118                "Failed to serialize certificate data for verification: {}",
119                e
120            ))
121        })?;
122
123        let signature = self.signature.as_ref().unwrap();
124        let signature_bytes = SignatureBytes::try_from(signature.as_slice()).map_err(|_| {
125            CertificateError::SignatureError("Failed to convert signature bytes".to_string())
126        })?;
127
128        let signature = Signature::from_bytes(&signature_bytes);
129        Ok(verifying_key.verify(&serialized, &signature).is_ok())
130    }
131}
132
133impl From<PerformanceRecord> for CertificateData {
134    fn from(record: PerformanceRecord) -> Self {
135        CertificateData {
136            game_path_name: record.game_path_id,
137            total_challenges: record.total_challenges,
138            solved_challenges: record.challenges_performance.len(),
139            performance_percentage: record.performance_percentage,
140            profile_name: record.profile_name,
141            date: record.date,
142            signature: None,
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_base64_serialization_deserialization() {
153        let certificate_data = CertificateData::new(
154            "Level A1".to_string(),
155            12,
156            10,
157            "Player".to_string(),
158            Utc::now(),
159        );
160        let base64_encoded = certificate_data.to_base64();
161        let decoded_certificate_data = CertificateData::from_base64(&base64_encoded).unwrap();
162
163        assert_eq!(certificate_data, decoded_certificate_data);
164    }
165
166    #[test]
167    fn test_base64_error() {
168        let base64_encoded = "invalid_base64";
169        let decoded_certificate_data = CertificateData::from_base64(base64_encoded);
170        assert!(decoded_certificate_data.is_err());
171
172        // Verify error type
173        match decoded_certificate_data {
174            Err(CertificateError::DecodingError) => {}
175            _ => panic!("Expected DecodingError"),
176        }
177    }
178
179    #[test]
180    fn test_certificate_data_new() {
181        let certificate_data = CertificateData::new(
182            "Level A1".to_string(),
183            12,
184            10,
185            "Player".to_string(),
186            Utc::now(),
187        );
188        assert_eq!(certificate_data.game_path_name, "Level A1");
189        assert_eq!(certificate_data.total_challenges, 12);
190        assert_eq!(certificate_data.solved_challenges, 10);
191        assert_eq!(certificate_data.performance_percentage, 83);
192    }
193
194    #[test]
195    fn test_signature_verification() {
196        let mut certificate_data = CertificateData::new(
197            "Level A1".to_string(),
198            12,
199            10,
200            "Player".to_string(),
201            Utc::now(),
202        );
203
204        certificate_data.create_signature().unwrap();
205        let is_verified = certificate_data.verify().unwrap();
206
207        assert!(
208            is_verified,
209            "The signature should be verified successfully."
210        );
211    }
212
213    #[test]
214    fn test_missing_signature_verification() {
215        let certificate_data = CertificateData::new(
216            "Level A1".to_string(),
217            12,
218            10,
219            "Player".to_string(),
220            Utc::now(),
221        );
222
223        // Signature hasn't been created yet
224        let is_verified = certificate_data.verify().unwrap();
225        assert!(!is_verified, "Verification should fail with no signature");
226    }
227
228    #[test]
229    fn test_sha256() {
230        let certificate_data = CertificateData::new(
231            "Level A1".to_string(),
232            12,
233            10,
234            "Player".to_string(),
235            Utc::now(),
236        );
237        let sha256 = certificate_data.to_sha256();
238        assert_eq!(sha256.len(), 64);
239    }
240
241    #[test]
242    fn test_new_data_copy() {
243        let mut certificate_data = CertificateData::new(
244            "Level A1".to_string(),
245            12,
246            10,
247            "Player".to_string(),
248            Utc::now(),
249        );
250        let certificate_data_copy = certificate_data.new_data_copy();
251        assert_eq!(certificate_data, certificate_data_copy);
252
253        certificate_data.create_signature().unwrap();
254
255        let certificate_data_copy = certificate_data.new_data_copy();
256        assert_ne!(certificate_data, certificate_data_copy);
257    }
258
259    #[test]
260    fn test_certificate_data_from_performance_record() {
261        let performance_record = PerformanceRecord {
262            game_path_id: "Level A1".to_string(),
263            total_challenges: 12,
264            challenges_performance: vec![],
265            performance_percentage: 0,
266            profile_name: "Player".to_string(),
267            date: Utc::now(),
268        };
269
270        let certificate_data: CertificateData = performance_record.into();
271        assert_eq!(certificate_data.game_path_name, "Level A1");
272        assert_eq!(certificate_data.total_challenges, 12);
273        assert_eq!(certificate_data.solved_challenges, 0);
274        assert_eq!(certificate_data.performance_percentage, 0);
275    }
276}