persist certificate trust to disk
This commit is contained in:
parent
177320d8fe
commit
0c4771e2a8
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "5152f8eab684376f6f4076cf392e22d7",
|
||||
"identityHash": "bc04f3d0c58f7e50f5c7973a7a06c9eb",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "account",
|
||||
|
@ -946,6 +946,67 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "certificate_trust",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `scope` TEXT NOT NULL, `fingerprint` BLOB NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scope",
|
||||
"columnName": "scope",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "fingerprint",
|
||||
"columnName": "fingerprint",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_certificate_trust_accountId_scope",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"accountId",
|
||||
"scope"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_certificate_trust_accountId_scope` ON `${TABLE_NAME}` (`accountId`, `scope`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "account",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"accountId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "chat",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `type` TEXT, `archived` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
|
@ -2376,7 +2437,7 @@
|
|||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5152f8eab684376f6f4076cf392e22d7')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bc04f3d0c58f7e50f5c7973a7a06c9eb')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import im.conversations.android.database.dao.AvatarDao;
|
|||
import im.conversations.android.database.dao.AxolotlDao;
|
||||
import im.conversations.android.database.dao.BlockingDao;
|
||||
import im.conversations.android.database.dao.BookmarkDao;
|
||||
import im.conversations.android.database.dao.CertificateTrustDao;
|
||||
import im.conversations.android.database.dao.ChatDao;
|
||||
import im.conversations.android.database.dao.DiscoDao;
|
||||
import im.conversations.android.database.dao.MessageDao;
|
||||
|
@ -29,6 +30,7 @@ import im.conversations.android.database.entity.AxolotlSignedPreKeyEntity;
|
|||
import im.conversations.android.database.entity.BlockedItemEntity;
|
||||
import im.conversations.android.database.entity.BookmarkEntity;
|
||||
import im.conversations.android.database.entity.BookmarkGroupEntity;
|
||||
import im.conversations.android.database.entity.CertificateTrustEntity;
|
||||
import im.conversations.android.database.entity.ChatEntity;
|
||||
import im.conversations.android.database.entity.DiscoEntity;
|
||||
import im.conversations.android.database.entity.DiscoExtensionEntity;
|
||||
|
@ -63,6 +65,7 @@ import im.conversations.android.database.entity.RosterItemGroupEntity;
|
|||
BlockedItemEntity.class,
|
||||
BookmarkEntity.class,
|
||||
BookmarkGroupEntity.class,
|
||||
CertificateTrustEntity.class,
|
||||
ChatEntity.class,
|
||||
DiscoEntity.class,
|
||||
DiscoExtensionEntity.class,
|
||||
|
@ -114,6 +117,8 @@ public abstract class ConversationsDatabase extends RoomDatabase {
|
|||
|
||||
public abstract BookmarkDao bookmarkDao();
|
||||
|
||||
public abstract CertificateTrustDao certificateTrustDao();
|
||||
|
||||
public abstract ChatDao chatDao();
|
||||
|
||||
public abstract DiscoDao discoDao();
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package im.conversations.android.database.dao;
|
||||
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
import im.conversations.android.database.entity.CertificateTrustEntity;
|
||||
import im.conversations.android.database.model.Account;
|
||||
import im.conversations.android.tls.ScopeFingerprint;
|
||||
|
||||
@Dao
|
||||
public abstract class CertificateTrustDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
public abstract void insert(final CertificateTrustEntity certificateTrustEntity);
|
||||
|
||||
@Query(
|
||||
"SELECT EXISTS (SELECT id FROM certificate_trust WHERE accountId=:account AND"
|
||||
+ " scope=:scope AND fingerprint=:fingerprint)")
|
||||
protected abstract boolean isTrusted(
|
||||
final long account, final String scope, final byte[] fingerprint);
|
||||
|
||||
public boolean isTrusted(final Account account, final ScopeFingerprint scopeFingerprint) {
|
||||
return isTrusted(account.id, scopeFingerprint.scope, scopeFingerprint.fingerprint.array());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package im.conversations.android.database.entity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
import im.conversations.android.tls.ScopeFingerprint;
|
||||
|
||||
@Entity(
|
||||
tableName = "certificate_trust",
|
||||
foreignKeys =
|
||||
@ForeignKey(
|
||||
entity = AccountEntity.class,
|
||||
parentColumns = {"id"},
|
||||
childColumns = {"accountId"},
|
||||
onDelete = ForeignKey.CASCADE),
|
||||
indices = {
|
||||
@Index(
|
||||
value = {"accountId", "scope"},
|
||||
unique = true)
|
||||
})
|
||||
public class CertificateTrustEntity {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
public Long id;
|
||||
|
||||
@NonNull public Long accountId;
|
||||
|
||||
@NonNull public String scope;
|
||||
|
||||
@NonNull public byte[] fingerprint;
|
||||
|
||||
public static CertificateTrustEntity of(
|
||||
final long accountId, final ScopeFingerprint scopeFingerprint) {
|
||||
final var entity = new CertificateTrustEntity();
|
||||
entity.accountId = accountId;
|
||||
entity.scope = scopeFingerprint.scope;
|
||||
entity.fingerprint = scopeFingerprint.fingerprint.array();
|
||||
return entity;
|
||||
}
|
||||
}
|
|
@ -9,9 +9,11 @@ import com.google.common.util.concurrent.MoreExecutors;
|
|||
import im.conversations.android.IDs;
|
||||
import im.conversations.android.database.CredentialStore;
|
||||
import im.conversations.android.database.entity.AccountEntity;
|
||||
import im.conversations.android.database.entity.CertificateTrustEntity;
|
||||
import im.conversations.android.database.model.Account;
|
||||
import im.conversations.android.database.model.AccountIdentifier;
|
||||
import im.conversations.android.database.model.Connection;
|
||||
import im.conversations.android.tls.ScopeFingerprint;
|
||||
import im.conversations.android.xmpp.ConnectionPool;
|
||||
import im.conversations.android.xmpp.XmppConnection;
|
||||
import im.conversations.android.xmpp.manager.RegistrationManager;
|
||||
|
@ -131,6 +133,20 @@ public class AccountRepository extends AbstractRepository {
|
|||
return database.accountDao().hasNoAccounts();
|
||||
}
|
||||
|
||||
public ListenableFuture<Void> setCertificateTrustedAsync(
|
||||
final Account account, final ScopeFingerprint scopeFingerprint) {
|
||||
return Futures.submit(
|
||||
() -> setCertificateTrusted(account, scopeFingerprint),
|
||||
database.getQueryExecutor());
|
||||
}
|
||||
|
||||
private void setCertificateTrusted(
|
||||
final Account account, final ScopeFingerprint scopeFingerprint) {
|
||||
this.database
|
||||
.certificateTrustDao()
|
||||
.insert(CertificateTrustEntity.of(account.id, scopeFingerprint));
|
||||
}
|
||||
|
||||
public static class AccountAlreadyExistsException extends IllegalStateException {
|
||||
public AccountAlreadyExistsException(BareJid address) {
|
||||
super(String.format("The account %s has already been setup", address));
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package im.conversations.android.tls;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import im.conversations.android.xmpp.manager.TrustManager;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ScopeFingerprint {
|
||||
public final String scope;
|
||||
public final ByteBuffer fingerprint;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("scope", scope)
|
||||
.add("fingerprint", TrustManager.fingerprint(fingerprint.array()))
|
||||
.toString();
|
||||
}
|
||||
|
||||
public ScopeFingerprint(final String scope, final byte[] fingerprint) {
|
||||
this(scope, ByteBuffer.wrap(fingerprint));
|
||||
}
|
||||
|
||||
public ScopeFingerprint(final String scope, final ByteBuffer fingerprint) {
|
||||
this.scope = scope;
|
||||
this.fingerprint = fingerprint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ScopeFingerprint that = (ScopeFingerprint) o;
|
||||
return Objects.equal(scope, that.scope) && Objects.equal(fingerprint, that.fingerprint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(scope, fingerprint);
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import im.conversations.android.R;
|
|||
import im.conversations.android.database.model.Account;
|
||||
import im.conversations.android.database.model.Connection;
|
||||
import im.conversations.android.repository.AccountRepository;
|
||||
import im.conversations.android.tls.ScopeFingerprint;
|
||||
import im.conversations.android.ui.Event;
|
||||
import im.conversations.android.util.ConnectionStates;
|
||||
import im.conversations.android.xmpp.ConnectionException;
|
||||
|
@ -58,28 +59,26 @@ public class SetupViewModel extends AndroidViewModel {
|
|||
private final MutableLiveData<Event<Target>> redirection = new MutableLiveData<>();
|
||||
|
||||
private final MutableLiveData<TrustDecision> trustDecision = new MutableLiveData<>();
|
||||
private final HashMap<TrustManager.ScopeFingerprint, Boolean> trustDecisions = new HashMap<>();
|
||||
private final HashMap<ScopeFingerprint, Boolean> trustDecisions = new HashMap<>();
|
||||
|
||||
private final Function<TrustManager.ScopeFingerprint, ListenableFuture<Boolean>>
|
||||
trustDecisionCallback =
|
||||
scopeFingerprint -> {
|
||||
final var decision = this.trustDecisions.get(scopeFingerprint);
|
||||
if (decision != null) {
|
||||
LOGGER.info("Using previous trust decision ({})", decision);
|
||||
return Futures.immediateFuture(decision);
|
||||
}
|
||||
LOGGER.info("Trust decision arrived in UI");
|
||||
final SettableFuture<Boolean> settableFuture = SettableFuture.create();
|
||||
final var trustDecision =
|
||||
new TrustDecision(scopeFingerprint, settableFuture);
|
||||
final var currentOperation = this.currentOperation;
|
||||
if (currentOperation != null) {
|
||||
currentOperation.cancel(false);
|
||||
}
|
||||
this.trustDecision.postValue(trustDecision);
|
||||
this.redirection.postValue(new Event<>(Target.TRUST_CERTIFICATE));
|
||||
return settableFuture;
|
||||
};
|
||||
private final Function<ScopeFingerprint, ListenableFuture<Boolean>> trustDecisionCallback =
|
||||
scopeFingerprint -> {
|
||||
final var decision = this.trustDecisions.get(scopeFingerprint);
|
||||
if (decision != null) {
|
||||
LOGGER.info("Using previous trust decision ({})", decision);
|
||||
return Futures.immediateFuture(decision);
|
||||
}
|
||||
LOGGER.info("Trust decision arrived in UI");
|
||||
final SettableFuture<Boolean> settableFuture = SettableFuture.create();
|
||||
final var trustDecision = new TrustDecision(scopeFingerprint, settableFuture);
|
||||
final var currentOperation = this.currentOperation;
|
||||
if (currentOperation != null) {
|
||||
currentOperation.cancel(false);
|
||||
}
|
||||
this.trustDecision.postValue(trustDecision);
|
||||
this.redirection.postValue(new Event<>(Target.TRUST_CERTIFICATE));
|
||||
return settableFuture;
|
||||
};
|
||||
|
||||
private final AccountRepository accountRepository;
|
||||
|
||||
|
@ -189,13 +188,12 @@ public class SetupViewModel extends AndroidViewModel {
|
|||
// TODO navigate back to sign in or show error?
|
||||
return true;
|
||||
}
|
||||
LOGGER.info(
|
||||
"trying to commit trust for fingerprint {}",
|
||||
TrustManager.fingerprint(trustDecision.scopeFingerprint.fingerprint.array()));
|
||||
LOGGER.info("committing trust for {}", trustDecision.scopeFingerprint);
|
||||
this.accountRepository.setCertificateTrustedAsync(account, trustDecision.scopeFingerprint);
|
||||
// in case the UI interface hook gets called again before this gets written to DB
|
||||
this.trustDecisions.put(trustDecision.scopeFingerprint, true);
|
||||
if (trustDecision.decision.isDone()) {
|
||||
ConnectionPool.getInstance(getApplication()).reconnect(account);
|
||||
this.accountRepository.reconnect(account);
|
||||
LOGGER.info("it was already done. we should reconnect");
|
||||
}
|
||||
trustDecision.decision.set(true);
|
||||
|
@ -265,6 +263,7 @@ public class SetupViewModel extends AndroidViewModel {
|
|||
private void setAccount(@NonNull final Account account) {
|
||||
this.account = account;
|
||||
this.registerTrustDecisionCallback();
|
||||
// TODO if the connection is already TLS_ERROR then do a quick reconnect
|
||||
this.decideNextStep(Target.ENTER_ADDRESS, account);
|
||||
}
|
||||
|
||||
|
@ -282,6 +281,7 @@ public class SetupViewModel extends AndroidViewModel {
|
|||
final var optionalTrustManager = getTrustManager();
|
||||
if (optionalTrustManager.isPresent()) {
|
||||
optionalTrustManager.get().setUserInterfaceCallback(this.trustDecisionCallback);
|
||||
LOGGER.info("Registered user interface callback");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -473,11 +473,10 @@ public class SetupViewModel extends AndroidViewModel {
|
|||
}
|
||||
|
||||
public static class TrustDecision {
|
||||
public final TrustManager.ScopeFingerprint scopeFingerprint;
|
||||
public final ScopeFingerprint scopeFingerprint;
|
||||
public final SettableFuture<Boolean> decision;
|
||||
|
||||
public TrustDecision(
|
||||
TrustManager.ScopeFingerprint scopeFingerprint, SettableFuture<Boolean> decision) {
|
||||
public TrustDecision(ScopeFingerprint scopeFingerprint, SettableFuture<Boolean> decision) {
|
||||
this.scopeFingerprint = scopeFingerprint;
|
||||
this.decision = decision;
|
||||
}
|
||||
|
|
|
@ -3,16 +3,15 @@ package im.conversations.android.xmpp.manager;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.hash.Hashing;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import im.conversations.android.AppSettings;
|
||||
import im.conversations.android.tls.ScopeFingerprint;
|
||||
import im.conversations.android.tls.TrustManagers;
|
||||
import im.conversations.android.xmpp.XmppConnection;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
@ -73,7 +72,6 @@ public class TrustManager extends AbstractManager {
|
|||
public void removeUserInterfaceCallback(
|
||||
final Function<ScopeFingerprint, ListenableFuture<Boolean>> callback) {
|
||||
if (this.userInterfaceCallback == callback) {
|
||||
LOGGER.info("Remove user interface callback");
|
||||
this.userInterfaceCallback = null;
|
||||
}
|
||||
}
|
||||
|
@ -82,33 +80,6 @@ public class TrustManager extends AbstractManager {
|
|||
return new ScopedTrustManager(scope);
|
||||
}
|
||||
|
||||
public static class ScopeFingerprint {
|
||||
public final String scope;
|
||||
public final ByteBuffer fingerprint;
|
||||
|
||||
public ScopeFingerprint(final String scope, final byte[] fingerprint) {
|
||||
this(scope, ByteBuffer.wrap(fingerprint));
|
||||
}
|
||||
|
||||
public ScopeFingerprint(final String scope, final ByteBuffer fingerprint) {
|
||||
this.scope = scope;
|
||||
this.fingerprint = fingerprint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ScopeFingerprint that = (ScopeFingerprint) o;
|
||||
return Objects.equal(scope, that.scope) && Objects.equal(fingerprint, that.fingerprint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(scope, fingerprint);
|
||||
}
|
||||
}
|
||||
|
||||
private class ScopedTrustManager implements X509TrustManager {
|
||||
|
||||
private final String scope;
|
||||
|
@ -141,7 +112,10 @@ public class TrustManager extends AbstractManager {
|
|||
final byte[] fingerprint =
|
||||
Hashing.sha256().hashBytes(certificate.getEncoded()).asBytes();
|
||||
final var scopeFingerprint = new ScopeFingerprint(scope, fingerprint);
|
||||
LOGGER.info("Looking up {} in database", fingerprint(fingerprint));
|
||||
if (getDatabase().certificateTrustDao().isTrusted(getAccount(), scopeFingerprint)) {
|
||||
LOGGER.info("Found {} in database", scopeFingerprint);
|
||||
return;
|
||||
}
|
||||
final var callback = TrustManager.this.userInterfaceCallback;
|
||||
if (callback == null) {
|
||||
throw new CertificateException(
|
||||
|
|
Loading…
Reference in a new issue