/*
 * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @author GeyserMC
 * @link https://github.com/GeyserMC/Geyser
 */

package org.geysermc.floodgate.crypto;

import lombok.RequiredArgsConstructor;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.security.Key;
import java.security.SecureRandom;

@RequiredArgsConstructor
public final class AesCipher implements FloodgateCipher {
    public static final int IV_LENGTH = 12;
    private static final int TAG_BIT_LENGTH = 128;
    private static final String CIPHER_NAME = "AES/GCM/NoPadding";

    private final SecureRandom secureRandom = new SecureRandom();
    private final Topping topping;
    private SecretKey secretKey;

    public void init(Key key) {
        if (!"AES".equals(key.getAlgorithm())) {
            throw new RuntimeException(
                    "Algorithm was expected to be AES, but got " + key.getAlgorithm()
            );
        }
        secretKey = (SecretKey) key;
    }

    public byte[] encrypt(byte[] data) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_NAME);

        byte[] iv = new byte[IV_LENGTH];
        secureRandom.nextBytes(iv);

        GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_LENGTH, iv);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
        byte[] cipherText = cipher.doFinal(data);

        if (topping != null) {
            iv = topping.encode(iv);
            cipherText = topping.encode(cipherText);
        }

        return ByteBuffer.allocate(HEADER.length + iv.length + cipherText.length + 1)
                .put(HEADER)
                .put(iv)
                .put((byte) 0x21)
                .put(cipherText)
                .array();
    }

    public byte[] decrypt(byte[] cipherTextWithIv) throws Exception {
        checkHeader(cipherTextWithIv);

        Cipher cipher = Cipher.getInstance(CIPHER_NAME);

        int bufferLength = cipherTextWithIv.length - HEADER.length;
        ByteBuffer buffer = ByteBuffer.wrap(cipherTextWithIv, HEADER.length, bufferLength);

        int ivLength = IV_LENGTH;

        if (topping != null) {
            int mark = buffer.position();

            // we need the first index, the second is for the actual data
            boolean found = false;
            while (buffer.hasRemaining() && !found) {
                if (buffer.get() == 0x21) {
                    found = true;
                }
            }

            ivLength = buffer.position() - mark - 1; // don't include the splitter itself
            // don't remove this cast, it'll cause problems if you remove it
            ((Buffer) buffer).position(mark); // reset to the pre-while index
        }

        byte[] iv = new byte[ivLength];
        buffer.get(iv);

        // don't remove this cast, it'll cause problems if you remove it
        ((Buffer) buffer).position(buffer.position() + 1); // skip splitter

        byte[] cipherText = new byte[buffer.remaining()];
        buffer.get(cipherText);

        if (topping != null) {
            iv = topping.decode(iv);
            cipherText = topping.decode(cipherText);
        }

        GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_LENGTH, iv);
        cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
        return cipher.doFinal(cipherText);
    }
}
