/* * test_omemo_crypto.c * * Unit tests for the OMEMO AES-256-GCM file crypto (src/omemo/crypto.c). * The decrypt direction streams plaintext before the tag is checked, so * callers rely on the returned error code to discard unverified output — * these tests pin that contract (issue #146, REQ-CRY-06). */ #include "config.h" #include #include #include #include "prof_cmocka.h" #ifdef HAVE_OMEMO #include "omemo/omemo.h" #include "omemo/crypto.h" #define TAG_LENGTH 16 static const unsigned char PLAINTEXT[] = "at-rest integrity check payload: 0123456789abcdef"; // gcrypt secure memory must be set up exactly once per process static int _crypto_init_once(void) { static gboolean done = FALSE; static int rc = 0; if (!done) { rc = omemo_crypto_init(); done = TRUE; } return rc; } static off_t _file_size(FILE* fh) { fseeko(fh, 0, SEEK_END); off_t size = ftello(fh); rewind(fh); return size; } // encrypt PLAINTEXT with a fixed key/nonce into a fresh tmpfile static FILE* _encrypted_tmpfile(unsigned char* key, unsigned char* nonce) { memset(key, 0x42, OMEMO_AESGCM_KEY_LENGTH); memset(nonce, 0x24, OMEMO_AESGCM_NONCE_LENGTH); FILE* plain = tmpfile(); FILE* cipher = tmpfile(); assert_non_null(plain); assert_non_null(cipher); assert_int_equal(sizeof(PLAINTEXT), fwrite(PLAINTEXT, 1, sizeof(PLAINTEXT), plain)); rewind(plain); assert_int_equal(GPG_ERR_NO_ERROR, aes256gcm_crypt_file(plain, cipher, (off_t)sizeof(PLAINTEXT), key, nonce, TRUE)); fclose(plain); rewind(cipher); return cipher; } // corrupt one byte at offset (negative counts from the end), return reopened stream static FILE* _flip_byte(FILE* cipher, long offset) { off_t size = _file_size(cipher); unsigned char* buf = g_malloc(size); assert_int_equal(size, fread(buf, 1, size, cipher)); fclose(cipher); long pos = offset >= 0 ? offset : (long)size + offset; buf[pos] ^= 0xFF; FILE* tampered = tmpfile(); assert_non_null(tampered); assert_int_equal(size, fwrite(buf, 1, size, tampered)); rewind(tampered); g_free(buf); return tampered; } void aes256gcm_crypt_file__roundtrip_succeeds(void** state) { assert_int_equal(0, _crypto_init_once()); unsigned char key[OMEMO_AESGCM_KEY_LENGTH]; unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH]; FILE* cipher = _encrypted_tmpfile(key, nonce); off_t cipher_size = _file_size(cipher); assert_int_equal((off_t)sizeof(PLAINTEXT) + TAG_LENGTH, cipher_size); FILE* decrypted = tmpfile(); assert_non_null(decrypted); assert_int_equal(GPG_ERR_NO_ERROR, aes256gcm_crypt_file(cipher, decrypted, cipher_size, key, nonce, FALSE)); unsigned char readback[sizeof(PLAINTEXT)]; rewind(decrypted); assert_int_equal(sizeof(PLAINTEXT), fread(readback, 1, sizeof(readback), decrypted)); assert_memory_equal(PLAINTEXT, readback, sizeof(PLAINTEXT)); fclose(cipher); fclose(decrypted); } void aes256gcm_crypt_file__rejects_tampered_tag(void** state) { assert_int_equal(0, _crypto_init_once()); unsigned char key[OMEMO_AESGCM_KEY_LENGTH]; unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH]; FILE* cipher = _flip_byte(_encrypted_tmpfile(key, nonce), -1); // last tag byte FILE* decrypted = tmpfile(); assert_non_null(decrypted); gcry_error_t res = aes256gcm_crypt_file(cipher, decrypted, _file_size(cipher), key, nonce, FALSE); assert_int_not_equal(GPG_ERR_NO_ERROR, res); fclose(cipher); fclose(decrypted); } void aes256gcm_crypt_file__rejects_tampered_ciphertext(void** state) { assert_int_equal(0, _crypto_init_once()); unsigned char key[OMEMO_AESGCM_KEY_LENGTH]; unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH]; FILE* cipher = _flip_byte(_encrypted_tmpfile(key, nonce), 0); // first payload byte FILE* decrypted = tmpfile(); assert_non_null(decrypted); gcry_error_t res = aes256gcm_crypt_file(cipher, decrypted, _file_size(cipher), key, nonce, FALSE); assert_int_not_equal(GPG_ERR_NO_ERROR, res); fclose(cipher); fclose(decrypted); } #endif