From ad58f8ab0437cf90c4c6a1a1e19eb4c0bd6f07fd Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Wed, 10 Feb 2021 20:49:14 +0100 Subject: [PATCH 1/2] Factor out pbkdf2 to its own repo --- Makefile | 2 +- benchmarks/measurements.erl | 24 -- c_src/fast_scram.c | 592 --------------------------------- rebar.config | 39 +-- rebar.lock | 9 +- src/fast_scram.app.src | 5 +- src/fast_scram.erl | 38 +-- src/fast_scram_definitions.erl | 2 +- test/erlang_scram.erl | 41 --- test/pbkdf2_SUITE.erl | 204 ------------ 10 files changed, 22 insertions(+), 934 deletions(-) delete mode 100644 benchmarks/measurements.erl delete mode 100644 c_src/fast_scram.c delete mode 100644 test/erlang_scram.erl delete mode 100644 test/pbkdf2_SUITE.erl diff --git a/Makefile b/Makefile index bf51fd6..3aa247f 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ codecov: _build/test/cover/ct.coverdata ./rebar3 as test codecov analyze rebar3: - wget https://github.com/erlang/rebar3/releases/download/3.13.2/rebar3 &&\ + wget https://github.com/erlang/rebar3/releases/download/3.14.3/rebar3 &&\ chmod u+x rebar3 dialyzer: rebar3 diff --git a/benchmarks/measurements.erl b/benchmarks/measurements.erl deleted file mode 100644 index f2d2e30..0000000 --- a/benchmarks/measurements.erl +++ /dev/null @@ -1,24 +0,0 @@ --module(measurements). - --export([ - sample_erlang_pbkdf2/1, - sample_fast_pbkdf2/1 - ]). - -sample_erlang_pbkdf2(#{hash := Hash, - strategy := Strategy, - iteration_count := IterationCount, - sample_size := SampleSize}) -> - Pass = crypto:strong_rand_bytes(16), - Salt = crypto:strong_rand_bytes(16), - Fun = fun() -> erlang_scram:hi(Hash, Pass, Salt, IterationCount) end, - stats_sample:sample(Strategy, Fun, SampleSize). - -sample_fast_pbkdf2(#{hash := Hash, - strategy := Strategy, - iteration_count := IterationCount, - sample_size := SampleSize}) -> - Pass = crypto:strong_rand_bytes(16), - Salt = crypto:strong_rand_bytes(16), - Fun = fun() -> fast_scram:hi(Hash, Pass, Salt, IterationCount) end, - stats_sample:sample(Strategy, Fun, SampleSize). diff --git a/c_src/fast_scram.c b/c_src/fast_scram.c deleted file mode 100644 index f44176a..0000000 --- a/c_src/fast_scram.c +++ /dev/null @@ -1,592 +0,0 @@ -/* - * fast-pbkdf2 - Optimal PBKDF2-HMAC calculation - * Written in 2015 by Joseph Birr-Pixton - * - * To the extent possible under law, the author(s) have dedicated all - * copyright and related and neighboring rights to this software to the - * public domain worldwide. This software is distributed without any - * warranty. - * - * You should have received a copy of the CC0 Public Domain Dedication - * along with this software. If not, see - * . - */ - -#include "erl_nif.h" - -#ifndef TIMESLICE_PERCENTAGE -#define TIMESLICE_PERCENTAGE 5 // announce a timeslice of 5 percent when indicated -#define ITERS_PER_SLOT 6 -/* On the single core of an 2,2 GHz Quad-Core Intel Core i7, in slightly below 1ms - * we achieve around 120 iterations for sha512. - * Also, we want to report percentage every 5% (TIMESLICE_PERCENTAGE). - * We therefore get that a slot in between iterations should take 6 iterations (ITERS_PER_SLOT). - */ -#endif - -#include -#include -#include -#include -#if defined(__GNUC__) -#include -#endif - -#include - -/* --- Common useful things --- */ - -static inline void write32_be(uint32_t n, uint8_t out[4]) -{ -#if defined(__GNUC__) && __GNUC__ >= 4 && __BYTE_ORDER == __LITTLE_ENDIAN - *(uint32_t *)(out) = __builtin_bswap32(n); -#else - out[0] = (n >> 24) & 0xff; - out[1] = (n >> 16) & 0xff; - out[2] = (n >> 8) & 0xff; - out[3] = n & 0xff; -#endif -} - -static inline void write64_be(uint64_t n, uint8_t out[8]) -{ -#if defined(__GNUC__) && __GNUC__ >= 4 && __BYTE_ORDER == __LITTLE_ENDIAN - *(uint64_t *)(out) = __builtin_bswap64(n); -#else - write32_be((n >> 32) & 0xffffffff, out); - write32_be(n & 0xffffffff, out + 4); -#endif -} - -/* Prepare block (of blocksz bytes) to contain md padding denoting a msg-size - * message (in bytes). block has a prefix of used bytes. - * Message length is expressed in 32 bits (so suitable for all sha1 and sha2 algorithms). */ -static inline void md_pad(uint8_t *block, size_t blocksz, size_t used, size_t msg) -{ - memset(block + used, 0, blocksz - used - 4); - block[used] = 0x80; - block += blocksz - 4; - write32_be((uint32_t) (msg * 8), block); -} - -/* Internal function/type names for hash-specific things. */ -#define XSTRINGIFY(s) STRINGIFY(s) -#define STRINGIFY(s) #s - -#define HMAC_CTX(_name) HMAC_ ## _name ## _ctx -#define HMAC_INIT(_name) HMAC_ ## _name ## _init -#define HMAC_UPDATE(_name) HMAC_ ## _name ## _update -#define HMAC_FINAL(_name) HMAC_ ## _name ## _final - -#define HMAC_CTX_ROUND(_name) HMAC_ ## _name ## _ctx_round // C struct -#define HMAC_CTX_ROUND_RES(_name) res_HMAC_ ## _name ## _ctx_round // Erlang Resource definition -#define HMAC_CTX_ROUND_NAME(_name) XSTRINGIFY(HMAC_CTX_ROUND(_name)) // Erlang atom-name - -#define PBKDF2_F(_name) pbkdf2_f_ ## _name -#define PBKDF2(_name) pbkdf2_ ## _name - -typedef struct { - ERL_NIF_TERM atom_sha; - ERL_NIF_TERM atom_sha224; - ERL_NIF_TERM atom_sha256; - ERL_NIF_TERM atom_sha384; - ERL_NIF_TERM atom_sha512; - ErlNifResourceType* HMAC_CTX_ROUND_RES(sha1); - ErlNifResourceType* HMAC_CTX_ROUND_RES(sha224); - ErlNifResourceType* HMAC_CTX_ROUND_RES(sha256); - ErlNifResourceType* HMAC_CTX_ROUND_RES(sha384); - ErlNifResourceType* HMAC_CTX_ROUND_RES(sha512); -} pbkdf2_st; - -/* This macro expands to decls for the whole implementation for a given - * hash function. Arguments are: - * - * _name like 'sha1', added to symbol names (e.g. sha256) - * _blocksz block size, in bytes (e.g. SHA256_CBLOCK) - * _hashsz digest output, in bytes (e.g. SHA256_DIGEST_LENGTH) - * _ctx hash context type (e.g. SHA256_Init) - * _init hash context initialisation function (e.g. SHA256_Update) - * args: (_ctx *c) - * _update hash context update function (e.g. SHA256_Update) - * args: (_ctx *c, const void *data, size_t ndata) - * _final hash context finish function (e.g. SHA256_Final) - * args: (void *out, _ctx *c) - * _xform hash context raw block update function (e.g. SHA256_Transform) - * args: (_ctx *c, const void *data) - * _xcpy hash context raw copy function (only need copy hash state) (e.g. sha256_cpy) - * args: (_ctx * restrict out, const _ctx *restrict in) - * _xtract hash context state extraction (e.g. sha256_extract) - * args: args (_ctx *restrict c, uint8_t *restrict out) - * _xxor hash context xor function (only need xor hash state) (e.g. sha256_xor) - * args: (_ctx *restrict out, const _ctx *restrict in) - * - * The resulting function is named PBKDF2(_name). - */ -#define DECL_PBKDF2(_name, _blocksz, _hashsz, _ctx, \ - _init, _update, _xform, _final, _xcpy, _xtract, _xxor) \ - \ - typedef struct { \ - _ctx inner; \ - _ctx outer; \ - } HMAC_CTX(_name); \ - \ - typedef struct { \ - HMAC_CTX(_name) startctx; \ - HMAC_CTX(_name) ctx; \ - _ctx result; \ - uint8_t Ublock[_blocksz]; \ - uint32_t iterations; \ - } HMAC_CTX_ROUND(_name); \ - \ - static inline void HMAC_INIT(_name)(HMAC_CTX(_name) *ctx, \ - const uint8_t *key, size_t nkey) \ - { \ - /* Prepare key: */ \ - uint8_t k[_blocksz]; \ - \ - /* Shorten long keys. */ \ - if (nkey > _blocksz) \ - { \ - _init(&ctx->inner); \ - _update(&ctx->inner, key, nkey); \ - _final(k, &ctx->inner); \ - key = k; \ - nkey = _hashsz; \ - } \ - \ - /* Standard doesn't cover case where blocksz < hashsz. */ \ - assert(nkey <= _blocksz); \ - \ - /* Right zero-pad short keys. */ \ - if (k != key) \ - memcpy(k, key, nkey); \ - if (_blocksz > nkey) \ - memset(k + nkey, 0, _blocksz - nkey); \ - \ - /* Start inner hash computation */ \ - uint8_t blk_inner[_blocksz]; \ - uint8_t blk_outer[_blocksz]; \ - \ - for (size_t i = 0; i < _blocksz; i++) \ - { \ - blk_inner[i] = 0x36 ^ k[i]; \ - blk_outer[i] = 0x5c ^ k[i]; \ - } \ - \ - _init(&ctx->inner); \ - _update(&ctx->inner, blk_inner, sizeof blk_inner); \ - \ - /* And outer. */ \ - _init(&ctx->outer); \ - _update(&ctx->outer, blk_outer, sizeof blk_outer); \ - } \ - \ - static inline void HMAC_UPDATE(_name)(HMAC_CTX(_name) *ctx, \ - const void *data, size_t ndata) \ - { \ - _update(&ctx->inner, data, ndata); \ - } \ - \ - static inline void HMAC_FINAL(_name)(HMAC_CTX(_name) *ctx, \ - uint8_t out[_hashsz]) \ - { \ - _final(out, &ctx->inner); \ - _update(&ctx->outer, out, _hashsz); \ - _final(out, &ctx->outer); \ - } \ - \ - /* --- PBKDF2 --- */ \ - ERL_NIF_TERM PBKDF2_F(_name)(ErlNifEnv *env, \ - int argc, const ERL_NIF_TERM argv[]) \ - { \ - pbkdf2_st *mod_st = enif_priv_data(env); \ - HMAC_CTX_ROUND(_name) *round_st; \ - enif_get_resource(env, argv[0], \ - mod_st->HMAC_CTX_ROUND_RES(_name), \ - ((void*) (&round_st))); \ - \ - while (1) { \ - for (uint32_t i = 0; i < ITERS_PER_SLOT && i < round_st->iterations-1; ++i) \ - { \ - /* Complete inner hash with previous U */ \ - _xcpy(&round_st->ctx.inner, &round_st->startctx.inner); \ - _xform(&round_st->ctx.inner, round_st->Ublock); \ - _xtract(&round_st->ctx.inner, round_st->Ublock); \ - /* Complete outer hash with inner output */ \ - _xcpy(&round_st->ctx.outer, &round_st->startctx.outer); \ - _xform(&round_st->ctx.outer, round_st->Ublock); \ - _xtract(&round_st->ctx.outer, round_st->Ublock); \ - _xxor(&round_st->result, &round_st->ctx.outer); \ - } \ - if (round_st->iterations <= ITERS_PER_SLOT) break; \ - round_st->iterations -= ITERS_PER_SLOT; \ - \ - /* Schedule again but with iterations decremented */ \ - if (enif_consume_timeslice(env, TIMESLICE_PERCENTAGE)) { \ - return enif_schedule_nif(env, HMAC_CTX_ROUND_NAME(_name), 0, \ - PBKDF2_F(_name), argc, argv); \ - } \ - } \ - \ - /* We're done, so we can release the resource */ \ - enif_release_resource(round_st); \ - /* Reform result into output buffer. */ \ - ERL_NIF_TERM erl_result; \ - unsigned char *output = enif_make_new_binary(env, _hashsz, &erl_result); \ - _xtract(&round_st->result, output); \ - return erl_result; \ - } \ - \ - static inline ERL_NIF_TERM PBKDF2(_name)(ErlNifEnv *env, \ - const uint8_t *pw, size_t npw, \ - const uint8_t *salt, size_t nsalt, \ - uint32_t iterations, uint32_t counter) \ - { \ - /* Retrieve the state resource descriptor from our priv data, */ \ - /* and allocate a new resource structure */ \ - pbkdf2_st *mod_st = enif_priv_data(env); \ - HMAC_CTX_ROUND(_name) *round_st = enif_alloc_resource( \ - mod_st->HMAC_CTX_ROUND_RES(_name), \ - sizeof(HMAC_CTX_ROUND(_name)) \ - ); \ - \ - HMAC_INIT(_name)(&round_st->startctx, pw, npw); \ - uint8_t countbuf[4]; \ - write32_be(counter, countbuf); \ - \ - /* Prepare loop-invariant padding block. */ \ - md_pad(round_st->Ublock, _blocksz, _hashsz, _blocksz + _hashsz); \ - \ - /* First iteration: \ - * U_1 = PRF(P, S || INT_32_BE(i)) \ - */ \ - round_st->ctx = round_st->startctx; \ - HMAC_UPDATE(_name)(&round_st->ctx, salt, nsalt); \ - HMAC_UPDATE(_name)(&round_st->ctx, countbuf, sizeof countbuf); \ - HMAC_FINAL(_name)(&round_st->ctx, round_st->Ublock); \ - round_st->result = round_st->ctx.outer; \ - round_st->iterations = iterations; \ - \ - ERL_NIF_TERM state_term = enif_make_resource(env, round_st); \ - const ERL_NIF_TERM tmp_argv[] = {state_term}; \ - return PBKDF2_F(_name)(env, 1, tmp_argv); \ - } - - -static inline void sha1_extract(SHA_CTX *restrict ctx, uint8_t *restrict out) -{ - write32_be(ctx->h0, out); - write32_be(ctx->h1, out + 4); - write32_be(ctx->h2, out + 8); - write32_be(ctx->h3, out + 12); - write32_be(ctx->h4, out + 16); -} - -static inline void sha1_cpy(SHA_CTX *restrict out, const SHA_CTX *restrict in) -{ - out->h0 = in->h0; - out->h1 = in->h1; - out->h2 = in->h2; - out->h3 = in->h3; - out->h4 = in->h4; -} - -static inline void sha1_xor(SHA_CTX *restrict out, const SHA_CTX *restrict in) -{ - out->h0 ^= in->h0; - out->h1 ^= in->h1; - out->h2 ^= in->h2; - out->h3 ^= in->h3; - out->h4 ^= in->h4; -} - -DECL_PBKDF2(sha1, - SHA_CBLOCK, - SHA_DIGEST_LENGTH, - SHA_CTX, - SHA1_Init, - SHA1_Update, - SHA1_Transform, - SHA1_Final, - sha1_cpy, - sha1_extract, - sha1_xor) - -static inline void sha224_extract(SHA256_CTX *restrict ctx, uint8_t *restrict out) -{ - write32_be(ctx->h[0], out); - write32_be(ctx->h[1], out + 4); - write32_be(ctx->h[2], out + 8); - write32_be(ctx->h[3], out + 12); - write32_be(ctx->h[4], out + 16); - write32_be(ctx->h[5], out + 20); - write32_be(ctx->h[6], out + 24); -} - -static inline void sha256_extract(SHA256_CTX *restrict ctx, uint8_t *restrict out) -{ - write32_be(ctx->h[0], out); - write32_be(ctx->h[1], out + 4); - write32_be(ctx->h[2], out + 8); - write32_be(ctx->h[3], out + 12); - write32_be(ctx->h[4], out + 16); - write32_be(ctx->h[5], out + 20); - write32_be(ctx->h[6], out + 24); - write32_be(ctx->h[7], out + 28); -} - -static inline void sha256_cpy(SHA256_CTX *restrict out, const SHA256_CTX *restrict in) -{ - out->h[0] = in->h[0]; - out->h[1] = in->h[1]; - out->h[2] = in->h[2]; - out->h[3] = in->h[3]; - out->h[4] = in->h[4]; - out->h[5] = in->h[5]; - out->h[6] = in->h[6]; - out->h[7] = in->h[7]; -} - -static inline void sha256_xor(SHA256_CTX *restrict out, const SHA256_CTX *restrict in) -{ - out->h[0] ^= in->h[0]; - out->h[1] ^= in->h[1]; - out->h[2] ^= in->h[2]; - out->h[3] ^= in->h[3]; - out->h[4] ^= in->h[4]; - out->h[5] ^= in->h[5]; - out->h[6] ^= in->h[6]; - out->h[7] ^= in->h[7]; -} - -DECL_PBKDF2(sha224, - SHA256_CBLOCK, - SHA224_DIGEST_LENGTH, - SHA256_CTX, - SHA224_Init, - SHA224_Update, - SHA256_Transform, - SHA224_Final, - sha256_cpy, - sha224_extract, - sha256_xor) - -DECL_PBKDF2(sha256, - SHA256_CBLOCK, - SHA256_DIGEST_LENGTH, - SHA256_CTX, - SHA256_Init, - SHA256_Update, - SHA256_Transform, - SHA256_Final, - sha256_cpy, - sha256_extract, - sha256_xor) - -static inline void sha384_extract(SHA512_CTX *restrict ctx, uint8_t *restrict out) -{ - write64_be(ctx->h[0], out); - write64_be(ctx->h[1], out + 8); - write64_be(ctx->h[2], out + 16); - write64_be(ctx->h[3], out + 24); - write64_be(ctx->h[4], out + 32); - write64_be(ctx->h[5], out + 40); -} - -static inline void sha512_extract(SHA512_CTX *restrict ctx, uint8_t *restrict out) -{ - write64_be(ctx->h[0], out); - write64_be(ctx->h[1], out + 8); - write64_be(ctx->h[2], out + 16); - write64_be(ctx->h[3], out + 24); - write64_be(ctx->h[4], out + 32); - write64_be(ctx->h[5], out + 40); - write64_be(ctx->h[6], out + 48); - write64_be(ctx->h[7], out + 56); -} - -static inline void sha512_cpy(SHA512_CTX *restrict out, const SHA512_CTX *restrict in) -{ - out->h[0] = in->h[0]; - out->h[1] = in->h[1]; - out->h[2] = in->h[2]; - out->h[3] = in->h[3]; - out->h[4] = in->h[4]; - out->h[5] = in->h[5]; - out->h[6] = in->h[6]; - out->h[7] = in->h[7]; -} - -static inline void sha512_xor(SHA512_CTX *restrict out, const SHA512_CTX *restrict in) -{ - out->h[0] ^= in->h[0]; - out->h[1] ^= in->h[1]; - out->h[2] ^= in->h[2]; - out->h[3] ^= in->h[3]; - out->h[4] ^= in->h[4]; - out->h[5] ^= in->h[5]; - out->h[6] ^= in->h[6]; - out->h[7] ^= in->h[7]; -} - -DECL_PBKDF2(sha384, - SHA512_CBLOCK, - SHA384_DIGEST_LENGTH, - SHA512_CTX, - SHA384_Init, - SHA384_Update, - SHA512_Transform, - SHA384_Final, - sha512_cpy, - sha384_extract, - sha512_xor) - -DECL_PBKDF2(sha512, - SHA512_CBLOCK, - SHA512_DIGEST_LENGTH, - SHA512_CTX, - SHA512_Init, - SHA512_Update, - SHA512_Transform, - SHA512_Final, - sha512_cpy, - sha512_extract, - sha512_xor) - -static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) -{ - (void) load_info; - pbkdf2_st* mod_st = enif_alloc(sizeof(pbkdf2_st)); - if(mod_st == NULL) { - return 1; - } - - mod_st->atom_sha = enif_make_atom(env, "sha"); - mod_st->atom_sha224 = enif_make_atom(env, "sha224"); - mod_st->atom_sha256 = enif_make_atom(env, "sha256"); - mod_st->atom_sha384 = enif_make_atom(env, "sha384"); - mod_st->atom_sha512 = enif_make_atom(env, "sha512"); - - mod_st->HMAC_CTX_ROUND_RES(sha1) = enif_open_resource_type( - env, NULL, HMAC_CTX_ROUND_NAME(sha1), - NULL, ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL - ); - - mod_st->HMAC_CTX_ROUND_RES(sha224) = enif_open_resource_type( - env, NULL, HMAC_CTX_ROUND_NAME(sha224), - NULL, ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL - ); - - mod_st->HMAC_CTX_ROUND_RES(sha256) = enif_open_resource_type( - env, NULL, HMAC_CTX_ROUND_NAME(sha256), - NULL, ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL - ); - - mod_st->HMAC_CTX_ROUND_RES(sha384) = enif_open_resource_type( - env, NULL, HMAC_CTX_ROUND_NAME(sha384), - NULL, ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL - ); - - mod_st->HMAC_CTX_ROUND_RES(sha512) = enif_open_resource_type( - env, NULL, HMAC_CTX_ROUND_NAME(sha512), - NULL, ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL - ); - - *priv_data = (void*) mod_st; - - return 0; -} - -static int reload(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) -{ - return 0; -} - -static int upgrade(ErlNifEnv* env, void** priv, void** old_priv, ERL_NIF_TERM info) -{ - return load(env, priv, info); -} - -static void unload(ErlNifEnv* env, void* priv) -{ - enif_free(priv); - return; -} - -ERL_NIF_TERM mk_error(ErlNifEnv* env, const char *error_msg) -{ - return enif_make_tuple2( - env, - enif_make_atom(env, "error"), - enif_make_atom(env, error_msg) - ); -} - -static ERL_NIF_TERM -pbkdf2_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) -{ - if(argc != 5) - return enif_make_badarg(env); - - ErlNifBinary password; - if (!enif_inspect_binary(env, argv[1], &password)) - return mk_error(env, "bad_password"); - - ErlNifBinary salt; - if (!enif_inspect_binary(env, argv[2], &salt)) - return mk_error(env, "bad_salt"); - - int iteration_count; - if (!enif_get_int(env, argv[3], &iteration_count)) - return mk_error(env, "bad_iteration_count"); - if (iteration_count <= 0) - return mk_error(env, "bad_iteration_count"); - - int counter; - if (!enif_get_int(env, argv[4], &counter)) - return mk_error(env, "bad_block_counter"); - if (counter <= 0) - return mk_error(env, "bad_block_counter"); - - /** Calculates PBKDF2-HMAC-SHA - * @p npw bytes at @p pw are the password input. - * @p nsalt bytes at @p salt are the salt input. - * @p iterations is the PBKDF2 iteration count and must be non-zero. - */ - pbkdf2_st *mod_st = (pbkdf2_st*) enif_priv_data(env); - - if(enif_is_identical(argv[0], mod_st->atom_sha)) { - return PBKDF2(sha1)(env, - password.data, password.size, - salt.data, salt.size, - iteration_count, counter); - } else if(enif_is_identical(argv[0], mod_st->atom_sha224)) { - return PBKDF2(sha224)(env, - password.data, password.size, - salt.data, salt.size, - iteration_count, counter); - } else if(enif_is_identical(argv[0], mod_st->atom_sha256)) { - return PBKDF2(sha256)(env, - password.data, password.size, - salt.data, salt.size, - iteration_count, counter); - } else if(enif_is_identical(argv[0], mod_st->atom_sha384)) { - return PBKDF2(sha384)(env, - password.data, password.size, - salt.data, salt.size, - iteration_count, counter); - } else if(enif_is_identical(argv[0], mod_st->atom_sha512)) { - return PBKDF2(sha512)(env, - password.data, password.size, - salt.data, salt.size, - iteration_count, counter); - } else { - return mk_error(env, "bad_hash"); - } -} - -static ErlNifFunc fastpbkdf2_nif_funcs[] = { - {"pbkdf2_block", 5, pbkdf2_nif} -}; - -ERL_NIF_INIT(fast_scram, fastpbkdf2_nif_funcs, load, reload, upgrade, unload); diff --git a/rebar.config b/rebar.config index 043027f..6a164f5 100644 --- a/rebar.config +++ b/rebar.config @@ -1,49 +1,20 @@ {erl_opts, [debug_info]}. -{deps, []}. +{deps, [{fast_pbkdf2, "1.0.0"}]}. +{plugins, [rebar3_hex]}. {profiles, [ {test, [ + {erl_opts, []}, {deps, [ - {statistics, {git, "https://github.com/NelsonVides/statistics.git", {branch, "master"}}}, {proper, "1.3.0"}, - {base16, {git, "https://github.com/esl/base16.git", {tag, "1.1.0"}}} + {base16, "1.0.0"} ]}, {plugins, [ {rebar3_codecov, {git, "https://github.com/esl/rebar3_codecov.git", {ref, "6bd31cc"}}} - ]}, - {erl_opts, [{d, 'STATISTICS'}]} + ]} ]} ] }. -{plugins, [pc, rebar3_hex]}. - -{artifacts, ["priv/fast_scram.so"]}. - -{port_specs, - [ - { - % Any arch - ".*", - % Create library - "priv/fast_scram.so", - % From files - ["c_src/*.c"], - % Using options - [ {env, [{"CFLAGS", "$CFLAGS -std=c99 -O3 -g -Wall -Wextra -fPIC"}, - {"LDLIBS", "$LDLIBS -lcrypto"}, - {"DRV_LINK_TEMPLATE", "$DRV_LINK_TEMPLATE $LDLIBS"}]}] - } - ]}. - -{provider_hooks, - [ - {post, - [ - {compile, {pc, compile}}, - {clean, {pc, clean}} - ]} - ]}. - {cover_enabled, true}. {cover_export_enabled, true}. diff --git a/rebar.lock b/rebar.lock index 57afcca..83d07a6 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1 +1,8 @@ -[]. +{"1.2.0", +[{<<"fast_pbkdf2">>,{pkg,<<"fast_pbkdf2">>,<<"1.0.0">>},0}]}. +[ +{pkg_hash,[ + {<<"fast_pbkdf2">>, <<"205314169DD56FA64F36CE94528659326EA0FDA45DE61C110677D57A7B714E37">>}]}, +{pkg_hash_ext,[ + {<<"fast_pbkdf2">>, <<"F312F8F07967468224C04BFCEFEBE3990DDFF9B6E8A1BCAF7298F82B0803FF29">>}]} +]. diff --git a/src/fast_scram.app.src b/src/fast_scram.app.src index 624e7fe..d49289f 100644 --- a/src/fast_scram.app.src +++ b/src/fast_scram.app.src @@ -1,6 +1,6 @@ {application, fast_scram, [{description, "A fast Salted Challenge Response Authentication Mechanism"}, - {vsn, "0.3.0"}, + {vsn, "0.4.0"}, {registered, []}, {applications, [kernel, @@ -10,6 +10,5 @@ {env,[]}, {modules, []}, {licenses, ["Apache 2.0"]}, - {links, [{"GitHub", "https://github.com/esl/fast_scram/"}]}, - {exclude_files, ["c_src/fast_scram.d"]} + {links, [{"GitHub", "https://github.com/esl/fast_scram/"}]} ]}. diff --git a/src/fast_scram.erl b/src/fast_scram.erl index 5ba6862..cd161a7 100644 --- a/src/fast_scram.erl +++ b/src/fast_scram.erl @@ -1,5 +1,4 @@ -module(fast_scram). --on_load(load/0). -include("types.hrl"). @@ -16,7 +15,7 @@ {continue, next_message(), fast_scram_state()} | {error, error_message(), fast_scram_state()}. --export([hi/4, pbkdf2_block/5, pbkdf2/5]). +-export([hi/4]). -export([mech_new/1, mech_step/2 @@ -302,7 +301,7 @@ apply_rules_until_match(Input, [Rule | RulesLeft], State) -> %%%=================================================================== -spec salted_password(sha_type(), binary(), binary(), non_neg_integer()) -> binary(). salted_password(Sha, Password, Salt, IterationCount) -> - ?MODULE:hi(Sha, Password, Salt, IterationCount). + fast_scram_definitions:salted_password(Sha, Password, Salt, IterationCount). -spec client_key(sha_type(), binary()) -> binary(). client_key(Sha, SaltedPassword) -> @@ -332,33 +331,6 @@ server_signature(Sha, ServerKey, AuthMessage) -> %%% NIF %%%=================================================================== -spec hi(sha_type(), binary(), binary(), non_neg_integer()) -> binary(). -hi(Hash, Password, Salt, IterationCount) -> - pbkdf2_block(Hash, Password, Salt, IterationCount, 1). - --spec pbkdf2_block(sha_type(), binary(), binary(), non_neg_integer(), non_neg_integer()) -> binary(). -pbkdf2_block(_Hash, _Password, _Salt, _IterationCount, _BlockSize) -> - erlang:nif_error(not_loaded). - --spec pbkdf2(sha_type(), binary(), binary(), non_neg_integer(), non_neg_integer()) -> binary(). -pbkdf2(Hash, Password, Salt, IterationCount, DkLen) -> - pbkdf2(Hash, Password, Salt, IterationCount, DkLen, 1, [], 0). - -pbkdf2(_Hash, _Password, _Salt, _IterationCount, DkLen, _BlockIndex, Acc, Len) when Len >= DkLen -> - Bin = iolist_to_binary(lists:reverse(Acc)), - binary:part(Bin, 0, DkLen); -pbkdf2(Hash, Password, Salt, IterationCount, DkLen, BlockIndex, Acc, Len) -> - Block = pbkdf2_block(Hash, Password, Salt, IterationCount, BlockIndex), - pbkdf2(Hash, Password, Salt, IterationCount, DkLen, BlockIndex + 1, [Block | Acc], byte_size(Block) + Len). - --spec load() -> any(). -load() -> - code:ensure_loaded(crypto), - PrivDir = case code:priv_dir(?MODULE) of - {error, _} -> - EbinDir = filename:dirname(code:which(?MODULE)), - AppPath = filename:dirname(EbinDir), - filename:join(AppPath, "priv"); - Path -> - Path - end, - erlang:load_nif(filename:join(PrivDir, ?MODULE_STRING), none). +hi(Hash, Password, Salt, IterationCount) + when ?is_valid_hash(Hash), is_binary(Password), is_binary(Salt), ?is_positive_integer(IterationCount) -> + fast_pbkdf2:pbkdf2(Hash, Password, Salt, IterationCount). diff --git a/src/fast_scram_definitions.erl b/src/fast_scram_definitions.erl index 0a5137c..e375148 100644 --- a/src/fast_scram_definitions.erl +++ b/src/fast_scram_definitions.erl @@ -34,7 +34,7 @@ -spec salted_password(sha_type(), binary(), binary(), non_neg_integer()) -> binary(). salted_password(Sha, Password, Salt, IterationCount) when ?is_valid_hash(Sha), is_binary(Password), is_binary(Salt), ?is_positive_integer(IterationCount) -> - fast_scram:hi(Sha, Password, Salt, IterationCount). + fast_pbkdf2:pbkdf2(Sha, Password, Salt, IterationCount). -spec client_key(sha_type(), binary()) -> binary(). client_key(Sha, SaltedPassword) diff --git a/test/erlang_scram.erl b/test/erlang_scram.erl deleted file mode 100644 index 3e41b22..0000000 --- a/test/erlang_scram.erl +++ /dev/null @@ -1,41 +0,0 @@ --module(erlang_scram). - --export([hi/4]). - --type sha_type() :: crypto:sha1() | crypto:sha2(). - --spec hi(sha_type(), binary(), binary(), non_neg_integer()) -> binary(). -hi(Sha, Password, Salt, 1) -> - crypto_hmac(Sha, Password, <>); -hi(Sha, Password, Salt, IterationCount) - when is_integer(IterationCount), IterationCount > 1 -> - U1 = crypto_hmac(Sha, Password, <>), - mask(U1, hi_round(Sha, Password, U1, IterationCount - 1)). - --spec hi_round(sha_type(), binary(), binary(), non_neg_integer()) -> binary(). -hi_round(Sha, Password, UPrev, 1) -> - crypto_hmac(Sha, Password, UPrev); -hi_round(Sha, Password, UPrev, IterationCount) -> - U = crypto_hmac(Sha, Password, UPrev), - mask(U, hi_round(Sha, Password, U, IterationCount - 1)). - --spec mask(binary(), binary()) -> binary(). -mask(Key, Data) -> - KeySize = size(Key) * 8, - <> = Key, - <> = Data, - C = A bxor B, - <>. - --ifdef(OTP_RELEASE). --if(?OTP_RELEASE >= 23). -crypto_hmac(Sha, Bin1, Bin2) -> - crypto:mac(hmac, Sha, Bin1, Bin2). --else. -crypto_hmac(Sha, Bin1, Bin2) -> - crypto:hmac(Sha, Bin1, Bin2). --endif. --else. -crypto_hmac(Sha, Bin1, Bin2) -> - crypto:hmac(Sha, Bin1, Bin2). --endif. diff --git a/test/pbkdf2_SUITE.erl b/test/pbkdf2_SUITE.erl deleted file mode 100644 index 67b5c06..0000000 --- a/test/pbkdf2_SUITE.erl +++ /dev/null @@ -1,204 +0,0 @@ --module(pbkdf2_SUITE). - -%% API --export([all/0, - groups/0, - init_per_suite/1, - end_per_suite/1, - init_per_group/2, - end_per_group/2, - init_per_testcase/2, - end_per_testcase/2]). - -%% test cases --export([ - erlang_and_nif_are_equivalent_sha1/1, - erlang_and_nif_are_equivalent_sha224/1, - erlang_and_nif_are_equivalent_sha256/1, - erlang_and_nif_are_equivalent_sha384/1, - erlang_and_nif_are_equivalent_sha512/1, - realtime_test/1 - ]). --export([ - test_vector_sha1_1/1, - test_vector_sha1_2/1, - test_vector_sha1_3/1, - test_vector_sha1_4/1, - test_vector_sha1_5/1, - test_vector_sha256_1/1, - test_vector_sha256_2/1, - test_vector_sha256_3/1, - test_vector_sha256_4/1, - test_vector_sha256_5/1, - test_vector_sha256_6/1 - ]). - --include_lib("common_test/include/ct.hrl"). --include_lib("proper/include/proper.hrl"). --include_lib("eunit/include/eunit.hrl"). - -all() -> - [ - {group, equivalents}, - {group, test_vectors}, - realtime_test - ]. - -groups() -> - [ - {equivalents, [parallel], - [ - erlang_and_nif_are_equivalent_sha1, - erlang_and_nif_are_equivalent_sha224, - erlang_and_nif_are_equivalent_sha256, - erlang_and_nif_are_equivalent_sha384, - erlang_and_nif_are_equivalent_sha512 - ]}, - {test_vectors, [parallel], - [ - test_vector_sha1_1, - test_vector_sha1_2, - test_vector_sha1_3, - test_vector_sha1_4, - test_vector_sha1_5, - test_vector_sha256_1, - test_vector_sha256_2, - test_vector_sha256_3, - test_vector_sha256_4, - test_vector_sha256_5, - test_vector_sha256_6 - ]} - ]. - -%%%=================================================================== -%%% Overall setup/teardown -%%%=================================================================== -init_per_suite(Config) -> - Config. - -end_per_suite(_Config) -> - ok. - -%%%=================================================================== -%%% Group specific setup/teardown -%%%=================================================================== -init_per_group(_Groupname, Config) -> - Config. - -end_per_group(_Groupname, _Config) -> - ok. - -%%%=================================================================== -%%% Testcase specific setup/teardown -%%%=================================================================== -init_per_testcase(_TestCase, Config) -> - Config. - -end_per_testcase(_TestCase, _Config) -> - ok. - -%%%=================================================================== -%%% Individual Test Cases (from groups() definition) -%%%=================================================================== - -erlang_and_nif_are_equivalent_sha1(_Config) -> - erlang_and_nif_are_equivalent_(sha). - -erlang_and_nif_are_equivalent_sha224(_Config) -> - erlang_and_nif_are_equivalent_(sha224). - -erlang_and_nif_are_equivalent_sha256(_Config) -> - erlang_and_nif_are_equivalent_(sha256). - -erlang_and_nif_are_equivalent_sha384(_Config) -> - erlang_and_nif_are_equivalent_(sha384). - -erlang_and_nif_are_equivalent_sha512(_Config) -> - erlang_and_nif_are_equivalent_(sha512). - -erlang_and_nif_are_equivalent_(Sha) -> - Prop = ?FORALL({Pass, Salt, Count}, - {binary(), binary(), range(2,20000)}, - fast_scram:hi(Sha, Pass, Salt, Count) - =:= erlang_scram:hi(Sha, Pass, Salt, Count) - ), - ?assert(proper:quickcheck(Prop, [verbose, long_result, - {numtests, 100}, - {start_size, 2}, - {max_size, 64}])). - - -%% Taken from the official RFC https://www.ietf.org/rfc/rfc6070.txt - -test_vector_sha1_1(_Config) -> - {P,S,It,DkLen,Result} = {<<"password">>,<<"salt">>,1,20, - base16:decode(<<"0c60c80f961f0e71f3a9b524af6012062fe037a6">>)}, - ?assertEqual(Result, fast_scram:pbkdf2(sha, P, S, It, DkLen)). - -test_vector_sha1_2(_Config) -> - {P,S,It,DkLen,Result} = {<<"password">>,<<"salt">>,2,20, - base16:decode(<<"ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957">>)}, - ?assertEqual(Result, fast_scram:pbkdf2(sha, P, S, It, DkLen)). - -test_vector_sha1_3(_Config) -> - {P,S,It,DkLen,Result} = {<<"password">>,<<"salt">>,4096,20, - base16:decode(<<"4b007901b765489abead49d926f721d065a429c1">>)}, - ?assertEqual(Result, fast_scram:pbkdf2(sha, P, S, It, DkLen)). - -test_vector_sha1_4(_Config) -> - {P,S,It,DkLen,Result} = {<<"password">>,<<"salt">>,16777216,20, - base16:decode(<<"eefe3d61cd4da4e4e9945b3d6ba2158c2634e984">>)}, - ?assertEqual(Result, fast_scram:pbkdf2(sha, P, S, It, DkLen)). - -test_vector_sha1_5(_Config) -> - {P,S,It,DkLen,Result} = {<<"passwordPASSWORDpassword">>,<<"saltSALTsaltSALTsaltSALTsaltSALTsalt">>,4096,25, - base16:decode(<<"3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038">>)}, - ?assertEqual(Result, fast_scram:pbkdf2(sha, P, S, It, DkLen)). - - -%% Taken from https://stackoverflow.com/a/5136918/8853275 -test_vector_sha256_1(_Config) -> - {P,S,It,DkLen,Result} = {<<"password">>, <<"salt">>, 1, 32, - base16:decode(<<"120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b">>)}, - ?assertEqual(Result, fast_scram:pbkdf2(sha256, P, S, It, DkLen)). - -test_vector_sha256_2(_Config) -> - {P,S,It,DkLen,Result} = {<<"password">>, <<"salt">>, 2, 32, - base16:decode(<<"ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43">>)}, - ?assertEqual(Result, fast_scram:pbkdf2(sha256, P, S, It, DkLen)). - -test_vector_sha256_3(_Config) -> - {P,S,It,DkLen,Result} = {<<"password">>, <<"salt">>, 4096, 32, - base16:decode(<<"c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a">>)}, - ?assertEqual(Result, fast_scram:pbkdf2(sha256, P, S, It, DkLen)). - -test_vector_sha256_4(_Config) -> - {P,S,It,DkLen,Result} = {<<"password">>, <<"salt">>, 16777216, 32, - base16:decode(<<"cf81c66fe8cfc04d1f31ecb65dab4089f7f179e89b3b0bcb17ad10e3ac6eba46">>)}, - ?assertEqual(Result, fast_scram:pbkdf2(sha256, P, S, It, DkLen)). - -test_vector_sha256_5(_Config) -> - {P,S,It,DkLen,Result} = {<<"passwordPASSWORDpassword">>,<<"saltSALTsaltSALTsaltSALTsaltSALTsalt">>,4096,40, - base16:decode(<<"348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9">>)}, - ?assertEqual(Result, fast_scram:pbkdf2(sha256, P, S, It, DkLen)). - -test_vector_sha256_6(_Config) -> - {P,S,It,DkLen,Result} = {<<"pass\0word">>, <<"sa\0lt">>, 4096, 16, - base16:decode(<<"89b69d0516f829893c696226650a8687">>)}, - ?assertEqual(Result, fast_scram:pbkdf2(sha256, P, S, It, DkLen)). - - --ifdef(STATISTICS). -realtime_test(_Config) -> - % Allocate two large binaries - A = crypto:strong_rand_bytes(64), - B = crypto:strong_rand_bytes(64), - Fun = fun() -> - fast_scram:hi(512, A, B, 10000) - end, - #{mean := AverageJitter} = stats_latency:realtime_latency_on_load(Fun, 20, 5000), - ?assert(AverageJitter < 50). --else. -realtime_test(_Config) -> - ok. --endif. From b441741cef9b98898dd95b700e7fca46f7df2a60 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Thu, 11 Feb 2021 14:05:11 +0100 Subject: [PATCH 2/2] Update README --- README.md | 50 ++++++++++++-------------------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index e52033f..b96cd7b 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,10 @@ [![codecov](https://codecov.io/gh/esl/fast_scram/branch/master/graph/badge.svg)](https://codecov.io/gh/esl/fast_scram) [![Hex](http://img.shields.io/hexpm/v/fast_scram.svg)](https://hex.pm/packages/fast_scram) -`fast_scram` is an Erlang implementation of the _Salted Challenge Response Authentication Mechanism_, -where the challenge algorithm is a carefully-optimised NIF, while respecting the latency properties -of the BEAM and the functional aspect of Erlang as a language. +`fast_scram` is a purely-functional Erlang implementation of the _Salted Challenge Response Authentication Mechanism_, where the challenge algorithm is implemented as a carefully-optimised NIF using [fast_pbkdf2][fast_pbkdf2]. ## Building -`fast_scram` is a rebar3-compatible OTP application, that uses the -[port_compiler](https://github.com/blt/port_compiler) for the C part of the code. - -Building is as easy as `rebar3 compile`, and using it in your projects as +`fast_scram` is a rebar3-compatible OTP application, building is as easy as `rebar3 compile`, and using it in your projects as ```erlang {plugins, [pc]}. {provider_hooks, @@ -23,15 +18,14 @@ Building is as easy as `rebar3 compile`, and using it in your projects as [{fast_scram, {git, "https://github.com/esl/fast_scram.git", {branch, "master"}}}]}. ``` - ## Using ### SCRAM In SCRAM, a `SaltedPassword` is defined as ``` SaltedPassword := Hi(Normalize(password), salt, i) ``` -This algorithm is precisely the one that pays the challenge, and it is the one we solve here with -the best performance. Simply do: +This algorithm is precisely the one that pays the challenge, and it is the one we solve here with the best performance. +Simply do: ```erlang SaltedPassword = fast_scram:hi(Hash, Password, Salt, IterationCount) ``` @@ -40,15 +34,6 @@ where `Hash` is the underlying hash function chosen as described by -type sha_type() :: crypto:sha1() | crypto:sha2(). ``` -### PBKDF2 -If what you desire is PBKDF2 (I assume that if that is what you want, then you know your RFC), in a -way that allows you to request longer derived keys, you may use `fast_scram:pbkdf2_block/5` with a -given block index and do the indexing and chunking yourself, or use `fast_scram:pbkdf2/5` for the -full algorithm. However, it doesn't really add much more entropy to the derived key to use outputs -larger than the output of the underlying hash, so you might as well, use `pbkdf2` where dkLen is -that of the hash's output, which is the same than `pbkdf2_block` with index `1`, which is simply the -`hi` function. - ### Full algorithm If you want to avoid reimplementing SCRAM again and again, you can use the extended API. The best example is that one of the tests. Given already configured states, the flow is as follows: @@ -219,30 +204,18 @@ See examples below. ## Performance ### The problem -SCRAM is a challenge-response authentication method, that is, it forces the client to compute a -challenge in order to authenticate him. But when the server implementation is slower than that -of an attacker, it makes the server vulnerable to DoS by hogging itself with computations. +SCRAM is a challenge-response authentication method, that is, it forces the client to compute a challenge in order to authenticate him. +But when the server implementation is slower than that of an attacker, it makes the server vulnerable to DoS by hogging itself with computations. We could see that on the CI and load-testing pipelines of [MongooseIM][MIM] for example. ### The solution -Is partial. We don't expect to have the fastest implementation, as that would be purely C code on -GPUs, so unfortunately an attacker will pretty much always have better chances there. _But_ we can -make the computation cheap enough for us that other computations —like the load of a session -establishment— will be more relevant than that of the challenge; and also that other defence -mechanisms like IP blacklisting or traffic shaping, will fire in good time. +Is partial. We don't expect to have the fastest implementation, as that would be purely C code on GPUs, so unfortunately an attacker will pretty much always have better chances there. +_But_ we can make the computation cheap enough for us that other computations —like the load of a session establishment— will be more relevant than that of the challenge; +and also that other defence mechanisms like IP blacklisting or traffic shaping, will fire in good time. ### The outcome -On average it's 10x faster on the machines I've tested it (you can compare using the provided module -in `./benchmarks/measurements.erl`), but while the erlang implementation consumes memory linearly to -the iteration count (1M it count with 120 clients quickly allocated 7GB of RAM, and 1M is common for -password managers for example), the NIF implementation does not allocate any more memory. Also, the -NIFS spend all of their time in user level alone, while the erlang one jumps to system calls in -around ~2% of the time (I'd guess due to some heavy allocation and garbage collection patterns). - - -## Credit where credit is due -The initial algorithm and optimisations were taken from Joseph Birr-Pixton's -[fastpbkdf2](https://github.com/ctz/fastpbkdf2)'s repository. +It all boils down to the right PBKDF2 implementation, as done in [fast_pbkdf2][fast_pbkdf2], which is on average 10x faster on the machines I've tested it. +But while the erlang implementation consumes memory linearly to the iteration count, the NIF implementation does not allocate any more memory. ## Read more: * SCRAM: [RFC5802](https://tools.ietf.org/html/rfc5802) @@ -253,3 +226,4 @@ The initial algorithm and optimisations were taken from Joseph Birr-Pixton's [MIM]: https://github.com/esl/MongooseIM [exml]: https://github.com/esl/exml/ +[fast_pbkdf2]: https://hex.pm/packages/fast_scram