name: apex-encoding-and-crypto description: "Use when Apex must sign, verify, encrypt, hash, encode, or decode payloads — including HMAC for webhook signatures, RSA/ECDSA signing for JWT bearer flows, AES for stored secrets, base64/hex/URL encoding, and digest comparisons for integration integrity. Triggers: 'Crypto.sign', 'Crypto.generateMac', 'EncodingUtil.base64Encode', 'JWT signing in Apex', 'verify webhook signature'. NOT for setting up Named Credentials or OAuth flows end-to-end — use apex-named-credentials-patterns; NOT for SOQL injection defense — use soql-security." category: apex salesforce-version: "Spring '25+" well-architected-pillars:
- Security
- Reliability tags:
- apex-encoding-and-crypto
- hmac
- jwt-signing
- base64
- encoding-util
- crypto-class triggers:
- "how do I verify an HMAC-SHA256 webhook signature in Apex"
- "signing a JWT assertion for OAuth 2.0 JWT bearer flow from Apex"
- "AES encrypt a secret before storing it in a custom field"
- "EncodingUtil base64 vs urlEncode vs hex decision"
- "compare two digests without a timing leak in Apex" inputs:
- "the payload type (bytes, string, record field) and desired algorithm"
- "whether the key material lives in Named Credential, Protected Custom Metadata, or Certificate"
- "the downstream consumer's expected encoding (base64, base64url, hex)" outputs:
- "a code path using Crypto / EncodingUtil with the right algorithm, key source, and encoding"
- "review findings for weak algorithms, non-constant-time comparisons, or hardcoded keys"
- "a test plan that pins known-answer vectors for the chosen algorithm" dependencies: [] version: 1.0.0 author: Pranav Nagrecha updated: 2026-04-23
Apex Encoding And Crypto
Use this skill when Apex code must sign, verify, hash, encrypt, or encode bytes — for integration signatures, JWT assertions, webhook verification, token obfuscation, or payload normalization. The purpose is to pick the right algorithm, pull key material from a managed source, and encode the output in the exact form the downstream consumer expects.
Before Starting
Gather this context before writing any Crypto or EncodingUtil call:
- What is the algorithm the other side actually uses? HMAC-SHA256, RSA-SHA256, AES-256-CBC, and SHA-512 all need different
Cryptomethod names and payload shapes. An HMAC is not a digest; a digest is not a signature. - Where is the key material? A hardcoded
'my-secret'literal in Apex fails every security review. Key material should come from a Named Credential (for HMAC shared secrets), a protected Custom Metadata record, or a Certificate stored in Setup (for RSA/ECDSA private keys signed viaCrypto.signWithCertificate). - What encoding does the consumer want? Base64 and base64url differ in three characters. Hex is lowercase by default. URL-form-encoded bodies need
EncodingUtil.urlEncode(value, 'UTF-8'), not bare concatenation. - Do you need a constant-time comparison? String equality (
==) short-circuits and leaks bytes to an attacker timing the response. UseCrypto.getRandomIntegerplus byte-wise loop, or compare full SHA-256 digests of both values instead of the raw HMACs.
Core Concepts
Crypto Does Symmetric And Asymmetric Work — With Different Methods
Crypto.generateDigest(algorithmName, input) produces a plain hash (MD5, SHA1, SHA-256, SHA-512). Hashes are not signatures — anyone can recompute them. Use them for integrity checks on non-adversarial payloads (deduplication, cache keys, file fingerprints).
Crypto.generateMac(algorithmName, input, privateKey) produces an HMAC keyed by a shared secret. Supported algorithms are HmacSHA1, HmacSHA256, HmacSHA384, HmacSHA512, and the weaker HmacMD5 — do not use HmacMD5 or HmacSHA1 for new work. HMACs are the right tool for webhook verification and request signing against a shared secret (Stripe, Slack, Twilio, GitHub all use HMAC-SHA256).
Crypto.sign(algorithmName, input, privateKey) produces an asymmetric signature using a raw private key blob. Crypto.signWithCertificate(algorithmName, input, certDevName) uses a certificate stored in Setup → Certificate and Key Management — this is the right pattern for OAuth 2.0 JWT bearer flow because the private key stays in the platform-managed certificate store and never appears as a literal in Apex.
Crypto.encrypt / Crypto.decrypt perform AES encryption with AES128, AES192, or AES256 in CBC mode with an explicit IV. Crypto.encryptWithManagedIV / decryptWithManagedIV let the platform generate and prepend the IV. Salesforce Shield Platform Encryption is a separate, superior capability for encrypting standard field storage — use it when the requirement is "the admin should never see this field" rather than wrapping Crypto.encrypt around every DML.
EncodingUtil Is For Bytes-To-Text And Text-To-Text
EncodingUtil.base64Encode(Blob) / EncodingUtil.base64Decode(String) convert between bytes and standard base64. There is no base64UrlEncode method — if the consumer wants base64url (JWT headers, WebPush), post-process: replace + with -, / with _, and strip = padding.
EncodingUtil.convertToHex(Blob) returns lowercase hex. Downstream consumers that want uppercase must .toUpperCase() the result. EncodingUtil.convertFromHex(String) is the reverse.
EncodingUtil.urlEncode(value, charset) produces percent-encoded text. Always pass 'UTF-8' — the other supported charsets are present for legacy and expose locale-dependent bugs.
Blob.toString(encoding) and Blob.valueOf(string) round-trip between Blob and String, but only for encodings the string actually represents. Blob.toString('UTF-8') on an arbitrary AES ciphertext will corrupt it — always keep ciphertexts as Blob and only encode at the boundary.
Key Material Belongs In Managed Storage, Not Apex
A hardcoded secret is the most common finding in security reviews of crypto code. Three correct alternatives, by use case:
- Certificate (Setup → Certificate and Key Management) — for RSA/ECDSA private keys used by
Crypto.signWithCertificate. The key is never readable from Apex. - Named Credential (Custom Header or External Credential) — for shared secrets used in HMAC. The secret is scoped to a principal and rotatable without code change.
- Protected Custom Metadata Type — for configuration-style secrets (signing key IDs, issuer strings, algorithm names). Mark the CMT
Protectedso only code in the managing package can read it.
Constant-Time Comparison Matters For Verification
When your code compares a received signature to a recomputed one, a == b may short-circuit at the first differing byte, giving an attacker a measurable timing oracle. Salesforce doesn't expose a native constant-time compare, but two safe patterns exist: (1) SHA-256 both values and compare the digests — the comparison is now over fixed-length opaque bytes, which is effectively constant-time for short inputs; or (2) XOR all bytes and test equality at the end. For most webhook verification paths, pattern (1) is simpler and readable.
Common Patterns
HMAC-SHA256 Webhook Verification With Constant-Time Compare
When to use: An external vendor POSTs a webhook with a signature header (X-Signature, Stripe-Signature, etc.) computed as HMAC-SHA256(secret, body). The Apex REST endpoint must reject forged payloads.
How it works:
@RestResource(urlMapping='/webhook/vendor/*')
global with sharing class VendorWebhookResource {
@HttpPost
global static void handle() {
RestRequest req = RestContext.request;
String signatureHeader = req.headers.get('X-Signature');
Blob rawBody = req.requestBody;
String secret = WebhookSecretProvider.current(); // Named Credential or protected CMT
if (String.isBlank(signatureHeader) || String.isBlank(secret)) {
RestContext.response.statusCode = 401;
return;
}
Blob computedMac = Crypto.generateMac('HmacSHA256', rawBody, Blob.valueOf(secret));
String computedHex = EncodingUtil.convertToHex(computedMac);
if (!constantTimeEquals(computedHex, signatureHeader.toLowerCase())) {
RestContext.response.statusCode = 401;
return;
}
WebhookDispatcher.dispatch(rawBody);
}
private static Boolean constantTimeEquals(String a, String b) {
Blob digestA = Crypto.generateDigest('SHA-256', Blob.valueOf(a));
Blob digestB = Crypto.generateDigest('SHA-256', Blob.valueOf(b));
return EncodingUtil.convertToHex(digestA) == EncodingUtil.convertToHex(digestB);
}
}
Why not the alternative: Direct == between computedHex and signatureHeader short-circuits on the first differing byte. Re-hashing both sides makes the comparison opaque to timing attackers. rawBody must come from req.requestBody, not from a re-serialized JSON — any whitespace change invalidates the MAC.
JWT Assertion Signing For OAuth 2.0 JWT Bearer Flow
When to use: Apex needs to obtain an OAuth 2.0 access token from a partner that supports the JWT bearer flow (Google, Salesforce-to-Salesforce, any RFC 7523 server). A Certificate stored in Setup holds the RSA private key.
How it works:
public with sharing class JwtAssertionBuilder {
private static final String CERT_NAME = 'Partner_Signing_Cert';
public static String build(String issuer, String audience, String subject) {
Map<String, String> header = new Map<String, String>{ 'alg' => 'RS256', 'typ' => 'JWT' };
Map<String, Object> claims = new Map<String, Object>{
'iss' => issuer,
'sub' => subject,
'aud' => audience,
'exp' => (Datetime.now().addMinutes(3).getTime() / 1000),
'iat' => (Datetime.now().getTime() / 1000)
};
String headerB64Url = base64Url(Blob.valueOf(JSON.serialize(header)));
String claimsB64Url = base64Url(Blob.valueOf(JSON.serialize(claims)));
String signingInput = headerB64Url + '.' + claimsB64Url;
Blob signature = Crypto.signWithCertificate('RSA-SHA256', Blob.valueOf(signingInput), CERT_NAME);
return signingInput + '.' + base64Url(signature);
}
private static String base64Url(Blob input) {
return EncodingUtil.base64Encode(input)
.replace('+', '-').replace('/', '_').replace('=', '');
}
}
Why not the alternative: Crypto.sign('RSA-SHA256', ..., privateKeyBlob) is the lower-level form and requires the private key to be loaded into the Apex transaction — a cardinal sin. signWithCertificate keeps the private key in the certificate store. Missing the base64url transformation produces a token the server rejects with invalid_grant.
Encrypting A Field-Level Secret With Platform-Managed IV
When to use: The org stores an external integration token on a custom field but must not display the plaintext in a report or debug log. Shield Platform Encryption is not licensed in this org.
How it works:
public with sharing class IntegrationTokenVault {
private static Blob key {
get { return EncodingUtil.base64Decode(IntegrationSecret__mdt.getInstance('ActiveKey').Key_Base64__c); }
}
public static String encrypt(String plaintext) {
Blob cipher = Crypto.encryptWithManagedIV('AES256', key, Blob.valueOf(plaintext));
return EncodingUtil.base64Encode(cipher);
}
public static String decrypt(String cipherB64) {
Blob cipher = EncodingUtil.base64Decode(cipherB64);
return Crypto.decryptWithManagedIV('AES256', key, cipher).toString();
}
}
Why not the alternative: Crypto.encrypt forces the caller to generate and persist an IV — a common source of IV reuse bugs. encryptWithManagedIV prepends a fresh 16-byte IV to the ciphertext so the only thing you store is a single opaque base64 blob. Shield Platform Encryption is still the preferred answer when it is licensed — it encrypts the physical field storage transparently with FIPS-validated keys.
Decision Guidance
| Situation | Recommended Approach | Reason |
|---|---|---|
| Verify a webhook signed by an external service | Crypto.generateMac('HmacSHA256', ...) + constant-time compare | Matches the vendor's signing method exactly |
| Sign a JWT assertion for OAuth JWT bearer flow | Crypto.signWithCertificate('RSA-SHA256', ..., certName) | Keeps the private key in the certificate store |
| Hash a record for deduplication or cache keys | Crypto.generateDigest('SHA-256', ...) | Not a security control — just a deterministic fingerprint |
| Encrypt a custom-field secret without Shield | Crypto.encryptWithManagedIV('AES256', ...) | Platform generates a fresh IV per call |
| Encode bytes for a JSON payload | EncodingUtil.base64Encode | Standard base64 with padding |
| Encode a token for a URL segment or JWT header | base64 then replace +→-, /→_, strip = | No native base64url method |
| Obscure a short ID for non-security reasons | Crypto.generateDigest('SHA-1', ...) | Not acceptable for HMACs or signatures |
Recommended Workflow
Step-by-step instructions for an AI agent or practitioner activating this skill:
- Identify the purpose — signature, MAC, digest, encryption, or encoding — and the exact algorithm the other system uses.
- Locate the key material in a Named Credential, Certificate, or protected Custom Metadata record; refuse to proceed if the key is a string literal in Apex.
- Write the transformation in the order the consumer expects: bytes → algorithm → encoding. Never collapse two boundaries (e.g.
Blob.valueOf(hmac.toString())corrupts the MAC). - For verification paths, wrap the equality check in a constant-time comparison (digest both sides, or XOR loop).
- Pin the behavior with a test that feeds a known input and asserts a known output (RFC 4231 HMAC test vectors are public and stable).
Review Checklist
- Algorithm name matches the consumer's specification exactly (
HmacSHA256, notHMAC-SHA-256, notSHA256-HMAC). - Key material is not a literal string in Apex; source is documented.
- Verification paths use a constant-time comparison, not
==on raw MACs. - Base64url transformation is applied for JWT segments (replace
+,/, strip=). - Ciphertexts and signatures stay as
Blobuntil the last encoding step. -
encryptWithManagedIVis preferred overencryptunless IV interop is required. - Weak algorithms (
MD5,SHA1,HmacMD5,HmacSHA1) are flagged and justified. - Test class pins at least one known-answer vector for the algorithm in use.
Salesforce-Specific Gotchas
Crypto.signWithCertificaterequires the cert in Setup, not a Static Resource — loading a.p12file from a Static Resource and trying to sign with its bytes will fail. Import the key into Certificate and Key Management and reference it by DeveloperName.- Base64url is not a built-in —
EncodingUtil.base64Encodealways produces standard base64. Forgetting to translate+//and strip=is the single most common JWT signing bug in Apex. Blob.valueOf(String)defaults to UTF-8, butBlob.toString(Blob)requires you to pass the encoding — a MAC computed over UTF-8 body bytes will differ from one computed over a re-serialized JSON that normalized character escapes.Crypto.generateAesKeyreturns a fresh key per call, not a stable one — developers use it to generate a key and then forget that the next transaction has no reference to that key. For persistent AES use, generate the key once externally and store it via protected CMT or Named Credential.Crypto.getRandomIntegeris cryptographically secure;Math.random()is not — useCrypto.getRandomIntegerorCrypto.getRandomLongfor nonces, session IDs, and PKCE verifiers. Never useMath.random()for anything security-relevant.- The
encryptWithManagedIVciphertext prepends the IV as the first 16 bytes — systems that expect an IV-separated structure will decode this incorrectly. Useencryptwith an explicit IV if you need interop with a spec that declares its own IV framing. Crypto.signwithRSAreturns PKCS#1 v1.5 signatures, not PSS — if the consumer expects PSS (RSASSA-PSS), there is no native Apex support; you must call an external signing service.
Output Artifacts
| Artifact | Description |
|---|---|
| Crypto/encoding code path | Apex using Crypto / EncodingUtil with correct algorithm, key source, and encoding transform |
| Key-material sourcing plan | Named Credential, Certificate, or protected CMT setup steps with rotation guidance |
| Test fixture with known-answer vectors | @IsTest method pinning algorithm output to published test vectors |
| Review findings for weak algorithms or literals | List of Crypto/EncodingUtil calls with MD5, SHA1, literal keys, or non-constant-time compares |
Related Skills
apex/apex-named-credentials-patterns— use when the key or secret is delivered through a Named Credential or External Credential principal.apex/apex-rest-services— use when the crypto path is wrapped inside an inbound@RestResourcewebhook endpoint.apex/callouts-and-http-integrations— use when the crypto output is attached to an outbound HTTP request.apex/custom-metadata-in-apex— use when signing keys or secrets live in protected Custom Metadata.security/shield-platform-encryption— use when the requirement is to encrypt Salesforce-stored field data, not Apex payloads in transit.