最近接到的电话自称来自入管部门或中国公安,有时还会播放中文自动语音。至少在我的周围,这不像是面向日本人的普通无差别诈骗电话。
我在中国旅行时曾在酒店填写联系方式,也在预订时使用过电话号码。号码也许经由某个环节流到了中文诈骗团伙手中。
我已经启用了警察厅推荐应用的诈骗及骚扰电话拦截功能,但这个号码仍然打了进来。该功能通过匹配已登记的危险号码工作,无法阻止所有新号码或未登记号码。2
我几乎没有接听海外电话的需要。我希望日本国内电话照常响铃,而日本国家代码 +81 以外的电话全部被拒绝。但我没有找到能按国家代码统一拦截国际电话的 iPhone 应用。
我原以为可以自己做一个:把来电号码当作字符串,用 hasPrefix 或正则表达式拒绝 81 以外的号码。我以为会有类似 grep 的条件匹配,并没有想到要枚举所有电话号码。
调查 Call Directory
Call Directory app extension 通过 CXCallDirectoryProvider.beginRequest(with:) 把拦截列表交给 iOS。
电话号码类型 CXCallDirectoryPhoneNumber 实际上是 Int64,不含 + 和空格,从国家代码开始。API 只能逐个添加完整号码。
context.addBlockingEntry(
withNextSequentialPhoneNumber: phoneNumber
)
没有前缀、正则表达式、国家代码或号码范围 API。完整号码必须去重并按升序添加,最后调用 completeRequest()。顺序、重复和数量问题分别产生 entriesOutOfOrder、duplicateEntries 和 maximumEntriesExceeded。4
extension 不会在每次来电时启动。Apple 说明识别数据需要一次性提供,来电时不能查询 Web 服务。3 因此没有地方执行 if !number.hasPrefix("81")。
Call Directory 的执行方式
- 用户在 iOS 来电拦截设置中启用 extension。
- 应用调用
reloadExtension(withIdentifier:)。 - iOS 启动 extension,并把 context 传给
beginRequest(with:)。 - extension 按顺序添加号码并调用
completeRequest()。 - 之后由 iOS 完成来电匹配。
context.isIncremental 为 true 时可以差量更新和删除号码,但差量模式也不支持范围或条件式。
查到这里我才意识到,拦截全部国际电话需要枚举目标号码并交给 Call Directory。国际号码遵循 ITU E.164,包含国家代码最长15位。1
这不是已分配号码的精确数量,但把15位空间作为简单上限,大约有 1015 种组合。排除 +81 几乎不改变数量级。只保存 Int64 就需要约 8 PB,加上索引可能达到数十 PB。因此我放弃覆盖全部号码,改为只计算实际频繁来电的 +1 833。
缩小到 +1 833
+1 是包含美国和加拿大等地区的北美编号计划国家代码,其后有10位数字。833 是免费电话区号,还剩7位,因此完整覆盖需要 107 = 10,000,000 条。
一千万个64位号码占 80,000,000 字节,约 76.3 MiB,尚未包括数组、数据库和系统索引。仅从存储容量看约80MB是现实的,但Apple未公开的条目上限和加载时间仍需测试。
Live Caller ID Lookup
Live Caller ID Lookup 可以在来电时查询服务器,但 extension 不会收到来电事件。LiveCallerIDLookupProtocol 只提供 serviceURL、tokenIssuerURL 和 userTierToken。5
@main
struct LookupExtension: LiveCallerIDLookupProtocol {
var context: LiveCallerIDLookupExtensionContext {
.init(serviceURL: serviceURL,
tokenIssuerURL: tokenIssuerURL,
userTierToken: token)
}
}
查询由 iOS 执行。Oblivious HTTP 隐藏用户IP,Privacy Pass进行匿名认证,KPIR把完整电话号码作为加密关键词搜索。6
服务端需要三个采用 Protocol Buffers 的 HTTP POST 端点。7
| 端点 | 用途 | 数据 |
|---|---|---|
/config | PIR配置和密钥状态 | ConfigRequest / ConfigResponse |
/key | 上传评估密钥 | EvaluationKeys |
/queries | 计算加密查询 | Requests / Responses |
拦截payload只有1字节:0放行,1拦截。发信者信息使用 CallIdentity Protocol Buffer。8
服务器不会收到明文号码,所以无法简单执行 hasPrefix("81")。它仍需要以完整号码为键的PIR数据集、Apple端点验证和KPIR服务器。
两种方式的区别
| Call Directory | Live Caller ID Lookup | |
|---|---|---|
| 数据 | 预先载入设备 | 服务器PIR数据集 |
| 查询 | iOS本地匹配 | iOS加密查询 |
| 单位 | 完整电话号码 | 以完整号码为键的条目 |
| 前缀条件 | 不支持 | 不能自由执行服务器代码 |
计算号码范围
输入号码前缀和总位数,计算完整号码条目数。
结论
+1 833 本身有实现余地:一千万条,约80MB。但它只能拦截这个前缀。对方换号码或国家代码后,又要重新调查、生成、分发并加载数据。
发信者号码也不是强身份认证。VoIP和电话转接可能显示伪造号码。910 对频繁更换或伪造号码的对方,号码拦截效果有限。
技术上可以枚举 +1 833,但每次号码变化都维护一次太麻烦,所以我放弃了。