feat: add Room integration via FipsSQLiteOpenHelperFactory in AAR

- Add FipsDatabase (SupportSQLiteDatabase impl) wrapping native SQLCipher JNI
- Add FipsSQLiteOpenHelperFactory for Room openHelperFactory() integration
- Add fips_db_jni.c with full SQLCipher JNI bridge (open/close/exec/prepare/step/bind/column)
- Add fips_init_jni.c for FIPSSQLCipher Kotlin class JNI bridge
- Add packaging/build_jni.sh to cross-compile libfips_jni.so per ABI
- Add packaging/android-lib Gradle module to compile Kotlin into classes.jar
- Fix FIPS init to use programmatic APIs (OSSL_PROVIDER_set_default_search_path,
  OSSL_LIB_CTX_load_config) bypassing AT_SECURE env var blocking on Android
- AAR now self-contained: classes.jar, libfips_jni.so, per-ABI fipsmodule.cnf,
  C sources in assets/native/
- Add Room demo section to sample app with FIPS-encrypted CRUD operations
- Remove dead code: appenv.c (unused setenv shim), fips_sqlcipher.h
- Update install.md to reflect simplified 2-step Android integration
This commit is contained in:
Christopher Fahlin
2026-05-04 20:47:05 -07:00
parent 5436c4fa14
commit debac2bedf
36 changed files with 1844 additions and 537 deletions
+21 -5
View File
@@ -30,11 +30,11 @@ ANDROID_ABI=x86_64 ./build.sh android # emulator
./build.sh package # both
```
### Test App (tests/android-fips)
### Sample App (samples/android)
```bash
cd tests/android-fips
./bootstrap.sh
cd samples/android
./bootstrap.sh # extracts native libs from AAR into jniLibs/
./gradlew assembleDebug
./gradlew installDebug
```
@@ -90,10 +90,26 @@ Bumping `OPENSSL_VERSION` requires updating `OPENSSL_URL_HASH` and re-running FI
## FIPS Initialization Helpers
`include/fips_init.h` + `src/fips_init.c` -- portable C API for FIPS provider bootstrap at app startup. Platform-specific wrappers:
- `src/fips_init_android.c` -- handles `OPENSSL_MODULES` / `FIPSMODULE_CNF` env from Context paths
`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.
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
## JNI Bridge
`src/fips_init_jni.c` -- JNI for `com.fips.sqlcipher.FIPSSQLCipher` (init, status, self-test)
`src/fips_db_jni.c` -- JNI for `com.fips.sqlcipher.FipsDatabase` (SQLCipher open/close/exec/prepare/step/bind/column)
Built by `packaging/build_jni.sh` into `libfips_jni.so` per ABI. CMake at `packaging/jni/CMakeLists.txt` links against `libcrypto.so` and `libsqlcipher.so`.
## AAR Kotlin Classes
`packaging/android-lib/` -- Gradle module that compiles into `classes.jar`:
- `FIPSSQLCipher` -- one-call FIPS init (extracts configs, loads libs, activates provider)
- `FipsDatabase` -- `SupportSQLiteDatabase` impl over native SQLCipher JNI
- `FipsSQLiteOpenHelperFactory` -- `SupportSQLiteOpenHelper.Factory` for Room integration
`scripts/verify_package_integrity.sh` -- CI script that extracts FIPS modules from AAR/XCFramework and validates SHA256, .symtab, and .rodata presence.
## Output
+7 -22
View File
@@ -25,37 +25,22 @@ typedef enum {
FIPS_INIT_ERR_PROPERTY_SET,
} fips_init_status_t;
// Human-readable description of a status code.
const char *fips_init_status_str(fips_init_status_t status);
// Initialize OpenSSL with FIPS provider from the given paths.
//
// module_dir: directory containing libfips.so (Android) or fips.dylib (iOS)
// conf_path: path to openssl.cnf that .includes fipsmodule.cnf
// (NULL = use OPENSSL_CONF env var, or generate minimal config)
//
// On Android, call this AFTER extracting assets/fips/* to the app's filesDir.
// On iOS, pass the path within the app bundle where fips.dylib is embedded.
//
// Returns FIPS_INIT_OK on success. On failure, the FIPS provider is NOT active
// and all crypto operations will fail (which is the correct behavior — you MUST
// NOT proceed with plaintext fallback under FIPS requirements).
// Initialize OpenSSL with FIPS provider.
// module_dir: directory containing fips.so (Android) or fips.dylib (iOS)
// conf_path: path to openssl.cnf (NULL = use OPENSSL_CONF env var)
fips_init_status_t fips_init(const char *module_dir, const char *conf_path);
// Re-run the FIPS self-test on demand (e.g., after app resume from background).
// The provider must already be loaded via fips_init().
// Returns 1 on success, 0 on failure.
int fips_self_test_rerun(void);
// Query whether the FIPS provider is currently active in the default context.
int fips_provider_is_active(void);
#ifdef __ANDROID__
// Android convenience: takes Context.getFilesDir() and
// ApplicationInfo.nativeLibraryDir paths. Handles OPENSSL_MODULES and
// FIPSMODULE_CNF env setup before calling fips_init().
// Android convenience wrapper.
// files_dir: Context.getFilesDir() — expects fips/openssl.cnf underneath
// modules_dir: directory containing fips.so (extracted by FIPSSQLCipher.kt)
fips_init_status_t fips_init_android(const char *files_dir,
const char *native_lib_dir);
const char *modules_dir);
#endif
#if defined(__APPLE__) && !defined(__ANDROID__)
+127 -202
View File
@@ -1,293 +1,218 @@
# FIPS SQLCipher — Integration Guide
Pre-built FIPS 140-3 compliant SQLCipher with OpenSSL 3.0.8 FIPS provider.
## Contents
```
out/
├── android/
│ └── fips-sqlcipher.aar # Android Archive (arm64-v8a + x86_64)
├── ios/
│ └── FIPSSQLCipher.xcframework # iOS (device arm64 + simulator arm64/x86_64)
├── include/
│ ├── fips_init.h # FIPS initialization API (C)
│ ├── fips_sqlcipher.h # Combined OpenSSL + SQLCipher header
│ └── fips_verify.hpp # C++ verification class
└── src/
├── fips_init.c # Core initialization (both platforms)
├── fips_init_android.c # Android-specific init helper
└── fips_init_ios.c # iOS-specific init helper
```
FIPS 140-3 compliant SQLCipher with OpenSSL 3.0.8 for Android and iOS.
---
## Android
### 1. Add the AAR
### AAR Contents
Copy `android/fips-sqlcipher.aar` into your project (e.g., `app/libs/`):
```
fips-sqlcipher.aar
├── jni/<abi>/
│ ├── libcrypto.so, libssl.so, libsqlcipher.so
│ ├── libfips.so (FIPS provider — never strip)
│ └── libfips_jni.so (JNI bridge for Kotlin helpers)
├── classes.jar
│ ├── FIPSSQLCipher — one-call FIPS init
│ ├── FipsDatabase — SupportSQLiteDatabase over FIPS SQLCipher
│ └── FipsSQLiteOpenHelperFactory — Room integration
├── assets/
│ ├── fips/<abi>/fipsmodule.cnf (per-ABI HMAC fingerprints)
│ └── native/{include,src}/ (C sources for custom JNI)
└── AndroidManifest.xml
```
### Step 1: Add the AAR
Copy `fips-sqlcipher.aar` to `app/libs/`:
```kotlin
// app/build.gradle.kts
dependencies {
implementation(files("libs/fips-sqlcipher.aar"))
}
```
### 2. Packaging options (critical for FIPS)
```kotlin
// app/build.gradle.kts
android {
packaging {
jniLibs {
// Preserve debug symbols in the FIPS module — stripping invalidates
// the incore HMAC and will cause FIPS self-test failure.
keepDebugSymbols += setOf("**/libfips.so")
useLegacyPackaging = false
}
packaging.jniLibs {
keepDebugSymbols += setOf("**/libfips.so") // stripping breaks FIPS HMAC
}
}
```
### 3. Library loading order (critical)
OpenSSL environment variables must be set BEFORE `libcrypto.so` loads. Use a
tiny "appenv" shim with no OpenSSL dependencies, loaded first:
```c
// appenv.c — compile into libappenv.so (no crypto link deps)
#include <jni.h>
#include <stdlib.h>
JNIEXPORT jint JNICALL
Java_com_yourapp_AppEnv_setenv(JNIEnv *env, jclass cls,
jstring jname, jstring jvalue) {
const char *n = (*env)->GetStringUTFChars(env, jname, NULL);
const char *v = (*env)->GetStringUTFChars(env, jvalue, NULL);
int rc = setenv(n, v, 1);
(*env)->ReleaseStringUTFChars(env, jname, n);
(*env)->ReleaseStringUTFChars(env, jvalue, v);
return rc;
}
```
Loading sequence in Kotlin:
### Step 2: Initialize at startup
```kotlin
// 1. Load appenv (no crypto deps)
System.loadLibrary("appenv")
// 2. Extract openssl.cnf from assets
val fipsDir = File(filesDir, "fips").apply { mkdirs() }
assets.open("fips/openssl.cnf").use { input ->
File(fipsDir, "openssl.cnf").outputStream().use { input.copyTo(it) }
}
// 3. Set env BEFORE loading libcrypto
AppEnv.setenv("OPENSSL_CONF", File(fipsDir, "openssl.cnf").absolutePath)
AppEnv.setenv("OPENSSL_MODULES", applicationInfo.nativeLibraryDir)
// 4. Load crypto stack
System.loadLibrary("crypto")
System.loadLibrary("sqlcipher")
// 5. Load your JNI bridge
System.loadLibrary("your_jni")
// Application.onCreate() or Activity.onCreate() — before any crypto or DB operations
FIPSSQLCipher.init(this)
```
### 4. Initialize FIPS at startup (native)
That's it. `FIPSSQLCipher.init()` handles:
Add `src/fips_init.c` and `src/fips_init_android.c` to your native CMakeLists:
1. Extracts per-ABI `fipsmodule.cnf` from assets to internal storage
2. Generates `openssl.cnf` with absolute `.include` paths (required because Android's `AT_SECURE` blocks `OPENSSL_CONF` env vars)
3. Extracts `fips.so` from the APK (renames from `libfips.so` — OpenSSL convention)
4. Loads native libraries in the correct order: `libcrypto.so``libfips_jni.so` → FIPS init → `libsqlcipher.so`
5. Calls `OSSL_PROVIDER_set_default_search_path()` + `OSSL_LIB_CTX_load_config()` + `OSSL_PROVIDER_load("fips")` programmatically
### Step 3: Use with Room (optional)
```kotlin
// AppDatabase.kt
@Database(entities = [MyEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun myDao(): MyDao
companion object {
fun create(context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.openHelperFactory(FipsSQLiteOpenHelperFactory("your-encryption-key"))
.build()
}
}
```
`FipsSQLiteOpenHelperFactory` implements `SupportSQLiteOpenHelper.Factory` and wraps all database operations through FIPS-validated SQLCipher encryption. Room handles migrations, DAOs, and queries normally — the encryption is transparent.
### Direct database access (without Room)
```kotlin
val db = FipsDatabase.open("/path/to/db", "encryption-key")
db.execSQL("CREATE TABLE demo (id INTEGER PRIMARY KEY, msg TEXT)")
db.insert("demo", SQLiteDatabase.CONFLICT_NONE, contentValuesOf("msg" to "hello"))
val cursor = db.query("SELECT * FROM demo")
db.close()
```
### Custom native JNI (advanced)
The AAR bundles C source files in `assets/native/` for apps that need custom native integration. Extract and add to your CMakeLists:
```cmake
add_library(your_jni SHARED your_jni.c fips_init.c fips_init_android.c)
add_library(your_jni SHARED
fips_init.c
fips_init_android.c
)
target_include_directories(your_jni PRIVATE path/to/include)
target_compile_definitions(your_jni PRIVATE SQLITE_HAS_CODEC)
target_link_libraries(your_jni PRIVATE crypto sqlcipher log)
```
Note: `SQLITE_HAS_CODEC` is required to declare `sqlite3_key()` and `sqlite3_rekey()`.
`SQLITE_HAS_CODEC` is required to declare `sqlite3_key()` / `sqlite3_rekey()`.
Call from JNI before any SQLCipher operation:
### 16 KB page alignment (Android 15+)
```c
#include "fips_init.h"
Add to your native CMakeLists for Google Play compatibility:
JNIEXPORT void JNICALL Java_com_yourapp_Native_initFips(
JNIEnv *env, jclass cls, jstring jfilesDir, jstring jnativeDir) {
const char *files = (*env)->GetStringUTFChars(env, jfilesDir, NULL);
const char *natv = (*env)->GetStringUTFChars(env, jnativeDir, NULL);
fips_init_status_t rc = fips_init_android(files, natv);
if (rc != FIPS_INIT_OK) {
// FIPS integrity compromised — abort or refuse to operate
__android_log_print(ANDROID_LOG_ERROR, "FIPS",
"FATAL: %s", fips_init_status_str(rc));
}
(*env)->ReleaseStringUTFChars(env, jfilesDir, files);
(*env)->ReleaseStringUTFChars(env, jnativeDir, natv);
}
```cmake
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,max-page-size=16384")
```
### 5. fipsmodule.cnf (NOT required)
The shipped `openssl.cnf` is minimal — it does NOT auto-activate the FIPS
provider via config. Instead, `fips_init_android()` loads it programmatically
via `OSSL_PROVIDER_load()`. No `fipsmodule.cnf` is needed.
### 6. ProGuard / R8
The AAR ships `proguard.txt` with:
```
-keep class com.fips.sqlcipher.** { *; }
```
The AAR's bundled libraries are already 16 KB aligned.
---
## iOS
### 1. Add the XCFramework
### Step 1: Add the XCFramework
Drag `ios/FIPSSQLCipher.xcframework` into your Xcode project. Ensure:
Drag `FIPSSQLCipher.xcframework` into your Xcode project. Set **Embed & Sign**.
- **Embed & Sign** is selected for the framework
- The `fips.dylib` in `Resources/fips/` is included in your **Copy Bundle Resources** phase
### Step 2: Link system frameworks
### 2. Link required system frameworks
In Build Phases → Link Binary With Libraries:
Build Phases > Link Binary With Libraries:
- `Security.framework`
- `libz.tbd`
### 3. Xcode build settings (critical for FIPS)
Apply these settings to prevent Xcode from stripping the FIPS module:
### Step 3: Disable stripping (critical)
Build Settings:
```
STRIP_INSTALLED_PRODUCT = NO
COPY_PHASE_STRIP = NO
STRIP_STYLE = non-global
DEPLOYMENT_POSTPROCESSING = NO
DEAD_CODE_STRIPPING = NO
```
Or apply the shipped xcconfig:
```
// In your .xcconfig or via Project > Build Settings
#include "path/to/fips_integrity.xcconfig"
Stripping invalidates the FIPS provider's HMAC integrity check.
### Step 4: Copy fips.dylib into the app bundle
Add a **Run Script** build phase:
```bash
FIPS_FW="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/FIPSSQLCipher.framework"
FIPS_DST="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/fips"
mkdir -p "$FIPS_DST"
if [ -f "$FIPS_FW/fips.dylib" ]; then
cp "$FIPS_FW/fips.dylib" "$FIPS_DST/"
elif [ -f "$FIPS_FW/Resources/fips/fips.dylib" ]; then
cp "$FIPS_FW/Resources/fips/fips.dylib" "$FIPS_DST/"
fi
```
### 4. Initialize FIPS at app launch
### Step 5: Add C sources + bridging header
Add `src/fips_init.c` and `src/fips_init_ios.c` to your Xcode target.
Add `fips_init.c` and `fips_init_ios.c` to your Xcode target.
**Swift (via bridging header):**
Create a bridging header:
```c
// BridgingHeader.h
#define SQLITE_HAS_CODEC 1 // Required for sqlite3_key/sqlite3_rekey
#define SQLITE_HAS_CODEC 1
#import "fips_init.h"
#import <openssl/crypto.h>
#import <openssl/provider.h>
#import <sqlite3.h>
// AppDelegate.swift
func application(_ application: UIApplication,
didFinishLaunchingWithOptions opts: ...) -> Bool {
let fipsDir = Bundle.main.resourcePath! + "/fips"
let rc = fips_init_ios(fipsDir)
guard rc == FIPS_INIT_OK else {
fatalError("FIPS init failed: \(String(cString: fips_init_status_str(rc)))")
}
return true
}
```
**Objective-C:**
### Step 6: Initialize at launch
```objc
#import "fips_init.h"
**Option A — Use the Swift helper** (recommended):
- (BOOL)application:(UIApplication *)app
didFinishLaunchingWithOptions:(NSDictionary *)opts {
NSString *fipsDir = [[NSBundle mainBundle].resourcePath
stringByAppendingPathComponent:@"fips"];
fips_init_status_t rc = fips_init_ios(fipsDir.UTF8String);
NSAssert(rc == FIPS_INIT_OK, @"FIPS: %s", fips_init_status_str(rc));
return YES;
}
```
### 5. Swift module import
The XCFramework includes a `module.modulemap`:
Copy `src/ios/FIPSSQLCipher.swift` into your project:
```swift
import FIPSSQLCipher
// sqlite3.h and openssl headers are available
// App init or AppDelegate
try FIPSSQLCipher.initialize()
```
### 6. Copy fips.dylib to app bundle
**Option B — Call C directly:**
Add a **Run Script** build phase (or use Copy Files):
```bash
FIPS_SRC="${BUILT_PRODUCTS_DIR}/FIPSSQLCipher.xcframework/ios-arm64/Resources/fips"
FIPS_DST="${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/fips"
mkdir -p "$FIPS_DST"
cp "$FIPS_SRC/fips.dylib" "$FIPS_DST/"
```swift
let fipsDir = Bundle.main.resourcePath! + "/fips"
let rc = fips_init_ios(fipsDir)
guard rc == FIPS_INIT_OK else {
fatalError("FIPS: \(String(cString: fips_init_status_str(rc)))")
}
```
---
## Runtime FIPS Verification
After initialization, verify FIPS compliance on demand:
## Verifying FIPS at runtime
```c
#include "fips_init.h"
// Check provider is loaded
assert(fips_provider_is_active());
// Re-run self-test (e.g., after app resume)
assert(fips_self_test_rerun());
assert(fips_provider_is_active()); // provider loaded?
assert(fips_self_test_rerun()); // POST/KAT still passing?
```
**C++ (optional):**
Android (Kotlin):
```cpp
#include "fips_verify.hpp"
auto result = fips::Verifier::verify_with_key("/path/to/db.sqlite", "my-key");
assert(result.provider_active);
assert(result.self_test_passed);
```kotlin
assert(FIPSSQLCipher.isActive) // provider loaded and active?
assert(FIPSSQLCipher.selfTest()) // POST/KAT re-run passes?
```
---
## What can break FIPS integrity
## What breaks FIPS
| Action | Effect | Mitigation |
|--------|--------|------------|
| `strip` on libfips.so / fips.dylib | Removes .symtab, invalidates HMAC | keepDebugSymbols (Android), STRIP_INSTALLED_PRODUCT=NO (iOS) |
| Bitcode recompilation (iOS) | Produces different binary | ENABLE_BITCODE=NO |
| AGP minification of native libs | May modify .so contents | useLegacyPackaging=false |
| Code signing without preserving structure | Can modify load commands | Normal codesign is safe |
| UPX/binary compression | Mutates all sections | Never compress FIPS modules |
---
## Minimum deployment targets
- **Android**: API 24 (Android 7.0)
- **iOS**: 17.0 (one major version behind current)
| Action | Effect | Fix |
|--------|--------|-----|
| Stripping libfips.so / fips.dylib | Invalidates HMAC | `keepDebugSymbols` / `STRIP_INSTALLED_PRODUCT=NO` |
| UPX / binary compression | Mutates sections | Never compress FIPS modules |
| Missing `SQLITE_HAS_CODEC` define | `sqlite3_key()` undeclared | Add to compile definitions |
## Versions
- OpenSSL: 3.0.8 (FIPS 140-3 validated)
- SQLCipher: v4.6.1
- OpenSSL 3.0.8 (FIPS 140-3 validated), SQLCipher v4.6.1
- Android: API 24+, iOS: 17.0+
+27
View File
@@ -0,0 +1,27 @@
plugins {
id("com.android.library") version "8.7.3"
id("org.jetbrains.kotlin.android") version "2.1.0"
}
android {
namespace = "com.fips.sqlcipher"
compileSdk = 35
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation("androidx.sqlite:sqlite:2.4.0")
implementation("androidx.sqlite:sqlite-framework:2.4.0")
}
+1
View File
@@ -0,0 +1 @@
android.useAndroidX=true
Binary file not shown.
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+252
View File
@@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+16
View File
@@ -0,0 +1,16 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "fips-sqlcipher-lib"
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,125 @@
package com.fips.sqlcipher
import android.content.Context
import android.os.Build
import android.util.Log
import java.io.File
import java.util.zip.ZipFile
/**
* FIPS SQLCipher initialization — call [init] once from Application.onCreate().
*
* class MyApp : Application() {
* override fun onCreate() {
* super.onCreate()
* FIPSSQLCipher.init(this)
* }
* }
*
* After init(), [isActive] returns true and all SQLCipher operations use
* FIPS-validated cryptography. No additional setup required.
*/
object FIPSSQLCipher {
private const val TAG = "FIPSSQLCipher"
@Volatile
private var initialized = false
/**
* Initialize the FIPS provider and load native libraries.
* Safe to call multiple times — subsequent calls are no-ops.
*
* @throws IllegalStateException if libfips.so is not found in the APK
*/
@Synchronized
@JvmStatic
fun init(context: Context) {
if (initialized) return
val appCtx = context.applicationContext
val fipsDir = File(appCtx.filesDir, "fips").apply { mkdirs() }
val modulesDir = File(fipsDir, "modules").apply { mkdirs() }
val abi = Build.SUPPORTED_ABIS[0]
extractAsset(appCtx, "fips/$abi/fipsmodule.cnf", File(fipsDir, "fipsmodule.cnf"))
generateOpenSslConf(fipsDir)
extractFipsModule(appCtx, modulesDir)
System.loadLibrary("crypto")
System.loadLibrary("fips_jni")
val rc = nativeInit(appCtx.filesDir.absolutePath, modulesDir.absolutePath)
if (rc != 0) {
val msg = nativeStatusMessage(rc)
Log.e(TAG, "FIPS init failed (code $rc): $msg")
throw SecurityException("FIPS initialization failed: $msg")
}
System.loadLibrary("sqlcipher")
initialized = true
Log.i(TAG, "FIPS provider active, all libraries loaded")
}
/** Returns true if the FIPS provider is loaded and active. */
@JvmStatic
val isActive: Boolean
get() = initialized && nativeProviderActive()
/** Re-runs the FIPS Power-On Self-Test (POST/KAT). */
@JvmStatic
fun selfTest(): Boolean = nativeSelfTest()
/**
* Returns the directory containing the extracted fips.so module.
* Useful if you need to pass the path to custom native code.
*/
@JvmStatic
fun modulesDir(context: Context): String =
File(context.filesDir, "fips/modules").absolutePath
private fun generateOpenSslConf(fipsDir: File) {
val conf = File(fipsDir, "openssl.cnf")
val fipsModuleCnf = File(fipsDir, "fipsmodule.cnf")
if (conf.exists()) return
conf.writeText(
"openssl_conf = openssl_init\n" +
".include ${fipsModuleCnf.absolutePath}\n" +
"[openssl_init]\n" +
"providers = provider_sect\n" +
"[provider_sect]\n" +
"fips = fips_sect\n" +
"base = base_sect\n" +
"[base_sect]\n" +
"activate = 1\n"
)
}
private fun extractAsset(context: Context, assetPath: String, target: File) {
if (target.exists()) return
context.assets.open(assetPath).use { input ->
target.outputStream().use { input.copyTo(it) }
}
}
private fun extractFipsModule(context: Context, modulesDir: File) {
val target = File(modulesDir, "fips.so")
val abi = Build.SUPPORTED_ABIS[0]
ZipFile(context.applicationInfo.sourceDir).use { apk ->
val entry = apk.getEntry("lib/$abi/libfips.so")
?: error("libfips.so not found in APK for ABI $abi")
if (target.exists() && target.length() == entry.size) return
apk.getInputStream(entry).use { input ->
target.outputStream().use { input.copyTo(it) }
}
}
target.setReadable(true, false)
target.setExecutable(true, false)
}
@JvmStatic private external fun nativeInit(filesDir: String, modulesDir: String): Int
@JvmStatic private external fun nativeStatusMessage(code: Int): String
@JvmStatic private external fun nativeProviderActive(): Boolean
@JvmStatic private external fun nativeSelfTest(): Boolean
}
@@ -0,0 +1,345 @@
package com.fips.sqlcipher
import android.content.ContentValues
import android.database.Cursor
import android.database.MatrixCursor
import android.database.SQLException
import android.database.sqlite.SQLiteTransactionListener
import android.os.CancellationSignal
import android.util.Pair
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteProgram
import androidx.sqlite.db.SupportSQLiteQuery
import androidx.sqlite.db.SupportSQLiteStatement
import java.util.Locale
class FipsDatabase private constructor(
private val dbPtr: Long,
private val dbPath: String,
) : SupportSQLiteDatabase {
private var transactionActive = false
private var transactionSuccessful = false
companion object {
fun open(path: String, key: String): FipsDatabase {
val ptr = nativeOpen(path, key)
if (ptr == 0L) throw SQLException("Failed to open database: $path")
return FipsDatabase(ptr, path)
}
@JvmStatic private external fun nativeOpen(path: String, key: String): Long
@JvmStatic private external fun nativeClose(ptr: Long)
@JvmStatic private external fun nativeExec(ptr: Long, sql: String)
@JvmStatic private external fun nativePrepare(dbPtr: Long, sql: String): Long
@JvmStatic private external fun nativeStep(stmtPtr: Long): Int
@JvmStatic private external fun nativeFinalize(stmtPtr: Long)
@JvmStatic private external fun nativeBindString(stmtPtr: Long, index: Int, value: String)
@JvmStatic private external fun nativeBindLong(stmtPtr: Long, index: Int, value: Long)
@JvmStatic private external fun nativeBindDouble(stmtPtr: Long, index: Int, value: Double)
@JvmStatic private external fun nativeBindNull(stmtPtr: Long, index: Int)
@JvmStatic private external fun nativeColumnCount(stmtPtr: Long): Int
@JvmStatic private external fun nativeColumnName(stmtPtr: Long, col: Int): String
@JvmStatic private external fun nativeColumnType(stmtPtr: Long, col: Int): Int
@JvmStatic private external fun nativeColumnString(stmtPtr: Long, col: Int): String
@JvmStatic private external fun nativeColumnLong(stmtPtr: Long, col: Int): Long
@JvmStatic private external fun nativeColumnDouble(stmtPtr: Long, col: Int): Double
@JvmStatic private external fun nativeReset(stmtPtr: Long)
@JvmStatic private external fun nativeLastInsertRowId(dbPtr: Long): Long
@JvmStatic private external fun nativeChanges(dbPtr: Long): Int
private const val SQLITE_ROW = 100
private const val SQLITE_INTEGER = 1
private const val SQLITE_FLOAT = 2
private const val SQLITE_TEXT = 3
private const val SQLITE_NULL = 5
init {
System.loadLibrary("fips_jni")
}
}
override fun compileStatement(sql: String): SupportSQLiteStatement =
FipsStatement(dbPtr, sql)
override fun beginTransaction() {
nativeExec(dbPtr, "BEGIN EXCLUSIVE")
transactionActive = true
transactionSuccessful = false
}
override fun beginTransactionNonExclusive() {
nativeExec(dbPtr, "BEGIN DEFERRED")
transactionActive = true
transactionSuccessful = false
}
override fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener) =
beginTransaction()
override fun beginTransactionWithListenerNonExclusive(transactionListener: SQLiteTransactionListener) =
beginTransactionNonExclusive()
override fun endTransaction() {
if (transactionActive) {
if (transactionSuccessful) {
nativeExec(dbPtr, "COMMIT")
} else {
nativeExec(dbPtr, "ROLLBACK")
}
transactionActive = false
transactionSuccessful = false
}
}
override fun setTransactionSuccessful() {
transactionSuccessful = true
}
override fun inTransaction(): Boolean = transactionActive
override val isDbLockedByCurrentThread: Boolean get() = true
override fun yieldIfContendedSafely(): Boolean = false
override fun yieldIfContendedSafely(sleepAfterYieldDelayMillis: Long): Boolean = false
override var version: Int
get() {
val cursor = query("PRAGMA user_version")
return cursor.use { if (it.moveToFirst()) it.getInt(0) else 0 }
}
set(value) {
execSQL("PRAGMA user_version = $value")
}
override val maximumSize: Long get() = Long.MAX_VALUE
override fun setMaximumSize(numBytes: Long): Long = numBytes
override var pageSize: Long
get() {
val cursor = query("PRAGMA page_size")
return cursor.use { if (it.moveToFirst()) it.getLong(0) else 4096 }
}
set(value) {
execSQL("PRAGMA page_size = $value")
}
override fun query(query: String): Cursor = query(SimpleSQLiteQuery(query))
override fun query(query: String, bindArgs: Array<out Any?>): Cursor =
query(SimpleSQLiteQuery(query, bindArgs))
override fun query(query: SupportSQLiteQuery): Cursor =
query(query, null)
override fun query(query: SupportSQLiteQuery, cancellationSignal: CancellationSignal?): Cursor {
val stmtPtr = nativePrepare(dbPtr, query.sql)
try {
query.bindTo(FipsProgram(stmtPtr))
val colCount = nativeColumnCount(stmtPtr)
val colNames = Array(colCount) { nativeColumnName(stmtPtr, it) }
val cursor = MatrixCursor(colNames)
while (nativeStep(stmtPtr) == SQLITE_ROW) {
val row = Array<Any?>(colCount) { col ->
when (nativeColumnType(stmtPtr, col)) {
SQLITE_INTEGER -> nativeColumnLong(stmtPtr, col)
SQLITE_FLOAT -> nativeColumnDouble(stmtPtr, col)
SQLITE_TEXT -> nativeColumnString(stmtPtr, col)
SQLITE_NULL -> null
else -> nativeColumnString(stmtPtr, col)
}
}
cursor.addRow(row)
}
return cursor
} finally {
nativeFinalize(stmtPtr)
}
}
override fun insert(table: String, conflictAlgorithm: Int, values: ContentValues): Long {
val cols = values.keySet().joinToString(",")
val placeholders = values.keySet().joinToString(",") { "?" }
val conflict = when (conflictAlgorithm) {
5 -> "OR REPLACE"
4 -> "OR IGNORE"
3 -> "OR ABORT"
2 -> "OR ROLLBACK"
1 -> "OR FAIL"
else -> ""
}
val sql = "INSERT $conflict INTO $table ($cols) VALUES ($placeholders)"
val stmtPtr = nativePrepare(dbPtr, sql)
try {
var i = 1
for (key in values.keySet()) {
bindValue(stmtPtr, i++, values.get(key))
}
nativeStep(stmtPtr)
return nativeLastInsertRowId(dbPtr)
} finally {
nativeFinalize(stmtPtr)
}
}
override fun update(table: String, conflictAlgorithm: Int, values: ContentValues,
whereClause: String?, whereArgs: Array<out Any?>?): Int {
val sets = values.keySet().joinToString(",") { "$it=?" }
val where = if (whereClause != null) " WHERE $whereClause" else ""
val sql = "UPDATE $table SET $sets$where"
val stmtPtr = nativePrepare(dbPtr, sql)
try {
var i = 1
for (key in values.keySet()) {
bindValue(stmtPtr, i++, values.get(key))
}
whereArgs?.forEach { bindValue(stmtPtr, i++, it) }
nativeStep(stmtPtr)
return nativeChanges(dbPtr)
} finally {
nativeFinalize(stmtPtr)
}
}
override fun delete(table: String, whereClause: String?, whereArgs: Array<out Any?>?): Int {
val where = if (whereClause != null) " WHERE $whereClause" else ""
val sql = "DELETE FROM $table$where"
val stmtPtr = nativePrepare(dbPtr, sql)
try {
var i = 1
whereArgs?.forEach { bindValue(stmtPtr, i++, it) }
nativeStep(stmtPtr)
return nativeChanges(dbPtr)
} finally {
nativeFinalize(stmtPtr)
}
}
override fun execSQL(sql: String) = nativeExec(dbPtr, sql)
override fun execSQL(sql: String, bindArgs: Array<out Any?>) {
val stmtPtr = nativePrepare(dbPtr, sql)
try {
var i = 1
bindArgs.forEach { bindValue(stmtPtr, i++, it) }
nativeStep(stmtPtr)
} finally {
nativeFinalize(stmtPtr)
}
}
override val isReadOnly: Boolean get() = false
override val isOpen: Boolean get() = dbPtr != 0L
override fun needUpgrade(newVersion: Int): Boolean = version < newVersion
override val path: String get() = dbPath
override fun setLocale(locale: Locale) {}
override fun setMaxSqlCacheSize(cacheSize: Int) {}
override fun setForeignKeyConstraintsEnabled(enabled: Boolean) {
execSQL("PRAGMA foreign_keys = ${if (enabled) "ON" else "OFF"}")
}
override fun enableWriteAheadLogging(): Boolean {
execSQL("PRAGMA journal_mode = WAL")
return true
}
override fun disableWriteAheadLogging() {
execSQL("PRAGMA journal_mode = DELETE")
}
override val isWriteAheadLoggingEnabled: Boolean get() = false
override val attachedDbs: List<Pair<String, String>>? get() = null
override val isDatabaseIntegrityOk: Boolean
get() {
val cursor = query("PRAGMA integrity_check")
return cursor.use { it.moveToFirst() && it.getString(0) == "ok" }
}
override fun close() {
nativeClose(dbPtr)
}
private fun bindValue(stmtPtr: Long, index: Int, value: Any?) {
when (value) {
null -> nativeBindNull(stmtPtr, index)
is Long -> nativeBindLong(stmtPtr, index, value)
is Int -> nativeBindLong(stmtPtr, index, value.toLong())
is Double -> nativeBindDouble(stmtPtr, index, value)
is Float -> nativeBindDouble(stmtPtr, index, value.toDouble())
is String -> nativeBindString(stmtPtr, index, value)
is Boolean -> nativeBindLong(stmtPtr, index, if (value) 1 else 0)
is ByteArray -> nativeBindString(stmtPtr, index, String(value))
else -> nativeBindString(stmtPtr, index, value.toString())
}
}
private class FipsProgram(private val stmtPtr: Long) : SupportSQLiteProgram {
override fun bindNull(index: Int) = nativeBindNull(stmtPtr, index)
override fun bindLong(index: Int, value: Long) = nativeBindLong(stmtPtr, index, value)
override fun bindDouble(index: Int, value: Double) = nativeBindDouble(stmtPtr, index, value)
override fun bindString(index: Int, value: String) = nativeBindString(stmtPtr, index, value)
override fun bindBlob(index: Int, value: ByteArray) =
nativeBindString(stmtPtr, index, String(value))
override fun clearBindings() = nativeReset(stmtPtr)
override fun close() {}
}
private class FipsStatement(
private val dbPtr: Long,
private val sql: String,
) : SupportSQLiteStatement {
private var stmtPtr: Long = nativePrepare(dbPtr, sql)
override fun execute() {
nativeStep(stmtPtr)
nativeReset(stmtPtr)
}
override fun executeUpdateDelete(): Int {
nativeStep(stmtPtr)
val changes = nativeChanges(dbPtr)
nativeReset(stmtPtr)
return changes
}
override fun executeInsert(): Long {
nativeStep(stmtPtr)
val rowId = nativeLastInsertRowId(dbPtr)
nativeReset(stmtPtr)
return rowId
}
override fun simpleQueryForLong(): Long {
val rc = nativeStep(stmtPtr)
val v = if (rc == SQLITE_ROW) nativeColumnLong(stmtPtr, 0) else 0L
nativeReset(stmtPtr)
return v
}
override fun simpleQueryForString(): String? {
val rc = nativeStep(stmtPtr)
val v = if (rc == SQLITE_ROW) nativeColumnString(stmtPtr, 0) else null
nativeReset(stmtPtr)
return v
}
override fun bindNull(index: Int) = nativeBindNull(stmtPtr, index)
override fun bindLong(index: Int, value: Long) = nativeBindLong(stmtPtr, index, value)
override fun bindDouble(index: Int, value: Double) = nativeBindDouble(stmtPtr, index, value)
override fun bindString(index: Int, value: String) = nativeBindString(stmtPtr, index, value)
override fun bindBlob(index: Int, value: ByteArray) =
nativeBindString(stmtPtr, index, String(value))
override fun clearBindings() = nativeReset(stmtPtr)
override fun close() {
if (stmtPtr != 0L) {
nativeFinalize(stmtPtr)
stmtPtr = 0
}
}
}
}
@@ -0,0 +1,84 @@
package com.fips.sqlcipher
import android.database.SQLException
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
class FipsSQLiteOpenHelperFactory(
private val key: String,
) : SupportSQLiteOpenHelper.Factory {
override fun create(configuration: SupportSQLiteOpenHelper.Configuration): SupportSQLiteOpenHelper =
FipsSQLiteOpenHelper(configuration, key)
}
private class FipsSQLiteOpenHelper(
private val configuration: SupportSQLiteOpenHelper.Configuration,
private val key: String,
) : SupportSQLiteOpenHelper {
private var db: FipsDatabase? = null
private var opened = false
override val databaseName: String?
get() = configuration.name
override fun setWriteAheadLoggingEnabled(enabled: Boolean) {
db?.let {
if (enabled) it.enableWriteAheadLogging()
else it.disableWriteAheadLogging()
}
}
override val writableDatabase: SupportSQLiteDatabase
get() = getOrCreateDatabase()
override val readableDatabase: SupportSQLiteDatabase
get() = getOrCreateDatabase()
@Synchronized
private fun getOrCreateDatabase(): FipsDatabase {
db?.let { return it }
val name = configuration.name
?: throw SQLException("In-memory databases not supported with FIPS SQLCipher")
val context = configuration.context
val dbFile = context.getDatabasePath(name)
dbFile.parentFile?.mkdirs()
val isNew = !dbFile.exists()
val database = FipsDatabase.open(dbFile.absolutePath, key)
if (isNew) {
configuration.callback.onCreate(database)
} else {
val currentVersion = database.version
val targetVersion = configuration.callback.version
when {
currentVersion == 0 -> {
configuration.callback.onCreate(database)
}
currentVersion < targetVersion -> {
configuration.callback.onUpgrade(database, currentVersion, targetVersion)
}
currentVersion > targetVersion -> {
configuration.callback.onDowngrade(database, currentVersion, targetVersion)
}
}
}
database.version = configuration.callback.version
configuration.callback.onOpen(database)
db = database
opened = true
return database
}
override fun close() {
db?.close()
db = null
opened = false
}
}
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# build_jni.sh -- Cross-compile libfips_jni.so for each Android ABI.
#
# Requires: ANDROID_NDK_ROOT set, dist/<abi>/lib/libcrypto.so present.
# Outputs: dist/<abi>/lib/libfips_jni.so
# ---------------------------------------------------------------------------
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DIST_ROOT="${REPO_ROOT}/dist"
ABIS="${ABIS:-arm64-v8a x86_64}"
NDK="${ANDROID_NDK_ROOT:?Set ANDROID_NDK_ROOT}"
TOOLCHAIN="${NDK}/build/cmake/android.toolchain.cmake"
if [ ! -f "${TOOLCHAIN}" ]; then
echo "ERROR: NDK toolchain not found: ${TOOLCHAIN}" >&2
exit 1
fi
for abi in ${ABIS}; do
crypto_lib="${DIST_ROOT}/${abi}/lib/libcrypto.so"
sqlcipher_lib="${DIST_ROOT}/${abi}/lib/libsqlcipher.so"
if [ ! -f "${crypto_lib}" ]; then
echo "ERROR: ${crypto_lib} not found. Build ${abi} first." >&2
exit 1
fi
if [ ! -f "${sqlcipher_lib}" ]; then
echo "ERROR: ${sqlcipher_lib} not found. Build ${abi} first." >&2
exit 1
fi
build_dir="${REPO_ROOT}/build/_jni_${abi}"
rm -rf "${build_dir}"
mkdir -p "${build_dir}"
echo "Building libfips_jni.so for ${abi}..."
cmake -S "${SCRIPT_DIR}/jni" -B "${build_dir}" \
-DCMAKE_TOOLCHAIN_FILE="${TOOLCHAIN}" \
-DANDROID_ABI="${abi}" \
-DANDROID_PLATFORM=android-24 \
-DCRYPTO_LIB="${crypto_lib}" \
-DSQLCIPHER_LIB="${sqlcipher_lib}" \
-DDIST_INCLUDE="${DIST_ROOT}/${abi}/include" \
-DCMAKE_BUILD_TYPE=Release \
-G "Unix Makefiles" >/dev/null
cmake --build "${build_dir}" --config Release -j"$(sysctl -n hw.ncpu 2>/dev/null || nproc)" >/dev/null
cp "${build_dir}/libfips_jni.so" "${DIST_ROOT}/${abi}/lib/"
echo " -> ${DIST_ROOT}/${abi}/lib/libfips_jni.so"
rm -rf "${build_dir}"
done
echo "JNI bridge built for: ${ABIS}"
+24
View File
@@ -0,0 +1,24 @@
cmake_minimum_required(VERSION 3.22)
project(fips_jni C)
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,max-page-size=16384")
set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../src")
set(INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../include")
set(DIST_INCLUDE_DIR "${DIST_INCLUDE}" CACHE PATH "dist/<abi>/include with OpenSSL headers")
add_library(crypto SHARED IMPORTED)
set_target_properties(crypto PROPERTIES IMPORTED_LOCATION "${CRYPTO_LIB}")
add_library(sqlcipher SHARED IMPORTED)
set_target_properties(sqlcipher PROPERTIES IMPORTED_LOCATION "${SQLCIPHER_LIB}")
add_library(fips_jni SHARED
"${SRC_DIR}/fips_init.c"
"${SRC_DIR}/fips_init_android.c"
"${SRC_DIR}/fips_init_jni.c"
"${SRC_DIR}/fips_db_jni.c"
)
target_include_directories(fips_jni PRIVATE "${INCLUDE_DIR}" "${DIST_INCLUDE_DIR}" "${DIST_INCLUDE_DIR}/sqlcipher")
target_compile_definitions(fips_jni PRIVATE SQLITE_HAS_CODEC)
target_link_libraries(fips_jni PRIVATE crypto sqlcipher log)
+70 -32
View File
@@ -5,15 +5,16 @@
# Produces: dist/fips-sqlcipher.aar
#
# Contents:
# jni/arm64-v8a/{libcrypto.so, libssl.so, libsqlcipher.so, libfips.so}
# jni/x86_64/{libcrypto.so, libssl.so, libsqlcipher.so, libfips.so}
# assets/fips/{openssl.cnf, fipsmodule.cnf}
# jni/<abi>/{libcrypto.so, libssl.so, libsqlcipher.so, libfips.so, libfips_jni.so}
# assets/fips/<abi>/fipsmodule.cnf (per-ABI HMAC fingerprints)
# assets/native/include/ (fips_init.h header)
# assets/native/src/ (fips_init.c, fips_init_android.c sources)
# classes.jar (compiled FIPSSQLCipher.kt)
# AndroidManifest.xml
# R.txt (empty, required by AGP)
# proguard.txt (keep rules for JNI)
# R.txt, proguard.txt
#
# Usage:
# ./packaging/package_aar.sh [--abis "arm64-v8a x86_64"]
# ./packaging/package_aar.sh [--abis "arm64-v8a x86_64"] [--skip-jni] [--skip-kotlin]
# ---------------------------------------------------------------------------
set -euo pipefail
@@ -23,11 +24,14 @@ DIST_ROOT="${REPO_ROOT}/dist"
ABIS="${ABIS:-arm64-v8a x86_64}"
AAR_NAME="fips-sqlcipher.aar"
SKIP_JNI=false
SKIP_KOTLIN=false
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--abis) ABIS="$2"; shift 2 ;;
--skip-jni) SKIP_JNI=true; shift ;;
--skip-kotlin) SKIP_KOTLIN=true; shift ;;
*) echo "Unknown arg: $1" >&2; exit 1 ;;
esac
done
@@ -36,6 +40,14 @@ STAGING="${DIST_ROOT}/_aar_staging"
rm -rf "${STAGING}"
mkdir -p "${STAGING}"
# ---------------------------------------------------------------------------
# Build JNI bridge (libfips_jni.so) for each ABI
# ---------------------------------------------------------------------------
if [ "${SKIP_JNI}" = false ]; then
echo "==> Building JNI bridge..."
ABIS="${ABIS}" "${SCRIPT_DIR}/build_jni.sh"
fi
# ---------------------------------------------------------------------------
# Stage JNI libs
# ---------------------------------------------------------------------------
@@ -49,7 +61,7 @@ for abi in ${ABIS}; do
jni_dst="${STAGING}/jni/${abi}"
mkdir -p "${jni_dst}"
for lib in libcrypto.so libssl.so libsqlcipher.so; do
for lib in libcrypto.so libssl.so libsqlcipher.so libfips_jni.so; do
if [ -f "${abi_dir}/lib/${lib}" ]; then
cp "${abi_dir}/lib/${lib}" "${jni_dst}/"
fi
@@ -61,18 +73,32 @@ for abi in ${ABIS}; do
done
# ---------------------------------------------------------------------------
# Stage FIPS config assets (use first ABI's configs as canonical)
# Stage per-ABI fipsmodule.cnf (each ABI has a unique HMAC fingerprint)
# ---------------------------------------------------------------------------
first_abi="${ABIS%% *}"
fips_src="${DIST_ROOT}/${first_abi}/fips"
mkdir -p "${STAGING}/assets/fips"
for abi in ${ABIS}; do
fips_src="${DIST_ROOT}/${abi}/fips"
mkdir -p "${STAGING}/assets/fips/${abi}"
if [ -f "${fips_src}/openssl.cnf" ]; then
cp "${fips_src}/openssl.cnf" "${STAGING}/assets/fips/"
fi
if [ -f "${fips_src}/fipsmodule.cnf" ]; then
cp "${fips_src}/fipsmodule.cnf" "${STAGING}/assets/fips/"
fi
if [ -f "${fips_src}/fipsmodule.cnf" ]; then
cp "${fips_src}/fipsmodule.cnf" "${STAGING}/assets/fips/${abi}/"
else
build_cnf="${REPO_ROOT}/build/${abi}/openssl-3.0.8/install/ssl/fipsmodule.cnf"
if [ -f "${build_cnf}" ]; then
cp "${build_cnf}" "${STAGING}/assets/fips/${abi}/"
else
echo "WARNING: fipsmodule.cnf not found for ${abi}" >&2
fi
fi
done
# ---------------------------------------------------------------------------
# Stage C/C++ source files (for developers who need custom native integration)
# ---------------------------------------------------------------------------
native_dst="${STAGING}/assets/native"
mkdir -p "${native_dst}/include" "${native_dst}/src"
cp "${REPO_ROOT}/include/fips_init.h" "${native_dst}/include/"
cp "${REPO_ROOT}/src/fips_init.c" "${native_dst}/src/"
cp "${REPO_ROOT}/src/fips_init_android.c" "${native_dst}/src/"
# ---------------------------------------------------------------------------
# AndroidManifest.xml
@@ -91,18 +117,31 @@ EOF
touch "${STAGING}/R.txt"
# ---------------------------------------------------------------------------
# proguard.txt -- keep JNI entry points
# proguard.txt
# ---------------------------------------------------------------------------
cat > "${STAGING}/proguard.txt" <<'EOF'
-keep class com.fips.sqlcipher.** { *; }
EOF
# ---------------------------------------------------------------------------
# classes.jar (empty, no Java code in this AAR)
# classes.jar (compiled FIPSSQLCipher.kt)
# ---------------------------------------------------------------------------
mkdir -p "${STAGING}/_classes"
(cd "${STAGING}/_classes" && jar cf "${STAGING}/classes.jar" .)
rm -rf "${STAGING}/_classes"
if [ "${SKIP_KOTLIN}" = false ]; then
echo "==> Compiling FIPSSQLCipher.kt..."
ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}" \
"${SCRIPT_DIR}/android-lib/gradlew" \
-p "${SCRIPT_DIR}/android-lib" \
assembleRelease -q
unzip -o -q \
"${SCRIPT_DIR}/android-lib/build/outputs/aar/fips-sqlcipher-lib-release.aar" \
classes.jar -d "${STAGING}"
echo " -> classes.jar ($(wc -c < "${STAGING}/classes.jar" | tr -d ' ') bytes)"
else
mkdir -p "${STAGING}/_classes"
(cd "${STAGING}/_classes" && jar cf "${STAGING}/classes.jar" .)
rm -rf "${STAGING}/_classes"
fi
# ---------------------------------------------------------------------------
# Zip into AAR
@@ -116,13 +155,12 @@ rm -rf "${STAGING}"
echo ""
echo "AAR packaged: ${AAR_OUT}"
echo " ABIs: ${ABIS}"
unzip -l "${AAR_OUT}" | grep -E '(\.so|\.cnf|Manifest|proguard|classes)' || true
unzip -l "${AAR_OUT}" | grep -E '(\.so|\.cnf|\.jar|\.c$|\.h$|Manifest|proguard)' || true
echo ""
echo "Integration (app/build.gradle.kts):"
echo " dependencies {"
echo " implementation(files(\"path/to/fips-sqlcipher.aar\"))"
echo " }"
echo " android.packaging.jniLibs {"
echo " useLegacyPackaging = false"
echo " keepDebugSymbols += setOf(\"**/libfips.so\")"
echo " }"
echo "Integration:"
echo " 1. Copy fips-sqlcipher.aar to app/libs/"
echo " 2. build.gradle.kts:"
echo " dependencies { implementation(files(\"libs/fips-sqlcipher.aar\")) }"
echo " android.packaging.jniLibs.keepDebugSymbols += setOf(\"**/libfips.so\")"
echo " 3. Application.onCreate():"
echo " FIPSSQLCipher.init(this)"
+7
View File
@@ -2,6 +2,7 @@ plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.devtools.ksp")
}
android {
@@ -72,4 +73,10 @@ dependencies {
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.material3:material3")
// Room database
val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
}
@@ -0,0 +1,5 @@
[fips_sect]
activate = 1
conditional-errors = 1
security-checks = 1
module-mac = D1:A5:90:A4:4B:2B:D4:B4:51:FC:95:18:08:69:85:74:E0:0A:F6:3D:05:69:7F:D0:63:44:B1:BC:B8:45:32:01
@@ -0,0 +1,5 @@
[fips_sect]
activate = 1
conditional-errors = 1
security-checks = 1
module-mac = C4:E1:A6:65:BB:AB:87:91:8E:64:31:88:9D:D7:9D:55:6F:DD:5D:17:4B:5C:3B:D1:E5:16:AA:5D:F0:98:94:25
@@ -7,23 +7,14 @@ set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,max-page-size
# Pre-built libs staged by bootstrap.sh (extracts from the AAR)
set(JNILIBS "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}")
# Import shared libraries from the AAR
add_library(crypto SHARED IMPORTED)
set_target_properties(crypto PROPERTIES IMPORTED_LOCATION "${JNILIBS}/libcrypto.so")
add_library(sqlcipher SHARED IMPORTED)
set_target_properties(sqlcipher PROPERTIES IMPORTED_LOCATION "${JNILIBS}/libsqlcipher.so")
# Headers: OpenSSL + sqlite3 staged from the dist, plus our fips_init.h
set(STAGED_INCLUDE "${CMAKE_SOURCE_DIR}/include")
# 1. appenv: setenv-only shim. No OpenSSL deps so it can be loaded BEFORE
# libcrypto/libfips, letting us set OPENSSL_CONF before the FIPS provider
# constructor runs.
add_library(appenv SHARED appenv.c)
target_link_libraries(appenv PRIVATE log)
# 2. Main JNI bridge: FIPS checks + SQLCipher operations.
add_library(fipsdemo SHARED
jni_bridge.c
fips_init.c
-17
View File
@@ -1,17 +0,0 @@
// appenv.c -- minimal setenv shim, deliberately free of OpenSSL deps.
// Loaded BEFORE libcrypto so that OPENSSL_CONF, FIPSMODULE_CNF, and
// OPENSSL_MODULES are in the process environment before OpenSSL initializes.
#include <jni.h>
#include <stdlib.h>
JNIEXPORT jint JNICALL
Java_com_example_fipsdemo_FipsNative_setenv(JNIEnv *env, jclass cls,
jstring jname, jstring jvalue) {
(void)cls;
const char *n = (*env)->GetStringUTFChars(env, jname, NULL);
const char *v = (*env)->GetStringUTFChars(env, jvalue, NULL);
int rc = setenv(n, v, 1);
(*env)->ReleaseStringUTFChars(env, jname, n);
(*env)->ReleaseStringUTFChars(env, jvalue, v);
return rc;
}
+32 -19
View File
@@ -47,45 +47,58 @@ fips_init_status_t fips_init(const char *module_dir, const char *conf_path) {
return FIPS_INIT_OK;
}
// Point OPENSSL_MODULES to the directory containing the FIPS provider binary
if (module_dir && module_dir[0]) {
#ifdef __ANDROID__
// Android: libfips.so
char probe[4096];
snprintf(probe, sizeof(probe), "%s/libfips.so", module_dir);
if (ACCESS(probe, F_OK) != 0) {
LOGE("FIPS module not found: %s\n", probe);
return FIPS_INIT_ERR_MODULE_MISSING;
}
const char *module_name = "fips.so";
#elif defined(__APPLE__)
const char *module_name = "fips.dylib";
#else
// iOS: fips.dylib
const char *module_name = "fips.so";
#endif
char probe[4096];
snprintf(probe, sizeof(probe), "%s/fips.dylib", module_dir);
snprintf(probe, sizeof(probe), "%s/%s", module_dir, module_name);
if (ACCESS(probe, F_OK) != 0) {
LOGE("FIPS module not found: %s\n", probe);
return FIPS_INIT_ERR_MODULE_MISSING;
}
#endif
setenv("OPENSSL_MODULES", module_dir, 1);
}
// Set OPENSSL_CONF if a config path was provided
// On Android, ossl_safe_getenv() returns NULL when AT_SECURE is set
// (common for app processes), so neither OPENSSL_CONF nor OPENSSL_MODULES
// env vars are visible to libcrypto. We set the search path first, then
// load config via OSSL_LIB_CTX_load_config which bypasses the env vars.
if (module_dir && module_dir[0]) {
OSSL_PROVIDER_set_default_search_path(NULL, module_dir);
}
if (conf_path && conf_path[0]) {
if (ACCESS(conf_path, F_OK) != 0) {
LOGE("Config not found: %s\n", conf_path);
return FIPS_INIT_ERR_CONF_MISSING;
}
setenv("OPENSSL_CONF", conf_path, 1);
if (OSSL_LIB_CTX_load_config(NULL, conf_path) != 1) {
unsigned long err;
while ((err = ERR_get_error()) != 0) {
char buf[256];
ERR_error_string_n(err, buf, sizeof(buf));
LOGE(" Config load error: %s\n", buf);
}
LOGE("Failed to load config: %s\n", conf_path);
return FIPS_INIT_ERR_CONF_MISSING;
}
}
// Load the FIPS provider. This triggers the incore HMAC-SHA256 integrity
// check followed by the Known Answer Tests (KATs). If the module was
// modified post-build (stripped, compressed, re-signed incorrectly), this
// call will fail.
g_fips_provider = OSSL_PROVIDER_load(NULL, "fips");
if (!g_fips_provider) {
unsigned long err = ERR_peek_last_error();
LOGE("FIPS provider load failed: %s\n", ERR_reason_error_string(err));
unsigned long err;
while ((err = ERR_get_error()) != 0) {
char buf[256];
ERR_error_string_n(err, buf, sizeof(buf));
LOGE(" OpenSSL error: %s\n", buf);
}
LOGE("FIPS provider load failed. OPENSSL_MODULES=%s\n",
getenv("OPENSSL_MODULES") ? getenv("OPENSSL_MODULES") : "(unset)");
return FIPS_INIT_ERR_PROVIDER_LOAD;
}
+7 -22
View File
@@ -25,37 +25,22 @@ typedef enum {
FIPS_INIT_ERR_PROPERTY_SET,
} fips_init_status_t;
// Human-readable description of a status code.
const char *fips_init_status_str(fips_init_status_t status);
// Initialize OpenSSL with FIPS provider from the given paths.
//
// module_dir: directory containing libfips.so (Android) or fips.dylib (iOS)
// conf_path: path to openssl.cnf that .includes fipsmodule.cnf
// (NULL = use OPENSSL_CONF env var, or generate minimal config)
//
// On Android, call this AFTER extracting assets/fips/* to the app's filesDir.
// On iOS, pass the path within the app bundle where fips.dylib is embedded.
//
// Returns FIPS_INIT_OK on success. On failure, the FIPS provider is NOT active
// and all crypto operations will fail (which is the correct behavior — you MUST
// NOT proceed with plaintext fallback under FIPS requirements).
// Initialize OpenSSL with FIPS provider.
// module_dir: directory containing fips.so (Android) or fips.dylib (iOS)
// conf_path: path to openssl.cnf (NULL = use OPENSSL_CONF env var)
fips_init_status_t fips_init(const char *module_dir, const char *conf_path);
// Re-run the FIPS self-test on demand (e.g., after app resume from background).
// The provider must already be loaded via fips_init().
// Returns 1 on success, 0 on failure.
int fips_self_test_rerun(void);
// Query whether the FIPS provider is currently active in the default context.
int fips_provider_is_active(void);
#ifdef __ANDROID__
// Android convenience: takes Context.getFilesDir() and
// ApplicationInfo.nativeLibraryDir paths. Handles OPENSSL_MODULES and
// FIPSMODULE_CNF env setup before calling fips_init().
// Android convenience wrapper.
// files_dir: Context.getFilesDir() — expects fips/openssl.cnf underneath
// modules_dir: directory containing fips.so (extracted by FIPSSQLCipher.kt)
fips_init_status_t fips_init_android(const char *files_dir,
const char *native_lib_dir);
const char *modules_dir);
#endif
#if defined(__APPLE__) && !defined(__ANDROID__)
@@ -1,61 +1,34 @@
// SPDX-License-Identifier: Apache-2.0
// Android-specific FIPS initialization helper.
// Android-specific FIPS initialization.
//
// On Android, the FIPS provider (libfips.so) is shipped inside the APK's
// jniLibs/<abi>/ and loaded via System.loadLibrary. The openssl.cnf is
// shipped in assets/fips/ and must be extracted to the app's internal storage
// before OpenSSL reads it.
//
// This file provides fips_init_android() which takes the app's files directory
// (Context.getFilesDir()) and handles:
// 1. Pointing OPENSSL_MODULES to the nativeLibraryDir (where Android unpacks .so)
// 2. Generating fipsmodule.cnf via the incore HMAC (on first run)
// 3. Calling fips_init() with the resolved paths
//
// The fipsmodule.cnf generation is equivalent to running:
// openssl fipsinstall -module libfips.so -out fipsmodule.cnf
// but done programmatically since we can't run the openssl CLI on device.
// fips_init_android() resolves the naming mismatch between Android's "libfips.so"
// (required by the jniLibs convention) and OpenSSL's "fips.so" (provider name
// convention), then delegates to fips_init() for the actual provider activation.
#ifdef __ANDROID__
#include "fips_init.h"
#include <openssl/provider.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/params.h>
#include <openssl/core_names.h>
#include <android/log.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#define LOG_TAG "fips_init_android"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
fips_init_status_t fips_init_android(const char *files_dir,
const char *native_lib_dir) {
if (!files_dir || !native_lib_dir) {
const char *modules_dir) {
if (!files_dir || !modules_dir) {
return FIPS_INIT_ERR_MODULE_MISSING;
}
char conf_dir[4096];
char conf_path[4096];
char module_cnf_path[4096];
snprintf(conf_dir, sizeof(conf_dir), "%s/fips", files_dir);
snprintf(conf_path, sizeof(conf_path), "%s/fips/openssl.cnf", files_dir);
snprintf(module_cnf_path, sizeof(module_cnf_path), "%s/fips/fipsmodule.cnf", files_dir);
// FIPSMODULE_CNF is no longer required by the minimal openssl.cnf shipped
// in the AAR. The FIPS provider is loaded programmatically below.
// Set it anyway for compatibility with custom configs that may .include it.
setenv("FIPSMODULE_CNF", module_cnf_path, 1);
// The native_lib_dir is where Android extracts jniLibs .so files at install.
// This is typically /data/app/<pkg>/lib/<abi>/ and contains libfips.so.
return fips_init(native_lib_dir, conf_path);
return fips_init(modules_dir, conf_path);
}
#endif // __ANDROID__
@@ -1,31 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// C/C++ interop header for FIPS-SQLCipher builds.
// Wraps OpenSSL and SQLCipher headers in extern "C" to prevent C++ name
// mangling and avoids pulling libc++ symbols into the FIPS module boundary.
#ifndef FIPS_SQLCIPHER_H
#define FIPS_SQLCIPHER_H
#ifdef __cplusplus
extern "C" {
#endif
#include <openssl/provider.h>
#include <openssl/crypto.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <sqlite3.h>
#ifdef __cplusplus
}
#endif
// When compiling C++ translation units that link against the FIPS provider:
// - Do NOT pass -fno-rtti or -fno-exceptions to the OpenSSL/SQLCipher
// object files themselves (they are pure C).
// - Your C++ code CAN use any RTTI/exception settings freely because the
// FIPS "incore" HMAC covers only the provider's .text/.rodata, not yours.
// - Avoid ODR violations: do not statically link libc++ into a shared lib
// that also dlopen()s libfips.so on Android. Use the NDK's shared libc++
// (the default) or link everything statically.
#endif // FIPS_SQLCIPHER_H
@@ -25,15 +25,15 @@ static jstring jstr(JNIEnv *env, const char *s) {
JNIEXPORT jint JNICALL
Java_com_example_fipsdemo_FipsNative_init(JNIEnv *env, jclass cls,
jstring jfilesDir,
jstring jnativeLibDir) {
jstring jmodulesDir) {
const char *files = (*env)->GetStringUTFChars(env, jfilesDir, NULL);
const char *natv = (*env)->GetStringUTFChars(env, jnativeLibDir, NULL);
const char *mods = (*env)->GetStringUTFChars(env, jmodulesDir, NULL);
fips_init_status_t rc = fips_init_android(files, natv);
fips_init_status_t rc = fips_init_android(files, mods);
LOGI("fips_init_android: %s", fips_init_status_str(rc));
(*env)->ReleaseStringUTFChars(env, jfilesDir, files);
(*env)->ReleaseStringUTFChars(env, jnativeLibDir, natv);
(*env)->ReleaseStringUTFChars(env, jmodulesDir, mods);
return (jint)rc;
}
@@ -1,56 +1,88 @@
package com.example.fipsdemo
import android.content.Context
import android.os.Build
import android.util.Log
import java.io.File
import java.util.zip.ZipFile
object FipsNative {
private const val TAG = "FipsNative"
fun loadLibraries(context: Context) {
// 1. Load appenv FIRST — no OpenSSL deps, just provides setenv().
System.loadLibrary("appenv")
// 2. Extract FIPS config from assets to internal storage.
val fipsDir = File(context.filesDir, "fips").apply { mkdirs() }
copyAsset(context, "fips/openssl.cnf", File(fipsDir, "openssl.cnf"))
val modulesDir = File(fipsDir, "modules").apply { mkdirs() }
val abi = Build.SUPPORTED_ABIS[0]
// 3. Set environment BEFORE loading libcrypto.
// OPENSSL_CONF points to minimal config (base provider only).
// OPENSSL_MODULES tells OpenSSL where to find libfips.so for
// programmatic loading via fips_init_android().
val nativeLibDir = context.applicationInfo.nativeLibraryDir
setenv("OPENSSL_CONF", File(fipsDir, "openssl.cnf").absolutePath)
setenv("OPENSSL_MODULES", nativeLibDir)
extractAsset(context, "fips/$abi/fipsmodule.cnf", File(fipsDir, "fipsmodule.cnf"))
generateOpenSslConf(fipsDir)
extractFipsModule(context, modulesDir)
// 4. Load crypto stack in dependency order.
// FIPS is NOT active yet — activated programmatically by init().
System.loadLibrary("crypto")
System.loadLibrary("sqlcipher")
// 5. Load our JNI bridge last.
System.loadLibrary("fipsdemo")
Log.i(TAG, "Libraries loaded; provider active=${providerActive()}")
val rc = init(context.filesDir.absolutePath, modulesDir.absolutePath)
if (rc != 0) {
Log.e(TAG, "fips_init_android returned $rc: ${initStatusMessage(rc)}")
} else {
Log.i(TAG, "FIPS provider loaded successfully")
}
System.loadLibrary("sqlcipher")
System.loadLibrary("fips_jni")
Log.i(TAG, "Libraries loaded, modules=${modulesDir.absolutePath}")
}
private fun copyAsset(context: Context, src: String, dst: File) {
if (dst.exists()) return
fun modulesDir(context: Context): String =
File(context.filesDir, "fips/modules").absolutePath
private fun generateOpenSslConf(fipsDir: File) {
val conf = File(fipsDir, "openssl.cnf")
val fipsModuleCnf = File(fipsDir, "fipsmodule.cnf")
if (conf.exists()) return
conf.writeText("""
|openssl_conf = openssl_init
|.include ${fipsModuleCnf.absolutePath}
|[openssl_init]
|providers = provider_sect
|[provider_sect]
|fips = fips_sect
|base = base_sect
|[base_sect]
|activate = 1
""".trimMargin() + "\n")
}
private fun extractAsset(context: Context, assetPath: String, target: File) {
if (target.exists()) return
try {
context.assets.open(src).use { input ->
dst.outputStream().use { input.copyTo(it) }
context.assets.open(assetPath).use { input ->
target.outputStream().use { input.copyTo(it) }
}
} catch (e: Exception) {
Log.w(TAG, "Asset $src not found", e)
Log.w(TAG, "Asset $assetPath not found", e)
}
}
// From appenv.so (loaded first, no crypto deps)
external fun setenv(name: String, value: String): Int
private fun extractFipsModule(context: Context, modulesDir: File) {
val target = File(modulesDir, "fips.so")
val abi = Build.SUPPORTED_ABIS[0]
ZipFile(context.applicationInfo.sourceDir).use { apk ->
val entry = apk.getEntry("lib/$abi/libfips.so")
?: error("libfips.so not found in APK for ABI $abi")
if (target.exists() && target.length() == entry.size) return
apk.getInputStream(entry).use { input ->
target.outputStream().use { input.copyTo(it) }
}
}
target.setReadable(true, false)
target.setExecutable(true, false)
Log.d(TAG, "Extracted fips.so (${target.length()} bytes) for $abi")
}
// From fipsdemo.so
external fun init(filesDir: String, nativeLibDir: String): Int
external fun init(filesDir: String, modulesDir: String): Int
external fun initStatusMessage(code: Int): String
external fun providerActive(): Boolean
external fun selfTest(): Boolean
@@ -17,6 +17,8 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.fipsdemo.db.AppDatabase
import com.example.fipsdemo.db.NoteEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -34,7 +36,7 @@ class MainActivity : ComponentActivity() {
) {
FipsDemoScreen(
filesDir = filesDir.absolutePath,
nativeLibDir = applicationInfo.nativeLibraryDir
modulesDir = FipsNative.modulesDir(this)
)
}
}
@@ -43,7 +45,7 @@ class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FipsDemoScreen(filesDir: String, nativeLibDir: String) {
fun FipsDemoScreen(filesDir: String, modulesDir: String) {
var initStatus by remember { mutableStateOf<String?>(null) }
var checks by remember { mutableStateOf<List<ComplianceCheck>>(emptyList()) }
var running by remember { mutableStateOf(false) }
@@ -51,7 +53,7 @@ fun FipsDemoScreen(filesDir: String, nativeLibDir: String) {
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
val rc = FipsNative.init(filesDir, nativeLibDir)
val rc = FipsNative.init(filesDir, modulesDir)
initStatus = if (rc == 0) "FIPS initialized"
else "INIT FAILED: ${FipsNative.initStatusMessage(rc)}"
}
@@ -66,80 +68,203 @@ fun FipsDemoScreen(filesDir: String, nativeLibDir: String) {
)
}
) { padding ->
Column(
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Init status card
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (initStatus?.startsWith("FIPS") == true)
Color(0xFF1B5E20) else Color(0xFFB71C1C)
)
) {
Text(
text = initStatus ?: "Initializing...",
modifier = Modifier.padding(16.dp),
color = Color.White,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp
)
item {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (initStatus?.startsWith("FIPS") == true)
Color(0xFF1B5E20) else Color(0xFFB71C1C)
)
) {
Text(
text = initStatus ?: "Initializing...",
modifier = Modifier.padding(16.dp),
color = Color.White,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Run suite button
Button(
onClick = {
running = true
scope.launch(Dispatchers.IO) {
val results = runComplianceSuite(filesDir)
withContext(Dispatchers.Main) {
checks = results
running = false
item {
Button(
onClick = {
running = true
scope.launch(Dispatchers.IO) {
val results = runComplianceSuite(filesDir)
withContext(Dispatchers.Main) {
checks = results
running = false
}
}
},
enabled = !running && initStatus != null,
modifier = Modifier.fillMaxWidth()
) {
if (running) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
color = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
}
},
enabled = !running && initStatus != null,
modifier = Modifier.fillMaxWidth()
) {
if (running) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
color = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
Text(if (running) "Running..." else "Run Compliance Suite")
}
Text(if (running) "Running..." else "Run Compliance Suite")
}
Spacer(modifier = Modifier.height(16.dp))
// Results summary
if (checks.isNotEmpty()) {
val passed = checks.count { it.passed }
val total = checks.size
Text(
text = "$passed / $total checks passed",
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = if (passed == total) Color(0xFF4CAF50) else Color(0xFFF44336)
)
Spacer(modifier = Modifier.height(12.dp))
}
// Results list
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(checks) { check ->
CheckCard(check)
item {
Text(
text = "$passed / $total checks passed",
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = if (passed == total) Color(0xFF4CAF50) else Color(0xFFF44336)
)
}
}
// Compliance check results
items(checks) { check ->
CheckCard(check)
}
// Room DB demo section
item {
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Room Database Demo",
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
Text(
text = "SQLCipher-backed Room CRUD operations",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
item {
RoomDemoSection(filesDir)
}
}
}
}
@Composable
fun RoomDemoSection(filesDir: String) {
val context = androidx.compose.ui.platform.LocalContext.current
val db = remember { AppDatabase.get(context) }
val scope = rememberCoroutineScope()
var notes by remember { mutableStateOf<List<NoteEntity>>(emptyList()) }
var noteCount by remember { mutableIntStateOf(0) }
var status by remember { mutableStateOf("Ready") }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = {
scope.launch(Dispatchers.IO) {
val n = db.noteDao().count() + 1
db.noteDao().insert(
NoteEntity(
title = "Note #$n",
body = "Created via Room + FIPS SQLCipher"
)
)
val all = db.noteDao().getAll()
withContext(Dispatchers.Main) {
notes = all
noteCount = all.size
status = "Inserted note #$n"
}
}
},
modifier = Modifier.weight(1f)
) {
Text("Add Note")
}
OutlinedButton(
onClick = {
scope.launch(Dispatchers.IO) {
db.noteDao().deleteAll()
withContext(Dispatchers.Main) {
notes = emptyList()
noteCount = 0
status = "All notes deleted"
}
}
},
modifier = Modifier.weight(1f)
) {
Text("Clear All")
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(12.dp)) {
Text(
text = "$noteCount notes in database",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp
)
Text(
text = status,
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = Color(0xFF81C784)
)
}
}
notes.take(5).forEach { note ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(
text = note.title,
fontWeight = FontWeight.Medium,
fontSize = 14.sp
)
Text(
text = note.body,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "id=${note.id}, ts=${note.createdAt}",
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
}
}
}
if (notes.size > 5) {
Text(
text = "... and ${notes.size - 5} more",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@@ -0,0 +1,29 @@
package com.example.fipsdemo.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.fips.sqlcipher.FipsSQLiteOpenHelperFactory
@Database(entities = [NoteEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
@Volatile
private var instance: AppDatabase? = null
fun get(context: Context, key: String = "demo-passphrase"): AppDatabase =
instance ?: synchronized(this) {
instance ?: Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"fips_demo.db"
)
.openHelperFactory(FipsSQLiteOpenHelperFactory(key))
.build()
.also { instance = it }
}
}
}
@@ -0,0 +1,20 @@
package com.example.fipsdemo.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface NoteDao {
@Insert
suspend fun insert(note: NoteEntity): Long
@Query("SELECT * FROM notes ORDER BY createdAt DESC")
suspend fun getAll(): List<NoteEntity>
@Query("SELECT COUNT(*) FROM notes")
suspend fun count(): Int
@Query("DELETE FROM notes")
suspend fun deleteAll()
}
@@ -0,0 +1,12 @@
package com.example.fipsdemo.db
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "notes")
data class NoteEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val body: String,
val createdAt: Long = System.currentTimeMillis(),
)
+1
View File
@@ -2,4 +2,5 @@ plugins {
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.0.21" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false
id("com.google.devtools.ksp") version "2.0.21-1.0.28" apply false
}
+221
View File
@@ -0,0 +1,221 @@
// SPDX-License-Identifier: Apache-2.0
// JNI bridge for com.fips.sqlcipher.FipsDatabase.
// Wraps SQLCipher operations for use as a Room SupportSQLiteOpenHelper backend.
#ifdef __ANDROID__
#include <jni.h>
#include <android/log.h>
#include <sqlite3.h>
#include <string.h>
#include <stdlib.h>
#define TAG "FipsDatabase"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
JNIEXPORT jlong JNICALL
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);
sqlite3 *db = NULL;
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;
}
if (key && key[0]) {
rc = sqlite3_key(db, key, (int)strlen(key));
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;
}
}
(*env)->ReleaseStringUTFChars(env, jpath, path);
(*env)->ReleaseStringUTFChars(env, jkey, key);
return (jlong)(intptr_t)db;
}
JNIEXPORT void JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeClose(JNIEnv *env, jclass cls,
jlong ptr) {
(void)env; (void)cls;
sqlite3 *db = (sqlite3 *)(intptr_t)ptr;
if (db) sqlite3_close(db);
}
JNIEXPORT void JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeExec(JNIEnv *env, jclass cls,
jlong ptr, jstring jsql) {
(void)cls;
sqlite3 *db = (sqlite3 *)(intptr_t)ptr;
const char *sql = (*env)->GetStringUTFChars(env, jsql, NULL);
char *err = NULL;
int rc = sqlite3_exec(db, sql, NULL, NULL, &err);
if (rc != SQLITE_OK) {
jclass ex = (*env)->FindClass(env, "android/database/SQLException");
(*env)->ThrowNew(env, ex, err ? err : sqlite3_errmsg(db));
}
if (err) sqlite3_free(err);
(*env)->ReleaseStringUTFChars(env, jsql, sql);
}
JNIEXPORT jlong JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativePrepare(JNIEnv *env, jclass cls,
jlong dbPtr, jstring jsql) {
(void)cls;
sqlite3 *db = (sqlite3 *)(intptr_t)dbPtr;
const char *sql = (*env)->GetStringUTFChars(env, jsql, NULL);
sqlite3_stmt *stmt = NULL;
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
(*env)->ReleaseStringUTFChars(env, jsql, sql);
if (rc != SQLITE_OK) {
jclass ex = (*env)->FindClass(env, "android/database/SQLException");
(*env)->ThrowNew(env, ex, sqlite3_errmsg(db));
return 0;
}
return (jlong)(intptr_t)stmt;
}
JNIEXPORT jint JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeStep(JNIEnv *env, jclass cls,
jlong stmtPtr) {
(void)env; (void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
return sqlite3_step(stmt);
}
JNIEXPORT void JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeFinalize(JNIEnv *env, jclass cls,
jlong stmtPtr) {
(void)env; (void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
if (stmt) sqlite3_finalize(stmt);
}
JNIEXPORT void JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeBindString(JNIEnv *env, jclass cls,
jlong stmtPtr,
jint index,
jstring jval) {
(void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
const char *val = (*env)->GetStringUTFChars(env, jval, NULL);
sqlite3_bind_text(stmt, index, val, -1, SQLITE_TRANSIENT);
(*env)->ReleaseStringUTFChars(env, jval, val);
}
JNIEXPORT void JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeBindLong(JNIEnv *env, jclass cls,
jlong stmtPtr,
jint index, jlong val) {
(void)env; (void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
sqlite3_bind_int64(stmt, index, val);
}
JNIEXPORT void JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeBindDouble(JNIEnv *env, jclass cls,
jlong stmtPtr,
jint index, jdouble val) {
(void)env; (void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
sqlite3_bind_double(stmt, index, val);
}
JNIEXPORT void JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeBindNull(JNIEnv *env, jclass cls,
jlong stmtPtr, jint index) {
(void)env; (void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
sqlite3_bind_null(stmt, index);
}
JNIEXPORT jint JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeColumnCount(JNIEnv *env, jclass cls,
jlong stmtPtr) {
(void)env; (void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
return sqlite3_column_count(stmt);
}
JNIEXPORT jstring JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeColumnName(JNIEnv *env, jclass cls,
jlong stmtPtr, jint col) {
(void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
const char *name = sqlite3_column_name(stmt, col);
return (*env)->NewStringUTF(env, name ? name : "");
}
JNIEXPORT jint JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeColumnType(JNIEnv *env, jclass cls,
jlong stmtPtr, jint col) {
(void)env; (void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
return sqlite3_column_type(stmt, col);
}
JNIEXPORT jstring JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeColumnString(JNIEnv *env, jclass cls,
jlong stmtPtr,
jint col) {
(void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
const char *text = (const char *)sqlite3_column_text(stmt, col);
return (*env)->NewStringUTF(env, text ? text : "");
}
JNIEXPORT jlong JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeColumnLong(JNIEnv *env, jclass cls,
jlong stmtPtr, jint col) {
(void)env; (void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
return sqlite3_column_int64(stmt, col);
}
JNIEXPORT jdouble JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeColumnDouble(JNIEnv *env, jclass cls,
jlong stmtPtr,
jint col) {
(void)env; (void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
return sqlite3_column_double(stmt, col);
}
JNIEXPORT void JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeReset(JNIEnv *env, jclass cls,
jlong stmtPtr) {
(void)env; (void)cls;
sqlite3_stmt *stmt = (sqlite3_stmt *)(intptr_t)stmtPtr;
sqlite3_reset(stmt);
sqlite3_clear_bindings(stmt);
}
JNIEXPORT jlong JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeLastInsertRowId(JNIEnv *env,
jclass cls,
jlong dbPtr) {
(void)env; (void)cls;
sqlite3 *db = (sqlite3 *)(intptr_t)dbPtr;
return sqlite3_last_insert_rowid(db);
}
JNIEXPORT jint JNICALL
Java_com_fips_sqlcipher_FipsDatabase_nativeChanges(JNIEnv *env, jclass cls,
jlong dbPtr) {
(void)env; (void)cls;
sqlite3 *db = (sqlite3 *)(intptr_t)dbPtr;
return sqlite3_changes(db);
}
#endif // __ANDROID__
+32 -19
View File
@@ -47,45 +47,58 @@ fips_init_status_t fips_init(const char *module_dir, const char *conf_path) {
return FIPS_INIT_OK;
}
// Point OPENSSL_MODULES to the directory containing the FIPS provider binary
if (module_dir && module_dir[0]) {
#ifdef __ANDROID__
// Android: libfips.so
char probe[4096];
snprintf(probe, sizeof(probe), "%s/libfips.so", module_dir);
if (ACCESS(probe, F_OK) != 0) {
LOGE("FIPS module not found: %s\n", probe);
return FIPS_INIT_ERR_MODULE_MISSING;
}
const char *module_name = "fips.so";
#elif defined(__APPLE__)
const char *module_name = "fips.dylib";
#else
// iOS: fips.dylib
const char *module_name = "fips.so";
#endif
char probe[4096];
snprintf(probe, sizeof(probe), "%s/fips.dylib", module_dir);
snprintf(probe, sizeof(probe), "%s/%s", module_dir, module_name);
if (ACCESS(probe, F_OK) != 0) {
LOGE("FIPS module not found: %s\n", probe);
return FIPS_INIT_ERR_MODULE_MISSING;
}
#endif
setenv("OPENSSL_MODULES", module_dir, 1);
}
// Set OPENSSL_CONF if a config path was provided
// On Android, ossl_safe_getenv() returns NULL when AT_SECURE is set
// (common for app processes), so neither OPENSSL_CONF nor OPENSSL_MODULES
// env vars are visible to libcrypto. We set the search path first, then
// load config via OSSL_LIB_CTX_load_config which bypasses the env vars.
if (module_dir && module_dir[0]) {
OSSL_PROVIDER_set_default_search_path(NULL, module_dir);
}
if (conf_path && conf_path[0]) {
if (ACCESS(conf_path, F_OK) != 0) {
LOGE("Config not found: %s\n", conf_path);
return FIPS_INIT_ERR_CONF_MISSING;
}
setenv("OPENSSL_CONF", conf_path, 1);
if (OSSL_LIB_CTX_load_config(NULL, conf_path) != 1) {
unsigned long err;
while ((err = ERR_get_error()) != 0) {
char buf[256];
ERR_error_string_n(err, buf, sizeof(buf));
LOGE(" Config load error: %s\n", buf);
}
LOGE("Failed to load config: %s\n", conf_path);
return FIPS_INIT_ERR_CONF_MISSING;
}
}
// Load the FIPS provider. This triggers the incore HMAC-SHA256 integrity
// check followed by the Known Answer Tests (KATs). If the module was
// modified post-build (stripped, compressed, re-signed incorrectly), this
// call will fail.
g_fips_provider = OSSL_PROVIDER_load(NULL, "fips");
if (!g_fips_provider) {
unsigned long err = ERR_peek_last_error();
LOGE("FIPS provider load failed: %s\n", ERR_reason_error_string(err));
unsigned long err;
while ((err = ERR_get_error()) != 0) {
char buf[256];
ERR_error_string_n(err, buf, sizeof(buf));
LOGE(" OpenSSL error: %s\n", buf);
}
LOGE("FIPS provider load failed. OPENSSL_MODULES=%s\n",
getenv("OPENSSL_MODULES") ? getenv("OPENSSL_MODULES") : "(unset)");
return FIPS_INIT_ERR_PROVIDER_LOAD;
}
+8 -35
View File
@@ -1,61 +1,34 @@
// SPDX-License-Identifier: Apache-2.0
// Android-specific FIPS initialization helper.
// Android-specific FIPS initialization.
//
// On Android, the FIPS provider (libfips.so) is shipped inside the APK's
// jniLibs/<abi>/ and loaded via System.loadLibrary. The openssl.cnf is
// shipped in assets/fips/ and must be extracted to the app's internal storage
// before OpenSSL reads it.
//
// This file provides fips_init_android() which takes the app's files directory
// (Context.getFilesDir()) and handles:
// 1. Pointing OPENSSL_MODULES to the nativeLibraryDir (where Android unpacks .so)
// 2. Generating fipsmodule.cnf via the incore HMAC (on first run)
// 3. Calling fips_init() with the resolved paths
//
// The fipsmodule.cnf generation is equivalent to running:
// openssl fipsinstall -module libfips.so -out fipsmodule.cnf
// but done programmatically since we can't run the openssl CLI on device.
// fips_init_android() resolves the naming mismatch between Android's "libfips.so"
// (required by the jniLibs convention) and OpenSSL's "fips.so" (provider name
// convention), then delegates to fips_init() for the actual provider activation.
#ifdef __ANDROID__
#include "fips_init.h"
#include <openssl/provider.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/params.h>
#include <openssl/core_names.h>
#include <android/log.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#define LOG_TAG "fips_init_android"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
fips_init_status_t fips_init_android(const char *files_dir,
const char *native_lib_dir) {
if (!files_dir || !native_lib_dir) {
const char *modules_dir) {
if (!files_dir || !modules_dir) {
return FIPS_INIT_ERR_MODULE_MISSING;
}
char conf_dir[4096];
char conf_path[4096];
char module_cnf_path[4096];
snprintf(conf_dir, sizeof(conf_dir), "%s/fips", files_dir);
snprintf(conf_path, sizeof(conf_path), "%s/fips/openssl.cnf", files_dir);
snprintf(module_cnf_path, sizeof(module_cnf_path), "%s/fips/fipsmodule.cnf", files_dir);
// FIPSMODULE_CNF is no longer required by the minimal openssl.cnf shipped
// in the AAR. The FIPS provider is loaded programmatically below.
// Set it anyway for compatibility with custom configs that may .include it.
setenv("FIPSMODULE_CNF", module_cnf_path, 1);
// The native_lib_dir is where Android extracts jniLibs .so files at install.
// This is typically /data/app/<pkg>/lib/<abi>/ and contains libfips.so.
return fips_init(native_lib_dir, conf_path);
return fips_init(modules_dir, conf_path);
}
#endif // __ANDROID__
+45
View File
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
// JNI bridge for com.fips.sqlcipher.FIPSSQLCipher.
// Exposes fips_init_android() and status helpers to Kotlin/Java.
#ifdef __ANDROID__
#include <jni.h>
#include "fips_init.h"
JNIEXPORT jint JNICALL
Java_com_fips_sqlcipher_FIPSSQLCipher_nativeInit(JNIEnv *env, jclass cls,
jstring jfilesDir,
jstring jmodulesDir) {
(void)cls;
const char *files = (*env)->GetStringUTFChars(env, jfilesDir, NULL);
const char *mods = (*env)->GetStringUTFChars(env, jmodulesDir, NULL);
fips_init_status_t rc = fips_init_android(files, mods);
(*env)->ReleaseStringUTFChars(env, jfilesDir, files);
(*env)->ReleaseStringUTFChars(env, jmodulesDir, mods);
return (jint)rc;
}
JNIEXPORT jstring JNICALL
Java_com_fips_sqlcipher_FIPSSQLCipher_nativeStatusMessage(JNIEnv *env,
jclass cls,
jint code) {
(void)cls;
return (*env)->NewStringUTF(env, fips_init_status_str((fips_init_status_t)code));
}
JNIEXPORT jboolean JNICALL
Java_com_fips_sqlcipher_FIPSSQLCipher_nativeProviderActive(JNIEnv *env,
jclass cls) {
(void)env; (void)cls;
return fips_provider_is_active() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jboolean JNICALL
Java_com_fips_sqlcipher_FIPSSQLCipher_nativeSelfTest(JNIEnv *env, jclass cls) {
(void)env; (void)cls;
return fips_self_test_rerun() ? JNI_TRUE : JNI_FALSE;
}
#endif // __ANDROID__