Skip to main content

Cryptography & Secure Design

You don't need to implement cryptographic algorithms โ€” you need to choose and use them correctly. Most vulnerabilities come from misuse, not math.

Related

See Keys, Signing & TLS for deep dives into public/private keys, JWKS, MLE, and TLS internals.


Core Concepts at a Glanceโ€‹

ConceptPurposeAlgorithm
Symmetric EncryptionEncrypt/decrypt with same keyAES-256-GCM
Asymmetric EncryptionEncrypt with public, decrypt with privateRSA-OAEP
HashingOne-way fingerprintSHA-256, SHA-3
Password HashingSlow hash with saltArgon2id, BCrypt
MACProve message integrity + authenticityHMAC-SHA256
Digital SignatureAuthenticity + non-repudiationRSA-PSS, ECDSA
Key ExchangeEstablish shared secret over public channelECDH
Authenticated EncryptionConfidentiality + integrity in oneAES-256-GCM

AES-GCM โ€” Symmetric Encryptionโ€‹

AES-256-GCM provides confidentiality (encryption) AND integrity (authentication tag). Always prefer over AES-CBC.

@Service
public class AesEncryptionService {
private static final int KEY_SIZE = 256;
private static final int IV_SIZE = 12; // 96-bit IV for GCM
private static final int TAG_LEN = 128; // Auth tag length

private final SecretKey secretKey;

public AesEncryptionService(@Value("${encryption.key}") String base64Key) {
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
this.secretKey = new SecretKeySpec(keyBytes, "AES");
}

public String encrypt(String plaintext) throws Exception {
// โš ๏ธ Generate FRESH random IV for EVERY encryption โ€” never reuse!
byte[] iv = new byte[IV_SIZE];
new SecureRandom().nextBytes(iv);

Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LEN, iv));
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));

// Prepend IV to ciphertext (IV is NOT secret, just must be unique per key)
byte[] combined = new byte[IV_SIZE + encrypted.length];
System.arraycopy(iv, 0, combined, 0, IV_SIZE);
System.arraycopy(encrypted, 0, combined, IV_SIZE, encrypted.length);
return Base64.getEncoder().encodeToString(combined);
}

public String decrypt(String ciphertext) throws Exception {
byte[] combined = Base64.getDecoder().decode(ciphertext);
byte[] iv = Arrays.copyOfRange(combined, 0, IV_SIZE);
byte[] encrypted = Arrays.copyOfRange(combined, IV_SIZE, combined.length);

Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LEN, iv));
try {
return new String(cipher.doFinal(encrypted), StandardCharsets.UTF_8);
} catch (AEADBadTagException e) {
throw new TamperingDetectedException("Ciphertext was tampered with");
}
}

public static String generateKey() throws Exception {
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(KEY_SIZE, new SecureRandom());
return Base64.getEncoder().encodeToString(kg.generateKey().getEncoded());
}
}

Common AES Pitfallsโ€‹

MistakeConsequenceFix
Reusing IV with same keyComplete plaintext recoveryAlways generate random IV per encryption
AES-CBC without MACPadding oracle attacksUse AES-GCM (includes auth tag)
Hardcoded keyKey in repo/binaryLoad from Vault / Secrets Manager
ECB modePatterns visible in ciphertextNever use ECB

HMAC โ€” Message Authentication Codeโ€‹

Proves integrity + authenticity of a message (requires a shared secret key).

public String generateHmac(String message, String secretKey) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(keySpec);
return Base64.getEncoder().encodeToString(
mac.doFinal(message.getBytes(StandardCharsets.UTF_8)));
}

// Webhook signature verification (e.g., GitHub, Stripe)
@PostMapping("/webhooks/payment")
public ResponseEntity<Void> receiveWebhook(
@RequestHeader("X-Signature-256") String signature,
@RequestBody String payload) {

String expected = "sha256=" + generateHmac(payload, webhookSecret);

// CRITICAL: constant-time comparison โ€” prevents timing attacks
if (!MessageDigest.isEqual(expected.getBytes(), signature.getBytes())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
processWebhook(payload);
return ResponseEntity.ok().build();
}

HMAC vs Digital Signatureโ€‹

HMACDigital Signature
KeySymmetric (shared secret)Asymmetric (private/public pair)
Non-repudiationโŒ Either party could generateโœ… Only private key holder can sign
PerformanceFastSlower
UseWebhooks, internal servicesJWTs, public APIs, code signing

Hashingโ€‹

// File integrity, fingerprinting (fast hash)
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(fileBytes);
String hexHash = HexFormat.of().formatHex(hash);

// Use SHA-3 for new designs (SHA-256 still fine for non-password uses)
Use CaseAlgorithmNotes
File integritySHA-256Fast, standard
Password storageArgon2id / BCryptMust be slow + salted
HMAC / message authHMAC-SHA256Needs secret key
Digital certificatesSHA-256SHA-1 is broken for certs

Key Managementโ€‹

Key Hierarchyโ€‹

Master Key (HSM โ€” Hardware Security Module)
โ†“ encrypts
Key Encryption Key (KEK) โ€” stored in Vault
โ†“ encrypts
Data Encryption Key (DEK) โ€” rotates frequently
โ†“ encrypts
Your Data

Key Rotation with Version Trackingโ€‹

@Entity
public class EncryptedRecord {
String encryptedData;
int keyVersion; // Track which DEK version encrypted this record
}

@Transactional
public void rotateKeys(int oldVersion, int newVersion) {
List<EncryptedRecord> records = repo.findByKeyVersion(oldVersion);
for (EncryptedRecord record : records) {
String plaintext = decrypt(record.getEncryptedData(), oldVersion);
record.setEncryptedData(encrypt(plaintext, newVersion));
record.setKeyVersion(newVersion);
repo.save(record);
}
}

Secure Random Numbersโ€‹

// โœ… Always use SecureRandom for security-sensitive values
SecureRandom rng = new SecureRandom();

// Session tokens
byte[] token = new byte[32];
rng.nextBytes(token);
String sessionId = Base64.getUrlEncoder().withoutPadding().encodeToString(token);

// 6-digit OTP
int otp = rng.nextInt(1_000_000);

// โŒ NEVER use Math.random() or java.util.Random for security โ€” predictable seed

Constant-Time Comparisonsโ€‹

// โŒ Vulnerable โ€” early return leaks timing information
boolean bad = userToken.equals(storedToken);

// โœ… Constant-time โ€” always takes the same time regardless of mismatch position
boolean safe = MessageDigest.isEqual(
userToken.getBytes(StandardCharsets.UTF_8),
storedToken.getBytes(StandardCharsets.UTF_8)
);
// Spring Security's PasswordEncoder.matches() is already constant-time

Secure Design Principlesโ€‹

PrincipleMeaning
Defense in DepthMultiple independent security controls
Least PrivilegeMinimal permissions needed to function
Fail SecureDefault to deny on failure
Don't Roll Your Own CryptoUse vetted libraries (BouncyCastle, JDK, Nimbus)
Secure by DefaultSecure configuration out of the box
Complete MediationCheck permissions on every access
Open DesignSecurity based on keys, not algorithm secrecy

Interview Questionsโ€‹

  1. What is the difference between encryption and hashing? When do you use each?
  2. Why is AES-GCM preferred over AES-CBC?
  3. What is the difference between a MAC (HMAC) and a digital signature?
  4. Why must IVs be unique (even if not secret) in AES-GCM?
  5. What is hybrid encryption and why is it used instead of pure RSA?
  6. What is a timing attack and how do you prevent it in Java?
  7. What is key rotation and how do you implement it without losing access to old data?
  8. Why is MD5 broken and what should you use instead for file integrity checks?
  9. What is a rainbow table attack and why does salting prevent it?
  10. What is the purpose of the GCM authentication tag?