/* * Copyright (C) 2016 The Android Open Source Project * Modifications Copyright (c) 2017 CommonsWare, LLC * * 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 com.abubusoft.kripton.android.sqlcipher; import com.abubusoft.kripton.android.Logger; import android.content.Context; import android.os.Build; import net.sqlcipher.DatabaseErrorHandler; import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabaseHook; import net.sqlcipher.database.SQLiteException; import net.sqlcipher.database.SQLiteOpenHelper; import androidx.annotation.RequiresApi; import androidx.sqlite.db.SupportSQLiteDatabase; import androidx.sqlite.db.SupportSQLiteOpenHelper; /** * SupportSQLiteOpenHelper implementation that works with SQLCipher for Android */ class KriptonSQLCipherHelper implements SupportSQLiteOpenHelper { private final OpenHelper delegate; private final byte[] passphrase; private final boolean clearPassphrase; private final boolean requiredPassphrase; KriptonSQLCipherHelper(Context context, String name, Callback callback, byte[] passphrase, KriptonSQLCipherHelperFactory.Options options) { SQLiteDatabase.loadLibs(context); clearPassphrase = options.clearPassphrase; delegate = createDelegate(context, name, callback, options); this.passphrase = passphrase; this.requiredPassphrase = options.requiredPassphrase; } private OpenHelper createDelegate(Context context, String name, final Callback callback, KriptonSQLCipherHelperFactory.Options options) { final Database[] dbRef = new Database[1]; return (new OpenHelper(context, name, dbRef, callback, options)); } /** * {@inheritDoc} */ @Override synchronized public String getDatabaseName() { return delegate.getDatabaseName(); } /** * {@inheritDoc} */ @Override @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) synchronized public void setWriteAheadLoggingEnabled(boolean enabled) { delegate.setWriteAheadLoggingEnabled(enabled); } /** * {@inheritDoc} * * NOTE: by default, this implementation zeros out the passphrase after * opening the database */ @Override synchronized public SupportSQLiteDatabase getWritableDatabase() { SupportSQLiteDatabase result; // if we don't have a passphrase, an exception will be thrown if (requiredPassphrase && passphrase == null) { Logger.fatal("Try to open ciphered database %s without any passphrase", getDatabaseName()); throw new SQLCipherPassphraseRequiredException(); } try { result = delegate.getWritableSupportDatabase(passphrase); } catch (SQLiteException e) { if (passphrase != null) { boolean isCleared = true; for (byte b : passphrase) { isCleared = isCleared && (b == (byte) 0); } if (isCleared) { throw new IllegalStateException("The passphrase appears to be cleared. This happens by" + "default the first time you use the factory to open a database, so we can remove the" + "cleartext passphrase from memory. If you close the database yourself, please use a" + "fresh SafeHelperFactory to reopen it. If something else (e.g., Room) closed the" + "database, and you cannot control that, use SafeHelperFactory.Options to opt out of" + "the automatic password clearing step. See the project README for more information."); } } throw e; } if (clearPassphrase && passphrase != null) { for (int i = 0; i < passphrase.length; i++) { passphrase[i] = (byte) 0; } } return (result); } /** * {@inheritDoc} * * NOTE: this implementation delegates to getWritableDatabase(), to ensure * that we only need the passphrase once */ @Override public SupportSQLiteDatabase getReadableDatabase() { return (getWritableDatabase()); } /** * {@inheritDoc} */ @Override synchronized public void close() { delegate.close(); } static class OpenHelper extends SQLiteOpenHelper { private final Database[] dbRef; private volatile Callback callback; private volatile boolean migrated; OpenHelper(Context context, String name, Database[] dbRef, Callback callback, KriptonSQLCipherHelperFactory.Options options) { super(context, name, null, callback.version, new SQLiteDatabaseHook() { @Override public void preKey(SQLiteDatabase database) { if (options != null && options.preKeySql != null) { database.rawExecSQL(options.preKeySql); } } @Override public void postKey(SQLiteDatabase database) { if (options != null && options.postKeySql != null) { database.rawExecSQL(options.postKeySql); } } }, new DatabaseErrorHandler() { @Override public void onCorruption(SQLiteDatabase dbObj) { Database db = dbRef[0]; if (db != null) { callback.onCorruption(db); } } }); this.dbRef = dbRef; this.callback = callback; } synchronized SupportSQLiteDatabase getWritableSupportDatabase(byte[] passphrase) { migrated = false; SQLiteDatabase db = super.getWritableDatabase(passphrase); if (migrated) { close(); return getWritableSupportDatabase(passphrase); } return getWrappedDb(db); } synchronized Database getWrappedDb(SQLiteDatabase db) { Database wrappedDb = dbRef[0]; if (wrappedDb == null) { wrappedDb = new Database(db); dbRef[0] = wrappedDb; } return (dbRef[0]); } /** * {@inheritDoc} */ @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { callback.onCreate(getWrappedDb(sqLiteDatabase)); } /** * {@inheritDoc} */ @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { migrated = true; callback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion); } /** * {@inheritDoc} */ @Override public void onConfigure(SQLiteDatabase db) { callback.onConfigure(getWrappedDb(db)); } /** * {@inheritDoc} */ @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { migrated = true; callback.onDowngrade(getWrappedDb(db), oldVersion, newVersion); } /** * {@inheritDoc} */ @Override public void onOpen(SQLiteDatabase db) { if (!migrated) { // from Google: "if we've migrated, we'll re-open the db so we // should not call the callback." callback.onOpen(getWrappedDb(db)); } } /** * {@inheritDoc} */ @Override public synchronized void close() { super.close(); dbRef[0] = null; } } }