-
We use Ed25519 for creating digital signatures. Public keys are 32 bytes in length, and signatures are 64 bytes.
-
We will start out with support for BLE (Bluetooth Low Energy) and add support for older versions later.
-
The term "table" is used in this document to refer to a hierarchical container for storage of related data. It does not necessarily imply the use of a relational datasystem.
-
Every hour, the app will generate a new Ed25519
SessionKey
. This key will be validated for local uniqueness and stored in a localsession_key
table:message SessionKey { option (primary_fields) = "public_key"; google.protobuf.Timestamp create_time = 1; bytes private_key = 2; bytes public_key = 3; }
-
The app will, acting as a peripheral device, broadcast the public key component of its current
SessionKey
using BLE advertisement packets. The advertisement will use the Viral Tracing service UUID:ServiceUUID = "0c86fc28-2ce7-40cf-9f65-8b6c5a191100"
The public key will be included as the value for the characteristic:
CharacteristicUUID = "ae2e879f-0b38-4fe3-9a23-f667fedffd70"
-
The app will, acting as a central device, listen out for such broadcasts from others. Depending on the platform, it may need to connect to the peripheral to read the value of the characteristic, i.e. the public key.
-
This information is then stored locally by adding an entry to the
contact
table:message Contact { ProximityLevel proximity = 1; bytes public_key = 2; google.protobuf.Timestamp seen_time = 3; }
Where proximity is defined as a coarse-grained enum:
enum ProximityLevel { PROXIMITY_UNSPECIFIED = 0; PROXIMITY_LEVEL_1 = 1; // less than 2m PROXIMITY_LEVEL_2 = 2; // between 2-4m PROXIMITY_LEVEL_3 = 3; // between 4-10m PROXIMITY_LEVEL_4 = 4; // more than 10m }
-
Now if this was the first time that the app had seen a public key, or if it didn't have a corresponding proof for its current
SessionKey
, it will establish a connection to the peripheral advertising it — possibly re-using the connection it had used to read the characteristic — and write the following request:type ProofRequest struct { uint8 version // defaulting to 1 for now [32]byte sender_key [32]byte receiver_key [64]byte signature }
The message will include the public key of the sender, i.e. the central device, the public key of the receiver, i.e. the specific public key seen from the peripheral, and the proof signature.
The proof signature is generated by creating an Ed25519 signature with the sender's public key on the concatenation of the prefix
contact:
with the raw bytes of the receiver's public key.On receiving this proof, the receiving peripheral device will validate it, i.e. that it was for a
SessionKey
it'd recently used, had a valid signature, etc., and store it locally in itsproof
table:message Proof { option (primary_fields) = "contact_key,session_key"; bytes contact_key = 1; google.protobuf.Timestamp create_time = 2; bytes session_key = 3; bytes signature = 4; }
The peripheral will then write a response on the established connection:
type ProofResponse struct { [32]byte signature }
The proof signature will be generated in the exact manner as in the first proof, except with the sender and receiver reversed. The connection initiator, i.e. the central device, will similarly validate and store the proof locally.
With both proofs exchanged, the connection can now be terminated.
-
When establishing connections, the app will preference connections to devices it has least recently seen — especially ones it hasn't seen before.
-
The app will, every day, purge all data from tables which are more than 90 days old.