calog/libs/calogCrypto.c
2026-07-03 02:13:23 -05:00

346 lines
13 KiB
C

// 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 <limits.h>
#include <stdint.h>
#include <stdlib.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>
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);
}