Skip to content

Release JavaScript GCP KMS Storage v1.1.0 - #1173

Closed
mgallego-keeper wants to merge 10 commits into
masterfrom
release/storage/javascript/gcp-kms/v1.0.1
Closed

mgallego-keeper wants to merge 10 commits into
masterfrom
release/storage/javascript/gcp-kms/v1.0.1

Conversation

@mgallego-keeper

@mgallego-keeper mgallego-keeper commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Release of @keeper-security/secrets-manager-gcp v1.1.0

Google Cloud Key Management Service integration for secure storage of Keeper Secrets Manager configuration.

Dependencies:

  • @keeper-security/secrets-manager-core v17.3.0
  • @google-cloud/kms v5.4.0

Maintenance

  • KSM-1218 - Updated axios, protobufjs, handlebars, js-yaml, and other dependencies to resolve open security advisories

Closes https://keeper.atlassian.net/browse/KSM-1218

stas-schaller and others added 2 commits September 16, 2026 13:06
…1165)

* fix(storage/js-gcp): bump axios, form-data, protobufjs, follow-redirects, handlebars, babel

- axios -> 1.19.0 (CVE-2026-40175, CVE-2026-42040)
- form-data -> 4.0.6 (CVE-2026-12143, CRLF injection via unescaped
  multipart field names/filenames)
- protobufjs -> 7.6.5 (CVE-2026-44292, CVE-2026-44294, CVE-2026-45740,
  CVE-2026-54269) - real production path via @google-cloud/kms ->
  google-gax -> protobufjs
- follow-redirects -> 1.16.0 (CVE-2026-40895), pulled in as a side effect
  of the axios bump
- handlebars -> 4.7.9 (CVE-2026-33938, CVE-2026-33941) - dev-only via
  ts-jest
- @babel/core -> 7.29.7 (CVE-2026-49356) - dev-only

Lockfile-only, no package.json range changes. Cherry-picked and scoped to
sdk/javascript/packages/gcp/package-lock.json from 831b7b48, 984fc17e,
and efde007d (Sergey Aldoukhov).

KSM-1218

* fix(storage/js-gcp): bump brace-expansion, @grpc/grpc-js, flatted, browserslist, baseline-browser-mapping, babel-plugin-transform-modules-systemjs, humanfs-node, js-yaml, picomatch

* docs(storage/js-gcp): add changelog entry for KSM-1218 dependency updates

---------

Co-authored-by: Sergey Aldoukhov <saldoukhov@gmail.com>
@socket-security

socket-security Bot commented Sep 16, 2026 •

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
Updatednpm/​axios@​1.13.5 ⏵ 1.19.098 +2100 +7510095100

View full report

stas-schaller and others added 2 commits September 21, 2026 13:16
…sions (#1181)

* fix(sdk/javascript): check ENOENT before recreating GCP KMS config

createConfigFileIfMissing() treated any fs.access failure the same as a
missing file, so a transient error (EACCES, EPERM, ESTALE) silently
overwrote the real on-disk config with an empty one instead of only
recreating it when the file is genuinely missing (ENOENT).

Types the catch clause as unknown instead of any, drops the now-unneeded
eslint-disable, avoids logging the literal string "undefined" when a
thrown value has no message, and reuses the already-resolved configPath
instead of recomputing dirname(resolve(...)) a second time.

Replaces the fs.access error handling test, which swallowed the
rejection with .catch(() => undefined) and only asserted no write
happened - a check that would still pass if the propagating throw were
replaced with a silent return - with table-driven assertions (EACCES,
EPERM, ESTALE, EIO, EBUSY) through the public init() and saveString()
entry points, asserting the rejection itself propagates with the right
error code.

KSM-1370

* fix(sdk/javascript): write GCP KMS config file with restrictive permissions

Route all four config-file write sites through a new atomic temp-file-
then-rename helper (writeFileAtomicSync in atomicWrite.ts), rather than
fs.writeFile(mode: 0o600) followed by fs.chmod. The mode option only
takes effect when a file is created, so it was a no-op against a config
file already on disk from an earlier SDK version, and the follow-up
chmod rewrote the same inode, so a process already holding a read handle
on the old file kept reading straight through the rewrite. The atomic
rename produces a new inode, closing both gaps.

The temp file opens with O_EXCL ("wx" instead of "w"), turning a name
collision with another writer's in-flight temp file into an EEXIST
instead of a silent truncation, and the containing directory is fsynced
after a successful rename so the rename itself survives a crash, not
just the file's data (EPERM/EISDIR, e.g. on Windows, is tolerated rather
than failing an otherwise-successful write).

Adds real-filesystem tests for the properties a mocked-fs assertion
can't catch: a stale reader fd not surviving the rewrite, decryptConfig's
autosave write reaching the file, saveConfig's own write independently
reaching mode 0600 on a pre-existing 0644 file, both writes inside
createConfigFileIfMissing routing through writeFileAtomicSync (not just
the first), and the temp file still landing at exactly 0600 under a
umask that strips owner bits from the initial open call.

Bumps to 1.1.0 (not 1.0.1/1.0.2): this is a real behavior change (an
already-exposed config file is now actually remediated, not just given
a new mode bit), not a no-behavior-change patch. Regenerates
package-lock.json, which still self-referenced 1.0.1 after package.json
was bumped.

Also resolves KSM-1458 (no atomic write anywhere in this file, filed
separately during the same security review): the temp-file-then-rename
approach this commit adds is exactly the fix that ticket asked for.

KSM-1450
…fig writes (#1183)

writeFileAtomicSync() (added in PR #1181, KSM-1450 and KSM-1370) writes to a
same directory temp file, fsyncs it, then renames it over the real config
path. This already gives the atomicity KSM-1458 asks for: a failed or
interrupted write can never leave the real config file truncated, since the
real path is only ever touched by a single rename once the new content is
fully written.

This adds the one regression test that property did not yet have. It
intercepts fs.writeSync at module resolution, not on the live fs object
(fs.writeSync is non configurable in current Node, so jest.spyOn cannot
redefine it there), and makes it report fewer bytes than were actually
written. It then asserts the original file's content and inode are both
unchanged, and that the failed temp file was cleaned up.

This needed its own file rather than living in
GCPKeyValueStorage.atomicWrite.test.ts, which deliberately does not mock
fs at all. jest.mock is hoisted per file, so adding a partial fs mock there
would apply to every test in that file, not just this one.

KSM-1458
@mgallego-keeper mgallego-keeper changed the title Release JavaScript GCP KMS Storage v1.0.1 Release JavaScript GCP KMS Storage v1.1.0 Sep 21, 2026
mgallego-keeper and others added 6 commits September 22, 2026 12:18
…ption (#1187)

loadConfig() decides whether the config file holds plaintext or ciphertext
by trying to JSON.parse it. The saveConfig() call that encrypts a plaintext
file sat inside that same try block, so a KMS failure while encrypting was
caught by the catch meant for a failed parse. The code then set jsonError,
logged "given file is encrypted file", and tried to decrypt content it had
already parsed as valid JSON.

A successful JSON.parse is proof the file is plaintext. Encrypting it is a
consequence of that result, not part of computing it, so the encryption now
runs in an `if (!jsonError)` block after detection finishes. A KMS outage, a
revoked permission, or a disabled key version propagates as the real KMS
error instead of being relabeled.

Before this change a KMS PERMISSION_DENIED surfaced as "Decryption failed :
Invalid header", identical to the error a genuinely corrupt file produces,
because decryptBuffer rejected the plaintext for having no blob header. An
operator who trusted that message could delete or replace an intact config
file. The file is left untouched on disk, and the two cases are now
distinguishable.

Propagates the original error rather than wrapping it, so the KMS error
type, message, and code survive, matching getKeyDetails() and the error
handling KSM-847 established in this package.

The decrypt path and both existing corrupt-file messages are unchanged:
"Decryption failed : Invalid header" for content that is neither JSON nor a
valid envelope, and "Failed to parse decrypted config file <path>" for an
envelope wrapping a non-JSON payload.

KSM-1460
… fails (#1188)

changeKey() snapshotted gcpKeyConfig and cryptoClient before switching keys,
but not keyType, isAsymmetric, or encryptionAlgorithm. getKeyDetails() had
already overwritten those three with the new key's values, so a failure in the
saveConfig() that follows left the object holding the old key paired with the
new key's algorithm.

For two asymmetric keys with different OAEP hashes that combination is
unrecoverable. The RSA public key comes from gcpKeyConfig while the OAEP hash
comes from encryptionAlgorithm, so the next save wraps the AES data key under
the old key's public key using the new key's hash. GCP derives the OAEP hash
from the key version itself and never from the decrypt request, so no key can
unwrap that blob: not the old key, whose declared hash no longer matches, and
not the new key, which was never used. The write itself reports success, so
nothing signals the loss until the next read, which loses the client ID, the
app key, and the device private key. Reproduced with real RSA keypairs in the
new test: both keys fail with an OAEP decoding error before this change, and
the old key reads the config back after it.

getKeyDetails() also assigned encryptionAlgorithm before the unsupported
key-purpose check could throw. It now computes the metadata into locals,
validates the purpose, and assigns the three fields only once the key is known
to be usable, so a rejected key never mutates state. changeKey() rolls its own
state back, but init() calls getKeyDetails() with no rollback at all.

KSM-1461
…hanged save (#1186)

* fix(sdk/javascript): recreate a deleted GCP KMS config file on an unchanged save

saveConfig() compared the in-memory config hash against lastSavedConfigHash
and returned early on a match, before it called createConfigFileIfMissing().
If the config file was deleted or replaced while the process kept running,
and the in-memory value had not changed, the early return fired before the
file was ever checked. saveString() and saveStorage() resolved successfully
and wrote nothing, so the caller believed the value was persisted and the
loss only became visible at the next restart.

The skip now also requires the config file to still be present, through a
new configFileExists() helper that does a single fs.access() on the resolved
path. The && short-circuit keeps that syscall off the force and changed-hash
paths. A missing file makes the guard false, so control falls through to the
full save path, which encrypts and writes the real this.config contents.

Reordering createConfigFileIfMissing() ahead of the early return would not
have been enough on its own. That method writes Buffer.from("{}") and then
an encrypted blob of "{}", not the real config, so a deleted file would have
been repaired with an empty placeholder and the data would still have been
lost. The existence check therefore forces a real save rather than standing
in for one.

configFileExists() reports false for any fs.access failure, matching
fs.existsSync semantics. It only decides whether a save can be skipped, and
a save that cannot be skipped goes on to createConfigFileIfMissing(), which
already holds the ENOENT-only policy for acting on an access failure and
rethrows every other code (KSM-1370). Keeping that policy in one place means
a path whose existence cannot be determined now reports the failure instead
of resolving silently.

Adds five real-filesystem regression tests in their own file. Three delete
the config file out from under a live instance and save the same value again,
asserting the recreated file decrypts back to the real config and that a
fresh instance's init() reads the value back, which is what catches a
placeholder-only repair. One pins the unverifiable-path behavior with an
ENOTDIR case, chosen over a chmod-based EACCES so it behaves the same for an
unprivileged user and for root. One asserts the no-changes skip still avoids
the write when the file is present.

KSM-1459

* fix(sdk/javascript): stop double-encrypting a recreated GCP KMS config file

The recovery path added for KSM-1459 called createConfigFileIfMissing()
unconditionally once a deleted file was confirmed missing. That method
itself encrypts and writes a "{}" placeholder before returning, and the
code right after it immediately encrypted and wrote the real config over
that same file. One recovery cost two KMS encrypt calls and two atomic
writes for content that was discarded as soon as it landed.

saveConfig() now tracks whether the no-changes-detected guard itself
already proved the file is missing. When it has, the recovery skips
createConfigFileIfMissing() and calls a new ensureConfigDirectoryExists()
helper instead, extracted from createConfigFileIfMissing() unchanged, so
only the directory-creation half of its work runs. The real encrypt-and-
write that already follows in saveConfig() then creates the file
directly, at one KMS call and one write.

Also tightens the comment above the guard, which read as if the "{}"
placeholder were a lasting outcome of skipping this check. It only
survives a crash between the two writes; under normal execution it is
overwritten within the same call, before saveConfig() ever returns.

Adds a regression test that pins the call counts directly, which the
existing content-only assertions could not have caught on their own.

Addresses review from Stas Schaller on PR #1186.

KSM-1459
…esetting it (#1184)

loadConfig() treated a zero-byte config file as an empty config: it
substituted "{}" for the file contents, which then fell through into the
plain-JSON branch, parsed cleanly, and reached saveConfig(). That
encrypted the empty object and wrote it back over the damaged file,
destroying the client id, app key and device private key with no
recovery path and only a warn-level log.

A zero-byte file is never a legitimate "no config yet" state.
createConfigFileIfMissing() runs first and only creates a file when
fs.access reports ENOENT, and the file it creates always holds a real
encrypted blob. Zero bytes can therefore only mean a file that already
existed was truncated by an interrupted write or corruption.

loadConfig() now logs at error level and throws, naming the config path,
and leaves the file on disk untouched so it can be restored from a
backup. A non-empty plaintext JSON config is still encrypted in place,
and an existing encrypted config still loads unchanged.

KSM-1455
…#1185)

The constructor picked the config path with `keyVaultConfigFileLocation ??
process.env.KSM_CONFIG_FILE ?? this.defaultConfigFileLocation`. `??` falls
back only on null and undefined, so an empty string passed straight through
and configFileLocation became "". resolve("") is the current working
directory, and fs.access on a directory succeeds, so
createConfigFileIfMissing() logged the working directory as an existing
config file and the failure surfaced later as a confusing empty path. An
empty environment variable is a common result of a Docker --env-file or a
Kubernetes ConfigMap entry with no value, so this is easy to hit by
accident.

Routes both fallback sources through a nonBlank() helper that maps a blank
value to undefined, so each step falls back the way the JSDoc already
promised. The test is value.trim() === "" rather than value === "", which
also covers an env file that pads the value instead of emptying it; a
non-blank value is returned unchanged, so no legitimate path is trimmed.

Adds table-driven regression tests over the empty and whitespace-only forms
of both sources, the no-regression cases for non-empty values, and one
assertion through createConfigFileIfMissing() that fs.access is called
against the resolved default config file and never against process.cwd() -
the field value alone can't show that the working directory was being
accepted as a config file.

KSM-1457
Removes the long-lived NPM_TOKEN pulled via ksm-action in favor of npm's
trusted publisher OIDC flow (matches secrets-manager-core's publish.npm.yml).
Requires the Trusted Publisher entry on npmjs.com to be configured to accept
this workflow filename/environment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mgallego-keeper
mgallego-keeper deleted the release/storage/javascript/gcp-kms/v1.0.1 branch September 22, 2026 18:26
@mgallego-keeper

Copy link
Copy Markdown
Contributor Author

This PR is closed. The release branch needed a rename, from v1.0.1 to v1.1.0. GitHub could not reopen this PR after that rename.

PR #1189 replaces this PR. It carries the exact same branch content, including every commit merged here.

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