<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Koji Murata</title>
    <description>The latest articles on DEV Community by Koji Murata (@malt03).</description>
    <link>https://dev.to/malt03</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3753193%2F735b6235-b042-44cf-84ca-dc7c8b2061e0.png</url>
      <title>DEV Community: Koji Murata</title>
      <link>https://dev.to/malt03</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/malt03"/>
    <language>en</language>
    <item>
      <title>🔐Current Passkeys Are a Single Point of Failure</title>
      <dc:creator>Koji Murata</dc:creator>
      <pubDate>Wed, 04 Feb 2026 14:53:25 +0000</pubDate>
      <link>https://dev.to/malt03/current-passkeys-are-a-single-point-of-failure-370i</link>
      <guid>https://dev.to/malt03/current-passkeys-are-a-single-point-of-failure-370i</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Cloud-synced passkeys + services skipping 2FA = your cloud account becomes a single point of failure. I built &lt;a href="https://github.com/malt03/local-passkey-manager" rel="noopener noreferrer"&gt;LocalPasskey&lt;/a&gt;, a macOS app that stores passkeys in Secure Enclave with no cloud sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passkeys Skip 2FA
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This seems reasonable. But combined with how passkeys are currently implemented, it creates a serious security hole.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud Sync by Default
&lt;/h2&gt;

&lt;p&gt;iCloud Keychain, Google Password Manager, 1Password, Bitwarden—every major passkey implementation is built around cloud synchronization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no option to store passkeys locally.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Attack Scenario
&lt;/h2&gt;

&lt;p&gt;Consider this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You enable 2FA on GitHub, Google, and other critical services&lt;/li&gt;
&lt;li&gt;You register passkeys for these services, stored in iCloud Keychain&lt;/li&gt;
&lt;li&gt;Service providers skip 2FA when you use passkeys&lt;/li&gt;
&lt;li&gt;An attacker compromises your Apple account&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All your passkeys sync to the attacker's device&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The attacker now has access to all your services—bypassing the 2FA you set up&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The 2FA you carefully configured becomes useless. Your Apple account is now a single point of failure for everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;But two problems prevent this from working:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Service providers ignore the flags&lt;/strong&gt;: Most services skip 2FA regardless of passkey type&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No local-only option exists&lt;/strong&gt;: Neither Apple nor major password managers offer a way to create device-bound passkeys&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unlike the first problem—which requires service providers to act—the second problem can be solved on the client side.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Built LocalPasskey
&lt;/h2&gt;

&lt;p&gt;Shameless plug: I built a macOS app that solves this.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/malt03" rel="noopener noreferrer"&gt;
        malt03
      &lt;/a&gt; / &lt;a href="https://github.com/malt03/local-passkey-manager" rel="noopener noreferrer"&gt;
        local-passkey-manager
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LocalPasskey&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A macOS passkey manager that stores credentials locally in the Secure Enclave.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/malt03/local-passkey-manager/blob/main/readme/screenshot.png?raw=true"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fmalt03%2Flocal-passkey-manager%2Fraw%2Fmain%2Freadme%2Fscreenshot.png%3Fraw%3Dtrue" alt="screenshot"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Motivation&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;The Single Point of Failure Problem&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You enable 2FA on important services (GitHub, Google, etc.)&lt;/li&gt;
&lt;li&gt;You register passkeys for these services, stored in iCloud Keychain&lt;/li&gt;
&lt;li&gt;Service providers skip 2FA when you use passkeys&lt;/li&gt;
&lt;li&gt;An attacker compromises your Apple account&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All your passkeys sync to the attacker's device&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The attacker now has access…&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/malt03/local-passkey-manager" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;Private keys are stored in the &lt;a href="https://en.wikipedia.org/wiki/Security_and_privacy_of_iOS#Secure_Enclave" rel="noopener noreferrer"&gt;Secure Enclave&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cannot be extracted, even with root access&lt;/li&gt;
&lt;li&gt;Can only be used for signing after biometric verification&lt;/li&gt;
&lt;li&gt;Never synced to iCloud&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if your Apple account is compromised, your passkeys remain safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;p&gt;The key settings are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kSecAttrTokenIDSecureEnclave&lt;/code&gt;: Generate the key inside the Secure Enclave&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kSecAttrAccessibleWhenUnlockedThisDeviceOnly&lt;/code&gt;: Only accessible on this device (prevents iCloud sync)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.privateKeyUsage, .biometryAny&lt;/code&gt;: Require biometric authentication to use the private key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if the app has vulnerabilities, even if an attacker gains root access to your machine, the private keys cannot be extracted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;accessControl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SecAccessControlCreateWithFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kSecAttrAccessibleWhenUnlockedThisDeviceOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;privateKeyUsage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;biometryAny&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;kSecAttrKeyType&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kSecAttrKeyTypeECSECPrimeRandom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kSecAttrKeySizeInBits&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kSecAttrTokenID&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kSecAttrTokenIDSecureEnclave&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kSecPrivateKeyAttrs&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;kSecAttrIsPermanent&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;kSecAttrAccessControl&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;accessControl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;kSecAttrAccessGroup&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;accessGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Unmanaged&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;CFError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;privateKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SecKeyCreateRandomKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFDictionary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="nf"&gt;takeRetainedValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;Trusting an individual developer to improve your security is questionable at best. I recommend reading the code and building it yourself.&lt;/p&gt;

&lt;p&gt;You only need to check &lt;a href="https://github.com/malt03/local-passkey-manager/blob/v1.0.0/CredentialProvider/Sources/Registration.swift#L70-L97" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Verify that keys are stored in a way that prevents extraction.&lt;/p&gt;

&lt;p&gt;If you do trust me, you can download the dmg from &lt;a href="https://github.com/malt03/local-passkey-manager/releases" rel="noopener noreferrer"&gt;https://github.com/malt03/local-passkey-manager/releases&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;System Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;General&lt;/strong&gt; &amp;gt; &lt;strong&gt;AutoFill &amp;amp; Passwords&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enable LocalPasskey under "AutoFill from"&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Known Bugs (Apple's Fault)
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BE/BS flags are forced to 1: Relying parties can't distinguish device-bound from cloud-synced credentials
(&lt;a href="https://developer.apple.com/forums/thread/813844" rel="noopener noreferrer"&gt;https://developer.apple.com/forums/thread/813844&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;AAGUID is overwritten to zeros: Relying parties can't identify which passkey manager created a credential
(&lt;a href="https://developer.apple.com/forums/thread/814547" rel="noopener noreferrer"&gt;https://developer.apple.com/forums/thread/814547&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apple hasn't responded to either issue. If you care about proper passkey implementation on macOS, please boost these threads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
If this post gets enough attention and Apple decides to implement this natively, that would be the best outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/html/2501.07380v1" rel="noopener noreferrer"&gt;Device-Device-Bound vs. Synced Credentials: A Comparative Evaluation of Passkey Authentication&lt;/a&gt; - University of Oslo&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.microsoft.com/en-us/research/video/detecting-compromise-of-passkey-storage-on-the-cloud/" rel="noopener noreferrer"&gt;Detecting Compromise of Passkey Storage on the Cloud&lt;/a&gt; - Microsoft Research&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://yourpasskeyisweak.com/" rel="noopener noreferrer"&gt;Your (Synced) Passkey is Weak&lt;/a&gt; - DEFCON 33&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thehackernews.com/2025/10/how-attackers-bypass-synced-passkeys.html" rel="noopener noreferrer"&gt;How Attackers Bypass Synced Passkeys&lt;/a&gt; - The Hacker News&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>passkey</category>
      <category>webauthn</category>
      <category>swift</category>
    </item>
  </channel>
</rss>
