feat(mac): add Touch ID biometric login#555
Conversation
Password encrypted via safeStorage into macOS Keychain. Biometric prompt gates decryption — renderer cannot decrypt without passing Touch ID challenge, nor bypass the gate through localStorage alone.
|
@torkinos @hakaari @giorgikh93 Can anyone take a look please? I'd really appreciate to have this feature at the release |
|
@andrey-dru-me1 Thanks for this PR, the UX work is solid, especially the focus/cancel handling in the unlock card and the test coverage. But one thing should be fixed before merging: the Touch ID check does not really protect the master password. It only looks like it does. encrypts the master password with safeStorage, What I tested the encrypted text from ~/Library/Application Support/PearPass/Local Storage/leveldb/ To be exact about how hard this is: No prompt at all: any code running inside the PearPass process (for example a bad npm dependency) can read the key silently. Note: this PR keeps disable-library-validation in the entitlements, which makes it easier to load untrusted libraries into the process - exactly this case. Why this still matters Suggested fix Biometric keychain item: SecAccessControlCreateWithFlags(kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .biometryCurrentSet, …), or Thanks again for your work |
|
@giorgikh93 thanks for the detailed feedback, I really appreciate that. I'll come back with some fixes soon |
Replace the insecure approach of keeping an encrypted password in localStorage with native keychain storage of verification credentials (ciphertext, nonce, salt, hashedPassword). - Add native macOS keychain module (Objective-C) for credential storage - Add storeBiometricCredentials / retrieveBiometricCredentials / deleteBiometricCredentials electronAPI methods - Extract EnableTouchIdDialog from AppPreferencesContent into its own component using ModalContext - Re-store biometric credentials on master password change - Rewrite CardUnlockPearPass biometric login to use native retrieval - Remove safeStorage encryptString/unlockWithPassword codepath - Add module augmentation for getMasterEncryption and runActionScan Closes tetherto#555
Replace the insecure approach of keeping an encrypted password in localStorage with native keychain storage of verification credentials (ciphertext, nonce, salt, hashedPassword). - Add native macOS keychain module (Objective-C) for credential storage - Add storeBiometricCredentials / retrieveBiometricCredentials / deleteBiometricCredentials electronAPI methods - Extract EnableTouchIdDialog from AppPreferencesContent into its own component using ModalContext - Re-store biometric credentials on master password change - Rewrite CardUnlockPearPass biometric login to use native retrieval - Remove safeStorage encryptString/unlockWithPassword codepath - Add module augmentation for getMasterEncryption and runActionScan Closes tetherto#555
e052fce to
48ac308
Compare
Replace the insecure approach of keeping an encrypted password in localStorage with native keychain storage of verification credentials (ciphertext, nonce, salt, hashedPassword). - Add native macOS keychain module (Objective-C) for credential storage - Add storeBiometricCredentials / retrieveBiometricCredentials / deleteBiometricCredentials electronAPI methods - Extract EnableTouchIdDialog from AppPreferencesContent into its own component using ModalContext - Re-store biometric credentials on master password change - Rewrite CardUnlockPearPass biometric login to use native retrieval - Remove safeStorage encryptString/unlockWithPassword codepath - Add module augmentation for getMasterEncryption and runActionScan Closes tetherto#502
48ac308 to
5fa9127
Compare
|
I rewrote the storage layer. Instead of encrypting with What gets stored is the Argon2id-derived vault credentials ( Other things:
Also hit a bug during testing: if biometric creds were stale, PTAL. |
|
@giorgikh93 can you review the changes made please? |


Closes #502
Password encrypted via safeStorage into macOS Keychain. Biometric prompt gates decryption — renderer cannot decrypt without passing Touch ID challenge, nor bypass the gate through localStorage alone.
Requirements
Changes
electron/main.cjs): Added 4 IPC handlers:biometric:isAvailable— checkssystemPreferences.canPromptTouchID()biometric:promptTouchID— standalone Touch ID promptbiometric:encryptString— encrypts a string viasafeStorage(macOS Keychain), returns base64biometric:unlockWithPassword— atomic Touch ID prompt followed bysafeStoragedecryption; renderer sends the encrypted blob, main process returns decrypted password only if biometric scan succeedselectron/preload.cjs): Exposed 4 new electronAPI methods to renderersrc/types/electron.d.ts): ExtendedElectronAPIinterface with biometric methodsCardUnlockPearPass): Auto-triggers Touch ID on mount and window focus, with"Unlock with Touch ID"fallback link. Uses refs to avoid stale closures in async handlers. Migrated deprecated prop names (onChangeText→onChange,placeholderText→placeholder,errorMessage/variant→error)AppPreferencesContent): Touch ID toggle with password-verification dialog. User enters password to confirm; password is never stored — only encrypted viasafeStorageand persisted to localStorage as a ciphertext blob the renderer can't decryptBIOMETRIC_LOGIN_ENABLEDandBIOMETRIC_ENCRYPTED_PASSWORDkeysbuild-assets/entitlements.mac.plistwith Keychain access group and hardened runtime allowances; wired entitlements intoelectron-builder.mac.jsonTesting Notes
Things reviewers should pay attention to
biometricInFlightRefguards against concurrent biometric promptst,navigate,currentPagestored in refs to avoid stale captures in thetryBiometricLogincallbackdocument.hasFocus()), plus focus event listener for dock-icon clicksmain.cjslogs distinguish "user cancelled" vs "device unavailable" vs "decryption failed" for diagnosticsCardUnlockPearPassnow uses modern kit props (onChange,placeholder,error) — verify no regressions on the password fieldDependencies
systemPreferences.promptTouchID()false/ throw early; UI toggle is hidden