File size: 7,072 Bytes
d46f4a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package net.minecraft.client.multiplayer;

import com.google.common.base.Strings;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.mojang.authlib.exceptions.MinecraftClientException;
import com.mojang.authlib.minecraft.UserApiService;
import com.mojang.authlib.minecraft.InsecurePublicKeyException.MissingException;
import com.mojang.authlib.yggdrasil.response.KeyPairResponse;
import com.mojang.authlib.yggdrasil.response.KeyPairResponse.KeyPair;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.JsonOps;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.PublicKey;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
import net.minecraft.SharedConstants;
import net.minecraft.Util;
import net.minecraft.util.Crypt;
import net.minecraft.util.CryptException;
import net.minecraft.world.entity.player.ProfileKeyPair;
import net.minecraft.world.entity.player.ProfilePublicKey;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.slf4j.Logger;

@OnlyIn(Dist.CLIENT)
public class AccountProfileKeyPairManager implements ProfileKeyPairManager {
    private static final Logger LOGGER = LogUtils.getLogger();
    private static final Duration MINIMUM_PROFILE_KEY_REFRESH_INTERVAL = Duration.ofHours(1L);
    private static final Path PROFILE_KEY_PAIR_DIR = Path.of("profilekeys");
    private final UserApiService userApiService;
    private final Path profileKeyPairPath;
    private CompletableFuture<Optional<ProfileKeyPair>> keyPair = CompletableFuture.completedFuture(Optional.empty());
    private Instant nextProfileKeyRefreshTime = Instant.EPOCH;

    public AccountProfileKeyPairManager(UserApiService p_253640_, UUID p_254415_, Path p_253813_) {
        this.userApiService = p_253640_;
        this.profileKeyPairPath = p_253813_.resolve(PROFILE_KEY_PAIR_DIR).resolve(p_254415_ + ".json");
    }

    @Override
    public CompletableFuture<Optional<ProfileKeyPair>> prepareKeyPair() {
        this.nextProfileKeyRefreshTime = Instant.now().plus(MINIMUM_PROFILE_KEY_REFRESH_INTERVAL);
        this.keyPair = this.keyPair.thenCompose(this::readOrFetchProfileKeyPair);
        return this.keyPair;
    }

    @Override
    public boolean shouldRefreshKeyPair() {
        return this.keyPair.isDone() && Instant.now().isAfter(this.nextProfileKeyRefreshTime) ? this.keyPair.join().map(ProfileKeyPair::dueRefresh).orElse(true) : false;
    }

    private CompletableFuture<Optional<ProfileKeyPair>> readOrFetchProfileKeyPair(Optional<ProfileKeyPair> p_254074_) {
        return CompletableFuture.supplyAsync(() -> {
            if (p_254074_.isPresent() && !p_254074_.get().dueRefresh()) {
                if (!SharedConstants.IS_RUNNING_IN_IDE) {
                    this.writeProfileKeyPair(null);
                }

                return p_254074_;
            } else {
                try {
                    ProfileKeyPair profilekeypair = this.fetchProfileKeyPair(this.userApiService);
                    this.writeProfileKeyPair(profilekeypair);
                    return Optional.ofNullable(profilekeypair);
                } catch (CryptException | MinecraftClientException | IOException ioexception) {
                    LOGGER.error("Failed to retrieve profile key pair", (Throwable)ioexception);
                    this.writeProfileKeyPair(null);
                    return p_254074_;
                }
            }
        }, Util.nonCriticalIoPool());
    }

    private Optional<ProfileKeyPair> readProfileKeyPair() {
        if (Files.notExists(this.profileKeyPairPath)) {
            return Optional.empty();
        } else {
            try {
                Optional optional;
                try (BufferedReader bufferedreader = Files.newBufferedReader(this.profileKeyPairPath)) {
                    optional = ProfileKeyPair.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(bufferedreader)).result();
                }

                return optional;
            } catch (Exception exception) {
                LOGGER.error("Failed to read profile key pair file {}", this.profileKeyPairPath, exception);
                return Optional.empty();
            }
        }
    }

    private void writeProfileKeyPair(@Nullable ProfileKeyPair p_254227_) {
        try {
            Files.deleteIfExists(this.profileKeyPairPath);
        } catch (IOException ioexception) {
            LOGGER.error("Failed to delete profile key pair file {}", this.profileKeyPairPath, ioexception);
        }

        if (p_254227_ != null) {
            if (SharedConstants.IS_RUNNING_IN_IDE) {
                ProfileKeyPair.CODEC.encodeStart(JsonOps.INSTANCE, p_254227_).ifSuccess(p_254406_ -> {
                    try {
                        Files.createDirectories(this.profileKeyPairPath.getParent());
                        Files.writeString(this.profileKeyPairPath, p_254406_.toString());
                    } catch (Exception exception) {
                        LOGGER.error("Failed to write profile key pair file {}", this.profileKeyPairPath, exception);
                    }
                });
            }
        }
    }

    @Nullable
    private ProfileKeyPair fetchProfileKeyPair(UserApiService p_253844_) throws CryptException, IOException {
        KeyPairResponse keypairresponse = p_253844_.getKeyPair();
        if (keypairresponse != null) {
            ProfilePublicKey.Data profilepublickey$data = parsePublicKey(keypairresponse);
            return new ProfileKeyPair(
                Crypt.stringToPemRsaPrivateKey(keypairresponse.keyPair().privateKey()),
                new ProfilePublicKey(profilepublickey$data),
                Instant.parse(keypairresponse.refreshedAfter())
            );
        } else {
            return null;
        }
    }

    private static ProfilePublicKey.Data parsePublicKey(KeyPairResponse p_253834_) throws CryptException {
        KeyPair keypair = p_253834_.keyPair();
        if (keypair != null
            && !Strings.isNullOrEmpty(keypair.publicKey())
            && p_253834_.publicKeySignature() != null
            && p_253834_.publicKeySignature().array().length != 0) {
            try {
                Instant instant = Instant.parse(p_253834_.expiresAt());
                PublicKey publickey = Crypt.stringToRsaPublicKey(keypair.publicKey());
                ByteBuffer bytebuffer = p_253834_.publicKeySignature();
                return new ProfilePublicKey.Data(instant, publickey, bytebuffer.array());
            } catch (IllegalArgumentException | DateTimeException datetimeexception) {
                throw new CryptException(datetimeexception);
            }
        } else {
            throw new CryptException(new MissingException("Missing public key"));
        }
    }
}