konnektoren_core/certificates/
certificate_data.rs1use 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 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 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}