diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..ff6a36a --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,21 @@ +# Configuration for probot-stale - https://github.com/probot/stale +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 14 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 14 +# Issues with these labels will never be considered stale +exemptLabels: + - bug + - enhancement + - security +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + Hello, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. + You may also label this issue as "bug", "enhancement", or "security" and I will leave it open. + Thank you for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: > + Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to reopen with up-to-date information. +only: issues diff --git a/.gitignore b/.gitignore index 8ab456e..61a7947 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,13 @@ .DS_Store **/*.class **/*.apk -target \ No newline at end of file +target +*.log +build +.gradle +.idea +local.properties +.idea/** +*.iml +*.aar +*.jar diff --git a/AndroidManifest.xml b/AndroidManifest.xml deleted file mode 100644 index 1c12d1e..0000000 --- a/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4fdcf4e --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2008, ZETETIC LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the ZETETIC LLC nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b59efa1 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +### Deprecated + +This project exists in support of the `android-database-sqlcipher` project which has been [officially deprecated](https://www.zetetic.net/blog/2023/08/31/sqlcipher-4.5.5-release#sqlcipher-android-455). The long-term replacement is [`sqlcipher-android`](https://github.com/sqlcipher/sqlcipher-android). Instructions for migrating from `android-database-sqlcipher` to `sqlcipher-android`may be found [here](https://www.zetetic.net/sqlcipher/sqlcipher-for-android-migration/). + + +To run: clone this repo and open with a recent version of Android Studio, or [build from the command line](https://developer.android.com/studio/build/building-cmdline). + +It is possible to run on an emulator or device, as documented in the [SQLCipher for Android compatibility section](https://github.com/sqlcipher/android-database-sqlcipher#compatibility). + +More information can be found in [SQLCipher for Android](https://zetetic.net/sqlcipher/sqlcipher-for-android/). + +### Creating A New Test + +1. Open this repository within Android Studio +2. Add a new class within `net.zetetic.tests` package that extends `SQLCipherTest`: + +```Java +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; + +public class AwesomeTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + // Add your scenario here + return true; + } catch (Exception e) { + return false; + } + } + + @Override + public String getName() { + return "Awesome Test"; + } +} +``` + +3. Add `AwesomeTest` to the [`TestSuiteRunner`](https://github.com/sqlcipher/sqlcipher-android-tests/blob/master/src/main/java/net/zetetic/tests/TestSuiteRunner.java): + +```Java +tests.add(new AwesomeTest()); +``` +4. Build and run the application on an Android device or emulator diff --git a/README.org b/README.org deleted file mode 100644 index 6aa39db..0000000 --- a/README.org +++ /dev/null @@ -1,3 +0,0 @@ -To run, clone this repo and make sure you have the Android SDK installed. -- Currently support Android OS 2.1 - 4.3, simply run on either an emulator or device. -- More information can be found at [[http://sqlcipher.net/sqlcipher-for-android/][SQLCipher for Android]]. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..76f05b3 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,41 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "net.zetetic.sqlcipher.test" + minSdkVersion 21 + targetSdkVersion 33 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + namespace 'net.zetetic' +} + +dependencies { + // For testing JAR-based distribution: + // implementation files('libs/sqlcipher.jar') + + // For testing local AAR packages: + //implementation (name: 'android-database-sqlcipher-4.5.4-release', ext: 'aar') + + // For testing on remote AAR references: + implementation 'net.zetetic:android-database-sqlcipher:4.5.4@aar' + + implementation "androidx.sqlite:sqlite:2.2.0" + + // For Room tests: + def room_version = "2.5.0" + implementation "androidx.room:room-runtime:$room_version" + annotationProcessor "androidx.room:room-compiler:$room_version" + + // For version comparison + implementation "org.apache.maven:maven-artifact:3.8.7" +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..d8c364f --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/nparker/bin/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..51b202b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/1x-user-version.db b/app/src/main/assets/1x-user-version.db similarity index 100% rename from assets/1x-user-version.db rename to app/src/main/assets/1x-user-version.db diff --git a/assets/1x.db b/app/src/main/assets/1x.db similarity index 100% rename from assets/1x.db rename to app/src/main/assets/1x.db diff --git a/assets/2x.db b/app/src/main/assets/2x.db similarity index 100% rename from assets/2x.db rename to app/src/main/assets/2x.db diff --git a/app/src/main/assets/corrupt.db b/app/src/main/assets/corrupt.db new file mode 100644 index 0000000..878444c Binary files /dev/null and b/app/src/main/assets/corrupt.db differ diff --git a/app/src/main/assets/hello.db b/app/src/main/assets/hello.db new file mode 100644 index 0000000..45e3d0e Binary files /dev/null and b/app/src/main/assets/hello.db differ diff --git a/app/src/main/assets/mutf8.db b/app/src/main/assets/mutf8.db new file mode 100644 index 0000000..f4df6ae Binary files /dev/null and b/app/src/main/assets/mutf8.db differ diff --git a/app/src/main/assets/scrolling.db b/app/src/main/assets/scrolling.db new file mode 100644 index 0000000..834262b Binary files /dev/null and b/app/src/main/assets/scrolling.db differ diff --git a/app/src/main/assets/sqlcipher-3.0-testkey.db b/app/src/main/assets/sqlcipher-3.0-testkey.db new file mode 100644 index 0000000..6af4430 Binary files /dev/null and b/app/src/main/assets/sqlcipher-3.0-testkey.db differ diff --git a/assets/unencrypted.db b/app/src/main/assets/unencrypted.db similarity index 100% rename from assets/unencrypted.db rename to app/src/main/assets/unencrypted.db diff --git a/app/src/main/java/net/zetetic/NativeInitializer.java b/app/src/main/java/net/zetetic/NativeInitializer.java new file mode 100644 index 0000000..27a3345 --- /dev/null +++ b/app/src/main/java/net/zetetic/NativeInitializer.java @@ -0,0 +1,16 @@ +package net.zetetic; +import android.util.Log; + +import net.sqlcipher.database.SQLiteDatabase; + +public class NativeInitializer { + + static { + Log.i("NativeInitializer", "Before loadLibs"); + SQLiteDatabase.loadLibs(ZeteticApplication.getInstance()); + Log.i("NativeInitializer", "After loadLibs"); + } + + public static void initialize(){} + +} diff --git a/app/src/main/java/net/zetetic/QueryHelper.java b/app/src/main/java/net/zetetic/QueryHelper.java new file mode 100644 index 0000000..01b4a5b --- /dev/null +++ b/app/src/main/java/net/zetetic/QueryHelper.java @@ -0,0 +1,45 @@ +package net.zetetic; + +import android.database.Cursor; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import java.util.ArrayList; +import java.util.List; + + +public class QueryHelper { + + public static List getListFromQuery(SupportSQLiteDatabase database, String query){ + Cursor cursor = database.query(query, new String[]{}); + List results = new ArrayList<>(); + if(cursor != null){ + while(cursor.moveToNext()){ + results.add(cursor.getString(0)); + } + cursor.close(); + } + return results; + } + + public static String singleValueFromQuery(SupportSQLiteDatabase database, String query){ + Cursor cursor = database.query(query, new String[]{}); + String value = ""; + if(cursor != null){ + cursor.moveToFirst(); + value = cursor.getString(0); + cursor.close(); + } + return value; + } + + public static int singleIntegerValueFromQuery(SupportSQLiteDatabase database, String query){ + Cursor cursor = database.query(query, new String[]{}); + int value = 0; + if(cursor != null){ + cursor.moveToFirst(); + value = cursor.getInt(0); + cursor.close(); + } + return value; + } +} diff --git a/app/src/main/java/net/zetetic/ScrollingCursorAdapter.java b/app/src/main/java/net/zetetic/ScrollingCursorAdapter.java new file mode 100644 index 0000000..83491c8 --- /dev/null +++ b/app/src/main/java/net/zetetic/ScrollingCursorAdapter.java @@ -0,0 +1,29 @@ +package net.zetetic; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.TextView; + +import net.sqlcipher.Cursor; + +public class ScrollingCursorAdapter extends CursorAdapter { + + public ScrollingCursorAdapter(Context context, Cursor cursor){ + super(context, cursor, 0); + } + + @Override + public View newView(Context context, android.database.Cursor cursor, ViewGroup parent) { + return LayoutInflater.from(context).inflate(R.layout.cursor_item, parent, false); + } + + @Override + public void bindView(View view, Context context, android.database.Cursor cursor) { + TextView messageDisplay = view.findViewById(R.id.message); + String message = cursor.getString (cursor.getColumnIndex("a")); + messageDisplay.setText(message); + } +} diff --git a/src/main/java/net/zetetic/TestResultAdapter.java b/app/src/main/java/net/zetetic/TestResultAdapter.java similarity index 80% rename from src/main/java/net/zetetic/TestResultAdapter.java rename to app/src/main/java/net/zetetic/TestResultAdapter.java index eac0292..f3e1bb7 100644 --- a/src/main/java/net/zetetic/TestResultAdapter.java +++ b/app/src/main/java/net/zetetic/TestResultAdapter.java @@ -33,8 +33,12 @@ public View getView(int position, View view, ViewGroup parent) { TestResult result = getItem(position); holder.testName.setText(result.getName()); holder.testStatus.setText(result.toString()); - holder.testMessage.setVisibility(isNullOrEmpty(result.getMessage()) ? View.GONE : View.VISIBLE); - holder.testMessage.setText(result.getMessage()); + boolean messageAvailable = !isNullOrEmpty(result.getMessage()) || !isNullOrEmpty(result.getError()); + holder.testMessage.setVisibility(messageAvailable ? View.VISIBLE : View.GONE); + if(messageAvailable){ + String message = isNullOrEmpty(result.getError()) ? result.getMessage() : result.getError(); + holder.testMessage.setText(message); + } int displayColor = result.isSuccess() ? Color.GREEN : Color.RED; holder.testStatus.setTextColor(displayColor); return view; diff --git a/app/src/main/java/net/zetetic/ZeteticApplication.java b/app/src/main/java/net/zetetic/ZeteticApplication.java new file mode 100644 index 0000000..d4ec90a --- /dev/null +++ b/app/src/main/java/net/zetetic/ZeteticApplication.java @@ -0,0 +1,147 @@ +package net.zetetic; + +import android.app.Activity; +import android.app.Application; +import android.util.Log; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; + +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; + +import java.io.*; + +public class ZeteticApplication extends Application { + + public static String DATABASE_NAME = "test.db"; + public static String DATABASE_PASSWORD = "test"; + private static ZeteticApplication instance; + private Activity activity; + public static final String TAG = "Zetetic"; + public static final String ONE_X_DATABASE = "1x.db"; + public static final String ONE_X_USER_VERSION_DATABASE = "1x-user-version.db"; + public static final String UNENCRYPTED_DATABASE = "unencrypted.db"; + public static final String licenseCode = ""; + + public ZeteticApplication() { + instance = this; + } + + public static ZeteticApplication getInstance() { + return instance; + } + + @Override + public void onCreate() { + super.onCreate(); + NativeInitializer.initialize(); + } + + public void setCurrentActivity(Activity activity) { + this.activity = activity; + } + + public Activity getCurrentActivity() { + return activity; + } + + public void prepareDatabaseEnvironment() { + Log.i(TAG, "Entered prepareDatabaseEnvironment"); + Log.i(TAG, "Before getDatabasePath"); + File databaseFile = getDatabasePath(DATABASE_NAME); + Log.i(TAG, "Before mkdirs on parent of database path"); + databaseFile.getParentFile().mkdirs(); + + if (databaseFile.exists()) { + Log.i(TAG, "Before delete of database file"); + databaseFile.delete(); + } +// databaseFile.mkdirs(); +// databaseFile.delete(); + } + + public boolean includesLicenseCode(){ + return licenseCode != null && licenseCode.length() > 0; + } + + public boolean supportsMinLibraryVersionRequired(String requiredVersionString) { + try { + DefaultArtifactVersion requiredVersion = new DefaultArtifactVersion(requiredVersionString); + DefaultArtifactVersion actualVersion = new DefaultArtifactVersion(SQLiteDatabase.SQLCIPHER_ANDROID_VERSION); + return actualVersion.compareTo(requiredVersion) >= 0; + } catch (Exception ex){ + return false; + } + } + + public SQLiteDatabase createDatabase(File databaseFile) { + return createDatabase(databaseFile, null); + } + + public SQLiteDatabase createDatabase(File databaseFile, SQLiteDatabaseHook hook) { + Log.i(TAG, "Entered ZeteticApplication::createDatabase"); + Log.i(TAG, "Before SQLiteDatabase.openOrCreateDatabase"); + return SQLiteDatabase.openOrCreateDatabase(databaseFile.getPath(), DATABASE_PASSWORD, null, wrapHook(hook)); + } + + public void extractAssetToDatabaseDirectory(String fileName) throws IOException { + + int length; + InputStream sourceDatabase = ZeteticApplication.getInstance().getAssets().open(fileName); + File destinationPath = ZeteticApplication.getInstance().getDatabasePath(fileName); + OutputStream destination = new FileOutputStream(destinationPath); + + byte[] buffer = new byte[4096]; + while ((length = sourceDatabase.read(buffer)) > 0) { + destination.write(buffer, 0, length); + } + sourceDatabase.close(); + destination.flush(); + destination.close(); + } + + public void delete1xDatabase() { + + File databaseFile = getInstance().getDatabasePath(ONE_X_DATABASE); + databaseFile.delete(); + } + + public void deleteDatabaseFileAndSiblings(String databaseName) { + + File databaseFile = ZeteticApplication.getInstance().getDatabasePath(databaseName); + File databasesDirectory = new File(databaseFile.getParent()); + for (File file : databasesDirectory.listFiles()) { + file.delete(); + } + } + + public SQLiteDatabaseHook wrapHook(final SQLiteDatabaseHook hook) { + if (hook == null) + { + return keyHook; + } + return new SQLiteDatabaseHook() { + @Override + public void preKey(SQLiteDatabase database) { + keyHook.preKey(database); + hook.preKey(database); + } + + @Override + public void postKey(SQLiteDatabase database) { + keyHook.postKey(database); + hook.preKey(database); + } + }; + } + + SQLiteDatabaseHook keyHook = new SQLiteDatabaseHook() { + @Override + public void preKey(SQLiteDatabase database) { + database.rawExecSQL(String.format("PRAGMA cipher_license = '%s';", licenseCode)); + } + public void postKey(SQLiteDatabase database) { + } + }; + +} diff --git a/src/main/java/net/zetetic/ZeteticContentProvider.java b/app/src/main/java/net/zetetic/ZeteticContentProvider.java similarity index 95% rename from src/main/java/net/zetetic/ZeteticContentProvider.java rename to app/src/main/java/net/zetetic/ZeteticContentProvider.java index 80f4d6b..9ec5c02 100644 --- a/src/main/java/net/zetetic/ZeteticContentProvider.java +++ b/app/src/main/java/net/zetetic/ZeteticContentProvider.java @@ -5,6 +5,7 @@ import android.database.Cursor; import android.net.Uri; import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; import net.sqlcipher.database.SQLiteQueryBuilder; import java.io.File; @@ -27,11 +28,8 @@ public boolean onCreate() { @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - - SQLiteDatabase.loadLibs(ZeteticApplication.getInstance()); File databasePath = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.DATABASE_NAME); - database = ZeteticApplication.getInstance().createDatabase(databasePath); - + database = ZeteticApplication.getInstance().createDatabase(databasePath, null); createDatabaseWithData(database); SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); builder.setTables("t1"); diff --git a/src/main/java/net/zetetic/ZeteticContentProvider2.java b/app/src/main/java/net/zetetic/ZeteticContentProvider2.java similarity index 91% rename from src/main/java/net/zetetic/ZeteticContentProvider2.java rename to app/src/main/java/net/zetetic/ZeteticContentProvider2.java index c10d186..42826f6 100644 --- a/src/main/java/net/zetetic/ZeteticContentProvider2.java +++ b/app/src/main/java/net/zetetic/ZeteticContentProvider2.java @@ -6,8 +6,10 @@ import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; +import android.util.Log; import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; import net.sqlcipher.database.SQLiteQueryBuilder; public class ZeteticContentProvider2 extends ContentProvider { @@ -18,9 +20,7 @@ public class ZeteticContentProvider2 extends ContentProvider { private SQLiteDatabase database; - public ZeteticContentProvider2() { - - } + public ZeteticContentProvider2() {} @Override public boolean onCreate() { @@ -29,8 +29,6 @@ public boolean onCreate() { @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - - SQLiteDatabase.loadLibs(ZeteticApplication.getInstance()); File databasePath = ZeteticApplication.getInstance().getDatabasePath(DATABASE_NAME); database = ZeteticApplication.getInstance().createDatabase(databasePath); @@ -64,7 +62,7 @@ private void createDatabaseWithData(SQLiteDatabase database) { database.execSQL("create table if not exists t1(a varchar, b blob);"); ContentValues values = new ContentValues(); values.put("a", "one for the money"); - values.put("b", "two for the show".getBytes()); + values.put("b", "two for the shownwp".getBytes()); database.insert("t1", null, values); } } diff --git a/app/src/main/java/net/zetetic/activities/TestRunnerSelectionActivity.java b/app/src/main/java/net/zetetic/activities/TestRunnerSelectionActivity.java new file mode 100644 index 0000000..a8e5b4b --- /dev/null +++ b/app/src/main/java/net/zetetic/activities/TestRunnerSelectionActivity.java @@ -0,0 +1,51 @@ +package net.zetetic.activities; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; + +import net.zetetic.R; + +public class TestRunnerSelectionActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.test_runners); + Button behaviorSuite = findViewById(R.id.run_behavior_test_suite); + Button scrollingCursorSuite = findViewById(R.id.run_scrolling_test_suite); + Button supportSuite = findViewById(R.id.run_support_test_suite); + if(behaviorSuite != null){ + behaviorSuite.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(TestRunnerSelectionActivity.this, + TestSuiteBehaviorsActivity.class); + startActivity(intent); + } + }); + } + if(scrollingCursorSuite != null){ + scrollingCursorSuite.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(TestRunnerSelectionActivity.this, + TestScrollingCursorActivity.class); + startActivity(intent); + } + }); + } + if(supportSuite != null){ + supportSuite.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(TestRunnerSelectionActivity.this, + TestSuiteBehaviorsActivity.class).putExtra(TestSuiteBehaviorsActivity.EXTRA_IS_SUPPORT, true); + startActivity(intent); + } + }); + } + } +} diff --git a/app/src/main/java/net/zetetic/activities/TestScrollingCursorActivity.java b/app/src/main/java/net/zetetic/activities/TestScrollingCursorActivity.java new file mode 100644 index 0000000..03c4a13 --- /dev/null +++ b/app/src/main/java/net/zetetic/activities/TestScrollingCursorActivity.java @@ -0,0 +1,111 @@ +package net.zetetic.activities; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.ListView; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import net.sqlcipher.CrossProcessCursorWrapper; +import net.sqlcipher.Cursor; +import net.sqlcipher.CursorWindow; +import net.sqlcipher.CustomCursorWindowAllocation; +import net.sqlcipher.database.SQLiteCursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.zetetic.R; +import net.zetetic.ScrollingCursorAdapter; +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class TestScrollingCursorActivity extends Activity { + + private SQLiteDatabase database; + private String databaseFilename = "scrolling.db"; + private long allocationSize = 1024 * 1024 * 4; + ListView listView; + RadioGroup options; + Cursor cursor; + + SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { + public void preKey(SQLiteDatabase database) {} + public void postKey(SQLiteDatabase database) { + database.execSQL("PRAGMA kdf_iter = 64000;"); + database.execSQL("PRAGMA cipher_page_size = 1024;"); + database.execSQL("PRAGMA cipher_hmac_algorithm = HMAC_SHA1;"); + database.execSQL("PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1;"); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.scrolling_cursor_view); + try { + initializeEnvironment(); + CursorWindow.setCursorWindowAllocation(new CustomCursorWindowAllocation(allocationSize, 0, allocationSize)); + options = findViewById(R.id.options); + listView = findViewById(R.id.listView); + RadioButton optimizeButton = findViewById(R.id.optimize_cursor); + RadioButton explicitButton = findViewById(R.id.explicit_cursor); + optimizeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + bindContent(false); + } + }); + explicitButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + bindContent(true); + } + }); + } catch (Exception e) { + Log.e(getClass().getSimpleName(), e.toString()); + } + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + if(cursor != null) cursor.close(); + if(database != null) database.close(); + } + + private void bindContent(boolean value){ + setRadioButtonState(false); + if(cursor != null) cursor.close(); + cursor = database.rawQuery("SELECT * FROM t1;", null); + ((SQLiteCursor)((CrossProcessCursorWrapper)cursor).getWrappedCursor()).setFillWindowForwardOnly(value); + final ScrollingCursorAdapter cursorAdapter = new ScrollingCursorAdapter(this, cursor); + listView.setAdapter(cursorAdapter); + listView.post(new Runnable() { + @Override + public void run() { + listView.setSelection(cursorAdapter.getCount() - 1); + setRadioButtonState(true); + } + }); + } + + void initializeEnvironment(){ + try{ + File databasePath = getDatabasePath(databaseFilename); + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory(databaseFilename); + database = SQLiteDatabase.openDatabase(databasePath.getAbsolutePath(), + ZeteticApplication.DATABASE_PASSWORD, null, SQLiteDatabase.OPEN_READWRITE, hook); + } catch (Exception e){ + Log.e(getClass().getSimpleName(), e.toString()); + } + } + + void setRadioButtonState(final boolean value) { + for (int index = 0; index < options.getChildCount(); index++) { + View item = options.getChildAt(index); + if (item != null) item.setEnabled(value); + } + } +} diff --git a/app/src/main/java/net/zetetic/activities/TestSuiteBehaviorsActivity.java b/app/src/main/java/net/zetetic/activities/TestSuiteBehaviorsActivity.java new file mode 100644 index 0000000..4a6711d --- /dev/null +++ b/app/src/main/java/net/zetetic/activities/TestSuiteBehaviorsActivity.java @@ -0,0 +1,131 @@ +package net.zetetic.activities; + +import android.app.Activity; +import android.media.AudioManager; +import android.media.ToneGenerator; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.ArrayAdapter; +import android.widget.HeaderViewListAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import net.zetetic.R; +import net.zetetic.TestResultAdapter; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.ResultNotifier; +import net.zetetic.tests.TestResult; +import net.zetetic.tests.TestSuiteRunner; +import net.zetetic.tests.support.SupportSuiteRunner; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class TestSuiteBehaviorsActivity extends Activity implements ResultNotifier { + static final String EXTRA_IS_SUPPORT = "isSupport"; + private String TAG = this.getClass().getSimpleName(); + ListView resultsView; + List results; + View statsView; + File testResults; + + public TestSuiteBehaviorsActivity(){ + results = new ArrayList(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.i(TAG, "onCreate"); + setContentView(R.layout.main); + testResults = new File(getApplication().getFilesDir(), "test-results.log"); + deleteTestResultsLog(); + onButtonClick(null); + } + + public void onButtonClick(View view) { + deleteTestResultsLog(); + results.clear(); + hideStats(); + findViewById(R.id.executeSuite).setEnabled(false); + resultsView = findViewById(R.id.test_suite_results); + ZeteticApplication.getInstance().setCurrentActivity(this); + this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + if (getIntent().getBooleanExtra(EXTRA_IS_SUPPORT, false)) { + new SupportSuiteRunner(this).execute(this); + } + else { + new TestSuiteRunner(this).execute(this); + } + } + + @Override + @SuppressWarnings("unchecked") + public void send(TestResult result) { + + results.add(result); + Log.i(TAG, String.format("%s - success:%s", result.getName(), result.isSuccess())); + HeaderViewListAdapter adapter = (HeaderViewListAdapter) resultsView.getAdapter(); + if(adapter == null){ + statsView = View.inflate(this, R.layout.test_stats, null); + resultsView.addHeaderView(statsView); + hideStats(); + resultsView.setAdapter(new TestResultAdapter(ZeteticApplication.getInstance(), results)); + } else { + ((ArrayAdapter)adapter.getWrappedAdapter()).notifyDataSetChanged(); + } + } + + @Override + public void complete() { + + TextView stats = statsView.findViewById(R.id.stats); + int successCount = 0; + List failedTests = new ArrayList(); + for(TestResult result : results){ + if(result.isSuccess()){ + successCount += 1; + } else { + failedTests.add(result.getName()); + } + } + String message = String.format(Locale.getDefault(), + "Passed: %d Failed: %d", successCount, results.size() - successCount); + deleteTestResultsLog(); + try { + FileOutputStream resultStream = new FileOutputStream(testResults); + resultStream.write(String.format("%s\n", message).getBytes()); + for(String test : failedTests){ + resultStream.write(test.getBytes()); + } + resultStream.flush(); + resultStream.close(); + } catch (Exception e) { + Log.i(TAG, "Failed to write test suite results", e); + } + Log.i(TAG, message); + stats.setText(message); + stats.setVisibility(View.VISIBLE); + findViewById(R.id.executeSuite).setEnabled(true); + ToneGenerator toneG = new ToneGenerator(AudioManager.STREAM_ALARM, 100); + toneG.startTone(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 200); + this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void deleteTestResultsLog(){ + if(testResults.exists()){ + testResults.delete(); + } + } + + private void hideStats(){ + if(statsView != null){ + statsView.findViewById(R.id.stats).setVisibility(View.GONE); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/zetetic/tests/AES128CipherTest.java b/app/src/main/java/net/zetetic/tests/AES128CipherTest.java similarity index 95% rename from src/main/java/net/zetetic/tests/AES128CipherTest.java rename to app/src/main/java/net/zetetic/tests/AES128CipherTest.java index 5e7452e..3bafab1 100644 --- a/src/main/java/net/zetetic/tests/AES128CipherTest.java +++ b/app/src/main/java/net/zetetic/tests/AES128CipherTest.java @@ -40,7 +40,7 @@ public void preKey(SQLiteDatabase database) {} @Override public void postKey(SQLiteDatabase database) { - database.execSQL("PRAGMA cipher = 'aes-128-cbc'"); + database.rawExecSQL("PRAGMA cipher = 'aes-128-cbc'"); } }); } diff --git a/app/src/main/java/net/zetetic/tests/AccessDatabaseTest.java b/app/src/main/java/net/zetetic/tests/AccessDatabaseTest.java new file mode 100644 index 0000000..c57b8fd --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/AccessDatabaseTest.java @@ -0,0 +1,37 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class AccessDatabaseTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.close(); + try { + int rows = 0; + String filename = "encrypted.db"; + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory(filename); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(filename); + database = SQLiteDatabase.openDatabase(databasePath.getAbsolutePath(), "test", null, SQLiteDatabase.OPEN_READWRITE); + if(database != null){ + Cursor cursor = database.rawQuery("SELECT COUNT(*) FROM sqlite_master;", new Object[]{}); + if(cursor != null){ + cursor.moveToFirst(); + rows = cursor.getInt(0); + cursor.close(); + } + } + return rows > 0; + } catch (Exception e) { + return false; + } + } + + @Override + public String getName() { + return "Access Database File Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/AttachDatabaseTest.java b/app/src/main/java/net/zetetic/tests/AttachDatabaseTest.java new file mode 100644 index 0000000..05dacb3 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/AttachDatabaseTest.java @@ -0,0 +1,32 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; + +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class AttachDatabaseTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + boolean status; + String password = "test123"; + File fooDatabase = ZeteticApplication.getInstance().getDatabasePath("foo.db"); + if(fooDatabase.exists()){ + fooDatabase.delete(); + } + database.execSQL("ATTACH database ? AS encrypted KEY ?", new Object[]{fooDatabase.getAbsolutePath(), password}); + database.execSQL("create table encrypted.t1(a,b);"); + database.execSQL("insert into encrypted.t1(a,b) values(?,?);", new Object[]{"one for the money", "two for the show"}); + int rowCount = QueryHelper.singleIntegerValueFromQuery(database, "select count(*) from encrypted.t1;"); + status = rowCount == 1; + database.close(); + return status; + } + + @Override + public String getName() { + return "Attach database test"; + } +} diff --git a/src/main/java/net/zetetic/tests/AttachExistingDatabaseTest.java b/app/src/main/java/net/zetetic/tests/AttachExistingDatabaseTest.java similarity index 71% rename from src/main/java/net/zetetic/tests/AttachExistingDatabaseTest.java rename to app/src/main/java/net/zetetic/tests/AttachExistingDatabaseTest.java index 5f3d9b9..785daa9 100644 --- a/src/main/java/net/zetetic/tests/AttachExistingDatabaseTest.java +++ b/app/src/main/java/net/zetetic/tests/AttachExistingDatabaseTest.java @@ -3,6 +3,7 @@ import android.database.Cursor; import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabaseHook; +import net.zetetic.QueryHelper; import net.zetetic.ZeteticApplication; import java.io.File; @@ -10,12 +11,17 @@ public class AttachExistingDatabaseTest extends SQLCipherTest { + private int originalKdfLength; + @Override protected SQLiteDatabase createDatabase(File databasePath) { SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { public void preKey(SQLiteDatabase database) {} public void postKey(SQLiteDatabase database) { database.execSQL("PRAGMA cipher_default_kdf_iter = 4000;"); + database.execSQL("PRAGMA cipher_default_page_size = 1024;"); + database.execSQL("PRAGMA cipher_default_hmac_algorithm = HMAC_SHA1;"); + database.execSQL("PRAGMA cipher_default_kdf_algorithm = PBKDF2_HMAC_SHA1;"); } }; return SQLiteDatabase.openOrCreateDatabase(databasePath, @@ -31,6 +37,7 @@ public boolean execute(SQLiteDatabase database) { String otherPath = other.getAbsolutePath(); String attach = String.format("attach database ? as other key ?"); database.rawExecSQL("pragma cipher_default_use_hmac = off"); + originalKdfLength = QueryHelper.singleIntegerValueFromQuery(database, "PRAGMA kdf_iter;"); database.rawExecSQL("pragma cipher_default_kdf_iter = 4000;"); database.execSQL(attach, new Object[]{otherPath, ZeteticApplication.DATABASE_PASSWORD}); Cursor result = database.rawQuery("select * from other.t1", new String[]{}); @@ -43,7 +50,6 @@ public boolean execute(SQLiteDatabase database) { result.close(); } database.execSQL("detach database other"); - database.rawExecSQL("pragma cipher_default_kdf_iter = 64000;"); return a.length() > 0 && b.length() > 0; } catch (IOException e) { return false; @@ -53,7 +59,12 @@ public boolean execute(SQLiteDatabase database) { @Override protected void tearDown(SQLiteDatabase database) { ZeteticApplication.getInstance().delete1xDatabase(); - database.execSQL("PRAGMA cipher_default_kdf_iter = 64000;"); + database.rawExecSQL(String.format("PRAGMA cipher_default_kdf_iter = %s", originalKdfLength)); + database.rawExecSQL("pragma cipher_default_use_hmac = on"); + database.execSQL("PRAGMA cipher_default_kdf_iter = 256000;"); + database.execSQL("PRAGMA cipher_default_page_size = 4096;"); + database.execSQL("PRAGMA cipher_default_hmac_algorithm = HMAC_SHA512;"); + database.execSQL("PRAGMA cipher_default_kdf_algorithm = PBKDF2_HMAC_SHA512;"); } @Override diff --git a/src/main/java/net/zetetic/tests/AttachNewDatabaseTest.java b/app/src/main/java/net/zetetic/tests/AttachNewDatabaseTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/AttachNewDatabaseTest.java rename to app/src/main/java/net/zetetic/tests/AttachNewDatabaseTest.java diff --git a/src/main/java/net/zetetic/tests/AutoVacuumOverReadTest.java b/app/src/main/java/net/zetetic/tests/AutoVacuumOverReadTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/AutoVacuumOverReadTest.java rename to app/src/main/java/net/zetetic/tests/AutoVacuumOverReadTest.java diff --git a/src/main/java/net/zetetic/tests/AverageOpenTimeTest.java b/app/src/main/java/net/zetetic/tests/AverageOpenTimeTest.java similarity index 57% rename from src/main/java/net/zetetic/tests/AverageOpenTimeTest.java rename to app/src/main/java/net/zetetic/tests/AverageOpenTimeTest.java index c655959..10b772b 100644 --- a/src/main/java/net/zetetic/tests/AverageOpenTimeTest.java +++ b/app/src/main/java/net/zetetic/tests/AverageOpenTimeTest.java @@ -1,6 +1,9 @@ package net.zetetic.tests; +import android.util.Log; import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.zetetic.QueryHelper; import net.zetetic.ZeteticApplication; import java.io.File; @@ -10,6 +13,14 @@ public class AverageOpenTimeTest extends SQLCipherTest { int MAX_ATTEMPTS = 10; + SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { + public void preKey(SQLiteDatabase sqLiteDatabase) { + } + public void postKey(SQLiteDatabase sqLiteDatabase) { + //sqLiteDatabase.rawExecSQL("PRAGMA kdf_iter = 64000;"); + } + }; + @Override public boolean execute(SQLiteDatabase database) { @@ -19,16 +30,24 @@ public boolean execute(SQLiteDatabase database) { File databasePath = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.DATABASE_NAME); for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { Date start = new Date(); - database = SQLiteDatabase.openOrCreateDatabase(databasePath, ZeteticApplication.DATABASE_PASSWORD, null); + database = SQLiteDatabase.openOrCreateDatabase(databasePath, ZeteticApplication.DATABASE_PASSWORD, null, hook); Date end = new Date(); + String kdf = QueryHelper.singleValueFromQuery(database, "PRAGMA kdf_iter;"); + Log.i(TAG, String.format("KDF value:%s", kdf)); database.close(); runs += (end.getTime() - start.getTime()) / 1000.0f; } + status = true; setMessage(String.format("%s attempts, %.3f second average open time", MAX_ATTEMPTS, runs/MAX_ATTEMPTS)); return status; } + @Override + protected SQLiteDatabase createDatabase(File databasePath) { + return SQLiteDatabase.openOrCreateDatabase(databasePath, ZeteticApplication.DATABASE_PASSWORD, null, hook); + } + @Override public String getName() { return "Average Database Open Time"; diff --git a/app/src/main/java/net/zetetic/tests/BeginTransactionTest.java b/app/src/main/java/net/zetetic/tests/BeginTransactionTest.java new file mode 100644 index 0000000..fb7684c --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/BeginTransactionTest.java @@ -0,0 +1,17 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; + +public class BeginTransactionTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.beginTransaction(); + database.endTransaction(); + return true; + } + + @Override + public String getName() { + return "Begin transaction test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/BindBooleanRawQueryTest.java b/app/src/main/java/net/zetetic/tests/BindBooleanRawQueryTest.java new file mode 100644 index 0000000..dbef240 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/BindBooleanRawQueryTest.java @@ -0,0 +1,27 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class BindBooleanRawQueryTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", true}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", new Object[]{true}); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + int b = cursor.getInt(1); + cursor.close(); + return a.equals("one for the money") && b == 1; + } + } + return false; + } + + @Override + public String getName() { + return "Bind Boolean for RawQuery Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/BindByteArrayRawQueryTest.java b/app/src/main/java/net/zetetic/tests/BindByteArrayRawQueryTest.java new file mode 100644 index 0000000..5c681af --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/BindByteArrayRawQueryTest.java @@ -0,0 +1,34 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +import java.security.SecureRandom; +import java.util.Arrays; + +public class BindByteArrayRawQueryTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + + SecureRandom random = new SecureRandom(); + byte[] randomData = new byte[20]; + random.nextBytes(randomData); + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", randomData}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", new Object[]{randomData}); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + byte[] b = cursor.getBlob(1); + cursor.close(); + return a.equals("one for the money") && Arrays.equals(randomData, b); + } + } + return false; + } + + @Override + public String getName() { + return "Bind Byte Array for RawQuery Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/BindDoubleRawQueryTest.java b/app/src/main/java/net/zetetic/tests/BindDoubleRawQueryTest.java new file mode 100644 index 0000000..087ff8e --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/BindDoubleRawQueryTest.java @@ -0,0 +1,27 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class BindDoubleRawQueryTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", 2.0d}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", new Object[]{2.0d}); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + Double b = cursor.getDouble(1); + cursor.close(); + return a.equals("one for the money") && b == 2.0d; + } + } + return false; + } + + @Override + public String getName() { + return "Bind Double for RawQuery Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/BindFloatRawQueryTest.java b/app/src/main/java/net/zetetic/tests/BindFloatRawQueryTest.java new file mode 100644 index 0000000..889c2c2 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/BindFloatRawQueryTest.java @@ -0,0 +1,27 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class BindFloatRawQueryTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", 2.25f}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", new Object[]{2.25f}); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + float b = cursor.getFloat(1); + cursor.close(); + return a.equals("one for the money") && b == 2.25f; + } + } + return false; + } + + @Override + public String getName() { + return "Bind Float for RawQuery Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/BindLongRawQueryTest.java b/app/src/main/java/net/zetetic/tests/BindLongRawQueryTest.java new file mode 100644 index 0000000..b12b26e --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/BindLongRawQueryTest.java @@ -0,0 +1,27 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class BindLongRawQueryTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", 2L}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", new Object[]{2L}); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + long b = cursor.getLong(1); + cursor.close(); + return a.equals("one for the money") && b == 2L; + } + } + return false; + } + + @Override + public String getName() { + return "Bind Long for RawQuery Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/BindStringRawQueryTest.java b/app/src/main/java/net/zetetic/tests/BindStringRawQueryTest.java new file mode 100644 index 0000000..50a2fe2 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/BindStringRawQueryTest.java @@ -0,0 +1,27 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class BindStringRawQueryTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", "two for the show"}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", new Object[]{"two for the show"}); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + String b = cursor.getString(1); + cursor.close(); + return a.equals("one for the money") && b.equals("two for the show"); + } + } + return false; + } + + @Override + public String getName() { + return "Bind String for RawQuery Test"; + } +} diff --git a/src/main/java/net/zetetic/tests/CanThrowSQLiteExceptionTest.java b/app/src/main/java/net/zetetic/tests/CanThrowSQLiteExceptionTest.java similarity index 90% rename from src/main/java/net/zetetic/tests/CanThrowSQLiteExceptionTest.java rename to app/src/main/java/net/zetetic/tests/CanThrowSQLiteExceptionTest.java index c7c54c9..684554f 100644 --- a/src/main/java/net/zetetic/tests/CanThrowSQLiteExceptionTest.java +++ b/app/src/main/java/net/zetetic/tests/CanThrowSQLiteExceptionTest.java @@ -1,7 +1,7 @@ package net.zetetic.tests; import net.sqlcipher.database.SQLiteDatabase; -import net.sqlcipher.database.SQLiteException; +import android.database.sqlite.SQLiteException; public class CanThrowSQLiteExceptionTest extends SQLCipherTest { diff --git a/app/src/main/java/net/zetetic/tests/ChangePasswordTest.java b/app/src/main/java/net/zetetic/tests/ChangePasswordTest.java new file mode 100644 index 0000000..51fb260 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/ChangePasswordTest.java @@ -0,0 +1,53 @@ +package net.zetetic.tests; + +import android.util.Log; + +import net.sqlcipher.database.SQLiteDatabase; + +import net.zetetic.QueryHelper; +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class ChangePasswordTest extends SQLCipherTest { + + File databaseFile = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.DATABASE_NAME); + String secondPassword = "second password"; + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("CREATE TABLE t1(a,b);"); + database.execSQL("INSERT INTO t1(a,b) VALUES(?,?)", new Object[]{"one for the money", "two for the show"}); + database.changePassword(secondPassword); + database.close(); + + database = SQLiteDatabase.openOrCreateDatabase(databaseFile, secondPassword, null); + int count = QueryHelper.singleIntegerValueFromQuery(database, "SELECT COUNT(*) FROM t1;"); + database.close(); + + if (count != 1) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: incorrect record count"); + return false; + } + + // Verify that these will not cause a crash: + database = SQLiteDatabase.openOrCreateDatabase(databaseFile, secondPassword, null); + String nullPasswordString = null; + database.changePassword(nullPasswordString); + try { + char[] nullPassword = null; + database.changePassword(nullPassword); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception", e); + return false; + } + database.close(); + + return true; + } + + @Override + public String getName() { + return "Change Password Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CheckIsDatabaseIntegrityOkTest.java b/app/src/main/java/net/zetetic/tests/CheckIsDatabaseIntegrityOkTest.java new file mode 100644 index 0000000..8e3a5eb --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CheckIsDatabaseIntegrityOkTest.java @@ -0,0 +1,15 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; + +public class CheckIsDatabaseIntegrityOkTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + return database.isDatabaseIntegrityOk(); + } + + @Override + public String getName() { + return "Check Database Integrity"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CheckIsWriteAheadLoggingEnabledTest.java b/app/src/main/java/net/zetetic/tests/CheckIsWriteAheadLoggingEnabledTest.java new file mode 100644 index 0000000..2919c63 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CheckIsWriteAheadLoggingEnabledTest.java @@ -0,0 +1,19 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; + +public class CheckIsWriteAheadLoggingEnabledTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.enableWriteAheadLogging(); + boolean statusWhenEnabled = database.isWriteAheadLoggingEnabled(); + database.disableWriteAheadLogging(); + boolean statusWhenDisabled = database.isWriteAheadLoggingEnabled(); + return statusWhenEnabled && !statusWhenDisabled; + } + + @Override + public String getName() { + return "Test isWriteAheadLoggingEnabled"; + } +} diff --git a/src/main/java/net/zetetic/tests/CipherMigrateTest.java b/app/src/main/java/net/zetetic/tests/CipherMigrateTest.java similarity index 80% rename from src/main/java/net/zetetic/tests/CipherMigrateTest.java rename to app/src/main/java/net/zetetic/tests/CipherMigrateTest.java index 745e70c..3dd425a 100644 --- a/src/main/java/net/zetetic/tests/CipherMigrateTest.java +++ b/app/src/main/java/net/zetetic/tests/CipherMigrateTest.java @@ -22,11 +22,13 @@ public boolean execute(SQLiteDatabase database) { public void preKey(SQLiteDatabase database) {} public void postKey(SQLiteDatabase database) { String value = QueryHelper.singleValueFromQuery(database, "PRAGMA cipher_migrate"); + setMessage(String.format("cipher_migrate result:%s", value)); status[0] = Integer.valueOf(value) == 0; } }; - database = SQLiteDatabase.openOrCreateDatabase(olderFormatDatabase, - ZeteticApplication.DATABASE_PASSWORD, null, hook); + database = SQLiteDatabase.openDatabase(olderFormatDatabase.getPath(), + ZeteticApplication.DATABASE_PASSWORD, null, + SQLiteDatabase.OPEN_READWRITE | SQLiteDatabase.CREATE_IF_NECESSARY, hook); if(database != null){ database.close(); } diff --git a/app/src/main/java/net/zetetic/tests/ClosedDatabaseTest.java b/app/src/main/java/net/zetetic/tests/ClosedDatabaseTest.java new file mode 100644 index 0000000..6d7e074 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/ClosedDatabaseTest.java @@ -0,0 +1,387 @@ +package net.zetetic.tests; + +import android.util.Log; +import net.sqlcipher.DatabaseErrorHandler; +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class ClosedDatabaseTest extends SQLCipherTest { + + @Override + public TestResult run() { + + TestResult result = new TestResult(getName(), false); + try { + result.setResult(execute(null)); + SQLiteDatabase.releaseMemory(); + } catch (Exception e) { + Log.v(ZeteticApplication.TAG, e.toString()); + } + return result; + } + + @Override + public boolean execute(SQLiteDatabase null_database_ignored) { + + File closedDatabasePath = ZeteticApplication.getInstance().getDatabasePath("closed-db-test.db"); + + boolean status = false; + + try { + SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(closedDatabasePath, "", null); + + database.close(); + + status = execute_closed_database_tests(database); + } catch (Exception e) { + // Uncaught [unexpected] exception: + Log.e(ZeteticApplication.TAG, "Unexpected exception", e); + return false; + } + finally { + closedDatabasePath.delete(); + } + + if (!status) return false; + + status = false; + + final File corruptDatabase = ZeteticApplication.getInstance().getDatabasePath("corrupt.db"); + + // run closed database tests again in custom DatabaseErrorHandler: + try { + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("corrupt.db"); + + // ugly trick from: http://stackoverflow.com/questions/5977735/setting-outer-variable-from-anonymous-inner-class + final boolean[] inner_status_slot = new boolean[1]; + + // on a database object that was NEVER actually opened (due to a corrupt database file): + SQLiteDatabase database = SQLiteDatabase.openDatabase(corruptDatabase.getPath(), "", null, SQLiteDatabase.CREATE_IF_NECESSARY, null, new DatabaseErrorHandler() { + @Override + public void onCorruption(SQLiteDatabase db) { + try { + inner_status_slot[0] = execute_closed_database_tests(db); + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + } + + try { + corruptDatabase.delete(); + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + } + } + }); + + status = inner_status_slot[0]; + + // NOTE: database not expected to be null, but double-check: + if (database == null) { + Log.e(TAG, "ERROR: got null database object"); + return false; + } + + database.close(); + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + return false; + } + finally { + corruptDatabase.delete(); + } + + return status; + } + + @SuppressWarnings("deprecation") + boolean execute_closed_database_tests(SQLiteDatabase database) { + try { + /* operations that check if db is closed (and throw IllegalStateException): */ + try { + // should throw IllegalStateException: + database.beginTransaction(); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.beginTransaction() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.beginTransaction() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.endTransaction(); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.endTransaction() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.endTransaction() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.setTransactionSuccessful(); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.setTransactionSuccessful() did NOT throw throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.setTransactionSuccessful() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.getVersion(); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.getVersion() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.getVersion() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.setVersion(111); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.setVersion() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.setVersion() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.getMaximumSize(); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.getMaximumSize() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.getMaximumSize() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.setMaximumSize(111); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.setMaximumSize() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.setMaximumSize() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.getPageSize(); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.getPageSize() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.getPageSize() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.setPageSize(111); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.setPageSize() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.setPageSize() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.compileStatement("SELECT 1;"); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.compileStatement() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.compileStatement() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.query("t1", new String[]{"a", "b"}, null, null, null, null, null); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.query() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.query() did throw exception on closed database OK", e); + } + + // TODO: cover more query functions + + try { + // should throw IllegalStateException: + database.execSQL("SELECT 1;"); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.execSQL(String) did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.execSQL(String) did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.execSQL("SELECT 1;", new Object[1]); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.execSQL(String, Object[]) did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.execSQL(String, Object[]) did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.rawExecSQL("SELECT 1;"); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.rawExecSQL() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.rawExecSQL() did throw exception on closed database OK", e); + } + + /* operations that do not explicitly check if db is closed + * ([should] throw SQLiteException on a closed database): */ + +// try { +// // should throw IllegalStateException: +// database.setLocale(Locale.getDefault()); +// +// // should not get here: +// Log.e(ZeteticApplication.TAG, "SQLiteDatabase.setLocale() did NOT throw exception on closed database"); +// return false; +// } catch (SQLiteException e) { +// Log.v(ZeteticApplication.TAG, "SQLiteDatabase.setLocale() did throw exception on closed database OK", e); +// } + + try { + // [should] throw an exception on a closed database: + database.changePassword("new-password"); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.changePassword(String) did NOT throw exception on closed database"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.changePassword(String) did throw exception on closed database OK", e); + } + + try { + // [should] throw an exception on a closed database: + database.changePassword("new-password".toCharArray()); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.changePassword(char []) did NOT throw exception on closed database"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.changePassword(char []) did throw exception on closed database OK", e); + } + + try { + // [should] throw an exception on a closed database: + database.markTableSyncable("aa", "bb"); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.markTableSyncable(String, String) did NOT throw exception on closed database"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.markTableSyncable(String, String) did throw exception on closed database OK", e); + } + + try { + // [should] throw an exception on a closed database: + database.markTableSyncable("aa", "bb", "cc"); + + // ... + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.markTableSyncable(String, String, String) did NOT throw exception on closed database"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.markTableSyncable(String, String, String) did throw exception on closed database OK", e); + + // SIGNAL that this test must be updated: + //Log.e(ZeteticApplication.TAG, "BEHAVIOR CHANGED - please update the test"); + //return false; + } + + try { + // should throw IllegalStateException [since it calls getVersion()]: + database.needUpgrade(111); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.needUpgrade() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.needUpgrade() did throw exception on closed database OK", e); + } + + /* operations that are NOT expected to throw an exception if the database is closed ([should] not crash) */ + + + /* XXX TODO: these functions should check the db state, + * TBD either throw or simply return false if the db is closed */ + database.yieldIfContended(); + database.yieldIfContendedSafely(); + database.yieldIfContendedSafely(100); + + database.setLockingEnabled(false); + database.setLockingEnabled(true); + + database.inTransaction(); + database.isDbLockedByCurrentThread(); + database.isDbLockedByOtherThreads(); + + database.close(); + + database.isReadOnly(); + database.isOpen(); + + database.isInCompiledSqlCache("SELECT 1;"); + database.purgeFromCompiledSqlCache("SELECT 1;"); + database.resetCompiledSqlCache(); + + database.getMaxSqlCacheSize(); + + try { + // should throw IllegalStateException: + database.setMaxSqlCacheSize(111); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.setMaxSqlCacheSize() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.setMaxSqlCacheSize() did throw exception on closed database OK", e); + } + + } catch (Exception e) { + // Uncaught [unexpected] exception: + Log.e(ZeteticApplication.TAG, "Unexpected exception", e); + return false; + } + + return true; + } + + @Override + public String getName() { + return "Closed Database Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CompileBeginTest.java b/app/src/main/java/net/zetetic/tests/CompileBeginTest.java new file mode 100644 index 0000000..dda3e38 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CompileBeginTest.java @@ -0,0 +1,21 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; + +public class CompileBeginTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + database.compileStatement("begin").execute(); + return true; + }catch (Exception e){ + return false; + } + } + + @Override + public String getName() { + return "Compile Begin Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CompileStatementSyntaxErrorMessageTest.java b/app/src/main/java/net/zetetic/tests/CompileStatementSyntaxErrorMessageTest.java new file mode 100644 index 0000000..15501d6 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CompileStatementSyntaxErrorMessageTest.java @@ -0,0 +1,41 @@ +package net.zetetic.tests; + +import android.database.Cursor; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteStatement; +import android.database.sqlite.SQLiteException; + +import net.zetetic.ZeteticApplication; + +import android.util.Log; + +public class CompileStatementSyntaxErrorMessageTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + SQLiteStatement ignored = database.compileStatement("INSERT INTO mytable (mydata) VALUES"); + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: DID throw SQLiteException", e); + String message = e.getMessage(); + setMessage(message); + // TBD missing error code etc. + if (!message.matches("incomplete input: , while compiling: INSERT INTO mytable \\(mydata\\) VALUES")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT exception message: " + message); + return false; + } + return true; + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: DID throw other exception", e); + return false; + } + + return false; + } + + @Override + public String getName() { + return "Compile statement syntax error message Test"; + } +} diff --git a/src/main/java/net/zetetic/tests/CompiledSQLUpdateTest.java b/app/src/main/java/net/zetetic/tests/CompiledSQLUpdateTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/CompiledSQLUpdateTest.java rename to app/src/main/java/net/zetetic/tests/CompiledSQLUpdateTest.java diff --git a/app/src/main/java/net/zetetic/tests/ComputeKDFTest.java b/app/src/main/java/net/zetetic/tests/ComputeKDFTest.java new file mode 100644 index 0000000..02ba6eb --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/ComputeKDFTest.java @@ -0,0 +1,21 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; + +public class ComputeKDFTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.rawExecSQL("PRAGMA cipher_kdf_compute;"); + String kdf = QueryHelper.singleValueFromQuery(database, "PRAGMA kdf_iter;"); + setMessage(String.format("Computed KDF:%s", kdf)); + return true; + } + + @Override + public String getName() { + return "Compute KDF Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CopyStringToBufferNullTest.java b/app/src/main/java/net/zetetic/tests/CopyStringToBufferNullTest.java new file mode 100644 index 0000000..554c542 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CopyStringToBufferNullTest.java @@ -0,0 +1,36 @@ +package net.zetetic.tests; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class CopyStringToBufferNullTest extends SQLCipherTest { + + private CharArrayBuffer charArrayBuffer = null; + + @Override + public boolean execute(SQLiteDatabase database) { + boolean result = false; + try { + database.execSQL("create table t1(a TEXT, b TEXT);"); + database.execSQL("insert into t1(a,b) values(500, 500);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(0, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + "500".equals(actualValue); + } + } catch (IllegalArgumentException ex){ + result = true; + } finally { + return result; + } + + } + + @Override + public String getName() { + return "Copy String To Buffer Null Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestFloatLargeBuffer.java b/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestFloatLargeBuffer.java new file mode 100644 index 0000000..711a59c --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestFloatLargeBuffer.java @@ -0,0 +1,31 @@ +package net.zetetic.tests; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class CopyStringToBufferTestFloatLargeBuffer extends SQLCipherTest { + + private CharArrayBuffer charArrayBuffer = new CharArrayBuffer(128); + + @Override + public boolean execute(SQLiteDatabase database) { + + database.execSQL("create table t1(a REAL, b REAL);"); + database.execSQL("insert into t1(a,b) values(123.45, 67.89);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(1, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + return "67.89".equals(actualValue); + } + return false; + + } + + @Override + public String getName() { + return "Copy String To Buffer Test Float Large Buffer"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestFloatSmallBuffer.java b/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestFloatSmallBuffer.java new file mode 100644 index 0000000..21d5b16 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestFloatSmallBuffer.java @@ -0,0 +1,31 @@ +package net.zetetic.tests; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class CopyStringToBufferTestFloatSmallBuffer extends SQLCipherTest { + + private CharArrayBuffer charArrayBuffer = new CharArrayBuffer(1); + + @Override + public boolean execute(SQLiteDatabase database) { + + database.execSQL("create table t1(a REAL, b REAL);"); + database.execSQL("insert into t1(a,b) values(123.45, 67.89);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(1, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + return "67.89".equals(actualValue); + } + return false; + + } + + @Override + public String getName() { + return "Copy String To Buffer Test Float Small Buffer"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestIntegerLargeBuffer.java b/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestIntegerLargeBuffer.java new file mode 100644 index 0000000..2dc7fd5 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestIntegerLargeBuffer.java @@ -0,0 +1,31 @@ +package net.zetetic.tests; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class CopyStringToBufferTestIntegerLargeBuffer extends SQLCipherTest { + + private CharArrayBuffer charArrayBuffer = new CharArrayBuffer(128); + + @Override + public boolean execute(SQLiteDatabase database) { + + database.execSQL("create table t1(a INTEGER, b INTEGER);"); + database.execSQL("insert into t1(a,b) values(123, 456);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(1, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + return "456".equals(actualValue); + } + return false; + + } + + @Override + public String getName() { + return "Copy String To Buffer Test Integer Large Buffer"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestIntegerSmallBuffer.java b/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestIntegerSmallBuffer.java new file mode 100644 index 0000000..b9e384c --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestIntegerSmallBuffer.java @@ -0,0 +1,31 @@ +package net.zetetic.tests; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class CopyStringToBufferTestIntegerSmallBuffer extends SQLCipherTest { + + private CharArrayBuffer charArrayBuffer = new CharArrayBuffer(1); + + @Override + public boolean execute(SQLiteDatabase database) { + + database.execSQL("create table t1(a INTEGER, b INTEGER);"); + database.execSQL("insert into t1(a,b) values(123, 456);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(1, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + return "456".equals(actualValue); + } + return false; + + } + + @Override + public String getName() { + return "Copy String To Buffer Test Integer Small Buffer"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestStringLargeBuffer.java b/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestStringLargeBuffer.java new file mode 100644 index 0000000..44b9e5a --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestStringLargeBuffer.java @@ -0,0 +1,30 @@ +package net.zetetic.tests; + +import android.database.CharArrayBuffer; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class CopyStringToBufferTestStringLargeBuffer extends SQLCipherTest { + + private CharArrayBuffer charArrayBuffer = new CharArrayBuffer(128); + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a TEXT, b TEXT);"); + database.execSQL("insert into t1(a,b) values(500, 500);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(0, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + return "500".equals(actualValue); + } + return false; + } + + @Override + public String getName() { + return "Copy String To Buffer Test String Large Buffer"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestStringSmallBuffer.java b/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestStringSmallBuffer.java new file mode 100644 index 0000000..b509e7b --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CopyStringToBufferTestStringSmallBuffer.java @@ -0,0 +1,29 @@ +package net.zetetic.tests; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class CopyStringToBufferTestStringSmallBuffer extends SQLCipherTest { + + private CharArrayBuffer charArrayBuffer = new CharArrayBuffer(1); + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a TEXT, b TEXT);"); + database.execSQL("insert into t1(a,b) values(500, 500);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(0, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + return "500".equals(actualValue); + } + return false; + } + + @Override + public String getName() { + return "Copy String To Buffer Test String Small Buffer"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CorruptDatabaseTest.java b/app/src/main/java/net/zetetic/tests/CorruptDatabaseTest.java new file mode 100644 index 0000000..6369aa0 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CorruptDatabaseTest.java @@ -0,0 +1,89 @@ +package net.zetetic.tests; + +import android.database.Cursor; +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabaseCorruptException; +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class CorruptDatabaseTest extends SQLCipherTest { + + @Override + public TestResult run() { + + TestResult result = new TestResult(getName(), false); + try { + result.setResult(execute(null)); + SQLiteDatabase.releaseMemory(); + } catch (Exception e) { + Log.v(ZeteticApplication.TAG, e.toString()); + } + return result; + } + + @Override + public boolean execute(SQLiteDatabase null_database_ignored) { + + File unencryptedDatabase = ZeteticApplication.getInstance().getDatabasePath("corrupt.db"); + + try { + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("corrupt.db"); + + SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(unencryptedDatabase, "", null); + + // NOTE: database not expected to be null, but double-check: + if (database == null) { + Log.e(TAG, "ERROR: got null database object"); + return false; + } + + // *Should* have been recovered: + Cursor cursor = database.rawQuery("select * from sqlite_master;", null); + + if (cursor == null) { + Log.e(TAG, "NOT EXPECTED: database.rawQuery() returned null cursor"); + return false; + } + + // *Should* corrupt the database file that is already open: + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("corrupt.db"); + + try { + // Attempt to write to corrupt database file *should* fail: + database.execSQL("CREATE TABLE t1(a,b);"); + + // NOT EXPECTED to get here: + Log.e(TAG, "NOT EXPECTED: CREATE TABLE succeeded "); + return false; + } catch (SQLiteDatabaseCorruptException ex) { + Log.v(TAG, "Caught SQLiteDatabaseCorruptException as expected OK"); + } + + // *Expected* to be closed now + if (database.isOpen()) { + Log.e(TAG, "NOT EXPECTED: database is still open"); + return false; + } + + // Try with a null DatabaseErrorHandler: + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("corrupt.db"); + SQLiteDatabase.openOrCreateDatabase(unencryptedDatabase, "", null, null, null); + + return true; + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + return false; + } + finally { + unencryptedDatabase.delete(); + } + } + + @Override + public String getName() { + return "Corrupt Database Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CreateNonEncryptedDatabaseTest.java b/app/src/main/java/net/zetetic/tests/CreateNonEncryptedDatabaseTest.java new file mode 100644 index 0000000..ae32e86 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CreateNonEncryptedDatabaseTest.java @@ -0,0 +1,103 @@ +package net.zetetic.tests; + +import android.database.Cursor; + +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +import net.zetetic.ZeteticApplication; + +import android.util.Log; + +import java.io.File; +import java.io.IOException; + +import java.util.UUID; + +public class CreateNonEncryptedDatabaseTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + + database.close(); + + String unencryptedDatabaseName = "unencrypted-" + UUID.randomUUID().toString() + ".db"; + File unencryptedDatabase = ZeteticApplication.getInstance().getDatabasePath(unencryptedDatabaseName); + + try { + String noPasswordString = ""; + database = SQLiteDatabase.openOrCreateDatabase(unencryptedDatabase, noPasswordString, null); + database.execSQL("CREATE TABLE test_table(test_column);"); + database.execSQL("INSERT INTO test_table VALUES(?);", new Object[]{"test value"}); + database.close(); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception", e); + return false; + } + + try { + char[] nullPassword = null; + database = SQLiteDatabase.openOrCreateDatabase(unencryptedDatabase.getPath(), nullPassword, null, null); + Cursor cursor = database.rawQuery("SELECT * FROM test_table;", new String[]{}); + cursor.moveToFirst(); + String a = cursor.getString(0); + cursor.close(); + database.close(); + if (a == null || !a.equals("test value")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT or MISSING test value"); + return false; + } + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception when reading database with zero-length password", e); + return false; + } + + try { + char[] noPassword = new char[0]; + database = SQLiteDatabase.openOrCreateDatabase(unencryptedDatabase.getPath(), noPassword, null, null); + Cursor cursor = database.rawQuery("SELECT * FROM test_table;", new String[]{}); + cursor.moveToFirst(); + String a = cursor.getString(0); + cursor.close(); + database.close(); + if (a == null || !a.equals("test value")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT or MISSING test value"); + return false; + } + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception when reading database with zero-length password", e); + return false; + } + + try { + SQLiteDatabase.openOrCreateDatabase(unencryptedDatabase, ZeteticApplication.DATABASE_PASSWORD, null); + + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() of unencrypted database with a password did not fail"); + return false; + } catch (SQLiteException e){ + Log.v(ZeteticApplication.TAG, + "SQLiteDatabase.openOrCreateDatabase() of unencrypted database with a password did throw a SQLiteException as expected OK", e); + } catch (Exception e){ + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with invalid password did throw an unexpected exception", e); + return false; + } + + try { + String nullPasswordString = null; + SQLiteDatabase.create(null, nullPasswordString) + .close(); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception", e); + return false; + } + + return true; + } + + @Override + public String getName() { + return "Create Non-Encrypted Database Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/CreateOpenDatabaseWithByteArrayTest.java b/app/src/main/java/net/zetetic/tests/CreateOpenDatabaseWithByteArrayTest.java new file mode 100644 index 0000000..e74ff71 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CreateOpenDatabaseWithByteArrayTest.java @@ -0,0 +1,47 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class CreateOpenDatabaseWithByteArrayTest extends SQLCipherTest { + + private String databaseName = "foo.db"; + + @Override + public boolean execute(SQLiteDatabase database) { + boolean status = false; + database.close(); + byte[] key = generateRandomByteArray(32); + File newDatabasePath = ZeteticApplication.getInstance().getDatabasePath(databaseName); + newDatabasePath.delete(); + database = SQLiteDatabase.openOrCreateDatabase(newDatabasePath.getPath(), key, null); + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{1, 2}); + database.close(); + database = SQLiteDatabase.openOrCreateDatabase(newDatabasePath.getPath(), key, null); + Cursor cursor = database.rawQuery("select * from t1;", null); + if (cursor != null) { + cursor.moveToNext(); + int a = cursor.getInt(0); + int b = cursor.getInt(1); + cursor.close(); + status = a == 1 && b == 2; + } + return status; + } + + @Override + public String getName() { + return "Create/Open with Byte Array Test"; + } + + @Override + protected void tearDown(SQLiteDatabase database) { + super.tearDown(database); + File newDatabasePath = ZeteticApplication.getInstance().getDatabasePath(databaseName); + newDatabasePath.delete(); + } +} diff --git a/src/main/java/net/zetetic/tests/CrossProcessCursorQueryTest.java b/app/src/main/java/net/zetetic/tests/CrossProcessCursorQueryTest.java similarity index 99% rename from src/main/java/net/zetetic/tests/CrossProcessCursorQueryTest.java rename to app/src/main/java/net/zetetic/tests/CrossProcessCursorQueryTest.java index b6d26e3..d40b059 100644 --- a/src/main/java/net/zetetic/tests/CrossProcessCursorQueryTest.java +++ b/app/src/main/java/net/zetetic/tests/CrossProcessCursorQueryTest.java @@ -7,7 +7,7 @@ import net.zetetic.ZeteticContentProvider; public class CrossProcessCursorQueryTest extends SQLCipherTest { - + @Override public boolean execute(SQLiteDatabase database) { diff --git a/src/main/java/net/zetetic/tests/CursorAccessTest.java b/app/src/main/java/net/zetetic/tests/CursorAccessTest.java similarity index 70% rename from src/main/java/net/zetetic/tests/CursorAccessTest.java rename to app/src/main/java/net/zetetic/tests/CursorAccessTest.java index 045fe45..75a2bf5 100644 --- a/src/main/java/net/zetetic/tests/CursorAccessTest.java +++ b/app/src/main/java/net/zetetic/tests/CursorAccessTest.java @@ -7,6 +7,7 @@ import net.zetetic.ZeteticApplication; import java.io.File; +import java.util.Random; public class CursorAccessTest extends SQLCipherTest { @@ -28,11 +29,18 @@ public boolean execute(SQLiteDatabase database) { results.moveToFirst(); int type_a = results.getType(0); int type_b = results.getType(1); + int type_c = results.getType(2); + int type_d = results.getType(3); + int type_e = results.getType(4); results.close(); db.close(); - return (type_a == Cursor.FIELD_TYPE_STRING) && (type_b == Cursor.FIELD_TYPE_INTEGER); + return type_a == Cursor.FIELD_TYPE_STRING && + type_b == Cursor.FIELD_TYPE_INTEGER && + type_c == Cursor.FIELD_TYPE_NULL && + type_d == Cursor.FIELD_TYPE_FLOAT && + type_e == Cursor.FIELD_TYPE_BLOB; } @Override @@ -48,8 +56,10 @@ public MyHelper(Context context) { @Override public void onCreate(SQLiteDatabase database) { - database.execSQL("create table t1(a text,b integer)"); - database.execSQL("insert into t1(a,b) values(?, ?)", new Object[]{"test1", 100}); + database.execSQL("create table t1(a text, b integer, c text, d real, e blob)"); + byte[] data = new byte[10]; + new Random().nextBytes(data); + database.execSQL("insert into t1(a, b, c, d, e) values(?, ?, ?, ?, ?)", new Object[]{"test1", 100, null, 3.25, data}); } @Override diff --git a/app/src/main/java/net/zetetic/tests/CustomCorruptionHandlerTest.java b/app/src/main/java/net/zetetic/tests/CustomCorruptionHandlerTest.java new file mode 100644 index 0000000..cb0a9a9 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/CustomCorruptionHandlerTest.java @@ -0,0 +1,388 @@ +package net.zetetic.tests; + +import android.util.Log; +import net.sqlcipher.DatabaseErrorHandler; +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabaseCorruptException; +import android.database.sqlite.SQLiteException; +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class CustomCorruptionHandlerTest extends SQLCipherTest { + + @Override + public TestResult run() { + + TestResult result = new TestResult(getName(), false); + try { + result.setResult(execute(null)); + SQLiteDatabase.releaseMemory(); + } catch (Exception e) { + Log.v(ZeteticApplication.TAG, e.toString()); + } + return result; + } + + @Override + public boolean execute(SQLiteDatabase null_database_ignored) { + + boolean status = false; + + final File corruptDatabase = ZeteticApplication.getInstance().getDatabasePath("corrupt.db"); + + // ugly trick from: http://stackoverflow.com/questions/5977735/setting-outer-variable-from-anonymous-inner-class + final boolean[] inner_status_slot = new boolean[1]; + + // normal recovery test with custom error handler that actually cleans it up: + try { + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("corrupt.db"); + + // make sure custom onCorruption() function is called: + SQLiteDatabase database = SQLiteDatabase.openDatabase(corruptDatabase.getPath(), "", null, SQLiteDatabase.CREATE_IF_NECESSARY, null, new DatabaseErrorHandler() { + @Override + public void onCorruption(SQLiteDatabase db) { + try { + Log.i(TAG, "Custom onCorruption() called"); + inner_status_slot[0] = true; + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + } + + try { + // clean it up! + corruptDatabase.delete(); + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + inner_status_slot[0] = false; + } + } + }); + + status = inner_status_slot[0]; + + // NOTE: database not expected to be null, but double-check: + if (database == null) { + Log.e(TAG, "ERROR: got null database object"); + return false; + } + + database.close(); + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + return false; + } + finally { + corruptDatabase.delete(); + } + + if (!status) return false; + inner_status_slot[0] = status = false; + + // does not recover due to missing SQLiteDatabase.CREATE_IF_NECESSARY flag: + try { + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("corrupt.db"); + + // make sure custom onCorruption() function is called: + SQLiteDatabase database = SQLiteDatabase.openDatabase(corruptDatabase.getPath(), "", null, 0, null, new DatabaseErrorHandler() { + @Override + public void onCorruption(SQLiteDatabase db) { + try { + Log.i(TAG, "Custom onCorruption() called"); + inner_status_slot[0] = true; + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + } + + try { + // clean it up! + corruptDatabase.delete(); + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + inner_status_slot[0] = false; + } + } + }); + + // should not get here: + Log.e(TAG, "UNEXPECTED RESULT: recovered from corrupt database without SQLiteDatabase.CREATE_IF_NECESSARY flag"); + database.close(); + corruptDatabase.delete(); + return false; + } catch (SQLiteException ex) { + Log.i(TAG, "Could not recover, as expected OK", ex); + //Log.i(TAG, "inner_status_slot[0]: " + inner_status_slot[0]) + status = inner_status_slot[0]; + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + return false; + } + + if (!status) return false; + inner_status_slot[0] = status = false; + + // attempt to recover with custom error handler but database is not cleaned up: + try { + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("corrupt.db"); + + // make sure custom onCorruption() function is called: + SQLiteDatabase database = SQLiteDatabase.openDatabase(corruptDatabase.getPath(), "", null, SQLiteDatabase.CREATE_IF_NECESSARY, null, new DatabaseErrorHandler() { + @Override + public void onCorruption(SQLiteDatabase db) { + try { + inner_status_slot[0] = true; + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + } + } + }); + + // should not get here: + Log.e(TAG, "UNEXPECTED RESULT: recovered from corrupt database that was not cleaned up"); + database.close(); + corruptDatabase.delete(); + return false; + } catch (SQLiteException ex) { + Log.i(TAG, "Could not recover, as expected OK", ex); + status = inner_status_slot[0]; + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + return false; + } + + if (!status) return false; + inner_status_slot[0] = status = false; + + // custom error handler throws: + try { + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("corrupt.db"); + + // make sure custom onCorruption() function is called: + SQLiteDatabase database = SQLiteDatabase.openDatabase(corruptDatabase.getPath(), "", null, SQLiteDatabase.CREATE_IF_NECESSARY, null, new DatabaseErrorHandler() { + @Override + public void onCorruption(SQLiteDatabase db) { + try { + inner_status_slot[0] = true; + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + } + + throw new RuntimeException("abort"); + } + }); + + // should not get here: + Log.e(TAG, "UNEXPECTED RESULT: recovered from corrupt database that was not cleaned up"); + database.close(); + corruptDatabase.delete(); + return false; + } catch (RuntimeException ex) { + Log.v(TAG, "Caught RuntimeException as thrown by custom error handler OK"); + status = inner_status_slot[0]; + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + return false; + } + + if (!status) return false; + inner_status_slot[0] = status = false; + + // extra fun: + final boolean[] inner_fun_slot = new boolean[1]; + + // tell custom error handler NOT to delete corrupt database during sql query: + try { + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("corrupt.db"); + + // make sure custom onCorruption() function is called: + SQLiteDatabase database = SQLiteDatabase.openDatabase(corruptDatabase.getPath(), "", null, SQLiteDatabase.CREATE_IF_NECESSARY, null, new DatabaseErrorHandler() { + @Override + public void onCorruption(SQLiteDatabase db) { + try { + Log.i(TAG, "Custom onCorruption() called"); + inner_status_slot[0] = true; + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + } + + if (inner_fun_slot[0]) return; + + try { + // clean it up! + corruptDatabase.delete(); + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + inner_status_slot[0] = false; + } + } + }); + + // NOTE: database not expected to be null, but double-check: + if (database == null) { + Log.e(TAG, "ERROR: got null database object"); + return false; + } + + if (!inner_status_slot[0]) { + Log.e(TAG, "ERROR: Custom onCorruption() NOT called"); + database.close(); + return false; + } + + // tell custom error handler NOT to delete database: + inner_fun_slot[0] = true; + + // *Should* corrupt the database file that is already open: + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("corrupt.db"); + + try { + // Attempt to write to corrupt database file *should* fail: + database.execSQL("CREATE TABLE t1(a,b);"); + + // NOT EXPECTED to get here: + Log.e(TAG, "NOT EXPECTED: CREATE TABLE succeeded "); + return false; + } catch (SQLiteDatabaseCorruptException ex) { + Log.v(TAG, "Caught SQLiteDatabaseCorruptException as expected OK"); + } + + // NOTE: the database is NOT closed, try it again + if (!database.isOpen()) { + Log.e(TAG, "BEHAVIOR CHANGED: database was closed after sql encountered corruption but error handler did not delete the database"); + return false; + } + + // custom error handler back to normal: + inner_fun_slot[0] = false; + + try { + // Attempt to write to corrupt database file *should* fail: + database.execSQL("CREATE TABLE t1(a,b);"); + + // NOT EXPECTED to get here: + Log.e(TAG, "NOT EXPECTED: CREATE TABLE succeeded "); + return false; + } catch (SQLiteDatabaseCorruptException ex) { + Log.v(TAG, "Caught SQLiteDatabaseCorruptException as expected OK"); + } + + // For some reason, database is still open here. + if (!database.isOpen()) { + Log.e(TAG, "BEHAVIOR CHANGED: database closed here"); + return false; + } + + database.close(); + + status = true; + + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + return false; + } + finally { + corruptDatabase.delete(); + } + + if (!status) return false; + inner_status_slot[0] = status = false; + inner_fun_slot[0] = false; + + // tell custom error handler to throw runtime error during sql query: + try { + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("corrupt.db"); + + // make sure custom onCorruption() function is called: + SQLiteDatabase database = SQLiteDatabase.openDatabase(corruptDatabase.getPath(), "", null, SQLiteDatabase.CREATE_IF_NECESSARY, null, new DatabaseErrorHandler() { + @Override + public void onCorruption(SQLiteDatabase db) { + try { + Log.i(TAG, "Custom onCorruption() called"); + inner_status_slot[0] = true; + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + } + + if (inner_fun_slot[0]) throw new RuntimeException("abort"); + + try { + // clean it up! + corruptDatabase.delete(); + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + inner_status_slot[0] = false; + } + } + }); + + // NOTE: database not expected to be null, but double-check: + if (database == null) { + Log.e(TAG, "ERROR: got null database object"); + return false; + } + + if (!inner_status_slot[0]) { + Log.e(TAG, "ERROR: Custom onCorruption() NOT called"); + database.close(); + return false; + } + + // tell custom error handler NOT to delete database: + inner_fun_slot[0] = true; + + // *Should* corrupt the database file that is already open: + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("corrupt.db"); + + try { + // Attempt to write to corrupt database file *should* fail: + database.execSQL("CREATE TABLE t1(a,b);"); + + // NOT EXPECTED to get here: + Log.e(TAG, "NOT EXPECTED: CREATE TABLE succeeded "); + return false; + } catch (RuntimeException ex) { + Log.v(TAG, "Caught RuntimeException as thrown by custom error handler OK"); + } + + // NOTE: the database is NOT expected to closed + if (!database.isOpen()) { + Log.e(TAG, "BEHAVIOR CHANGED: database was closed after sql encountered corruption but error handler did not delete the database"); + return false; + } + + database.close(); + + status = true; + + } catch (Exception ex) { + // Uncaught exception (not expected): + Log.e(TAG, "UNEXPECTED EXCEPTION", ex); + return false; + } + finally { + corruptDatabase.delete(); + } + + return status; + } + + @Override + public String getName() { + return "Custom Corruption Handler Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/DefaultCursorWindowAllocationTest.java b/app/src/main/java/net/zetetic/tests/DefaultCursorWindowAllocationTest.java new file mode 100644 index 0000000..ee59c62 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/DefaultCursorWindowAllocationTest.java @@ -0,0 +1,47 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class DefaultCursorWindowAllocationTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + try { + int rowCount = 0; + int rows = 10000; + //final int dataSize = 2048; + final int dataSize = 16384; + buildDatabase(database, rows, 1, new RowColumnValueBuilder() { + @Override + public Object buildRowColumnValue(String[] columns, int row, int column) { + return generateRandomByteArray(dataSize); + } + }); + rows = 1; + Cursor cursor = database.rawQuery("SELECT count(length(a)) FROM t1;", new Object[]{}); + if(cursor == null) return false; + while(cursor.moveToNext()){ + //byte[] data = cursor.getBlob(0); + int size = cursor.getInt(0); + cursor.close(); +// if(data.length != dataSize) { +// cursor.close(); +// return false; +// } + rowCount++; + } + cursor.close(); + return rowCount == rows; + } catch (Exception e){ + String message = String.format("Error:%s", e.getMessage()); + log(message); + setMessage(message); + return false; + } + } + + @Override + public String getName() { + return "Default cursor window allocation test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/DeleteFunctionTest.java b/app/src/main/java/net/zetetic/tests/DeleteFunctionTest.java new file mode 100644 index 0000000..94f775e --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/DeleteFunctionTest.java @@ -0,0 +1,45 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class DeleteFunctionTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + + boolean status = false; + database.rawExecSQL("create table t1(itemId TEXT UNIQUE, userId INTEGER);"); + database.execSQL("insert into t1(itemId, userId) values(?, ?);", new Object[]{"123", 456}); + int currentCount = getCurrentCount(database, "123", 456); + if(currentCount != 1) { + setMessage("Initial insert failed"); + return status; + } + database.delete("t1", "itemId = ? and userId = ?", new String[]{"123", String.valueOf(456)}); + currentCount = getCurrentCount(database, "123", 456); + status = currentCount == 0; + return status; + } + + private int getCurrentCount(SQLiteDatabase database, String itemId, int userId) { + int count = 0; + try { + Cursor cursor = database.rawQuery("select count(*) from t1 where itemId = ? and userId = ?;", + new String[]{itemId, String.valueOf(userId)}); + if(cursor != null){ + cursor.moveToFirst(); + count = cursor.getInt(0); + cursor.close(); + } + } + catch (Exception ex){} + finally { + return count; + } + } + + @Override + public String getName() { + return "Delete Function Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/DeleteTableWithNullWhereArgsTest.java b/app/src/main/java/net/zetetic/tests/DeleteTableWithNullWhereArgsTest.java new file mode 100644 index 0000000..6419fcb --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/DeleteTableWithNullWhereArgsTest.java @@ -0,0 +1,20 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; + +public class DeleteTableWithNullWhereArgsTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + String[] args = null; + int rowsDeleted = 0; + database.rawExecSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{1, 2}); + rowsDeleted = database.delete("t1", "a = 1", args); + return rowsDeleted == 1; + } + + @Override + public String getName() { + return "Delete Table Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/DisableWriteAheadLoggingTest.java b/app/src/main/java/net/zetetic/tests/DisableWriteAheadLoggingTest.java new file mode 100644 index 0000000..1f663c2 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/DisableWriteAheadLoggingTest.java @@ -0,0 +1,25 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; + +public class DisableWriteAheadLoggingTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + boolean result = database.enableWriteAheadLogging(); + String currentMode = getJournalModeState(database); + if(!result || !currentMode.equals("wal")) return false; + database.disableWriteAheadLogging(); + currentMode = getJournalModeState(database); + return currentMode.equals("delete"); + } + + private String getJournalModeState(SQLiteDatabase database){ + return QueryHelper.singleValueFromQuery(database, "PRAGMA journal_mode;"); + } + + @Override + public String getName() { + return "Disable WAL mode"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/EnableForeignKeyConstraintsTest.java b/app/src/main/java/net/zetetic/tests/EnableForeignKeyConstraintsTest.java new file mode 100644 index 0000000..9b963e3 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/EnableForeignKeyConstraintsTest.java @@ -0,0 +1,25 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; + +public class EnableForeignKeyConstraintsTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + String initialState = getForeignKeyState(database); + if(!initialState.equals("0")) return false; + database.setForeignKeyConstraintsEnabled(true); + String currentState = getForeignKeyState(database); + if(!currentState.equals("1")) return false; + return true; + } + + private String getForeignKeyState(SQLiteDatabase database){ + return QueryHelper.singleValueFromQuery(database, "PRAGMA foreign_keys"); + } + + @Override + public String getName() { + return "Enable Foreign Key Constraints"; + } +} diff --git a/src/main/java/net/zetetic/tests/EnableForeignKeySupportTest.java b/app/src/main/java/net/zetetic/tests/EnableForeignKeySupportTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/EnableForeignKeySupportTest.java rename to app/src/main/java/net/zetetic/tests/EnableForeignKeySupportTest.java diff --git a/app/src/main/java/net/zetetic/tests/EnableWriteAheadLoggingTest.java b/app/src/main/java/net/zetetic/tests/EnableWriteAheadLoggingTest.java new file mode 100644 index 0000000..47d5ff3 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/EnableWriteAheadLoggingTest.java @@ -0,0 +1,18 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; + +public class EnableWriteAheadLoggingTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + boolean result = database.enableWriteAheadLogging(); + String currentMode = QueryHelper.singleValueFromQuery(database, "PRAGMA journal_mode;"); + return result && currentMode.equals("wal"); + } + + @Override + public String getName() { + return "Enable WAL mode"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/ExecuteInsertConstraintErrorMessageTest.java b/app/src/main/java/net/zetetic/tests/ExecuteInsertConstraintErrorMessageTest.java new file mode 100644 index 0000000..a14bcdc --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/ExecuteInsertConstraintErrorMessageTest.java @@ -0,0 +1,43 @@ +package net.zetetic.tests; + +import android.database.Cursor; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteStatement; +import android.database.sqlite.SQLiteConstraintException; + +import net.zetetic.ZeteticApplication; + +import android.util.Log; + +public class ExecuteInsertConstraintErrorMessageTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("CREATE TABLE tt(a UNIQUE, b)"); + database.execSQL("INSERT INTO tt VALUES (101, 'Alice')"); + try { + SQLiteStatement insertStatement = database.compileStatement("INSERT INTO tt VALUES (101, 'Betty')"); + long ignored = insertStatement.executeInsert(); + } catch (SQLiteConstraintException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: DID throw SQLiteConstraintException", e); + String message = e.getMessage(); + setMessage(message); + if (!message.matches("error code 19 \\(extended error code 2067\\): UNIQUE constraint failed: tt\\.a")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT exception message: " + message); + return false; + } + return true; + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: DID throw other exception", e); + return false; + } + + return false; + } + + @Override + public String getName() { + return "Execute insert constraint error message Test"; + } +} diff --git a/src/main/java/net/zetetic/tests/ExportToUnencryptedDatabase.java b/app/src/main/java/net/zetetic/tests/ExportToUnencryptedDatabase.java similarity index 82% rename from src/main/java/net/zetetic/tests/ExportToUnencryptedDatabase.java rename to app/src/main/java/net/zetetic/tests/ExportToUnencryptedDatabase.java index c80fa20..0577f91 100644 --- a/src/main/java/net/zetetic/tests/ExportToUnencryptedDatabase.java +++ b/app/src/main/java/net/zetetic/tests/ExportToUnencryptedDatabase.java @@ -17,23 +17,17 @@ public boolean execute(SQLiteDatabase database) { database.close(); ZeteticApplication.getInstance().deleteDatabase(ZeteticApplication.DATABASE_NAME); File databaseFile = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.DATABASE_NAME); - SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { - public void preKey(SQLiteDatabase sqLiteDatabase) { - sqLiteDatabase.rawExecSQL("PRAGMA cipher_default_use_hmac = off;"); - } - public void postKey(SQLiteDatabase sqLiteDatabase) {} - }; - database = SQLiteDatabase.openOrCreateDatabase(databaseFile, ZeteticApplication.DATABASE_PASSWORD, null, hook); - + database = SQLiteDatabase.openOrCreateDatabase(databaseFile, ZeteticApplication.DATABASE_PASSWORD, null); database.rawExecSQL("create table t1(a,b);"); database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", "two for the show"}); unencryptedFile = ZeteticApplication.getInstance().getDatabasePath("plaintext.db"); + ZeteticApplication.getInstance().deleteDatabase("plaintext.db"); database.rawExecSQL(String.format("ATTACH DATABASE '%s' as plaintext KEY '';", unencryptedFile.getAbsolutePath())); database.rawExecSQL("SELECT sqlcipher_export('plaintext');"); database.rawExecSQL("DETACH DATABASE plaintext;"); - SQLiteDatabase unencryptedDatabase = SQLiteDatabase.openOrCreateDatabase(unencryptedFile, "", null, hook); + SQLiteDatabase unencryptedDatabase = SQLiteDatabase.openOrCreateDatabase(unencryptedFile, "", null, null); Cursor cursor = unencryptedDatabase.rawQuery("select * from t1;", new String[]{}); String a = ""; String b = ""; diff --git a/app/src/main/java/net/zetetic/tests/FIPSTest.java b/app/src/main/java/net/zetetic/tests/FIPSTest.java new file mode 100644 index 0000000..19b729f --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/FIPSTest.java @@ -0,0 +1,22 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; + +public class FIPSTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + + String version = QueryHelper.singleValueFromQuery(database, "PRAGMA cipher_version;"); + setMessage(String.format("SQLCipher version:%s", version)); + int expectedValue = version.contains("FIPS") ? 1 : 0; + + int status = QueryHelper.singleIntegerValueFromQuery(database, "PRAGMA cipher_fips_status;"); + return status == expectedValue; + } + + @Override + public String getName() { + return "FIPS Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/FTS5Test.java b/app/src/main/java/net/zetetic/tests/FTS5Test.java new file mode 100644 index 0000000..ec2f6fe --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/FTS5Test.java @@ -0,0 +1,24 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class FTS5Test extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("CREATE VIRTUAL TABLE email USING fts5(sender, title, body);"); + database.execSQL("insert into email(sender, title, body) values(?, ?, ?);", + new Object[]{"foo@bar.com", "Test Email", "This is a test email message."}); + Cursor cursor = database.rawQuery("select * from email where email match ?;", new String[]{"test"}); + if(cursor != null){ + cursor.moveToFirst(); + return cursor.getString(cursor.getColumnIndex("sender")).equals("foo@bar.com"); + } + return false; + } + + @Override + public String getName() { + return "FTS5 Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/FixedCursorWindowAllocationTest.java b/app/src/main/java/net/zetetic/tests/FixedCursorWindowAllocationTest.java new file mode 100644 index 0000000..ad2e551 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/FixedCursorWindowAllocationTest.java @@ -0,0 +1,51 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.CursorWindow; +import net.sqlcipher.CursorWindowAllocation; +import net.sqlcipher.CustomCursorWindowAllocation; +import net.sqlcipher.database.SQLiteDatabase; + +public class FixedCursorWindowAllocationTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + int rowCount = 0; + int rows = 100; + long allocationSize = 1024; + final int dataSize = 491; + CursorWindowAllocation fixedAllocation = new CustomCursorWindowAllocation(allocationSize, 0, allocationSize); + CursorWindow.setCursorWindowAllocation(fixedAllocation); + buildDatabase(database, rows, 1, new RowColumnValueBuilder() { + @Override + public Object buildRowColumnValue(String[] columns, int row, int column) { + return generateRandomByteArray(dataSize); + } + }); + + Cursor cursor = database.rawQuery("SELECT * FROM t1;", new Object[]{}); + if(cursor == null) return false; + while(cursor.moveToNext()){ + byte[] data = cursor.getBlob(0); + if(data.length != dataSize) { + cursor.close(); + return false; + } + rowCount++; + } + cursor.close(); + return rowCount == rows; + } catch (Exception e){ + String message = String.format("Error:%s", e.getMessage()); + log(message); + setMessage(message); + return false; + } + } + + @Override + public String getName() { + return "Small Cursor Window Allocation Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/ForeignKeyConstraintsEnabledWithTransactionTest.java b/app/src/main/java/net/zetetic/tests/ForeignKeyConstraintsEnabledWithTransactionTest.java new file mode 100644 index 0000000..af17ac7 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/ForeignKeyConstraintsEnabledWithTransactionTest.java @@ -0,0 +1,23 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; + +public class ForeignKeyConstraintsEnabledWithTransactionTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.beginTransaction(); + try { + database.setForeignKeyConstraintsEnabled(true); + }catch (IllegalStateException ex){ + if(ex.getMessage().equals("Foreign key constraints may not be changed while in a transaction")) { + return true; + } + } + return false; + } + + @Override + public String getName() { + return "Disallow foreign key constraints in transaction"; + } +} diff --git a/src/main/java/net/zetetic/tests/FullTextSearchTest.java b/app/src/main/java/net/zetetic/tests/FullTextSearchTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/FullTextSearchTest.java rename to app/src/main/java/net/zetetic/tests/FullTextSearchTest.java diff --git a/app/src/main/java/net/zetetic/tests/GetAttachedDatabasesTest.java b/app/src/main/java/net/zetetic/tests/GetAttachedDatabasesTest.java new file mode 100644 index 0000000..3db50a2 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/GetAttachedDatabasesTest.java @@ -0,0 +1,29 @@ +package net.zetetic.tests; + +import android.util.Pair; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.ZeteticApplication; + +import java.io.File; +import java.util.List; +import java.util.UUID; + +public class GetAttachedDatabasesTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + UUID id = UUID.randomUUID(); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(id.toString()); + List> attached = database.getAttachedDbs(); + boolean initialAttach = attached.size() == 1 && attached.get(0).first.equals("main"); + database.execSQL("ATTACH database ? as foo;", + new Object[]{databasePath.getAbsolutePath()}); + attached = database.getAttachedDbs(); + return initialAttach && attached.size() == 2; + } + + @Override + public String getName() { + return "Get Attached Databases Test"; + } +} diff --git a/src/main/java/net/zetetic/tests/GetTypeFromCrossProcessCursorWrapperTest.java b/app/src/main/java/net/zetetic/tests/GetTypeFromCrossProcessCursorWrapperTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/GetTypeFromCrossProcessCursorWrapperTest.java rename to app/src/main/java/net/zetetic/tests/GetTypeFromCrossProcessCursorWrapperTest.java diff --git a/app/src/main/java/net/zetetic/tests/GrowingCursorWindowAllocationTest.java b/app/src/main/java/net/zetetic/tests/GrowingCursorWindowAllocationTest.java new file mode 100644 index 0000000..3773884 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/GrowingCursorWindowAllocationTest.java @@ -0,0 +1,53 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.CursorWindow; +import net.sqlcipher.CursorWindowAllocation; +import net.sqlcipher.CustomCursorWindowAllocation; +import net.sqlcipher.database.SQLiteDatabase; + +public class GrowingCursorWindowAllocationTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + try { + int rowCount = 0; + int rows = 20000; + long intialAllocationSize = 128 * 1024; + long growthAllocationSize = 1024 * 1024; + long maxAllocationSize = 4 * 1024 * 1024; + final int dataSize = 2000; + CursorWindowAllocation fixedAllocation = + new CustomCursorWindowAllocation(intialAllocationSize, growthAllocationSize, maxAllocationSize); + CursorWindow.setCursorWindowAllocation(fixedAllocation); + buildDatabase(database, rows, 1, new RowColumnValueBuilder() { + @Override + public Object buildRowColumnValue(String[] columns, int row, int column) { + return generateRandomByteArray(dataSize); + } + }); + + Cursor cursor = database.rawQuery("SELECT * FROM t1;", new Object[]{}); + if(cursor == null) return false; + while(cursor.moveToNext()){ + byte[] data = cursor.getBlob(0); + if(data.length != dataSize) { + cursor.close(); + return false; + } + rowCount++; + } + cursor.close(); + return rowCount == rows; + } catch (Exception e){ + String message = String.format("Error:%s", e.getMessage()); + log(message); + setMessage(message); + return false; + } + } + + @Override + public String getName() { + return "Growing Cursor Window Allocation Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/ICUTest.java b/app/src/main/java/net/zetetic/tests/ICUTest.java new file mode 100644 index 0000000..973f3c1 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/ICUTest.java @@ -0,0 +1,25 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class ICUTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + String expected = "КАКОЙ-ТО КИРИЛЛИЧЕСКИЙ ТЕКСТ"; // SOME Cyrillic TEXT + String actual = ""; + + Cursor result = database.rawQuery("select UPPER('Какой-то кириллический текст') as u1", new String[]{}); + if (result != null) { + result.moveToFirst(); + actual = result.getString(0); + result.close(); + } + return actual.equals(expected); + } + + @Override + public String getName() { + return "ICU Test"; + } +} diff --git a/src/main/java/net/zetetic/tests/ImportUnencryptedDatabaseTest.java b/app/src/main/java/net/zetetic/tests/ImportUnencryptedDatabaseTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/ImportUnencryptedDatabaseTest.java rename to app/src/main/java/net/zetetic/tests/ImportUnencryptedDatabaseTest.java diff --git a/app/src/main/java/net/zetetic/tests/InsertWithOnConflictTest.java b/app/src/main/java/net/zetetic/tests/InsertWithOnConflictTest.java new file mode 100644 index 0000000..65c9d48 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/InsertWithOnConflictTest.java @@ -0,0 +1,23 @@ +package net.zetetic.tests; + +import android.content.ContentValues; +import net.sqlcipher.database.SQLiteDatabase; + +public class InsertWithOnConflictTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table user(_id integer primary key autoincrement, email text unique not null);"); + ContentValues values = new ContentValues(); + values.put("email", "foo@bar.com"); + long id = database.insertWithOnConflict("user", null, values, + SQLiteDatabase.CONFLICT_IGNORE); + long error = database.insertWithOnConflict("user", null, values, + SQLiteDatabase.CONFLICT_IGNORE); + return id == 1 && error == -1; + } + + @Override + public String getName() { + return "Insert with OnConflict"; + } +} diff --git a/src/main/java/net/zetetic/tests/InterprocessBlobQueryTest.java b/app/src/main/java/net/zetetic/tests/InterprocessBlobQueryTest.java similarity index 88% rename from src/main/java/net/zetetic/tests/InterprocessBlobQueryTest.java rename to app/src/main/java/net/zetetic/tests/InterprocessBlobQueryTest.java index 43894b6..e47cf15 100644 --- a/src/main/java/net/zetetic/tests/InterprocessBlobQueryTest.java +++ b/app/src/main/java/net/zetetic/tests/InterprocessBlobQueryTest.java @@ -14,7 +14,7 @@ public boolean execute(SQLiteDatabase database) { Activity activity = ZeteticApplication.getInstance().getCurrentActivity(); Uri providerUri = ZeteticContentProvider2.CONTENT_URI; - android.database.Cursor cursor = activity.managedQuery(providerUri, null, null, null, null); + android.database.Cursor cursor = activity.getContentResolver().query(providerUri, null, null, null, null); StringBuilder buffer = new StringBuilder(); while (cursor.moveToNext()) { buffer.append(cursor.getString(0)); diff --git a/app/src/main/java/net/zetetic/tests/InvalidOpenArgumentTest.java b/app/src/main/java/net/zetetic/tests/InvalidOpenArgumentTest.java new file mode 100644 index 0000000..5123a01 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/InvalidOpenArgumentTest.java @@ -0,0 +1,84 @@ +package net.zetetic.tests; + +import android.util.Log; + +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class InvalidOpenArgumentTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.close(); + + try { + String nullPathString = null; + database = SQLiteDatabase.openOrCreateDatabase(nullPathString, ZeteticApplication.DATABASE_PASSWORD, null); + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with null path string did not fail"); + return false; + } catch (IllegalArgumentException e){ + Log.v(ZeteticApplication.TAG, + "SQLiteDatabase.openOrCreateDatabase() with null path string did throw an IllegalArgumentException as expected OK", e); + } catch (Exception e){ + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with null path string did throw an unexpected exception type", e); + return false; + } + + try { + String nullPathString = null; + database = SQLiteDatabase.openOrCreateDatabase(nullPathString, ZeteticApplication.DATABASE_PASSWORD, null, null, null); + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with null path string did not fail"); + return false; + } catch (IllegalArgumentException e){ + Log.v(ZeteticApplication.TAG, + "SQLiteDatabase.openOrCreateDatabase() with null path string did throw an IllegalArgumentException as expected OK", e); + } catch (Exception e){ + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with null path string did throw an unexpected exception type", e); + return false; + } + + try { + File nullFile = null; + database = SQLiteDatabase.openOrCreateDatabase(nullFile, ZeteticApplication.DATABASE_PASSWORD, null); + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with null file did not fail"); + return false; + } catch (IllegalArgumentException e){ + Log.v(ZeteticApplication.TAG, + "EXPECTED RESULT: SQLiteDatabase.openOrCreateDatabase() with null file did throw an IllegalArgumentException OK", e); + } catch (Exception e){ + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with no password did throw an unexpected exception type", e); + return false; + } + + try { + File nullFile = null; + database = SQLiteDatabase.openOrCreateDatabase(nullFile, ZeteticApplication.DATABASE_PASSWORD, null, null, null); + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with null file did not fail"); + return false; + } catch (IllegalArgumentException e){ + Log.v(ZeteticApplication.TAG, + "EXPECTED RESULT: SQLiteDatabase.openOrCreateDatabase() with null file did throw an IllegalArgumentException OK", e); + } catch (Exception e){ + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with null file did throw an unexpected exception type", e); + return false; + } + + return true; + } + + @Override + public String getName() { + return "Invalid Open Argument Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/InvalidPasswordTest.java b/app/src/main/java/net/zetetic/tests/InvalidPasswordTest.java new file mode 100644 index 0000000..3061901 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/InvalidPasswordTest.java @@ -0,0 +1,69 @@ +package net.zetetic.tests; + +import android.util.Log; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import android.database.sqlite.SQLiteException; + +import net.zetetic.ZeteticApplication; + +import java.io.File; +import java.util.UUID; + +public class InvalidPasswordTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.rawExecSQL("create table t1(a,b);"); + database.close(); + + File databaseFile = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.DATABASE_NAME); + + try { + SQLiteDatabase.openOrCreateDatabase(databaseFile, UUID.randomUUID().toString(), null); + + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with invalid password did not fail"); + return false; + } catch (SQLiteException e){ + Log.v(ZeteticApplication.TAG, + "SQLiteDatabase.openOrCreateDatabase() with invalid password did throw a SQLiteException as expected OK", e); + } catch (Exception e){ + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with invalid password did throw an unexpected exception", e); + return false; + } + + try { + String noPassword = ""; + SQLiteDatabase.openOrCreateDatabase(databaseFile, noPassword, null); + + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with no password did NOT fail on an encrypted database"); + return false; + } catch (SQLiteException e){ + Log.v(ZeteticApplication.TAG, + "SQLiteDatabase.openOrCreateDatabase() with no password did throw a SQLiteException as expected OK", e); + } catch (Exception e){ + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with no password did throw an unexpected exception type", e); + return false; + } + + try { + database = SQLiteDatabase.openOrCreateDatabase(databaseFile, ZeteticApplication.DATABASE_PASSWORD, null); + database.execSQL("insert into t1(a,b) values(?, ?)", new Object[]{"testing", "123"}); + } catch (Exception e){ + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: attempt to access database with correct password did throw an unexpected exception", e); + return false; + } + + return true; + } + + @Override + public String getName() { + return "Invalid Password Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/JavaClientLibraryVersionTest.java b/app/src/main/java/net/zetetic/tests/JavaClientLibraryVersionTest.java new file mode 100644 index 0000000..79ac869 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/JavaClientLibraryVersionTest.java @@ -0,0 +1,20 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; + +public class +JavaClientLibraryVersionTest extends SQLCipherTest { + + private final String EXPECTED_SQLCIPHER_ANDROID_VERSION = "4.5.4"; + + @Override + public boolean execute(SQLiteDatabase database) { + setMessage(String.format("Report:%s", SQLiteDatabase.SQLCIPHER_ANDROID_VERSION)); + return SQLiteDatabase.SQLCIPHER_ANDROID_VERSION.equals(EXPECTED_SQLCIPHER_ANDROID_VERSION); + } + + @Override + public String getName() { + return "Java Client Library Version Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/JsonCastTest.java b/app/src/main/java/net/zetetic/tests/JsonCastTest.java new file mode 100644 index 0000000..e7ae516 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/JsonCastTest.java @@ -0,0 +1,23 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class JsonCastTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + String name = "Bob Smith", queryName = ""; + String query = String.format("select cast(json_extract('{\"user\":\"%s\"}','$.user') as TEXT);", name); + Cursor cursor = database.rawQuery(query, new Object[]{}); + if(cursor != null && cursor.moveToFirst()){ + queryName = cursor.getString(0); + cursor.close(); + } + return name.equals(queryName); + } + + @Override + public String getName() { + return "JSON cast test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/LargeDatabaseCursorAccessTest.java b/app/src/main/java/net/zetetic/tests/LargeDatabaseCursorAccessTest.java new file mode 100644 index 0000000..87d3227 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/LargeDatabaseCursorAccessTest.java @@ -0,0 +1,102 @@ +package net.zetetic.tests; + +import android.util.Log; + +import net.sqlcipher.Cursor; +import net.sqlcipher.CursorWindow; +import net.sqlcipher.CustomCursorWindowAllocation; +import net.sqlcipher.database.SQLiteDatabase; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class LargeDatabaseCursorAccessTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + int rowCount = 1000; + long windowAllocationSize = 1024 * 1024 / 20; + buildDatabase(database, rowCount, 30, new RowColumnValueBuilder() { + @Override + public Object buildRowColumnValue(String[] columns, int row, int column) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + String columnName = columns[column]; + String value = String.format("%s%d", columnName, row); + return digest.digest(value.getBytes("UTF-8")); + } catch (Exception e) { + Log.e(TAG, e.toString()); + return null; + } + } + }); + + Integer[] randomRows = generateRandomNumbers(rowCount, rowCount); + CursorWindow.setCursorWindowAllocation(new CustomCursorWindowAllocation(windowAllocationSize, 0, windowAllocationSize)); + Cursor cursor = database.rawQuery("SELECT * FROM t1;", null); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + int row = 0; + Log.i(TAG, "Walking cursor forward"); + while(cursor.moveToNext()){ + if (!CompareDigestForAllColumns(cursor, digest, row)) return false; + row++; + } + Log.i(TAG, "Walking cursor backward"); + while(cursor.moveToPrevious()){ + row--; + if (!CompareDigestForAllColumns(cursor, digest, row)) return false; + } + Log.i(TAG, "Walking cursor randomly"); + for(int randomRow : randomRows){ + cursor.moveToPosition(randomRow); + if (!CompareDigestForAllColumns(cursor, digest, randomRow)) return false; + } + + } catch (Exception e){ + return false; + } + return true; + } + + @Override + public String getName() { + return "Large Database Cursor Access Test"; + } + + private boolean CompareDigestForAllColumns(Cursor cursor, MessageDigest digest, int row) throws UnsupportedEncodingException { + int columnCount = cursor.getColumnCount(); + for(int column = 0; column < columnCount; column++){ + Log.i(TAG, String.format("Comparing SHA-1 digest for row:%d", row)); + String columnName = cursor.getColumnName(column); + byte[] actual = cursor.getBlob(column); + String value = String.format("%s%d", columnName, row); + byte[] expected = digest.digest(value.getBytes("UTF-8")); + if(!Arrays.equals(actual, expected)){ + Log.e(TAG, String.format("SHA-1 digest mismatch for row:%d column:%d", row, column)); + return false; + } + } + return true; + } + + private Integer[] generateRandomNumbers(int max, int times){ + SecureRandom random = new SecureRandom(); + List numbers = new ArrayList<>(); + for(int index = 0; index < times; index++){ + boolean alreadyExists; + do { + int value = random.nextInt(max); + alreadyExists = numbers.contains(value); + if(!alreadyExists){ + numbers.add(value); + } + } while(alreadyExists); + } + return numbers.toArray(new Integer[0]); + } +} diff --git a/src/main/java/net/zetetic/tests/LoopingCountQueryTest.java b/app/src/main/java/net/zetetic/tests/LoopingCountQueryTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/LoopingCountQueryTest.java rename to app/src/main/java/net/zetetic/tests/LoopingCountQueryTest.java diff --git a/app/src/main/java/net/zetetic/tests/LoopingInsertTest.java b/app/src/main/java/net/zetetic/tests/LoopingInsertTest.java new file mode 100644 index 0000000..5079603 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/LoopingInsertTest.java @@ -0,0 +1,27 @@ +package net.zetetic.tests; + +import android.util.Log; + +import net.sqlcipher.database.SQLiteDatabase; + +public class LoopingInsertTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("CREATE TABLE some_table(name TEXT, surname TEXT);"); + long startTime = System.currentTimeMillis(); + database.execSQL("begin;"); + for(int index = 0; index < 10000; index++){ + database.execSQL("insert into some_table(name, surname) values(?, ?)", + new Object[]{"one for the money", "two for the show"}); + } + database.execSQL("commit;"); + long diff = System.currentTimeMillis() - startTime; + Log.e(TAG, String.format("Inserted in: %d ms", diff)); + return true; + } + + @Override + public String getName() { + return "Looping Insert Test"; + } +} diff --git a/src/main/java/net/zetetic/tests/LoopingQueryTest.java b/app/src/main/java/net/zetetic/tests/LoopingQueryTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/LoopingQueryTest.java rename to app/src/main/java/net/zetetic/tests/LoopingQueryTest.java diff --git a/app/src/main/java/net/zetetic/tests/MUTF8ToUTF8WithNullMigrationTest.java b/app/src/main/java/net/zetetic/tests/MUTF8ToUTF8WithNullMigrationTest.java new file mode 100644 index 0000000..d36ec7c --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/MUTF8ToUTF8WithNullMigrationTest.java @@ -0,0 +1,61 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.zetetic.ZeteticApplication; + +import java.io.File; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class MUTF8ToUTF8WithNullMigrationTest extends SQLCipherTest { + + int keyCount = 0; + String filename = "mutf8.db"; + char[] password = new char[]{'t', 'e', 's', 't', '\u0000', '1', '2', '3'}; + SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { + public void preKey(SQLiteDatabase database) { + keyCount++; + } + public void postKey(SQLiteDatabase database) { + database.execSQL("PRAGMA kdf_iter = 64000;"); + database.execSQL("PRAGMA cipher_page_size = 1024;"); + database.execSQL("PRAGMA cipher_hmac_algorithm = HMAC_SHA1;"); + database.execSQL("PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1;"); + } + }; + + @Override + public boolean execute(SQLiteDatabase database) { + + boolean status = false; + database.close(); + try { + File databaseFile = ZeteticApplication.getInstance().getDatabasePath(filename); + if(databaseFile.exists()){ + databaseFile.delete(); + } + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory(filename); + database = SQLiteDatabase.openDatabase(databaseFile.getAbsolutePath(), password, null, + SQLiteDatabase.OPEN_READWRITE, hook); + if (keyCount != 2) { + return status; + } + database.close(); + keyCount = 0; + database = SQLiteDatabase.openDatabase(databaseFile.getAbsolutePath(), password, null, + SQLiteDatabase.OPEN_READWRITE, hook); + status = keyCount == 1; + } catch (Exception e) { + log(String.format("Error during modified UTF-8 to UTF-8 migration:%s", e.toString())); + } + return status; + } + + @Override + public String getName() { + return "Migrate from modified UTF-8 to UTF-8 with null test"; + } +} diff --git a/src/main/java/net/zetetic/tests/MigrateDatabaseFrom1xFormatToCurrentFormat.java b/app/src/main/java/net/zetetic/tests/MigrateDatabaseFrom1xFormatToCurrentFormat.java similarity index 92% rename from src/main/java/net/zetetic/tests/MigrateDatabaseFrom1xFormatToCurrentFormat.java rename to app/src/main/java/net/zetetic/tests/MigrateDatabaseFrom1xFormatToCurrentFormat.java index 3aadb0c..55a6778 100644 --- a/src/main/java/net/zetetic/tests/MigrateDatabaseFrom1xFormatToCurrentFormat.java +++ b/app/src/main/java/net/zetetic/tests/MigrateDatabaseFrom1xFormatToCurrentFormat.java @@ -6,6 +6,9 @@ import net.zetetic.ZeteticApplication; import java.io.File; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; public class MigrateDatabaseFrom1xFormatToCurrentFormat extends SQLCipherTest { @@ -22,7 +25,7 @@ public void postKey(SQLiteDatabase database) { database.rawExecSQL("PRAGMA cipher_migrate;"); } }; - SQLiteDatabase source = SQLiteDatabase.openOrCreateDatabase(sourceDatabase, password, null, hook); + SQLiteDatabase source = SQLiteDatabase.openOrCreateDatabase(sourceDatabase.getPath(), password, null, hook); Cursor result = source.rawQuery("select * from t1", new String[]{}); if(result != null){ result.moveToFirst(); diff --git a/src/main/java/net/zetetic/tests/MigrationUserVersion.java b/app/src/main/java/net/zetetic/tests/MigrationUserVersion.java similarity index 93% rename from src/main/java/net/zetetic/tests/MigrationUserVersion.java rename to app/src/main/java/net/zetetic/tests/MigrationUserVersion.java index f46dce7..5326fa4 100644 --- a/src/main/java/net/zetetic/tests/MigrationUserVersion.java +++ b/app/src/main/java/net/zetetic/tests/MigrationUserVersion.java @@ -34,7 +34,9 @@ public void postKey(SQLiteDatabase database) { database.rawExecSQL("PRAGMA cipher_migrate;"); } }); - return originalDatabase.getVersion() > 0; + boolean status = originalDatabase.getVersion() > 0; + originalDatabase.close(); + return status; } catch (Exception e) { return false; diff --git a/app/src/main/java/net/zetetic/tests/MultiThreadReadWriteTest.java b/app/src/main/java/net/zetetic/tests/MultiThreadReadWriteTest.java new file mode 100644 index 0000000..c668a1d --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/MultiThreadReadWriteTest.java @@ -0,0 +1,215 @@ +package net.zetetic.tests; + +import android.util.Log; + +import android.database.Cursor; + +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +import net.zetetic.ZeteticApplication; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class MultiThreadReadWriteTest extends SQLCipherTest { + + final File databaseFile = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.DATABASE_NAME); + final String password = ZeteticApplication.DATABASE_PASSWORD; + private static SQLiteDatabase instance; + ThreadExecutor executor; + int recordsPerThread = 20; + int threadCount = 40; + + @Override + public boolean execute(SQLiteDatabase database) { + database.close(); + executor = new ThreadExecutor(threadCount, recordsPerThread, DatabaseAccessType.Singleton); + return executor.run(); + } + + synchronized SQLiteDatabase getDatabase(DatabaseAccessType accessType) { + if (accessType == DatabaseAccessType.InstancePerRequest) { + return SQLiteDatabase.openOrCreateDatabase(databaseFile, password, null); + } else if (accessType == DatabaseAccessType.Singleton) { + if (instance == null) { + instance = SQLiteDatabase.openOrCreateDatabase(databaseFile, password, null); + } + return instance; + } + return null; + } + + synchronized void closeDatabase(SQLiteDatabase database, DatabaseAccessType accessType){ + if(accessType == DatabaseAccessType.InstancePerRequest){ + database.close(); + } + } + + private class Writer implements Runnable { + + int id; + int size; + private DatabaseAccessType accessType; + + public Writer(int id, int size, DatabaseAccessType accessType) { + this.id = id; + this.size = size; + this.accessType = accessType; + } + + @Override + public void run() { + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); + Log.i(TAG, String.format("writer thread %d beginning", id)); + // not expected to throw: + SQLiteDatabase writer = getDatabase(accessType); + + try { + writer.execSQL("create table if not exists t1(a,b)"); + + for (int index = 0; index < size; index++) { + Log.i(TAG, String.format("writer thread %d - insert data for row:%d", id, index)); + writer.execSQL("insert into t1(a,b) values(?, ?)", + new Object[]{"one for the money", "two for the show"}); + } + } catch (SQLiteException ex) { Log.e(TAG, "caught exception, bailing", ex); } + catch (IllegalStateException ex) { Log.e(TAG, "caught exception, bailing", ex); } + + closeDatabase(writer, accessType); + Log.i(TAG, String.format("writer thread %d terminating", id)); + } + } + + private class Reader implements Runnable { + + int id; + private DatabaseAccessType accessType; + + public Reader(int id, DatabaseAccessType accessType) { + this.id = id; + this.accessType = accessType; + } + + @Override + public void run() { + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); + Log.i(TAG, String.format("reader thread %d beginning", id)); + SQLiteDatabase reader = getDatabase(accessType); + int currentCount = 0; + int updatedCount = getCurrentTableCount(reader); + while (currentCount == 0 || currentCount != updatedCount) { + logRecordsBetween(reader, currentCount, updatedCount); + currentCount = updatedCount; + updatedCount = getCurrentTableCount(reader); + } + closeDatabase(reader, accessType); + Log.i(TAG, String.format("reader thread %d terminating", id)); + } + + synchronized void logRecordsBetween(SQLiteDatabase reader, int start, int end) { + if(!reader.isOpen()) return; + Cursor results = null; + try { + results = reader.rawQuery("select rowid, * from t1 where rowid between ? and ?", + new String[]{String.valueOf(start), String.valueOf(end)}); + } catch (IllegalStateException ex) { Log.e(TAG, "caught exception, bailing", ex); } + catch (SQLiteException ex) { Log.e(TAG, "caught exception, bailing", ex); } + + if (results != null) { + Log.i(TAG, String.format("reader thread %d - writing results %d to %d", id, start, end)); + while (results.moveToNext()) { + Log.i(TAG, String.format("reader thread %d - record:%d, a:%s b:%s", + id, results.getInt(0), results.getString(1), results.getString(2))); + } + results.close(); + } + } + + synchronized int getCurrentTableCount(SQLiteDatabase database) { + int count = 0; + + if (!database.isOpen()) return -1; + Cursor cursor = null; + + // I. Attempt database.rawQuery()-bail (explicitly return 0) if it throws: + try { + cursor = database.rawQuery("select count(*) from t1;", new String[]{}); + } catch (IllegalStateException ex) { Log.e(TAG, "caught exception, bailing with count = " + count, ex); return 0; } + catch (SQLiteException ex) { Log.e(TAG, "caught exception, bailing", ex); return 0; } + + // II. Attempt to get the count from the cursor: + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + count = cursor.getInt(0); + } + cursor.close(); + } catch (IllegalStateException ex) { Log.e(TAG, "caught exception, bailing with count = " + count, ex); } + } + + return count; + } + } + + private class ThreadExecutor { + + private int threadCount; + private int recordsPerThread; + private DatabaseAccessType accessType; + List readerThreads; + List writerThreads; + boolean status = false; + + public ThreadExecutor(int threads, int recordsPerThread, DatabaseAccessType accessType) { + + if (threads % 2 != 0) { + throw new IllegalArgumentException("Threads must be split evenly between readers and writers"); + } + this.threadCount = threads / 2; + this.recordsPerThread = recordsPerThread; + this.accessType = accessType; + readerThreads = new ArrayList(); + writerThreads = new ArrayList(); + } + + public boolean run() { + + for (int index = 0; index < threadCount; index++) { + readerThreads.add(new Thread(new Reader(index, accessType))); + writerThreads.add(new Thread(new Writer(index, recordsPerThread, accessType))); + } + for (int index = 0; index < threadCount; index++) { + Log.i(TAG, String.format("Starting writer thread %d", index)); + writerThreads.get(index).start(); + } + try { + for (int index = 0; index < threadCount; index++) { + Log.i(TAG, String.format("Starting reader thread %d", index)); + readerThreads.get(index).start(); + } + Log.i(TAG, String.format("Request reader thread %d to join", 0)); + readerThreads.get(threadCount - 1).join(); + status = true; + } catch (InterruptedException e) { + Log.i(TAG, "Thread join failure", e); + } + if(accessType == DatabaseAccessType.Singleton){ + getDatabase(accessType).close(); + } + return status; + } + } + + + private enum DatabaseAccessType { + Singleton, + InstancePerRequest + } + + @Override + public String getName() { + return "Multi-threaded read/write test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/NestedTransactionsTest.java b/app/src/main/java/net/zetetic/tests/NestedTransactionsTest.java new file mode 100644 index 0000000..4970db1 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/NestedTransactionsTest.java @@ -0,0 +1,26 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; + + +public class NestedTransactionsTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.rawExecSQL("savepoint foo;"); + database.rawExecSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?,?);", new Object[]{"one for the money", "two for the show"}); + database.rawExecSQL("savepoint bar;"); + database.execSQL("insert into t1(a,b) values(?,?);", new Object[]{"three to get ready", "go man go"}); + database.rawExecSQL("rollback transaction to bar;"); + database.rawExecSQL("commit;"); + int count = QueryHelper.singleIntegerValueFromQuery(database, "select count(*) from t1;"); + return count == 1; + } + + @Override + public String getName() { + return "Nested Transactions Test"; + } +} diff --git a/src/main/java/net/zetetic/tests/NullQueryResultTest.java b/app/src/main/java/net/zetetic/tests/NullQueryResultTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/NullQueryResultTest.java rename to app/src/main/java/net/zetetic/tests/NullQueryResultTest.java diff --git a/app/src/main/java/net/zetetic/tests/NullRawQueryTest.java b/app/src/main/java/net/zetetic/tests/NullRawQueryTest.java new file mode 100644 index 0000000..07f6a89 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/NullRawQueryTest.java @@ -0,0 +1,29 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class NullRawQueryTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", "two for the show"}); + Cursor cursor = database.rawQuery("select * from t1;", null); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + String b = cursor.getString(1); + cursor.close(); + return a.equals("one for the money") && b.equals("two for the show"); + } + } + + + return false; + } + + @Override + public String getName() { + return "Bind Null Raw Query Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/OpenReadOnlyDatabaseTest.java b/app/src/main/java/net/zetetic/tests/OpenReadOnlyDatabaseTest.java new file mode 100644 index 0000000..b316ef1 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/OpenReadOnlyDatabaseTest.java @@ -0,0 +1,25 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class OpenReadOnlyDatabaseTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.close(); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.DATABASE_NAME); + database = SQLiteDatabase.openDatabase(databasePath.getAbsolutePath(), ZeteticApplication.DATABASE_PASSWORD, + null, SQLiteDatabase.OPEN_READONLY); + boolean opened = database.isOpen(); + database.close(); + return opened; + } + + @Override + public String getName() { + return "Open Read Only Database Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/OpenSQLCipher3DatabaseTest.java b/app/src/main/java/net/zetetic/tests/OpenSQLCipher3DatabaseTest.java new file mode 100644 index 0000000..758dc5f --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/OpenSQLCipher3DatabaseTest.java @@ -0,0 +1,46 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class OpenSQLCipher3DatabaseTest extends SQLCipherTest { + + String databaseFile = "sqlcipher-3.0-testkey.db"; + + @Override + public boolean execute(SQLiteDatabase database) { + boolean result = false; + try { + database.close(); + SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { + public void preKey(SQLiteDatabase database) {} + public void postKey(SQLiteDatabase database) { + database.execSQL("PRAGMA kdf_iter = 64000;"); + database.execSQL("PRAGMA cipher_page_size = 1024;"); + database.execSQL("PRAGMA cipher_hmac_algorithm = HMAC_SHA1;"); + database.execSQL("PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1;"); + } + }; + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory(databaseFile); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(databaseFile); + database = SQLiteDatabase.openDatabase(databasePath.getAbsolutePath(), "testkey", + null, SQLiteDatabase.OPEN_READWRITE, hook); + Cursor cursor = database.rawQuery("SELECT COUNT(*) FROM sqlite_master;", null); + if(cursor != null){ + cursor.moveToFirst(); + int count = cursor.getInt(0); + result = count > 0; + } + } catch (Exception ex) {} + return result; + } + + @Override + public String getName() { + return "Open SQLCipher 3 Database"; + } +} diff --git a/src/main/java/net/zetetic/tests/PragmaCipherVersionTest.java b/app/src/main/java/net/zetetic/tests/PragmaCipherVersionTest.java similarity index 57% rename from src/main/java/net/zetetic/tests/PragmaCipherVersionTest.java rename to app/src/main/java/net/zetetic/tests/PragmaCipherVersionTest.java index fae6b65..de47ce2 100644 --- a/src/main/java/net/zetetic/tests/PragmaCipherVersionTest.java +++ b/app/src/main/java/net/zetetic/tests/PragmaCipherVersionTest.java @@ -1,21 +1,28 @@ package net.zetetic.tests; import android.database.Cursor; +import android.util.Log; import net.sqlcipher.database.SQLiteDatabase; public class PragmaCipherVersionTest extends SQLCipherTest { - private final String CURRENT_CIPHER_VERSION = "3.0.0"; + private final String CURRENT_CIPHER_VERSION = "4.5.4"; @Override public boolean execute(SQLiteDatabase database) { + Log.i(TAG, "Before rawQuery"); Cursor cursor = database.rawQuery("PRAGMA cipher_version", new String[]{}); + Log.i(TAG, "After rawQuery"); if(cursor != null){ + Log.i(TAG, "Before cursor.moveToNext()"); cursor.moveToNext(); + Log.i(TAG, "Before cursor.getString(0)"); String cipherVersion = cursor.getString(0); + Log.i(TAG, "Before cursor.close"); cursor.close(); - return cipherVersion.equals(CURRENT_CIPHER_VERSION); + setMessage(String.format("Reported:%s", cipherVersion)); + return cipherVersion.contains(CURRENT_CIPHER_VERSION); } return false; } @@ -24,4 +31,4 @@ public boolean execute(SQLiteDatabase database) { public String getName() { return "PRAGMA cipher_version Test"; } -} \ No newline at end of file +} diff --git a/app/src/main/java/net/zetetic/tests/PreventUpdateWithNullWhereArgsFromThrowingExceptionTest.java b/app/src/main/java/net/zetetic/tests/PreventUpdateWithNullWhereArgsFromThrowingExceptionTest.java new file mode 100644 index 0000000..090282e --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/PreventUpdateWithNullWhereArgsFromThrowingExceptionTest.java @@ -0,0 +1,34 @@ +package net.zetetic.tests; + +import android.content.ContentValues; +import android.database.Cursor; + +import net.sqlcipher.database.SQLiteDatabase; + +class PreventUpdateWithNullWhereArgsFromThrowingExceptionTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?)", new Object[]{"foo", "bar"}); + ContentValues values = new ContentValues(); + values.put("b", "baz"); + database.update("t1", SQLiteDatabase.CONFLICT_ABORT, values, null, null); + long count = 0; + Cursor cursor = database.query("select count(*) from t1 where b = ?;", new Object[]{"baz"}); + if(cursor != null && cursor.moveToNext()){ + count = cursor.getLong(0); + } + return count == 1; + } + catch (Exception ex){ + return false; + } + } + + @Override + public String getName() { + return "Prevent update with null where args from throwing"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/QueryDataSizeTest.java b/app/src/main/java/net/zetetic/tests/QueryDataSizeTest.java new file mode 100644 index 0000000..4d3a799 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/QueryDataSizeTest.java @@ -0,0 +1,22 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteQueryStats; + +public class QueryDataSizeTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("CREATE TABLE t1(a,b);"); + database.execSQL("INSERT INTO t1(a,b) VALUES(?, ?);", + new Object[]{generateRandomByteArray(256), generateRandomByteArray(256)}); + database.execSQL("INSERT INTO t1(a,b) VALUES(?, ?);", + new Object[]{generateRandomByteArray(1024), generateRandomByteArray(64)}); + SQLiteQueryStats result = database.getQueryStats("SELECT * FROM t1;", new Object[]{}); + return result.getTotalQueryResultSize() > 0 && result.getLargestIndividualRowSize() > 0; + } + + @Override + public String getName() { + return "Query Data Size Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/QueryFloatToStringTest.java b/app/src/main/java/net/zetetic/tests/QueryFloatToStringTest.java new file mode 100644 index 0000000..ed4e510 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/QueryFloatToStringTest.java @@ -0,0 +1,17 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; + +public class QueryFloatToStringTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + String value = QueryHelper.singleValueFromQuery(database, "SELECT 42.09;"); + return value.equals("42.09"); + } + + @Override + public String getName() { + return "Query Float to String"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/QueryIntegerToStringTest.java b/app/src/main/java/net/zetetic/tests/QueryIntegerToStringTest.java new file mode 100644 index 0000000..2d868eb --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/QueryIntegerToStringTest.java @@ -0,0 +1,17 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; + +public class QueryIntegerToStringTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + String value = QueryHelper.singleValueFromQuery(database, "SELECT 123;"); + return value.equals("123"); + } + + @Override + public String getName() { + return "Query Integer to String"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/QueryLimitTest.java b/app/src/main/java/net/zetetic/tests/QueryLimitTest.java new file mode 100644 index 0000000..490d7a7 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/QueryLimitTest.java @@ -0,0 +1,58 @@ +package net.zetetic.tests; + +import android.content.ContentValues; +import android.database.Cursor; + +import net.sqlcipher.database.SQLiteDatabase; + +public class QueryLimitTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + + int query1FirstId = -1; + int query1RowCount = 0; + int limit = 20; + int offset = 5; + int recordsToInsertInSourceTable = 30; + String create = "CREATE TABLE source(id INTEGER(20) PRIMARY KEY, name VARCHAR(50));"; + String other = "CREATE TABLE destination(id INTEGER(20) PRIMARY KEY, name VARCHAR(50));"; + String insert = String.format("INSERT INTO destination(id, name) SELECT * FROM source ORDER BY ID ASC LIMIT %d OFFSET %d;", + limit, offset); + String query1 = "SELECT * FROM destination ORDER BY id ASC;"; + + database.rawExecSQL(create); + database.beginTransaction(); + for(int index = 0; index < recordsToInsertInSourceTable; index++){ + ContentValues values = new ContentValues(); + values.put("id", String.valueOf(index)); + values.put("name", String.format("name%d", index)); + database.insert("source", null, values); + } + database.setTransactionSuccessful(); + database.endTransaction(); + + database.execSQL(other); + database.execSQL(insert); + + Cursor cursor = database.rawQuery(query1, null); + if(cursor != null){ + while (cursor.moveToNext()){ + if(query1FirstId == -1) { + query1FirstId = cursor.getInt(cursor.getColumnIndex("id")); + } + query1RowCount++; + log(String.format("id:%d name:%s", + cursor.getInt(cursor.getColumnIndex("id")), + cursor.getString(cursor.getColumnIndex("name")))); + } + cursor.close(); + } + return query1FirstId == offset && query1RowCount == limit; + } + + @Override + public String getName() { + return "Query Limit Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/QueryNonEncryptedDatabaseTest.java b/app/src/main/java/net/zetetic/tests/QueryNonEncryptedDatabaseTest.java new file mode 100644 index 0000000..2bdb946 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/QueryNonEncryptedDatabaseTest.java @@ -0,0 +1,137 @@ +package net.zetetic.tests; + +import android.database.Cursor; + +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +import net.zetetic.ZeteticApplication; + +import android.util.Log; + +import java.io.File; +import java.io.IOException; + +public class QueryNonEncryptedDatabaseTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + + database.close(); + File unencryptedDatabase = ZeteticApplication.getInstance().getDatabasePath("unencrypted.db"); + String nullPasswordString = null; + + try { + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("unencrypted.db"); + } catch (IOException e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: caught IOException", e); + return false; + } + + boolean success = false; + + try { + SQLiteDatabase.openOrCreateDatabase(unencryptedDatabase, ZeteticApplication.DATABASE_PASSWORD, null); + + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() of unencrypted database with a password did not fail"); + return false; + } catch (SQLiteException e){ + Log.v(ZeteticApplication.TAG, + "SQLiteDatabase.openOrCreateDatabase() of unencrypted database with a password did throw a SQLiteException as expected OK", e); + } catch (Exception e){ + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with invalid password did throw an unexpected exception", e); + return false; + } + + try { + database = SQLiteDatabase.openOrCreateDatabase(unencryptedDatabase, nullPasswordString, null); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception", e); + return false; + } + + try { + database = SQLiteDatabase.openOrCreateDatabase(unencryptedDatabase.getPath(), nullPasswordString, null, null, null); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception", e); + return false; + } + + try { + database = SQLiteDatabase.openDatabase(unencryptedDatabase.getPath(), nullPasswordString, null, SQLiteDatabase.OPEN_READWRITE); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception", e); + return false; + } + + try { + database = SQLiteDatabase.openDatabase(unencryptedDatabase.getPath(), nullPasswordString, null, SQLiteDatabase.OPEN_READWRITE, null, null); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception", e); + return false; + } + + try { + char[] nullPassword = null; + database = SQLiteDatabase.openOrCreateDatabase(unencryptedDatabase.getPath(), nullPassword, null, null); + Cursor cursor = database.rawQuery("SELECT * FROM t1", new String[]{}); + cursor.moveToFirst(); + String a = cursor.getString(0); + String b = cursor.getString(1); + cursor.close(); + database.close(); + + if (!a.equals("one for the money") || !b.equals("two for the show")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: incorrect data from unencrypted database"); + return false; + } + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception", e); + return false; + } + + try { + char[] noPasswordChars = new char[0]; + database = SQLiteDatabase.openOrCreateDatabase(unencryptedDatabase.getPath(), noPasswordChars, null, null); + Cursor cursor = database.rawQuery("SELECT * FROM t1", new String[]{}); + cursor.moveToFirst(); + String a = cursor.getString(0); + String b = cursor.getString(1); + cursor.close(); + database.close(); + + if (!a.equals("one for the money") || !b.equals("two for the show")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: incorrect data from unencrypted database"); + return false; + } + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception", e); + return false; + } + + try { + database = SQLiteDatabase.openOrCreateDatabase(unencryptedDatabase, "", null); + Cursor cursor = database.rawQuery("select * from t1", new String[]{}); + cursor.moveToFirst(); + String a = cursor.getString(0); + String b = cursor.getString(1); + cursor.close(); + database.close(); + + success = a.equals("one for the money") && + b.equals("two for the show"); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception when reading database with blank password", e); + return false; + } + + return success; + } + + @Override + public String getName() { + return "Query Non-Encrypted Database Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/QueryTenThousandDataTest.java b/app/src/main/java/net/zetetic/tests/QueryTenThousandDataTest.java new file mode 100644 index 0000000..bc1025c --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/QueryTenThousandDataTest.java @@ -0,0 +1,324 @@ +package net.zetetic.tests; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteStatement; + +import java.util.ArrayList; +import java.util.List; + +/** + * QueryTenThousandDataTest + * + * It collected two questions: + * 1.In the Samsung Galaxy Note 5, Initially insert 14000 rows of data, the first time to query all the data, it is normal. + * When I execute more than one query all the data, the exception occurs. + * The exception follows: + * Fatal signal 11 (SIGSEGV), code 1, fault addr 0x70bef5dc in tid 26296 (AsyncTask #2) + * + * 2.Regardless of any device, it consume 4 to 8 seconds when query 14000 rows of data. + * When I do not use SQLCipher to query 14000 rows of data, it takes only 200 to 400 milliseconds + * + * Would you be able to tell me how to solve the second problem which query slowly? Sincere thanks. + * @author force + * + */ +public class QueryTenThousandDataTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + + createTable(database, true); + + insertData(database); + + Cursor cursor = database.rawQuery("SELECT * FROM UserInfo", new String[]{}); + log("Query ten thousand row data cursor move"); + long beforeCursorMove = System.nanoTime(); + List userList = new ArrayList(); + try { + for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + UserEntity userEntity = readEntity(cursor, 0); + userList.add(userEntity); + } + long afterCursorMove = System.nanoTime(); + log(String.format("Query thousand row data userList size:%d", userList.size())); + log(String.format("Complete cursor operation time:%d ms", + toMilliseconds(beforeCursorMove, afterCursorMove))); + } finally { + cursor.close(); + } + + return true; + } + + @Override + public String getName() { + return "Query fourteen thousand rows Test"; + } + + private long toMilliseconds(long before, long after){ + return (after - before)/1000000L; + } + + + public void createTable(SQLiteDatabase db, boolean ifNotExists) { + String constraint = ifNotExists? "IF NOT EXISTS ": ""; + db.execSQL("CREATE TABLE " + constraint + "'UserInfo' (" + + "'aaaa' INTEGER PRIMARY KEY AUTOINCREMENT ," + + "'bbbb' INTEGER NOT NULL UNIQUE," + + "'cccc' INTEGER NOT NULL ," + + "'dddd' TEXT NOT NULL ," + + "'eeee' TEXT NOT NULL ," + + "'ffff' TEXT NOT NULL ," + + "'gggg' TEXT NOT NULL ," + + "'hhhh' TEXT NOT NULL ," + + "'iiii' TEXT NOT NULL ," + + "'jjjj' INTEGER NOT NULL ," + + "'kkkk' INTEGER NOT NULL ," + + "'llll' INTEGER NOT NULL ," + + "'mmmm' INTEGER NOT NULL ," + + "'nnnn' TEXT NOT NULL );"); + } + + public void insertData(SQLiteDatabase database) { + + try { + String sql = "INSERT INTO UserInfo ( aaaa, bbbb, cccc, dddd, eeee, ffff, gggg, hhhh, iiii, jjjj, kkkk, llll, mmmm, nnnn ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + database.beginTransaction(); + SQLiteStatement stmt = database.compileStatement(sql); + + for (int index = 0; index < 14000; index++) { + stmt.bindLong(1, Long.valueOf(index + "")); + stmt.bindDouble(2, index); + stmt.bindDouble(3, 1); + stmt.bindString(4, "tom"); + stmt.bindString(5, "lucy"); + stmt.bindString(6, "force"); + stmt.bindString(7, "http"); + stmt.bindString(8, "0201111"); + stmt.bindString(9, "email"); + stmt.bindDouble(10, 222); + stmt.bindDouble(11, 1); + stmt.bindDouble(12, 333); + stmt.bindDouble(13, 444); + stmt.bindString(14, "short"); + + stmt.execute(); + stmt.clearBindings(); + } + } finally { + database.setTransactionSuccessful(); + database.endTransaction(); + } + } + + public UserEntity readEntity(Cursor cursor, int offset) { + UserEntity entity = new UserEntity( // + cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), + cursor.getInt(offset + 1), + cursor.getInt(offset + 2), + cursor.getString(offset + 3), + cursor.getString(offset + 4), + cursor.getString(offset + 5), + cursor.getString(offset + 6), + cursor.getString(offset + 7), + cursor.getString(offset + 8), + cursor.getInt(offset + 9), + cursor.getInt(offset + 10), + cursor.getInt(offset + 11), + cursor.getInt(offset + 12), + cursor.getString(offset + 13) + ); + return entity; + } + + public class UserEntity { + + private int gender; + /** Not-null value. */ + private String pinyinName; + /** Not-null value. */ + private String realName; + /** Not-null value. */ + private String phone; + /** Not-null value. */ + private String shortPhone; + /** Not-null value. */ + + protected Long id; + protected int peerId; + /** Not-null value. + * userEntity --> nickName + * groupEntity --> groupName + * */ + protected String mainName; + /** Not-null value.*/ + protected String avatar; + protected int created; + protected int updated; + private int searchType; + + private int tempGroupRoleType; + private String email; + private int departmentId; + private int status; + private int msgUpdataTime; + + public UserEntity(Long id, int peerId, int gender, String mainName, + String pinyinName, String realName, String avatar, + String phone, String email, int departmentId, + int status, int created, int updated, String shortPhone) { + this.id = id; + this.peerId = peerId; + this.gender = gender; + this.mainName = mainName; + this.pinyinName = pinyinName; + this.realName = realName; + this.avatar = avatar; + this.phone = phone; + this.email = email; + this.departmentId = departmentId; + this.status = status; + this.created = created; + this.updated = updated; + this.shortPhone = shortPhone; + } + + public int getGender() { + return gender; + } + + public void setGender(int gender) { + this.gender = gender; + } + + public String getPinyinName() { + return pinyinName; + } + + public void setPinyinName(String pinyinName) { + this.pinyinName = pinyinName; + } + + public String getRealName() { + return realName; + } + + public void setRealName(String realName) { + this.realName = realName; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getShortPhone() { + return shortPhone; + } + + public void setShortPhone(String shortPhone) { + this.shortPhone = shortPhone; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public int getPeerId() { + return peerId; + } + + public void setPeerId(int peerId) { + this.peerId = peerId; + } + + public String getMainName() { + return mainName; + } + + public void setMainName(String mainName) { + this.mainName = mainName; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } + + public int getCreated() { + return created; + } + + public void setCreated(int created) { + this.created = created; + } + + public int getUpdated() { + return updated; + } + + public void setUpdated(int updated) { + this.updated = updated; + } + + public int getSearchType() { + return searchType; + } + + public void setSearchType(int searchType) { + this.searchType = searchType; + } + + public int getTempGroupRoleType() { + return tempGroupRoleType; + } + + public void setTempGroupRoleType(int tempGroupRoleType) { + this.tempGroupRoleType = tempGroupRoleType; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public int getDepartmentId() { + return departmentId; + } + + public void setDepartmentId(int departmentId) { + this.departmentId = departmentId; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public int getMsgUpdataTime() { + return msgUpdataTime; + } + + public void setMsgUpdataTime(int msgUpdataTime) { + this.msgUpdataTime = msgUpdataTime; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/zetetic/tests/RTreeTest.java b/app/src/main/java/net/zetetic/tests/RTreeTest.java new file mode 100644 index 0000000..533c69b --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/RTreeTest.java @@ -0,0 +1,28 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class RTreeTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + int id = 0; + String create = "CREATE VIRTUAL TABLE demo_index USING rtree(id, minX, maxX, minY, maxY);"; + String insert = "INSERT INTO demo_index VALUES(?, ?, ?, ?, ?);"; + database.execSQL(create); + database.execSQL(insert, new Object[]{1, -80.7749, -80.7747, 35.3776, 35.3778}); + Cursor cursor = database.rawQuery("SELECT * FROM demo_index WHERE maxY < ?;", + new Object[]{36}); + if(cursor != null){ + cursor.moveToNext(); + id = cursor.getInt(0); + cursor.close(); + } + return id == 1; + } + + @Override + public String getName() { + return "RTree Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/RawExecSQLExceptionTest.java b/app/src/main/java/net/zetetic/tests/RawExecSQLExceptionTest.java new file mode 100644 index 0000000..186335d --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/RawExecSQLExceptionTest.java @@ -0,0 +1,38 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +import net.zetetic.ZeteticApplication; + +import android.util.Log; + +public class RawExecSQLExceptionTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + + try { + database.rawExecSQL("select foo from bar"); + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: DID throw SQLiteException", e); + String message = e.getMessage(); + setMessage(message); + if (!message.matches("no such table: bar")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT exception message: " + message); + return false; + } + return true; + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: DID throw other exception", e); + return false; + } + + return false; + } + + @Override + public String getName() { + return "rawExecSQL Exception Test"; + } +} diff --git a/src/main/java/net/zetetic/tests/RawExecSQLTest.java b/app/src/main/java/net/zetetic/tests/RawExecSQLTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/RawExecSQLTest.java rename to app/src/main/java/net/zetetic/tests/RawExecSQLTest.java diff --git a/app/src/main/java/net/zetetic/tests/RawQueryNoSuchFunctionErrorMessageTest.java b/app/src/main/java/net/zetetic/tests/RawQueryNoSuchFunctionErrorMessageTest.java new file mode 100644 index 0000000..cf0e736 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/RawQueryNoSuchFunctionErrorMessageTest.java @@ -0,0 +1,40 @@ +package net.zetetic.tests; + +import android.database.Cursor; + +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +import net.zetetic.ZeteticApplication; + +import android.util.Log; + +public class RawQueryNoSuchFunctionErrorMessageTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + Cursor ignored = database.rawQuery("SELECT UPER('Test')", null); + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: DID throw SQLiteException", e); + String message = e.getMessage(); + setMessage(message); + // TBD missing error code etc. + if (!message.matches("no such function: UPER: .*\\, while compiling: SELECT UPER\\('Test'\\)")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT exception message: " + message); + return false; + } + return true; + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: DID throw other exception", e); + return false; + } + + return false; + } + + @Override + public String getName() { + return "rawQuery no such function error message Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/RawQueryNonsenseStatementErrorMessageTest.java b/app/src/main/java/net/zetetic/tests/RawQueryNonsenseStatementErrorMessageTest.java new file mode 100644 index 0000000..e8a8320 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/RawQueryNonsenseStatementErrorMessageTest.java @@ -0,0 +1,42 @@ +package net.zetetic.tests; + +import android.database.Cursor; + +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +import net.zetetic.ZeteticApplication; + +import android.util.Log; + +import java.util.regex.Pattern; + +public class RawQueryNonsenseStatementErrorMessageTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + Cursor ignored = database.rawQuery("101", null); + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: DID throw SQLiteException", e); + String message = e.getMessage(); + setMessage(message); + // TBD missing error code etc. + if (!message.matches("near \"101\": syntax error: .*\\, while compiling: 101")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT exception message: " + message); + return false; + } + return true; + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: DID throw other exception", e); + return false; + } + + return false; + } + + @Override + public String getName() { + return "rawQuery nonsense statement error message Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/RawQuerySyntaxErrorMessageTest.java b/app/src/main/java/net/zetetic/tests/RawQuerySyntaxErrorMessageTest.java new file mode 100644 index 0000000..af3455c --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/RawQuerySyntaxErrorMessageTest.java @@ -0,0 +1,40 @@ +package net.zetetic.tests; + +import android.database.Cursor; + +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +import net.zetetic.ZeteticApplication; + +import android.util.Log; + +public class RawQuerySyntaxErrorMessageTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + Cursor ignored = database.rawQuery("SLCT 1", null); + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: DID throw SQLiteException", e); + String message = e.getMessage(); + setMessage(message); + // TBD missing error code etc. + if (!message.matches("near \"SLCT\": syntax error: .*\\, while compiling: SLCT 1")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT exception message: " + message); + return false; + } + return true; + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: DID throw other exception", e); + return false; + } + + return false; + } + + @Override + public String getName() { + return "rawQuery syntax error message Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/RawQueryTest.java b/app/src/main/java/net/zetetic/tests/RawQueryTest.java new file mode 100644 index 0000000..c8fb435 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/RawQueryTest.java @@ -0,0 +1,29 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class RawQueryTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + int rows = 0; + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", + new Object[]{"one for the money", "two for the show"}); + Cursor cursor = database.rawQuery("select * from t1;", null, 1, 1); + if(cursor != null){ + while(cursor.moveToNext()) { + cursor.getString(0); + rows++; + } + cursor.close(); + } + return rows > 0; + } + + @Override + public String getName() { + return "Raw Query Test"; + } +} diff --git a/src/main/java/net/zetetic/tests/RawRekeyTest.java b/app/src/main/java/net/zetetic/tests/RawRekeyTest.java similarity index 65% rename from src/main/java/net/zetetic/tests/RawRekeyTest.java rename to app/src/main/java/net/zetetic/tests/RawRekeyTest.java index e5797fb..64f5061 100644 --- a/src/main/java/net/zetetic/tests/RawRekeyTest.java +++ b/app/src/main/java/net/zetetic/tests/RawRekeyTest.java @@ -1,6 +1,8 @@ package net.zetetic.tests; +import android.util.Log; import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; import net.zetetic.ZeteticApplication; import java.io.File; @@ -13,10 +15,15 @@ public class RawRekeyTest extends SQLCipherTest { @Override public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?,?)", new Object[]{"one for the money", "two for the show"}); database.rawExecSQL(rekeyCommand); database.close(); database = SQLiteDatabase.openOrCreateDatabase(databaseFile, password, null); - return database != null; + int count = QueryHelper.singleIntegerValueFromQuery(database, "select count(*) from t1;"); + boolean status = count == 1; + database.close(); + return status; } @Override diff --git a/app/src/main/java/net/zetetic/tests/ReadWriteDatabaseToExternalStorageTest.java b/app/src/main/java/net/zetetic/tests/ReadWriteDatabaseToExternalStorageTest.java new file mode 100644 index 0000000..119cae1 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/ReadWriteDatabaseToExternalStorageTest.java @@ -0,0 +1,66 @@ +package net.zetetic.tests; + +import android.os.Environment; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class ReadWriteDatabaseToExternalStorageTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + + File externalDatabaseFile = null; + try { + if(isExternalStorageReadable() && isExternalStorageWritable()) { + database.close(); + File databases = ZeteticApplication.getInstance().getExternalFilesDir("databases"); + externalDatabaseFile = new File(databases, "test.db"); + database = SQLiteDatabase.openOrCreateDatabase(externalDatabaseFile, "test", null); + if(database == null){ + setMessage("Unable to create database on external storage"); + return false; + } + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", + new Object[]{"one for the money", "two for the show"}); + Cursor cursor = database.rawQuery("select * from t1;", null); + if(cursor != null){ + cursor.moveToFirst(); + String a = cursor.getString(0); + String b = cursor.getString(1); + cursor.close(); + return "one for the money".equals(a) && + "two for the show".equals(b); + } + return false; + } else { + setMessage("External storage unavailable"); + return true; + } + } + finally { + if(externalDatabaseFile != null){ + externalDatabaseFile.delete(); + } + } + } + + @Override + public String getName() { + return "Read/Write to External Storage"; + } + + public boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state); + } + + public boolean isExternalStorageReadable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state) || + Environment.MEDIA_MOUNTED_READ_ONLY.equals(state); + } + +} diff --git a/app/src/main/java/net/zetetic/tests/ReadWriteUserVersionTest.java b/app/src/main/java/net/zetetic/tests/ReadWriteUserVersionTest.java new file mode 100644 index 0000000..4a96f4d --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/ReadWriteUserVersionTest.java @@ -0,0 +1,18 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; + +public class ReadWriteUserVersionTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + int version = 4; + database.setVersion(version); + int readVersion = database.getVersion(); + return version == readVersion; + } + + @Override + public String getName() { + return "Read/write user_version"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/ReadWriteWriteAheadLoggingTest.java b/app/src/main/java/net/zetetic/tests/ReadWriteWriteAheadLoggingTest.java new file mode 100644 index 0000000..514c09e --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/ReadWriteWriteAheadLoggingTest.java @@ -0,0 +1,51 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.CursorWindow; +import net.sqlcipher.database.SQLiteDatabase; + +public class ReadWriteWriteAheadLoggingTest extends SQLCipherTest { + @Override + public boolean execute(final SQLiteDatabase database) { + + try { + final int[] a = new int[1]; + final int[] b = new int[1]; + database.setLockingEnabled(false); + boolean walEnabled = database.enableWriteAheadLogging(); + if (!walEnabled) return false; + + //database.execSQL("PRAGMA read_uncommitted = 1;"); + + database.execSQL("CREATE TABLE t1(a,b)"); + database.rawQuery("INSERT INTO t1(a,b) VALUES(?,?);", new Object[]{1, 2}); + database.beginTransaction(); + //database.beginTransactionNonExclusive(); + database.rawQuery("DELETE FROM t1 WHERE a = ?;", new Object[]{1}); + Thread t = new Thread(new Runnable() { + @Override + public void run() { + Cursor cursor = database.rawQuery("SELECT COUNT(*) FROM t1 WHERE a = ?;", new Object[]{1}); + if (cursor != null && cursor.moveToFirst()) { + a[0] = cursor.getInt(0); + b[0] = cursor.getInt(0); + log(String.format("Retrieved %d rows back", a[0])); + } + } + }); + t.start(); + t.join(); + database.setTransactionSuccessful(); + database.endTransaction(); + //return a[0] == 1 && b[0] == 2; + return a[0] == 0 && b[0] == 0; + } catch (InterruptedException ex){ + return false; + } + } + + @Override + public String getName() { + return "Read/Write WAL Test"; + } +} diff --git a/src/main/java/net/zetetic/tests/ReadableDatabaseTest.java b/app/src/main/java/net/zetetic/tests/ReadableDatabaseTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/ReadableDatabaseTest.java rename to app/src/main/java/net/zetetic/tests/ReadableDatabaseTest.java diff --git a/src/main/java/net/zetetic/tests/ReadableWritableAccessTest.java b/app/src/main/java/net/zetetic/tests/ReadableWritableAccessTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/ReadableWritableAccessTest.java rename to app/src/main/java/net/zetetic/tests/ReadableWritableAccessTest.java diff --git a/app/src/main/java/net/zetetic/tests/ReadableWritableInvalidPasswordTest.java b/app/src/main/java/net/zetetic/tests/ReadableWritableInvalidPasswordTest.java new file mode 100644 index 0000000..b2fe5be --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/ReadableWritableInvalidPasswordTest.java @@ -0,0 +1,164 @@ +package net.zetetic.tests; + +import android.content.Context; + +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import net.sqlcipher.database.SQLiteOpenHelper; + +import net.zetetic.ZeteticApplication; + +import android.util.Log; + +import java.io.File; + +public class ReadableWritableInvalidPasswordTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("CREATE TABLE tt(data);"); + database.execSQL("INSERT INTO tt VALUES(?)", new Object[]{"test data"}); + database.close(); + + MyHelper myHelper = new MyHelper(); + + try { + myHelper.getWritableDatabase("invalid password"); + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: successfully opened writable encrypted database with invalid password"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: SQLiteException when opening writable encrypted database with invalid password OK", e); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: other exception when opening writable encrypted database with invalid password", e); + return false; + } + + try { + myHelper.getReadableDatabase("invalid password"); + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: successfully opened readable encrypted database with invalid password"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: SQLiteException when opening readable encrypted database with invalid password OK", e); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: other exception when opening readable encrypted database with invalid password", e); + return false; + } + + try { + myHelper.getWritableDatabase(""); + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: successfully opened writable encrypted database with blank password String"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: SQLiteException when opening writable encrypted database with blank password String OK", e); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: other exception when opening writable encrypted database with blank password String", e); + return false; + } + + try { + myHelper.getReadableDatabase(""); + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: successfully opened readable encrypted database with blank password String"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: SQLiteException when opening readable encrypted database with blank password String OK", e); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: other exception when opening readable encrypted database with blank password String", e); + return false; + } + + try { + myHelper.getWritableDatabase(new char[0]); + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: successfully opened writable encrypted database with blank password char array"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: SQLiteException when opening writable encrypted database with blank password char array OK", e); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: other exception when opening writable encrypted database with blank password char array", e); + return false; + } + + try { + myHelper.getReadableDatabase(new char[0]); + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: successfully opened readable encrypted database with blank password char array"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: SQLiteException when opening readable encrypted database with blank password char array OK", e); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: other exception when opening readable encrypted database with blank password char array", e); + return false; + } + + char[] nullPassword = null; + + try { + myHelper.getWritableDatabase(nullPassword); + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: successfully opened writable encrypted database with null password char array"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: SQLiteException when opening writable encrypted database with null password char array OK", e); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: other exception when opening writable encrypted database with null password char array", e); + return false; + } + + try { + myHelper.getReadableDatabase(nullPassword); + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: successfully opened readable encrypted database with null password char array"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: SQLiteException when opening readable encrypted database with null password char array OK", e); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: other exception when opening readable encrypted database with null password char array", e); + return false; + } + + String nullPasswordString = null; + + try { + myHelper.getWritableDatabase(nullPasswordString); + + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: successfully opened writable encrypted database with null password String"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: SQLiteException when opening writable encrypted database with null password String OK", e); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception when opening writable database with null password String", e); + return false; + } + + try { + myHelper.getReadableDatabase(nullPasswordString); + + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: successfully opened readable encrypted database with null password String"); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: SQLiteException when opening readable encrypted database with null password String OK", e); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: exception when opening readable encrypted with null password String", e); + return false; + } + + return true; + } + + @Override + public String getName() { + return "Readable/Writable Invalid Password Test"; + } + + private class MyHelper extends SQLiteOpenHelper { + public MyHelper() { + super(ZeteticApplication.getInstance(), ZeteticApplication.DATABASE_NAME, null, 1); + } + + @Override + public void onCreate(SQLiteDatabase database) { + Log.v(ZeteticApplication.TAG, "Do nothing in MyHelper.onCreate()"); + } + + @Override + public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { + Log.v(ZeteticApplication.TAG, "Do nothing in MyHelper.onUpgrade()"); + } + } +} diff --git a/app/src/main/java/net/zetetic/tests/ResetQueryCacheFinalizesSqlStatementAndReleasesReferenceTest.java b/app/src/main/java/net/zetetic/tests/ResetQueryCacheFinalizesSqlStatementAndReleasesReferenceTest.java new file mode 100644 index 0000000..8b111c9 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/ResetQueryCacheFinalizesSqlStatementAndReleasesReferenceTest.java @@ -0,0 +1,35 @@ +package net.zetetic.tests; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +class ResetQueryCacheFinalizesSqlStatementAndReleasesReferenceTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + int initialColumnCount = 0, updatedColumnCount = 0; + int initialExpectedColumnCount = 2; + int updatedColumnExpectedCount = 3; + database.execSQL("create table t1(a, b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", + new Object[]{"foo", "bar"}); + Cursor cursor = database.query("t1", null,null,null,null,null,null); + if(cursor != null && cursor.moveToNext()){ + initialColumnCount = cursor.getColumnCount(); + cursor.close(); + } + database.resetCompiledSqlCache(); + database.execSQL("alter table t1 add c text null;"); + cursor = database.query("t1", null,null,null,null,null,null); + if(cursor != null && cursor.moveToNext()){ + updatedColumnCount = cursor.getColumnCount(); + cursor.close(); + } + return initialColumnCount == initialExpectedColumnCount + && updatedColumnCount == updatedColumnExpectedCount; + } + + @Override + public String getName() { + return "Reset Query cache"; + } +} diff --git a/src/main/java/net/zetetic/tests/ResultNotifier.java b/app/src/main/java/net/zetetic/tests/ResultNotifier.java similarity index 100% rename from src/main/java/net/zetetic/tests/ResultNotifier.java rename to app/src/main/java/net/zetetic/tests/ResultNotifier.java diff --git a/app/src/main/java/net/zetetic/tests/RowColumnValueBuilder.java b/app/src/main/java/net/zetetic/tests/RowColumnValueBuilder.java new file mode 100644 index 0000000..c04c94a --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/RowColumnValueBuilder.java @@ -0,0 +1,5 @@ +package net.zetetic.tests; + +public interface RowColumnValueBuilder { + Object buildRowColumnValue(String[] columns, int row, int column); +} diff --git a/app/src/main/java/net/zetetic/tests/SQLCipherTest.java b/app/src/main/java/net/zetetic/tests/SQLCipherTest.java new file mode 100644 index 0000000..a80e0f6 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/SQLCipherTest.java @@ -0,0 +1,287 @@ +package net.zetetic.tests; + +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.zetetic.QueryHelper; +import net.zetetic.ZeteticApplication; + +import java.io.File; +import java.security.SecureRandom; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public abstract class SQLCipherTest { + + public abstract boolean execute(SQLiteDatabase database); + public abstract String getName(); + public String TAG = getClass().getSimpleName(); + SecureRandom random = new SecureRandom(); + private TestResult result; + + private SQLiteDatabase database; + private File databasePath; + + protected void internalSetUp() { + Log.i(TAG, "Before prepareDatabaseEnvironment"); + ZeteticApplication.getInstance().prepareDatabaseEnvironment(); + Log.i(TAG, "Before getDatabasePath"); + databasePath = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.DATABASE_NAME); + Log.i(TAG, "Before createDatabase"); + database = createDatabase(databasePath); + Log.i(TAG, "Before setUp"); + setUp(); + } + + public TestResult run() { + + result = new TestResult(getName(), false); + try { + internalSetUp(); + long startTime = System.nanoTime(); + result.setResult(execute(database)); + long endTime = System.nanoTime(); + Log.i(TAG, String.format("Test complete: %s ran in %.2f seconds using library version %s", getName(), (endTime - startTime)/1000000000.0d, SQLiteDatabase.SQLCIPHER_ANDROID_VERSION)); + internalTearDown(); + } catch (Exception e) { + Log.v(ZeteticApplication.TAG, e.toString()); + } + return result; + } + + protected double toSeconds(long start, long end){ + return (end - start)/1000000000.0d; + } + + protected void setMessage(String message){ + result.setMessage(message); + } + + private void internalTearDown(){ + tearDown(database); + SQLiteDatabase.releaseMemory(); + database.close(); + if(databasePath != null && databasePath.exists()){ + databasePath.delete(); + } + } + + protected SQLiteDatabase createDatabase(File databasePath){ + Log.i(TAG, "Before ZeteticApplication.getInstance().createDatabase"); + return ZeteticApplication.getInstance().createDatabase(databasePath, new SQLiteDatabaseHook() { + @Override + public void preKey(SQLiteDatabase database) { + createDatabasePreKey(database); + } + + @Override + public void postKey(SQLiteDatabase database) { + createDatabasePostKey(database); + } + }); + } + + private String generateColumnName(List columnNames, int columnIndex){ + String labels = "abcdefghijklmnopqrstuvwxyz"; + String element = columnIndex < labels.length() + ? String.valueOf(labels.charAt(columnIndex)) + : String.valueOf(labels.charAt(random.nextInt(labels.length() - 1))); + while(columnNames.contains(element) || ReservedWords.contains(element.toUpperCase())){ + element += labels.charAt(random.nextInt(labels.length() - 1)); + } + columnNames.add(element); + Log.i(TAG, String.format("Generated column name:%s for index:%d", element, columnIndex)); + return element; + } + + protected void buildDatabase(SQLiteDatabase database, int rows, int columns, RowColumnValueBuilder builder) { + List columnNames = new ArrayList<>(); + Log.i(TAG, String.format("Building database with %s rows, %d columns", + NumberFormat.getInstance().format(rows), columns)); + String createTemplate = "CREATE TABLE t1(%s);"; + String insertTemplate = "INSERT INTO t1 VALUES(%s);"; + StringBuilder createBuilder = new StringBuilder(); + StringBuilder insertBuilder = new StringBuilder(); + for (int column = 0; column < columns; column++) { + String columnName = generateColumnName(columnNames, column); + createBuilder.append(String.format("%s BLOB%s", + columnName, + column != columns - 1 ? "," : "")); + insertBuilder.append(String.format("?%s", column != columns - 1 ? "," : "")); + } + String create = String.format(createTemplate, createBuilder.toString()); + String insert = String.format(insertTemplate, insertBuilder.toString()); + database.execSQL("DROP TABLE IF EXISTS t1;"); + database.execSQL(create); + database.execSQL("BEGIN;"); + String[] names = columnNames.toArray(new String[0]); + for (int row = 0; row < rows; row++) { + Object[] insertArgs = new Object[columns]; + for (int column = 0; column < columns; column++) { + insertArgs[column] = builder.buildRowColumnValue(names, row, column); + } + database.execSQL(insert, insertArgs); + } + database.execSQL("COMMIT;"); + Log.i(TAG, String.format("Database built with %d columns, %d rows", columns, rows)); + } + + protected byte[] generateRandomByteArray(int size) { + byte[] data = new byte[size]; + random.nextBytes(data); + return data; + } + + protected void setUp(){}; + protected void tearDown(SQLiteDatabase database){}; + protected void createDatabasePreKey(SQLiteDatabase database){}; + protected void createDatabasePostKey(SQLiteDatabase database){}; + + protected void log(String message){ + Log.i(TAG, message); + } + + protected void logPragmaSetting(SQLiteDatabase database) { + String[] pragmas = new String[]{"cipher_version", "kdf_iter", "cipher_page_size", "cipher_use_hmac", + "cipher_hmac_algorithm", "cipher_kdf_algorithm"}; + String[] defaultPragmas = new String[]{"cipher_default_kdf_iter", "cipher_default_page_size", + "cipher_default_use_hmac", "cipher_default_hmac_algorithm", "cipher_default_kdf_algorithm"}; + for (String pragma : pragmas) { + String value = QueryHelper.singleValueFromQuery(database, String.format("PRAGMA %s;", pragma)); + log(String.format("PRAGMA %s set to %s", pragma, value)); + } + for (String defaultPragma : defaultPragmas) { + String value = QueryHelper.singleValueFromQuery(database, String.format("PRAGMA %s;", defaultPragma)); + log(String.format("PRAGMA %s set to %s", defaultPragma, value)); + } + } + + protected List ReservedWords = Arrays.asList(new String[]{ + "ABORT", + "ACTION", + "ADD", + "AFTER", + "ALL", + "ALTER", + "ANALYZE", + "AND", + "AS", + "ASC", + "ATTACH", + "AUTOINCREMENT", + "BEFORE", + "BEGIN", + "BETWEEN", + "BY", + "CASCADE", + "CASE", + "CAST", + "CHECK", + "COLLATE", + "COLUMN", + "COMMIT", + "CONFLICT", + "CONSTRAINT", + "CREATE", + "CROSS", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "DATABASE", + "DEFAULT", + "DEFERRABLE", + "DEFERRED", + "DELETE", + "DESC", + "DETACH", + "DISTINCT", + "DROP", + "EACH", + "ELSE", + "END", + "ESCAPE", + "EXCEPT", + "EXCLUSIVE", + "EXISTS", + "EXPLAIN", + "FAIL", + "FOR", + "FOREIGN", + "FROM", + "FULL", + "GLOB", + "GROUP", + "HAVING", + "IF", + "IGNORE", + "IMMEDIATE", + "IN", + "INDEX", + "INDEXED", + "INITIALLY", + "INNER", + "INSERT", + "INSTEAD", + "INTERSECT", + "INTO", + "IS", + "ISNULL", + "JOIN", + "KEY", + "LEFT", + "LIKE", + "LIMIT", + "MATCH", + "NATURAL", + "NO", + "NOT", + "NOTNULL", + "NULL", + "OF", + "OFFSET", + "ON", + "OR", + "ORDER", + "OUTER", + "PLAN", + "PRAGMA", + "PRIMARY", + "QUERY", + "RAISE", + "RECURSIVE", + "REFERENCES", + "REGEXP", + "REINDEX", + "RELEASE", + "RENAME", + "REPLACE", + "RESTRICT", + "RIGHT", + "ROLLBACK", + "ROW", + "SAVEPOINT", + "SELECT", + "SET", + "TABLE", + "TEMP", + "TEMPORARY", + "THEN", + "TO", + "TRANSACTION", + "TRIGGER", + "UNION", + "UNIQUE", + "UPDATE", + "USING", + "VACUUM", + "VALUES", + "VIEW", + "VIRTUAL", + "WHEN", + "WHERE", + "WITH", + "WITHOUT" + }); +} diff --git a/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperConfigureTest.java b/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperConfigureTest.java new file mode 100644 index 0000000..2d4430f --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperConfigureTest.java @@ -0,0 +1,51 @@ +package net.zetetic.tests; + +import android.content.Context; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteOpenHelper; +import net.zetetic.ZeteticApplication; + +import java.io.File; +import java.util.UUID; + +public class SQLiteOpenHelperConfigureTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + + UUID name = UUID.randomUUID(); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(name.toString()); + ConfigureHelper helper = new ConfigureHelper(ZeteticApplication.getInstance(), + databasePath.getAbsolutePath()); + helper.getWritableDatabase("foo"); + return helper.onConfigureCalled; + } + + @Override + public String getName() { + return "SQLiteOpenHelper Configure Test"; + } + + class ConfigureHelper extends SQLiteOpenHelper { + + public boolean onConfigureCalled = false; + + public ConfigureHelper(Context context, String databasePath) { + super(context, ZeteticApplication.DATABASE_NAME, null, 1); + } + + public void onConfigure(SQLiteDatabase database){ + onConfigureCalled = true; + } + + @Override + public void onCreate(SQLiteDatabase sqLiteDatabase) { + + } + + @Override + public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { + } + } + +} diff --git a/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperEnableWriteAheadLogAfterGetDatabaseTest.java b/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperEnableWriteAheadLogAfterGetDatabaseTest.java new file mode 100644 index 0000000..0ddc9c3 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperEnableWriteAheadLogAfterGetDatabaseTest.java @@ -0,0 +1,44 @@ +package net.zetetic.tests; + +import android.content.Context; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteOpenHelper; +import net.zetetic.ZeteticApplication; + +import java.io.File; +import java.util.UUID; + +public class SQLiteOpenHelperEnableWriteAheadLogAfterGetDatabaseTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + + database.close(); + UUID name = UUID.randomUUID(); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(name.toString()); + DatabaseHelper helper = new DatabaseHelper(ZeteticApplication.getInstance(), + databasePath.getAbsolutePath()); + database = helper.getWritableDatabase("foo"); + helper.setWriteAheadLoggingEnabled(true); + return database.isWriteAheadLoggingEnabled(); + } + + @Override + public String getName() { + return "SQLiteOpenHelper Enable WAL After Get DB"; + } + + class DatabaseHelper extends SQLiteOpenHelper { + + public DatabaseHelper(Context context, String databasePath) { + super(context, ZeteticApplication.DATABASE_NAME, null, 1); + } + + @Override + public void onCreate(SQLiteDatabase database) { } + + @Override + public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion){} + } + +} diff --git a/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperEnableWriteAheadLogBeforeGetDatabaseTest.java b/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperEnableWriteAheadLogBeforeGetDatabaseTest.java new file mode 100644 index 0000000..d045474 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperEnableWriteAheadLogBeforeGetDatabaseTest.java @@ -0,0 +1,43 @@ +package net.zetetic.tests; + +import android.content.Context; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteOpenHelper; +import net.zetetic.ZeteticApplication; + +import java.io.File; +import java.util.UUID; + +public class SQLiteOpenHelperEnableWriteAheadLogBeforeGetDatabaseTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + + database.close(); + UUID name = UUID.randomUUID(); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(name.toString()); + DatabaseHelper helper = new DatabaseHelper(ZeteticApplication.getInstance(), + databasePath.getAbsolutePath()); + helper.setWriteAheadLoggingEnabled(true); + database = helper.getWritableDatabase("foo"); + return database.isWriteAheadLoggingEnabled(); + } + + @Override + public String getName() { + return "SQLiteOpenHelper Enable WAL Before Get DB"; + } + + class DatabaseHelper extends SQLiteOpenHelper { + + public DatabaseHelper(Context context, String databasePath) { + super(context, ZeteticApplication.DATABASE_NAME, null, 1); + } + + @Override + public void onCreate(SQLiteDatabase database) { } + + @Override + public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion){} + } +} diff --git a/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperGetNameTest.java b/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperGetNameTest.java new file mode 100644 index 0000000..b57cd09 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperGetNameTest.java @@ -0,0 +1,40 @@ +package net.zetetic.tests; + +import android.content.Context; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteOpenHelper; +import net.zetetic.ZeteticApplication; + +import java.io.File; +import java.util.UUID; + +public class SQLiteOpenHelperGetNameTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + UUID name = UUID.randomUUID(); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(name.toString()); + DatabaseHelper helper = new DatabaseHelper(ZeteticApplication.getInstance(), + databasePath.getAbsolutePath()); + return databasePath.getAbsolutePath().equals(helper.getDatabaseName()); + } + + @Override + public String getName() { + return "SQLiteOpenHelper GetName Test"; + } + + class DatabaseHelper extends SQLiteOpenHelper { + + public DatabaseHelper(Context context, String databasePath) { + super(context, databasePath, null, 1); + } + + @Override + public void onCreate(SQLiteDatabase database) { } + + @Override + public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion){} + } + +} diff --git a/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperOnDowngradeTest.java b/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperOnDowngradeTest.java new file mode 100644 index 0000000..3a119ac --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperOnDowngradeTest.java @@ -0,0 +1,53 @@ +package net.zetetic.tests; + +import android.content.Context; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteOpenHelper; +import net.zetetic.ZeteticApplication; + +import java.io.File; +import java.util.UUID; + +public class SQLiteOpenHelperOnDowngradeTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + + UUID name = UUID.randomUUID(); + String password = "foo"; + File databasePath = ZeteticApplication.getInstance().getDatabasePath(name.toString()); + DatabaseHelper helper = new DatabaseHelper(ZeteticApplication.getInstance(), + databasePath.getAbsolutePath(), 2); + helper.getWritableDatabase(password); + if(helper.onDowngradeCalled) return false; + helper.close(); + helper = new DatabaseHelper(ZeteticApplication.getInstance(), databasePath.getAbsolutePath(), 1); + helper.getWritableDatabase(password); + return helper.onDowngradeCalled; + } + + @Override + public String getName() { + return "SQLiteOpenHelper OnDowngrade Test"; + } + + class DatabaseHelper extends SQLiteOpenHelper { + + public boolean onDowngradeCalled = false; + + public DatabaseHelper(Context context, String databasePath, int version) { + super(context, ZeteticApplication.DATABASE_NAME, null, version); + } + + @Override + public void onCreate(SQLiteDatabase database) { } + + @Override + public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion){} + + public void onDowngrade(SQLiteDatabase database, int oldVersion, int newVersion){ + onDowngradeCalled = true; + } + } +} diff --git a/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperWithByteArrayKeyTest.java b/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperWithByteArrayKeyTest.java new file mode 100644 index 0000000..9d24d17 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/SQLiteOpenHelperWithByteArrayKeyTest.java @@ -0,0 +1,69 @@ +package net.zetetic.tests; + +import android.content.Context; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteOpenHelper; +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class SQLiteOpenHelperWithByteArrayKeyTest extends SQLCipherTest { + + + private String databaseName = "foo.db"; + + @Override + public boolean execute(SQLiteDatabase database) { + boolean status = false; + database.close(); + byte[] key = generateRandomByteArray(32); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(databaseName); + SQLiteOpenHelper helper = new TestHelper(ZeteticApplication.getInstance(), databasePath.getPath()); + database = helper.getWritableDatabase(key); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{1, 2}); + database.close(); + helper.close(); + + helper = new TestHelper(ZeteticApplication.getInstance(), databasePath.getPath()); + database = helper.getWritableDatabase(key); + Cursor cursor = database.rawQuery("select * from t1;", null); + if (cursor != null) { + cursor.moveToNext(); + int a = cursor.getInt(0); + int b = cursor.getInt(1); + cursor.close(); + status = a == 1 && b == 2; + } + return status; + } + + @Override + public String getName() { + return "SQLiteOpenHelper with Byte Array Key"; + } + + private class TestHelper extends SQLiteOpenHelper { + + private static final int version = 1; + + public TestHelper(Context context, String databasePath){ + super(context, databaseName, null, version, null); + } + + public TestHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { + super(context, databaseName, factory, version); + } + + @Override + public void onCreate(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + } + + @Override + public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { + } + } + +} diff --git a/app/src/main/java/net/zetetic/tests/SimpleQueryTest.java b/app/src/main/java/net/zetetic/tests/SimpleQueryTest.java new file mode 100644 index 0000000..703ceee --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/SimpleQueryTest.java @@ -0,0 +1,40 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteStatement; + +import java.util.UUID; + +public class SimpleQueryTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a, b) values(?, ?);", + new Object[]{UUID.randomUUID().toString(), "foo"}); + database.execSQL("insert into t1(a, b) values(?, ?);", + new Object[]{UUID.randomUUID().toString(), "bar"}); + int count = 0; + Cursor cursor = database.rawQuery("select * from t1 where a = ?1 or ?1 = -1;", + new Object[]{-1}); + if(cursor != null){ + while (cursor.moveToNext()){ + count++; + } + cursor.close(); + } + +// Works +// SQLiteStatement statement = database.compileStatement("select count(*) from t1 where a = ?1 or ?1 = -1;"); +// statement. +// statement.bindLong(1, -1); +// long count = statement.simpleQueryForLong(); +// statement.close(); + return count == 2; + } + + @Override + public String getName() { + return "Simple Query Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/SoundexTest.java b/app/src/main/java/net/zetetic/tests/SoundexTest.java new file mode 100644 index 0000000..11f1090 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/SoundexTest.java @@ -0,0 +1,20 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; + +public class SoundexTest extends SQLCipherTest { + + String SQLCIPHER_SOUNDEX = "S421"; + + @Override + public boolean execute(SQLiteDatabase database) { + String soundex = QueryHelper.singleValueFromQuery(database, "SELECT soundex('sqlcipher');"); + return SQLCIPHER_SOUNDEX.equals(soundex); + } + + @Override + public String getName() { + return "Soundex Test"; + } +} diff --git a/src/main/java/net/zetetic/tests/StatusMemoryUsedTest.java b/app/src/main/java/net/zetetic/tests/StatusMemoryUsedTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/StatusMemoryUsedTest.java rename to app/src/main/java/net/zetetic/tests/StatusMemoryUsedTest.java diff --git a/app/src/main/java/net/zetetic/tests/SummingStepTest.java b/app/src/main/java/net/zetetic/tests/SummingStepTest.java new file mode 100644 index 0000000..15ef3ae --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/SummingStepTest.java @@ -0,0 +1,37 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +import java.util.Locale; + +// Provided via GitHub Issue: https://github.com/sqlcipher/android-database-sqlcipher/issues/526 +public class SummingStepTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table sample_session(_id , start_time TEXT , client_id TEXT, type_id TEXT, value TEXT);"); + database.execSQL("create table sample_point(_id ,start_time TEXT , client_id TEXT, type_id TEXT, value TEXT);"); + database.execSQL("insert into sample_session(_id , start_time ,client_id, type_id, value) values(1, 1000,5 ,2, 0);"); + database.execSQL("insert into sample_point(_id , start_time ,client_id, type_id, value) values(1, 1000,5 ,2, 46);"); + database.execSQL("insert into sample_point(_id , start_time ,client_id, type_id, value) values(2, 1000,6 ,2, 46);"); + String query = "select sample_session._id,sample_session.start_time,sample_session.type_id, sample_session.client_id," + + "sum ( case sample_point.type_id when 2 then sample_point.value else 0 end) as step, sum ( case sample_point.type_id when 4 then sample_point.value else 0 end) as calorie, sum ( case sample_point.type_id when 3 then sample_point.value else 0 end) as distance, " + + "sum ( case sample_point.type_id when 5 then sample_point.value else 0 end) as altitude_offset from sample_session INNER JOIN sample_point ON sample_session.start_time = sample_point.start_time and sample_session.client_id = sample_point.client_id " + + "where sample_session.client_id =? and sample_point.client_id =? and sample_point.type_id in ( ?,?,?,? ) " + + "group by sample_session.start_time ORDER BY sample_session.start_time DESC limit 0,1000"; + Cursor cursor = database.rawQuery(query, new String[]{"5","5","2","4","3","5"}); + if(cursor != null){ + cursor.moveToFirst(); + int index = cursor.getColumnIndex("step"); + long value = cursor.getInt(index); + setMessage(String.format(Locale.getDefault(), "Expected:%d Returned:%d", 46, value)); + return value == 46; + } + return false; + } + + @Override + public String getName() { + return "SummingStepTest Test"; + } +} diff --git a/src/main/java/net/zetetic/tests/TestResult.java b/app/src/main/java/net/zetetic/tests/TestResult.java similarity index 77% rename from src/main/java/net/zetetic/tests/TestResult.java rename to app/src/main/java/net/zetetic/tests/TestResult.java index d129969..d101977 100644 --- a/src/main/java/net/zetetic/tests/TestResult.java +++ b/app/src/main/java/net/zetetic/tests/TestResult.java @@ -5,10 +5,16 @@ public class TestResult { private String name; private boolean success; private String message; + private String error; public TestResult(String name, boolean success){ + this(name, success, ""); + } + + public TestResult(String name, boolean success, String error){ this.name = name; this.success = success; + this.error = error; } public void setResult(boolean success){ @@ -23,6 +29,8 @@ public boolean isSuccess() { return success; } + public String getError() {return error;} + @Override public String toString() { return isSuccess() ? "OK" : "FAILED"; diff --git a/app/src/main/java/net/zetetic/tests/TestSuiteRunner.java b/app/src/main/java/net/zetetic/tests/TestSuiteRunner.java new file mode 100644 index 0000000..58321d6 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/TestSuiteRunner.java @@ -0,0 +1,200 @@ +package net.zetetic.tests; + +import android.app.Activity; +import android.os.AsyncTask; +import android.os.Build; +import android.util.Log; +import android.view.WindowManager; + +import net.sqlcipher.CursorWindow; +import net.sqlcipher.CursorWindowAllocation; +import net.zetetic.ZeteticApplication; + +import java.util.ArrayList; +import java.util.List; + +public class TestSuiteRunner extends AsyncTask { + + String TAG = getClass().getSimpleName(); + private ResultNotifier notifier; + private Activity activity; + + public TestSuiteRunner(Activity activity) { + this.activity = activity; + if (this.activity != null) { + this.activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + + @Override + protected Void doInBackground(ResultNotifier... resultNotifiers) { + this.notifier = resultNotifiers[0]; + Log.i(ZeteticApplication.TAG, String.format("Running test suite on %s platform", Build.CPU_ABI)); + runSuite(); + return null; + } + + @Override + protected void onProgressUpdate(TestResult... values) { + notifier.send(values[0]); + } + + @Override + protected void onPostExecute(Void aVoid) { + notifier.complete(); + if (this.activity != null) { + this.activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + + private void runSuite() { + CursorWindowAllocation defaultAllocation = CursorWindow.getCursorWindowAllocation(); + for (SQLCipherTest test : getTestsToRun()) { + try { + CursorWindow.setCursorWindowAllocation(defaultAllocation); + Log.i(ZeteticApplication.TAG, "Running test:" + test.getName()); + TestResult result = test.run(); + publishProgress(result); + + } catch (Throwable e) { + Log.i(ZeteticApplication.TAG, e.toString()); + publishProgress(new TestResult(test.getName(), false, e.toString())); + } + finally { + CursorWindow.setCursorWindowAllocation(defaultAllocation); + } + } + } + + private List getTestsToRun() { + List tests = new ArrayList<>(); + if(ZeteticApplication.getInstance().supportsMinLibraryVersionRequired("4.5.4")) { + tests.add(new ResetQueryCacheFinalizesSqlStatementAndReleasesReferenceTest()); + } + if(ZeteticApplication.getInstance().supportsMinLibraryVersionRequired("4.5.4")){ + tests.add(new PreventUpdateWithNullWhereArgsFromThrowingExceptionTest()); + } + tests.add(new SummingStepTest()); + tests.add(new JsonCastTest()); + tests.add(new SimpleQueryTest()); + tests.add(new DefaultCursorWindowAllocationTest()); + tests.add(new DeleteTableWithNullWhereArgsTest()); + tests.add(new LoopingInsertTest()); + tests.add(new FIPSTest()); + tests.add(new PragmaCipherVersionTest()); + tests.add(new VerifyCipherProviderTest()); + tests.add(new VerifyCipherProviderVersionTest()); + tests.add(new JavaClientLibraryVersionTest()); + tests.add(new ReadWriteUserVersionTest()); + tests.add(new QueryDataSizeTest()); + tests.add(new FixedCursorWindowAllocationTest()); + tests.add(new GrowingCursorWindowAllocationTest()); + tests.add(new ReadWriteWriteAheadLoggingTest()); + tests.add(new CreateOpenDatabaseWithByteArrayTest()); + tests.add(new SQLiteOpenHelperWithByteArrayKeyTest()); + tests.add(new SQLiteOpenHelperEnableWriteAheadLogBeforeGetDatabaseTest()); + tests.add(new SQLiteOpenHelperEnableWriteAheadLogAfterGetDatabaseTest()); + tests.add(new SQLiteOpenHelperGetNameTest()); + tests.add(new SQLiteOpenHelperOnDowngradeTest()); + tests.add(new SQLiteOpenHelperConfigureTest()); + tests.add(new CheckIsDatabaseIntegrityOkTest()); + tests.add(new GetAttachedDatabasesTest()); + tests.add(new EnableForeignKeyConstraintsTest()); + tests.add(new ForeignKeyConstraintsEnabledWithTransactionTest()); + tests.add(new EnableWriteAheadLoggingTest()); + tests.add(new DisableWriteAheadLoggingTest()); + tests.add(new CheckIsWriteAheadLoggingEnabledTest()); + tests.add(new WriteAheadLoggingWithTransactionTest()); + tests.add(new WriteAheadLoggingWithInMemoryDatabaseTest()); + tests.add(new WriteAheadLoggingWithAttachedDatabaseTest()); + tests.add(new TransactionNonExclusiveTest()); + tests.add(new TransactionWithListenerTest()); + tests.add(new LargeDatabaseCursorAccessTest()); + + tests.add(new TimeLargeByteArrayQueryTest()); + + tests.add(new QueryLimitTest()); + tests.add(new RTreeTest()); + tests.add(new ReadWriteDatabaseToExternalStorageTest()); + tests.add(new BeginTransactionTest()); + tests.add(new QueryTenThousandDataTest()); + tests.add(new CompileBeginTest()); + tests.add(new TimeQueryExecutionTest()); + tests.add(new UnicodeTest()); + tests.add(new QueryIntegerToStringTest()); + tests.add(new QueryFloatToStringTest()); + tests.add(new ClosedDatabaseTest()); + tests.add(new AttachDatabaseTest()); + tests.add(new CipherMigrateTest()); + tests.add(new GetTypeFromCrossProcessCursorWrapperTest()); + tests.add(new InvalidPasswordTest()); + tests.add(new NullQueryResultTest()); + tests.add(new CrossProcessCursorQueryTest()); + tests.add(new InterprocessBlobQueryTest()); + tests.add(new LoopingQueryTest()); + tests.add(new LoopingCountQueryTest()); + tests.add(new AttachNewDatabaseTest()); + tests.add(new AttachExistingDatabaseTest()); + tests.add(new CanThrowSQLiteExceptionTest()); + tests.add(new RawExecSQLTest()); + tests.add(new RawExecSQLExceptionTest()); + tests.add(new CompiledSQLUpdateTest()); + tests.add(new AES128CipherTest()); + tests.add(new MigrateDatabaseFrom1xFormatToCurrentFormat()); + tests.add(new StatusMemoryUsedTest()); + tests.add(new ImportUnencryptedDatabaseTest()); + tests.add(new FullTextSearchTest()); + tests.add(new ReadableDatabaseTest()); + tests.add(new AutoVacuumOverReadTest()); + tests.add(new ReadableWritableAccessTest()); + tests.add(new CursorAccessTest()); + tests.add(new VerifyOnUpgradeIsCalledTest()); + tests.add(new MigrationUserVersion()); + tests.add(new ExportToUnencryptedDatabase()); + tests.add(new QueryNonEncryptedDatabaseTest()); + tests.add(new EnableForeignKeySupportTest()); + tests.add(new AverageOpenTimeTest()); + tests.add(new NestedTransactionsTest()); + tests.add(new ComputeKDFTest()); + tests.add(new SoundexTest()); + tests.add(new RawQueryTest()); + tests.add(new OpenReadOnlyDatabaseTest()); + tests.add(new RawRekeyTest()); + tests.add(new CorruptDatabaseTest()); + tests.add(new CustomCorruptionHandlerTest()); + tests.add(new MultiThreadReadWriteTest()); + tests.add(new VerifyUTF8EncodingForKeyTest()); + tests.add(new TextAsIntegerTest()); + tests.add(new TextAsDoubleTest()); + tests.add(new TextAsLongTest()); + tests.add(new CreateNonEncryptedDatabaseTest()); + tests.add(new ChangePasswordTest()); + tests.add(new ReadableWritableInvalidPasswordTest()); + tests.add(new InvalidOpenArgumentTest()); + tests.add(new CopyStringToBufferTestFloatSmallBuffer()); + tests.add(new CopyStringToBufferTestFloatLargeBuffer()); + tests.add(new CopyStringToBufferTestIntegerSmallBuffer()); + tests.add(new CopyStringToBufferTestIntegerLargeBuffer()); + tests.add(new CopyStringToBufferTestStringSmallBuffer()); + tests.add(new CopyStringToBufferTestStringLargeBuffer()); + tests.add(new CopyStringToBufferNullTest()); + tests.add(new OpenSQLCipher3DatabaseTest()); + tests.add(new MUTF8ToUTF8WithNullMigrationTest()); + tests.add(new RawQuerySyntaxErrorMessageTest()); + tests.add(new RawQueryNonsenseStatementErrorMessageTest()); + tests.add(new RawQueryNoSuchFunctionErrorMessageTest()); + tests.add(new CompileStatementSyntaxErrorMessageTest()); + tests.add(new ExecuteInsertConstraintErrorMessageTest()); + tests.add(new InsertWithOnConflictTest()); + tests.add(new FTS5Test()); + tests.add(new BindBooleanRawQueryTest()); + tests.add(new BindStringRawQueryTest()); + tests.add(new BindDoubleRawQueryTest()); + tests.add(new BindLongRawQueryTest()); + tests.add(new BindFloatRawQueryTest()); + tests.add(new BindByteArrayRawQueryTest()); + tests.add(new NullRawQueryTest()); + tests.add(new ReadWriteDatabaseToExternalStorageTest()); + return tests; + } +} diff --git a/app/src/main/java/net/zetetic/tests/TextAsDoubleTest.java b/app/src/main/java/net/zetetic/tests/TextAsDoubleTest.java new file mode 100644 index 0000000..4d76bde --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/TextAsDoubleTest.java @@ -0,0 +1,24 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class TextAsDoubleTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a TEXT);"); + database.execSQL("insert into t1(a) values(3.14159265359);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + double value = cursor.getDouble(0); + return value == 3.14159265359; + } + return false; + } + + @Override + public String getName() { + return "Text As Double Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/TextAsIntegerTest.java b/app/src/main/java/net/zetetic/tests/TextAsIntegerTest.java new file mode 100644 index 0000000..3a17853 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/TextAsIntegerTest.java @@ -0,0 +1,25 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class TextAsIntegerTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a TEXT);"); + database.execSQL("insert into t1(a) values(15);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + int value = cursor.getInt(0); + return value == 15; + } + return false; + } + + @Override + public String getName() { + return "Text as Integer Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/TextAsLongTest.java b/app/src/main/java/net/zetetic/tests/TextAsLongTest.java new file mode 100644 index 0000000..76af730 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/TextAsLongTest.java @@ -0,0 +1,25 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class TextAsLongTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a TEXT, b TEXT);"); + database.execSQL("insert into t1(a,b) values(500, 500);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + long value = cursor.getLong(0); + return value == 500; + } + return false; + } + + @Override + public String getName() { + return "Text as Long Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/TimeLargeByteArrayQueryTest.java b/app/src/main/java/net/zetetic/tests/TimeLargeByteArrayQueryTest.java new file mode 100644 index 0000000..79c0a3c --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/TimeLargeByteArrayQueryTest.java @@ -0,0 +1,158 @@ +package net.zetetic.tests; + +import android.util.Log; + +import net.sqlcipher.Cursor; +import net.sqlcipher.CursorWindow; +import net.sqlcipher.CursorWindowAllocation; +import net.sqlcipher.CustomCursorWindowAllocation; +import net.sqlcipher.database.SQLiteDatabase; + +import java.text.NumberFormat; +import java.util.Locale; + +public class TimeLargeByteArrayQueryTest extends SQLCipherTest { + + long allocationSize = 1024 * 1024 * 4; + CursorWindowAllocation allocation = new CustomCursorWindowAllocation(allocationSize, 0, allocationSize); + + @Override + public boolean execute(SQLiteDatabase database) { + + buildDatabase(database, 200, 128, new RowColumnValueBuilder() { + @Override + public Object buildRowColumnValue(String[] columns, int row, int column) { + return generateRandomByteArray(16); + } + }); + runSamplingScenario(database, 30, retrieveAllData); + return true; + } + + public interface CursorScenario { + Stats run(Cursor cursor); + String getName(); + CursorWindowAllocation getAllocation(); + } + + CursorScenario mixedRowAccess = new CursorScenario() { + //int[] rowIndexes = new int[]{1, 500, 1000, 5000, 9999}; + //int[] rowIndexes = new int[]{1, 250, 750, 999}; + int[] rowIndexes = new int[]{18000, 9000, 4500, 1000}; + @Override + public Stats run(Cursor cursor) { + long totalBytes = 0; + int columnCount = cursor.getColumnCount(); + for (int row = 0; row < rowIndexes.length; row++) { + int index = rowIndexes[row]; + long start = System.nanoTime(); + cursor.moveToPosition(index); + for (int column = 0; column < columnCount; column++) { + byte[] data = cursor.getBlob(column); + totalBytes += data.length; + } + long stop = System.nanoTime(); + Log.i(TAG, String.format("%s scenario read data for row:%d in %.2f seconds", + getName(), index, toSeconds(start, stop))); + } + return new Stats(rowIndexes.length, totalBytes); + } + + @Override + public String getName() { + return "Mixed row"; + } + + @Override + public CursorWindowAllocation getAllocation() { + return allocation; + } + }; + + CursorScenario retrieveAllData = new CursorScenario() { + @Override + public Stats run(Cursor cursor) { + long rows = 0; + long totalBytes = 0; + int columnCount = cursor.getColumnCount(); + while (cursor.moveToNext()) { + rows++; + byte[][] data = new byte[columnCount][]; + for (int column = 0; column < columnCount; column++) { + data[column] = cursor.getBlob(column); + totalBytes += data[column].length; + } + } + return new Stats(rows, totalBytes); + } + + @Override + public String getName() { + return "Full query"; + } + + @Override + public CursorWindowAllocation getAllocation() { + return allocation; + } + }; + + private void runSamplingScenario(SQLiteDatabase database, int runs, CursorScenario scenario) { + double totalQueryTime = 0; + for (int run = 0; run < runs; run++) { + Stats result = timeQuery(database, "SELECT * FROM t1;", scenario); + String message = String.format(Locale.getDefault(), + "%s sample:%d rows:%s time:%.2f seconds data:%s", + scenario.getName(), + run + 1, + NumberFormat.getInstance().format(result.RowCount), + result.QueryTime, + bytesToString(result.TotalBytes)); + totalQueryTime += result.QueryTime; + Log.i(TAG, message); + } + String message = String.format(Locale.getDefault(), + "%s average cursor time:%.2f seconds for %d samples", + scenario.getName(), totalQueryTime / runs, runs); + Log.i(TAG, message); + setMessage(message); + } + + private Stats timeQuery(SQLiteDatabase database, String query, CursorScenario scenario) { + long start = System.nanoTime(); + CursorWindow.setCursorWindowAllocation(scenario.getAllocation()); + Cursor cursor = database.rawQuery(query, new Object[]{}); + if (cursor == null) return new Stats(); + Stats stats = scenario.run(cursor); + long stop = System.nanoTime(); + cursor.close(); + stats.QueryTime = toSeconds(start, stop); + return stats; + } + + private String bytesToString(long bytes) { + int unit = 1024; + if (bytes < unit) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(unit)); + String pre = String.valueOf("KMGTPE".charAt(exp - 1)); + return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); + } + + @Override + public String getName() { + return "Time Large Byte Array Query Test"; + } + + class Stats { + public double QueryTime; + public long RowCount; + public long TotalBytes; + + public Stats(){} + + public Stats(long rowCount, long totalBytes){ + this.RowCount = rowCount; + this.TotalBytes = totalBytes; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/zetetic/tests/TimeQueryExecutionTest.java b/app/src/main/java/net/zetetic/tests/TimeQueryExecutionTest.java new file mode 100644 index 0000000..a7258fe --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/TimeQueryExecutionTest.java @@ -0,0 +1,56 @@ +package net.zetetic.tests; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; + +public class TimeQueryExecutionTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + + int ROWS = 1000; + long beforeTotal = System.nanoTime(); + database.rawExecSQL("CREATE TABLE t1(a);"); + for(int index = 0; index < ROWS; index++){ + database.execSQL("INSERT INTO t1(a) values(?);", new Object[]{String.valueOf(index)}); + } + long before = System.nanoTime(); + Cursor cursor = database.rawQuery("SELECT * from t1;", new String[]{}); + long after = System.nanoTime(); + log(String.format("SELECT * from t1; took %d ms", + toMilliseconds(before, after))); + if(cursor != null){ + before = System.nanoTime(); + cursor.moveToFirst(); + after = System.nanoTime(); + log(String.format("moveToFirst() took %d ms", + toMilliseconds(before, after))); + long beforeCursorMove = System.nanoTime(); + while(cursor.moveToNext()){ + before = System.nanoTime(); + String value = cursor.getString(0); + after = System.nanoTime(); + log(String.format("getInt(0) returned:%s took %d ms\n", + value, toMilliseconds(before, after))); + } + long afterCursorMove = System.nanoTime(); + log(String.format("Complete cursor operation time:%d ms", + toMilliseconds(beforeCursorMove, afterCursorMove))); + cursor.close(); + } + database.close(); + long totalRuntime = toMilliseconds(beforeTotal, System.nanoTime()); + String message = String.format("Total runtime:%d ms\n", totalRuntime); + log(message); + setMessage(message); + return true; + } + + private long toMilliseconds(long before, long after){ + return (after - before)/1000000L; + } + + @Override + public String getName() { + return "Time Query Execution Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/TransactionNonExclusiveTest.java b/app/src/main/java/net/zetetic/tests/TransactionNonExclusiveTest.java new file mode 100644 index 0000000..cc1ec46 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/TransactionNonExclusiveTest.java @@ -0,0 +1,20 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; + +public class TransactionNonExclusiveTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.beginTransactionNonExclusive(); + database.execSQL("create table t1(a,b);"); + database.setTransactionSuccessful(); + database.endTransaction(); + return true; + } + + @Override + public String getName() { + return "Transaction Immediate Mode"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/TransactionWithListenerTest.java b/app/src/main/java/net/zetetic/tests/TransactionWithListenerTest.java new file mode 100644 index 0000000..1644d46 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/TransactionWithListenerTest.java @@ -0,0 +1,40 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteTransactionListener; + +public class TransactionWithListenerTest extends SQLCipherTest { + + boolean beginCalled = false; + + @Override + public boolean execute(SQLiteDatabase database) { + database.beginTransactionWithListener(listener); + database.execSQL("create table t1(a,b);"); + database.setTransactionSuccessful(); + database.endTransaction(); + return beginCalled; + } + + @Override + public String getName() { + return "Transaction Exclusive Mode"; + } + + SQLiteTransactionListener listener = new SQLiteTransactionListener() { + @Override + public void onBegin() { + beginCalled = true; + } + + @Override + public void onCommit() { + + } + + @Override + public void onRollback() { + + } + }; +} diff --git a/app/src/main/java/net/zetetic/tests/UnicodeTest.java b/app/src/main/java/net/zetetic/tests/UnicodeTest.java new file mode 100644 index 0000000..20deaa9 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/UnicodeTest.java @@ -0,0 +1,28 @@ +package net.zetetic.tests; + +import android.database.Cursor; +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteStatement; +import java.io.UnsupportedEncodingException; + +public class UnicodeTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + //if (android.os.Build.VERSION.SDK_INT >= 23) { // Android M + // This will crash on Android releases 1.X-5.X due the following Android bug: + // https://code.google.com/p/android/issues/detail?id=81341 + SQLiteStatement st = database.compileStatement("SELECT '\uD83D\uDE03'"); // SMILING FACE (MOUTH OPEN) + String res = st.simpleQueryForString(); + String message = String.format("Returned value:%s", res); + setMessage(message); + Log.i(TAG, message); + return res.equals("\uD83D\uDE03"); + //} + } + + @Override + public String getName() { + return "Unicode Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/VerifyCipherProviderTest.java b/app/src/main/java/net/zetetic/tests/VerifyCipherProviderTest.java new file mode 100644 index 0000000..18a0d53 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/VerifyCipherProviderTest.java @@ -0,0 +1,20 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; + +public class VerifyCipherProviderTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + String provider = QueryHelper.singleValueFromQuery(database, + "PRAGMA cipher_provider;"); + setMessage(String.format("Reported:%s", provider)); + return provider.contains("openssl"); + } + + @Override + public String getName() { + return "Verify Cipher Provider Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/VerifyCipherProviderVersionTest.java b/app/src/main/java/net/zetetic/tests/VerifyCipherProviderVersionTest.java new file mode 100644 index 0000000..34d73cd --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/VerifyCipherProviderVersionTest.java @@ -0,0 +1,21 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; + +public class VerifyCipherProviderVersionTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + String provider = QueryHelper.singleValueFromQuery(database, + "PRAGMA cipher_provider_version;"); + setMessage(String.format("Reported:%s", provider)); + return provider.contains("OpenSSL 1.1.1") || + provider.contains("OpenSSL 1.0.2u-fips"); + } + + @Override + public String getName() { + return "Verify Cipher Provider Version"; + } +} diff --git a/src/main/java/net/zetetic/tests/VerifyOnUpgradeIsCalledTest.java b/app/src/main/java/net/zetetic/tests/VerifyOnUpgradeIsCalledTest.java similarity index 100% rename from src/main/java/net/zetetic/tests/VerifyOnUpgradeIsCalledTest.java rename to app/src/main/java/net/zetetic/tests/VerifyOnUpgradeIsCalledTest.java diff --git a/app/src/main/java/net/zetetic/tests/VerifyUTF8EncodingForKeyTest.java b/app/src/main/java/net/zetetic/tests/VerifyUTF8EncodingForKeyTest.java new file mode 100644 index 0000000..53b2898 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/VerifyUTF8EncodingForKeyTest.java @@ -0,0 +1,73 @@ +package net.zetetic.tests; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import android.database.sqlite.SQLiteException; +import net.sqlcipher.database.SQLiteStatement; +import net.zetetic.ZeteticApplication; + +import java.io.File; + +public class VerifyUTF8EncodingForKeyTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + + try { + String password = "hello"; + String invalidPassword = "ŨťŬŬů"; + SQLiteDatabase sourceDatabase; + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("hello.db"); + File sourceDatabaseFile = ZeteticApplication.getInstance().getDatabasePath("hello.db"); + database.close(); + try { + sourceDatabase = SQLiteDatabase.openDatabase(sourceDatabaseFile.getPath(), invalidPassword, null, SQLiteDatabase.OPEN_READWRITE); + if(queryContent(sourceDatabase)){ + sourceDatabase.close(); + setMessage(String.format("Database should not open with password:%s", invalidPassword)); + return false; + } + } catch (SQLiteException ex){ + log(String.format("Error opening database:%s", ex.getMessage())); + } + SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { + public void preKey(SQLiteDatabase db) {} + public void postKey(SQLiteDatabase db) { + SQLiteStatement statement = db.compileStatement("PRAGMA cipher_migrate;"); + long result = statement.simpleQueryForLong(); + statement.close(); + String message = String.format("cipher_migrate result:%d", result); + setMessage(message); + log(message); + } + }; + sourceDatabase = SQLiteDatabase.openDatabase(sourceDatabaseFile.getAbsolutePath(), + password, null, + SQLiteDatabase.OPEN_READWRITE | SQLiteDatabase.CREATE_IF_NECESSARY, hook); + return queryContent(sourceDatabase); + } catch (Exception e) { + log(String.format("Error attempting to open database:%s", e.getMessage())); + return false; + } + } + + private boolean queryContent(SQLiteDatabase source){ + Cursor result = source.rawQuery("select * from t1", new String[]{}); + if(result != null){ + result.moveToFirst(); + String a = result.getString(0); + String b = result.getString(1); + result.close(); + source.close(); + return a.equals("one for the money") && + b.equals("two for the show"); + } + return false; + } + + @Override + public String getName() { + return "Verify Only UTF-8 Key Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/WriteAheadLoggingWithAttachedDatabaseTest.java b/app/src/main/java/net/zetetic/tests/WriteAheadLoggingWithAttachedDatabaseTest.java new file mode 100644 index 0000000..9decb39 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/WriteAheadLoggingWithAttachedDatabaseTest.java @@ -0,0 +1,23 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.ZeteticApplication; + +import java.io.File; +import java.util.UUID; + +public class WriteAheadLoggingWithAttachedDatabaseTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + UUID name = UUID.randomUUID(); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(name.toString()); + database.execSQL("ATTACH DATABASE ? as foo;", new Object[]{databasePath.getAbsolutePath()}); + boolean result = database.enableWriteAheadLogging(); + return result == false; + } + + @Override + public String getName() { + return "Disallow WAL Mode with Attached DB"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/WriteAheadLoggingWithInMemoryDatabaseTest.java b/app/src/main/java/net/zetetic/tests/WriteAheadLoggingWithInMemoryDatabaseTest.java new file mode 100644 index 0000000..1698edd --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/WriteAheadLoggingWithInMemoryDatabaseTest.java @@ -0,0 +1,19 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; + +public class WriteAheadLoggingWithInMemoryDatabaseTest extends SQLCipherTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.close(); + database = SQLiteDatabase.openDatabase(SQLiteDatabase.MEMORY, "", + null, SQLiteDatabase.OPEN_READWRITE); + boolean result = database.enableWriteAheadLogging(); + return result == false; + } + + @Override + public String getName() { + return "Disallow WAL Mode with in memory DB"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/WriteAheadLoggingWithTransactionTest.java b/app/src/main/java/net/zetetic/tests/WriteAheadLoggingWithTransactionTest.java new file mode 100644 index 0000000..4a1f149 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/WriteAheadLoggingWithTransactionTest.java @@ -0,0 +1,24 @@ +package net.zetetic.tests; + +import net.sqlcipher.database.SQLiteDatabase; + +public class WriteAheadLoggingWithTransactionTest extends SQLCipherTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.beginTransaction(); + try { + database.enableWriteAheadLogging(); + } catch (IllegalStateException ex){ + if(ex.getMessage().equals("Write Ahead Logging cannot be enabled while in a transaction")) { + return true; + } + } + return false; + } + + @Override + public String getName() { + return "Disallow WAL Mode while in transaction"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/AES128CipherTest.java b/app/src/main/java/net/zetetic/tests/support/AES128CipherTest.java new file mode 100644 index 0000000..2868b99 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/AES128CipherTest.java @@ -0,0 +1,67 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class AES128CipherTest implements ISupportTest { + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { + @Override + public void preKey(SQLiteDatabase sqLiteDatabase) { + + } + + @Override + public void postKey(SQLiteDatabase sqLiteDatabase) { + sqLiteDatabase.rawExecSQL("PRAGMA cipher = 'aes-128-cbc'"); + } + }; + SupportFactory factory = new SupportFactory(passphrase, hook); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + String actual = ""; + String value = "hey"; + database.execSQL("create table t1(a)"); + database.execSQL("insert into t1(a) values (?)", new Object[]{value}); + Cursor c = database.query("select * from t1", new String[]{}); + if(c != null){ + c.moveToFirst(); + actual = c.getString(0); + c.close(); + } + result.setResult(actual.equals(value)); + + return result; + } + + @Override + public String getName() { + return "AES-128 Bit Cipher Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/AttachDatabaseTest.java b/app/src/main/java/net/zetetic/tests/support/AttachDatabaseTest.java new file mode 100644 index 0000000..9279db6 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/AttachDatabaseTest.java @@ -0,0 +1,31 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import java.io.File; + +public class AttachDatabaseTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + boolean status; + String password = "test123"; + File fooDatabase = ZeteticApplication.getInstance().getDatabasePath("foo.db"); + if(fooDatabase.exists()){ + fooDatabase.delete(); + } + database.execSQL("ATTACH database ? AS encrypted KEY ?", new Object[]{fooDatabase.getAbsolutePath(), password}); + database.execSQL("create table encrypted.t1(a,b);"); + database.execSQL("insert into encrypted.t1(a,b) values(?,?);", new Object[]{"one for the money", "two for the show"}); + int rowCount = QueryHelper.singleIntegerValueFromQuery(database, "select count(*) from encrypted.t1;"); + status = rowCount == 1; + database.close(); + return status; + } + + @Override + public String getName() { + return "Attach database test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/AttachNewDatabaseTest.java b/app/src/main/java/net/zetetic/tests/support/AttachNewDatabaseTest.java new file mode 100644 index 0000000..17ee2bb --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/AttachNewDatabaseTest.java @@ -0,0 +1,39 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import java.io.File; + +public class AttachNewDatabaseTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase encryptedDatabase) { + + encryptedDatabase.execSQL("create table t1(a,b)"); + encryptedDatabase.execSQL("insert into t1(a,b) values(?, ?)", new Object[]{"one", "two"}); + + String newKey = "foo"; + File newDatabasePath = ZeteticApplication.getInstance().getDatabasePath("normal.db"); + String attachCommand = "ATTACH DATABASE ? as encrypted KEY ?"; + String createCommand = "create table encrypted.t1(a,b)"; + String insertCommand = "insert into encrypted.t1 SELECT * from t1"; + String detachCommand = "DETACH DATABASE encrypted"; + encryptedDatabase.execSQL(attachCommand, new Object[]{newDatabasePath.getAbsolutePath(), newKey}); + encryptedDatabase.execSQL(createCommand); + encryptedDatabase.execSQL(insertCommand); + encryptedDatabase.execSQL(detachCommand); + + return true; + } + + @Override + protected void tearDown(SQLiteDatabase database) { + File newDatabasePath = ZeteticApplication.getInstance().getDatabasePath("normal.db"); + newDatabasePath.delete(); + } + + @Override + public String getName() { + return "Attach New Database Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/AutoVacuumOverReadTest.java b/app/src/main/java/net/zetetic/tests/support/AutoVacuumOverReadTest.java new file mode 100644 index 0000000..6d9fc3c --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/AutoVacuumOverReadTest.java @@ -0,0 +1,29 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class AutoVacuumOverReadTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + + database.execSQL("BEGIN EXCLUSIVE"); + + String createTableMessages = "create table Message (_id integer primary key autoincrement, syncServerId text, syncServerTimeStamp integer, displayName text, timeStamp integer, subject text, flagRead integer, flagLoaded integer, flagFavorite integer, flagAttachment integer, flags integer, clientId integer, messageId text, mailboxKey integer, accountKey integer, fromList text, toList text, ccList text, bccList text, replyToList text, meetingInfo text);"; + String createTableMessagesUpdates = "create table Message_Updates (_id integer unique, syncServerId text, syncServerTimeStamp integer, displayName text, timeStamp integer, subject text, flagRead integer, flagLoaded integer, flagFavorite integer, flagAttachment integer, flags integer, clientId integer, messageId text, mailboxKey integer, accountKey integer, fromList text, toList text, ccList text, bccList text, replyToList text, meetingInfo text);"; + String createTableMessageDeletes = "create table Message_Deletes (_id integer unique, syncServerId text, syncServerTimeStamp integer, displayName text, timeStamp integer, subject text, flagRead integer, flagLoaded integer, flagFavorite integer, flagAttachment integer, flags integer, clientId integer, messageId text, mailboxKey integer, accountKey integer, fromList text, toList text, ccList text, bccList text, replyToList text, meetingInfo text);"; + + database.execSQL(createTableMessageDeletes); + database.execSQL(createTableMessagesUpdates); + database.execSQL(createTableMessages); + + database.execSQL("COMMIT TRANSACTION"); + + return true; + } + + @Override + public String getName() { + return "Autovacuum Over Read Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/BeginTransactionTest.java b/app/src/main/java/net/zetetic/tests/support/BeginTransactionTest.java new file mode 100644 index 0000000..746c8ad --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/BeginTransactionTest.java @@ -0,0 +1,18 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class BeginTransactionTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.beginTransaction(); + database.endTransaction(); + return true; + } + + @Override + public String getName() { + return "Begin transaction test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/BindBooleanRawQueryTest.java b/app/src/main/java/net/zetetic/tests/support/BindBooleanRawQueryTest.java new file mode 100644 index 0000000..175d660 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/BindBooleanRawQueryTest.java @@ -0,0 +1,28 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class BindBooleanRawQueryTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", true}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", new Object[]{true}); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + int b = cursor.getInt(1); + cursor.close(); + return a.equals("one for the money") && b == 1; + } + } + return false; + } + + @Override + public String getName() { + return "Bind Boolean for RawQuery Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/BindByteArrayRawQueryTest.java b/app/src/main/java/net/zetetic/tests/support/BindByteArrayRawQueryTest.java new file mode 100644 index 0000000..5ed9d61 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/BindByteArrayRawQueryTest.java @@ -0,0 +1,34 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; +import java.security.SecureRandom; +import java.util.Arrays; + +public class BindByteArrayRawQueryTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + + SecureRandom random = new SecureRandom(); + byte[] randomData = new byte[20]; + random.nextBytes(randomData); + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", randomData}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", new Object[]{randomData}); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + byte[] b = cursor.getBlob(1); + cursor.close(); + return a.equals("one for the money") && Arrays.equals(randomData, b); + } + } + return false; + } + + @Override + public String getName() { + return "Bind Byte Array for RawQuery Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/BindDoubleRawQueryTest.java b/app/src/main/java/net/zetetic/tests/support/BindDoubleRawQueryTest.java new file mode 100644 index 0000000..1330997 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/BindDoubleRawQueryTest.java @@ -0,0 +1,28 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class BindDoubleRawQueryTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", 2.0d}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", new Object[]{2.0d}); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + Double b = cursor.getDouble(1); + cursor.close(); + return a.equals("one for the money") && b == 2.0d; + } + } + return false; + } + + @Override + public String getName() { + return "Bind Double for RawQuery Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/BindFloatRawQueryTest.java b/app/src/main/java/net/zetetic/tests/support/BindFloatRawQueryTest.java new file mode 100644 index 0000000..ed9bbc4 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/BindFloatRawQueryTest.java @@ -0,0 +1,28 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class BindFloatRawQueryTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", 2.25f}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", new Object[]{2.25f}); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + float b = cursor.getFloat(1); + cursor.close(); + return a.equals("one for the money") && b == 2.25f; + } + } + return false; + } + + @Override + public String getName() { + return "Bind Float for RawQuery Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/BindLongRawQueryTest.java b/app/src/main/java/net/zetetic/tests/support/BindLongRawQueryTest.java new file mode 100644 index 0000000..b66840c --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/BindLongRawQueryTest.java @@ -0,0 +1,28 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class BindLongRawQueryTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", 2L}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", new Object[]{2L}); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + long b = cursor.getLong(1); + cursor.close(); + return a.equals("one for the money") && b == 2L; + } + } + return false; + } + + @Override + public String getName() { + return "Bind Long for RawQuery Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/BindStringRawQueryTest.java b/app/src/main/java/net/zetetic/tests/support/BindStringRawQueryTest.java new file mode 100644 index 0000000..f5a046e --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/BindStringRawQueryTest.java @@ -0,0 +1,28 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class BindStringRawQueryTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", "two for the show"}); + Cursor cursor = database.rawQuery("select * from t1 where b = ?;", new Object[]{"two for the show"}); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + String b = cursor.getString(1); + cursor.close(); + return a.equals("one for the money") && b.equals("two for the show"); + } + } + return false; + } + + @Override + public String getName() { + return "Bind String for RawQuery Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CanThrowSQLiteExceptionTest.java b/app/src/main/java/net/zetetic/tests/support/CanThrowSQLiteExceptionTest.java new file mode 100644 index 0000000..0e81e5b --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CanThrowSQLiteExceptionTest.java @@ -0,0 +1,23 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import net.zetetic.tests.SQLCipherTest; + +public class CanThrowSQLiteExceptionTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + + try{ + throw new SQLiteException(); + }catch (SQLiteException ex){ + return true; + } + } + + @Override + public String getName() { + return "SQLiteException Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CheckIsDatabaseIntegrityOkTest.java b/app/src/main/java/net/zetetic/tests/support/CheckIsDatabaseIntegrityOkTest.java new file mode 100644 index 0000000..f6de654 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CheckIsDatabaseIntegrityOkTest.java @@ -0,0 +1,16 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class CheckIsDatabaseIntegrityOkTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + return database.isDatabaseIntegrityOk(); + } + + @Override + public String getName() { + return "Check Database Integrity"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CheckIsWriteAheadLoggingEnabledTest.java b/app/src/main/java/net/zetetic/tests/support/CheckIsWriteAheadLoggingEnabledTest.java new file mode 100644 index 0000000..62486e5 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CheckIsWriteAheadLoggingEnabledTest.java @@ -0,0 +1,20 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class CheckIsWriteAheadLoggingEnabledTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.enableWriteAheadLogging(); + boolean statusWhenEnabled = database.isWriteAheadLoggingEnabled(); + database.disableWriteAheadLogging(); + boolean statusWhenDisabled = database.isWriteAheadLoggingEnabled(); + return statusWhenEnabled && !statusWhenDisabled; + } + + @Override + public String getName() { + return "Test isWriteAheadLoggingEnabled"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CipherMigrateTest.java b/app/src/main/java/net/zetetic/tests/support/CipherMigrateTest.java new file mode 100644 index 0000000..eeb5b97 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CipherMigrateTest.java @@ -0,0 +1,69 @@ +package net.zetetic.tests.support; + +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.QueryHelper; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import net.zetetic.tests.TestResult; +import java.io.File; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class CipherMigrateTest implements ISupportTest { + + File olderFormatDatabase = ZeteticApplication.getInstance().getDatabasePath("2x.db"); + + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + final boolean[] status = {false}; + try { + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("2x.db"); + SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { + public void preKey(SQLiteDatabase database) {} + public void postKey(SQLiteDatabase database) { + String value = QueryHelper.singleValueFromQuery(database, "PRAGMA cipher_migrate"); + status[0] = Integer.valueOf(value) == 0; + } + }; + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase, hook); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(olderFormatDatabase.getAbsolutePath()) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + if(database != null){ + database.close(); + } + } catch (Exception e) { + Log.i("CipherMigrateTest", "error", e); + } + result.setResult(status[0]); + + return result; + } + + @Override + public String getName() { + return "Cipher Migrate Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/ClosedDatabaseTest.java b/app/src/main/java/net/zetetic/tests/support/ClosedDatabaseTest.java new file mode 100644 index 0000000..f2ee33d --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ClosedDatabaseTest.java @@ -0,0 +1,298 @@ +package net.zetetic.tests.support; + +import android.util.Log; +import net.sqlcipher.DatabaseErrorHandler; +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import java.io.File; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class ClosedDatabaseTest implements ISupportTest { + private static final String TAG = "ClosedDatabaseTest"; + + @Override + public TestResult run() { + + TestResult result = new TestResult(getName(), false); + try { + result.setResult(execute()); + SQLiteDatabase.releaseMemory(); + } catch (Exception e) { + Log.v(ZeteticApplication.TAG, e.toString()); + } + return result; + } + + public boolean execute() { + + File closedDatabasePath = ZeteticApplication.getInstance().getDatabasePath("closed-db-test.db"); + + boolean status = false; + + try { + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(closedDatabasePath.getAbsolutePath()) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + database.close(); + + status = execute_closed_database_tests(database); + } catch (Exception e) { + // Uncaught [unexpected] exception: + Log.e(ZeteticApplication.TAG, "Unexpected exception", e); + return false; + } + finally { + closedDatabasePath.delete(); + } + + return status; + } + + @SuppressWarnings("deprecation") + boolean execute_closed_database_tests(SupportSQLiteDatabase database) { + try { + /* operations that check if db is closed (and throw IllegalStateException): */ + try { + // should throw IllegalStateException: + database.beginTransaction(); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.beginTransaction() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.beginTransaction() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.endTransaction(); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.endTransaction() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.endTransaction() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.setTransactionSuccessful(); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.setTransactionSuccessful() did NOT throw throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.setTransactionSuccessful() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.getVersion(); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.getVersion() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.getVersion() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.setVersion(111); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.setVersion() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.setVersion() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.getMaximumSize(); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.getMaximumSize() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.getMaximumSize() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.setMaximumSize(111); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.setMaximumSize() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.setMaximumSize() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.getPageSize(); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.getPageSize() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.getPageSize() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.setPageSize(111); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.setPageSize() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.setPageSize() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.compileStatement("SELECT 1;"); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.compileStatement() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.compileStatement() did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.query("t1", new String[]{"a", "b"}); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.query() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.query() did throw exception on closed database OK", e); + } + + // TODO: cover more query functions + + try { + // should throw IllegalStateException: + database.execSQL("SELECT 1;"); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.execSQL(String) did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.execSQL(String) did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.execSQL("SELECT 1;", new Object[1]); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.execSQL(String, Object[]) did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.execSQL(String, Object[]) did throw exception on closed database OK", e); + } + + try { + // should throw IllegalStateException: + database.execSQL("SELECT 1;"); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.rawExecSQL() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.rawExecSQL() did throw exception on closed database OK", e); + } + + /* operations that do not explicitly check if db is closed + * ([should] throw SQLiteException on a closed database): */ + +// try { +// // should throw IllegalStateException: +// database.setLocale(Locale.getDefault()); +// +// // should not get here: +// Log.e(ZeteticApplication.TAG, "SQLiteDatabase.setLocale() did NOT throw exception on closed database"); +// return false; +// } catch (SQLiteException e) { +// Log.v(ZeteticApplication.TAG, "SQLiteDatabase.setLocale() did throw exception on closed database OK", e); +// } + + try { + // should throw IllegalStateException [since it calls getVersion()]: + database.needUpgrade(111); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.needUpgrade() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.needUpgrade() did throw exception on closed database OK", e); + } + + /* operations that are NOT expected to throw an exception if the database is closed ([should] not crash) */ + + + /* XXX TODO: these functions should check the db state, + * TBD either throw or simply return false if the db is closed */ + database.yieldIfContendedSafely(); + database.yieldIfContendedSafely(100); + + database.inTransaction(); + database.isDbLockedByCurrentThread(); + + database.close(); + + database.isReadOnly(); + database.isOpen(); + + try { + // should throw IllegalStateException: + database.setMaxSqlCacheSize(111); + + // should not get here: + Log.e(ZeteticApplication.TAG, "SQLiteDatabase.setMaxSqlCacheSize() did NOT throw exception on closed database"); + return false; + } catch (IllegalStateException e) { + Log.v(ZeteticApplication.TAG, "SQLiteDatabase.setMaxSqlCacheSize() did throw exception on closed database OK", e); + } + + } catch (Exception e) { + // Uncaught [unexpected] exception: + Log.e(ZeteticApplication.TAG, "Unexpected exception", e); + return false; + } + + return true; + } + + @Override + public String getName() { + return "Closed Database Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CompileBeginTest.java b/app/src/main/java/net/zetetic/tests/support/CompileBeginTest.java new file mode 100644 index 0000000..971c8c2 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CompileBeginTest.java @@ -0,0 +1,22 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class CompileBeginTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + database.compileStatement("begin").execute(); + return true; + }catch (Exception e){ + return false; + } + } + + @Override + public String getName() { + return "Compile Begin Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CompileStatementSyntaxErrorMessageTest.java b/app/src/main/java/net/zetetic/tests/support/CompileStatementSyntaxErrorMessageTest.java new file mode 100644 index 0000000..031cfb9 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CompileStatementSyntaxErrorMessageTest.java @@ -0,0 +1,38 @@ +package net.zetetic.tests.support; + +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import net.sqlcipher.database.SQLiteStatement; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; + +public class CompileStatementSyntaxErrorMessageTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + SQLiteStatement ignored = database.compileStatement("INSERT INTO mytable (mydata) VALUES"); + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: DID throw SQLiteException", e); + String message = e.getMessage(); + setMessage(message); + // TBD missing error code etc. + if (!message.matches("incomplete input: , while compiling: INSERT INTO mytable \\(mydata\\) VALUES")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT exception message: " + message); + return false; + } + return true; + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: DID throw other exception", e); + return false; + } + + return false; + } + + @Override + public String getName() { + return "Compile statement syntax error message Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CompiledSQLUpdateTest.java b/app/src/main/java/net/zetetic/tests/support/CompiledSQLUpdateTest.java new file mode 100644 index 0000000..ff4602b --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CompiledSQLUpdateTest.java @@ -0,0 +1,23 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteStatement; +import net.zetetic.tests.SQLCipherTest; + +public class CompiledSQLUpdateTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + + database.rawExecSQL("create table ut1(a text, b integer)"); + database.execSQL("insert into ut1(a, b) values (?,?)", new Object[]{"s1", new Integer(100)}); + + SQLiteStatement st = database.compileStatement("update ut1 set b = 101 where b = 100"); + long recs = st.executeUpdateDelete(); + return (recs == 1); + } + + @Override + public String getName() { + return "Compiled SQL update test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/ComputeKDFTest.java b/app/src/main/java/net/zetetic/tests/support/ComputeKDFTest.java new file mode 100644 index 0000000..47e7132 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ComputeKDFTest.java @@ -0,0 +1,21 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.tests.SQLCipherTest; + +public class ComputeKDFTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.rawExecSQL("PRAGMA cipher_kdf_compute;"); + String kdf = QueryHelper.singleValueFromQuery(database, "PRAGMA kdf_iter;"); + setMessage(String.format("Computed KDF:%s", kdf)); + return true; + } + + @Override + public String getName() { + return "Compute KDF Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferNullTest.java b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferNullTest.java new file mode 100644 index 0000000..4a939a7 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferNullTest.java @@ -0,0 +1,37 @@ +package net.zetetic.tests.support; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class CopyStringToBufferNullTest extends SupportTest { + + private CharArrayBuffer charArrayBuffer = null; + + @Override + public boolean execute(SQLiteDatabase database) { + boolean result = false; + try { + database.execSQL("create table t1(a TEXT, b TEXT);"); + database.execSQL("insert into t1(a,b) values(500, 500);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(0, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + "500".equals(actualValue); + } + } catch (IllegalArgumentException ex){ + result = true; + } finally { + return result; + } + + } + + @Override + public String getName() { + return "Copy String To Buffer Null Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestFloatLargeBuffer.java b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestFloatLargeBuffer.java new file mode 100644 index 0000000..59e6450 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestFloatLargeBuffer.java @@ -0,0 +1,32 @@ +package net.zetetic.tests.support; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class CopyStringToBufferTestFloatLargeBuffer extends SupportTest { + + private CharArrayBuffer charArrayBuffer = new CharArrayBuffer(128); + + @Override + public boolean execute(SQLiteDatabase database) { + + database.execSQL("create table t1(a REAL, b REAL);"); + database.execSQL("insert into t1(a,b) values(123.45, 67.89);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(1, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + return "67.89".equals(actualValue); + } + return false; + + } + + @Override + public String getName() { + return "Copy String To Buffer Test Float Large Buffer"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestFloatSmallBuffer.java b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestFloatSmallBuffer.java new file mode 100644 index 0000000..301b502 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestFloatSmallBuffer.java @@ -0,0 +1,32 @@ +package net.zetetic.tests.support; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class CopyStringToBufferTestFloatSmallBuffer extends SupportTest { + + private CharArrayBuffer charArrayBuffer = new CharArrayBuffer(1); + + @Override + public boolean execute(SQLiteDatabase database) { + + database.execSQL("create table t1(a REAL, b REAL);"); + database.execSQL("insert into t1(a,b) values(123.45, 67.89);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(1, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + return "67.89".equals(actualValue); + } + return false; + + } + + @Override + public String getName() { + return "Copy String To Buffer Test Float Small Buffer"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestIntegerLargeBuffer.java b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestIntegerLargeBuffer.java new file mode 100644 index 0000000..1c89a1c --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestIntegerLargeBuffer.java @@ -0,0 +1,32 @@ +package net.zetetic.tests.support; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class CopyStringToBufferTestIntegerLargeBuffer extends SupportTest { + + private CharArrayBuffer charArrayBuffer = new CharArrayBuffer(128); + + @Override + public boolean execute(SQLiteDatabase database) { + + database.execSQL("create table t1(a INTEGER, b INTEGER);"); + database.execSQL("insert into t1(a,b) values(123, 456);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(1, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + return "456".equals(actualValue); + } + return false; + + } + + @Override + public String getName() { + return "Copy String To Buffer Test Integer Large Buffer"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestIntegerSmallBuffer.java b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestIntegerSmallBuffer.java new file mode 100644 index 0000000..3882966 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestIntegerSmallBuffer.java @@ -0,0 +1,32 @@ +package net.zetetic.tests.support; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class CopyStringToBufferTestIntegerSmallBuffer extends SupportTest { + + private CharArrayBuffer charArrayBuffer = new CharArrayBuffer(1); + + @Override + public boolean execute(SQLiteDatabase database) { + + database.execSQL("create table t1(a INTEGER, b INTEGER);"); + database.execSQL("insert into t1(a,b) values(123, 456);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(1, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + return "456".equals(actualValue); + } + return false; + + } + + @Override + public String getName() { + return "Copy String To Buffer Test Integer Small Buffer"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestStringLargeBuffer.java b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestStringLargeBuffer.java new file mode 100644 index 0000000..af54c29 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestStringLargeBuffer.java @@ -0,0 +1,30 @@ +package net.zetetic.tests.support; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class CopyStringToBufferTestStringLargeBuffer extends SupportTest { + + private CharArrayBuffer charArrayBuffer = new CharArrayBuffer(128); + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a TEXT, b TEXT);"); + database.execSQL("insert into t1(a,b) values(500, 500);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(0, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + return "500".equals(actualValue); + } + return false; + } + + @Override + public String getName() { + return "Copy String To Buffer Test String Large Buffer"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestStringSmallBuffer.java b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestStringSmallBuffer.java new file mode 100644 index 0000000..673ca47 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CopyStringToBufferTestStringSmallBuffer.java @@ -0,0 +1,30 @@ +package net.zetetic.tests.support; + +import android.database.CharArrayBuffer; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class CopyStringToBufferTestStringSmallBuffer extends SupportTest { + + private CharArrayBuffer charArrayBuffer = new CharArrayBuffer(1); + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a TEXT, b TEXT);"); + database.execSQL("insert into t1(a,b) values(500, 500);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + cursor.copyStringToBuffer(0, charArrayBuffer); + String actualValue = new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied); + return "500".equals(actualValue); + } + return false; + } + + @Override + public String getName() { + return "Copy String To Buffer Test String Small Buffer"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CreateOpenDatabaseWithByteArrayTest.java b/app/src/main/java/net/zetetic/tests/support/CreateOpenDatabaseWithByteArrayTest.java new file mode 100644 index 0000000..20241d7 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CreateOpenDatabaseWithByteArrayTest.java @@ -0,0 +1,47 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import java.io.File; + +public class CreateOpenDatabaseWithByteArrayTest extends SupportTest { + + private String databaseName = "foo.db"; + + @Override + public boolean execute(SQLiteDatabase database) { + boolean status = false; + database.close(); + byte[] key = generateRandomByteArray(32); + File newDatabasePath = ZeteticApplication.getInstance().getDatabasePath(databaseName); + newDatabasePath.delete(); + database = SQLiteDatabase.openOrCreateDatabase(newDatabasePath.getPath(), key, null); + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{1, 2}); + database.close(); + database = SQLiteDatabase.openOrCreateDatabase(newDatabasePath.getPath(), key, null); + Cursor cursor = database.rawQuery("select * from t1;", null); + if (cursor != null) { + cursor.moveToNext(); + int a = cursor.getInt(0); + int b = cursor.getInt(1); + cursor.close(); + status = a == 1 && b == 2; + } + return status; + } + + @Override + public String getName() { + return "Create/Open with Byte Array Test"; + } + + @Override + protected void tearDown(SQLiteDatabase database) { + super.tearDown(database); + File newDatabasePath = ZeteticApplication.getInstance().getDatabasePath(databaseName); + newDatabasePath.delete(); + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/CursorAccessTest.java b/app/src/main/java/net/zetetic/tests/support/CursorAccessTest.java new file mode 100644 index 0000000..2db98d3 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/CursorAccessTest.java @@ -0,0 +1,72 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import java.io.File; +import java.util.Random; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class CursorAccessTest implements ISupportTest { + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + ZeteticApplication.getInstance().deleteDatabaseFileAndSiblings(ZeteticApplication.DATABASE_NAME); + String databasesFolderPath = ZeteticApplication.getInstance() + .getDatabasePath(ZeteticApplication.DATABASE_NAME).getParent(); + File databasesFolder = new File(databasesFolderPath); + databasesFolder.delete(); + + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + db.execSQL("create table t1(a text, b integer, c text, d real, e blob)"); + byte[] data = new byte[10]; + new Random().nextBytes(data); + db.execSQL("insert into t1(a, b, c, d, e) values(?, ?, ?, ?, ?)", new Object[]{"test1", 100, null, 3.25, data}); + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + Cursor results = database.query("select * from t1", new String[]{}); + results.moveToFirst(); + int type_a = results.getType(0); + int type_b = results.getType(1); + int type_c = results.getType(2); + int type_d = results.getType(3); + int type_e = results.getType(4); + + results.close(); + helper.close(); + + result.setResult(type_a == Cursor.FIELD_TYPE_STRING && + type_b == Cursor.FIELD_TYPE_INTEGER && + type_c == Cursor.FIELD_TYPE_NULL && + type_d == Cursor.FIELD_TYPE_FLOAT && + type_e == Cursor.FIELD_TYPE_BLOB); + + return result; + } + + @Override + public String getName() { + return "Cursor Access Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/DecryptedRoomTest.java b/app/src/main/java/net/zetetic/tests/support/DecryptedRoomTest.java new file mode 100644 index 0000000..832c372 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/DecryptedRoomTest.java @@ -0,0 +1,133 @@ +package net.zetetic.tests.support; + +import android.annotation.SuppressLint; +import android.app.Activity; + +import androidx.annotation.NonNull; +import androidx.room.Dao; +import androidx.room.Database; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; +import androidx.room.Insert; +import androidx.room.PrimaryKey; +import androidx.room.Query; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class DecryptedRoomTest implements ISupportTest { + private static final String DB_NAME = "room.db"; + + private final Activity activity; + + @Entity() + public static class ParentEntity { + @PrimaryKey(autoGenerate = true) long id; + int intValue; + float floatValue; + double doubleValue; + char charValue; + boolean boolValue; + } + + @Entity(foreignKeys = @ForeignKey(entity = ParentEntity.class, + parentColumns = "id", childColumns = "parentId", onDelete = ForeignKey.CASCADE), + indices = @Index("parentId")) + public static class ChildEntity { + @PrimaryKey @NonNull String uuid; + String stringValue; + long parentId; + } + + @Dao + public static abstract class TestDao { + @Query("SELECT * FROM ChildEntity WHERE parentId = :parentId or :parentId = -1") + abstract List getAllChildrenForParent(long parentId); + + @Query("SELECT * FROM ChildEntity") + abstract List getAllChildren(); + + @Insert + abstract void insert(List entities); + + @Insert + abstract long insert(ParentEntity entity); + } + + @Database(entities = {ParentEntity.class, ChildEntity.class}, version = 1) + public static abstract class TestDatabase extends RoomDatabase { + abstract TestDao testDao(); + } + + public DecryptedRoomTest(Activity activity) { + this.activity = activity; + } + + @SuppressLint("DefaultLocale") + public TestResult run() { + File dbFile = ZeteticApplication.getInstance().getDatabasePath(DB_NAME); + + if (dbFile.exists()){ + dbFile.delete(); + } + + final TestResult result = new TestResult(getName(), false); + final TestDatabase room = Room.databaseBuilder(activity, TestDatabase.class, DB_NAME) + .build(); + ParentEntity parent = new ParentEntity(); + + parent.boolValue = true; + parent.charValue = 'x'; + parent.intValue = 1337; + parent.doubleValue = 3.14159; + parent.floatValue = 2.71828f; + parent.id = room.testDao().insert(parent); + + result.setResult(true); + + ChildEntity firstChild = new ChildEntity(); + + firstChild.uuid = UUID.randomUUID().toString(); + firstChild.stringValue = "Um, hi!"; + firstChild.parentId = parent.id; + + ChildEntity secondChild = new ChildEntity(); + + secondChild.uuid = UUID.randomUUID().toString(); + secondChild.stringValue = "And now for something completely different"; + secondChild.parentId = parent.id; + + List children = new ArrayList<>(); + + children.add(firstChild); + children.add(secondChild); + + room.testDao().insert(children); + + List allChildren = room.testDao().getAllChildren(); + List allChildrenNegativeOne = room.testDao().getAllChildrenForParent(-1); + List other = room.testDao().getAllChildrenForParent( parent.id); + + //the query should have returned both the list with same sizes here + if (allChildren.size() != allChildrenNegativeOne.size()) { + result.setResult(false); + result.setMessage(String.format("expected all children, found %d from entity and %d from main query", allChildrenNegativeOne.size(), allChildren.size())); + return result; + } + + return result; + } + + @Override + public String getName() { + return "Decrypted Room Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/DisableWriteAheadLoggingTest.java b/app/src/main/java/net/zetetic/tests/support/DisableWriteAheadLoggingTest.java new file mode 100644 index 0000000..9bd1134 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/DisableWriteAheadLoggingTest.java @@ -0,0 +1,26 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.tests.SQLCipherTest; + +public class DisableWriteAheadLoggingTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + boolean result = database.enableWriteAheadLogging(); + String currentMode = getJournalModeState(database); + if(!result || !currentMode.equals("wal")) return false; + database.disableWriteAheadLogging(); + currentMode = getJournalModeState(database); + return currentMode.equals("delete"); + } + + private String getJournalModeState(SQLiteDatabase database){ + return QueryHelper.singleValueFromQuery(database, "PRAGMA journal_mode;"); + } + + @Override + public String getName() { + return "Disable WAL mode"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/EnableForeignKeyConstraintsTest.java b/app/src/main/java/net/zetetic/tests/support/EnableForeignKeyConstraintsTest.java new file mode 100644 index 0000000..ddd2d95 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/EnableForeignKeyConstraintsTest.java @@ -0,0 +1,26 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.tests.SQLCipherTest; + +public class EnableForeignKeyConstraintsTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + String initialState = getForeignKeyState(database); + if(!initialState.equals("0")) return false; + database.setForeignKeyConstraintsEnabled(true); + String currentState = getForeignKeyState(database); + if(!currentState.equals("1")) return false; + return true; + } + + private String getForeignKeyState(SQLiteDatabase database){ + return QueryHelper.singleValueFromQuery(database, "PRAGMA foreign_keys"); + } + + @Override + public String getName() { + return "Enable Foreign Key Constraints"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/EnableForeignKeySupportTest.java b/app/src/main/java/net/zetetic/tests/support/EnableForeignKeySupportTest.java new file mode 100644 index 0000000..7276393 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/EnableForeignKeySupportTest.java @@ -0,0 +1,20 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.tests.SQLCipherTest; + +public class EnableForeignKeySupportTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + String defaultValue = QueryHelper.singleValueFromQuery(database, "PRAGMA foreign_keys"); + database.rawExecSQL("PRAGMA foreign_keys = ON;"); + String updatedValue = QueryHelper.singleValueFromQuery(database, "PRAGMA foreign_keys"); + return defaultValue.equals("0") && updatedValue.equals("1"); + } + + @Override + public String getName() { + return "Enable Foreign Key Support Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/EnableWriteAheadLoggingTest.java b/app/src/main/java/net/zetetic/tests/support/EnableWriteAheadLoggingTest.java new file mode 100644 index 0000000..b2f1c2f --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/EnableWriteAheadLoggingTest.java @@ -0,0 +1,19 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.tests.SQLCipherTest; + +public class EnableWriteAheadLoggingTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + boolean result = database.enableWriteAheadLogging(); + String currentMode = QueryHelper.singleValueFromQuery(database, "PRAGMA journal_mode;"); + return result && currentMode.equals("wal"); + } + + @Override + public String getName() { + return "Enable WAL mode"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/EncryptBytesTest.java b/app/src/main/java/net/zetetic/tests/support/EncryptBytesTest.java new file mode 100644 index 0000000..f06fbbf --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/EncryptBytesTest.java @@ -0,0 +1,136 @@ +package net.zetetic.tests.support; + +import android.app.Activity; +import android.database.Cursor; + +import androidx.room.Dao; +import androidx.room.Database; +import androidx.room.Entity; +import androidx.room.PrimaryKey; +import androidx.room.RawQuery; +import androidx.room.Room; +import androidx.room.RoomDatabase; +import androidx.sqlite.db.SimpleSQLiteQuery; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; + +import java.io.File; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Random; + +public class EncryptBytesTest implements ISupportTest { + + Activity activity; + + public EncryptBytesTest(Activity activity) { + this.activity = activity; + } + + @Dao + public interface BlobDao { + @RawQuery + byte[] blobRawGet(SimpleSQLiteQuery query); + } + + public static class Encryptor { + BlobDao dao; + byte[] encryptionKey; + + public Encryptor(BlobDao dao, byte[] encryptionKey) { + this.dao = dao; + this.encryptionKey = encryptionKey; + } + + public byte[] encryptBytes(byte[] material) { + return dao.blobRawGet(new SimpleSQLiteQuery("select sqlcipher_vle_encrypt(?, ?)", + new Object[]{material, encryptionKey})); + } + + public byte[] decryptBytes(byte[] material) { + return dao.blobRawGet(new SimpleSQLiteQuery("select sqlcipher_vle_decrypt(?, ?)", + new Object[]{material, encryptionKey})); + } + } + + public static class Encryptor2 { + RoomDatabase roomDatabase; + byte[] encryptionKey; + + public Encryptor2(RoomDatabase roomDatabase, byte[] encryptionKey) { + this.roomDatabase = roomDatabase; + this.encryptionKey = encryptionKey; + } + + public byte[] encryptBytes(byte[] material) { + byte[] result = null; + Cursor cursor = roomDatabase.getOpenHelper().getReadableDatabase().query("select sqlcipher_vle_encrypt(?, ?)", + new Object[]{material, encryptionKey}); + if(cursor != null && cursor.moveToNext()){ + result = cursor.getBlob(0); + cursor.close(); + } + return result; + } + + public byte[] decryptBytes(byte[] material) { + byte[] result = null; + Cursor cursor = roomDatabase.getOpenHelper().getReadableDatabase().query("select sqlcipher_vle_decrypt(?, ?)", + new Object[]{material, encryptionKey}); + if(cursor != null && cursor.moveToNext()){ + result = cursor.getBlob(0); + cursor.close(); + } + return result; + } + } + + @Entity + public static class BlobEntity { + @PrimaryKey(autoGenerate = true) + long id; + } + + @Database(entities = {BlobEntity.class}, version = 1) + public static abstract class BlobDatabase extends RoomDatabase { + abstract BlobDao blobDao(); + } + + + public TestResult run() { + TestResult result = new TestResult(getName(), false); + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase, ZeteticApplication.getInstance().wrapHook(null)); + File databaseFile = ZeteticApplication.getInstance().getDatabasePath("test.db"); + if(databaseFile.exists()) { + databaseFile.delete(); + } + BlobDatabase room = Room.databaseBuilder(activity, BlobDatabase.class, databaseFile.getAbsolutePath()) + .openHelperFactory(factory) + .build(); + + byte[] key = generateRandomBytes(64); + //Encryptor encryptor = new Encryptor(room.blobDao(), key); + Encryptor2 encryptor = new Encryptor2(room, key); + + byte[] source = "hi".getBytes(); + byte[] encrypted = encryptor.encryptBytes(source); + byte[] decrypted = encryptor.decryptBytes(encrypted); + result.setResult(Arrays.equals(source, decrypted)); + return result; + } + + private byte[] generateRandomBytes(int length) { + Random random = new SecureRandom(); + byte[] value = new byte[length]; + random.nextBytes(value); + return value; + } + + public String getName() { + return "Encrypt Bytes Test"; + } +} \ No newline at end of file diff --git a/app/src/main/java/net/zetetic/tests/support/EncryptedRoomTest.java b/app/src/main/java/net/zetetic/tests/support/EncryptedRoomTest.java new file mode 100644 index 0000000..5894653 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/EncryptedRoomTest.java @@ -0,0 +1,151 @@ +package net.zetetic.tests.support; + +import android.annotation.SuppressLint; +import android.app.Activity; + +import androidx.annotation.NonNull; +import androidx.room.Dao; +import androidx.room.Database; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; +import androidx.room.Insert; +import androidx.room.PrimaryKey; +import androidx.room.Query; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class EncryptedRoomTest implements ISupportTest { + private static final String DB_NAME = "room.db"; + + private final Activity activity; + + @Entity() + public static class ParentEntity { + @PrimaryKey(autoGenerate = true) long id; + int intValue; + float floatValue; + double doubleValue; + char charValue; + boolean boolValue; + } + + @Entity(foreignKeys = @ForeignKey(entity = ParentEntity.class, + parentColumns = "id", childColumns = "parentId", onDelete = ForeignKey.CASCADE), + indices = @Index("parentId")) + public static class ChildEntity { + @PrimaryKey @NonNull String uuid; + String stringValue; + long parentId; + } + + @Dao + public static abstract class TestDao { + @Query("SELECT * FROM ChildEntity WHERE parentId = :parentId or :parentId = -1") + abstract List getAllChildrenForParent(long parentId); + + @Query("SELECT * FROM ChildEntity") + abstract List getAllChildren(); + + @Insert + abstract void insert(List entities); + + @Insert + abstract long insert(ParentEntity entity); + } + + @Database(entities = {ParentEntity.class, ChildEntity.class}, version = 1) + public static abstract class TestDatabase extends RoomDatabase { + abstract TestDao testDao(); + } + + public EncryptedRoomTest(Activity activity) { + this.activity = activity; + } + + @SuppressLint("DefaultLocale") + public TestResult run() { + File dbFile = ZeteticApplication.getInstance().getDatabasePath(DB_NAME); + + if (dbFile.exists()){ + dbFile.delete(); + } + + final TestResult result = new TestResult(getName(), false); + final byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + final SupportFactory factory = new SupportFactory(passphrase, ZeteticApplication.getInstance().wrapHook(null)); + final TestDatabase room = Room.databaseBuilder(activity, TestDatabase.class, DB_NAME) + .openHelperFactory(factory) + .build(); + ParentEntity parent = new ParentEntity(); + + parent.boolValue = true; + parent.charValue = 'x'; + parent.intValue = 1337; + parent.doubleValue = 3.14159; + parent.floatValue = 2.71828f; + parent.id = room.testDao().insert(parent); + + result.setResult(true); + + ChildEntity firstChild = new ChildEntity(); + + firstChild.uuid = UUID.randomUUID().toString(); + firstChild.stringValue = "Um, hi!"; + firstChild.parentId = parent.id; + + ChildEntity secondChild = new ChildEntity(); + + secondChild.uuid = UUID.randomUUID().toString(); + secondChild.stringValue = "And now for something completely different"; + secondChild.parentId = parent.id; + + List children = new ArrayList<>(); + + children.add(firstChild); + children.add(secondChild); + + room.testDao().insert(children); + + List allChildren = room.testDao().getAllChildren(); + List allChildrenNegativeOne = room.testDao().getAllChildrenForParent( -1); + List other = room.testDao().getAllChildrenForParent( parent.id); + +// SQLiteDatabase db = (SQLiteDatabase)room.getOpenHelper().getReadableDatabase(); +// String[] projection = new String[] { "*" }; +// String[] args = new String[] { "-1" }; +// Cursor c = db.query("ChildEntity", projection, "parentId = ?1 or ?1 = -1", args, null, null, null); +// +// //the query should have returned both the list with same sizes here +// if (allChildren.size() != c.getCount()) { +// result.setResult(false); +// result.setMessage(String.format("expected all children, found %d from entity and %d from main query", c.getCount(), allChildren.size())); +// return result; +// } + + //the query should have returned both the list with same sizes here + if (allChildren.size() != allChildrenNegativeOne.size()) { + result.setResult(false); + result.setMessage(String.format("expected all children, found %d from entity and %d from main query", allChildrenNegativeOne.size(), allChildren.size())); + return result; + } + + return result; + } + + @Override + public String getName() { + return "Encrypted Room Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/ExecuteInsertConstraintErrorMessageTest.java b/app/src/main/java/net/zetetic/tests/support/ExecuteInsertConstraintErrorMessageTest.java new file mode 100644 index 0000000..c4e5ce7 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ExecuteInsertConstraintErrorMessageTest.java @@ -0,0 +1,40 @@ +package net.zetetic.tests.support; + +import android.util.Log; +import android.database.sqlite.SQLiteConstraintException; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteStatement; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; + +public class ExecuteInsertConstraintErrorMessageTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("CREATE TABLE tt(a UNIQUE, b)"); + database.execSQL("INSERT INTO tt VALUES (101, 'Alice')"); + try { + SQLiteStatement insertStatement = database.compileStatement("INSERT INTO tt VALUES (101, 'Betty')"); + long ignored = insertStatement.executeInsert(); + } catch (SQLiteConstraintException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: DID throw SQLiteConstraintException", e); + String message = e.getMessage(); + setMessage(message); + if (!message.matches("error code 19 \\(extended error code 2067\\): UNIQUE constraint failed: tt\\.a")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT exception message: " + message); + return false; + } + return true; + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: DID throw other exception", e); + return false; + } + + return false; + } + + @Override + public String getName() { + return "Execute insert constraint error message Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/ExportToUnencryptedDatabase.java b/app/src/main/java/net/zetetic/tests/support/ExportToUnencryptedDatabase.java new file mode 100644 index 0000000..39d5cd0 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ExportToUnencryptedDatabase.java @@ -0,0 +1,98 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import net.zetetic.tests.TestResult; +import java.io.File; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class ExportToUnencryptedDatabase implements ISupportTest { + + File unencryptedFile; + + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + ZeteticApplication.getInstance().deleteDatabase(ZeteticApplication.DATABASE_NAME); + + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory + factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", "two for the show"}); + unencryptedFile = ZeteticApplication.getInstance().getDatabasePath("plaintext.db"); + ZeteticApplication.getInstance().deleteDatabase("plaintext.db"); + database.execSQL(String.format("ATTACH DATABASE '%s' as plaintext KEY '';", + unencryptedFile.getAbsolutePath())); + ((SQLiteDatabase)database).rawExecSQL("SELECT sqlcipher_export('plaintext');"); + database.execSQL("DETACH DATABASE plaintext;"); + helper.close(); + + passphrase = new byte[0]; + factory = new SupportFactory(passphrase); + cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(unencryptedFile.getAbsolutePath()) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + helper = factory.create(cfg); + SupportSQLiteDatabase unencryptedDatabase = helper.getWritableDatabase(); + + Cursor cursor = unencryptedDatabase.query("select * from t1;", new String[]{}); + String a = ""; + String b = ""; + while(cursor.moveToNext()){ + a = cursor.getString(0); + b = cursor.getString(1); + } + cursor.close(); + helper.close(); + + result.setResult(a.equals("one for the money") && + b.equals("two for the show")); + + unencryptedFile.delete(); + + return result; + } + + @Override + public String getName() { + return "Export to Unencrypted Database"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/FIPSTest.java b/app/src/main/java/net/zetetic/tests/support/FIPSTest.java new file mode 100644 index 0000000..1383008 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/FIPSTest.java @@ -0,0 +1,23 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.tests.SQLCipherTest; + +public class FIPSTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + + String version = QueryHelper.singleValueFromQuery(database, "PRAGMA cipher_version;"); + setMessage(String.format("SQLCipher version:%s", version)); + int expectedValue = version.contains("FIPS") ? 1 : 0; + + int status = QueryHelper.singleIntegerValueFromQuery(database, "PRAGMA cipher_fips_status;"); + return status == expectedValue; + } + + @Override + public String getName() { + return "FIPS Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/FTS5Test.java b/app/src/main/java/net/zetetic/tests/support/FTS5Test.java new file mode 100644 index 0000000..a850fe3 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/FTS5Test.java @@ -0,0 +1,25 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class FTS5Test extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("CREATE VIRTUAL TABLE email USING fts5(sender, title, body);"); + database.execSQL("insert into email(sender, title, body) values(?, ?, ?);", + new Object[]{"foo@bar.com", "Test Email", "This is a test email message."}); + Cursor cursor = database.rawQuery("select * from email where email match ?;", new String[]{"test"}); + if(cursor != null){ + cursor.moveToFirst(); + return cursor.getString(cursor.getColumnIndex("sender")).equals("foo@bar.com"); + } + return false; + } + + @Override + public String getName() { + return "FTS5 Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/FixedCursorWindowAllocationTest.java b/app/src/main/java/net/zetetic/tests/support/FixedCursorWindowAllocationTest.java new file mode 100644 index 0000000..e185a95 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/FixedCursorWindowAllocationTest.java @@ -0,0 +1,53 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.CursorWindow; +import net.sqlcipher.CursorWindowAllocation; +import net.sqlcipher.CustomCursorWindowAllocation; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.RowColumnValueBuilder; +import net.zetetic.tests.SQLCipherTest; + +public class FixedCursorWindowAllocationTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + int rowCount = 0; + int rows = 100; + long allocationSize = 1024; + final int dataSize = 491; + CursorWindowAllocation fixedAllocation = new CustomCursorWindowAllocation(allocationSize, 0, allocationSize); + CursorWindow.setCursorWindowAllocation(fixedAllocation); + buildDatabase(database, rows, 1, new RowColumnValueBuilder() { + @Override + public Object buildRowColumnValue(String[] columns, int row, int column) { + return generateRandomByteArray(dataSize); + } + }); + + Cursor cursor = database.rawQuery("SELECT * FROM t1;", new Object[]{}); + if(cursor == null) return false; + while(cursor.moveToNext()){ + byte[] data = cursor.getBlob(0); + if(data.length != dataSize) { + cursor.close(); + return false; + } + rowCount++; + } + cursor.close(); + return rowCount == rows; + } catch (Exception e){ + String message = String.format("Error:%s", e.getMessage()); + log(message); + setMessage(message); + return false; + } + } + + @Override + public String getName() { + return "Small Cursor Window Allocation Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/ForeignKeyConstraintsEnabledWithTransactionTest.java b/app/src/main/java/net/zetetic/tests/support/ForeignKeyConstraintsEnabledWithTransactionTest.java new file mode 100644 index 0000000..fa09087 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ForeignKeyConstraintsEnabledWithTransactionTest.java @@ -0,0 +1,24 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class ForeignKeyConstraintsEnabledWithTransactionTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.beginTransaction(); + try { + database.setForeignKeyConstraintsEnabled(true); + }catch (IllegalStateException ex){ + if(ex.getMessage().equals("Foreign key constraints may not be changed while in a transaction")) { + return true; + } + } + return false; + } + + @Override + public String getName() { + return "Disallow foreign key constraints in transaction"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/FullTextSearchTest.java b/app/src/main/java/net/zetetic/tests/support/FullTextSearchTest.java new file mode 100644 index 0000000..9fb9efa --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/FullTextSearchTest.java @@ -0,0 +1,39 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class FullTextSearchTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + + database.execSQL("CREATE VIRTUAL TABLE sites USING fts4(domain, url, title, meta_keys, body)"); + database.execSQL("CREATE TABLE keywords (keyword TEXT)"); + + database.execSQL("insert into sites(domain, url, title, meta_keys, body) values(?, ?, ?, ?, ?)", + new Object[]{"sqlcipher.net", "http://sqlcipher.net", + "Home - SQLCipher - Open Source Full Database Encryption for SQLite", + "sqlcipher, sqlite", ""}); + database.execSQL("insert into keywords(keyword) values(?)", new Object[]{"SQLCipher"}); + database.execSQL("insert into keywords(keyword) values(?)", new Object[]{"SQLite"}); + + String query = "SELECT keyword FROM keywords INNER JOIN sites ON sites.title MATCH keywords.keyword"; + Cursor result = database.rawQuery(query, new String[]{}); + int resultCount = 0; + while (result.moveToNext()){ + String row = result.getString(0); + if(row != null){ + resultCount++; + } + } + result.close(); + return resultCount > 0; + } + + @Override + public String getName() { + return "Full Text Search Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/GetAttachedDatabasesTest.java b/app/src/main/java/net/zetetic/tests/support/GetAttachedDatabasesTest.java new file mode 100644 index 0000000..4cc8b6f --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/GetAttachedDatabasesTest.java @@ -0,0 +1,28 @@ +package net.zetetic.tests.support; + +import android.util.Pair; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import java.io.File; +import java.util.List; +import java.util.UUID; + +public class GetAttachedDatabasesTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + UUID id = UUID.randomUUID(); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(id.toString()); + List> attached = database.getAttachedDbs(); + boolean initialAttach = attached.size() == 1 && attached.get(0).first.equals("main"); + database.execSQL("ATTACH database ? as foo;", + new Object[]{databasePath.getAbsolutePath()}); + attached = database.getAttachedDbs(); + return initialAttach && attached.size() == 2; + } + + @Override + public String getName() { + return "Get Attached Databases Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/GetTypeFromCrossProcessCursorWrapperTest.java b/app/src/main/java/net/zetetic/tests/support/GetTypeFromCrossProcessCursorWrapperTest.java new file mode 100644 index 0000000..c9acce6 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/GetTypeFromCrossProcessCursorWrapperTest.java @@ -0,0 +1,26 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class GetTypeFromCrossProcessCursorWrapperTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", "two for the show"}); + Cursor cursor = database.query("t1", new String[]{"a", "b"}, null, null, null, null, null); + cursor.moveToFirst(); + int type_a = cursor.getType(0); + int type_b = cursor.getType(1); + cursor.close(); + database.close(); + return (type_a == Cursor.FIELD_TYPE_STRING) && (type_b == Cursor.FIELD_TYPE_STRING); + } + + @Override + public String getName() { + return "Get Type from CrossProcessCursorWrapper"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/GrowingCursorWindowAllocationTest.java b/app/src/main/java/net/zetetic/tests/support/GrowingCursorWindowAllocationTest.java new file mode 100644 index 0000000..1b1afd6 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/GrowingCursorWindowAllocationTest.java @@ -0,0 +1,55 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.CursorWindow; +import net.sqlcipher.CursorWindowAllocation; +import net.sqlcipher.CustomCursorWindowAllocation; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.RowColumnValueBuilder; +import net.zetetic.tests.SQLCipherTest; + +public class GrowingCursorWindowAllocationTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + try { + int rowCount = 0; + int rows = 20000; + long intialAllocationSize = 128 * 1024; + long growthAllocationSize = 1024 * 1024; + long maxAllocationSize = 4 * 1024 * 1024; + final int dataSize = 2000; + CursorWindowAllocation fixedAllocation = + new CustomCursorWindowAllocation(intialAllocationSize, growthAllocationSize, maxAllocationSize); + CursorWindow.setCursorWindowAllocation(fixedAllocation); + buildDatabase(database, rows, 1, new RowColumnValueBuilder() { + @Override + public Object buildRowColumnValue(String[] columns, int row, int column) { + return generateRandomByteArray(dataSize); + } + }); + + Cursor cursor = database.rawQuery("SELECT * FROM t1;", new Object[]{}); + if(cursor == null) return false; + while(cursor.moveToNext()){ + byte[] data = cursor.getBlob(0); + if(data.length != dataSize) { + cursor.close(); + return false; + } + rowCount++; + } + cursor.close(); + return rowCount == rows; + } catch (Exception e){ + String message = String.format("Error:%s", e.getMessage()); + log(message); + setMessage(message); + return false; + } + } + + @Override + public String getName() { + return "Growing Cursor Window Allocation Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/ISupportTest.java b/app/src/main/java/net/zetetic/tests/support/ISupportTest.java new file mode 100644 index 0000000..abd6706 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ISupportTest.java @@ -0,0 +1,8 @@ +package net.zetetic.tests.support; + +import net.zetetic.tests.TestResult; + +interface ISupportTest { + String getName(); + TestResult run(); +} diff --git a/app/src/main/java/net/zetetic/tests/support/ImportUnencryptedDatabaseTest.java b/app/src/main/java/net/zetetic/tests/support/ImportUnencryptedDatabaseTest.java new file mode 100644 index 0000000..d4d94bf --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ImportUnencryptedDatabaseTest.java @@ -0,0 +1,99 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import net.zetetic.tests.TestResult; +import java.io.File; +import java.io.IOException; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class ImportUnencryptedDatabaseTest implements ISupportTest { + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + File unencryptedDatabase = ZeteticApplication.getInstance().getDatabasePath("unencrypted.db"); + File encryptedDatabase = ZeteticApplication.getInstance().getDatabasePath("encrypted.db"); + + try { + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("unencrypted.db"); + byte[] passphrase = new byte[0]; + SupportFactory factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(unencryptedDatabase.getAbsolutePath()) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + database.execSQL(String.format("ATTACH DATABASE '%s' AS encrypted KEY '%s'", + encryptedDatabase.getAbsolutePath(), ZeteticApplication.DATABASE_PASSWORD)); + // database.execSQL("select sqlcipher_export('encrypted')"); + ((SQLiteDatabase)database).rawExecSQL("select sqlcipher_export('encrypted')"); + database.execSQL("DETACH DATABASE encrypted"); + helper.close(); + + passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + + factory = new SupportFactory(passphrase); + cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(encryptedDatabase.getAbsolutePath()) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + helper = factory.create(cfg); + database = helper.getWritableDatabase(); + + Cursor cursor = database.query("select * from t1", new String[]{}); + cursor.moveToFirst(); + String a = cursor.getString(0); + String b = cursor.getString(1); + cursor.close(); + helper.close(); + + result.setResult(a.equals("one for the money") && + b.equals("two for the show")); + return result; + } catch (IOException e) { + result.setResult(false); + + return result; + } + finally { + unencryptedDatabase.delete(); + encryptedDatabase.delete(); + } + } + + @Override + public String getName() { + return "Import Unencrypted Database Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/InsertWithOnConflictTest.java b/app/src/main/java/net/zetetic/tests/support/InsertWithOnConflictTest.java new file mode 100644 index 0000000..c3da118 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/InsertWithOnConflictTest.java @@ -0,0 +1,24 @@ +package net.zetetic.tests.support; + +import android.content.ContentValues; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class InsertWithOnConflictTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table user(_id integer primary key autoincrement, email text unique not null);"); + ContentValues values = new ContentValues(); + values.put("email", "foo@bar.com"); + long id = database.insertWithOnConflict("user", null, values, + SQLiteDatabase.CONFLICT_IGNORE); + long error = database.insertWithOnConflict("user", null, values, + SQLiteDatabase.CONFLICT_IGNORE); + return id == 1 && error == -1; + } + + @Override + public String getName() { + return "Insert with OnConflict"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/InvalidPasswordTest.java b/app/src/main/java/net/zetetic/tests/support/InvalidPasswordTest.java new file mode 100644 index 0000000..042ba9a --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/InvalidPasswordTest.java @@ -0,0 +1,104 @@ +package net.zetetic.tests.support; + +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import java.util.UUID; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class InvalidPasswordTest implements ISupportTest { + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + database.execSQL("create table t1(a,b);"); + helper.close(); + + passphrase = SQLiteDatabase.getBytes(UUID.randomUUID().toString().toCharArray()); + factory = new SupportFactory(passphrase); + + try { + helper = factory.create(cfg); + database = helper.getWritableDatabase(); + + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with invalid password did not fail"); + result.setResult(false); + return result; + } catch (SQLiteException e){ + Log.v(ZeteticApplication.TAG, + "SQLiteDatabase.openOrCreateDatabase() with invalid password did throw a SQLiteException as expected OK", e); + } catch (Exception e){ + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with invalid password did throw an unexpected exception", e); + result.setResult(false); + return result; + } + + passphrase = SQLiteDatabase.getBytes("".toCharArray()); + factory = new SupportFactory(passphrase); + + try { + helper = factory.create(cfg); + database = helper.getWritableDatabase(); + + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with no password did NOT fail on an encrypted database"); + result.setResult(false); + return result; + } catch (SQLiteException e){ + Log.v(ZeteticApplication.TAG, + "SQLiteDatabase.openOrCreateDatabase() with no password did throw a SQLiteException as expected OK", e); + } catch (Exception e){ + Log.e(ZeteticApplication.TAG, + "NOT EXPECTED: SQLiteDatabase.openOrCreateDatabase() with no password did throw an unexpected exception type", e); + result.setResult(false); + return result; + } + + passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + factory = new SupportFactory(passphrase); + + try { + helper = factory.create(cfg); + database = helper.getWritableDatabase(); + database.execSQL("insert into t1(a,b) values(?, ?)", new Object[]{"testing", "123"}); + } catch (Exception e){ + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: attempt to access database with correct password did throw an unexpected exception", e); + result.setResult(false); + return result; + } + + result.setResult(true); + return result; + } + + @Override + public String getName() { + return "Invalid Password Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/JavaClientLibraryVersionTest.java b/app/src/main/java/net/zetetic/tests/support/JavaClientLibraryVersionTest.java new file mode 100644 index 0000000..72ce5e8 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/JavaClientLibraryVersionTest.java @@ -0,0 +1,20 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class JavaClientLibraryVersionTest extends SupportTest { + + private final String EXPECTED_SQLCIPHER_ANDROID_VERSION = "4.5.4"; + + @Override + public boolean execute(SQLiteDatabase database) { + setMessage(String.format("Report:%s", SQLiteDatabase.SQLCIPHER_ANDROID_VERSION)); + return SQLiteDatabase.SQLCIPHER_ANDROID_VERSION.equals(EXPECTED_SQLCIPHER_ANDROID_VERSION); + } + + @Override + public String getName() { + return "Java Client Library Version Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/LargeDatabaseCursorAccessTest.java b/app/src/main/java/net/zetetic/tests/support/LargeDatabaseCursorAccessTest.java new file mode 100644 index 0000000..c893757 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/LargeDatabaseCursorAccessTest.java @@ -0,0 +1,102 @@ +package net.zetetic.tests.support; + +import android.util.Log; +import net.sqlcipher.Cursor; +import net.sqlcipher.CursorWindow; +import net.sqlcipher.CustomCursorWindowAllocation; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.RowColumnValueBuilder; +import net.zetetic.tests.SQLCipherTest; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class LargeDatabaseCursorAccessTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + int rowCount = 1000; + long windowAllocationSize = 1024 * 1024 / 20; + buildDatabase(database, rowCount, 30, new RowColumnValueBuilder() { + @Override + public Object buildRowColumnValue(String[] columns, int row, int column) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + String columnName = columns[column]; + String value = String.format("%s%d", columnName, row); + return digest.digest(value.getBytes("UTF-8")); + } catch (Exception e) { + Log.e(TAG, e.toString()); + return null; + } + } + }); + + Integer[] randomRows = generateRandomNumbers(rowCount, rowCount); + CursorWindow.setCursorWindowAllocation(new CustomCursorWindowAllocation(windowAllocationSize, 0, windowAllocationSize)); + Cursor cursor = database.rawQuery("SELECT * FROM t1;", null); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + int row = 0; + Log.i(TAG, "Walking cursor forward"); + while(cursor.moveToNext()){ + if (!CompareDigestForAllColumns(cursor, digest, row)) return false; + row++; + } + Log.i(TAG, "Walking cursor backward"); + while(cursor.moveToPrevious()){ + row--; + if (!CompareDigestForAllColumns(cursor, digest, row)) return false; + } + Log.i(TAG, "Walking cursor randomly"); + for(int randomRow : randomRows){ + cursor.moveToPosition(randomRow); + if (!CompareDigestForAllColumns(cursor, digest, randomRow)) return false; + } + + } catch (Exception e){ + return false; + } + return true; + } + + @Override + public String getName() { + return "Large Database Cursor Access Test"; + } + + private boolean CompareDigestForAllColumns(Cursor cursor, MessageDigest digest, int row) throws UnsupportedEncodingException { + int columnCount = cursor.getColumnCount(); + for(int column = 0; column < columnCount; column++){ + Log.i(TAG, String.format("Comparing SHA-1 digest for row:%d", row)); + String columnName = cursor.getColumnName(column); + byte[] actual = cursor.getBlob(column); + String value = String.format("%s%d", columnName, row); + byte[] expected = digest.digest(value.getBytes("UTF-8")); + if(!Arrays.equals(actual, expected)){ + Log.e(TAG, String.format("SHA-1 digest mismatch for row:%d column:%d", row, column)); + return false; + } + } + return true; + } + + private Integer[] generateRandomNumbers(int max, int times){ + SecureRandom random = new SecureRandom(); + List numbers = new ArrayList<>(); + for(int index = 0; index < times; index++){ + boolean alreadyExists; + do { + int value = random.nextInt(max); + alreadyExists = numbers.contains(value); + if(!alreadyExists){ + numbers.add(value); + } + } while(alreadyExists); + } + return numbers.toArray(new Integer[0]); + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/LoopingCountQueryTest.java b/app/src/main/java/net/zetetic/tests/support/LoopingCountQueryTest.java new file mode 100644 index 0000000..8cc2763 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/LoopingCountQueryTest.java @@ -0,0 +1,32 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class LoopingCountQueryTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + int counter = 0; + int iterations = 10; + database.execSQL("create table t1(a);"); + database.execSQL("insert into t1(a) values (?)", new Object[]{"foo"}); + StringBuilder buffer = new StringBuilder(); + while(counter < iterations){ + Cursor cursor = database.rawQuery("select count(*) from t1", null); + if(cursor != null){ + cursor.moveToFirst(); + buffer.append(cursor.getInt(0)); + cursor.close(); + } + counter++; + } + return buffer.toString().length() > 0; + } + + @Override + public String getName() { + return "Looping Count Query Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/LoopingInsertTest.java b/app/src/main/java/net/zetetic/tests/support/LoopingInsertTest.java new file mode 100644 index 0000000..cd6399e --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/LoopingInsertTest.java @@ -0,0 +1,27 @@ +package net.zetetic.tests.support; + +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class LoopingInsertTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("CREATE TABLE some_table(name TEXT, surname TEXT);"); + long startTime = System.currentTimeMillis(); + database.execSQL("begin;"); + for(int index = 0; index < 10000; index++){ + database.execSQL("insert into some_table(name, surname) values(?, ?)", + new Object[]{"one for the money", "two for the show"}); + } + database.execSQL("commit;"); + long diff = System.currentTimeMillis() - startTime; + Log.e(TAG, String.format("Inserted in: %d ms", diff)); + return true; + } + + @Override + public String getName() { + return "Looping Insert Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/LoopingQueryTest.java b/app/src/main/java/net/zetetic/tests/support/LoopingQueryTest.java new file mode 100644 index 0000000..277211a --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/LoopingQueryTest.java @@ -0,0 +1,32 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class LoopingQueryTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + int counter = 0; + int iterations = 1000; + database.execSQL("create table t1(a);"); + database.execSQL("insert into t1(a) values (?)", new Object[]{"foo"}); + StringBuilder buffer = new StringBuilder(); + while(counter < iterations){ + Cursor cursor = database.rawQuery("select * from t1", null); + if(cursor != null){ + cursor.moveToFirst(); + buffer.append(cursor.getString(0)); + cursor.close(); + } + counter++; + } + return buffer.toString().length() > 0; + } + + @Override + public String getName() { + return "Looping Query Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/MigrateDatabaseFrom1xFormatToCurrentFormat.java b/app/src/main/java/net/zetetic/tests/support/MigrateDatabaseFrom1xFormatToCurrentFormat.java new file mode 100644 index 0000000..bb514b8 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/MigrateDatabaseFrom1xFormatToCurrentFormat.java @@ -0,0 +1,76 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import java.io.File; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class MigrateDatabaseFrom1xFormatToCurrentFormat implements ISupportTest { + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + try { + final File sourceDatabase = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.ONE_X_DATABASE); + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory(ZeteticApplication.ONE_X_DATABASE); + SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { + @Override + public void preKey(SQLiteDatabase sqLiteDatabase) { + + } + + @Override + public void postKey(SQLiteDatabase sqLiteDatabase) { + sqLiteDatabase.rawExecSQL("PRAGMA cipher_migrate;"); + } + }; + SupportFactory factory = new SupportFactory(passphrase, hook); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(sourceDatabase.getAbsolutePath()) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + Cursor c = database.query("select * from t1", new String[]{}); + if(c != null){ + c.moveToFirst(); + String a = c.getString(0); + String b = c.getString(1); + c.close(); + database.close(); + result.setResult(a.equals("one for the money") && + b.equals("two for the show")); + + return result; + } + result.setResult(false); + return result; + + } catch (Exception e) { + result.setResult(false); + return result; + } + } + + @Override + public String getName() { + return "Migrate Database 1.x to Current Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/MigrationUserVersion.java b/app/src/main/java/net/zetetic/tests/support/MigrationUserVersion.java new file mode 100644 index 0000000..9e0302e --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/MigrationUserVersion.java @@ -0,0 +1,73 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import net.zetetic.tests.TestResult; +import java.io.File; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class MigrationUserVersion implements ISupportTest { + + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + try { + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory(ZeteticApplication.ONE_X_USER_VERSION_DATABASE); + + File sourceDatabase = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.ONE_X_USER_VERSION_DATABASE); + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { + @Override + public void preKey(SQLiteDatabase sqLiteDatabase) { + + } + + @Override + public void postKey(SQLiteDatabase sqLiteDatabase) { + sqLiteDatabase.rawExecSQL("PRAGMA cipher_migrate;"); + } + }; + SupportFactory factory = new SupportFactory(passphrase, hook); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(sourceDatabase.getAbsolutePath()) + .callback(new SupportSQLiteOpenHelper.Callback(5) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + boolean status = database.getVersion() > 0; + helper.close(); + result.setResult(status); + + return result; + + } catch (Exception e) { + result.setResult(false); + result.setMessage(e.getMessage()); + + return result; + } + } + + @Override + public String getName() { + return "Migrate Database 1.x to Current with user_version"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/NestedTransactionsTest.java b/app/src/main/java/net/zetetic/tests/support/NestedTransactionsTest.java new file mode 100644 index 0000000..69dcd8a --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/NestedTransactionsTest.java @@ -0,0 +1,27 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.tests.SQLCipherTest; + + +public class NestedTransactionsTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.rawExecSQL("savepoint foo;"); + database.rawExecSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?,?);", new Object[]{"one for the money", "two for the show"}); + database.rawExecSQL("savepoint bar;"); + database.execSQL("insert into t1(a,b) values(?,?);", new Object[]{"three to get ready", "go man go"}); + database.rawExecSQL("rollback transaction to bar;"); + database.rawExecSQL("commit;"); + int count = QueryHelper.singleIntegerValueFromQuery(database, "select count(*) from t1;"); + return count == 1; + } + + @Override + public String getName() { + return "Nested Transactions Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/NullQueryResultTest.java b/app/src/main/java/net/zetetic/tests/support/NullQueryResultTest.java new file mode 100644 index 0000000..973dc78 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/NullQueryResultTest.java @@ -0,0 +1,28 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class NullQueryResultTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values (?, ?)", new Object[]{"foo", null}); + database.execSQL("insert into t1(a,b) values (?, ?)", new Object[]{"bar", null}); + Cursor cursor = database.rawQuery("select a from t1", null); + StringBuilder buffer = new StringBuilder(); + while(cursor.moveToNext()){ + buffer.append(cursor.getString(0)); + } + cursor.close(); + return buffer.toString().length() > 0; + } + + @Override + public String getName() { + return "Null Query Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/NullRawQueryTest.java b/app/src/main/java/net/zetetic/tests/support/NullRawQueryTest.java new file mode 100644 index 0000000..8f64e76 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/NullRawQueryTest.java @@ -0,0 +1,30 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class NullRawQueryTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{"one for the money", "two for the show"}); + Cursor cursor = database.rawQuery("select * from t1;", null); + if(cursor != null){ + if(cursor.moveToFirst()) { + String a = cursor.getString(0); + String b = cursor.getString(1); + cursor.close(); + return a.equals("one for the money") && b.equals("two for the show"); + } + } + + + return false; + } + + @Override + public String getName() { + return "Bind Null Raw Query Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/PragmaCipherVersionTest.java b/app/src/main/java/net/zetetic/tests/support/PragmaCipherVersionTest.java new file mode 100644 index 0000000..fb1cb6e --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/PragmaCipherVersionTest.java @@ -0,0 +1,35 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class PragmaCipherVersionTest extends SupportTest { + + private final String CURRENT_CIPHER_VERSION = "4.5.4"; + + @Override + public boolean execute(SQLiteDatabase database) { + + Log.i(TAG, "Before rawQuery"); + Cursor cursor = database.rawQuery("PRAGMA cipher_version", new String[]{}); + Log.i(TAG, "After rawQuery"); + if(cursor != null){ + Log.i(TAG, "Before cursor.moveToNext()"); + cursor.moveToNext(); + Log.i(TAG, "Before cursor.getString(0)"); + String cipherVersion = cursor.getString(0); + Log.i(TAG, "Before cursor.close"); + cursor.close(); + setMessage(String.format("Reported:%s", cipherVersion)); + return cipherVersion.contains(CURRENT_CIPHER_VERSION); + } + return false; + } + + @Override + public String getName() { + return "PRAGMA cipher_version Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/QueryDataSizeTest.java b/app/src/main/java/net/zetetic/tests/support/QueryDataSizeTest.java new file mode 100644 index 0000000..21acf66 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/QueryDataSizeTest.java @@ -0,0 +1,23 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteQueryStats; +import net.zetetic.tests.SQLCipherTest; + +public class QueryDataSizeTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("CREATE TABLE t1(a,b);"); + database.execSQL("INSERT INTO t1(a,b) VALUES(?, ?);", + new Object[]{generateRandomByteArray(256), generateRandomByteArray(256)}); + database.execSQL("INSERT INTO t1(a,b) VALUES(?, ?);", + new Object[]{generateRandomByteArray(1024), generateRandomByteArray(64)}); + SQLiteQueryStats result = database.getQueryStats("SELECT * FROM t1;", new Object[]{}); + return result.getTotalQueryResultSize() > 0 && result.getLargestIndividualRowSize() > 0; + } + + @Override + public String getName() { + return "Query Data Size Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/QueryFloatToStringTest.java b/app/src/main/java/net/zetetic/tests/support/QueryFloatToStringTest.java new file mode 100644 index 0000000..0df82b5 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/QueryFloatToStringTest.java @@ -0,0 +1,18 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.tests.SQLCipherTest; + +public class QueryFloatToStringTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + String value = QueryHelper.singleValueFromQuery(database, "SELECT 42.09;"); + return value.equals("42.09"); + } + + @Override + public String getName() { + return "Query Float to String"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/QueryIntegerToStringTest.java b/app/src/main/java/net/zetetic/tests/support/QueryIntegerToStringTest.java new file mode 100644 index 0000000..f4dad37 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/QueryIntegerToStringTest.java @@ -0,0 +1,18 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.tests.SQLCipherTest; + +public class QueryIntegerToStringTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + String value = QueryHelper.singleValueFromQuery(database, "SELECT 123;"); + return value.equals("123"); + } + + @Override + public String getName() { + return "Query Integer to String"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/QueryLimitTest.java b/app/src/main/java/net/zetetic/tests/support/QueryLimitTest.java new file mode 100644 index 0000000..424c296 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/QueryLimitTest.java @@ -0,0 +1,58 @@ +package net.zetetic.tests.support; + +import android.content.ContentValues; +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class QueryLimitTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + + int query1FirstId = -1; + int query1RowCount = 0; + int limit = 20; + int offset = 5; + int recordsToInsertInSourceTable = 30; + String create = "CREATE TABLE source(id INTEGER(20) PRIMARY KEY, name VARCHAR(50));"; + String other = "CREATE TABLE destination(id INTEGER(20) PRIMARY KEY, name VARCHAR(50));"; + String insert = String.format("INSERT INTO destination(id, name) SELECT * FROM source ORDER BY ID ASC LIMIT %d OFFSET %d;", + limit, offset); + String query1 = "SELECT * FROM destination ORDER BY id ASC;"; + + database.rawExecSQL(create); + database.beginTransaction(); + for(int index = 0; index < recordsToInsertInSourceTable; index++){ + ContentValues values = new ContentValues(); + values.put("id", String.valueOf(index)); + values.put("name", String.format("name%d", index)); + database.insert("source", null, values); + } + database.setTransactionSuccessful(); + database.endTransaction(); + + database.execSQL(other); + database.execSQL(insert); + + Cursor cursor = database.rawQuery(query1, null); + if(cursor != null){ + while (cursor.moveToNext()){ + if(query1FirstId == -1) { + query1FirstId = cursor.getInt(cursor.getColumnIndex("id")); + } + query1RowCount++; + log(String.format("id:%d name:%s", + cursor.getInt(cursor.getColumnIndex("id")), + cursor.getString(cursor.getColumnIndex("name")))); + } + cursor.close(); + } + return query1FirstId == offset && query1RowCount == limit; + } + + @Override + public String getName() { + return "Query Limit Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/QueryTenThousandDataTest.java b/app/src/main/java/net/zetetic/tests/support/QueryTenThousandDataTest.java new file mode 100644 index 0000000..4e4d576 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/QueryTenThousandDataTest.java @@ -0,0 +1,324 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteStatement; +import net.zetetic.tests.SQLCipherTest; +import java.util.ArrayList; +import java.util.List; + +/** + * QueryTenThousandDataTest + * + * It collected two questions: + * 1.In the Samsung Galaxy Note 5, Initially insert 14000 rows of data, the first time to query all the data, it is normal. + * When I execute more than one query all the data, the exception occurs. + * The exception follows: + * Fatal signal 11 (SIGSEGV), code 1, fault addr 0x70bef5dc in tid 26296 (AsyncTask #2) + * + * 2.Regardless of any device, it consume 4 to 8 seconds when query 14000 rows of data. + * When I do not use SQLCipher to query 14000 rows of data, it takes only 200 to 400 milliseconds + * + * Would you be able to tell me how to solve the second problem which query slowly? Sincere thanks. + * @author force + * + */ +public class QueryTenThousandDataTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + + createTable(database, true); + + insertData(database); + + Cursor cursor = database.rawQuery("SELECT * FROM UserInfo", new String[]{}); + log("Query ten thousand row data cursor move"); + long beforeCursorMove = System.nanoTime(); + List userList = new ArrayList(); + try { + for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + UserEntity userEntity = readEntity(cursor, 0); + userList.add(userEntity); + } + long afterCursorMove = System.nanoTime(); + log(String.format("Query thousand row data userList size:%d", userList.size())); + log(String.format("Complete cursor operation time:%d ms", + toMilliseconds(beforeCursorMove, afterCursorMove))); + } finally { + cursor.close(); + } + + return true; + } + + @Override + public String getName() { + return "Query fourteen thousand rows Test"; + } + + private long toMilliseconds(long before, long after){ + return (after - before)/1000000L; + } + + + public void createTable(SQLiteDatabase db, boolean ifNotExists) { + String constraint = ifNotExists? "IF NOT EXISTS ": ""; + db.execSQL("CREATE TABLE " + constraint + "'UserInfo' (" + + "'aaaa' INTEGER PRIMARY KEY AUTOINCREMENT ," + + "'bbbb' INTEGER NOT NULL UNIQUE," + + "'cccc' INTEGER NOT NULL ," + + "'dddd' TEXT NOT NULL ," + + "'eeee' TEXT NOT NULL ," + + "'ffff' TEXT NOT NULL ," + + "'gggg' TEXT NOT NULL ," + + "'hhhh' TEXT NOT NULL ," + + "'iiii' TEXT NOT NULL ," + + "'jjjj' INTEGER NOT NULL ," + + "'kkkk' INTEGER NOT NULL ," + + "'llll' INTEGER NOT NULL ," + + "'mmmm' INTEGER NOT NULL ," + + "'nnnn' TEXT NOT NULL );"); + } + + public void insertData(SQLiteDatabase database) { + + try { + String sql = "INSERT INTO UserInfo ( aaaa, bbbb, cccc, dddd, eeee, ffff, gggg, hhhh, iiii, jjjj, kkkk, llll, mmmm, nnnn ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + database.beginTransaction(); + SQLiteStatement stmt = database.compileStatement(sql); + + for (int index = 0; index < 14000; index++) { + stmt.bindLong(1, Long.valueOf(index + "")); + stmt.bindDouble(2, index); + stmt.bindDouble(3, 1); + stmt.bindString(4, "tom"); + stmt.bindString(5, "lucy"); + stmt.bindString(6, "force"); + stmt.bindString(7, "http"); + stmt.bindString(8, "0201111"); + stmt.bindString(9, "email"); + stmt.bindDouble(10, 222); + stmt.bindDouble(11, 1); + stmt.bindDouble(12, 333); + stmt.bindDouble(13, 444); + stmt.bindString(14, "short"); + + stmt.execute(); + stmt.clearBindings(); + } + } finally { + database.setTransactionSuccessful(); + database.endTransaction(); + } + } + + public UserEntity readEntity(Cursor cursor, int offset) { + UserEntity entity = new UserEntity( // + cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), + cursor.getInt(offset + 1), + cursor.getInt(offset + 2), + cursor.getString(offset + 3), + cursor.getString(offset + 4), + cursor.getString(offset + 5), + cursor.getString(offset + 6), + cursor.getString(offset + 7), + cursor.getString(offset + 8), + cursor.getInt(offset + 9), + cursor.getInt(offset + 10), + cursor.getInt(offset + 11), + cursor.getInt(offset + 12), + cursor.getString(offset + 13) + ); + return entity; + } + + public class UserEntity { + + private int gender; + /** Not-null value. */ + private String pinyinName; + /** Not-null value. */ + private String realName; + /** Not-null value. */ + private String phone; + /** Not-null value. */ + private String shortPhone; + /** Not-null value. */ + + protected Long id; + protected int peerId; + /** Not-null value. + * userEntity --> nickName + * groupEntity --> groupName + * */ + protected String mainName; + /** Not-null value.*/ + protected String avatar; + protected int created; + protected int updated; + private int searchType; + + private int tempGroupRoleType; + private String email; + private int departmentId; + private int status; + private int msgUpdataTime; + + public UserEntity(Long id, int peerId, int gender, String mainName, + String pinyinName, String realName, String avatar, + String phone, String email, int departmentId, + int status, int created, int updated, String shortPhone) { + this.id = id; + this.peerId = peerId; + this.gender = gender; + this.mainName = mainName; + this.pinyinName = pinyinName; + this.realName = realName; + this.avatar = avatar; + this.phone = phone; + this.email = email; + this.departmentId = departmentId; + this.status = status; + this.created = created; + this.updated = updated; + this.shortPhone = shortPhone; + } + + public int getGender() { + return gender; + } + + public void setGender(int gender) { + this.gender = gender; + } + + public String getPinyinName() { + return pinyinName; + } + + public void setPinyinName(String pinyinName) { + this.pinyinName = pinyinName; + } + + public String getRealName() { + return realName; + } + + public void setRealName(String realName) { + this.realName = realName; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getShortPhone() { + return shortPhone; + } + + public void setShortPhone(String shortPhone) { + this.shortPhone = shortPhone; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public int getPeerId() { + return peerId; + } + + public void setPeerId(int peerId) { + this.peerId = peerId; + } + + public String getMainName() { + return mainName; + } + + public void setMainName(String mainName) { + this.mainName = mainName; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } + + public int getCreated() { + return created; + } + + public void setCreated(int created) { + this.created = created; + } + + public int getUpdated() { + return updated; + } + + public void setUpdated(int updated) { + this.updated = updated; + } + + public int getSearchType() { + return searchType; + } + + public void setSearchType(int searchType) { + this.searchType = searchType; + } + + public int getTempGroupRoleType() { + return tempGroupRoleType; + } + + public void setTempGroupRoleType(int tempGroupRoleType) { + this.tempGroupRoleType = tempGroupRoleType; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public int getDepartmentId() { + return departmentId; + } + + public void setDepartmentId(int departmentId) { + this.departmentId = departmentId; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public int getMsgUpdataTime() { + return msgUpdataTime; + } + + public void setMsgUpdataTime(int msgUpdataTime) { + this.msgUpdataTime = msgUpdataTime; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/zetetic/tests/support/RTreeTest.java b/app/src/main/java/net/zetetic/tests/support/RTreeTest.java new file mode 100644 index 0000000..a4bb8be --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/RTreeTest.java @@ -0,0 +1,29 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class RTreeTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + int id = 0; + String create = "CREATE VIRTUAL TABLE demo_index USING rtree(id, minX, maxX, minY, maxY);"; + String insert = "INSERT INTO demo_index VALUES(?, ?, ?, ?, ?);"; + database.execSQL(create); + database.execSQL(insert, new Object[]{1, -80.7749, -80.7747, 35.3776, 35.3778}); + Cursor cursor = database.rawQuery("SELECT * FROM demo_index WHERE maxY < ?;", + new Object[]{36}); + if(cursor != null){ + cursor.moveToNext(); + id = cursor.getInt(0); + cursor.close(); + } + return id == 1; + } + + @Override + public String getName() { + return "RTree Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/RawExecSQLExceptionTest.java b/app/src/main/java/net/zetetic/tests/support/RawExecSQLExceptionTest.java new file mode 100644 index 0000000..f06eb2a --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/RawExecSQLExceptionTest.java @@ -0,0 +1,37 @@ +package net.zetetic.tests.support; + +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; + +public class RawExecSQLExceptionTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + + try { + database.rawExecSQL("select foo from bar"); + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: DID throw SQLiteException", e); + String message = e.getMessage(); + setMessage(message); + if (!message.matches("no such table: bar")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT exception message: " + message); + return false; + } + return true; + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: DID throw other exception", e); + return false; + } + + return false; + } + + @Override + public String getName() { + return "rawExecSQL Exception Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/RawExecSQLTest.java b/app/src/main/java/net/zetetic/tests/support/RawExecSQLTest.java new file mode 100644 index 0000000..9178013 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/RawExecSQLTest.java @@ -0,0 +1,28 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class RawExecSQLTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + + String actual = ""; + String value = "hey"; + database.rawExecSQL("create table t1(a)"); + database.execSQL("insert into t1(a) values (?)", new Object[]{value}); + Cursor result = database.rawQuery("select * from t1", new String[]{}); + if(result != null){ + result.moveToFirst(); + actual = result.getString(0); + result.close(); + } + return actual.equals(value); + } + + @Override + public String getName() { + return "rawExecSQL Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/RawQueryNoSuchFunctionErrorMessageTest.java b/app/src/main/java/net/zetetic/tests/support/RawQueryNoSuchFunctionErrorMessageTest.java new file mode 100644 index 0000000..f6445a6 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/RawQueryNoSuchFunctionErrorMessageTest.java @@ -0,0 +1,38 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; + +public class RawQueryNoSuchFunctionErrorMessageTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + Cursor ignored = database.rawQuery("SELECT UPER('Test')", null); + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: DID throw SQLiteException", e); + String message = e.getMessage(); + setMessage(message); + // TBD missing error code etc. + if (!message.matches("no such function: UPER: .*\\, while compiling: SELECT UPER\\('Test'\\)")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT exception message: " + message); + return false; + } + return true; + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: DID throw other exception", e); + return false; + } + + return false; + } + + @Override + public String getName() { + return "rawQuery no such function error message Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/RawQueryNonsenseStatementErrorMessageTest.java b/app/src/main/java/net/zetetic/tests/support/RawQueryNonsenseStatementErrorMessageTest.java new file mode 100644 index 0000000..02a4cb7 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/RawQueryNonsenseStatementErrorMessageTest.java @@ -0,0 +1,38 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; + +public class RawQueryNonsenseStatementErrorMessageTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + Cursor ignored = database.rawQuery("101", null); + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: DID throw SQLiteException", e); + String message = e.getMessage(); + setMessage(message); + // TBD missing error code etc. + if (!message.matches("near \"101\": syntax error: .*\\, while compiling: 101")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT exception message: " + message); + return false; + } + return true; + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: DID throw other exception", e); + return false; + } + + return false; + } + + @Override + public String getName() { + return "rawQuery nonsense statement error message Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/RawQuerySyntaxErrorMessageTest.java b/app/src/main/java/net/zetetic/tests/support/RawQuerySyntaxErrorMessageTest.java new file mode 100644 index 0000000..feffcac --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/RawQuerySyntaxErrorMessageTest.java @@ -0,0 +1,38 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; + +public class RawQuerySyntaxErrorMessageTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + try { + Cursor ignored = database.rawQuery("SLCT 1", null); + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: DID throw SQLiteException", e); + String message = e.getMessage(); + setMessage(message); + // TBD missing error code etc. + if (!message.matches("near \"SLCT\": syntax error: .*\\, while compiling: SLCT 1")) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: INCORRECT exception message: " + message); + return false; + } + return true; + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: DID throw other exception", e); + return false; + } + + return false; + } + + @Override + public String getName() { + return "rawQuery syntax error message Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/RawQueryTest.java b/app/src/main/java/net/zetetic/tests/support/RawQueryTest.java new file mode 100644 index 0000000..5d6f231 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/RawQueryTest.java @@ -0,0 +1,30 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class RawQueryTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + int rows = 0; + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", + new Object[]{"one for the money", "two for the show"}); + Cursor cursor = database.rawQuery("select * from t1;", null, 1, 1); + if(cursor != null){ + while(cursor.moveToNext()) { + cursor.getString(0); + rows++; + } + cursor.close(); + } + return rows > 0; + } + + @Override + public String getName() { + return "Raw Query Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/RawRekeyTest.java b/app/src/main/java/net/zetetic/tests/support/RawRekeyTest.java new file mode 100644 index 0000000..c4b04d0 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/RawRekeyTest.java @@ -0,0 +1,78 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.QueryHelper; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import java.io.File; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class RawRekeyTest implements ISupportTest { + + String password = "x\'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99\'"; + String rekeyCommand = String.format("PRAGMA rekey = \"%s\";", password); + File databaseFile = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.DATABASE_NAME); + + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase, ZeteticApplication.getInstance().wrapHook(null)); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?,?)", new Object[]{"one for the money", "two for the show"}); + database.query(rekeyCommand); + helper.close(); + + passphrase = SQLiteDatabase.getBytes(password.toCharArray()); + factory = new SupportFactory(passphrase, ZeteticApplication.getInstance().wrapHook(null)); + cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + helper = factory.create(cfg); + database = helper.getWritableDatabase(); + + int count = QueryHelper.singleIntegerValueFromQuery(database, "select count(*) from t1;"); + result.setResult(count == 1); + helper.close(); + return result; + } + + @Override + public String getName() { + return "Raw Rekey Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/ReadWriteDatabaseToExternalStorageTest.java b/app/src/main/java/net/zetetic/tests/support/ReadWriteDatabaseToExternalStorageTest.java new file mode 100644 index 0000000..48b942b --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ReadWriteDatabaseToExternalStorageTest.java @@ -0,0 +1,98 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import android.os.Environment; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import java.io.File; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class ReadWriteDatabaseToExternalStorageTest implements ISupportTest { + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + File externalDatabaseFile = null; + try { + if(isExternalStorageReadable() && isExternalStorageWritable()) { + File databases = ZeteticApplication.getInstance().getExternalFilesDir("databases"); + + externalDatabaseFile = new File(databases, "test.db"); + + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(externalDatabaseFile.getAbsolutePath()) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + if (database == null){ + result.setMessage("Unable to create database on external storage"); + result.setResult(false); + + return result; + } + + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?, ?);", + new Object[]{"one for the money", "two for the show"}); + + Cursor cursor = database.query("select * from t1;", null); + + if (cursor != null){ + cursor.moveToFirst(); + String a = cursor.getString(0); + String b = cursor.getString(1); + cursor.close(); + result.setResult("one for the money".equals(a) && + "two for the show".equals(b)); + } + + return result; + } else { + result.setMessage("External storage unavailable"); + result.setResult(false); + + return result; + } + } + finally { + if (externalDatabaseFile != null){ + externalDatabaseFile.delete(); + } + } + } + + @Override + public String getName() { + return "Read/Write to External Storage"; + } + + public boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state); + } + + public boolean isExternalStorageReadable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state) || + Environment.MEDIA_MOUNTED_READ_ONLY.equals(state); + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/ReadWriteUserVersionTest.java b/app/src/main/java/net/zetetic/tests/support/ReadWriteUserVersionTest.java new file mode 100644 index 0000000..6f0ebc1 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ReadWriteUserVersionTest.java @@ -0,0 +1,19 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class ReadWriteUserVersionTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + int version = 4; + database.setVersion(version); + int readVersion = database.getVersion(); + return version == readVersion; + } + + @Override + public String getName() { + return "Read/write user_version"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/ReadWriteWriteAheadLoggingTest.java b/app/src/main/java/net/zetetic/tests/support/ReadWriteWriteAheadLoggingTest.java new file mode 100644 index 0000000..7b19d6b --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ReadWriteWriteAheadLoggingTest.java @@ -0,0 +1,51 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class ReadWriteWriteAheadLoggingTest extends SupportTest { + @Override + public boolean execute(final SQLiteDatabase database) { + + try { + final int[] a = new int[1]; + final int[] b = new int[1]; + database.setLockingEnabled(false); + boolean walEnabled = database.enableWriteAheadLogging(); + if (!walEnabled) return false; + + //database.execSQL("PRAGMA read_uncommitted = 1;"); + + database.execSQL("CREATE TABLE t1(a,b)"); + database.rawQuery("INSERT INTO t1(a,b) VALUES(?,?);", new Object[]{1, 2}); + database.beginTransaction(); + //database.beginTransactionNonExclusive(); + database.rawQuery("DELETE FROM t1 WHERE a = ?;", new Object[]{1}); + Thread t = new Thread(new Runnable() { + @Override + public void run() { + Cursor cursor = database.rawQuery("SELECT COUNT(*) FROM t1 WHERE a = ?;", new Object[]{1}); + if (cursor != null && cursor.moveToFirst()) { + a[0] = cursor.getInt(0); + b[0] = cursor.getInt(0); + log(String.format("Retrieved %d rows back", a[0])); + } + } + }); + t.start(); + t.join(); + database.setTransactionSuccessful(); + database.endTransaction(); + //return a[0] == 1 && b[0] == 2; + return a[0] == 0 && b[0] == 0; + } catch (InterruptedException ex){ + return false; + } + } + + @Override + public String getName() { + return "Read/Write WAL Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/ReadableDatabaseTest.java b/app/src/main/java/net/zetetic/tests/support/ReadableDatabaseTest.java new file mode 100644 index 0000000..863957b --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ReadableDatabaseTest.java @@ -0,0 +1,64 @@ +package net.zetetic.tests.support; + +import android.content.Context; +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteOpenHelper; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import net.zetetic.tests.TestResult; +import java.io.File; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class ReadableDatabaseTest implements ISupportTest { + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + File databaseFile = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.DATABASE_NAME); + File databasesDirectory = new File(databaseFile.getParent()); + for(File file : databasesDirectory.listFiles()){ + file.delete(); + } + + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + db.execSQL("create table t1(a,b)"); + db.execSQL("insert into t1(a,b) values(?, ?)", new Object[]{"one for the money", + "two for the show"}); + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + Cursor results = database.query("select * from t1", new String[]{}); + int resultCount = 0; + while (results.moveToNext()){ + resultCount++; + } + helper.close(); + result.setResult(resultCount > 0); + + return result; + } + + @Override + public String getName() { + return "Readable Database Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/ReadableWritableAccessTest.java b/app/src/main/java/net/zetetic/tests/support/ReadableWritableAccessTest.java new file mode 100644 index 0000000..ffcb021 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ReadableWritableAccessTest.java @@ -0,0 +1,66 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import java.io.File; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class ReadableWritableAccessTest implements ISupportTest { + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + File databaseFile = ZeteticApplication.getInstance().getDatabasePath(ZeteticApplication.DATABASE_NAME); + File databasesDirectory = new File(databaseFile.getParent()); + for(File file : databasesDirectory.listFiles()){ + file.delete(); + } + + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + db.execSQL("create table t1(a,b)"); + db.execSQL("insert into t1(a,b) values(?, ?)", new Object[]{"one for the money", + "two for the show"}); + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase writableDatabase = helper.getWritableDatabase(); + + writableDatabase.beginTransaction(); + + SupportSQLiteDatabase readableDatabase = helper.getReadableDatabase(); + Cursor results = readableDatabase.query("select count(*) from t1", new String[]{}); + results.moveToFirst(); + int rowCount = results.getInt(0); + + results.close(); + writableDatabase.endTransaction(); + helper.close(); + helper.close(); + result.setResult(rowCount == 1); + + return result; + } + + @Override + public String getName() { + return "Readable/Writable Access Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/ReadableWritableInvalidPasswordTest.java b/app/src/main/java/net/zetetic/tests/support/ReadableWritableInvalidPasswordTest.java new file mode 100644 index 0000000..e94d3c4 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/ReadableWritableInvalidPasswordTest.java @@ -0,0 +1,76 @@ +package net.zetetic.tests.support; + +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import net.sqlcipher.database.SQLiteOpenHelper; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class ReadableWritableInvalidPasswordTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("CREATE TABLE tt(data);"); + database.execSQL("INSERT INTO tt VALUES(?)", new Object[]{"test data"}); + database.close(); + + try { + SupportSQLiteDatabase db = open("invalid password"); + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: successfully opened writable encrypted database with invalid password"); + db.close(); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: SQLiteException when opening writable encrypted database with invalid password OK", e); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: other exception when opening writable encrypted database with invalid password", e); + return false; + } + + try { + SupportSQLiteDatabase db = open(""); + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: successfully opened writable encrypted database with blank password String"); + db.close(); + return false; + } catch (SQLiteException e) { + Log.v(ZeteticApplication.TAG, "EXPECTED RESULT: SQLiteException when opening writable encrypted database with blank password String OK", e); + } catch (Exception e) { + Log.e(ZeteticApplication.TAG, "NOT EXPECTED: other exception when opening writable encrypted database with blank password String", e); + return false; + } + + return true; + } + + @Override + public String getName() { + return "Readable/Writable Invalid Password Test"; + } + + private SupportSQLiteDatabase open(String password) { + byte[] passphrase = SQLiteDatabase.getBytes(password.toCharArray()); + SupportFactory + factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + return helper.getWritableDatabase(); + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/RoomTest.java b/app/src/main/java/net/zetetic/tests/support/RoomTest.java new file mode 100644 index 0000000..ca38701 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/RoomTest.java @@ -0,0 +1,290 @@ +package net.zetetic.tests.support; + +import android.annotation.SuppressLint; +import android.app.Activity; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import androidx.annotation.NonNull; +import androidx.room.Dao; +import androidx.room.Database; +import androidx.room.Delete; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; +import androidx.room.Insert; +import androidx.room.PrimaryKey; +import androidx.room.Query; +import androidx.room.Room; +import androidx.room.RoomDatabase; +import androidx.room.Update; + +public class RoomTest implements ISupportTest { + private static final String DB_NAME = "room.db"; + private final Activity activity; + + @Entity() + public static class ParentEntity { + @PrimaryKey(autoGenerate = true) long id; + int intValue; + float floatValue; + double doubleValue; + char charValue; + boolean boolValue; + } + + @Entity(foreignKeys = @ForeignKey(entity = ParentEntity.class, + parentColumns = "id", childColumns = "parentId", onDelete = ForeignKey.CASCADE), + indices = @Index("parentId")) + public static class ChildEntity { + @PrimaryKey @NonNull String uuid; + String stringValue; + long parentId; + } + + @Dao + public static abstract class TestDao { + @Query("SELECT * FROM ParentEntity") + abstract List getAllParents(); + + @Query("SELECT * FROM ParentEntity WHERE id = :id") + abstract ParentEntity findParentById(long id); + + @Query("SELECT * FROM ChildEntity WHERE parentId = :parentId") + abstract List getAllChildrenForParent(long parentId); + + @Query("SELECT * FROM ChildEntity WHERE uuid = :uuid") + abstract ChildEntity findChildById(String uuid); + + @Insert + abstract void insert(List entities); + + @Insert + abstract long insert(ParentEntity entity); + + @Update + abstract void update(ChildEntity entity); + + @Update + abstract void update(ParentEntity entity); + + @Delete + abstract void delete(ChildEntity entity); + + @Delete + abstract void delete(ParentEntity entity); + } + + @Database(entities = {ParentEntity.class, ChildEntity.class}, version = 1) + public static abstract class TestDatabase extends RoomDatabase { + abstract TestDao testDao(); + } + + public RoomTest(Activity activity) { + this.activity = activity; + } + + @SuppressLint("DefaultLocale") + public TestResult run() { + File dbFile = ZeteticApplication.getInstance().getDatabasePath(DB_NAME); + + if (dbFile.exists()){ + dbFile.delete(); + } + + final TestResult result = new TestResult(getName(), false); + final byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + final SupportFactory factory = new SupportFactory(passphrase); + final TestDatabase room = Room.databaseBuilder(activity, TestDatabase.class, DB_NAME) + .openHelperFactory(factory) + .build(); + ParentEntity parent = new ParentEntity(); + + parent.boolValue = true; + parent.charValue = 'x'; + parent.intValue = 1337; + parent.doubleValue = 3.14159; + parent.floatValue = 2.71828f; + parent.id = room.testDao().insert(parent); + + List parents = room.testDao().getAllParents(); + + if (parents.size() != 1) { + result.setResult(false); + result.setMessage(String.format("expected 1 parent, found %d", parents.size())); + return result; + } + + ParentEntity retrievedParent = parents.get(0); + + if (!assertParent(retrievedParent, parent)) { + result.setResult(false); + result.setMessage("retrieved parent from getAllParents() did not match original"); + return result; + } + + retrievedParent = room.testDao().findParentById(parent.id); + + if (retrievedParent == null) { + result.setResult(false); + result.setMessage(String.format("retrieved parent from findParentById() was null for %d", parent.id)); + return result; + } + + if (!assertParent(retrievedParent, parent)) { + result.setResult(false); + result.setMessage("retrieved parent from findParentById() did not match original"); + return result; + } + + result.setResult(true); + + ChildEntity firstChild = new ChildEntity(); + + firstChild.uuid = UUID.randomUUID().toString(); + firstChild.stringValue = "Um, hi!"; + firstChild.parentId = parent.id; + + ChildEntity secondChild = new ChildEntity(); + + secondChild.uuid = UUID.randomUUID().toString(); + secondChild.stringValue = "And now for something completely different"; + secondChild.parentId = parent.id; + + List children = new ArrayList<>(); + + children.add(firstChild); + children.add(secondChild); + + room.testDao().insert(children); + + List retrievedChildren = room.testDao().getAllChildrenForParent(parent.id); + + if (retrievedChildren.size() != 2) { + result.setResult(false); + result.setMessage(String.format("expected 2 children, found %d", retrievedChildren.size())); + return result; + } + + ChildEntity retrievedChild = retrievedChildren.get(0); + + if (!assertChild(retrievedChild, firstChild) && !assertChild(retrievedChild, secondChild)) { + result.setResult(false); + result.setMessage("retrieved child from getAllChildrenForParent() did not match either original"); + return result; + } + + retrievedChild = retrievedChildren.get(1); + + if (!assertChild(retrievedChild, firstChild) && !assertChild(retrievedChild, secondChild)) { + result.setResult(false); + result.setMessage("retrieved child from getAllChildrenForParent() did not match either original"); + return result; + } + + parent.boolValue = false; + parent.charValue = 'z'; + parent.intValue = 65536; + parent.doubleValue = 2.02214076e23; // # of atoms in a mole + parent.floatValue = 299729.458f; // speed of light in km/s + + room.testDao().update(parent); + + retrievedParent = room.testDao().findParentById(parent.id); + + if (!assertParent(retrievedParent, parent)) { + result.setResult(false); + result.setMessage("retrieved parent from post-update findParentById() did not match original"); + return result; + } + + secondChild.stringValue = "urgent pegboard untied kimono boiler downstairs"; + + room.testDao().update(secondChild); + + retrievedChild = room.testDao().findChildById(secondChild.uuid); + + if (!assertChild(retrievedChild, secondChild)) { + result.setResult(false); + result.setMessage("retrieved child from post-updated findChildById() did not match original"); + return result; + } + + room.testDao().delete(firstChild); + + retrievedChildren = room.testDao().getAllChildrenForParent(parent.id); + + if (retrievedChildren.size() != 1) { + result.setResult(false); + result.setMessage(String.format("expected 1 children post-delete, found %d", retrievedChildren.size())); + return result; + } + + retrievedChild = retrievedChildren.get(0); + + if (!assertChild(retrievedChild, secondChild)) { + result.setResult(false); + result.setMessage("retrieved child from post-delete getAllChildrenForParent() did not match original"); + return result; + } + + room.testDao().delete(parent); + + if (room.testDao().getAllParents().size() != 0) { + result.setResult(false); + result.setMessage("after delete of parent, parent count != 0"); + return result; + } + + if (room.testDao().findParentById(parent.id) != null) { + result.setResult(false); + result.setMessage("after delete of parent, was able to retrieve parent"); + return result; + } + + if (room.testDao().getAllChildrenForParent(parent.id).size() != 0) { + result.setResult(false); + result.setMessage("after delete of parent, child count != 0"); + return result; + } + + if (room.testDao().findChildById(firstChild.uuid) != null) { + result.setResult(false); + result.setMessage("after delete of parent, was able to retrieve first child"); + return result; + } + + if (room.testDao().findChildById(secondChild.uuid) != null) { + result.setResult(false); + result.setMessage("after delete of parent, was able to retrieve second child"); + return result; + } + + return result; + } + + @Override + public String getName() { + return "Room Test"; + } + + private boolean assertParent(ParentEntity one, ParentEntity two) { + return one.id == two.id && + one.boolValue == two.boolValue && + one.charValue == two.charValue && + one.intValue == two.intValue && + Math.abs(one.floatValue - two.floatValue) < 0.01 && + Math.abs(one.doubleValue - two.doubleValue) < 0.01; + } + + private boolean assertChild(ChildEntity one, ChildEntity two) { + return one.uuid.equals(two.uuid) && + one.stringValue.equals(two.stringValue) && + one.parentId == two.parentId; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/RoomUpsertTest.java b/app/src/main/java/net/zetetic/tests/support/RoomUpsertTest.java new file mode 100644 index 0000000..0fed65e --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/RoomUpsertTest.java @@ -0,0 +1,85 @@ +package net.zetetic.tests.support; + +import android.app.Activity; +import android.util.Log; + +import androidx.room.Dao; +import androidx.room.Database; +import androidx.room.Entity; +import androidx.room.PrimaryKey; +import androidx.room.Query; +import androidx.room.Room; +import androidx.room.RoomDatabase; +import androidx.room.Upsert; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; + +import java.io.File; + +class RoomUpsertTest implements ISupportTest { + + private Activity activity; + private static final String DB_NAME = "room.db"; + private String TAG = getClass().getSimpleName(); + + public RoomUpsertTest(Activity activity){ + + this.activity = activity; + } + + @Override + public String getName() { + return "Room Upsert Test"; + } + + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + File databasePath = this.activity.getDatabasePath(DB_NAME); + if(databasePath.exists()){ + databasePath.delete(); + } + try { + final byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + final SupportFactory factory = new SupportFactory(passphrase); + final UserDatabase room = Room.databaseBuilder(activity, UserDatabase.class, DB_NAME) + .openHelperFactory(factory) + .build(); + User user = new User(); + user.name = "Foo Bar"; + user.age = 41; + user.id = room.userDao().upsert(user); + user.age = 42; + room.userDao().upsert(user); + User[] searchUser = room.userDao().findById(user.id); + result.setResult(searchUser[0].age == 42); + } + catch (android.database.sqlite.SQLiteConstraintException ex){ + Log.i(TAG, "Error in Room Upsert test", ex); + } + return result; + } + + @Entity + public static class User { + @PrimaryKey(autoGenerate = true) long id; + String name; + int age; + } + + @Dao + public static abstract class UserDao { + @Upsert + abstract long upsert(User user); + @Query("SELECT * FROM user WHERE id=:id") + abstract User[] findById(long id); + } + + @Database(entities = {User.class}, version = 1) + public static abstract class UserDatabase extends RoomDatabase { + abstract UserDao userDao(); + } +} \ No newline at end of file diff --git a/app/src/main/java/net/zetetic/tests/support/SQLiteCompiledSqlExceptionTest.java b/app/src/main/java/net/zetetic/tests/support/SQLiteCompiledSqlExceptionTest.java new file mode 100644 index 0000000..37770de --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/SQLiteCompiledSqlExceptionTest.java @@ -0,0 +1,97 @@ +package net.zetetic.tests.support; + +import android.app.Activity; +import android.content.ClipData; +import android.util.Log; + +import androidx.room.Dao; +import androidx.room.Database; +import androidx.room.Delete; +import androidx.room.Entity; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.PrimaryKey; +import androidx.room.Query; +import androidx.room.RawQuery; +import androidx.room.Room; +import androidx.room.RoomDatabase; +import androidx.sqlite.db.SimpleSQLiteQuery; + +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class SQLiteCompiledSqlExceptionTest implements ISupportTest { + + private Activity activity; + private TestDatabase testDatabase; + + public SQLiteCompiledSqlExceptionTest(Activity activity) { + this.activity = activity; + } + + public TestResult run() { + boolean result = false; + try { + String key = "password"; + File dbFile = ZeteticApplication.getInstance().getDatabasePath("test.db"); + if (dbFile.exists()){ + dbFile.delete(); + } + SupportFactory factory = new SupportFactory(key.getBytes(), ZeteticApplication.getInstance().wrapHook(null)); + testDatabase = Room.databaseBuilder(ZeteticApplication.getInstance(), TestDatabase.class, dbFile.getName()) + .openHelperFactory(factory) // works without a problem if you comment out this line + .build(); + + for (int i = 0; i < 100; i++) { + testInsert(); + List args = new ArrayList<>(Arrays.asList(new Integer[]{111, 112})); + testDatabase.itemDao().deleteAll(args); + } + result = true; + } catch (Exception ex){ + Log.e(getClass().getSimpleName(), "Exception in test", ex); + } + return new TestResult(getName(), result); + } + + private void testInsert(){ + for(int i = 0; i < 100; i++){ + testDatabase.itemDao().insert(new Item(i)); + } + } + + public String getName() { + return "SQLiteCompiledSqlException Test"; + } + + @Dao + public interface ItemDao { + @Insert(onConflict = OnConflictStrategy.IGNORE) + void insert(Item item); + + @Query("DELETE FROM item WHERE id IN (:ids)") + abstract void deleteAll(List ids); + } + + @Entity + public static class Item { + @PrimaryKey(autoGenerate = true) + long id; + int value; + + public Item(int value){ + this.value = value; + } + } + + @Database(entities = {Item.class}, version = 1) + public static abstract class TestDatabase extends RoomDatabase { + abstract ItemDao itemDao(); + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperConfigureTest.java b/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperConfigureTest.java new file mode 100644 index 0000000..f5a7fcf --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperConfigureTest.java @@ -0,0 +1,60 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class SQLiteOpenHelperConfigureTest implements ISupportTest { + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + TestCallback callback = new TestCallback(2); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(callback) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + + helper.getWritableDatabase(); + result.setResult(callback.onConfigureCalled); + helper.close(); + + return result; + } + + @Override + public String getName() { + return "SQLiteOpenHelper Configure Test"; + } + + private static class TestCallback extends SupportSQLiteOpenHelper.Callback { + boolean onConfigureCalled = false; + + TestCallback(int version) { + super(version); + } + + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + + @Override + public void onConfigure(SupportSQLiteDatabase db) { + onConfigureCalled = true; + } + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperEnableWriteAheadLogAfterGetDatabaseTest.java b/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperEnableWriteAheadLogAfterGetDatabaseTest.java new file mode 100644 index 0000000..721395b --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperEnableWriteAheadLogAfterGetDatabaseTest.java @@ -0,0 +1,51 @@ +package net.zetetic.tests.support; + +import android.content.Context; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteOpenHelper; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import net.zetetic.tests.TestResult; +import java.io.File; +import java.util.UUID; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class SQLiteOpenHelperEnableWriteAheadLogAfterGetDatabaseTest implements ISupportTest { + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + helper.setWriteAheadLoggingEnabled(true); + result.setResult(database.isWriteAheadLoggingEnabled()); + helper.close(); + + return result; + } + + @Override + public String getName() { + return "SQLiteOpenHelper Enable WAL After Get DB"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperEnableWriteAheadLogBeforeGetDatabaseTest.java b/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperEnableWriteAheadLogBeforeGetDatabaseTest.java new file mode 100644 index 0000000..777bd3b --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperEnableWriteAheadLogBeforeGetDatabaseTest.java @@ -0,0 +1,47 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class SQLiteOpenHelperEnableWriteAheadLogBeforeGetDatabaseTest implements ISupportTest { + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + helper.setWriteAheadLoggingEnabled(true); + + SupportSQLiteDatabase database = helper.getWritableDatabase(); + result.setResult(database.isWriteAheadLoggingEnabled()); + helper.close(); + + return result; + } + + @Override + public String getName() { + return "SQLiteOpenHelper Enable WAL Before Get DB"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperGetNameTest.java b/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperGetNameTest.java new file mode 100644 index 0000000..c63058d --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperGetNameTest.java @@ -0,0 +1,39 @@ +package net.zetetic.tests.support; + +import android.content.Context; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteOpenHelper; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import java.io.File; +import java.util.UUID; + +public class SQLiteOpenHelperGetNameTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + UUID name = UUID.randomUUID(); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(name.toString()); + DatabaseHelper helper = new DatabaseHelper(ZeteticApplication.getInstance(), + databasePath.getAbsolutePath()); + return databasePath.getAbsolutePath().equals(helper.getDatabaseName()); + } + + @Override + public String getName() { + return "SQLiteOpenHelper GetName Test"; + } + + class DatabaseHelper extends SQLiteOpenHelper { + + public DatabaseHelper(Context context, String databasePath) { + super(context, databasePath, null, 1); + } + + @Override + public void onCreate(SQLiteDatabase database) { } + + @Override + public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion){} + } + +} diff --git a/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperOnDowngradeTest.java b/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperOnDowngradeTest.java new file mode 100644 index 0000000..efd4b01 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperOnDowngradeTest.java @@ -0,0 +1,80 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class SQLiteOpenHelperOnDowngradeTest implements ISupportTest { + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + TestCallback callback = new TestCallback(2); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(callback) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + + helper.getWritableDatabase(); + + if (callback.onDowngradeCalled) { + result.setResult(false); + return result; + } + + helper.close(); + + passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + factory = new SupportFactory(passphrase); + callback = new TestCallback(1); + cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(callback) + .build(); + helper = factory.create(cfg); + + helper.getWritableDatabase(); + result.setResult(callback.onDowngradeCalled); + helper.close(); + + return result; + } + + @Override + public String getName() { + return "SQLiteOpenHelper OnDowngrade Test"; + } + + private static class TestCallback extends SupportSQLiteOpenHelper.Callback { + boolean onDowngradeCalled = false; + + TestCallback(int version) { + super(version); + } + + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + + @Override + public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + onDowngradeCalled = true; + } + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperWithByteArrayKeyTest.java b/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperWithByteArrayKeyTest.java new file mode 100644 index 0000000..03d2821 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/SQLiteOpenHelperWithByteArrayKeyTest.java @@ -0,0 +1,68 @@ +package net.zetetic.tests.support; + +import android.content.Context; +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteOpenHelper; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import java.io.File; + +public class SQLiteOpenHelperWithByteArrayKeyTest extends SupportTest { + + + private String databaseName = "foo.db"; + + @Override + public boolean execute(SQLiteDatabase database) { + boolean status = false; + database.close(); + byte[] key = generateRandomByteArray(32); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(databaseName); + SQLiteOpenHelper helper = new TestHelper(ZeteticApplication.getInstance(), databasePath.getPath()); + database = helper.getWritableDatabase(key); + database.execSQL("insert into t1(a,b) values(?, ?);", new Object[]{1, 2}); + database.close(); + helper.close(); + + helper = new TestHelper(ZeteticApplication.getInstance(), databasePath.getPath()); + database = helper.getWritableDatabase(key); + Cursor cursor = database.rawQuery("select * from t1;", null); + if (cursor != null) { + cursor.moveToNext(); + int a = cursor.getInt(0); + int b = cursor.getInt(1); + cursor.close(); + status = a == 1 && b == 2; + } + return status; + } + + @Override + public String getName() { + return "SQLiteOpenHelper with Byte Array Key"; + } + + private class TestHelper extends SQLiteOpenHelper { + + private static final int version = 1; + + public TestHelper(Context context, String databasePath){ + super(context, databaseName, null, version, null); + } + + public TestHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { + super(context, databaseName, factory, version); + } + + @Override + public void onCreate(SQLiteDatabase database) { + database.execSQL("create table t1(a,b);"); + } + + @Override + public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { + } + } + +} diff --git a/app/src/main/java/net/zetetic/tests/support/SoundexTest.java b/app/src/main/java/net/zetetic/tests/support/SoundexTest.java new file mode 100644 index 0000000..7a8961d --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/SoundexTest.java @@ -0,0 +1,21 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.tests.SQLCipherTest; + +public class SoundexTest extends SupportTest { + + String SQLCIPHER_SOUNDEX = "S421"; + + @Override + public boolean execute(SQLiteDatabase database) { + String soundex = QueryHelper.singleValueFromQuery(database, "SELECT soundex('sqlcipher');"); + return SQLCIPHER_SOUNDEX.equals(soundex); + } + + @Override + public String getName() { + return "Soundex Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/StatusMemoryUsedTest.java b/app/src/main/java/net/zetetic/tests/support/StatusMemoryUsedTest.java new file mode 100644 index 0000000..23ad086 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/StatusMemoryUsedTest.java @@ -0,0 +1,25 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class StatusMemoryUsedTest extends SupportTest { + + public static final int SQLITE_STATUS_MEMORY_USED = 0; + + @Override + public boolean execute(SQLiteDatabase database) { + + int originalMemory = database.status(SQLITE_STATUS_MEMORY_USED, false); + database.execSQL("create table t1(a,b)"); + database.execSQL("insert into t1(a,b) values(?, ?)", + new Object[]{"one for the money", "two for the show"}); + int currentMemory = database.status(SQLITE_STATUS_MEMORY_USED, false); + return originalMemory != currentMemory; + } + + @Override + public String getName() { + return "Status Memory Used Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/SupportSuiteRunner.java b/app/src/main/java/net/zetetic/tests/support/SupportSuiteRunner.java new file mode 100644 index 0000000..e0de72d --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/SupportSuiteRunner.java @@ -0,0 +1,175 @@ +package net.zetetic.tests.support; + +import android.app.Activity; +import android.os.AsyncTask; +import android.os.Build; +import android.util.Log; + +import net.sqlcipher.CursorWindow; +import net.sqlcipher.CursorWindowAllocation; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.ResultNotifier; +import net.zetetic.tests.TestResult; +import java.util.ArrayList; +import java.util.List; + +public class SupportSuiteRunner extends AsyncTask { + + String TAG = getClass().getSimpleName(); + private ResultNotifier notifier; + private Activity activity; + + public SupportSuiteRunner(Activity activity) { + this.activity = activity; + } + + @Override + protected Void doInBackground(ResultNotifier... resultNotifiers) { + this.notifier = resultNotifiers[0]; + Log.i(ZeteticApplication.TAG, String.format("Running test suite on %s platform", Build.CPU_ABI)); + runSuite(); + return null; + } + + @Override + protected void onProgressUpdate(TestResult... values) { + notifier.send(values[0]); + } + + @Override + protected void onPostExecute(Void aVoid) { + notifier.complete(); + } + + private void runSuite() { + CursorWindowAllocation defaultAllocation = CursorWindow.getCursorWindowAllocation(); + for (ISupportTest test : getTestsToRun()) { + try { + CursorWindow.setCursorWindowAllocation(defaultAllocation); + Log.i(ZeteticApplication.TAG, "Running test:" + test.getName()); + TestResult result = test.run(); + publishProgress(result); + + } catch (Throwable e) { + Log.i(ZeteticApplication.TAG, e.toString()); + publishProgress(new TestResult(test.getName(), false, e.toString())); + } + finally { + CursorWindow.setCursorWindowAllocation(defaultAllocation); + } + } + } + + private List getTestsToRun() { + List tests = new ArrayList<>(); +// tests.add(new SQLiteCompiledSqlExceptionTest(activity)); + if(ZeteticApplication.getInstance().supportsMinLibraryVersionRequired("4.5.4")) { + tests.add(new RoomUpsertTest(activity)); + } + if(ZeteticApplication.getInstance().includesLicenseCode()){ + tests.add(new EncryptBytesTest(activity)); + } + tests.add(new EncryptedRoomTest(activity)); + tests.add(new DecryptedRoomTest(activity)); + tests.add(new LoopingInsertTest()); + tests.add(new FIPSTest()); + tests.add(new PragmaCipherVersionTest()); + tests.add(new VerifyCipherProviderTest()); + tests.add(new VerifyCipherProviderVersionTest()); + tests.add(new JavaClientLibraryVersionTest()); + tests.add(new ReadWriteUserVersionTest()); + tests.add(new QueryDataSizeTest()); + tests.add(new FixedCursorWindowAllocationTest()); + tests.add(new GrowingCursorWindowAllocationTest()); + tests.add(new ReadWriteWriteAheadLoggingTest()); + tests.add(new CreateOpenDatabaseWithByteArrayTest()); + tests.add(new SQLiteOpenHelperWithByteArrayKeyTest()); + tests.add(new SQLiteOpenHelperEnableWriteAheadLogBeforeGetDatabaseTest()); + tests.add(new SQLiteOpenHelperEnableWriteAheadLogAfterGetDatabaseTest()); + tests.add(new SQLiteOpenHelperGetNameTest()); + tests.add(new SQLiteOpenHelperOnDowngradeTest()); + tests.add(new SQLiteOpenHelperConfigureTest()); + tests.add(new CheckIsDatabaseIntegrityOkTest()); + tests.add(new GetAttachedDatabasesTest()); + tests.add(new EnableForeignKeyConstraintsTest()); + tests.add(new ForeignKeyConstraintsEnabledWithTransactionTest()); + tests.add(new EnableWriteAheadLoggingTest()); + tests.add(new DisableWriteAheadLoggingTest()); + tests.add(new CheckIsWriteAheadLoggingEnabledTest()); + tests.add(new WriteAheadLoggingWithTransactionTest()); + tests.add(new WriteAheadLoggingWithInMemoryDatabaseTest()); + tests.add(new WriteAheadLoggingWithAttachedDatabaseTest()); + tests.add(new TransactionNonExclusiveTest()); + tests.add(new TransactionWithListenerTest()); + tests.add(new LargeDatabaseCursorAccessTest()); + tests.add(new QueryLimitTest()); + tests.add(new RTreeTest()); + tests.add(new ReadWriteDatabaseToExternalStorageTest()); + tests.add(new BeginTransactionTest()); + tests.add(new QueryTenThousandDataTest()); + tests.add(new CompileBeginTest()); + tests.add(new TimeQueryExecutionTest()); + tests.add(new UnicodeTest()); + tests.add(new QueryIntegerToStringTest()); + tests.add(new QueryFloatToStringTest()); + tests.add(new ClosedDatabaseTest()); + tests.add(new AttachDatabaseTest()); + tests.add(new CipherMigrateTest()); + tests.add(new GetTypeFromCrossProcessCursorWrapperTest()); + tests.add(new InvalidPasswordTest()); + tests.add(new NullQueryResultTest()); + tests.add(new LoopingQueryTest()); + tests.add(new LoopingCountQueryTest()); + tests.add(new AttachNewDatabaseTest()); + tests.add(new CanThrowSQLiteExceptionTest()); + tests.add(new RawExecSQLTest()); + tests.add(new RawExecSQLExceptionTest()); + tests.add(new CompiledSQLUpdateTest()); + tests.add(new AES128CipherTest()); + tests.add(new MigrateDatabaseFrom1xFormatToCurrentFormat()); + tests.add(new StatusMemoryUsedTest()); + tests.add(new ImportUnencryptedDatabaseTest()); + tests.add(new FullTextSearchTest()); + tests.add(new ReadableDatabaseTest()); + tests.add(new AutoVacuumOverReadTest()); + tests.add(new ReadableWritableAccessTest()); + tests.add(new CursorAccessTest()); + tests.add(new VerifyOnUpgradeIsCalledTest()); + tests.add(new MigrationUserVersion()); + tests.add(new ExportToUnencryptedDatabase()); + tests.add(new EnableForeignKeySupportTest()); + tests.add(new NestedTransactionsTest()); + tests.add(new ComputeKDFTest()); + tests.add(new SoundexTest()); + tests.add(new RawQueryTest()); + tests.add(new RawRekeyTest()); + tests.add(new VerifyUTF8EncodingForKeyTest()); + tests.add(new TextAsIntegerTest()); + tests.add(new TextAsDoubleTest()); + tests.add(new TextAsLongTest()); + tests.add(new ReadableWritableInvalidPasswordTest()); + tests.add(new CopyStringToBufferTestFloatSmallBuffer()); + tests.add(new CopyStringToBufferTestFloatLargeBuffer()); + tests.add(new CopyStringToBufferTestIntegerSmallBuffer()); + tests.add(new CopyStringToBufferTestIntegerLargeBuffer()); + tests.add(new CopyStringToBufferTestStringSmallBuffer()); + tests.add(new CopyStringToBufferTestStringLargeBuffer()); + tests.add(new CopyStringToBufferNullTest()); + tests.add(new RawQuerySyntaxErrorMessageTest()); + tests.add(new RawQueryNonsenseStatementErrorMessageTest()); + tests.add(new RawQueryNoSuchFunctionErrorMessageTest()); + tests.add(new CompileStatementSyntaxErrorMessageTest()); + tests.add(new ExecuteInsertConstraintErrorMessageTest()); + tests.add(new InsertWithOnConflictTest()); + tests.add(new FTS5Test()); + tests.add(new BindBooleanRawQueryTest()); + tests.add(new BindStringRawQueryTest()); + tests.add(new BindDoubleRawQueryTest()); + tests.add(new BindLongRawQueryTest()); + tests.add(new BindFloatRawQueryTest()); + tests.add(new BindByteArrayRawQueryTest()); + tests.add(new NullRawQueryTest()); + tests.add(new RoomTest(activity)); + return tests; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/SupportTest.java b/app/src/main/java/net/zetetic/tests/support/SupportTest.java new file mode 100644 index 0000000..19e3495 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/SupportTest.java @@ -0,0 +1,290 @@ +package net.zetetic.tests.support; + +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.QueryHelper; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.RowColumnValueBuilder; +import net.zetetic.tests.TestResult; +import java.io.File; +import java.security.SecureRandom; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public abstract class SupportTest implements ISupportTest { + + public abstract boolean execute(SQLiteDatabase database); + public String TAG = getClass().getSimpleName(); + SecureRandom random = new SecureRandom(); + private TestResult result; + + private SQLiteDatabase database; + + protected void internalSetUp() { + Log.i(TAG, "Before prepareDatabaseEnvironment"); + ZeteticApplication.getInstance().prepareDatabaseEnvironment(); + Log.i(TAG, "Before createDatabase"); + database = createDatabase(); + Log.i(TAG, "Before setUp"); + setUp(); + } + + @Override + public TestResult run() { + + result = new TestResult(getName(), false); + try { + internalSetUp(); + long startTime = System.nanoTime(); + result.setResult(execute(database)); + long endTime = System.nanoTime(); + Log.i(TAG, String.format("Test complete: %s ran in %.2f seconds using library version %s", getName(), (endTime - startTime)/1000000000.0d, SQLiteDatabase.SQLCIPHER_ANDROID_VERSION)); + internalTearDown(); + } catch (Exception e) { + Log.v(ZeteticApplication.TAG, "Exception running "+getClass().getSimpleName(), e); + } + return result; + } + + protected double toSeconds(long start, long end){ + return (end - start)/1000000000.0d; + } + + protected void setMessage(String message){ + result.setMessage(message); + } + + private void internalTearDown(){ + tearDown(database); + SQLiteDatabase.releaseMemory(); + database.close(); + ZeteticApplication.getInstance().deleteDatabaseFileAndSiblings(ZeteticApplication.DATABASE_NAME); + } + + protected SQLiteDatabase createDatabase() { + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase, ZeteticApplication.getInstance().wrapHook(null)); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + + return (SQLiteDatabase)factory.create(cfg).getWritableDatabase(); + } + + private String generateColumnName(List columnNames, int columnIndex){ + String labels = "abcdefghijklmnopqrstuvwxyz"; + String element = columnIndex < labels.length() + ? String.valueOf(labels.charAt(columnIndex)) + : String.valueOf(labels.charAt(random.nextInt(labels.length() - 1))); + while(columnNames.contains(element) || ReservedWords.contains(element.toUpperCase())){ + element += labels.charAt(random.nextInt(labels.length() - 1)); + } + columnNames.add(element); + Log.i(TAG, String.format("Generated column name:%s for index:%d", element, columnIndex)); + return element; + } + + protected void buildDatabase(SQLiteDatabase database, int rows, int columns, RowColumnValueBuilder builder) { + List columnNames = new ArrayList<>(); + Log.i(TAG, String.format("Building database with %s rows, %d columns", + NumberFormat.getInstance().format(rows), columns)); + String createTemplate = "CREATE TABLE t1(%s);"; + String insertTemplate = "INSERT INTO t1 VALUES(%s);"; + StringBuilder createBuilder = new StringBuilder(); + StringBuilder insertBuilder = new StringBuilder(); + for (int column = 0; column < columns; column++) { + String columnName = generateColumnName(columnNames, column); + createBuilder.append(String.format("%s BLOB%s", + columnName, + column != columns - 1 ? "," : "")); + insertBuilder.append(String.format("?%s", column != columns - 1 ? "," : "")); + } + String create = String.format(createTemplate, createBuilder.toString()); + String insert = String.format(insertTemplate, insertBuilder.toString()); + database.execSQL("DROP TABLE IF EXISTS t1;"); + database.execSQL(create); + String[] names = columnNames.toArray(new String[0]); + for (int row = 0; row < rows; row++) { + Object[] insertArgs = new Object[columns]; + for (int column = 0; column < columns; column++) { + insertArgs[column] = builder.buildRowColumnValue(names, row, column); + } + database.execSQL(insert, insertArgs); + } + Log.i(TAG, String.format("Database built with %d columns, %d rows", columns, rows)); + } + + protected byte[] generateRandomByteArray(int size) { + byte[] data = new byte[size]; + random.nextBytes(data); + return data; + } + + protected void setUp(){}; + protected void tearDown(SQLiteDatabase database){}; + + protected void log(String message){ + Log.i(TAG, message); + } + + protected void logPragmaSetting(SQLiteDatabase database) { + String[] pragmas = new String[]{"cipher_version", "kdf_iter", "cipher_page_size", "cipher_use_hmac", + "cipher_hmac_algorithm", "cipher_kdf_algorithm"}; + String[] defaultPragmas = new String[]{"cipher_default_kdf_iter", "cipher_default_page_size", + "cipher_default_use_hmac", "cipher_default_hmac_algorithm", "cipher_default_kdf_algorithm"}; + for (String pragma : pragmas) { + String value = QueryHelper.singleValueFromQuery(database, String.format("PRAGMA %s;", pragma)); + log(String.format("PRAGMA %s set to %s", pragma, value)); + } + for (String defaultPragma : defaultPragmas) { + String value = QueryHelper.singleValueFromQuery(database, String.format("PRAGMA %s;", defaultPragma)); + log(String.format("PRAGMA %s set to %s", defaultPragma, value)); + } + } + + protected List ReservedWords = Arrays.asList(new String[]{ + "ABORT", + "ACTION", + "ADD", + "AFTER", + "ALL", + "ALTER", + "ANALYZE", + "AND", + "AS", + "ASC", + "ATTACH", + "AUTOINCREMENT", + "BEFORE", + "BEGIN", + "BETWEEN", + "BY", + "CASCADE", + "CASE", + "CAST", + "CHECK", + "COLLATE", + "COLUMN", + "COMMIT", + "CONFLICT", + "CONSTRAINT", + "CREATE", + "CROSS", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "DATABASE", + "DEFAULT", + "DEFERRABLE", + "DEFERRED", + "DELETE", + "DESC", + "DETACH", + "DISTINCT", + "DROP", + "EACH", + "ELSE", + "END", + "ESCAPE", + "EXCEPT", + "EXCLUSIVE", + "EXISTS", + "EXPLAIN", + "FAIL", + "FOR", + "FOREIGN", + "FROM", + "FULL", + "GLOB", + "GROUP", + "HAVING", + "IF", + "IGNORE", + "IMMEDIATE", + "IN", + "INDEX", + "INDEXED", + "INITIALLY", + "INNER", + "INSERT", + "INSTEAD", + "INTERSECT", + "INTO", + "IS", + "ISNULL", + "JOIN", + "KEY", + "LEFT", + "LIKE", + "LIMIT", + "MATCH", + "NATURAL", + "NO", + "NOT", + "NOTNULL", + "NULL", + "OF", + "OFFSET", + "ON", + "OR", + "ORDER", + "OUTER", + "PLAN", + "PRAGMA", + "PRIMARY", + "QUERY", + "RAISE", + "RECURSIVE", + "REFERENCES", + "REGEXP", + "REINDEX", + "RELEASE", + "RENAME", + "REPLACE", + "RESTRICT", + "RIGHT", + "ROLLBACK", + "ROW", + "SAVEPOINT", + "SELECT", + "SET", + "TABLE", + "TEMP", + "TEMPORARY", + "THEN", + "TO", + "TRANSACTION", + "TRIGGER", + "UNION", + "UNIQUE", + "UPDATE", + "USING", + "VACUUM", + "VALUES", + "VIEW", + "VIRTUAL", + "WHEN", + "WHERE", + "WITH", + "WITHOUT" + }); +} diff --git a/app/src/main/java/net/zetetic/tests/support/TextAsDoubleTest.java b/app/src/main/java/net/zetetic/tests/support/TextAsDoubleTest.java new file mode 100644 index 0000000..8484ecc --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/TextAsDoubleTest.java @@ -0,0 +1,25 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class TextAsDoubleTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a TEXT);"); + database.execSQL("insert into t1(a) values(3.14159265359);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + double value = cursor.getDouble(0); + return value == 3.14159265359; + } + return false; + } + + @Override + public String getName() { + return "Text As Double Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/TextAsIntegerTest.java b/app/src/main/java/net/zetetic/tests/support/TextAsIntegerTest.java new file mode 100644 index 0000000..f19154c --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/TextAsIntegerTest.java @@ -0,0 +1,26 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class TextAsIntegerTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a TEXT);"); + database.execSQL("insert into t1(a) values(15);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + int value = cursor.getInt(0); + return value == 15; + } + return false; + } + + @Override + public String getName() { + return "Text as Integer Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/TextAsLongTest.java b/app/src/main/java/net/zetetic/tests/support/TextAsLongTest.java new file mode 100644 index 0000000..0816b85 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/TextAsLongTest.java @@ -0,0 +1,26 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class TextAsLongTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.execSQL("create table t1(a TEXT, b TEXT);"); + database.execSQL("insert into t1(a,b) values(500, 500);"); + Cursor cursor = database.rawQuery("select * from t1;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + long value = cursor.getLong(0); + return value == 500; + } + return false; + } + + @Override + public String getName() { + return "Text as Long Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/TimeQueryExecutionTest.java b/app/src/main/java/net/zetetic/tests/support/TimeQueryExecutionTest.java new file mode 100644 index 0000000..d73c3da --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/TimeQueryExecutionTest.java @@ -0,0 +1,57 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class TimeQueryExecutionTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + + int ROWS = 1000; + long beforeTotal = System.nanoTime(); + database.rawExecSQL("CREATE TABLE t1(a);"); + for(int index = 0; index < ROWS; index++){ + database.execSQL("INSERT INTO t1(a) values(?);", new Object[]{String.valueOf(index)}); + } + long before = System.nanoTime(); + Cursor cursor = database.rawQuery("SELECT * from t1;", new String[]{}); + long after = System.nanoTime(); + log(String.format("SELECT * from t1; took %d ms", + toMilliseconds(before, after))); + if(cursor != null){ + before = System.nanoTime(); + cursor.moveToFirst(); + after = System.nanoTime(); + log(String.format("moveToFirst() took %d ms", + toMilliseconds(before, after))); + long beforeCursorMove = System.nanoTime(); + while(cursor.moveToNext()){ + before = System.nanoTime(); + String value = cursor.getString(0); + after = System.nanoTime(); + log(String.format("getInt(0) returned:%s took %d ms\n", + value, toMilliseconds(before, after))); + } + long afterCursorMove = System.nanoTime(); + log(String.format("Complete cursor operation time:%d ms", + toMilliseconds(beforeCursorMove, afterCursorMove))); + cursor.close(); + } + database.close(); + long totalRuntime = toMilliseconds(beforeTotal, System.nanoTime()); + String message = String.format("Total runtime:%d ms\n", totalRuntime); + log(message); + setMessage(message); + return true; + } + + private long toMilliseconds(long before, long after){ + return (after - before)/1000000L; + } + + @Override + public String getName() { + return "Time Query Execution Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/TransactionNonExclusiveTest.java b/app/src/main/java/net/zetetic/tests/support/TransactionNonExclusiveTest.java new file mode 100644 index 0000000..e94d1e7 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/TransactionNonExclusiveTest.java @@ -0,0 +1,21 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class TransactionNonExclusiveTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.beginTransactionNonExclusive(); + database.execSQL("create table t1(a,b);"); + database.setTransactionSuccessful(); + database.endTransaction(); + return true; + } + + @Override + public String getName() { + return "Transaction Immediate Mode"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/TransactionWithListenerTest.java b/app/src/main/java/net/zetetic/tests/support/TransactionWithListenerTest.java new file mode 100644 index 0000000..87911b9 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/TransactionWithListenerTest.java @@ -0,0 +1,41 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteTransactionListener; +import net.zetetic.tests.SQLCipherTest; + +public class TransactionWithListenerTest extends SupportTest { + + boolean beginCalled = false; + + @Override + public boolean execute(SQLiteDatabase database) { + database.beginTransactionWithListener(listener); + database.execSQL("create table t1(a,b);"); + database.setTransactionSuccessful(); + database.endTransaction(); + return beginCalled; + } + + @Override + public String getName() { + return "Transaction Exclusive Mode"; + } + + SQLiteTransactionListener listener = new SQLiteTransactionListener() { + @Override + public void onBegin() { + beginCalled = true; + } + + @Override + public void onCommit() { + + } + + @Override + public void onRollback() { + + } + }; +} diff --git a/app/src/main/java/net/zetetic/tests/support/UnicodeTest.java b/app/src/main/java/net/zetetic/tests/support/UnicodeTest.java new file mode 100644 index 0000000..41a69c7 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/UnicodeTest.java @@ -0,0 +1,27 @@ +package net.zetetic.tests.support; + +import android.util.Log; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteStatement; +import net.zetetic.tests.SQLCipherTest; + +public class UnicodeTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + //if (android.os.Build.VERSION.SDK_INT >= 23) { // Android M + // This will crash on Android releases 1.X-5.X due the following Android bug: + // https://code.google.com/p/android/issues/detail?id=81341 + SQLiteStatement st = database.compileStatement("SELECT '\uD83D\uDE03'"); // SMILING FACE (MOUTH OPEN) + String res = st.simpleQueryForString(); + String message = String.format("Returned value:%s", res); + setMessage(message); + Log.i(TAG, message); + return res.equals("\uD83D\uDE03"); + //} + } + + @Override + public String getName() { + return "Unicode Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/VerifyCipherProviderTest.java b/app/src/main/java/net/zetetic/tests/support/VerifyCipherProviderTest.java new file mode 100644 index 0000000..a8bac86 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/VerifyCipherProviderTest.java @@ -0,0 +1,21 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.tests.SQLCipherTest; + +public class VerifyCipherProviderTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + String provider = QueryHelper.singleValueFromQuery(database, + "PRAGMA cipher_provider;"); + setMessage(String.format("Reported:%s", provider)); + return provider.contains("openssl"); + } + + @Override + public String getName() { + return "Verify Cipher Provider Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/VerifyCipherProviderVersionTest.java b/app/src/main/java/net/zetetic/tests/support/VerifyCipherProviderVersionTest.java new file mode 100644 index 0000000..f32a56b --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/VerifyCipherProviderVersionTest.java @@ -0,0 +1,22 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.QueryHelper; +import net.zetetic.tests.SQLCipherTest; + +public class VerifyCipherProviderVersionTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + String provider = QueryHelper.singleValueFromQuery(database, + "PRAGMA cipher_provider_version;"); + setMessage(String.format("Reported:%s", provider)); + return provider.contains("OpenSSL 1.1.1") || + provider.contains("OpenSSL 1.0.2u-fips"); + } + + @Override + public String getName() { + return "Verify Cipher Provider Version"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/VerifyOnUpgradeIsCalledTest.java b/app/src/main/java/net/zetetic/tests/support/VerifyOnUpgradeIsCalledTest.java new file mode 100644 index 0000000..7679719 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/VerifyOnUpgradeIsCalledTest.java @@ -0,0 +1,74 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.TestResult; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class VerifyOnUpgradeIsCalledTest implements ISupportTest { + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + TestCallback callback = new TestCallback(1); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(callback) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + + helper.getWritableDatabase(); + + if (callback.onUpgradeCalled) { + result.setResult(false); + return result; + } + + helper.close(); + + passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + factory = new SupportFactory(passphrase); + callback = new TestCallback(2); + cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(ZeteticApplication.DATABASE_NAME) + .callback(callback) + .build(); + helper = factory.create(cfg); + + helper.getWritableDatabase(); + result.setResult(callback.onUpgradeCalled); + helper.close(); + + return result; + } + + @Override + public String getName() { + return "Verify onUpgrade Is Called Test"; + } + + private static class TestCallback extends SupportSQLiteOpenHelper.Callback { + boolean onUpgradeCalled = false; + + TestCallback(int version) { + super(version); + } + + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + onUpgradeCalled = true; + } + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/VerifyUTF8EncodingForKeyTest.java b/app/src/main/java/net/zetetic/tests/support/VerifyUTF8EncodingForKeyTest.java new file mode 100644 index 0000000..6479702 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/VerifyUTF8EncodingForKeyTest.java @@ -0,0 +1,118 @@ +package net.zetetic.tests.support; + +import android.database.Cursor; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import android.database.sqlite.SQLiteException; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import net.zetetic.tests.TestResult; +import java.io.File; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class VerifyUTF8EncodingForKeyTest implements ISupportTest { + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + try { + String password = "hello"; + String invalidPassword = "ŨťŬŬů"; + SupportSQLiteDatabase sourceDatabase; + ZeteticApplication.getInstance().extractAssetToDatabaseDirectory("hello.db"); + File sourceDatabaseFile = ZeteticApplication.getInstance().getDatabasePath("hello.db"); + + byte[] passphrase = SQLiteDatabase.getBytes(invalidPassword.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(sourceDatabaseFile.getAbsolutePath()) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + + try { + sourceDatabase = helper.getWritableDatabase(); + + if(queryContent(sourceDatabase)){ + sourceDatabase.close(); + result.setMessage(String.format("Database should not open with password:%s", invalidPassword)); + result.setResult(false); + + return result; + } + } catch (SQLiteException ex){} + + passphrase = SQLiteDatabase.getBytes(password.toCharArray()); + SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { + @Override + public void preKey(SQLiteDatabase sqLiteDatabase) { + + } + + @Override + public void postKey(SQLiteDatabase sqLiteDatabase) { + sqLiteDatabase.rawExecSQL("PRAGMA cipher_migrate;"); + } + }; + factory = new SupportFactory(passphrase, hook); + cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(sourceDatabaseFile.getAbsolutePath()) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + helper = factory.create(cfg); + + sourceDatabase = helper.getWritableDatabase(); + result.setResult(queryContent(sourceDatabase)); + helper.close(); + } catch (Exception e) { + result.setMessage(e.getMessage()); + result.setResult(false); + } + + return result; + } + + private boolean queryContent(SupportSQLiteDatabase source){ + Cursor result = source.query("select * from t1", new String[]{}); + if(result != null){ + result.moveToFirst(); + String a = result.getString(0); + String b = result.getString(1); + result.close(); + return a.equals("one for the money") && + b.equals("two for the show"); + } + return false; + } + + @Override + public String getName() { + return "Verify Only UTF-8 Key Test"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/WriteAheadLoggingWithAttachedDatabaseTest.java b/app/src/main/java/net/zetetic/tests/support/WriteAheadLoggingWithAttachedDatabaseTest.java new file mode 100644 index 0000000..497dac5 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/WriteAheadLoggingWithAttachedDatabaseTest.java @@ -0,0 +1,23 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import java.io.File; +import java.util.UUID; + +public class WriteAheadLoggingWithAttachedDatabaseTest extends SupportTest { + @Override + public boolean execute(SQLiteDatabase database) { + UUID name = UUID.randomUUID(); + File databasePath = ZeteticApplication.getInstance().getDatabasePath(name.toString()); + database.execSQL("ATTACH DATABASE ? as foo;", new Object[]{databasePath.getAbsolutePath()}); + boolean result = database.enableWriteAheadLogging(); + return result == false; + } + + @Override + public String getName() { + return "Disallow WAL Mode with Attached DB"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/WriteAheadLoggingWithInMemoryDatabaseTest.java b/app/src/main/java/net/zetetic/tests/support/WriteAheadLoggingWithInMemoryDatabaseTest.java new file mode 100644 index 0000000..936fed3 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/WriteAheadLoggingWithInMemoryDatabaseTest.java @@ -0,0 +1,48 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SupportFactory; +import net.zetetic.ZeteticApplication; +import net.zetetic.tests.SQLCipherTest; +import net.zetetic.tests.TestResult; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class WriteAheadLoggingWithInMemoryDatabaseTest implements ISupportTest { + @Override + public TestResult run() { + TestResult result = new TestResult(getName(), false); + + byte[] passphrase = SQLiteDatabase.getBytes(ZeteticApplication.DATABASE_PASSWORD.toCharArray()); + SupportFactory factory = new SupportFactory(passphrase); + SupportSQLiteOpenHelper.Configuration cfg = + SupportSQLiteOpenHelper.Configuration.builder(ZeteticApplication.getInstance()) + .name(null) + .callback(new SupportSQLiteOpenHelper.Callback(1) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + // unused + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + // unused + } + }) + .build(); + SupportSQLiteOpenHelper helper = factory.create(cfg); + SupportSQLiteDatabase database = helper.getWritableDatabase(); + + result.setResult(!database.enableWriteAheadLogging()); + + helper.close(); + + return result; + } + + @Override + public String getName() { + return "Disallow WAL Mode with in memory DB"; + } +} diff --git a/app/src/main/java/net/zetetic/tests/support/WriteAheadLoggingWithTransactionTest.java b/app/src/main/java/net/zetetic/tests/support/WriteAheadLoggingWithTransactionTest.java new file mode 100644 index 0000000..e37ccd1 --- /dev/null +++ b/app/src/main/java/net/zetetic/tests/support/WriteAheadLoggingWithTransactionTest.java @@ -0,0 +1,25 @@ +package net.zetetic.tests.support; + +import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.tests.SQLCipherTest; + +public class WriteAheadLoggingWithTransactionTest extends SupportTest { + + @Override + public boolean execute(SQLiteDatabase database) { + database.beginTransaction(); + try { + database.enableWriteAheadLogging(); + } catch (IllegalStateException ex){ + if(ex.getMessage().equals("Write Ahead Logging cannot be enabled while in a transaction")) { + return true; + } + } + return false; + } + + @Override + public String getName() { + return "Disallow WAL Mode while in transaction"; + } +} diff --git a/app/src/main/res/drawable-hdpi/icon.png b/app/src/main/res/drawable-hdpi/icon.png new file mode 100644 index 0000000..f2c6cf2 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon.png differ diff --git a/app/src/main/res/drawable-ldpi/icon.png b/app/src/main/res/drawable-ldpi/icon.png new file mode 100644 index 0000000..2e41a9b Binary files /dev/null and b/app/src/main/res/drawable-ldpi/icon.png differ diff --git a/app/src/main/res/drawable-mdpi/icon.png b/app/src/main/res/drawable-mdpi/icon.png new file mode 100644 index 0000000..d970657 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/icon.png differ diff --git a/app/src/main/res/drawable-xhdpi/icon.png b/app/src/main/res/drawable-xhdpi/icon.png new file mode 100644 index 0000000..9025742 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/icon.png differ diff --git a/app/src/main/res/layout/cursor_item.xml b/app/src/main/res/layout/cursor_item.xml new file mode 100644 index 0000000..a74e696 --- /dev/null +++ b/app/src/main/res/layout/cursor_item.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/res/layout/main.xml b/app/src/main/res/layout/main.xml similarity index 100% rename from res/layout/main.xml rename to app/src/main/res/layout/main.xml diff --git a/app/src/main/res/layout/scrolling_cursor_view.xml b/app/src/main/res/layout/scrolling_cursor_view.xml new file mode 100644 index 0000000..4e4a6a6 --- /dev/null +++ b/app/src/main/res/layout/scrolling_cursor_view.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/res/layout/test_result_row.xml b/app/src/main/res/layout/test_result_row.xml similarity index 94% rename from res/layout/test_result_row.xml rename to app/src/main/res/layout/test_result_row.xml index 2397f87..0bb3e90 100644 --- a/res/layout/test_result_row.xml +++ b/app/src/main/res/layout/test_result_row.xml @@ -25,8 +25,7 @@ android:layout_width="fill_parent" android:gravity="left" android:textSize="14sp" - android:singleLine="true" - android:ellipsize="end" + android:singleLine="false" android:layout_below="@+id/test_name" android:layout_toLeftOf="@+id/test_status"/> \ No newline at end of file diff --git a/app/src/main/res/layout/test_runners.xml b/app/src/main/res/layout/test_runners.xml new file mode 100644 index 0000000..a220b54 --- /dev/null +++ b/app/src/main/res/layout/test_runners.xml @@ -0,0 +1,21 @@ + + +