Lately I have been receiving calls that claim to be from immigration authorities or Chinese public security. Some play an automated message in Chinese. They do not feel like the usual indiscriminate scam calls aimed at Japanese speakers.
I used my phone number for hotel forms and reservations while traveling in China. It may have reached a Chinese-speaking fraud group through one of those routes.
I had enabled the scam and nuisance-number blocking feature of an app recommended by Japan's National Police Agency, but this number still got through. It checks known dangerous numbers, so it cannot stop every new or unregistered number.2
I rarely need to receive calls from abroad. I wanted domestic calls to ring normally and everything outside Japan's country code, +81, to be rejected. I could not find an iPhone app that blocks international calls by country code.
I assumed I could build one: receive the caller number as a string and reject anything outside 81 with hasPrefix or a regular expression. I expected a grep-like condition and had no reason to think every number would need to be enumerated.
Looking into Call Directory
A Call Directory app extension supplies its blocking list to iOS from CXCallDirectoryProvider.beginRequest(with:).
The phone-number type is CXCallDirectoryPhoneNumber, an Int64. It contains digits beginning with the country code, without + or spaces. Blocking is added one complete number at a time.
context.addBlockingEntry(
withNextSequentialPhoneNumber: phoneNumber
)
There is no API for prefixes, regular expressions, country codes, or ranges. Complete numbers must be unique and ascending before completeRequest(). Invalid ordering, duplicates, and excessive entries produce entriesOutOfOrder, duplicateEntries, and maximumEntriesExceeded.4
The extension does not run for every incoming call. Apple says identification data is supplied all at once and cannot be fetched from a web service during a call.3 There is nowhere to execute if !number.hasPrefix("81") at call time.
Call Directory execution model
- The user enables the extension in iOS call-blocking settings.
- The app requests
CXCallDirectoryManager.reloadExtension(withIdentifier:). - iOS launches the extension and passes a context to
beginRequest(with:). - The extension adds numbers in order and calls
completeRequest(). - iOS performs subsequent call matching.
The app preloads data into the system directory rather than deciding whether each call should be blocked. getEnabledStatusForExtension checks its state and openSettings() opens the relevant settings.
When context.isIncremental is true, entries can be changed incrementally and removed with removeBlockingEntry(withPhoneNumber:). Incremental updates still do not add support for ranges or conditions.
At this point I realized that blocking every international call would require enumerating the target numbers and loading them into Call Directory. A country-code rule sounded small, so I had not considered the data size.
International telephone numbers follow ITU E.164 and can contain up to 15 digits including the country code.1
As a simple upper bound rather than a count of assigned numbers, a 15-digit space contains about 1015 combinations. Excluding +81 barely changes the order of magnitude. Storing only Int64 values would take about 8 PB, and indexes could push it into tens of petabytes. I abandoned complete coverage and narrowed the test to +1 833, the range I was actually seeing.
Narrowing it to +1 833
+1 is the country code for the North American Numbering Plan, which includes the United States and Canada. It is followed by ten digits. Because 833 is a toll-free area code, seven digits remain. Covering every +1 833 number requires 107 = 10,000,000 entries.
Ten million 64-bit numbers occupy 80,000,000 bytes, about 76.3 MiB, before arrays, databases, and system indexes. Around 80 MB is practical from a storage perspective, though Apple's unpublished entry limit and loading time would still need testing.
Live Caller ID Lookup
Live Caller ID Lookup can consult a server during an incoming call, but the extension does not receive the call event. LiveCallerIDLookupProtocol supplies only serviceURL, tokenIssuerURL, and userTierToken.5
@main
struct LookupExtension: LiveCallerIDLookupProtocol {
var context: LiveCallerIDLookupExtensionContext {
.init(serviceURL: serviceURL,
tokenIssuerURL: tokenIssuerURL,
userTierToken: token)
}
}
iOS performs the lookup. Oblivious HTTP hides the user's IP address, Privacy Pass provides anonymous authentication, and KPIR searches for the complete phone number as an encrypted keyword.6
The service needs three Protocol Buffers HTTP POST endpoints.7
| Endpoint | Purpose | Data |
|---|---|---|
/config | PIR configuration and key status | ConfigRequest / ConfigResponse |
/key | Upload evaluation keys | EvaluationKeys |
/queries | Evaluate encrypted queries | Requests / Responses |
The blocking payload is one byte: 0 allows a call and 1 blocks it. Caller identity uses a CallIdentity Protocol Buffer and may include a name, icon, category, and cache lifetime.8
The server never receives the queried number in plaintext. It therefore cannot simply run hasPrefix("81"). It needs a PIR dataset keyed by complete phone numbers, as well as Apple endpoint validation and a KPIR server.
Comparison
| Call Directory | Live Caller ID Lookup | |
|---|---|---|
| Data | Preloaded on device | Server-side PIR dataset |
| Lookup | Local iOS match | Encrypted iOS query |
| Unit | Complete phone number | Entry keyed by complete number |
| Prefix rule | Unavailable | No arbitrary server callback |
Calculate the range
Enter a prefix and the total number of digits to calculate the number of complete entries.
Conclusion
+1 833 alone leaves room for an implementation: ten million entries and roughly 80 MB. But it only blocks that prefix. If callers switch numbers or country codes, the range must be researched, generated, distributed, and reloaded again.
Caller ID is not strong authentication either. VoIP and forwarding paths can present a spoofed number.910 Number-based blocking has limited value against callers who rotate or spoof numbers.
Enumerating +1 833 is technically possible. Repeating the maintenance every time the caller changes numbers is tedious, so I stopped.
References
- 1ITU-T E.164
- 2NPA-recommended apps
- 3Identifying and blocking calls
- 4CXCallDirectoryExtensionContext
- 5Getting up-to-date calling and blocking information
- 6How Live Caller ID Lookup preserves privacy
- 7Setting up the HTTP endpoints
- 8Formatting blocking and identity data
- 9NPA White Paper
- 10FCC: Caller ID Spoofing