1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
use base64::Engine;
use chrono::{DateTime, Utc};
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
use serde::Deserialize;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FetchCertificatesError {
#[error("Http error: {0}")]
Http(#[from] reqwest::Error),
#[error("Couldn't parse pkcs8 private key: {0}")]
Pkcs8(#[from] rsa::pkcs8::Error),
}
/// Fetch the Mojang-provided key-pair for your player, which is used for
/// cryptographically signing chat messages.
pub async fn fetch_certificates(
minecraft_access_token: &str,
) -> Result<Certificates, FetchCertificatesError> {
let client = reqwest::Client::new();
let res = client
.post("https://api.minecraftservices.com/player/certificates")
.header("Authorization", format!("Bearer {minecraft_access_token}"))
.send()
.await?
.json::<CertificatesResponse>()
.await?;
log::trace!("{:?}", res);
// using RsaPrivateKey::from_pkcs8_pem gives an error with decoding base64 so we
// just decode it ourselves
// remove the first and last lines of the private key
let private_key_pem_base64 = res
.key_pair
.private_key
.lines()
.skip(1)
.take_while(|line| !line.starts_with('-'))
.collect::<String>();
let private_key_der = base64::engine::general_purpose::STANDARD
.decode(private_key_pem_base64)
.unwrap();
let public_key_pem_base64 = res
.key_pair
.public_key
.lines()
.skip(1)
.take_while(|line| !line.starts_with('-'))
.collect::<String>();
let public_key_der = base64::engine::general_purpose::STANDARD
.decode(public_key_pem_base64)
.unwrap();
// the private key also contains the public key so it's basically a keypair
let private_key = RsaPrivateKey::from_pkcs8_der(&private_key_der).unwrap();
let certificates = Certificates {
private_key,
public_key_der,
signature_v1: base64::engine::general_purpose::STANDARD
.decode(&res.public_key_signature)
.unwrap(),
signature_v2: base64::engine::general_purpose::STANDARD
.decode(&res.public_key_signature_v2)
.unwrap(),
expires_at: res.expires_at,
refresh_after: res.refreshed_after,
};
Ok(certificates)
}
/// A chat signing certificate.
#[derive(Clone, Debug)]
pub struct Certificates {
/// The RSA private key.
pub private_key: RsaPrivateKey,
/// The RSA public key encoded as DER.
pub public_key_der: Vec<u8>,
pub signature_v1: Vec<u8>,
pub signature_v2: Vec<u8>,
pub expires_at: DateTime<Utc>,
pub refresh_after: DateTime<Utc>,
}
#[derive(Debug, Deserialize)]
pub struct CertificatesResponse {
#[serde(rename = "keyPair")]
pub key_pair: KeyPairResponse,
/// base64 string; signed data
#[serde(rename = "publicKeySignature")]
pub public_key_signature: String,
/// base64 string; signed data
#[serde(rename = "publicKeySignatureV2")]
pub public_key_signature_v2: String,
/// Date like `2022-04-30T00:11:32.174783069Z`
#[serde(rename = "expiresAt")]
pub expires_at: DateTime<Utc>,
/// Date like `2022-04-29T16:11:32.174783069Z`
#[serde(rename = "refreshedAfter")]
pub refreshed_after: DateTime<Utc>,
}
#[derive(Debug, Deserialize)]
pub struct KeyPairResponse {
/// -----BEGIN RSA PRIVATE KEY-----
/// ...
/// -----END RSA PRIVATE KEY-----
#[serde(rename = "privateKey")]
pub private_key: String,
/// -----BEGIN RSA PUBLIC KEY-----
/// ...
/// -----END RSA PUBLIC KEY-----
#[serde(rename = "publicKey")]
pub public_key: String,
}
|