diff --git a/sys/Makefile.dep b/sys/Makefile.dep index 34f0b3ca23..0c02884a14 100644 --- a/sys/Makefile.dep +++ b/sys/Makefile.dep @@ -738,6 +738,10 @@ ifneq (,$(filter random,$(USEMODULE))) USEMODULE += luid endif +ifneq (,$(filter hashes,$(USEMODULE))) + USEMODULE += crypto +endif + ifneq (,$(filter asymcute,$(USEMODULE))) USEMODULE += sock_udp USEMODULE += sock_util diff --git a/sys/hashes/pbkdf2.c b/sys/hashes/pbkdf2.c new file mode 100644 index 0000000000..4e89f391be --- /dev/null +++ b/sys/hashes/pbkdf2.c @@ -0,0 +1,112 @@ +/* + * 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- only sha256 is supported + * at the moment, and the key size is fixed. + * + * @author Juan I Carrano + * + * @} + */ + +#include + +#include "hashes/sha256.h" +#include "hashes/pbkdf2.h" +#include "crypto/helper.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, const 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] = {0}; + + 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)); + + crypto_secure_wipe(&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); + + if (iterations == 0) { + crypto_secure_wipe(&inner_copy, sizeof(inner_copy)); + crypto_secure_wipe(&outer_copy, sizeof(outer_copy)); + } + } + + crypto_secure_wipe(&inner, sizeof(inner)); + crypto_secure_wipe(&outer, sizeof(outer)); + crypto_secure_wipe(&tmp_digest, sizeof(tmp_digest)); +} diff --git a/sys/include/hashes/pbkdf2.h b/sys/include/hashes/pbkdf2.h new file mode 100644 index 0000000000..11483ff2ab --- /dev/null +++ b/sys/include/hashes/pbkdf2.h @@ -0,0 +1,59 @@ +/* + * 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. + */ +/** + * @defgroup sys_hashes_pbkdf2 PBKDF2 + * @ingroup sys_hashes + * @brief PBKDF2 key derivation implementation. + * @{ + * + * @file + * @brief PBKDF2 key derivation implementation. + * + * @author Juan I Carrano + * + * @} + */ + +#ifndef HASHES_PBKDF2_H +#define HASHES_PBKDF2_H + +#include "hashes/sha256.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief PBKDF2 key size length + * + * @note Currently only one derived key length is supported (32) + */ +#define PBKDF2_KEY_SIZE SHA256_DIGEST_LENGTH + +/** + * @brief Create a key from a password and hash using PBKDF2. + * + * @param[in] password password pointer + * @param[in] password_len length of password + * @param[in] salt salt pointer + * @param[in] salt_len salt length, recommended 64bit + * @param[in] iterations number of rounds. Must be >1. + * NIST’s detailed guide (Appendix A.2.2), + * recommended 10000 + * @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); + +#ifdef __cplusplus +} +#endif + +#endif /* HASHES_PBKDF2_H */ diff --git a/tests/pbkdf2/Makefile b/tests/pbkdf2/Makefile new file mode 100644 index 0000000000..e625d910cd --- /dev/null +++ b/tests/pbkdf2/Makefile @@ -0,0 +1,11 @@ +include ../Makefile.tests_common + +# This application uses getchar and thus expects input from stdio +USEMODULE += stdin +USEMODULE += hashes +USEMODULE += base64 + +# Use a terminal that does not introduce extra characters into the stream. +RIOT_TERMINAL ?= socat + +include $(RIOTBASE)/Makefile.include diff --git a/tests/pbkdf2/Makefile.ci b/tests/pbkdf2/Makefile.ci new file mode 100644 index 0000000000..ff454e3604 --- /dev/null +++ b/tests/pbkdf2/Makefile.ci @@ -0,0 +1,7 @@ +BOARD_INSUFFICIENT_MEMORY := \ + arduino-duemilanove \ + arduino-nano \ + arduino-uno \ + atmega328p \ + nucleo-l011k4 \ + # diff --git a/tests/pbkdf2/README b/tests/pbkdf2/README new file mode 100644 index 0000000000..11645704bb --- /dev/null +++ b/tests/pbkdf2/README @@ -0,0 +1,11 @@ +Test PBKDF2 implementation +========================== + +This test evaluates the RIOT implementation against a reference. The objective +is flexibility and clarity, and for this reason there are no hard coded vectors, +but instead the test is interactive, with the DUT processing vectors given +through the serial interface. + +This means that the test is slower, but more complete and trustworthy. + +The test is completely automated. diff --git a/tests/pbkdf2/main.c b/tests/pbkdf2/main.c new file mode 100644 index 0000000000..b865fc61ec --- /dev/null +++ b/tests/pbkdf2/main.c @@ -0,0 +1,146 @@ +/* + * 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. + */ + +/** + * @{ + * + * @file + * @brief Test PBKDF2-sha256 implementation. + * + * @author Juan Carrano + * + * This application reads (password, salt, iterations) tuples from the + * standard input and outputs the derived key. + * + * The salt must be base64 encoded. The key is printed as base64. + * @} + */ + +#include +#include +#include +#include + +#include "base64.h" +#include "hashes/pbkdf2.h" + +const char error_message[] = "{error}"; +const char input_message[] = "{ready}"; + +#define LINEBUF_SZ (128) + +enum TEST_STATE { + TEST_READ_PASS, + TEST_READ_SALT, + TEST_READ_ITERS, + TEST_COMPUTE, + TEST_ERROR +}; + + +static void _clear_input(void) +{ + /* clear input buffer */ + int c; + while ( (c = getchar()) != '\n' && c != EOF ) { } +} + +int main(void) +{ + static char linebuf[LINEBUF_SZ]; + + /* There will be a few bytes wasted here */ + static char password[LINEBUF_SZ]; + static uint8_t salt[LINEBUF_SZ]; + static uint8_t key[PBKDF2_KEY_SIZE]; + + size_t passwd_len = 0, salt_len = 0; + int iterations = 0; + + enum TEST_STATE state = TEST_READ_PASS; + + _clear_input(); + + while ((puts(input_message), fgets(linebuf, LINEBUF_SZ, stdin) != NULL)) { + char *s_end; + int conversion_status, line_len = strlen(linebuf)-1; + size_t b64_buff_size; + + linebuf[line_len] = '\0'; + + switch (state) { + case TEST_READ_PASS: + strcpy(password, linebuf); + passwd_len = line_len; + state++; + + break; + case TEST_READ_SALT: + /* work around bug in base64_decode */ + if (line_len == 0) { + salt_len = 0; + conversion_status = BASE64_SUCCESS; + } else { + salt_len = sizeof(salt); + conversion_status = base64_decode((uint8_t*)linebuf, + line_len+1, + salt, &salt_len); + } + + if (conversion_status == BASE64_SUCCESS) { + state++; + } else { + state = TEST_ERROR; + } + + break; + case TEST_READ_ITERS: + iterations = strtol(linebuf, &s_end, 10); + + if (*s_end != '\0') { + state = TEST_ERROR; + } else { + state++; + } + + break; + default: + assert(1); + break; + } + + switch (state) { + case TEST_COMPUTE: + pbkdf2_sha256((uint8_t*)password, passwd_len, salt, salt_len, + iterations, key); + + b64_buff_size = sizeof(linebuf); + conversion_status = base64_encode(key, sizeof(key), + (uint8_t*)linebuf, + &b64_buff_size); + + if (conversion_status == BASE64_SUCCESS) { + linebuf[b64_buff_size] = 0; + puts(linebuf); + } else { + puts(error_message); + } + + state = TEST_READ_PASS; + break; + case TEST_ERROR: + puts(error_message); + state = TEST_READ_PASS; + break; + default: + break; + } + } + + return 0; +} diff --git a/tests/pbkdf2/tests/01-rfc.py b/tests/pbkdf2/tests/01-rfc.py new file mode 100755 index 0000000000..394b891f2f --- /dev/null +++ b/tests/pbkdf2/tests/01-rfc.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# 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. +# +# Author: Juan Carrano +"""Vector from RFC 7914 section 11""" + +import os + +import hashlib +import test_base + +KEY_SIZE = hashlib.sha256().digest_size + +v_easy = """55 ac 04 6e 56 e3 08 9f ec 16 91 c2 25 44 b6 05 + f9 41 85 21 6d de 04 65 e6 8b 9d 57 c2 0d ac bc + 49 ca 9c cc f1 79 b6 45 99 16 64 b3 9d 77 ef 31 + 7c 71 b8 45 b1 e3 0b d5 09 11 20 41 d3 a1 97 83""" + +v_hard = """ + 4d dc d8 f6 0b 98 be 21 83 0c ee 5e f2 27 01 f9 + 64 1a 44 18 d0 4c 04 14 ae ff 08 87 6b 34 ab 56 + a1 d4 25 a1 22 58 33 54 9a db 84 1b 51 c9 b3 17 + 6a 27 2b de bb a1 d0 78 47 8f 62 b3 97 f3 3c 8d""" + + +def process_octets(s): + return bytes(int(x, 16) for x in s.split())[:KEY_SIZE] + + +VECTORS = [ + ('passwd', b"salt", 1, process_octets(v_easy)) + ] + +if os.environ.get('BOARD') == 'native': + VECTORS.append(("Password", b"NaCl", 80000, process_octets(v_hard))) + +if __name__ == "__main__": + test_base.main(VECTORS) diff --git a/tests/pbkdf2/tests/02-random.py b/tests/pbkdf2/tests/02-random.py new file mode 100755 index 0000000000..b29add0d2e --- /dev/null +++ b/tests/pbkdf2/tests/02-random.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# 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. +# +# Author: Juan Carrano +"""Random test vectors""" + +from bisect import bisect as _bisect +from itertools import accumulate as _accumulate +import hashlib +import random as rand + +import test_base + + +_pass_chars = [c for c in (chr(x) for x in range(128)) + if c.isprintable()] + + +class random2(rand.Random): + # Murdock uses python 3.5 where random.choices is not available, this + # is a verbatim copy from python 3.6 + def choices(self, population, weights=None, *, cum_weights=None, k=1): + """Return a k sized list of population elements chosen with replacement. + If the relative weights or cumulative weights are not specified, + the selections are made with equal probability. + """ + random = self.random + if cum_weights is None: + if weights is None: + _int = int + total = len(population) + return [population[_int(random() * total)] for i in range(k)] + cum_weights = list(_accumulate(weights)) + elif weights is not None: + raise TypeError('Cannot specify both weights and cumulative weights') + if len(cum_weights) != len(population): + raise ValueError('The number of weights does not match the population') + bisect = _bisect.bisect + total = cum_weights[-1] + hi = len(cum_weights) - 1 + return [population[bisect(cum_weights, random() * total, 0, hi)] + for i in range(k)] + + +randgen = random2(42) + + +def randompass(length): + return "".join(randgen.choices(_pass_chars, k=length)) + + +def randomsalt(bytes_): + return (randgen.getrandbits(bytes_*8).to_bytes(bytes_, 'big') + if bytes_ else b'') + + +def randomvector(pass_len, salt_len, iters): + pass_ = randompass(pass_len) + salt = randomsalt(salt_len) + key = hashlib.pbkdf2_hmac('sha256', pass_.encode('ascii'), salt, iters) + + return pass_, salt, iters, key + + +VECTORS = [ + randomvector(0, 16, 10), + randomvector(8, 0, 10), + randomvector(9, 64, 1), + randomvector(65, 38, 20), + randomvector(32, 15, 12), + randomvector(48, 32, 15), + ] + + +if __name__ == "__main__": + test_base.main(VECTORS) diff --git a/tests/pbkdf2/tests/test_base.py b/tests/pbkdf2/tests/test_base.py new file mode 100644 index 0000000000..c6a639929e --- /dev/null +++ b/tests/pbkdf2/tests/test_base.py @@ -0,0 +1,45 @@ +# 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. +# +# Author: Juan Carrano + +import os +import sys +import base64 +from functools import partial + +MAX_LINE = 128 + + +def safe_encode(data): + """Empty lines will confuse the target, replace them with padding.""" + return base64.b64encode(data).decode('ascii') if data else "" + + +def test(vectors, child): + def _safe_expect_exact(s): + idx = child.expect_exact([s+'\r\n', '{error}\r\n']) + assert idx == 0 + return idx + + def _safe_sendline(l): + assert len(l) < MAX_LINE + _safe_expect_exact('{ready}') + child.sendline(l) + + for passwd, salt, iters, key in vectors: + _safe_sendline(passwd) + _safe_sendline(safe_encode(salt)) + _safe_sendline(str(iters)) + + expected_key = base64.b64encode(key).decode('ascii') + _safe_expect_exact(expected_key) + + +def main(vectors): + sys.path.append(os.path.join(os.environ['RIOTTOOLS'], 'testrunner')) + from testrunner import run + sys.exit(run(partial(test, vectors)))