fix: establish durable keyring publication boundary

Fsync the keyring directory after both complete temporary files are written and synced, before replacing the primary document. Pre-publication directory failures now preserve the prior primary state and remain rollback-safe.

Keep the durable recovery temporary file available when later directory or backup publication work fails, and verify that it restores the committed keyring.

Rank equal-time recovery candidates deterministically with recovery temps before primary temps and backups, and consider equal-time commit temps alongside a valid primary.
This commit is contained in:
Sucukdeluxe
2026-08-22 15:36:11 +02:00
parent 8041aeead9
commit 82578b5f6e
2 changed files with 181 additions and 9 deletions
+21 -9
View File
@@ -96,6 +96,13 @@ function createOnlineBackupKeyring({
&& /^\d+\.[0-9a-f-]+\.(?:primary|recovery)\.tmp$/u.test(value.slice(temporaryPrefix.length));
}
function recoveryCandidatePriority(candidatePath) {
const name = path.basename(candidatePath);
if (name.endsWith('.recovery.tmp')) return 0;
if (name.endsWith('.primary.tmp')) return 1;
return 2;
}
function encryptionError(error) {
return issueError(error?.code === 'SECRET_STORE_UNAVAILABLE' ? KEYRING_ERROR_CODES.unavailable : KEYRING_ERROR_CODES.encrypt);
}
@@ -141,12 +148,20 @@ function createOnlineBackupKeyring({
for (const candidatePath of candidates) {
try {
const stats = await fsImpl.stat(candidatePath);
ranked.push({ candidatePath, modified: stats.mtimeMs });
ranked.push({
candidatePath,
modified: stats.mtimeMs,
priority: recoveryCandidatePriority(candidatePath)
});
} catch (error) {
if (error?.code !== 'ENOENT') ranked.push({ candidatePath, modified: 0 });
if (error?.code !== 'ENOENT') ranked.push({ candidatePath, modified: 0, priority: recoveryCandidatePriority(candidatePath) });
}
}
ranked.sort((left, right) => right.modified - left.modified);
ranked.sort((left, right) =>
right.modified - left.modified
|| left.priority - right.priority
|| left.candidatePath.localeCompare(right.candidatePath)
);
return ranked.map(candidate => candidate.candidatePath);
}
@@ -229,7 +244,7 @@ function createOnlineBackupKeyring({
try {
candidateModified = (await fsImpl.stat(candidatePath)).mtimeMs;
} catch {}
if (candidateModified <= primaryModified) continue;
if (candidateModified < primaryModified) continue;
const candidate = await readCandidate(candidatePath);
if (candidate.status !== 'valid') continue;
const state = inspectSource({ ...candidate, recovered: true });
@@ -353,12 +368,11 @@ function createOnlineBackupKeyring({
const primaryTemporaryPath = temporaryPath('primary');
const recoveryTemporaryPath = temporaryPath('recovery');
await fsImpl.mkdir(directory, { recursive: true });
let primaryPublished = false;
try {
await writeAndSync(primaryTemporaryPath, contents);
await writeAndSync(recoveryTemporaryPath, contents);
await syncDirectory();
await fsImpl.rename(primaryTemporaryPath, filePath);
primaryPublished = true;
} catch (error) {
await removeFile(primaryTemporaryPath);
await removeFile(recoveryTemporaryPath);
@@ -370,9 +384,7 @@ function createOnlineBackupKeyring({
await fsImpl.rename(recoveryTemporaryPath, backupPath);
recoveryPath = null;
await syncDirectory();
} catch {
if (!primaryPublished) throw issueError(KEYRING_ERROR_CODES.structure);
}
} catch {}
try {
await cleanupTemporaryFiles(recoveryPath);
} catch {}