// Module for cryptography

#include <c_errno.h>
#include "module.h"
#include "lauxlib.h"
#include "platform.h"
#include "c_types.h"
#include "c_stdlib.h"
#include "flash_fs.h"
#include "../crypto/digests.h"
#include "../crypto/mech.h"

#include "user_interface.h"

#include "rom.h"

/**
  * hash = crypto.sha1(input)
  *
  *	Calculates raw SHA1 hash of input string.
  * Input is arbitrary string, output is raw 20-byte hash as string.
  */
static int crypto_sha1( lua_State* L )
{
  SHA1_CTX ctx;
  uint8_t digest[20];
  // Read the string from lua (with length)
  int len;
  const char* msg = luaL_checklstring(L, 1, &len);
  // Use the SHA* functions in the rom
  SHA1Init(&ctx);
  SHA1Update(&ctx, msg, len);
  SHA1Final(digest, &ctx);

  // Push the result as a lua string
  lua_pushlstring(L, digest, 20);
  return 1;
}

#ifdef LUA_USE_MODULES_ENCODER
static int call_encoder( lua_State* L, const char *function ) {
  if (lua_gettop(L) != 1) { 
    luaL_error(L, "%s must have one argument", function);
  }
  lua_getfield(L, LUA_GLOBALSINDEX, "encoder");
  if (!lua_istable(L, -1) && !lua_isrotable(L, -1)) { // also need table just in case encoder has been overloaded
    luaL_error(L, "Cannot find encoder.%s", function); 
  }
  lua_getfield(L, -1, function);
  lua_insert(L, 1);    //move function below the argument
  lua_pop(L, 1);       //and dump the encoder rotable from stack.
  lua_call(L,1,1);     // call encoder.xxx(string)
  return 1;
}

static int crypto_base64_encode (lua_State* L) { 
  return call_encoder(L, "toBase64"); 
}
static int crypto_hex_encode (lua_State* L) { 
  return call_encoder(L, "toHex"); 
}
#else
static const char* bytes64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/**
  * encoded = crypto.toBase64(raw)
  *
  * Encodes raw binary string as base64 string.
  */
static int crypto_base64_encode( lua_State* L )
{
  int len;
  const char* msg = luaL_checklstring(L, 1, &len);
  int blen = (len + 2) / 3 * 4;
  char* out = (char*)c_malloc(blen);
  int j = 0, i;
  for (i = 0; i < len; i += 3) {
    int a = msg[i];
    int b = (i + 1 < len) ? msg[i + 1] : 0;
    int c = (i + 2 < len) ? msg[i + 2] : 0;
    out[j++] = bytes64[a >> 2];
    out[j++] = bytes64[((a & 3) << 4) | (b >> 4)];
    out[j++] = (i + 1 < len) ? bytes64[((b & 15) << 2) | (c >> 6)] : 61;
    out[j++] = (i + 2 < len) ? bytes64[(c & 63)] : 61;
  }
  lua_pushlstring(L, out, j);
  c_free(out);
  return 1;
}

/**
  * encoded = crypto.toHex(raw)
  *
  *	Encodes raw binary string as hex string.
  */
static int crypto_hex_encode( lua_State* L)
{
  int len;
  const char* msg = luaL_checklstring(L, 1, &len);
  char* out = (char*)c_malloc(len * 2);
  int i, j = 0;
  for (i = 0; i < len; i++) {
    out[j++] = crypto_hexbytes[msg[i] >> 4];
    out[j++] = crypto_hexbytes[msg[i] & 0xf];
  }
  lua_pushlstring(L, out, len*2);
  c_free(out);
  return 1;
}
#endif
/**
  * masked = crypto.mask(message, mask)
  *
  * Apply a mask (repeated if shorter than message) as XOR to each byte.
  */
static int crypto_mask( lua_State* L )
{
  int len, mask_len;
  const char* msg = luaL_checklstring(L, 1, &len);
  const char* mask = luaL_checklstring(L, 2, &mask_len);
  int i;
  char* copy = (char*)c_malloc(len);
  for (i = 0; i < len; i++) {
    copy[i] = msg[i] ^ mask[i % 4];
  }
  lua_pushlstring(L, copy, len);
  c_free(copy);
  return 1;
}

static inline int bad_mech (lua_State *L) { return luaL_error (L, "unknown hash mech"); }
static inline int bad_mem  (lua_State *L) { return luaL_error (L, "insufficient memory"); }
static inline int bad_file (lua_State *L) { return luaL_error (L, "file does not exist"); }

/* rawdigest = crypto.hash("MD5", str)
 * strdigest = crypto.toHex(rawdigest)
 */
static int crypto_lhash (lua_State *L)
{
  const digest_mech_info_t *mi = crypto_digest_mech (luaL_checkstring (L, 1));
  if (!mi)
    return bad_mech (L);
  size_t len = 0;
  const char *data = luaL_checklstring (L, 2, &len);

  uint8_t digest[mi->digest_size];
  if (crypto_hash (mi, data, len, digest) != 0)
    return bad_mem (L);

  lua_pushlstring (L, digest, sizeof (digest));
  return 1;
}

typedef struct digest_user_datum_t  {
	const digest_mech_info_t *mech_info;
	void *ctx;
} digest_user_datum_t;

/* General Usage for extensible hash functions:
 * sha = crypto.new_hash("MD5")
 * sha.update("Data")
 * sha.update("Data2")
 * strdigest = crypto.toHex(sha.finalize())
 */

/* crypto.new_hash("MECHTYPE") */
static int crypto_new_hash (lua_State *L)
{
  const digest_mech_info_t *mi = crypto_digest_mech (luaL_checkstring (L, 1));
  if (!mi)
    return bad_mech (L);

  void *ctx = os_malloc (mi->ctx_size);
  if (ctx==NULL)
    return bad_mem (L);

  mi->create (ctx);

  // create a userdataum with specific metatable
  digest_user_datum_t *dudat = (digest_user_datum_t *)lua_newuserdata(L, sizeof(digest_user_datum_t));
  luaL_getmetatable(L, "crypto.hash");
  lua_setmetatable(L, -2);

  // Set pointers to the mechanics and CTX
  dudat->mech_info = mi;
  dudat->ctx       = ctx;

  return 1; // Pass userdata object back
}

/* Called as object, params:
   1 - userdata "this"
   2 - new string to add to the hash state  */
static int crypto_hash_update (lua_State *L)
{
  NODE_DBG("enter crypto_hash_update.\n");
  digest_user_datum_t *dudat;
  size_t sl;

  dudat = (digest_user_datum_t *)luaL_checkudata(L, 1, "crypto.hash");
  luaL_argcheck(L, dudat, 1, "crypto.hash expected");

  const digest_mech_info_t *mi = dudat->mech_info;

  size_t len = 0;
  const char *data = luaL_checklstring (L, 2, &len);

  mi->update (dudat->ctx, data, len);

  return 0;  // No return value
}

/* Called as object, no params. Returns digest of default size. */
static int crypto_hash_finalize (lua_State *L)
{
  NODE_DBG("enter crypto_hash_update.\n");
  digest_user_datum_t *dudat;
  size_t sl;

  dudat = (digest_user_datum_t *)luaL_checkudata(L, 1, "crypto.hash");
  luaL_argcheck(L, dudat, 1, "crypto.hash expected");

  const digest_mech_info_t *mi = dudat->mech_info;

  uint8_t digest[mi->digest_size]; // Allocate as local
  mi->finalize (digest, dudat->ctx);

  lua_pushlstring (L, digest, sizeof (digest));
  return 1;
}

/* Frees memory for the user datum and CTX hash state */
static int crypto_hash_gcdelete (lua_State *L)
{
  NODE_DBG("enter crypto_hash_delete.\n");
  digest_user_datum_t *dudat;

  dudat = (digest_user_datum_t *)luaL_checkudata(L, 1, "crypto.hash");
  luaL_argcheck(L, dudat, 1, "crypto.hash expected");

  os_free(dudat->ctx);

  return 0;
}


/* rawdigest = crypto.hash("MD5", filename)
 * strdigest = crypto.toHex(rawdigest)
 */
static int crypto_flhash (lua_State *L)
{
  const digest_mech_info_t *mi = crypto_digest_mech (luaL_checkstring (L, 1));
  if (!mi)
    return bad_mech (L);
  const char *filename = luaL_checkstring (L, 2);

  // Open the file
  int file_fd = fs_open (filename, FS_RDONLY);
  if(file_fd < FS_OPEN_OK) {
    return bad_file(L);
  }

  // Compute hash
  uint8_t digest[mi->digest_size];
  int returncode = crypto_fhash (mi, &fs_read, file_fd, digest);

  // Finish up
  fs_close(file_fd);

  if (returncode == ENOMEM)
    return bad_mem (L);
  else if (returncode == EINVAL)
    return bad_mech(L);
  else
    lua_pushlstring (L, digest, sizeof (digest));

  return 1;
}


/* rawsignature = crypto.hmac("SHA1", str, key)
 * strsignature = crypto.toHex(rawsignature)
 */
static int crypto_lhmac (lua_State *L)
{
  const digest_mech_info_t *mi = crypto_digest_mech (luaL_checkstring (L, 1));
  if (!mi)
    return bad_mech (L);
  size_t len = 0;
  const char *data = luaL_checklstring (L, 2, &len);
  size_t klen = 0;
  const char *key = luaL_checklstring (L, 3, &klen);

  uint8_t digest[mi->digest_size];
  if (crypto_hmac (mi, data, len, key, klen, digest) != 0)
    return bad_mem (L);

  lua_pushlstring (L, digest, sizeof (digest));
  return 1;
}



static const crypto_mech_t *get_mech (lua_State *L, int idx)
{
  const char *name = luaL_checkstring (L, idx);

  const crypto_mech_t *mech = crypto_encryption_mech (name);
  if (mech)
    return mech;

  luaL_error (L, "unknown cipher: %s", name);
  __builtin_unreachable ();
}

static int crypto_encdec (lua_State *L, bool enc)
{
  const crypto_mech_t *mech = get_mech (L, 1);
  size_t klen;
  const char *key = luaL_checklstring (L, 2, &klen);
  size_t dlen;
  const char *data = luaL_checklstring (L, 3, &dlen);

  size_t ivlen;
  const char *iv = luaL_optlstring (L, 4, "", &ivlen);

  size_t bs = mech->block_size;
  size_t outlen = ((dlen + bs -1) / bs) * bs;
  char *buf = (char *)os_zalloc (outlen);
  if (!buf)
    return luaL_error (L, "crypto init failed");

  crypto_op_t op =
  {
    key, klen,
    iv, ivlen,
    data, dlen,
    buf, outlen,
    enc ? OP_ENCRYPT : OP_DECRYPT
  };
  if (!mech->run (&op))
  {
    os_free (buf);
    return luaL_error (L, "crypto op failed");
  }
  else
  {
    lua_pushlstring (L, buf, outlen);
    // note: if lua_pushlstring runs out of memory, we leak buf :(
    os_free (buf);
    return 1;
  }
}

static int lcrypto_encrypt (lua_State *L)
{
  return crypto_encdec (L, true);
}

static int lcrypto_decrypt (lua_State *L)
{
  return crypto_encdec (L, false);
}

// Hash function map
static const LUA_REG_TYPE crypto_hash_map[] = {
  { LSTRKEY( "update" ),  LFUNCVAL( crypto_hash_update ) },
  { LSTRKEY( "finalize" ),   LFUNCVAL( crypto_hash_finalize ) },
  { LSTRKEY( "__gc" ),    LFUNCVAL( crypto_hash_gcdelete ) },
  { LSTRKEY( "__index" ), LROVAL( crypto_hash_map ) },
  { LNILKEY, LNILVAL }
};


// Module function map
static const LUA_REG_TYPE crypto_map[] = {
  { LSTRKEY( "sha1" ),     LFUNCVAL( crypto_sha1 ) },
  { LSTRKEY( "toBase64" ), LFUNCVAL( crypto_base64_encode ) },
  { LSTRKEY( "toHex" ),    LFUNCVAL( crypto_hex_encode ) },
  { LSTRKEY( "mask" ),     LFUNCVAL( crypto_mask ) },
  { LSTRKEY( "hash"   ),   LFUNCVAL( crypto_lhash ) },
  { LSTRKEY( "fhash"  ),   LFUNCVAL( crypto_flhash ) },
  { LSTRKEY( "new_hash"   ),   LFUNCVAL( crypto_new_hash ) },
  { LSTRKEY( "hmac"   ),   LFUNCVAL( crypto_lhmac ) },
  { LSTRKEY( "encrypt" ),  LFUNCVAL( lcrypto_encrypt ) },
  { LSTRKEY( "decrypt" ),  LFUNCVAL( lcrypto_decrypt ) },
  { LNILKEY, LNILVAL }
};

int luaopen_crypto ( lua_State *L )
{
  luaL_rometatable(L, "crypto.hash", (void *)crypto_hash_map);  // create metatable for crypto.hash
  return 0;
}

NODEMCU_MODULE(CRYPTO, "crypto", crypto_map, luaopen_crypto);