/**
 *  Copyright © 2025, Luis Andrés Lange <https://javacomm.net>
 *
 *  Previously released under Apache License 2.0; now licensed under MPL 2.0.
 *
 *  This Source Code Form is subject to the terms of the Mozilla Public
 *  License, v. 2.0. If a copy of the MPL was not distributed with this
 *  file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 *  ----------------------------------------------------------------------------
 *
 *  Exhibit B - "Incompatible With Secondary Licenses" Notice
 *
 *  This Source Code Form is "Incompatible With Secondary Licenses",
 *  as defined by the Mozilla Public License, v. 2.0.
 *
 *  In short:
 *  - This file may be used, modified, and distributed under MPL 2.0 only.
 *  - It may NOT be relicensed under GPL, LGPL, AGPL, or any other Secondary License.
 *
 *  Rationale:
 *  - Ensures that the code remains MPL-2.0.
 *  - Avoids legal conflicts with GPL-licensed libraries (e.g., VideoLAN).
 *  - Maximizes usability for commercial and security-critical applications.
 *
 */
package net.javacomm.protocol.crypto;

import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;



/**
 * Mit Crypto können Texte verschlüsselt werden. Unterstützt werden AES und RSA.
 */
public final class Crypto {

  public final static int IV_LENGTH = 12;

  public Crypto() {}



  /**
   * Erzeuge einen AES-Schlüssel mit einer 128 Bitlänge.
   * 
   * @return dieser AES-Schlüssel
   */
  public static SecretKey createAES() {
    return createAES(AES_Bitlength.AES_128);
  }



  /**
   * Erzeuge einen AES Schlüssel.
   * 
   * @return dieser AES-Schlüssel
   */
  public static SecretKey createAES(AES_Bitlength bitlength) {
    KeyGenerator keyGen;
    try {
      keyGen = KeyGenerator.getInstance("AES");
      keyGen.init(bitlength.getBitlength());
      return keyGen.generateKey();
    }
    catch (NoSuchAlgorithmException e) {
      throw new CryptoException(e);
    }
  }



  /**
   * Erstelle ein RSA-Schlüsselpaar.
   * 
   * @return dieses Schlüsselpaar
   * 
   * @throws NoSuchAlgorithmException
   */
  public static KeyPair createRSAGenerator() throws NoSuchAlgorithmException {
    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
    keyGen.initialize(2048); // Schlüsselgröße
    return keyGen.genKeyPair();
  }



  /**
   * Ein Geheimtext wird zu Klartext.
   * 
   * @param encrypted
   *                  dieser geheime Text ist Base64 codiert
   * @param secretKey
   *                  mit diesem Schlüssel wird dechiffriert
   * 
   * @return dieser Klartext
   */
  public static String decryptAES(String encrypted, SecretKey secretKey) {
    try {

      byte[] encryptedBytes = Base64.getDecoder().decode(encrypted);

      // Extrahiere IV
      byte[] iv = new byte[IV_LENGTH];
      System.arraycopy(encryptedBytes, 0, iv, 0, iv.length);

      // Extrahiere Ciphertext
      int ciphertextLength = encryptedBytes.length - IV_LENGTH;
      byte[] ciphertext = new byte[ciphertextLength];
      System.arraycopy(encryptedBytes, IV_LENGTH, ciphertext, 0, ciphertextLength);

      // Initialisiere Cipher
      Cipher cipher;
      byte[] encodedSecretkey = secretKey.getEncoded();

      switch(encodedSecretkey.length) {
        case 16:
          cipher = Cipher.getInstance("AES_128/GCM/NOPADDING");
          break;
        case 32:
          cipher = Cipher.getInstance("AES_256/GCM/NOPADDING");
          break;
        default:
          throw new CryptoException(encodedSecretkey.length + " - invalid bitlength");
      }

      GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);
      cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);

      // Entschlüssele die Daten
      byte[] plaintextBytes = cipher.doFinal(ciphertext);
      return new String(plaintextBytes, "UTF-8");
    }
    catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException
        | BadPaddingException | InvalidAlgorithmParameterException | UnsupportedEncodingException e) {
      throw new CryptoException(e);
    }
  }



  /**
   * Ein Klartext wird zu einem Geheimtext.
   * 
   * @param data
   *                  dieser Text wird verschlüsselt
   * @param secretKey
   *                  dieser Schlüssel
   * 
   * @return dieser geheime Text ist Base64 codiert
   */
  public static String encryptAES(String data, SecretKey secretKey) {
    try {
      Cipher cipher;
      byte[] encodedSecretkey = secretKey.getEncoded();
      switch(encodedSecretkey.length) {
        case 16:
          cipher = Cipher.getInstance("AES_128/GCM/NOPADDING");
          break;
        case 32:
          cipher = Cipher.getInstance("AES_256/GCM/NOPADDING");
          break;
        default:
          throw new CryptoException(encodedSecretkey.length + " - invalid bitlength");
      }

      // 128 Bit Taglänge wird für GCM empfohlen
      // iv 12 Bytes GCM-Spezifikation (RFC 5116)
      byte[] iv = new byte[IV_LENGTH];
      SecureRandom random = new SecureRandom();
      random.nextBytes(iv);
      GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);

      cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
      byte[] encryptedBytes = cipher.doFinal(data.getBytes()); // ohne IV

      // IV und encryptedBytes werden in combined gespeichert
      byte[] combined = new byte[iv.length + encryptedBytes.length];
      System.arraycopy(iv, 0, combined, 0, IV_LENGTH);
      System.arraycopy(encryptedBytes, 0, combined, IV_LENGTH, encryptedBytes.length);

      return Base64.getEncoder().encodeToString(combined);
    }
    catch (IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException
        | InvalidKeyException | InvalidAlgorithmParameterException e) {
      throw new CryptoException(e);
    }
  }



  /**
   * Verschlüssel einen Text mit einem RSA Public Key. Als Chiffre wird
   * "RSA/ECB/PKCS1Padding" verwendet.
   * 
   * @param data
   *                  dieser geheime Text
   * @param publicKey
   *                  RSA Public Key
   * @return der verschlüsselte Text
   */
  public static String encrypteRSA(String data, PublicKey publicKey) {
    Cipher cipher;
    try {
      cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
      cipher.init(Cipher.ENCRYPT_MODE, publicKey);
      byte[] encryptedBytes = cipher.doFinal(data.getBytes("UTF-8"));
      return Base64.getEncoder().encodeToString(encryptedBytes);
    }
    catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException
        | BadPaddingException | UnsupportedEncodingException e) {
      throw new CryptoException(e);
    }
  }



  /**
   * Gib den geheimen Text als Klartext zurück.
   * 
   * @param data
   *                   dieser Text ist verschlüsselt
   * @param privateKey
   *                   dieser Private Key
   * 
   * @return dieser Klartext
   */
  public static String decryptRSA(String data, PrivateKey privateKey) {

    try {
      byte[] encryptedBytes = Base64.getDecoder().decode(data);
      Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
      cipher.init(Cipher.DECRYPT_MODE, privateKey);
      byte[] decryptedBytes = cipher.doFinal(encryptedBytes);

      // Ergebnis in String umwandeln
      String decryptedText = new String(decryptedBytes, "UTF-8");
      return decryptedText;
    }
    catch (Exception e) {
      throw new CryptoException(e);
    }

  }



  /**
   * Von Base64 in einen AES Schlüssel zurückverwandeln.
   * 
   * @param base64Key
   *                  dieser AES Schlüssel ist Base64 codiert
   * 
   * @return dieser AES Schlüssel
   */
  public static SecretKey getAESFromBase64(String base64Key) {
    byte[] decodedKey = Base64.getDecoder().decode(base64Key);
    return new SecretKeySpec(decodedKey, "AES");
  }



  /**
   * Konvertiere von SecretKey nach Base64.
   * 
   * @param secretKey
   *                  dieser Schlüssel
   * 
   * @return dieser Schlüssel als Base64 String
   */
  public static String getBase64FromAES(SecretKey secretKey) {
    return Base64.getEncoder().encodeToString(secretKey.getEncoded());
  }



  /**
   * Lade den RSA Public Key aus einem Base64 codierten String. Der codierte
   * String ist X509 formatiert.
   * 
   * @param publicKeyPEM
   *                     dieser RSA Public Key ist Base64 codiert
   * @return dieser Public Key
   * 
   */
  public static PublicKey loadPublicRSAKey(String publicKeyPEM) {
    try {
      byte[] decodedKey = Base64.getDecoder().decode(publicKeyPEM);
      X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey);
      KeyFactory keyFactory = KeyFactory.getInstance("RSA");
      return keyFactory.generatePublic(keySpec);
    }
    catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
      throw new CryptoException(e);
    }
  }



  /**
   * Lade den RSA Private Key aus einem Base64 codierten String. Der codierte
   * String ist X509 formatiert.
   * 
   * @param privateKeyPEM
   *                      dieser RSA Private Key ist Base64 codiert
   * 
   * @return dieser Private Key
   * 
   */
  public static PrivateKey loadPrivateRSAKey(String privateKeyPEM) {
    try {
      byte[] decodedKey = Base64.getDecoder().decode(privateKeyPEM);
      PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
      KeyFactory keyFactory = KeyFactory.getInstance("RSA");
      return keyFactory.generatePrivate(keySpec);
    }
    catch (Exception e) {
      throw new CryptoException(e);
    }
  }

}
