Last active
May 9, 2020 21:53
-
-
Save pixeltrix/6ce590b710513af4c011f74c96ef3a49 to your computer and use it in GitHub Desktop.
Ruby implementation of the NHS contact tracing app's messages and how they're decrypted
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "openssl" | |
require "securerandom" | |
########################## | |
### iOS/Android Device ### | |
########################## | |
# Installation id - returned by the registration request | |
uuid = "E1D160C7-F6E8-48BC-8687-63C696D910CB" | |
uuid_bytes = uuid.scan(/[0-9A-Z]{2}/).map { |s| s.to_i(16) }.pack("C*") | |
# Secret key used to sign broadcasts - returned by the registration request | |
secret_key = "3WEejyPI2UdjPKXb4PnedwqZudEqKURiuFKHzdOKZsE=" | |
# Sytem-wide public key used to encrypt the data - returned by the registration request | |
server_key = OpenSSL::PKey.read <<~KEY | |
-----BEGIN PUBLIC KEY----- | |
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5Km+yAxWRLRKJmJbfvZtSneKjlzu | |
0MWLAsmfDa3jzDB9QHEZVez8RlrX5w7vwjt01qQ9AdcitRxAL/v6vQEjsg== | |
-----END PUBLIC KEY----- | |
KEY | |
# ISO 3166 country code number | |
country_code = 826 | |
# Broadcast ID is valid for 24 hours | |
current_time = Time.now.to_i | |
start_date = current_time - current_time % 86400 | |
end_date = start_date + 86400 | |
# Payload is a combination of start date, end date, id and country code | |
payload = [ | |
[start_date].pack("N"), # 32-bit integer, unsigned, network order | |
[end_date].pack("N"), # 32-bit integer, unsigned, network order | |
uuid_bytes, | |
[country_code].pack("n"), # 16-bit integer, unsigned, network order | |
].join | |
# Random key per rotation period which is currently 24 hours | |
random_key = OpenSSL::PKey::EC.generate("prime256v1") | |
# Generate symmetric key for AES encryption | |
shared_key = random_key.dh_compute_key(server_key.public_key) | |
# Convert the ephemeral public key to bytes, removing the first byte | |
# which indicates form of the point on the elliptic curve. In this case | |
# it's assumed to be in uncompressed form. | |
shared_info = random_key.public_key.to_octet_string(:uncompressed)[1..-1] | |
# Derive AES key using ANSI x9.63 KDF | |
key = OpenSSL::Digest::SHA256.digest(shared_key + "\x00\x00\x00\x01" + shared_info) | |
# Encrypt the payload using AES-256-GCM | |
# The iv is set to zero since we're only using the key once. | |
cipher = OpenSSL::Cipher::AES.new(256, :GCM) | |
cipher.encrypt | |
cipher.key = key | |
cipher.iv_len = 16 | |
cipher.iv = [0,0,0,0].pack("N*") | |
cipher.auth_data = "" | |
ciphertext = cipher.update(payload) + cipher.final | |
# Final cryptogram is 106 bytes in size: | |
# - 64 bytes public key | |
# - 26 bytes encrypted data | |
# - 16 bytes AES-GCM tag | |
cryptogram = shared_info + ciphertext + cipher.auth_tag | |
# Array to hold our transmitted messages for decryption in the server section below | |
messages = [] | |
10.times do | |
payload = [ | |
[country_code].pack("n"), # 16-bit integer, unsigned, network order | |
cryptogram, | |
[128].pack("C"), # tx power, 8-bit integer | |
[Time.now.to_i].pack("N"), # time of broadcast, 32-bit integer, unsigned, network order | |
].join | |
# Generate and append HMAC for verification using secret key linked to uuid | |
payload += OpenSSL::HMAC.digest("SHA256", payload, secret_key)[0..15] | |
# Transmit | |
messages << payload.unpack("H*").first.upcase | |
puts messages.last | |
# Application transmits every 8 seconds in reality | |
sleep 2 | |
end | |
################### | |
### Server-side ### | |
################### | |
# Private key stored securely on server | |
private_key = OpenSSL::PKey.read <<~KEY | |
-----BEGIN EC PRIVATE KEY----- | |
MHcCAQEEINGxYo26SVpkM5uLK2PmNgRI9UOa8b82a1nE4ggL2lGNoAoGCCqGSM49 | |
AwEHoUQDQgAE5Km+yAxWRLRKJmJbfvZtSneKjlzu0MWLAsmfDa3jzDB9QHEZVez8 | |
RlrX5w7vwjt01qQ9AdcitRxAL/v6vQEjsg== | |
-----END EC PRIVATE KEY----- | |
KEY | |
# Database of registrations | |
# Represented here by a map of uuid => secret key so we can verify the message | |
registrations = { | |
"E1D160C7-F6E8-48BC-8687-63C696D910CB" => "3WEejyPI2UdjPKXb4PnedwqZudEqKURiuFKHzdOKZsE=" | |
} | |
messages.each do |message| | |
data = [message].pack("H*") | |
# Remove the hmac to verify once we know the uuid | |
payload = data[0..-17] | |
hmac = data[-16..-1] | |
# Extract country code, cryptogram, tx power and broadcast time | |
country_code = payload[0..1].unpack("n").first | |
cryptogram = payload[2..107] | |
tx_power = payload[108].unpack("C").first | |
broadcast_at = Time.at(payload[109..-1].unpack("N").first).getutc | |
# Extract the public key, ciphertext and AES GCM tag | |
raw_key = cryptogram[0..63] | |
ciphertext = cryptogram[64..-17] | |
auth_tag = cryptogram[-16..-1] | |
# Convert the raw public key into an OpenSSL::PKey::EC::Point instance | |
public_key = OpenSSL::BN.new("\x04#{raw_key}", 2) | |
public_key = OpenSSL::PKey::EC::Point.new(private_key.group, public_key) | |
# Reconstruct the shared key from the ephemeral public key | |
shared_key = private_key.dh_compute_key(public_key) | |
# Reconstruct AES key using ANSI x9.63 KDF | |
key = OpenSSL::Digest::SHA256.digest(shared_key + "\x00\x00\x00\x01" + raw_key) | |
# Decrypt the ciphertext using AES-256-GCM | |
cipher = OpenSSL::Cipher::AES.new(256, :GCM) | |
cipher.decrypt | |
cipher.iv_len = 16 | |
cipher.key = key | |
cipher.iv = [0,0,0,0].pack("N*") | |
cipher.auth_tag = auth_tag | |
cipher.auth_data = "" | |
plaintext = cipher.update(ciphertext) + cipher.final | |
# Extract the message details | |
start_date = Time.at(plaintext[0..3].unpack("N").first).getutc | |
end_date = Time.at(plaintext[4..7].unpack("N").first).getutc | |
uuid_bytes = plaintext[8..-3] | |
country = plaintext[-2..-1].unpack("n").first | |
uuid = "%X%X%X%X-%X%X-%X%X-%X%X-%X%X%X%X%X%X" % uuid_bytes.unpack("C*") | |
if registrations.key?(uuid) | |
server_hmac = OpenSSL::HMAC.digest("SHA256", payload, registrations[uuid])[0..15] | |
if hmac == server_hmac | |
puts <<~MSG | |
------------------------------------------------ | |
UUID: #{uuid} | |
Country: #{country} | |
Start Date: #{start_date} | |
End Date: #{end_date} | |
TX Power: #{tx_power} | |
Time: #{broadcast_at} | |
MSG | |
else | |
puts "Error: invalid message" | |
end | |
else | |
puts "Error: uuid not found" | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
033A33F1694DEC1401F4B544E40CF758775228D18225769D590E27613053AA549830AB667027A5E8A34F81D7601F0186589A0967F31C2946C849DEAC3087A82EF3BB323F2556955F4B3CAEC632B7BA2117F2692476D423ED74643508C7C65D83B5370896BAD6D5E8AC35C5E4805EB5358F7C227668861764F2ABAF0B298CFA3390 | |
033A33F1694DEC1401F4B544E40CF758775228D18225769D590E27613053AA549830AB667027A5E8A34F81D7601F0186589A0967F31C2946C849DEAC3087A82EF3BB323F2556955F4B3CAEC632B7BA2117F2692476D423ED74643508C7C65D83B5370896BAD6D5E8AC35C5E4805EB53591F73760A95259BC38F0DE99B366EA7233 | |
033A33F1694DEC1401F4B544E40CF758775228D18225769D590E27613053AA549830AB667027A5E8A34F81D7601F0186589A0967F31C2946C849DEAC3087A82EF3BB323F2556955F4B3CAEC632B7BA2117F2692476D423ED74643508C7C65D83B5370896BAD6D5E8AC35C5E4805EB53593C70236F6E31AFA70B056C446FE2934DC | |
033A33F1694DEC1401F4B544E40CF758775228D18225769D590E27613053AA549830AB667027A5E8A34F81D7601F0186589A0967F31C2946C849DEAC3087A82EF3BB323F2556955F4B3CAEC632B7BA2117F2692476D423ED74643508C7C65D83B5370896BAD6D5E8AC35C5E4805EB5359570EC26F0300B75AAEBD77CB244A1A7ED | |
033A33F1694DEC1401F4B544E40CF758775228D18225769D590E27613053AA549830AB667027A5E8A34F81D7601F0186589A0967F31C2946C849DEAC3087A82EF3BB323F2556955F4B3CAEC632B7BA2117F2692476D423ED74643508C7C65D83B5370896BAD6D5E8AC35C5E4805EB53597A76F1301B6C95B96251E3F4D34271DDE | |
033A33F1694DEC1401F4B544E40CF758775228D18225769D590E27613053AA549830AB667027A5E8A34F81D7601F0186589A0967F31C2946C849DEAC3087A82EF3BB323F2556955F4B3CAEC632B7BA2117F2692476D423ED74643508C7C65D83B5370896BAD6D5E8AC35C5E4805EB5359973B3B6915FF5F1E319A50E8CCE6551BF | |
033A33F1694DEC1401F4B544E40CF758775228D18225769D590E27613053AA549830AB667027A5E8A34F81D7601F0186589A0967F31C2946C849DEAC3087A82EF3BB323F2556955F4B3CAEC632B7BA2117F2692476D423ED74643508C7C65D83B5370896BAD6D5E8AC35C5E4805EB5359B754152E68B48D38EBAF7DE2A274D7F39 | |
033A33F1694DEC1401F4B544E40CF758775228D18225769D590E27613053AA549830AB667027A5E8A34F81D7601F0186589A0967F31C2946C849DEAC3087A82EF3BB323F2556955F4B3CAEC632B7BA2117F2692476D423ED74643508C7C65D83B5370896BAD6D5E8AC35C5E4805EB5359D6341FEF65CA49B3A239BBBF845B22A9C | |
033A33F1694DEC1401F4B544E40CF758775228D18225769D590E27613053AA549830AB667027A5E8A34F81D7601F0186589A0967F31C2946C849DEAC3087A82EF3BB323F2556955F4B3CAEC632B7BA2117F2692476D423ED74643508C7C65D83B5370896BAD6D5E8AC35C5E4805EB5359FC4F35A7A832E9BF16B517865140AE896 | |
033A33F1694DEC1401F4B544E40CF758775228D18225769D590E27613053AA549830AB667027A5E8A34F81D7601F0186589A0967F31C2946C849DEAC3087A82EF3BB323F2556955F4B3CAEC632B7BA2117F2692476D423ED74643508C7C65D83B5370896BAD6D5E8AC35C5E4805EB535A10D05D561BB4DF48FB28ED22EC57C34F7 | |
------------------------------------------------ | |
UUID: E1D160C7-F6E8-48BC-8687-63C696D910CB | |
Country: 826 | |
Start Date: 2020-05-08 00:00:00 UTC | |
End Date: 2020-05-09 00:00:00 UTC | |
TX Power: 128 | |
Time: 2020-05-08 10:33:51 UTC | |
------------------------------------------------ | |
UUID: E1D160C7-F6E8-48BC-8687-63C696D910CB | |
Country: 826 | |
Start Date: 2020-05-08 00:00:00 UTC | |
End Date: 2020-05-09 00:00:00 UTC | |
TX Power: 128 | |
Time: 2020-05-08 10:33:53 UTC | |
------------------------------------------------ | |
UUID: E1D160C7-F6E8-48BC-8687-63C696D910CB | |
Country: 826 | |
Start Date: 2020-05-08 00:00:00 UTC | |
End Date: 2020-05-09 00:00:00 UTC | |
TX Power: 128 | |
Time: 2020-05-08 10:33:55 UTC | |
------------------------------------------------ | |
UUID: E1D160C7-F6E8-48BC-8687-63C696D910CB | |
Country: 826 | |
Start Date: 2020-05-08 00:00:00 UTC | |
End Date: 2020-05-09 00:00:00 UTC | |
TX Power: 128 | |
Time: 2020-05-08 10:33:57 UTC | |
------------------------------------------------ | |
UUID: E1D160C7-F6E8-48BC-8687-63C696D910CB | |
Country: 826 | |
Start Date: 2020-05-08 00:00:00 UTC | |
End Date: 2020-05-09 00:00:00 UTC | |
TX Power: 128 | |
Time: 2020-05-08 10:33:59 UTC | |
------------------------------------------------ | |
UUID: E1D160C7-F6E8-48BC-8687-63C696D910CB | |
Country: 826 | |
Start Date: 2020-05-08 00:00:00 UTC | |
End Date: 2020-05-09 00:00:00 UTC | |
TX Power: 128 | |
Time: 2020-05-08 10:34:01 UTC | |
------------------------------------------------ | |
UUID: E1D160C7-F6E8-48BC-8687-63C696D910CB | |
Country: 826 | |
Start Date: 2020-05-08 00:00:00 UTC | |
End Date: 2020-05-09 00:00:00 UTC | |
TX Power: 128 | |
Time: 2020-05-08 10:34:03 UTC | |
------------------------------------------------ | |
UUID: E1D160C7-F6E8-48BC-8687-63C696D910CB | |
Country: 826 | |
Start Date: 2020-05-08 00:00:00 UTC | |
End Date: 2020-05-09 00:00:00 UTC | |
TX Power: 128 | |
Time: 2020-05-08 10:34:05 UTC | |
------------------------------------------------ | |
UUID: E1D160C7-F6E8-48BC-8687-63C696D910CB | |
Country: 826 | |
Start Date: 2020-05-08 00:00:00 UTC | |
End Date: 2020-05-09 00:00:00 UTC | |
TX Power: 128 | |
Time: 2020-05-08 10:34:07 UTC | |
------------------------------------------------ | |
UUID: E1D160C7-F6E8-48BC-8687-63C696D910CB | |
Country: 826 | |
Start Date: 2020-05-08 00:00:00 UTC | |
End Date: 2020-05-09 00:00:00 UTC | |
TX Power: 128 | |
Time: 2020-05-08 10:34:09 UTC | |
------------------------------------------------ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I think the ruby implementation of
SecKeyCreateEncryptedData
with the algorithmeciesEncryptionStandardVariableIVX963SHA256AESGCM
is correct - happy to be corrected if not.This is just a direct decryption of the transmitted messages - in reality when the app receives these it also includes the RSSI and submits that along with the above messages to the server so it can do a comparison of apparent signal strength to transmitted power and estimate distance between the two devices