// calogCrypto.c -- calog cryptography library (see calogCrypto.h). Thin, binary-safe // bindings over OpenSSL's one-shot primitives (EVP_Digest, HMAC, RAND_bytes, // EVP_EncodeBlock/EVP_DecodeBlock): SHA-256/SHA-1 hashing, HMAC-SHA-256, random bytes, // base64/hex codecs, and version-4 UUIDs. No shared state: every native is a pure function // of its arguments. #include "calogCrypto.h" #include "calogInternal.h" #include #include #include #include #include #include #include static int32_t cryptoBase64DecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t cryptoBase64EncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t cryptoBytesToHex(CalogValueT *result, const unsigned char *bytes, size_t length); static int32_t cryptoDigestHex(CalogValueT *args, int32_t argCount, CalogValueT *result, const EVP_MD *md, const char *usage); static int32_t cryptoEqualsNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t cryptoHashSha1Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t cryptoHashSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t cryptoHexDecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t cryptoHexEncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t cryptoHmacSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t cryptoPbkdf2Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t cryptoRandomBytesNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t cryptoUuidNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); // The single source of truth for lowercase hex digit rendering, shared by cryptoBytesToHex // and cryptoUuidNative. static const char cryptoHexDigits[] = "0123456789abcdef"; // PBKDF2 bounds. The floor is not a recommendation -- it is the point below which the result is // not a password hash at all; pick the real count by timing the target machine. The ceiling and the // length cap exist so a script cannot ask for work that never returns: every native here is // registered inline (calogRegisterBatch registers inline), so a derivation holds the calling // script's own thread for its duration rather than the host's, which is what makes a deliberately // slow native safe to offer at all. #define CRYPTO_TEXT_(x) #x #define CRYPTO_TEXT(x) CRYPTO_TEXT_(x) #define CRYPTO_PBKDF2_ITERATIONS_MIN 1000 #define CRYPTO_PBKDF2_ITERATIONS_MAX 10000000 #define CRYPTO_PBKDF2_LENGTH_MIN 16 #define CRYPTO_PBKDF2_LENGTH_MAX 1024 // Every inline native this library exposes. One table so registration is a single // checked call (calogRegisterBatch) rather than a run of calls whose status was dropped. static const CalogNativeEntryT gCryptoNatives[] = { { "cryptoHashSha256", cryptoHashSha256Native }, { "cryptoHashSha1", cryptoHashSha1Native }, { "cryptoHmacSha256", cryptoHmacSha256Native }, { "cryptoRandomBytes", cryptoRandomBytesNative }, { "cryptoBase64Encode", cryptoBase64EncodeNative }, { "cryptoBase64Decode", cryptoBase64DecodeNative }, { "cryptoHexEncode", cryptoHexEncodeNative }, { "cryptoHexDecode", cryptoHexDecodeNative }, { "cryptoUuid", cryptoUuidNative }, { "cryptoEquals", cryptoEqualsNative }, { "cryptoPbkdf2", cryptoPbkdf2Native }, }; int32_t calogCryptoRegister(CalogT *calog) { return calogRegisterBatch(calog, gCryptoNatives, (int64_t)(sizeof(gCryptoNatives) / sizeof(gCryptoNatives[0])), NULL); } static int32_t cryptoBase64DecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { const unsigned char *in; unsigned char *out; int64_t length; size_t outCap; int32_t decoded; int32_t pad; int32_t status; (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "cryptoBase64Decode expects (text)"); } length = args[0].as.s.length; in = (const unsigned char *)args[0].as.s.bytes; // EVP_DecodeBlock ignores trailing whitespace before decoding; trim it here too so the // decoded length and the '=' padding count are computed from the real base64 content // (otherwise "YQ==\n" would decode with a spurious trailing NUL). while (length > 0) { unsigned char c; c = in[length - 1]; if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v') { length--; } else { break; } } if (length == 0) { return calogValueString(result, "", 0); } if ((length % 4) != 0 || length > INT_MAX) { return calogFail(result, calogErrArgE, "cryptoBase64Decode: invalid base64 length"); } // EVP_DecodeBlock treats '=' as a zero sextet no matter where it appears, so without this // check an interior '=' (e.g. "YQ==YQ==") would decode as silent garbage instead of failing. // Valid padding is at most the final two characters, and if present must run to the end. { int64_t firstPad; int64_t padIndex; firstPad = -1; for (padIndex = 0; padIndex < length; padIndex++) { if (in[padIndex] == '=') { firstPad = padIndex; break; } } if (firstPad >= 0) { if (firstPad < length - 2) { return calogFail(result, calogErrArgE, "cryptoBase64Decode: invalid base64 data"); } for (padIndex = firstPad; padIndex < length; padIndex++) { if (in[padIndex] != '=') { return calogFail(result, calogErrArgE, "cryptoBase64Decode: invalid base64 data"); } } } } outCap = ((size_t)length / 4) * 3; out = (unsigned char *)malloc(outCap); if (out == NULL) { return calogFail(result, calogErrOomE, "cryptoBase64Decode: out of memory"); } // EVP_DecodeBlock always emits a multiple of three bytes and does NOT drop the bytes that // correspond to '=' padding, so trim one output byte per trailing '=' ourselves. decoded = EVP_DecodeBlock(out, in, (int)length); if (decoded < 0) { free(out); return calogFail(result, calogErrArgE, "cryptoBase64Decode: invalid base64 data"); } pad = 0; if (in[length - 1] == '=') { pad++; if (in[length - 2] == '=') { pad++; } } status = calogValueString(result, (const char *)out, (int64_t)(decoded - pad)); free(out); if (status != calogOkE) { return calogFail(result, status, "cryptoBase64Decode: out of memory"); } return calogOkE; } static int32_t cryptoBase64EncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { unsigned char *out; size_t inLen; size_t outCap; int32_t written; int32_t status; (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "cryptoBase64Encode expects (data)"); } // EVP_EncodeBlock returns its output length as an int, accumulated in units of 4 bytes per // 3 input bytes; inputs beyond (INT_MAX / 4) * 3 bytes would overflow that return value. if (args[0].as.s.length > (INT_MAX / 4) * 3) { return calogFail(result, calogErrRangeE, "cryptoBase64Encode: input too large"); } inLen = (size_t)args[0].as.s.length; if (inLen == 0) { return calogValueString(result, "", 0); } outCap = ((inLen + 2) / 3) * 4 + 1; // 4 base64 chars per 3 input bytes, plus the NUL EVP writes out = (unsigned char *)malloc(outCap); if (out == NULL) { return calogFail(result, calogErrOomE, "cryptoBase64Encode: out of memory"); } written = EVP_EncodeBlock(out, (const unsigned char *)args[0].as.s.bytes, (int)inLen); status = calogValueString(result, (const char *)out, (int64_t)written); free(out); if (status != calogOkE) { return calogFail(result, status, "cryptoBase64Encode: out of memory"); } return calogOkE; } static int32_t cryptoBytesToHex(CalogValueT *result, const unsigned char *bytes, size_t length) { char *hex; size_t index; int32_t status; if (length == 0) { return calogValueString(result, "", 0); } hex = (char *)malloc(length * 2); if (hex == NULL) { return calogErrOomE; } for (index = 0; index < length; index++) { unsigned char byte; byte = bytes[index]; hex[index * 2] = cryptoHexDigits[byte >> 4]; hex[index * 2 + 1] = cryptoHexDigits[byte & 0x0F]; } status = calogValueString(result, hex, (int64_t)(length * 2)); free(hex); return status; } static int32_t cryptoDigestHex(CalogValueT *args, int32_t argCount, CalogValueT *result, const EVP_MD *md, const char *usage) { unsigned char digest[EVP_MAX_MD_SIZE]; unsigned int digestLen; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, usage); } digestLen = 0; if (EVP_Digest(args[0].as.s.bytes, (size_t)args[0].as.s.length, digest, &digestLen, md, NULL) != 1) { return calogFail(result, calogErrUnsupportedE, "digest computation failed"); } return cryptoBytesToHex(result, digest, (size_t)digestLen); } static int32_t cryptoEqualsNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogStringE) { return calogFail(result, calogErrArgE, "cryptoEquals expects (a, b)"); } // Lengths are compared first and in the clear: they are not a secret, and CRYPTO_memcmp has // nothing to say about two different-sized buffers. if (args[0].as.s.length != args[1].as.s.length) { calogValueBool(result, false); return calogOkE; } calogValueBool(result, CRYPTO_memcmp(args[0].as.s.bytes, args[1].as.s.bytes, (size_t)args[0].as.s.length) == 0); return calogOkE; } static int32_t cryptoHashSha1Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; return cryptoDigestHex(args, argCount, result, EVP_sha1(), "cryptoHashSha1 expects (data)"); } static int32_t cryptoHashSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; return cryptoDigestHex(args, argCount, result, EVP_sha256(), "cryptoHashSha256 expects (data)"); } static int32_t cryptoHexDecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { const unsigned char *in; unsigned char *out; int64_t length; int64_t outLen; int64_t index; int32_t status; (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "cryptoHexDecode expects (hexText)"); } length = args[0].as.s.length; if ((length % 2) != 0) { return calogFail(result, calogErrArgE, "cryptoHexDecode: odd-length hex string"); } if (length == 0) { return calogValueString(result, "", 0); } in = (const unsigned char *)args[0].as.s.bytes; outLen = length / 2; out = (unsigned char *)malloc((size_t)outLen); if (out == NULL) { return calogFail(result, calogErrOomE, "cryptoHexDecode: out of memory"); } for (index = 0; index < outLen; index++) { int32_t hi; int32_t lo; hi = calogHexNibble(in[index * 2]); lo = calogHexNibble(in[index * 2 + 1]); if (hi < 0 || lo < 0) { free(out); return calogFail(result, calogErrArgE, "cryptoHexDecode: invalid hex digit"); } out[index] = (unsigned char)((hi << 4) | lo); } status = calogValueString(result, (const char *)out, outLen); free(out); if (status != calogOkE) { return calogFail(result, status, "cryptoHexDecode: out of memory"); } return calogOkE; } static int32_t cryptoHexEncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "cryptoHexEncode expects (data)"); } return cryptoBytesToHex(result, (const unsigned char *)args[0].as.s.bytes, (size_t)args[0].as.s.length); } static int32_t cryptoHmacSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { unsigned char digest[EVP_MAX_MD_SIZE]; unsigned int digestLen; (void)userData; calogValueNil(result); if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogStringE) { return calogFail(result, calogErrArgE, "cryptoHmacSha256 expects (key, data)"); } if (args[0].as.s.length > INT_MAX) { return calogFail(result, calogErrRangeE, "cryptoHmacSha256: key too large"); } digestLen = 0; if (HMAC(EVP_sha256(), args[0].as.s.bytes, (int)args[0].as.s.length, (const unsigned char *)args[1].as.s.bytes, (size_t)args[1].as.s.length, digest, &digestLen) == NULL) { return calogFail(result, calogErrUnsupportedE, "cryptoHmacSha256 computation failed"); } return cryptoBytesToHex(result, digest, (size_t)digestLen); } static int32_t cryptoPbkdf2Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { unsigned char *derived; int64_t iterations; int64_t length; int32_t status; (void)userData; calogValueNil(result); if (argCount != 4 || args[0].type != calogStringE || args[1].type != calogStringE || args[2].type != calogIntE || args[3].type != calogIntE) { return calogFail(result, calogErrArgE, "cryptoPbkdf2 expects (password, salt, iterations, length)"); } iterations = args[2].as.i; length = args[3].as.i; if (iterations < CRYPTO_PBKDF2_ITERATIONS_MIN || iterations > CRYPTO_PBKDF2_ITERATIONS_MAX) { return calogFail(result, calogErrRangeE, "cryptoPbkdf2: iterations must be " CRYPTO_TEXT(CRYPTO_PBKDF2_ITERATIONS_MIN) " to " CRYPTO_TEXT(CRYPTO_PBKDF2_ITERATIONS_MAX)); } if (length < CRYPTO_PBKDF2_LENGTH_MIN || length > CRYPTO_PBKDF2_LENGTH_MAX) { return calogFail(result, calogErrRangeE, "cryptoPbkdf2: length must be " CRYPTO_TEXT(CRYPTO_PBKDF2_LENGTH_MIN) " to " CRYPTO_TEXT(CRYPTO_PBKDF2_LENGTH_MAX) " bytes"); } if (args[0].as.s.length > INT_MAX || args[1].as.s.length > INT_MAX) { return calogFail(result, calogErrRangeE, "cryptoPbkdf2: password or salt too large"); } derived = (unsigned char *)malloc((size_t)length); if (derived == NULL) { return calogErrOomE; } if (PKCS5_PBKDF2_HMAC(args[0].as.s.bytes, (int)args[0].as.s.length, (const unsigned char *)args[1].as.s.bytes, (int)args[1].as.s.length, (int)iterations, EVP_sha256(), (int)length, derived) != 1) { free(derived); return calogFail(result, calogErrUnsupportedE, "cryptoPbkdf2 derivation failed"); } status = cryptoBytesToHex(result, derived, (size_t)length); // The derived key is the thing worth stealing out of a freed heap block, so it does not merely // go out of scope. OPENSSL_cleanse is not elided the way a plain memset can be. OPENSSL_cleanse(derived, (size_t)length); free(derived); return status; } static int32_t cryptoRandomBytesNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { unsigned char *buf; int64_t count; int32_t status; (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogIntE) { return calogFail(result, calogErrArgE, "cryptoRandomBytes expects (count)"); } count = args[0].as.i; if (count < 0 || count > INT_MAX) { return calogFail(result, calogErrRangeE, "cryptoRandomBytes: count out of range"); } if (count == 0) { return calogValueString(result, "", 0); } buf = (unsigned char *)malloc((size_t)count); if (buf == NULL) { return calogFail(result, calogErrOomE, "cryptoRandomBytes: out of memory"); } if (RAND_bytes(buf, (int)count) != 1) { free(buf); return calogFail(result, calogErrUnsupportedE, "cryptoRandomBytes: RAND_bytes failed"); } status = calogValueString(result, (const char *)buf, count); free(buf); if (status != calogOkE) { return calogFail(result, status, "cryptoRandomBytes: out of memory"); } return calogOkE; } static int32_t cryptoUuidNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { unsigned char bytes[16]; char text[37]; int32_t bi; int32_t ti; (void)args; (void)userData; calogValueNil(result); if (argCount != 0) { return calogFail(result, calogErrArgE, "cryptoUuid expects no arguments"); } if (RAND_bytes(bytes, (int)sizeof(bytes)) != 1) { return calogFail(result, calogErrUnsupportedE, "cryptoUuid: RAND_bytes failed"); } bytes[6] = (unsigned char)((bytes[6] & 0x0F) | 0x40); // version 4 bytes[8] = (unsigned char)((bytes[8] & 0x3F) | 0x80); // RFC 4122 variant ti = 0; for (bi = 0; bi < 16; bi++) { if (bi == 4 || bi == 6 || bi == 8 || bi == 10) { text[ti++] = '-'; } text[ti++] = cryptoHexDigits[bytes[bi] >> 4]; text[ti++] = cryptoHexDigits[bytes[bi] & 0x0F]; } text[ti] = '\0'; return calogValueString(result, text, 36); }