security

Practical Application Security for Developers - Part 1: Cryptographic Foundations

Understand hashes, HMAC, AES, and practical cryptographic decisions for backend systems with Java examples.

Reading Time: 11 min readAuthor: DeepTechHub
#security#appsec#cryptography#hmac#aes
Practical Application Security for Developers - Part 1: Cryptographic Foundations

Practical Application Security for Developers - Part 1: Cryptographic Foundations

As a developer, you don't need to be a cryptographer - but you do need to know which cryptographic tool solves which problem, and how to avoid the handful of mistakes that actually cause real breaches in production.

Use this running example throughout the article: the Order Service asks the Warehouse Service to issue a refund, the Warehouse Service forwards refund.json to the Payment Service, and a single flipped byte changes $999 to $9999. Or worse, an attacker on the network deliberately rewrites the payload. These are the problems cryptography was designed to solve, and this article gives you the practical toolkit to handle them correctly.

We'll cover three foundational primitives - hashing, message authentication, and symmetric encryption - with production-ready Java code you can drop straight into your project.


Why Developers Need to Understand Cryptography

Whether your system is a monolith or a fleet of microservices, it must answer four questions:

QuestionPropertyTool
Has this data been tampered with?IntegrityHash functions
Who actually sent this?AuthenticationMAC / Digital signatures
Can anyone else read this?ConfidentialityEncryption
Can the sender deny they sent it?Non-repudiationDigital signatures

Each question maps to a specific primitive. By the end of this article, you'll know exactly which one to reach for.


Service Identity in Distributed Systems

When the Payment Service receives a request, how does it know it actually came from the Order Service and not an attacker who found the internal endpoint? Two approaches:

ApproachHow it worksWhen to use
API KeysEach service gets a secret token, included in every request headerStarting out; simple internal services
Mutual TLS (mTLS)Services prove identity via X.509 certificates — like showing passports to each otherService meshes (Istio); high-security requirements

Practical advice: Start with API keys in a secrets manager. Graduate to mTLS when you adopt a service mesh or your threat model demands it.


Cryptographic Hash Functions

Back to our refund scenario — we need the Payment Service to detect if refund.json was corrupted or tampered with in transit. This is what hash functions solve.

A cryptographic hash takes any input and produces a fixed-size fingerprint (digest). What makes it cryptographic:

  • One-way: You cannot reconstruct the original from the hash
  • Collision-resistant: Two different inputs won't produce the same output
  • Avalanche effect: A single-bit change in input completely changes the output

Real-world scenario: The Warehouse Service computes SHA-256 of refund.json, sends both the file and the hash. The Payment Service recomputes the hash and compares. If they match — file is intact. If not — reject it.

Limitation: A plain hash doesn't protect against an attacker who can modify both the file and the hash. We'll fix that with MACs below.

The SHA Family

AlgorithmOutputStatusUse case
SHA-1160-bitBroken (collision found 2017)Legacy only — never for security
SHA-256256-bitSecureDefault choice for everything
SHA-512512-bitSecureWhen you need extra margin
SHA-3 (Keccak)256-bitSecureDefense-in-depth, compliance

Rule of thumb: Use SHA-256. Reach for SHA-3 only if compliance specifically requires it or you want a backup that uses a completely different internal design.

Hashing in Java (JCA)

Java's Java Cryptography Architecture (JCA) uses a provider-based model where you request algorithms by name:

import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
 
public class HashUtil {
 
    public static String sha3Hex(String resourcePath)
            throws IOException, NoSuchAlgorithmException {
 
        try (InputStream in = HashUtil.class.getClassLoader()
                .getResourceAsStream(resourcePath)) {
 
            if (in == null) {
                throw new IllegalArgumentException(
                    "Resource not found: " + resourcePath);
            }
 
            byte[] digest = MessageDigest.getInstance("SHA3-256")
                                         .digest(in.readAllBytes());
            return HexFormat.of().formatHex(digest);
        }
    }
}

Google Tink Alternative

For production systems doing more than just hashing — encryption, signing, key management — consider Google Tink. It makes cryptographic misuse harder by design:

import com.google.crypto.tink.subtle.EngineFactory;
import java.security.MessageDigest;
import java.util.HexFormat;
 
public class TinkHashUtil {
 
    public static String sha3Hex(byte[] data) throws Exception {
        MessageDigest md = EngineFactory.MESSAGE_DIGEST
                                        .getInstance("SHA3-256");
        return HexFormat.of().formatHex(md.digest(data));
    }
}

For plain hashing, the difference is minimal. Where Tink shines is encryption — it forces safe patterns and makes dangerous operations (like ECB mode) simply unavailable.


Message Authentication Codes (MAC)

A plain hash proves data wasn't accidentally changed. But what if you need to prove it wasn't deliberately modified by someone in the middle? The answer: hash the message together with a secret key that only sender and receiver know.

  • Hash: Anyone with the message can compute it → proves integrity only
  • MAC: Only someone with the secret key can compute it → proves integrity + authenticity

HMAC: The Industry Standard

HMAC (Hash-based MAC, RFC 2104) combines a hash function with a shared secret key. In practice, that means the receiver can check two things at once:

  1. The payload was not changed in transit
  2. The sender knew the shared secret when the MAC was created

That is why HMAC shows up in so many systems:

  • Stripe webhooks - Stripe signs the payload so your server can reject forged events.
  • GitHub webhooks - GitHub signs the request body so your handler knows the payload really came from GitHub.
  • AWS Signature Version 4 - AWS signs API requests so services can verify the caller and the request contents.
  • JWT HS256 - The issuer signs the token with a shared secret so the verifier can trust the claims.

Java Implementation

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.HexFormat;
 
public class HmacUtil {
 
    public static byte[] hmacSha256(byte[] key, byte[] data) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key, "HmacSHA256"));
        return mac.doFinal(data);
    }
 
    public static String hmacSha256Hex(byte[] key, byte[] data) throws Exception {
        return HexFormat.of().formatHex(hmacSha256(key, data));
    }
}

Practical Example: Webhook Verification

When a payment provider sends your service a webhook, it signs the payload with your shared secret. Here's how you verify it:

String receivedSig = request.getHeader("X-Signature");
byte[] webhookSecret = System.getenv("WEBHOOK_SECRET").getBytes();
byte[] body = request.getBody();
 
String computedSig = HmacUtil.hmacSha256Hex(webhookSecret, body);
 
if (!MessageDigest.isEqual(computedSig.getBytes(), receivedSig.getBytes())) {
    throw new SecurityException("Invalid webhook signature — rejecting request");
}

Why MessageDigest.isEqual() instead of .equals()? A regular string comparison returns false at the first differing byte — an attacker can measure response time differences to guess the correct signature byte-by-byte (timing attack). MessageDigest.isEqual() always takes constant time regardless of where the mismatch is.


Symmetric Encryption with AES

Hashing proves integrity. MACs prove integrity + authenticity. But neither hides the data. When you need confidentiality — making sure no one can read what you're sending — you need encryption.

AES (Advanced Encryption Standard) is your go-to symmetric cipher. "Symmetric" means both sides use the same key. It's been the global standard since 2001, implemented in CPU hardware (AES-NI), and has survived two decades of cryptanalysis without a practical break.

AES works on 128-bit (16-byte) blocks. The strategy for handling multiple blocks is the mode of operation — and choosing the right mode is where developers most commonly go wrong.

AES-CBC: Know It, But Avoid It

Cipher Block Chaining was the default for years. Each block is XORed with the previous ciphertext block, requiring a random IV for the first block.

The critical problem: CBC provides confidentiality only, not integrity. An attacker can flip bits in ciphertext and cause predictable changes in decrypted plaintext (bit-flipping attack). The fix is adding HMAC — but the order matters:

CompositionSecurityExample
Encrypt-then-MACSafe — MAC protects ciphertext before decryptionIPsec
MAC-then-EncryptVulnerable — led to POODLE, Lucky13Old TLS 1.2
Encrypt-and-MACLeaks info — MAC computed on plaintextSSH

Bottom line: Don't use CBC unless forced by a legacy system. If you must, always Encrypt-then-MAC. Otherwise, just use GCM.

AES-GCM: Your Default Choice

Galois/Counter Mode is authenticated encryption — it encrypts and produces an authentication tag in one operation. Tampered ciphertext = automatic decryption failure. No separate MAC step needed.

Why GCM wins:

  • One operation: Confidentiality + integrity + authenticity
  • Hardware accelerated: CPU instructions for both AES and GCM polynomial math
  • Used everywhere: TLS 1.3, AWS KMS, Google Cloud KMS, Android Keystore

The one rule you must never break: Never reuse a nonce (IV) with the same key. GCM uses a 96-bit nonce. Two messages encrypted with the same key + nonce = attacker recovers both plaintexts by XORing ciphertexts. Always generate a fresh random nonce per encryption.

Java Implementation

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.security.SecureRandom;
 
public class AesGcmUtil {
 
    private static final int GCM_TAG_BITS = 128;
    private static final int GCM_NONCE_BYTES = 12;
 
    public static byte[] encrypt(SecretKey key, byte[] plaintext, byte[] aad)
            throws Exception {
        byte[] nonce = new byte[GCM_NONCE_BYTES];
        new SecureRandom().nextBytes(nonce);
 
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, key,
                    new GCMParameterSpec(GCM_TAG_BITS, nonce));
 
        if (aad != null) cipher.updateAAD(aad);
 
        byte[] ciphertext = cipher.doFinal(plaintext);
 
        // Prepend nonce so the receiver can extract it for decryption
        byte[] output = new byte[nonce.length + ciphertext.length];
        System.arraycopy(nonce, 0, output, 0, nonce.length);
        System.arraycopy(ciphertext, 0, output, nonce.length, ciphertext.length);
        return output;
    }
 
    public static byte[] decrypt(SecretKey key, byte[] nonceAndCiphertext, byte[] aad)
            throws Exception {
        byte[] nonce = new byte[GCM_NONCE_BYTES];
        System.arraycopy(nonceAndCiphertext, 0, nonce, 0, nonce.length);
 
        byte[] ciphertext = new byte[nonceAndCiphertext.length - nonce.length];
        System.arraycopy(nonceAndCiphertext, nonce.length,
                         ciphertext, 0, ciphertext.length);
 
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, key,
                    new GCMParameterSpec(GCM_TAG_BITS, nonce));
 
        if (aad != null) cipher.updateAAD(aad);
 
        return cipher.doFinal(ciphertext); // throws AEADBadTagException if tampered
    }
}

The aad parameter (Additional Authenticated Data) lets you authenticate metadata — like HTTP headers or message type — without encrypting it. The receiver reads it in clear text, but any modification causes decryption to fail.

If decryption throws AEADBadTagException: The data was tampered with, wrong key was used, or nonce got corrupted. Log it, alert on it, reject the data.


Common Mistakes That Cause Real Breaches

  1. Using ECB mode — Encrypts each block independently. Same plaintext block = same ciphertext block, leaking patterns (the famous "ECB penguin"). If your codebase contains AES/ECB/, it's a bug.

  2. Reusing GCM nonces — Same key + same nonce on two messages = attacker XORs ciphertexts to recover both plaintexts. Has caused real production breaches.

  3. Comparing MACs with .equals() — String comparison short-circuits at first differing byte. Attackers measure timing differences across thousands of requests to forge valid MACs byte-by-byte. Always use constant-time comparison.

  4. Rolling your own crypto — Use JCA, Google Tink, or Bouncy Castle. You implement the protocol (which algorithm, how to manage keys), not the primitive.

  5. Hardcoding keys in sourcegit log --all -S "secretKey" finds this in more repos than anyone wants to admit. Use environment variables at minimum, a secrets manager (Vault, AWS Secrets Manager) in production.


Quick Decision Guide

Your problemToolWhy
Did this file arrive intact?SHA-256Detects any change to content
Did this webhook really come from Stripe?HMAC-SHA256Only holder of shared secret can produce valid MAC
Can anyone snoop on this DB column?AES-256-GCMEncrypts + detects tampering in one step
Legacy system requires AES-CBCAES-CBC + HMAC (Encrypt-then-MAC)CBC alone has no integrity protection

What's Next

We've covered the symmetric world — where both parties share the same key. But this raises an obvious question: how do two services that have never communicated before agree on a shared key without an eavesdropper stealing it?

In Part 2, we'll solve this with public key cryptography - the math that lets two strangers establish trust over an insecure channel. We'll cover RSA, elliptic curves, digital signatures, and the X.509 certificate system that makes HTTPS possible.

Continue Learning

Explore more guides and resources to deepen your knowledge.