337 lines
11 KiB
C
337 lines
11 KiB
C
/*
|
|
* NIP-04: Encrypted Direct Message Implementation
|
|
* https://github.com/nostr-protocol/nips/blob/master/04.md
|
|
*/
|
|
|
|
#include "nip004.h"
|
|
#include "utils.h"
|
|
#include "nostr_common.h"
|
|
#include "crypto/nostr_secp256k1.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
// Include our AES implementation
|
|
#include "crypto/nostr_aes.h"
|
|
|
|
// Forward declarations for internal functions
|
|
static int aes_cbc_encrypt(const unsigned char* key, const unsigned char* iv,
|
|
const unsigned char* input, size_t input_len,
|
|
unsigned char* output);
|
|
static int aes_cbc_decrypt(const unsigned char* key, const unsigned char* iv,
|
|
const unsigned char* input, size_t input_len,
|
|
unsigned char* output);
|
|
static size_t pkcs7_pad(unsigned char* data, size_t data_len, size_t block_size);
|
|
static size_t pkcs7_unpad(unsigned char* data, size_t data_len);
|
|
|
|
// Memory clearing utility
|
|
static void memory_clear(const void *p, size_t len) {
|
|
if (p && len) {
|
|
memset((void *)p, 0, len);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// AES-256-CBC ENCRYPTION/DECRYPTION USING TINYAES
|
|
// =============================================================================
|
|
|
|
static int aes_cbc_encrypt(const unsigned char* key, const unsigned char* iv,
|
|
const unsigned char* input, size_t input_len,
|
|
unsigned char* output) {
|
|
if (!key || !iv || !input || !output || input_len % 16 != 0) {
|
|
return -1;
|
|
}
|
|
|
|
// Initialize AES context with key and IV
|
|
struct AES_ctx ctx;
|
|
AES_init_ctx_iv(&ctx, key, iv);
|
|
|
|
// Copy input to output (tinyAES works in-place)
|
|
memcpy(output, input, input_len);
|
|
|
|
// Encrypt using AES-256-CBC
|
|
AES_CBC_encrypt_buffer(&ctx, output, input_len);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int aes_cbc_decrypt(const unsigned char* key, const unsigned char* iv,
|
|
const unsigned char* input, size_t input_len,
|
|
unsigned char* output) {
|
|
if (!key || !iv || !input || !output || input_len % 16 != 0) {
|
|
return -1;
|
|
}
|
|
|
|
// Initialize AES context with key and IV
|
|
struct AES_ctx ctx;
|
|
AES_init_ctx_iv(&ctx, key, iv);
|
|
|
|
// Copy input to output (tinyAES works in-place)
|
|
memcpy(output, input, input_len);
|
|
|
|
// Decrypt using AES-256-CBC
|
|
AES_CBC_decrypt_buffer(&ctx, output, input_len);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// PKCS#7 padding functions
|
|
static size_t pkcs7_pad(unsigned char* data, size_t data_len, size_t block_size) {
|
|
size_t padding = block_size - (data_len % block_size);
|
|
for (size_t i = 0; i < padding; i++) {
|
|
data[data_len + i] = (unsigned char)padding;
|
|
}
|
|
return data_len + padding;
|
|
}
|
|
|
|
static size_t pkcs7_unpad(unsigned char* data, size_t data_len) {
|
|
if (data_len == 0) return 0;
|
|
|
|
unsigned char padding = data[data_len - 1];
|
|
if (padding == 0 || padding > 16) return 0; // Invalid padding
|
|
|
|
// Verify padding
|
|
for (size_t i = data_len - padding; i < data_len; i++) {
|
|
if (data[i] != padding) return 0; // Invalid padding
|
|
}
|
|
|
|
return data_len - padding;
|
|
}
|
|
|
|
// =============================================================================
|
|
// NIP-04 IMPLEMENTATION
|
|
// =============================================================================
|
|
|
|
int nostr_nip04_encrypt(const unsigned char* sender_private_key,
|
|
const unsigned char* recipient_public_key,
|
|
const char* plaintext,
|
|
char* output,
|
|
size_t output_size) {
|
|
if (!sender_private_key || !recipient_public_key || !plaintext || !output) {
|
|
return NOSTR_ERROR_INVALID_INPUT;
|
|
}
|
|
|
|
size_t plaintext_len = strlen(plaintext);
|
|
|
|
if (plaintext_len > NOSTR_NIP04_MAX_PLAINTEXT_SIZE) {
|
|
return NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL;
|
|
}
|
|
|
|
// FIX: Calculate final size requirements EARLY before any allocations
|
|
// CRITICAL: Account for PKCS#7 padding which ALWAYS adds 1-16 bytes
|
|
// If plaintext_len is a multiple of 16, PKCS#7 adds a full 16-byte block
|
|
size_t padded_len = ((plaintext_len / 16) + 1) * 16; // Always add one full block for PKCS#7
|
|
size_t ciphertext_b64_max = ((padded_len + 2) / 3) * 4 + 1;
|
|
size_t iv_b64_max = ((16 + 2) / 3) * 4 + 1; // Always 25 bytes
|
|
size_t estimated_result_len = ciphertext_b64_max + 4 + iv_b64_max; // +4 for "?iv="
|
|
|
|
// FIX: Check output buffer size BEFORE doing any work
|
|
if (estimated_result_len > output_size) {
|
|
return NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL;
|
|
}
|
|
|
|
// Step 1: Compute ECDH shared secret
|
|
unsigned char shared_secret[32];
|
|
if (ecdh_shared_secret(sender_private_key, recipient_public_key, shared_secret) != 0) {
|
|
return NOSTR_ERROR_CRYPTO_FAILED;
|
|
}
|
|
|
|
// Step 2: Generate random IV (16 bytes)
|
|
unsigned char iv[16];
|
|
if (nostr_secp256k1_get_random_bytes(iv, 16) != 1) {
|
|
return NOSTR_ERROR_CRYPTO_FAILED;
|
|
}
|
|
|
|
// Step 3: Pad plaintext using PKCS#7
|
|
unsigned char* padded_data = malloc(padded_len);
|
|
if (!padded_data) {
|
|
return NOSTR_ERROR_MEMORY_FAILED;
|
|
}
|
|
|
|
memcpy(padded_data, plaintext, plaintext_len);
|
|
size_t actual_padded_len = pkcs7_pad(padded_data, plaintext_len, 16);
|
|
|
|
// Step 4: Encrypt using AES-256-CBC
|
|
unsigned char* ciphertext = malloc(padded_len);
|
|
if (!ciphertext) {
|
|
free(padded_data);
|
|
return NOSTR_ERROR_MEMORY_FAILED;
|
|
}
|
|
|
|
if (aes_cbc_encrypt(shared_secret, iv, padded_data, actual_padded_len, ciphertext) != 0) {
|
|
free(padded_data);
|
|
free(ciphertext);
|
|
return NOSTR_ERROR_CRYPTO_FAILED;
|
|
}
|
|
|
|
// Step 5: Base64 encode ciphertext and IV
|
|
size_t ciphertext_b64_len = ((actual_padded_len + 2) / 3) * 4 + 1;
|
|
size_t iv_b64_len = ((16 + 2) / 3) * 4 + 1;
|
|
|
|
char* ciphertext_b64 = malloc(ciphertext_b64_len);
|
|
char* iv_b64 = malloc(iv_b64_len);
|
|
|
|
if (!ciphertext_b64 || !iv_b64) {
|
|
free(padded_data);
|
|
free(ciphertext);
|
|
free(ciphertext_b64);
|
|
free(iv_b64);
|
|
return NOSTR_ERROR_MEMORY_FAILED;
|
|
}
|
|
|
|
// FIX: Pass buffer sizes to base64_encode and check for success
|
|
size_t ct_b64_len = base64_encode(ciphertext, actual_padded_len, ciphertext_b64, ciphertext_b64_len);
|
|
size_t iv_b64_len_actual = base64_encode(iv, 16, iv_b64, iv_b64_len);
|
|
|
|
// FIX: Check if encoding succeeded
|
|
if (ct_b64_len == 0 || iv_b64_len_actual == 0) {
|
|
free(padded_data);
|
|
free(ciphertext);
|
|
free(ciphertext_b64);
|
|
free(iv_b64);
|
|
memory_clear(shared_secret, 32);
|
|
return NOSTR_ERROR_CRYPTO_FAILED;
|
|
}
|
|
|
|
// Step 6: Format as "ciphertext?iv=iv_base64" - size check moved earlier, now guaranteed to fit
|
|
size_t result_len = ct_b64_len + 4 + iv_b64_len_actual + 1; // +4 for "?iv=", +1 for null
|
|
|
|
if (result_len > output_size) {
|
|
free(padded_data);
|
|
free(ciphertext);
|
|
free(ciphertext_b64);
|
|
free(iv_b64);
|
|
memory_clear(shared_secret, 32);
|
|
return NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL;
|
|
}
|
|
|
|
snprintf(output, output_size, "%s?iv=%s", ciphertext_b64, iv_b64);
|
|
|
|
// Cleanup
|
|
memory_clear(shared_secret, 32);
|
|
memory_clear(padded_data, padded_len);
|
|
memory_clear(ciphertext, padded_len);
|
|
free(padded_data);
|
|
free(ciphertext);
|
|
free(ciphertext_b64);
|
|
free(iv_b64);
|
|
|
|
return NOSTR_SUCCESS;
|
|
}
|
|
|
|
int nostr_nip04_decrypt(const unsigned char* recipient_private_key,
|
|
const unsigned char* sender_public_key,
|
|
const char* encrypted_data,
|
|
char* output,
|
|
size_t output_size) {
|
|
if (!recipient_private_key || !sender_public_key || !encrypted_data || !output) {
|
|
return NOSTR_ERROR_INVALID_INPUT;
|
|
}
|
|
|
|
// Step 1: Parse encrypted data format "ciphertext?iv=iv_base64"
|
|
char* separator = strstr(encrypted_data, "?iv=");
|
|
if (!separator) {
|
|
return NOSTR_ERROR_NIP04_INVALID_FORMAT;
|
|
}
|
|
|
|
size_t ciphertext_b64_len = separator - encrypted_data;
|
|
const char* iv_b64 = separator + 4; // Skip "?iv="
|
|
|
|
if (ciphertext_b64_len == 0 || strlen(iv_b64) == 0) {
|
|
return NOSTR_ERROR_NIP04_INVALID_FORMAT;
|
|
}
|
|
|
|
// Step 2: Create null-terminated copy of ciphertext base64
|
|
char* ciphertext_b64 = malloc(ciphertext_b64_len + 1);
|
|
if (!ciphertext_b64) {
|
|
return NOSTR_ERROR_MEMORY_FAILED;
|
|
}
|
|
|
|
memcpy(ciphertext_b64, encrypted_data, ciphertext_b64_len);
|
|
ciphertext_b64[ciphertext_b64_len] = '\0';
|
|
|
|
// Step 3: Calculate proper buffer sizes for decoded data
|
|
// Base64 decoding: 4 chars -> 3 bytes, so max decoded size is (len * 3) / 4
|
|
size_t max_ciphertext_len = ((ciphertext_b64_len + 3) / 4) * 3;
|
|
size_t max_iv_len = ((strlen(iv_b64) + 3) / 4) * 3;
|
|
|
|
// Allocate buffers with proper sizes
|
|
unsigned char* ciphertext = malloc(max_ciphertext_len);
|
|
unsigned char* iv_buffer = malloc(max_iv_len);
|
|
|
|
if (!ciphertext || !iv_buffer) {
|
|
free(ciphertext_b64);
|
|
free(ciphertext);
|
|
free(iv_buffer);
|
|
return NOSTR_ERROR_MEMORY_FAILED;
|
|
}
|
|
|
|
// Step 4: Base64 decode ciphertext and IV
|
|
size_t ciphertext_len = base64_decode(ciphertext_b64, ciphertext);
|
|
size_t iv_len = base64_decode(iv_b64, iv_buffer);
|
|
|
|
if (ciphertext_len == 0 || iv_len != 16 || ciphertext_len % 16 != 0) {
|
|
free(ciphertext_b64);
|
|
free(ciphertext);
|
|
free(iv_buffer);
|
|
return NOSTR_ERROR_NIP04_INVALID_FORMAT;
|
|
}
|
|
|
|
// Copy IV to fixed-size buffer for safety
|
|
unsigned char iv[16];
|
|
memcpy(iv, iv_buffer, 16);
|
|
free(iv_buffer);
|
|
|
|
// Step 5: Compute ECDH shared secret
|
|
unsigned char shared_secret[32];
|
|
if (ecdh_shared_secret(recipient_private_key, sender_public_key, shared_secret) != 0) {
|
|
free(ciphertext_b64);
|
|
free(ciphertext);
|
|
return NOSTR_ERROR_CRYPTO_FAILED;
|
|
}
|
|
|
|
// Step 6: Decrypt using AES-256-CBC
|
|
unsigned char* plaintext_padded = malloc(ciphertext_len);
|
|
if (!plaintext_padded) {
|
|
free(ciphertext_b64);
|
|
free(ciphertext);
|
|
return NOSTR_ERROR_MEMORY_FAILED;
|
|
}
|
|
|
|
if (aes_cbc_decrypt(shared_secret, iv, ciphertext, ciphertext_len, plaintext_padded) != 0) {
|
|
free(ciphertext_b64);
|
|
free(ciphertext);
|
|
free(plaintext_padded);
|
|
return NOSTR_ERROR_NIP04_DECRYPT_FAILED;
|
|
}
|
|
|
|
// Step 7: Remove PKCS#7 padding
|
|
size_t plaintext_len = pkcs7_unpad(plaintext_padded, ciphertext_len);
|
|
if (plaintext_len == 0 || plaintext_len > ciphertext_len) {
|
|
free(ciphertext_b64);
|
|
free(ciphertext);
|
|
free(plaintext_padded);
|
|
return NOSTR_ERROR_NIP04_DECRYPT_FAILED;
|
|
}
|
|
|
|
// Step 8: Copy to output buffer and null-terminate
|
|
if (plaintext_len + 1 > output_size) {
|
|
free(ciphertext_b64);
|
|
free(ciphertext);
|
|
free(plaintext_padded);
|
|
return NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL;
|
|
}
|
|
|
|
memcpy(output, plaintext_padded, plaintext_len);
|
|
output[plaintext_len] = '\0';
|
|
|
|
// Cleanup
|
|
memory_clear(shared_secret, 32);
|
|
memory_clear(plaintext_padded, ciphertext_len);
|
|
free(ciphertext_b64);
|
|
free(ciphertext);
|
|
free(plaintext_padded);
|
|
|
|
return NOSTR_SUCCESS;
|
|
}
|