Skip to content

Commit 0ac0d09

Browse files
committed
feat: add MojangLauncher
1 parent 206e510 commit 0ac0d09

File tree

9 files changed

+430
-6
lines changed

9 files changed

+430
-6
lines changed

CmlLib.Core.Auth.Microsoft.sln

Lines changed: 150 additions & 6 deletions
Large diffs are not rendered by default.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<ItemGroup>
4+
<ProjectReference Include="..\..\src\CmlLib.Core.Auth.Microsoft.MojangLauncher\CmlLib.Core.Auth.Microsoft.MojangLauncher.csproj" />
5+
</ItemGroup>
6+
7+
<PropertyGroup>
8+
<OutputType>Exe</OutputType>
9+
<TargetFramework>net8.0</TargetFramework>
10+
<ImplicitUsings>enable</ImplicitUsings>
11+
<Nullable>enable</Nullable>
12+
</PropertyGroup>
13+
14+
</Project>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
using CmlLib.Core;
2+
using CmlLib.Core.Auth.Microsoft.MojangLauncher;
3+
4+
var accounts = MojangLauncherAccounts.Load(MinecraftPath.GetOSDefaultPath());
5+
accounts.GetSessions();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<ItemGroup>
4+
<ProjectReference Include="..\CmlLib.Core.Auth.Microsoft\CmlLib.Core.Auth.Microsoft.csproj" />
5+
</ItemGroup>
6+
7+
<PropertyGroup>
8+
<TargetFramework>netstandard2.1</TargetFramework>
9+
<ImplicitUsings>enable</ImplicitUsings>
10+
<Nullable>enable</Nullable>
11+
<LangVersion>12</LangVersion>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="9.0.8" />
16+
</ItemGroup>
17+
</Project>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
using System.ComponentModel;
2+
3+
namespace System.Runtime.CompilerServices
4+
{
5+
[EditorBrowsable(EditorBrowsableState.Never)]
6+
internal static class IsExternalInit {} // should be internal
7+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace CmlLib.Core.Auth.Microsoft.MojangLauncher;
5+
6+
public record MojangLauncherAccount(
7+
[property:JsonPropertyName("accessToken")] string? AccessToken,
8+
[property:JsonPropertyName("accessTokenExpiresAt")] string? AccessTokenExpiresAt,
9+
[property:JsonPropertyName("avatar")] string? Avatar,
10+
[property:JsonPropertyName("localId")] string? LocalId,
11+
[property:JsonPropertyName("minecraftProfile")] MojangLauncherMinecraftProfile? MinecraftProfile,
12+
[property:JsonPropertyName("remoteId")] string? RemoteId,
13+
[property:JsonPropertyName("type")] string? Type,
14+
[property:JsonPropertyName("username")] string? Username)
15+
{
16+
public static IReadOnlyList<MojangLauncherAccount> ReadFile(string path)
17+
{
18+
using var accountsFile = File.OpenRead(path);
19+
using var accountsDoc = JsonDocument.Parse(accountsFile);
20+
return accountsDoc.RootElement
21+
.GetProperty("accounts")
22+
.EnumerateObject()
23+
.Select(item => item.Value.Deserialize<MojangLauncherAccount>())
24+
.Where(item => item != null)
25+
.ToList()!;
26+
}
27+
}
28+
29+
public record MojangLauncherMinecraftProfile(
30+
[property:JsonPropertyName("id")] string? Id,
31+
[property:JsonPropertyName("name")] string? Name
32+
);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
namespace CmlLib.Core.Auth.Microsoft.MojangLauncher;
2+
3+
public class MojangLauncherAccounts
4+
{
5+
public static MojangLauncherAccounts Load(string path)
6+
{
7+
var accountsPath = Path.Combine(path, "launcher_accounts.json");
8+
var accounts = MojangLauncherAccount.ReadFile(accountsPath);
9+
10+
var msaCredentialsPath = Path.Combine(path, "launcher_msa_credentials.bin");
11+
var msaCredentials = MojangLauncherMsaCredentials.ReadEncryptedFile(msaCredentialsPath);
12+
13+
return new MojangLauncherAccounts(accounts, msaCredentials);
14+
}
15+
16+
private readonly IReadOnlyList<MojangLauncherAccount> _accounts;
17+
private readonly MojangLauncherMsaCredentials _msaCredentials;
18+
19+
private MojangLauncherAccounts(IReadOnlyList<MojangLauncherAccount> accounts, MojangLauncherMsaCredentials msaCredentials)
20+
{
21+
_accounts = accounts;
22+
_msaCredentials = msaCredentials;
23+
}
24+
25+
public void GetSessions()
26+
{
27+
foreach (var account in _accounts)
28+
{
29+
Console.WriteLine(account.AccessToken);
30+
Console.WriteLine(account.AccessTokenExpiresAt);
31+
Console.WriteLine(account.Avatar?.Length);
32+
Console.WriteLine(account.LocalId);
33+
Console.WriteLine(account.MinecraftProfile?.Id);
34+
Console.WriteLine(account.MinecraftProfile?.Name);
35+
Console.WriteLine(account.RemoteId);
36+
Console.WriteLine(account.Type);
37+
Console.WriteLine(account.Username);
38+
39+
var tokens = _msaCredentials
40+
.Tokens
41+
.Where(tokens => tokens.Xuid == account.RemoteId)
42+
.SelectMany(tokens => tokens.Tokens)
43+
.Where(token => token.RelyingParty == "rp://api.minecraftservices.com");
44+
45+
foreach (var token in tokens)
46+
{
47+
Console.WriteLine(token.IdentityType);
48+
Console.WriteLine(token.RelyingParty);
49+
Console.WriteLine(token.Sandbox);
50+
Console.WriteLine(token.TokenData.IssueInstant);
51+
Console.WriteLine(token.TokenData.NotAfter);
52+
Console.WriteLine(token.TokenData.Token);
53+
Console.WriteLine(token.TokenData.UserHash);
54+
Console.WriteLine(token.TokenType);
55+
}
56+
}
57+
}
58+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Security.Cryptography;
2+
using System.Text;
3+
using System.Text.Json;
4+
5+
namespace CmlLib.Core.Auth.Microsoft.MojangLauncher;
6+
7+
public class MojangLauncherMsaCredentials
8+
{
9+
public static MojangLauncherMsaCredentials ReadEncryptedFile(string path)
10+
{
11+
var fileContent = File.ReadAllBytes(path);
12+
var decryptedContent = ProtectedData.Unprotect(fileContent, null, DataProtectionScope.CurrentUser);
13+
var json = Encoding.UTF8.GetString(decryptedContent);
14+
using var doc = JsonDocument.Parse(json);
15+
16+
var activeUserXuid = doc.RootElement.GetProperty("activeUserXuid").GetString();
17+
var tokens = MsaParser.ParseFromRoot(doc.RootElement);
18+
return new MojangLauncherMsaCredentials
19+
{
20+
ActiveUserXuid = activeUserXuid,
21+
Tokens = tokens
22+
};
23+
}
24+
25+
public string? ActiveUserXuid { get; set; }
26+
public List<MsaRetailTokens> Tokens { get; set; } = [];
27+
28+
public IEnumerable<MsaRetailToken> FindTokens(string xuid, string relyingParty)
29+
{
30+
return Tokens
31+
.Where(tokens => tokens.Xuid == xuid)
32+
.SelectMany(tokens => tokens.Tokens)
33+
.Where(token => token.RelyingParty == relyingParty);
34+
}
35+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace CmlLib.Core.Auth.Microsoft.MojangLauncher;
5+
6+
public record MsaRetailTokens(
7+
string Xuid,
8+
IReadOnlyCollection<MsaRetailToken> Tokens
9+
);
10+
11+
public class MsaRetailToken
12+
{
13+
[property: JsonPropertyName("IdentityType")]
14+
public string? IdentityType { get; set; }
15+
16+
[property: JsonPropertyName("Sandbox")]
17+
public string? Sandbox { get; set; }
18+
19+
[property: JsonPropertyName("TokenType")]
20+
public string? TokenType { get; set; }
21+
22+
[property: JsonPropertyName("RelyingParty")]
23+
public string? RelyingParty { get; set; }
24+
25+
[property: JsonPropertyName("TokenData")]
26+
public MsaRetailTokenData TokenData { get; set; } = new();
27+
}
28+
29+
public class MsaRetailTokenData
30+
{
31+
[property: JsonPropertyName("Token")]
32+
public string? Token { get; set; }
33+
34+
[property: JsonPropertyName("NotAfter")]
35+
public string? NotAfter { get; set; }
36+
37+
[property: JsonPropertyName("IssueInstant")]
38+
public string? IssueInstant { get; set; }
39+
40+
[property: JsonIgnore]
41+
public string? UserHash { get; set; }
42+
}
43+
44+
public class MsaParser
45+
{
46+
public static List<MsaRetailTokens> ParseFromRoot(JsonElement root)
47+
{
48+
return root
49+
.GetProperty("credentials")
50+
.EnumerateObject()
51+
.Select(item =>
52+
{
53+
var xuid = item.Name;
54+
var tokens = item.Value.EnumerateObject()
55+
.Where(item => item.Name.Contains("RETAIL"))
56+
.SelectMany(item => ParseRetailToken(item.Value.ToString()))
57+
.ToList();
58+
return new MsaRetailTokens(xuid, tokens);
59+
})
60+
.ToList();
61+
}
62+
63+
public static List<MsaRetailToken> ParseRetailToken(string json)
64+
{
65+
using var doc = JsonDocument.Parse(json);
66+
return ParseRetailToken(doc.RootElement);
67+
}
68+
69+
public static List<MsaRetailToken> ParseRetailToken(JsonElement root)
70+
{
71+
try
72+
{
73+
return root
74+
.GetProperty("tokens")
75+
.EnumerateArray()
76+
.Select(element =>
77+
{
78+
var token = element.Deserialize<MsaRetailToken>();
79+
if (token == null)
80+
return null;
81+
82+
try
83+
{
84+
token.TokenData.UserHash = GetUserHash(element);
85+
}
86+
catch (Exception ex)
87+
{
88+
Console.WriteLine(ex.ToString());
89+
}
90+
return token;
91+
})
92+
.Where(token => token != null)
93+
.ToList()!;
94+
}
95+
catch
96+
{
97+
return [];
98+
}
99+
}
100+
101+
private static string GetUserHash(JsonElement token)
102+
{
103+
return token
104+
.GetProperty("TokenData")
105+
.GetProperty("DisplayClaims")
106+
.GetProperty("xui")
107+
.EnumerateArray()
108+
.First()
109+
.GetProperty("uhs")
110+
.ToString();
111+
}
112+
}

0 commit comments

Comments
 (0)