From 1317ba9ec63899a63860f5c30775ad9a06f6ffda Mon Sep 17 00:00:00 2001 From: Arda Kartal Date: Sat, 15 Aug 2026 20:20:05 +0300 Subject: [PATCH 1/3] feat: add secure Linux distribution support --- .github/workflows/ci.yml | 13 ++- .github/workflows/release.yml | 87 +++++++++++++------ README.md | 40 +++++++-- package.json | 32 ++++++- src/main/index.ts | 3 +- src/main/security/ElectronSafeStorageVault.ts | 26 ++++-- src/renderer/src/utils/qrDecoder.ts | 2 +- 7 files changed, 155 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f38bae5..7d62cb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,11 @@ permissions: jobs: verify: - runs-on: windows-latest + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -18,4 +22,9 @@ jobs: node-version: 22 cache: npm - run: npm ci - - run: npm run verify + - name: Verify on Windows + if: runner.os == 'Windows' + run: npm run verify + - name: Verify on Linux + if: runner.os == 'Linux' + run: xvfb-run --auto-servernum npm run verify diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b989d5..1fedbd5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release +name: Release on: push: @@ -6,11 +6,19 @@ on: - 'v*' permissions: - contents: write + contents: read jobs: - release: - runs-on: windows-latest + build: + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + platform: windows + - os: ubuntu-latest + platform: linux + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 @@ -22,34 +30,59 @@ jobs: - name: Install dependencies run: npm ci - - name: Verify project - run: npm run format:check && npm run lint && npm run test && npm run build + - name: Verify project on Windows + if: runner.os == 'Windows' + run: npm run verify - - name: Build Windows Executable + - name: Verify project on Linux + if: runner.os == 'Linux' + run: xvfb-run --auto-servernum npm run verify + + - name: Build Windows installer + if: runner.os == 'Windows' run: npx electron-builder --win nsis --x64 --publish never - - name: Generate SHA-256 checksum - shell: pwsh - run: | - $installer = Get-ChildItem -LiteralPath dist -Filter '*.exe' | Select-Object -First 1 - if (-not $installer) { throw 'Windows installer was not created.' } - $hash = (Get-FileHash -LiteralPath $installer.FullName -Algorithm SHA256).Hash.ToLowerInvariant() - "$hash $($installer.Name)" | Set-Content -LiteralPath dist/SHA256SUMS.txt -Encoding ascii + - name: Build Linux packages + if: runner.os == 'Linux' + run: npx electron-builder --linux AppImage deb --x64 --publish never + + - name: Upload packaged artifacts + uses: actions/upload-artifact@v4 + with: + name: authly-${{ matrix.platform }} + path: | + dist/*.exe + dist/*.exe.blockmap + dist/*.AppImage + dist/*.AppImage.blockmap + dist/*.deb + dist/latest.yml + dist/latest-linux.yml + if-no-files-found: error + + publish: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: authly-* + path: dist + merge-multiple: true + + - name: Generate SHA-256 checksums + working-directory: dist + run: sha256sum * > SHA256SUMS.txt - - name: Create GitHub Release and upload verified artifacts - shell: pwsh + - name: Create GitHub release env: GH_TOKEN: ${{ github.token }} run: | - $installer = Get-ChildItem -LiteralPath dist -Filter '*.exe' | Select-Object -First 1 - $blockmap = Get-ChildItem -LiteralPath dist -Filter '*.exe.blockmap' | Select-Object -First 1 - $metadata = Get-Item -LiteralPath dist/latest.yml - if (-not $blockmap) { throw 'Windows update blockmap was not created.' } - gh release create "${{ github.ref_name }}" ` - $installer.FullName ` - $blockmap.FullName ` - $metadata.FullName ` - dist/SHA256SUMS.txt ` - --verify-tag ` - --title "${{ github.ref_name }}" ` + gh release create "${{ github.ref_name }}" \ + dist/* \ + --repo "${{ github.repository }}" \ + --verify-tag \ + --title "${{ github.ref_name }}" \ --generate-notes diff --git a/README.md b/README.md index f6326e9..06713ce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Authly -Authly is a Windows-first, offline desktop authenticator built with Electron, React, TypeScript, Vite, SQLite, and DPAPI OS-level encryption. It provides a secure, lightweight alternative to cloud-connected authenticator apps, keeping all multi-factor authentication (2FA/TOTP) secrets strictly local on your device. +Authly is a cross-platform, offline desktop authenticator for Windows and Linux, built with Electron, React, TypeScript, Vite, and SQLite. It provides a secure, lightweight alternative to cloud-connected authenticator apps, keeping all multi-factor authentication (2FA/TOTP) secrets strictly local on your device. ## Features @@ -8,7 +8,7 @@ Authly is a Windows-first, offline desktop authenticator built with Electron, Re - **Account Management**: Add accounts manually via secret keys, otpauth:// URIs, or scanning QR images. - **Master PIN Security**: Protect app startup and sensitive account data with an encrypted Master PIN lock. - **Encrypted Backup & Recovery**: Export and import full account backups protected with PBKDF2 + AES-256-GCM encryption. -- **System Tray & Hotkeys**: Minimize to Windows system tray, quick clipboard copy with auto-clipboard clear after 15 seconds. +- **System Tray & Hotkeys**: Minimize to the system tray, quick clipboard copy with auto-clipboard clear after 15 seconds. - **Theme & Appearance**: System, dark, and light visual modes built with custom UI tokens and clean accessibility. - **GitHub Release Update Check**: In-app one-click update checking against official GitHub releases. - **Isolated Renderer Security**: Strict IPC sandboxing, custom authapp:// protocol, zero raw Node/filesystem access in renderer. @@ -18,13 +18,14 @@ Authly is a Windows-first, offline desktop authenticator built with Electron, Re Authly is designed with a local-first security architecture: - **Offline Operating Mode**: Secrets and database files never leave your computer. -- **OS-Level Safe Storage**: TOTP secrets are encrypted using Windows Data Protection API (DPAPI). +- **OS-Level Safe Storage**: TOTP secrets are encrypted using Windows DPAPI or a Linux desktop keyring (GNOME Keyring/KWallet). - **Auto-Clipboard Clean**: Copied 2FA codes are automatically wiped from system clipboard after 15 seconds. - **Zero Analytics**: No telemetry, tracking, or cloud backend services. ## Requirements -- Windows 10/11 (x64) +- Windows 10/11 (x64), or a modern x64 Linux distribution +- Linux: an installed and unlocked GNOME Keyring or KWallet service - Node.js 20.19 or newer - npm 11 or newer @@ -48,13 +49,13 @@ npm run dev Run full quality gates (formatting, linting, tests, strict TypeScript build, and visual/runtime smoke testing): -```powershell +```bash npm run verify ``` Run individual quality checks: -```powershell +```bash npm run typecheck npm run lint npm test @@ -66,15 +67,36 @@ npm run smoke Package the standalone Windows NSIS installer: -```powershell +```bash npm run package:win ``` -Installers and packaged outputs are generated in the `dist/` directory. +Package the Linux AppImage and Debian package: + +```bash +npm run package:linux +``` + +Installers and packaged outputs are generated in the `dist/` directory. The AppImage runs on most modern Linux distributions; the `.deb` package targets Debian, Ubuntu, and their derivatives. + +To run the AppImage: + +```bash +chmod +x Authly-*.AppImage +./Authly-*.AppImage +``` + +To install the Debian package: + +```bash +sudo apt install ./Authly-*.deb +``` ## Local Data -Runtime data (SQLite database, encrypted Vault secrets, application settings) is stored locally in Windows `%APPDATA%\authapp` and is excluded from Git. Do not commit personal backups, local database files, or master PIN hashes. +Runtime data (SQLite database, encrypted Vault secrets, application settings) is stored in Electron's per-user application-data directory (`%APPDATA%\authapp` on Windows and usually `~/.config/authapp` on Linux) and is excluded from Git. Do not commit personal backups, local database files, or master PIN hashes. + +On Linux, Authly deliberately refuses to store new secrets when Electron falls back to its insecure `basic_text` backend. Start and unlock GNOME Keyring or KWallet before using the app. ## License diff --git a/package.json b/package.json index 7a6cf80..86b3b76 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "name": "authapp", "version": "0.3.9", "private": true, - "description": "Windows-first, offline desktop authenticator", + "description": "Cross-platform, offline desktop authenticator for Windows and Linux", + "desktopName": "Authly", "author": "Arda Kartal ", "repository": { "type": "git", @@ -27,7 +28,9 @@ "smoke": "node scripts/smoke.mjs", "smoke:visual": "node scripts/smoke.mjs --visual", "verify": "npm run format:check && npm run lint && npm run test && npm run build && npm run smoke", - "package:win": "npm run verify && electron-builder --win nsis --x64" + "package:win": "npm run verify && electron-builder --win nsis --x64", + "package:linux": "npm run verify && electron-builder --linux AppImage deb --x64", + "package:linux:fast": "npm run build && electron-builder --linux AppImage deb --x64" }, "dependencies": { "@fontsource/ibm-plex-sans": "5.3.0", @@ -104,6 +107,31 @@ } ] }, + "linux": { + "icon": "resources/icon.png", + "category": "Utility;Security", + "executableName": "authly", + "syncDesktopName": true, + "artifactName": "Authly-${version}-${arch}.${ext}", + "target": [ + { + "target": "AppImage", + "arch": [ + "x64" + ] + }, + { + "target": "deb", + "arch": [ + "x64" + ] + } + ] + }, + "deb": { + "packageCategory": "utils", + "priority": "optional" + }, "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, diff --git a/src/main/index.ts b/src/main/index.ts index 5ef0a29..dae6492 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -33,6 +33,7 @@ app.commandLine.appendSwitch('js-flags', '--max-old-space-size=128'); const isSmokeTest = process.env['AUTHAPP_SMOKE_TEST'] === '1'; if (isSmokeTest) { + if (process.platform === 'linux') app.commandLine.appendSwitch('password-store', 'basic'); const smokeUserData = join(app.getPath('temp'), `authapp-smoke-userdata-${process.pid}`); app.setPath('userData', smokeUserData); } @@ -48,7 +49,7 @@ app.whenReady().then(async () => { if (!developmentUrl) registerLocalProtocol(join(__dirname, '../renderer')); const databasePath = isSmokeTest ? ':memory:' : join(app.getPath('userData'), 'authapp.db'); localDatabase = new LocalDatabase(databasePath); - const vault = new ElectronSafeStorageVault(); + const vault = new ElectronSafeStorageVault(isSmokeTest); if (isSmokeTest) { const encrypted = await vault.encryptSecret('runtime-smoke-value'); const decrypted = await vault.decryptSecret(encrypted); diff --git a/src/main/security/ElectronSafeStorageVault.ts b/src/main/security/ElectronSafeStorageVault.ts index bef358a..ad1c6ed 100644 --- a/src/main/security/ElectronSafeStorageVault.ts +++ b/src/main/security/ElectronSafeStorageVault.ts @@ -2,17 +2,15 @@ import { safeStorage } from 'electron'; import type { VaultDecryptResult, VaultService } from './VaultService'; export class ElectronSafeStorageVault implements VaultService { + constructor(private readonly allowInsecureLinuxBackend = false) {} + async encryptSecret(secret: string): Promise { - if (!(await safeStorage.isAsyncEncryptionAvailable())) { - throw new Error('Secure OS encryption is unavailable.'); - } + await this.assertSecureStorageAvailable(); return safeStorage.encryptStringAsync(secret); } async decryptSecret(encryptedSecret: Buffer): Promise { - if (!(await safeStorage.isAsyncEncryptionAvailable())) { - throw new Error('Secure OS encryption is unavailable.'); - } + await this.assertSecureStorageAvailable(); const result = await safeStorage.decryptStringAsync(encryptedSecret); return { secret: result.result, @@ -21,4 +19,20 @@ export class ElectronSafeStorageVault implements VaultService { : {}), }; } + + private async assertSecureStorageAvailable(): Promise { + if (!(await safeStorage.isAsyncEncryptionAvailable())) { + throw new Error('Secure OS encryption is unavailable.'); + } + + if ( + process.platform === 'linux' && + !this.allowInsecureLinuxBackend && + safeStorage.getSelectedStorageBackend() === 'basic_text' + ) { + throw new Error( + 'Secure OS keyring is unavailable. Install and unlock GNOME Keyring or KWallet.', + ); + } + } } diff --git a/src/renderer/src/utils/qrDecoder.ts b/src/renderer/src/utils/qrDecoder.ts index 7d3cbbc..7bbf315 100644 --- a/src/renderer/src/utils/qrDecoder.ts +++ b/src/renderer/src/utils/qrDecoder.ts @@ -1,4 +1,4 @@ -import jsQR from 'jsQR'; +import jsQR from 'jsqr'; import type { CreateAccountRequest } from '@shared/schemas/account'; import { parseOtpMigrationUri } from './otpMigrationParser'; From 7083c8444ee7154d7c2d10946da1af4d7e868850 Mon Sep 17 00:00:00 2001 From: Arda Kartal Date: Sat, 15 Aug 2026 20:23:58 +0300 Subject: [PATCH 2/3] ci: allow Electron smoke test on Linux runners --- .github/workflows/ci.yml | 2 ++ .github/workflows/release.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d62cb0..261a09a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,4 +27,6 @@ jobs: run: npm run verify - name: Verify on Linux if: runner.os == 'Linux' + env: + ELECTRON_DISABLE_SANDBOX: '1' run: xvfb-run --auto-servernum npm run verify diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fedbd5..1a3afc2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,6 +36,8 @@ jobs: - name: Verify project on Linux if: runner.os == 'Linux' + env: + ELECTRON_DISABLE_SANDBOX: '1' run: xvfb-run --auto-servernum npm run verify - name: Build Windows installer From 50c68e8b3ff3ea4c1ccfac58c843775dac74630a Mon Sep 17 00:00:00 2001 From: Arda Kartal Date: Sat, 15 Aug 2026 20:28:57 +0300 Subject: [PATCH 3/3] ci: preserve required verify status --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 261a09a..7ae5b77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,14 +2,14 @@ name: CI on: push: - branches: ['**'] + branches: [master] pull_request: permissions: contents: read jobs: - verify: + verify-platform: strategy: fail-fast: false matrix: @@ -30,3 +30,9 @@ jobs: env: ELECTRON_DISABLE_SANDBOX: '1' run: xvfb-run --auto-servernum npm run verify + + verify: + needs: verify-platform + runs-on: ubuntu-latest + steps: + - run: echo "Windows and Linux verification passed."