# CryptoUtils.java

    /*
    ** Module   : CryptoUtis.java
    ** Abstract : Provides support for the 4GL cryptography
    **
    ** Copyright (c) 2019-2024, Golden Code Development Corporation.
    **
    **
    ** -#- -I- --Date-- ----------------------------------------Description---------------------------------------
    ** 001 IAS 20190518 Created initial version.
    ** 002 CA  20220318 Fixed GENERATE-PBE-KEY, tail bytes are added by computing the hash of the previous hash
    **                  appended with the password.
    ** 003 TJD 20240220 Removed compiler warnings related with libraries updates
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.util;

    import java.nio.charset.*;
    import java.security.*;
    import java.util.*;
    import java.util.function.Supplier;

    import javax.crypto.*;
    import javax.crypto.spec.*;

    import org.apache.commons.codec.binary.*;
    import org.bouncycastle.crypto.*;
    import org.bouncycastle.crypto.digests.*;
    import org.bouncycastle.crypto.engines.*;
    import org.bouncycastle.crypto.generators.*;
    import org.bouncycastle.crypto.params.*;

    /**
     * Provides helper classes for the 4GL cryptography
     */

    public class CryptoUtils
    {
       /**
        * Supported symmetric ciphers' list as Set Please note that ImmutableSet.of()
        * "preserves order" so the names will be iterated in the order they are
        * specified below
        */
       public static final Set<String> CIPHERS = Collections.unmodifiableSet(
             new LinkedHashSet<>(Arrays.asList(
                   "DES3_CBC_168", "DES3_ECB_168", "DES3_CFB_168", "DES3_OFB_168", "DES_CBC_56",
                   "DES_ECB_56", "DES_CFB_56", "DES_OFB_56", "AES_CBC_128", "AES_CBC_192",
                   "AES_CBC_256", "AES_ECB_128", "AES_ECB_192", "AES_ECB_256", "AES_CFB_128",
                   "AES_CFB_192", "AES_CFB_256", "AES_OFB_128", "AES_OFB_192", "AES_OFB_256",
                   "RC4_ECB_128"
                )
             )
       );

       /**
        * Supported PBE hash algorithms
        */
       public static final Map<String, Supplier<Digest>> DIGESTS =
             Collections.unmodifiableMap(
                   new LinkedHashMap<String, Supplier<Digest>>()
                   {{
                      put("MD5", () -> new MD5Digest());
                      put("SHA-1", () -> new SHA1Digest());
                      put("SHA-256", () -> new SHA256Digest());
                      put("SHA-512", () -> new SHA512Digest());
                   }}
             );

       /**
        * Generates a password-based encryption key, based on the PKCS#5/RFC 2898 standard
        *
        * @param password
        *        password
        * @param salt
        *        optional salt
        * @param digestName
        *        a name of the hash algorithm
        * @param rounds
        *        a number of rounds
        *
        * @return
        *        a 16-byte password-based encryption key, based on the PKCS#5/RFC 2898
        * @throws NoSuchAlgorithmException
        */
       public static byte[] generatePbeKey(byte[] password, byte[] salt,
                                           String digestName, int rounds)
       throws NoSuchAlgorithmException
       {
          Supplier<Digest> digestFactory = DIGESTS.get(digestName.toUpperCase());
          if (digestFactory == null)
          {
             throw new UnsupportedOperationException("Unknown digest: [" + digestName + "]");
          }
          Digest digest = digestFactory.get();
          PBEParametersGenerator generator = new PKCS5S1ParametersGenerator(digest);
          byte[] bsalt;
          if (salt != null && salt.length > 0)
          {
             bsalt = new byte[8];
             System.arraycopy(salt, 0, bsalt, 0, Math.min(bsalt.length, salt.length));
          }
          else
          {
             bsalt = new byte[0];
          }
          int dL = digest.getDigestSize();
          int kL = SecurityPolicyManager.getSymmetricCiperParams().getKeySize();
          generator.init(password, bsalt, rounds);
          KeyParameter keyParameter = (KeyParameter)generator.generateDerivedParameters(
                   Math.min(dL, kL) * 8
          );
          if (kL <= dL)
          {
             return keyParameter.getKey();
          }

          // Generate tail bytes, which is not PBKDF1.  4GL uses the OpenSSL encryption library, which generates
          // the tail bytes by hashing the previous hash appended with the password, using this formula:
          // D_i = HASH^count(D_(i-1) || data || salt)
          byte[] key = new byte[kL];
          int pos = 0;
          byte[] k1 = keyParameter.getKey();
          System.arraycopy(k1, 0, key, 0, k1.length);
          pos += k1.length;
          kL -= dL;

          do
          {
             byte[] newPass = new byte[k1.length + password.length];
             // append the password to the previous hash
             System.arraycopy(k1, 0, newPass, 0, k1.length);
             System.arraycopy(password, 0, newPass, k1.length, password.length);
             generator.init(newPass, bsalt, rounds);

             keyParameter = (KeyParameter)generator.generateDerivedParameters(dL * 8);
             k1 = keyParameter.getKey();
             System.arraycopy(k1, 0, key, pos, Math.min(kL, k1.length));
             pos += k1.length;
             kL -= dL;
          }
          while (kL > dL );
          return key;
       }
       /**
        * The supported symmetric ciphers
        */
       public static enum CipherEngine
       {
          AES("AES"), DES("DES"), DES3("DESede"), RC4("RC4");
          private final String name;

          private CipherEngine(String name)
          {
             this.name = name;
          }

          /**
           * @see java.lang.Enum#toString()
           */
          @Override
          public String toString()
          {
             return name;
          }

       }

       /**
        * The supported symmetric ciphers' modes
        */
       public static enum CipherMode
       {
          CBC, CFB, ECB, OFB
       }

       /**
        * Symmetric cipher parameters
        */
       public static class CipherParams
       {
          /** Cipher algorithm name */
          private final CipherEngine engine;
          /** Cipher algorithm mode */
          private final CipherMode mode;
          /** Cipher key size */
          private final int keySize;
          /** Cipher IV size */
          private final int ivSize;

          /**
           * Constructor which creates CipherParams instance for a 4GL style spec string
           *
           * @param spec
           *        A 4GL style spec string
           */
          public CipherParams(String spec)
          {
             if (spec == null || !CIPHERS.contains(spec))
             {
                throw new IllegalArgumentException(
                      "Symmetic encryption algorithm [" + spec + "] is not supported");
             }
             String[] parts = spec.toUpperCase().split("_");
             this.engine = CipherEngine.valueOf(parts[0]);
             this.mode = CipherMode.valueOf(parts[1]);
             int bs = Integer.parseInt(parts[2]);
             if (engine == CipherEngine.DES)
             {
                bs = 64;
             }
             if (engine == CipherEngine.DES3)
             {
                bs = 192;
             }
             this.keySize = bs / 8;
             this.ivSize = mode == CipherMode.ECB ? 0
                   : engine == CipherEngine.DES || engine == CipherEngine.DES3 ? 8
                         : engine == CipherEngine.AES ? 16 : 0;
          }

          /**
           * Get cipher code
           *
           * @return cipher code
           */
          public CipherEngine getEngine()
          {
             return engine;
          }

          /**
           * Get cipher mode
           *
           * @return cipher mode
           */
          public CipherMode getMode()
          {
             return mode;
          }

          /**
           * Get key size in bytes
           *
           * @return key size in bytes
           */
          public int getKeySize()
          {
             return keySize;
          }

          /**
           * Get IV size in bytes
           *
           * @return IV size in bytes
           */
          public int getIvSize()
          {
             return ivSize;
          }

          /**
           * Get empty byte array of the required IV size
           *
           * @return empty byte array of the required IV size
           */
          public byte[] newIV()
          {
             return new byte[ivSize];
          }

          /**
           * Get empty byte array of the required key size
           *
           * @return empty byte array of the required key size
           */
          public byte[] newKey()
          {
             return new byte[keySize];
          }

          /**
           * Encrypt/Decript byte array
           *
           * @param data
           *        source data
           * @param mode
           *        operation mode
           * @param key
           *        encryption/decryption key
           * @param iv
           *        encryption/decryption IV
           *
           * @return
           *       the result of the encryption/decryption
           *
           * @throws NoSuchAlgorithmException
           * @throws NoSuchProviderException
           * @throws NoSuchPaddingException
           * @throws InvalidKeyException
           * @throws InvalidAlgorithmParameterException
           * @throws IllegalBlockSizeException
           * @throws BadPaddingException
           */
          public byte[] process(byte[] data, int mode, byte[] key, byte[] iv)
          throws NoSuchAlgorithmException,
                 NoSuchProviderException,
                 NoSuchPaddingException,
                 InvalidKeyException,
                 InvalidAlgorithmParameterException,
                 IllegalBlockSizeException,
                 BadPaddingException {
             Cipher cipher = cipher();
             SecretKeySpec secretKeySpec = new SecretKeySpec(key, engine.toString());
             byte[] ivn = newIV();
             if (iv == null || iv.length == 0)
             {
                if (ivn.length == 0)
                {
                   cipher.init(mode, secretKeySpec);
                }
                else
                {
                   cipher.init(mode, secretKeySpec, new IvParameterSpec(ivn));
                }
             }
             else
             {
                System.arraycopy(iv, 0, ivn, 0, Math.min(ivn.length, iv.length));
                cipher.init(mode, secretKeySpec, new IvParameterSpec(ivn));
             }
             return cipher.doFinal(data);
          }

          /**
           * return String representation of the CipherParams instance
           */
          @Override
          public String toString()
          {
             return "CipherParams [engine=" + engine + ", mode=" + mode + ", keySize=" + keySize
                   + ", ivSize=" + ivSize + "]";
          }

          /**
           * Get a new Cipher instance for the specified parameters
           *
           * @return a new Cipher instance for the specified parameters
           *
           * @throws NoSuchAlgorithmException
           * @throws NoSuchProviderException
           * @throws NoSuchPaddingException
           */
          public Cipher cipher()
                throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException
          {
             Cipher cipher = null;
             switch (engine)
             {
             case AES:
                BlockCipher bc = AESEngine.newInstance();
                switch (mode)
                {
                case CBC:
                   cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
                   break;
                case CFB:
                   cipher = Cipher.getInstance("AES/CFB/NoPadding");
                   break;
                case ECB:
                   cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
                   break;
                case OFB:
                   cipher = Cipher.getInstance("AES/OFB/NoPadding");
                   break;
                default:
                   break;
                }
                break;
             case DES:
                switch (mode)
                {
                case CBC:
                   cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
                   break;
                case CFB:
                   cipher = Cipher.getInstance("DES/CFB/NoPadding");
                   break;
                case ECB:
                   cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
                   break;
                case OFB:
                   cipher = Cipher.getInstance("DES/OFB/NoPadding");
                   break;
                default:
                   break;
                }
                break;
             case DES3:
                switch (mode)
                {
                case CBC:
                   cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
                   break;
                case CFB:
                   cipher = Cipher.getInstance("DESede/CFB/NoPadding");
                   break;
                case ECB:
                   cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
                   break;
                case OFB:
                   cipher = Cipher.getInstance("DESede/OFB/NoPadding");
                   break;
                default:
                   break;
                }
                break;
             case RC4:
                cipher = Cipher.getInstance("RC4/ECB/NoPadding");
             }
             return cipher;
          }
       }

       /**
        * Test encryption/decryption with supported algorithms
        * @param args
        *        command line args
        * @throws Exception
        */
       public static void main(String... args) throws Exception
       {
          final String plain = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eius"
    //                        + "mod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad m"
    //                        + "inim veniam, quis nostrud exercitation ullamco laboris nisi ut aliqu"
    //                        + "ip ex ea commodo consequat. Duis aute irure dolor in reprehenderit i"
    //                        + "n voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excep"
    //                        + "teur sint occaecat cupidatat non proident, sunt in culpa qui officia"
    //                        + " deserunt mollit anim id est laborum."
          ;
          testAll(plain);
          generatePbeKey("123".getBytes(), null, "SHA-1", 10);
          generatePbeKey("123".getBytes(), new byte[8], "SHA-1", 10);
          byte[] mac = new byte[] {
                0x60, 0x62, 0x7f, 0x77, 0x62, 0x75, 0x63, 0x63
          };
          byte[] key = new byte[] {
                0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30
          };
          for(int i = 0; i < key.length; i++)
          {
             key[i] ^= mac[i];
          }
          System.out.println(new String(key, StandardCharsets.UTF_8));
       }

       /**
        * Test encryption/decryption for all supported ciphers
        *
        * @param plain
        *        string to be encrypted
        *
        * @throws Exception
        */
       private static void testAll(final String plain) throws Exception
       {
          final Random rnd = new Random();
          for (String c : CIPHERS)
          {
             byte[] plainBytes = plain.getBytes(StandardCharsets.UTF_8);
             System.out.println(c);
             CipherParams cp = SecurityPolicyManager.getSymmetricCipherParams(c);
             System.out.println(cp.toString());
             boolean equals = test(rnd, plainBytes, cp);
             System.out.println(equals ? "OK." : "Failed.");
          }
       }

       /**
        * Test encryption/decryption for a particular cipher
        *
        * @param rnd
        *        Random generator
        * @param plainBytes
        *        bytes to be encrypted
        * @param cp
        *        cipher parameters
        *
        * @return <code>true</code> if operation succeeded or <code>false</code>
        *         otherwise
        *
        * @throws NoSuchAlgorithmException
        * @throws NoSuchProviderException
        * @throws NoSuchPaddingException
        * @throws InvalidKeyException
        * @throws InvalidAlgorithmParameterException
        * @throws IllegalBlockSizeException
        * @throws BadPaddingException
        */
       private static boolean test(Random rnd, byte[] plainBytes, CipherParams cp)
             throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException,
             InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException,
             BadPaddingException
       {
          byte[] key = cp.newKey();
          rnd.nextBytes(key);
          byte[] iv = cp.newIV();
          rnd.nextBytes(iv);

          // encrypt
          byte[] encrypted = cp.process(plainBytes, Cipher.ENCRYPT_MODE, key, iv);
          System.out.printf("%d %s\n", encrypted.length, String.valueOf(Hex.encodeHex(encrypted)));

          byte[] decrypted = cp.process(encrypted, Cipher.DECRYPT_MODE, key, iv);
          if (plainBytes.length != decrypted.length)
          {
             System.out.printf("%d != %d\n", plainBytes.length, decrypted.length);
             return false;
          }
          boolean equals = true;
          for (int i = 0; i < decrypted.length; i++)
          {
             if (decrypted[i] != plainBytes[i])
             {
                equals = false;
                System.out.printf("!%d: %d != %d\n", i, decrypted[i], plainBytes[i]);
             }
          }
          if (!equals)
          {
             String result = new String(decrypted, StandardCharsets.UTF_8);
             System.out.printf("[%s]\n", result);
          }
          return equals;
       }
    }