2022-08-15 23:55:44 +08:00
using System ;
using System.Collections.Generic ;
2022-08-27 02:10:44 +08:00
using System.Security.Cryptography ;
2022-08-15 23:55:44 +08:00
using System.Text ;
2026-03-22 16:39:26 +00:00
using System.Text.Json.Nodes ;
2023-01-13 16:12:10 +08:00
using MinecraftClient.Protocol.Handlers ;
2022-08-27 02:10:44 +08:00
using MinecraftClient.Protocol.Message ;
2023-01-13 16:12:10 +08:00
using static MinecraftClient . Protocol . Message . LastSeenMessageList ;
2022-08-15 23:55:44 +08:00
2022-12-06 15:50:17 +08:00
namespace MinecraftClient.Protocol.ProfileKey
2022-08-15 23:55:44 +08:00
{
static class KeyUtils
{
2022-08-27 02:10:44 +08:00
private static readonly SHA256 sha256Hash = SHA256 . Create ( ) ;
private static readonly string certificates = "https://api.minecraftservices.com/player/certificates" ;
2022-08-15 23:55:44 +08:00
2024-01-13 03:00:02 +08:00
public static PlayerKeyPair ? GetNewProfileKeys ( string accessToken , bool isYggdrasil )
2022-08-15 23:55:44 +08:00
{
ProxiedWebRequest . Response ? response = null ;
try
{
2026-03-22 22:19:59 +01:00
if ( ! isYggdrasil & & string . IsNullOrWhiteSpace ( accessToken ) )
return null ;
2024-02-20 20:31:22 +08:00
if ( ! isYggdrasil )
{
2024-01-13 03:00:02 +08:00
var request = new ProxiedWebRequest ( certificates )
{
Accept = "application/json"
} ;
request . Headers . Add ( "Authorization" , string . Format ( "Bearer {0}" , accessToken ) ) ;
2022-08-15 23:55:44 +08:00
2024-01-13 03:00:02 +08:00
response = request . Post ( "application/json" , "" ) ;
2022-08-15 23:55:44 +08:00
2024-01-13 03:00:02 +08:00
if ( Settings . Config . Logging . DebugMessages )
{
ConsoleIO . WriteLine ( response . Body . ToString ( ) ) ;
}
2026-03-22 22:19:59 +01:00
if ( response . StatusCode < 200 | | response . StatusCode > = 300 )
{
throw new InvalidOperationException ( string . IsNullOrWhiteSpace ( response . Body )
? "Certificate endpoint returned an error response."
: response . Body ) ;
}
2022-08-15 23:55:44 +08:00
}
2024-01-13 03:00:02 +08:00
// see https://github.com/yushijinhun/authlib-injector/blob/da910956eaa30d2f6c2c457222d188aeb53b0d1f/src/main/java/moe/yushi/authlibinjector/httpd/ProfileKeyFilter.java#L49
// POST to "https://api.minecraftservices.com/player/certificates" with authlib-injector will get a dummy response
2026-03-22 16:39:26 +00:00
var json = isYggdrasil ? MakeDummyResponse ( ) : Json . ParseJson ( response ! . Body ) ;
2026-03-22 22:19:59 +01:00
if ( json ? [ "keyPair" ] ? [ "publicKey" ] = = null
| | json [ "keyPair" ] ? [ "privateKey" ] = = null
| | json [ "publicKeySignature" ] = = null
| | json [ "publicKeySignatureV2" ] = = null
| | json [ "expiresAt" ] = = null
| | json [ "refreshedAfter" ] = = null )
{
throw new InvalidOperationException ( "Certificate endpoint returned an unexpected payload." ) ;
}
2024-01-13 03:00:02 +08:00
// Error here
2026-03-22 16:39:26 +00:00
PublicKey publicKey = new ( pemKey : json ! [ "keyPair" ] ! [ "publicKey" ] ! . GetStringValue ( ) ,
sig : json [ "publicKeySignature" ] ! . GetStringValue ( ) ,
sigV2 : json [ "publicKeySignatureV2" ] ! . GetStringValue ( ) ) ;
2022-08-15 23:55:44 +08:00
2026-03-22 16:39:26 +00:00
PrivateKey privateKey = new ( pemKey : json [ "keyPair" ] ! [ "privateKey" ] ! . GetStringValue ( ) ) ;
2022-08-15 23:55:44 +08:00
return new PlayerKeyPair ( publicKey , privateKey ,
2026-03-22 16:39:26 +00:00
expiresAt : json [ "expiresAt" ] ! . GetStringValue ( ) ,
refreshedAfter : json [ "refreshedAfter" ] ! . GetStringValue ( ) ) ;
2022-08-15 23:55:44 +08:00
}
catch ( Exception e )
{
2022-12-06 15:50:17 +08:00
int code = response = = null ? 0 : response . StatusCode ;
2022-08-15 23:55:44 +08:00
ConsoleIO . WriteLineFormatted ( "§cFetch profile key failed: HttpCode = " + code + ", Error = " + e . Message ) ;
2022-10-05 15:02:30 +08:00
if ( Settings . Config . Logging . DebugMessages )
2022-08-15 23:55:44 +08:00
{
ConsoleIO . WriteLineFormatted ( "§c" + e . StackTrace ) ;
}
return null ;
}
}
2022-12-06 15:50:17 +08:00
public static byte [ ] DecodePemKey ( string key , string prefix , string suffix )
2022-08-15 23:55:44 +08:00
{
int i = key . IndexOf ( prefix ) ;
if ( i ! = - 1 )
{
i + = prefix . Length ;
int j = key . IndexOf ( suffix , i ) ;
key = key [ i . . j ] ;
}
2022-12-06 15:50:17 +08:00
key = key . Replace ( "\r" , string . Empty ) ;
key = key . Replace ( "\n" , string . Empty ) ;
2022-08-15 23:55:44 +08:00
return Convert . FromBase64String ( key ) ;
}
2022-08-27 02:10:44 +08:00
public static byte [ ] ComputeHash ( byte [ ] data )
{
return sha256Hash . ComputeHash ( data ) ;
}
public static byte [ ] GetSignatureData ( string message , Guid uuid , DateTimeOffset timestamp , ref byte [ ] salt )
2022-08-15 23:55:44 +08:00
{
List < byte > data = new ( ) ;
data . AddRange ( salt ) ;
2022-08-27 02:10:44 +08:00
data . AddRange ( uuid . ToBigEndianBytes ( ) ) ;
byte [ ] timestampByte = BitConverter . GetBytes ( timestamp . ToUnixTimeSeconds ( ) ) ;
Array . Reverse ( timestampByte ) ;
data . AddRange ( timestampByte ) ;
data . AddRange ( Encoding . UTF8 . GetBytes ( message ) ) ;
return data . ToArray ( ) ;
}
public static byte [ ] GetSignatureData ( string message , DateTimeOffset timestamp , ref byte [ ] salt , LastSeenMessageList lastSeenMessages )
{
List < byte > data = new ( ) ;
2022-08-15 23:55:44 +08:00
2022-08-27 02:10:44 +08:00
data . AddRange ( salt ) ;
2022-08-15 23:55:44 +08:00
byte [ ] timestampByte = BitConverter . GetBytes ( timestamp . ToUnixTimeSeconds ( ) ) ;
Array . Reverse ( timestampByte ) ;
data . AddRange ( timestampByte ) ;
data . AddRange ( Encoding . UTF8 . GetBytes ( message ) ) ;
2022-08-27 02:10:44 +08:00
data . Add ( 70 ) ;
lastSeenMessages . WriteForSign ( data ) ;
return data . ToArray ( ) ;
}
2023-01-13 16:12:10 +08:00
public static byte [ ] GetSignatureData_1_19_3 ( string message , Guid playerUuid , Guid chatUuid , int messageIndex , DateTimeOffset timestamp , ref byte [ ] salt , AcknowledgedMessage [ ] lastSeenMessages )
{
List < byte > data = new ( ) ;
// net.minecraft.network.message.SignedMessage#update
data . AddRange ( DataTypes . GetInt ( 1 ) ) ;
// message link
// net.minecraft.network.message.MessageLink#update
data . AddRange ( DataTypes . GetUUID ( playerUuid ) ) ;
data . AddRange ( DataTypes . GetUUID ( chatUuid ) ) ;
data . AddRange ( DataTypes . GetInt ( messageIndex ) ) ;
// message body
// net.minecraft.network.message.MessageBody#update
data . AddRange ( salt ) ;
data . AddRange ( DataTypes . GetLong ( timestamp . ToUnixTimeSeconds ( ) ) ) ;
byte [ ] messageBytes = Encoding . UTF8 . GetBytes ( message ) ;
data . AddRange ( DataTypes . GetInt ( messageBytes . Length ) ) ;
data . AddRange ( messageBytes ) ;
data . AddRange ( DataTypes . GetInt ( lastSeenMessages . Length ) ) ;
foreach ( AcknowledgedMessage ack in lastSeenMessages )
data . AddRange ( ack . signature ) ;
return data . ToArray ( ) ;
}
2022-08-27 02:10:44 +08:00
public static byte [ ] GetSignatureData ( byte [ ] ? precedingSignature , Guid sender , byte [ ] bodySign )
{
List < byte > data = new ( ) ;
if ( precedingSignature ! = null )
data . AddRange ( precedingSignature ) ;
data . AddRange ( sender . ToBigEndianBytes ( ) ) ;
data . AddRange ( bodySign ) ;
2022-08-15 23:55:44 +08:00
return data . ToArray ( ) ;
}
2023-01-13 15:53:33 +08:00
public static byte [ ] GetSignatureData ( string message , DateTimeOffset timestamp , ref byte [ ] salt , int messageCount , Guid sender , Guid sessionUuid )
2023-01-11 17:25:25 +08:00
{
List < byte > data = new ( ) ;
// TODO!
2023-01-13 15:53:33 +08:00
byte [ ] unknownInt1 = BitConverter . GetBytes ( 1 ) ;
Array . Reverse ( unknownInt1 ) ;
data . AddRange ( unknownInt1 ) ;
data . AddRange ( sender . ToBigEndianBytes ( ) ) ;
data . AddRange ( sessionUuid . ToBigEndianBytes ( ) ) ;
byte [ ] msgCountByte = BitConverter . GetBytes ( messageCount ) ;
Array . Reverse ( msgCountByte ) ;
data . AddRange ( msgCountByte ) ;
data . AddRange ( salt ) ;
byte [ ] timestampByte = BitConverter . GetBytes ( timestamp . ToUnixTimeSeconds ( ) ) ;
Array . Reverse ( timestampByte ) ;
data . AddRange ( timestampByte ) ;
byte [ ] msgByte = Encoding . UTF8 . GetBytes ( message ) ;
byte [ ] msgLengthByte = BitConverter . GetBytes ( msgByte . Length ) ;
Array . Reverse ( msgLengthByte ) ;
data . AddRange ( msgLengthByte ) ;
data . AddRange ( msgByte ) ;
byte [ ] unknownInt2 = BitConverter . GetBytes ( 0 ) ;
Array . Reverse ( unknownInt2 ) ;
data . AddRange ( unknownInt2 ) ;
2023-01-11 17:25:25 +08:00
return data . ToArray ( ) ;
}
2026-03-22 16:39:26 +00:00
// Delegate to the shared Json.EscapeString backed by System.Text.Json
public static string EscapeString ( string src ) = > Json . EscapeString ( src ) ;
2024-01-13 03:00:02 +08:00
2026-03-22 16:39:26 +00:00
public static JsonNode MakeDummyResponse ( )
2024-01-13 03:00:02 +08:00
{
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider ( 2048 ) ;
var mimePublicKey = Convert . ToBase64String ( rsa . ExportSubjectPublicKeyInfo ( ) ) ;
var mimePrivateKey = Convert . ToBase64String ( rsa . ExportPkcs8PrivateKey ( ) ) ;
string publicKeyPEM = $"-----BEGIN RSA PUBLIC KEY-----\n{mimePublicKey}\n-----END RSA PUBLIC KEY-----\n" ;
string privateKeyPEM = $"-----BEGIN RSA PRIVATE KEY-----\n{mimePrivateKey}\n-----END RSA PRIVATE KEY-----\n" ;
DateTime now = DateTime . UtcNow ;
DateTime expiresAt = now . AddHours ( 48 ) ;
DateTime refreshedAfter = now . AddHours ( 36 ) ;
string format = "yyyy-MM-ddTHH:mm:ss.ffffffZ" ;
2024-02-20 20:31:22 +08:00
2026-03-22 16:39:26 +00:00
return new JsonObject
{
["keyPair"] = new JsonObject
{
["privateKey"] = privateKeyPEM ,
["publicKey"] = publicKeyPEM
} ,
["publicKeySignature"] = "AA==" ,
["publicKeySignatureV2"] = "AA==" ,
["expiresAt"] = expiresAt . ToString ( format ) ,
["refreshedAfter"] = refreshedAfter . ToString ( format )
} ;
2024-01-13 03:00:02 +08:00
}
2022-08-15 23:55:44 +08:00
}
}