Skip to content

Instantly share code, notes, and snippets.

@pixeltrix
Last active May 9, 2020 21:53
Show Gist options
  • Save pixeltrix/6ce590b710513af4c011f74c96ef3a49 to your computer and use it in GitHub Desktop.
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
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
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
------------------------------------------------
@pixeltrix
Copy link
Author

I think the ruby implementation of SecKeyCreateEncryptedData with the algorithm eciesEncryptionStandardVariableIVX963SHA256AESGCM 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment