From fce38ec6f0f031d1b11225e4030a03b4ceec37e3 Mon Sep 17 00:00:00 2001 From: vin <49336820+Draculabo@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:34:32 +0800 Subject: [PATCH] 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 --- README.md | 47 ++++++++++++ README.zh-CN.md | 47 ++++++++++++ flake.lock | 27 +++++++ flake.nix | 121 +++++++++++++++++++++++++++++++ src/ipc/database/cloudHandler.ts | 48 ++++++++---- src/utils/security.ts | 33 ++++++--- 6 files changed, 300 insertions(+), 23 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/README.md b/README.md index 8acc6a1..bd15ffc 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,53 @@ Download the latest release for your platform from the [Releases](https://github | Windows (x64/ARM64) | [.exe installer](https://github.com/Draculabo/AntigravityManager/releases/latest) | | macOS | [.dmg installer](https://github.com/Draculabo/AntigravityManager/releases/latest) | | Linux | [.deb / .rpm](https://github.com/Draculabo/AntigravityManager/releases/latest) | +| NixOS / Nix | `nix run github:Draculabo/AntigravityManager` | + +### ❄️ Nix Integration + +You can integrate Antigravity Manager into your Nix configuration using the provided flake. + +#### 🛠️ NixOS / Home Manager + +Add the flake to your `flake.nix` inputs: + +```nix +inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + antigravity-manager.url = "github:Draculabo/AntigravityManager"; +}; +``` + +To integrate with your system, add `antigravity-manager.overlays.default` to your `nixpkgs.overlays`. This ensures the package is correctly built against your own `nixpkgs` instance and respects your global configuration. + +```nix +# Add to your NixOS/Home Manager configuration +{ + nixpkgs.overlays = [ inputs.antigravity-manager.overlays.default ]; + + # Antigravity Manager has an unfree license (cc-by-nc-sa-40) + nixpkgs.config.allowUnfree = true; + + environment.systemPackages = [ + pkgs.antigravity-manager + ]; +} +``` + +#### 💻 Development Shell + +If you're using the overlay above, you can simply add it to your `devShell`: + +```nix +devShells.default = pkgs.mkShell { + packages = [ + pkgs.antigravity-manager + ]; +}; +``` + +> [!NOTE] +> Since the package is unfree, you **must** have `allowUnfree = true;` enabled in your `nixpkgs` configuration for it to evaluate correctly. ### Build from Source diff --git a/README.zh-CN.md b/README.zh-CN.md index d3014e4..dcc4593 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -192,6 +192,53 @@ | Windows (x64/ARM64) | [.exe 安装包](https://github.com/Draculabo/AntigravityManager/releases/latest) | | macOS | [.dmg 安装包](https://github.com/Draculabo/AntigravityManager/releases/latest) | | Linux | [.deb / .rpm](https://github.com/Draculabo/AntigravityManager/releases/latest) | +| NixOS / Nix | `nix run github:Draculabo/AntigravityManager` | + +### ❄️ Nix 集成 + +你可以通过项目提供的 flake 将 Antigravity Manager 集成到你的 Nix 配置中。 + +#### 🛠️ NixOS / Home Manager + +先在你的 `flake.nix` 的 `inputs` 中加入: + +```nix +inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + antigravity-manager.url = "github:Draculabo/AntigravityManager"; +}; +``` + +然后把 `antigravity-manager.overlays.default` 添加到 `nixpkgs.overlays`。这样会基于你自己的 `nixpkgs` 实例构建,并继承你的全局配置。 + +```nix +# 添加到你的 NixOS/Home Manager 配置中 +{ + nixpkgs.overlays = [ inputs.antigravity-manager.overlays.default ]; + + # Antigravity Manager 使用非自由许可证 (cc-by-nc-sa-40) + nixpkgs.config.allowUnfree = true; + + environment.systemPackages = [ + pkgs.antigravity-manager + ]; +} +``` + +#### 💻 开发 Shell + +如果你已经使用上面的 overlay,也可以直接在 `devShell` 里加入: + +```nix +devShells.default = pkgs.mkShell { + packages = [ + pkgs.antigravity-manager + ]; +}; +``` + +> [!NOTE] +> 由于该包是非自由软件,你必须在 `nixpkgs` 配置中开启 `allowUnfree = true;`,否则会评估失败。 ### 从源码构建 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..926d5b3 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1774106199, + "narHash": "sha256-US5Tda2sKmjrg2lNHQL3jRQ6p96cgfWh3J1QBliQ8Ws=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6c9a78c09ff4d6c21d0319114873508a6ec01655", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a9f43da --- /dev/null +++ b/flake.nix @@ -0,0 +1,121 @@ +{ + description = "Antigravity Manager"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { + self, + nixpkgs, + }: let + supportedSystems = ["x86_64-linux" "aarch64-linux"]; + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + version = "0.10.0"; + + sources = { + "x86_64-linux" = { + url = "https://github.com/Draculabo/AntigravityManager/releases/download/v${version}/Antigravity.Manager_${version}_amd64.deb"; + sha256 = "5fc08b4a9308c71fc00e44b95d4fb3178182aea8821708fa51fdf2bf5dbbb856"; + }; + "aarch64-linux" = { + url = "https://github.com/Draculabo/AntigravityManager/releases/download/v${version}/Antigravity.Manager_${version}_arm64.deb"; + sha256 = "666c9659efa66bb6be6a8e7fb0c384ceb799a17245eab374bf9b3d7901c5c4cb"; + }; + }; + + mkPackage = pkgs: let + system = pkgs.stdenv.hostPlatform.system; + source = sources.${system} or (throw "Unsupported system: ${system}"); + in + pkgs.stdenv.mkDerivation { + pname = "antigravity-manager"; + inherit version; + + src = pkgs.fetchurl { + inherit (source) url sha256; + }; + + nativeBuildInputs = with pkgs; [ + autoPatchelfHook + makeWrapper + zstd + ]; + + buildInputs = with pkgs; [ + alsa-lib + at-spi2-atk + atk + cups + dbus + expat + glib + gtk3 + libsecret + libdrm + libxkbcommon + mesa + nspr + nss + pango + systemd + libx11 + libxcomposite + libxdamage + libxext + libxfixes + libxrandr + libxcb + ]; + + unpackPhase = '' + ar x $src + tar xf data.tar.* --no-same-permissions + ''; + + installPhase = '' + mkdir -p $out/bin $out/lib $out/share + cp -r usr/lib/antigravity-manager $out/lib/ + cp -r usr/share/* $out/share/ + + ln -s $out/lib/antigravity-manager/antigravity-manager $out/bin/antigravity-manager + ''; + + postFixup = '' + # Wrap the executable so it finds Wayland/X11 and libsecret + wrapProgram $out/bin/antigravity-manager \ + --add-flags "--enable-features=WaylandWindowDecorations" \ + --add-flags "--ozone-platform-hint=auto" \ + --prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath [pkgs.libsecret]} + + if [ -e $out/share/applications/antigravity-manager.desktop ]; then + substituteInPlace $out/share/applications/antigravity-manager.desktop \ + --replace "/usr/lib/antigravity-manager/antigravity-manager" "antigravity-manager" + fi + ''; + + meta = with pkgs.lib; { + description = "Professional multi-account manager for Google Gemini & Claude AI"; + homepage = "https://github.com/Draculabo/AntigravityManager"; + license = licenses.cc-by-nc-sa-40; + maintainers = []; + platforms = ["x86_64-linux" "aarch64-linux"]; + mainProgram = "antigravity-manager"; + }; + }; + in { + overlays.default = final: prev: { + antigravity-manager = mkPackage final; + }; + + packages = forAllSystems (system: let + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + in { + default = mkPackage pkgs; + }); + }; +} diff --git a/src/ipc/database/cloudHandler.ts b/src/ipc/database/cloudHandler.ts index 78836ff..2f59485 100644 --- a/src/ipc/database/cloudHandler.ts +++ b/src/ipc/database/cloudHandler.ts @@ -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 { diff --git a/src/utils/security.ts b/src/utils/security.ts index ccafc0d..2a4341f 100644 --- a/src/utils/security.ts +++ b/src/utils/security.ts @@ -92,22 +92,29 @@ async function readSafeStorageKey(keyPath: string): Promise { 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 { 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 { } 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); } }