diff --git a/examples/default/Makefile b/examples/default/Makefile index 81b193abc7d1..53adc73763fb 100644 --- a/examples/default/Makefile +++ b/examples/default/Makefile @@ -36,6 +36,9 @@ USEMODULE += shell_commands USEMODULE += ps # include and auto-initialize all available sensors USEMODULE += saul_default +USEMODULE += xtimer +USEMODULE += crypto +USEMODULE += hashes BOARD_PROVIDES_NETIF := acd52832 airfy-beacon b-l072z-lrwan1 cc2538dk fox \ iotlab-m3 iotlab-a8-m3 lobaro-lorabox lsn50 mulle microbit msba2 \ diff --git a/examples/default/login.c b/examples/default/login.c new file mode 100644 index 000000000000..3321ee3ce730 --- /dev/null +++ b/examples/default/login.c @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2019 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @ingroup examples + * @{ + * + * @file + * @brief Proof of concept of user/password protection for the serial + * interface (shell). + * + * @author Juan I Carrano + * + * @} + */ + +#include +#include + +#include "crypto/helper.h" +#include "xtimer.h" + +#include "pbkdf2.h" + +const char user[] = "admin"; + +#define PBKDF2_ITERS 1000 +static const uint8_t salt[] = {0x70, 0x16, 0x2E, 0x1F, 0x2E, 0x38, 0x5, 0x8E, + 0xF0, 0x6B, 0xB1, 0x3, 0xCE, 0xE3, 0x6E, 0x73, + 0x3D, 0x28, 0x5C, 0xEE, 0xDA, 0x15, 0xB0, 0x5B, + 0x3F, 0xF7, 0x67, 0xB6, 0x24, 0xD0, 0xBE, 0x47, + 0x57, 0x2E, 0x43, 0xAA, 0x74, 0xC8, 0xF1, 0x7F, + 0x55, 0x3B, 0x2F, 0xA4, 0xE2, 0x8F, 0xAD, 0x4D, + 0x28, 0x63, 0x27, 0x3, 0x8B, 0xDF, 0x11, 0x23, + 0xB3, 0x4B, 0x97, 0x74, 0x1C, 0x4E, 0xB3, 0x62}; +static const uint8_t key[] = {0xD7, 0xEE, 0xF0, 0xCA, 0xD5, 0x29, 0x92, 0xE1, + 0xDB, 0x40, 0xE0, 0x2B, 0xD5, 0xFC, 0xD1, 0x84, + 0xD5, 0xD2, 0xDB, 0x26, 0xA9, 0xFE, 0x45, 0x64, + 0x4B, 0x6C, 0x9, 0x26, 0xB0, 0x56, 0xCD, 0x47}; + +static bool is_line_delim(char c) +{ + return c == '\r' || c == '\n'; +} + +static bool is_line_cancel(char c) +{ + return c == 0x03 || c == 0x04; +} + +enum LINE_TAINT { + LINE_OK = 0, + LINE_LONG = 1, + LINE_OK_DONE = 2, + LINE_LONG_DONE = 3, + LINE_CANCELLED = 4, +}; + +#define MARK_DONE(t) ((t) | LINE_OK_DONE) +#define IS_DONE(t) ((t) & LINE_OK_DONE) + +/** + * Get a line of input, while echoing it back. + * + * The line is read to line_buf. If mask_char is not zero, then tht character + * is echoed instead of the one inputted. EOF, ctrl-c, ctrl-d cancel the input + * and (-LINE_CANCELLED) is returned. + * Otherwise, return the number of characters read or, if the buffer size was + * exceeded, (-LINE_LONG). + * + * At most buf_size-1 chracters will be stored, and the last character will + * always be a terminator. + * + * Even if the buffer size is exceeded, characters will continue to be read + * from the input. + */ +static int gets_echoing(char *line_buf, size_t buf_size, char mask_char) +{ + size_t length = 0; + enum LINE_TAINT state = LINE_OK; + + do { + int c = getchar(); + + if (c == EOF || is_line_cancel(c)) { + state = LINE_CANCELLED; + } else if (is_line_delim(c)) { + state = MARK_DONE(state); + } else { + if (length + 1 < buf_size) { + line_buf[length++] = c; + } else { + state = LINE_LONG; + } + putchar(mask_char? mask_char: c); + fflush(stdout); + } + } while (state != LINE_CANCELLED && !IS_DONE(state)); + + line_buf[length++] = '\0'; + + return state == LINE_OK_DONE? (int)length : -state; +} + +enum LOGIN_STATE { + LOGIN_WRONG, + LOGIN_OK_ONE, + LOGIN_OK_BOTH, +}; + +static bool login(char *line_buf, size_t buf_size) +{ + int read_len; + int state = LOGIN_WRONG; + + assert(buf_size >= sizeof(user)); + assert(PBKDF2_KEY_SIZE >= sizeof(key)); + + fputs("Username: ", stdout); + fflush(stdout); + + read_len = gets_echoing(line_buf, buf_size, 0); + + putchar('\r'); + putchar('\n'); + + if (read_len == -LINE_CANCELLED) { + goto login_end; + } else if (read_len > 0) { + state += !!crypto_equals((uint8_t*)line_buf, (uint8_t*)user, sizeof(user)); + } + + fputs("Password: ", stdout); + fflush(stdout); + + read_len = gets_echoing(line_buf, buf_size, '*'); + + putchar('\r'); + putchar('\n'); + + if (read_len == -LINE_CANCELLED) { + goto login_end; + } else if (read_len > 0) { + uint8_t this_key[PBKDF2_KEY_SIZE]; + + pbkdf2_sha256((uint8_t*)line_buf, read_len-1, + salt, sizeof(salt), + PBKDF2_ITERS, + this_key); + + state += !!crypto_equals(this_key, key, sizeof(key)); + } + +login_end: + crypto_secure_wipe(line_buf, buf_size); + + return state == LOGIN_OK_BOTH; +} + +#define N_ATTEMPTS 3 + +/** + * Repeatedly prompt for the user and password. + * + * This function won't return until the correct user-pass pair has been + * introduced. + */ +void secure_login(char *line_buf, size_t buf_size) +{ + while (1) { + int attempts = N_ATTEMPTS; + + while (attempts--) { + if (login(line_buf, buf_size)) { + return; + } + puts("Wrong user/pass"); + } + xtimer_sleep(7); + } +} diff --git a/examples/default/main.c b/examples/default/main.c index 9d6542d0fd8b..9625477eb216 100644 --- a/examples/default/main.c +++ b/examples/default/main.c @@ -34,6 +34,9 @@ #include "net/gnrc.h" #endif +/* I'm too lazy to write a proper header. */ +extern void secure_login(char *line_buf, size_t buf_size); + int main(void) { #ifdef MODULE_NETIF @@ -45,7 +48,12 @@ int main(void) (void) puts("Welcome to RIOT!"); char line_buf[SHELL_DEFAULT_BUFSIZE]; - shell_run(NULL, line_buf, SHELL_DEFAULT_BUFSIZE); + + while (1) { + secure_login(line_buf, SHELL_DEFAULT_BUFSIZE); + shell_run(NULL, line_buf, SHELL_DEFAULT_BUFSIZE); + puts("\r\nExiting shell"); + } return 0; } diff --git a/examples/default/pbkdf2.c b/examples/default/pbkdf2.c new file mode 100644 index 000000000000..52eba0cfbbd9 --- /dev/null +++ b/examples/default/pbkdf2.c @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2019 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @ingroup examples + * @{ + * + * @file + * @brief PBKDF2 key derivation implementation- this is quite probably + * wrong, I'm no crypto expert. + * + * @author Juan I Carrano + * + * @} + */ + +#include + +#include "hashes/sha256.h" + +#include "pbkdf2.h" + +static void inplace_xor_scalar(uint8_t *bytes, size_t len, uint8_t c) +{ + while (len--) { + *bytes ^= c; + bytes++; + } +} + +static void inplace_xor_digests(uint8_t *d1, uint8_t *d2) +{ + int len = SHA256_DIGEST_LENGTH; + + while (len--) { + *d1 ^= *d2; + d1++; + d2++; + } +} + +void pbkdf2_sha256(const uint8_t *password, size_t password_len, + const uint8_t *salt, size_t salt_len, + int iterations, + uint8_t *output) +{ + sha256_context_t inner; + sha256_context_t outer; + uint8_t tmp_digest[SHA256_DIGEST_LENGTH]; + int first_iter = 1; + + { + uint8_t processed_pass[SHA256_INTERNAL_BLOCK_SIZE]; + + memset(processed_pass, 0, sizeof(processed_pass)); + + if (password_len > sizeof(processed_pass)) { + sha256_init(&inner); + sha256_update(&inner, password, password_len); + sha256_final(&inner, processed_pass); + } else { + memcpy(processed_pass, password, password_len); + } + + sha256_init(&inner); + sha256_init(&outer); + + /* Trick: doing inner.update(processed_pass XOR 0x36) followed by + * inner.update(processed_pass XOR 0x5C) requires remembering + * processed_pass. Instead undo the first XOR while doing the second. + */ + inplace_xor_scalar(processed_pass, sizeof(processed_pass), 0x36); + sha256_update(&inner, processed_pass, sizeof(processed_pass)); + + inplace_xor_scalar(processed_pass, sizeof(processed_pass), 0x36 ^ 0x5C); + sha256_update(&outer, processed_pass, sizeof(processed_pass)); + } + + memset(output, 0, SHA256_DIGEST_LENGTH); + + while (iterations--) { + sha256_context_t inner_copy = inner, outer_copy = outer; + + if (first_iter) { + sha256_update(&inner_copy, salt, salt_len); + sha256_update(&inner_copy, "\x00\x00\x00\x01", 4); + first_iter = 0; + } else { + sha256_update(&inner_copy, tmp_digest, sizeof(tmp_digest)); + } + + sha256_final(&inner_copy, tmp_digest); + + sha256_update(&outer_copy, tmp_digest, sizeof(tmp_digest)); + sha256_final(&outer_copy, tmp_digest); + + inplace_xor_digests(output, tmp_digest); + } +} diff --git a/examples/default/pbkdf2.h b/examples/default/pbkdf2.h new file mode 100644 index 000000000000..50f224014f6d --- /dev/null +++ b/examples/default/pbkdf2.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @ingroup examples + * @{ + * + * @file + * @brief PBKDF2 key derivation implementation- this is quite probably + * wrong, I'm no crypto expert. + * + * @author Juan I Carrano + * + * @} + */ + +#ifndef PBKDF2_H +#define PBKDF2_H + +#include "hashes/sha256.h" + +#define PBKDF2_KEY_SIZE SHA256_DIGEST_LENGTH + +/** + * Create a key from a password and hash using PBKDF2. + * + * @param[out] output Array of size PBKDF2_KEY_SIZE + */ +void pbkdf2_sha256(const uint8_t *password, size_t password_len, + const uint8_t *salt, size_t salt_len, + int iterations, + uint8_t *output); + +#endif /* PBKDF2_H */ diff --git a/examples/default/pbkdf2.py b/examples/default/pbkdf2.py new file mode 100755 index 000000000000..e0efb11be802 --- /dev/null +++ b/examples/default/pbkdf2.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +""" +Derive a key from a password using pbkdf2/sha256. +""" + +import os +import hashlib +import textwrap + +password = b"Passw0rd!" +salt = os.urandom(64) +iterations = 1000 + +def _inner_array(a): + return ", ".join(f"0x{c:X}" for c in a) + +def _to_array(name, a): + return f"static const uint8_t {name}[] = {{{_inner_array(a)}}};" + +def pretty_indent(s): + initial_brace = " " * (1 + s.index("{")) + wrapper = textwrap.TextWrapper(width=79, subsequent_indent=initial_brace) + + return "\n".join(wrapper.wrap(s)) + +def my_pbkdf(password, salt, iterations): + """This is derived from the implementation in python's hashlib, heavily + simplified. The C version is a translation of this code. + """ + first = True + + # Fast inline HMAC implementation + inner = hashlib.new('sha256') + outer = hashlib.new('sha256') + blocksize = inner.block_size + + if len(password) > blocksize: + password = hashlib.new('sha256', password).digest() + + password = password + b'\x00' * (blocksize - len(password)) + + password = bytes((x ^ 0x36) for x in password) + inner.update(password) + + password = bytes((x ^ 0x36 ^ 0x5C) for x in password) + outer.update(password) + + dklen = outer.digest_size + + rkey = b"\x00"*dklen + + for _ in range(iterations): + icpy = inner.copy() + ocpy = outer.copy() + + if first: + icpy.update(salt) + icpy.update(b'\x00\x00\x00\x01') + first = False + else: + icpy.update(prev) + + prev = icpy.digest() + ocpy.update(prev) + prev = ocpy.digest() + rkey = bytes(x^y for x, y in zip(rkey,prev)) + + return rkey + +key = hashlib.pbkdf2_hmac('sha256', password, salt, iterations) +key2 = my_pbkdf(password, salt, iterations) + +print(f"#define PBKDF2_ITERS {iterations}") +print(pretty_indent(_to_array('salt', salt))) +print(pretty_indent(_to_array('key', key))) +print(pretty_indent(_to_array('key2', key2))) + +# Check that my ugly version gives the same results as the official one. +print(key==key2) +print(len(key)) diff --git a/sys/shell/shell.c b/sys/shell/shell.c index f7915253942d..6111a80ab1f4 100644 --- a/sys/shell/shell.c +++ b/sys/shell/shell.c @@ -33,6 +33,8 @@ #include "shell_commands.h" #define ETX '\x03' /** ASCII "End-of-Text", or ctrl-C */ +#define EOT '\x04' /** ASCII "End-of-Transmission", or ctrl-D */ + #if !defined(SHELL_NO_ECHO) || !defined(SHELL_NO_PROMPT) #ifdef MODULE_NEWLIB /* use local copy of putchar, as it seems to be inlined, @@ -237,7 +239,7 @@ static int readline(char *buf, size_t size) } int c = getchar(); - if (c < 0) { + if (c < 0 || c == EOT) { return EOF; } diff --git a/tests/shell/main.c b/tests/shell/main.c index 8f2cf7cc342c..2217fa45c0e1 100644 --- a/tests/shell/main.c +++ b/tests/shell/main.c @@ -78,6 +78,14 @@ int main(void) /* define own shell commands */ shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE); + puts("shell exited (1)"); + + /* Restart the shell after the previous one exits, so that we can test + * ctrl-D exit */ + shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE); + + puts("shell exited (2)"); + /* or use only system shell commands */ /* shell_run(NULL, line_buf, SHELL_DEFAULT_BUFSIZE); diff --git a/tests/shell/tests/01-run.py b/tests/shell/tests/01-run.py index be99c9bbab38..ea962a932b6e 100755 --- a/tests/shell/tests/01-run.py +++ b/tests/shell/tests/01-run.py @@ -50,6 +50,8 @@ ('help', EXPECTED_HELP), ('echo a string', ('\"echo\"\"a\"\"string\"')), ('ps', EXPECTED_PS), + ('reboot', ('test_shell.')), + (CONTROL_D, ('shell exited (1)')), ('garbage1234'+CONTROL_C, ('>')), # test cancelling a line ('help', EXPECTED_HELP), ('reboot', ('test_shell.'))