OpenWork Secure Vault Security Model and Key Management Explained
OpenWork protects user OAuth credentials using a desktop-only vault that leverages Electron's SafeStorage API with lazy-loaded, OS-encrypted key files protected by strict filesystem permissions.
The OpenWork repository implements a security architecture designed to keep cryptographic material safeguarding user-level OAuth credentials confined to the desktop environment. This approach ensures sensitive encryption keys never exist in plaintext on disk and remain bound to the operating system's native secure storage capabilities. The implementation prioritizes minimal attack surface through lazy initialization and defensive programming via atomic file operations and comprehensive error handling.
Core Security Principles of the OpenWork Vault
The OpenWork vault architecture rests on three foundational security principles that shape every implementation decision in apps/desktop/electron/secure-vault-key.mjs.
Desktop-Only Key Confinement
OpenWork deliberately restricts vault key storage to the local desktop environment. The cryptographic material protecting OAuth credentials never leaves the device, eliminating network-based exfiltration risks for the master encryption key itself. This design choice reflects OpenWork's threat model: compromise of cloud storage should not automatically grant access to decrypted user credentials.
Lazy-Load Activation
The vault system remains entirely dormant until a user explicitly opts into OpenWork-managed OAuth. The encrypted key file is created only upon first use, leaving the OS secure storage untouched for users who never enable the feature. This lazy provider pattern reduces the persistent attack surface and prevents unnecessary security dependencies.
OS-Native Encryption Binding
Rather than implementing custom cryptography, OpenWork delegates all encryption operations to Electron's SafeStorage module. This binds key confidentiality to the operating system's proven keychain or credential store implementations—macOS Keychain, Windows Data Protection API, or Linux Secret Service.
Key Generation and Storage Implementation
The vault key lifecycle follows a deterministic sequence implemented in createDesktopVaultKeyProvider.
Random Key Generation with crypto.randomBytes
When no existing key file is detected, OpenWork generates a 32-byte cryptographically random key:
import { randomBytes } from "node:crypto";
// From secure-vault-key.mjs implementation
const newKey = randomBytes(32); // 256 bits of entropy
This key provides 256-bit security suitable for AES-256-GCM or equivalent symmetric encryption of OAuth credentials.
Secure Storage Encryption via SafeStorage.encryptStringAsync
Before any filesystem write, the raw key material undergoes platform-specific encryption:
import { SafeStorage } from "electron";
// Async encryption bound to OS secure storage
const encryptedKey = await SafeStorage.encryptStringAsync(
newKey.toString("base64")
);
The encryptStringAsync method ensures the ciphertext can only be decrypted by the same OS user on the same machine, leveraging hardware-backed protection where available.
Atomic File Creation with Permission Lockdown
The implementation prevents race conditions and enforces least-privilege access:
- Write to temporary file with mode
0o600 - Explicit
chmodto owner-read/write only - Atomic
renameto final path - Cleanup of temporary file on any failure
This sequence ensures the encrypted key material never exists with world-readable permissions, even transiently.
Platform Security Hardening
OpenWork's security model includes platform-specific validations to prevent downgrade attacks.
Async Encryption Availability Verification
The provider validates that secure async encryption is supported before attempting operations:
// Platform safety check from secure-vault-key.mjs
if (!safeStorage.isAsyncEncryptionAvailable()) {
throw new Error("SafeStorage async encryption unavailable");
}
On Linux, this check specifically prevents fallback to the insecure "basic_text" backend, forcing utilization of a proper password store.
Re-Encryption on Storage Migration
The implementation detects when the OS storage layer indicates ciphertext should be refreshed:
// From decryption result handling
if (decrypted.shouldReEncrypt) {
// Rewrites file with fresh encryption under current OS key
await rewriteEncryptedKey(filePath, decrypted.plaintext);
}
This capability supports key rotation scenarios including hardware migration and OS-level credential store changes.
Lazy Provider Pattern and Caching
The createDesktopVaultKeyProvider function returns a closure that implements sophisticated loading semantics:
import { createDesktopVaultKeyProvider } from "./secure-vault-key.mjs";
const getVaultKey = createDesktopVaultKeyProvider({
filePath: "/user-data/vault.key",
loadSafeStorage: () => SafeStorage,
});
// First call triggers generation/loading and caches the promise
const keyPromise = getVaultKey();
// Subsequent calls receive identical promise—single load guarantee
const samePromise = getVaultKey();
Key provider characteristics:
- Promise caching eliminates redundant decryption operations
- Error cache clearing enables retry after transient failures
- Process-scoped singleton prevents key regeneration races
Error Handling and Edge Cases
The security model distinguishes between expected and exceptional conditions:
| Condition | Behavior | Rationale |
|---|---|---|
ENOENT (missing file) |
Trigger new key generation | First-use initialization |
| Decryption failure | Propagate error | Potential tampering or corruption |
| Filesystem permission errors | Propagate error | System integrity issue |
shouldReEncrypt flag |
Transparent re-encryption | Maintain security freshness |
This error taxonomy prevents silent failures that could mask security-relevant events while permitting graceful first-run experiences.
Practical Integration Example
The following pattern demonstrates proper vault key acquisition for credential encryption:
import { createDesktopVaultKeyProvider } from "./secure-vault-key.mjs";
import { SafeStorage } from "electron";
import { createCipheriv, randomBytes } from "node:crypto";
const vaultKeyProvider = createDesktopVaultKeyProvider({
filePath: `${app.getPath("userData")}/openwork-vault.key`,
loadSafeStorage: () => SafeStorage,
});
async function encryptOAuthToken(token) {
const vaultKey = await vaultKeyProvider(); // Buffer of 32 bytes
const iv = randomBytes(16);
const cipher = createCipheriv("aes-256-gcm", vaultKey, iv);
const encrypted = Buffer.concat([
cipher.update(token, "utf8"),
cipher.final()
]);
const authTag = cipher.getAuthTag();
return {
iv: iv.toString("base64"),
ciphertext: encrypted.toString("base64"),
tag: authTag.toString("base64"),
};
}
Key Rotation Procedure
Manual rotation follows the same atomic patterns as initial creation:
import { randomBytes } from "node:crypto";
import { writeFile, chmod, rename, rm } from "node:fs/promises";
import { SafeStorage } from "electron";
async function rotateVaultKey(filePath, safeStorage) {
const newKey = randomBytes(32);
const encrypted = await safeStorage.encryptStringAsync(
newKey.toString("base64")
);
const tmpPath = `${filePath}.${process.pid}.tmp`;
try {
await writeFile(tmpPath, encrypted, { mode: 0o600 });
await rename(tmpPath, filePath);
return newKey;
} catch (err) {
await rm(tmpPath, { force: true });
throw err;
}
}
Rotation should be accompanied by re-encryption of all stored OAuth credentials with the new key material.
Source File Reference
The complete implementation resides in specific files within the OpenWork repository:
apps/desktop/electron/secure-vault-key.mjs— Core vault key provider with generation, encryption, and file protection logicapps/desktop/electron/secure-vault-key.test.mjs— Comprehensive verification of security behaviors including error conditions and re-encryptionapps/desktop/electron/connect-link-keys.mjs— Integration example showing key utilization for service linking
Summary
- OpenWork's secure vault binds encryption keys to OS-native secure storage via Electron's
SafeStorageAPI, never persisting plaintext key material - The lazy-load design creates the encrypted key file only upon first OAuth opt-in, minimizing persistent security dependencies
- Atomic file operations with
0o600permissions and temporary-write patterns prevent race conditions and permission leaks - Platform checks explicitly reject insecure Linux fallbacks and verify async encryption availability before operations
- Promise caching in
createDesktopVaultKeyProviderensures single key load per process with retry capability after failures
Frequently Asked Questions
What encryption protects the OpenWork vault key at rest?
The 32-byte vault key is encrypted by the operating system's native secure storage through Electron's SafeStorage.encryptStringAsync before any filesystem write. On macOS this uses Keychain services; on Windows, the Data Protection API; on Linux, the Secret Service API or compatible store. This binds decryption to the original OS user account and machine hardware where supported.
How does OpenWork prevent the vault key from being stolen by malware?
Multiple layers reduce exposure: the encrypted file has 0o600 permissions (owner-only access); the plaintext key exists only briefly in Node.js memory during lazy loading; and the OS secure storage typically requires user session authentication for decryption. However, running malware with the user's privileges could potentially access the decrypted key—this is an inherent limitation of desktop security models, mitigated by OpenWork's confinement of credentials to the local vault.
Can the OpenWork vault key be recovered if the user loses their laptop?
No. Because SafeStorage encryption is bound to the specific OS installation and user account, the encrypted key file from apps/desktop/electron/secure-vault-key.mjs cannot be decrypted on different hardware or after OS reinstallation. Users must re-authenticate with OAuth providers to generate new stored credentials; OpenWork does not implement cloud backup of the master vault key by design.
What triggers re-encryption of the vault key file?
The shouldReEncrypt property returned by SafeStorage.decryptString indicates when the underlying OS storage layer recommends ciphertext refresh. This occurs during OS migrations, hardware security module changes, or explicit keychain rotations. OpenWork detects this flag transparently and rewrites the vault file with freshly encrypted key material without user intervention.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →