346 lines
13 KiB
C
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);
|
|
}
|