Fixing Jasypt ENC() decryption failures in Spring Boot

Updated 2026-08-09

EncryptionOperationNotPossibleException is Jasypt's only answer to every decryption problem. Wrong master password, wrong algorithm, wrong iteration count, a stray newline in an environment variable — they all raise the same exception with no detail attached. That is deliberate (a crypto library should not tell you which parameter was almost right), but it makes debugging miserable, because the stack trace points at Jasypt when the actual problem is a mismatch between the parameters used at encryption time and the parameters your Spring Boot app derives from its jasypt.encryptor.* properties at decryption time.

This guide walks through the actual bytes of a PBEWithMD5AndDES ciphertext, produced with the real Jasypt 1.9.3 CLI and re-derived by hand with Python and OpenSSL, so you can see exactly which knobs exist. Then it goes through the mismatches in the order they tend to bite, including the one that accounts for most reports since 2019: the jasypt-spring-boot 3.0.0 default-algorithm change.

What is actually inside an ENC() value

Encrypting the string db-secret-42 with master password masterkey through the Jasypt 1.9.3 CLI (org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI, algorithm PBEWithMD5AndDES) produced MSKZIDbW7ms9xFqvc6rdSPRUrlROnCQ1 on our run. Base64-decode it and you get exactly 24 bytes: an 8-byte random salt (here 3122992036d6ee6b) followed by 16 bytes of DES-CBC ciphertext. There is no header, no version byte, no MAC — salt then ciphertext, that is the whole container.

The key and IV are derived from the password and salt with PKCS#5 PBKDF1 over MD5: hash password-plus-salt once, then re-hash the digest 999 more times. Of the final 16-byte digest, the first 8 bytes are the DES key and the last 8 are the CBC IV. You can replay this outside the JVM:

Re-deriving a real Jasypt ciphertext by hand (Python + OpenSSL)
import base64, hashlib

raw = base64.b64decode("MSKZIDbW7ms9xFqvc6rdSPRUrlROnCQ1")
salt, body = raw[:8], raw[8:]           # 8-byte salt || DES-CBC ciphertext

t = hashlib.md5(b"masterkey" + salt).digest()
for _ in range(999):                    # PKCS#5 PBKDF1, 1000 iterations total
    t = hashlib.md5(t).digest()
key, iv = t[:8], t[8:16]

# openssl enc -des-cbc -d -provider legacy -provider default \
#   -K <key hex> -iv <iv hex>   ->   "db-secret-42" + valid PKCS#5 padding

With the default 1000 iterations this decrypts cleanly to db-secret-42. Run the same derivation with a single MD5 pass and you get afd542d7a48e6402bbe7aebefd637cdb — garbage with invalid padding. Every parameter in that loop is a place where encryptor and decryptor can silently disagree. Also note the salt is random per encryption: encrypting the same value twice gives different ENC() strings. That is correct behavior, not corruption. (OpenSSL 3.x moved DES to the legacy provider, hence the -provider flags.)

That single-pass result is worth dwelling on, because it is the reason the Jasypt tool on this site is not wire-compatible with Java Jasypt. The tool implements the container format described above — 8-byte random salt, MD5-derived key and IV, DES-CBC with PKCS#7 padding, Base64 of salt-plus-ciphertext — so anything you encrypt in it decrypts in it, and the output has the same shape and length as real Jasypt output. But two implementation details sit underneath. First, it runs a single MD5 pass instead of PBKDF1's 1000 iterations. Second, its crypto-js password encoding stores each character as a 32-bit word — the password masterkey becomes 36 bytes (0000006d00000061...) instead of the 9 UTF-8 bytes Java hashes.

I verified both directions: ciphertext from real Jasypt 1.9.3 fails in the tool's scheme even with iterations forced to 1, and the tool's output is not decryptable by the Java library. That is exactly why the tool displays a compatibility warning instead of claiming drop-in equivalence. Use it to understand the format, generate test fixtures for its own round-trip, or demo the salt-randomization behavior — and use your actual Java application (or the CLI shown above) as the source of truth for values that must interoperate. If you want to peel the layers yourself, the Base64 decoder and hex converter on this site are the right first two steps, and the standalone DES tool covers the raw cipher.

The mismatches, ranked by how often they bite

RankCauseTypical setupFix
1Algorithm default changed in jasypt-spring-boot 3.0.0Values encrypted with the jasypt CLI (defaults to PBEWithMD5AndDES) or an old app; decrypting app is on 3.x/4.x, which defaults to PBEWITHHMACSHA512ANDAES_256Pin jasypt.encryptor.algorithm explicitly on both sides, or re-encrypt with the new default
2IV generator mismatchAlgorithm set to an AES-based PBE but iv-generator-classname left at NoIvGenerator, or vice versa — the decryptor then mis-parses where salt/IV end and ciphertext beginsRandomIvGenerator for AES algorithms, NoIvGenerator when pinning legacy PBEWithMD5AndDES
3Master password differs invisiblyJASYPT_ENCRYPTOR_PASSWORD carries a trailing newline, commonly from a Kubernetes Secret encoded with echo instead of printfCompare byte counts, not what you see; encode secrets with printf '%s'
4Iteration count changed on one sideSomeone set key-obtention-iterations away from 1000 in one environmentKeep 1000 everywhere unless you change it everywhere
5Value mangled in the config fileQuotes pasted into .properties, or a long ENC() string wrapped across linesKeep ENC(...) on one line; no quotes in .properties

Cause 1 deserves the detail. The jasypt-spring-boot 3.0.0 release notes say it plainly: "Changed default encryption to PBEWITHHMACSHA512ANDAES_256". So an app that decrypted fine on 2.1.x starts throwing EncryptionOperationNotPossibleException at startup after a dependency bump, with zero code changes. If you cannot re-encrypt everything immediately, pin the old behavior:

application.yml — pin legacy settings for values encrypted before 3.0.0
jasypt:
  encryptor:
    algorithm: PBEWithMD5AndDES
    iv-generator-classname: org.jasypt.iv.NoIvGenerator

Both lines matter. The 3.x default IV generator is RandomIvGenerator, which prepends a random IV to the payload; PBEWithMD5AndDES derives its IV from the password and salt instead, so the decryptor must be told there is no IV to strip. Set the algorithm without the IV generator and you trade one opaque exception for another. Note the AES-256 default also requires unrestricted JCE policy, which is only guaranteed out of the box on Java 9+ (and late Java 8 builds).

Cause 3 is the one people refuse to believe until they measure it. echo masterkey | base64 yields bWFzdGVya2V5Cg== — ten bytes, newline included — while printf '%s' masterkey | base64 yields bWFzdGVya2V5. Feed the first into a Kubernetes Secret and your master password is masterkey\n. PBKDF1 hashes whatever bytes it gets, the derived DES key is completely different, and decryption fails exactly as if the password were wrong. Because it is.

How jasypt.encryptor.* properties map to the crypto

PropertyDefault (jasypt-spring-boot 3.x/4.x)What it controls
jasypt.encryptor.passwordnone — requiredInput to PBKDF1/PBKDF2 key derivation; never appears in the ciphertext
jasypt.encryptor.algorithmPBEWITHHMACSHA512ANDAES_256Cipher + KDF; was PBEWithMD5AndDES before 3.0.0
jasypt.encryptor.key-obtention-iterations1000KDF iteration count; must match exactly
jasypt.encryptor.salt-generator-classnameorg.jasypt.salt.RandomSaltGeneratorSalt strategy; random salt is why ciphertexts differ per run
jasypt.encryptor.iv-generator-classnameorg.jasypt.iv.RandomIvGeneratorWhether an IV is generated and prepended to the payload
jasypt.encryptor.string-output-typebase64Encoding of the final blob (base64 or hexadecimal)
jasypt.encryptor.pool-size1Encryptor pool; performance only, never a decryption-failure cause

Debugging discipline: when a value fails to decrypt, write down these seven values for the encrypting side and the decrypting side and diff them. In practice the diff is never empty; the exception only feels mysterious because the defaults moved underneath one side.

Quoting: .properties vs .yml
# application.properties - quotes become PART OF THE VALUE; this fails:
spring.datasource.password="ENC(MSKZIDbW7ms9xFqvc6rdSPRUrlROnCQ1)"
# correct:
spring.datasource.password=ENC(MSKZIDbW7ms9xFqvc6rdSPRUrlROnCQ1)

# application.yml - unquoted or quoted both work:
spring:
  datasource:
    password: ENC(MSKZIDbW7ms9xFqvc6rdSPRUrlROnCQ1)

Questions people ask

Why does encrypting the same value twice produce different ENC() strings?

The salt generator produces a fresh random 8-byte salt per encryption, and the salt is stored in the first 8 bytes of the payload. Different salt means a different derived key and IV, so the ciphertext differs. Any of those ciphertexts decrypts to the same plaintext under the same master password.

Can I recover a value if I lost the jasypt.encryptor.password?

Not through Jasypt — there is no backdoor and the password is not stored anywhere in the ciphertext. Your realistic options are restoring the password from wherever it was provisioned (CI variables, a vault, deployment manifests) or re-encrypting new values.

Is PBEWithMD5AndDES acceptable for new projects?

No. It exists in JCE for PKCS#5 v1.5 compatibility: MD5 is broken for collision resistance and single DES has a 56-bit key. Use the jasypt-spring-boot 3.x default PBEWITHHMACSHA512ANDAES_256 for anything new, and treat the legacy pin in this guide as a migration bridge, not a destination.

Why did decryption break after a Spring Boot upgrade with no config changes?

Almost certainly the jasypt-spring-boot 3.0.0 default change riding along with the upgrade: the default algorithm moved from PBEWithMD5AndDES to PBEWITHHMACSHA512ANDAES_256 and the default IV generator became RandomIvGenerator. Pin the old algorithm and NoIvGenerator, or re-encrypt your values under the new defaults.

Try it yourself