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;
}
}