-
Notifications
You must be signed in to change notification settings - Fork 285
[Bug]: device-key-ed25519.json rewritten non-atomically in ClearStoredTokens → torn write silently rotates device identity #888
Copy link
Copy link
Closed
Labels
P2Normal priority bug or improvement with limited blast radius.Normal priority bug or improvement with limited blast radius.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.ClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-security-reviewClawSweeper marked this issue as needing security-sensitive review.ClawSweeper marked this issue as needing security-sensitive review.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.ClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.ClawSweeper found a high-confidence source-level issue reproduction.impact:auth-providerThis issue is about auth, provider routing, model choice, or SecretRef resolution.This issue is about auth, provider routing, model choice, or SecretRef resolution.impact:data-lossThis issue is about lost, corrupted, or silently dropped user/session/config data.This issue is about lost, corrupted, or silently dropped user/session/config data.impact:securityThis issue is about security boundaries, credentials, authz, sandboxing, or sensitive data.This issue is about security boundaries, credentials, authz, sandboxing, or sensitive data.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.Very strong issue quality with high-confidence source-level or clear reproduction.
Description
Activity
Metadata
Metadata
Assignees
Labels
P2Normal priority bug or improvement with limited blast radius.Normal priority bug or improvement with limited blast radius.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.ClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-security-reviewClawSweeper marked this issue as needing security-sensitive review.ClawSweeper marked this issue as needing security-sensitive review.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.ClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.ClawSweeper found a high-confidence source-level issue reproduction.impact:auth-providerThis issue is about auth, provider routing, model choice, or SecretRef resolution.This issue is about auth, provider routing, model choice, or SecretRef resolution.impact:data-lossThis issue is about lost, corrupted, or silently dropped user/session/config data.This issue is about lost, corrupted, or silently dropped user/session/config data.impact:securityThis issue is about security boundaries, credentials, authz, sandboxing, or sensitive data.This issue is about security boundaries, credentials, authz, sandboxing, or sensitive data.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.Very strong issue quality with high-confidence source-level or clear reproduction.
Type
Fields
Priority
None yet
Summary
DeviceIdentityStore.ClearStoredTokensrewrites the authoritative device-identity filedevice-key-ed25519.jsonwith a non-atomicFile.WriteAllBytes. A crash, power-loss, or forced process-kill mid-write can leave a torn / zero-byte / truncated key file. On the next startup,DeviceIdentity.LoadExisting()treats an unparseable/empty file as invalid and silently regenerates a brand-new keypair — rotating the device identity and discarding the (non-regenerable) Ed25519 private key and any device tokens. The node then has to re-pair/re-enroll with the gateway.This is the exact failure mode the codebase already guards against for this same file: the canonical writer
DeviceIdentity.AtomicWriteKeyFilestages to a sibling temp file andFile.Moves it into place specifically "so a process-kill or power-loss mid-write cannot leave a torn/zero-byte key file that the next LoadOrCreate would silently rotate the identity over."ClearStoredTokensis the one writer of this file that bypasses that protection.Bug class: non-atomic persisted write of authoritative, non-regenerable security material (data-loss / auth).
Affected code
Non-atomic write of the identity file:
src/OpenClaw.Connection/DeviceIdentityStore.cs:60—File.WriteAllBytes(keyPath, ms.ToArray());insideClearStoredTokens, after a read-modify (stripDeviceToken/DeviceTokenScopes/NodeDeviceToken/NodeDeviceTokenScopes).The recovery-less read path that turns a torn write into identity loss:
src/OpenClaw.Shared/DeviceIdentity.cs:179-208—LoadExisting()catches any exception (and thedata == null || PrivateKeyBase64 emptycase) and callsGenerateNew(), which overwrites the file with a fresh keypair.The canonical atomic writer for the very same file (the in-repo correct pattern):
src/OpenClaw.Shared/DeviceIdentity.cs:523-543—AtomicWriteKeyFile(temp + ACL-restrict +File.Move(overwrite:true), with temp cleanup).src/OpenClaw.Shared/DeviceIdentity.cs:106-155—TryClearDeviceTokenForRoleperforms the same logical operation (read → null out a role's token fields → keep the keypair → write back) but routes throughAtomicWriteKeyFile.ClearStoredTokensis its drifted, non-atomic twin.Reachable, authoritative callers (this is not dead code):
src/OpenClaw.Connection/GatewayConnectionManager.cs:570and:623(token clear on disconnect / logout / role switch), plusConnectionStatusWindowandConnectionPage.A second, related non-atomic write of the same file exists at
src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs:2750(File.WriteAllText(identityKeyPath, identityBackup)on the direct-connect rollback-restore path) — worth funneling through the same helper in one pass.Steps to reproduce (analysis-derived, not captured on a live machine)
device-key-ed25519.jsonholds a keypair + an operator/node token.GatewayConnectionManagercallsClearStoredTokens).File.WriteAllBytesatDeviceIdentityStore.cs:60is mid-write (power-loss, OS kill, disk-full at the wrong moment). The file is left truncated/partial.DeviceIdentity.Initialize()→LoadExisting()throws on the truncated JSON →GenerateNew().Expected: clearing a token must never be able to destroy the keypair; an interrupted clear leaves the previous, intact identity file in place (atomic replace), so the device keeps its identity.
Actual (by code path): an interrupted clear can truncate the file; the next launch silently rotates the entire device identity, discarding the private key and all tokens. The device de-enrolls and must be re-paired/re-approved on the gateway.
Why this is a real defect, not intended behavior
AtomicWriteKeyFile, DeviceIdentity.cs:514-520).TryClearDeviceTokenForRole) already does it atomically.ClearStoredTokensdoes the same job non-atomically — classic duplicate-implementation drift, not a deliberate design choice.(The torn-write window itself is timing-dependent and was reasoned from the source, not reproduced on hardware — flagged honestly as analysis, not an observed crash.)
Proposed fix options
ClearStoredTokenswrite through the same atomic temp-file +File.Move(overwrite:true)+ ACL-restrict path asDeviceIdentity.AtomicWriteKeyFile. Best done by reusing one helper rather than a third copy — e.g. expose the atomic writer (or aDeviceIdentity.ClearAllStoredTokens(dataPath)that nulls all four token fields and callsAtomicWriteKeyFile) and haveClearStoredTokensdelegate to it. This also de-duplicates the two clear-token implementations.ClearStoredTokenswhere it is but replace the rawFile.WriteAllBytes(keyPath, …)with a local atomic temp+rename mirroringAtomicWriteKeyFile(preserve the file's existing ACL/permissions on the rename, per the existing helper'sTryRestrictSensitiveFileAcl). Apply the same to theConnectionPage.xaml.cs:2750rollback-restore write.LoadExisting(), beforeGenerateNew()on a parse failure, distinguish "file present but unreadable/torn" from "file absent" and fail loud / preserve rather than silently rotating, so a corrupt identity is recoverable (e.g. back up the bad file and require explicit re-enroll) instead of silently destroyed.Option A keeps the change Cold-shaped and consistent with the existing contract; C is a separate hardening that could ship independently.
What was / was not tested
main@87dc3dc: the non-atomic write site, the recovery-lessLoadExisting → GenerateNewpath, the atomic sibling that proves intent, and the live callers.File.Moveatomicity is NTFS-specific and this is a Windows-targeted .NET app, which can't be exercised in a Linux sandbox. Filed as an investigated issue rather than a PR because converting a non-atomic write to temp+rename changes failure semantics on protected/permission-sensitive destinations (and this is auth material), so the maintainer should choose the storage/permission contract and add the Windows-side regression coverage.Filed with AI assistance (automated source-pattern mining + analysis). Findings were source-confirmed against the cited code on
main; the reproduction is analysis-derived, not captured on hardware. No fix has been pushed — happy for a maintainer to pick the contract in Option A/B/C.Generated by Claude Code