When J.J. and I designed the Hacker School door system, one of our problems was keeping the system secure.
There are two parts of the door system, DoorDuino, which is an Arduino that unlocks the door, and Doorbot, which is a Sinatra server that registers users and receives text messages. These two pieces talk to each other over HTTP. When it receives a text, Doorbot sends an HTTP request to DoorDuino, which unlocks the door.
Unfortunately, this system isn't the most secure. If an attacker were to spy on the connection between the two, they could see the unlock requests, and just send them again to unlock the door whenever they wanted, circumventing the Twilio number verification. We need some sort of encryption to protect the connection between the devices.
The obvious solution would have been SSL. With SSL, the URL that is being requested is encrypted from attackers, and the connection is protected from replay attacks. Even if an attacker were to copy one of the Sinatra server's SSL-protected requests bit by bit, and try to resend it, the Arduino would reject it. This would have been the perfect solution, except for one problem — Arduinos do not support SSL, and probably wouldn't have enough memory to do it properly, anyway.
If we don't have enough horsepower to do SSL, is there any crypto we can do? Thankfully, there is a SHA-256 library for Arduinos, so we decided to use that to sign the incoming HTTP requests. SHA-256 is a cryptographic hash, which is basically one-way encryption. Any message can be easily encrypted into a SHA256 hash, but if you have a SHA-256 hash, it's very, very difficult to figure out what message was put in, especially if the message was long. If we needed the message to be secret, for an attacker to not know what message was being sent, we would have had to use more complicated two-way encryption. Fortunately, we don't care if attackers can see the message. We just want to make sure that they can't fake a new unlock request of their own.
So, with this basic encryption strategy, we take the hash of the password, and then send it over to the Arduino.
sha256("SUPER SECRET PASSWORD")
The Arduino calculates the sha256
of "SUPER SECRET PASSWORD"
, and makes sure that the hashes match before unlocking the door.
However, it's pretty obvious that this is insecure. If an attacker were spying on the connection to the Arduino, they would see a request like this come in.
POST /unlock/e88798b1ebdf2e7b9f698255b5dcb5270a6fcc92c4b HTTP/1.1
The second part of the URL is the hash of the password. Since this is a hash, the attacker can't figure out the original password1. However, the hacker could just send the exact same POST
request to the Arduino, and the door would unlock. They don't need to know the password to unlock the door. This is called a replay attack.
In order to protect our system against replay attacks, we need a way to make each valid request unique. If identical requests are valid, the system is susceptible to replay attacks. Usually, the way to make requests unique like this is by using a cryptographic nonce, which is a unique number or word used in the request that is only valid once. SSL uses nonces with a handshake, which would work something like this:
- Sinatra requests a nonce from the Arduino.
- The Arduino sends a unique nonce,
19283029
, to Sinatra. - Sinatra computes the hash of the nonce plus the password:
sha256("SUPER SECRET PASSWORD" + "19283029")
, and sends it to the Arduino. - The Arduino makes sure the hash is valid, and unlocks the door.
With this method, if an attacker were to attempt to replay the attack, the nonce would have already been used, and the Arduino would reject the request. And, since the attacker needs to know the password to create new valid requests, they can't just request a new nonce and send a request with it.
However, this requires the Arduino to do quite a bit, and we want our system to be as simple as possible, so instead, we use a simple incrementing nonce. Instead of a handshake, the Doorbot sends an unlock request to the Arduino right away. To do this, Doorbot finds the last nonce used, and increments it by one to get a new nonce, which we'll say is 182
. Doorbot then calculates the hash of the nonce added to the password.
sha256("secret_password" + "182")
# => 72672c56a4a44c86c2fb47d02365c46a988fbde44daaf9d7dd7ee393fa2a3a0c
Doorbot then sends an HTTP request to the Arduino with the nonce and the hash:
POST /182/72672c56a4a44c86c2fb47d02365c46a988fbde44daaf9d7dd7ee393fa2a3a0c HTTP/1.1
The Arduino receives the request, makes sure the nonce is larger than any previous nonce used, and that the hash is actually the hash of the nonce plus the secret password. If everything is good, the Arduino unlocks the door.
If an attacker tries to resend a request, the Arduino can see that the nonce is not larger, and reject the request. If they just increment the nonce, they'll be unable to generate a valid hash without the secret password, so that request will fail. Additionally, the Arduino doesn't have to keep any state except the largest nonce used so far. The handshake method would require the Arduino to generate and keep an array of valid nonces, which would have been more complicated and difficult to debug. This method is also nice because it does not require the Arduino to have any concept of time2.
So this method seems pretty secure, right? I thought so too, but Mindy Preston told me to look up hash length extension attacks. The system above is potentially vulnerable. In the next blog post, I'll use my SHA-256 implementation in Rust to explain how to exploit a length extension attack, how the internals of SHA-256 make it possible, and how to protect against it.
Assuming the original password was long enough. 2: Getting the current time is surprisingly difficult on the Arduino.