1use base64::Engine;
2use chrono::{DateTime, Utc};
3use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
4use serde::Deserialize;
5use thiserror::Error;
6use tracing::trace;
7
8#[derive(Debug, Error)]
9pub enum FetchCertificatesError {
10 #[error("Http error: {0}")]
11 Http(#[from] reqwest::Error),
12 #[error("Couldn't parse pkcs8 private key: {0}")]
13 Pkcs8(#[from] rsa::pkcs8::Error),
14}
15
16pub async fn fetch_certificates(
19 minecraft_access_token: &str,
20) -> Result<Certificates, FetchCertificatesError> {
21 let client = reqwest::Client::new();
22
23 let res = client
24 .post("https://api.minecraftservices.com/player/certificates")
25 .header("Authorization", format!("Bearer {minecraft_access_token}"))
26 .send()
27 .await?
28 .json::<CertificatesResponse>()
29 .await?;
30 trace!("{:?}", res);
31
32 let private_key_pem_base64 = res
37 .key_pair
38 .private_key
39 .lines()
40 .skip(1)
41 .take_while(|line| !line.starts_with('-'))
42 .collect::<String>();
43 let private_key_der = base64::engine::general_purpose::STANDARD
44 .decode(private_key_pem_base64)
45 .unwrap();
46
47 let public_key_pem_base64 = res
48 .key_pair
49 .public_key
50 .lines()
51 .skip(1)
52 .take_while(|line| !line.starts_with('-'))
53 .collect::<String>();
54 let public_key_der = base64::engine::general_purpose::STANDARD
55 .decode(public_key_pem_base64)
56 .unwrap();
57
58 let private_key = RsaPrivateKey::from_pkcs8_der(&private_key_der).unwrap();
60
61 let certificates = Certificates {
62 private_key,
63 public_key_der,
64
65 signature_v1: base64::engine::general_purpose::STANDARD
66 .decode(&res.public_key_signature)
67 .unwrap(),
68 signature_v2: base64::engine::general_purpose::STANDARD
69 .decode(&res.public_key_signature_v2)
70 .unwrap(),
71
72 expires_at: res.expires_at,
73 refresh_after: res.refreshed_after,
74 };
75
76 Ok(certificates)
77}
78
79#[derive(Clone, Debug)]
81pub struct Certificates {
82 pub private_key: RsaPrivateKey,
84 pub public_key_der: Vec<u8>,
86
87 pub signature_v1: Vec<u8>,
88 pub signature_v2: Vec<u8>,
89
90 pub expires_at: DateTime<Utc>,
91 pub refresh_after: DateTime<Utc>,
92}
93
94#[derive(Debug, Deserialize)]
95pub struct CertificatesResponse {
96 #[serde(rename = "keyPair")]
97 pub key_pair: KeyPairResponse,
98
99 #[serde(rename = "publicKeySignature")]
101 pub public_key_signature: String,
102
103 #[serde(rename = "publicKeySignatureV2")]
105 pub public_key_signature_v2: String,
106
107 #[serde(rename = "expiresAt")]
109 pub expires_at: DateTime<Utc>,
110
111 #[serde(rename = "refreshedAfter")]
113 pub refreshed_after: DateTime<Utc>,
114}
115
116#[derive(Debug, Deserialize)]
117pub struct KeyPairResponse {
118 #[serde(rename = "privateKey")]
122 pub private_key: String,
123
124 #[serde(rename = "publicKey")]
128 pub public_key: String,
129}