mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
* fix(orcad): seal integration credentials on the headless server instead of storing them in plaintext The Node SecretStore always reported encryption unavailable, so every credential writer on orcad fell back to plaintext. orcad now seals with AES-256-GCM using a host-owned master key kept in the OS keyring (macOS Keychain, Linux Secret Service) when reachable at first start, else in a 0600 profile file bound to the machine ID. A key that cannot load fails closed instead of downgrading. Existing plaintext credentials are sealed on startup without loss. * docs: describe how the headless server seals credentials and its limits * fix(orcad): unique credential staging files, no keyring items for temp profiles, isolate startup test * fix(orcad): keep the credential key with the credentials, one record per machine * fix(orcad): never re-key over an unreadable record, serialize first-start key creation, refuse an unrestricted key file * fix(orcad): verify a key record's Windows ACL and wait out a concurrent first start's keyring write * fix(orcad): keep a keyring claim and fail closed when its store result is uncertain * fix(credentials): restrict the staged file's Windows ACL before it replaces a credential * fix(orcad): a temporary profile never creates a key in, or re-seals, the real credential home * ci: baseline the two single-file credential ACL restrictions * fix(credentials): stage Windows credentials born restricted and verify the key record asynchronously; document rollback * test(bitbucket): inject the write failure into the Windows staging writer too
127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
import { writeFileSync } from 'node:fs'
|
|
import { reportSecurePathHardening as report } from './secure-path-hardening-report'
|
|
import { loadWindowsSecurity, type WindowsSecurity } from './windows-security-addon'
|
|
import { findSecureDaclViolation, hardenedDaclSddl } from './windows-secure-dacl'
|
|
|
|
/**
|
|
* Restricts a directory on the thread pool: a rewrite walks every descendant, seconds on a large
|
|
* userData. Never rejects; failures are reported and resolve false so the caller can retry.
|
|
*/
|
|
export function restrictWindowsDirectory(dirPath: string): Promise<boolean> {
|
|
const security = securityFor(dirPath)
|
|
return security ? restrictAsync(security, dirPath, true) : Promise.resolve(false)
|
|
}
|
|
|
|
async function restrictAsync(
|
|
security: WindowsSecurity,
|
|
targetPath: string,
|
|
isDirectory: boolean
|
|
): Promise<boolean> {
|
|
// Verify first: a directory rewrite walks every descendant (seconds on a large userData), so
|
|
// a path that already reads back correct must never pay it again.
|
|
if (violationOf(security, targetPath, isDirectory) === null) {
|
|
return true
|
|
}
|
|
try {
|
|
await security.addon.setDaclSddlAsync(
|
|
targetPath,
|
|
hardenedDaclSddl(security.userSid, isDirectory)
|
|
)
|
|
} catch (error) {
|
|
report(targetPath, 'apply', String(error))
|
|
return false
|
|
}
|
|
return verifyAfterApply(security, targetPath, isDirectory)
|
|
}
|
|
|
|
/** Restricts one file on the thread pool; never rejects, resolves whether the DACL holds. */
|
|
export function restrictWindowsFile(filePath: string): Promise<boolean> {
|
|
const security = securityFor(filePath)
|
|
return security ? restrictAsync(security, filePath, false) : Promise.resolve(false)
|
|
}
|
|
|
|
/** Verifies and, only when that fails, rewrites the DACL on the calling thread. */
|
|
export function restrictWindowsPathSync(targetPath: string, isDirectory: boolean): boolean {
|
|
const security = securityFor(targetPath)
|
|
if (!security) {
|
|
return false
|
|
}
|
|
if (violationOf(security, targetPath, isDirectory) === null) {
|
|
return true
|
|
}
|
|
try {
|
|
security.addon.setDaclSddl(targetPath, hardenedDaclSddl(security.userSid, isDirectory))
|
|
} catch (error) {
|
|
// Why not fatal: a failed apply must not crash the caller; false leaves the path retryable.
|
|
report(targetPath, 'apply', String(error))
|
|
return false
|
|
}
|
|
return verifyAfterApply(security, targetPath, isDirectory)
|
|
}
|
|
|
|
/**
|
|
* Writes a NEW file that is restricted from the moment it exists, so its contents are never
|
|
* readable under the directory's (possibly broader) inherited DACL. Returns whether the
|
|
* restriction holds. Without the addon the file is written plainly and this is false; with it, a
|
|
* failed create throws like any other write failure.
|
|
*/
|
|
export function writeRestrictedWindowsFile(targetPath: string, contents: string | Buffer): boolean {
|
|
const security = securityFor(targetPath)
|
|
if (!security) {
|
|
writeFileSync(targetPath, contents, { encoding: 'utf-8', mode: 0o600 })
|
|
return false
|
|
}
|
|
security.addon.createFileWithDacl(
|
|
targetPath,
|
|
hardenedDaclSddl(security.userSid, false),
|
|
typeof contents === 'string' ? Buffer.from(contents, 'utf-8') : contents
|
|
)
|
|
// Why read it back: FAT32 and some shares accept the descriptor and silently drop it.
|
|
return verifyAfterApply(security, targetPath, false)
|
|
}
|
|
|
|
function verifyAfterApply(
|
|
security: WindowsSecurity,
|
|
targetPath: string,
|
|
isDirectory: boolean
|
|
): boolean {
|
|
const invalid = violationOf(security, targetPath, isDirectory)
|
|
if (invalid) {
|
|
report(targetPath, 'verify', invalid)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
/** Why the DACL on disk is not acceptable, or null when it is. */
|
|
function violationOf(
|
|
security: WindowsSecurity,
|
|
targetPath: string,
|
|
isDirectory: boolean
|
|
): string | null {
|
|
try {
|
|
return findSecureDaclViolation(
|
|
security.addon.getDaclAces(targetPath),
|
|
security.userSid,
|
|
isDirectory
|
|
)
|
|
} catch (error) {
|
|
return String(error)
|
|
}
|
|
}
|
|
|
|
let reportedLoadFailure = false
|
|
|
|
function securityFor(targetPath: string): WindowsSecurity | null {
|
|
const loaded = loadWindowsSecurity()
|
|
if ('error' in loaded) {
|
|
// Once per process: a missing addon is a packaging fault, and every secure write would repeat it.
|
|
if (!reportedLoadFailure) {
|
|
reportedLoadFailure = true
|
|
report(targetPath, 'addon', loaded.error)
|
|
}
|
|
return null
|
|
}
|
|
return loaded
|
|
}
|