package org.awesomeapp.messenger.provider; import info.guardianproject.cacheword.CacheWordHandler; import java.lang.reflect.Method; import java.nio.CharBuffer; import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabase.CursorFactory; import net.sqlcipher.database.SQLiteException; import net.sqlcipher.database.SQLiteOpenHelper; import android.content.Context; import android.util.Log; /** * A helper class to manage database creation and version management. You create * a subclass implementing {@link #onCreate}, {@link #onUpgrade} and optionally * {@link #onOpen}, and this class takes care of opening the database if it * exists, creating it if it does not, and upgrading it as necessary. * Transactions are used to make sure the database is always in a sensible * state. * <p> * For an example, see the NotePadProvider class in the NotePad sample * application, in the <em>samples/</em> directory of the SDK. * </p> */ public abstract class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final String TAG = "SQLCipherOpenHelper"; protected Context mContext; // shame we have to duplicate this here private CacheWordHandler mHandler; public SQLCipherOpenHelper(CacheWordHandler cacheWord, Context context, String name, CursorFactory factory, int version) { super(context, name, factory, version, new SQLCipherV3MigrationHook(context)); if (cacheWord == null) throw new IllegalArgumentException("CacheWordHandler is null"); mHandler = cacheWord; } /** * Create and/or open a database that will be used for reading and writing. * Once opened successfully, the database is cached, so you can call this * method every time you need to write to the database. Make sure to call * {@link #close} when you no longer need it. * <p> * Errors such as bad permissions or a full disk may cause this operation to * fail, but future attempts may succeed if the problem is fixed. * </p> * * @throws SQLiteException if the database cannot be opened for writing * @return a read/write database object valid until {@link #close} is called */ public synchronized SQLiteDatabase getWritableDatabase() { if (mHandler.isLocked()) throw new SQLiteException("Database locked. Decryption key unavailable."); return super.getWritableDatabase(encodeRawKey(mHandler.getEncryptionKey())); } /** * Create and/or open a database. This will be the same object returned by * {@link #getWritableDatabase} unless some problem, such as a full disk, * requires the database to be opened read-only. In that case, a read-only * database object will be returned. If the problem is fixed, a future call * to {@link #getWritableDatabase} may succeed, in which case the read-only * database object will be closed and the read/write object will be returned * in the future. * * @throws SQLiteException if the database cannot be opened * @return a database object valid until {@link #getWritableDatabase} or * {@link #close} is called. */ public synchronized SQLiteDatabase getReadableDatabase() { if (mHandler.isLocked()) throw new SQLiteException("Database locked. Decryption key unavailable."); return super.getReadableDatabase(encodeRawKey(mHandler.getEncryptionKey())); } /** * Formats a byte sequence into the literal string format expected by * SQLCipher: hex'HEX ENCODED BYTES' The key data must be 256 bits (32 * bytes) wide. The key data will be formatted into a 64 character hex * string with a special prefix and suffix SQLCipher uses to distinguish raw * key data from a password. * * @link http://sqlcipher.net/sqlcipher-api/#key * @param raw_key a 32 byte array * @return the encoded key */ public static char[] encodeRawKey(byte[] raw_key) { if (raw_key.length != 32) throw new IllegalArgumentException("provided key not 32 bytes (256 bits) wide"); final String kPrefix; final String kSuffix; if (sqlcipher_uses_native_key) { Log.d(TAG, "sqlcipher uses native method to set key"); kPrefix = "x'"; kSuffix = "'"; } else { Log.d(TAG, "sqlcipher uses PRAGMA to set key - SPECIAL HACK IN PROGRESS"); kPrefix = "x''"; kSuffix = "''"; } final char[] key_chars = encodeHex(raw_key, HEX_DIGITS_LOWER); if (key_chars.length != 64) throw new IllegalStateException("encoded key is not 64 bytes wide"); char[] kPrefix_c = kPrefix.toCharArray(); char[] kSuffix_c = kSuffix.toCharArray(); CharBuffer cb = CharBuffer.allocate(kPrefix_c.length + kSuffix_c.length + key_chars.length); cb.put(kPrefix_c); cb.put(key_chars); cb.put(kSuffix_c); return cb.array(); } /** * @see #encodeRawKey(byte[]) */ public static String encodeRawKeyToStr(byte[] raw_key) { return new String(encodeRawKey(raw_key)); } /* * Special hack for detecting whether or not we're using a new SQLCipher for * Android library The old version uses the PRAGMA to set the key, which * requires escaping of the single quote characters. The new version calls a * native method to set the key instead. * @see https://github.com/sqlcipher/android-database-sqlcipher/pull/95 */ private static final boolean sqlcipher_uses_native_key = check_sqlcipher_uses_native_key(); private static boolean check_sqlcipher_uses_native_key() { for (Method method : SQLiteDatabase.class.getDeclaredMethods()) { if (method.getName().equals("native_key")) return true; } return false; } private static final char[] HEX_DIGITS_LOWER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; private static char[] encodeHex(final byte[] data, final char[] toDigits) { final int l = data.length; final char[] out = new char[l << 1]; // two characters form the hex value. for (int i = 0, j = 0; i < l; i++) { out[j++] = toDigits[(0xF0 & data[i]) >>> 4]; out[j++] = toDigits[0x0F & data[i]]; } return out; } }