Deobfuscating SailPoint IdentityIQ Configuration Secrets


Background

SailPoint IdentityIQ (IIQ) uses a custom encryption scheme to protect application-level configuration secrets. These include database credentials, connector passwords, service account tokens, and similar operational values. These are not user passwords. IdentityIQ does not manage them through lifecycle or authentication logic. Instead, internal utility classes encrypt them and store the results directly in configuration files and the database.

SailPoint does not document how to decrypt these values outside a running IdentityIQ instance. In configuration files, they appear as opaque strings like:

1:ACP:DDVEEogM5MwWjarEM7jgR1hIMAurKChltZba9W/ug+Bd5CvR3+uKJOBVxiRpY8Fj

with no supported way to confirm what they contain unless you have a working IdentityIQ system and elevated access. The number before :ACP: is the alias. That value selects which AES key IdentityIQ uses for encryption and decryption.

This post documents the exact encryption and key-handling behavior used by IdentityIQ. The analysis comes directly from decompiled Java classes. It also introduces iiq_decrypt.py, a standalone Python tool that performs decryption without requiring the IIQ runtime.

Note: This is not an exploit. Everything described here reflects normal IdentityIQ behavior. The point is simple: if an attacker can read the configuration files, the encryption offers limited protection.

This tool is available on GitHub: https://github.com/covertchannelblog/iiq_decrypt


Scope and Constraints

In scope

  • Configuration secrets stored in the alias:ACP:<base64> format
  • Default-key encryption (alias 1)
  • Keystore-backed encryption (alias 2+)
  • Key storage, masking, and retrieval logic (iiq.cfg, iiq.dat)
  • Decryption without a running IIQ instance

Out of scope

  • End-user passwords or authentication mechanisms
  • Authorization bypasses
  • Tampering, re-encryption, or privilege escalation

High-Level Architecture

IdentityIQ implements configuration encryption using three cooperating components:

  1. Transformer – Performs AES encryption and decryption and formats the output
  2. SPKeyStore – Chooses the correct key based on the alias prefix
  3. AsyncHandler – Applies a reversible masking routine to the keystore password

All cryptographic operations ultimately use 128-bit AES. The surrounding key-handling logic is specific to IdentityIQ and undocumented.


Transformer Class: Encryption Function

At the lowest level, IdentityIQ uses a standard AES construction:

  • Algorithm: AES/CBC/PKCS5Padding
  • IV: Random 16-byte initialization vector
  • Layout: IV (16 bytes) followed by ciphertext
  • Encoding: Base64

This behavior appears directly in the decompiled Transformer class.

Decompiled excerpt (Transformer.java)

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] iv = new byte[16];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);

IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);

byte[] encrypted = cipher.doFinal(clearText.getBytes("UTF-8"));

After encryption, IdentityIQ prepends the IV to the ciphertext and Base64-encodes the result:

byte[] combined = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);

String encoded = Base64.encodeBytes(combined);

The final storage format is:

<alias>:ACP:<base64( [IV (16 bytes)] [ciphertext] )>

SPKeyStore Class: Key Resolution

The alias prefix at the start of the encrypted string determines which AES key IdentityIQ uses.

Alias 1: The Default Key

Alias 1 maps to a static AES key compiled directly into the IdentityIQ codebase.

Decompiled excerpt (SPKeyStore.java)

private static final byte[] DEFAULT_KEY = new byte[] {
    0x2d, 0x45, 0x7a, 0x13, ...
};

During key resolution:

if ("1".equals(alias)) {
    return new SecretKeySpec(DEFAULT_KEY, "AES");
}

This has two important consequences:

  • Every IdentityIQ deployment ships with the same default key
  • Anyone with the ciphertext can decrypt secrets that use alias 1

The default key exists to simplify initial setup, but it often remains in use indefinitely.


Alias 2+: Keystore-Backed Keys

Aliases greater than 1 resolve to keys stored in a Java JCEKS keystore, usually located at:

WEB-INF/classes/iiq.dat

Key retrieval is direct:

Key key = keyStore.getKey(alias, password);
return new SecretKeySpec(key.getEncoded(), "AES");

IdentityIQ does not apply key derivation, stretching, or wrapping beyond the JCEKS format.


AsyncHandler: Password Masking

Access to iiq.dat requires a password stored in iiq.cfg. This value is not plaintext, but the unmasking logic and decryption key live in the application binary.

The code responsible sits in AsyncHandler.

Decompiled excerpt (SPKeyStore.java — inner class AsyncHandler)

byte[] decoded = Base64.decode(maskedValue);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, defaultKey);
byte[] unmasked = cipher.doFinal(decoded);

After decryption, the method slices out fixed offsets:

byte[] password = Arrays.copyOfRange(unmasked, 624, unmasked.length - 312);

The actual password sits between fixed-length blocks of random data. This hides its position, not its value.

Implication: If you can read iiq.cfg, you can recover the keystore password using logic already embedded in IdentityIQ.


End-to-End Flow

When you put the pieces together, decryption follows a simple decision path:

Flowchart showing IdentityIQ decryption decision tree
Figure 1: IdentityIQ decryption flow from encrypted string to plaintext secret

The Tool

iiq_decrypt.py is a small Python implementation of the behavior described above.

Capabilities

  • Default-key decryption (1:ACP:) with no external files
  • Keystore-backed decryption (2+:ACP:) using iiq.cfg and iiq.dat
  • JCEKS key enumeration for alias validation
  • Hard failure on padding or key mismatch

Dependencies

  • pycryptodome (AES primitives)
  • pyjks (JCEKS parsing)
Example tool screenshot using both encryption types
Figure 2: Example screenshot of the tool with both supported encryption types

The tool was validated against a live IdentityIQ 8.5 environment:

  • Default-key secrets, including database credentials
  • Keystore-backed secrets such as connector passwords
  • Plaintext values containing special characters

Security Implications

  • Alias 1 relies on a global, static AES key baked into the binary
  • The keystore password masking is fully reversible
  • Read access to iiq.cfg and iiq.dat allows recovery of all configuration secrets
  • The design discourages casual inspection but does not stop a local attacker

Bottom Line: Treat iiq.cfg and iiq.dat as equivalent to plaintext secrets. Anyone who has these files can recover every encrypted configuration value.


Decryption Demo

IdentityIQ :ACP: Decrypt (default key)


 


Limitations

  • Secrets with aliases greater than 1 require access to both iiq.cfg and iiq.dat.