최근 입관이나 중국 공안을 사칭하는 전화가 걸려온다. 중국어 자동 음성이 재생될 때도 있다. 적어도 내 주변에서는 일본인을 대상으로 무차별 발신하는 일반적인 스팸 전화와는 달라 보였다.
중국을 여행할 때 호텔에 연락처를 적고 예약에도 전화번호를 사용했다. 그 과정 어딘가에서 중국어권 사기 조직에 번호가 넘어갔을지도 모른다.
경찰청 추천 앱의 사기·스팸 번호 차단 기능도 켜 두었지만 이 번호는 통과했다. 등록된 위험 번호를 대조하는 방식이므로 새 번호나 미등록 번호까지 모두 막을 수는 없다.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 문서에도 식별 정보는 한꺼번에 제공하며 착신 시 웹 서비스를 조회할 수 없다고 적혀 있다.3 착신 시점에 if !number.hasPrefix("81")를 실행할 곳이 없다.
Call Directory 실행 모델
- 사용자가 iOS 설정에서 extension을 활성화한다.
- 앱이
reloadExtension(withIdentifier:)를 요청한다. - iOS가 extension을 실행해
beginRequest(with:)에 context를 전달한다. - extension이 번호를 순서대로 추가하고
completeRequest()를 호출한다. - 이후 착신 대조는 iOS가 수행한다.
context.isIncremental이 true이면 차분 갱신과 삭제가 가능하지만 범위나 조건식을 지원하게 되는 것은 아니다.
여기까지 조사하고 나서야 국제전화를 전부 막으려면 대상 번호를 열거해 Call Directory에 넣어야 한다는 것을 알았다. 국제전화 번호는 ITU E.164를 따르며 국가번호를 포함해 최대 15자리다.1
실제 할당 번호 수가 아닌 단순 상한이지만 15자리 공간은 약 1015개다. +81을 제외해도 규모는 거의 같다. Int64만 약 8PB이고 인덱스를 더하면 수십 PB가 될 수 있다. 전체 차단은 포기하고 실제로 걸려오던 +1 833만 계산했다.
+1 833으로 범위 축소
+1은 미국과 캐나다 등을 포함하는 북미 번호 계획의 국가번호이며 뒤에 10자리가 온다. 833 뒤에는 7자리가 남으므로 모두 포함하면 107 = 10,000,000건이다.
64비트 번호 1천만 개는 80,000,000바이트, 약 76.3MiB다. 저장 용량만 보면 약 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 endpoint가 필요하다.7
| Endpoint | 역할 | 데이터 |
|---|---|---|
/config | PIR 설정과 키 상태 | ConfigRequest / ConfigResponse |
/key | 평가 키 업로드 | EvaluationKeys |
/queries | 암호화 질의 평가 | Requests / Responses |
차단 payload는 1바이트이며 0은 허용, 1은 차단이다. 발신자 정보는 CallIdentity Protocol Buffer로 반환한다.8
서버는 평문 전화번호를 받지 않으므로 hasPrefix("81")를 실행할 수 없다. 완전한 번호를 키로 하는 PIR 데이터셋과 Apple endpoint 검증, KPIR 서버가 필요하다.
두 방식 비교
| Call Directory | Live Caller ID Lookup | |
|---|---|---|
| 데이터 | 단말에 미리 로드 | 서버 PIR 데이터셋 |
| 조회 | iOS 로컬 대조 | iOS 암호화 질의 |
| 단위 | 완전한 전화번호 | 완전한 번호가 키인 항목 |
| 접두사 | 지원 안 함 | 임의 서버 판정 없음 |
건수 계산
접두사와 전체 자릿수로 완전한 번호의 건수를 계산한다.
결론
+1 833만이라면 1천만 건, 약 80MB라서 구현할 여지는 있다. 하지만 상대가 번호나 국가번호를 바꾸면 다시 범위를 조사하고 생성해 배포·갱신해야 한다.
발신자 번호는 강한 본인 인증도 아니다. VoIP나 전화 전달 경로에서 표시 번호가 위조될 수 있다.910 번호를 자주 바꾸거나 위조하는 상대에게 번호 기반 차단의 효과는 제한적이다.
+1 833 열거는 기술적으로 가능하다. 번호가 바뀔 때마다 관리하는 것이 번거로워서 만들지 않았다.