Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to new sqlcipher package #87

Merged
merged 6 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pretixscan/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.multidex:multidex:2.0.1'
implementation "androidx.preference:preference-ktx:1.2.1"
implementation 'androidx.sqlite:sqlite:2.2.0' // 2.4.0 requires compileSDK 34

implementation 'com.louiscad.splitties:splitties-toast:3.0.0'
implementation 'com.github.traex.rippleeffect:library:1.3'
Expand All @@ -141,7 +142,7 @@ dependencies {
implementation "io.requery:requery:$requery_version"
implementation "io.requery:requery-android:$requery_version"
implementation "io.requery:requery-kotlin:$requery_version"
implementation 'net.zetetic:android-database-sqlcipher:3.5.9'
implementation 'net.zetetic:sqlcipher-android:4.5.5'
implementation "net.sourceforge.streamsupport:streamsupport-cfuture:$cfuture_version"
implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_core_version"
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_kotlin_version"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ import eu.pretix.libpretixsync.check.TicketCheckProvider
import eu.pretix.libpretixsync.db.Migrations
import eu.pretix.libpretixui.android.covid.DGC
import eu.pretix.pretixscan.droid.connectivity.ConnectivityHelper
import eu.pretix.pretixscan.droid.db.SqlCipherDatabaseSource
import eu.pretix.pretixscan.utils.KeystoreHelper
import io.requery.BlockingEntityStore
import io.requery.Persistable
import io.requery.android.sqlcipher.SqlCipherDatabaseSource
import io.requery.android.sqlite.DatabaseSource
import io.requery.sql.EntityDataStore
import net.zetetic.database.sqlcipher.SQLiteConnection
import net.zetetic.database.sqlcipher.SQLiteDatabase
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook
import java.util.concurrent.locks.ReentrantLock


Expand All @@ -31,6 +34,23 @@ class PretixScan : MultiDexApplication() {
var flipperInit: FlipperInitializer.IntializationResult? = null
lateinit var connectivityHelper: ConnectivityHelper

private fun migrateSqlCipher() {
val dbPass = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) KeystoreHelper.secureValue(KEYSTORE_PASSWORD, true)
else KEYSTORE_PASSWORD

System.loadLibrary("sqlcipher")

val databaseFile = getDatabasePath(Models.DEFAULT.name)
SQLiteDatabase.openOrCreateDatabase(databaseFile, dbPass, null, null, object: SQLiteDatabaseHook {
override fun preKey(connection: SQLiteConnection) {
}

override fun postKey(connection: SQLiteConnection) {
connection.execute("PRAGMA cipher_migrate;", emptyArray(), null)
raphaelm marked this conversation as resolved.
Show resolved Hide resolved
}
})
}

val data: BlockingEntityStore<Persistable>
get() {
if (dataStore == null) {
Expand All @@ -44,19 +64,34 @@ class PretixScan : MultiDexApplication() {
val dbPass = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) KeystoreHelper.secureValue(KEYSTORE_PASSWORD, true)
else KEYSTORE_PASSWORD

var source = SqlCipherDatabaseSource(this,
Models.DEFAULT, Models.DEFAULT.getName(), dbPass, Migrations.CURRENT_VERSION)
source.setLoggingEnabled(false)
var source = SqlCipherDatabaseSource(
this,
Models.DEFAULT,
Models.DEFAULT.name,
dbPass,
Migrations.CURRENT_VERSION
)
try {
// check if database has been decrypted
source.readableDatabase.rawQuery("select count(*) from sqlite_master;", emptyArray()) //source.getReadableDatabase().getSyncedTables() ???
source.readableDatabase.rawQuery("select count(*) from sqlite_master;", emptyArray())
} catch (e: SQLiteException) {
// if not, delete it
this.deleteDatabase(Models.DEFAULT.getName())
// and create a new one
source = SqlCipherDatabaseSource(this,
Models.DEFAULT, Models.DEFAULT.getName(), dbPass, Migrations.CURRENT_VERSION)
try {
migrateSqlCipher()
source.readableDatabase.rawQuery("select count(*) from sqlite_master;", emptyArray())
} catch (e: SQLiteException) {
// still not decrypted? then we probably lost the key due to a keystore issue
// let's start fresh, there's no reasonable other way to let the user out of this
this.deleteDatabase(Models.DEFAULT.getName())
source = SqlCipherDatabaseSource(
this,
Models.DEFAULT,
Models.DEFAULT.name,
dbPass,
Migrations.CURRENT_VERSION
)
}
}
source.setLoggingEnabled(false)

val configuration = source.configuration
dataStore = EntityDataStore(configuration)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Based on code
* Copyright 2019 requery.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package eu.pretix.pretixscan.droid.db


import android.database.sqlite.SQLiteConstraintException
import android.database.sqlite.SQLiteException
import io.requery.android.sqlite.BaseConnection
import net.zetetic.database.sqlcipher.SQLiteDatabase
import java.sql.DatabaseMetaData
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.SQLException
import java.sql.SQLFeatureNotSupportedException
import java.sql.SQLIntegrityConstraintViolationException
import java.sql.Statement

/**
* [java.sql.Connection] implementation using SQLCipher SQLite Android API.
*
* @author Nikhil Purushe
*/
internal class SqlCipherConnection(val database: SQLiteDatabase) : BaseConnection() {
private val metaData: SqlCipherMetaData
private var enteredTransaction: Boolean = false

init {
autoCommit = true
metaData = SqlCipherMetaData(this)
}

override fun ensureTransaction() {
if (!autoCommit) {
if (!database.inTransaction()) {
database.beginTransaction()
enteredTransaction = true
}
}
}

@Throws(SQLException::class)
override fun execSQL(sql: String) {
try {
database.execSQL(sql)
} catch (e: SQLiteException) {
throwSQLException(e)
}

}

@Throws(SQLException::class)
override fun commit() {
if (autoCommit) {
throw SQLException("commit called while in autoCommit mode")
}
if (database.inTransaction() && enteredTransaction) {
try {
database.setTransactionSuccessful()
} catch (e: IllegalStateException) {
throw SQLException(e)
} finally {
database.endTransaction()
enteredTransaction = false
}
}
}

@Throws(SQLException::class)
override fun createStatement(): Statement {
return SqlCipherStatement(this)
}

@Throws(SQLException::class)
override fun createStatement(resultSetType: Int, resultSetConcurrency: Int): Statement {
return createStatement(resultSetType,
resultSetConcurrency, ResultSet.HOLD_CURSORS_OVER_COMMIT)
}

@Throws(SQLException::class)
override fun createStatement(resultSetType: Int, resultSetConcurrency: Int,
resultSetHoldability: Int): Statement {
if (resultSetConcurrency == ResultSet.CONCUR_UPDATABLE) {
throw SQLFeatureNotSupportedException("CONCUR_UPDATABLE not supported")
}
return SqlCipherStatement(this)
}

@Throws(SQLException::class)
override fun getMetaData(): DatabaseMetaData {
return metaData
}

@Throws(SQLException::class)
override fun isClosed(): Boolean {
return !database.isOpen()
}

@Throws(SQLException::class)
override fun isReadOnly(): Boolean {
return database.isReadOnly()
}

@Throws(SQLException::class)
override fun prepareStatement(sql: String, autoGeneratedKeys: Int): PreparedStatement {
return SqlCipherPreparedStatement(this, sql, autoGeneratedKeys)
}

@Throws(SQLException::class)
override fun prepareStatement(sql: String,
resultSetType: Int,
resultSetConcurrency: Int,
resultSetHoldability: Int): PreparedStatement {
return SqlCipherPreparedStatement(this, sql, Statement.NO_GENERATED_KEYS)
}

@Throws(SQLException::class)
override fun prepareStatement(sql: String, columnNames: Array<String>): PreparedStatement {
return SqlCipherPreparedStatement(this, sql, Statement.RETURN_GENERATED_KEYS)
}

@Throws(SQLException::class)
override fun rollback() {
if (autoCommit) {
throw SQLException("commit called while in autoCommit mode")
}
database.endTransaction()
}

companion object {
@Throws(SQLException::class)
fun throwSQLException(exception: SQLiteException) {
if (exception is SQLiteConstraintException) {
throw SQLIntegrityConstraintViolationException(exception)
}
throw SQLException(exception)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Based on code
* Copyright 2019 requery.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package eu.pretix.pretixscan.droid.db

import android.content.Context
import io.requery.android.DefaultMapping
import io.requery.android.LoggingListener
import io.requery.android.sqlite.DatabaseProvider
import io.requery.android.sqlite.SchemaUpdater
import io.requery.meta.EntityModel
import io.requery.sql.Configuration
import io.requery.sql.ConfigurationBuilder
import io.requery.sql.Mapping
import io.requery.sql.Platform
import io.requery.sql.SchemaModifier
import io.requery.sql.TableCreationMode
import io.requery.sql.platform.SQLite
import io.requery.util.function.Function
import net.zetetic.database.sqlcipher.SQLiteDatabase
import net.zetetic.database.sqlcipher.SQLiteOpenHelper
import java.sql.Connection

open class SqlCipherDatabaseSource(context: Context,
private val model: EntityModel,
name: String,
password: String?,
version: Int)
: SQLiteOpenHelper(context, name, password, null, version, 0, null, null, true), DatabaseProvider<SQLiteDatabase> {

private val platform: Platform
private val mapping: Mapping
private var db: SQLiteDatabase? = null
private var _configuration: Configuration? = null
private var loggingEnabled: Boolean = false
private var mode: TableCreationMode? = null

init {
this.platform = SQLite()
this.mapping = onCreateMapping(platform)
this.mode = TableCreationMode.CREATE_NOT_EXISTS
System.loadLibrary("sqlcipher");
}

override fun setLoggingEnabled(enable: Boolean) {
this.loggingEnabled = enable
}

override fun setTableCreationMode(mode: TableCreationMode) {
this.mode = mode
}

protected fun onCreateMapping(platform: Platform): Mapping {
return DefaultMapping(platform)
}

protected fun onConfigure(builder: ConfigurationBuilder) {
if (loggingEnabled) {
val loggingListener = LoggingListener()
builder.addStatementListener(loggingListener)
}
}

private fun getConnection(db: SQLiteDatabase): SqlCipherConnection {
synchronized(this) {
return SqlCipherConnection(db)
}
}

override fun getConfiguration(): Configuration {
if (_configuration == null) {
val builder = ConfigurationBuilder(this, model)
.setMapping(mapping)
.setPlatform(platform)
.setBatchUpdateSize(1000)
onConfigure(builder)
_configuration = builder.build()
}
return _configuration!!
}

override fun onCreate(db: SQLiteDatabase) {
this.db = db
SchemaModifier(configuration).createTables(TableCreationMode.CREATE)
}

override fun onConfigure(db: SQLiteDatabase) {}

override fun onOpen(db: SQLiteDatabase?) {
super.onOpen(db)
if (!db!!.isReadOnly) {
db.execSQL("PRAGMA foreign_keys = ON")
}
}

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
this.db = db
val updater = SchemaUpdater(configuration,
Function { s -> db.rawQuery(s, null) }, mode)
updater.update()
}

override fun getConnection(): Connection {
synchronized(this) {
if (db == null) {
db = getWritableDatabase()
}
return getConnection(db!!)
}
}
}
Loading
Loading