Liberating an always-on core from all my iCloud devices
How migrating from 1Password to iCloud Keychain triggered a bug that spread to all my Apple devices and how I fixed it.
# The setup: Migrating 1Password to iCloud Keychain
I have been a loving 1Password users for many years, appreciating the UX/UI, its system-wide integration and its ease of use. Unfortunately, its latest iterations donât match my needs and disagree with my trust model. Syncing my encrypted vault to a third party doesnât sound like a good idea, and Iâd rather not have to deal with another Electron desktop app.
I stuck with 1Password 7 for as long as I could. But I knew its time was coming. Version 8 (the one shipping cloud sync through a subscription model) had been out for two years already. And version 7 started feeling like being the last one at a party and being gently asked to leave: it didnât get any new feature (e.g., passkey), and I worried that at some point it would reach end of life and stop getting security patches.
So, on a winter morning I decided to migrate to iCloud Keychain for most of the passwords and a self-hosted Bitwarden instance for a few more complex items1. This story focuses on iCloud Keychain.
I used 1Passwordâs export function to get a CSV of about 1000 items compatible
with iCloud Keychain import. I stored the file in ramfs
to avoid forgetting
them on the file-system and worked within a temporarily air-gapped system. I
then went forward and imported the CSV in Keychain. The migration went ok and I
could see the passwords in macOSâ built-in password manager.
Browsing felt great! I could log into websites without having to juggle between multiple overlapping autocompletion pop-ups (1Passwordâs, iCloud Keychainâs). So far so good, less painful than I expected.
Happy with the result, I disabled 1Passwordâs browser extension and decided to keep the application around just in case I had to look up the odd items that did not correctly migrate2.
# Troubles: cloud-synced performance cores
Pardon the bad screenshot quality, didnât have enough CPU to make it better.
The silver lining did not last long and troubles began right away. I noticed one
of the M1 Mac performance cores running 100% the secd
process â a
âcentralized keychain agentâ according to man secd(8)
. kill
-ing the process
temporarily alleviated the problem, only until it would automatically restart.
Both the Macâs battery-life and performance suffered from it3.
Like bad news do, troubles also spread fast. In my case, to all other my iCloud devices.
Opening the âPasswordâ screen on an iPhone or a (dated) iPad started feeling like navigating through a sea of jelly, where letters typed into the search bar would filter results slower than your average LLM output. Looking up websites credentials in the âPasswordâ screen became a frustrating experience. Even Safari scrolling suffered from this, with the screen moving about 1 third of the vertical space and then just⊠stopping. Oh, donât even mention battery life. Or whether the Watch was affected (probably).
I tried ignoring the problem for a few weeks, hoping it would go away. It did not, so I set my mind up to solve it.
I started with my usual debugging playbook. Google suggested deleting the local Keychain store, but I suspected this wouldnât work as my issue spread across devices4. Next, I tried to take a look at the Keychain itself, but I couldnât, because the âKeychain Accessâ app on the Mac froze on start and did not allow navigating.
đŠ Insert obligatory âWe need to go deeperâ meme, minus copyright trouble and/or AI-generated art.
Next I decided to look at Console logs5, which dutifully gathered 9K lines of
secd
logs over 19 seconds (about 500 lines per second):
» cat secd.logs | grep secd | head -n1
default 09:18:18.073714+0200 SecWarning com.apple.securityd secd 81301 0xd6a2fa
SecDbKeychainItemV7: item's secret data exceeds reasonable size (468128 bytes) (com.apple.security.sos)
» cat secd.logs | grep secd | tail -n1
default 09:18:37.972543+0200 signpost com.apple.security.signposts secd 81301 0xd6be63
END [34492] 0.352725s: SOSCCProcessSyncWithPeers SOSSignpostNameSOSCCProcessSyncWithPeers=__##__signpost.telemetry#____#number1#_##_#1##__##
» cat secd.logs | grep secd | wc -l
9243
# Going in circles: unreasonable items
The very first line caught my eyes. Some item is clearly being unreasonable and maybe that was my clue?
Apple open-sources some of its components, including the Security framework. The line producing that output is here. The source code does not help much here, but it tells us that reasonable size is 32KiB.
Is there a way to find the offending items? Keychain Access was not working, so
I ruled it out. But inspecting secd
through âActivity Monitorâ shows a list
of open files:
# ... snip
/Users/aldur/Library/Keychains/07BF5E04-DAB8-58C9-A0EC-BEB8B068B906/keychain-2.db
/Users/aldur/Library/Keychains/07BF5E04-DAB8-58C9-A0EC-BEB8B068B906/keychain-2.db-shm
/Users/aldur/Library/Keychains/07BF5E04-DAB8-58C9-A0EC-BEB8B068B906/keychain-2.db-wal
# ... snip
/usr/libexec/secd
This .db
file looks promising and its .db-wal
friends makes it likely to be
an .sqlite
database. Nice, we can work with that!
» file /Users/aldur/Library/Keychains/07BF5E04-DAB8-58C9-A0EC-BEB8B068B906/keychain-2.db
/Users/aldur/Library/Keychains/07BF5E04-DAB8-58C9-A0EC-BEB8B068B906/keychain-2.db:
SQLite 3.x database, last written using SQLite version 3043002, writer version
2, read version 2, file counter 12070, database pages 12203, 1st free page
11827, free pages 1818, cookie 0x34e, schema 4, largest root page 187, UTF-8,
vacuum mode 1, version-valid-for 12070
Opening the database revealed a set of tables and columns with short,
uninformative names. But the source code and the logs told us the âreasonableâ
size and the unreasonable one â starting from there, I hacked together a quick
Python script to enumerate all entries (in all rows in all tables in the .db
)
and sort them by descending size:
Table | Row | Column | Size (bytes) |
---|---|---|---|
genp | 5 | data | 507933 |
genp | 76 | data | 470066 |
genp | 77 | data | 190289 |
genp | 418 | data | 138309 |
genp | 903 | data | 31318 |
genp | 436 | data | 31301 |
ckmirror | 2502 | ckrecord | 15920 |
ckmirror | 119 | ckrecord | 14056 |
ckmirror | 2451 | ckrecord | 12212 |
ckmirror | 605 | ckrecord | 11600 |
Our results show that row number 5 of table genp
, column data
weights 507933
bytes (half a megabyte). Whatâs in that table? First, the schema:
sqlite3 /Users/aldur/Library/Keychains/07BF5E04-DAB8-58C9-A0EC-BEB8B068B906/keychain-2.db
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> .schema genp
CREATE TABLE genp (
rowid INTEGER PRIMARY KEY AUTOINCREMENT,
cdat REAL,
mdat REAL,
desc BLOB,
icmt BLOB,
crtr INTEGER,
type INTEGER,
scrp INTEGER,
labl BLOB,
alis BLOB,
invi INTEGER,
nega INTEGER,
cusi INTEGER,
prot BLOB,
acct BLOB NOT NULL DEFAULT '',
svce BLOB NOT NULL DEFAULT '',
gena BLOB,
data BLOB,
agrp TEXT NOT NULL,
pdmn TEXT,
sync INTEGER NOT NULL DEFAULT 0,
tomb INTEGER NOT NULL DEFAULT 0,
sha1 BLOB,
vwht TEXT,
tkid TEXT,
musr BLOB NOT NULL,
UUID TEXT,
sysb INTEGER DEFAULT 0,
pcss INTEGER,
pcsk BLOB,
pcsi BLOB,
persistref BLOB NOT NULL,
clip INTEGER NOT NULL DEFAULT 0,
ggrp TEXT,
UNIQUE (acct, svce, agrp, sync, vwht, tkid, musr, ggrp)
);
As I said, not very informative names.
Luckily, there are a few details about each column
here. If we
select the protection domain pdmn
and the Keychain access group agrp
we can
find out how these keys passwords are used:
sqlite> SELECT rowid, agrp, pdmn, length(data)
FROM genp
ORDER BY length(data)
DESC LIMIT 5
;
6|com.apple.security.sos|dku|507933
122|com.apple.security.sos|dk|470066
123|com.apple.security.sos|dk|190288
465|InternetAccounts|dku|138009
22034|InternetAccounts|cku|31327
Wait, something does not look right. Sure, those are big items. But they
belong to Appleâs own security.sos
Secure Object Sync according to
this6, and itâs
unlikely my migration messed with them: Appleâs Secure keychain
syncing
creates âcirclesâ of devices which share their cryptographic identities and then
use them to circulate the encrypted keychain material. But our first offender
from the table above is marked as dku
(kSecAttrAccessibleAlwaysThisDeviceOnly
), which means that particular key
shall never leave the device. Confirmed then, this is not what is causing our
troubles.
Time to go back at digging through the logs.
# The murder weapon
After painstakingly searching through the lines, I saw this:
default 09:18:18.983936+0200 item com.apple.securityd secd 81301 0xd6bd90
insert failed for item <private> with Error Domain=com.apple.utilities.sqlite3
Code=19 "finalize: 0x138026f10: [19->2067] UNIQUE constraint failed: inet.acct,
inet.sdmn, inet.srvr, inet.ptcl, inet.atyp, inet.port, inet.path, inet.agrp,
inet.sync, inet.vwht, inet.tkid, inet.musr, inet.ggrp"
UserInfo={numberOfErrorsDeep=1, NSDescription=finalize: 0x138026f10: [19->2067]
UNIQUE constraint failed: inet.acct, inet.sdmn, inet.srvr, inet.ptcl, inet.atyp,
inet.port, inet.path, inet.agrp, inet.sync, inet.vwht, inet.tkid, inet.musr,
inet.ggrp, NSUnderlyingError=0x140a85c00 {Error
Domain=com.apple.utilities.sqlite3 Code=19 "step: [19->2067] UNIQUE constraint
failed: inet.acct, inet.sdmn, inet.srvr, inet.ptcl, inet.atyp, inet.port,
inet.path, inet.agrp, inet.sync, inet.vwht, inet.tkid, inet.musr, inet.ggrp"
UserInfo={numberOfErrorsDeep=0, NSDescription=step: [19->2067] UNIQUE constraint
failed: inet.acct, inet.sdmn, inet.srvr, inet.ptcl, inet.atyp, inet.port,
inet.path, inet.agrp, inet.sync, inet.vwht, inet.tkid, inet.musr, inet.ggrp}}}
Note that it is marked at default
level, despite being an error. Thatâs why I
couldnât find this when filtering by log level.
Now we are onto something! The next lines look interesting too:
default 09:18:18.984428+0200 SecError com.apple.securityd secd 81301 0xd6bd90
Safari[97852]/1#138 LF=0 add Error Domain=NSOSStatusErrorDomain Code=-25299 "duplicate item O,inet,C8A04B48,S,ak,com.apple.password-manager,0,desc,type,labl,acct,sdmn,srvr,ptcl,atyp,port,path,v_Data,musr,20240510071818.983305Z,CE2942AF" (errKCDuplicateItem / errSecDuplicateItem: / The item already exists.) UserInfo={numberOfErrorsDeep=0, NSDescription=duplicate item O,inet,C8A04B48,S,ak,com.apple.password-manager,0,desc,type,labl,acct,sdmn,srvr,ptcl,atyp,port,path,v_Data,musr,20240510071818.983305Z,CE2942AF}
default 09:18:18.986063+0200 item com.apple.securityd secd 81301 0xd6bd90
replaced <private> in <SecDbConnection rw open>
default 09:18:18.986093+0200 item com.apple.securityd secd 81301 0xd6bd90
with <private> in <SecDbConnection rw open>
Overall, this pattern of lines repeats 19 times in the log. Hereâs what I think was happening: Keychain was struggling to reconcile two somehow conflicting items, trying to write them, failing, and repeating the process in a busy loop. All the time, across all iCloud-synced devices.
So what is causing it?
# The unmasking
Yes, itâs a Scooby-Doo reference. đ¶
I had previously seen <private>
log
entries
before, being part of Appleâs unified logging. Since macOS Catalina, we can use
a configuration
profile
to show the clear-text entries. After
finding and
applying an appropriate .mobileconfig
, I collected a new batch of logs and
could finally find out the problem: somehow, github.com
had two conflicting
entries that Keychain could not reconciliate.
This did not fix the problem yet, but I was definitely closer. How to fix it,
though? The security
command line tool does not work for iCloud Keychain
items; the Keychain Access app was freezing on me. I didnât know of any other
way to remove those entries from the Keychain and I was afraid of modifying the
sqlite
DB by hand, possibly creating more havoc.
After a while, I noticed that Keychain Access was not exactly freezing, but it was constantly re-setting its state to a âstartâ point: Thatâs where it âclickedâ: at every iteration of the loop above, at every integrity error, the app was re-setting. Maybe if I could stop the loop for a moment, I would be able to search for the entries and delete them?
After some frustrating trial and error I discovered that disconnecting from the
internet, killing secd
, closing all apps (specifically, Safari was on my
suspects list), I could barely use the Keychain Access app.
The moment I deleted the entries, it all went quiet. The change also immediately propagated to all devices. I could again search for passwords and fill log-in information without a hitch.
# The aftermath
With the mystery solved and the problem finally fixed, I was feeling pretty satisfied.
But I also wondered: how could someone with less patience or technical inclinations solve this? Our problem was particularly nasty because it spread across all user devices, was hard to debug on mobile (lacking a clear way to inspect background tasks), and took a toll on both battery life and performance. Nothing deal-breaking, but a turn for the worse in digital quality of life.
As an industry, what can we do better to prevent this or to improve its remediation? As always, there are trade-offs involved. In my case, the migration from a different password manager caused the bug: something that a tiny minority of users will ever do. And an even small fraction will possibly trigger the same bug. The trade-off might simply be to let this be, as it is counter-economical to fix its root cause.
Even if that is the case, I still believe it is important to let users repair their devices (both on the software and the hardware side). The iCloud Keychain (and more in general the full iCloud suite) lacks proper tooling and transparency. We should strive to provide good diagnostic, clear run-books, and ensure they are easily discoverable (as opposed to all useless search results I found, that will only get worse with slop).
This post doesnât have the presumption to do any of that. But at least it documents the issue and show how I solved it. In case you encounter the same trouble, I hope your online search will bring you here and show you the solution.
Thank you for reading and until next time! đ
# Footnotes
-
I didnât manage to escape the Electron hell, since Bitwardenâs macOS client isnât native either. ↩
-
The migration requires entries to have a
URL
assigned to be imported in Keychain. I manually migrated all other entries types (e.g. credit cards, passports, etc.). ↩ -
I still find it pretty impressive that the M2 would manage to run for a few hours with a core running full-steam all the time. ↩
-
Also, I would not even know how to delete it on an iPhone. ↩
-
I have lightly edited terminal output to display it on multiple lines (e.g., one for the logs metadata, one for the content) to avoid long horizontal scrolling, especially painful on mobile devices. ↩
-
Different from the more recent addition to iPhones enabling satellite SOS calls on emergencies. ↩