fix(fips): add cipher config API, key zeroization, and provider leak guard

- Add fips_configure_cipher() to pin FIPS-approved SQLCipher PRAGMAs
  (HMAC-SHA512, PBKDF2-HMAC-SHA512, 256k iterations, 4096-byte pages)
- Call fips_configure_cipher() after sqlite3_key() in JNI and sample app
- Copy key material to OPENSSL_malloc buffer and zeroize with
  OPENSSL_cleanse after use (SP 800-132 §5.3)
- Fix provider leak: unload FIPS provider on EVP_set_default_properties
  failure to prevent silent FIPS bypass
- Delete stale DB before round-trip compliance test
- Add .codegraph/ and .grepai/ to .gitignore
- Document fips_configure_cipher API and key handling in CLAUDE.md
This commit is contained in:
Christopher Fahlin
2026-05-09 12:32:48 -07:00
parent debac2bedf
commit 06429cfdb4
9 changed files with 116 additions and 18 deletions
+4 -2
View File
@@ -114,8 +114,10 @@ samples/android/local.properties
.mise.local.toml
# grepai
.grepai/*.gob
.grepai/*.lock
.grepai/
# codegraph
.codegraph/
# Secrets & Config
.env
+4
View File
@@ -92,6 +92,10 @@ Bumping `OPENSSL_VERSION` requires updating `OPENSSL_URL_HASH` and re-running FI
`include/fips_init.h` + `src/fips_init.c` -- portable C API for FIPS provider bootstrap at app startup. Uses programmatic APIs (`OSSL_PROVIDER_set_default_search_path`, `OSSL_LIB_CTX_load_config`, `OSSL_PROVIDER_load`) to bypass Android's `AT_SECURE` which blocks all `OPENSSL_*` env vars in app processes.
`fips_configure_cipher(sqlite3 *db)` -- must be called immediately after `sqlite3_key()` and before any I/O. Pins FIPS-approved cipher PRAGMAs: AES-256-CBC, HMAC-SHA512, PBKDF2-HMAC-SHA512 with 256k iterations, 4096-byte pages. These match SQLCipher 4.x defaults but are set explicitly for FIPS audit trail.
Key material handling in JNI (`src/fips_db_jni.c`): key is copied into an `OPENSSL_malloc` buffer and zeroized with `OPENSSL_cleanse` after use per SP 800-132 §5.3.
Platform-specific wrappers:
- `src/fips_init_android.c` -- sets search path + loads config with absolute paths, then loads provider
- `src/fips_init_ios.c` -- loads provider from bundle resource path
+5
View File
@@ -35,6 +35,11 @@ fips_init_status_t fips_init(const char *module_dir, const char *conf_path);
int fips_self_test_rerun(void);
int fips_provider_is_active(void);
// Configure a freshly keyed SQLCipher connection for FIPS-compliant operation.
// Call immediately after sqlite3_key(). Returns SQLITE_OK (0) on success.
typedef struct sqlite3 sqlite3;
int fips_configure_cipher(sqlite3 *db);
#ifdef __ANDROID__
// Android convenience wrapper.
// files_dir: Context.getFilesDir() — expects fips/openssl.cnf underneath
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
#include <sqlite3.h>
#include "fips_init.h"
#include <openssl/provider.h>
#include <openssl/crypto.h>
@@ -117,6 +118,8 @@ fips_init_status_t fips_init(const char *module_dir, const char *conf_path) {
// are restricted to FIPS-approved implementations only.
if (EVP_set_default_properties(NULL, "fips=yes") != 1) {
LOGE("Failed to set fips=yes default properties\n");
OSSL_PROVIDER_unload(g_fips_provider);
g_fips_provider = NULL;
return FIPS_INIT_ERR_PROPERTY_SET;
}
@@ -124,6 +127,31 @@ fips_init_status_t fips_init(const char *module_dir, const char *conf_path) {
return FIPS_INIT_OK;
}
int fips_configure_cipher(sqlite3 *db) {
static const char *pragmas[] = {
"PRAGMA cipher_compatibility = 4",
"PRAGMA cipher_plaintext_header_size = 0",
"PRAGMA cipher_use_hmac = ON",
"PRAGMA cipher_hmac_algorithm = HMAC_SHA512",
"PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512",
"PRAGMA cipher_default_kdf_iter = 256000",
"PRAGMA cipher_page_size = 4096",
NULL,
};
for (const char **p = pragmas; *p; ++p) {
char *err = NULL;
int rc = sqlite3_exec(db, *p, NULL, NULL, &err);
if (rc != SQLITE_OK) {
LOGE("FIPS cipher config failed [%s]: %s\n", *p,
err ? err : sqlite3_errmsg(db));
if (err) sqlite3_free(err);
return rc;
}
if (err) sqlite3_free(err);
}
return SQLITE_OK;
}
int fips_self_test_rerun(void) {
if (!g_fips_provider) return 0;
return OSSL_PROVIDER_self_test(g_fips_provider) == 1 ? 1 : 0;
@@ -35,6 +35,11 @@ fips_init_status_t fips_init(const char *module_dir, const char *conf_path);
int fips_self_test_rerun(void);
int fips_provider_is_active(void);
// Configure a freshly keyed SQLCipher connection for FIPS-compliant operation.
// Call immediately after sqlite3_key(). Returns SQLITE_OK (0) on success.
typedef struct sqlite3 sqlite3;
int fips_configure_cipher(sqlite3 *db);
#ifdef __ANDROID__
// Android convenience wrapper.
// files_dir: Context.getFilesDir() — expects fips/openssl.cnf underneath
+11 -5
View File
@@ -84,6 +84,12 @@ static int exec_sql(sqlite3 *db, const char *sql) {
return rc;
}
static int key_and_configure(sqlite3 *db, const char *key, int key_len) {
int rc = sqlite3_key(db, key, key_len);
if (rc != SQLITE_OK) return rc;
return fips_configure_cipher(db);
}
JNIEXPORT jstring JNICALL
Java_com_example_fipsdemo_FipsNative_dbRoundTrip(JNIEnv *env, jclass cls,
jstring jpath, jstring jkey) {
@@ -98,8 +104,8 @@ Java_com_example_fipsdemo_FipsNative_dbRoundTrip(JNIEnv *env, jclass cls,
snprintf(result, sizeof(result), "FAIL: open: %s", sqlite3_errmsg(db));
goto out;
}
if (sqlite3_key(db, key, (int)strlen(key)) != SQLITE_OK) {
snprintf(result, sizeof(result), "FAIL: key");
if (key_and_configure(db, key, (int)strlen(key)) != SQLITE_OK) {
snprintf(result, sizeof(result), "FAIL: key/configure: %s", sqlite3_errmsg(db));
goto out;
}
if (exec_sql(db, "CREATE TABLE IF NOT EXISTS demo(id INTEGER PRIMARY KEY, msg TEXT, ts REAL)") != SQLITE_OK) {
@@ -140,13 +146,13 @@ Java_com_example_fipsdemo_FipsNative_dbRekey(JNIEnv *env, jclass cls,
jboolean ok = JNI_FALSE;
if (sqlite3_open(path, &db) != SQLITE_OK) goto out;
if (sqlite3_key(db, oldK, (int)strlen(oldK)) != SQLITE_OK) goto out;
if (key_and_configure(db, oldK, (int)strlen(oldK)) != SQLITE_OK) goto out;
if (exec_sql(db, "SELECT count(*) FROM sqlite_master") != SQLITE_OK) goto out;
if (sqlite3_rekey(db, newK, (int)strlen(newK)) != SQLITE_OK) goto out;
sqlite3_close(db); db = NULL;
if (sqlite3_open(path, &db) != SQLITE_OK) goto out;
if (sqlite3_key(db, newK, (int)strlen(newK)) != SQLITE_OK) goto out;
if (key_and_configure(db, newK, (int)strlen(newK)) != SQLITE_OK) goto out;
if (exec_sql(db, "SELECT count(*) FROM sqlite_master") == SQLITE_OK) ok = JNI_TRUE;
out:
@@ -168,7 +174,7 @@ Java_com_example_fipsdemo_FipsNative_dbWrongKeyRejected(JNIEnv *env, jclass cls,
jboolean rejected = JNI_FALSE;
if (sqlite3_open(path, &db) == SQLITE_OK) {
if (sqlite3_key(db, bad, (int)strlen(bad)) == SQLITE_OK) {
if (key_and_configure(db, bad, (int)strlen(bad)) == SQLITE_OK) {
int rc = sqlite3_exec(db, "SELECT count(*) FROM sqlite_master",
NULL, NULL, NULL);
rejected = (rc != SQLITE_OK) ? JNI_TRUE : JNI_FALSE;
@@ -29,6 +29,7 @@ fun runComplianceSuite(filesDir: String): List<ComplianceCheck> {
v.startsWith("OpenSSL 3.0.") to v
},
check("DB Write/Read", "SQLCipher encrypted round-trip via FIPS crypto") {
java.io.File(dbPath).delete()
val result = FipsNative.dbRoundTrip(dbPath, key)
result.startsWith("OK") to result
},
+30 -11
View File
@@ -5,9 +5,11 @@
#include <jni.h>
#include <android/log.h>
#include <openssl/crypto.h>
#include <sqlite3.h>
#include <string.h>
#include <stdlib.h>
#include "fips_init.h"
#define TAG "FipsDatabase"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
@@ -17,32 +19,49 @@ Java_com_fips_sqlcipher_FipsDatabase_nativeOpen(JNIEnv *env, jclass cls,
jstring jpath, jstring jkey) {
(void)cls;
const char *path = (*env)->GetStringUTFChars(env, jpath, NULL);
const char *key = (*env)->GetStringUTFChars(env, jkey, NULL);
const char *jkey_str = (*env)->GetStringUTFChars(env, jkey, NULL);
sqlite3 *db = NULL;
jlong result = 0;
int key_len = (int)strlen(jkey_str);
char *key = (char *)OPENSSL_malloc(key_len + 1);
if (!key) {
(*env)->ReleaseStringUTFChars(env, jpath, path);
(*env)->ReleaseStringUTFChars(env, jkey, jkey_str);
return 0;
}
memcpy(key, jkey_str, key_len + 1);
(*env)->ReleaseStringUTFChars(env, jkey, jkey_str);
int rc = sqlite3_open(path, &db);
if (rc != SQLITE_OK) {
LOGE("sqlite3_open(%s) failed: %s", path, sqlite3_errmsg(db));
if (db) sqlite3_close(db);
(*env)->ReleaseStringUTFChars(env, jpath, path);
(*env)->ReleaseStringUTFChars(env, jkey, key);
return 0;
goto out;
}
if (key && key[0]) {
rc = sqlite3_key(db, key, (int)strlen(key));
if (key[0]) {
rc = sqlite3_key(db, key, key_len);
if (rc != SQLITE_OK) {
LOGE("sqlite3_key failed: %s", sqlite3_errmsg(db));
sqlite3_close(db);
(*env)->ReleaseStringUTFChars(env, jpath, path);
(*env)->ReleaseStringUTFChars(env, jkey, key);
return 0;
goto out;
}
rc = fips_configure_cipher(db);
if (rc != SQLITE_OK) {
LOGE("fips_configure_cipher failed: %s", sqlite3_errmsg(db));
sqlite3_close(db);
goto out;
}
}
result = (jlong)(intptr_t)db;
out:
OPENSSL_cleanse(key, key_len);
OPENSSL_free(key);
(*env)->ReleaseStringUTFChars(env, jpath, path);
(*env)->ReleaseStringUTFChars(env, jkey, key);
return (jlong)(intptr_t)db;
return result;
}
JNIEXPORT void JNICALL
+28
View File
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
#include <sqlite3.h>
#include "fips_init.h"
#include <openssl/provider.h>
#include <openssl/crypto.h>
@@ -117,6 +118,8 @@ fips_init_status_t fips_init(const char *module_dir, const char *conf_path) {
// are restricted to FIPS-approved implementations only.
if (EVP_set_default_properties(NULL, "fips=yes") != 1) {
LOGE("Failed to set fips=yes default properties\n");
OSSL_PROVIDER_unload(g_fips_provider);
g_fips_provider = NULL;
return FIPS_INIT_ERR_PROPERTY_SET;
}
@@ -124,6 +127,31 @@ fips_init_status_t fips_init(const char *module_dir, const char *conf_path) {
return FIPS_INIT_OK;
}
int fips_configure_cipher(sqlite3 *db) {
static const char *pragmas[] = {
"PRAGMA cipher_compatibility = 4",
"PRAGMA cipher_plaintext_header_size = 0",
"PRAGMA cipher_use_hmac = ON",
"PRAGMA cipher_hmac_algorithm = HMAC_SHA512",
"PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512",
"PRAGMA cipher_default_kdf_iter = 256000",
"PRAGMA cipher_page_size = 4096",
NULL,
};
for (const char **p = pragmas; *p; ++p) {
char *err = NULL;
int rc = sqlite3_exec(db, *p, NULL, NULL, &err);
if (rc != SQLITE_OK) {
LOGE("FIPS cipher config failed [%s]: %s\n", *p,
err ? err : sqlite3_errmsg(db));
if (err) sqlite3_free(err);
return rc;
}
if (err) sqlite3_free(err);
}
return SQLITE_OK;
}
int fips_self_test_rerun(void) {
if (!g_fips_provider) return 0;
return OSSL_PROVIDER_self_test(g_fips_provider) == 1 ? 1 : 0;