* 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:
1 parent
61716bb512
commit
fce38ec6f0
6 files changed
+300
-23
No files matched your search
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user