Refactor OpenSSL usage into a platform-specific file to allow other crypto backends

This commit is contained in:
Cameron Gutman
2021-04-17 17:47:53 -05:00
parent 5782246b30
commit d62ee951a0
6 changed files with 279 additions and 210 deletions

View File

@@ -6,8 +6,6 @@
#include <enet/enet.h>
#include <openssl/evp.h>
// NV control stream packet header for TCP
typedef struct _NVCTL_TCP_PACKET_HEADER {
unsigned short type;
@@ -65,11 +63,8 @@ static int currentEnetSequenceNumber;
static bool idrFrameRequired;
static LINKED_BLOCKING_QUEUE invalidReferenceFrameTuples;
static EVP_CIPHER_CTX* cipherContext;
#if OPENSSL_VERSION_NUMBER < 0x10100000L
#define EVP_CIPHER_CTX_reset(x) EVP_CIPHER_CTX_cleanup(x); EVP_CIPHER_CTX_init(x)
#endif
static PPLT_CRYPTO_CONTEXT encryptionCtx;
static PPLT_CRYPTO_CONTEXT decryptionCtx;
#define CONN_IMMEDIATE_POOR_LOSS_RATE 30
#define CONN_CONSECUTIVE_POOR_LOSS_RATE 15
@@ -251,7 +246,8 @@ int initializeControlStream(void) {
lastConnectionStatusUpdate = CONN_STATUS_OKAY;
currentEnetSequenceNumber = 0;
usePeriodicPing = APP_VERSION_AT_LEAST(7, 1, 415);
cipherContext = EVP_CIPHER_CTX_new();
encryptionCtx = PltCreateCryptoContext();
decryptionCtx = PltCreateCryptoContext();
return 0;
}
@@ -269,7 +265,8 @@ void freeFrameInvalidationList(PLINKED_BLOCKING_QUEUE_ENTRY entry) {
// Cleans up control stream
void destroyControlStream(void) {
LC_ASSERT(stopping);
EVP_CIPHER_CTX_free(cipherContext);
PltDestroyCryptoContext(encryptionCtx);
PltDestroyCryptoContext(decryptionCtx);
PltCloseEvent(&invalidateRefFramesEvent);
freeFrameInvalidationList(LbqDestroyLinkedBlockingQueue(&invalidReferenceFrameTuples));
PltDeleteMutex(&enetMutex);
@@ -390,57 +387,23 @@ static PNVCTL_TCP_PACKET_HEADER readNvctlPacketTcp(void) {
}
static bool encryptControlMessage(PNVCTL_ENCRYPTED_PACKET_HEADER encPacket, PNVCTL_ENET_PACKET_HEADER_V2 packet) {
bool ret = false;
int len;
unsigned char iv[16];
int encryptedSize = sizeof(*packet) + packet->payloadLength;
// This is a truncating cast, but it's what Nvidia does, so we have to mimic it.
memset(iv, 0, sizeof(iv));
iv[0] = (unsigned char)encPacket->seq;
if (EVP_EncryptInit_ex(cipherContext, EVP_aes_128_gcm(), NULL, NULL, NULL) != 1) {
goto gcm_cleanup;
}
if (EVP_CIPHER_CTX_ctrl(cipherContext, EVP_CTRL_GCM_SET_IVLEN, 16, NULL) != 1) {
goto gcm_cleanup;
}
if (EVP_EncryptInit_ex(cipherContext, NULL, NULL,
(const unsigned char*)StreamConfig.remoteInputAesKey, iv) != 1) {
goto gcm_cleanup;
}
// Encrypt into the space after the encrypted header and GCM tag
int encryptedSize = sizeof(*packet) + packet->payloadLength;
if (EVP_EncryptUpdate(cipherContext, ((unsigned char*)(encPacket + 1)) + AES_GCM_TAG_LENGTH,
&encryptedSize, (const unsigned char*)packet, encryptedSize) != 1) {
goto gcm_cleanup;
}
// GCM encryption won't ever fill ciphertext here but we have to call it anyway
if (EVP_EncryptFinal_ex(cipherContext, ((unsigned char*)(encPacket + 1)), &len) != 1) {
goto gcm_cleanup;
}
LC_ASSERT(len == 0);
// Read the tag into the space after the encrypted header
if (EVP_CIPHER_CTX_ctrl(cipherContext, EVP_CTRL_GCM_GET_TAG, 16, (unsigned char*)(encPacket + 1)) != 1) {
ret = -1;
goto gcm_cleanup;
}
ret = true;
gcm_cleanup:
EVP_CIPHER_CTX_reset(cipherContext);
return ret;
return PltEncryptMessage(encryptionCtx, ALGORITHM_AES_GCM,
(unsigned char*)StreamConfig.remoteInputAesKey, sizeof(StreamConfig.remoteInputAesKey),
iv, sizeof(iv),
(unsigned char*)(encPacket + 1), AES_GCM_TAG_LENGTH, // Write tag into the space after the encrypted header
(unsigned char*)packet, encryptedSize,
((unsigned char*)(encPacket + 1)) + AES_GCM_TAG_LENGTH, &encryptedSize); // Write ciphertext after the GCM tag
}
// Caller must free() *packet on success!!!
static bool decryptControlMessageToV1(PNVCTL_ENCRYPTED_PACKET_HEADER encPacket, PNVCTL_ENET_PACKET_HEADER_V1* packet, int* packetLength) {
bool ret = false;
int len;
unsigned char iv[16];
*packet = NULL;
@@ -458,60 +421,28 @@ static bool decryptControlMessageToV1(PNVCTL_ENCRYPTED_PACKET_HEADER encPacket,
memset(iv, 0, sizeof(iv));
iv[0] = (unsigned char)encPacket->seq;
if (EVP_DecryptInit_ex(cipherContext, EVP_aes_128_gcm(), NULL, NULL, NULL) != 1) {
goto gcm_cleanup;
}
if (EVP_CIPHER_CTX_ctrl(cipherContext, EVP_CTRL_GCM_SET_IVLEN, 16, NULL) != 1) {
goto gcm_cleanup;
}
if (EVP_DecryptInit_ex(cipherContext, NULL, NULL,
(const unsigned char*)StreamConfig.remoteInputAesKey, iv) != 1) {
goto gcm_cleanup;
}
int plaintextLength = encPacket->length - sizeof(encPacket->seq) - AES_GCM_TAG_LENGTH;
*packet = malloc(plaintextLength);
if (*packet == NULL) {
goto gcm_cleanup;
return false;
}
// Decrypt into the packet we allocated
if (EVP_DecryptUpdate(cipherContext, (unsigned char*)*packet, &plaintextLength,
((unsigned char*)(encPacket + 1)) + AES_GCM_TAG_LENGTH, plaintextLength) != 1) {
goto gcm_cleanup;
if (!PltDecryptMessage(decryptionCtx, ALGORITHM_AES_GCM,
(unsigned char*)StreamConfig.remoteInputAesKey, sizeof(StreamConfig.remoteInputAesKey),
iv, sizeof(iv),
(unsigned char*)(encPacket + 1), AES_GCM_TAG_LENGTH, // The tag is located right after the header
((unsigned char*)(encPacket + 1)) + AES_GCM_TAG_LENGTH, plaintextLength, // The ciphertext is after the tag
(unsigned char*)*packet, &plaintextLength)) {
free(*packet);
return false;
}
// Set the GCM tag before calling EVP_DecryptFinal_ex()
if (EVP_CIPHER_CTX_ctrl(cipherContext, EVP_CTRL_GCM_SET_TAG, 16, (unsigned char*)(encPacket + 1)) != 1) {
ret = -1;
goto gcm_cleanup;
}
// GCM will never have additional plaintext here, but we need to call it to
// ensure that the GCM authentication tag is correct for this data.
if (EVP_DecryptFinal_ex(cipherContext, (unsigned char*)*packet, &len) != 1) {
goto gcm_cleanup;
}
LC_ASSERT(len == 0);
// Now we do an in-place V2 to V1 header conversion, so our existing parsing code doesn't have to change.
// All we need to do is eliminate the new length field in V2 by shifting everything by 2 bytes.
memmove(((unsigned char*)*packet) + 2, ((unsigned char*)*packet) + 4, plaintextLength - 4);
*packetLength = plaintextLength - 2;
ret = true;
gcm_cleanup:
EVP_CIPHER_CTX_reset(cipherContext);
if (!ret && *packet) {
free(*packet);
*packet = NULL;
}
return ret;
return true;
}
static bool sendMessageEnet(short ptype, short paylen, const void* payload) {
@@ -760,16 +691,12 @@ static void controlReceiveThreadFunc(void* context) {
continue;
}
// We (ab)use this lock to protect the cryptoContext too
PltLockMutex(&enetMutex);
ctlHdr = NULL;
if (!decryptControlMessageToV1((PNVCTL_ENCRYPTED_PACKET_HEADER)event.packet->data, &ctlHdr, &packetLength)) {
PltUnlockMutex(&enetMutex);
Limelog("Failed to decrypt control packet of size %d\n", event.packet->dataLength);
enet_packet_destroy(event.packet);
continue;
}
PltUnlockMutex(&enetMutex);
}
else {
// What do we do here???

View File

@@ -4,13 +4,10 @@
#include "LinkedBlockingQueue.h"
#include "Input.h"
#include <openssl/evp.h>
static SOCKET inputSock = INVALID_SOCKET;
static unsigned char currentAesIv[16];
static bool initialized;
static EVP_CIPHER_CTX* cipherContext;
static bool cipherInitialized;
static PPLT_CRYPTO_CONTEXT cryptoContext;
static LINKED_BLOCKING_QUEUE packetQueue;
static PLT_THREAD inputSendThread;
@@ -18,8 +15,6 @@ static PLT_THREAD inputSendThread;
#define MAX_INPUT_PACKET_SIZE 128
#define INPUT_STREAM_TIMEOUT_SEC 10
#define ROUND_TO_PKCS7_PADDED_LEN(x) ((((x) + 15) / 16) * 16)
// Contains input stream packets
typedef struct _PACKET_HOLDER {
int packetLength;
@@ -36,19 +31,13 @@ typedef struct _PACKET_HOLDER {
LINKED_BLOCKING_QUEUE_ENTRY entry;
} PACKET_HOLDER, *PPACKET_HOLDER;
#if OPENSSL_VERSION_NUMBER < 0x10100000L
#define EVP_CIPHER_CTX_reset(x) EVP_CIPHER_CTX_cleanup(x); EVP_CIPHER_CTX_init(x)
#endif
// Initializes the input stream
int initializeInputStream(void) {
memcpy(currentAesIv, StreamConfig.remoteInputAesIv, sizeof(currentAesIv));
// Initialized on first packet
cipherInitialized = false;
LbqInitializeLinkedBlockingQueue(&packetQueue, 30);
cryptoContext = PltCreateCryptoContext();
return 0;
}
@@ -56,10 +45,7 @@ int initializeInputStream(void) {
void destroyInputStream(void) {
PLINKED_BLOCKING_QUEUE_ENTRY entry, nextEntry;
if (cipherInitialized) {
EVP_CIPHER_CTX_free(cipherContext);
cipherInitialized = false;
}
PltDestroyCryptoContext(cryptoContext);
entry = LbqDestroyLinkedBlockingQueue(&packetQueue);
@@ -73,113 +59,38 @@ void destroyInputStream(void) {
}
}
static int addPkcs7PaddingInPlace(unsigned char* plaintext, int plaintextLen) {
int i;
int paddedLength = ROUND_TO_PKCS7_PADDED_LEN(plaintextLen);
unsigned char paddingByte = (unsigned char)(16 - (plaintextLen % 16));
for (i = plaintextLen; i < paddedLength; i++) {
plaintext[i] = paddingByte;
}
return paddedLength;
}
static int encryptData(const unsigned char* plaintext, int plaintextLen,
static int encryptData(unsigned char* plaintext, int plaintextLen,
unsigned char* ciphertext, int* ciphertextLen) {
int ret;
int len;
// Starting in Gen 7, AES GCM is used for encryption
if (AppVersionQuad[0] >= 7) {
if (!cipherInitialized) {
if ((cipherContext = EVP_CIPHER_CTX_new()) == NULL) {
return -1;
}
cipherInitialized = true;
if (!PltEncryptMessage(cryptoContext, ALGORITHM_AES_GCM,
(unsigned char*)StreamConfig.remoteInputAesKey, sizeof(StreamConfig.remoteInputAesKey),
currentAesIv, sizeof(currentAesIv),
ciphertext, 16,
plaintext, plaintextLen,
&ciphertext[16], ciphertextLen)) {
return -1;
}
// Gen 7 servers use 128-bit AES GCM
if (EVP_EncryptInit_ex(cipherContext, EVP_aes_128_gcm(), NULL, NULL, NULL) != 1) {
ret = -1;
goto gcm_cleanup;
}
// Gen 7 servers uses 16 byte IVs
if (EVP_CIPHER_CTX_ctrl(cipherContext, EVP_CTRL_GCM_SET_IVLEN, 16, NULL) != 1) {
ret = -1;
goto gcm_cleanup;
}
// Initialize again but now provide our key and current IV
if (EVP_EncryptInit_ex(cipherContext, NULL, NULL,
(const unsigned char*)StreamConfig.remoteInputAesKey, currentAesIv) != 1) {
ret = -1;
goto gcm_cleanup;
}
// Encrypt into the caller's buffer, leaving room for the auth tag to be prepended
if (EVP_EncryptUpdate(cipherContext, &ciphertext[16], ciphertextLen, plaintext, plaintextLen) != 1) {
ret = -1;
goto gcm_cleanup;
}
// GCM encryption won't ever fill ciphertext here but we have to call it anyway
if (EVP_EncryptFinal_ex(cipherContext, ciphertext, &len) != 1) {
ret = -1;
goto gcm_cleanup;
}
LC_ASSERT(len == 0);
// Read the tag into the caller's buffer
if (EVP_CIPHER_CTX_ctrl(cipherContext, EVP_CTRL_GCM_GET_TAG, 16, ciphertext) != 1) {
ret = -1;
goto gcm_cleanup;
}
// Increment the ciphertextLen to account for the tag
*ciphertextLen += 16;
ret = 0;
gcm_cleanup:
EVP_CIPHER_CTX_reset(cipherContext);
return 0;
}
else {
// PKCS7 padding may need to be added in-place, so we must copy this into a buffer
// that can safely be modified.
unsigned char paddedData[MAX_INPUT_PACKET_SIZE];
int paddedLength;
if (!cipherInitialized) {
if ((cipherContext = EVP_CIPHER_CTX_new()) == NULL) {
ret = -1;
goto cbc_cleanup;
}
cipherInitialized = true;
// Prior to Gen 7, 128-bit AES CBC is used for encryption
if (EVP_EncryptInit_ex(cipherContext, EVP_aes_128_cbc(), NULL,
(const unsigned char*)StreamConfig.remoteInputAesKey, currentAesIv) != 1) {
ret = -1;
goto cbc_cleanup;
}
}
// Pad the data to the required block length
memcpy(paddedData, plaintext, plaintextLen);
paddedLength = addPkcs7PaddingInPlace(paddedData, plaintextLen);
if (EVP_EncryptUpdate(cipherContext, ciphertext, ciphertextLen, paddedData, paddedLength) != 1) {
ret = -1;
goto cbc_cleanup;
}
ret = 0;
cbc_cleanup:
// Nothing to do
;
// Prior to Gen 7, 128-bit AES CBC is used for encryption
return PltEncryptMessage(cryptoContext, ALGORITHM_AES_CBC,
(unsigned char*)StreamConfig.remoteInputAesKey, sizeof(StreamConfig.remoteInputAesKey),
currentAesIv, sizeof(currentAesIv),
NULL, 0,
paddedData, plaintextLen,
ciphertext, ciphertextLen) ? 0 : -1;
}
return ret;
}
// Input thread proc

View File

@@ -4,6 +4,7 @@
#include "Platform.h"
#include "PlatformSockets.h"
#include "PlatformThreads.h"
#include "PlatformCrypto.h"
#include "Video.h"
#include "RtpFecQueue.h"

197
src/PlatformCrypto.c Normal file
View File

@@ -0,0 +1,197 @@
#include "Limelight-internal.h"
#include <openssl/evp.h>
#include <openssl/rand.h>
static int addPkcs7PaddingInPlace(unsigned char* plaintext, int plaintextLen) {
int paddedLength = ROUND_TO_PKCS7_PADDED_LEN(plaintextLen);
unsigned char paddingByte = (unsigned char)(16 - (plaintextLen % 16));
memset(&plaintext[plaintextLen], paddingByte, paddedLength - plaintextLen);
return paddedLength;
}
// For CBC modes, inputData buffer must be allocated with length rounded up to next multiple of 16 and inputData buffer may be modified!
bool PltEncryptMessage(PPLT_CRYPTO_CONTEXT ctx, int algorithm,
unsigned char* key, int keyLength,
unsigned char* iv, int ivLength,
unsigned char* tag, int tagLength,
unsigned char* inputData, int inputDataLength,
unsigned char* outputData, int* outputDataLength) {
bool ret = false;
const EVP_CIPHER* cipher;
switch (algorithm) {
case ALGORITHM_AES_CBC:
LC_ASSERT(keyLength == 16);
LC_ASSERT(tag == NULL);
LC_ASSERT(tagLength == 0);
cipher = EVP_aes_128_cbc();
break;
case ALGORITHM_AES_GCM:
LC_ASSERT(keyLength == 16);
LC_ASSERT(tag != NULL);
LC_ASSERT(tagLength > 0);
cipher = EVP_aes_128_gcm();
break;
default:
LC_ASSERT(false);
return false;
}
if (algorithm == ALGORITHM_AES_GCM) {
if (EVP_EncryptInit_ex(ctx->ctx, cipher, NULL, NULL, NULL) != 1) {
goto cleanup;
}
if (EVP_CIPHER_CTX_ctrl(ctx->ctx, EVP_CTRL_GCM_SET_IVLEN, ivLength, NULL) != 1) {
goto cleanup;
}
if (EVP_EncryptInit_ex(ctx->ctx, NULL, NULL, key, iv) != 1) {
goto cleanup;
}
}
else {
if (!ctx->initialized) {
if (EVP_EncryptInit_ex(ctx->ctx, cipher, NULL, key, iv) != 1) {
goto cleanup;
}
ctx->initialized = true;
}
inputDataLength = addPkcs7PaddingInPlace(inputData, inputDataLength);
}
if (EVP_EncryptUpdate(ctx->ctx, outputData, outputDataLength, inputData, inputDataLength) != 1) {
goto cleanup;
}
if (algorithm == ALGORITHM_AES_GCM) {
int len;
// GCM encryption won't ever fill ciphertext here but we have to call it anyway
if (EVP_EncryptFinal_ex(ctx->ctx, outputData, &len) != 1) {
goto cleanup;
}
LC_ASSERT(len == 0);
if (EVP_CIPHER_CTX_ctrl(ctx->ctx, EVP_CTRL_GCM_GET_TAG, tagLength, tag) != 1) {
goto cleanup;
}
}
ret = true;
cleanup:
if (algorithm == ALGORITHM_AES_GCM) {
EVP_CIPHER_CTX_reset(ctx->ctx);
}
return ret;
}
bool PltDecryptMessage(PPLT_CRYPTO_CONTEXT ctx, int algorithm,
unsigned char* key, int keyLength,
unsigned char* iv, int ivLength,
unsigned char* tag, int tagLength,
unsigned char* inputData, int inputDataLength,
unsigned char* outputData, int* outputDataLength) {
bool ret = false;
const EVP_CIPHER* cipher;
switch (algorithm) {
case ALGORITHM_AES_CBC:
LC_ASSERT(keyLength == 16);
LC_ASSERT(tag == NULL);
LC_ASSERT(tagLength == 0);
cipher = EVP_aes_128_cbc();
break;
case ALGORITHM_AES_GCM:
LC_ASSERT(keyLength == 16);
LC_ASSERT(tag != NULL);
LC_ASSERT(tagLength > 0);
cipher = EVP_aes_128_gcm();
break;
default:
LC_ASSERT(false);
return false;
}
if (algorithm == ALGORITHM_AES_GCM) {
if (EVP_DecryptInit_ex(ctx->ctx, cipher, NULL, NULL, NULL) != 1) {
goto cleanup;
}
if (EVP_CIPHER_CTX_ctrl(ctx->ctx, EVP_CTRL_GCM_SET_IVLEN, ivLength, NULL) != 1) {
goto cleanup;
}
if (EVP_DecryptInit_ex(ctx->ctx, NULL, NULL, key, iv) != 1) {
goto cleanup;
}
}
else {
if (!ctx->initialized) {
if (EVP_DecryptInit_ex(ctx->ctx, cipher, NULL, key, iv) != 1) {
goto cleanup;
}
ctx->initialized = true;
}
}
if (EVP_DecryptUpdate(ctx->ctx, outputData, outputDataLength, inputData, inputDataLength) != 1) {
goto cleanup;
}
if (algorithm == ALGORITHM_AES_GCM) {
int len;
// Set the GCM tag before calling EVP_DecryptFinal_ex()
if (EVP_CIPHER_CTX_ctrl(ctx->ctx, EVP_CTRL_GCM_SET_TAG, tagLength, tag) != 1) {
goto cleanup;
}
// GCM will never have additional plaintext here, but we need to call it to
// ensure that the GCM authentication tag is correct for this data.
if (EVP_DecryptFinal_ex(ctx->ctx, outputData, &len) != 1) {
goto cleanup;
}
LC_ASSERT(len == 0);
}
ret = true;
cleanup:
if (algorithm == ALGORITHM_AES_GCM) {
EVP_CIPHER_CTX_reset(ctx->ctx);
}
return ret;
}
PPLT_CRYPTO_CONTEXT PltCreateCryptoContext(void) {
PPLT_CRYPTO_CONTEXT ctx = malloc(sizeof(*ctx));
if (!ctx) {
return NULL;
}
ctx->initialized = false;
ctx->ctx = EVP_CIPHER_CTX_new();
if (!ctx->ctx) {
free(ctx);
return NULL;
}
return ctx;
}
void PltDestroyCryptoContext(PPLT_CRYPTO_CONTEXT ctx) {
EVP_CIPHER_CTX_free(ctx->ctx);
free(ctx);
}
void PltGenerateRandomData(unsigned char* data, int length) {
RAND_bytes(data, length);
}

35
src/PlatformCrypto.h Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include <stdbool.h>
// Hide the real OpenSSL definition from other code
typedef struct evp_cipher_ctx_st EVP_CIPHER_CTX;
typedef struct _PLT_CRYPTO_CONTEXT {
bool initialized; // Used for CBC only
EVP_CIPHER_CTX* ctx;
} PLT_CRYPTO_CONTEXT, *PPLT_CRYPTO_CONTEXT;
#define ROUND_TO_PKCS7_PADDED_LEN(x) ((((x) + 15) / 16) * 16)
PPLT_CRYPTO_CONTEXT PltCreateCryptoContext();
void PltDestroyCryptoContext(PPLT_CRYPTO_CONTEXT ctx);
#define ALGORITHM_AES_CBC 1
#define ALGORITHM_AES_GCM 2
bool PltEncryptMessage(PPLT_CRYPTO_CONTEXT ctx, int algorithm,
unsigned char* key, int keyLength,
unsigned char* iv, int ivLength,
unsigned char* tag, int tagLength,
unsigned char* inputData, int inputDataLength,
unsigned char* outputData, int* outputDataLength);
bool PltDecryptMessage(PPLT_CRYPTO_CONTEXT ctx, int algorithm,
unsigned char* key, int keyLength,
unsigned char* iv, int ivLength,
unsigned char* tag, int tagLength,
unsigned char* inputData, int inputDataLength,
unsigned char* outputData, int* outputDataLength);
void PltGenerateRandomData(unsigned char* data, int length);

View File

@@ -1,7 +1,5 @@
#include "Limelight-internal.h"
#include <openssl/rand.h>
#define STUN_RECV_TIMEOUT_SEC 3
#define STUN_MESSAGE_BINDING_REQUEST 0x0001
@@ -85,7 +83,7 @@ int LiFindExternalAddressIP4(const char* stunServer, unsigned short stunPort, un
reqMsg.messageType = htons(STUN_MESSAGE_BINDING_REQUEST);
reqMsg.messageLength = 0;
reqMsg.magicCookie = htonl(STUN_MESSAGE_COOKIE);
RAND_bytes(reqMsg.transactionId, sizeof(reqMsg.transactionId));
PltGenerateRandomData(reqMsg.transactionId, sizeof(reqMsg.transactionId));
bytesRead = SOCKET_ERROR;
for (i = 0; i < STUN_RECV_TIMEOUT_SEC * 1000 / UDP_RECV_POLL_TIMEOUT_MS && bytesRead <= 0; i++) {