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.
- Best Practice: Use the strongest available instance of
- Avoid Seeding SecureRandom:
- Best Practice: Do not manually seed
SecureRandom
unless you have a secure and unpredictable seed.
- Best Practice: Do not manually 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
overJCEKS
as it’s more widely supported and considered more secure.
- Best Practice: Prefer
- 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[]
toString
can leave residual data in memory becauseString
objects are immutable and cannot be erased from memory immediately.
- Best Practice: Instead of “String pass = new String(password);” , can be used “char[] pass = password;”. Converting sensitive data like passwords from
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 asprime256v1
).
- Best Practice: Use standardized curves like
- 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.
- Best Practice: Generate keys using
- 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.
- Best Practice: Use
- 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.
- Best Practice: Always use
- Key Storage:
- Best Practice: Store keys securely using a
KeyStore
or similar secure storage.
- Best Practice: Store keys securely using a
- 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.