// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.core.auth;

import java.nio.ByteBuffer;
import java.security.spec.AlgorithmParameterSpec;
import java.security.Key;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.Cipher;
import java.security.SecureRandom;
import java.io.IOException;
import java.nio.file.OpenOption;
import org.bson.BsonDocument;
import org.bson.BsonValue;
import com.hypixel.hytale.server.core.util.BsonUtil;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import javax.crypto.spec.SecretKeySpec;
import java.security.spec.KeySpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.SecretKeyFactory;
import com.hypixel.hytale.common.util.HardwareUtil;
import java.util.logging.Level;
import java.time.Instant;
import javax.annotation.Nonnull;
import java.util.UUID;
import javax.annotation.Nullable;
import javax.crypto.SecretKey;
import java.nio.file.Path;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.logger.HytaleLogger;

public class EncryptedAuthCredentialStore implements IAuthCredentialStore
{
    private static final HytaleLogger LOGGER;
    private static final String ALGORITHM = "AES/GCM/NoPadding";
    private static final int GCM_IV_LENGTH = 12;
    private static final int GCM_TAG_LENGTH = 128;
    private static final int KEY_LENGTH = 256;
    private static final int PBKDF2_ITERATIONS = 100000;
    private static final byte[] SALT;
    private static final BuilderCodec<StoredCredentials> CREDENTIALS_CODEC;
    private final Path path;
    @Nullable
    private final SecretKey encryptionKey;
    private OAuthTokens tokens;
    @Nullable
    private UUID profile;
    
    public EncryptedAuthCredentialStore(@Nonnull final Path path) {
        this.tokens = new OAuthTokens(null, null, null);
        this.path = path;
        this.encryptionKey = deriveKey();
        if (this.encryptionKey == null) {
            EncryptedAuthCredentialStore.LOGGER.at(Level.WARNING).log("Cannot derive encryption key - encrypted storage will not persist credentials");
        }
        else {
            this.load();
        }
    }
    
    @Nullable
    private static SecretKey deriveKey() {
        final UUID hardwareId = HardwareUtil.getUUID();
        if (hardwareId == null) {
            return null;
        }
        try {
            final SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            final PBEKeySpec spec = new PBEKeySpec(hardwareId.toString().toCharArray(), EncryptedAuthCredentialStore.SALT, 100000, 256);
            final SecretKey tmp = factory.generateSecret(spec);
            return new SecretKeySpec(tmp.getEncoded(), "AES");
        }
        catch (final Exception e) {
            EncryptedAuthCredentialStore.LOGGER.at(Level.WARNING).withCause(e).log("Failed to derive encryption key");
            return null;
        }
    }
    
    private void load() {
        if (this.encryptionKey == null || !Files.exists(this.path, new LinkOption[0])) {
            return;
        }
        try {
            final byte[] encrypted = Files.readAllBytes(this.path);
            final byte[] decrypted = this.decrypt(encrypted);
            if (decrypted == null) {
                EncryptedAuthCredentialStore.LOGGER.at(Level.WARNING).log("Failed to decrypt credentials from %s - file may be corrupted or from different hardware", this.path);
                return;
            }
            final BsonDocument doc = BsonUtil.readFromBytes(decrypted);
            if (doc == null) {
                EncryptedAuthCredentialStore.LOGGER.at(Level.WARNING).log("Failed to parse credentials from %s", this.path);
                return;
            }
            final StoredCredentials stored = EncryptedAuthCredentialStore.CREDENTIALS_CODEC.decode(doc);
            if (stored != null) {
                this.tokens = new OAuthTokens(stored.accessToken, stored.refreshToken, stored.expiresAt);
                this.profile = stored.profileUuid;
            }
            EncryptedAuthCredentialStore.LOGGER.at(Level.INFO).log("Loaded encrypted credentials from %s", this.path);
        }
        catch (final Exception e) {
            EncryptedAuthCredentialStore.LOGGER.at(Level.WARNING).withCause(e).log("Failed to load encrypted credentials from %s", this.path);
        }
    }
    
    private void save() {
        if (this.encryptionKey == null) {
            EncryptedAuthCredentialStore.LOGGER.at(Level.WARNING).log("Cannot save credentials - no encryption key available");
            return;
        }
        try {
            final StoredCredentials stored = new StoredCredentials();
            stored.accessToken = this.tokens.accessToken();
            stored.refreshToken = this.tokens.refreshToken();
            stored.expiresAt = this.tokens.accessTokenExpiresAt();
            stored.profileUuid = this.profile;
            final BsonDocument doc = (BsonDocument)EncryptedAuthCredentialStore.CREDENTIALS_CODEC.encode(stored);
            final byte[] plaintext = BsonUtil.writeToBytes(doc);
            final byte[] encrypted = this.encrypt(plaintext);
            if (encrypted == null) {
                EncryptedAuthCredentialStore.LOGGER.at(Level.SEVERE).log("Failed to encrypt credentials");
                return;
            }
            Files.write(this.path, encrypted, new OpenOption[0]);
        }
        catch (final IOException e) {
            EncryptedAuthCredentialStore.LOGGER.at(Level.SEVERE).withCause(e).log("Failed to save encrypted credentials to %s", this.path);
        }
    }
    
    @Nullable
    private byte[] encrypt(@Nonnull final byte[] plaintext) {
        if (this.encryptionKey == null) {
            return null;
        }
        try {
            final byte[] iv = new byte[12];
            new SecureRandom().nextBytes(iv);
            final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            cipher.init(1, this.encryptionKey, new GCMParameterSpec(128, iv));
            final byte[] ciphertext = cipher.doFinal(plaintext);
            final ByteBuffer result = ByteBuffer.allocate(iv.length + ciphertext.length);
            result.put(iv);
            result.put(ciphertext);
            return result.array();
        }
        catch (final Exception e) {
            EncryptedAuthCredentialStore.LOGGER.at(Level.SEVERE).withCause(e).log("Encryption failed");
            return null;
        }
    }
    
    @Nullable
    private byte[] decrypt(@Nonnull final byte[] encrypted) {
        if (this.encryptionKey == null || encrypted.length < 12) {
            return null;
        }
        try {
            final ByteBuffer buffer = ByteBuffer.wrap(encrypted);
            final byte[] iv = new byte[12];
            buffer.get(iv);
            final byte[] ciphertext = new byte[buffer.remaining()];
            buffer.get(ciphertext);
            final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            cipher.init(2, this.encryptionKey, new GCMParameterSpec(128, iv));
            return cipher.doFinal(ciphertext);
        }
        catch (final Exception e) {
            EncryptedAuthCredentialStore.LOGGER.at(Level.WARNING).withCause(e).log("Decryption failed");
            return null;
        }
    }
    
    @Override
    public void setTokens(@Nonnull final OAuthTokens tokens) {
        this.tokens = tokens;
        this.save();
    }
    
    @Nonnull
    @Override
    public OAuthTokens getTokens() {
        return this.tokens;
    }
    
    @Override
    public void setProfile(@Nullable final UUID uuid) {
        this.profile = uuid;
        this.save();
    }
    
    @Nullable
    @Override
    public UUID getProfile() {
        return this.profile;
    }
    
    @Override
    public void clear() {
        this.tokens = new OAuthTokens(null, null, null);
        this.profile = null;
        try {
            Files.deleteIfExists(this.path);
        }
        catch (final IOException e) {
            EncryptedAuthCredentialStore.LOGGER.at(Level.WARNING).withCause(e).log("Failed to delete encrypted credentials file %s", this.path);
        }
    }
    
    static {
        // 
        // This method could not be decompiled.
        // 
        // Original Bytecode:
        // 
        //     3: putstatic       com/hypixel/hytale/server/core/auth/EncryptedAuthCredentialStore.LOGGER:Lcom/hypixel/hytale/logger/HytaleLogger;
        //     6: ldc_w           "HytaleAuthCredentialStore"
        //     9: getstatic       java/nio/charset/StandardCharsets.UTF_8:Ljava/nio/charset/Charset;
        //    12: invokevirtual   java/lang/String.getBytes:(Ljava/nio/charset/Charset;)[B
        //    15: putstatic       com/hypixel/hytale/server/core/auth/EncryptedAuthCredentialStore.SALT:[B
        //    18: ldc             Lcom/hypixel/hytale/server/core/auth/EncryptedAuthCredentialStore$StoredCredentials;.class
        //    20: invokedynamic   BootstrapMethod #0, get:()Ljava/util/function/Supplier;
        //    25: invokestatic    com/hypixel/hytale/codec/builder/BuilderCodec.builder:(Ljava/lang/Class;Ljava/util/function/Supplier;)Lcom/hypixel/hytale/codec/builder/BuilderCodec$Builder;
        //    28: new             Lcom/hypixel/hytale/codec/KeyedCodec;
        //    31: dup            
        //    32: ldc_w           "AccessToken"
        //    35: getstatic       com/hypixel/hytale/codec/Codec.STRING:Lcom/hypixel/hytale/codec/codecs/simple/StringCodec;
        //    38: invokespecial   com/hypixel/hytale/codec/KeyedCodec.<init>:(Ljava/lang/String;Lcom/hypixel/hytale/codec/Codec;)V
        //    41: invokedynamic   BootstrapMethod #1, accept:()Ljava/util/function/BiConsumer;
        //    46: invokedynamic   BootstrapMethod #2, apply:()Ljava/util/function/Function;
        //    51: invokevirtual   com/hypixel/hytale/codec/builder/BuilderCodec$Builder.append:(Lcom/hypixel/hytale/codec/KeyedCodec;Ljava/util/function/BiConsumer;Ljava/util/function/Function;)Lcom/hypixel/hytale/codec/builder/BuilderField$FieldBuilder;
        //    54: invokevirtual   com/hypixel/hytale/codec/builder/BuilderField$FieldBuilder.add:()Lcom/hypixel/hytale/codec/builder/BuilderCodec$BuilderBase;
        //    57: checkcast       Lcom/hypixel/hytale/codec/builder/BuilderCodec$Builder;
        //    60: new             Lcom/hypixel/hytale/codec/KeyedCodec;
        //    63: dup            
        //    64: ldc_w           "RefreshToken"
        //    67: getstatic       com/hypixel/hytale/codec/Codec.STRING:Lcom/hypixel/hytale/codec/codecs/simple/StringCodec;
        //    70: invokespecial   com/hypixel/hytale/codec/KeyedCodec.<init>:(Ljava/lang/String;Lcom/hypixel/hytale/codec/Codec;)V
        //    73: invokedynamic   BootstrapMethod #3, accept:()Ljava/util/function/BiConsumer;
        //    78: invokedynamic   BootstrapMethod #4, apply:()Ljava/util/function/Function;
        //    83: invokevirtual   com/hypixel/hytale/codec/builder/BuilderCodec$Builder.append:(Lcom/hypixel/hytale/codec/KeyedCodec;Ljava/util/function/BiConsumer;Ljava/util/function/Function;)Lcom/hypixel/hytale/codec/builder/BuilderField$FieldBuilder;
        //    86: invokevirtual   com/hypixel/hytale/codec/builder/BuilderField$FieldBuilder.add:()Lcom/hypixel/hytale/codec/builder/BuilderCodec$BuilderBase;
        //    89: checkcast       Lcom/hypixel/hytale/codec/builder/BuilderCodec$Builder;
        //    92: new             Lcom/hypixel/hytale/codec/KeyedCodec;
        //    95: dup            
        //    96: ldc_w           "ExpiresAt"
        //    99: getstatic       com/hypixel/hytale/codec/Codec.INSTANT:Lcom/hypixel/hytale/codec/function/FunctionCodec;
        //   102: invokespecial   com/hypixel/hytale/codec/KeyedCodec.<init>:(Ljava/lang/String;Lcom/hypixel/hytale/codec/Codec;)V
        //   105: invokedynamic   BootstrapMethod #5, accept:()Ljava/util/function/BiConsumer;
        //   110: invokedynamic   BootstrapMethod #6, apply:()Ljava/util/function/Function;
        //   115: invokevirtual   com/hypixel/hytale/codec/builder/BuilderCodec$Builder.append:(Lcom/hypixel/hytale/codec/KeyedCodec;Ljava/util/function/BiConsumer;Ljava/util/function/Function;)Lcom/hypixel/hytale/codec/builder/BuilderField$FieldBuilder;
        //   118: invokevirtual   com/hypixel/hytale/codec/builder/BuilderField$FieldBuilder.add:()Lcom/hypixel/hytale/codec/builder/BuilderCodec$BuilderBase;
        //   121: checkcast       Lcom/hypixel/hytale/codec/builder/BuilderCodec$Builder;
        //   124: new             Lcom/hypixel/hytale/codec/KeyedCodec;
        //   127: dup            
        //   128: ldc_w           "ProfileUuid"
        //   131: getstatic       com/hypixel/hytale/codec/Codec.UUID_STRING:Lcom/hypixel/hytale/codec/function/FunctionCodec;
        //   134: invokespecial   com/hypixel/hytale/codec/KeyedCodec.<init>:(Ljava/lang/String;Lcom/hypixel/hytale/codec/Codec;)V
        //   137: invokedynamic   BootstrapMethod #7, accept:()Ljava/util/function/BiConsumer;
        //   142: invokedynamic   BootstrapMethod #8, apply:()Ljava/util/function/Function;
        //   147: invokevirtual   com/hypixel/hytale/codec/builder/BuilderCodec$Builder.append:(Lcom/hypixel/hytale/codec/KeyedCodec;Ljava/util/function/BiConsumer;Ljava/util/function/Function;)Lcom/hypixel/hytale/codec/builder/BuilderField$FieldBuilder;
        //   150: invokevirtual   com/hypixel/hytale/codec/builder/BuilderField$FieldBuilder.add:()Lcom/hypixel/hytale/codec/builder/BuilderCodec$BuilderBase;
        //   153: checkcast       Lcom/hypixel/hytale/codec/builder/BuilderCodec$Builder;
        //   156: invokevirtual   com/hypixel/hytale/codec/builder/BuilderCodec$Builder.build:()Lcom/hypixel/hytale/codec/builder/BuilderCodec;
        //   159: putstatic       com/hypixel/hytale/server/core/auth/EncryptedAuthCredentialStore.CREDENTIALS_CODEC:Lcom/hypixel/hytale/codec/builder/BuilderCodec;
        //   162: return         
        // 
        // The error that occurred was:
        // 
        // java.lang.UnsupportedOperationException: The requested operation is not supported.
        //     at com.strobel.util.ContractUtils.unsupported(ContractUtils.java:27)
        //     at com.strobel.assembler.metadata.TypeReference.getRawType(TypeReference.java:284)
        //     at com.strobel.assembler.metadata.TypeReference.getRawType(TypeReference.java:279)
        //     at com.strobel.assembler.metadata.TypeReference.makeGenericType(TypeReference.java:154)
        //     at com.strobel.assembler.metadata.TypeSubstitutionVisitor.visitParameterizedType(TypeSubstitutionVisitor.java:225)
        //     at com.strobel.assembler.metadata.TypeSubstitutionVisitor.visitParameterizedType(TypeSubstitutionVisitor.java:25)
        //     at com.strobel.assembler.metadata.ParameterizedType.accept(ParameterizedType.java:103)
        //     at com.strobel.assembler.metadata.TypeSubstitutionVisitor.visit(TypeSubstitutionVisitor.java:40)
        //     at com.strobel.assembler.metadata.TypeSubstitutionVisitor.visitParameterizedType(TypeSubstitutionVisitor.java:211)
        //     at com.strobel.assembler.metadata.TypeSubstitutionVisitor.visitParameterizedType(TypeSubstitutionVisitor.java:25)
        //     at com.strobel.assembler.metadata.ParameterizedType.accept(ParameterizedType.java:103)
        //     at com.strobel.assembler.metadata.TypeSubstitutionVisitor.visit(TypeSubstitutionVisitor.java:40)
        //     at com.strobel.assembler.metadata.TypeSubstitutionVisitor.visitMethod(TypeSubstitutionVisitor.java:314)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferCall(TypeAnalysis.java:2611)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1040)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:790)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferCall(TypeAnalysis.java:2689)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1040)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:782)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:778)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1510)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:790)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferCall(TypeAnalysis.java:2689)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1040)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:790)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferCall(TypeAnalysis.java:2689)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1040)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:782)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:778)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1510)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:790)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferCall(TypeAnalysis.java:2689)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1040)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:782)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:778)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1083)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.runInference(TypeAnalysis.java:684)
        //     at com.strobel.decompiler.ast.TypeAnalysis.runInference(TypeAnalysis.java:667)
        //     at com.strobel.decompiler.ast.TypeAnalysis.runInference(TypeAnalysis.java:373)
        //     at com.strobel.decompiler.ast.TypeAnalysis.run(TypeAnalysis.java:95)
        //     at com.strobel.decompiler.ast.AstOptimizer.optimize(AstOptimizer.java:344)
        //     at com.strobel.decompiler.ast.AstOptimizer.optimize(AstOptimizer.java:42)
        //     at com.strobel.decompiler.languages.java.ast.AstMethodBodyBuilder.createMethodBody(AstMethodBodyBuilder.java:206)
        //     at com.strobel.decompiler.languages.java.ast.AstMethodBodyBuilder.createMethodBody(AstMethodBodyBuilder.java:93)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.createMethodBody(AstBuilder.java:868)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.createMethod(AstBuilder.java:761)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.addTypeMembers(AstBuilder.java:638)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.createTypeCore(AstBuilder.java:605)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.createTypeNoCache(AstBuilder.java:195)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.createType(AstBuilder.java:162)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.addType(AstBuilder.java:137)
        //     at com.strobel.decompiler.languages.java.JavaLanguage.buildAst(JavaLanguage.java:71)
        //     at com.strobel.decompiler.languages.java.JavaLanguage.decompileType(JavaLanguage.java:59)
        //     at com.strobel.decompiler.DecompilerDriver.decompileType(DecompilerDriver.java:333)
        //     at com.strobel.decompiler.DecompilerDriver.decompileJar(DecompilerDriver.java:254)
        //     at com.strobel.decompiler.DecompilerDriver.main(DecompilerDriver.java:129)
        // 
        throw new IllegalStateException("An error occurred while decompiling this method.");
    }
    
    private static class StoredCredentials
    {
        @Nullable
        String accessToken;
        @Nullable
        String refreshToken;
        @Nullable
        Instant expiresAt;
        @Nullable
        UUID profileUuid;
    }
}
