Skip to content

Release Java SDK v18.0.0 - #1127

Open
stas-schaller wants to merge 15 commits into
masterfrom
release/sdk/java/core/v18.0.0
Open

Release Java SDK v18.0.0#1127
stas-schaller wants to merge 15 commits into
masterfrom
release/sdk/java/core/v18.0.0

Conversation

@stas-schaller

Copy link
Copy Markdown
Contributor

Summary

Release branch for v18.0.0 of the Java SDK. Bundles bug fixes for folder key handling, getFolders crash safety, per-item delete error surfacing, secure config writes, and several API additions.

Changes

Bug Fixes

  • getFolders binding fix (KSM-1045): getFolders() failed with "App key is missing" when called as the first method on a freshly bound application. It now processes encryptedAppKey from the server binding response the same way getSecrets() does.
  • Parent-cycle guard (KSM-1270): getSharedFolderKey looped indefinitely when server folder data contained a parent cycle. The function now tracks visited UIDs and exits on re-visit; getFolders skips the affected folders.
  • getFolders crash safety (KSM-1081): getFolders() threw when any folder in the response had a corrupted or missing key. The SDK now skips undecryptable folders and returns the rest normally.
  • deleteFolder response type (KSM-1086): deleteFolder() decoded the response into the records-only type, silently dropping all folder data. It now returns a typed SecretsManagerDeleteFolderResponse with per-folder status.
  • generatePassword entropy (KSM-1203): generatePassword used Random.Default (non-cryptographic) for the final character shuffle. The shuffle now uses SecureRandom.
  • Secure config writes (KSM-1262): Config and cache files are now written via a temp-file swap with 0600 permissions set before data is written, closing the window where other local users could read the file during a write.
  • Server key ID validation (KSM-1248): Server-supplied key IDs are validated against the embedded public key table before being stored. An unrecognized key ID throws SecretsManagerException and leaves storage unchanged.
  • API compatibility (KSM-1269 / internal): Java CI now runs the test matrix on pull requests targeting release branches.

New Features

  • HTTP timeouts (KSM-1207): Added connectTimeoutMillis (default 5 000 ms) and readTimeoutMillis (default 30 000 ms) to SecretsManagerOptions. A stalled server now causes a SocketTimeoutException rather than an indefinite hang.
  • isEditable (KSM-1176): KeeperRecord now exposes isEditable: Boolean from the server response envelope.

Breaking Changes

  • deleteFolder() returns SecretsManagerDeleteFolderResponse instead of SecretsManagerDeleteResponse. Callers that read .records on the old return type must switch to .folders.
  • KeeperRecord and SecretsManagerOptions each gained a constructor parameter. Kotlin copy() and synthetic constructors changed arity — rebuild dependents against v18.0.0 rather than replacing the jar in place.

Security Impact

KSM-1045 touches the initial key derivation path: fetchAndDecryptFolders now decrypts encryptedAppKey using the client key and stores the result as the app master key, matching the behavior already in fetchAndDecryptSecrets. No new cryptographic operations introduced.

KSM-1203 corrects the entropy source for password generation. The final character shuffle previously used Random.Default (a non-cryptographic PRNG); it now uses SecureRandom. The rest of the generation path was already cryptographically secure.

KSM-1262 closes a TOCTOU window on POSIX systems where a local user could read a config or cache file during a write.

Related Issues

  • Jira: KSM-1045, KSM-1270, KSM-1081, KSM-1086, KSM-1203, KSM-1262, KSM-1248, KSM-1207, KSM-1176, KSM-1269

stas-schaller and others added 15 commits July 24, 2026 13:27
Removes a section-header decorator, several em dashes, four "This method"
docstring restatements, and inline ticket references from comments across
core. Ticket refs belong in commit messages, not code; the substantive
explanation in each is kept. No behavior change.
…#1109)

KSM-1248: reject server-supplied key_id values outside the keeperPublicKeys
map (keys 7-18) before writing them to storage. A hostile server cannot
poison the stored key_id and break subsequent calls.

KSM-1262: saveCachedValue and LocalConfigStorage.saveToFile now write to a
temp file with 0600 permissions set before any data is written, then replace
the target via Files.move(ATOMIC_MOVE). This eliminates the window where the
file was world-readable between create and chmod.

Also removes ticket refs from comments and rewrites affected comments in
STE-flavored prose.
When the config or cache directory is not writable, the temp-file
creation fails with an AccessDeniedException that names the temp path.
Wrap createTempFile in both saveToFile and saveCachedValue to rethrow as
SecretsManagerException naming the config/cache path and its parent,
matching the guidance already in the changelog.

Guard testConfigFileAtomicWriteIsOwnerOnly with org.junit.Assume so it
skips rather than fails on Windows or non-POSIX file systems.
* fix(java): use SecureRandom for generatePassword shuffle (KSM-1203)

The final shuffle in generatePassword used Kotlin's Random.Default (a
non-cryptographic PRNG), making the character arrangement predictable from
PRNG state even though character selection was already secure. Switch to
Collections.shuffle with SecureRandom.getInstanceStrong() so the entire
password generation path uses a CSPRNG.

* docs(java): add KSM-1203 changelog entry
* feat(java): expose isEditable on KeeperRecord (KSM-1176)

The server returns isEditable in the record envelope and the private DTO
already parsed it, but KeeperRecord had no corresponding field. Add
isEditable: Boolean = false to KeeperRecord (default preserves existing
callers) and forward record.isEditable in decryptRecord().

* docs(java): add KSM-1176 changelog entry
…1116)

* fix(java): set connect/read timeouts on all HttpsURLConnection calls (KSM-1207)

HttpsURLConnection defaults to timeout 0 (infinite), allowing a stalled
or hostile server to block the caller indefinitely. Add connectTimeoutMillis
(default 5s) and readTimeoutMillis (default 30s) to SecretsManagerOptions
and thread them through to postFunction. Apply module-level constants at the
downloadFile and uploadFile private helpers whose callers don't carry options.

* docs(java): add KSM-1207 changelog entry

* fix(java): use module constants for postFunction timeout defaults
…ase (#1117)

* fix(java): keep the four-argument postFunction callable from Java (KSM-1207)

Kotlin default arguments do not exist in bytecode, so adding connectTimeoutMillis
and readTimeoutMillis replaced the published static
postFunction(String, TransmissionKey, EncryptedPayload, boolean) rather than
extending it. Java callers stop compiling, and callers already compiled against
17.3.0 fail at runtime with NoSuchMethodError. @jvmoverloads restores the four-
and five-argument forms alongside the new one.

Affects the ServiceNow credential resolver and the hello-secret-custom-caching
example, both of which call the four-argument form.

* fix(java): keep KeeperRecord's constructor Java-compatible (KSM-1176)

isEditable was inserted between revision and files. KeeperRecord carries no
@jvmoverloads, so Java sees only the full-arity constructor and the parameter
count went from nine to ten, breaking every Java caller. Moving the field to the
end and adding @jvmoverloads restores the original nine-argument constructor
exactly, with no change to the Kotlin API or to JSON decoding (KeeperRecord is
not @serializable).

The internal construction site now uses named arguments so a future field cannot
silently shift positions again.

* fix(java): apply the caller's timeouts to the file upload transport (KSM-1207)

uploadFile(options, ownerRecord, file) already holds the options and passes them
to postQuery for the add_file request, but the private helper that performs the
actual upload hardcoded the module defaults. A caller raising readTimeoutMillis
for a slow link saw it honoured for the metadata request and silently ignored
for the upload in the same call.

downloadFile keeps the module constants: its public entry points take a
KeeperFile and no options, so there is nothing to thread through.

* test(java): verify readTimeout actually bounds a stalled server (KSM-1207)

The previous test asserted that SecretsManagerOptions echoes its own constructor
arguments, which holds for any Kotlin data class and passes unchanged if both
connection.readTimeout assignments are deleted.

TimeoutTest points postFunction at a ServerSocket that accepts the connection and
then goes silent, so the client blocks reading the ServerHello. With the timeout
applied the call fails in about one second; without it the read never returns, so
the call runs under a watchdog on a daemon thread that fails the test at twenty
seconds instead of hanging the Gradle test JVM. The defaults are asserted
separately, since 5000/30000 is the documented contract.

Connect timeout has no behavioural test: it needs a blackholed address, which is
not reproducible across CI environments.

* chore(java): bump core version to 17.4.0

The release branch and the changelog both say 17.4.0, but build.gradle.kts and
KEEPER_CLIENT_VERSION still said 17.3.0. publish.maven.java.core.yml takes no
version input and reads the coordinate straight from build.gradle.kts, so the
release would have tried to republish 17.3.0 to Maven Central and reported
mj17.3.0 in the client version header.

* test(java): anchor the accepted socket so GC cannot close it mid-handshake

The socket returned by accept() was discarded, so it turned unreachable as soon
as the acceptor thread exited. A GC cycle inside the one-second window then closed
the server side underneath the TLS handshake and the client failed on the wrong
exception, making the assertion look like the timeout code was broken.

Confirmed under forced GC, and not marginal: with the socket discarded the call
failed in 30-90ms with SSLException on JDK 8 and SocketException on JDK 21, never
once with SocketTimeoutException. Anchored, it times out at ~1.04s on both. The
same negative control run through Gradle fails in 0.15s on the wrong exception
type.

Held in an AtomicReference rather than a captured var: the acceptor thread writes
it and the test thread reads it to close, and a captured var compiles to a
non-volatile Ref.ObjectRef, so the reader could see a stale null and silently skip
the close. Joining the acceptor before closing makes the handoff ordered, and the
close in a finally block stops the test leaking the socket either way.

Raised by Stas Schaller in review of #1117.
…llow-ups (#1120)

* docs(java): document the 17.4.0 breaking changes in the changelog

The 17.4.0 section listed only features and fixes. Enumerating every class
in the compiled jar and diffing javap output against master shows exactly
three public members change relative to 17.3.0, plus the two synthetic
constructors Kotlin emits for default arguments:

  deleteFolder()                  return type changed
  KeeperRecord.copy()             arity changed
  SecretsManagerOptions.copy()    arity changed

deleteFolder() was already called out in the release PR description but
never reached the changelog, which is what users actually read on upgrade.

The second entry covers a case easy to state too narrowly. Adding a
constructor parameter changes copy(), but it also changes the synthetic
$default constructor, so Kotlin code that merely builds a KeeperRecord
while omitting a defaulted argument breaks on a jar swap too. Verified:
a caller compiled against 17.3.0 and run against 17.4.0 throws

  NoSuchMethodError: KeeperRecord.<init>(..., int, DefaultConstructorMarker)

Recompiling resolves it. Java call sites are unaffected either way, since
both types carry @jvmoverloads.

Matches the Breaking Changes block the 17.3.0 section already uses.

* fix(java): keep the underlying IOException when a config write fails (KSM-1262)

Both temp-file write paths caught IOException and threw a
SecretsManagerException that discarded the original exception, so the
stack trace and errno were lost. The message also asserted a cause it
could not know: createTempFile fails on a full or read-only volume, a
missing parent directory, or an fd limit, none of which are a permission
problem, and the operator was told the directory was not writable.

SecretsManagerException gains an optional cause. @jvmoverloads keeps the
single-argument constructor, so Java callers and the existing subclasses
that call super(message) are unaffected; javap confirms no member was
removed from the class.

* docs(java): document KeeperRecord.isEditable semantics (KSM-1176)

The field carries a backend permission decision, and the only description
of it lived in the changelog. Records the three things a caller needs: the
meaning of each value, that the SDK does not enforce it before an update,
and that the `false` default is reachable only through direct construction
because the response envelope requires the field.

* test(java): assert the record decrypts before reading isEditable (KSM-1176)

The test indexed records[0] directly. fetchAndDecryptSecrets swallows
per-record failures to stderr, so a decrypt regression would have surfaced
as IndexOutOfBoundsException rather than a readable assertion; both
neighbouring tests already guard on the record count first.

Folds the two cases into a loop over true/false. Verified the assertion
still discriminates: hardcoding isEditable = true in decryptRecord fails
the test, and reverting passes.
Add release/sdk/java/core/** to pull_request and push triggers in
test.java.yml so CI fires on PRs targeting or pushing to the release branch.
…1118)

* fix(java): KSM-1270 guard getSharedFolderKey against parent-cycle infinite loop

Replace unbounded while(true) with a HashSet<String> visited guard.
Add testGetFoldersSkipsFolderParentCycle to verify the call completes
and returns an empty list instead of hanging.

* KSM-1270 Address Mateo's review: test timeout, distinct cycle error, README fix

- Add @test(timeout = 5_000) so a regression hangs for at most 5 s rather than blocking the entire Gradle test task indefinitely
- Throw SecretsManagerException with a cycle-specific message instead of returning null, so operators can distinguish a parent cycle from other folder-key lookup failures in stderr output
- Fold the two-line KSM-1270 README entry into one line to match the surrounding bullets
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedmaven/​com.google.guava/​guava@​33.4.6-jre ⏵ 33.1.0-jre36 +610090100100
Updatedmaven/​org.junit.jupiter/​junit-jupiter@​5.12.1 ⏵ 5.10.21001009010070

View full report

@socket-security

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: maven com.google.guava:guava is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: examples/java/hello-secret-custom-caching/gradle/libs.versions.tomlmaven/com.google.guava/guava@33.1.0-jre

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore maven/com.google.guava/guava@33.1.0-jre. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: maven com.google.guava:guava is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: examples/java/hello-secret-custom-caching/gradle/libs.versions.tomlmaven/com.google.guava/guava@33.1.0-jre

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore maven/com.google.guava/guava@33.1.0-jre. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: maven org.junit.jupiter:junit-jupiter-engine is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?maven/org.junit.jupiter/junit-jupiter@5.10.2maven/org.junit.jupiter/junit-jupiter-engine@5.10.2

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore maven/org.junit.jupiter/junit-jupiter-engine@5.10.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: maven org.junit.platform:junit-platform-engine is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?maven/org.junit.jupiter/junit-jupiter@5.10.2maven/org.junit.platform/junit-platform-engine@1.10.2

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore maven/org.junit.platform/junit-platform-engine@1.10.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants