Skip to content

Instantly share code, notes, and snippets.

Last active April 10, 2024 07:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save airy10/5205dc851fbd0715fcd7a5cdde25e7c8 to your computer and use it in GitHub Desktop.
Save airy10/5205dc851fbd0715fcd7a5cdde25e7c8 to your computer and use it in GitHub Desktop.
Decrypt all beacons files from ~/Library/
// airtag-decryptor.swift
// Decrypt all beacons files from ~/Library/ - updated when FindMy is running
// Results in /tmp/ - same file hierarchy
// Created by Matus on 28/01/2024. -
// Modified by Airy
import Cocoa
import Foundation
import CryptoKit
extension URL {
var isDirectory: Bool {
(try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
// Function to decrypt using AES-GCM
func decryptRecordFile(fileURL: URL, key: SymmetricKey) throws -> [String: Any] {
// Read data from the file
let data = try Data(contentsOf: fileURL)
// Convert data to a property list (plist)
guard let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [Any] else {
throw MyError.invalidFileFormat
// Extract nonce, tag, and ciphertext
guard plist.count >= 3,
let nonceData = plist[0] as? Data,
let tagData = plist[1] as? Data,
let ciphertextData = plist[2] as? Data else {
throw MyError.invalidPlistFormat
let sealedBox = try AES.GCM.SealedBox(nonce: AES.GCM.Nonce(data: nonceData), ciphertext: ciphertextData, tag: tagData)
// Decrypt using AES-GCM
let decryptedData = try, using: key)
// Convert decrypted data to a property list
guard let decryptedPlist = try PropertyListSerialization.propertyList(from: decryptedData, options: [], format: nil) as? [String: Any] else {
throw MyError.invalidDecryptedData
return decryptedPlist
func decryptDirectory(filePath: String, outputPath: String, key: SymmetricKey) throws {
let baseURL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first
if let contentURL = baseURL?.appending(path: filePath) {
if contentURL.isDirectory {
if let urls = try? FileManager.default.contentsOfDirectory(at: contentURL, includingPropertiesForKeys: nil) {
for url in urls {
let path = (filePath as NSString).appendingPathComponent(url.lastPathComponent)
try decryptDirectory(filePath: path, outputPath: outputPath, key: key)
} else {
do {
let decryptedPlist = try decryptRecordFile(fileURL: contentURL, key: key)
// Save decrypted plist as a file in the current directory
let name = contentURL.lastPathComponent as NSString
if let outputName = (name.deletingPathExtension as NSString).appendingPathExtension("plist") {
let dir = (filePath as NSString).deletingLastPathComponent
let outputDirPath = (outputPath as NSString).appendingPathComponent(dir)
try FileManager.default.createDirectory(atPath: outputDirPath, withIntermediateDirectories: true)
let outputURL = URL(fileURLWithPath: outputDirPath).appending(path: outputName)
try decryptedPlist, format: .xml, options: 0).write(to: outputURL)
} catch {
print("Error:", error)
// -> Hex format key from `security find-generic-password -l 'BeaconStore' -w`
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrLabel as String: "BeaconStore",
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnAttributes as String: true,
kSecReturnData as String: true]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else { throw MyError.noPassword }
guard status == errSecSuccess else { throw MyError.keychainError(status: status) }
guard let existingItem = item as? [String : Any] else { throw MyError.invalidItem }
if let keyData = existingItem[kSecValueData as String] as? Data {
let key = SymmetricKey(data: keyData)
let basePath = ""
let outputPath = NSTemporaryDirectory()
try decryptDirectory(filePath: basePath, outputPath: outputPath, key: key)
let resultURL = URL(filePath: basePath, relativeTo: URL(filePath: outputPath))
enum MyError: Error {
case invalidFileFormat
case invalidPlistFormat
case invalidDecryptedData
case noPassword
case invalidItem
case keychainError(status: OSStatus)
Copy link

airy10 commented Feb 7, 2024

(updated to directly query the keychain without launching some external command)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment