Repository navigation
Release JavaScript GCP KMS Storage v1.1.0 - #1173
Closed
mgallego-keeper wants to merge 10 commits into
Closed
mgallego-keeper wants to merge 10 commits into
mgallego-keeper wants to merge 10 commits into
Conversation
…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>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
mgallego-keeper
marked this pull request as draft
September 16, 2026 17:33
…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
…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
deleted the
release/storage/javascript/gcp-kms/v1.0.1
branch
September 22, 2026 18:26
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Maintenance
Closes https://keeper.atlassian.net/browse/KSM-1218