Cryptography in Java

Cryptography is the practice of securing communication from unauthorized access or disclosure using mathematical algorithms and protocols. It involves the use of codes and ciphers to transform messages into a form that is unreadable to anyone who does not have the key to decode it.

There are several cryptography techniques used to secure digital communication and protect sensitive data. Some of the most common techniques are:

  1. Symmetric Cryptography: Uses the same key for both encryption and decryption.
  2. Asymmetric Cryptography: Uses a pair of keys – a public key and a private key – to encrypt and decrypt messages.
  3. Hash Functions: A hash function is a mathematical function that converts a message or data into a fixed-length string of characters. Hash functions are commonly used for data integrity and authentication purposes.
  4. Digital Signatures: Digital signatures are a technique used to verify the authenticity and integrity of digital documents, messages, or software. They use public key cryptography to create a unique digital fingerprint of a message or document, which is added to the message or document. The recipient can then use the sender’s public key to verify the signature.

Hashing:

Hashing is a technique used in cryptography to convert data of any size into a fixed-size output, called a hash value or digest. The hash function takes an input message or data and applies a mathematical algorithm to create a fixed-size output. The resulting hash value is unique to the input data, and any small change to the input data results in a completely different hash value.

One of the most common uses of hashing is in password storage. Instead of storing the actual password, a hash of the password is stored. When a user enters their password, it is hashed and compared to the stored hash to verify its correctness.

Some commonly used hash functions include SHA-256 and MD5. However, as computing power increases, the security of some hash functions can be compromised, leading to the development of stronger hash functions.

public class HashManager {

    private static final String SHA2_ALGORITHM = "SHA-256";
    private final String algorithm;

    public HashManager() {
        this.algorithm = SHA2_ALGORITHM;
    }

    public HashManager(String algorithm) {
        this.algorithm = algorithm;
    }

    public byte[] generateRandomSalt(){
        byte[] salt = new byte[16];
        SecureRandom secureRandom = new SecureRandom();
        secureRandom.nextBytes(salt);
        return salt;
    }


    public byte[] createSHA2Hash(String input, byte[] salt) throws Exception{
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        byteStream.write(salt);
        byteStream.write(input.getBytes());
        byte[] valueToHash = byteStream.toByteArray();

        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        return messageDigest.digest(valueToHash);
    }

    public boolean verify(String input, byte[] hash, byte[] salt) throws Exception {
        byte[] digest = createSHA2Hash(input, salt);
        String digestChecksum = DatatypeConverter.printHexBinary(digest).toUpperCase();
        String hashChecksum = DatatypeConverter.printHexBinary(hash).toUpperCase();
        return digestChecksum.equals(hashChecksum);
    }

}

In cryptography, a salt is a random or pseudo-random string of data that is added to a plaintext before it is hashed, the salt is then stored alongside the resulting hash value. The purpose of using a salt is to prevent attackers from using precomputed tables of hashed values.

The HashManager class provides functionality to generate SHA-2 hash values and verify them against a given input and salt. Here’s an overview of the methods provided by this class:

  • generateRandomSalt(): Generates a random 16-byte salt value using the SecureRandom class and returns it as a byte array.
  • createSHA2Hash(String input, byte[] salt): Concatenates the salt and input byte arrays, computes the SHA-2 hash of the concatenated byte array, and returns the hash value as a byte array.
  • verify(String input, byte[] hash, byte[] salt): Computes the SHA-2 hash of the input string and salt, compares the resulting hash value to the hash value passed in as an argument, and returns true if they match, false otherwise.

Note that the createSHA2Hash() method uses the ByteArrayOutputStream class to concatenate the salt and input byte arrays before computing the hash value. This is done to ensure that the salt value is included in the hash computation and not just appended to the hash output.

Also note that the hash values returned by the createSHA2Hash() method are byte arrays, which are not very human-readable. If you need to display a hash value to a user, you might want to convert it to a more user-friendly format such as a hexadecimal string. You can do this using the DatatypeConverter.printHexBinary() method, as demonstrated in the verify() method.

Testing:

class HashManagerTest {

    @Test
    void createSHA2Hash() {
        assertAll(() -> {
            HashManager manager = new HashManager();
            String input1 = "String to hash";
            String input2 = "Another string to hash";

            byte[] salt = manager.generateRandomSalt();

            byte[] input1hash = manager.createSHA2Hash(input1, salt);
            byte[] input2hash = manager.createSHA2Hash(input2, salt);

            assertTrue(manager.verify(input1, input1hash, salt));
            assertTrue(manager.verify(input2, input2hash, salt));
            assertFalse(manager.verify(input1, input2hash, salt));
            assertFalse(manager.verify(input2, input1hash, salt));
        });
    }
}

Symmetric Cryptography:

In symmetric cryptography, the same key is used for both the encryption and decryption of a message. This means that both the sender and the receiver of a message must have access to the same key in order to securely communicate. Symmetric cryptography is also known as secret-key cryptography or shared-key cryptography.

The biggest drawback is key management. Because the same key is used for both encryption and decryption, the key must be kept secret and secure from unauthorized access.

Some commonly used symmetric encryption algorithms include Advanced Encryption Standard (AES), Data Encryption Standard (DES), and Triple DES (3DES).

public class AESManager {

    private static IvParameterSpec createIV() {
        byte[] iv = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
        return new IvParameterSpec(iv);
    }

    private static SecretKeySpec createSecretKeySpec(String secretKey) throws Exception {
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        String salt = "THE_SALT";
        KeySpec spec = new PBEKeySpec(secretKey.toCharArray(), salt.getBytes(), 65536, 256);
        SecretKey tmp = factory.generateSecret(spec);
        return new SecretKeySpec(tmp.getEncoded(), "AES");
    }

    public static String encrypt(String strToEncrypt, String key) throws Exception {

        // secret key
        SecretKeySpec secretKeySpec = createSecretKeySpec(key);

        // iv Parameter
        IvParameterSpec ivSpec = createIV();

        //Cipher ENCRYPT_MODE
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivSpec);

        byte[] bytes = strToEncrypt.getBytes(StandardCharsets.UTF_8);
        byte[] encrypted = cipher.doFinal(bytes);
        return bytesToB64String(encrypted);
    }

    public static String decrypt(String strToDecrypt, String key) throws Exception {

        // secret key
        SecretKeySpec secretKeySpec = createSecretKeySpec(key);

        //iv Parameter
        IvParameterSpec ivSpec = createIV();

        //Cipher DECRYPT_MODE
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivSpec);
        byte[] encrypted = b64StringToBytes(strToDecrypt);
        byte[] bytes = cipher.doFinal(encrypted);
        return new String(bytes);
    }


}

createIV():

  • This method creates an IvParameterSpec object, which is used to initialize the AES cipher in CBC mode. The initialization vector (IV) is set to an array of 16 zeros.

createSecretKeySpec(String secretKey):

  • This method creates a secret key for use with the AES cipher. It uses the PBKDF2 algorithm with HMAC-SHA256 to derive a 256-bit key from the given secretKey and a fixed salt value.
  • The resulting SecretKeySpec object is used to initialize the cipher in both the encryption and decryption methods.

encrypt(String strToEncrypt, String key):

  • This method takes in a plaintext string strToEncrypt and a key string and returns an encrypted string.
  • The plaintext is first converted to a byte array using UTF-8 encoding.
  • The createSecretKeySpec and createIV methods are called to create the SecretKeySpec and IvParameterSpec objects needed to initialize the cipher.
  • The Cipher class is used to initialize the cipher in encryption mode with the given SecretKeySpec and IvParameterSpec objects.
  • The plaintext byte array is encrypted using the doFinal method of the cipher and the resulting byte array is converted to a Base64-encoded string.

decrypt(String strToDecrypt, String key):

  • This method takes in an encrypted string strToDecrypt and a key string, and returns a plaintext string.
  • The Base64-encoded string is first decoded to a byte array using the b64StringToBytes method.
  • The createSecretKeySpec and createIV methods are called to create the SecretKeySpec and IvParameterSpec objects needed to initialize the cipher.
  • The Cipher class is used to initialize the cipher in decryption mode with the given SecretKeySpec and IvParameterSpec objects.
  • The encrypted byte array is decrypted using the doFinal method of the cipher and the resulting byte array is converted to a plaintext string using UTF-8 encoding.

Overall, this code provides a simple implementation of AES encryption and decryption in Java using CBC mode with PKCS#5 padding. However, it is important to note that the fixed IV and salt values used in the code are not secure and should be replaced with randomly generated values for actual use.

Testing:

class AESManagerTest {

    String key = "THE KEY";
    String plainText = "THE PLAIN TEXT";

    @Test
    @DisplayName("Decrypting an encrypted AES string, encrypted with the same decryption key, should return the original plan text")
    public void testAESManager() throws Exception {

        String encrypted = AESManager.encrypt(plainText, key);

        String decrypted = AESManager.decrypt(encrypted, key);

        assertEquals(plainText, decrypted);
    }
}

Asymmetric Cryptography:

Asymmetric cryptography, also known as public key cryptography, is a cryptographic technique that uses a pair of keys – a public key and a private key – to encrypt and decrypt messages. The keys are mathematically related but are not identical, so while the public key can be shared with anyone, the private key must be kept secret.

In asymmetric cryptography, the public key is used to encrypt a message, while the private key is used to decrypt the message. This means that anyone can send a message to the owner of the public key, but only the owner of the private key can read the message.

One of the advantages of asymmetric cryptography is that it does not require a secure channel for key exchange, as the public key can be openly shared.

Some commonly used asymmetric encryption algorithms include RSA, Elliptic Curve Cryptography (ECC), and Diffie-Hellman key exchange.

public class KeyPairManager {

    private static final String RSA = "RSA";
    private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    private final String algorithm;

    public KeyPairManager(String algorithm) {
        this.algorithm = algorithm;
    }

    public KeyPairManager() {
        this.algorithm = RSA;
    }

    public KeyPair generateRSAKeyPair() throws NoSuchAlgorithmException {
        SecureRandom secureRandom = new SecureRandom();
        java.security.KeyPairGenerator keyPairGenerator = java.security.KeyPairGenerator.getInstance(algorithm);
        keyPairGenerator.initialize(4096, secureRandom);
        return keyPairGenerator.generateKeyPair();
    }

    public PublicKey transformPublic(String publicKeyString) throws Exception {
        byte[] publicBytes = b64StringToBytes(publicKeyString);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        return keyFactory.generatePublic(keySpec);
    }

    public PrivateKey transformPrivate(String privateKeyString) throws Exception {
        byte[] privateBytes = b64StringToBytes(privateKeyString);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        return keyFactory.generatePrivate(keySpec);
    }

}

The class has two constructors, one that takes an algorithm parameter to specify the algorithm used for the key pair generation (defaulting to RSA), and one without parameters that uses the default algorithm.

The generateRSAKeyPair() method generates an RSA key pair with a key size of 4096 bits using a secure random number generator.

The transformPublic() and transformPrivate() methods convert Base64 encoded strings into PublicKey and PrivateKey objects, respectively, using the provided algorithm (RSA by default). These methods use the X509EncodedKeySpec and PKCS8EncodedKeySpec classes, respectively, to decode the Base64 strings and create key specifications, which are then used to create the public or private key objects via the KeyFactory class.

It’s worth noting that this code does not include any methods for persisting or managing key pairs, which may be necessary depending on the use case.

Testing:

class KeyPairManagerTest {

    KeyPairManager generator = new KeyPairManager();

    @Test
    void generateRSAKeyPair() {
        assertAll(() -> {
            KeyPair pair = generator.generateRSAKeyPair();
            String pbkStr = Base64Utils.bytesToB64String(pair.getPublic().getEncoded());
            String prkStr = Base64Utils.bytesToB64String(pair.getPrivate().getEncoded());

            System.out.printf("Public: %s\nPrivate: %s\n", pbkStr, prkStr);
        });
    }

    @Test
    void transformPublic() {

        assertAll(() -> {
            KeyPair pair = generator.generateRSAKeyPair();
            PublicKey publicKey = pair.getPublic();
            String pbKeyStr = Base64Utils.bytesToB64String(publicKey.getEncoded());
            PublicKey publicKey2 = generator.transformPublic(pbKeyStr);
            assertEquals(publicKey, publicKey2);
        });
    }

    @Test
    void transformPrivate() {
        assertAll(() -> {
            KeyPair pair = generator.generateRSAKeyPair();
            PrivateKey privateKey = pair.getPrivate();
            String prKeyStr = Base64Utils.bytesToB64String(privateKey.getEncoded());
            PrivateKey privateKey2 = generator.transformPrivate(prKeyStr);
            assertEquals(privateKey, privateKey2);
        });
    }
}

The RSA Manager:

public class RSAManager {

    private static final String RSA = "RSA";
    private static final String ALGORITHM = "RSA/ECB/PKCS1Padding";

    private final String algorithm;

    public RSAManager() {
        this.algorithm = ALGORITHM;
    }

    public RSAManager(String algorithm) {
        this.algorithm = algorithm;
    }

    public KeyPair generateRSAKeyPair() throws Exception {
        KeyPairManager keyPairGenerator = new KeyPairManager(RSA);
        return keyPairGenerator.generateRSAKeyPair();

    }

    public String encrypt(String plainText, Key key) throws Exception{
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] bytes = plainText.getBytes();
        byte[] encryptedBytes = cipher.doFinal(bytes);
        return bytesToB64String(encryptedBytes);
    }

    public String decrypt(String cipherText, Key key) throws Exception{
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, key);
        byte[] encryptedBytes = b64StringToBytes(cipherText);
        byte[] bytes = cipher.doFinal(encryptedBytes);
        return new String(bytes);
    }
}

The RSAManager class is a utility class that provides methods to encrypt and decrypt text using RSA encryption. It also provides a method to generate an RSA key pair using the KeyPairManager class.

The class has two constructors, one with no parameters that sets the encryption algorithm to “RSA/ECB/PKCS1Padding”, and one that takes an algorithm parameter that can be used to specify a different encryption algorithm.

The generateRSAKeyPair method creates an instance of KeyPairManager and uses it to generate an RSA key pair.

The encrypt method takes a plain text string and an instance of Key (which could be either a PublicKey or a PrivateKey) as input and returns the encrypted text as a Base64-encoded string. The method first initializes a Cipher instance for encryption, then encrypts the input text using the input key, and finally returns the encrypted text as a Base64-encoded string.

The decrypt method takes an encrypted text string and an instance of Key as input and returns the decrypted text as a string. The method first initializes a Cipher instance for decryption, then decrypts the input text using the input key, and finally returns the decrypted text as a string.

Testing:

class RSAManagerTest {

    RSAManager manager = new RSAManager();

    @Test
    void encryptPublicAndDecryptPrivate() {
        assertAll(() -> {
            KeyPair pair = manager.generateRSAKeyPair();
            String message = "THE_MESSAGE";
            String encMessage = manager.encrypt(message, pair.getPublic());
            String decMessage = manager.decrypt(encMessage, pair.getPrivate());
            assertEquals(message, decMessage);
        });

    }

    @Test
    void encryptPrivateAndDecryptPublic() {
        assertAll(() -> {
            KeyPair pair = manager.generateRSAKeyPair();
            String message = "THE_MESSAGE_2";
            String encMessage = manager.encrypt(message, pair.getPrivate());
            String decMessage = manager.decrypt(encMessage, pair.getPublic());
            assertEquals(message, decMessage);
        });
    }
}

Digital Signatures:

A digital signature is a cryptographic technique used to verify the authenticity and integrity of digital documents, messages, or software. It provides a way to ensure that a message or document has not been tampered with during transmission and that the sender is whom they claim to be.

Digital signatures use public key cryptography to create a unique digital fingerprint of a message or document. The sender uses their private key to create the digital signature, which is added to the message or document. The recipient can then use the sender’s public key to verify the signature, ensuring that the message or document has not been altered and that the sender is who they claim to be.

public class DigitalSignatureManager {

    private static final String SIGNING_ALGORITHM = "SHA256withRSA";
    private final String signingAlgorithm;

    public DigitalSignatureManager() {
        this.signingAlgorithm = SIGNING_ALGORITHM;
    }

    public DigitalSignatureManager(String signingAlgorithm) {
        this.signingAlgorithm = signingAlgorithm;
    }

    public String createDigitalSignature(byte[] input, PrivateKey privateKey) throws Exception {
        byte[] signature = createDigitalSignatureBytes(input, privateKey);
        return Base64Utils.bytesToB64String(signature);
    }

    public String createDigitalSignature(String input, PrivateKey privateKey) throws Exception {
        byte[] in = input.getBytes();
        return createDigitalSignature(in, privateKey);
    }

    public byte[] createDigitalSignatureBytes(byte[] input, PrivateKey privateKey) throws Exception {
        Signature signature = Signature.getInstance(signingAlgorithm);
        signature.initSign(privateKey);
        signature.update(input);
        return signature.sign();
    }

    public byte[] createDigitalSignatureBytes(String input, PrivateKey privateKey) throws Exception {
        byte[] in = input.getBytes();
        return createDigitalSignatureBytes(in, privateKey);
    }

    public boolean verifyDigitalSignature(byte[] input, byte[] signatureToVerify, PublicKey publicKey) throws Exception{
        Signature signature = Signature.getInstance(signingAlgorithm);
        signature.initVerify(publicKey);
        signature.update(input);
        return signature.verify(signatureToVerify);
    }

    public boolean verifyDigitalSignature(String input, String signatureToVerify, PublicKey publicKey) throws Exception{
        byte[] in = input.getBytes();
        byte[] signature = Base64Utils.b64StringToBytes(signatureToVerify);
        return verifyDigitalSignature(in, signature, publicKey);
    }


}

DigitalSignatureManager provides methods for creating and verifying digital signatures using the RSA algorithm.

The class has two constructors, one of which takes a signingAlgorithm parameter that allows the user to specify the signature algorithm to use, and the other uses the default value of “SHA256withRSA”.

The createDigitalSignature methods allow the user to create a digital signature for a given input data, either as a byte array or as a string, using a private key. The method returns the digital signature as a base64-encoded string.

The createDigitalSignatureBytes methods are similar to the createDigitalSignature methods, but they return the digital signature as a byte array instead of a base64-encoded string.

The verifyDigitalSignature methods allow the user to verify the authenticity of a digital signature for a given input data, either as a byte array or as a string, using a public key. The method returns true if the signature is valid, and false otherwise.

Overall, this class provides a simple interface for creating and verifying digital signatures using the RSA algorithm, which can be useful for ensuring the authenticity and integrity of data.

Testing:

class DigitalSignatureManagerTest {

    @Test
    void createAndVerifyDigitalSignature() {
        assertAll(() -> {

            KeyPairManager keyPairManager = new KeyPairManager();
            KeyPair pair = keyPairManager.generateRSAKeyPair();

            DigitalSignatureManager manager = new DigitalSignatureManager();
            String message = "The message must be signed";

            String signature = manager.createDigitalSignature(message, pair.getPrivate());

            assertTrue(manager.verifyDigitalSignature(message, signature, pair.getPublic()));
        });
    }
}

Cryptography is a crucial element in the security of modern communication systems. The strength of the encryption algorithm and the key length determine the level of security provided by cryptography. As technology advances, so does the need for more sophisticated cryptographic techniques to keep data secure.

Full code available: samsaydali7/info-sec (github.com)

Leave a Comment

Your email address will not be published. Required fields are marked *