Skip to content

fix(zcode): read ZCode 3.18's vault-encrypted credentials on Windows - #1161

Open
Lucentio-Huang wants to merge 3 commits into
magpie-community:mainfrom
Lucentio-Huang:zcode-vault-windows
Open

Lucentio-Huang wants to merge 3 commits into
magpie-community:mainfrom
Lucentio-Huang:zcode-vault-windows

Conversation

@Lucentio-Huang

Copy link
Copy Markdown
Contributor

ZCode 3.18 encrypts ~/.zcode/v2/credentials.json with a key kept in the OS vault instead of the machine user: values now read enc:v2:<vault>:<fingerprint>:<iv>.<tag>.<data> where 3.17 and earlier read enc:v1:<iv>.<tag>.<data>, the fingerprint being sha256(key) as 24 hex. decrypt knew only v1, so on Windows an account signed in "as ZCode" read no jwt at all, magpie saying the sign-in has no ZCode session ("ZCode isn't signed in on this computer").

decrypt now hands an enc:v2 value to decryptV2, which on Windows reads the vault through PowerShell's CredRead:

  • the vault item is a generic credential named after the sha256 of the credentials.json path (ZCode's own path join), whose blob is the 43-char base64url key as UTF-16LE
  • the read is remembered per fingerprint: a hit for good (until another fingerprint comes, that is the vault rotated), a miss for five minutes, then one more try, so a sign-in reads once
  • the spawn gets an absolute powershell.exe path and its own minimal env, as magpie's plugin host runs with a sanitized env (no PATH, no SystemRoot), and PowerShell starts with neither
  • a failed read is noted in the temp folder's zcode-vault-debug.log, without any credential material, so a vault that misbehaves in the host env says why

ZCODE_CREDENTIAL_V2_SECRET (43 base64url chars, checked strictly as ZCode checks its own) stands in for the vault, as ZCODE_CREDENTIAL_SECRET does for the machine user, and answers enc:v2:env: — which the new vault tests use, so they run on any platform. A v1 value and a v2 one in one store read side by side.

This covers Windows alone; 3.18 on macOS keeps its key in the Keychain, which #1116 covers — the two should be reconciled when that lands.

Per the repo's credential-handling rule: the read touches only ZCode's own vault item on the user's machine, nothing leaves it, and no credential material is written anywhere (the debug log names no key and no value).

Tested on Windows 11, ZCode 3.18.2, magpie 0.1.4179: "ZCode app's sign-in" reads the vault and signs in, the plan windows show, and a request serves. bun test packages/zcode passes (274).

ZCode 3.18 encrypts ~/.zcode/v2/credentials.json with a key kept in the
OS vault instead of the machine user: values read
enc:v2:<vault>:<fingerprint>:<iv>.<tag>.<data>, the fingerprint being
sha256(key)'s first 24 hex. decrypt knew only v1, so on Windows an
account signed in "as ZCode" read nothing and magpie said ZCode isn't
signed in on this computer.

decrypt hands an enc:v2 value to decryptV2, which on Windows reads the
vault through PowerShell's CredRead - a generic credential named after
the sha256 of the credentials.json path, whose blob is the 43-char
base64url key as UTF-16LE - and remembers a hit per fingerprint (a miss
for five minutes, then one more try). The spawn gets an absolute
powershell path and its own minimal env, as magpie's plugin host runs
with a sanitized env PowerShell starts without. A failed read is noted
in the temp folder's zcode-vault-debug.log, without any credential
material. ZCODE_CREDENTIAL_V2_SECRET (43 base64url chars) stands in for
the vault, as ZCODE_CREDENTIAL_SECRET does for the machine user, and
answers enc:v2:env:, which the new vault tests use. macOS keeps its key
in the Keychain, which magpie-community#1116 covers.
@yetone

yetone commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

Thanks @Lucentio-Huang, the write-up is clear. This one reads a key out of the Windows Credential Manager (PowerShell CredRead) and writes a debug log to the temp folder, so under this repo's rules it waits for the maintainer to look at it before a full review and merge. I'll come back to it with a run on a Windows box once that's done.

@yetone

yetone commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

Review, before this merges: the vault read runs a script that can be someone else's.

readVaultKey writes the PowerShell to join(tmpdir(), "zcode-vault-read.ps1"), but only if (!existsSync(script)). If a file is already there, it runs that file with -ExecutionPolicy Bypass. The output is the user's ZCode vault key.

  • The comment above v2Env says the plugin host runs with a sanitized env (no PATH, no SystemRoot). If TEMP/TMP are gone too, Node's os.tmpdir() on Windows falls back to %SystemRoot%\temp, that is C:\Windows\Temp. Any local user can create files there. A file another account put at that path then runs as this user and gets the key, and with it the ZCode sign-in.
  • Even in the user's own %TEMP%, the file is never rewritten. A later version that changes V2_PS would keep running the old script.

Please don't run a script from a fixed path. Either pass it inline (-EncodedCommand with the UTF-16LE base64 of the script and the target as data), or write it into a fresh mkdtempSync folder on each read and delete it after. Pass v2Target() as data only, as it is now.

zcode-vault-debug.log has the same placement problem. It holds no key material, but it says when a vault read fails, so it shouldn't go to a shared folder either.

The rest looks fine: it reads only ZCode's own Credential Manager item (CredRead on the sha256-of-path target), checks the fingerprint before using a key, and sends nothing over the network.

The review is right about the fixed path: readVaultKey wrote the
PowerShell to tmpdir() and ran it from there when it existed, and
tmpdir() falls back to C:\Windows\Temp when the host env has no
TEMP/TMP, where a file another account planted runs as this user and
gets the key; and a changed script kept running the old bytes. The
script now goes into a fresh mkdtempSync folder each read and the
folder is deleted after, so nothing planted is picked up and nothing
stale runs; the target stays data (-Target). PowerShell's own temp
files land in that same folder, not C:\Windows\Temp.

The debug log moves beside the plugin, out of the shared temp folder:
it holds no key material, but it names when a vault read failed, so it
stays in the user's own profile.
@Lucentio-Huang

Copy link
Copy Markdown
Contributor Author

All three points addressed in 24b1d52:

  1. Nothing runs off disk at a fixed path. Each read writes the script into a fresh mkdtempSync folder (os.tmpdir()/zcode-vault-XXXX), runs it with -File from there, and deletes the folder in a finally — so nothing planted at a predictable path is picked up and a changed V2_PS always runs. The target still goes as data (-Target v2Target()), never inside the script.
    1. PowerShell's own temp files land in that same fresh folder (TEMP/TMP point at it), so when the host env has no TEMP/TMP and os.tmpdir() would fall back to C:\Windows\Temp, the read writes nothing there.
    1. The debug log moved beside the plugin, out of the shared temp folder: the plugin's install folder is the user's own profile, so the log stays private (and it still names no key material — only which stage failed).
      bun test packages/zcode passes (274), and the real vault on a Windows 11 box reads through the new path.

@yetone

yetone commented Nov 15, 2026

Copy link
Copy Markdown
Contributor

Thanks @Lucentio-Huang, the new head (24b1d52) is noted. As said above, this one reads a credential out of Credential Manager, so it waits for the maintainer to look before I go further or merge. Nothing more needed from you for now.

@Lucentio-Huang

Copy link
Copy Markdown
Contributor Author

Ran this branch on a Windows 11 box (ZCode 3.18.2 signed in, magpie 0.1.4179, this branch's packages/zcode/index.mjs copied over the installed plugin's) — it works end to end:

  • "ZCode app's sign-in" reads the vault and signs in: the account card shows the account, the Start Plan and the Weekend Build windows with live usage.
  • magpie's 测试模型 fires real requests through the read: all four models pass (GLM-5.3-Flash, GLM-5.3-Flash-Trial, GLM-5.3, GLM-5.3-Trial). One earlier run had the two -Trial entries 429 with "user concurrency limit exceeded (3008)" — the account's few request slots were busy from the other tests firing at once; a retry after a moment passes all four.
  • bun test packages/zcode passes (274).

Repro on a Windows box, from a checkout of this branch:

  1. Sign in to ZCode 3.18+ once (its vault item is what the read finds), magpie installed.
  2. Copy packages/zcode/index.mjs over the installed plugin's index.mjs (%USERPROFILE%\.config\magpie\plugins\node_modules\@magpie-community\opencode-zcode-auth\), restart magpie.
  3. magpie: 供应商 → ZCode → 测试模型 — all four should go green; the account card shows the vault-read sign-in.

Happy to re-run anything else on the box.

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