├── .github └── workflows │ ├── clippy.yml │ ├── rustfmt.yml │ └── tests.yml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.toml ├── Makefile ├── README.md ├── backend-service ├── Cargo.toml └── src │ ├── main.rs │ ├── server.rs │ ├── store.rs │ └── utils.rs └── contact-tracing ├── Cargo.toml ├── README.md ├── src ├── dtkey.rs ├── lib.rs ├── rpi.rs ├── tkey.rs └── utils.rs └── tests └── basic.rs /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Run clippy 13 | run: make lint 14 | -------------------------------------------------------------------------------- /.github/workflows/rustfmt.yml: -------------------------------------------------------------------------------- 1 | name: Rustfmt 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Run rustfmt 13 | run: make format-check 14 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Test 13 | run: make test 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "contact-tracing", 4 | "backend-service", 5 | ] 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | build: 4 | @cargo build 5 | 6 | doc: 7 | @cargo doc 8 | 9 | test: cargotest 10 | 11 | cargotest: 12 | @cd contact-tracing; cargo test 13 | @cd contact-tracing; cargo check --no-default-features 14 | @cd contact-tracing; cargo test --all-features 15 | @cd backend-service; cargo check 16 | 17 | format: 18 | @rustup component add rustfmt 2> /dev/null 19 | @cargo fmt --all 20 | 21 | format-check: 22 | @rustup component add rustfmt 2> /dev/null 23 | @cargo fmt --all -- --check 24 | 25 | lint: 26 | @rustup component add clippy 2> /dev/null 27 | @cargo clippy --all 28 | 29 | update-readme: 30 | @cd contact-tracing; cargo readme | perl -p -e "s/\]\(([^\/]+)\)/](https:\/\/docs.rs\/contact-tracing\/latest\/contact_tracing\/\\1)/" > README.md 31 | 32 | server: 33 | @cd backend-service; RUST_LOG=debug cargo run 34 | 35 | server-reload: 36 | @cd backend-service; RUST_LOG=debug systemfd --no-pid -s http::5000 -- cargo watch -x run 37 | 38 | .PHONY: all doc test cargotest format format-check lint update-readme server server-reload 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proximity-tracing 2 | 3 | This repository holds various things related to covid-19 contact tracing. Specifically 4 | it has an implementation of the [Apple/Google](https://covid19-static.cdn-apple.com/applications/covid19/current/static/contact-tracing/pdf/ContactTracing-CryptographySpecification.pdf) 5 | contact tracing protocol written in Rust. 6 | -------------------------------------------------------------------------------- /backend-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "backend-service" 3 | version = "0.1.0" 4 | authors = ["Armin Ronacher "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | warp = "0.2.2" 11 | futures = "0.3.4" 12 | tokio = { version = "0.2.13", features = ["macros"] } 13 | pretty_env_logger = "0.4.0" 14 | serde_json = "1.0.48" 15 | listenfd = "0.3.3" 16 | hyper = "0.13.4" 17 | serde = { version = "1.0.106", features = ["derive"] } 18 | chrono = "0.4.11" 19 | crc = "1.8.1" 20 | bytes = "0.5.4" 21 | http = "0.2.1" 22 | serde_cbor = "0.11.1" 23 | log = "0.4.8" 24 | sha2 = "0.8.1" 25 | hmac = "0.7.1" 26 | aes = "0.3.2" 27 | contact-tracing = { path = "../contact-tracing", features = ["serde"] } -------------------------------------------------------------------------------- /backend-service/src/main.rs: -------------------------------------------------------------------------------- 1 | mod server; 2 | mod store; 3 | mod utils; 4 | 5 | #[tokio::main] 6 | pub async fn main() { 7 | pretty_env_logger::init(); 8 | server::serve().await; 9 | } 10 | -------------------------------------------------------------------------------- /backend-service/src/server.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::sync::Arc; 3 | 4 | use chrono::{NaiveDateTime, TimeZone, Utc}; 5 | use contact_tracing::DailyTracingKey; 6 | use hyper::{service::make_service_fn, Server}; 7 | use serde::{Deserialize, Serialize}; 8 | use warp::Filter; 9 | 10 | use crate::store::DailyTracingKeyStore; 11 | use crate::utils::{api_reply, response_format}; 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct BackendState { 15 | store: Arc, 16 | } 17 | 18 | #[derive(Serialize, Deserialize, Debug)] 19 | pub struct DailyTracingKeyStoreRequest { 20 | keys: Vec<(u32, DailyTracingKey)>, 21 | } 22 | 23 | pub async fn serve() { 24 | let backend_state = Arc::new(BackendState { 25 | store: Arc::new(DailyTracingKeyStore::open("db").unwrap()), 26 | }); 27 | 28 | macro_rules! pass_state { 29 | () => { 30 | warp::any().map({ 31 | let backend_state = backend_state.clone(); 32 | move || backend_state.clone() 33 | }) 34 | }; 35 | } 36 | 37 | let make_svc = make_service_fn(move |_| { 38 | let fetch = warp::path("fetch") 39 | .and(warp::path::param()) 40 | .and(pass_state!()) 41 | .map(|ts: u64, state: Arc| { 42 | let ts = Utc.from_utc_datetime(&NaiveDateTime::from_timestamp(ts as i64, 0)); 43 | state.store.fetch_buckets(ts).unwrap() 44 | }) 45 | .map(api_reply); 46 | 47 | let submit = warp::path("submit") 48 | .and(warp::body::json()) 49 | .and(pass_state!()) 50 | .map( 51 | |data: DailyTracingKeyStoreRequest, state: Arc| { 52 | for (day_num, key) in data.keys { 53 | state.store.add_daily_tracing_key(day_num, key).unwrap(); 54 | } 55 | }, 56 | ) 57 | .map(api_reply); 58 | 59 | let routes = response_format().and(fetch.or(submit)); 60 | let svc = warp::service(routes); 61 | async move { Ok::<_, Infallible>(svc) } 62 | }); 63 | 64 | let mut listenfd = listenfd::ListenFd::from_env(); 65 | let server = if let Some(l) = listenfd.take_tcp_listener(0).unwrap() { 66 | Server::from_tcp(l).unwrap() 67 | } else { 68 | Server::bind(&([127, 0, 0, 1], 5000).into()) 69 | }; 70 | server.serve(make_svc).await.unwrap(); 71 | } 72 | -------------------------------------------------------------------------------- /backend-service/src/store.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashSet}; 2 | use std::fmt; 3 | use std::fs; 4 | use std::io::{self, BufReader, BufWriter, Read, Write}; 5 | use std::path::{Path, PathBuf}; 6 | use std::sync::RwLock; 7 | 8 | use bytes::{Buf, BufMut, BytesMut}; 9 | use chrono::{DateTime, Utc}; 10 | use crc::crc32; 11 | 12 | use contact_tracing::{day_number_for_timestamp, DailyTracingKey}; 13 | 14 | const DAYS_WINDOW: u32 = 21; 15 | 16 | /// Abstracts over an append only file of daily tracing keys 17 | pub struct DailyTracingKeyStore { 18 | path: PathBuf, 19 | buckets: RwLock>>, 20 | } 21 | 22 | impl fmt::Debug for DailyTracingKeyStore { 23 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 24 | f.debug_struct("DailyTracingKeyStore") 25 | .field("path", &self.path) 26 | .finish() 27 | } 28 | } 29 | 30 | impl DailyTracingKeyStore { 31 | /// Opens a daily tracing key store 32 | pub fn open>(p: P) -> Result { 33 | let path = p.as_ref().to_path_buf(); 34 | fs::create_dir_all(&path)?; 35 | Ok(DailyTracingKeyStore { 36 | path, 37 | buckets: RwLock::new(BTreeMap::new()), 38 | }) 39 | } 40 | 41 | /// Returns the current bucket. 42 | pub fn current_day(&self) -> u32 { 43 | day_number_for_timestamp(&Utc::now()) 44 | } 45 | 46 | /// Ensure bucket is loaded from disk. 47 | fn ensure_day_loaded(&self, bucket: u32) -> Result { 48 | // we only upsert so if the bucket was already loaded, we don't 49 | // need to do anything 50 | if self.buckets.read().unwrap().contains_key(&bucket) { 51 | return Ok(false); 52 | } 53 | 54 | let mut buckets = self.buckets.write().unwrap(); 55 | let path = self.path.join(&format!("_{}.bucket", bucket)); 56 | 57 | let mut set = HashSet::new(); 58 | if let Ok(mut f) = fs::File::open(path).map(BufReader::new) { 59 | loop { 60 | let mut buf = [0u8; 20]; 61 | match f.read(&mut buf)? { 62 | 0 => break, 63 | x if x != buf.len() => { 64 | return Err(io::Error::new( 65 | io::ErrorKind::InvalidData, 66 | "something went very wrong", 67 | )); 68 | } 69 | _ => {} 70 | } 71 | let key = DailyTracingKey::from_bytes(&buf[..16]).unwrap(); 72 | let checksum = crc32::checksum_ieee(key.as_bytes()); 73 | if (&buf[16..]).get_u32_le() != checksum { 74 | return Err(io::Error::new( 75 | io::ErrorKind::InvalidData, 76 | "bad checksum, corrupted file", 77 | )); 78 | } 79 | set.insert(key); 80 | } 81 | } 82 | 83 | buckets.insert(bucket, set); 84 | 85 | Ok(true) 86 | } 87 | 88 | /// Returns all buckets after a certain timestamp 89 | pub fn fetch_buckets( 90 | &self, 91 | timestamp: DateTime, 92 | ) -> Result, io::Error> { 93 | let mut rv = vec![]; 94 | let bucket_start = day_number_for_timestamp(×tamp); 95 | let bucket_end = self.current_day(); 96 | 97 | match bucket_end.checked_sub(bucket_start) { 98 | None => return Ok(vec![]), 99 | Some(diff) if diff > 24 * DAYS_WINDOW => { 100 | return Err(io::Error::new( 101 | io::ErrorKind::InvalidInput, 102 | "reading too far into the past", 103 | )) 104 | } 105 | _ => {} 106 | } 107 | 108 | for bucket in bucket_start..=bucket_end { 109 | self.ensure_day_loaded(bucket)?; 110 | if let Some(set) = self.buckets.read().unwrap().get(&bucket) { 111 | rv.extend(set); 112 | } 113 | } 114 | 115 | Ok(rv) 116 | } 117 | 118 | /// Checks if a tracing key is already known. 119 | pub fn has_daily_tracing_key(&self, key: DailyTracingKey) -> Result { 120 | let now = self.current_day(); 121 | for bucket in (now - DAYS_WINDOW)..now { 122 | self.ensure_day_loaded(bucket)?; 123 | if let Some(set) = self.buckets.read().unwrap().get(&bucket) { 124 | if set.contains(&key) { 125 | return Ok(true); 126 | } 127 | } 128 | } 129 | Ok(false) 130 | } 131 | 132 | /// Adds a tracing key at the current timestamp. 133 | pub fn add_daily_tracing_key( 134 | &self, 135 | day_number: u32, 136 | key: DailyTracingKey, 137 | ) -> Result { 138 | // check if this key has already been seen in the last 21 days 139 | if self.has_daily_tracing_key(key)? { 140 | return Ok(false); 141 | } 142 | 143 | let path = self.path.join(&format!("_{}.bucket", day_number)); 144 | let mut buckets = self.buckets.write().unwrap(); 145 | let mut file = BufWriter::new( 146 | fs::OpenOptions::new() 147 | .create(true) 148 | .append(true) 149 | .open(&path)?, 150 | ); 151 | let mut msg = BytesMut::new(); 152 | msg.put_slice(key.as_bytes()); 153 | msg.put_u32_le(crc32::checksum_ieee(key.as_bytes())); 154 | file.write_all(&msg)?; 155 | buckets 156 | .entry(day_number) 157 | .or_insert_with(Default::default) 158 | .insert(key); 159 | Ok(true) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /backend-service/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | 3 | use http::header::{HeaderValue, CONTENT_TYPE}; 4 | use http::{Response, StatusCode}; 5 | use hyper::Body; 6 | use serde::Serialize; 7 | use warp::{Filter, Reply}; 8 | 9 | thread_local! { 10 | static RESPONSE_FORMAT: Cell = Cell::new(ResponseFormat::Json); 11 | } 12 | 13 | #[derive(Clone, Copy, PartialEq, Eq)] 14 | pub enum ResponseFormat { 15 | Json, 16 | Cbor, 17 | } 18 | 19 | pub fn response_format() -> impl Filter + Copy { 20 | warp::header("accept") 21 | .map(|value: String| { 22 | RESPONSE_FORMAT.with(|format| { 23 | format.set(if value.eq_ignore_ascii_case("application/x-cbor") { 24 | ResponseFormat::Cbor 25 | } else { 26 | ResponseFormat::Json 27 | }); 28 | }); 29 | }) 30 | .untuple_one() 31 | } 32 | 33 | pub fn api_reply(val: T) -> ApiReply 34 | where 35 | T: Serialize, 36 | { 37 | ApiReply { 38 | inner: match RESPONSE_FORMAT.with(|x| x.get()) { 39 | ResponseFormat::Json => ::serde_json::to_vec(&val).map_err(|err| { 40 | log::error!("Invalid json serialization: {:?}", err); 41 | }), 42 | ResponseFormat::Cbor => ::serde_cbor::to_vec(&val).map_err(|err| { 43 | log::error!("Invalid cbor serialization: {:?}", err); 44 | }), 45 | }, 46 | } 47 | } 48 | 49 | /// An API response. 50 | pub struct ApiReply { 51 | inner: Result, ()>, 52 | } 53 | 54 | impl Reply for ApiReply { 55 | #[inline] 56 | fn into_response(self) -> Response { 57 | match self.inner { 58 | Ok(body) => { 59 | let mut res = Response::new(body.into()); 60 | res.headers_mut() 61 | .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); 62 | res 63 | } 64 | Err(()) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contact-tracing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "contact-tracing" 3 | version = "0.2.1" 4 | authors = ["Armin Ronacher "] 5 | edition = "2018" 6 | description = "Implementation of the apple/google contact tracing protocol" 7 | license = "Apache-2.0" 8 | homepage = "https://github.com/mitsuhiko/proximity-tracing" 9 | repository = "https://github.com/mitsuhiko/proximity-tracing" 10 | documentation = "https://docs.rs/contact-tracing" 11 | keywords = ["covid-19", "contact-tracing"] 12 | readme = "README.md" 13 | 14 | [package.metadata.docs.rs] 15 | all-features = true 16 | 17 | [features] 18 | default = ["chrono"] 19 | base64 = ["base64_"] 20 | serde = ["base64", "serde_"] 21 | 22 | [dependencies] 23 | derive_more = "0.99.5" 24 | base64_ = { package = "base64", version = "0.12.0", optional = true } 25 | serde_ = { package = "serde", version = "1.0.106", optional = true } 26 | chrono = { version = "0.4.11", optional = true } 27 | sha2 = "0.8.1" 28 | hmac = "0.7.1" 29 | hkdf = "0.8.0" 30 | bytes = "0.5.4" 31 | rand = "0.7.3" 32 | -------------------------------------------------------------------------------- /contact-tracing/README.md: -------------------------------------------------------------------------------- 1 | # contact-tracing 2 | 3 | This crate implements the apple/google proximity contact tracing. 4 | 5 | The version of this implementation is the [initial reference 6 | spec](https://covid19-static.cdn-apple.com/applications/covid19/current/static/contact-tracing/pdf/ContactTracing-CryptographySpecification.pdf) 7 | from April 2020. 8 | 9 | ## Features 10 | 11 | * `chrono`: Adds timestamp operations to all structs (on by default) 12 | * `serde`: Adds serde support (implies `base64`) 13 | * `base64`: Adds base64 encoding/decoding through `Display` and `FromStr` 14 | 15 | ## Broadcast Example 16 | 17 | To broadcast one needs a tracing key and the rolling proximity identifier 18 | (RPI) for a given time. The RPI is normally created from the daily tracing 19 | key but there is a shortcut to derive it automatically: 20 | 21 | ```rust 22 | use contact_tracing::{TracingKey, DailyTracingKey, Rpi}; 23 | 24 | let tkey = TracingKey::unique(); 25 | let rpi = Rpi::for_now(&tkey); 26 | ``` 27 | 28 | ## Infection Checking Example 29 | 30 | Infection checking uses the daily tracing keys directly: 31 | 32 | ```rust 33 | use contact_tracing::{TracingKey, DailyTracingKey, Rpi}; 34 | 35 | // normally these would come from the internet somewhere 36 | let tkey = TracingKey::unique(); 37 | let dtkey = DailyTracingKey::for_today(&tkey); 38 | 39 | for (tin, rpi) in dtkey.iter_rpis().enumerate() { 40 | // check your database of contacts against the TIN and RPIs generated 41 | // for each daily tracing key downloaded. The TIN should be within 42 | // some reasonable window of the timestamp you captured. 43 | } 44 | ``` 45 | 46 | License: Apache-2.0 47 | -------------------------------------------------------------------------------- /contact-tracing/src/dtkey.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use bytes::{BufMut, BytesMut}; 4 | use derive_more::{Display, Error}; 5 | use hkdf::Hkdf; 6 | use hmac::{Hmac, Mac}; 7 | use sha2::Sha256; 8 | 9 | #[cfg(feature = "chrono")] 10 | use chrono::{DateTime, Utc}; 11 | 12 | use crate::rpi::Rpi; 13 | use crate::tkey::TracingKey; 14 | use crate::utils::Base64DebugFmtHelper; 15 | 16 | #[cfg(feature = "chrono")] 17 | use crate::utils::day_number_for_timestamp; 18 | 19 | /// A compact representation of contact numbers. 20 | #[derive(Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] 21 | pub struct DailyTracingKey { 22 | bytes: [u8; 16], 23 | } 24 | 25 | impl fmt::Debug for DailyTracingKey { 26 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 27 | f.debug_tuple("DailyTracingKey") 28 | .field(&Base64DebugFmtHelper(self)) 29 | .finish() 30 | } 31 | } 32 | 33 | impl DailyTracingKey { 34 | /// Returns the daily tracing key for a day. 35 | pub fn for_day(tk: &TracingKey, day: u32) -> DailyTracingKey { 36 | let h = Hkdf::::new(None, tk.as_bytes()); 37 | let mut info = BytesMut::from(&"CT-DTK"[..]); 38 | info.put_u32_le(day); 39 | let mut out = [0u8; 16]; 40 | h.expand(&info, &mut out).unwrap(); 41 | DailyTracingKey::from_bytes(&out[..]).unwrap() 42 | } 43 | 44 | /// Returns the daily tracing key for today. 45 | #[cfg(feature = "chrono")] 46 | pub fn for_today(tk: &TracingKey) -> DailyTracingKey { 47 | DailyTracingKey::for_timestamp(tk, &Utc::now()) 48 | } 49 | 50 | /// Returns the daily tracing key for a timestamp. 51 | #[cfg(feature = "chrono")] 52 | pub fn for_timestamp(tk: &TracingKey, timestamp: &DateTime) -> DailyTracingKey { 53 | DailyTracingKey::for_day(tk, day_number_for_timestamp(timestamp)) 54 | } 55 | 56 | /// Creates a daily tracing key from raw bytes. 57 | pub fn from_bytes(b: &[u8]) -> Result { 58 | if b.len() != 16 { 59 | return Err(InvalidDailyTracingKey); 60 | } 61 | let mut bytes = [0u8; 16]; 62 | bytes.copy_from_slice(b); 63 | Ok(DailyTracingKey { bytes }) 64 | } 65 | 66 | /// Returns the bytes behind the daily tracing key 67 | pub fn as_bytes(&self) -> &[u8] { 68 | &self.bytes 69 | } 70 | 71 | /// Generates all RPIs for a day. 72 | /// 73 | /// If you need the TINs too just use `.enumerate()`. 74 | pub fn iter_rpis(&self) -> impl Iterator { 75 | let clone = *self; 76 | let mut tin = 0; 77 | std::iter::from_fn(move || { 78 | clone.get_rpi_for_tin(tin).map(|rv| { 79 | tin += 1; 80 | rv 81 | }) 82 | }) 83 | } 84 | 85 | /// Returns the RPI for a time interval number. 86 | /// 87 | /// If the time interval is out of range this returns `None` 88 | pub fn get_rpi_for_tin(&self, tin: u8) -> Option { 89 | if tin > 143 { 90 | return None; 91 | } 92 | 93 | let mut hmac = Hmac::::new_varkey(&self.as_bytes()).unwrap(); 94 | let mut info = BytesMut::from(&"CT-RPI"[..]); 95 | info.put_u8(tin); 96 | hmac.input(&info[..]); 97 | let result = hmac.result(); 98 | let bytes = &result.code()[..]; 99 | Some(Rpi::from_bytes(&bytes[..16]).unwrap()) 100 | } 101 | } 102 | 103 | /// Returned if a daily tracing key is invalid. 104 | #[derive(Error, Display, Debug)] 105 | #[display(fmt = "invalid daily tracing key")] 106 | pub struct InvalidDailyTracingKey; 107 | 108 | #[cfg(feature = "base64")] 109 | mod base64_impl { 110 | use super::*; 111 | use std::{fmt, str}; 112 | 113 | impl str::FromStr for DailyTracingKey { 114 | type Err = InvalidDailyTracingKey; 115 | 116 | fn from_str(value: &str) -> Result { 117 | let mut bytes = [0u8; 16]; 118 | if value.len() != 22 { 119 | return Err(InvalidDailyTracingKey); 120 | } 121 | base64_::decode_config_slice(value, base64_::URL_SAFE_NO_PAD, &mut bytes[..]) 122 | .map_err(|_| InvalidDailyTracingKey)?; 123 | Ok(DailyTracingKey { bytes }) 124 | } 125 | } 126 | 127 | #[cfg(feature = "base64")] 128 | impl fmt::Display for DailyTracingKey { 129 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 130 | let mut buf = [0u8; 50]; 131 | let len = base64_::encode_config_slice(self.bytes, base64_::URL_SAFE_NO_PAD, &mut buf); 132 | f.write_str(unsafe { std::str::from_utf8_unchecked(&buf[..len]) }) 133 | } 134 | } 135 | } 136 | 137 | #[cfg(feature = "serde")] 138 | mod serde_impl { 139 | use super::*; 140 | 141 | use serde_::de::Deserializer; 142 | use serde_::ser::Serializer; 143 | use serde_::{Deserialize, Serialize}; 144 | 145 | impl Serialize for DailyTracingKey { 146 | fn serialize(&self, serializer: S) -> Result 147 | where 148 | S: Serializer, 149 | { 150 | if serializer.is_human_readable() { 151 | serializer.serialize_str(&self.to_string()) 152 | } else { 153 | serializer.serialize_bytes(self.as_bytes()) 154 | } 155 | } 156 | } 157 | 158 | impl<'de> Deserialize<'de> for DailyTracingKey { 159 | fn deserialize(deserializer: D) -> Result 160 | where 161 | D: Deserializer<'de>, 162 | { 163 | use serde_::de::Error; 164 | if deserializer.is_human_readable() { 165 | let s = String::deserialize(deserializer).map_err(D::Error::custom)?; 166 | s.parse().map_err(D::Error::custom) 167 | } else { 168 | let buf = Vec::::deserialize(deserializer).map_err(D::Error::custom)?; 169 | DailyTracingKey::from_bytes(&buf).map_err(D::Error::custom) 170 | } 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /contact-tracing/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate implements the apple/google proximity contact tracing. 2 | //! 3 | //! The version of this implementation is the [initial reference 4 | //! spec](https://covid19-static.cdn-apple.com/applications/covid19/current/static/contact-tracing/pdf/ContactTracing-CryptographySpecification.pdf) 5 | //! from April 2020. 6 | //! 7 | //! # Features 8 | //! 9 | //! * `chrono`: Adds timestamp operations to all structs (on by default) 10 | //! * `serde`: Adds serde support (implies `base64`) 11 | //! * `base64`: Adds base64 encoding/decoding through `Display` and `FromStr` 12 | //! 13 | //! # Broadcast Example 14 | //! 15 | //! To broadcast one needs a tracing key and the rolling proximity identifier 16 | //! (RPI) for a given time. The RPI is normally created from the daily tracing 17 | //! key but there is a shortcut to derive it automatically: 18 | //! 19 | //! ``` 20 | //! use contact_tracing::{TracingKey, DailyTracingKey, Rpi}; 21 | //! 22 | //! let tkey = TracingKey::unique(); 23 | //! let rpi = Rpi::for_now(&tkey); 24 | //! ``` 25 | //! 26 | //! # Infection Checking Example 27 | //! 28 | //! Infection checking uses the daily tracing keys directly: 29 | //! 30 | //! ``` 31 | //! use contact_tracing::{TracingKey, DailyTracingKey, Rpi}; 32 | //! 33 | //! // normally these would come from the internet somewhere 34 | //! let tkey = TracingKey::unique(); 35 | //! let dtkey = DailyTracingKey::for_today(&tkey); 36 | //! 37 | //! for (tin, rpi) in dtkey.iter_rpis().enumerate() { 38 | //! // check your database of contacts against the TIN and RPIs generated 39 | //! // for each daily tracing key downloaded. The TIN should be within 40 | //! // some reasonable window of the timestamp you captured. 41 | //! } 42 | //! ``` 43 | 44 | mod dtkey; 45 | mod rpi; 46 | mod tkey; 47 | mod utils; 48 | 49 | pub use dtkey::*; 50 | pub use rpi::*; 51 | pub use tkey::*; 52 | 53 | #[allow(unused_imports)] 54 | pub use utils::*; 55 | -------------------------------------------------------------------------------- /contact-tracing/src/rpi.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[cfg(feature = "chrono")] 4 | use chrono::{DateTime, Utc}; 5 | use derive_more::{Display, Error}; 6 | 7 | use crate::utils::Base64DebugFmtHelper; 8 | 9 | #[cfg(feature = "chrono")] 10 | use crate::utils::tin_for_timestamp; 11 | 12 | /// A Rolling Proximity Identifier. 13 | #[derive(Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] 14 | pub struct Rpi { 15 | bytes: [u8; 16], 16 | } 17 | 18 | impl fmt::Debug for Rpi { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | f.debug_tuple("Rpi") 21 | .field(&Base64DebugFmtHelper(self)) 22 | .finish() 23 | } 24 | } 25 | 26 | impl Rpi { 27 | /// Returns the RPI for a timestamp directly from a tracing key. 28 | #[cfg(feature = "chrono")] 29 | pub fn for_timestamp(tk: &crate::tkey::TracingKey, timestamp: &DateTime) -> Rpi { 30 | let dtkey = crate::dtkey::DailyTracingKey::for_timestamp(tk, timestamp); 31 | dtkey.get_rpi_for_tin(tin_for_timestamp(timestamp)).unwrap() 32 | } 33 | 34 | /// Returns the RPI that is for the current time interval. 35 | #[cfg(feature = "chrono")] 36 | pub fn for_now(tk: &crate::tkey::TracingKey) -> Rpi { 37 | Rpi::for_timestamp(tk, &Utc::now()) 38 | } 39 | 40 | /// Creates a RPI from raw bytes. 41 | pub fn from_bytes(b: &[u8]) -> Result { 42 | if b.len() != 16 { 43 | return Err(InvalidRpi); 44 | } 45 | let mut bytes = [0u8; 16]; 46 | bytes.copy_from_slice(b); 47 | Ok(Rpi { bytes }) 48 | } 49 | 50 | /// Returns the bytes behind the RPI 51 | pub fn as_bytes(&self) -> &[u8] { 52 | &self.bytes 53 | } 54 | } 55 | 56 | /// Raised if a RPI is invalid. 57 | #[derive(Error, Display, Debug)] 58 | #[display(fmt = "invalid rpi")] 59 | pub struct InvalidRpi; 60 | 61 | #[cfg(feature = "base64")] 62 | mod base64_impl { 63 | use super::*; 64 | use std::{fmt, str}; 65 | 66 | impl str::FromStr for Rpi { 67 | type Err = InvalidRpi; 68 | 69 | fn from_str(value: &str) -> Result { 70 | let mut bytes = [0u8; 16]; 71 | if value.len() != 22 { 72 | return Err(InvalidRpi); 73 | } 74 | base64_::decode_config_slice(value, base64_::URL_SAFE_NO_PAD, &mut bytes[..]) 75 | .map_err(|_| InvalidRpi)?; 76 | Ok(Rpi { bytes }) 77 | } 78 | } 79 | 80 | impl fmt::Display for Rpi { 81 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 82 | let mut buf = [0u8; 50]; 83 | let len = base64_::encode_config_slice(self.bytes, base64_::URL_SAFE_NO_PAD, &mut buf); 84 | f.write_str(unsafe { std::str::from_utf8_unchecked(&buf[..len]) }) 85 | } 86 | } 87 | } 88 | 89 | #[cfg(feature = "serde")] 90 | mod serde_impl { 91 | pub use super::*; 92 | 93 | use serde_::de::Deserializer; 94 | use serde_::ser::Serializer; 95 | use serde_::{Deserialize, Serialize}; 96 | 97 | impl Serialize for Rpi { 98 | fn serialize(&self, serializer: S) -> Result 99 | where 100 | S: Serializer, 101 | { 102 | if serializer.is_human_readable() { 103 | serializer.serialize_str(&self.to_string()) 104 | } else { 105 | serializer.serialize_bytes(self.as_bytes()) 106 | } 107 | } 108 | } 109 | 110 | impl<'de> Deserialize<'de> for Rpi { 111 | fn deserialize(deserializer: D) -> Result 112 | where 113 | D: Deserializer<'de>, 114 | { 115 | use serde_::de::Error; 116 | if deserializer.is_human_readable() { 117 | let s = String::deserialize(deserializer).map_err(D::Error::custom)?; 118 | s.parse().map_err(D::Error::custom) 119 | } else { 120 | let buf = Vec::::deserialize(deserializer).map_err(D::Error::custom)?; 121 | Rpi::from_bytes(&buf).map_err(D::Error::custom) 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /contact-tracing/src/tkey.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use derive_more::{Display, Error}; 4 | use rand::{thread_rng, RngCore}; 5 | 6 | use crate::utils::Base64DebugFmtHelper; 7 | 8 | /// A compact representation of contact numbers. 9 | #[derive(Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] 10 | pub struct TracingKey { 11 | bytes: [u8; 32], 12 | } 13 | 14 | impl fmt::Debug for TracingKey { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 16 | f.debug_tuple("TracingKey") 17 | .field(&Base64DebugFmtHelper(self)) 18 | .finish() 19 | } 20 | } 21 | 22 | impl TracingKey { 23 | /// Returns a new unique tracing key. 24 | pub fn unique() -> TracingKey { 25 | let mut bytes = [0u8; 32]; 26 | let mut rng = thread_rng(); 27 | rng.fill_bytes(&mut bytes[..]); 28 | TracingKey::from_bytes(&bytes[..]).unwrap() 29 | } 30 | 31 | /// loads a tracing key from raw bytes. 32 | pub fn from_bytes(b: &[u8]) -> Result { 33 | if b.len() != 32 { 34 | return Err(InvalidTracingKey); 35 | } 36 | let mut bytes = [0u8; 32]; 37 | bytes.copy_from_slice(b); 38 | Ok(TracingKey { bytes }) 39 | } 40 | 41 | /// Returns the bytes behind the tracing key. 42 | pub fn as_bytes(&self) -> &[u8] { 43 | &self.bytes 44 | } 45 | } 46 | 47 | /// Raised if a tracing key is invalid. 48 | #[derive(Error, Display, Debug)] 49 | #[display(fmt = "invalid tracing key")] 50 | pub struct InvalidTracingKey; 51 | 52 | #[cfg(feature = "base64")] 53 | mod base64_impl { 54 | use super::*; 55 | use std::{fmt, str}; 56 | 57 | impl str::FromStr for TracingKey { 58 | type Err = InvalidTracingKey; 59 | 60 | fn from_str(value: &str) -> Result { 61 | let mut bytes = [0u8; 32]; 62 | if value.len() != 43 { 63 | return Err(InvalidTracingKey); 64 | } 65 | base64_::decode_config_slice(value, base64_::URL_SAFE_NO_PAD, &mut bytes[..]) 66 | .map_err(|_| InvalidTracingKey)?; 67 | Ok(TracingKey { bytes }) 68 | } 69 | } 70 | 71 | impl fmt::Display for TracingKey { 72 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 73 | let mut buf = [0u8; 50]; 74 | let len = base64_::encode_config_slice(self.bytes, base64_::URL_SAFE_NO_PAD, &mut buf); 75 | f.write_str(unsafe { std::str::from_utf8_unchecked(&buf[..len]) }) 76 | } 77 | } 78 | } 79 | 80 | #[cfg(feature = "serde")] 81 | mod serde_impl { 82 | use super::*; 83 | 84 | use serde_::de::Deserializer; 85 | use serde_::ser::Serializer; 86 | use serde_::{Deserialize, Serialize}; 87 | 88 | impl Serialize for TracingKey { 89 | fn serialize(&self, serializer: S) -> Result 90 | where 91 | S: Serializer, 92 | { 93 | if serializer.is_human_readable() { 94 | serializer.serialize_str(&self.to_string()) 95 | } else { 96 | serializer.serialize_bytes(self.as_bytes()) 97 | } 98 | } 99 | } 100 | 101 | impl<'de> Deserialize<'de> for TracingKey { 102 | fn deserialize(deserializer: D) -> Result 103 | where 104 | D: Deserializer<'de>, 105 | { 106 | use serde_::de::Error; 107 | if deserializer.is_human_readable() { 108 | let s = String::deserialize(deserializer).map_err(D::Error::custom)?; 109 | s.parse().map_err(D::Error::custom) 110 | } else { 111 | let buf = Vec::::deserialize(deserializer).map_err(D::Error::custom)?; 112 | TracingKey::from_bytes(&buf).map_err(D::Error::custom) 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /contact-tracing/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[cfg(feature = "chrono")] 4 | use chrono::{Date, DateTime, Utc}; 5 | 6 | pub(crate) struct Base64DebugFmtHelper<'a, T>(pub &'a T); 7 | 8 | #[cfg(feature = "base64")] 9 | impl<'a, T: fmt::Display> fmt::Debug for Base64DebugFmtHelper<'a, T> { 10 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 11 | write!(f, "\"{}\"", self.0) 12 | } 13 | } 14 | 15 | #[cfg(not(feature = "base64"))] 16 | impl<'a, T: fmt::Debug> fmt::Debug for Base64DebugFmtHelper<'a, T> { 17 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 18 | fmt::Debug::fmt(self.0, f) 19 | } 20 | } 21 | 22 | /// Returns the day number for a timestamp. 23 | #[cfg(feature = "chrono")] 24 | pub fn day_number_for_timestamp(ts: &DateTime) -> u32 { 25 | (ts.timestamp() / (60 * 60 * 24)) as u32 26 | } 27 | 28 | /// Returns the TIN for a timestamp in a day. 29 | /// 30 | /// If the TIN does not exist (because it's for a different day) then it 31 | /// returns `None`. 32 | #[cfg(feature = "chrono")] 33 | pub fn tin_for_timestamp_checked(ts: &DateTime, day: Date) -> Option { 34 | let now = ts.timestamp(); 35 | let start_of_day = day.and_hms(0, 0, 0).timestamp(); 36 | let tin = (now - start_of_day) / (60 * 10); 37 | if tin >= 0 && tin <= 143 { 38 | Some(tin as u8) 39 | } else { 40 | None 41 | } 42 | } 43 | 44 | /// Returns the TIN for a timestamp. 45 | /// 46 | /// This does not validate the day. 47 | #[cfg(feature = "chrono")] 48 | pub fn tin_for_timestamp(ts: &DateTime) -> u8 { 49 | tin_for_timestamp_checked(ts, ts.date()).unwrap() 50 | } 51 | -------------------------------------------------------------------------------- /contact-tracing/tests/basic.rs: -------------------------------------------------------------------------------- 1 | use contact_tracing::{Rpi, TracingKey}; 2 | 3 | #[test] 4 | fn test_simple_broadcast() { 5 | let tkey = TracingKey::unique(); 6 | let _rpi = Rpi::for_now(&tkey); 7 | } 8 | --------------------------------------------------------------------------------