Edit: Here: https://github.com/WhisperSystems/Signal-Android/issues/4726
„[..] Signal has always done contact intersection with an ephemeral query of truncated hashes of phone numbers.„
The problem, which no one denies, is that the small number of possible valid e164 values leaves the scheme vulnerable. Also, last time I checked you could submit like 10k tokens at once, so using a modest amount of IP addresses and phone numbers to bypass the rate limiting it is feasible to determine every registered number in a city. Restricting this is hard because some people have huge address books.
The Silent Circle method is slightly better in my opinion, but not by much so I understand why Moxie doesn't want to bother changing it.
Back when Redphone was a separate app it used a bloom filter scheme.
There are really no good solutions to this problem. The safer ones currently know just don't scale.
The hashing is so people can't easily get a list of all the phone numbers, which is easy to work around, but, then again, they could just hammer the endpoint querying for all the various numbers anyway.
Standard OTT messaging architecture guarantees the service will see message envelopes anyway, so it's not worth the trade off of deploying PIR schemes of the differential or computational variety. Look for stuff by Ian Goldberg. Percy++ is a practical example you can run yourself.
For OTT contact sync the reasonable thing to do is just send the phone numbers. Blinding them by truncated hash is a nice gesture. What's not cool is sending all the other fields of the address book along with it.
We know and can easily verify that Signal is being good. But what can we do about less trustworthy services? The phone would need to apply permissions. Like, allow/deny filters of which fields of contacts or contact categories each app may have access to. An address book firewall, essentially. Considering how important messaging apps are in our lives and the amount of time we spend with them I think such granularity is warranted.
Silent Circle way: Server keeps hashes of registered users cached. Client hashes all the numbers it has, remembering the hash -> number map for them. Client sends a small number of the most significant bits of its hashes. Server treats this like a mask or wildcard search, returns all matches. Client knows which match, also gets hashes for other users they don't know. Sure, the client can reverse them, but the client could have probed for them anyway. The server maybe/probably doesn't learn enough to preimage what the client sent because of too many collisions. The downside is that the number of bits the client sends needs to be appropriate given the size of the database, though that can be mitigated by sharding by country code/area code/whatever.
What's the full number?
If you included the 10 byte phone number, that only adds 100 GB. Then just store each entry as a 26 byte hash/number struct sorted by hash.