/*-*- mode:c;indent-tabs-mode:t;c-basic-offset:8;tab-width:8;coding:utf-8   -*-│
│ vi: set et ft=c ts=8 sw=8 fenc=utf-8                                     :vi │
╚──────────────────────────────────────────────────────────────────────────────╝
│                                                                              │
│  largon2                                                                     │
│  Copyright © 2016 Thibault Charbonnier                                       │
│                                                                              │
│  Permission is hereby granted, free of charge, to any person obtaining       │
│  a copy of this software and associated documentation files (the             │
│  "Software"), to deal in the Software without restriction, including         │
│  without limitation the rights to use, copy, modify, merge, publish,         │
│  distribute, sublicense, and/or sell copies of the Software, and to          │
│  permit persons to whom the Software is furnished to do so, subject to       │
│  the following conditions:                                                   │
│                                                                              │
│  The above copyright notice and this permission notice shall be              │
│  included in all copies or substantial portions of the Software.             │
│                                                                              │
│  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,             │
│  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF          │
│  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.      │
│  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY        │
│  CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,        │
│  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE           │
│  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                      │
│                                                                              │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/isystem/stdio.h"
#include "libc/isystem/string.h"
#include "third_party/argon2/argon2.h"
#include "third_party/lua/lauxlib.h"
#include "third_party/lua/lua.h"
#include "third_party/lua/lualib.h"

__notice(largon2_notice, "\
largon2 (MIT License)\n\
Copyright 2016 Thibault Charbonnier");

// clang-format off
/***
Lua C binding for the Argon2 password hashing function.
Compatible with Lua 5.x and LuaJIT.

See the [Argon2 documentation](https://github.com/P-H-C/phc-winner-argon2)
for in-depth instructions and details about Argon2.

This module's version is compatible with Argon2
[20161029](https://github.com/P-H-C/phc-winner-argon2/releases/tag/20161029)
and later.

Note: this document is also valid for the
[lua-argon2-ffi](https://github.com/thibaultcha/lua-argon2-ffi) module: an FFI
implementation of this binding for LuaJIT which uses the same API as this
original implementation.

@module argon2
@author Thibault Charbonnier
@license MIT
@release 3.0.1
*/


/***
Argon2 hashing variants. Those fields are `userdatums`, read-only values that
can be fed to the module's configuration or the `hash_encoded` function.
See the [Argon2 documentation](https://github.com/P-H-C/phc-winner-argon2)
for a description of those variants.
@field argon2_i
@field argon2_d
@field argon2_id
@table variants
*/


/***
Argon2 hashing options. Those options can be given to `hash_encoded` as a table.
If values are omitted, the default values of this module will be used.
Default values of this module can be overridden with `m_cost()`, `t_cost()`,
`parallelism()`, `hash_len()`, and `variant()`.
@field t_cost Number of iterations (`number`, default: `3`).
    argon2.hash_encoded("password", "salt", { t_cost = 4 })
Can be set to a new default in lua-argon2 (C binding only) by calling:
    argon2.t_cost(4)
@field m_cost Sets memory usage as KiB (`number`, default: `4096`).
    argon2.hash_encoded("password", "salt", {
      m_cost = math.pow(2, 16) -- 2^16 aka 65536 KiB
    })
Can be set to a new default in lua-argon2 (C binding only) by calling:
    argon2.m_cost(16)
@field parallelism Number of threads and compute lanes (`number`, default: `1`).
    argon2.hash_encoded("password", "salt", { parallelism = 2 })
Can be set to a new default in lua-argon2 (C binding only) by calling:
    argon2.parallelism(2)
@field hash_len Length of the hash output length (`number`, default: `32`).
    argon2.hash_encoded("password", "salt", { hash_len = 64 })
Can be set to a new default in lua-argon2 (C binding only) by calling:
    argon2.hash_len(64)
@field variant Choose the Argon2 variant to use (Argon2i, Argon2d, Argon2id)
from the `variants` table. (`userdata`, default: `argon2.variants.argon2_i`).
    argon2.hash_encoded("password", "salt", { variant = argon2.variants.argon2_d })
Can be set to a new default in lua-argon2 (C binding only) by calling:
    argon2.variant(argon2.variants.argon2_i)
    argon2.variant(argon2.variants.argon2_d)
    argon2.variant(argon2.variants.argon2_id)
@table options
*/
#define LUA_ARGON2_DEFAULT_T_COST 3
#define LUA_ARGON2_DEFAULT_M_COST 4096
#define LUA_ARGON2_DEFAULT_PARALLELISM 1
#define LUA_ARGON2_DEFAULT_HASH_LEN 32


typedef struct largon2_config_s largon2_config_t;
struct largon2_config_s {
    uint32_t         m_cost;
    uint32_t         t_cost;
    uint32_t         parallelism;
    uint32_t         hash_len;
    argon2_type      variant;
};


/* CONFIGURATION */


static void
largon2_create_config(lua_State *L)
{
    largon2_config_t *cfg;

    cfg              = lua_newuserdata(L, sizeof(*cfg));
    cfg->t_cost      = LUA_ARGON2_DEFAULT_T_COST;
    cfg->m_cost      = LUA_ARGON2_DEFAULT_M_COST;
    cfg->parallelism = LUA_ARGON2_DEFAULT_PARALLELISM;
    cfg->hash_len    = LUA_ARGON2_DEFAULT_HASH_LEN;
    cfg->variant     = Argon2_id;
}


static largon2_config_t *
largon2_fetch_config(lua_State *L)
{
    largon2_config_t *cfg;

    cfg = lua_touserdata(L, lua_upvalueindex(1));
    if (!cfg)
        luaL_error(L, "could not retrieve argon2 config");

    return cfg;
}


static largon2_config_t *
largon2_arg_init(lua_State *L, int nargs)
{
    if (lua_gettop(L) > nargs) {
        luaL_error(L, "expecting no more than %d arguments, but got %d",
                   nargs, lua_gettop(L));
    }

    lua_settop(L, nargs);

    return largon2_fetch_config(L);
}


static void
largon2_integer_opt(lua_State *L, uint32_t optidx, uint32_t argidx,
                    uint32_t *property, const char *key)
{
    uint32_t        value;
    char            errmsg[64];

    if (!lua_isnil(L, optidx)) {
        if (lua_isnumber(L, optidx)) {
            value = lua_tonumber(L, optidx);
            *property = value;

        } else {
            sprintf(errmsg, "expected %s to be a number, got %s",
                    key, luaL_typename(L, optidx));
            luaL_argerror(L, argidx, errmsg);
        }
    }
}


static int
largon2_cfg_t_cost(lua_State *L)
{
    largon2_config_t *cfg = largon2_arg_init(L, 1);

    largon2_integer_opt(L, 1, 1, &cfg->t_cost, "t_cost");
    lua_pushinteger(L, cfg->t_cost);

    return 1;
}


static int
largon2_cfg_m_cost(lua_State *L)
{
    largon2_config_t *cfg = largon2_arg_init(L, 1);

    largon2_integer_opt(L, 1, 1, &cfg->m_cost, "m_cost");
    lua_pushinteger(L, cfg->m_cost);

    return 1;
}


static int
largon2_cfg_parallelism(lua_State *L)
{
    largon2_config_t *cfg = largon2_arg_init(L, 1);

    largon2_integer_opt(L, 1, 1, &cfg->parallelism, "parallelism");
    lua_pushinteger(L, cfg->parallelism);

    return 1;
}


static int
largon2_cfg_hash_len(lua_State *L)
{
    largon2_config_t *cfg = largon2_arg_init(L, 1);

    largon2_integer_opt(L, 1, 1, &cfg->hash_len, "hash_len");
    lua_pushinteger(L, cfg->hash_len);

    return 1;
}


static int
largon2_cfg_variant(lua_State *L)
{
    largon2_config_t *cfg = largon2_arg_init(L, 1);

    luaL_checktype(L, 1, LUA_TLIGHTUSERDATA);

    cfg->variant = (argon2_type) lua_touserdata(L, 1);

    return 1;
}


/* BINDINGS */


/***
Hashes a password with Argon2i, Argon2d, or Argon2id, producing an encoded
hash.
@function hash_encoded
@param[type=string] plain Plain string to hash_encoded.
@param[type=string] salt Salt to use to hash the plain string.
@param[type=table] options Options with which to hash the plain string. See
`options`. This parameter is optional, if values are omitted the default ones
will be used.
@treturn string `encoded`: Encoded hash computed by Argon2, or `nil` if an
error occurred.
@treturn string `err`: `nil`, or a string describing the error if any.

@usage
local hash, err = argon2.hash_encoded("password", "somesalt")
if err then
  error("could not hash_encoded: " .. err)
end

-- with options and variant
local hash, err = argon2.hash_encoded("password", "somesalt", {
  t_cost = 4,
  m_cost = math.pow(2, 16), -- 65536 KiB
  variant = argon2.variants.argon2_d
})
*/
static int
largon2_hash_encoded(lua_State *L)
{
    const char             *plain, *salt;
    char                   *encoded, *err_msg;
    size_t                  plainlen, saltlen;
    size_t                  encoded_len;
    uint32_t                t_cost;
    uint32_t                m_cost;
    uint32_t                hash_len;
    uint32_t                parallelism;
    argon2_type             variant;
    argon2_error_codes      ret_code;
    largon2_config_t       *cfg;
    luaL_Buffer             buf;

    plain = luaL_checklstring(L, 1, &plainlen);
    salt  = luaL_checklstring(L, 2, &saltlen);

    cfg = largon2_arg_init(L, 3);

    t_cost      = cfg->t_cost;
    m_cost      = cfg->m_cost;
    parallelism = cfg->parallelism;
    hash_len    = cfg->hash_len;
    variant     = cfg->variant;

    if (!lua_isnil(L, 3)) {
        if (!lua_istable(L, 3)) {
            luaL_argerror(L, 3, "expected to be a table");
        }

        lua_getfield(L, 3, "t_cost");
        largon2_integer_opt(L, -1, 3, &t_cost, "t_cost");
        lua_pop(L, 1);

        lua_getfield(L, 3, "m_cost");
        largon2_integer_opt(L, -1, 3, &m_cost, "m_cost");
        lua_pop(L, 1);

        lua_getfield(L, 3, "parallelism");
        largon2_integer_opt(L, -1, 3, &parallelism, "parallelism");
        lua_pop(L, 1);

        lua_getfield(L, 3, "hash_len");
        largon2_integer_opt(L, -1, 3, &hash_len, "hash_len");
        lua_pop(L, 1);

        lua_getfield(L, 3, "variant");
        if (!lua_isnil(L, -1)) {
            if (!lua_islightuserdata(L, -1)) {
                char errmsg[64];
                sprintf(errmsg, "expected variant to be a number, got %s",
                        luaL_typename(L, -1));
                luaL_argerror(L, 3, errmsg);
            }

            variant = (argon2_type) lua_touserdata(L, -1);
        }

        lua_pop(L, 1);
    }

    encoded_len = argon2_encodedlen(t_cost, m_cost, parallelism, saltlen,
                                    hash_len, variant);

    encoded = luaL_buffinitsize(L, &buf, encoded_len);

    if (variant == Argon2_d) {
        ret_code =
          argon2d_hash_encoded(t_cost, m_cost, parallelism, plain, plainlen,
                               salt, saltlen, hash_len, encoded, encoded_len);

    } else if (variant == Argon2_id) {
        ret_code =
          argon2id_hash_encoded(t_cost, m_cost, parallelism, plain, plainlen,
                                salt, saltlen, hash_len, encoded, encoded_len);

    } else {
        ret_code =
          argon2i_hash_encoded(t_cost, m_cost, parallelism, plain, plainlen,
                               salt, saltlen, hash_len, encoded, encoded_len);
    }

    luaL_pushresultsize(&buf, encoded_len - 1);

    if (ret_code != ARGON2_OK) {
        err_msg = (char *) argon2_error_message(ret_code);
        lua_pushnil(L);
        lua_pushstring(L, err_msg);
        return 2;
    }

    return 1;
}


/***
Verifies a password against an encoded string.
@function verify
@param[type=string] encoded Encoded string to verify the plain password against.
@param[type=string] password Plain password to verify.
@treturn boolean `ok`: `true` if the password matches, `false` if it is a
mismatch. If an error occurs during the verification, will be `nil`.
@treturn string `err`: `nil`, or a string describing the error if any. A
password mismatch will not return an error, but will return `ok = false`
instead.

@usage
local ok, err = argon2.verify(argon2i_hash, "password")
if err then
  -- failure to verify (*not* a password mismatch)
  error("could not verify: " .. err)
end

if not ok then
  -- password mismatch
  error("The password does not match the supplied hash")
end

-- with a argon2d hash
local ok, err = argon2.verify(argon2d_hash, "password")
*/
static int
largon2_verify(lua_State *L)
{
    const char             *plain, *encoded;
    size_t                  plainlen;
    argon2_type             variant;
    argon2_error_codes      ret_code;
    char                   *err_msg;

    if (lua_gettop(L) != 2) {
        return luaL_error(L, "expecting 2 arguments, but got %d",
                          lua_gettop(L));
    }

    encoded = luaL_checkstring(L, 1);
    plain   = luaL_checklstring(L, 2, &plainlen);

    if (strstr(encoded, "argon2d")) {
        variant = Argon2_d;

    } else if (strstr(encoded, "argon2id")) {
        variant = Argon2_id;

    } else {
        variant = Argon2_i;
    }

    ret_code = argon2_verify(encoded, plain, plainlen, variant);
    if (ret_code == ARGON2_VERIFY_MISMATCH) {
        lua_pushboolean(L, 0);
        return 1;
    }

    if (ret_code != ARGON2_OK) {
        err_msg = (char *) argon2_error_message(ret_code);
        lua_pushnil(L);
        lua_pushstring(L, err_msg);
        return 2;
    }

    lua_pushboolean(L, 1);

    return 1;
}


/* MODULE */


static void
largon2_push_argon2_variants_table(lua_State *L)
{
    lua_newtable(L);

    lua_pushlightuserdata(L, (void *) Argon2_i);
    lua_setfield(L, -2, "argon2_i");

    lua_pushlightuserdata(L, (void *) Argon2_d);
    lua_setfield(L, -2, "argon2_d");

    lua_pushlightuserdata(L, (void *) Argon2_id);
    lua_setfield(L, -2, "argon2_id");

    return;
}


static const luaL_Reg largon2[] = { { "verify", largon2_verify },
                                    { "hash_encoded", largon2_hash_encoded },
                                    { "t_cost", largon2_cfg_t_cost },
                                    { "m_cost", largon2_cfg_m_cost },
                                    { "parallelism", largon2_cfg_parallelism },
                                    { "hash_len", largon2_cfg_hash_len },
                                    { "variant", largon2_cfg_variant },
                                    { NULL, NULL } };


int
luaopen_argon2(lua_State *L)
{
    lua_newtable(L);

    largon2_create_config(L);
    luaL_setfuncs(L, largon2, 1);

    /* push argon2.variants table */

    largon2_push_argon2_variants_table(L);
    lua_setfield(L, -2, "variants");

    lua_pushliteral(L, "3.0.1");
    lua_setfield(L, -2, "_VERSION");

    lua_pushliteral(L, "Thibault Charbonnier");
    lua_setfield(L, -2, "_AUTHOR");

    lua_pushliteral(L, "MIT");
    lua_setfield(L, -2, "_LICENSE");

    lua_pushliteral(L, "https://github.com/thibaultcha/lua-argon2");
    lua_setfield(L, -2, "_URL");

    return 1;
}
