Cryptographic Implementations in Java (Best Practices and Tips)

Hey! In this blog post we will cover Java cryptographic implementations (best practices and tips). Let’s start!

We will start with RNG (Random Number Generator) which we need mostly to use in our projects, then will be talking about other topics.

1 – Secure Random Number Generation

import java.security.SecureRandom;
import java.util.Base64;

public class SecureRandomExample {

    public static void main(String[] args) throws Exception {
        SecureRandom secureRandom = SecureRandom.getInstanceStrong();
        byte[] randomBytes = new byte[32]; // 256 bits
        secureRandom.nextBytes(randomBytes);

        String randomBase64 = Base64.getEncoder().encodeToString(randomBytes);
        System.out.println("Secure Random Bytes: " + randomBase64);
    }
}

Best Practices and Improvements:

  • Use SecureRandom.getInstanceStrong():
    • Best Practice: Use the strongest available instance of SecureRandom for critical operations.
  • Avoid Seeding SecureRandom:
    • Best Practice: Do not manually seed SecureRandom unless you have a secure and unpredictable seed.

Additional Notes:

  • Entropy Sources: SecureRandom.getInstanceStrong() ensures that the random numbers are generated using the best available entropy source.

2 – Cryptographic Hash Functions

PBKDF2 Example;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

public class PasswordHashingExample {

    public static void main(String[] args) throws Exception {
        String password = "userPassword";
        byte[] salt = new byte[16];
        SecureRandom.getInstanceStrong().nextBytes(salt);

        PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 655360, 256); // 655360 iterations, 256-bit key
        SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        byte[] hash = skf.generateSecret(spec).getEncoded();

        String saltBase64 = Base64.getEncoder().encodeToString(salt);
        String hashBase64 = Base64.getEncoder().encodeToString(hash);

        System.out.println("Salt: " + saltBase64);
        System.out.println("Hash: " + hashBase64);
    }
}

Best Practices and Improvements:

  • Use HMAC for Authentication:
    • Best Practice: Use HMAC when you need to ensure data integrity and authenticity.
  • Password Hashing:
    • Best Practice: Use key derivation functions like PBKDF2, bcrypt, or scrypt for password storage.
  • Salt and Iterations:
    • Best Practice: Always use a unique salt and multiple iterations when hashing passwords. The salt size can be increased for more security if needed.

Additional Notes:

  • Password Security: Storing passwords securely is critical. Never store plain hashes; always use a salted and iterated hash function.
  • Iterations: The number of iterations should be sufficiently high to slow down brute-force attacks but not impact user experience.

More Information: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html

3 – Key Management and Storage

import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStore.SecretKeyEntry;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.util.Scanner;
import java.security.SecureRandom;

public class KeyStoreExample {
    private static final String KEYSTORE_TYPE = "PKCS12";
    private static final String KEY_ALGORITHM = "AES";
    private static final int KEY_SIZE = 256;
    private static final String KEYSTORE_FILENAME = "keystore.p12";
    
    public static void main(String[] args) {
        char[] keyStorePassword = null;
        Scanner scanner = null;
        
        try {
            // Generate a secret key with SecureRandom
            KeyGenerator keyGen = KeyGenerator.getInstance(KEY_ALGORITHM);
            keyGen.init(KEY_SIZE, SecureRandom.getInstanceStrong());
            SecretKey secretKey = keyGen.generateKey();
            
            // Prompt user for KeyStore password
            scanner = new Scanner(System.in);
            System.out.print("Enter KeyStore password: ");
            keyStorePassword = scanner.nextLine().toCharArray();
            
            // Validate password strength
            if (!isPasswordStrong(keyStorePassword)) {
                throw new IllegalArgumentException("Password is not strong enough. " +
                    "It must be at least 12 characters long and contain a mix of uppercase, lowercase, numbers, and special characters.");
            }
            
            // Create a KeyStore
            KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
            keyStore.load(null, keyStorePassword);
            
            // Set the entry
            KeyStore.ProtectionParameter protParam = 
                new KeyStore.PasswordProtection(keyStorePassword);
            SecretKeyEntry skEntry = new SecretKeyEntry(secretKey);
            keyStore.setEntry("secretKeyAlias", skEntry, protParam);
            
            // Create directory if it doesn't exist
            Path keystoreDir = Path.of(KEYSTORE_FILENAME).getParent();
            if (keystoreDir != null) {
                Files.createDirectories(keystoreDir);
            }
            
            // Store the KeyStore with restricted permissions
            try (FileOutputStream fos = new FileOutputStream(KEYSTORE_FILENAME)) {
                keyStore.store(fos, keyStorePassword);
                
                // Set file permissions to be readable only by owner
                Path keystorePath = Path.of(KEYSTORE_FILENAME);
                keystorePath.toFile().setReadable(false, false);
                keystorePath.toFile().setReadable(true, true);
                keystorePath.toFile().setWritable(false, false);
                keystorePath.toFile().setWritable(true, true);
            }
            
        } catch (Exception e) {
            System.err.println("Error creating KeyStore: " + e.getMessage());
            e.printStackTrace();
        } finally {
            // Clean up sensitive data
            if (keyStorePassword != null) {
                java.util.Arrays.fill(keyStorePassword, '\\0');
            }
            if (scanner != null) {
                scanner.close();
            }
        }
    }
    
    private static boolean isPasswordStrong(char[] password) {
        //String pass = new String(password);
        char[] pass = password;
        return password.length >= 12 &&
               pass.matches(".*[A-Z].*") &&    // At least one uppercase
               pass.matches(".*[a-z].*") &&    // At least one lowercase
               pass.matches(".*\\d.*") &&      // At least one digit
               pass.matches(".*[!@#$%^&*()].*"); // At least one special character
    }
}

Best Practices and Improvements:

  • Secure Password Handling:
    • Best Practice: Avoid hardcoding passwords; use environment variables or secure input methods.
  • Use Strong KeyStore Types:
    • Best Practice: Prefer PKCS12 over JCEKS as it’s more widely supported and considered more secure.
  • KeyStore Protection:
    • Best Practice: Protect the KeyStore file with proper file system permissions.
  • Variable Decleration:
    • Best Practice: Instead of “String pass = new String(password);” , can be used “char[] pass = password;”. Converting sensitive data like passwords from char[] to String can leave residual data in memory because String objects are immutable and cannot be erased from memory immediately.

Additional Notes:

  • Password Security: Reading passwords from the console prevents them from being exposed in the source code.
  • KeyStore Type: PKCS12 is a standard format and is preferred over proprietary formats.

4 – Elliptic Curve Cryptography (ECC)

import java.io.*;
import java.security.*;
import java.security.spec.*;
import java.util.Base64;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import javax.crypto.BadPaddingException;

public class ECDSAExample {
    private static final String CURVE_NAME = "secp256r1";
    private static final String SIGNATURE_ALGORITHM = "SHA256withECDSA";
    private static final String KEYSTORE_TYPE = "PKCS12";
    private static final String KEY_ALIAS = "ecdsaKey";
    
    private final KeyPair keyPair;
    private final Signature signature;
    
    public ECDSAExample() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
        this.keyPair = generateKeyPair();
        this.signature = initializeSignature();
    }
    
    private static KeyPair generateKeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
        ECGenParameterSpec ecSpec = new ECGenParameterSpec(CURVE_NAME);
        keyGen.initialize(ecSpec, SecureRandom.getInstanceStrong());
        return keyGen.generateKeyPair();
    }
    
    private static Signature initializeSignature() throws NoSuchAlgorithmException {
        return Signature.getInstance(SIGNATURE_ALGORITHM);
    }
    
    public void saveKeysToKeyStore(String keystorePath, char[] password) 
            throws KeyStoreException, IOException, NoSuchAlgorithmException, 
                   CertificateException {
        KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
        keyStore.load(null, password);
        
        // Store the key pair
        KeyStore.PrivateKeyEntry privateKeyEntry = new KeyStore.PrivateKeyEntry(
            keyPair.getPrivate(),
            new java.security.cert.Certificate[]{}
        );
        
        KeyStore.ProtectionParameter protectionParam = 
            new KeyStore.PasswordProtection(password);
        
        keyStore.setEntry(KEY_ALIAS, privateKeyEntry, protectionParam);
        
        // Save the KeyStore with restricted permissions
        try (FileOutputStream fos = new FileOutputStream(keystorePath)) {
            keyStore.store(fos, password);
            
            // Set file permissions
            File keystoreFile = new File(keystorePath);
            keystoreFile.setReadable(true, true);  // Readable only by owner
            keystoreFile.setWritable(true, true);  // Writable only by owner
        }
    }
    
    public byte[] sign(byte[] data) throws SignatureException, InvalidKeyException {
        synchronized(signature) {
            signature.initSign(keyPair.getPrivate(), SecureRandom.getInstanceStrong());
            signature.update(data);
            return signature.sign();
        }
    }
    
    public boolean verify(byte[] data, byte[] signatureBytes) 
            throws SignatureException, InvalidKeyException {
        synchronized(signature) {
            signature.initVerify(keyPair.getPublic());
            signature.update(data);
            return signature.verify(signatureBytes);
        }
    }
    
    public static class SignatureWrapper {
        private final byte[] data;
        private final byte[] signature;
        
        public SignatureWrapper(byte[] data, byte[] signature) {
            this.data = data.clone();
            this.signature = signature.clone();
        }
        
        public String getEncodedSignature() {
            return Base64.getEncoder().encodeToString(signature);
        }
        
        public byte[] getData() {
            return data.clone();
        }
        
        public byte[] getSignature() {
            return signature.clone();
        }
    }
    
    public static void main(String[] args) {
        try {
            ECDSAExample ecdsa = new ECDSAExample();
            String data = "Data to be signed using ECDSA";
            byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
            
            // Sign data
            byte[] digitalSignature = ecdsa.sign(dataBytes);
            SignatureWrapper wrapper = new SignatureWrapper(dataBytes, digitalSignature);
            
            System.out.println("ECDSA Signature: " + wrapper.getEncodedSignature());
            
            // Verify signature
            boolean isVerified = ecdsa.verify(wrapper.getData(), wrapper.getSignature());
            System.out.println("Signature Verified: " + isVerified);
            
            // Save keys to KeyStore
            char[] keystorePassword = "strongPassword123!@#".toCharArray();
            try {
                ecdsa.saveKeysToKeyStore("ecdsa-keystore.p12", keystorePassword);
                System.out.println("Keys saved to KeyStore successfully");
            } finally {
                // Clear sensitive data
                java.util.Arrays.fill(keystorePassword, '\\0');
            }
            
        } catch (InvalidKeyException e) {
            System.err.println("Invalid key: " + e.getMessage());
        } catch (SignatureException e) {
            System.err.println("Signature error: " + e.getMessage());
        } catch (NoSuchAlgorithmException e) {
            System.err.println("Algorithm not available: " + e.getMessage());
        } catch (Exception e) {
            System.err.println("An error occurred: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Best Practices and Improvements:

  • Use Recommended Curves:
    • Best Practice: Use standardized curves like secp256r1 (also known as prime256v1).
  • Key Storage:
    • Best Practice: Store keys securely instead of regenerating them each time.
  • Exception Handling and Encoding:
    • Best Practice: Handle exceptions properly and specify character encoding.

Additional Notes:

  • Advantages of ECC: ECC offers the same level of security as RSA but with smaller key sizes, leading to better performance.

5 – Digital Signatures

import java.io.*;
import java.security.*;
import java.security.cert.CertificateException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.crypto.BadPaddingException;

public class DigitalSignatureExample {
    private static final String ALGORITHM = "RSA";
    private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
    private static final int KEY_SIZE = 2048;
    private static final String KEYSTORE_TYPE = "PKCS12";
    private static final String KEY_ALIAS = "digitalSignatureKey";
    
    private final KeyPair keyPair;
    private final Signature signature;
    
    public DigitalSignatureExample() throws NoSuchAlgorithmException {
        this.keyPair = generateKeyPair();
        this.signature = initializeSignature();
    }
    
    private static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(ALGORITHM);
        // Use SecureRandom.getInstanceStrong() for better randomness
        keyPairGen.initialize(KEY_SIZE, SecureRandom.getInstanceStrong());
        return keyPairGen.generateKeyPair();
    }
    
    private static Signature initializeSignature() throws NoSuchAlgorithmException {
        return Signature.getInstance(SIGNATURE_ALGORITHM);
    }
    
    public void saveKeysToKeyStore(String keystorePath, char[] password) 
            throws KeyStoreException, IOException, NoSuchAlgorithmException, 
                   CertificateException {
        KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
        keyStore.load(null, password);
        
        // Store the key pair
        KeyStore.PrivateKeyEntry privateKeyEntry = new KeyStore.PrivateKeyEntry(
            keyPair.getPrivate(),
            new java.security.cert.Certificate[]{}
        );
        
        KeyStore.ProtectionParameter protectionParam = 
            new KeyStore.PasswordProtection(password);
        
        keyStore.setEntry(KEY_ALIAS, privateKeyEntry, protectionParam);
        
        // Save the KeyStore with restricted permissions
        try (FileOutputStream fos = new FileOutputStream(keystorePath)) {
            keyStore.store(fos, password);
            
            // Set file permissions to be restrictive
            File keystoreFile = new File(keystorePath);
            keystoreFile.setReadable(false, false);
            keystoreFile.setReadable(true, true);  // Readable only by owner
            keystoreFile.setWritable(false, false);
            keystoreFile.setWritable(true, true);  // Writable only by owner
        }
    }
    
    public SignatureResult sign(byte[] data) throws SignatureException, InvalidKeyException {
        if (data == null || data.length == 0) {
            throw new IllegalArgumentException("Data to sign cannot be null or empty");
        }
        
        synchronized(signature) {
            try {
                signature.initSign(keyPair.getPrivate(), SecureRandom.getInstanceStrong());
                signature.update(data);
                byte[] signatureBytes = signature.sign();
                return new SignatureResult(data, signatureBytes);
            } catch (NoSuchAlgorithmException e) {
                throw new SignatureException("Failed to initialize secure random", e);
            }
        }
    }
    
    public boolean verify(SignatureResult signatureResult) 
            throws SignatureException, InvalidKeyException {
        if (signatureResult == null) {
            throw new IllegalArgumentException("Signature result cannot be null");
        }
        
        synchronized(signature) {
            signature.initVerify(keyPair.getPublic());
            signature.update(signatureResult.getData());
            return signature.verify(signatureResult.getSignatureBytes());
        }
    }
    
    // Immutable class to hold signature results
    public static class SignatureResult {
        private final byte[] data;
        private final byte[] signatureBytes;
        
        public SignatureResult(byte[] data, byte[] signatureBytes) {
            this.data = data.clone();
            this.signatureBytes = signatureBytes.clone();
        }
        
        public byte[] getData() {
            return data.clone();
        }
        
        public byte[] getSignatureBytes() {
            return signatureBytes.clone();
        }
        
        public String getEncodedSignature() {
            return Base64.getEncoder().encodeToString(signatureBytes);
        }
    }
    
    public static void main(String[] args) {
        char[] keystorePassword = null;
        
        try {
            DigitalSignatureExample digitalSignature = new DigitalSignatureExample();
            String data = "This data needs to be signed.";
            byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
            
            // Sign data
            SignatureResult result = digitalSignature.sign(dataBytes);
            System.out.println("Digital Signature: " + result.getEncodedSignature());
            
            // Verify signature
            boolean isVerified = digitalSignature.verify(result);
            System.out.println("Signature Verified: " + isVerified);
            
            // Save keys to KeyStore
            keystorePassword = "StrongPassword123!@#".toCharArray();
            digitalSignature.saveKeysToKeyStore("digital-signature-keystore.p12", 
                                              keystorePassword);
            System.out.println("Keys saved to KeyStore successfully");
            
        } catch (InvalidKeyException e) {
            System.err.println("Invalid key error: " + e.getMessage());
        } catch (SignatureException e) {
            System.err.println("Signature error: " + e.getMessage());
        } catch (NoSuchAlgorithmException e) {
            System.err.println("Algorithm not available: " + e.getMessage());
        } catch (Exception e) {
            System.err.println("An error occurred: " + e.getMessage());
            e.printStackTrace();
        } finally {
            // Clear sensitive data
            if (keystorePassword != null) {
                java.util.Arrays.fill(keystorePassword, '\\0');
            }
        }
    }
}

Best Practices and Improvements:

  • Secure Key Generation and Storage:
    • Best Practice: Generate keys using SecureRandom and store them securely.
  • Use Appropriate Hash Algorithms:
    • Best Practice: Use strong hash functions like SHA-256 or higher.
  • Character Encoding:
    • Best Practice: Specify character encoding when converting strings to bytes.
  • Exception Handling:
    • Improvement: Handle exceptions properly to avoid revealing sensitive information.

Additional Notes:

  • Key Reuse: Avoid regenerating keys every time; instead, generate once and store securely.
  • SecureRandom in Signing: Using SecureRandom during signing enhances security against certain attacks.

6 – Symmetric Cryptography (AES)

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.security.KeyStore;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.io.*;

public class SymmetricCryptographyExample {
    private static final String ALGORITHM = "AES";
    private static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
    private static final int AES_KEY_SIZE = 256;
    private static final int GCM_NONCE_LENGTH = 12;
    private static final int GCM_TAG_LENGTH = 128; // In bits
    private static final String KEYSTORE_TYPE = "PKCS12";
    private static final String KEY_ALIAS = "aes-key";
    
    private final SecretKey key;
    private final SecureRandom secureRandom;
    
    public SymmetricCryptographyExample() throws Exception {
        this.key = generateKey();
        this.secureRandom = SecureRandom.getInstanceStrong();
    }
    
    private static SecretKey generateKey() throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
        keyGen.init(AES_KEY_SIZE, SecureRandom.getInstanceStrong());
        return keyGen.generateKey();
    }
    
    public void saveKeyToKeyStore(String keystorePath, char[] password) throws Exception {
        KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
        keyStore.load(null, password);
        
        KeyStore.SecretKeyEntry secretKeyEntry = new KeyStore.SecretKeyEntry(key);
        KeyStore.ProtectionParameter protectionParam = 
            new KeyStore.PasswordProtection(password);
        
        keyStore.setEntry(KEY_ALIAS, secretKeyEntry, protectionParam);
        
        try (FileOutputStream fos = new FileOutputStream(keystorePath)) {
            keyStore.store(fos, password);
            
            // Set restrictive file permissions
            File keystoreFile = new File(keystorePath);
            keystoreFile.setReadable(false, false);
            keystoreFile.setReadable(true, true);
            keystoreFile.setWritable(false, false);
            keystoreFile.setWritable(true, true);
        }
    }
    
    public static class EncryptedData {
        private final byte[] ciphertext;
        private final byte[] iv;
        
        public EncryptedData(byte[] ciphertext, byte[] iv) {
            this.ciphertext = ciphertext.clone();
            this.iv = iv.clone();
        }
        
        public byte[] getCiphertext() {
            return ciphertext.clone();
        }
        
        public byte[] getIv() {
            return iv.clone();
        }
        
        public String getEncodedCiphertext() {
            return Base64.getEncoder().encodeToString(ciphertext);
        }
        
        public String getEncodedIv() {
            return Base64.getEncoder().encodeToString(iv);
        }
        
        // Combine IV and ciphertext for storage/transmission
        public byte[] toBytes() {
            ByteBuffer buffer = ByteBuffer.allocate(iv.length + ciphertext.length);
            buffer.put(iv);
            buffer.put(ciphertext);
            return buffer.array();
        }
        
        public static EncryptedData fromBytes(byte[] combined) {
            ByteBuffer buffer = ByteBuffer.wrap(combined);
            byte[] iv = new byte[GCM_NONCE_LENGTH];
            buffer.get(iv);
            byte[] ciphertext = new byte[buffer.remaining()];
            buffer.get(ciphertext);
            return new EncryptedData(ciphertext, iv);
        }
    }
    
    public EncryptedData encrypt(byte[] plaintext) throws Exception {
        if (plaintext == null || plaintext.length == 0) {
            throw new IllegalArgumentException("Plaintext cannot be null or empty");
        }
        
        byte[] iv = new byte[GCM_NONCE_LENGTH];
        secureRandom.nextBytes(iv);
        
        Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
        GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
        
        byte[] ciphertext = cipher.doFinal(plaintext);
        return new EncryptedData(ciphertext, iv);
    }
    
    public byte[] decrypt(EncryptedData encryptedData) throws Exception {
        if (encryptedData == null) {
            throw new IllegalArgumentException("Encrypted data cannot be null");
        }
        
        Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
        GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, 
                                                            encryptedData.getIv());
        cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
        
        return cipher.doFinal(encryptedData.getCiphertext());
    }
    
    public static void main(String[] args) {
        char[] keystorePassword = null;
        
        try {
            SymmetricCryptographyExample crypto = new SymmetricCryptographyExample();
            String plaintext = "Hello, Symmetric Encryption!";
            System.out.println("Plaintext: " + plaintext);
            
            // Encrypt
            EncryptedData encryptedData = crypto.encrypt(
                plaintext.getBytes(StandardCharsets.UTF_8));
            System.out.println("Encrypted Text: " + encryptedData.getEncodedCiphertext());
            System.out.println("IV: " + encryptedData.getEncodedIv());
            
            // Decrypt
            byte[] decryptedBytes = crypto.decrypt(encryptedData);
            String decryptedText = new String(decryptedBytes, StandardCharsets.UTF_8);
            System.out.println("Decrypted Text: " + decryptedText);
            
            // Save key to KeyStore
            keystorePassword = "StrongPassword123!@#".toCharArray();
            crypto.saveKeyToKeyStore("symmetric-keystore.p12", keystorePassword);
            System.out.println("Key saved to KeyStore successfully");
            
        } catch (javax.crypto.AEADBadTagException e) {
            System.err.println("Authentication failed - data may have been tampered with");
        } catch (javax.crypto.IllegalBlockSizeException e) {
            System.err.println("Invalid data size: " + e.getMessage());
        } catch (javax.crypto.BadPaddingException e) {
            System.err.println("Decryption failed: " + e.getMessage());
        } catch (Exception e) {
            System.err.println("An error occurred: " + e.getMessage());
            e.printStackTrace();
        } finally {
            // Clear sensitive data
            if (keystorePassword != null) {
                java.util.Arrays.fill(keystorePassword, '\\0');
            }
        }
    }
}

Best Practices and Improvements:

  • Use SecureRandom Instance:
    • Best Practice: Use SecureRandom.getInstanceStrong() for generating nonces (IVs) and keys.
  • Unique Nonces (IVs):
    • Best Practice: Ensure the IV is unique for each encryption operation when using AES-GCM.
  • Store IV with Ciphertext:
    • Best Practice: Transmit or store the IV alongside the ciphertext, as it’s needed for decryption.
  • Explicit Character Encoding:
    • Best Practice: Specify character encoding when converting strings to bytes.
  • Authentication Tag Length:
    • Best Practice: Use a tag length of 128 bits (16 bytes) for AES-GCM to ensure message integrity.

Additional Notes:

  • IV Management: The IV is not secret and should be stored or transmitted with the ciphertext.
  • Authentication Tags: AES-GCM provides both confidentiality and integrity. Ensure the tag length is sufficient.

7 – Cert Pinning in Java

import javax.net.ssl.*;
import java.io.*;
import java.net.URL;
import java.security.*;
import java.security.cert.*;
import java.util.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;
import java.time.Duration;
import java.time.Instant;

public class CertificatePinningExample {
    private static final int CONNECT_TIMEOUT_MS = 10000;
    private static final int READ_TIMEOUT_MS = 15000;
    private static final String TLS_PROTOCOL = "TLSv1.3"; // Enforce TLS 1.3
    private static final Set<String> ALLOWED_CIPHERS = new HashSet<>(Arrays.asList(
        "TLS_AES_128_GCM_SHA256",
        "TLS_AES_256_GCM_SHA384"
    ));
    
    // Cache for certificate validation results
    private static final ConcurrentHashMap<String, CertValidationResult> validationCache = 
        new ConcurrentHashMap<>();
    private static final Duration CACHE_DURATION = Duration.ofMinutes(5);
    
    private final Map<String, Set<String>> pinnedPublicKeys;
    private final SSLContext sslContext;
    private final HostnameVerifier hostnameVerifier;
    
    public static class CertValidationResult {
        private final boolean isValid;
        private final Instant timestamp;
        
        public CertValidationResult(boolean isValid) {
            this.isValid = isValid;
            this.timestamp = Instant.now();
        }
        
        public boolean isValid() {
            return isValid && 
                   Duration.between(timestamp, Instant.now()).compareTo(CACHE_DURATION) < 0;
        }
    }
    
    public CertificatePinningExample(Map<String, Set<String>> pinnedPublicKeys) 
            throws GeneralSecurityException {
        this.pinnedPublicKeys = new HashMap<>(pinnedPublicKeys);
        this.sslContext = createSSLContext();
        this.hostnameVerifier = createHostnameVerifier();
    }
    
    private SSLContext createSSLContext() throws GeneralSecurityException {
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(
            TrustManagerFactory.getDefaultAlgorithm());
        tmf.init((KeyStore) null); // Use default trust store
        
        TrustManager[] trustManagers = new TrustManager[]{
            new X509TrustManager() {
                private final X509TrustManager defaultTrustManager = 
                    getDefaultTrustManager(tmf.getTrustManagers());
                
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType)
                        throws CertificateException {
                    defaultTrustManager.checkClientTrusted(chain, authType);
                }
                
                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType)
                        throws CertificateException {
                    if (chain == null || chain.length == 0) {
                        throw new CertificateException("Certificate chain is empty");
                    }
                    
                    // First, verify the certificate chain using the default trust manager
                    defaultTrustManager.checkServerTrusted(chain, authType);
                    
                    // Then verify the public key pin
                    String hostname = chain[0].getSubjectX500Principal().getName();
                    Set<String> expectedHashes = pinnedPublicKeys.get(hostname);
                    if (expectedHashes == null || expectedHashes.isEmpty()) {
                        throw new CertificateException("No pinned keys for " + hostname);
                    }
                    
                    try {
                        PublicKey publicKey = chain[0].getPublicKey();
                        String publicKeyHash = computePublicKeyHash(publicKey);
                        
                        if (!expectedHashes.contains(publicKeyHash)) {
                            throw new CertificateException(
                                "Public key hash does not match any pinned hash");
                        }
                        
                        // Cache successful validation
                        validationCache.put(hostname, new CertValidationResult(true));
                        
                    } catch (NoSuchAlgorithmException e) {
                        throw new CertificateException("Failed to compute key hash", e);
                    }
                }
                
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return defaultTrustManager.getAcceptedIssuers();
                }
            }
        };
        
        SSLContext context = SSLContext.getInstance(TLS_PROTOCOL);
        context.init(null, trustManagers, new SecureRandom());
        return context;
    }
    
    private X509TrustManager getDefaultTrustManager(TrustManager[] trustManagers) {
        for (TrustManager tm : trustManagers) {
            if (tm instanceof X509TrustManager) {
                return (X509TrustManager) tm;
            }
        }
        throw new IllegalStateException("No X509TrustManager found");
    }
    
    private String computePublicKeyHash(PublicKey publicKey) 
            throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        return Base64.getEncoder().encodeToString(
            md.digest(publicKey.getEncoded()));
    }
    
    private HostnameVerifier createHostnameVerifier() {
        return (hostname, session) -> {
            try {
                // Get the peer certificates
                Certificate[] certs = session.getPeerCertificates();
                if (certs.length == 0 || !(certs[0] instanceof X509Certificate)) {
                    return false;
                }
                
                X509Certificate serverCert = (X509Certificate) certs[0];
                
                // Verify hostname against the certificate
                return SSLCertificateSocketFactory.getDefaultHostnameVerifier()
                    .verify(hostname, serverCert);
                
            } catch (SSLException e) {
                return false;
            }
        };
    }
    
    public String makeHttpsRequest(String urlString) throws IOException {
        URL url = new URL(urlString);
        HttpsURLConnection connection = null;
        
        try {
            connection = (HttpsURLConnection) url.openConnection();
            connection.setSSLSocketFactory(sslContext.getSocketFactory());
            connection.setHostnameVerifier(hostnameVerifier);
            
            // Set timeouts
            connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
            connection.setReadTimeout(READ_TIMEOUT_MS);
            
            // Set secure properties
            connection.setRequestProperty("User-Agent", "CertificatePinningExample");
            connection.setRequestProperty("Accept", "text/plain, application/json");
            
            // Enable forward secrecy
            SSLSocketFactory sf = connection.getSSLSocketFactory();
            if (sf instanceof SSLSocketFactory) {
                SSLSocket socket = (SSLSocket) sf.createSocket();
                socket.setEnabledProtocols(new String[]{TLS_PROTOCOL});
                socket.setEnabledCipherSuites(
                    ALLOWED_CIPHERS.toArray(new String[0]));
            }
            
            // Make the request
            try (BufferedReader reader = new BufferedReader(
                     new InputStreamReader(connection.getInputStream(), 
                                         StandardCharsets.UTF_8))) {
                StringBuilder response = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    response.append(line).append('\\n');
                }
                return response.toString();
            }
            
        } catch (SSLHandshakeException e) {
            throw new SecurityException("SSL/TLS handshake failed: " + e.getMessage(), e);
        } catch (SSLException e) {
            throw new SecurityException("SSL/TLS error: " + e.getMessage(), e);
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
    
    public static void main(String[] args) {
        try {
            // Example pinned public key hashes
            Map<String, Set<String>> pinnedKeys = new HashMap<>();
            pinnedKeys.put("yourserver.com", new HashSet<>(Arrays.asList(
                "base64hash1",
                "base64hash2" // Backup hash
            )));
            
            CertificatePinningExample certPinning = 
                new CertificatePinningExample(pinnedKeys);
            
            String response = certPinning.makeHttpsRequest(
                "<https://yourserver.com/api/endpoint>");
            System.out.println("Response: " + response);
            
        } catch (SecurityException e) {
            System.err.println("Security error: " + e.getMessage());
        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Best Practices and Improvements:

  • Public Key Pinning:
    • Best Practice: Pin the public key instead of the certificate to accommodate certificate renewals.
    • Improvement: Extract the public key from the server’s certificate and compare its hash.
  • Avoid Hardcoding Certificates:
    • Best Practice: Avoid loading certificates from files in production.
    • Improvement: Embed the expected public key hash in the application code or configuration.
  • Hostname Verification:
    • Best Practice: Implement proper hostname verification to prevent man-in-the-middle attacks.
    • Improvement: Use a custom HostnameVerifier or ensure the default verifier is used.

Additional Notes:

  • Dynamic Certificate Updates: By pinning the public key hash, you allow the server to renew its certificate without breaking the pinning, as long as the public key remains the same.
  • Security Consideration: Hardcoding the public key hash reduces flexibility but increases security. Ensure you have a process for updating the hash when necessary.

8 – Asymmetric Cryptography in Java

import java.io.*;
import java.security.*;
import javax.crypto.Cipher;
import java.util.Base64;
import java.nio.charset.StandardCharsets;

public class AsymmetricCryptographyExample {
    private static final int KEY_SIZE = 2048;
    private static final String ALGORITHM = "RSA";
    private static final String TRANSFORMATION = "RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING";
    private final KeyPair keyPair;

    public AsymmetricCryptographyExample() throws NoSuchAlgorithmException {
        // Generate the RSA key pair with SecureRandom
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
        SecureRandom secureRandom = new SecureRandom();
        keyPairGenerator.initialize(KEY_SIZE, secureRandom);
        this.keyPair = keyPairGenerator.generateKeyPair();
        
        // Store keys in KeyStore (example implementation)
        storeKeys();
    }

    private void storeKeys() {
        try {
            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            keyStore.load(null, null);
            
            // Store private key
            KeyStore.PrivateKeyEntry privateKeyEntry = new KeyStore.PrivateKeyEntry(
                keyPair.getPrivate(),
                new Certificate[] { generateSelfSignedCertificate() }
            );
            
            keyStore.setEntry(
                "my-key-alias",
                privateKeyEntry,
                new KeyStore.PasswordProtection("keypass".toCharArray())
            );
            
            // Save KeyStore to file
            try (FileOutputStream fos = new FileOutputStream("keystore.p12")) {
                keyStore.store(fos, "storepass".toCharArray());
            }
        } catch (Exception e) {
            throw new SecurityException("Failed to store keys in KeyStore", e);
        }
    }

    private Certificate generateSelfSignedCertificate() {
        // Implementation for self-signed certificate generation
        // This is a placeholder - in production, use proper certificate management
        throw new UnsupportedOperationException("Certificate generation not implemented");
    }

    public String encrypt(String plainText) throws GeneralSecurityException {
        if (plainText == null || plainText.isEmpty()) {
            throw new IllegalArgumentException("Plain text cannot be null or empty");
        }
        
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
        
        byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    public String decrypt(String cipherText) throws GeneralSecurityException {
        if (cipherText == null || cipherText.isEmpty()) {
            throw new IllegalArgumentException("Cipher text cannot be null or empty");
        }
        
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
        
        byte[] cipherBytes = Base64.getDecoder().decode(cipherText);
        byte[] decryptedBytes = cipher.doFinal(cipherBytes);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    public static void main(String[] args) {
        try {
            AsymmetricCryptographyExample example = new AsymmetricCryptographyExample();
            
            String originalText = "Hello, World!";
            System.out.println("Original Text: " + originalText);
            
            String encryptedText = example.encrypt(originalText);
            System.out.println("Encrypted Text: " + encryptedText);
            
            String decryptedText = example.decrypt(encryptedText);
            System.out.println("Decrypted Text: " + decryptedText);
            
        } catch (GeneralSecurityException e) {
            System.err.println("Cryptographic error occurred: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Best Practices and Improvements:

  • Use SecureRandom:
    • Best Practice: Always use SecureRandom for key generation to ensure cryptographic strength.
  • Key Storage:
    • Best Practice: Store keys securely using a KeyStore or similar secure storage.
  • Exception Handling:
    • Best Practice: Handle exceptions properly and avoid exposing sensitive information.
    • Improvement: Replace e.printStackTrace() with proper logging or error messages.
  • Character Encoding:
    • Best Practice: Specify character encoding when converting strings to bytes.
    • Improvement: Use StandardCharsets.UTF_8 explicitly.
  • Data Size Considerations:
    • Best Practice: RSA is not suitable for encrypting large data directly.
    • Improvement: Implement hybrid encryption (encrypt data with a symmetric key and encrypt the symmetric key with RSA).

Additional Notes:

  • Hybrid Encryption: For encrypting larger data, generate a random AES key to encrypt the data and then encrypt the AES key with RSA.
  • Key Management: Implement key storage and retrieval using a KeyStore to avoid regenerating keys every time.

Recommendations:

  • OWASP Dev Guide – Principles of Cryptography https://owasp.org/www-project-developer-guide/draft/foundations/crypto_principles/
  • Java Cryptography Architecture (JCA) https://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/CryptoSpec.html

That was little bit long, thanks for your time and see you another post.