From 5d5eb24dc70f4884320e17252ea097d6fef5877d Mon Sep 17 00:00:00 2001 From: Konstantin Kushnir Date: Sun, 7 Jul 2024 13:33:54 +0300 Subject: [PATCH] Add basic cryptographic functions (#39) --- ChangeLog | 3 + configure | 151 ++++++++++++++++++++++++++++ configure.in | 26 ++++- generic/cookfs.c | 14 +++ generic/cookfs.h | 15 +++ generic/crypt.c | 234 +++++++++++++++++++++++++++++++++++++++++++ generic/crypt.h | 13 +++ generic/cryptCmd.c | 115 +++++++++++++++++++++ generic/cryptCmd.h | 8 ++ pkgconfig.tcl.in | 2 + tests/all.tcl | 4 + tests/crypt.test | 127 +++++++++++++++++++++++ tests/pkgconfig.test | 2 + 13 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 generic/crypt.c create mode 100644 generic/crypt.h create mode 100644 generic/cryptCmd.c create mode 100644 generic/cryptCmd.h create mode 100644 tests/crypt.test diff --git a/ChangeLog b/ChangeLog index f55c70a..f7ef03f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,6 @@ +2024-07-07 Konstantin Kushnir + * Add basic cryptographic functions + 2024-06-23 Konstantin Kushnir * Bumped version to 1.8.0 diff --git a/configure b/configure index af313de..bf747fb 100755 --- a/configure +++ b/configure @@ -657,6 +657,8 @@ MAKE_LIB EGREP GREP COOKFS_PKGCONFIG_USECPKGCONFIG +COOKFS_PKGCONFIG_FEATURE_CRYPT +COOKFS_PKGCONFIG_USECCRYPT COOKFS_PKGCONFIG_USETCLCMDS COOKFS_PKGCONFIG_USECWRITER COOKFS_PKGCONFIG_USECVFS @@ -800,6 +802,7 @@ enable_c_writerchannel enable_c_pkgconfig enable_c_vfs enable_c_writer +enable_c_crypt enable_tcl_commands ' ac_precious_vars='build_alias @@ -1450,6 +1453,7 @@ Optional Features: --disable-c-pkgconfig Use pkgconfig written in C. Defaults to true --disable-c-vfs Use VFS support written in C. Defaults to true --disable-c-writer Use writer handler written in C. Defaults to true + --disable-c-crypt Use crypt functions written in C. Defaults to true --disable-tcl-commands Enable Tcl commands for objects. Defaults to true Optional Packages: @@ -8687,6 +8691,14 @@ else $as_nop USECWRITER=yes fi +# Check whether --enable-c-crypt was given. +if test ${enable_c_crypt+y} +then : + enableval=$enable_c_crypt; USECCRYPT=${enableval} +else $as_nop + USECCRYPT=yes +fi + # Check whether --enable-tcl-commands was given. if test ${enable_tcl_commands+y} then : @@ -8736,6 +8748,140 @@ fi +if test ${USECCRYPT} = yes; then + printf "%s\n" "#define COOKFS_USECRYPT 1" >>confdefs.h + + printf "%s\n" "#define COOKFS_USECCRYPT 1" >>confdefs.h + + COOKFS_PKGCONFIG_USECCRYPT=1 + COOKFS_PKGCONFIG_FEATURE_CRYPT=1 + + vars="crypt.c cryptCmd.c" + for i in $vars; do + case $i in + \$*) + # allow $-var names + PKG_SOURCES="$PKG_SOURCES $i" + PKG_OBJECTS="$PKG_OBJECTS $i" + ;; + *) + # check for existence - allows for generic/win/unix VPATH + # To add more dirs here (like 'src'), you have to update VPATH + # in Makefile.in as well + if test ! -f "${srcdir}/$i" -a ! -f "${srcdir}/generic/$i" \ + -a ! -f "${srcdir}/win/$i" -a ! -f "${srcdir}/unix/$i" \ + -a ! -f "${srcdir}/macosx/$i" \ + ; then + as_fn_error $? "could not find source file '$i'" "$LINENO" 5 + fi + PKG_SOURCES="$PKG_SOURCES $i" + # this assumes it is in a VPATH dir + i=`basename $i` + # handle user calling this before or after TEA_SETUP_COMPILER + if test x"${OBJEXT}" != x ; then + j="`echo $i | sed -e 's/\.[^.]*$//'`.${OBJEXT}" + else + j="`echo $i | sed -e 's/\.[^.]*$//'`.\${OBJEXT}" + fi + PKG_OBJECTS="$PKG_OBJECTS $j" + ;; + esac + done + + + + + vars="7zip/C/Aes.c 7zip/C/AesOpt.c 7zip/C/Sha256.c 7zip/C/Sha256Opt.c" + for i in $vars; do + case $i in + \$*) + # allow $-var names + PKG_SOURCES="$PKG_SOURCES $i" + PKG_OBJECTS="$PKG_OBJECTS $i" + ;; + *) + # check for existence - allows for generic/win/unix VPATH + # To add more dirs here (like 'src'), you have to update VPATH + # in Makefile.in as well + if test ! -f "${srcdir}/$i" -a ! -f "${srcdir}/generic/$i" \ + -a ! -f "${srcdir}/win/$i" -a ! -f "${srcdir}/unix/$i" \ + -a ! -f "${srcdir}/macosx/$i" \ + ; then + as_fn_error $? "could not find source file '$i'" "$LINENO" 5 + fi + PKG_SOURCES="$PKG_SOURCES $i" + # this assumes it is in a VPATH dir + i=`basename $i` + # handle user calling this before or after TEA_SETUP_COMPILER + if test x"${OBJEXT}" != x ; then + j="`echo $i | sed -e 's/\.[^.]*$//'`.${OBJEXT}" + else + j="`echo $i | sed -e 's/\.[^.]*$//'`.\${OBJEXT}" + fi + PKG_OBJECTS="$PKG_OBJECTS $j" + ;; + esac + done + + + + if test "${TEA_PLATFORM}" = "windows" ; then + + vars="-lbcrypt" + for i in $vars; do + if test "${TEA_PLATFORM}" = "windows" -a "$GCC" = "yes" ; then + # Convert foo.lib to -lfoo for GCC. No-op if not *.lib + i=`echo "$i" | sed -e 's/^\([^-].*\)\.[lL][iI][bB]$/-l\1/'` + fi + PKG_LIBS="$PKG_LIBS $i" + done + + + fi + # Include CpuArch.c only if we are not using c-lzma. Otherwise, this file + # will be included when processing the lzma option. + if test ${USECPAGES} != yes || test ${USELZMA} != yes; then + + vars="7zip/C/CpuArch.c" + for i in $vars; do + case $i in + \$*) + # allow $-var names + PKG_SOURCES="$PKG_SOURCES $i" + PKG_OBJECTS="$PKG_OBJECTS $i" + ;; + *) + # check for existence - allows for generic/win/unix VPATH + # To add more dirs here (like 'src'), you have to update VPATH + # in Makefile.in as well + if test ! -f "${srcdir}/$i" -a ! -f "${srcdir}/generic/$i" \ + -a ! -f "${srcdir}/win/$i" -a ! -f "${srcdir}/unix/$i" \ + -a ! -f "${srcdir}/macosx/$i" \ + ; then + as_fn_error $? "could not find source file '$i'" "$LINENO" 5 + fi + PKG_SOURCES="$PKG_SOURCES $i" + # this assumes it is in a VPATH dir + i=`basename $i` + # handle user calling this before or after TEA_SETUP_COMPILER + if test x"${OBJEXT}" != x ; then + j="`echo $i | sed -e 's/\.[^.]*$//'`.${OBJEXT}" + else + j="`echo $i | sed -e 's/\.[^.]*$//'`.\${OBJEXT}" + fi + PKG_OBJECTS="$PKG_OBJECTS $j" + ;; + esac + done + + + + fi +else + COOKFS_PKGCONFIG_USECCRYPT=0 + COOKFS_PKGCONFIG_FEATURE_CRYPT=0 +fi + if test ${USECPAGES} = yes; then printf "%s\n" "#define COOKFS_USECPAGES 1" >>confdefs.h @@ -9656,6 +9802,7 @@ fi + if test ${USECPKGCONFIG} = yes; then COOKFS_PKGCONFIG_USECPKGCONFIG=1 printf "%s\n" "#define COOKFS_PKGCONFIG_FEATURE_ASIDE $COOKFS_PKGCONFIG_FEATURE_ASIDE" >>confdefs.h @@ -9686,6 +9833,10 @@ if test ${USECPKGCONFIG} = yes; then printf "%s\n" "#define COOKFS_PKGCONFIG_USETCLCMDS $COOKFS_PKGCONFIG_USETCLCMDS" >>confdefs.h + printf "%s\n" "#define COOKFS_PKGCONFIG_USECCRYPT $COOKFS_PKGCONFIG_USECCRYPT" >>confdefs.h + + printf "%s\n" "#define COOKFS_PKGCONFIG_FEATURE_CRYPT $COOKFS_PKGCONFIG_FEATURE_CRYPT" >>confdefs.h + printf "%s\n" "#define COOKFS_USECPKGCONFIG 1" >>confdefs.h diff --git a/configure.in b/configure.in index 6a40483..887e5e9 100644 --- a/configure.in +++ b/configure.in @@ -127,6 +127,7 @@ AC_ARG_ENABLE(c-writerchannel, [ --disable-c-writerchannel Use writer handler w AC_ARG_ENABLE(c-pkgconfig, [ --disable-c-pkgconfig Use pkgconfig written in C. Defaults to true], USECPKGCONFIG=${enableval}, USECPKGCONFIG=yes) AC_ARG_ENABLE(c-vfs, [ --disable-c-vfs Use VFS support written in C. Defaults to true], USECVFS=${enableval}, USECVFS=yes) AC_ARG_ENABLE(c-writer, [ --disable-c-writer Use writer handler written in C. Defaults to true], USECWRITER=${enableval}, USECWRITER=yes) +AC_ARG_ENABLE(c-crypt, [ --disable-c-crypt Use crypt functions written in C. Defaults to true], USECCRYPT=${enableval}, USECCRYPT=yes) AC_ARG_ENABLE(tcl-commands, [ --disable-tcl-commands Enable Tcl commands for objects. Defaults to true], USETCLCMDS=${enableval}, USETCLCMDS=yes) @@ -138,6 +139,26 @@ TEA_ADD_TCL_SOURCES([scripts/pages.tcl]) TEA_ADD_TCL_SOURCES([scripts/readerchannel.tcl]) TEA_ADD_TCL_SOURCES([scripts/fsindex.tcl]) +if test ${USECCRYPT} = yes; then + AC_DEFINE(COOKFS_USECRYPT) + AC_DEFINE(COOKFS_USECCRYPT) + COOKFS_PKGCONFIG_USECCRYPT=1 + COOKFS_PKGCONFIG_FEATURE_CRYPT=1 + TEA_ADD_SOURCES([crypt.c cryptCmd.c]) + TEA_ADD_SOURCES([7zip/C/Aes.c 7zip/C/AesOpt.c 7zip/C/Sha256.c 7zip/C/Sha256Opt.c]) + if test "${TEA_PLATFORM}" = "windows" ; then + TEA_ADD_LIBS([-lbcrypt]) + fi + # Include CpuArch.c only if we are not using c-lzma. Otherwise, this file + # will be included when processing the lzma option. + if test ${USECPAGES} != yes || test ${USELZMA} != yes; then + TEA_ADD_SOURCES([7zip/C/CpuArch.c]) + fi +else + COOKFS_PKGCONFIG_USECCRYPT=0 + COOKFS_PKGCONFIG_FEATURE_CRYPT=0 +fi + if test ${USECPAGES} = yes; then AC_DEFINE(COOKFS_USECPAGES) COOKFS_PKGCONFIG_USECPAGES=1 @@ -365,7 +386,6 @@ else COOKFS_PKGCONFIG_USETCLCMDS=1 fi - AC_SUBST(COOKFS_PKGCONFIG_FEATURE_ASIDE) AC_SUBST(COOKFS_PKGCONFIG_USECPAGES) AC_SUBST(COOKFS_PKGCONFIG_USECREADERCHAN) @@ -379,6 +399,8 @@ AC_SUBST(COOKFS_PKGCONFIG_FEATURE_METADATA) AC_SUBST(COOKFS_PKGCONFIG_USECVFS) AC_SUBST(COOKFS_PKGCONFIG_USECWRITER) AC_SUBST(COOKFS_PKGCONFIG_USETCLCMDS) +AC_SUBST(COOKFS_PKGCONFIG_USECCRYPT) +AC_SUBST(COOKFS_PKGCONFIG_FEATURE_CRYPT) if test ${USECPKGCONFIG} = yes; then COOKFS_PKGCONFIG_USECPKGCONFIG=1 @@ -396,6 +418,8 @@ if test ${USECPKGCONFIG} = yes; then AC_DEFINE_UNQUOTED(COOKFS_PKGCONFIG_USECVFS, $COOKFS_PKGCONFIG_USECVFS) AC_DEFINE_UNQUOTED(COOKFS_PKGCONFIG_USECWRITER, $COOKFS_PKGCONFIG_USECWRITER) AC_DEFINE_UNQUOTED(COOKFS_PKGCONFIG_USETCLCMDS, $COOKFS_PKGCONFIG_USETCLCMDS) + AC_DEFINE_UNQUOTED(COOKFS_PKGCONFIG_USECCRYPT, $COOKFS_PKGCONFIG_USECCRYPT) + AC_DEFINE_UNQUOTED(COOKFS_PKGCONFIG_FEATURE_CRYPT, $COOKFS_PKGCONFIG_FEATURE_CRYPT) AC_DEFINE(COOKFS_USECPKGCONFIG) else diff --git a/generic/cookfs.c b/generic/cookfs.c index 660331a..d009dd0 100644 --- a/generic/cookfs.c +++ b/generic/cookfs.c @@ -8,6 +8,7 @@ */ #include "cookfs.h" +#include "crypt.h" #ifdef COOKFS_USECPAGES #include "pagesCmd.h" @@ -33,6 +34,10 @@ #include "writerchannelCmd.h" #endif /* COOKFS_USECWRITERCHAN */ +#ifdef COOKFS_USECCRYPT +#include "cryptCmd.h" +#endif /* COOKFS_USECCRYPT */ + /* *---------------------------------------------------------------------- * @@ -118,6 +123,15 @@ Cookfs_Init(Tcl_Interp *interp) // cppcheck-suppress unusedFunction return TCL_ERROR; } +#if defined(COOKFS_USECCRYPT) + Cookfs_CryptInit(); +#if defined(COOKFS_USETCLCMDS) + if (Cookfs_InitCryptCmd(interp) != TCL_OK) { + return TCL_ERROR; + } +#endif +#endif + #if defined(COOKFS_USECVFS) if (Cookfs_InitVfsMountCmd(interp) != TCL_OK) { return TCL_ERROR; diff --git a/generic/cookfs.h b/generic/cookfs.h index 5507517..4364fb0 100644 --- a/generic/cookfs.h +++ b/generic/cookfs.h @@ -20,9 +20,18 @@ #include #ifdef COOKFS_INTERNAL_DEBUG +#ifndef __FUNCTION_NAME__ + #ifdef _WIN32 // WINDOWS + #define __FUNCTION_NAME__ __FUNCTION__ + #else // GCC + #define __FUNCTION_NAME__ __func__ + #endif +#endif #define CookfsLog(a) {a; printf("\n"); fflush(stdout);} +#define CookfsLog2(a) {printf("%s: ", __FUNCTION_NAME__); a; printf("\n"); fflush(stdout);} #else #define CookfsLog(a) {} +#define CookfsLog2(a) {} #endif #define UNUSED(expr) do { (void)(expr); } while (0) @@ -49,6 +58,10 @@ #define Tcl_NewSizeIntFromObj Tcl_NewWideIntObj #endif /* TCL_SIZE_MAX */ +#ifndef Tcl_BounceRefCount +#define Tcl_BounceRefCount(x) Tcl_IncrRefCount((x));Tcl_DecrRefCount((x)) +#endif + #define SET_ERROR(e) \ if (err != NULL) { *err = (e); } @@ -90,6 +103,8 @@ static Tcl_Config const cookfs_pkgconfig[] = { {"c-writerchannel", STRINGIFY(COOKFS_PKGCONFIG_USECWRITERCHAN)}, {"c-vfs", STRINGIFY(COOKFS_PKGCONFIG_USECVFS)}, {"c-writer", STRINGIFY(COOKFS_PKGCONFIG_USECWRITER)}, + {"c-crypt", STRINGIFY(COOKFS_PKGCONFIG_USECCRYPT)}, + {"feature-crypt", STRINGIFY(COOKFS_PKGCONFIG_FEATURE_CRYPT)}, {"feature-aside", STRINGIFY(COOKFS_PKGCONFIG_FEATURE_ASIDE)}, {"feature-bzip2", STRINGIFY(COOKFS_PKGCONFIG_USEBZ2)}, {"feature-lzma", STRINGIFY(COOKFS_PKGCONFIG_USELZMA)}, diff --git a/generic/crypt.c b/generic/crypt.c new file mode 100644 index 0000000..2463212 --- /dev/null +++ b/generic/crypt.c @@ -0,0 +1,234 @@ +/* + * crypt.c + * + * Provides implementation of cryptographic functions + * + * (c) 2024 Konstantin Kushnir + */ + +#include "cookfs.h" + +// for getpid() +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#ifndef STRICT +#define STRICT // See MSDN Article Q83456 +#endif +#include +#undef WIN32_LEAN_AND_MEAN +// for BCryptGenRandom() +#include +// for NT_SUCCESS() +#include +#endif /* _WIN32 */ + +#include "../7zip/C/Aes.h" +#include "../7zip/C/Sha256.h" + +void Cookfs_CryptInit(void) { + Sha256Prepare(); + AesGenTables(); +} + +typedef struct { + CSha256 inner; + CSha256 outer; +} HMAC_CTX; + +static void Cookfs_HmacInit(HMAC_CTX *ctx, unsigned char *key, + Tcl_Size keySize) +{ + unsigned char k[SHA256_BLOCK_SIZE]; + + // Key longer than sha256 blockSize must be shortened + if (keySize > SHA256_BLOCK_SIZE) { + Sha256_Init(&ctx->inner); + Sha256_Update(&ctx->inner, key, keySize); + Sha256_Final(&ctx->inner, k); + // cppcheck-suppress unreadVariable symbolName=key + key = k; + keySize = SHA256_DIGEST_SIZE; + } else { + memcpy(k, key, keySize); + } + + // pad the key with zeros on the right + if (SHA256_BLOCK_SIZE > keySize) { + memset(k + keySize, 0, SHA256_BLOCK_SIZE - keySize); + } + + unsigned char block_inner[SHA256_BLOCK_SIZE]; + unsigned char block_outer[SHA256_BLOCK_SIZE]; + + for (int i = 0; i < SHA256_BLOCK_SIZE; i++) { + block_inner[i] = 0x36 ^ k[i]; + block_outer[i] = 0x5c ^ k[i]; + } + + Sha256_Init(&ctx->inner); + Sha256_Update(&ctx->inner, block_inner, sizeof(block_inner)); + + Sha256_Init(&ctx->outer); + Sha256_Update(&ctx->outer, block_outer, sizeof(block_outer)); +} + +static inline void Cookfs_HmacUpdate(HMAC_CTX *ctx, unsigned char *data, + Tcl_Size dataSize) +{ + Sha256_Update(&ctx->inner, data, dataSize); +} + +static inline void Cookfs_HmacFinal(HMAC_CTX *ctx, unsigned char *output) { + Sha256_Final(&ctx->inner, output); + Sha256_Update(&ctx->outer, output, SHA256_DIGEST_SIZE); + Sha256_Final(&ctx->outer, output); +} + +void Cookfs_Pbkdf2Hmac(unsigned char *secret, Tcl_Size secretSize, + unsigned char *salt, Tcl_Size saltSize, unsigned int iterations, + unsigned int dklen, unsigned char *output) +{ + CookfsLog2(printf("enter; secretSize: %" TCL_SIZE_MODIFIER "d, saltSize: %" + TCL_SIZE_MODIFIER "d, iter: %u, dklen: %u", secretSize, saltSize, + iterations, dklen)); + + HMAC_CTX ctx; + + unsigned char md[SHA256_DIGEST_SIZE]; + + int counter = 1; + + while (dklen) { + unsigned char c[4]; + Cookfs_Int2Binary(&counter, c, 1); + + Cookfs_HmacInit(&ctx, secret, secretSize); + Cookfs_HmacUpdate(&ctx, salt, saltSize); + Cookfs_HmacUpdate(&ctx, c, sizeof(c)); + Cookfs_HmacFinal(&ctx, md); + + unsigned int want_copy = (dklen > SHA256_DIGEST_SIZE ? + SHA256_DIGEST_SIZE : dklen); + + memcpy(output, md, want_copy); + CookfsLog2(printf("want_copy: %u", want_copy)); + + for (unsigned int ic = 1; ic < iterations; ic++) { + Cookfs_HmacInit(&ctx, secret, secretSize); + Cookfs_HmacUpdate(&ctx, md, sizeof(md)); + Cookfs_HmacFinal(&ctx, md); + for (unsigned int i = 0; i < want_copy; i++) { + output[i] ^= md[i]; + } + } + + dklen -= want_copy; + output += want_copy; + counter++; + } +} + +void Cookfs_RandomGenerate(Tcl_Interp *interp, unsigned char *buf, Tcl_Size size) { + + CookfsLog2(printf("enter, want to generate %" TCL_SIZE_MODIFIER "d" + " bytes", size)); + + // Cleanup random destination storage + memset(buf, 0, size); + +#ifdef _WIN32 + // Use BCryptGenRandom() on Windows. + DWORD status = BCryptGenRandom(NULL, buf, size, + BCRYPT_USE_SYSTEM_PREFERRED_RNG); + if(NT_SUCCESS(status)) { + CookfsLog2(printf("return result from windows-specific rng" + " generator")); + return; + } + CookfsLog2(printf("WARNING: windows-specific rng generator failed")); + goto fallback_tcl; +#else + // Try using /dev/urandom if it exists + Tcl_Obj *urandom = Tcl_NewStringObj("/dev/urandom", -1); + Tcl_IncrRefCount(urandom); + Tcl_Channel chan = Tcl_FSOpenFileChannel(NULL, urandom, "rb", 0); + Tcl_DecrRefCount(urandom); + + if (chan != NULL) { + Tcl_Size read = Tcl_Read(chan, (char *)buf, size); + Tcl_Close(NULL, chan); + if (read == size) { + CookfsLog2(printf("return result from /dev/urandom")); + return; + } + CookfsLog2(printf("WARNING: could not read enough bytes from /dev/urandom," + " got only %" TCL_SIZE_MODIFIER "d bytes", read)); + } + CookfsLog2(printf("WARNING: could not open /dev/urandom, error code: %d", + Tcl_GetErrno())); + goto fallback_tcl; +#endif /* _WIN32 */ + +fallback_tcl: + + // Fallback to Tcl random number generator if the platform-specific random + // number generator did not work. + + if (interp != NULL) { + CookfsLog2(printf("try to use Tcl rng generator")); + double r; + for (Tcl_Size i = 0; i < size; i += sizeof(r)) { + if (Tcl_EvalEx(interp, "::tcl::mathfunc::rand", -1, 0) != TCL_OK) { + CookfsLog2(printf("WARNING: got error from Tcl: %s", + Tcl_GetString(Tcl_GetObjResult(interp)))); + goto fallback_c; + } + if (Tcl_GetDoubleFromObj(NULL, Tcl_GetObjResult(interp), &r) + != TCL_OK) + { + CookfsLog2(printf("WARNING: got something else than double" + " from Tcl: %s", Tcl_GetString(Tcl_GetObjResult(interp)))); + goto fallback_c; + } + unsigned int want_copy = ((unsigned)i + sizeof(r) > (unsigned)size ? + (unsigned)size - (unsigned)i : sizeof(r)); + CookfsLog2(printf("copy %u bytes from Tcl rng generator", + want_copy)); + memcpy(&buf[i], (void *)&r, want_copy); + } + CookfsLog2(printf("return result from Tcl rng generator")); + return; + } else { + CookfsLog2(printf("WARNING: could not use Tcl rng generator, interp" + " is NULL")); + } + +fallback_c: + + // Fallback to C random number generator if enything else did not work. + CookfsLog2(printf("try to use C rng generator")); + + // Initializing the rng generator to the current time may not be secure + // enough, since timestamps can be detected from the archive file or + // files within the archive. However, the current number of + // microseconds+pid should be sufficient. + Tcl_Time now; + Tcl_GetTime(&now); + srand(now.sec ^ now.usec ^ getpid()); + int r; + for (Tcl_Size i = 0; i < size; i += sizeof(r)) { + r = rand(); + unsigned int want_copy = ((unsigned)i + sizeof(r) > (unsigned)size ? + (unsigned)size - (unsigned)i : sizeof(r)); + CookfsLog2(printf("copy %u bytes from C rng generator", + want_copy)); + memcpy(&buf[i], (void *)&r, want_copy); + } + + CookfsLog2(printf("return result from C rng generator")); + return; + +} + diff --git a/generic/crypt.h b/generic/crypt.h new file mode 100644 index 0000000..09e1bcc --- /dev/null +++ b/generic/crypt.h @@ -0,0 +1,13 @@ +/* (c) 2014 Konstantin Kushnir */ + +#ifndef COOKFS_CRYPT_H +#define COOKFS_CRYPT_H 1 + +void Cookfs_CryptInit(void); + +void Cookfs_RandomGenerate(Tcl_Interp *interp, unsigned char *buf, Tcl_Size size); +void Cookfs_Pbkdf2Hmac(unsigned char *secret, Tcl_Size secretSize, + unsigned char *salt, Tcl_Size saltSize, unsigned int iterations, + unsigned int dklen, unsigned char *output); + +#endif /* COOKFS_CRYPT_H */ diff --git a/generic/cryptCmd.c b/generic/cryptCmd.c new file mode 100644 index 0000000..4c0d70a --- /dev/null +++ b/generic/cryptCmd.c @@ -0,0 +1,115 @@ +/* + * cryptCmd.c + * + * Provides Tcl commands for cryptographic functions + * + * (c) 2024 Konstantin Kushnir + */ + +#include "cookfs.h" +#include "crypt.h" + +static int CookfsRandomGenerateObjectCmd(ClientData clientData, + Tcl_Interp *interp, int objc, Tcl_Obj *const objv[]) +{ + UNUSED(clientData); + + if (objc != 2) { + Tcl_WrongNumArgs(interp, 1, objv, "size"); + return TCL_ERROR; + } + + int size; + if (Tcl_GetIntFromObj(interp, objv[1], &size) != TCL_OK) { + return TCL_ERROR; + } + + Tcl_Obj *rc = Tcl_NewByteArrayObj(NULL, 0); + unsigned char *str = Tcl_SetByteArrayLength(rc, size); + + Cookfs_RandomGenerate(interp, str, size); + Tcl_SetObjResult(interp, rc); + + return TCL_OK; +} + +static int CookfsPbkdf2HmacObjectCmd(ClientData clientData, + Tcl_Interp *interp, int objc, Tcl_Obj *const objv[]) +{ + UNUSED(clientData); + + if (objc < 3) { + Tcl_WrongNumArgs(interp, 1, objv, "?-iterations iterations? ?-dklen" + " dklen? password salt"); + return TCL_ERROR; + } + + static const char *const options[] = { + "-iterations", "-dklen", NULL + }; + + // Default values for iterations and dklen + int opt_vals[2] = { 1, 32 }; + + int idx = 1; + for (; idx < objc - 2; idx++) { + + int opt; + if (Tcl_GetIndexFromObj(interp, objv[idx], options, "option", 0, &opt) + != TCL_OK) + { + return TCL_ERROR; + } + + if (++idx >= objc - 2) { + Tcl_SetObjResult(interp, Tcl_ObjPrintf("missing argument to" + " %s option", options[opt])); + return TCL_ERROR; + } + + if (Tcl_GetIntFromObj(NULL, objv[idx], &opt_vals[opt]) != TCL_OK + || opt_vals[opt] < 1) + { + Tcl_SetObjResult(interp, Tcl_ObjPrintf("option %s requires" + " an unsigned integer >= 1 as value, but got \"%s\"", + options[opt], Tcl_GetString(objv[idx]))); + return TCL_ERROR; + } + + } + + Tcl_Size passLen; + unsigned char *passStr = Tcl_GetByteArrayFromObj(objv[idx++], &passLen); + + Tcl_Size saltLen; + unsigned char *saltStr = Tcl_GetByteArrayFromObj(objv[idx++], &saltLen); + + Tcl_Obj *rc = Tcl_NewByteArrayObj(NULL, 0); + unsigned char *str = Tcl_SetByteArrayLength(rc, opt_vals[1]); + + Cookfs_Pbkdf2Hmac(passStr, passLen, saltStr, saltLen, opt_vals[0], + opt_vals[1], str); + + Tcl_SetObjResult(interp, rc); + + return TCL_OK; +} + +int Cookfs_InitCryptCmd(Tcl_Interp *interp) { + + Tcl_CreateNamespace(interp, "::cookfs::c::crypt", NULL, NULL); + + Tcl_CreateObjCommand(interp, "::cookfs::c::crypt::rng", + CookfsRandomGenerateObjectCmd, (ClientData) NULL, NULL); + + Tcl_CreateObjCommand(interp, "::cookfs::c::crypt::pbkdf2_hmac", + CookfsPbkdf2HmacObjectCmd, (ClientData) NULL, NULL); + + Tcl_CreateAlias(interp, "::cookfs::crypt::rng", interp, + "::cookfs::c::crypt::rng", 0, NULL); + Tcl_CreateAlias(interp, "::cookfs::crypt::pbkdf2_hmac", interp, + "::cookfs::c::crypt::pbkdf2_hmac", 0, NULL); + + return TCL_OK; + +} diff --git a/generic/cryptCmd.h b/generic/cryptCmd.h new file mode 100644 index 0000000..a70b045 --- /dev/null +++ b/generic/cryptCmd.h @@ -0,0 +1,8 @@ +/* (c) 2014 Konstantin Kushnir */ + +#ifndef COOKFS_CRYPTCMD_H +#define COOKFS_CRYPTCMD_H 1 + +int Cookfs_InitCryptCmd(Tcl_Interp *interp); + +#endif /* COOKFS_CRYPTCMD_H */ diff --git a/pkgconfig.tcl.in b/pkgconfig.tcl.in index 8bf51be..c0d19f0 100644 --- a/pkgconfig.tcl.in +++ b/pkgconfig.tcl.in @@ -14,6 +14,7 @@ proc cookfs::pkgconfig {{command ""} {value ""} {newValue ""}} { c-readerchannel "@COOKFS_PKGCONFIG_USECREADERCHAN@" c-writerchannel "@COOKFS_PKGCONFIG_USECWRITERCHAN@" c-writer "@COOKFS_PKGCONFIG_USECWRITER@" + c-crypt "@COOKFS_PKGCONFIG_USECCRYPT@" tcl-commands "@COOKFS_PKGCONFIG_USETCLCMDS@" @@ -23,6 +24,7 @@ proc cookfs::pkgconfig {{command ""} {value ""} {newValue ""}} { feature-zstd "@COOKFS_PKGCONFIG_USEZSTD@" feature-brotli "@COOKFS_PKGCONFIG_USEBROTLI@" feature-metadata "@COOKFS_PKGCONFIG_FEATURE_METADATA@" + feature-crypt "@COOKFS_PKGCONFIG_FEATURE_CRYPT@" } set pkgconfigInitialized 1 } diff --git a/tests/all.tcl b/tests/all.tcl index 8aa7406..73b9026 100644 --- a/tests/all.tcl +++ b/tests/all.tcl @@ -43,6 +43,10 @@ if {[cookfs::pkgconfig get feature-metadata]} { lappend constraints cookfsMetadata } +if {[cookfs::pkgconfig get feature-crypt]} { + lappend constraints cookfsCrypt +} + tcltest::testConstraint cookfsCompressionNone 1 tcltest::testConstraint cookfsCompressionZlib 1 tcltest::testConstraint cookfsCompressionBz2 [cookfs::pkgconfig get feature-bzip2] diff --git a/tests/crypt.test b/tests/crypt.test new file mode 100644 index 0000000..a05a70b --- /dev/null +++ b/tests/crypt.test @@ -0,0 +1,127 @@ + +tcltest::test cookfsCrypt-1.1.1 "Test crypt::rng without arguments" -constraints {cookfsCrypt enabledTclCmds} -body { + cookfs::crypt::rng +} -returnCodes error -result {wrong # args: should be "cookfs::crypt::rng size"} + +tcltest::test cookfsCrypt-1.1.2 "Test crypt::rng with wrong # args" -constraints {cookfsCrypt enabledTclCmds} -body { + cookfs::crypt::rng 1 2 +} -returnCodes error -result {wrong # args: should be "cookfs::crypt::rng size"} + +tcltest::test cookfsCrypt-1.1.3 "Test crypt::rng with bad arg" -constraints {cookfsCrypt enabledTclCmds} -body { + cookfs::crypt::rng a +} -returnCodes error -result {expected integer but got "a"} + +tcltest::test cookfsCrypt-1.2 "Test crypt::rng, get 1 random byte" -constraints {cookfsCrypt enabledTclCmds} -body { + string length [cookfs::crypt::rng 1] +} -result 1 + +tcltest::test cookfsCrypt-1.3 "Test crypt::rng, get 2 random bytes" -constraints {cookfsCrypt enabledTclCmds} -body { + string length [cookfs::crypt::rng 2] +} -result 2 + +tcltest::test cookfsCrypt-1.4 "Test crypt::rng, get 8 random bytes and ensure they are not 0" -constraints {cookfsCrypt enabledTclCmds} -body { + set rng [cookfs::crypt::rng 8] + set result [string length $rng] + set rng [binary encode hex $rng] + if { $rng eq [string repeat {00} 8] } { + append result " Not ok: $rng" + } + set result +} -cleanup { + unset -nocomplain rng +} -result [list 8] + +tcltest::test cookfsCrypt-1.5 "Test crypt::rng, get 65 random bytes and ensure they are not 0" -constraints {cookfsCrypt enabledTclCmds} -body { + set rng [cookfs::crypt::rng 65] + set result [string length $rng] + set rng [binary encode hex $rng] + if { $rng eq [string repeat {00} 65] } { + append result " Not ok: $rng" + } + set result +} -cleanup { + unset -nocomplain rng +} -result [list 65] + +tcltest::test cookfsCrypt-2.1.1 "Test crypt::pbkdf2_hmac without arguments" -constraints {cookfsCrypt enabledTclCmds} -body { + cookfs::crypt::pbkdf2_hmac +} -returnCodes error -result {wrong # args: should be "cookfs::crypt::pbkdf2_hmac ?-iterations iterations? ?-dklen dklen? password salt"} + +tcltest::test cookfsCrypt-2.1.2 "Test crypt::pbkdf2_hmac with 1 argument" -constraints {cookfsCrypt enabledTclCmds} -body { + cookfs::crypt::pbkdf2_hmac a +} -returnCodes error -result {wrong # args: should be "cookfs::crypt::pbkdf2_hmac ?-iterations iterations? ?-dklen dklen? password salt"} + +tcltest::test cookfsCrypt-2.1.3 "Test crypt::pbkdf2_hmac with wrong args" -constraints {cookfsCrypt enabledTclCmds} -body { + cookfs::crypt::pbkdf2_hmac a b c +} -returnCodes error -result {bad option "a": must be -iterations or -dklen} + +tcltest::test cookfsCrypt-2.1.4 "Test crypt::pbkdf2_hmac with no value for -iterations" -constraints {cookfsCrypt enabledTclCmds} -body { + cookfs::crypt::pbkdf2_hmac -iterations b c +} -returnCodes error -result {missing argument to -iterations option} + +tcltest::test cookfsCrypt-2.1.5 "Test crypt::pbkdf2_hmac with no value for -dklen" -constraints {cookfsCrypt enabledTclCmds} -body { + cookfs::crypt::pbkdf2_hmac -dklen b c +} -returnCodes error -result {missing argument to -dklen option} + +tcltest::test cookfsCrypt-2.1.6 "Test crypt::pbkdf2_hmac with bad value for -iterations" -constraints {cookfsCrypt enabledTclCmds} -body { + cookfs::crypt::pbkdf2_hmac -iterations a b c +} -returnCodes error -result {option -iterations requires an unsigned integer >= 1 as value, but got "a"} + +tcltest::test cookfsCrypt-2.1.7 "Test crypt::pbkdf2_hmac with number for -iterations" -constraints {cookfsCrypt enabledTclCmds} -body { + cookfs::crypt::pbkdf2_hmac -iterations 0 b c +} -returnCodes error -result {option -iterations requires an unsigned integer >= 1 as value, but got "0"} + +tcltest::test cookfsCrypt-2.1.8 "Test crypt::pbkdf2_hmac with bad value for -dklen" -constraints {cookfsCrypt enabledTclCmds} -body { + cookfs::crypt::pbkdf2_hmac -dklen a b c +} -returnCodes error -result {option -dklen requires an unsigned integer >= 1 as value, but got "a"} + +tcltest::test cookfsCrypt-2.1.9 "Test crypt::pbkdf2_hmac with number for -dklen" -constraints {cookfsCrypt enabledTclCmds} -body { + cookfs::crypt::pbkdf2_hmac -dklen 0 b c +} -returnCodes error -result {option -dklen requires an unsigned integer >= 1 as value, but got "0"} + +tcltest::test cookfsCrypt-2.2.1 "Test crypt::pbkdf2_hmac default dklen = 32" -constraints {cookfsCrypt enabledTclCmds} -body { + string length [cookfs::crypt::pbkdf2_hmac a b] +} -result 32 + +tcltest::test cookfsCrypt-2.2.2 "Test crypt::pbkdf2_hmac with dklen = 1" -constraints {cookfsCrypt enabledTclCmds} -body { + string length [cookfs::crypt::pbkdf2_hmac -dklen 1 a b] +} -result 1 + +tcltest::test cookfsCrypt-2.2.3 "Test crypt::pbkdf2_hmac with dklen = 2" -constraints {cookfsCrypt enabledTclCmds} -body { + string length [cookfs::crypt::pbkdf2_hmac -dklen 2 a b] +} -result 2 + +tcltest::test cookfsCrypt-2.2.4 "Test crypt::pbkdf2_hmac with dklen = 201" -constraints {cookfsCrypt enabledTclCmds} -body { + string length [cookfs::crypt::pbkdf2_hmac -dklen 201 a b] +} -result 201 + +tcltest::test cookfsCrypt-2.3.1 "Test crypt::pbkdf2_hmac known test vector #1" -constraints {cookfsCrypt enabledTclCmds} -body { + binary encode hex [cookfs::crypt::pbkdf2_hmac -iterations 1 "password" "salt"] +} -result {120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b} + +tcltest::test cookfsCrypt-2.3.2 "Test crypt::pbkdf2_hmac known test vector #2" -constraints {cookfsCrypt enabledTclCmds} -body { + binary encode hex [cookfs::crypt::pbkdf2_hmac -iterations 2 "password" "salt"] +} -result {ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43} + +tcltest::test cookfsCrypt-2.3.3 "Test crypt::pbkdf2_hmac known test vector #3" -constraints {cookfsCrypt enabledTclCmds} -body { + binary encode hex [cookfs::crypt::pbkdf2_hmac -iterations 4096 "password" "salt"] +} -result {c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a} + +tcltest::test cookfsCrypt-2.3.4 "Test crypt::pbkdf2_hmac known test vector #4" -constraints {cookfsCrypt enabledTclCmds} -body { + binary encode hex [cookfs::crypt::pbkdf2_hmac -iterations 4096 -dklen 16 "pass\0word" "sa\0lt"] +} -result {89b69d0516f829893c696226650a8687} + +tcltest::test cookfsCrypt-2.3.5 "Test crypt::pbkdf2_hmac known test vector #5" -constraints {cookfsCrypt enabledTclCmds} -body { + binary encode hex [cookfs::crypt::pbkdf2_hmac -iterations 4096 -dklen 40 "passwordPASSWORDpassword" "saltSALTsaltSALTsaltSALTsaltSALTsalt"] +} -result {348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9} + +tcltest::test cookfsCrypt-2.3.6 "Test crypt::pbkdf2_hmac known test vector #6" -constraints {cookfsCrypt enabledTclCmds} -body { + binary encode hex [cookfs::crypt::pbkdf2_hmac -iterations 2048 -dklen 64 \ + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" \ + "6d6e656d6f6e6963e383a1e383bce38388e383abe382abe38299e3838fe38299e382a6e38299e382a1e381afe3829ae381afe38299e3818fe38299e3829de38299e381a1e381a1e38299e58d81e4babae58d81e889b2"] +} -result {3b19907cb907d1ee6e5a0ecb80bd66e2776d1f2c73f4789eafcad94fda832e970471ceb0d200ede70e63ae021044cf4b58b1011e34252ace8d94a48c287906ec} + +tcltest::test cookfsCrypt-2.3.7 "Test crypt::pbkdf2_hmac empty values" -constraints {cookfsCrypt enabledTclCmds} -body { + binary encode hex [cookfs::crypt::pbkdf2_hmac "" ""] +} -result {f7ce0b653d2d72a4108cf5abe912ffdd777616dbbb27a70e8204f3ae2d0f6fad} + diff --git a/tests/pkgconfig.test b/tests/pkgconfig.test index 6031640..45075cc 100644 --- a/tests/pkgconfig.test +++ b/tests/pkgconfig.test @@ -17,6 +17,7 @@ tcltest::test cookfsPkgconfig-1 "Check pkgconfig fields and their types" -body { } -cleanup { unset -nocomplain result f v } -result { +c-crypt bool c-fsindex bool c-pages bool c-readerchannel bool @@ -26,6 +27,7 @@ c-writerchannel bool feature-aside bool feature-brotli bool feature-bzip2 bool +feature-crypt bool feature-lzma bool feature-metadata bool feature-zstd bool