azalea_auth/
certs.rs

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
16/// Fetch the Mojang-provided key-pair for your player, which is used for
17/// cryptographically signing chat messages.
18pub 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    // using RsaPrivateKey::from_pkcs8_pem gives an error with decoding base64 so we
33    // just decode it ourselves
34
35    // remove the first and last lines of the private key
36    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    // the private key also contains the public key so it's basically a keypair
59    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/// A chat signing certificate.
80#[derive(Clone, Debug)]
81pub struct Certificates {
82    /// The RSA private key.
83    pub private_key: RsaPrivateKey,
84    /// The RSA public key encoded as DER.
85    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    /// base64 string; signed data
100    #[serde(rename = "publicKeySignature")]
101    pub public_key_signature: String,
102
103    /// base64 string; signed data
104    #[serde(rename = "publicKeySignatureV2")]
105    pub public_key_signature_v2: String,
106
107    /// Date like `2022-04-30T00:11:32.174783069Z`
108    #[serde(rename = "expiresAt")]
109    pub expires_at: DateTime<Utc>,
110
111    /// Date like `2022-04-29T16:11:32.174783069Z`
112    #[serde(rename = "refreshedAfter")]
113    pub refreshed_after: DateTime<Utc>,
114}
115
116#[derive(Debug, Deserialize)]
117pub struct KeyPairResponse {
118    /// -----BEGIN RSA PRIVATE KEY-----
119    /// ...
120    /// -----END RSA PRIVATE KEY-----
121    #[serde(rename = "privateKey")]
122    pub private_key: String,
123
124    /// -----BEGIN RSA PUBLIC KEY-----
125    /// ...
126    /// -----END RSA PUBLIC KEY-----
127    #[serde(rename = "publicKey")]
128    pub public_key: String,
129}