439 lines
18 KiB
C
439 lines
18 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 "calogInternal.h"
|
|
|
|
#include <limits.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
|
|
#include <openssl/crypto.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 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);
|
|
}
|