// 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 #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 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 cryptoHexNibble(unsigned char c); static int32_t cryptoHmacSha256Native(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); int32_t calogCryptoRegister(CalogT *calog) { calogRegisterInline(calog, "hashSha256", cryptoHashSha256Native, NULL); calogRegisterInline(calog, "hashSha1", cryptoHashSha1Native, NULL); calogRegisterInline(calog, "hmacSha256", cryptoHmacSha256Native, NULL); calogRegisterInline(calog, "randomBytes", cryptoRandomBytesNative, NULL); calogRegisterInline(calog, "base64Encode", cryptoBase64EncodeNative, NULL); calogRegisterInline(calog, "base64Decode", cryptoBase64DecodeNative, NULL); calogRegisterInline(calog, "hexEncode", cryptoHexEncodeNative, NULL); calogRegisterInline(calog, "hexDecode", cryptoHexDecodeNative, NULL); calogRegisterInline(calog, "uuid", cryptoUuidNative, NULL); return calogOkE; } 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, "base64Decode 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, "base64Decode: invalid base64 length"); } outCap = ((size_t)length / 4) * 3; out = (unsigned char *)malloc(outCap); if (out == NULL) { return calogFail(result, calogErrOomE, "base64Decode: 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, "base64Decode: 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, "base64Decode: 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, "base64Encode expects (data)"); } if (args[0].as.s.length > INT_MAX) { return calogFail(result, calogErrRangeE, "base64Encode: 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, "base64Encode: 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, "base64Encode: out of memory"); } return calogOkE; } static int32_t cryptoBytesToHex(CalogValueT *result, const unsigned char *bytes, size_t length) { static const char digits[] = "0123456789abcdef"; 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] = digits[byte >> 4]; hex[index * 2 + 1] = digits[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 cryptoHashSha1Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; return cryptoDigestHex(args, argCount, result, EVP_sha1(), "hashSha1 expects (data)"); } static int32_t cryptoHashSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; return cryptoDigestHex(args, argCount, result, EVP_sha256(), "hashSha256 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, "hexDecode expects (hexText)"); } length = args[0].as.s.length; if ((length % 2) != 0) { return calogFail(result, calogErrArgE, "hexDecode: 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, "hexDecode: out of memory"); } for (index = 0; index < outLen; index++) { int32_t hi; int32_t lo; hi = cryptoHexNibble(in[index * 2]); lo = cryptoHexNibble(in[index * 2 + 1]); if (hi < 0 || lo < 0) { free(out); return calogFail(result, calogErrArgE, "hexDecode: 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, "hexDecode: 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, "hexEncode expects (data)"); } return cryptoBytesToHex(result, (const unsigned char *)args[0].as.s.bytes, (size_t)args[0].as.s.length); } static int32_t cryptoHexNibble(unsigned char c) { if (c >= '0' && c <= '9') { return (int32_t)(c - '0'); } if (c >= 'a' && c <= 'f') { return (int32_t)(c - 'a' + 10); } if (c >= 'A' && c <= 'F') { return (int32_t)(c - 'A' + 10); } return -1; } 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, "hmacSha256 expects (key, data)"); } if (args[0].as.s.length > INT_MAX) { return calogFail(result, calogErrRangeE, "hmacSha256: 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, "hmacSha256 computation failed"); } return cryptoBytesToHex(result, digest, (size_t)digestLen); } 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, "randomBytes expects (count)"); } count = args[0].as.i; if (count < 0 || count > INT_MAX) { return calogFail(result, calogErrRangeE, "randomBytes: 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, "randomBytes: out of memory"); } if (RAND_bytes(buf, (int)count) != 1) { free(buf); return calogFail(result, calogErrUnsupportedE, "randomBytes: RAND_bytes failed"); } status = calogValueString(result, (const char *)buf, count); free(buf); if (status != calogOkE) { return calogFail(result, status, "randomBytes: out of memory"); } return calogOkE; } static int32_t cryptoUuidNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { static const char digits[] = "0123456789abcdef"; 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, "uuid expects no arguments"); } if (RAND_bytes(bytes, (int)sizeof(bytes)) != 1) { return calogFail(result, calogErrUnsupportedE, "uuid: 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++] = digits[bytes[bi] >> 4]; text[ti++] = digits[bytes[bi] & 0x0F]; } text[ti] = '\0'; return calogValueString(result, text, 36); }