DEV Community

Cover image for 🔐Current Passkeys Are a Single Point of Failure
Koji Murata
Koji Murata

Posted on

🔐Current Passkeys Are a Single Point of Failure

TL;DR: Cloud-synced passkeys + services skipping 2FA = your cloud account becomes a single point of failure. I built LocalPasskey, a macOS app that stores passkeys in Secure Enclave with no cloud sync.

Passkeys Skip 2FA

Many services—Google, GitHub, and others—skip TOTP and other second-factor verification when you authenticate with a passkey. Passkeys are treated as a strong single factor because they're phishing-resistant and cryptographically secure.

This seems reasonable. But combined with how passkeys are currently implemented, it creates a serious security hole.

Cloud Sync by Default

iCloud Keychain, Google Password Manager, 1Password, Bitwarden—every major passkey implementation is built around cloud synchronization.

There is no option to store passkeys locally.

The Attack Scenario

Consider this:

  1. You enable 2FA on GitHub, Google, and other critical services
  2. You register passkeys for these services, stored in iCloud Keychain
  3. Service providers skip 2FA when you use passkeys
  4. An attacker compromises your Apple account
  5. All your passkeys sync to the attacker's device
  6. The attacker now has access to all your services—bypassing the 2FA you set up

The 2FA you carefully configured becomes useless. Your Apple account is now a single point of failure for everything.

The Root Cause

The WebAuthn spec actually includes BE (Backup Eligible) and BS (Backup State) flags. Service providers can use these to distinguish between cloud-synced and device-bound passkeys. This enables risk-based authentication: require additional verification for synced passkeys, trust device-bound ones.

But two problems prevent this from working:

  1. Service providers ignore the flags: Most services skip 2FA regardless of passkey type
  2. No local-only option exists: Neither Apple nor major password managers offer a way to create device-bound passkeys

Unlike the first problem—which requires service providers to act—the second problem can be solved on the client side.

I Built LocalPasskey

Shameless plug: I built a macOS app that solves this.

LocalPasskey

A macOS passkey manager that stores credentials locally in the Secure Enclave.

screenshot

Motivation

Modern passkey solutions—whether Apple's iCloud Keychain or third-party password managers like 1Password, Bitwarden, and Dashlane—are designed around cloud synchronization. While this provides convenience, it turns your cloud account into a single point of failure for all your passkey-protected services.

The Single Point of Failure Problem

Many service providers skip TOTP or other second-factor verification when users authenticate with passkeys, treating them as a strong single factor. This seems reasonable—passkeys are phishing-resistant and cryptographically secure. However, when passkeys are synced to the cloud, this creates a dangerous single point of failure:

  1. You enable 2FA on important services (GitHub, Google, etc.)
  2. You register passkeys for these services, stored in iCloud Keychain
  3. Service providers skip 2FA when you use passkeys
  4. An attacker compromises your Apple account
  5. All your passkeys sync to the attacker's device
  6. The attacker now has access…

Private keys are stored in the Secure Enclave:

  • Cannot be extracted, even with root access
  • Can only be used for signing after biometric verification
  • Never synced to iCloud

Even if your Apple account is compromised, your passkeys remain safe.

Implementation Details

The key settings are:

  • kSecAttrTokenIDSecureEnclave: Generate the key inside the Secure Enclave
  • kSecAttrAccessibleWhenUnlockedThisDeviceOnly: Only accessible on this device (prevents iCloud sync)
  • .privateKeyUsage, .biometryAny: Require biometric authentication to use the private key

Even if the app has vulnerabilities, even if an attacker gains root access to your machine, the private keys cannot be extracted.

let accessControl = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
    [.privateKeyUsage, .biometryAny],
    nil
)!

let attributes: [String: Any] = [
    kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
    kSecAttrKeySizeInBits as String: 256,
    kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
    kSecPrivateKeyAttrs as String: [
        kSecAttrIsPermanent as String: true,
        kSecAttrAccessControl as String: accessControl,
        kSecAttrAccessGroup as String: accessGroup,
    ],
]

var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
    throw error!.takeRetainedValue()
}
Enter fullscreen mode Exit fullscreen mode

Usage

Installation

Trusting an individual developer to improve your security is questionable at best. I recommend reading the code and building it yourself.

You only need to check here. Verify that keys are stored in a way that prevents extraction.

If you do trust me, you can download the dmg from https://github.com/malt03/local-passkey-manager/releases.

Setup

  1. Open System Settings > General > AutoFill & Passwords
  2. Enable LocalPasskey under "AutoFill from"

Known Bugs (Apple's Fault)

Apple's Credential Provider Extension has bugs that prevent LocalPasskey from reporting accurate credential information. These don't affect security—your private keys are still protected—but the metadata is wrong:

Apple hasn't responded to either issue. If you care about proper passkey implementation on macOS, please boost these threads.

Final Thoughts

This app shouldn't need to exist. If Apple added a "store locally only" option to their Passwords app, that would be the end of it.
If this post gets enough attention and Apple decides to implement this natively, that would be the best outcome.

Further Reading

Top comments (0)