Compare commits

...

10 Commits

Author SHA1 Message Date
King Kévin a6a4c1a6cc minor menu display fixes 2024-04-18 05:18:21 +02:00
King Kévin ca87f6cd63 show prog menu 2024-04-18 03:23:30 +02:00
King Kévin 35aef360cf doc: add bootloader 2024-04-18 03:14:59 +02:00
King Kévin bb15bbda72 doc: add attack 2024-04-18 02:59:29 +02:00
King Kévin 47528d2b7e doc: fix typo 2024-04-18 02:47:33 +02:00
King Kévin 5adb4421ef add build date 2024-04-17 07:47:46 +02:00
King Kévin dcf4dedd2b add flash using dfu-util 2024-04-17 06:23:46 +02:00
King Kévin 0c9ae4bbfd fix send tab and enter 2024-04-17 06:23:20 +02:00
King Kévin 16af15e47d minor: fix formatting 2024-04-17 06:22:59 +02:00
King Kévin d17bc7b3a4 add mass erase menu 2024-04-17 06:22:34 +02:00
4 changed files with 225 additions and 78 deletions

View File

@ -4,28 +4,28 @@ purpose
=======
passkey is a USB dongle to paste credentials.
It appears a serial port to enter the credentials, and HID keyboard to paste them.
It appears as a serial port to enter the credentials, and HID keyboard to paste them.
Usage:
- insert the passkey dongle in the USB port, buttons facing up
- insert the passkey dongle in the USB port, with "passkey" text facing up
- it will appear as serial port to enter the credentials, and HID keyboard to paste them
- the red light will blink, indicating it no credentials have been entered yet
- connect to it using your favorite serial terminal (e.g. [putty](https://putty.org/) or [picocom](https://github.com/npat-efault/picocom)) using any baud rate
- the red light will blink, indicating no credentials have been entered yet
- connect to it using your favourite serial terminal (e.g. [putty](https://putty.org/) or [picocom](https://github.com/npat-efault/picocom)) using any baud rate
- press 'c' to enter your credentials to paste (username and password)
- the light will remain on
- press one button of the dongle for it to just paste the password
- press the other button to paste the username and password (tab separated)
You can configure it over the serial terminal:
You can configure passkey over the serial terminal:
- press 'h' to list all available options
- press 'b' to swap the button, changing which is for the password or username and password
- press 'l' to set using keyboard layout should be used: use the same are configured in your OS, else the pasted credentials might not be exactly the ones you entered
- press 'b' to swap the buttons, changing which is for the password or username and password
- press 'l' to set which keyboard layout should be used: use the same are configured in your OS, else some letter in the pasted credentials might be different
- by default the credentials will be cleared 12 hours after they have been entered. Press 'g' to change this time, up to 12 hours
- by default the credentials will be cleared 3 hours after the last time they have been pasted. Press 'r' to change this time, up to 3 hours
- press 'a' to authenticate the device. Enter a random word, and it will provide a corresponding result. Copy the provided text to the [homepage](https://passkey.cuvoodoo.info/). If the response is the same, the device is running the original firmware.
- press 'k' to enter your own key, which you need to remember. You can then authenticate the device locally (without using the homepage), using any SHA256 calculator.
- press 'a' to authenticate the device. Enter a random word, and it will provide an URL and a corresponding result. Click on the link, and if the response is the same, the device is running the original firmware.
- press 'k' to enter your own key, which you need to remember. You can then authenticate the device locally (without using the link), using any SHA256 calculator.
security
========
@ -43,45 +43,61 @@ You can clear the credentials by:
- unplugging the dongle
- pressing both buttons at the same time
- pressing 4 times CapsLock within 2 seconds on your keyboard
- pressing 4 times NumLock within 2 seconds on your keyboard
- pressing 4 times CapsLock or NumLock within 2 seconds on your keyboard
The credentials will be cleared up to 12 hours after they have been entered.
The credentials will be cleared up to 3 hours after the last time they have been pasted.
This decreases the risk of having them leaked if the device is left unattended for too long.
This decreases the risk of having them leaked if the dongle is left unattended for too long.
These timeouts can be configured over the serial port.
The dongle is locked after been programmed.
The dongle can be locked after been programmed.
This prevents from:
- using the SWD interface to attach a debugger on the test point and dump the credentials from the running system
- using the SWD interface to attach a debugger on the test points and dump the credentials from the running system
- using the bootloader to flash a new malicious firmware (e.g. that could store the credentials on non-volatile memory and reveal them later)
- mass erase the device, to re-enable debugging and flashing
The flash memory is also write protected.
Only the last 4 kB of flash are re-writable, as the are used to store the configuration set over serial.
The makes it very hard from exploiting the runtime firmware and overwrite.
The flash memory can also write protected.
Only the last 4 kB of flash are re-writable, as they are used to store the configuration set over serial.
This makes it very hard from exploiting the runtime firmware and overwriting it.
To verify if the firmware is original, use the authentication menu.
If you get the passkey from CuVoodoo, you will also has received it's ID per email.
This ID should the prefix of the authentication token.
Each device has been programmed with an individual key before being locked.
Matching the result with the website ensures the key is the same.
To avoid using the website, you can set a user key, and perform the authentication locally using any SHA256 calculator.
risks
-----
The [STM32F042F6P](https://www.st.com/en/microcontrollers-microprocessors/stm32f042f6.html) micro-controller does not have security certifications.
If the device locking mechanism can be circumvented (e.g. using fault injection), a malicious firmware could be installed.
Because of that, it is not recommended to leave the dongle unattended.
It has a hole to pass a string through and attach it to a key-chain you keep with you.
This string also allows to easily unplug the device from the computer.
It is also recommend to draw on the back of the device, so it becomes unique and hard to tamper with unnoticed.
It is also recommended to draw on the back of the device, so it becomes unique and hard to tamper with unnoticed.
Obermaier and Tatschner [showed in 20179(https://www.usenix.org/system/files/conference/woot17/woot17-paper-obermaier.pdf) how to degrade the readout protection.
This involved decapsulating the chip, which would make it easy to see that the device has been tampered with once you added a drawing onto it.
Other side channel or fault injection attacks could exist to retrieve the key in a less destructive way.
Since the passkey is inexpensive, just toss it away and get a new one if you suspect it has been tampered with.
The other possibility would be for and attacker to intercept the device on the shipping way, extract the ID and corresponding manufacturer key, and programming them on a new chip, along with a malicious firmware.
This malicious device could still pass authentication.
To prevent this attack, you can manufacturer the passkey yourself.
The board design files and firmware source code are open and available.
The bill of material in around $2, and the parts can be hand soldered with little experience.
firmware
========
The devices is based on a [STM32F042F6P](https://www.st.com/en/microcontrollers-microprocessors/stm32f042f6.html) micro-controller.
The firmware uses [TinyUSB](https://github.com/hathach/tinyusb).
The device comes locked, preventing it to be re-flashed.
I connected a DAPlink programmer to flash it, using the SWD test points on the back.
To compile and flash it:
The firmware uses the [TinyUSB](https://github.com/hathach/tinyusb) USB stack.
If you got the passkey from CuVoodoo, it comes configured with a manufacturer key and locked.
You can also request for a blank devices, and manufacturer it and flash the firmware yourself.
To compile the firmware, you will need and ARM GCC toolchain and run these commands:
~~~
git clone https://git.cuvoodoo.info/kingkevin/passkey_fw
@ -90,5 +106,34 @@ git checkout passkey
cd examples/device/hid_cdc_passkey
make BOARD=stm32f042passkey get-deps
make BOARD=stm32f042passkey
make flash
~~~
When no firmware has been programmed on the passkey, the STM32 bootloader will allow to flash the firmware over USB using dfu-util:
~~~
make dfu
~~~
Re-plug the dongle in the USB port and the passkey will boot the flashed firmware.
Using the serial terminal you can then enter the manufacturer key by pressing 'K'.
Finally, you can lock the firmware by pressing 'L'.
This it not revocable.
Once locked, you can re-flash the device, change de manufacturer key, or debug the device.
Before it is locked, you can erase the firmware using 'E'.
Re-plugging the dongle will start the bootloader again, allowing to re-flash a firmware.
You can also connect an SWD programmer, such as a DAPlink, on the test points.
The pinout is a follow:
- G: ground
- C: SWCLK
- D: SWDIO
- T: the UART TX used to print debug messages
You can then flash and debug the firmware.
~~~
make flash
make debug
~~~

View File

@ -0,0 +1,65 @@
#pragma once
/** build year as number */
#define COMPUTE_BUILD_YEAR \
( \
(__DATE__[ 7] - '0') * 1000 + \
(__DATE__[ 8] - '0') * 100 + \
(__DATE__[ 9] - '0') * 10 + \
(__DATE__[10] - '0') \
)
/** build day as number */
#define COMPUTE_BUILD_DAY \
( \
((__DATE__[4] >= '0') ? (__DATE__[4] - '0') * 10 : 0) + \
(__DATE__[5] - '0') \
)
/** check if build month is January */
#define BUILD_MONTH_IS_JAN (__DATE__[0] == 'J' && __DATE__[1] == 'a' && __DATE__[2] == 'n')
/** check if build month is February */
#define BUILD_MONTH_IS_FEB (__DATE__[0] == 'F')
/** check if build month is March */
#define BUILD_MONTH_IS_MAR (__DATE__[0] == 'M' && __DATE__[1] == 'a' && __DATE__[2] == 'r')
/** check if build month is April */
#define BUILD_MONTH_IS_APR (__DATE__[0] == 'A' && __DATE__[1] == 'p')
/** check if build month is May */
#define BUILD_MONTH_IS_MAY (__DATE__[0] == 'M' && __DATE__[1] == 'a' && __DATE__[2] == 'y')
/** check if build month is June */
#define BUILD_MONTH_IS_JUN (__DATE__[0] == 'J' && __DATE__[1] == 'u' && __DATE__[2] == 'n')
/** check if build month is July */
#define BUILD_MONTH_IS_JUL (__DATE__[0] == 'J' && __DATE__[1] == 'u' && __DATE__[2] == 'l')
/** check if build month is August */
#define BUILD_MONTH_IS_AUG (__DATE__[0] == 'A' && __DATE__[1] == 'u')
/** check if build month is September */
#define BUILD_MONTH_IS_SEP (__DATE__[0] == 'S')
/** check if build month is October */
#define BUILD_MONTH_IS_OCT (__DATE__[0] == 'O')
/** check if build month is November */
#define BUILD_MONTH_IS_NOV (__DATE__[0] == 'N')
/** check if build month is December */
#define BUILD_MONTH_IS_DEC (__DATE__[0] == 'D')
/** build month as number */
#define COMPUTE_BUILD_MONTH \
( \
(BUILD_MONTH_IS_JAN) ? 1 : \
(BUILD_MONTH_IS_FEB) ? 2 : \
(BUILD_MONTH_IS_MAR) ? 3 : \
(BUILD_MONTH_IS_APR) ? 4 : \
(BUILD_MONTH_IS_MAY) ? 5 : \
(BUILD_MONTH_IS_JUN) ? 6 : \
(BUILD_MONTH_IS_JUL) ? 7 : \
(BUILD_MONTH_IS_AUG) ? 8 : \
(BUILD_MONTH_IS_SEP) ? 9 : \
(BUILD_MONTH_IS_OCT) ? 10 : \
(BUILD_MONTH_IS_NOV) ? 11 : \
(BUILD_MONTH_IS_DEC) ? 12 : \
/* error default */ 99 \
)
/** check if build date is unknown */
#define BUILD_DATE_IS_BAD (__DATE__[0] == '?')
/** build year as number if known, or 0 if unknown */
#define BUILD_YEAR ((BUILD_DATE_IS_BAD) ? 0 : COMPUTE_BUILD_YEAR)
/** build month as number if known, or 0 if unknown */
#define BUILD_MONTH ((BUILD_DATE_IS_BAD) ? 99 : COMPUTE_BUILD_MONTH)
/** build day as number if known, or 0 if unknown */
#define BUILD_DAY ((BUILD_DATE_IS_BAD) ? 99 : COMPUTE_BUILD_DAY)

View File

@ -13,6 +13,7 @@
#include "usb_descriptors.h"
#include "keylayouts.h"
#include "WjCryptLib_Sha256.h"
#include "date.h"
// get the length of an array
#define LENGTH(x) (sizeof(x) / sizeof((x)[0]))
@ -76,6 +77,12 @@ static const char* help_str[] = {
"l set keyboard layout\r\n",
};
static const char* prog_str[] = {
"K set manufacturer key\r\n",
"L lock device\r\n",
"E erase firmware\r\n",
};
enum {
MENU_HOME,
MENU_USER,
@ -115,6 +122,13 @@ uint32_t board_button2_read(void)
return BUTTON2_STATE_ACTIVE == HAL_GPIO_ReadPin(BUTTON2_PORT, BUTTON2_PIN);
}
static bool locked(void)
{
FLASH_OBProgramInitTypeDef ob;
HAL_FLASHEx_OBGetConfig(&ob);
return (OB_RDP_LEVEL_2 == ob.RDPLevel);
}
static void clear_credentials(void)
{
// clear credentials
@ -139,7 +153,7 @@ static void load_config(void)
if (!manuf_ok) {
memset(manuf_key, 0, sizeof(manuf_key));
}
memcpy(&config, (uint8_t*)FLASH_CONFIG_ADDR, sizeof(config));
uint8_t crc = 0x42; // start with non-zero value to have a checksum work with empty config
for (uint16_t i = 0; i < sizeof(config); i++) {
@ -266,7 +280,9 @@ int main(void)
// init device stack on configured roothub port
tud_init(BOARD_TUD_RHPORT);
printf("\r\npasskey v2\r\n");
printf("\r\npasskey\r\n");
printf("hardware version: 2\r\n"); // for now we just have version 2
printf("firmware date: %04u-%02u-%02u\r\n", BUILD_YEAR, BUILD_MONTH, BUILD_DAY); // show firmware build date
load_config();
FLASH_OBProgramInitTypeDef ob;
@ -368,7 +384,7 @@ void cdc_task(void)
case 'c': // input credentials
i = 0; // reset index
menu = MENU_USER; // go to corresponding menu
str = "\r\nusername: ";
str = "\r\nenter credentials to paste\r\nusername: ";
break;
case 'b': // swap buttons
config.button_swap = !config.button_swap;
@ -402,50 +418,55 @@ void cdc_task(void)
}
break;
case 'l': // set keyboard layout
tud_cdc_write(buf, 1); // since echo will be off
tud_cdc_write_flush();
echo = false;
tud_cdc_write(buf, 1); // since echo will be off
tud_cdc_write_str("\n\r");
tud_cdc_write_flush();
for (uint8_t j = 0; j < LENGTH(name_asciimap); j++) {
snprintf(tmp, sizeof(tmp), "%02u %s\r\n", j, name_asciimap[j]);
tud_cdc_write_str_flush(tmp);
}
snprintf(tmp, sizeof(tmp), "\r\nset keyboard layout [%s]: ", name_asciimap[config.layout]);
snprintf(tmp, sizeof(tmp), "set keyboard layout [%s]: ", name_asciimap[config.layout]);
tud_cdc_write_str_flush(tmp);
i = 0; // reset index
menu = MENU_LAYOUT;
break;
// hidden menu
case 'K': // set manufacturer key
{
FLASH_OBProgramInitTypeDef ob;
HAL_FLASHEx_OBGetConfig(&ob);
if (OB_RDP_LEVEL_2 == ob.RDPLevel) {
str = "\r\ndevice locked\r\n";
} else {
i = 0; // reset index
menu = MENU_MANUF;
str = "\r\nenter manufacturer key (up to 32 char): ";
}
if (!locked()) {
i = 0; // reset index
menu = MENU_MANUF;
str = "\r\nenter manufacturer key (up to 32 char): ";
}
break;
case 'L': // lock device
{
if (!locked()) {
tud_cdc_write_str_flush("\r\nlocking device\r\n");
FLASH_OBProgramInitTypeDef ob;
HAL_FLASHEx_OBGetConfig(&ob);
if (OB_RDP_LEVEL_2 == ob.RDPLevel) {
str = "\r\ndevice locked\r\n";
} else {
str = "\r\nlocking device\r\n";
ob.RDPLevel = OB_RDP_LEVEL_2; // level 2, disabling debug, and write protecting option byte
ob.WRPState = OB_WRPSTATE_ENABLE; // write protect pages
ob.WRPPage = 0x7; // all but last sector holding the config
HAL_FLASH_Unlock();
HAL_FLASH_OB_Unlock();
//HAL_FLASHEx_OBProgram(&ob);
HAL_FLASH_OB_Lock();
HAL_FLASH_Lock();
HAL_FLASH_OB_Launch();
ob.RDPLevel = OB_RDP_LEVEL_2; // level 2, disabling debug, and write protecting option byte
ob.WRPState = OB_WRPSTATE_ENABLE; // write protect pages
ob.WRPPage = 0x7; // all but last sector holding the config
HAL_FLASH_Unlock();
HAL_FLASH_OB_Unlock();
//HAL_FLASHEx_OBProgram(&ob);
HAL_FLASH_OB_Lock();
HAL_FLASH_Lock();
HAL_FLASH_OB_Launch();
}
break;
case 'E': // mass erase to restart STM32 bootloader
if (!locked()) {
tud_cdc_write_str_flush("\r\nerasing firmware\r\n");
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef EraseInitStruct;
EraseInitStruct.TypeErase = FLASH_TYPEERASE_MASSERASE;
uint32_t error;
if (HAL_FLASHEx_Erase(&EraseInitStruct, &error) != HAL_OK) {
printf("mass erase failed\r\n");
}
HAL_FLASH_Lock();
HAL_FLASH_OB_Launch(); // just to restart
}
break;
case '\r':
@ -459,6 +480,11 @@ void cdc_task(void)
for (uint8_t j = 0; j < LENGTH(help_str); j++) {
tud_cdc_write_str_flush(help_str[j]);
}
if (!locked()) {
for (uint8_t j = 0; j < LENGTH(prog_str); j++) {
tud_cdc_write_str_flush(prog_str[j]);
}
}
}
break;
case MENU_USER:
@ -714,33 +740,41 @@ void tud_cdc_rx_cb(uint8_t itf)
//--------------------------------------------------------------------+
// USB HID
//--------------------------------------------------------------------+
static void send_hid_report(char c)
static void send_hid_report(uint8_t modifier, uint8_t keycode)
{
// skip if hid is not ready yet
if ( !tud_hid_ready() ) {
return;
}
// ensure it's a printable ASCII
if (0 != c && c < ' ') {
return;
uint8_t keycodes[6] = {keycode, 0, 0, 0, 0, 0};
if (keycode) {
key_when = board_millis(); // remember when we sent the key
key_pressed = true; // remember we pressed a key
} else {
key_pressed = false; // key released
}
tud_hid_keyboard_report(REPORT_ID_KEYBOARD, modifier, keycodes);
}
static void send_hid_char(char c)
{
// set default to US
if (config.layout >= LENGTH(map_asciimap)) {
config.layout = 0;
}
uint8_t keycode[6] = {0, 0, 0, 0, 0, 0};
if (c) {
if (0 == c) {
send_hid_report(0, 0);
} else if ('\t' == c) {
send_hid_report(0, KEY_TAB);
} else if ('\r' == c || '\n' == c) {
send_hid_report(0, KEY_ENTER);
} else if (c >= ' ' && c <= '~') {
const uint16_t code = map_asciimap[config.layout][c - ' '];
const uint8_t modifier = code >> 8;
keycode[0] = code & 0xff;
tud_hid_keyboard_report(REPORT_ID_KEYBOARD, modifier, keycode);
key_when = board_millis(); // remember when we sent the key
key_pressed = true; // remember we pressed a key
} else {
tud_hid_keyboard_report(REPORT_ID_KEYBOARD, 0, keycode); // release press
key_pressed = false;
const uint8_t keycode = code & 0xff;
send_hid_report(modifier, keycode);
}
}
@ -757,10 +791,10 @@ void hid_task(void)
return;
}
if (pass_paste) { // send password
if (pass_paste < strlen(username)) { // send password
send_hid_report(password[pass_paste++]);
if (pass_paste < strlen(password)) { // send password
send_hid_char(password[pass_paste++]);
} else if (pass_paste == strlen(password)) { // press enter
send_hid_report('\n');
send_hid_char('\n');
pass_paste++;
} else if (pass_paste == (strlen(password) + 1)) { // finished pasting
user_paste = 0;
@ -770,13 +804,13 @@ void hid_task(void)
}
if (user_paste) { // send username
if (user_paste < strlen(username)) { // send username
send_hid_report(username[user_paste++]);
send_hid_char(username[user_paste++]);
} else if (user_paste == strlen(username)) { // switch to password
send_hid_report('\t');
send_hid_char('\t');
user_paste++;
} else if (user_paste == (strlen(username) + 1)) { // start sending password
pass_paste = 0;
send_hid_report(password[pass_paste++]);
send_hid_char(password[pass_paste++]);
user_paste++;
}
}
@ -795,17 +829,17 @@ static void paste_credentials(bool user)
pasted_when = board_millis();
if (user) {
send_hid_report(username[user_paste++]);
send_hid_char(username[user_paste++]);
printf("pasting username and password\r\n");
} else {
send_hid_report(password[pass_paste++]);
send_hid_char(password[pass_paste++]);
printf("pasting password\r\n");
}
}
void button_task(void)
{
static uint32_t button1_ms = 0; // when the button has been pressed (in ms)
static uint32_t button1_ms = 0; // when the button has been pressed (in ms)
static bool button1_ok = false; // if the button is pressed (after debounce)
static uint32_t button2_ms = 0; // when the button has been pressed (in ms)
static bool button2_ok = false; // if the button is pressed (after debounce)
@ -887,7 +921,7 @@ void tud_hid_report_complete_cb(uint8_t instance, uint8_t const* report, uint16_
if ((report[0] == REPORT_ID_KEYBOARD) && key_pressed) {
//tud_hid_keyboard_report(REPORT_ID_KEYBOARD, 0, NULL); // release press
send_hid_report(0); // release key
send_hid_report(0, 0); // release key
//return;
}
}
@ -915,7 +949,7 @@ void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_
static uint8_t kbd_leds_last = 0; // last state
static uint32_t kbd_leds_when = 0; // when did the state change
static uint8_t kbd_leds_count = 0; // how many times it changed (within a certain period)
if (report_type == HID_REPORT_TYPE_OUTPUT) {
// Set keyboard LED e.g Capslock, Numlock etc...
if (report_id == REPORT_ID_KEYBOARD) {

View File

@ -23,3 +23,6 @@ flash: $(BUILD)/$(PROJECT).elf
debug: $(BUILD)/$(PROJECT).elf
$(GDB) --eval-command='target remote | $(OOCD) --file interface/$(OOCD_INTERFACE).cfg --command "transport select swd" --file target/$(OOCD_TARGET).cfg --command "gdb_port pipe; log_output /dev/null; init"' $<
dfu: $(BUILD)/$(PROJECT).bin
dfu-util --device 0483:df11 --cfg 1 --intf 0 --alt 0 --detach --dfuse-address 0x08000000 --download $<