diff options
Diffstat (limited to 'src/main')
53 files changed, 1722 insertions, 1373 deletions
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index c6f74538a..255939a4f 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -6,28 +6,15 @@ import android.util.Log; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.whispersystems.libaxolotl.AxolotlAddress; -import org.whispersystems.libaxolotl.DuplicateMessageException; import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.IdentityKeyPair; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidKeyIdException; -import org.whispersystems.libaxolotl.InvalidMessageException; -import org.whispersystems.libaxolotl.InvalidVersionException; -import org.whispersystems.libaxolotl.LegacyMessageException; -import org.whispersystems.libaxolotl.NoSessionException; import org.whispersystems.libaxolotl.SessionBuilder; -import org.whispersystems.libaxolotl.SessionCipher; import org.whispersystems.libaxolotl.UntrustedIdentityException; -import org.whispersystems.libaxolotl.ecc.Curve; -import org.whispersystems.libaxolotl.ecc.ECKeyPair; import org.whispersystems.libaxolotl.ecc.ECPublicKey; -import org.whispersystems.libaxolotl.protocol.CiphertextMessage; -import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; -import org.whispersystems.libaxolotl.protocol.WhisperMessage; -import org.whispersystems.libaxolotl.state.AxolotlStore; import org.whispersystems.libaxolotl.state.PreKeyBundle; import org.whispersystems.libaxolotl.state.PreKeyRecord; -import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import org.whispersystems.libaxolotl.util.KeyHelper; @@ -73,547 +60,6 @@ public class AxolotlService { private final FetchStatusMap fetchStatusMap; private final SerialSingleThreadExecutor executor; - public static class SQLiteAxolotlStore implements AxolotlStore { - - public static final String PREKEY_TABLENAME = "prekeys"; - public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; - public static final String SESSION_TABLENAME = "sessions"; - public static final String IDENTITIES_TABLENAME = "identities"; - public static final String ACCOUNT = "account"; - public static final String DEVICE_ID = "device_id"; - public static final String ID = "id"; - public static final String KEY = "key"; - public static final String FINGERPRINT = "fingerprint"; - public static final String NAME = "name"; - public static final String TRUSTED = "trusted"; - public static final String OWN = "ownkey"; - - public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; - public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; - - private final Account account; - private final XmppConnectionService mXmppConnectionService; - - private IdentityKeyPair identityKeyPair; - private int localRegistrationId; - private int currentPreKeyId = 0; - - public enum Trust { - UNDECIDED(0), - TRUSTED(1), - UNTRUSTED(2), - COMPROMISED(3), - INACTIVE(4); - - private static final Map<Integer, Trust> trustsByValue = new HashMap<>(); - - static { - for (Trust trust : Trust.values()) { - trustsByValue.put(trust.getCode(), trust); - } - } - - private final int code; - - Trust(int code){ - this.code = code; - } - - public int getCode() { - return this.code; - } - - public String toString() { - switch(this){ - case UNDECIDED: - return "Trust undecided "+getCode(); - case TRUSTED: - return "Trusted "+getCode(); - case COMPROMISED: - return "Compromised "+getCode(); - case INACTIVE: - return "Inactive "+getCode(); - case UNTRUSTED: - default: - return "Untrusted "+getCode(); - } - } - - public static Trust fromBoolean(Boolean trusted) { - return trusted?TRUSTED:UNTRUSTED; - } - - public static Trust fromCode(int code) { - return trustsByValue.get(code); - } - }; - - private static IdentityKeyPair generateIdentityKeyPair() { - Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl IdentityKeyPair..."); - ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); - IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), - identityKeyPairKeys.getPrivateKey()); - return ownKey; - } - - private static int generateRegistrationId() { - Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl registration ID..."); - int reg_id = KeyHelper.generateRegistrationId(true); - return reg_id; - } - - public SQLiteAxolotlStore(Account account, XmppConnectionService service) { - this.account = account; - this.mXmppConnectionService = service; - this.localRegistrationId = loadRegistrationId(); - this.currentPreKeyId = loadCurrentPreKeyId(); - for (SignedPreKeyRecord record : loadSignedPreKeys()) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got Axolotl signed prekey record:" + record.getId()); - } - } - - public int getCurrentPreKeyId() { - return currentPreKeyId; - } - - // -------------------------------------- - // IdentityKeyStore - // -------------------------------------- - - private IdentityKeyPair loadIdentityKeyPair() { - String ownName = account.getJid().toBareJid().toString(); - IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account, - ownName); - - if (ownKey != null) { - return ownKey; - } else { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl key for account " + ownName); - ownKey = generateIdentityKeyPair(); - mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey); - } - return ownKey; - } - - private int loadRegistrationId() { - return loadRegistrationId(false); - } - - private int loadRegistrationId(boolean regenerate) { - String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); - int reg_id; - if (!regenerate && regIdString != null) { - reg_id = Integer.valueOf(regIdString); - } else { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl registration id for account " + account.getJid()); - reg_id = generateRegistrationId(); - boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); - if (success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to write new key to the database!"); - } - } - return reg_id; - } - - private int loadCurrentPreKeyId() { - String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); - int reg_id; - if (regIdString != null) { - reg_id = Integer.valueOf(regIdString); - } else { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve current prekey id for account " + account.getJid()); - reg_id = 0; - } - return reg_id; - } - - public void regenerate() { - mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); - account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0)); - identityKeyPair = loadIdentityKeyPair(); - localRegistrationId = loadRegistrationId(true); - currentPreKeyId = 0; - mXmppConnectionService.updateAccountUi(); - } - - /** - * Get the local client's identity key pair. - * - * @return The local client's persistent identity key pair. - */ - @Override - public IdentityKeyPair getIdentityKeyPair() { - if(identityKeyPair == null) { - identityKeyPair = loadIdentityKeyPair(); - } - return identityKeyPair; - } - - /** - * Return the local client's registration ID. - * <p/> - * Clients should maintain a registration ID, a random number - * between 1 and 16380 that's generated once at install time. - * - * @return the local client's registration ID. - */ - @Override - public int getLocalRegistrationId() { - return localRegistrationId; - } - - /** - * Save a remote client's identity key - * <p/> - * Store a remote client's identity key as trusted. - * - * @param name The name of the remote client. - * @param identityKey The remote client's identity key. - */ - @Override - public void saveIdentity(String name, IdentityKey identityKey) { - if(!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) { - mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey); - } - } - - /** - * Verify a remote client's identity key. - * <p/> - * Determine whether a remote client's identity is trusted. Convention is - * that the TextSecure protocol is 'trust on first use.' This means that - * an identity key is considered 'trusted' if there is no entry for the recipient - * in the local store, or if it matches the saved key for a recipient in the local - * store. Only if it mismatches an entry in the local store is it considered - * 'untrusted.' - * - * @param name The name of the remote client. - * @param identityKey The identity key to verify. - * @return true if trusted, false if untrusted. - */ - @Override - public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - return true; - } - - public Trust getFingerprintTrust(String fingerprint) { - return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint); - } - - public void setFingerprintTrust(String fingerprint, Trust trust) { - mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust); - } - - public Set<IdentityKey> getContactUndecidedKeys(String bareJid, Trust trust) { - return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust); - } - - public long getContactNumTrustedKeys(String bareJid) { - return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid); - } - - // -------------------------------------- - // SessionStore - // -------------------------------------- - - /** - * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple, - * or a new SessionRecord if one does not currently exist. - * <p/> - * It is important that implementations return a copy of the current durable information. The - * returned SessionRecord may be modified, but those changes should not have an effect on the - * durable session state (what is returned by subsequent calls to this method) without the - * store method being called here first. - * - * @param address The name and device ID of the remote client. - * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or - * a new SessionRecord if one does not currently exist. - */ - @Override - public SessionRecord loadSession(AxolotlAddress address) { - SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); - return (session != null) ? session : new SessionRecord(); - } - - /** - * Returns all known devices with active sessions for a recipient - * - * @param name the name of the client. - * @return all known sub-devices with active sessions. - */ - @Override - public List<Integer> getSubDeviceSessions(String name) { - return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, - new AxolotlAddress(name, 0)); - } - - /** - * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @param record the current SessionRecord for the remote client. - */ - @Override - public void storeSession(AxolotlAddress address, SessionRecord record) { - mXmppConnectionService.databaseBackend.storeSession(account, address, record); - } - - /** - * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @return true if a {@link SessionRecord} exists, false otherwise. - */ - @Override - public boolean containsSession(AxolotlAddress address) { - return mXmppConnectionService.databaseBackend.containsSession(account, address); - } - - /** - * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - */ - @Override - public void deleteSession(AxolotlAddress address) { - mXmppConnectionService.databaseBackend.deleteSession(account, address); - } - - /** - * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. - * - * @param name the name of the remote client. - */ - @Override - public void deleteAllSessions(String name) { - mXmppConnectionService.databaseBackend.deleteAllSessions(account, - new AxolotlAddress(name, 0)); - } - - // -------------------------------------- - // PreKeyStore - // -------------------------------------- - - /** - * Load a local PreKeyRecord. - * - * @param preKeyId the ID of the local PreKeyRecord. - * @return the corresponding PreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. - */ - @Override - public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { - PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); - if (record == null) { - throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); - } - return record; - } - - /** - * Store a local PreKeyRecord. - * - * @param preKeyId the ID of the PreKeyRecord to store. - * @param record the PreKeyRecord. - */ - @Override - public void storePreKey(int preKeyId, PreKeyRecord record) { - mXmppConnectionService.databaseBackend.storePreKey(account, record); - currentPreKeyId = preKeyId; - boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId)); - if (success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to write new prekey id to the database!"); - } - } - - /** - * @param preKeyId A PreKeyRecord ID. - * @return true if the store has a record for the preKeyId, otherwise false. - */ - @Override - public boolean containsPreKey(int preKeyId) { - return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); - } - - /** - * Delete a PreKeyRecord from local storage. - * - * @param preKeyId The ID of the PreKeyRecord to remove. - */ - @Override - public void removePreKey(int preKeyId) { - mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); - } - - // -------------------------------------- - // SignedPreKeyStore - // -------------------------------------- - - /** - * Load a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the local SignedPreKeyRecord. - * @return the corresponding SignedPreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. - */ - @Override - public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { - SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); - if (record == null) { - throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); - } - return record; - } - - /** - * Load all local SignedPreKeyRecords. - * - * @return All stored SignedPreKeyRecords. - */ - @Override - public List<SignedPreKeyRecord> loadSignedPreKeys() { - return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); - } - - /** - * Store a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. - * @param record the SignedPreKeyRecord. - */ - @Override - public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { - mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); - } - - /** - * @param signedPreKeyId A SignedPreKeyRecord ID. - * @return true if the store has a record for the signedPreKeyId, otherwise false. - */ - @Override - public boolean containsSignedPreKey(int signedPreKeyId) { - return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); - } - - /** - * Delete a SignedPreKeyRecord from local storage. - * - * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. - */ - @Override - public void removeSignedPreKey(int signedPreKeyId) { - mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); - } - } - - public static class XmppAxolotlSession { - private final SessionCipher cipher; - private Integer preKeyId = null; - private final SQLiteAxolotlStore sqLiteAxolotlStore; - private final AxolotlAddress remoteAddress; - private final Account account; - private String fingerprint = null; - - public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, String fingerprint) { - this(account, store, remoteAddress); - this.fingerprint = fingerprint; - } - - public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { - this.cipher = new SessionCipher(store, remoteAddress); - this.remoteAddress = remoteAddress; - this.sqLiteAxolotlStore = store; - this.account = account; - } - - public Integer getPreKeyId() { - return preKeyId; - } - - public void resetPreKeyId() { - - preKeyId = null; - } - - public String getFingerprint() { - return fingerprint; - } - - protected void setTrust(SQLiteAxolotlStore.Trust trust) { - sqLiteAxolotlStore.setFingerprintTrust(fingerprint, trust); - } - - protected SQLiteAxolotlStore.Trust getTrust() { - return sqLiteAxolotlStore.getFingerprintTrust(fingerprint); - } - - @Nullable - public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) { - byte[] plaintext = null; - SQLiteAxolotlStore.Trust trust = getTrust(); - switch (trust) { - case INACTIVE: - case UNDECIDED: - case UNTRUSTED: - case TRUSTED: - try { - try { - PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); - String fingerprint = message.getIdentityKey().getFingerprint().replaceAll("\\s", ""); - if (this.fingerprint != null && !this.fingerprint.equals(fingerprint)) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Had session with fingerprint "+ this.fingerprint+", received message with fingerprint "+fingerprint); - } else { - this.fingerprint = fingerprint; - plaintext = cipher.decrypt(message); - if (message.getPreKeyId().isPresent()) { - preKeyId = message.getPreKeyId().get(); - } - } - } catch (InvalidMessageException | InvalidVersionException e) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"WhisperMessage received"); - WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); - plaintext = cipher.decrypt(message); - } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); - } - } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); - } - - if (plaintext != null && trust == SQLiteAxolotlStore.Trust.INACTIVE) { - setTrust(SQLiteAxolotlStore.Trust.TRUSTED); - } - - break; - - case COMPROMISED: - default: - // ignore - break; - } - return plaintext; - } - - @Nullable - public XmppAxolotlMessage.XmppAxolotlMessageHeader processSending(@NonNull byte[] outgoingMessage) { - SQLiteAxolotlStore.Trust trust = getTrust(); - if (trust == SQLiteAxolotlStore.Trust.TRUSTED) { - CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); - XmppAxolotlMessage.XmppAxolotlMessageHeader header = - new XmppAxolotlMessage.XmppAxolotlMessageHeader(remoteAddress.getDeviceId(), - ciphertextMessage.serialize()); - return header; - } else { - return null; - } - } - } - private static class AxolotlAddressMap<T> { protected Map<String, Map<Integer, T>> map; protected final Object MAP_LOCK = new Object(); @@ -680,7 +126,7 @@ public class AxolotlService { private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) { for (Integer deviceId : deviceIds) { AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building session for remote address: "+axolotlAddress.toString()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString()); String fingerprint = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey().getFingerprint().replaceAll("\\s", ""); this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, fingerprint)); } @@ -704,8 +150,13 @@ public class AxolotlService { @Override public void put(AxolotlAddress address, XmppAxolotlSession value) { super.put(address, value); + value.setNotFresh(); xmppConnectionService.syncRosterToDisk(account); } + + public void put(XmppAxolotlSession session) { + this.put(session.getRemoteAddress(), session); + } } private static enum FetchStatus { @@ -717,9 +168,9 @@ public class AxolotlService { private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> { } - + public static String getLogprefix(Account account) { - return LOGPREFIX+" ("+account.getJid().toBareJid().toString()+"): "; + return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): "; } public AxolotlService(Account account, XmppConnectionService connectionService) { @@ -740,12 +191,12 @@ public class AxolotlService { return axolotlStore.getIdentityKeyPair().getPublicKey(); } - public Set<IdentityKey> getKeysWithTrust(SQLiteAxolotlStore.Trust trust) { - return axolotlStore.getContactUndecidedKeys(account.getJid().toBareJid().toString(), trust); + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) { + return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust); } - public Set<IdentityKey> getKeysWithTrust(SQLiteAxolotlStore.Trust trust, Contact contact) { - return axolotlStore.getContactUndecidedKeys(contact.getJid().toBareJid().toString(), trust); + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact) { + return axolotlStore.getContactKeysWithTrust(contact.getJid().toBareJid().toString(), trust); } public long getNumTrustedKeys(Contact contact) { @@ -782,7 +233,7 @@ public class AxolotlService { } public int getOwnDeviceId() { - return axolotlStore.loadRegistrationId(); + return axolotlStore.getLocalRegistrationId(); } public Set<Integer> getOwnDeviceIds() { @@ -790,9 +241,9 @@ public class AxolotlService { } private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds, - final SQLiteAxolotlStore.Trust from, - final SQLiteAxolotlStore.Trust to) { - for(Integer deviceId:deviceIds) { + final XmppAxolotlSession.Trust from, + final XmppAxolotlSession.Trust to) { + for (Integer deviceId : deviceIds) { AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId); XmppAxolotlSession session = sessions.get(address); if (session != null && session.getFingerprint() != null @@ -803,24 +254,32 @@ public class AxolotlService { } public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) { - if(jid.toBareJid().equals(account.getJid().toBareJid())) { + if (jid.toBareJid().equals(account.getJid().toBareJid())) { if (deviceIds.contains(getOwnDeviceId())) { deviceIds.remove(getOwnDeviceId()); } - for(Integer deviceId : deviceIds) { - AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(),deviceId); - if(sessions.get(ownDeviceAddress) == null) { - buildSessionFromPEP(null, ownDeviceAddress, false); + for (Integer deviceId : deviceIds) { + AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId); + if (sessions.get(ownDeviceAddress) == null) { + buildSessionFromPEP(ownDeviceAddress); } } } Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString())); expiredDevices.removeAll(deviceIds); - setTrustOnSessions(jid, expiredDevices, SQLiteAxolotlStore.Trust.TRUSTED, - SQLiteAxolotlStore.Trust.INACTIVE); + setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED, + XmppAxolotlSession.Trust.INACTIVE_TRUSTED); + setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED, + XmppAxolotlSession.Trust.INACTIVE_UNDECIDED); + setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED, + XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED); Set<Integer> newDevices = new HashSet<>(deviceIds); - setTrustOnSessions(jid, newDevices, SQLiteAxolotlStore.Trust.INACTIVE, - SQLiteAxolotlStore.Trust.TRUSTED); + setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED, + XmppAxolotlSession.Trust.TRUSTED); + setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED, + XmppAxolotlSession.Trust.UNDECIDED); + setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED, + XmppAxolotlSession.Trust.UNTRUSTED); this.deviceIds.put(jid, deviceIds); mXmppConnectionService.keyStatusUpdated(); publishOwnDeviceIdIfNeeded(); @@ -830,7 +289,7 @@ public class AxolotlService { Set<Integer> deviceIds = new HashSet<>(); deviceIds.add(getOwnDeviceId()); IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Wiping all other devices from Pep:" + publish); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -840,7 +299,7 @@ public class AxolotlService { } public void purgeKey(IdentityKey identityKey) { - axolotlStore.setFingerprintTrust(identityKey.getFingerprint().replaceAll("\\s",""), SQLiteAxolotlStore.Trust.COMPROMISED); + axolotlStore.setFingerprintTrust(identityKey.getFingerprint().replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED); } public void publishOwnDeviceIdIfNeeded() { @@ -856,7 +315,7 @@ public class AxolotlService { if (!deviceIds.contains(getOwnDeviceId())) { deviceIds.add(getOwnDeviceId()); IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -877,19 +336,19 @@ public class AxolotlService { Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); boolean flush = false; if (bundle == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received invalid bundle:" + packet); - bundle = new PreKeyBundle(-1, -1, -1 , null, -1, null, null, null); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet); + bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); flush = true; } if (keys == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received invalid prekeys:" + packet); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet); } try { boolean changed = false; // Validate IdentityKey IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); changed = true; } @@ -898,16 +357,16 @@ public class AxolotlService { int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); try { signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); - if ( flush - ||!bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) + if (flush + || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); changed = true; } } catch (InvalidKeyIdException e) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); changed = true; @@ -929,32 +388,32 @@ public class AxolotlService { int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); if (newKeys > 0) { List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys( - axolotlStore.getCurrentPreKeyId()+1, newKeys); + axolotlStore.getCurrentPreKeyId() + 1, newKeys); preKeyRecords.addAll(newRecords); for (PreKeyRecord record : newRecords) { axolotlStore.storePreKey(record.getId(), record); } changed = true; - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding " + newKeys + " new preKeys to PEP."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP."); } - if(changed) { + if (changed) { IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), preKeyRecords, getOwnDeviceId()); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+ ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { // TODO: implement this! - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Published bundle, got: " + packet); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Published bundle, got: " + packet); } }); } } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); - return; + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + return; } } }); @@ -965,49 +424,41 @@ public class AxolotlService { Jid jid = contact.getJid().toBareJid(); AxolotlAddress address = new AxolotlAddress(jid.toString(), 0); return sessions.hasAny(address) || - ( deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty()); + (deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty()); } - public SQLiteAxolotlStore.Trust getFingerprintTrust(String fingerprint) { + + public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) { return axolotlStore.getFingerprintTrust(fingerprint); } - public void setFingerprintTrust(String fingerprint, SQLiteAxolotlStore.Trust trust) { + public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) { axolotlStore.setFingerprintTrust(fingerprint, trust); } - private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address, final boolean flushWaitingQueueAfterFetch) { + private void buildSessionFromPEP(final AxolotlAddress address) { Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.getDeviceId()); try { IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice( Jid.fromString(address.getName()), address.getDeviceId()); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Retrieving bundle: " + bundlesPacket); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket); mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() { private void finish() { - AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(),0); + AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0); if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) { - if (flushWaitingQueueAfterFetch && conversation != null) { - conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_AXOLOTL, - new Conversation.OnMessageFound() { - @Override - public void onMessageFound(Message message) { - processSending(message,false); - } - }); - } mXmppConnectionService.keyStatusUpdated(); } } @Override public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received preKey IQ packet, processing..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); final IqParser parser = mXmppConnectionService.getIqParser(); final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet); final PreKeyBundle bundle = parser.bundle(packet); if (preKeyBundleList.isEmpty() || bundle == null) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"preKey IQ packet invalid: " + packet); + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet); fetchStatusMap.put(address, FetchStatus.ERROR); finish(); return; @@ -1034,8 +485,8 @@ public class AxolotlService { XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", "")); sessions.put(address, session); fetchStatusMap.put(address, FetchStatus.SUCCESS); - } catch (UntrustedIdentityException|InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error building session for " + address + ": " + } catch (UntrustedIdentityException | InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": " + e.getClass().getName() + ", " + e.getMessage()); fetchStatusMap.put(address, FetchStatus.ERROR); } @@ -1044,7 +495,7 @@ public class AxolotlService { } }); } catch (InvalidJidException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got address with invalid jid: " + address.getName()); + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName()); } } @@ -1052,13 +503,13 @@ public class AxolotlService { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + conversation.getContact().getJid().toBareJid()); Jid contactJid = conversation.getContact().getJid().toBareJid(); Set<AxolotlAddress> addresses = new HashSet<>(); - if(deviceIds.get(contactJid) != null) { - for(Integer foreignId:this.deviceIds.get(contactJid)) { + if (deviceIds.get(contactJid) != null) { + for (Integer foreignId : this.deviceIds.get(contactJid)) { AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId); - if(sessions.get(address) == null) { + if (sessions.get(address) == null) { IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - if ( identityKey != null ) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " + address.toString() + ", adding to cache..."); + if (identityKey != null) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); sessions.put(address, session); } else { @@ -1070,13 +521,13 @@ public class AxolotlService { } else { Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); } - if(deviceIds.get(account.getJid().toBareJid()) != null) { - for(Integer ownId:this.deviceIds.get(account.getJid().toBareJid())) { + if (deviceIds.get(account.getJid().toBareJid()) != null) { + for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) { AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId); - if(sessions.get(address) == null) { + if (sessions.get(address) == null) { IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - if ( identityKey != null ) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " + address.toString() + ", adding to cache..."); + if (identityKey != null) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); sessions.put(address, session); } else { @@ -1090,19 +541,21 @@ public class AxolotlService { return addresses; } - public boolean createSessionsIfNeeded(final Conversation conversation, final boolean flushWaitingQueueAfterFetch) { + public boolean createSessionsIfNeeded(final Conversation conversation) { Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); boolean newSessions = false; Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation); for (AxolotlAddress address : addresses) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString()); FetchStatus status = fetchStatusMap.get(address); - if ( status == null || status == FetchStatus.ERROR ) { - fetchStatusMap.put(address, FetchStatus.PENDING); - this.buildSessionFromPEP(conversation, address, flushWaitingQueueAfterFetch); - newSessions = true; + if (status == null || status == FetchStatus.ERROR) { + fetchStatusMap.put(address, FetchStatus.PENDING); + this.buildSessionFromPEP(address); + newSessions = true; + } else if (status == FetchStatus.PENDING) { + newSessions = true; } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already fetching bundle for " + address.toString()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString()); } } @@ -1110,56 +563,60 @@ public class AxolotlService { } public boolean hasPendingKeyFetches(Conversation conversation) { - AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(),0); - AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0); + AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0); + AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(), 0); return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) - ||fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING); + || fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING); } @Nullable - public XmppAxolotlMessage encrypt(Message message ){ - final String content; - if (message.hasFileOnRemoteHost()) { - content = message.getFileParams().url.toString(); - } else { - content = message.getBody(); - } - final XmppAxolotlMessage axolotlMessage; - try { - axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(), - getOwnDeviceId(), content); - } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); - return null; - } + private XmppAxolotlMessage buildHeader(Contact contact) { + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage( + contact.getJid().toBareJid(), getOwnDeviceId()); - if(findSessionsforContact(message.getContact()).isEmpty()) { + Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact); + Set<XmppAxolotlSession> ownSessions = findOwnSessions(); + if (contactSessions.isEmpty()) { return null; } - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl foreign headers..."); - for (XmppAxolotlSession session : findSessionsforContact(message.getContact())) { - Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.remoteAddress.toString()); - //if(!session.isTrusted()) { - // TODO: handle this properly - // continue; - // } - axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey())); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements..."); + for (XmppAxolotlSession session : contactSessions) { + Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString()); + axolotlMessage.addDevice(session); } - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl own headers..."); - for (XmppAxolotlSession session : findOwnSessions()) { - Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.remoteAddress.toString()); - // if(!session.isTrusted()) { - // TODO: handle this properly - // continue; - // } - axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey())); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements..."); + for (XmppAxolotlSession session : ownSessions) { + Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString()); + axolotlMessage.addDevice(session); + } + + return axolotlMessage; + } + + @Nullable + public XmppAxolotlMessage encrypt(Message message) { + XmppAxolotlMessage axolotlMessage = buildHeader(message.getContact()); + + if (axolotlMessage != null) { + final String content; + if (message.hasFileOnRemoteHost()) { + content = message.getFileParams().url.toString(); + } else { + content = message.getBody(); + } + try { + axolotlMessage.encrypt(content); + } catch (CryptoFailedException e) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); + return null; + } } return axolotlMessage; } - private void processSending(final Message message, final boolean delay) { + public void preparePayloadMessage(final Message message, final boolean delay) { executor.execute(new Runnable() { @Override public void run() { @@ -1168,79 +625,89 @@ public class AxolotlService { mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); //mXmppConnectionService.updateConversationUi(); } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Generated message, caching: " + message.getUuid()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); messageCache.put(message.getUuid(), axolotlMessage); - mXmppConnectionService.resendMessage(message,delay); + mXmppConnectionService.resendMessage(message, delay); } } }); } - public void prepareMessage(final Message message,final boolean delay) { - if (!messageCache.containsKey(message.getUuid())) { - boolean newSessions = createSessionsIfNeeded(message.getConversation(), true); - if (!newSessions) { - this.processSending(message,delay); + public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) { + executor.execute(new Runnable() { + @Override + public void run() { + XmppAxolotlMessage axolotlMessage = buildHeader(contact); + onMessageCreatedCallback.run(axolotlMessage); } - } + }); } public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid()); if (axolotlMessage != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Cache hit: " + message.getUuid()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid()); messageCache.remove(message.getUuid()); } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Cache miss: " + message.getUuid()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid()); } return axolotlMessage; } - public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { - XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; + private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + return (identityKey != null) + ? new XmppAxolotlSession(account, axolotlStore, address, + identityKey.getFingerprint().replaceAll("\\s", "")) + : null; + } + + private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) { AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(), message.getSenderDeviceId()); - - boolean newSession = false; XmppAxolotlSession session = sessions.get(senderAddress); if (session == null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Account: "+account.getJid()+" No axolotl session found while parsing received message " + message); - // TODO: handle this properly - IdentityKey identityKey = axolotlStore.loadSession(senderAddress).getSessionState().getRemoteIdentityKey(); - if ( identityKey != null ) { - session = new XmppAxolotlSession(account, axolotlStore, senderAddress, identityKey.getFingerprint().replaceAll("\\s", "")); - } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message); + session = recreateUncachedSession(senderAddress); + if (session == null) { session = new XmppAxolotlSession(account, axolotlStore, senderAddress); } - newSession = true; } + return session; + } - for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { - if (header.getRecipientDeviceId() == getOwnDeviceId()) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found axolotl header matching own device ID, processing..."); - byte[] payloadKey = session.processReceiving(header); - if (payloadKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got payload key from axolotl header. Decrypting message..."); - try{ - plaintextMessage = message.decrypt(session, payloadKey, session.getFingerprint()); - } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage()); - break; - } - } - Integer preKeyId = session.getPreKeyId(); - if (preKeyId != null) { - publishBundlesIfNeeded(); - session.resetPreKeyId(); - } - break; + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) { + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; + + XmppAxolotlSession session = getReceivingSession(message); + try { + plaintextMessage = message.decrypt(session, getOwnDeviceId()); + Integer preKeyId = session.getPreKeyId(); + if (preKeyId != null) { + publishBundlesIfNeeded(); + session.resetPreKeyId(); } + } catch (CryptoFailedException e) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage()); } - if (newSession && plaintextMessage != null) { - sessions.put(senderAddress, session); + if (session.isFresh() && plaintextMessage != null) { + sessions.put(session); } return plaintextMessage; } + + public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) { + XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage = null; + + XmppAxolotlSession session = getReceivingSession(message); + keyTransportMessage = message.getParameters(session, getOwnDeviceId()); + + if (session.isFresh() && keyTransportMessage != null) { + sessions.put(session); + } + + return keyTransportMessage; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java new file mode 100644 index 000000000..3d40a4089 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.crypto.axolotl; + +public interface OnMessageCreatedCallback { + void run(XmppAxolotlMessage message); +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java new file mode 100644 index 000000000..190eb88a8 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java @@ -0,0 +1,421 @@ +package eu.siacs.conversations.crypto.axolotl; + +import android.util.Log; +import android.util.LruCache; + +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.ecc.Curve; +import org.whispersystems.libaxolotl.ecc.ECKeyPair; +import org.whispersystems.libaxolotl.state.AxolotlStore; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; +import org.whispersystems.libaxolotl.util.KeyHelper; + +import java.util.List; +import java.util.Set; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; + +public class SQLiteAxolotlStore implements AxolotlStore { + + public static final String PREKEY_TABLENAME = "prekeys"; + public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; + public static final String SESSION_TABLENAME = "sessions"; + public static final String IDENTITIES_TABLENAME = "identities"; + public static final String ACCOUNT = "account"; + public static final String DEVICE_ID = "device_id"; + public static final String ID = "id"; + public static final String KEY = "key"; + public static final String FINGERPRINT = "fingerprint"; + public static final String NAME = "name"; + public static final String TRUSTED = "trusted"; + public static final String OWN = "ownkey"; + + public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; + public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; + + private static final int NUM_TRUSTS_TO_CACHE = 100; + + private final Account account; + private final XmppConnectionService mXmppConnectionService; + + private IdentityKeyPair identityKeyPair; + private int localRegistrationId; + private int currentPreKeyId = 0; + + private final LruCache<String, XmppAxolotlSession.Trust> trustCache = + new LruCache<String, XmppAxolotlSession.Trust>(NUM_TRUSTS_TO_CACHE) { + @Override + protected XmppAxolotlSession.Trust create(String fingerprint) { + return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint); + } + }; + + private static IdentityKeyPair generateIdentityKeyPair() { + Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair..."); + ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); + return new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), + identityKeyPairKeys.getPrivateKey()); + } + + private static int generateRegistrationId() { + Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID..."); + return KeyHelper.generateRegistrationId(true); + } + + public SQLiteAxolotlStore(Account account, XmppConnectionService service) { + this.account = account; + this.mXmppConnectionService = service; + this.localRegistrationId = loadRegistrationId(); + this.currentPreKeyId = loadCurrentPreKeyId(); + for (SignedPreKeyRecord record : loadSignedPreKeys()) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got Axolotl signed prekey record:" + record.getId()); + } + } + + public int getCurrentPreKeyId() { + return currentPreKeyId; + } + + // -------------------------------------- + // IdentityKeyStore + // -------------------------------------- + + private IdentityKeyPair loadIdentityKeyPair() { + String ownName = account.getJid().toBareJid().toString(); + IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account, + ownName); + + if (ownKey != null) { + return ownKey; + } else { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl key for account " + ownName); + ownKey = generateIdentityKeyPair(); + mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey); + } + return ownKey; + } + + private int loadRegistrationId() { + return loadRegistrationId(false); + } + + private int loadRegistrationId(boolean regenerate) { + String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); + int reg_id; + if (!regenerate && regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid()); + reg_id = generateRegistrationId(); + boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); + if (success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new key to the database!"); + } + } + return reg_id; + } + + private int loadCurrentPreKeyId() { + String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); + int reg_id; + if (regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid()); + reg_id = 0; + } + return reg_id; + } + + public void regenerate() { + mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); + trustCache.evictAll(); + account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0)); + identityKeyPair = loadIdentityKeyPair(); + localRegistrationId = loadRegistrationId(true); + currentPreKeyId = 0; + mXmppConnectionService.updateAccountUi(); + } + + /** + * Get the local client's identity key pair. + * + * @return The local client's persistent identity key pair. + */ + @Override + public IdentityKeyPair getIdentityKeyPair() { + if (identityKeyPair == null) { + identityKeyPair = loadIdentityKeyPair(); + } + return identityKeyPair; + } + + /** + * Return the local client's registration ID. + * <p/> + * Clients should maintain a registration ID, a random number + * between 1 and 16380 that's generated once at install time. + * + * @return the local client's registration ID. + */ + @Override + public int getLocalRegistrationId() { + return localRegistrationId; + } + + /** + * Save a remote client's identity key + * <p/> + * Store a remote client's identity key as trusted. + * + * @param name The name of the remote client. + * @param identityKey The remote client's identity key. + */ + @Override + public void saveIdentity(String name, IdentityKey identityKey) { + if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) { + mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey); + } + } + + /** + * Verify a remote client's identity key. + * <p/> + * Determine whether a remote client's identity is trusted. Convention is + * that the TextSecure protocol is 'trust on first use.' This means that + * an identity key is considered 'trusted' if there is no entry for the recipient + * in the local store, or if it matches the saved key for a recipient in the local + * store. Only if it mismatches an entry in the local store is it considered + * 'untrusted.' + * + * @param name The name of the remote client. + * @param identityKey The identity key to verify. + * @return true if trusted, false if untrusted. + */ + @Override + public boolean isTrustedIdentity(String name, IdentityKey identityKey) { + return true; + } + + public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) { + return (fingerprint == null)? null : trustCache.get(fingerprint); + } + + public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) { + mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust); + trustCache.remove(fingerprint); + } + + public Set<IdentityKey> getContactKeysWithTrust(String bareJid, XmppAxolotlSession.Trust trust) { + return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust); + } + + public long getContactNumTrustedKeys(String bareJid) { + return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid); + } + + // -------------------------------------- + // SessionStore + // -------------------------------------- + + /** + * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple, + * or a new SessionRecord if one does not currently exist. + * <p/> + * It is important that implementations return a copy of the current durable information. The + * returned SessionRecord may be modified, but those changes should not have an effect on the + * durable session state (what is returned by subsequent calls to this method) without the + * store method being called here first. + * + * @param address The name and device ID of the remote client. + * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or + * a new SessionRecord if one does not currently exist. + */ + @Override + public SessionRecord loadSession(AxolotlAddress address) { + SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); + return (session != null) ? session : new SessionRecord(); + } + + /** + * Returns all known devices with active sessions for a recipient + * + * @param name the name of the client. + * @return all known sub-devices with active sessions. + */ + @Override + public List<Integer> getSubDeviceSessions(String name) { + return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, + new AxolotlAddress(name, 0)); + } + + /** + * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @param record the current SessionRecord for the remote client. + */ + @Override + public void storeSession(AxolotlAddress address, SessionRecord record) { + mXmppConnectionService.databaseBackend.storeSession(account, address, record); + } + + /** + * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @return true if a {@link SessionRecord} exists, false otherwise. + */ + @Override + public boolean containsSession(AxolotlAddress address) { + return mXmppConnectionService.databaseBackend.containsSession(account, address); + } + + /** + * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + */ + @Override + public void deleteSession(AxolotlAddress address) { + mXmppConnectionService.databaseBackend.deleteSession(account, address); + } + + /** + * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. + * + * @param name the name of the remote client. + */ + @Override + public void deleteAllSessions(String name) { + AxolotlAddress address = new AxolotlAddress(name, 0); + mXmppConnectionService.databaseBackend.deleteAllSessions(account, + address); + } + + // -------------------------------------- + // PreKeyStore + // -------------------------------------- + + /** + * Load a local PreKeyRecord. + * + * @param preKeyId the ID of the local PreKeyRecord. + * @return the corresponding PreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. + */ + @Override + public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { + PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); + if (record == null) { + throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); + } + return record; + } + + /** + * Store a local PreKeyRecord. + * + * @param preKeyId the ID of the PreKeyRecord to store. + * @param record the PreKeyRecord. + */ + @Override + public void storePreKey(int preKeyId, PreKeyRecord record) { + mXmppConnectionService.databaseBackend.storePreKey(account, record); + currentPreKeyId = preKeyId; + boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId)); + if (success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new prekey id to the database!"); + } + } + + /** + * @param preKeyId A PreKeyRecord ID. + * @return true if the store has a record for the preKeyId, otherwise false. + */ + @Override + public boolean containsPreKey(int preKeyId) { + return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); + } + + /** + * Delete a PreKeyRecord from local storage. + * + * @param preKeyId The ID of the PreKeyRecord to remove. + */ + @Override + public void removePreKey(int preKeyId) { + mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); + } + + // -------------------------------------- + // SignedPreKeyStore + // -------------------------------------- + + /** + * Load a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the local SignedPreKeyRecord. + * @return the corresponding SignedPreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. + */ + @Override + public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { + SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); + if (record == null) { + throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); + } + return record; + } + + /** + * Load all local SignedPreKeyRecords. + * + * @return All stored SignedPreKeyRecords. + */ + @Override + public List<SignedPreKeyRecord> loadSignedPreKeys() { + return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); + } + + /** + * Store a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. + * @param record the SignedPreKeyRecord. + */ + @Override + public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { + mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); + } + + /** + * @param signedPreKeyId A SignedPreKeyRecord ID. + * @return true if the store has a record for the signedPreKeyId, otherwise false. + */ + @Override + public boolean containsSignedPreKey(int signedPreKeyId) { + return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); + } + + /** + * Delete a SignedPreKeyRecord from local storage. + * + * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. + */ + @Override + public void removeSignedPreKey(int signedPreKeyId) { + mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index 24afeaeae..cf950d6da 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -1,15 +1,16 @@ package eu.siacs.conversations.crypto.axolotl; -import android.support.annotation.Nullable; import android.util.Base64; +import android.util.Log; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; -import java.util.HashSet; -import java.util.Set; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -20,112 +21,142 @@ import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.jid.Jid; public class XmppAxolotlMessage { + public static final String CONTAINERTAG = "encrypted"; + public static final String HEADER = "header"; + public static final String SOURCEID = "sid"; + public static final String KEYTAG = "key"; + public static final String REMOTEID = "rid"; + public static final String IVTAG = "iv"; + public static final String PAYLOAD = "payload"; + + private static final String KEYTYPE = "AES"; + private static final String CIPHERMODE = "AES/GCM/NoPadding"; + private static final String PROVIDER = "BC"; + private byte[] innerKey; - private byte[] ciphertext; - private byte[] iv; - private final Set<XmppAxolotlMessageHeader> headers; + private byte[] ciphertext = null; + private byte[] iv = null; + private final Map<Integer, byte[]> keys; private final Jid from; private final int sourceDeviceId; - public static class XmppAxolotlMessageHeader { - private final int recipientDeviceId; - private final byte[] content; - - public XmppAxolotlMessageHeader(int deviceId, byte[] content) { - this.recipientDeviceId = deviceId; - this.content = content; - } + public static class XmppAxolotlPlaintextMessage { + private final String plaintext; + private final String fingerprint; - public XmppAxolotlMessageHeader(Element header) { - if("header".equals(header.getName())) { - this.recipientDeviceId = Integer.parseInt(header.getAttribute("rid")); - this.content = Base64.decode(header.getContent(),Base64.DEFAULT); - } else { - throw new IllegalArgumentException("Argument not a <header> Element!"); - } + public XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) { + this.plaintext = plaintext; + this.fingerprint = fingerprint; } - public int getRecipientDeviceId() { - return recipientDeviceId; + public String getPlaintext() { + return plaintext; } - public byte[] getContents() { - return content; - } - public Element toXml() { - Element headerElement = new Element("header"); - // TODO: generate XML - headerElement.setAttribute("rid", getRecipientDeviceId()); - headerElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT)); - return headerElement; + public String getFingerprint() { + return fingerprint; } } - public static class XmppAxolotlPlaintextMessage { - private final AxolotlService.XmppAxolotlSession session; - private final String plaintext; + public static class XmppAxolotlKeyTransportMessage { private final String fingerprint; + private final byte[] key; + private final byte[] iv; - public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext, String fingerprint) { - this.session = session; - this.plaintext = plaintext; + public XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) { this.fingerprint = fingerprint; + this.key = key; + this.iv = iv; } - public String getPlaintext() { - return plaintext; + public String getFingerprint() { + return fingerprint; } - public AxolotlService.XmppAxolotlSession getSession() { - return session; + public byte[] getKey() { + return key; } - public String getFingerprint() { - return fingerprint; + public byte[] getIv() { + return iv; } } - public XmppAxolotlMessage(Jid from, Element axolotlMessage) { + private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException { this.from = from; - this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id")); - this.headers = new HashSet<>(); - for(Element child:axolotlMessage.getChildren()) { - switch(child.getName()) { - case "header": - headers.add(new XmppAxolotlMessageHeader(child)); + Element header = axolotlMessage.findChild(HEADER); + this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID)); + List<Element> keyElements = header.getChildren(); + this.keys = new HashMap<>(keyElements.size()); + for (Element keyElement : keyElements) { + switch (keyElement.getName()) { + case KEYTAG: + try { + Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID)); + byte[] key = Base64.decode(keyElement.getContent(), Base64.DEFAULT); + this.keys.put(recipientId, key); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(e); + } break; - case "message": - iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT); - ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT); + case IVTAG: + if (this.iv != null) { + throw new IllegalArgumentException("Duplicate iv entry"); + } + iv = Base64.decode(keyElement.getContent(), Base64.DEFAULT); break; default: + Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString()); break; } } + Element payloadElement = axolotlMessage.findChild(PAYLOAD); + if (payloadElement != null) { + ciphertext = Base64.decode(payloadElement.getContent(), Base64.DEFAULT); + } } - public XmppAxolotlMessage(Jid from, int sourceDeviceId, String plaintext) throws CryptoFailedException{ + public XmppAxolotlMessage(Jid from, int sourceDeviceId) { this.from = from; this.sourceDeviceId = sourceDeviceId; - this.headers = new HashSet<>(); - this.encrypt(plaintext); + this.keys = new HashMap<>(); + this.iv = generateIv(); + this.innerKey = generateKey(); } - private void encrypt(String plaintext) throws CryptoFailedException { + public static XmppAxolotlMessage fromElement(Element element, Jid from) { + return new XmppAxolotlMessage(element, from); + } + + private static byte[] generateKey() { try { - KeyGenerator generator = KeyGenerator.getInstance("AES"); + KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE); generator.init(128); - SecretKey secretKey = generator.generateKey(); - SecureRandom random = new SecureRandom(); - this.iv = new byte[16]; - random.nextBytes(iv); + return generator.generateKey().getEncoded(); + } catch (NoSuchAlgorithmException e) { + Log.e(Config.LOGTAG, e.getMessage()); + return null; + } + } + + private static byte[] generateIv() { + SecureRandom random = new SecureRandom(); + byte[] iv = new byte[16]; + random.nextBytes(iv); + return iv; + } + + public void encrypt(String plaintext) throws CryptoFailedException { + try { + SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE); IvParameterSpec ivSpec = new IvParameterSpec(iv); - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC"); + Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); this.innerKey = secretKey.getEncoded(); this.ciphertext = cipher.doFinal(plaintext.getBytes()); @@ -148,17 +179,14 @@ public class XmppAxolotlMessage { return ciphertext; } - public Set<XmppAxolotlMessageHeader> getHeaders() { - return headers; - } - - public void addHeader(@Nullable XmppAxolotlMessageHeader header) { - if (header != null) { - headers.add(header); + public void addDevice(XmppAxolotlSession session) { + byte[] key = session.processSending(innerKey); + if (key != null) { + keys.put(session.getRemoteAddress().getDeviceId(), key); } } - public byte[] getInnerKey(){ + public byte[] getInnerKey() { return innerKey; } @@ -166,37 +194,55 @@ public class XmppAxolotlMessage { return this.iv; } - public Element toXml() { - // TODO: generate outer XML, add in header XML - Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX); - message.setAttribute("id", sourceDeviceId); - for(XmppAxolotlMessageHeader header: headers) { - message.addChild(header.toXml()); + public Element toElement() { + Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX); + Element headerElement = encryptionElement.addChild(HEADER); + headerElement.setAttribute(SOURCEID, sourceDeviceId); + for (Map.Entry<Integer, byte[]> keyEntry : keys.entrySet()) { + Element keyElement = new Element(KEYTAG); + keyElement.setAttribute(REMOTEID, keyEntry.getKey()); + keyElement.setContent(Base64.encodeToString(keyEntry.getValue(), Base64.DEFAULT)); + headerElement.addChild(keyElement); + } + headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.DEFAULT)); + if (ciphertext != null) { + Element payload = encryptionElement.addChild(PAYLOAD); + payload.setContent(Base64.encodeToString(ciphertext, Base64.DEFAULT)); } - Element payload = message.addChild("message"); - payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT)); - payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT)); - return message; + return encryptionElement; } + private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) { + byte[] encryptedKey = keys.get(sourceDeviceId); + return (encryptedKey != null) ? session.processReceiving(encryptedKey) : null; + } - public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key, String fingerprint) throws CryptoFailedException { - XmppAxolotlPlaintextMessage plaintextMessage = null; - try { - - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC"); - SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - - cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); - - String plaintext = new String(cipher.doFinal(ciphertext)); - plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext, fingerprint); + public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) { + byte[] key = unpackKey(session, sourceDeviceId); + return (key != null) + ? new XmppAxolotlKeyTransportMessage(session.getFingerprint(), key, getIV()) + : null; + } - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | InvalidAlgorithmParameterException | IllegalBlockSizeException - | BadPaddingException | NoSuchProviderException e) { - throw new CryptoFailedException(e); + public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { + XmppAxolotlPlaintextMessage plaintextMessage = null; + byte[] key = unpackKey(session, sourceDeviceId); + if (key != null) { + try { + Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER); + SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); + + String plaintext = new String(cipher.doFinal(ciphertext)); + plaintextMessage = new XmppAxolotlPlaintextMessage(plaintext, session.getFingerprint()); + + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException | IllegalBlockSizeException + | BadPaddingException | NoSuchProviderException e) { + throw new CryptoFailedException(e); + } } return plaintextMessage; } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java new file mode 100644 index 000000000..c4053854d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java @@ -0,0 +1,196 @@ +package eu.siacs.conversations.crypto.axolotl; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.DuplicateMessageException; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.InvalidMessageException; +import org.whispersystems.libaxolotl.InvalidVersionException; +import org.whispersystems.libaxolotl.LegacyMessageException; +import org.whispersystems.libaxolotl.NoSessionException; +import org.whispersystems.libaxolotl.SessionCipher; +import org.whispersystems.libaxolotl.UntrustedIdentityException; +import org.whispersystems.libaxolotl.protocol.CiphertextMessage; +import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; +import org.whispersystems.libaxolotl.protocol.WhisperMessage; + +import java.util.HashMap; +import java.util.Map; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; + +public class XmppAxolotlSession { + private final SessionCipher cipher; + private final SQLiteAxolotlStore sqLiteAxolotlStore; + private final AxolotlAddress remoteAddress; + private final Account account; + private String fingerprint = null; + private Integer preKeyId = null; + private boolean fresh = true; + + public enum Trust { + UNDECIDED(0), + TRUSTED(1), + UNTRUSTED(2), + COMPROMISED(3), + INACTIVE_TRUSTED(4), + INACTIVE_UNDECIDED(5), + INACTIVE_UNTRUSTED(6); + + private static final Map<Integer, Trust> trustsByValue = new HashMap<>(); + + static { + for (Trust trust : Trust.values()) { + trustsByValue.put(trust.getCode(), trust); + } + } + + private final int code; + + Trust(int code) { + this.code = code; + } + + public int getCode() { + return this.code; + } + + public String toString() { + switch (this) { + case UNDECIDED: + return "Trust undecided " + getCode(); + case TRUSTED: + return "Trusted " + getCode(); + case COMPROMISED: + return "Compromised " + getCode(); + case INACTIVE_TRUSTED: + return "Inactive (Trusted)" + getCode(); + case INACTIVE_UNDECIDED: + return "Inactive (Undecided)" + getCode(); + case INACTIVE_UNTRUSTED: + return "Inactive (Untrusted)" + getCode(); + case UNTRUSTED: + default: + return "Untrusted " + getCode(); + } + } + + public static Trust fromBoolean(Boolean trusted) { + return trusted ? TRUSTED : UNTRUSTED; + } + + public static Trust fromCode(int code) { + return trustsByValue.get(code); + } + } + + public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, String fingerprint) { + this(account, store, remoteAddress); + this.fingerprint = fingerprint; + } + + public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { + this.cipher = new SessionCipher(store, remoteAddress); + this.remoteAddress = remoteAddress; + this.sqLiteAxolotlStore = store; + this.account = account; + } + + public Integer getPreKeyId() { + return preKeyId; + } + + public void resetPreKeyId() { + + preKeyId = null; + } + + public String getFingerprint() { + return fingerprint; + } + + public AxolotlAddress getRemoteAddress() { + return remoteAddress; + } + + public boolean isFresh() { + return fresh; + } + + public void setNotFresh() { + this.fresh = false; + } + + protected void setTrust(Trust trust) { + sqLiteAxolotlStore.setFingerprintTrust(fingerprint, trust); + } + + protected Trust getTrust() { + Trust trust = sqLiteAxolotlStore.getFingerprintTrust(fingerprint); + return (trust == null) ? Trust.UNDECIDED : trust; + } + + @Nullable + public byte[] processReceiving(byte[] encryptedKey) { + byte[] plaintext = null; + Trust trust = getTrust(); + switch (trust) { + case INACTIVE_TRUSTED: + case UNDECIDED: + case UNTRUSTED: + case TRUSTED: + try { + try { + PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); + String fingerprint = message.getIdentityKey().getFingerprint().replaceAll("\\s", ""); + if (this.fingerprint != null && !this.fingerprint.equals(fingerprint)) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.fingerprint + ", received message with fingerprint " + fingerprint); + } else { + this.fingerprint = fingerprint; + plaintext = cipher.decrypt(message); + if (message.getPreKeyId().isPresent()) { + preKeyId = message.getPreKeyId().get(); + } + } + } catch (InvalidMessageException | InvalidVersionException e) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received"); + WhisperMessage message = new WhisperMessage(encryptedKey); + plaintext = cipher.decrypt(message); + } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); + } + } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); + } + + if (plaintext != null && trust == Trust.INACTIVE_TRUSTED) { + setTrust(Trust.TRUSTED); + } + + break; + + case COMPROMISED: + default: + // ignore + break; + } + return plaintext; + } + + @Nullable + public byte[] processSending(@NonNull byte[] outgoingMessage) { + Trust trust = getTrust(); + if (trust == Trust.TRUSTED) { + CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); + return ciphertextMessage.serialize(); + } else { + return null; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 2e858eb99..3a73f78bf 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -563,42 +563,51 @@ public class Conversation extends AbstractEntity implements Blockable { return this.nextCounterpart; } - public int getLatestEncryption() { - int latestEncryption = this.getLatestMessage().getEncryption(); - if ((latestEncryption == Message.ENCRYPTION_DECRYPTED) - || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) { - return Message.ENCRYPTION_PGP; - } else { - return latestEncryption; + private int getMostRecentlyUsedOutgoingEncryption() { + synchronized (this.messages) { + for(int i = this.messages.size() -1; i >= 0; --i) { + final Message m = this.messages.get(0); + if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) { + final int e = m.getEncryption(); + if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) { + return Message.ENCRYPTION_PGP; + } else { + return e; + } + } + } } + return Message.ENCRYPTION_NONE; } - public int getNextEncryption(boolean force) { - int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1); - if (next == -1) { - int latest = this.getLatestEncryption(); - if (latest == Message.ENCRYPTION_NONE) { - if (force && getMode() == MODE_SINGLE) { - return Message.ENCRYPTION_OTR; - } else if (getContact().getPresences().size() == 1) { - if (getContact().getOtrFingerprints().size() >= 1) { - return Message.ENCRYPTION_OTR; + private int getMostRecentlyUsedIncomingEncryption() { + synchronized (this.messages) { + for(int i = this.messages.size() -1; i >= 0; --i) { + final Message m = this.messages.get(0); + if (m.getStatus() == Message.STATUS_RECEIVED) { + final int e = m.getEncryption(); + if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) { + return Message.ENCRYPTION_PGP; } else { - return latest; + return e; } - } else { - return latest; } - } else { - return latest; } } - if (next == Message.ENCRYPTION_NONE && force - && getMode() == MODE_SINGLE) { - return Message.ENCRYPTION_OTR; - } else { - return next; + return Message.ENCRYPTION_NONE; + } + + public int getNextEncryption() { + int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1); + if (next == -1) { + int outgoing = this.getMostRecentlyUsedOutgoingEncryption(); + if (outgoing == Message.ENCRYPTION_NONE) { + return this.getMostRecentlyUsedIncomingEncryption(); + } else { + return outgoing; + } } + return next; } public void setNextEncryption(int encryption) { diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java index ca3254484..d35a4b017 100644 --- a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java +++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java @@ -1,26 +1,7 @@ package eu.siacs.conversations.entities; -import android.util.Log; - import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -import eu.siacs.conversations.Config; + import eu.siacs.conversations.utils.MimeUtils; public class DownloadableFile extends File { @@ -29,8 +10,7 @@ public class DownloadableFile extends File { private long expectedSize = 0; private String sha1sum; - private Key aeskey; - private String mime; + private byte[] aeskey; private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf }; @@ -44,15 +24,7 @@ public class DownloadableFile extends File { } public long getExpectedSize() { - if (this.aeskey != null) { - if (this.expectedSize == 0) { - return 0; - } else { - return (this.expectedSize / 16 + 1) * 16; - } - } else { - return this.expectedSize; - } + return this.expectedSize; } public String getMimeType() { @@ -78,91 +50,38 @@ public class DownloadableFile extends File { this.sha1sum = sum; } - public void setKey(byte[] key) { - if (key.length == 48) { + public void setKeyAndIv(byte[] keyIvCombo) { + if (keyIvCombo.length == 48) { byte[] secretKey = new byte[32]; byte[] iv = new byte[16]; - System.arraycopy(key, 0, iv, 0, 16); - System.arraycopy(key, 16, secretKey, 0, 32); - this.aeskey = new SecretKeySpec(secretKey, "AES"); + System.arraycopy(keyIvCombo, 0, iv, 0, 16); + System.arraycopy(keyIvCombo, 16, secretKey, 0, 32); + this.aeskey = secretKey; this.iv = iv; - } else if (key.length >= 32) { + } else if (keyIvCombo.length >= 32) { byte[] secretKey = new byte[32]; - System.arraycopy(key, 0, secretKey, 0, 32); - this.aeskey = new SecretKeySpec(secretKey, "AES"); - } else if (key.length >= 16) { + System.arraycopy(keyIvCombo, 0, secretKey, 0, 32); + this.aeskey = secretKey; + } else if (keyIvCombo.length >= 16) { byte[] secretKey = new byte[16]; - System.arraycopy(key, 0, secretKey, 0, 16); - this.aeskey = new SecretKeySpec(secretKey, "AES"); + System.arraycopy(keyIvCombo, 0, secretKey, 0, 16); + this.aeskey = secretKey; } } - public Key getKey() { - return this.aeskey; + public void setKey(byte[] key) { + this.aeskey = key; } - public InputStream createInputStream() { - if (this.getKey() == null) { - try { - return new FileInputStream(this); - } catch (FileNotFoundException e) { - return null; - } - } else { - try { - IvParameterSpec ips = new IvParameterSpec(iv); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips); - Log.d(Config.LOGTAG, "opening encrypted input stream"); - return new CipherInputStream(new FileInputStream(this), cipher); - } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); - return null; - } catch (NoSuchPaddingException e) { - Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); - return null; - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); - return null; - } catch (InvalidAlgorithmParameterException e) { - Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); - return null; - } catch (FileNotFoundException e) { - return null; - } - } + public void setIv(byte[] iv) { + this.iv = iv; } - public OutputStream createOutputStream() { - if (this.getKey() == null) { - try { - return new FileOutputStream(this); - } catch (FileNotFoundException e) { - return null; - } - } else { - try { - IvParameterSpec ips = new IvParameterSpec(this.iv); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips); - Log.d(Config.LOGTAG, "opening encrypted output stream"); - return new CipherOutputStream(new FileOutputStream(this), - cipher); - } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); - return null; - } catch (NoSuchPaddingException e) { - Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); - return null; - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); - return null; - } catch (InvalidAlgorithmParameterException e) { - Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); - return null; - } catch (FileNotFoundException e) { - return null; - } - } + public byte[] getKey() { + return this.aeskey; + } + + public byte[] getIv() { + return this.iv; } } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 698775c8a..0eff99cf7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -8,7 +8,7 @@ import java.net.URL; import java.util.Arrays; import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.UIHelper; @@ -51,6 +51,7 @@ public class Message extends AbstractEntity { public static final String ENCRYPTION = "encryption"; public static final String STATUS = "status"; public static final String TYPE = "type"; + public static final String CARBON = "carbon"; public static final String REMOTE_MSG_ID = "remoteMsgId"; public static final String SERVER_MSG_ID = "serverMsgId"; public static final String RELATIVE_FILE_PATH = "relativeFilePath"; @@ -68,6 +69,7 @@ public class Message extends AbstractEntity { protected int encryption; protected int status; protected int type; + protected boolean carbon = false; protected String relativeFilePath; protected boolean read = true; protected String remoteMsgId = null; @@ -85,8 +87,11 @@ public class Message extends AbstractEntity { public Message(Conversation conversation, String body, int encryption) { this(conversation, body, encryption, STATUS_UNSEND); } - public Message(Conversation conversation, String body, int encryption, int status) { + this(conversation, body, encryption, status, false); + } + + public Message(Conversation conversation, String body, int encryption, int status, boolean carbon) { this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), conversation.getJid() == null ? null : conversation.getJid().toBareJid(), @@ -96,6 +101,7 @@ public class Message extends AbstractEntity { encryption, status, TYPE_TEXT, + false, null, null, null, @@ -105,8 +111,9 @@ public class Message extends AbstractEntity { private Message(final String uuid, final String conversationUUid, final Jid counterpart, final Jid trueCounterpart, final String body, final long timeSent, - final int encryption, final int status, final int type, final String remoteMsgId, - final String relativeFilePath, final String serverMsgId, final String fingerprint) { + final int encryption, final int status, final int type, final boolean carbon, + final String remoteMsgId, final String relativeFilePath, + final String serverMsgId, final String fingerprint) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; @@ -116,6 +123,7 @@ public class Message extends AbstractEntity { this.encryption = encryption; this.status = status; this.type = type; + this.carbon = carbon; this.remoteMsgId = remoteMsgId; this.relativeFilePath = relativeFilePath; this.serverMsgId = serverMsgId; @@ -154,6 +162,7 @@ public class Message extends AbstractEntity { cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), cursor.getInt(cursor.getColumnIndex(STATUS)), cursor.getInt(cursor.getColumnIndex(TYPE)), + cursor.getInt(cursor.getColumnIndex(CARBON))>0, cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), @@ -188,6 +197,7 @@ public class Message extends AbstractEntity { values.put(ENCRYPTION, encryption); values.put(STATUS, status); values.put(TYPE, type); + values.put(CARBON, carbon ? 1 : 0); values.put(REMOTE_MSG_ID, remoteMsgId); values.put(RELATIVE_FILE_PATH, relativeFilePath); values.put(SERVER_MSG_ID, serverMsgId); @@ -312,6 +322,14 @@ public class Message extends AbstractEntity { this.type = type; } + public boolean isCarbon() { + return carbon; + } + + public void setCarbon(boolean carbon) { + this.carbon = carbon; + } + public void setTrueCounterpart(Jid trueCounterpart) { this.trueCounterpart = trueCounterpart; } @@ -416,11 +434,14 @@ public class Message extends AbstractEntity { } public String getMergedBody() { - final Message next = this.next(); - if (this.mergeable(next)) { - return getBody().trim() + MERGE_SEPARATOR + next.getMergedBody(); + StringBuilder body = new StringBuilder(this.body.trim()); + Message current = this; + while(current.mergeable(current.next())) { + current = current.next(); + body.append(MERGE_SEPARATOR); + body.append(current.getBody().trim()); } - return getBody().trim(); + return body.toString(); } public boolean hasMeCommand() { @@ -428,20 +449,23 @@ public class Message extends AbstractEntity { } public int getMergedStatus() { - final Message next = this.next(); - if (this.mergeable(next)) { - return next.getStatus(); + int status = this.status; + Message current = this; + while(current.mergeable(current.next())) { + current = current.next(); + status = current.status; } - return getStatus(); + return status; } public long getMergedTimeSent() { - Message next = this.next(); - if (this.mergeable(next)) { - return next.getMergedTimeSent(); - } else { - return getTimeSent(); + long time = this.timeSent; + Message current = this; + while(current.mergeable(current.next())) { + current = current.next(); + time = current.timeSent; } + return time; } public boolean wasMergedIntoPrevious() { @@ -683,6 +707,37 @@ public class Message extends AbstractEntity { public boolean isTrusted() { return conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint) - == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED; + == XmppAxolotlSession.Trust.TRUSTED; + } + + private int getPreviousEncryption() { + for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){ + if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) { + continue; + } + return iterator.getEncryption(); + } + return ENCRYPTION_NONE; + } + + private int getNextEncryption() { + for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){ + if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) { + continue; + } + return iterator.getEncryption(); + } + return conversation.getNextEncryption(); + } + + public boolean isValidInSession() { + int pastEncryption = this.getPreviousEncryption(); + int futureEncryption = this.getNextEncryption(); + + boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE + || futureEncryption == ENCRYPTION_NONE + || pastEncryption != futureEncryption; + + return inUnencryptedSession || this.getEncryption() == pastEncryption; } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 88ff8f46c..898d218e4 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -254,12 +254,15 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file) { + public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) { IqPacket packet = new IqPacket(IqPacket.TYPE.GET); packet.setTo(host); Element request = packet.addChild("request",Xmlns.HTTP_UPLOAD); request.addChild("filename").setContent(file.getName()); request.addChild("size").setContent(String.valueOf(file.getExpectedSize())); + if (mime != null) { + request.addChild("content-type", mime); + } return packet; } } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 045916725..a06a0dddb 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -63,7 +63,7 @@ public class MessageGenerator extends AbstractGenerator { if (axolotlMessage == null) { return null; } - packet.setAxolotlMessage(axolotlMessage.toXml()); + packet.setAxolotlMessage(axolotlMessage.toElement()); return packet; } diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 30d9a393a..79e4654be 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.http; import android.content.Intent; import android.net.Uri; -import android.os.SystemClock; import android.util.Log; import java.io.BufferedInputStream; @@ -21,6 +20,8 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; @@ -83,7 +84,7 @@ public class HttpDownloadConnection implements Transferable { this.file = mXmppConnectionService.getFileBackend().getFile(message, false); String reference = mUrl.getRef(); if (reference != null && reference.length() == 96) { - this.file.setKey(CryptoHelper.hexToBytes(reference)); + this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference)); } if ((this.message.getEncryption() == Message.ENCRYPTION_OTR @@ -187,6 +188,8 @@ public class HttpDownloadConnection implements Transferable { private boolean interactive = false; + private OutputStream os; + public FileDownloader(boolean interactive) { this.interactive = interactive; } @@ -199,14 +202,16 @@ public class HttpDownloadConnection implements Transferable { updateImageBounds(); finish(); } catch (SSLHandshakeException e) { + FileBackend.close(os); changeStatus(STATUS_OFFER); } catch (IOException e) { + FileBackend.close(os); mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host); cancel(); } } - private void download() throws SSLHandshakeException, IOException { + private void download() throws IOException { HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); if (connection instanceof HttpsURLConnection) { mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); @@ -215,10 +220,7 @@ public class HttpDownloadConnection implements Transferable { BufferedInputStream is = new BufferedInputStream(connection.getInputStream()); file.getParentFile().mkdirs(); file.createNewFile(); - OutputStream os = file.createOutputStream(); - if (os == null) { - throw new IOException(); - } + os = AbstractConnectionManager.createOutputStream(file,true); long transmitted = 0; long expected = file.getExpectedSize(); int count = -1; diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index c25bf13a2..768915a94 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -1,7 +1,10 @@ package eu.siacs.conversations.http; import android.app.PendingIntent; +import android.content.Intent; +import android.net.Uri; import android.util.Log; +import android.util.Pair; import java.io.IOException; import java.io.InputStream; @@ -18,6 +21,7 @@ import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.UiCallback; import eu.siacs.conversations.utils.CryptoHelper; @@ -37,13 +41,15 @@ public class HttpUploadConnection implements Transferable { private Account account; private DownloadableFile file; private Message message; + private String mime; private URL mGetUrl; private URL mPutUrl; private byte[] key = null; private long transmitted = 0; - private long expected = 1; + + private InputStream mFileInputStream; public HttpUploadConnection(HttpConnectionManager httpConnectionManager) { this.mHttpConnectionManager = httpConnectionManager; @@ -67,7 +73,7 @@ public class HttpUploadConnection implements Transferable { @Override public int getProgress() { - return (int) ((((double) transmitted) / expected) * 100); + return (int) ((((double) transmitted) / file.getExpectedSize()) * 100); } @Override @@ -78,28 +84,30 @@ public class HttpUploadConnection implements Transferable { private void fail() { mHttpConnectionManager.finishUploadConnection(this); message.setTransferable(null); - mXmppConnectionService.markMessage(message,Message.STATUS_SEND_FAILED); + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + FileBackend.close(mFileInputStream); } public void init(Message message, boolean delay) { this.message = message; message.setTransferable(this); - mXmppConnectionService.markMessage(message,Message.STATUS_UNSEND); + mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); this.account = message.getConversation().getAccount(); this.file = mXmppConnectionService.getFileBackend().getFile(message, false); - this.file.setExpectedSize(this.file.getSize()); + this.mime = this.file.getMimeType(); this.delayed = delay; - if (Config.ENCRYPT_ON_HTTP_UPLOADED || message.getEncryption() == Message.ENCRYPTION_AXOLOTL || message.getEncryption() == Message.ENCRYPTION_OTR) { this.key = new byte[48]; mXmppConnectionService.getRNG().nextBytes(this.key); - this.file.setKey(this.key); + this.file.setKeyAndIv(this.key); } - + Pair<InputStream,Integer> pair = AbstractConnectionManager.createInputStream(file,true); + this.file.setExpectedSize(pair.second); + this.mFileInputStream = pair.first; Jid host = account.getXmppConnection().findDiscoItemByFeature(Xmlns.HTTP_UPLOAD); - IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file); + IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file,mime); mXmppConnectionService.sendIqPacket(account, request, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -134,7 +142,6 @@ public class HttpUploadConnection implements Transferable { private void upload() { OutputStream os = null; - InputStream is = null; HttpURLConnection connection = null; try { Log.d(Config.LOGTAG, "uploading to " + mPutUrl.toString()); @@ -144,30 +151,31 @@ public class HttpUploadConnection implements Transferable { } connection.setRequestMethod("PUT"); connection.setFixedLengthStreamingMode((int) file.getExpectedSize()); + connection.setRequestProperty("Content-Type", mime == null ? "application/octet-stream" : mime); connection.setDoOutput(true); connection.connect(); os = connection.getOutputStream(); - is = file.createInputStream(); transmitted = 0; - expected = file.getExpectedSize(); int count = -1; byte[] buffer = new byte[4096]; - while (((count = is.read(buffer)) != -1) && !canceled) { + while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) { transmitted += count; os.write(buffer, 0, count); mXmppConnectionService.updateConversationUi(); } os.flush(); os.close(); - is.close(); + mFileInputStream.close(); int code = connection.getResponseCode(); if (code == 200 || code == 201) { Log.d(Config.LOGTAG, "finished uploading file"); - Message.FileParams params = message.getFileParams(); if (key != null) { mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key)); } mXmppConnectionService.getFileBackend().updateFileParams(message, mGetUrl); + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(file)); + mXmppConnectionService.sendBroadcast(intent); message.setTransferable(null); message.setCounterpart(message.getConversation().getJid().toBareJid()); if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { @@ -188,16 +196,17 @@ public class HttpUploadConnection implements Transferable { } }); } else { - mXmppConnectionService.resendMessage(message,delayed); + mXmppConnectionService.resendMessage(message, delayed); } } else { fail(); } } catch (IOException e) { - Log.d(Config.LOGTAG, e.getMessage()); + e.printStackTrace(); + Log.d(Config.LOGTAG,"http upload failed "+e.getMessage()); fail(); } finally { - FileBackend.close(is); + FileBackend.close(mFileInputStream); FileBackend.close(os); if (connection != null) { connection.disconnect(); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 6e7b3276d..5786487f2 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -73,11 +73,9 @@ public class MessageParser extends AbstractParser implements body = otrSession.transformReceiving(body); SessionStatus status = otrSession.getSessionStatus(); if (body == null && status == SessionStatus.ENCRYPTED) { - conversation.setNextEncryption(Message.ENCRYPTION_OTR); mXmppConnectionService.onOtrSessionEstablished(conversation); return null; } else if (body == null && status == SessionStatus.FINISHED) { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); conversation.resetOtrSession(); mXmppConnectionService.updateConversationUi(); return null; @@ -101,8 +99,8 @@ public class MessageParser extends AbstractParser implements private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) { Message finishedMessage = null; AxolotlService service = conversation.getAccount().getAxolotlService(); - XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(from.toBareJid(), axolotlMessage); - XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); + XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid()); + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage); if(plaintextMessage != null) { finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status); finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint()); @@ -202,6 +200,13 @@ public class MessageParser extends AbstractParser implements if (packet.getType() == MessagePacket.TYPE_ERROR) { Jid from = packet.getFrom(); if (from != null) { + Element error = packet.findChild("error"); + String text = error == null ? null : error.findChildContent("text"); + if (text != null) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + text); + } else if (error != null) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + error); + } Message message = mXmppConnectionService.markMessage(account, from.toBareJid(), packet.getId(), @@ -223,6 +228,7 @@ public class MessageParser extends AbstractParser implements final MessagePacket packet; Long timestamp = null; final boolean isForwarded; + boolean isCarbon = false; String serverMsgId = null; final Element fin = original.findChild("fin", "urn:xmpp:mam:0"); if (fin != null) { @@ -253,7 +259,8 @@ public class MessageParser extends AbstractParser implements return; } timestamp = f != null ? f.second : null; - isForwarded = f != null; + isCarbon = f != null; + isForwarded = isCarbon; } else { packet = original; isForwarded = false; @@ -265,7 +272,7 @@ public class MessageParser extends AbstractParser implements final String body = packet.getBody(); final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user"); final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); - final Element axolotlEncrypted = packet.findChild("axolotl_message", AxolotlService.PEP_PREFIX); + final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); int status; final Jid counterpart; final Jid to = packet.getTo(); @@ -339,6 +346,7 @@ public class MessageParser extends AbstractParser implements message.setCounterpart(counterpart); message.setRemoteMsgId(remoteMsgId); message.setServerMsgId(serverMsgId); + message.setCarbon(isCarbon); message.setTime(timestamp); message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0"); if (conversation.getMode() == Conversation.MODE_MULTI) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 3120c0085..9fe47512b 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -27,6 +27,8 @@ import java.util.concurrent.CopyOnWriteArrayList; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -40,7 +42,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 15; + private static final int DATABASE_VERSION = 16; private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -55,56 +57,56 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Contact.JID + ") ON CONFLICT REPLACE);"; private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE " - + AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME + "(" - + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + AxolotlService.SQLiteAxolotlStore.ID + " INTEGER, " - + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + AxolotlService.SQLiteAxolotlStore.ACCOUNT + + SQLiteAxolotlStore.PREKEY_TABLENAME + "(" + + SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + SQLiteAxolotlStore.ID + " INTEGER, " + + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + SQLiteAxolotlStore.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " - + AxolotlService.SQLiteAxolotlStore.ID + + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " + + SQLiteAxolotlStore.ID + ") ON CONFLICT REPLACE" +");"; private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE " - + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "(" - + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + AxolotlService.SQLiteAxolotlStore.ID + " INTEGER, " - + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + AxolotlService.SQLiteAxolotlStore.ACCOUNT + + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "(" + + SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + SQLiteAxolotlStore.ID + " INTEGER, " + + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + SQLiteAxolotlStore.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " - + AxolotlService.SQLiteAxolotlStore.ID + + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " + + SQLiteAxolotlStore.ID + ") ON CONFLICT REPLACE"+ ");"; private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE " - + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME + "(" - + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " - + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " - + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + AxolotlService.SQLiteAxolotlStore.ACCOUNT + + SQLiteAxolotlStore.SESSION_TABLENAME + "(" + + SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + SQLiteAxolotlStore.NAME + " TEXT, " + + SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " + + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + SQLiteAxolotlStore.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " - + AxolotlService.SQLiteAxolotlStore.NAME + ", " - + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " + + SQLiteAxolotlStore.NAME + ", " + + SQLiteAxolotlStore.DEVICE_ID + ") ON CONFLICT REPLACE" +");"; private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE " - + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME + "(" - + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " - + AxolotlService.SQLiteAxolotlStore.OWN + " INTEGER, " - + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " TEXT, " - + AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, " - + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + AxolotlService.SQLiteAxolotlStore.ACCOUNT + + SQLiteAxolotlStore.IDENTITIES_TABLENAME + "(" + + SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + SQLiteAxolotlStore.NAME + " TEXT, " + + SQLiteAxolotlStore.OWN + " INTEGER, " + + SQLiteAxolotlStore.FINGERPRINT + " TEXT, " + + SQLiteAxolotlStore.TRUSTED + " INTEGER, " + + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + SQLiteAxolotlStore.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " - + AxolotlService.SQLiteAxolotlStore.NAME + ", " - + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " + + SQLiteAxolotlStore.NAME + ", " + + SQLiteAxolotlStore.FINGERPRINT + ") ON CONFLICT IGNORE" +");"; @@ -139,6 +141,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Message.RELATIVE_FILE_PATH + " TEXT, " + Message.SERVER_MSG_ID + " TEXT, " + Message.FINGERPRINT + " TEXT, " + + Message.CARBON + " INTEGER, " + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.CONVERSATION + ") REFERENCES " + Conversation.TABLENAME + "(" + Conversation.UUID @@ -294,6 +297,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.FINGERPRINT + " TEXT"); } + if (oldVersion < 16 && newVersion >= 16) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.CARBON + " INTEGER"); + } } public static synchronized DatabaseBackend getInstance(Context context) { @@ -567,11 +574,11 @@ public class DatabaseBackend extends SQLiteOpenHelper { String[] selectionArgs = {account.getUuid(), contact.getName(), Integer.toString(contact.getDeviceId())}; - Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, + Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME, columns, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " = ? ", + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.NAME + " = ? AND " + + SQLiteAxolotlStore.DEVICE_ID + " = ? ", selectionArgs, null, null, null); @@ -584,7 +591,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { if(cursor.getCount() != 0) { cursor.moveToFirst(); try { - session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); + session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (IOException e) { cursor.close(); throw new AssertionError(e); @@ -597,19 +604,19 @@ public class DatabaseBackend extends SQLiteOpenHelper { public List<Integer> getSubDeviceSessions(Account account, AxolotlAddress contact) { List<Integer> devices = new ArrayList<>(); final SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {AxolotlService.SQLiteAxolotlStore.DEVICE_ID}; + String[] columns = {SQLiteAxolotlStore.DEVICE_ID}; String[] selectionArgs = {account.getUuid(), contact.getName()}; - Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, + Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME, columns, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ?", + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.NAME + " = ?", selectionArgs, null, null, null); while(cursor.moveToNext()) { devices.add(cursor.getInt( - cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.DEVICE_ID))); + cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID))); } cursor.close(); @@ -626,11 +633,11 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); - values.put(AxolotlService.SQLiteAxolotlStore.NAME, contact.getName()); - values.put(AxolotlService.SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); - values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(),Base64.DEFAULT)); - values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - db.insert(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, null, values); + values.put(SQLiteAxolotlStore.NAME, contact.getName()); + values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); + values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(),Base64.DEFAULT)); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values); } public void deleteSession(Account account, AxolotlAddress contact) { @@ -638,30 +645,30 @@ public class DatabaseBackend extends SQLiteOpenHelper { String[] args = {account.getUuid(), contact.getName(), Integer.toString(contact.getDeviceId())}; - db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " = ? ", + db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.NAME + " = ? AND " + + SQLiteAxolotlStore.DEVICE_ID + " = ? ", args); } public void deleteAllSessions(Account account, AxolotlAddress contact) { SQLiteDatabase db = this.getWritableDatabase(); String[] args = {account.getUuid(), contact.getName()}; - db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ?", + db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + + SQLiteAxolotlStore.NAME + " = ?", args); } private Cursor getCursorForPreKey(Account account, int preKeyId) { SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; + String[] columns = {SQLiteAxolotlStore.KEY}; String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)}; - Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, + Cursor cursor = db.query(SQLiteAxolotlStore.PREKEY_TABLENAME, columns, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " - + AxolotlService.SQLiteAxolotlStore.ID + "=?", + SQLiteAxolotlStore.ACCOUNT + "=? AND " + + SQLiteAxolotlStore.ID + "=?", selectionArgs, null, null, null); @@ -674,7 +681,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { if(cursor.getCount() != 0) { cursor.moveToFirst(); try { - record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); + record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (IOException e ) { throw new AssertionError(e); } @@ -693,28 +700,28 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void storePreKey(Account account, PreKeyRecord record) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); - values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId()); - values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT)); - values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - db.insert(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, null, values); + values.put(SQLiteAxolotlStore.ID, record.getId()); + values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT)); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values); } public void deletePreKey(Account account, int preKeyId) { SQLiteDatabase db = this.getWritableDatabase(); String[] args = {account.getUuid(), Integer.toString(preKeyId)}; - db.delete(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " - + AxolotlService.SQLiteAxolotlStore.ID + "=?", + db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + + SQLiteAxolotlStore.ID + "=?", args); } private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) { SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; + String[] columns = {SQLiteAxolotlStore.KEY}; String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)}; - Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, columns, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + AxolotlService.SQLiteAxolotlStore.ID + "=?", + SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?", selectionArgs, null, null, null); @@ -727,7 +734,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { if(cursor.getCount() != 0) { cursor.moveToFirst(); try { - record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); + record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (IOException e ) { throw new AssertionError(e); } @@ -739,17 +746,17 @@ public class DatabaseBackend extends SQLiteOpenHelper { public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) { List<SignedPreKeyRecord> prekeys = new ArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; + String[] columns = {SQLiteAxolotlStore.KEY}; String[] selectionArgs = {account.getUuid()}; - Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, columns, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=?", + SQLiteAxolotlStore.ACCOUNT + "=?", selectionArgs, null, null, null); while(cursor.moveToNext()) { try { - prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)), Base64.DEFAULT))); + prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT))); } catch (IOException ignored) { } } @@ -767,18 +774,18 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void storeSignedPreKey(Account account, SignedPreKeyRecord record) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); - values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId()); - values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT)); - values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - db.insert(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values); + values.put(SQLiteAxolotlStore.ID, record.getId()); + values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT)); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values); } public void deleteSignedPreKey(Account account, int signedPreKeyId) { SQLiteDatabase db = this.getWritableDatabase(); String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)}; - db.delete(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " - + AxolotlService.SQLiteAxolotlStore.ID + "=?", + db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + + SQLiteAxolotlStore.ID + "=?", args); } @@ -792,24 +799,24 @@ public class DatabaseBackend extends SQLiteOpenHelper { private Cursor getIdentityKeyCursor(Account account, String name, Boolean own, String fingerprint) { final SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {AxolotlService.SQLiteAxolotlStore.TRUSTED, - AxolotlService.SQLiteAxolotlStore.KEY}; + String[] columns = {SQLiteAxolotlStore.TRUSTED, + SQLiteAxolotlStore.KEY}; ArrayList<String> selectionArgs = new ArrayList<>(4); selectionArgs.add(account.getUuid()); - String selectionString = AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?"; + String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?"; if (name != null){ selectionArgs.add(name); - selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.NAME + " = ?"; + selectionString += " AND " + SQLiteAxolotlStore.NAME + " = ?"; } if (fingerprint != null){ selectionArgs.add(fingerprint); - selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ?"; + selectionString += " AND " + SQLiteAxolotlStore.FINGERPRINT + " = ?"; } if (own != null){ selectionArgs.add(own?"1":"0"); - selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.OWN + " = ?"; + selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?"; } - Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, + Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, columns, selectionString, selectionArgs.toArray(new String[selectionArgs.size()]), @@ -824,7 +831,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { if(cursor.getCount() != 0) { cursor.moveToFirst(); try { - identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); + identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (InvalidKeyException e) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); } @@ -838,18 +845,18 @@ public class DatabaseBackend extends SQLiteOpenHelper { return loadIdentityKeys(account, name, null); } - public Set<IdentityKey> loadIdentityKeys(Account account, String name, AxolotlService.SQLiteAxolotlStore.Trust trust) { + public Set<IdentityKey> loadIdentityKeys(Account account, String name, XmppAxolotlSession.Trust trust) { Set<IdentityKey> identityKeys = new HashSet<>(); Cursor cursor = getIdentityKeyCursor(account, name, false); while(cursor.moveToNext()) { if ( trust != null && - cursor.getInt(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.TRUSTED)) + cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED)) != trust.getCode()) { continue; } try { - identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0)); + identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0)); } catch (InvalidKeyException e) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name); } @@ -864,55 +871,55 @@ public class DatabaseBackend extends SQLiteOpenHelper { String[] args = { account.getUuid(), name, - String.valueOf(AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED.getCode()) + String.valueOf(XmppAxolotlSession.Trust.TRUSTED.getCode()) }; - return DatabaseUtils.queryNumEntries(db, AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?" - + " AND " + AxolotlService.SQLiteAxolotlStore.NAME + " = ?" - + " AND " + AxolotlService.SQLiteAxolotlStore.TRUSTED + " = ?", + return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?" + + " AND " + SQLiteAxolotlStore.NAME + " = ?" + + " AND " + SQLiteAxolotlStore.TRUSTED + " = ?", args ); } private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) { - storeIdentityKey(account, name, own, fingerprint, base64Serialized, AxolotlService.SQLiteAxolotlStore.Trust.UNDECIDED); + storeIdentityKey(account, name, own, fingerprint, base64Serialized, XmppAxolotlSession.Trust.UNDECIDED); } - private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, AxolotlService.SQLiteAxolotlStore.Trust trusted) { + private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, XmppAxolotlSession.Trust trusted) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); - values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - values.put(AxolotlService.SQLiteAxolotlStore.NAME, name); - values.put(AxolotlService.SQLiteAxolotlStore.OWN, own ? 1 : 0); - values.put(AxolotlService.SQLiteAxolotlStore.FINGERPRINT, fingerprint); - values.put(AxolotlService.SQLiteAxolotlStore.KEY, base64Serialized); - values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trusted.getCode()); - db.insert(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + values.put(SQLiteAxolotlStore.NAME, name); + values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0); + values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint); + values.put(SQLiteAxolotlStore.KEY, base64Serialized); + values.put(SQLiteAxolotlStore.TRUSTED, trusted.getCode()); + db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); } - public AxolotlService.SQLiteAxolotlStore.Trust isIdentityKeyTrusted(Account account, String fingerprint) { + public XmppAxolotlSession.Trust isIdentityKeyTrusted(Account account, String fingerprint) { Cursor cursor = getIdentityKeyCursor(account, fingerprint); - AxolotlService.SQLiteAxolotlStore.Trust trust = null; + XmppAxolotlSession.Trust trust = null; if (cursor.getCount() > 0) { cursor.moveToFirst(); - int trustValue = cursor.getInt(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.TRUSTED)); - trust = AxolotlService.SQLiteAxolotlStore.Trust.fromCode(trustValue); + int trustValue = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED)); + trust = XmppAxolotlSession.Trust.fromCode(trustValue); } cursor.close(); return trust; } - public boolean setIdentityKeyTrust(Account account, String fingerprint, AxolotlService.SQLiteAxolotlStore.Trust trust) { + public boolean setIdentityKeyTrust(Account account, String fingerprint, XmppAxolotlSession.Trust trust) { SQLiteDatabase db = this.getWritableDatabase(); String[] selectionArgs = { account.getUuid(), fingerprint }; ContentValues values = new ContentValues(); - values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trust.getCode()); - int rows = db.update(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? ", + values.put(SQLiteAxolotlStore.TRUSTED, trust.getCode()); + int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.FINGERPRINT + " = ? ", selectionArgs); return rows == 1; } @@ -922,7 +929,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) { - storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED); + storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), XmppAxolotlSession.Trust.TRUSTED); } public void recreateAxolotlDb() { @@ -931,13 +938,13 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void recreateAxolotlDb(SQLiteDatabase db) { Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+">>> (RE)CREATING AXOLOTL DATABASE <<<"); - db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME); + db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME); db.execSQL(CREATE_SESSIONS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME); + db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME); db.execSQL(CREATE_PREKEYS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); + db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME); + db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.IDENTITIES_TABLENAME); db.execSQL(CREATE_IDENTITIES_STATEMENT); } @@ -948,17 +955,17 @@ public class DatabaseBackend extends SQLiteOpenHelper { String[] deleteArgs= { accountName }; - db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", + db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?", deleteArgs); - db.delete(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", + db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?", deleteArgs); - db.delete(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", + db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?", deleteArgs); - db.delete(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", + db.delete(SQLiteAxolotlStore.IDENTITIES_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?", deleteArgs); } } diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java index 676a09c97..b7e7c8d3a 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -1,5 +1,33 @@ package eu.siacs.conversations.services; +import android.util.Log; +import android.util.Pair; + +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.DownloadableFile; + public class AbstractConnectionManager { protected XmppConnectionService mXmppConnectionService; @@ -20,4 +48,73 @@ public class AbstractConnectionManager { return 524288; } } + + public static Pair<InputStream,Integer> createInputStream(DownloadableFile file, boolean gcm) { + FileInputStream is; + int size; + try { + is = new FileInputStream(file); + size = (int) file.getSize(); + if (file.getKey() == null) { + return new Pair<InputStream,Integer>(is,size); + } + } catch (FileNotFoundException e) { + return null; + } + try { + if (gcm) { + AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); + cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv())); + InputStream cis = new org.bouncycastle.crypto.io.CipherInputStream(is, cipher); + return new Pair<>(cis, cipher.getOutputSize(size)); + } else { + IvParameterSpec ips = new IvParameterSpec(file.getIv()); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips); + Log.d(Config.LOGTAG, "opening encrypted input stream"); + return new Pair<InputStream,Integer>(new CipherInputStream(is, cipher),(size / 16 + 1) * 16); + } + } catch (InvalidKeyException e) { + return null; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (NoSuchPaddingException e) { + return null; + } catch (InvalidAlgorithmParameterException e) { + return null; + } + } + + public static OutputStream createOutputStream(DownloadableFile file, boolean gcm) { + FileOutputStream os; + try { + os = new FileOutputStream(file); + if (file.getKey() == null) { + return os; + } + } catch (FileNotFoundException e) { + return null; + } + try { + if (gcm) { + AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); + cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv())); + return new org.bouncycastle.crypto.io.CipherOutputStream(os, cipher); + } else { + IvParameterSpec ips = new IvParameterSpec(file.getIv()); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips); + Log.d(Config.LOGTAG, "opening encrypted output stream"); + return new CipherOutputStream(os, cipher); + } + } catch (InvalidKeyException e) { + return null; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (NoSuchPaddingException e) { + return null; + } catch (InvalidAlgorithmParameterException e) { + return null; + } + } } diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index b99069a64..7dcf2e83a 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -330,9 +330,10 @@ public class NotificationService { private Message getImage(final Iterable<Message> messages) { for (final Message message : messages) { - if (message.getType() == Message.TYPE_IMAGE + if (message.getType() != Message.TYPE_TEXT && message.getTransferable() == null - && message.getEncryption() != Message.ENCRYPTION_PGP) { + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getFileParams().height > 0) { return message; } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index fa0a87096..ffe587d69 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -349,7 +349,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void attachLocationToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) { - int encryption = conversation.getNextEncryption(forceEncryption()); + int encryption = conversation.getNextEncryption(); if (encryption == Message.ENCRYPTION_PGP) { encryption = Message.ENCRYPTION_DECRYPTED; } @@ -368,12 +368,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa final Uri uri, final UiCallback<Message> callback) { final Message message; - if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", - Message.ENCRYPTION_DECRYPTED); + if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { + message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); } else { - message = new Message(conversation, "", - conversation.getNextEncryption(forceEncryption())); + message = new Message(conversation, "", conversation.getNextEncryption()); } message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_FILE); @@ -409,12 +407,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) { final Message message; - if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", - Message.ENCRYPTION_DECRYPTED); + if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { + message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); } else { - message = new Message(conversation, "", - conversation.getNextEncryption(forceEncryption())); + message = new Message(conversation, "",conversation.getNextEncryption()); } message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_IMAGE); @@ -424,7 +420,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void run() { try { getFileBackend().copyImageToPrivateStorage(message, uri); - if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { + if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { getPgpEngine().encrypt(message, callback); } else { callback.success(message); @@ -759,6 +755,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } break; case Message.ENCRYPTION_AXOLOTL: + message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); if (message.needsUploading()) { if (account.httpUploadAvailable() || message.fixCounterpart()) { this.sendFileMessage(message,delay); @@ -768,8 +765,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } else { XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message); if (axolotlMessage == null) { - account.getAxolotlService().prepareMessage(message,delay); - message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); + account.getAxolotlService().preparePayloadMessage(message, delay); } else { packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage); } @@ -2533,8 +2529,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } for (final Message msg : messages) { + msg.setTime(System.currentTimeMillis()); markMessage(msg, Message.STATUS_WAITING); - this.resendMessage(msg,true); + this.resendMessage(msg,false); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 435293582..02b8962c5 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -111,6 +111,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd private LinearLayout keys; private LinearLayout tags; private boolean showDynamicTags; + private String messageFingerprint; private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() { @@ -193,6 +194,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } catch (final InvalidJidException ignored) { } } + this.messageFingerprint = getIntent().getStringExtra("fingerprint"); setContentView(R.layout.activity_contact_details); contactJidTv = (TextView) findViewById(R.id.details_contactjid); @@ -386,7 +388,8 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys( contact.getAccount(), contact.getJid().toBareJid().toString())) { - hasKeys |= addFingerprintRow(keys, contact.getAccount(), identityKey); + boolean highlight = identityKey.getFingerprint().replaceAll("\\s", "").equals(messageFingerprint); + hasKeys |= addFingerprintRow(keys, contact.getAccount(), identityKey, highlight); } if (contact.getPgpKeyId() != 0) { hasKeys = true; diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 7f7dcd617..e9b361f4f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -39,7 +39,7 @@ import de.timroes.android.listview.EnhancedListView; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.axolotl.AxolotlService.SQLiteAxolotlStore.Trust; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Contact; @@ -402,7 +402,7 @@ public class ConversationActivity extends XmppActivity } else { menuAdd.setVisible(!isConversationsOverviewHideable()); if (this.getSelectedConversation() != null) { - if (this.getSelectedConversation().getNextEncryption(forceEncryption()) != Message.ENCRYPTION_NONE) { + if (this.getSelectedConversation().getNextEncryption() != Message.ENCRYPTION_NONE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { menuSecure.setIcon(R.drawable.ic_lock_white_24dp); } else { @@ -515,7 +515,7 @@ public class ConversationActivity extends XmppActivity break; } final Conversation conversation = getSelectedConversation(); - final int encryption = conversation.getNextEncryption(forceEncryption()); + final int encryption = conversation.getNextEncryption(); if (encryption == Message.ENCRYPTION_PGP) { if (hasPgp()) { if (conversation.getContact().getPgpKeyId() != 0) { @@ -792,6 +792,7 @@ public class ConversationActivity extends XmppActivity xmppConnectionService.databaseBackend.updateConversation(conversation); fragment.updateChatMsgHint(); invalidateOptionsMenu(); + refreshUi(); return true; } }); @@ -803,15 +804,10 @@ public class ConversationActivity extends XmppActivity if (conversation.getMode() == Conversation.MODE_MULTI) { otr.setEnabled(false); axolotl.setEnabled(false); - } else { - if (forceEncryption()) { - none.setVisible(false); - } - } - if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) { + } else if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) { axolotl.setEnabled(false); } - switch (conversation.getNextEncryption(forceEncryption())) { + switch (conversation.getNextEncryption()) { case Message.ENCRYPTION_NONE: none.setChecked(true); break; @@ -822,8 +818,7 @@ public class ConversationActivity extends XmppActivity pgp.setChecked(true); break; case Message.ENCRYPTION_AXOLOTL: - popup.getMenu().findItem(R.id.encryption_choice_axolotl) - .setChecked(true); + axolotl.setChecked(true); break; default: none.setChecked(true); @@ -836,8 +831,7 @@ public class ConversationActivity extends XmppActivity protected void muteConversationDialog(final Conversation conversation) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.disable_notifications); - final int[] durations = getResources().getIntArray( - R.array.mute_options_durations); + final int[] durations = getResources().getIntArray(R.array.mute_options_durations); builder.setItems(R.array.mute_options_descriptions, new OnClickListener() { @@ -1269,10 +1263,6 @@ public class ConversationActivity extends XmppActivity }); } - public boolean forceEncryption() { - return getPreferences().getBoolean("force_encryption", false); - } - public boolean useSendButtonToIndicateStatus() { return getPreferences().getBoolean("send_button_status", false); } @@ -1287,12 +1277,12 @@ public class ConversationActivity extends XmppActivity protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) { AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService(); - boolean hasPendingKeys = !axolotlService.getKeysWithTrust(Trust.UNDECIDED, + boolean hasPendingKeys = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, mSelectedConversation.getContact()).isEmpty() || !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty(); boolean hasNoTrustedKeys = axolotlService.getNumTrustedKeys(mSelectedConversation.getContact()) == 0; if( hasPendingKeys || hasNoTrustedKeys) { - axolotlService.createSessionsIfNeeded(mSelectedConversation, false); + axolotlService.createSessionsIfNeeded(mSelectedConversation); Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class); intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString()); intent.putExtra("account", mSelectedConversation.getAccount().getJid().toBareJid().toString()); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 80f63d812..79cb006d1 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -293,23 +293,27 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (body.length() == 0 || this.conversation == null) { return; } - Message message = new Message(conversation, body, conversation.getNextEncryption(activity.forceEncryption())); + Message message = new Message(conversation, body, conversation.getNextEncryption()); if (conversation.getMode() == Conversation.MODE_MULTI) { if (conversation.getNextCounterpart() != null) { message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_PRIVATE); } } - if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) { - sendOtrMessage(message); - } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) { - sendPgpMessage(message); - } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_AXOLOTL) { - if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) { - sendAxolotlMessage(message); - } - } else { - sendPlainTextMessage(message); + switch (conversation.getNextEncryption()) { + case Message.ENCRYPTION_OTR: + sendOtrMessage(message); + break; + case Message.ENCRYPTION_PGP: + sendPgpMessage(message); + break; + case Message.ENCRYPTION_AXOLOTL: + if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) { + sendAxolotlMessage(message); + } + break; + default: + sendPlainTextMessage(message); } } @@ -320,7 +324,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa R.string.send_private_message_to, conversation.getNextCounterpart().getResourcepart())); } else { - switch (conversation.getNextEncryption(activity.forceEncryption())) { + switch (conversation.getNextEncryption()) { case Message.ENCRYPTION_NONE: mEditMessage .setHint(getString(R.string.send_plain_text_message)); @@ -392,12 +396,13 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa highlightInConference(user); } } else { - activity.switchToContactDetails(message.getContact()); + activity.switchToContactDetails(message.getContact(), message.getAxolotlFingerprint()); } } else { Account account = message.getConversation().getAccount(); Intent intent = new Intent(activity, EditAccountActivity.class); intent.putExtra("jid", account.getJid().toBareJid().toString()); + intent.putExtra("fingerprint", message.getAxolotlFingerprint()); startActivity(intent); } } @@ -832,7 +837,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } catch (final NoSuchElementException ignored) { } - activity.xmppConnectionService.updateConversationUi(); + activity.refreshUi(); } }); } @@ -1210,11 +1215,11 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (resultCode == Activity.RESULT_OK) { if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) { final String body = mEditMessage.getText().toString(); - Message message = new Message(conversation, body, conversation.getNextEncryption(activity.forceEncryption())); + Message message = new Message(conversation, body, conversation.getNextEncryption()); sendAxolotlMessage(message); } else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) { int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID); - activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption(activity.forceEncryption())); + activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption()); } } } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index f65485b11..cd4f48d0f 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -74,6 +74,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate private Jid jidToEdit; private Account mAccount; + private String messageFingerprint; private boolean mFetchingAvatar = false; @@ -388,6 +389,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } catch (final InvalidJidException | NullPointerException ignored) { this.jidToEdit = null; } + this.messageFingerprint = getIntent().getStringExtra("fingerprint"); if (this.jidToEdit != null) { this.mRegisterNew.setVisibility(View.GONE); if (getActionBar() != null) { @@ -571,7 +573,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate if(ownKey.equals(identityKey)) { continue; } - hasKeys |= addFingerprintRow(keys, mAccount, identityKey); + boolean highlight = identityKey.getFingerprint().replaceAll("\\s", "").equals(messageFingerprint); + hasKeys |= addFingerprintRow(keys, mAccount, identityKey, highlight); } if (hasKeys) { keysCard.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java index 1bf07f3ea..37ddf5906 100644 --- a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java @@ -16,7 +16,7 @@ import java.util.Map; import java.util.Set; import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.AxolotlService.SQLiteAxolotlStore.Trust; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -118,8 +118,8 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate boolean hasForeignKeys = false; for(final IdentityKey identityKey : ownKeysToTrust.keySet()) { hasOwnKeys = true; - addFingerprintRowWithListeners(ownKeys, contact.getAccount(), identityKey, - Trust.fromBoolean(ownKeysToTrust.get(identityKey)), false, + addFingerprintRowWithListeners(ownKeys, contact.getAccount(), identityKey, false, + XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(identityKey)), false, new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @@ -134,8 +134,8 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate } for(final IdentityKey identityKey : foreignKeysToTrust.keySet()) { hasForeignKeys = true; - addFingerprintRowWithListeners(foreignKeys, contact.getAccount(), identityKey, - Trust.fromBoolean(foreignKeysToTrust.get(identityKey)), false, + addFingerprintRowWithListeners(foreignKeys, contact.getAccount(), identityKey, false, + XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(identityKey)), false, new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @@ -171,11 +171,11 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate } private void getFingerprints(final Account account) { - Set<IdentityKey> ownKeysSet = account.getAxolotlService().getKeysWithTrust(Trust.UNDECIDED); - Set<IdentityKey> foreignKeysSet = account.getAxolotlService().getKeysWithTrust(Trust.UNDECIDED, contact); + Set<IdentityKey> ownKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED); + Set<IdentityKey> foreignKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, contact); if (hasNoTrustedKeys) { - ownKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(Trust.UNTRUSTED)); - foreignKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(Trust.UNTRUSTED, contact)); + ownKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED)); + foreignKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, contact)); } for(final IdentityKey identityKey : ownKeysSet) { if(!ownKeysToTrust.containsKey(identityKey)) { @@ -226,12 +226,12 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate for(IdentityKey identityKey:ownKeysToTrust.keySet()) { contact.getAccount().getAxolotlService().setFingerprintTrust( identityKey.getFingerprint().replaceAll("\\s", ""), - Trust.fromBoolean(ownKeysToTrust.get(identityKey))); + XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(identityKey))); } for(IdentityKey identityKey:foreignKeysToTrust.keySet()) { contact.getAccount().getAxolotlService().setFingerprintTrust( identityKey.getFingerprint().replaceAll("\\s", ""), - Trust.fromBoolean(foreignKeysToTrust.get(identityKey))); + XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(identityKey))); } } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 835685107..3a163ba45 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -70,7 +70,7 @@ import java.util.concurrent.RejectedExecutionException; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -351,7 +351,7 @@ public abstract class XmppActivity extends Activity { mPrimaryTextColor = getResources().getColor(R.color.black87); mSecondaryTextColor = getResources().getColor(R.color.black54); mTertiaryTextColor = getResources().getColor(R.color.black12); - mColorRed = getResources().getColor(R.color.red500); + mColorRed = getResources().getColor(R.color.red800); mColorOrange = getResources().getColor(R.color.orange500); mColorGreen = getResources().getColor(R.color.green500); mPrimaryColor = getResources().getColor(R.color.green500); @@ -424,10 +424,15 @@ public abstract class XmppActivity extends Activity { } public void switchToContactDetails(Contact contact) { + switchToContactDetails(contact, null); + } + + public void switchToContactDetails(Contact contact, String messageFingerprint) { Intent intent = new Intent(this, ContactDetailsActivity.class); intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); intent.putExtra("account", contact.getAccount().getJid().toBareJid().toString()); intent.putExtra("contact", contact.getJid().toString()); + intent.putExtra("fingerprint", messageFingerprint); startActivity(intent); } @@ -608,32 +613,25 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } - protected boolean addFingerprintRow(LinearLayout keys, final Account account, IdentityKey identityKey) { + protected boolean addFingerprintRow(LinearLayout keys, final Account account, IdentityKey identityKey, boolean highlight) { final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); - final AxolotlService.SQLiteAxolotlStore.Trust trust = account.getAxolotlService() + final XmppAxolotlSession.Trust trust = account.getAxolotlService() .getFingerprintTrust(fingerprint); - return addFingerprintRowWithListeners(keys, account, identityKey, trust, true, + return addFingerprintRowWithListeners(keys, account, identityKey, highlight, trust, true, new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked != (trust == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED)) { - account.getAxolotlService().setFingerprintTrust(fingerprint, - (isChecked) ? AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED : - AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); - } - refreshUi(); - xmppConnectionService.updateAccountUi(); - xmppConnectionService.updateConversationUi(); + account.getAxolotlService().setFingerprintTrust(fingerprint, + (isChecked) ? XmppAxolotlSession.Trust.TRUSTED : + XmppAxolotlSession.Trust.UNTRUSTED); } }, new View.OnClickListener() { @Override public void onClick(View v) { account.getAxolotlService().setFingerprintTrust(fingerprint, - AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); - refreshUi(); - xmppConnectionService.updateAccountUi(); - xmppConnectionService.updateConversationUi(); + XmppAxolotlSession.Trust.UNTRUSTED); + v.setEnabled(true); } } @@ -641,13 +639,14 @@ public abstract class XmppActivity extends Activity { } protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account, - final IdentityKey identityKey, - AxolotlService.SQLiteAxolotlStore.Trust trust, - boolean showTag, - CompoundButton.OnCheckedChangeListener - onCheckedChangeListener, - View.OnClickListener onClickListener) { - if (trust == AxolotlService.SQLiteAxolotlStore.Trust.COMPROMISED) { + final IdentityKey identityKey, + boolean highlight, + XmppAxolotlSession.Trust trust, + boolean showTag, + CompoundButton.OnCheckedChangeListener + onCheckedChangeListener, + View.OnClickListener onClickListener) { + if (trust == XmppAxolotlSession.Trust.COMPROMISED) { return false; } View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false); @@ -668,7 +667,7 @@ public abstract class XmppActivity extends Activity { switch (trust) { case UNTRUSTED: case TRUSTED: - trustToggle.setChecked(trust == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED, false); + trustToggle.setChecked(trust == XmppAxolotlSession.Trust.TRUSTED, false); trustToggle.setEnabled(true); key.setTextColor(getPrimaryTextColor()); keyType.setTextColor(getSecondaryTextColor()); @@ -679,7 +678,15 @@ public abstract class XmppActivity extends Activity { key.setTextColor(getPrimaryTextColor()); keyType.setTextColor(getSecondaryTextColor()); break; - case INACTIVE: + case INACTIVE_UNTRUSTED: + case INACTIVE_UNDECIDED: + trustToggle.setOnClickListener(null); + trustToggle.setChecked(false, false); + trustToggle.setEnabled(false); + key.setTextColor(getTertiaryTextColor()); + keyType.setTextColor(getTertiaryTextColor()); + break; + case INACTIVE_TRUSTED: trustToggle.setOnClickListener(null); trustToggle.setChecked(true, false); trustToggle.setEnabled(false); @@ -693,6 +700,12 @@ public abstract class XmppActivity extends Activity { } else { keyType.setVisibility(View.GONE); } + if (highlight) { + keyType.setTextColor(getResources().getColor(R.color.accent)); + keyType.setText(getString(R.string.axolotl_fingerprint_selected_message)); + } else { + keyType.setText(getString(R.string.axolotl_fingerprint)); + } key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); keys.addView(view); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 083b78d2d..a3bd18eb4 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -27,7 +27,7 @@ import android.widget.Toast; import java.util.List; import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -96,19 +96,15 @@ public class MessageAdapter extends ArrayAdapter<Message> { return this.getItemViewType(getItem(position)); } - private int getMessageTextColor(Message message) { - int type = this.getItemViewType(message); - + private int getMessageTextColor(int type, boolean primary) { if (type == SENT) { - return activity.getResources().getColor(R.color.black87); - } else if (type == RECEIVED) { - return activity.getResources().getColor(R.color.white); + return activity.getResources().getColor(primary ? R.color.black87 : R.color.black54); + } else { + return activity.getResources().getColor(primary ? R.color.white : R.color.white70); } - - return activity.getPrimaryTextColor(); } - private void displayStatus(ViewHolder viewHolder, Message message) { + private void displayStatus(ViewHolder viewHolder, Message message, int type) { String filesize = null; String info = null; boolean error = false; @@ -163,24 +159,37 @@ public class MessageAdapter extends ArrayAdapter<Message> { } break; } - if (error) { + if (error && type == SENT) { viewHolder.time.setTextColor(activity.getWarningTextColor()); } else { - viewHolder.time.setTextColor(this.getMessageTextColor(message)); + viewHolder.time.setTextColor(this.getMessageTextColor(type,false)); } if (message.getEncryption() == Message.ENCRYPTION_NONE) { viewHolder.indicator.setVisibility(View.GONE); } else { viewHolder.indicator.setVisibility(View.VISIBLE); if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - AxolotlService.SQLiteAxolotlStore.Trust trust = message.getConversation() + XmppAxolotlSession.Trust trust = message.getConversation() .getAccount().getAxolotlService().getFingerprintTrust( message.getAxolotlFingerprint()); - if(trust == null || trust != AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED) { - viewHolder.indicator.setColorFilter(Color.RED); + if(trust == null || trust != XmppAxolotlSession.Trust.TRUSTED) { + viewHolder.indicator.setColorFilter(activity.getWarningTextColor()); + viewHolder.indicator.setAlpha(1.0f); } else { viewHolder.indicator.clearColorFilter(); + if (type == SENT) { + viewHolder.indicator.setAlpha(0.57f); + } else { + viewHolder.indicator.setAlpha(0.7f); + } + } + } else { + viewHolder.indicator.clearColorFilter(); + if (type == SENT) { + viewHolder.indicator.setAlpha(0.57f); + } else { + viewHolder.indicator.setAlpha(0.7f); } } } @@ -214,19 +223,19 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } - private void displayInfoMessage(ViewHolder viewHolder, String text) { + private void displayInfoMessage(ViewHolder viewHolder, String text, int type) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); } viewHolder.image.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody.setText(text); - viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor()); + viewHolder.messageBody.setTextColor(getMessageTextColor(type,false)); viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); viewHolder.messageBody.setTextIsSelectable(false); } - private void displayDecryptionFailed(ViewHolder viewHolder) { + private void displayDecryptionFailed(ViewHolder viewHolder, int type) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); } @@ -234,7 +243,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody.setText(getContext().getString( R.string.decryption_failed)); - viewHolder.messageBody.setTextColor(activity.getWarningTextColor()); + viewHolder.messageBody.setTextColor(getMessageTextColor(type,false)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); viewHolder.messageBody.setTextIsSelectable(false); } @@ -252,7 +261,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setText(span); } - private void displayTextMessage(final ViewHolder viewHolder, final Message message) { + private void displayTextMessage(final ViewHolder viewHolder, final Message message, int type) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); } @@ -310,7 +319,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } else { viewHolder.messageBody.setText(""); } - viewHolder.messageBody.setTextColor(this.getMessageTextColor(message)); + viewHolder.messageBody.setTextColor(this.getMessageTextColor(type,true)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); viewHolder.messageBody.setTextIsSelectable(true); } @@ -519,7 +528,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } else if (transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) { displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message))); } else { - displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first); + displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first,type); } } else if (message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { displayImageMessage(viewHolder, message); @@ -531,10 +540,9 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { if (activity.hasPgp()) { - displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message)); + displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message),type); } else { - displayInfoMessage(viewHolder, - activity.getString(R.string.install_openkeychain)); + displayInfoMessage(viewHolder,activity.getString(R.string.install_openkeychain),type); if (viewHolder != null) { viewHolder.message_box .setOnClickListener(new OnClickListener() { @@ -547,7 +555,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { - displayDecryptionFailed(viewHolder); + displayDecryptionFailed(viewHolder,type); } else { if (GeoHelper.isGeoUri(message.getBody())) { displayLocationMessage(viewHolder,message); @@ -556,11 +564,19 @@ public class MessageAdapter extends ArrayAdapter<Message> { } else if (message.treatAsDownloadable() == Message.Decision.MUST) { displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message))); } else { - displayTextMessage(viewHolder, message); + displayTextMessage(viewHolder, message, type); + } + } + + if (type == RECEIVED) { + if(message.isValidInSession()) { + viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received); + } else { + viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning); } } - displayStatus(viewHolder, message); + displayStatus(viewHolder, message, type); return view; } diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java index 36d03b30a..4d0dd3dab 100644 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -38,17 +38,14 @@ public class DNSHelper { public static Bundle getSRVRecord(final Jid jid) throws IOException { final String host = jid.getDomainpart(); String dns[] = client.findDNS(); - - if (dns != null) { - for (String dnsserver : dns) { - InetAddress ip = InetAddress.getByName(dnsserver); - Bundle b = queryDNS(host, ip); - if (b.containsKey("values")) { - return b; - } + for (int i = 0; i < dns.length; ++i) { + InetAddress ip = InetAddress.getByName(dns[i]); + Bundle b = queryDNS(host, ip); + if (b.containsKey("values") || i == dns.length - 1) { + return b; } } - return queryDNS(host, InetAddress.getByName("8.8.8.8")); + return null; } public static Bundle queryDNS(String host, InetAddress dnsServer) { @@ -132,7 +129,6 @@ public class DNSHelper { } catch (SocketTimeoutException e) { bundle.putString("error", "timeout"); } catch (Exception e) { - e.printStackTrace(); bundle.putString("error", "unhandled"); } return bundle; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 7c81d988c..c41c5174e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -164,6 +164,9 @@ public class XmppConnection implements Runnable { } } else { final Bundle result = DNSHelper.getSRVRecord(account.getServer()); + if (result == null) { + throw new IOException("unhandled exception in DNS resolver"); + } final ArrayList<Parcelable> values = result.getParcelableArrayList("values"); if ("timeout".equals(result.getString("error"))) { throw new IOException("timeout in dns"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index e15fc5224..e9ca66b7c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -2,9 +2,11 @@ package eu.siacs.conversations.xmpp.jingle; import android.content.Intent; import android.net.Uri; -import android.os.SystemClock; import android.util.Log; +import android.util.Pair; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -14,13 +16,19 @@ import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.OnMessageCreatedCallback; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jid.Jid; @@ -66,8 +74,13 @@ public class JingleConnection implements Transferable { private boolean acceptedAutomatically = false; + private XmppAxolotlMessage mXmppAxolotlMessage; + private JingleTransport transport = null; + private OutputStream mFileOutputStream; + private InputStream mFileInputStream; + private OnIqPacketReceived responseListener = new OnIqPacketReceived() { @Override @@ -113,6 +126,14 @@ public class JingleConnection implements Transferable { } }; + public InputStream getFileInputStream() { + return this.mFileInputStream; + } + + public OutputStream getFileOutputStream() { + return this.mFileOutputStream; + } + private OnProxyActivated onProxyActivated = new OnProxyActivated() { @Override @@ -194,7 +215,22 @@ public class JingleConnection implements Transferable { mXmppConnectionService.sendIqPacket(account,response,null); } - public void init(Message message) { + public void init(final Message message) { + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + Conversation conversation = message.getConversation(); + conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation.getContact(), new OnMessageCreatedCallback() { + @Override + public void run(XmppAxolotlMessage xmppAxolotlMessage) { + init(message, xmppAxolotlMessage); + } + }); + } else { + init(message, null); + } + } + + private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) { + this.mXmppAxolotlMessage = xmppAxolotlMessage; this.contentCreator = "initiator"; this.contentName = this.mJingleConnectionManager.nextRandomId(); this.message = message; @@ -238,8 +274,7 @@ public class JingleConnection implements Transferable { }); mergeCandidate(candidate); } else { - Log.d(Config.LOGTAG, - "no primary candidate of our own was found"); + Log.d(Config.LOGTAG,"no primary candidate of our own was found"); sendInitRequest(); } } @@ -267,13 +302,16 @@ public class JingleConnection implements Transferable { this.contentCreator = content.getAttribute("creator"); this.contentName = content.getAttribute("name"); this.transportId = content.getTransportId(); - this.mergeCandidates(JingleCandidate.parse(content.socks5transport() - .getChildren())); + this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren())); this.fileOffer = packet.getJingleContent().getFileOffer(); mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null); if (fileOffer != null) { + Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX); + if (encrypted != null) { + this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().toBareJid()); + } Element fileSize = fileOffer.findChild("size"); Element fileNameElement = fileOffer.findChild("name"); if (fileNameElement != null) { @@ -319,10 +357,8 @@ public class JingleConnection implements Transferable { message.setBody(Long.toString(size)); conversation.add(message); mXmppConnectionService.updateConversationUi(); - if (size < this.mJingleConnectionManager - .getAutoAcceptFileSize()) { - Log.d(Config.LOGTAG, "auto accepting file from " - + packet.getFrom()); + if (size < this.mJingleConnectionManager.getAutoAcceptFileSize()) { + Log.d(Config.LOGTAG, "auto accepting file from "+ packet.getFrom()); this.acceptedAutomatically = true; this.sendAccept(); } else { @@ -333,22 +369,32 @@ public class JingleConnection implements Transferable { + " allowed size:" + this.mJingleConnectionManager .getAutoAcceptFileSize()); - this.mXmppConnectionService.getNotificationService() - .push(message); + this.mXmppConnectionService.getNotificationService().push(message); } - this.file = this.mXmppConnectionService.getFileBackend() - .getFile(message, false); - if (message.getEncryption() == Message.ENCRYPTION_OTR) { + this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false); + if (mXmppAxolotlMessage != null) { + XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage); + if (transportMessage != null) { + message.setEncryption(Message.ENCRYPTION_AXOLOTL); + this.file.setKey(transportMessage.getKey()); + this.file.setIv(transportMessage.getIv()); + message.setAxolotlFingerprint(transportMessage.getFingerprint()); + } else { + Log.d(Config.LOGTAG,"could not process KeyTransportMessage"); + } + } else if (message.getEncryption() == Message.ENCRYPTION_OTR) { byte[] key = conversation.getSymmetricKey(); if (key == null) { this.sendCancel(); this.fail(); return; } else { - this.file.setKey(key); + this.file.setKeyAndIv(key); } } + this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file,message.getEncryption() == Message.ENCRYPTION_AXOLOTL); this.file.setExpectedSize(size); + Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize()); } else { this.sendCancel(); this.fail(); @@ -364,19 +410,30 @@ public class JingleConnection implements Transferable { Content content = new Content(this.contentCreator, this.contentName); if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { content.setTransportId(this.transportId); - this.file = this.mXmppConnectionService.getFileBackend().getFile( - message, false); + this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false); + Pair<InputStream,Integer> pair; if (message.getEncryption() == Message.ENCRYPTION_OTR) { Conversation conversation = this.message.getConversation(); if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not set symmetric key"); cancel(); } + this.file.setKeyAndIv(conversation.getSymmetricKey()); + pair = AbstractConnectionManager.createInputStream(this.file,false); + this.file.setExpectedSize(pair.second); content.setFileOffer(this.file, true); - this.file.setKey(conversation.getSymmetricKey()); + } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + this.file.setKey(mXmppAxolotlMessage.getInnerKey()); + this.file.setIv(mXmppAxolotlMessage.getIV()); + pair = AbstractConnectionManager.createInputStream(this.file,true); + this.file.setExpectedSize(pair.second); + content.setFileOffer(this.file, false).addChild(mXmppAxolotlMessage.toElement()); } else { + pair = AbstractConnectionManager.createInputStream(this.file,false); + this.file.setExpectedSize(pair.second); content.setFileOffer(this.file, false); } + this.mFileInputStream = pair.first; this.transportId = this.mJingleConnectionManager.nextRandomId(); content.setTransportId(this.transportId); content.socks5transport().setChildren(getCandidatesAsElements()); @@ -748,6 +805,8 @@ public class JingleConnection implements Transferable { if (this.transport != null && this.transport instanceof JingleInbandTransport) { this.transport.disconnect(); } + FileBackend.close(mFileInputStream); + FileBackend.close(mFileOutputStream); if (this.message != null) { if (this.responder.equals(account.getJid())) { this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED)); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java index 6f31f1639..ab7ab73b8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -93,7 +93,7 @@ public class JingleInbandTransport extends JingleTransport { digest.reset(); file.getParentFile().mkdirs(); file.createNewFile(); - this.fileOutputStream = file.createOutputStream(); + this.fileOutputStream = connection.getFileOutputStream(); if (this.fileOutputStream == null) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not create output stream"); callback.onFileTransferAborted(); @@ -112,15 +112,11 @@ public class JingleInbandTransport extends JingleTransport { this.onFileTransmissionStatusChanged = callback; this.file = file; try { - if (this.file.getKey() != null) { - this.remainingSize = (this.file.getSize() / 16 + 1) * 16; - } else { - this.remainingSize = this.file.getSize(); - } + this.remainingSize = this.file.getExpectedSize(); this.fileSize = this.remainingSize; this.digest = MessageDigest.getInstance("SHA-1"); this.digest.reset(); - fileInputStream = this.file.createInputStream(); + fileInputStream = connection.getFileInputStream(); if (fileInputStream == null) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could no create input stream"); callback.onFileTransferAborted(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 8d74f44e3..7545dd646 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -106,13 +106,13 @@ public class JingleSocks5Transport extends JingleTransport { try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.reset(); - fileInputStream = file.createInputStream(); + fileInputStream = connection.getFileInputStream(); if (fileInputStream == null) { Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create input stream"); callback.onFileTransferAborted(); return; } - long size = file.getSize(); + long size = file.getExpectedSize(); long transmitted = 0; int count; byte[] buffer = new byte[8192]; @@ -157,7 +157,7 @@ public class JingleSocks5Transport extends JingleTransport { socket.setSoTimeout(30000); file.getParentFile().mkdirs(); file.createNewFile(); - fileOutputStream = file.createOutputStream(); + fileOutputStream = connection.getFileOutputStream(); if (fileOutputStream == null) { callback.onFileTransferAborted(); Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create output stream"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java index e832d3f58..b32111584 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java @@ -1,5 +1,31 @@ package eu.siacs.conversations.xmpp.jingle; +import android.util.Log; +import android.util.Pair; + +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.DownloadableFile; public abstract class JingleTransport { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index bcadbe778..f752cc5d3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -25,17 +25,18 @@ public class Content extends Element { this.transportId = sid; } - public void setFileOffer(DownloadableFile actualFile, boolean otr) { + public Element setFileOffer(DownloadableFile actualFile, boolean otr) { Element description = this.addChild("description", "urn:xmpp:jingle:apps:file-transfer:3"); Element offer = description.addChild("offer"); Element file = offer.addChild("file"); - file.addChild("size").setContent(Long.toString(actualFile.getSize())); + file.addChild("size").setContent(Long.toString(actualFile.getExpectedSize())); if (otr) { file.addChild("name").setContent(actualFile.getName() + ".otr"); } else { file.addChild("name").setContent(actualFile.getName()); } + return file; } public Element getFileOffer() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java index 7b36fc497..398102e12 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java @@ -39,6 +39,9 @@ public class IqPacket extends AbstractStanza { public TYPE getType() { final String type = getAttribute("type"); + if (type == null) { + return TYPE.INVALID; + } switch (type) { case "error": return TYPE.ERROR; diff --git a/src/main/res/drawable-hdpi/message_bubble_received.9.png b/src/main/res/drawable-hdpi/message_bubble_received.9.png Binary files differindex 7433cd581..1a496771e 100644 --- a/src/main/res/drawable-hdpi/message_bubble_received.9.png +++ b/src/main/res/drawable-hdpi/message_bubble_received.9.png diff --git a/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png Binary files differnew file mode 100644 index 000000000..9cd2f825d --- /dev/null +++ b/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png diff --git a/src/main/res/drawable-hdpi/message_bubble_sent.9.png b/src/main/res/drawable-hdpi/message_bubble_sent.9.png Binary files differindex 9f8a1c074..2bb4e04f6 100644 --- a/src/main/res/drawable-hdpi/message_bubble_sent.9.png +++ b/src/main/res/drawable-hdpi/message_bubble_sent.9.png diff --git a/src/main/res/drawable-mdpi/message_bubble_received.9.png b/src/main/res/drawable-mdpi/message_bubble_received.9.png Binary files differindex fd67e2450..7e134402f 100644 --- a/src/main/res/drawable-mdpi/message_bubble_received.9.png +++ b/src/main/res/drawable-mdpi/message_bubble_received.9.png diff --git a/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png Binary files differnew file mode 100644 index 000000000..38f75e051 --- /dev/null +++ b/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png diff --git a/src/main/res/drawable-mdpi/message_bubble_sent.9.png b/src/main/res/drawable-mdpi/message_bubble_sent.9.png Binary files differindex 315c18d77..b42664a85 100644 --- a/src/main/res/drawable-mdpi/message_bubble_sent.9.png +++ b/src/main/res/drawable-mdpi/message_bubble_sent.9.png diff --git a/src/main/res/drawable-xhdpi/message_bubble_received.9.png b/src/main/res/drawable-xhdpi/message_bubble_received.9.png Binary files differindex 56f39294c..dc8ee5346 100644 --- a/src/main/res/drawable-xhdpi/message_bubble_received.9.png +++ b/src/main/res/drawable-xhdpi/message_bubble_received.9.png diff --git a/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png Binary files differnew file mode 100644 index 000000000..b5be808f8 --- /dev/null +++ b/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png diff --git a/src/main/res/drawable-xhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xhdpi/message_bubble_sent.9.png Binary files differindex 293d64f81..f1c462326 100644 --- a/src/main/res/drawable-xhdpi/message_bubble_sent.9.png +++ b/src/main/res/drawable-xhdpi/message_bubble_sent.9.png diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received.9.png Binary files differindex 42a7d69b3..6ce844871 100644 --- a/src/main/res/drawable-xxhdpi/message_bubble_received.9.png +++ b/src/main/res/drawable-xxhdpi/message_bubble_received.9.png diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png Binary files differnew file mode 100644 index 000000000..a31818698 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png diff --git a/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png Binary files differindex 762bcd916..9f36a6494 100644 --- a/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png +++ b/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png Binary files differindex 41d52aa73..e7fa6754b 100644 --- a/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png +++ b/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png Binary files differnew file mode 100644 index 000000000..398e53a2d --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png Binary files differindex 1b9604b38..be428cd2e 100644 --- a/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png +++ b/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/contact_key.xml index 6b86f80a5..0f61a15f1 100644 --- a/src/main/res/layout/contact_key.xml +++ b/src/main/res/layout/contact_key.xml @@ -28,6 +28,7 @@ android:textColor="@color/black54" android:layout_alignParentLeft="true" android:layout_below="@+id/key" + android:maxLines="1" android:textSize="?attr/TextSizeInfo"/> <TextView diff --git a/src/main/res/layout/message_received.xml b/src/main/res/layout/message_received.xml index 0539afb84..83f622668 100644 --- a/src/main/res/layout/message_received.xml +++ b/src/main/res/layout/message_received.xml @@ -91,7 +91,6 @@ android:gravity="center_vertical" android:text="@string/sending" android:textColor="@color/white70" - android:alpha="0.70" android:textSize="?attr/TextSizeInfo" /> </LinearLayout> </LinearLayout> diff --git a/src/main/res/layout/message_sent.xml b/src/main/res/layout/message_sent.xml index 2540943d4..139dec0e8 100644 --- a/src/main/res/layout/message_sent.xml +++ b/src/main/res/layout/message_sent.xml @@ -81,7 +81,6 @@ android:gravity="center_vertical" android:text="@string/sending" android:textColor="@color/black54" - android:alpha="0.54" android:textSize="?attr/TextSizeInfo" /> <ImageView diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml index 85a442c84..a5d7eea30 100644 --- a/src/main/res/values/colors.xml +++ b/src/main/res/values/colors.xml @@ -13,5 +13,6 @@ <color name="grey200">#ffeeeeee</color> <color name="grey800">#ff424242</color> <color name="red500">#fff44336</color> + <color name="red800">#ffc62828</color> <color name="orange500">#ffff9800</color> </resources>
\ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 7e7913a91..4253c7a24 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -80,7 +80,7 @@ <string name="choose_presence">Choose presence to contact</string> <string name="send_plain_text_message">Send plain text message</string> <string name="send_otr_message">Send OTR encrypted message</string> - <string name="send_axolotl_message">Send Axolotl encrypted message</string> + <string name="send_axolotl_message">Send Multi-End encrypted message</string> <string name="send_pgp_message">Send OpenPGP encrypted message</string> <string name="your_nick_has_been_changed">Your nickname has been changed</string> <string name="send_unencrypted">Send unencrypted</string> @@ -155,7 +155,7 @@ <string name="encryption_choice_none">Plain text</string> <string name="encryption_choice_otr">OTR</string> <string name="encryption_choice_pgp">OpenPGP</string> - <string name="encryption_choice_axolotl">Axolotl</string> + <string name="encryption_choice_axolotl">Multi-End</string> <string name="mgmt_account_edit">Edit account</string> <string name="mgmt_account_delete">Delete account</string> <string name="mgmt_account_disable">Temporarily disable</string> @@ -208,13 +208,14 @@ <string name="reception_failed">Reception failed</string> <string name="your_fingerprint">Your fingerprint</string> <string name="otr_fingerprint">OTR fingerprint</string> - <string name="axolotl_fingerprint">Axolotl fingerprint</string> - <string name="this_device_axolotl_fingerprint">Own Axolotl fingerprint</string> + <string name="axolotl_fingerprint">Multi-End fingerprint</string> + <string name="axolotl_fingerprint_selected_message">Multi-End fingerprint of message</string> + <string name="this_device_axolotl_fingerprint">Own Multi-End fingerprint</string> <string name="other_devices">Other devices</string> - <string name="trust_keys">Trust Axolotl Keys</string> + <string name="trust_keys">Trust Multi-End Keys</string> <string name="fetching_keys">Fetching keys...</string> <string name="done">Done</string> - <string name="axolotl_devicelist">Other own Axolotl Devices</string> + <string name="axolotl_devicelist">Other own Multi-End Devices</string> <string name="verify">Verify</string> <string name="decrypt">Decrypt</string> <string name="conferences">Conferences</string> @@ -321,7 +322,7 @@ <string name="pref_conference_name">Conference name</string> <string name="pref_conference_name_summary">Use room’s subject instead of JID to identify conferences</string> <string name="toast_message_otr_fingerprint">OTR fingerprint copied to clipboard!</string> - <string name="toast_message_axolotl_fingerprint">Axolotl fingerprint copied to clipboard!</string> + <string name="toast_message_axolotl_fingerprint">Multi-End fingerprint copied to clipboard!</string> <string name="conference_banned">You are banned from this conference</string> <string name="conference_members_only">This conference is members only</string> <string name="conference_kicked">You have been kicked from this conference</string> @@ -388,11 +389,11 @@ <string name="reset">Reset</string> <string name="account_image_description">Account avatar</string> <string name="copy_otr_clipboard_description">Copy OTR fingerprint to clipboard</string> - <string name="copy_axolotl_clipboard_description">Copy Axolotl fingerprint to clipboard</string> + <string name="copy_axolotl_clipboard_description">Copy Multi-End fingerprint to clipboard</string> <string name="regenerate_axolotl_key">Copy Axolotl fingerprint to clipboard</string> <string name="wipe_axolotl_pep">Wipe other devices from PEP</string> <string name="clear_other_devices">Clear devices</string> - <string name="clear_other_devices_desc">Are you sure you want to clear all other devices from the axolotl announcement? The next time your devices connect, they will reannounce themselves, but they might not receive messages sent in the meantime.</string> + <string name="clear_other_devices_desc">Are you sure you want to clear all other devices from the Multi-End announcement? The next time your devices connect, they will reannounce themselves, but they might not receive messages sent in the meantime.</string> <string name="purge_key">Purge key</string> <string name="purge_key_desc_part1">Are you sure you want to purge this key?</string> <string name="purge_key_desc_part2">It will irreversibly be considered compromised, and you can never build a session with it again.</string> @@ -473,7 +474,6 @@ <string name="received_location">Received location</string> <string name="title_undo_swipe_out_conversation">Conversation closed</string> <string name="title_undo_swipe_out_muc">Left conference</string> - <string name="pref_certificate_options">Certificate options</string> <string name="pref_dont_trust_system_cas_title">Don’t trust system CAs</string> <string name="pref_dont_trust_system_cas_summary">All certificates must be manually approved</string> <string name="pref_remove_trusted_certificates_title">Remove certificates</string> diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index c457fa56f..a0968b45b 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -1,12 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> -<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" > +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> - <PreferenceCategory android:title="@string/pref_general" > + <PreferenceCategory android:title="@string/pref_general"> <CheckBoxPreference android:defaultValue="true" android:key="grant_new_contacts" android:summary="@string/pref_grant_presence_updates_summary" - android:title="@string/pref_grant_presence_updates" /> + android:title="@string/pref_grant_presence_updates"/> <ListPreference android:defaultValue="Mobile" @@ -14,65 +14,65 @@ android:entryValues="@array/resources" android:key="resource" android:summary="@string/pref_xmpp_resource_summary" - android:title="@string/pref_xmpp_resource" /> + android:title="@string/pref_xmpp_resource"/> <ListPreference android:defaultValue="1048576" android:entries="@array/filesizes" android:entryValues="@array/filesizes_values" android:key="auto_accept_file_size" android:summary="@string/pref_accept_files_summary" - android:title="@string/pref_accept_files" /> + android:title="@string/pref_accept_files"/> <CheckBoxPreference android:defaultValue="true" android:key="confirm_messages" android:summary="@string/pref_confirm_messages_summary" - android:title="@string/pref_confirm_messages" /> + android:title="@string/pref_confirm_messages"/> <CheckBoxPreference android:defaultValue="true" android:key="chat_states" android:summary="@string/pref_chat_states_summary" - android:title="@string/pref_chat_states" /> + android:title="@string/pref_chat_states"/> </PreferenceCategory> <PreferenceCategory - android:title="@string/pref_notification_settings" - android:key="notifications"> + android:key="notifications" + android:title="@string/pref_notification_settings"> <CheckBoxPreference android:defaultValue="true" android:key="show_notification" android:summary="@string/pref_notifications_summary" - android:title="@string/pref_notifications" /> - <PreferenceScreen + android:title="@string/pref_notifications"/> + <PreferenceScreen android:dependency="show_notification" + android:key="quiet_hours" android:summary="@string/pref_quiet_hours_summary" - android:title="@string/title_pref_quiet_hours" - android:key="quiet_hours"> + android:title="@string/title_pref_quiet_hours"> <CheckBoxPreference - android:defaultValue="false" - android:key="enable_quiet_hours" - android:summary="@string/pref_quiet_hours_summary" - android:title="@string/title_pref_enable_quiet_hours" /> + android:defaultValue="false" + android:key="enable_quiet_hours" + android:summary="@string/pref_quiet_hours_summary" + android:title="@string/title_pref_enable_quiet_hours"/> <eu.siacs.conversations.ui.TimePreference - android:dependency="enable_quiet_hours" - android:key="quiet_hours_start" - android:negativeButtonText="@string/cancel" - android:positiveButtonText="@string/set" - android:title="@string/title_pref_quiet_hours_start_time" /> + android:dependency="enable_quiet_hours" + android:key="quiet_hours_start" + android:negativeButtonText="@string/cancel" + android:positiveButtonText="@string/set" + android:title="@string/title_pref_quiet_hours_start_time"/> <eu.siacs.conversations.ui.TimePreference - android:dependency="enable_quiet_hours" - android:key="quiet_hours_end" - android:negativeButtonText="@string/cancel" - android:positiveButtonText="@string/set" - android:title="@string/title_pref_quiet_hours_end_time" /> - </PreferenceScreen> - <CheckBoxPreference + android:dependency="enable_quiet_hours" + android:key="quiet_hours_end" + android:negativeButtonText="@string/cancel" + android:positiveButtonText="@string/set" + android:title="@string/title_pref_quiet_hours_end_time"/> + </PreferenceScreen> + <CheckBoxPreference android:defaultValue="true" android:dependency="show_notification" android:key="vibrate_on_notification" android:summary="@string/pref_vibrate_summary" - android:title="@string/pref_vibrate" /> + android:title="@string/pref_vibrate"/> <RingtonePreference android:defaultValue="content://settings/system/notification_sound" @@ -80,98 +80,91 @@ android:key="notification_ringtone" android:ringtoneType="notification" android:summary="@string/pref_sound_summary" - android:title="@string/pref_sound" /> + android:title="@string/pref_sound"/> <CheckBoxPreference android:defaultValue="false" android:dependency="show_notification" android:key="always_notify_in_conference" android:summary="@string/pref_conference_notifications_summary" - android:title="@string/pref_conference_notifications" /> + android:title="@string/pref_conference_notifications"/> </PreferenceCategory> - <PreferenceCategory android:title="@string/pref_ui_options" > + <PreferenceCategory android:title="@string/pref_ui_options"> <CheckBoxPreference android:defaultValue="true" android:key="use_subject" android:summary="@string/pref_conference_name_summary" - android:title="@string/pref_conference_name" /> + android:title="@string/pref_conference_name"/> <CheckBoxPreference android:defaultValue="false" android:key="use_larger_font" android:summary="@string/pref_use_larger_font_summary" - android:title="@string/pref_use_larger_font" /> + android:title="@string/pref_use_larger_font"/> <CheckBoxPreference android:defaultValue="true" android:key="send_button_status" android:summary="@string/pref_use_send_button_to_indicate_status_summary" - android:title="@string/pref_use_send_button_to_indicate_status" /> + android:title="@string/pref_use_send_button_to_indicate_status"/> <ListPreference - android:key="quick_action" android:defaultValue="recent" + android:dialogTitle="@string/choose_quick_action" android:entries="@array/quick_actions" android:entryValues="@array/quick_action_values" + android:key="quick_action" android:summary="@string/pref_quick_action_summary" - android:title="@string/pref_quick_action" - android:dialogTitle="@string/choose_quick_action"/> + android:title="@string/pref_quick_action"/> <CheckBoxPreference android:defaultValue="false" android:key="show_dynamic_tags" android:summary="@string/pref_show_dynamic_tags_summary" - android:title="@string/pref_show_dynamic_tags" /> + android:title="@string/pref_show_dynamic_tags"/> </PreferenceCategory> <PreferenceCategory - android:title="@string/pref_advanced_options" - android:key="advanced"> + android:key="advanced" + android:title="@string/pref_advanced_options"> <PreferenceScreen + android:key="expert" android:summary="@string/pref_expert_options_summary" - android:title="@string/pref_expert_options" - android:key="expert"> - <PreferenceCategory android:title="@string/pref_encryption_settings" > - <CheckBoxPreference - android:defaultValue="false" - android:key="force_encryption" - android:summary="@string/pref_force_encryption_summary" - android:title="@string/pref_force_encryption" /> + android:title="@string/pref_expert_options"> + <PreferenceCategory android:title="@string/pref_encryption_settings"> <CheckBoxPreference android:defaultValue="false" android:key="dont_save_encrypted" android:summary="@string/pref_dont_save_encrypted_summary" - android:title="@string/pref_dont_save_encrypted" /> + android:title="@string/pref_dont_save_encrypted"/> + <CheckBoxPreference + android:defaultValue="false" + android:key="dont_trust_system_cas" + android:summary="@string/pref_dont_trust_system_cas_summary" + android:title="@string/pref_dont_trust_system_cas_title"/> + <Preference + android:key="remove_trusted_certificates" + android:summary="@string/pref_remove_trusted_certificates_summary" + android:title="@string/pref_remove_trusted_certificates_title"/> + </PreferenceCategory> + <PreferenceCategory android:title="@string/pref_input_options"> + <CheckBoxPreference + android:defaultValue="false" + android:key="enter_is_send" + android:summary="@string/pref_enter_is_send_summary" + android:title="@string/pref_enter_is_send"/> + <CheckBoxPreference + android:defaultValue="false" + android:key="display_enter_key" + android:summary="@string/pref_display_enter_key_summary" + android:title="@string/pref_display_enter_key"/> </PreferenceCategory> - <PreferenceCategory android:title="@string/pref_input_options"> - <CheckBoxPreference - android:defaultValue="false" - android:key="enter_is_send" - android:title="@string/pref_enter_is_send" - android:summary="@string/pref_enter_is_send_summary" /> - <CheckBoxPreference - android:defaultValue="false" - android:key="display_enter_key" - android:title="@string/pref_display_enter_key" - android:summary="@string/pref_display_enter_key_summary" /> - </PreferenceCategory> - <PreferenceCategory android:title="@string/pref_certificate_options"> - <CheckBoxPreference - android:defaultValue="false" - android:key="dont_trust_system_cas" - android:title="@string/pref_dont_trust_system_cas_title" - android:summary="@string/pref_dont_trust_system_cas_summary" /> - <Preference - android:key="remove_trusted_certificates" - android:title="@string/pref_remove_trusted_certificates_title" - android:summary="@string/pref_remove_trusted_certificates_summary" /> - </PreferenceCategory> - <PreferenceCategory android:title="@string/pref_expert_options_other" > + <PreferenceCategory android:title="@string/pref_expert_options_other"> <CheckBoxPreference android:defaultValue="true" android:key="indicate_received" android:summary="@string/pref_use_indicate_received_summary" - android:title="@string/pref_use_indicate_received" /> + android:title="@string/pref_use_indicate_received"/> <CheckBoxPreference android:defaultValue="true" android:key="keep_foreground_service" - android:title="@string/pref_keep_foreground_service" - android:summary="@string/pref_keep_foreground_service_summary" /> + android:summary="@string/pref_keep_foreground_service_summary" + android:title="@string/pref_keep_foreground_service"/> </PreferenceCategory> </PreferenceScreen> @@ -179,9 +172,9 @@ android:defaultValue="true" android:key="never_send" android:summary="@string/pref_never_send_crash_summary" - android:title="@string/pref_never_send_crash" /> + android:title="@string/pref_never_send_crash"/> </PreferenceCategory> - <eu.siacs.conversations.ui.AboutPreference - android:summary="@string/pref_about_conversations_summary" - android:title="@string/title_activity_about" /> + <eu.siacs.conversations.ui.AboutPreference + android:summary="@string/pref_about_conversations_summary" + android:title="@string/title_activity_about"/> </PreferenceScreen> |