/* * Copyright (C) 2011 Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.thoughtcrime.securesms.database; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import net.sqlcipher.database.SQLiteDatabase; import org.greenrobot.eventbus.EventBus; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.util.Base64; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.guava.Optional; import java.io.IOException; public class IdentityDatabase extends Database { @SuppressWarnings("unused") private static final String TAG = IdentityDatabase.class.getSimpleName(); private static final String TABLE_NAME = "identities"; private static final String ID = "_id"; private static final String ADDRESS = "address"; private static final String IDENTITY_KEY = "key"; private static final String TIMESTAMP = "timestamp"; private static final String FIRST_USE = "first_use"; private static final String NONBLOCKING_APPROVAL = "nonblocking_approval"; private static final String VERIFIED = "verified"; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + ADDRESS + " TEXT UNIQUE, " + IDENTITY_KEY + " TEXT, " + FIRST_USE + " INTEGER DEFAULT 0, " + TIMESTAMP + " INTEGER DEFAULT 0, " + VERIFIED + " INTEGER DEFAULT 0, " + NONBLOCKING_APPROVAL + " INTEGER DEFAULT 0);"; public enum VerifiedStatus { DEFAULT, VERIFIED, UNVERIFIED; public int toInt() { if (this == DEFAULT) return 0; else if (this == VERIFIED) return 1; else if (this == UNVERIFIED) return 2; else throw new AssertionError(); } public static VerifiedStatus forState(int state) { if (state == 0) return DEFAULT; else if (state == 1) return VERIFIED; else if (state == 2) return UNVERIFIED; else throw new AssertionError("No such state: " + state); } } IdentityDatabase(Context context, SQLCipherOpenHelper databaseHelper) { super(context, databaseHelper); } public Cursor getIdentities() { SQLiteDatabase database = databaseHelper.getReadableDatabase(); return database.query(TABLE_NAME, null, null, null, null, null, null); } public @Nullable IdentityReader readerFor(@Nullable Cursor cursor) { if (cursor == null) return null; return new IdentityReader(cursor); } public Optional getIdentity(Address address) { SQLiteDatabase database = databaseHelper.getReadableDatabase(); Cursor cursor = null; try { cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[] {address.serialize()}, null, null, null); if (cursor != null && cursor.moveToFirst()) { return Optional.of(getIdentityRecord(cursor)); } } catch (InvalidKeyException | IOException e) { throw new AssertionError(e); } finally { if (cursor != null) cursor.close(); } return Optional.absent(); } public void saveIdentity(Address address, IdentityKey identityKey, VerifiedStatus verifiedStatus, boolean firstUse, long timestamp, boolean nonBlockingApproval) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); String identityKeyString = Base64.encodeBytes(identityKey.serialize()); ContentValues contentValues = new ContentValues(); contentValues.put(ADDRESS, address.serialize()); contentValues.put(IDENTITY_KEY, identityKeyString); contentValues.put(TIMESTAMP, timestamp); contentValues.put(VERIFIED, verifiedStatus.toInt()); contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval ? 1 : 0); contentValues.put(FIRST_USE, firstUse ? 1 : 0); database.replace(TABLE_NAME, null, contentValues); EventBus.getDefault().post(new IdentityRecord(address, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval)); } public void setApproval(Address address, boolean nonBlockingApproval) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues contentValues = new ContentValues(2); contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval); database.update(TABLE_NAME, contentValues, ADDRESS + " = ?", new String[] {address.serialize()}); } public void setVerified(Address address, IdentityKey identityKey, VerifiedStatus verifiedStatus) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues contentValues = new ContentValues(1); contentValues.put(VERIFIED, verifiedStatus.toInt()); int updated = database.update(TABLE_NAME, contentValues, ADDRESS + " = ? AND " + IDENTITY_KEY + " = ?", new String[] {address.serialize(), Base64.encodeBytes(identityKey.serialize())}); if (updated > 0) { Optional record = getIdentity(address); if (record.isPresent()) EventBus.getDefault().post(record.get()); } } private IdentityRecord getIdentityRecord(@NonNull Cursor cursor) throws IOException, InvalidKeyException { String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)); String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY)); long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)); int verifiedStatus = cursor.getInt(cursor.getColumnIndexOrThrow(VERIFIED)); boolean nonblockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(NONBLOCKING_APPROVAL)) == 1; boolean firstUse = cursor.getInt(cursor.getColumnIndexOrThrow(FIRST_USE)) == 1; IdentityKey identity = new IdentityKey(Base64.decode(serializedIdentity), 0); return new IdentityRecord(Address.fromSerialized(address), identity, VerifiedStatus.forState(verifiedStatus), firstUse, timestamp, nonblockingApproval); } public static class IdentityRecord { private final Address address; private final IdentityKey identitykey; private final VerifiedStatus verifiedStatus; private final boolean firstUse; private final long timestamp; private final boolean nonblockingApproval; private IdentityRecord(Address address, IdentityKey identitykey, VerifiedStatus verifiedStatus, boolean firstUse, long timestamp, boolean nonblockingApproval) { this.address = address; this.identitykey = identitykey; this.verifiedStatus = verifiedStatus; this.firstUse = firstUse; this.timestamp = timestamp; this.nonblockingApproval = nonblockingApproval; } public Address getAddress() { return address; } public IdentityKey getIdentityKey() { return identitykey; } public long getTimestamp() { return timestamp; } public VerifiedStatus getVerifiedStatus() { return verifiedStatus; } public boolean isApprovedNonBlocking() { return nonblockingApproval; } public boolean isFirstUse() { return firstUse; } @Override public String toString() { return "{address: " + address + ", identityKey: " + identitykey + ", verifiedStatus: " + verifiedStatus + ", firstUse: " + firstUse + "}"; } } public class IdentityReader { private final Cursor cursor; IdentityReader(@NonNull Cursor cursor) { this.cursor = cursor; } public @Nullable IdentityRecord getNext() { if (cursor.moveToNext()) { try { return getIdentityRecord(cursor); } catch (IOException | InvalidKeyException e) { throw new AssertionError(e); } } return null; } public void close() { cursor.close(); } } }