Continue PR #142: security + Nix docs follow-up (#148)

* feat: add Nix Flake release and update documentation for Nix integration

- Added flake.nix with native library patching for Linux
- Included SHA256 hashes for v0.10.0 DEB releases (x86_64 and arm64)
- Updated README.md with instructions for nix run, NixOS, Home Manager, and devShells
- Generated flake.lock for reproducibility

* refactor: improve Nix Flake patterns for better consumer control

- Extracted derivation logic into a reusable `mkPackage` function
- Exposed `overlays.default` to allow consumers to use their own nixpkgs instance
- Updated `pkgs.system` to `pkgs.stdenv.hostPlatform.system` to follow modern Nix patterns
- Ensured default `packages` definition correctly handles unfree configuration for testing with `nix build` or `nix run`

* docs: update README with advanced Nix integration patterns

- Added Option 1 (Direct Integration) and Option 2 (Overlay) documentation
- Explained how to use the overlay for full nixpkgs control
- Fixed Markdown styling and lint issues in the Nix integration section

* docs: simplify Nix documentation to favor Overlay integration

- Removed "Option 1" (direct integration) as per user feedback
- Promoted the Overlay method as the standard way to consume the package
- Updated devShell examples to rely on the overlayed package set for consistency

* fix(security): prevent master key overwriting on decryption failure and improve account loading resilience

- Modify readSafeStorageKey and generatePrimaryMasterKey to stop if decryption fails for an existing file, preventing permanent data loss when keyring is temporarily locked.
- Update CloudAccountRepo.getAccounts and getAccount to gracefully skip corrupted account records instead of throwing errors.
- Added detailed logging for security failures to aid in diagnostics.

* docs(readme): sync Nix/NixOS integration docs in README.zh-CN

---------

Co-authored-by: Youssef <[email protected]>
This commit is contained in:
vinandYoussef authored and GitHub committed 2026-04-06 21:34:32 +08:00
1 parent 61716bb512
commit fce38ec6f0
6 files changed
+300 -23

No files matched your search

+34 -14
View File
@@ -638,21 +638,41 @@ export class CloudAccountRepo {
return undefined;
}
const tokenResult = await decryptAndMigrateField(
orm,
normalizedRow.id,
'tokenJson',
normalizedRow.tokenJson,
);
const quotaResult = await decryptAndMigrateField(
orm,
normalizedRow.id,
'quotaJson',
normalizedRow.quotaJson,
);
let tokenResult: DecryptFieldResult;
try {
tokenResult = await decryptAndMigrateField(
orm,
normalizedRow.id,
'tokenJson',
normalizedRow.tokenJson,
);
} catch (error) {
logger.error(
`[CloudAccountRepo] getAccount ${id} failed - Decryption failed for token`,
error,
);
return undefined;
}
if (!tokenResult.value) {
throw new Error(`Missing token data for account ${normalizedRow.id}`);
let quotaResult: DecryptFieldResult;
try {
quotaResult = await decryptAndMigrateField(
orm,
normalizedRow.id,
'quotaJson',
normalizedRow.quotaJson,
);
} catch (error) {
logger.error(
`[CloudAccountRepo] getAccount ${id} failed - Decryption failed for quota`,
error,
);
quotaResult = { value: null, migrated: false };
}
const tokenValue = tokenResult.value;
if (!tokenValue) {
return undefined;
}
return {
+24 -9
View File
@@ -92,22 +92,29 @@ async function readSafeStorageKey(keyPath: string): Promise<Buffer | null> {
return null;
}
let encryptedKey: Buffer;
try {
encryptedKey = await fs.readFile(keyPath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
logger.error('Security: Failed to read safeStorage key file', error);
throw error;
}
try {
const encryptedKey = await fs.readFile(keyPath);
const hexKey = safeStorage.decryptString(encryptedKey);
if (!/^[a-f0-9]+$/i.test(hexKey) || hexKey.length !== 64) {
logger.warn('Security: safeStorage key has invalid format');
return null;
}
return Buffer.from(hexKey, 'hex');
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
logger.warn('Security: Failed to read safeStorage key file', error);
return null;
logger.error('Security: Decryption of safeStorage key file failed. Keyring might be locked.', error);
throw error;
}
}
@@ -169,14 +176,15 @@ async function readFileFallbackKey(keyPath: string): Promise<Buffer | null> {
return Buffer.from(content, 'hex');
}
// If it's not a valid hex key but file exists, it might be a safeStorage encrypted key
return null;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
logger.warn('Security: Error reading fallback key file', error);
return null;
logger.error('Security: Error reading fallback key file', error);
throw error;
}
}
@@ -293,6 +301,13 @@ async function generatePrimaryMasterKey(): Promise<MasterKeyState> {
}
return { key: result.key, source: 'safeStorage' };
} catch (error) {
// If we failed to decrypt but the file exists, we should NOT proceed to other fallbacks
// as they might overwrite the existing file and cause permanent data loss.
const fileExists = await fs.access(keyPath).then(() => true).catch(() => false);
if (fileExists) {
logger.error('Security: safeStorage key file exists but decryption failed. Keyring might be locked. Stopping to prevent data loss.', error);
throw error;
}
logger.warn('Security: safeStorage failed, trying keytar', error);
}
}