ios太封闭了,能收集的信息十分有限,下面列出遇到过的常见信息~
指纹信息收集
硬件信息
内存
同型号的总内存是固定值,看代码:
import Foundation
ProcessInfo.processInfo.physicalMemory
磁盘
同型号的总磁盘空间是固定的,正常用户的可用空间应该符合使用规律,获取代码如下:
import Foundation
let attributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory())
let totalSpace = attributes[.systemSize] as? Int64 ?? 0
let freeSpace = attributes[.systemFreeSize] as? Int64 ?? 0
CPU
同型号CPU核心数和指令集架构一样,而且可以看使用率,代码如下:
import Foundation
let processInfo = ProcessInfo.processInfo
let processorCount = processInfo.processorCount
var architecture = "Unknown"
#if arch(arm64)
architecture = "arm64"
#elseif arch(x86_64)
architecture = "x86_64" // Simulator
#endif
屏幕
包括固定的屏幕大小,像素比等和与用户相关亮度信息,如下:
import UIKit
screen = UIScreen.main
screen.nativeBounds.size // 物理像素
screen.bounds.size // 逻辑点
screen.nativeScale // 缩放因子
screen.brightness // 亮度
型号
不是硬件但同型号硬件一致,
import UIKit
let device = UIDevice.current
device.name
device.model
device.systemName
device.systemVersion
网络信息
获取SIM卡的运营商,国家代码等:
import CoreTelephony
let networkInfo = CTTelephonyNetworkInfo()
if let carrier = networkInfo.serviceSubscriberCellularProviders?.first?.value {
print("Carrier Name: \(carrier.carrierName ?? "N/A")")
print("Mobile Country Code: \(carrier.mobileCountryCode ?? "N/A")")
print("Mobile Network Code: \(carrier.mobileNetworkCode ?? "N/A")")
print("ISO Country Code: \(carrier.isoCountryCode ?? "N/A")")
}
文件信息
这里它能检测很多文件的创建/修改/访问时间和inode等信息,如:
/System/Library/CoreServices/SystemVersion.plist
/var
/etc/group
/etc/hosts
/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e
# ...
下面是获取代码:
import Foundation
func getFileStatInfo(for filePath: String) {
var fileStat = stat() // C 结构体
if stat(filePath, &fileStat) == 0 { // stat() 函数成功返回0,失败返回-1
let inode = fileStat.st_ino
print("Inode: \(inode)")
let atime = fileStat.st_atimespec // at - 访问时间 (文件内容最后被读取)
let accessDate = Date(timeIntervalSince1970: TimeInterval(atime.tv_sec) + TimeInterval(atime.tv_nsec) / 1_000_000_000)
print("Access Time (at): \(accessDate)")
let mtime = fileStat.st_mtimespec // mt - 修改时间 (文件内容最后被修改)
let modificationDate = Date(timeIntervalSince1970: TimeInterval(mtime.tv_sec) + TimeInterval(mtime.tv_nsec) / 1_000_000_000)
print("Modify Time (mt): \(modificationDate)")
let ctime = fileStat.st_ctimespec // ct - 更改时间 (文件元数据最后被修改,如权限、所有者等)
let changeDate = Date(timeIntervalSince1970: TimeInterval(ctime.tv_sec) + TimeInterval(ctime.tv_nsec) / 1_000_000_000)
print("Change Time (ct): \(changeDate)")
let btime = fileStat.st_birthtimespec // bt - 创建时间
let birthDate = Date(timeIntervalSince1970: TimeInterval(btime.tv_sec) + TimeInterval(btime.tv_nsec) / 1_000_000_000)
print("Birth Time (bt): \(birthDate)")
} else {
print("Error getting stat info: \(String(cString: strerror(errno)))")
}
}
应用安装时间
import Foundation
let documentsURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let attributes = try FileManager.default.attributesOfItem(atPath: documentsURL.path)
if let creationDate = attributes[.creationDate] as? Date {
print("App installed around: \(creationDate)")
}
系统信息
系统运行时间
本次开机后运行多久了~:
import Foundation
let uptime = ProcessInfo.processInfo.systemUptime
系统和内核版本
import UIKit
struct SystemVersionInfo {
let systemVersion: String
let kernelVersion: String
}
func getSystemVersionInfo() -> SystemVersionInfo {
let device = UIDevice.current
// 获取内核版本需要使用 uname
var systemInfo = utsname()
uname(&systemInfo)
let kernelVersion = withUnsafePointer(to: &systemInfo.release) {
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
String(cString: $0)
}
}
return SystemVersionInfo(
systemVersion: device.systemVersion,
kernelVersion: kernelVersion
)
}
唯一标识符
IDFV
IDFA(Identifier for Vendor)是个无需权限就能获取的ID,对同一个开发者(同一个Team ID)的所有App,在同一台设备上获取到的IDFV都是相同的,但若同意开发者的所有APP都被卸载下次安装又会生成一个新的,获取代码如下:
import UIKit
func getIDFV() -> String? {
return UIDevice.current.identifierForVendor?.uuidString
}
if let idfv = getIDFV() {
print("Identifier for Vendor (IDFV): \(idfv)")
}
IDFA
IDFA(Identifier for Advertisers)这是专门为广告商追踪用户行为而设计的标识符,所有APP获取的都是一样的,但是用户可以在设置 -> 隐私与安全性 -> 跟踪中重置IDFA,或者完全关闭允许App请求跟踪,这样就拿不到它了。要获取它需要在Info.plist中添加NSUserTrackingUsageDescription来说明目的,用户可根据说明决定是否允许,获取代码如下:
import AdSupport
import AppTrackingTransparency
func requestIDFA() {
ATTrackingManager.requestTrackingAuthorization { status in // 请求用户授权
switch status {
case .authorized:
let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString // 用户授权,可以获取IDFA
print("Identifier for Advertisers (IDFA): \(idfa)")
case .denied, .restricted, .notDetermined:
print("IDFA not available.") // 用户拒绝、受限或未决定
@unknown default:
print("Unknown status.")
}
}
}
自定义+KeyChain存储
就是自己去生成一个ID,把它存在keychain里,这样即使应用被卸载默认也不会删除keychain,下次安装后依然能读取到,比如可以:
import Foundation
import Security
public final class KeychainHelper {
private static let service = "com.xiaobeta.deviceservice"
private static let account = "deviceIdentifier"
public static func getOrCreateDeviceUUID() -> String {
// 优先从 Keychain 中读取
if let uuid = searchDeviceUUID() {
return uuid
}
// 如果 Keychain 中没有,则创建一个新的并保存
let newUUID = UUID().uuidString
saveDeviceUUID(uuid: newUUID)
return newUUID
}
// MARK: - Private Core Functions
private static func saveDeviceUUID(uuid: String) {
guard let data = uuid.data(using: .utf8) else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
// 先尝试删除旧的项目,忽略结果(因为它可能本就不存在)
SecItemDelete(query as CFDictionary)
// 添加新的项目
var newItem = query
newItem[kSecValueData as String] = data
SecItemAdd(newItem as CFDictionary, nil)
}
/// 从 Keychain 中查找设备UUID。
private static func searchDeviceUUID() -> String? {
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess,
let data = item as? Data,
let uuid = String(data: data, encoding: .utf8)
else {
return nil
}
return uuid
}
}
DeviceCheck
这个有点不讲武德,苹果官方虽然不提供device id,但为了让厂商打击作弊提供了DC机制,该机制由Apple官方为每个团队提供两比特空间,开发者可定义4种状态,该机制由TrustZone去做签名,且签名密钥是硬编码的设备证书,所以用它来封设备简直是绝绝子:
import Foundation
import DeviceCheck
class DeviceCheckManager {
// Your server endpoint where you'll send the token
private let yourServerURL = URL(string: "https://your.server.com/verifyDevice")!
/// Generates a DeviceCheck token and sends it to your server for validation.
func validateDevice() {
// 1. First, check if the current device supports DeviceCheck.
guard DCDevice.current.isSupported else {
print("DeviceCheck is not supported on this device.")
// Handle the case for unsupported devices (e.g., older OS, simulator)
return
}
// 2. Generate the single-use token. This is an asynchronous call.
DCDevice.current.generateToken { (tokenData, error) in
guard let tokenData = tokenData else {
if let error = error {
print("Error generating DeviceCheck token: \(error.localizedDescription)")
}
return
}
// 3. The token is binary data. Convert it to a Base64 string to send as JSON.
let tokenString = tokenData.base64EncodedString()
print("Generated DeviceCheck Token: \(tokenString)")
// 4. Send the token to your server.
self.sendTokenToServer(token: tokenString)
}
}
/// A placeholder function for sending the token to your server.
private func sendTokenToServer(token: String) {
var request = URLRequest(url: yourServerURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = ["device_token": token]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
// Example of a network call
URLSession.shared.dataTask(with: request) { (data, response, error) in
// Handle the server's response here.
// Your server would reply indicating if the user gets the promotion, etc.
print("Server response received.")
}.resume()
}
}
// --- 使用示例 ---
let deviceCheckManager = DeviceCheckManager()
// Call this when you need to check the device, e.g., when a user tries to access a promotion.
// deviceCheckManager.validateDevice()
服务端收到token后向apple去读些数据:
def query_device_bits(device_token: str):
"""Queries Apple's server for the current state of the two bits."""
auth_token = generate_auth_token()
headers = {"Authorization": f"Bearer {auth_token}"}
payload = {
"device_token": device_token,
"transaction_id": str(uuid.uuid4()),
"timestamp": int(time.time() * 1000) # Milliseconds
}
try:
response = requests.post(QUERY_URL, json=payload, headers=headers)
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
# Response body is JSON: {"bit0":false,"bit1":false,"last_update_time":"2022-10"}
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error querying device bits: {e}")
if e.response:
print(f"Response body: {e.response.text}")
return None
def update_device_bits(device_token: str, bit0: bool, bit1: bool):
"""Updates the state of the two bits on Apple's server."""
auth_token = generate_auth_token()
headers = {"Authorization": f"Bearer {auth_token}"}
payload = {
"device_token": device_token,
"transaction_id": str(uuid.uuid4()),
"timestamp": int(time.time() * 1000),
"bit0": bit0,
"bit1": bit1
}
try:
response = requests.post(UPDATE_URL, json=payload, headers=headers)
# A successful update returns a 200 OK with an empty body.
response.raise_for_status()
print("Successfully updated device bits.")
return True
except requests.exceptions.RequestException as e:
print(f"Error updating device bits: {e}")
if e.response:
print(f"Response body: {e.response.text}")
return False
注:苹果对隐私保护很看重,所以直接的device id老早就被禁了,直接忽略
活动信息
定位
定位当然很重要,除了IP定位,这里更关注系统提供的接口,ios下有精确定位和模糊定位两种,都需要在Info.plist中添加NSLocationWhenInUseUsageDescription或NSLocationAlwaysAndWhenInUseUsageDescription(后台定位)说明目的,用户允许后可获取:
import CoreLocation
class LocationManager: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
private var completion: ((CLLocation?) -> Void)?
override init() {
super.init()
manager.delegate = self
}
func requestLocation(completion: @escaping (CLLocation?) -> Void) {
self.completion = completion
manager.requestWhenInUseAuthorization() // 请求“使用期间”授权
manager.requestLocation() // 请求单次定位
}
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
completion?(locations.first)
completion = nil // 防止重复回调
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Failed to get location: \(error)")
completion?(nil)
completion = nil
}
// 授权状态变化
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if manager.authorizationStatus == .authorizedWhenInUse || manager.authorizationStatus == .authorizedAlways {
manager.requestLocation()
}
}
}
let locationManager = LocationManager()
func fetchLocation() {
locationManager.requestLocation { location in
if let location = location {
print("Latitude: \(location.coordinate.latitude)")
print("Longitude: \(location.coordinate.longitude)")
} else {
print("Could not retrieve location.")
}
}
}
传感器
传感器数据看是不是人在活动,不像安卓那么宽松,在ios上需要在Info.plist中添加NSMotionUsageDescription说明目的来请求运动数据,用户允许后才可获取:
import CoreMotion
let motionManager = CMMotionManager()
func startMonitoringMotion() {
// 1. 加速计
if motionManager.isAccelerometerAvailable {
motionManager.accelerometerUpdateInterval = 1.0 // 更新频率
motionManager.startAccelerometerUpdates(to: .main) { (data, error) in
if let acceleration = data?.acceleration {
print("Accelerometer: x=\(acceleration.x), y=\(acceleration.y), z=\(acceleration.z)")
}
}
}
// 2. 陀螺仪
if motionManager.isGyroAvailable {
motionManager.gyroUpdateInterval = 1.0
motionManager.startGyroUpdates(to: .main) { (data, error) in
if let rotationRate = data?.rotationRate {
print("Gyroscope: x=\(rotationRate.x), y=\(rotationRate.y), z=\(rotationRate.z)")
}
}
}
}
func stopMonitoringMotion() {
motionManager.stopAccelerometerUpdates()
motionManager.stopGyroUpdates()
}
startMonitoringMotion()
// ...
stopMonitoringMotion()
电池信息
含充放电状态和电量信息,直接可获取:
import UIKit
struct BatteryInfo {
let level: Float // 0.0 to 1.0, -1.0 if unknown
let state: UIDevice.BatteryState // unplugged, charging, full, unknown
}
func getBatteryInfo() -> BatteryInfo? {
let device = UIDevice.current
// 关键:必须先启用监控
guard device.isBatteryMonitoringEnabled else {
device.isBatteryMonitoringEnabled = true
// 首次启用后,可能需要短暂延迟才能获取到准确值
return nil
}
return BatteryInfo(level: device.batteryLevel, state: device.batteryState)
}
if let battery = getBatteryInfo() {
print("Battery Level: \(Int(battery.level * 100))%")
switch battery.state {
case .unplugged: print("Battery State: Unplugged")
case .charging: print("Battery State: Charging")
case .full: print("Battery State: Full")
case .unknown: print("Battery State: Unknown")
@unknown default: break
}
}
区域
包括时区和语言:
import Foundation
let locale = Locale.current
print("Locale Identifier: \(locale.identifier)")
print("Preferred Languages: \(Locale.preferredLanguages.joined(separator: ", "))")
print("Time Zone: \(TimeZone.current.identifier)")
安装的应用
ios下无法直接获取应用列表,不过还是可以用url scheme探测部分特定(注册过URL Scheme)的app是否存在,但这需要提前将所有要探测的URL Schema添加到Info.plist的LSApplicationQueriesSchemes之中,如:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>weixin</string>
<string>alipays</string>
</array>
之后就可以用如下代码探测了:
import UIKit
func isAppInstalled(scheme: String) -> Bool {
// 确保 scheme 后面有 "://"
let formattedScheme = scheme.hasSuffix("://") ? scheme : "\(scheme)://"
if let url = URL(string: formattedScheme) {
return UIApplication.shared.canOpenURL(url)
}
return false
}
if isAppInstalled(scheme: "weixin") {
print("WeChat is installed.")
} else {
print("WeChat is not installed.")
}
当前应用信息
安装时间 版本 plist版本
截屏录屏
对于截屏,ios会在截屏的同时发出一个通知事件,我们可以在视图被加载时,设置一个事件观察者去监听:
private func setupScreenshotObserver() {
NotificationCenter.default.addObserver(
self,
selector: #selector(didTakeScreenshot),
name: UIApplication.userDidTakeScreenshotNotification,
object: nil
)
}
对于录屏,本身是没有通知的,不过当有录屏/屏幕镜像等时,它会设置UIScreen.main.isCaptured=true,我们可以观察这个属性变化来监听:
private func setupScreenRecordingObserver() {
NotificationCenter.default.addObserver(
self,
selector: #selector(screenCaptureStatusDidChange), // 监听变化
name: UIScreen.capturedDidChangeNotification,
object: nil
)
}
@objc private func screenCaptureStatusDidChange() {
print("屏幕捕获状态发生变化!")
checkScreenCaptureStatus()
}
private func checkScreenCaptureStatus() { // 初始时需要主动调用它,因为有可能在进入程序前就开始录屏了
let isCaptured = UIScreen.main.isCaptured
print("当前屏幕是否被捕获: \(isCaptured)")
}
上面都是检测是否有截屏录屏操作,若想要阻止截屏和录屏,可以在视图最上层添加一个隐藏的isSecureTextEntry,当出现它时ios会将整个屏幕变黑!
辅助功能
...
分析检测
抓包
代理
检测是否使用了代理:
import SystemConfiguration
func isProxyEnabled() -> Bool {
guard let proxySettings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() as? [String: Any],
let proxies = proxySettings[kCFNetworkProxiesHTTPProxy as String] as? String,
!proxies.isEmpty
else {
return false
}
let httpEnabled = (proxySettings[kCFNetworkProxiesHTTPEnable as String] as? NSNumber)?.boolValue ?? false
return httpEnabled
}
VPN
VPN也算代理:
import Foundation
func isVPNConnected() -> Bool {
var ifaddr: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&ifaddr) == 0 else { return false }
defer { freeifaddrs(ifaddr) }
var cursor = ifaddr
while let pointer = cursor {
let interface = pointer.pointee
let name = String(cString: interface.ifa_name)
if name.starts(with: "utun") || name.starts(with: "ppp") {
if (interface.ifa_flags & UInt32(IFF_UP)) != 0 {
return true
}
}
cursor = interface.ifa_next
}
return false
}
调试检测
import Foundation
func isDebuggerAttached() -> Bool {
var info = kinfo_proc()
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
var size = MemoryLayout<kinfo_proc>.stride
let junk = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0)
if junk == 0 {
return (info.kp_proc.p_flag & P_TRACED) != 0
}
return false
}
异常环境
重打包检测
重打包有多种检测方式,下面分别说明。 1.根据签名和Bundle Identifier 先说bundle id,直接查就好了,但有可能会被hook,所以要用多种方式查询:
Bundle.main.bundleIdentifier
再看签名:
guard let profilePath = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision") else {
return false // App Store 版本没有这个文件,此方法主要用于非App Store渠道
}
do {
let profileData = try String(contentsOfFile: profilePath, encoding: .ascii)
// 这是一个简化的检查,仅查找Team ID字符串是否存在
// 更严格的检查需要完整解析这个plist文件
return profileData.contains(originalTeamID)
} catch {
return false
}
2.根据APNS APNS与App的Bundle Identifier和签名证书是强绑定的,一个合法的App实例在注册推送通知时,会从Apple服务器获取一个针对该App和该设备的唯一Push Token,服务器可以使用APNS证书向这个Push Token发送通知。如果App被重打包,它的Bundle Identifier和签名都变了,因此它从Apple获取的Push Token将与APNS证书不匹配,服务器尝试向这个无效的token发送通知时将会失败:
import UIKit
import UserNotifications
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
registerForPushNotifications()
return true
}
func registerForPushNotifications() { // 请求推送通知权限
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
print("Permission granted: \(granted)")
guard granted else { return }
DispatchQueue.main.async { // 获取权限后,在主线程注册
UIApplication.shared.registerForRemoteNotifications()
}
}
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // 成功获取到 Push Token 后的回调
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() // 将二进制的 token 转换成十六进制字符串,以便发送给服务器
print("Successfully registered for notifications. Device Token: \(tokenString)")
sendTokenToServerForValidation(token: tokenString) // 将 token 发送到开发者服务器进行验证
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { // 注册失败的回调
print("Failed to register for notifications: \(error.localizedDescription)")
}
private func sendTokenToServerForValidation(token: String) { // 发送 Token 到服务器的辅助函数
guard let url = URL(string: "https://ios-fp.betamao.com/validate-token") else { return } // 开发者的服务器地址
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = ["device_token": token]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request).resume()
}
}
服务端再尝试推送静默消息即可(再进一步可以让客户端将推送的Nonce上报),推送也有两种方式,这里直接以p8证书去推:
apns_client = APNsClient(
team_id=TEAM_ID,
auth_key_id=KEY_ID,
auth_key_filepath=KEY_FILE_PATH, # .p8 签名文件
topic=APP_BUNDLE_ID,
use_sandbox=False # True为开发环境
)
@app.route('/validate-token', methods=['POST'])
def validate_token():
"""接收客户端发来的 device_token 并尝试发送静默推送进行验证"""
data = request.get_json()
if not data or 'device_token' not in data:
return jsonify({"status": "error", "message": "Missing device_token"}), 400
device_token = data['device_token']
print(f"Received token for validation: {device_token}")
payload = Payload(content_available=True) # 构造一个静默推送 (silent push):content-available=1, 但没有 alert, sound, badge
try:
apns_client.send_notification(device_token, payload) # 尝试向这个 token 发送推送
print(f"SUCCESS: Token {device_token} is valid for bundle ID {APP_BUNDLE_ID}.") # 如果代码能执行到这里,没有抛出异常,说明Apple服务器接受了这个token,没问题
return jsonify({"status": "success", "message": "Token is valid"}), 200
except Exception as e:
print(f"FAILURE: Token {device_token} is INVALID. Reason: {e}") # 如果发送失败,apns2库会抛出异常(BadDeviceToken 错误)
return jsonify({"status": "error", "message": f"Token is invalid: {e}"}), 400
3.根据ATTest 这就是Apple出的专门用来对抗重打包的机制,属于DeviceCheck中的一项,它也是使用硬件级的安全特性(Secure Enclave)来向服务器提供加密证明,证实与服务器通信的确实是未经修改的、正版的App:
import DeviceCheck
import CryptoKit // For SHA256
class AppAttestManager {
let attestURL = URL(string: "https://ios-fp.betamao.com/submit-attestation")!
let protectedAPI_URL = URL(string: "https://ios-fp.betamao.com/protected-api")!
var keyId: String?
func attestDevice() { // 生成密钥并获取证明,然后发送给服务器
let attestService = DCAppAttestService.shared
guard attestService.isSupported else { return }
attestService.generateKey { [weak self] (keyId, error) in // 生成密钥对
guard let keyId = keyId else { return }
self?.keyId = keyId
let serverChallenge = "one-time-challenge-from-server".data(using: .utf8)! // 从服务器获取的一个challenge (一次性的)
let challengeHash = Data(SHA256.hash(data: serverChallenge))
attestService.attestKey(keyId, clientDataHash: challengeHash) { (attestationObject, error) in // 请求Apple对密钥进行证明
guard let attestation = attestationObject else { return }
self?.sendAttestationToServer(keyId: keyId, attestation: attestation) // 将 keyId 和 attestationObject 发送到服务器
}
}
}
func accessProtectedAPI() { // 为受保护的API请求生成断言(签名)
guard let keyId = self.keyId else {
print("Key ID not available. Please attest first.")
return
}
let requestBody = ["message": "hello world"] // 准备要发送的请求体
let requestData = try! JSONSerialization.data(withJSONObject: requestBody)
let requestHash = Data(SHA256.hash(data: requestData)) // 对请求体进行哈希
DCAppAttestService.shared.generateAssertion(keyId, clientDataHash: requestHash) { [weak self] (assertionObject, error) in // 使用硬件私钥对哈希进行签名,生成断言
guard let assertion = assertionObject else { return }
self?.sendRequestWithAssertion(requestData: requestData, keyId: keyId, assertion: assertion) // 将原始请求和断言一起发送到服务器
}
}
private func sendAttestationToServer(keyId: String, attestation: Data) {
// ... 发送到开发者服务端
}
private func sendRequestWithAssertion(requestData: Data, keyId: String, assertion: Data) {
var request = URLRequest(url: protectedAPI_URL)
request.httpMethod = "POST"
request.httpBody = requestData
request.setValue(keyId, forHTTPHeaderField: "X-Key-Id") // 将 keyId 和断言(签名)放在请求头中
request.setValue(assertion.base64EncodedString(), forHTTPHeaderField: "X-Assertion-Signature")
// ... URLSession code ...
}
}
越狱检测
要检测越狱,思路为: 1.是否存在常见越狱文件 2.是否存在常见越狱后的软件 3.是否有超出正常应用的权限
import UIKit
func isDeviceJailbroken() -> Bool {
let jailbreakFilePaths = [
"/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt"
]
for path in jailbreakFilePaths { // 检查常见的越狱文件
if FileManager.default.fileExists(atPath: path) { return true }
}
if let cydiaURL = URL(string: "cydia://"), UIApplication.shared.canOpenURL(cydiaURL) { // 例:检查是否可以打开 Cydia 的 URL Scheme
return true
}
do {
try "jb_test".write(toFile: "/private/jb_test.txt", atomically: true, encoding: .utf8) // 检查是否可以写入系统目录
try? FileManager.default.removeItem(atPath: "/private/jb_test.txt")
return true
} catch {
// 写入失败是正常的
}
return false
}