diff --git a/beginner/README.md b/beginner/README.md index fc16b0f..c885d69 100644 --- a/beginner/README.md +++ b/beginner/README.md @@ -386,6 +386,22 @@ If you haven't use a stack-allocated collection before note that you'll need to P.S. The plaintext string is *not* stored in `puzzle.hex` so running `strings` on it will not give you the answer. +These are our recommended steps to tackle the problem. Each step is demonstrated in a separate example: + +1. Send a one letter packet (e.g. `A`) to the radio to get a feel for how the mapping works. Then do a few more letters. Check out example `radio-puzzle-1` + +2. Get familiar with the dictionary API. Do some insertions and look ups. What happens if the dictionary gets full? See `radio-puzzle-2` + +3. Next, get mappings from the radio and insert them into the dictionary. See `radio-puzzle-3` + +4. You'll probably want a buffer to place the plaintext in. We suggest using `heapless::Vec` for this. See `radio-puzzle-4` (NB It is also possible to decrypt the packet in place) + +5. Simulate decryption: fetch the encrypted string and "process" each of its bytes. See `radio-puzzle-5` + +6. Now merge steps 3 and 5: build a dictionary, retrieve the secret string and do the reverse mapping to decrypt the message. See `radio-puzzle-6` + +7. As a final step, send the decrypted string to the Dongle and check if it was correct or not. See `radio-puzzle-7` + ## Starting a project from scratch So far we have been using a pre-made Cargo project to work with the nRF52840 DK. In this section we'll see how to create a new embedded project for any microcontroller. diff --git a/beginner/apps/Cargo.lock b/beginner/apps/Cargo.lock index 92c0383..02b2681 100644 --- a/beginner/apps/Cargo.lock +++ b/beginner/apps/Cargo.lock @@ -7,10 +7,22 @@ dependencies = [ "cortex-m", "cortex-m-rt", "dk", + "heapless", "log", "panic-log", ] +[[package]] +name = "as-slice" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37dfb65bc03b2bc85ee827004f14a6817e04160e3b1a28931986a666a9290e70" +dependencies = [ + "generic-array 0.12.3", + "generic-array 0.13.2", + "stable_deref_trait", +] + [[package]] name = "bare-metal" version = "0.2.5" @@ -26,6 +38,12 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + [[package]] name = "cast" version = "0.2.3" @@ -104,6 +122,45 @@ dependencies = [ "typenum", ] +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed1e761351b56f54eb9dcd0cfaca9fd0daecf93918e1cfc01c8a3d26ee7adcd" +dependencies = [ + "typenum", +] + +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73a8a2391a3bc70b31f60e7a90daa5755a360559c0b6b9c5cfc0fee482362dc0" +dependencies = [ + "as-slice", + "generic-array 0.13.2", + "hash32", + "stable_deref_trait", +] + [[package]] name = "log" version = "0.4.8" @@ -233,6 +290,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "stable_deref_trait" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" + [[package]] name = "syn" version = "1.0.30" diff --git a/beginner/apps/Cargo.toml b/beginner/apps/Cargo.toml index a5483c0..f8b9db6 100644 --- a/beginner/apps/Cargo.toml +++ b/beginner/apps/Cargo.toml @@ -8,6 +8,7 @@ version = "0.1.0" cortex-m = "0.6.2" cortex-m-rt = "0.6.12" dk = { path = "../../boards/dk", features = ["beginner"] } +heapless = "0.5.5" log = "0.4.8" panic-log = { path = "../../common/panic-log" } diff --git a/beginner/apps/src/bin/radio-puzzle-1.rs b/beginner/apps/src/bin/radio-puzzle-1.rs new file mode 100644 index 0000000..7713554 --- /dev/null +++ b/beginner/apps/src/bin/radio-puzzle-1.rs @@ -0,0 +1,57 @@ +#![deny(unused_must_use)] +#![no_main] +#![no_std] + +use cortex_m_rt::entry; +use dk::ieee802154::{Channel, Packet}; +use panic_log as _; // the panicking behavior + +const TEN_MS: u32 = 10_000; + +#[entry] +fn main() -> ! { + let board = dk::init().unwrap(); + let mut radio = board.radio; + let mut timer = board.timer; + + // puzzle.hex uses channel 25 + radio.set_channel(Channel::_25); + + let mut packet = Packet::new(); + + // letter 'A' (uppercase) + let source = 65; + // let source = b'A'; // this is the same as above + // TODO try other letters + + // single letter (byte) packet + packet.copy_from_slice(&[source]); + + radio.send(&packet); + + if radio.recv_timeout(&mut packet, &mut timer, TEN_MS).is_ok() { + // response should be one byte large + if packet.len() == 1 { + let destination = packet[0]; + + log::info!("{} -> {}", source, destination); + // or cast to `char` for a more readable output + log::info!("{:?} -> {:?}", source as char, destination as char); + } else { + log::error!("response packet was not a single byte"); + dk::exit() + } + } else { + log::error!("no response or response packet was corrupted"); + dk::exit() + } + + // TODO next do the whole ASCII range [0, 127] + // start small: just 'A' and 'B' at first + // OR for source in b'A'..=b'B' (NOTE: inclusive range) + for source in 65..67 { + // TODO similar procedure as above + } + + dk::exit() +} diff --git a/beginner/apps/src/bin/radio-puzzle-2.rs b/beginner/apps/src/bin/radio-puzzle-2.rs new file mode 100644 index 0000000..cfb8d53 --- /dev/null +++ b/beginner/apps/src/bin/radio-puzzle-2.rs @@ -0,0 +1,37 @@ +#![deny(unused_must_use)] +#![no_main] +#![no_std] + +use cortex_m_rt::entry; +// NOTE you can use `FnvIndexMap` instead of `LinearMap`; the former may have better +// lookup performance when the dictionary contains a large number of items but performance is +// not important for this exercise +use heapless::{consts, LinearMap}; +use panic_log as _; // the panicking behavior + +#[entry] +fn main() -> ! { + dk::init().unwrap(); + + // a dictionary with capacity for 2 elements + let mut dict = LinearMap::<_, _, consts::U2>::new(); + // ^^ capacity; this is a type not a value + + // do some insertions + dict.insert(b'A', b'*').expect("dictionary full"); + dict.insert(b'B', b'/').expect("dictionary full"); + + // do some lookups + let key = b'A'; + let value = dict[&key]; // the key needs to be passed by reference + + log::info!("{} -> {}", key, value); + // more readable + log::info!("{:?} -> {:?}", key as char, value as char); + + // TODO try another insertion + // TODO try looking up a key not contained in the dictionary + // TODO try changing the capacity + + dk::exit() +} diff --git a/beginner/apps/src/bin/radio-puzzle-3.rs b/beginner/apps/src/bin/radio-puzzle-3.rs new file mode 100644 index 0000000..fd7e6ad --- /dev/null +++ b/beginner/apps/src/bin/radio-puzzle-3.rs @@ -0,0 +1,51 @@ +#![deny(unused_must_use)] +#![no_main] +#![no_std] + +use cortex_m_rt::entry; +use dk::ieee802154::{Channel, Packet}; +use heapless::{consts, LinearMap}; +use panic_log as _; // the panicking behavior + +const TEN_MS: u32 = 10_000; + +#[entry] +fn main() -> ! { + let board = dk::init().unwrap(); + let mut radio = board.radio; + let mut timer = board.timer; + + // puzzle.hex uses channel 25 + radio.set_channel(Channel::_25); + + // TODO increase capacity + let mut dict = LinearMap::::new(); + + let mut packet = Packet::new(); + // TODO do the whole ASCII range [0, 127] + for source in b'A'..=b'B' { + packet.copy_from_slice(&[source]); + + radio.send(&packet); + + if radio.recv_timeout(&mut packet, &mut timer, TEN_MS).is_ok() { + // response should be one byte large + if packet.len() == 1 { + let destination = packet[0]; + + // TODO insert the key-value pair + // dict.insert(/* ? */, /* ? */).expect("dictionary full"); + } else { + log::error!("response packet was not a single byte"); + dk::exit() + } + } else { + log::error!("no response or response packet was corrupted"); + dk::exit() + } + } + + log::info!("{:?}", dict); + + dk::exit() +} diff --git a/beginner/apps/src/bin/radio-puzzle-4.rs b/beginner/apps/src/bin/radio-puzzle-4.rs new file mode 100644 index 0000000..9a1295e --- /dev/null +++ b/beginner/apps/src/bin/radio-puzzle-4.rs @@ -0,0 +1,36 @@ +#![deny(unused_must_use)] +#![no_main] +#![no_std] + +use core::str; + +use cortex_m_rt::entry; +use heapless::{consts, Vec}; +use panic_log as _; // the panicking behavior + +#[entry] +fn main() -> ! { + dk::init().unwrap(); + + // a buffer with capacity for 2 bytes + let mut buffer = Vec::::new(); + // ^^ capacity; this is a type not a value + + // do some insertions + buffer.push(b'H').expect("buffer full"); + buffer.push(b'i').expect("buffer full"); + + // look into the contents so far + log::info!("{:?}", buffer); + // or more readable + // NOTE as long as you only push bytes in the ASCII range (0..=127) the conversion should work + log::info!( + "{}", + str::from_utf8(&buffer).expect("content was not UTF-8") + ); + + // TODO try another insertion + // TODO try changing the capacity + + dk::exit() +} diff --git a/beginner/apps/src/bin/radio-puzzle-5.rs b/beginner/apps/src/bin/radio-puzzle-5.rs new file mode 100644 index 0000000..8196c05 --- /dev/null +++ b/beginner/apps/src/bin/radio-puzzle-5.rs @@ -0,0 +1,56 @@ +#![deny(unused_must_use)] +#![no_main] +#![no_std] + +use core::str; + +use cortex_m_rt::entry; +use dk::ieee802154::{Channel, Packet}; +use heapless::{consts, Vec}; +use panic_log as _; // the panicking behavior + +const TEN_MS: u32 = 10_000; + +#[entry] +fn main() -> ! { + let board = dk::init().unwrap(); + let mut radio = board.radio; + let mut timer = board.timer; + + // puzzle.hex uses channel 25 + radio.set_channel(Channel::_25); + + let mut packet = Packet::new(); + + /* # Retrieve the secret string */ + packet.copy_from_slice(&[]); // empty packet + radio.send(&packet); + + if radio.recv_timeout(&mut packet, &mut timer, TEN_MS).is_err() { + log::error!("no response or response packet was corrupted"); + dk::exit() + } + + log::info!( + "ciphertext: {}", + str::from_utf8(&packet).expect("packet was not valid UTF-8") + ); + + /* # Decrypt the string */ + let mut buf = Vec::::new(); + + // iterate over the bytes + for input in packet.iter() { + // process each byte + // here we should do the reverse mapping; instead we'll do a shift for illustrative purposes + let output = input + 1; + buf.push(output).expect("buffer full"); + } + + log::info!( + "plaintext: {}", + str::from_utf8(&buf).expect("buffer contains non-UTF-8 data") + ); + + dk::exit() +} diff --git a/beginner/apps/src/bin/radio-puzzle-6.rs b/beginner/apps/src/bin/radio-puzzle-6.rs new file mode 100644 index 0000000..2d01721 --- /dev/null +++ b/beginner/apps/src/bin/radio-puzzle-6.rs @@ -0,0 +1,82 @@ +#![deny(unused_must_use)] +#![no_main] +#![no_std] + +use core::str; + +use cortex_m_rt::entry; +use dk::ieee802154::{Channel, Packet}; +use heapless::{consts, LinearMap, Vec}; +use panic_log as _; // the panicking behavior + +const TEN_MS: u32 = 10_000; + +#[entry] +fn main() -> ! { + let board = dk::init().unwrap(); + let mut radio = board.radio; + let mut timer = board.timer; + + /* # Build a dictionary */ + // TODO increase capacity + let mut dict = LinearMap::::new(); + + // puzzle.hex uses channel 25 + radio.set_channel(Channel::_25); + + let mut packet = Packet::new(); + // TODO do the whole ASCII range [0, 127] + for source in b'A'..=b'B' { + packet.copy_from_slice(&[source]); + + radio.send(&packet); + + if radio.recv_timeout(&mut packet, &mut timer, TEN_MS).is_ok() { + // response should be one byte large + if packet.len() == 1 { + let destination = packet[0]; + + // TODO insert the key-value pair + // dict.insert(/* ? */, /* ? */).expect("dictionary full"); + } else { + log::error!("response packet was not a single byte"); + dk::exit() + } + } else { + log::error!("no response or response packet was corrupted"); + dk::exit() + } + } + + /* # Retrieve the secret string */ + packet.copy_from_slice(&[]); // empty packet + radio.send(&packet); + + if radio.recv_timeout(&mut packet, &mut timer, TEN_MS).is_err() { + log::error!("no response or response packet was corrupted"); + dk::exit() + } + + log::info!( + "ciphertext: {}", + str::from_utf8(&packet).expect("packet was not valid UTF-8") + ); + + /* # Decrypt the string */ + let mut buffer = Vec::::new(); + + // iterate over the bytes + for byte in packet.iter() { + // NOTE this should map from the encrypted letter to the plaintext letter + let key = byte; + let value = dict[key]; + buffer.push(value).expect("buffer full"); + } + + log::info!( + "plaintext: {}", + str::from_utf8(&buffer).expect("buffer contains non-UTF-8 data") + ); + + dk::exit() +} diff --git a/beginner/apps/src/bin/radio-puzzle-7.rs b/beginner/apps/src/bin/radio-puzzle-7.rs new file mode 100644 index 0000000..a742fb0 --- /dev/null +++ b/beginner/apps/src/bin/radio-puzzle-7.rs @@ -0,0 +1,97 @@ +#![deny(unused_must_use)] +#![no_main] +#![no_std] + +use core::str; + +use cortex_m_rt::entry; +use dk::ieee802154::{Channel, Packet}; +use heapless::{consts, LinearMap, Vec}; +use panic_log as _; // the panicking behavior + +const TEN_MS: u32 = 10_000; + +#[entry] +fn main() -> ! { + let board = dk::init().unwrap(); + let mut radio = board.radio; + let mut timer = board.timer; + + /* # Build a dictionary */ + // TODO increase capacity + let mut dict = LinearMap::::new(); + + // puzzle.hex uses channel 25 + radio.set_channel(Channel::_25); + + let mut packet = Packet::new(); + // TODO do the whole ASCII range [0, 127] + for source in b'A'..=b'B' { + packet.copy_from_slice(&[source]); + + radio.send(&packet); + + if radio.recv_timeout(&mut packet, &mut timer, TEN_MS).is_ok() { + // response should be one byte large + if packet.len() == 1 { + let destination = packet[0]; + + // TODO insert the key-value pair + // dict.insert(/* ? */, /* ? */).expect("dictionary full"); + } else { + log::error!("response packet was not a single byte"); + dk::exit() + } + } else { + log::error!("no response or response packet was corrupted"); + dk::exit() + } + } + + /* # Retrieve the secret string */ + packet.copy_from_slice(&[]); // empty packet + radio.send(&packet); + + if radio.recv_timeout(&mut packet, &mut timer, TEN_MS).is_err() { + log::error!("no response or response packet was corrupted"); + dk::exit() + } + + log::info!( + "ciphertext: {}", + str::from_utf8(&packet).expect("packet was not valid UTF-8") + ); + + /* # Decrypt the string */ + let mut buffer = Vec::::new(); + + // iterate over the bytes + for byte in packet.iter() { + // NOTE this should map from the encrypted letter to the plaintext letter + let key = byte; + let value = dict[key]; + buffer.push(value).expect("buffer full"); + } + + log::info!( + "plaintext: {}", + str::from_utf8(&buffer).expect("buffer contains non-UTF-8 data") + ); + + /* # Verify decrypted text */ + packet.copy_from_slice(&buffer); + + radio.send(&packet); + + if radio.recv_timeout(&mut packet, &mut timer, TEN_MS).is_err() { + log::error!("no response or response packet was corrupted"); + dk::exit() + } + + log::info!( + "Dongle response: {}", + str::from_utf8(&packet).expect("response was not UTF-8") + ); + + dk::exit() +}