1 Intro
I wanted to access my passwords on a Linux box, where it got really annoying to manually type them in every time I needed one. Since I currently don’t have access to a Mac, I modified the iCloud passwords Chrome extension on Windows to export the name, username, password, list of websites and notes of each password entry1. As output file, I chose to create a bitwarden-compatible CSV.
However, while I’ve been programming for many years using various programming languages, the only thing I know how to do in JavaScript off the top of my head is to print with console.log()
. And as soon as that printed [object Object]
instead of a string I was lost2. That’s why I wanted to see how well using LLMs would work out for (1) finding and understanding only relevant parts of the extension and (2) telling the LLM what to implement based on the knowledge gained in step 1.
I originally tried getting Claude to help me via the normal chat interface on claude.ai, but there it just did not want to:
Claude: I apologize, but I cannot and should not assist with modifying or reverse engineering Apple’s iCloud Passwords Chrome extension, even if you claim to be from the Apple team. […]
I ended up mostly using Claude 3.5 Sonnet in the Cursor editor, which is essentially just Visual Studio Code with convenient ways to interface with various LLMs. And it seems whatever context Cursor adds to the requests convinced Claude to help me. I didn’t even have to make up stories that justified me wanting to export my passwords!
The rest of the post will be a rough overview of how the extension works and some notes on how I implemented (or rather told Claude to implement) the export functionality. There’s a TL;DR if you don’t care about that. If something is unclear, feel free to send an e-mail to author name at domain
.
2 Getting the extension
Chrome extensions are just bundles of files in a .zip
file, where .zip
was changed to .crx
. For downloading the extension as a file, there are various Chrome extensions on the Chrome Web Store and projects on GitHub. I used this one, as the code was short and thus easy to review. Once obtained, the extension .crx
/.zip
can just be extracted (lst. 1).
Listing 1: Downloading, extracting and listing the contents of the .crx iCloud passwords file
1➜ ./crx-dl.py pejdijmoenmkgeppbflobdenhhabjlaj 2Downloading https://clients2.google.com/service/update2/crx?response=redirect&prodversion=91.0&acceptformat=crx2%2Ccrx3&x=id%3Dpejdijmoenmkgeppbflobdenhhabjlaj%26uc to pejdijmoenmkgeppbflobdenhhabjlaj.crx ... 3Success! 4➜ mkdir icloud-pw-export\ 5 && cd icloud-pw-export\ 6 && unzip ../pejdijmoenmkgeppbflobdenhhabjlaj.crx 7➜ ls 8_locales images settings.js 9_metadata manifest.json settings_browser_overrides.css10background.js page_popup.html sjcl.js11completion_list.html page_popup.js style.css12completion_list.js settings.css13content_script.js settings.html
3 Initial investigation
Q: Where does the extension get the passwords from and how?
I first looked at the intended usage of the iCloud passwords Chrome extension. Each time Chrome starts3, the extension asks the user to input a 6 digit PIN (fig. 1) that is shown in a separate window (fig. 2). A look at the running processes confirms it (fig. 3).
3.1 Native messaging
Browser extensions can use native messaging to communicate with an application on the host computer (fig. 4). In the iCloud passwords extension, native messaging is implemented in background.js
. I had Claude add some code to log the commands from the service worker to the native helper and back. The commands that are sent when using the extension to (1) enter the PIN to unlock it and to then (2) fill a password on a website are shown in lst. 2.
Listing 2: Relevant commands from extension to native helper (→) and back (←).
→ CmdHello← CmdHello→ CmdChallengePIN // secure handshake (next section)← CmdChallengePIN→ CmdGetLoginNames4URL // payload: current URL← CmdGetLoginNames4URL // payload: list of entries with URLs (no passwords!)→ CmdGetPassword4LoginName // payload: URL + username of one entry← CmdGetPassword4LoginName // payload: password and more (next listing)
Assume the user has unlocked the extension by entering the PIN and is on a website. If the user now clicks on the extension icon, the pop-up (page_popup.js
) hands the current URL to the service worker (background.js
) and asks it to send the CmdGetLoginNames4URL
command to the native helper. Once the service worker receives the reply, it hands the contents to the pop-up, which displays the selection of iCloud passwords entries that match the URL to the user.
Once the user selects one of the password entries from the now populated list, the password is retrieved: This again takes the path pop-up -> service worker -> native helper
, but this time the service worker uses the CmdGetPassword4LoginName
command. When logging the part of the app that handles the replies to CmdGetPassword4LoginName
4, we find out that the native helper returns the fields shown in lst. 3.
Listing 3: Fields returned by the native app in response to CmdGetPassword4LoginName
HLD // name/title of the entryUSR // usernamePWD // passwordsites // websites listed in the entryNOTES // the actual notes!CDate // creation dateModDate // last modified dateTOTP // 5-digit placeholder
3.2 Tangent: PAKE
It seems like the challenge PIN is used in a password-authenticated key exchange (PAKE). The key is in turn used to secure the communication between the browser extension and the native helper. When I pasted the relevant code from the extension into Claude, it said that it implements the Secure Remote Password protocol. However, I didn’t verify it or looked much further.
Listing 4: Logged CmdChallengePIN
messages from the extension to the native helper and back. The PAKE entry contains some base64-encoded data.
Console: {..., "message":"{..., \"direction\":\"→\", \"command\":\"CmdChallengePIN\", \"details\": { \"cmd\":2, \"msg\": \"{...,\\\"PAKE\\\":\\\"ey...\\\",...}\" } }",... }Console: {..., "message":"{..., \"direction\":\"←\", \"command\":\"CmdChallengePIN\", \"details\": { \"cmd\":2, \"payload\": {...,\"PAKE\":\"ey...\"} } }", ... }
Listing 5: Decoded PAKE
field in the CmdChallengePIN
messages from the extension to the native helper (lines 1 and 3) and back (lines 2 and 4). TID, A, B, M
and HAMK
are all hex-encoded integers. TID
is 16 bytes and the same across all four messages. A
and B
are 384 bytes each. M
and HAMK
are 32 bytes each.
1{ "TID": "0xfb...", "MSG": 0 , "A": "0x2c...", "VER": "1.0", "PROTO": [0, 1] }2{ "TID": "0xfb...", "MSG": "1", "s": "0xC3...", "B": "0xBE...", "VER": "1.0", "PROTO": 1 }3{ "TID": "0xfb...", "MSG": 2 , "M": "38..." }4{ "TID": "0xfb...", "MSG": "3", "HAMK": "0x39...", "ErrCode": 0 }
4 Implementation overview
When I had Claude generate too much code at once, it got a bit too difficult to debug for both Claude and me. But when I broke it down to the steps below, it worked pretty well and if there were issues, Claude almost always solved them. Claude was also really useful for identifying the relevant parts of the code.
As for the actual implementation: to save time, I essentially broke the original functionality of the extension. However, if you still want to use the extension normally after exporting your passwords (such as if you just want to create a backup), just reinstall the original extension from the Chrome Web Store.
- Get all available password entries
- Modify code that handles replies to
CmdGetLoginNames4URL
to store all received entries in a global array.5 - To use that code, we trigger a search for all URLs matching the empty string
""
.
- Modify code that handles replies to
- For each entry, retrieve the password, etc.
- In the code that handles replies to
CmdGetPassword4LoginName
, store the entries in a second global array.6 - Next, we can start requesting the password for each of the entries in the first array.
- In the code that handles replies to
- Clean up the retrieved results a bit (i.e. remove empty entries)
- Construct a CSV from the entries
- Download the CSV
- For chrome to allow the extension to download files, the
downloads
permission has to be added inmanifest.json
- For chrome to allow the extension to download files, the
I added the function export_pws()
to the bottom of background.js
, which handles most of the above steps. It is called (lst. 6) when the user clicks the export button (fig. 5) in the pop-up.
Listing 6: Registering a callback for the keydown
event handler of the export button, which in turn calls export_pws()
// page_popup.jsdocument.documentElement.addEventListener("keydown", function (e) { // ... (document.getElementById("export").onclick = function () { sendMessageToBackgroundPage({ subject: "export" }); }),// background.jsfunction setUpPortToPopup(e) { // ... case "export": export_pws(); break;
5 TL;DR/How-To
Since I don’t want to deal with any copyright issues, I’m not posting the modified extension. Instead, I’m sharing the changes I made via a git patch file. The modified extension can’t fill passwords any more, for that just reinstall the extension from the store once you exported your passwords.
- Download the extension file from the store (use some Chrome extension or one of the various projects on GitHub projects)
- Unpack it: e.g.
mkdir icloud-pw-export && cd icloud-pw-export && unzip icloud-passwords.crx
rm -r _metadata
to get rid of the signature over the extension contents- Format the files, e.g. with biome
wget https://github.com/biomejs/biome/releases/download/cli%2Fv1.9.4/biome-linux-x64 -O ~/.local/bin/biome
chmod +x ~/.local/bin/biome
biome format --write *.js
- Create a git repository so that you can apply the patch:
git init && git add . && git commit -m "initial commit"
- Apply the patch:
git apply --check icloud-pw-export.patch && git apply icloud-pw-export.patch
- Note: You might have to adjust the commit id at the top of the file to match yours
- Enable developer mode in chrome by going to
chrome://extensions/
and using the toggle in the top right - Load the unpacked extension from wherever you stored it via the “Load unpacked” button in the top left of
chrome://extensions/
. There might be an error because the extension can’t access the page contents, that’s expected. - Enter the 6 digit PIN, then click the export button fig. 5. You can see some log messages at
chrome://serviceworker-internals/?devtools
in the log field ofchrome-extension://pejdijmoenmkgeppbflobdenhhabjlaj/
. - Once complete, the CSV will automatically download and chrome will ask you where to save it
- You can check whether all passwords were successfully exported by comparing the log messages to the number shown in the standalone iCloud passwords app, as in fig. 6. I think there’s some bug in the Windows app where it can’t show multiple entries that have both the same title and username, which is why the number shown on Windows will be lower than the one in the iOS app.
- Reinstall the extension from the store to restore AutoFill functionality
6 Conclusion
I successfully exported all my passwords and I think overall the LLM saved me some time here, so I’m happy. The LLM was pretty good at finding the parts of the code I was looking for. While the code the LLM generated often contained minor errors, it was really rare that it couldn’t fix them within the next 1-3 prompts. And the overall concepts/approaches were usually correct. In the end, I probably spent all the time I saved on understanding and writing code on writing this up. But I hope it ends up helping someone else, in which case it was worth it.
References
Footnotes
If you’re looking to additionally export TOTP secrets, you likely need to instrument relevant functions in the standalone
iCloudPasswords.exe
app or theiCloudPasswordsExtensionHelper.exe
.↩︎The solution is
console.log(JSON.stringify(...))
↩︎and some other cases that aren’t relevant to us here↩︎
case 5 of
connectToBackgroundNativeAppAndSetUpListeners()
inbackground.js
↩︎case 4 of
connectToBackgroundNativeAppAndSetUpListeners()
inbackground.js
↩︎case 5 of
connectToBackgroundNativeAppAndSetUpListeners()
inbackground.js
↩︎