I've been curious of setting up a challenge response server and client for awhile. I'm still wearing a Pebble daily, and haven't touched c in years, so I figured I should put this all together and throw some encryption in the mix.
I still haven't found a better smartwatch. Now this may be me holding on to what the pebble was and not what it is now, but theres been nothing come market since that I'm interested in putting on my wrist. The open source nature and great documentation led to my first "embedded" like programming experience. Making watchfaces that grabbed weather and put it on your wrist, or counting how many beers you've had that night. It was both fun and some of the first, and arguable still, only programs I've written that get used, let alone daily.
With the advent of big data and advertisement payouts, the days of having something on your wrist that isn't tracking literally your life, are over. Selling your average heart rate to the big ugly must be quite the business. Following the sale of Pebble to Fitbit for $23 million, which was complete robbery after Pebble's 3/4 billion dollar offers years prior. But they just couldn't make it work. What Fitbit discovered was you don't make money off the device, you make money off the data. Which must be the reason the king of ads is attempting to close the purchase of Fitbit this year for a whopping $2.1 billion.
Sorry about that I'm just still bitter over great open-source and developer friendly companies not being able to make it vs the tech giants, without selling their soles to the devil that is.
So now that, that's off my chest we can look at what I've actually done here. A challenge-response client running on the Pebble and a server written in rust.
For the client I've utilized a great little open source project called Tiny-AES-c. It aims to be the smallest c implementation of AES available and clocking in at around 550 loc its not bad. Most importantly it runs easily on the Pebble. Here's the source Tiny-AES-c.
The first step was to get encryption working on the Pebble. This was easy,
follow the README.md
for Tiny-AES and we get encryption working.
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, KEY, IV);
AES_CBC_encrypt_buffer(&ctx, cipher_buf, cipher_size);
Now this was great until I started writing the server and used a CBC implementation that added Pkcs7 padding. It took me about a day of back and forth testing, "is the null byte included... no it mustn't be... wait yes it is... nope, nope it's definitely not". Something that I wish I could find evidence of somewhere online but no luck.
So our final encryption function looks like this:
static int encrypt(char buf[]) {
int blocksize = 16;
int plaintext_len = strlen(buf);
int pad_size = blocksize - (plaintext_len % blocksize);
int cipher_size = plaintext_len + pad_size;
uint8_t cipher_buf[cipher_size];
memcpy(cipher_buf, buf, cipher_size);
memset(cipher_buf + plaintext_len, (char)pad_size, pad_size);
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, KEY, IV);
AES_CBC_encrypt_buffer(&ctx, cipher_buf, cipher_size);
char hex_buf[cipher_size * 2];
int j = 0;
for (int i = 0; i < cipher_size; i++) {
j += snprintf(hex_buf + j, 4, "%02x", cipher_buf[i]);
}
memcpy(buf, hex_buf, sizeof(hex_buf));
return sizeof(hex_buf);
}
We grab the length of our text, pad size, and the size of the nearest blocksize divisible buffer that will fit our data. Copy our data into a buffer of perfect size, then encrypt the buffer and write out the data as a hex string.
For decryption we have a somewhat similar implementation:
static void decrypt(char buf[]) {
struct AES_ctx ctx;
int hex_string_size = strlen(buf);
// create buffer to decode hex in
char decode_buf[hex_string_size];
memcpy(decode_buf, buf, hex_string_size);
// for every 1 hex byte ie 2 chars there is 1 int
uint8_t cipher_buf[hex_string_size/2];
memset(cipher_buf, 0, sizeof(cipher_buf));
int i, j;
for (i = 0, j = 0; i < hex_string_size; i += 2, j++) {
char hex_byte[2];
int hex_byte_size = sizeof(char) * 2;
// read 2 char bytes of the hex string into buffer
memcpy(&hex_byte, decode_buf + i, hex_byte_size);
int int_val = hex_to_int(hex_byte, hex_byte_size);
cipher_buf[j] = int_val;
}
int cipher_buf_size = sizeof(cipher_buf);
// decrypt challenge
AES_init_ctx_iv(&ctx, KEY, IV);
AES_CBC_decrypt_buffer(&ctx, cipher_buf, cipher_buf_size);
// copy decrypted string back into buffer passed to function
memset(buf, 0, hex_string_size);
memcpy(buf, cipher_buf, cipher_buf_size);
int pad = cipher_buf[cipher_buf_size - 1];
for (int i = cipher_buf_size - 1; i >= 0; i--) {
if (buf[i] != pad) {
buf[i + 1] = '\0';
break;
}
}
}
Here we copy a hex string into a buffer for decoding. Turning the hex string into an array of u8s. Then run that through decryption and write out our decrypted string into a buffer and remove all the Pkcs7 padding.
Unfortunately the pebble doesn't include some c string functions like strtoul, so I included a hex to int converter to take the hex byte and get an integer representation.
static int hex_to_int(char hex_byte[], int len) {
int base = 1;
int int_val = 0;
for (int i = len - 1; i >= 0; i--) {
if (hex_byte[i]>='0' && hex_byte[i]<='9') {
int_val += (hex_byte[i] - 48)*base;
base = base * 16;
} else if (hex_byte[i]>='a' && hex_byte[i]<='f') {
int_val += (hex_byte[i] - 87)*base;
base = base*16;
}
}
return int_val;
}
Now what we need is a way to request a challenge from the server:
static void request_challenge(){
DictionaryIterator *out_iter;
char message[] = "requesting challenge";
AppMessageResult result = app_message_outbox_begin(&out_iter);
if(result == APP_MSG_OK) {
dict_write_cstring(out_iter, MESSAGE_KEY_challenge, message);
// Send this message
result = app_message_outbox_send();
if(result != APP_MSG_OK) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Error sending the outbox: %d", (int)result);
}
} else {
// The outbox cannot be used right now
APP_LOG(APP_LOG_LEVEL_ERROR, "Error preparing the outbox: %d", (int)result);
}
}
This is simple used as a flag to notify Pebble's Javascript engine which runs on the phone, to then send a request to the server. Here's the Javascript XmlHTTPrequest:
Pebble.addEventListener('ready', function() {
init.eventListeners();
});
init = {
eventListeners: function () {
Pebble.addEventListener('appmessage', function(e) {
appmessage(e);
});
}
}
function appmessage(e) {
var dict = e.payload;
if (dict['challenge']) {
console.log("requesting challenge");
request_challenge();
return;
}
if (dict['response']) {
send_response(JSON.stringify(dict['response']));
}
}
function request_challenge() {
var request = new XMLHttpRequest();
request.onload = function() {
var dict = {
'challenge': this.responseText,
}
Pebble.sendAppMessage(dict, function() {
console.log("Sent challenge to watch for decryption");
}, function(e) {
console.log('Unable to send challenge to watch: ' + JSON.stringify(e));
});
}
request.open('GET', 'http://192.168.1.100:8080/challenge');
request.send()
}
With this we setup a listener for the Pebble 'ready' state and initialize our 'appmessage' handlers. When the app message dictionary contains a key with 'challenge' it initiates a challenge request. The request simply hits a '/challenge' route and gets an encrypted hex string in return.
The hex string gets sent back to the Pebble for decryption using the decryption function above.
static void inbox_received_callback(DictionaryIterator *iter, void *context) {
Tuple *ready_tuple = dict_find(iter, MESSAGE_KEY_challenge);
char *challenge = ready_tuple->value->cstring;
decrypt(challenge);
char buf[INBOX_SIZE];
memcpy(buf, challenge, strlen(challenge) + 1);
append_an_a(buf);
send_response(buf);
}
We then simply call append_an_a() which does exactly that appends an 'a' to the decrypted string.
static void append_an_a(char buf[]) {
int str_len = strlen(buf);
buf[str_len] = 'a';
buf[str_len + 1] = '\0';
}
And then send the response back.
static void send_response(char message[]) {
DictionaryIterator *out_iter;
int cipher_size = encrypt(message);
// explicitly set null bite
message[cipher_size] = '\0';
AppMessageResult result = app_message_outbox_begin(&out_iter);
if(result == APP_MSG_OK) {
dict_write_cstring(out_iter, MESSAGE_KEY_response, message);
// Send this message
result = app_message_outbox_send();
if(result != APP_MSG_OK) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Error sending the outbox: %d", (int)result);
}
} else {
// The outbox cannot be used right now
APP_LOG(APP_LOG_LEVEL_ERROR, "Error preparing the outbox: %d", (int)result);
}
}
The response is encrypted before being sent back to the server.
So that's the basics of the Pebble/client side of the project. Now let's check out the server.
For the server I used the hyper web framework, which is both quite easy to implement and by default supports tokio, which is something I haven't played around with up until this project.
Our main function sets up the hyper framework and initializes a sharable reference to a challenge string which can be passed through to the hyper services. This runs asynchronously allowing for the possibility of tons of simultaneous connections. Which is totally warranted for this case, where there is specifically always a single client. It's cool alright I just wanted to try it out.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = ([0, 0, 0, 0], 8080).into();
let challenge = Arc::new(Mutex::new(None::<String>));
let service = make_service_fn(move |_| {
let challenge_ref = Arc::clone(&challenge);
async move {
Ok::<_, hyper::Error>(service_fn(move |req| {
service_handler(req, challenge_ref.clone())
}))
}
});
let server = Server::bind(&addr).serve(service);
println!("Listening on http://{}", addr);
server.await?;
Ok(())
}
In the 'service_handler()' function we describe our routes. Let's look at '/challenge' first.
async fn service_handler(
req: Request<Body>,
challenge: Arc<Mutex<Option<String>>>
) -> Result<Response<Body>, hyper::Error> {
let mut challenge = challenge.lock().await;
match (req.method(), req.uri().path()) {
(&Method::GET, "/challenge") => {
let challenge_string = create_challenge()
.await
.unwrap_or("unable to aquire challenge".to_string());
*challenge = Some(challenge_string.clone());
let enc_string = encrypt_str(&challenge_string);
Ok(Response::new(enc_string.into()))
},
Here we can see the other side of the discussion. Where '/challenge' calls a create_challenge() function placing the result into our shared reference to the 'challenge' variable in the main function. It then calls encrypt_str() with a reference to the challenge string returning the result as a response.
So in create_challenge() we'll create our challenge.
async fn create_challenge() -> Result<String, Box<dyn error::Error>> {
let mut rng = thread_rng();
let challenge_size = rng.gen_range(32, 62);
println!("Challenge size: {}", challenge_size);
let challenge_string: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.take(challenge_size)
.collect();
Ok(challenge_string)
}
Use rng to grab a random string size then fill a string with random alphanumeric characters and return. For some reason the max size of the challenge can be 62. Something to do with my buffer sizes in the c code, also something I'm not particularly interested in debugging... 62's good enough right...
Now that we've got a challenge string we'll encrypt it.
fn encrypt_str(challenge_string: &str) -> String {
type Aes128Cbc = Cbc<Aes128, Pkcs7>;
let iv = hex!("000102030405060708090a0b0c0d0e0f");
let key = hex!("2b7e151628aed2a6abf7158809cf4f3c");
let plaintext = challenge_string.as_bytes();
let cipher = Aes128Cbc::new_var(&key, &iv).unwrap();
// buffer must have enough space for message+padding
let mut buffer = [0u8; 1024];
// copy message to the buffer
let pos = plaintext.len();
buffer[..pos].copy_from_slice(plaintext);
let ciphertext = cipher.encrypt(&mut buffer, pos).unwrap();
let mut hex_buf = String::new();
for n in ciphertext {
hex_buf.push_str(format!("{:02x}", n).as_ref());
}
// println!("{}", hex_buf);
hex_buf.into()
}
Initialize an AES implementation using Pkcs7 padding and read our challenge_string into a byte array. Next fill an oversized buffer with our plaintext and encrypt it. Then take our encrypted byte array and write it out as a hex string and return it. This hex string then gets sent to the client.
The client as described above decrypts, appends an 'a', re-encrypts and sends the response back which we handle here in the '/response' route.
(&Method::POST, "/response") => {
let bytes = body::to_bytes(req.into_body()).await?;
let hex = str::from_utf8(&bytes)
.expect("hex to string failure")
.replace("\"", "");
let response_plaintext = decrypt_str(hex.into());
if let Some(c) = &mut *challenge {
c.push_str("a");
println!("\nChallenge: {}\n", c);
println!("Response: {}\n", response_plaintext);
if c == &response_plaintext {
Ok(Response::new("ya done did it".into()))
} else {
Ok(Response::new("Not a chance fool".into()))
}
} else {
Ok(Response::new("You have not requested a challenge".into()))
}
}
_ => {
println!("sending default response");
Ok(Response::new("Hello, World".into()))
}
}
}
We take the response read it into a string and decrypt it. Now we can finally check the comparison to our stored challenge which we append an 'a' to. If successful a response of "ya done did it" is sent back to the Pebble/client.
fn decrypt_str(enc_response: String) -> String {
type Aes128Cbc = Cbc<Aes128, Pkcs7>;
let iv = hex!("000102030405060708090a0b0c0d0e0f");
let key = hex!("2b7e151628aed2a6abf7158809cf4f3c");
let cipher = Aes128Cbc::new_var(&key, &iv).unwrap();
let mut decoded_hex = hex::decode(enc_response).unwrap();
let decrypted_ciphertext = cipher.decrypt(&mut decoded_hex).unwrap();
let decrypted_str = str::from_utf8(decrypted_ciphertext).unwrap();
// println!("{}", decrypted_str);
decrypted_str.into()
}
That's the implementation it was an interesting project, and although I'm sure my c is atrocious it does work! For this to be actual MFA I'll need to use this as a method to login to something... Maybe my desktop but that'll be another project for now this is good.