xpipe-io/xpipe · error · IllegalArgumentException

No secret available to encrypt

Error message

No secret available to encrypt

What it means

In with(), when a principal already has an existing entry but the secret or principal changed (keep == false), a new ciphertext must be produced. If the caller passed a null secret in that situation, there is nothing to encrypt and an IllegalArgumentException is thrown. Passing the same secret and same principal (keep == true) avoids the throw.

Source

Thrown at app/src/main/java/io/xpipe/app/secret/MultiPrincipalSecret.java:209

        for (EncryptionPrincipal principal : scope.getPrincipals()) {
            var existingEntry = entries.stream()
                    .filter(entry -> entry.getPrincipal().equals(principal))
                    .findFirst();

            // Keep existing entry if possible if not accessible
            if (!principal.isAccessible()) {
                if (existingEntry.isPresent()) {
                    l.add(existingEntry.get());
                }
                continue;
            }

            if (existingEntry.isPresent()) {
                var principalUnchanged = existingEntry.get().getToken().matches(principal);
                var keep = secretUnchanged && principalUnchanged;

                if (!keep && secret == null) {
                    throw new IllegalArgumentException("No secret available to encrypt");
                }

                l.add(new Entry(
                        principal,
                        keep
                                ? existingEntry.get().getEncrypted()
                                : AesSecretValue.encrypt(secret.getSecret(), principal.getSecretKey())
                                        .getEncryptedValue(),
                        keep ? iteration : iteration + 1,
                        keep ? existingEntry.get().getToken() : EncryptionToken.of(principal)));
            } else {
                if (secret == null) {
                    throw new IllegalArgumentException("No secret available to encrypt");
                }

                var enc = AesSecretValue.encrypt(secret.getSecret(), principal.getSecretKey());
                l.add(new Entry(principal, enc.getEncryptedValue(), iteration + 1, EncryptionToken.of(principal)));
            }

View on GitHub (pinned to d85ca821ba)

Solutions

  1. Pass the actual secret value when principals change
  2. If you intend to clear the secret, also remove the corresponding principal from the set
  3. Reuse the existing MultiPrincipalSecret instance when secret is unchanged (it returns this)

Example fix

// before
secret.with(null, scope); // principals changed -> throw
// after
secret.with(currentSecretValue, scope);
Defensive patterns

Strategy: validation

Validate before calling

if (secret == null && principalsChanged) {
    throw new IllegalArgumentException("Provide a secret when principals change");
}

Try / catch

try { return secret.with(s, scope); } catch (IllegalArgumentException e) { if (e.getMessage().equals("No secret available to encrypt")) { /* supply a secret value or skip update */ } else throw e; }

Prevention

When it happens

Trigger: Calling with(null, scope) / withUpdatedPrincipals with a null secret while the new principal differs from the existing entry's token, so keep == false and encryption is required.

Common situations: Clearing a secret by passing null but also changing the principal set; building update flows that pass null for 'unchanged' while principals were rotated.

Related errors


AI-assisted analysis of xpipe-io/xpipe@d85ca821ba (2026-09-06). Data as JSON: /api/errors/a171ec79dda5484d. Report an issue: GitHub.