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.
- Screenshots
- Quick start
- Project layout
- Build for each platform
- Continuous integration
- Sensitive values — the
.envfile - Security model
- Master password & biometric flow
- LAN sync
- Localization
- Regenerating the app icons
- Troubleshooting
- Contributing
- License
macOS:
| Home | Credential detail |
|---|---|
![]() |
![]() |
| New credential dialog | Settings sheet |
|---|---|
![]() |
![]() |
Windows / Android screenshots will land in the same
assets/screens/folder withwindows-*.png/android-*.pngprefixes.
Requirements:
- Flutter 3.41.9 (matches the CI matrix and the Dart
^3.11.5pin inpubspec.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 / androidFirst launch tip. The app seeds a
Demo Projectand the default environments (local,dev,staging,qa,test,preprod,prod). Delete the demo and start adding your own projects from the sidebar.
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.
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.
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.
flutter run -d windows
flutter build windows --release
# build\windows\x64\runner\Release\One Cred.exeBiometric on Windows uses Windows Hello when available.
flutter run -d <android-id>
flutter build apk --release
flutter build appbundle --release # Play StoreMainActivity extends FlutterFragmentActivity (required by local_auth). The manifest declares USE_BIOMETRIC and USE_FINGERPRINT.
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:
- Sets up Flutter 3.41.9 (and JDK 17 for the Android job) via
subosito/flutter-action. - Provisions a
.envfrom the optionalAPP_PEPPERGitHub Actions secret, falling back to.env.examplewhen the secret isn't set. Add a repository secret namedAPP_PEPPERto bake your team's pepper into CI builds. - Runs
flutter analyze(build fails on any analyzer error). - 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.
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)) # PowerShellthen put it into .env:
APP_PEPPER=<your-hex-string>
Limitation worth knowing. Because the
.envis 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.vaultfile (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.
One Cred makes intentional, honest trade‑offs. Read this before trusting it with your secrets.
-
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,usernameandnotescolumns are stored asenc:v1:<base64(nonce ‖ ciphertext ‖ tag)>(AES‑GCM‑256). The key is derived frommaster-password ‖ APP_PEPPERvia 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‑backedEncryptedSharedPreferenceson 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.
- Biometric is the primary unlock path: the derived AES key is stashed in the OS keychain (via
-
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.
-
Exported
.vaultfiles ARE encrypted. Format:passphrase ‖ APP_PEPPER ── PBKDF2-HMAC-SHA256(200 000) ─► key AES-GCM-256(plaintext, key, random-nonce-12B) → ciphertext ‖ tag-16BBrute‑forcing requires both the passphrase and the build pepper, then 200k PBKDF2 iterations per guess. The integrity tag detects tampering.
-
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. -
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.
| 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.
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.
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).
- 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.vaultfiles. - 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.255broadcast, so it doesn't cross subnets or escape the network. - No persistence on Device A: the PIN lives only in memory during the session.
- 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.
- macOS — entitlements
network.client+network.serverare 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.
Settings → LAN sync:
- On the device that already has the credentials, tap Send. A PIN appears.
- On the other device, tap Receive. Wait for the first one to show in the list, tap it.
- 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).
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:
- Drop
assets/translations/<code>.json(copyen.jsonand translate). - Add the matching
Locale('<code>')toEasyLocalization(supportedLocales: …)inlib/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'}).
# (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_iconsThe source icon is fully procedural, so tweaking the gradient / accent dot / font weight only requires editing assets/icon/generate.py.
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.json —
easy_localization falls back to English when a key is missing, but the
build doesn't fail on it.
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>.jsonand add the locale tomain.dart. - iOS / Linux platform shells (the Dart code is already cross‑platform; only the runners are missing).
- CSV / 1Password / Bitwarden importers.
Style:
flutter analyzemust be clean (CI fails on errors).- Run
dart format .before committing. - Don't add new user‑facing strings without adding both
assets/translations/en.jsonandassets/translations/it.jsonentries. - Prefer many small, single‑purpose widget files over big monolithic ones — the repo is organised that way (see Project layout).
MIT. Do whatever you want with it; just keep the copyright notice and don't blame us if you lose your secrets.



