Skip to content

Repository files navigation

One Cred

build License: MIT

A local‑first credential manager built for developers. Cross‑platform (Android · macOS · Windows). Open source under MIT.

 ╔═════════════════════════════════════════════════════════════╗
 ║   1   One credential vault. Local. Encrypted. Yours.        ║
 ╚═════════════════════════════════════════════════════════════╝
  • Local‑first SQLite — no cloud, no account.
  • Projects + environments — colour‑coded for instant visual scanning.
  • Two‑factor unlock — biometric (Face ID / Touch ID / fingerprint / Windows Hello) with a master password fallback that's always available, even when biometric is unavailable or fails.
  • At‑rest field encryption — sensitive credential columns (secret, username, notes) are AES‑GCM‑256 encrypted on disk with a key derived from the master password (PBKDF2 200k + optional pepper). The derived key is also stashed in the OS keychain (flutter_secure_storage) so the biometric unlock path can restore it without re‑typing — biometric substitutes the master password, which stays as a fallback. Cross‑platform (Android / macOS / Windows).
  • Auto‑lock on idle — configurable inactivity timeout (Off / 1 / 5 / 15 / 30 min) plus the existing lock on background.
  • Clipboard auto‑clear — copied secrets are wiped after a configurable delay (Off / 10 / 30 / 60 / 120 s), only if the clipboard still contains the value we wrote.
  • Encrypted import/export — PBKDF2‑HMAC‑SHA256 (200k iterations) + AES‑GCM‑256 with an optional bundled pepper.
  • LAN sync (P2P) — pair two devices on the same network with a one‑time 6‑digit PIN. No cloud, no manual file dance.
  • Localized with easy_localization — Italian and English, device‑locale follow, English fallback.
  • Sidebar UI with collapse / icon‑only mode and a dark neon theme with bright accents.
  • Automated builds — every push produces signed Android APK and Windows artifacts via GitHub Actions.

Table of contents

  1. Screenshots
  2. Quick start
  3. Project layout
  4. Build for each platform
  5. Continuous integration
  6. Sensitive values — the .env file
  7. Security model
  8. Master password & biometric flow
  9. LAN sync
  10. Localization
  11. Regenerating the app icons
  12. Troubleshooting
  13. Contributing
  14. License

Screenshots

macOS:

Home Credential detail
Home screen with sidebar and a credential card Credential detail panel with secret hidden
New credential dialog Settings sheet
New credential dialog with project and env preselected Settings sheet with biometric, master password, language and encrypted export

Windows / Android screenshots will land in the same assets/screens/ folder with windows-*.png / android-*.png prefixes.


Quick start

Requirements:

  • Flutter 3.41.9 (matches the CI matrix and the Dart ^3.11.5 pin in pubspec.yaml).
  • Xcode for macOS, Visual Studio + C++ desktop workload for Windows, Android Studio / JDK 17 for Android.
git clone https://github.com/<your-org>/one-cred.git
cd one-cred

# .env is git-ignored — copy the template and (optionally) add your pepper
cp .env.example .env
$EDITOR .env

flutter pub get
flutter run -d macos      # or windows / android

First launch tip. The app seeds a Demo Project and the default environments (local, dev, staging, qa, test, preprod, prod). Delete the demo and start adding your own projects from the sidebar.

Project layout

lib/
├─ db/                       SQLite schema + CRUD (sqflite + sqflite_common_ffi)
├─ dialogs/                  Project, env, credential, passphrase, settings sheet
├─ models/                   Project, EnvDef, Credential
├─ screens/                  HomeShell, LockScreen, AppLockGate
├─ security/                 BiometricService, MasterPasswordService, VaultCrypto, VaultIO, VaultKeyService, VaultCipher
├─ state/                    AppState, LockController, IdleLockService, UiPrefs
├─ theme/                    AppTheme, AppColors, env palettes
├─ util/                     AppLog (colored logger), ClipboardService, SoundService
└─ widgets/
   ├─ common/                Shared widgets (BrandMark, GlowDot, EnvBadge, ConfirmDialog, ColorSwatchPicker, …)
   ├─ credential/            ReadableField, SecretField, CopyIconButton
   ├─ sidebar/               Brand, section header, project / env / footer tiles
   ├─ credential_card.dart
   ├─ credential_detail.dart
   ├─ credential_list.dart
   ├─ empty_state.dart
   ├─ mobile_detail_sheet.dart
   └─ top_bar.dart

assets/translations/         en.json, it.json — loaded by easy_localization
assets/icon/                 Source PNG + generator script for app icons
.github/workflows/           CI definitions (build.yml)
.env                         Loaded at runtime by flutter_dotenv (git-ignored)
android/ macos/ windows/     Platform shells (manifests, entitlements, .rc)

Each widget lives in its own file: the codebase favours many small, single‑purpose files over a few large ones. The shared atoms in lib/widgets/common/ (e.g. BrandMark, GlowDot, EnvBadge, SectionLabel, SettingsRow, ConfirmDialog, LabeledDropdown, ColorSwatchPicker) are reused across the dialogs, the lock screen and the sidebar.

Build for each platform

All commands assume you have done cp .env.example .env && flutter pub get. The .env file is bundled as a Flutter asset, so a build picks it up automatically — no extra flags required.

macOS

flutter run    -d macos                # dev
flutter build  macos    --release
open build/macos/Build/Products/Release/"One Cred.app"

The macOS shell is sandboxed; the entitlement com.apple.security.files.user-selected.read-write is enabled so file‑picker import/export works. Biometric prompts go through LocalAuthentication, with NSFaceIDUsageDescription set in Info.plist.

Windows

flutter run    -d windows
flutter build  windows    --release
# build\windows\x64\runner\Release\One Cred.exe

Biometric on Windows uses Windows Hello when available.

Android

flutter run    -d <android-id>
flutter build  apk             --release
flutter build  appbundle       --release    # Play Store

MainActivity extends FlutterFragmentActivity (required by local_auth). The manifest declares USE_BIOMETRIC and USE_FINGERPRINT.

Continuous integration

Every push (any branch) and every pull request triggers .github/workflows/build.yml, which produces:

Platform Artifact Where
Android app-release.apk Actions tab → run → one-cred-android-<sha>
Windows one-cred-windows.zip Actions tab → run → one-cred-windows-<sha>

The workflow:

  1. Sets up Flutter 3.41.9 (and JDK 17 for the Android job) via subosito/flutter-action.
  2. Provisions a .env from the optional APP_PEPPER GitHub Actions secret, falling back to .env.example when the secret isn't set. Add a repository secret named APP_PEPPER to bake your team's pepper into CI builds.
  3. Runs flutter analyze (build fails on any analyzer error).
  4. Builds release artifacts and uploads them with 14‑day retention.

To trigger a manual build, use the Run workflow button on the Actions page or workflow_dispatch.

Sensitive values — the .env file

One Cred keeps secrets out of the source tree. The .env at the repo root is loaded at app start by flutter_dotenv and bundled as an asset (declared in pubspec.yaml > flutter > assets). It is git‑ignored, so every developer ships their own personal build.

Variable Type Purpose
APP_PEPPER string Extra secret blended into PBKDF2 alongside the user passphrase. Two installs with different peppers produce non‑interoperable .vault exports. Empty by default.

Generate a strong pepper:

openssl rand -hex 32        # macOS / Linux
# or
[Convert]::ToHexString([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32))   # PowerShell

then put it into .env:

APP_PEPPER=<your-hex-string>

Limitation worth knowing. Because the .env is bundled as an asset, the value lives inside the app bundle and can be extracted from a release build by anyone with access to the binary. The pepper still raises the cost of attacking an exported .vault file (the attacker would need to also obtain the binary), but it is not a substitute for a strong user passphrase. If you want a higher bar, integrate platform‑specific secure storage (Keychain / Keystore / DPAPI) — see the roadmap.

Security model

One Cred makes intentional, honest trade‑offs. Read this before trusting it with your secrets.

  1. At rest, the sensitive columns of the SQLite DB are encrypted. Credentials live in a SQLite file under the platform's per‑app application‑support directory; the secret, username and notes columns are stored as enc:v1:<base64(nonce ‖ ciphertext ‖ tag)> (AES‑GCM‑256). The key is derived from master-password ‖ APP_PEPPER via PBKDF2‑HMAC‑SHA256 (200k iterations) with a persisted 16‑byte random salt; it lives only in memory once the user unlocks with their password. Metadata that's needed for indexing/filtering (name, url, tags) stays in plaintext.

    • Biometric is the primary unlock path: the derived AES key is stashed in the OS keychain (via flutter_secure_storage — Keychain on iOS/macOS, Keystore‑backed EncryptedSharedPreferences on Android, DPAPI on Windows) when the master password is set or verified. A successful biometric prompt loads the key from the keychain into memory, so the user never has to retype the master password on cold start. The master password is the fallback: used the first time after the feature ships (the keychain entry doesn't exist yet) and every time the sensor is unavailable / refuses / the keychain read fails.
    • Setting / changing / clearing the master password triggers a transparent re‑encryption pass over the credentials table (AppDatabase.reEncryptAll / decryptAllToPlain).
    • dumpAll() returns decrypted plaintext, so encrypted export (.vault) and LAN sync continue to work and remain interoperable across devices with different vault keys.
  2. The unlock gate combines two factors (see Master password & biometric flow):

    • Biometric — Face ID / Touch ID / fingerprint / Windows Hello via the OS APIs.
    • Master password — PBKDF2‑HMAC‑SHA256 (200k iterations) over a 16‑byte salt. Only the hash and salt are stored (in SharedPreferences). Constant‑time comparison on verify.
    • When biometric is enabled, the master password is always set up first so the user has a fallback if biometric is removed, denied or fails repeatedly.
  3. Exported .vault files ARE encrypted. Format:

    passphrase ‖ APP_PEPPER  ── PBKDF2-HMAC-SHA256(200 000) ─►  key
    AES-GCM-256(plaintext, key, random-nonce-12B) → ciphertext ‖ tag-16B
    

    Brute‑forcing requires both the passphrase and the build pepper, then 200k PBKDF2 iterations per guess. The integrity tag detects tampering.

  4. Lock on background + idle. When the app is sent to the background (AppLifecycleState.paused) and the gate is enabled, the app re‑locks. In addition, an idle timer (Settings → Auto‑lock on idle) locks the app after a configurable period without pointer / scroll activity. A 5 s tick checks the elapsed time, so in the worst case lock fires up to 5 s after the configured threshold. The idle row is disabled while the biometric lock is off, mirroring the unlock prerequisites.

  5. Clipboard auto‑clear. Copies go through ClipboardService, which schedules a wipe after the configured delay (Settings → Clipboard auto‑clear). The wipe is conditional: it reads the clipboard back and only clears it if the contents still match the value we wrote — anything the user copied afterwards is left alone. Set the delay to Off to disable. The OS may apply its own clipboard policy on top (e.g. iOS's "Cleared by …" toast).

Found something concerning? Please open a private security advisory rather than a public issue.

Master password & biometric flow

Step What happens
1. User flips Biometric lock ON in Settings. If a master password isn't set yet, a dialog asks the user to choose one (8+ chars, confirm). The PBKDF2 hash + salt are stored locally in SharedPreferences.
2. If the device supports biometrics, the OS prompt is shown. A successful authentication enables the lock. Cancelling reverts the toggle.
3. If the device does NOT support biometrics, the lock still turns ON. The lock screen will use the master password only.
4. App is sent to the background or the user taps Lock now. The lock screen overlays everything until unlocked.
5. Lock screen behaviour. Auto‑triggers biometric on mount (when supported). The master password field is always visible — typing it and pressing Unlock verifies it via constant‑time PBKDF2 comparison.
6. Change master password. Available in Settings → Change master password. Requires confirmation with the new password (no need to re‑enter the old one — the biometric/master gate already covered access).

This combination means the user can always get in even when biometrics fail, get reset by the OS, or aren't available on the device at all.

LAN sync

Two devices on the same Wi‑Fi can hand the vault to each other without an exported file or any cloud round‑trip. The wire format reuses the same AES‑GCM + PBKDF2 envelope as the .vault export, but the passphrase is a fresh 6‑digit PIN generated for each session.

How it works

 Device A (send)                        Device B (receive)
 ─────────────────                      ─────────────────
 1. Generates random 6‑digit PIN.
 2. Binds an HTTP server on
    an ephemeral TCP port.
 3. UDP-broadcasts a beacon on
    255.255.255.255:47823 every 2s:
       { app:'one-cred', device, port }
                                        4. Listens on UDP 47823, builds
                                           a live peer list (deviceName,
                                           ip, port).
                                        5. User picks Device A and types
                                           the PIN shown on A's screen.
                                        6. POST http://A:port/sync
                                           body: {"pin": "123456"}
 7. Verifies PIN. On match, dumps
    DB → encrypts with
    VaultCrypto(pin + APP_PEPPER) →
    returns ciphertext (200 OK).
    On mismatch, returns 403 with
    a 1s delay; 5 fails ⇒ 429 + close.
                                        8. Decrypts and merges projects /
                                           envs / credentials into the
                                           local SQLite (INSERT OR
                                           REPLACE by id for projects /
                                           credentials, IGNORE for envs).

Security properties

  • Confidentiality: the payload is AES‑GCM‑256 with a 12‑byte nonce. Key = PBKDF2-HMAC-SHA256(pin || APP_PEPPER, salt, 200 000 iters, 256 bits). Same crypto as .vault files.
  • Authentication: GCM's 16‑byte tag detects tampering and any wrong‑key attempt (no oracle on the receiver side).
  • PIN brute force: 6 digits = 10⁶ guesses. Mitigated by (a) the server rate‑limits with a 1s delay per attempt and disconnects after 5 failures, (b) the session times out after 3 minutes, (c) the payload is also single‑use — after one successful pull, the server shuts down.
  • Same‑LAN scope: discovery uses 255.255.255.255 broadcast, so it doesn't cross subnets or escape the network.
  • No persistence on Device A: the PIN lives only in memory during the session.

Threat model — what it does NOT defend against

  • A device already on your Wi‑Fi can attempt to brute‑force the PIN. With 5 attempts before shutdown that's 5/10⁶ ≈ 0.0005% chance per session, so practically safe, but treat untrusted networks as untrusted.
  • MITM on the LAN is theoretically possible without TLS pinning. The PIN integrity check (GCM tag) means a MITM that doesn't know the PIN can't read or alter the payload — but they could replay the encrypted blob if they sniffed it and later guessed the PIN. Avoid running sync on networks you don't control.

Platforms

  • macOS — entitlements network.client + network.server are pre‑configured. On first run macOS asks to allow incoming connections; click Allow.
  • Android — manifest declares INTERNET, ACCESS_NETWORK_STATE, ACCESS_WIFI_STATE, CHANGE_WIFI_MULTICAST_STATE. Some battery‑saver settings can throttle background UDP — keep the app foregrounded during sync.
  • Windows — works out of the box. Windows Firewall will prompt to allow inbound traffic the first time; accept on a private network.

Usage

Settings → LAN sync:

  1. On the device that already has the credentials, tap Send. A PIN appears.
  2. On the other device, tap Receive. Wait for the first one to show in the list, tap it.
  3. Type the 6‑digit PIN. Transfer happens automatically.

Both sheets close the session as soon as the transfer succeeds (or after 3 minutes of inactivity).

Localization

Strings live in plain JSON under assets/translations/, loaded by easy_localization.

  • en.json — fallback.
  • it.json — Italian.

The app picks the device locale; unknown locales fall back to English. Adding a new language is two steps:

  1. Drop assets/translations/<code>.json (copy en.json and translate).
  2. Add the matching Locale('<code>') to EasyLocalization(supportedLocales: …) in lib/main.dart.

No code generation required — easy_localization reads the JSON at runtime.

When you add new user‑facing strings, add the key to both en.json and it.json. The keys use camelCase, and placeholders are {name} style — call sites use tr(namedArgs: {'name': 'value'}).

Regenerating the app icons

# (one‑time) install Pillow
python3 -m pip install --user Pillow

# regenerate the source 1024×1024 PNGs
python3 assets/icon/generate.py

# fan out into platform‑specific sizes
dart run flutter_launcher_icons

The source icon is fully procedural, so tweaking the gradient / accent dot / font weight only requires editing assets/icon/generate.py.

Troubleshooting

google_fonts was unable to load font … Operation not permitted on macOS. The macOS app sandbox blocks outbound network by default. The entitlement com.apple.security.network.client is already enabled in both macos/Runner/DebugProfile.entitlements and Release.entitlements so google_fonts can fetch Inter / JetBrains Mono on first launch and cache them locally. If you removed it, re‑add it. Fonts only need the network once — subsequent launches read from the on‑disk cache.

App stays locked after enabling biometric on a device that doesn't support it. Toggle Biometric lock OFF from the lock screen via the master password field, or wipe the app's local data. The unlock gate auto‑disables itself if neither biometric nor master password is configured.

flutter analyze complains about missing translations. Add the key to both assets/translations/en.json and it.jsoneasy_localization falls back to English when a key is missing, but the build doesn't fail on it.

Contributing

PRs welcome. Some good first issues:

  • Storing the pepper in Keychain / Keystore / DPAPI instead of the bundled .env.
  • Additional locales (es, fr, de, …) — drop a assets/translations/<code>.json and add the locale to main.dart.
  • iOS / Linux platform shells (the Dart code is already cross‑platform; only the runners are missing).
  • CSV / 1Password / Bitwarden importers.

Style:

  • flutter analyze must be clean (CI fails on errors).
  • Run dart format . before committing.
  • Don't add new user‑facing strings without adding both assets/translations/en.json and assets/translations/it.json entries.
  • Prefer many small, single‑purpose widget files over big monolithic ones — the repo is organised that way (see Project layout).

License

MIT. Do whatever you want with it; just keep the copyright notice and don't blame us if you lose your secrets.

About

A credential manager designed for developers

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages