StoredCredentials class

Пакет: com.hypixel.hytale.server.core.auth

Файл: com/hypixel/hytale/server/core/auth/EncryptedAuthCredentialStore.java

Поля (4)

МодификаторыТипИмя
String accessToken
Instant expiresAt
UUID profileUuid
String refreshToken

Исходный код

Показать/скрыть
class="kw">package com.hypixel.hytale.server.core.auth;

class="kw">import com.hypixel.hytale.codec.Codec;
class="kw">import com.hypixel.hytale.codec.KeyedCodec;
class="kw">import com.hypixel.hytale.codec.builder.BuilderCodec;
class="kw">import com.hypixel.hytale.common.util.HardwareUtil;
class="kw">import com.hypixel.hytale.logger.HytaleLogger;
class="kw">import com.hypixel.hytale.server.core.util.BsonUtil;
class="kw">import java.io.IOException;
class="kw">import java.nio.ByteBuffer;
class="kw">import java.nio.charset.StandardCharsets;
class="kw">import java.nio.file.Files;
class="kw">import java.nio.file.Path;
class="kw">import java.nio.file.attribute.PosixFilePermission;
class="kw">import java.security.SecureRandom;
class="kw">import java.time.Instant;
class="kw">import java.util.ArrayList;
class="kw">import java.util.List;
class="kw">import java.util.Set;
class="kw">import java.util.UUID;
class="kw">import java.util.logging.Level;
class="kw">import javax.annotation.Nonnull;
class="kw">import javax.annotation.Nullable;
class="kw">import javax.crypto.Cipher;
class="kw">import javax.crypto.SecretKey;
class="kw">import javax.crypto.SecretKeyFactory;
class="kw">import javax.crypto.spec.GCMParameterSpec;
class="kw">import javax.crypto.spec.PBEKeySpec;
class="kw">import javax.crypto.spec.SecretKeySpec;
class="kw">import org.bson.BsonDocument;

class="kw">public class EncryptedAuthCredentialStore class="kw">implements IAuthCredentialStore {
   class="kw">private class="kw">static class="kw">final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
   class="kw">private class="kw">static class="kw">final String ENV_AUTH_KEY = "HYTALE_AUTH_KEY";
   class="kw">private class="kw">static class="kw">final String ENV_AUTH_KEY_FILE = "HYTALE_AUTH_KEY_FILE";
   class="kw">private class="kw">static class="kw">final String ALGORITHM = "AES/GCM/NoPadding";
   class="kw">private class="kw">static class="kw">final int GCM_IV_LENGTH = 12;
   class="kw">private class="kw">static class="kw">final int GCM_TAG_LENGTH = 128;
   class="kw">private class="kw">static class="kw">final int KEY_LENGTH = 256;
   class="kw">private class="kw">static class="kw">final int PBKDF2_ITERATIONS = 100000;
   class="kw">private class="kw">static class="kw">final byte[] SALT = "HytaleAuthCredentialStore".getBytes(StandardCharsets.UTF_8);
   class="kw">private class="kw">static class="kw">final Set<PosixFilePermission> OWNER_ONLY_PERMISSIONS = Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE);
   class="kw">private class="kw">static class="kw">final BuilderCodec<EncryptedAuthCredentialStore.StoredCredentials> CREDENTIALS_CODEC = BuilderCodec.builder(
         EncryptedAuthCredentialStore.StoredCredentials.class, EncryptedAuthCredentialStore.StoredCredentials::new
      )
      .append(new KeyedCodec<>("AccessToken", Codec.STRING), (var0, var1) -> var0.accessToken = var1, var0 -> var0.accessToken)
      .add()
      .append(new KeyedCodec<>("RefreshToken", Codec.STRING), (var0, var1) -> var0.refreshToken = var1, var0 -> var0.refreshToken)
      .add()
      .append(new KeyedCodec<>("ExpiresAt", Codec.INSTANT), (var0, var1) -> var0.expiresAt = var1, var0 -> var0.expiresAt)
      .add()
      .append(new KeyedCodec<>("ProfileUuid", Codec.UUID_STRING), (var0, var1) -> var0.profileUuid = var1, var0 -> var0.profileUuid)
      .add()
      .build();
   class="kw">private class="kw">final Path path;
   @Nullable
   class="kw">private class="kw">final SecretKey encryptionKey;
   class="kw">private class="kw">final List<SecretKey> decryptionKeys;
   class="kw">private IAuthCredentialStore.OAuthTokens tokens = new IAuthCredentialStore.OAuthTokens(null, null, null);
   @Nullable
   class="kw">private UUID profile;

   class="kw">public EncryptedAuthCredentialStore(@Nonnull Path var1) {
      this.path = var1;
      EncryptedAuthCredentialStore.ResolvedPassphrases var2 = resolvePassphrases(var1);
      this.encryptionKey = var2.writePassphrase != null ? deriveKey(var2.writePassphrase) : null;
      this.decryptionKeys = deriveKeys(var2.readPassphrases);
      if (this.encryptionKey == null) {
         LOGGER.at(Level.WARNING).log("Cannot derive encryption key - encrypted storage will not persist credentials");
      } else {
         this.load();
      }
   }

   @Nullable
   class="kw">private class="kw">static SecretKey deriveKey(@Nonnull String var0) {
      PBEKeySpec var1 = new PBEKeySpec(var0.toCharArray(), SALT, 100000, 256);

      try {
         SecretKeyFactory var2 = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
         SecretKey var3 = var2.generateSecret(var1);
         class="kw">return new SecretKeySpec(var3.getEncoded(), "AES");
      } catch (Exception var8) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var8)).log("Failed to derive encryption key");
         class="kw">return null;
      } class="kw">finally {
         var1.clearPassword();
      }
   }

   @Nonnull
   class="kw">private class="kw">static List<SecretKey> deriveKeys(@Nonnull List<String> var0) {
      ArrayList var1 = new ArrayList<>();

      for (String var3 : var0) {
         SecretKey var4 = deriveKey(var3);
         if (var4 != null) {
            var1.add(var4);
         }
      }

      class="kw">return var1;
   }

   @Nonnull
   class="kw">private class="kw">static EncryptedAuthCredentialStore.ResolvedPassphrases resolvePassphrases(@Nonnull Path var0) {
      String var1 = null;
      String var2 = null;
      String var3 = null;
      String var4 = null;
      String var5 = System.getenv("HYTALE_AUTH_KEY_FILE");
      if (var5 != null && !var5.isBlank()) {
         String var6;
         try {
            var6 = Files.readString(Path.of(var5), StandardCharsets.UTF_8).trim();
         } catch (IOException var10) {
            throw new IllegalStateException("Failed to read encryption key file specified by HYTALE_AUTH_KEY_FILE: " + var5, var10);
         }

         if (var6.isEmpty()) {
            throw new IllegalStateException("Encryption key file specified by HYTALE_AUTH_KEY_FILE is empty: " + var5);
         }

         LOGGER.at(Level.INFO).log("Using encryption passphrase from %s", "HYTALE_AUTH_KEY_FILE");
         var1 = var6;
      }

      String var12 = System.getenv("HYTALE_AUTH_KEY");
      if (var12 != null && !var12.isBlank()) {
         LOGGER.at(Level.INFO).log("Using encryption passphrase from %s", "HYTALE_AUTH_KEY");
         var2 = var12;
      }

      UUID var7 = HardwareUtil.getUUID();
      if (var7 != null) {
         var3 = var7.toString();
      }

      var4 = loadGeneratedKeyFile(var0);
      String var8;
      if (var1 != null) {
         var8 = var1;
      } else if (var2 != null) {
         var8 = var2;
      } else if (var3 != null) {
         var8 = var3;
      } else if (var4 != null) {
         var8 = var4;
      } else {
         var8 = generateKeyFile(var0);
      }

      ArrayList var9 = new ArrayList<>(4);
      if (var1 != null) {
         var9.add(var1);
      }

      if (var2 != null) {
         var9.add(var2);
      }

      if (var4 != null) {
         var9.add(var4);
      }

      if (var3 != null) {
         var9.add(var3);
      }

      class="kw">return new EncryptedAuthCredentialStore.ResolvedPassphrases(var8, var9);
   }

   @Nonnull
   class="kw">public class="kw">static Path keyFilePath(@Nonnull Path var0) {
      String var1 = var0.getFileName().toString();
      String var2 = var1.contains(".") ? var1.substring(0, var1.lastIndexOf(46)) + ".key" : var1 + ".key";
      class="kw">return var0.resolveSibling(var2);
   }

   @Nullable
   class="kw">private class="kw">static String loadGeneratedKeyFile(@Nonnull Path var0) {
      Path var1 = keyFilePath(var0);
      if (!Files.exists(var1)) {
         class="kw">return null;
      }

      try {
         String var2 = Files.readString(var1, StandardCharsets.UTF_8).trim();
         if (!var2.isEmpty()) {
            LOGGER.at(Level.INFO).log("Using auto-generated encryption key from %s", var1);
            class="kw">return var2;
         }
      } catch (IOException var3) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var3)).log("Failed to read generated key file %s", var1);
      }

      class="kw">return null;
   }

   @Nullable
   class="kw">private class="kw">static String generateKeyFile(@Nonnull Path var0) {
      Path var1 = keyFilePath(var0);

      try {
         String var2 = UUID.randomUUID().toString();
         Files.writeString(var1, var2, StandardCharsets.UTF_8);
         restrictFilePermissions(var1);
         LOGGER.at(Level.INFO).log("Generated new encryption key file at %s", var1);
         class="kw">return var2;
      } catch (IOException var3) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var3)).log("Failed to generate key file at %s", var1);
         class="kw">return null;
      }
   }

   class="kw">private class="kw">static void restrictFilePermissions(@Nonnull Path var0) {
      try {
         Files.setPosixFilePermissions(var0, OWNER_ONLY_PERMISSIONS);
      } catch (UnsupportedOperationException var2) {
      } catch (IOException var3) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var3)).log("Failed to set file permissions on %s", var0);
      }
   }

   class="kw">private void load() {
      if (this.encryptionKey != null && Files.exists(this.path)) {
         try {
            byte[] var1 = Files.readAllBytes(this.path);
            byte[] var2 = null;
            boolean var3 = false;

            for (SecretKey var5 : this.decryptionKeys) {
               var2 = decrypt(var1, var5);
               if (var2 != null) {
                  if (!var5.equals(this.encryptionKey)) {
                     LOGGER.at(Level.INFO).log("Decrypted credentials using fallback key - will re-encrypt with current key");
                     var3 = true;
                  }
                  break;
               }
            }

            if (var2 == null) {
               LOGGER.at(Level.WARNING).log("Failed to decrypt credentials from %s - file may be corrupted or from different key source", this.path);
               class="kw">return;
            }

            BsonDocument var7 = BsonUtil.readFromBytes(var2);
            if (var7 == null) {
               LOGGER.at(Level.WARNING).log("Failed to parse credentials from %s", this.path);
               class="kw">return;
            }

            EncryptedAuthCredentialStore.StoredCredentials var8 = CREDENTIALS_CODEC.decode(var7);
            if (var8 != null) {
               this.tokens = new IAuthCredentialStore.OAuthTokens(var8.accessToken, var8.refreshToken, var8.expiresAt);
               this.profile = var8.profileUuid;
            }

            LOGGER.at(Level.INFO).log("Loaded encrypted credentials from %s", this.path);
            if (var3) {
               this.save();
               LOGGER.at(Level.INFO).log("Migrated credentials to current encryption key");
            }
         } catch (Exception var6) {
            ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var6)).log("Failed to load encrypted credentials from %s", this.path);
         }
      }
   }

   class="kw">private void save() {
      if (this.encryptionKey == null) {
         LOGGER.at(Level.WARNING).log("Cannot save credentials - no encryption key available");
      } else {
         try {
            EncryptedAuthCredentialStore.StoredCredentials var1 = new EncryptedAuthCredentialStore.StoredCredentials();
            var1.accessToken = this.tokens.accessToken();
            var1.refreshToken = this.tokens.refreshToken();
            var1.expiresAt = this.tokens.accessTokenExpiresAt();
            var1.profileUuid = this.profile;
            BsonDocument var2 = (BsonDocument)CREDENTIALS_CODEC.encode(var1);
            byte[] var3 = BsonUtil.writeToBytes(var2);
            byte[] var4 = this.encrypt(var3);
            if (var4 == null) {
               LOGGER.at(Level.SEVERE).log("Failed to encrypt credentials");
               class="kw">return;
            }

            Files.write(this.path, var4);
            restrictFilePermissions(this.path);
         } catch (IOException var5) {
            ((HytaleLogger.Api)LOGGER.at(Level.SEVERE).withCause(var5)).log("Failed to save encrypted credentials to %s", this.path);
         }
      }
   }

   @Nullable
   class="kw">private byte[] encrypt(@Nonnull byte[] var1) {
      if (this.encryptionKey == null) {
         class="kw">return null;
      }

      try {
         byte[] var2 = new byte[12];
         new SecureRandom().nextBytes(var2);
         Cipher var3 = Cipher.getInstance("AES/GCM/NoPadding");
         var3.init(1, this.encryptionKey, new GCMParameterSpec(128, var2));
         byte[] var4 = var3.doFinal(var1);
         ByteBuffer var5 = ByteBuffer.allocate(var2.length + var4.length);
         var5.put(var2);
         var5.put(var4);
         class="kw">return var5.array();
      } catch (Exception var6) {
         ((HytaleLogger.Api)LOGGER.at(Level.SEVERE).withCause(var6)).log("Encryption failed");
         class="kw">return null;
      }
   }

   @Nullable
   class="kw">private class="kw">static byte[] decrypt(@Nonnull byte[] var0, @Nonnull SecretKey var1) {
      if (var0.length < 12) {
         class="kw">return null;
      }

      try {
         ByteBuffer var2 = ByteBuffer.wrap(var0);
         byte[] var3 = new byte[12];
         var2.get(var3);
         byte[] var4 = new byte[var2.remaining()];
         var2.get(var4);
         Cipher var5 = Cipher.getInstance("AES/GCM/NoPadding");
         var5.init(2, var1, new GCMParameterSpec(128, var3));
         class="kw">return var5.doFinal(var4);
      } catch (Exception var6) {
         class="kw">return null;
      }
   }

   @Override
   class="kw">public void setTokens(@Nonnull IAuthCredentialStore.OAuthTokens var1) {
      this.tokens = var1;
      this.save();
   }

   @Nonnull
   @Override
   class="kw">public IAuthCredentialStore.OAuthTokens getTokens() {
      class="kw">return this.tokens;
   }

   @Override
   class="kw">public void setProfile(@Nullable UUID var1) {
      this.profile = var1;
      this.save();
   }

   @Nullable
   @Override
   class="kw">public UUID getProfile() {
      class="kw">return this.profile;
   }

   @Override
   class="kw">public void clear() {
      this.tokens = new IAuthCredentialStore.OAuthTokens(null, null, null);
      this.profile = null;

      try {
         Files.deleteIfExists(this.path);
      } catch (IOException var2) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var2)).log("Failed to delete encrypted credentials file %s", this.path);
      }
   }

   class="kw">private record ResolvedPassphrases(@Nullable String writePassphrase, @Nonnull List<String> readPassphrases) {
      class="kw">private ResolvedPassphrases {
      }
   }

   class="kw">private class="kw">static class StoredCredentials {
      @Nullable
      String accessToken;
      @Nullable
      String refreshToken;
      @Nullable
      Instant expiresAt;
      @Nullable
      UUID profileUuid;

      class="kw">private StoredCredentials() {
      }
   }
}