mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-02 19:02:06 +00:00
security: durable vault writes, path validation, and safe restore semantics
Continues the security-review fixes. Durability and concurrency: - Vault, config, fingerprint-registry and parent-seed writes are now atomic: temp file in the same directory, fsync, rename, then an fsync of the directory. Previously the target was truncated and rewritten in place, so a crash could destroy the vault. - The lock now spans the whole read-modify-write cycle, not just the write. Serializing writes alone still lost changes -- two commands each read the same index, mutated their copy, and wrote in turn. Every mutating command goes through mutateVault(), which holds the lock from open to save. A test runs five concurrent writers and asserts all five entries survive. - 0600 is now enforced on existing files too: writeFile's mode argument only applies on creation, so a loosened vault stayed loosened. - The index is schema-validated before it can reach disk. Path handling: - Profile fingerprints are validated (16 uppercase hex) before being used in any path, and removeProfile additionally checks the resolved directory still sits directly under the app directory. A tampered registry could otherwise have steered a recursive delete elsewhere. - Malformed entries in fingerprints.json are dropped on read. Restore semantics: - `nostr restore` merges remote state into the local vault instead of replacing it. Replacing silently discarded entries created since the last sync; that now requires --replace --yes, and any overwritten vault is copied aside first. - Deltas are bound to the manifest they extend (filtered on the e tag, and re-checked locally), so a relay cannot replay a validly-signed delta from another snapshot lineage into a restore. - Manifest selection breaks created_at ties by relay order, so a same-second republish cannot leave a restore on a stale snapshot. - mergeIndexPayloads gained an index0 option. The default still refuses to merge populated Python atlas state rather than drop it; restore uses preserve-current, keeping the local block verbatim, because Python recomputes canonical views on load and the default would block every restore into a Python-created profile. 193 core + 69 CLI tests green; 36/36 cross-implementation checks green. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
9926b48e16
commit
55879eaa1b
9 files changed
+519
-115
No files matched your search
@@ -3,7 +3,7 @@
|
||||
* (seedpass_config.json.enc, encrypted under the index key).
|
||||
*/
|
||||
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
utf8,
|
||||
} from "@seedpass/core";
|
||||
import { CONFIG_FILENAME } from "./appDir.js";
|
||||
import { atomicWrite } from "./vaultFile.js";
|
||||
|
||||
export const DEFAULT_RELAYS = [
|
||||
"wss://relay.snort.social",
|
||||
@@ -69,5 +70,5 @@ export async function saveConfig(
|
||||
): Promise<void> {
|
||||
const key = deriveIndexKeyBytes(mnemonic);
|
||||
const payload = await encryptV3(key, utf8(JSON.stringify(config)));
|
||||
await writeFile(join(profileDir, CONFIG_FILENAME), payload, { mode: 0o600 });
|
||||
await atomicWrite(join(profileDir, CONFIG_FILENAME), payload);
|
||||
}
|
||||
Reference in new issue
Block a user