sys/hashes/pbkdf2: Add PBKDF2-sha256 implementation.
This add an implementation of PBKDF2 using sha256 hmac. Only one derived key length is supported (32) though it should not be hard to extend it. The testing is done with both random (with fixed seed) vectors amd vectors from rfc7914.
This commit is contained in:
parent
a83927d031
commit
62667848c4
100
sys/hashes/pbkdf2.c
Normal file
100
sys/hashes/pbkdf2.c
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* 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 <j.carrano@fu-berlin.de>
|
||||||
|
*
|
||||||
|
* @}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "hashes/sha256.h"
|
||||||
|
#include "hashes/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, 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
sys/include/hashes/pbkdf2.h
Normal file
59
sys/include/hashes/pbkdf2.h
Normal file
@ -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 <j.carrano@fu-berlin.de>
|
||||||
|
*
|
||||||
|
* @}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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 */
|
||||||
11
tests/pbkdf2/Makefile
Normal file
11
tests/pbkdf2/Makefile
Normal file
@ -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
|
||||||
7
tests/pbkdf2/Makefile.ci
Normal file
7
tests/pbkdf2/Makefile.ci
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
BOARD_INSUFFICIENT_MEMORY := \
|
||||||
|
arduino-duemilanove \
|
||||||
|
arduino-nano \
|
||||||
|
arduino-uno \
|
||||||
|
atmega328p \
|
||||||
|
nucleo-l011k4 \
|
||||||
|
#
|
||||||
11
tests/pbkdf2/README
Normal file
11
tests/pbkdf2/README
Normal file
@ -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.
|
||||||
146
tests/pbkdf2/main.c
Normal file
146
tests/pbkdf2/main.c
Normal file
@ -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 <j.carrano@fu-berlin.de>
|
||||||
|
*
|
||||||
|
* 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 <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
42
tests/pbkdf2/tests/01-rfc.py
Executable file
42
tests/pbkdf2/tests/01-rfc.py
Executable file
@ -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 <j.carrano@fu-berlin.de>
|
||||||
|
"""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)
|
||||||
80
tests/pbkdf2/tests/02-random.py
Executable file
80
tests/pbkdf2/tests/02-random.py
Executable file
@ -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 <j.carrano@fu-berlin.de>
|
||||||
|
"""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)
|
||||||
45
tests/pbkdf2/tests/test_base.py
Normal file
45
tests/pbkdf2/tests/test_base.py
Normal file
@ -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 <j.carrano@fu-berlin.de>
|
||||||
|
|
||||||
|
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)))
|
||||||
Loading…
x
Reference in New Issue
Block a user