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:
@@ -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
@@ -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
@@ -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+
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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" "$@"
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -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
|
||||
}
|
||||
}
|
||||
Executable
+58
@@ -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}"
|
||||
@@ -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
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
Reference in New Issue
Block a user