diff options
author | lookshe <github@lookshe.org> | 2016-03-06 19:42:55 +0100 |
---|---|---|
committer | lookshe <github@lookshe.org> | 2016-03-06 19:42:55 +0100 |
commit | 3c400703e082a1b180b35d891b8fb3460c7d5b87 (patch) | |
tree | 28738dd90fc41b4ab71897f38d324828778ad2e3 /src/main/java/de/thedevstack/conversationsplus/crypto | |
parent | 72114d732427266024cdd6e27cd8d1aa60afae2f (diff) | |
parent | f28d77dc42f6bac5a026e0b1c78562dee8de45ac (diff) |
Merge branch 'trz/rebase' into trz/rename
Diffstat (limited to 'src/main/java/de/thedevstack/conversationsplus/crypto')
16 files changed, 2664 insertions, 63 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/OtrService.java b/src/main/java/de/thedevstack/conversationsplus/crypto/OtrService.java index e36007d7..34023d9f 100644 --- a/src/main/java/de/thedevstack/conversationsplus/crypto/OtrService.java +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/OtrService.java @@ -1,5 +1,18 @@ package de.thedevstack.conversationsplus.crypto; +import net.java.otr4j.OtrEngineHost; +import net.java.otr4j.OtrException; +import net.java.otr4j.OtrPolicy; +import net.java.otr4j.OtrPolicyImpl; +import net.java.otr4j.crypto.OtrCryptoEngineImpl; +import net.java.otr4j.crypto.OtrCryptoException; +import net.java.otr4j.session.FragmenterInstructions; +import net.java.otr4j.session.InstanceTag; +import net.java.otr4j.session.SessionID; + +import org.json.JSONException; +import org.json.JSONObject; + import java.math.BigInteger; import java.security.KeyFactory; import java.security.KeyPair; @@ -11,32 +24,18 @@ import java.security.spec.DSAPrivateKeySpec; import java.security.spec.DSAPublicKeySpec; import java.security.spec.InvalidKeySpecException; -import org.json.JSONException; -import org.json.JSONObject; - import de.thedevstack.android.logcat.Logging; - -import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.generator.MessageGenerator; import de.thedevstack.conversationsplus.services.XmppConnectionService; -import de.thedevstack.conversationsplus.utils.CryptoHelper; import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; -import net.java.otr4j.OtrEngineHost; -import net.java.otr4j.OtrException; -import net.java.otr4j.OtrPolicy; -import net.java.otr4j.OtrPolicyImpl; -import net.java.otr4j.crypto.OtrCryptoEngineImpl; -import net.java.otr4j.crypto.OtrCryptoException; -import net.java.otr4j.session.InstanceTag; -import net.java.otr4j.session.SessionID; -import net.java.otr4j.session.FragmenterInstructions; - public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { private Account account; @@ -181,10 +180,7 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID()); } packet.setBody(body); - packet.addChild("private", "urn:xmpp:carbons:2"); - packet.addChild("no-copy", "urn:xmpp:hints"); - packet.addChild("no-permanent-store", "urn:xmpp:hints"); - packet.addChild("no-permanent-storage", "urn:xmpp:hints"); + MessageGenerator.addMessageHints(packet); try { Jid jid = Jid.fromSessionID(session); Conversation conversation = mXmppConnectionService.find(account,jid); diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpDecryptionService.java b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpDecryptionService.java new file mode 100644 index 00000000..b833f6e4 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpDecryptionService.java @@ -0,0 +1,162 @@ +package de.thedevstack.conversationsplus.crypto; + +import android.app.PendingIntent; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.ui.UiCallback; + +public class PgpDecryptionService { + + private final XmppConnectionService xmppConnectionService; + private final ConcurrentHashMap<String, List<Message>> messages = new ConcurrentHashMap<>(); + private final ConcurrentHashMap<String, Boolean> decryptingMessages = new ConcurrentHashMap<>(); + private Boolean keychainLocked = false; + private final Object keychainLockedLock = new Object(); + + public PgpDecryptionService(XmppConnectionService xmppConnectionService) { + this.xmppConnectionService = xmppConnectionService; + } + + public void add(Message message) { + if (isRunning()) { + decryptDirectly(message); + } else { + store(message); + } + } + + public void addAll(List<Message> messagesList) { + if (!messagesList.isEmpty()) { + String conversationUuid = messagesList.get(0).getConversation().getUuid(); + if (!messages.containsKey(conversationUuid)) { + List<Message> list = Collections.synchronizedList(new LinkedList<Message>()); + messages.put(conversationUuid, list); + } + synchronized (messages.get(conversationUuid)) { + messages.get(conversationUuid).addAll(messagesList); + } + decryptAllMessages(); + } + } + + public void onKeychainUnlocked() { + synchronized (keychainLockedLock) { + keychainLocked = false; + } + decryptAllMessages(); + } + + public void onKeychainLocked() { + synchronized (keychainLockedLock) { + keychainLocked = true; + } + xmppConnectionService.updateConversationUi(); + } + + public void onOpenPgpServiceBound() { + decryptAllMessages(); + } + + public boolean isRunning() { + synchronized (keychainLockedLock) { + return !keychainLocked; + } + } + + private void store(Message message) { + if (messages.containsKey(message.getConversation().getUuid())) { + messages.get(message.getConversation().getUuid()).add(message); + } else { + List<Message> messageList = Collections.synchronizedList(new LinkedList<Message>()); + messageList.add(message); + messages.put(message.getConversation().getUuid(), messageList); + } + } + + private void decryptAllMessages() { + for (String uuid : messages.keySet()) { + decryptMessages(uuid); + } + } + + private void decryptMessages(final String uuid) { + synchronized (decryptingMessages) { + Boolean decrypting = decryptingMessages.get(uuid); + if ((decrypting != null && !decrypting) || decrypting == null) { + decryptingMessages.put(uuid, true); + decryptMessage(uuid); + } + } + } + + private void decryptMessage(final String uuid) { + Message message = null; + synchronized (messages.get(uuid)) { + while (!messages.get(uuid).isEmpty()) { + if (messages.get(uuid).get(0).getEncryption() == Message.ENCRYPTION_PGP) { + if (isRunning()) { + message = messages.get(uuid).remove(0); + } + break; + } else { + messages.get(uuid).remove(0); + } + } + if (message != null && xmppConnectionService.getPgpEngine() != null) { + xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() { + + @Override + public void userInputRequried(PendingIntent pi, Message message) { + messages.get(uuid).add(0, message); + decryptingMessages.put(uuid, false); + } + + @Override + public void success(Message message) { + xmppConnectionService.updateConversationUi(); + decryptMessage(uuid); + } + + @Override + public void error(int error, Message message) { + message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); + xmppConnectionService.updateConversationUi(); + decryptMessage(uuid); + } + }); + } else { + decryptingMessages.put(uuid, false); + } + } + } + + private void decryptDirectly(final Message message) { + if (message.getEncryption() == Message.ENCRYPTION_PGP && xmppConnectionService.getPgpEngine() != null) { + xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() { + + @Override + public void userInputRequried(PendingIntent pi, Message message) { + store(message); + } + + @Override + public void success(Message message) { + xmppConnectionService.updateConversationUi(); + xmppConnectionService.getNotificationService().updateNotification(false); + } + + @Override + public void error(int error, Message message) { + message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); + xmppConnectionService.updateConversationUi(); + } + }); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java index 08aa8272..8a5c6101 100644 --- a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java @@ -1,5 +1,13 @@ package de.thedevstack.conversationsplus.crypto; +import android.app.PendingIntent; +import android.content.Intent; +import android.net.Uri; + +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; @@ -9,12 +17,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URL; -import org.openintents.openpgp.OpenPgpSignatureResult; -import org.openintents.openpgp.util.OpenPgpApi; -import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; - import de.thedevstack.conversationsplus.ConversationsPlusPreferences; -import de.thedevstack.conversationsplus.persistance.FileBackend; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.StreamUtil; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Contact; @@ -22,13 +27,9 @@ import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.http.HttpConnectionManager; +import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.ui.UiCallback; -import de.thedevstack.conversationsplus.utils.MessageUtil; - -import android.app.PendingIntent; -import android.content.Intent; -import android.net.Uri; public class PgpEngine { private OpenPgpApi api; @@ -39,26 +40,24 @@ public class PgpEngine { this.mXmppConnectionService = service; } - public void decrypt(final Message message, - final UiCallback<Message> callback) { + public void decrypt(final Message message, final UiCallback<Message> callback) { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message - .getConversation().getAccount().getJid().toBareJid().toString()); + final String uuid = message.getUuid(); if (message.getType() == Message.TYPE_TEXT) { - InputStream is = new ByteArrayInputStream(message.getBody() - .getBytes()); + InputStream is = new ByteArrayInputStream(message.getBody().getBytes()); final OutputStream os = new ByteArrayOutputStream(); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { @Override public void onReturn(Intent result) { - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, - OpenPgpApi.RESULT_CODE_ERROR)) { + notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_DECRYPT_VERIFY, result); + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: try { os.flush(); - if (message.getEncryption() == Message.ENCRYPTION_PGP) { + if (message.getEncryption() == Message.ENCRYPTION_PGP + && message.getUuid().equals(uuid)) { message.setBody(os.toString()); message.setEncryption(Message.ENCRYPTION_DECRYPTED); final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager(); @@ -69,6 +68,7 @@ public class PgpEngine { && ConversationsPlusPreferences.autoAcceptFileSize() > 0) { manager.createNewDownloadConnection(message); } + mXmppConnectionService.updateMessage(message); callback.success(message); } } catch (IOException e) { @@ -99,6 +99,7 @@ public class PgpEngine { @Override public void onReturn(Intent result) { + notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_DECRYPT_VERIFY, result); switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: @@ -131,21 +132,19 @@ public class PgpEngine { } } - public void encrypt(final Message message, - final UiCallback<Message> callback) { - + public void encrypt(final Message message, final UiCallback<Message> callback) { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_ENCRYPT); - if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { - long[] keys = { message.getConversation().getContact() - .getPgpKeyId() }; + final Conversation conversation = message.getConversation(); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + long[] keys = { + conversation.getContact().getPgpKeyId(), + conversation.getAccount().getPgpId() + }; params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys); } else { - params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, message.getConversation() - .getMucOptions().getPgpKeyIds()); + params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, conversation.getMucOptions().getPgpKeyIds()); } - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message - .getConversation().getAccount().getJid().toBareJid().toString()); if (!message.needsUploading()) { params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); @@ -161,6 +160,7 @@ public class PgpEngine { @Override public void onReturn(Intent result) { + notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_ENCRYPT, result); switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: @@ -198,15 +198,22 @@ public class PgpEngine { DownloadableFile outputFile = FileBackend.getFile(message, false); outputFile.getParentFile().mkdirs(); outputFile.createNewFile(); - InputStream is = new FileInputStream(inputFile); - OutputStream os = new FileOutputStream(outputFile); + final InputStream is = new FileInputStream(inputFile); + final OutputStream os = new FileOutputStream(outputFile); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { @Override public void onReturn(Intent result) { + notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_ENCRYPT, result); switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: + try { + os.flush(); + } catch (IOException ignored) { + //ignored + } + StreamUtil.close(os); callback.success(message); break; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: @@ -249,10 +256,10 @@ public class PgpEngine { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid().toBareJid().toString()); InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes()); ByteArrayOutputStream os = new ByteArrayOutputStream(); Intent result = api.executeApi(params, is, os); + notifyPgpDecryptionService(account, OpenPgpApi.ACTION_DECRYPT_VERIFY, result); switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: @@ -271,18 +278,45 @@ public class PgpEngine { return 0; } + public void chooseKey(final Account account, final UiCallback<Account> callback) { + Intent p = new Intent(); + p.setAction(OpenPgpApi.ACTION_GET_SIGN_KEY_ID); + api.executeApiAsync(p, null, null, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + callback.success(account); + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + account); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, account); + } + } + }); + } + public void generateSignature(final Account account, String status, final UiCallback<Account> callback) { + if (account.getPgpId() == -1) { + return; + } Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_CLEARTEXT_SIGN); params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - params.setAction(OpenPgpApi.ACTION_SIGN); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid().toBareJid().toString()); + params.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, account.getPgpId()); InputStream is = new ByteArrayInputStream(status.getBytes()); final OutputStream os = new ByteArrayOutputStream(); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { @Override public void onReturn(Intent result) { + notifyPgpDecryptionService(account, OpenPgpApi.ACTION_SIGN, result); switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { case OpenPgpApi.RESULT_CODE_SUCCESS: StringBuilder signatureBuilder = new StringBuilder(); @@ -308,7 +342,7 @@ public class PgpEngine { callback.error(R.string.openpgp_error, account); return; } - account.setKey("pgp_signature", signatureBuilder.toString()); + account.setPgpSignature(signatureBuilder.toString()); callback.success(account); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: @@ -327,8 +361,6 @@ public class PgpEngine { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_GET_KEY); params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() - .getJid().toBareJid().toString()); api.executeApiAsync(params, null, null, new IOpenPgpCallback() { @Override @@ -353,8 +385,6 @@ public class PgpEngine { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_GET_KEY); params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() - .getJid().toBareJid().toString()); Intent result = api.executeApi(params, null, null); return (PendingIntent) result .getParcelableExtra(OpenPgpApi.RESULT_INTENT); @@ -364,9 +394,21 @@ public class PgpEngine { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_GET_KEY); params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid().toBareJid().toString()); Intent result = api.executeApi(params, null, null); return (PendingIntent) result .getParcelableExtra(OpenPgpApi.RESULT_INTENT); } + + private void notifyPgpDecryptionService(Account account, String action, final Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + if (OpenPgpApi.ACTION_SIGN.equals(action)) { + account.getPgpDecryptionService().onKeychainUnlocked(); + } + break; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + account.getPgpDecryptionService().onKeychainLocked(); + break; + } + } } diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/XmppDomainVerifier.java b/src/main/java/de/thedevstack/conversationsplus/crypto/XmppDomainVerifier.java new file mode 100644 index 00000000..c9f65a97 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/XmppDomainVerifier.java @@ -0,0 +1,127 @@ +package de.thedevstack.conversationsplus.crypto; + +import android.util.Log; +import android.util.Pair; + +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.DERTaggedObject; +import org.bouncycastle.asn1.DERUTF8String; +import org.bouncycastle.asn1.DLSequence; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; + +import java.io.IOException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +public class XmppDomainVerifier implements HostnameVerifier { + + private static final String LOGTAG = "XmppDomainVerifier"; + + private final String SRVName = "1.3.6.1.5.5.7.8.7"; + private final String xmppAddr = "1.3.6.1.5.5.7.8.5"; + + @Override + public boolean verify(String domain, SSLSession sslSession) { + try { + Certificate[] chain = sslSession.getPeerCertificates(); + if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) { + return false; + } + X509Certificate certificate = (X509Certificate) chain[0]; + Collection<List<?>> alternativeNames = certificate.getSubjectAlternativeNames(); + List<String> xmppAddrs = new ArrayList<>(); + List<String> srvNames = new ArrayList<>(); + List<String> domains = new ArrayList<>(); + if (alternativeNames != null) { + for (List<?> san : alternativeNames) { + Integer type = (Integer) san.get(0); + if (type == 0) { + Pair<String, String> otherName = parseOtherName((byte[]) san.get(1)); + if (otherName != null) { + switch (otherName.first) { + case SRVName: + srvNames.add(otherName.second); + break; + case xmppAddr: + xmppAddrs.add(otherName.second); + break; + default: + Log.d(LOGTAG, "oid: " + otherName.first + " value: " + otherName.second); + } + } + } else if (type == 2) { + Object value = san.get(1); + if (value instanceof String) { + domains.add((String) value); + } + } + } + } + if (srvNames.size() == 0 && xmppAddrs.size() == 0 && domains.size() == 0) { + X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject(); + RDN[] rdns = x500name.getRDNs(BCStyle.CN); + for (int i = 0; i < rdns.length; ++i) { + domains.add(IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[i].getFirst().getValue())); + } + } + Log.d(LOGTAG, "searching for " + domain + " in srvNames: " + srvNames + " xmppAddrs: " + xmppAddrs + " domains:" + domains); + return xmppAddrs.contains(domain) || srvNames.contains("_xmpp-client." + domain) || matchDomain(domain, domains); + } catch (Exception e) { + return false; + } + } + + private static Pair<String, String> parseOtherName(byte[] otherName) { + try { + ASN1Primitive asn1Primitive = ASN1Primitive.fromByteArray(otherName); + if (asn1Primitive instanceof DERTaggedObject) { + ASN1Primitive inner = ((DERTaggedObject) asn1Primitive).getObject(); + if (inner instanceof DLSequence) { + DLSequence sequence = (DLSequence) inner; + if (sequence.size() >= 2 && sequence.getObjectAt(1) instanceof DERTaggedObject) { + String oid = sequence.getObjectAt(0).toString(); + ASN1Primitive value = ((DERTaggedObject) sequence.getObjectAt(1)).getObject(); + if (value instanceof DERUTF8String) { + return new Pair<>(oid, ((DERUTF8String) value).getString()); + } else if (value instanceof DERIA5String) { + return new Pair<>(oid, ((DERIA5String) value).getString()); + } + } + } + } + return null; + } catch (IOException e) { + return null; + } + } + + private static boolean matchDomain(String needle, List<String> haystack) { + for (String entry : haystack) { + if (entry.startsWith("*.")) { + int i = needle.indexOf('.'); + Log.d(LOGTAG, "comparing " + needle.substring(i) + " and " + entry.substring(1)); + if (i != -1 && needle.substring(i).equals(entry.substring(1))) { + Log.d(LOGTAG, "domain " + needle + " matched " + entry); + return true; + } + } else { + if (entry.equals(needle)) { + Log.d(LOGTAG, "domain " + needle + " matched " + entry); + return true; + } + } + } + return false; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlService.java b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlService.java new file mode 100644 index 00000000..987cf940 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlService.java @@ -0,0 +1,110 @@ +package de.thedevstack.conversationsplus.crypto.axolotl; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; + +import java.security.cert.X509Certificate; +import java.util.Set; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.xmpp.OnAdvancedStreamFeaturesLoaded; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +/** + * Created by tzur on 02.03.2016. + */ +public interface AxolotlService extends OnAdvancedStreamFeaturesLoaded { + + String LOGPREFIX = "AxolotlService"; + + String PEP_PREFIX = "eu.siacs.conversations.axolotl"; + String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; + String PEP_BUNDLES = PEP_PREFIX + ".bundles"; + String PEP_VERIFICATION = PEP_PREFIX + ".verification"; + + enum FetchStatus { + PENDING, + SUCCESS, + SUCCESS_VERIFIED, + TIMEOUT, + ERROR + } + + boolean fetchMapHasErrors(Contact contact); + + String getOwnFingerprint(); + + Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust); + + Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact); + + long getNumTrustedKeys(Contact contact); + + Set<String> getFingerprintsForOwnSessions(); + + Set<String> getFingerprintsForContact(Contact contact); + + boolean isPepBroken(); + + void regenerateKeys(boolean wipeOther); + + int getOwnDeviceId(); + + Set<Integer> getOwnDeviceIds(); + + void registerDevices(Jid jid, @NonNull Set<Integer> deviceIds); + + void wipeOtherPepDevices(); + + void purgeKey(String fingerprint); + + void publishOwnDeviceIdIfNeeded(); + + void publishOwnDeviceId(Set<Integer> deviceIds); + + void publishDeviceVerificationAndBundle(SignedPreKeyRecord signedPreKeyRecord, + Set<PreKeyRecord> preKeyRecords, + boolean announceAfter, + boolean wipe); + + void publishBundlesIfNeeded(boolean announce, boolean wipe); + + boolean isContactAxolotlCapable(Contact contact); + + XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint); + + X509Certificate getFingerprintCertificate(String fingerprint); + + void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust); + + Set<AxolotlAddress> findDevicesWithoutSession(Conversation conversation); + + Set<AxolotlAddress> findDevicesWithoutSession(Jid contactJid); + + boolean createSessionsIfNeeded(Conversation conversation); + + boolean trustedSessionVerified(Conversation conversation); + + boolean hasPendingKeyFetches(Account account, Contact contact); + + @Nullable + XmppAxolotlMessage encrypt(Message message); + + void preparePayloadMessage(Message message, boolean delay); + + void prepareKeyTransportMessage(Contact contact, OnMessageCreatedCallback onMessageCreatedCallback); + + XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message); + + XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message); + + XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java new file mode 100644 index 00000000..5a05b34c --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java @@ -0,0 +1,1021 @@ +package de.thedevstack.conversationsplus.crypto.axolotl; + +import android.os.Bundle; +import android.security.KeyChain; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; +import android.util.Pair; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.SessionBuilder; +import org.whispersystems.libaxolotl.UntrustedIdentityException; +import org.whispersystems.libaxolotl.ecc.ECPublicKey; +import org.whispersystems.libaxolotl.state.PreKeyBundle; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; +import org.whispersystems.libaxolotl.util.KeyHelper; + +import java.security.PrivateKey; +import java.security.Security; +import java.security.Signature; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.parser.IqParser; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.SerialSingleThreadExecutor; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +public class AxolotlServiceImpl implements AxolotlService { + + public static final int NUM_KEYS_TO_PUBLISH = 100; + public static final int publishTriesThreshold = 3; + + private final Account account; + private final XmppConnectionService mXmppConnectionService; + private final SQLiteAxolotlStore axolotlStore; + private final SessionMap sessions; + private final Map<Jid, Set<Integer>> deviceIds; + private final Map<String, XmppAxolotlMessage> messageCache; + private final FetchStatusMap fetchStatusMap; + private final SerialSingleThreadExecutor executor; + private int numPublishTriesOnEmptyPep = 0; + private boolean pepBroken = false; + + @Override + public void onAdvancedStreamFeaturesAvailable(Account account) { + if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().pep()) { + publishBundlesIfNeeded(true, false); + } else { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping OMEMO initialization"); + } + } + + @Override + public boolean fetchMapHasErrors(Contact contact) { + Jid jid = contact.getJid().toBareJid(); + if (deviceIds.get(jid) != null) { + for (Integer foreignId : this.deviceIds.get(jid)) { + AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId); + if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) { + return true; + } + } + } + return false; + } + + private static class AxolotlAddressMap<T> { + protected Map<String, Map<Integer, T>> map; + protected final Object MAP_LOCK = new Object(); + + public AxolotlAddressMap() { + this.map = new HashMap<>(); + } + + public void put(AxolotlAddress address, T value) { + synchronized (MAP_LOCK) { + Map<Integer, T> devices = map.get(address.getName()); + if (devices == null) { + devices = new HashMap<>(); + map.put(address.getName(), devices); + } + devices.put(address.getDeviceId(), value); + } + } + + public T get(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map<Integer, T> devices = map.get(address.getName()); + if (devices == null) { + return null; + } + return devices.get(address.getDeviceId()); + } + } + + public Map<Integer, T> getAll(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map<Integer, T> devices = map.get(address.getName()); + if (devices == null) { + return new HashMap<>(); + } + return devices; + } + } + + public boolean hasAny(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map<Integer, T> devices = map.get(address.getName()); + return devices != null && !devices.isEmpty(); + } + } + + public void clear() { + map.clear(); + } + + } + + private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> { + private final XmppConnectionService xmppConnectionService; + private final Account account; + + public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) { + super(); + this.xmppConnectionService = service; + this.account = account; + this.fillMap(store); + } + + private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) { + for (Integer deviceId : deviceIds) { + AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString()); + IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); + if(Config.X509_VERIFICATION) { + X509Certificate certificate = store.getFingerprintCertificate(identityKey.getFingerprint().replaceAll("\\s", "")); + if (certificate != null) { + Bundle information = CryptoHelper.extractCertificateInformation(certificate); + try { + final String cn = information.getString("subject_cn"); + final Jid jid = Jid.fromString(bareJid); + Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn); + account.getRoster().getContact(jid).setCommonName(cn); + } catch (final InvalidJidException ignored) { + //ignored + } + } + } + this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); + } + } + + private void fillMap(SQLiteAxolotlStore store) { + List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString()); + putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store); + for (Contact contact : account.getRoster().getContacts()) { + Jid bareJid = contact.getJid().toBareJid(); + String address = bareJid.toString(); + deviceIds = store.getSubDeviceSessions(address); + putDevicesForJid(address, deviceIds, store); + } + + } + + @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 class FetchStatusMap extends AxolotlAddressMap<FetchStatus> { + + } + + public static String getLogprefix(Account account) { + return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): "; + } + + public AxolotlServiceImpl(Account account, XmppConnectionService connectionService) { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + this.mXmppConnectionService = connectionService; + this.account = account; + this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); + this.deviceIds = new HashMap<>(); + this.messageCache = new HashMap<>(); + this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account); + this.fetchStatusMap = new FetchStatusMap(); + this.executor = new SerialSingleThreadExecutor(); + } + + @Override + public String getOwnFingerprint() { + return axolotlStore.getIdentityKeyPair().getPublicKey().getFingerprint().replaceAll("\\s", ""); + } + + @Override + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) { + return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust); + } + + @Override + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact) { + return axolotlStore.getContactKeysWithTrust(contact.getJid().toBareJid().toString(), trust); + } + + @Override + public long getNumTrustedKeys(Contact contact) { + return axolotlStore.getContactNumTrustedKeys(contact.getJid().toBareJid().toString()); + } + + private AxolotlAddress getAddressForJid(Jid jid) { + return new AxolotlAddress(jid.toString(), 0); + } + + private Set<XmppAxolotlSession> findOwnSessions() { + AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid()); + return new HashSet<>(this.sessions.getAll(ownAddress).values()); + } + + private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + return new HashSet<>(this.sessions.getAll(contactAddress).values()); + } + + @Override + public Set<String> getFingerprintsForOwnSessions() { + Set<String> fingerprints = new HashSet<>(); + for (XmppAxolotlSession session : findOwnSessions()) { + fingerprints.add(session.getFingerprint()); + } + return fingerprints; + } + + @Override + public Set<String> getFingerprintsForContact(final Contact contact) { + Set<String> fingerprints = new HashSet<>(); + for (XmppAxolotlSession session : findSessionsforContact(contact)) { + fingerprints.add(session.getFingerprint()); + } + return fingerprints; + } + + private boolean hasAny(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + return sessions.hasAny(contactAddress); + } + + @Override + public boolean isPepBroken() { + return this.pepBroken; + } + + @Override + public void regenerateKeys(boolean wipeOther) { + axolotlStore.regenerate(); + sessions.clear(); + fetchStatusMap.clear(); + publishBundlesIfNeeded(true, wipeOther); + } + + @Override + public int getOwnDeviceId() { + return axolotlStore.getLocalRegistrationId(); + } + + @Override + public Set<Integer> getOwnDeviceIds() { + return this.deviceIds.get(account.getJid().toBareJid()); + } + + private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> 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 + && session.getTrust() == from) { + session.setTrust(to); + } + } + } + + @Override + public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) { + if (jid.toBareJid().equals(account.getJid().toBareJid())) { + if (!deviceIds.isEmpty()) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Received non-empty own device list. Resetting publish attemps and pepBroken status."); + pepBroken = false; + numPublishTriesOnEmptyPep = 0; + } + if (deviceIds.contains(getOwnDeviceId())) { + deviceIds.remove(getOwnDeviceId()); + } else { + publishOwnDeviceId(deviceIds); + } + 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, XmppAxolotlSession.Trust.TRUSTED, + XmppAxolotlSession.Trust.INACTIVE_TRUSTED); + setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED_X509, + XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509); + 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, XmppAxolotlSession.Trust.INACTIVE_TRUSTED, + XmppAxolotlSession.Trust.TRUSTED); + setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509, + XmppAxolotlSession.Trust.TRUSTED_X509); + 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(null); + } + + @Override + public void wipeOtherPepDevices() { + if (pepBroken) { + Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... "); + return; + } + Set<Integer> deviceIds = new HashSet<>(); + deviceIds.add(getOwnDeviceId()); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Wiping all other devices from Pep:" + publish); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + } + }); + } + + @Override + public void purgeKey(final String fingerprint) { + axolotlStore.setFingerprintTrust(fingerprint.replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED); + } + + @Override + public void publishOwnDeviceIdIfNeeded() { + if (pepBroken) { + Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... "); + return; + } + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids."); + } else { + Element item = mXmppConnectionService.getIqParser().getItem(packet); + Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + if (!deviceIds.contains(getOwnDeviceId())) { + publishOwnDeviceId(deviceIds); + } + } + } + }); + } + + @Override + public void publishOwnDeviceId(Set<Integer> deviceIds) { + Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds); + if (!deviceIdsCopy.contains(getOwnDeviceId())) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist."); + if (deviceIdsCopy.isEmpty()) { + if (numPublishTriesOnEmptyPep >= publishTriesThreshold) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting..."); + pepBroken = true; + return; + } else { + numPublishTriesOnEmptyPep++; + Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")"); + } + } else { + numPublishTriesOnEmptyPep = 0; + } + deviceIdsCopy.add(getOwnDeviceId()); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIdsCopy); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() != IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error")); + } + } + }); + } + } + + @Override + public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord, + final Set<PreKeyRecord> preKeyRecords, + final boolean announceAfter, + final boolean wipe) { + try { + IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey(); + PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); + X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias()); + Signature verifier = Signature.getInstance("sha256WithRSA"); + verifier.initSign(x509PrivateKey,mXmppConnectionService.getRNG()); + verifier.update(axolotlPublicKey.serialize()); + byte[] signature = verifier.sign(); + IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + ": publish verification for device "+getOwnDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) { + if (pepBroken) { + Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... "); + return; + } + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + return; //ignore timeout. do nothing + } + + if (packet.getType() == IqPacket.TYPE.ERROR) { + Element error = packet.findChild("error"); + if (error == null || !error.hasChild("item-not-found")) { + pepBroken = true; + Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet); + return; + } + } + + PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); + Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); + boolean flush = false; + if (bundle == null) { + Log.w(Config.LOGTAG, AxolotlServiceImpl.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, AxolotlServiceImpl.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, AxolotlServiceImpl.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); + changed = true; + } + + // Validate signedPreKeyRecord + ID + SignedPreKeyRecord signedPreKeyRecord; + int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); + try { + signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); + if (flush + || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) + || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { + Log.i(Config.LOGTAG, AxolotlServiceImpl.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, AxolotlServiceImpl.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } + + // Validate PreKeys + Set<PreKeyRecord> preKeyRecords = new HashSet<>(); + if (keys != null) { + for (Integer id : keys.keySet()) { + try { + PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); + if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { + preKeyRecords.add(preKeyRecord); + } + } catch (InvalidKeyIdException ignored) { + } + } + } + int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); + if (newKeys > 0) { + List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId() + 1, newKeys); + preKeyRecords.addAll(newRecords); + for (PreKeyRecord record : newRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + changed = true; + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP."); + } + + + if (changed) { + if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) { + mXmppConnectionService.publishDisplayName(account); + publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); + } else { + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); + } + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current"); + if (wipe) { + wipeOtherPepDevices(); + } else if (announce) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); + publishOwnDeviceIdIfNeeded(); + } + } + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + } + } + }); + } + + private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord, + Set<PreKeyRecord> preKeyRecords, + final boolean announceAfter, + final boolean wipe) { + IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( + signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), + preKeyRecords, getOwnDeviceId()); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Successfully published bundle. "); + if (wipe) { + wipeOtherPepDevices(); + } else if (announceAfter) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); + publishOwnDeviceIdIfNeeded(); + } + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error")); + } + } + }); + } + + @Override + public boolean isContactAxolotlCapable(Contact contact) { + Jid jid = contact.getJid().toBareJid(); + return hasAny(contact) || + (deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty()); + } + + @Override + public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) { + return axolotlStore.getFingerprintTrust(fingerprint); + } + + @Override + public X509Certificate getFingerprintCertificate(String fingerprint) { + return axolotlStore.getFingerprintCertificate(fingerprint); + } + + @Override + public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) { + axolotlStore.setFingerprintTrust(fingerprint, trust); + } + + private void verifySessionWithPEP(final XmppAxolotlSession session) { + Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); + final AxolotlAddress address = session.getRemoteAddress(); + final IdentityKey identityKey = session.getIdentityKey(); + try { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.fromString(address.getName()), address.getDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Pair<X509Certificate[],byte[]> verification = mXmppConnectionService.getIqParser().verification(packet); + if (verification != null) { + try { + Signature verifier = Signature.getInstance("sha256WithRSA"); + verifier.initVerify(verification.first[0]); + verifier.update(identityKey.serialize()); + if (verifier.verify(verification.second)) { + try { + mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); + String fingerprint = session.getFingerprint(); + Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint); + setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509); + axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); + fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); + Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); + try { + final String cn = information.getString("subject_cn"); + final Jid jid = Jid.fromString(address.getName()); + Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn); + account.getRoster().getContact(jid).setCommonName(cn); + } catch (final InvalidJidException ignored) { + //ignored + } + finishBuildingSessionsFromPEP(address); + return; + } catch (Exception e) { + Log.d(Config.LOGTAG,"could not verify certificate"); + } + } + } catch (Exception e) { + Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); + } + } else { + Log.d(Config.LOGTAG,"no verification found"); + } + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + } + }); + } catch (InvalidJidException e) { + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + } + } + + private void finishBuildingSessionsFromPEP(final AxolotlAddress address) { + AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0); + if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) + && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) { + FetchStatus report = null; + if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.SUCCESS_VERIFIED) + | fetchStatusMap.getAll(address).containsValue(FetchStatus.SUCCESS_VERIFIED)) { + report = FetchStatus.SUCCESS_VERIFIED; + } else if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.ERROR) + || fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) { + report = FetchStatus.ERROR; + } + mXmppConnectionService.keyStatusUpdated(report); + } + } + + private void buildSessionFromPEP(final AxolotlAddress address) { + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Building new sesstion for " + address.toString()); + if (address.getDeviceId() == getOwnDeviceId()) { + throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!"); + } + + try { + IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice( + Jid.fromString(address.getName()), address.getDeviceId()); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket); + mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + fetchStatusMap.put(address, FetchStatus.TIMEOUT); + } else if (packet.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.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, AxolotlServiceImpl.getLogprefix(account) + "preKey IQ packet invalid: " + packet); + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + return; + } + Random random = new Random(); + final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); + if (preKey == null) { + //should never happen + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + return; + } + + final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(), + preKey.getPreKeyId(), preKey.getPreKey(), + bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), + bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + + try { + SessionBuilder builder = new SessionBuilder(axolotlStore, address); + builder.process(preKeyBundle); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey()); + sessions.put(address, session); + if (Config.X509_VERIFICATION) { + verifySessionWithPEP(session); + } else { + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + } + } catch (UntrustedIdentityException | InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Error building session for " + address + ": " + + e.getClass().getName() + ", " + e.getMessage()); + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + } + } else { + fetchStatusMap.put(address, FetchStatus.ERROR); + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error")); + finishBuildingSessionsFromPEP(address); + } + } + }); + } catch (InvalidJidException e) { + Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Got address with invalid jid: " + address.getName()); + } + } + + @Override + public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) { + return findDevicesWithoutSession(conversation.getContact().getJid().toBareJid()); + } + + @Override + public Set<AxolotlAddress> findDevicesWithoutSession(final Jid contactJid) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Finding devices without session for " + contactJid); + Set<AxolotlAddress> addresses = new HashSet<>(); + if (deviceIds.get(contactJid) != null) { + for (Integer foreignId : this.deviceIds.get(contactJid)) { + AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId); + if (sessions.get(address) == null) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if (identityKey != null) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Found device " + contactJid + ":" + foreignId); + if (fetchStatusMap.get(address) != FetchStatus.ERROR) { + addresses.add(address); + } else { + Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken"); + } + } + } + } + } else { + Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Have no target devices in PEP!"); + } + 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) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if (identityKey != null) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId); + if (fetchStatusMap.get(address) != FetchStatus.ERROR) { + addresses.add(address); + } else { + Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken"); + } + } + } + } + } + + return addresses; + } + + @Override + public boolean createSessionsIfNeeded(final Conversation conversation) { + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Creating axolotl sessions if needed..."); + boolean newSessions = false; + Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation); + for (AxolotlAddress address : addresses) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Processing device: " + address.toString()); + FetchStatus status = fetchStatusMap.get(address); + if (status == null || status == FetchStatus.TIMEOUT) { + fetchStatusMap.put(address, FetchStatus.PENDING); + this.buildSessionFromPEP(address); + newSessions = true; + } else if (status == FetchStatus.PENDING) { + newSessions = true; + } else { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Already fetching bundle for " + address.toString()); + } + } + + return newSessions; + } + + @Override + public boolean trustedSessionVerified(final Conversation conversation) { + Set<XmppAxolotlSession> sessions = findSessionsforContact(conversation.getContact()); + sessions.addAll(findOwnSessions()); + boolean verified = false; + for(XmppAxolotlSession session : sessions) { + if (session.getTrust().trusted()) { + if (session.getTrust() == XmppAxolotlSession.Trust.TRUSTED_X509) { + verified = true; + } else { + return false; + } + } + } + return verified; + } + + @Override + public boolean hasPendingKeyFetches(Account account, Contact contact) { + AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0); + AxolotlAddress foreignAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); + return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) + || fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING); + + } + + @Nullable + private XmppAxolotlMessage buildHeader(Contact contact) { + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage( + contact.getJid().toBareJid(), getOwnDeviceId()); + + Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact); + Set<XmppAxolotlSession> ownSessions = findOwnSessions(); + if (contactSessions.isEmpty()) { + return null; + } + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Building axolotl foreign keyElements..."); + for (XmppAxolotlSession session : contactSessions) { + Log.v(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + session.getRemoteAddress().toString()); + axolotlMessage.addDevice(session); + } + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Building axolotl own keyElements..."); + for (XmppAxolotlSession session : ownSessions) { + Log.v(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + session.getRemoteAddress().toString()); + axolotlMessage.addDevice(session); + } + + return axolotlMessage; + } + + @Override + @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; + } + + @Override + public void preparePayloadMessage(final Message message, final boolean delay) { + executor.execute(new Runnable() { + @Override + public void run() { + XmppAxolotlMessage axolotlMessage = encrypt(message); + if (axolotlMessage == null) { + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + //mXmppConnectionService.updateConversationUi(); + } else { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); + messageCache.put(message.getUuid(), axolotlMessage); + mXmppConnectionService.resendMessage(message, delay); + } + } + }); + } + + @Override + public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) { + executor.execute(new Runnable() { + @Override + public void run() { + XmppAxolotlMessage axolotlMessage = buildHeader(contact); + onMessageCreatedCallback.run(axolotlMessage); + } + }); + } + + @Override + public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { + XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid()); + if (axolotlMessage != null) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Cache hit: " + message.getUuid()); + messageCache.remove(message.getUuid()); + } else { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Cache miss: " + message.getUuid()); + } + return axolotlMessage; + } + + private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + return (identityKey != null) + ? new XmppAxolotlSession(account, axolotlStore, address, identityKey) + : null; + } + + private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) { + AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(), + message.getSenderDeviceId()); + XmppAxolotlSession session = sessions.get(senderAddress); + if (session == null) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.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); + } + } + return session; + } + + @Override + 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(false, false); + session.resetPreKeyId(); + } + } catch (CryptoFailedException e) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage()); + } + + if (session.isFresh() && plaintextMessage != null) { + putFreshSession(session); + } + + return plaintextMessage; + } + + @Override + public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) { + XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; + + XmppAxolotlSession session = getReceivingSession(message); + keyTransportMessage = message.getParameters(session, getOwnDeviceId()); + + if (session.isFresh() && keyTransportMessage != null) { + putFreshSession(session); + } + + return keyTransportMessage; + } + + private void putFreshSession(XmppAxolotlSession session) { + Log.d(Config.LOGTAG,"put fresh session"); + sessions.put(session); + if (Config.X509_VERIFICATION) { + if (session.getIdentityKey() != null) { + verifySessionWithPEP(session); + } else { + Log.e(Config.LOGTAG,account.getJid().toBareJid()+": identity key was empty after reloading for x509 verification"); + } + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceStub.java b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceStub.java new file mode 100644 index 00000000..a07905ab --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceStub.java @@ -0,0 +1,196 @@ +package de.thedevstack.conversationsplus.crypto.axolotl; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; + +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Set; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Contact; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +/** + * Axolotl Service Stub implementation to avoid axolotl usage. + */ +public class AxolotlServiceStub implements AxolotlService { + + @Override + public boolean fetchMapHasErrors(Contact contact) { + return false; + } + + @Override + public String getOwnFingerprint() { + return null; + } + + @Override + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) { + return Collections.emptySet(); + } + + @Override + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact) { + return Collections.emptySet(); + } + + @Override + public long getNumTrustedKeys(Contact contact) { + return 0; + } + + @Override + public Set<String> getFingerprintsForOwnSessions() { + return Collections.emptySet(); + } + + @Override + public Set<String> getFingerprintsForContact(Contact contact) { + return Collections.emptySet(); + } + + @Override + public boolean isPepBroken() { + return true; + } + + @Override + public void regenerateKeys(boolean wipeOther) { + + } + + @Override + public int getOwnDeviceId() { + return 0; + } + + @Override + public Set<Integer> getOwnDeviceIds() { + return Collections.emptySet(); + } + + @Override + public void registerDevices(Jid jid, @NonNull Set<Integer> deviceIds) { + + } + + @Override + public void wipeOtherPepDevices() { + + } + + @Override + public void purgeKey(String fingerprint) { + + } + + @Override + public void publishOwnDeviceIdIfNeeded() { + + } + + @Override + public void publishOwnDeviceId(Set<Integer> deviceIds) { + + } + + @Override + public void publishDeviceVerificationAndBundle(SignedPreKeyRecord signedPreKeyRecord, Set<PreKeyRecord> preKeyRecords, boolean announceAfter, boolean wipe) { + + } + + @Override + public void publishBundlesIfNeeded(boolean announce, boolean wipe) { + + } + + @Override + public boolean isContactAxolotlCapable(Contact contact) { + return false; + } + + @Override + public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) { + return XmppAxolotlSession.Trust.TRUSTED; + } + + @Override + public X509Certificate getFingerprintCertificate(String fingerprint) { + return null; + } + + @Override + public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) { + + } + + @Override + public Set<AxolotlAddress> findDevicesWithoutSession(Conversation conversation) { + return Collections.emptySet(); + } + + @Override + public Set<AxolotlAddress> findDevicesWithoutSession(Jid contactJid) { + return Collections.emptySet(); + } + + @Override + public boolean createSessionsIfNeeded(Conversation conversation) { + return false; + } + + @Override + public boolean trustedSessionVerified(Conversation conversation) { + return false; + } + + @Override + public boolean hasPendingKeyFetches(Account account, Contact contact) { + return false; + } + + @Nullable + @Override + public XmppAxolotlMessage encrypt(Message message) { + return null; + } + + @Override + public void preparePayloadMessage(Message message, boolean delay) { + + } + + @Override + public void prepareKeyTransportMessage(Contact contact, OnMessageCreatedCallback onMessageCreatedCallback) { + + } + + @Override + public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { + return null; + } + + @Override + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) { + return null; + } + + @Override + public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) { + return null; + } + + @Override + public void onAdvancedStreamFeaturesAvailable(Account account) { + + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/CryptoFailedException.java b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/CryptoFailedException.java new file mode 100644 index 00000000..df281248 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/CryptoFailedException.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.crypto.axolotl; + +public class CryptoFailedException extends Exception { + public CryptoFailedException(Exception e){ + super(e); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/NoSessionsCreatedException.java b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/NoSessionsCreatedException.java new file mode 100644 index 00000000..bcb1c6a8 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/NoSessionsCreatedException.java @@ -0,0 +1,4 @@ +package de.thedevstack.conversationsplus.crypto.axolotl; + +public class NoSessionsCreatedException extends Throwable{ +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/OnMessageCreatedCallback.java b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/OnMessageCreatedCallback.java new file mode 100644 index 00000000..082b514b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/OnMessageCreatedCallback.java @@ -0,0 +1,5 @@ +package de.thedevstack.conversationsplus.crypto.axolotl; + +public interface OnMessageCreatedCallback { + void run(XmppAxolotlMessage message); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/SQLiteAxolotlStore.java new file mode 100644 index 00000000..35f630bb --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/SQLiteAxolotlStore.java @@ -0,0 +1,429 @@ +package de.thedevstack.conversationsplus.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.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.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 CERTIFICATE = "certificate"; + + 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, AxolotlServiceImpl.getLogprefix(account) + "Got Axolotl signed prekey record:" + record.getId()); + } + } + + public int getCurrentPreKeyId() { + return currentPreKeyId; + } + + // -------------------------------------- + // IdentityKeyStore + // -------------------------------------- + + private IdentityKeyPair loadIdentityKeyPair() { + IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account); + + if (ownKey != null) { + return ownKey; + } else { + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Could not retrieve own IdentityKeyPair"); + ownKey = generateIdentityKeyPair(); + mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, 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, AxolotlServiceImpl.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, AxolotlServiceImpl.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, AxolotlServiceImpl.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 void setFingerprintCertificate(String fingerprint, X509Certificate x509Certificate) { + mXmppConnectionService.databaseBackend.setIdentityKeyCertificate(account, fingerprint, x509Certificate); + } + + public X509Certificate getFingerprintCertificate(String fingerprint) { + return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(account, 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, AxolotlServiceImpl.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/de/thedevstack/conversationsplus/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/XmppAxolotlMessage.java new file mode 100644 index 00000000..837b601c --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/XmppAxolotlMessage.java @@ -0,0 +1,249 @@ +package de.thedevstack.conversationsplus.crypto.axolotl; + +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.HashMap; +import java.util.List; +import java.util.Map; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.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 = null; + private byte[] iv = null; + private final Map<Integer, byte[]> keys; + private final Jid from; + private final int sourceDeviceId; + + public static class XmppAxolotlPlaintextMessage { + private final String plaintext; + private final String fingerprint; + + public XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) { + this.plaintext = plaintext; + this.fingerprint = fingerprint; + } + + public String getPlaintext() { + return plaintext; + } + + + public String getFingerprint() { + return fingerprint; + } + } + + public static class XmppAxolotlKeyTransportMessage { + private final String fingerprint; + private final byte[] key; + private final byte[] iv; + + public XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) { + this.fingerprint = fingerprint; + this.key = key; + this.iv = iv; + } + + public String getFingerprint() { + return fingerprint; + } + + public byte[] getKey() { + return key; + } + + public byte[] getIv() { + return iv; + } + } + + private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException { + this.from = from; + 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 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) { + this.from = from; + this.sourceDeviceId = sourceDeviceId; + this.keys = new HashMap<>(); + this.iv = generateIv(); + this.innerKey = generateKey(); + } + + public static XmppAxolotlMessage fromElement(Element element, Jid from) { + return new XmppAxolotlMessage(element, from); + } + + private static byte[] generateKey() { + try { + KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE); + generator.init(128); + 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(CIPHERMODE, PROVIDER); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); + this.innerKey = secretKey.getEncoded(); + this.ciphertext = cipher.doFinal(plaintext.getBytes()); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException + | InvalidAlgorithmParameterException e) { + throw new CryptoFailedException(e); + } + } + + public Jid getFrom() { + return this.from; + } + + public int getSenderDeviceId() { + return sourceDeviceId; + } + + public byte[] getCiphertext() { + return ciphertext; + } + + public void addDevice(XmppAxolotlSession session) { + byte[] key = session.processSending(innerKey); + if (key != null) { + keys.put(session.getRemoteAddress().getDeviceId(), key); + } + } + + public byte[] getInnerKey() { + return innerKey; + } + + public byte[] getIV() { + return this.iv; + } + + 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)); + } + return encryptionElement; + } + + private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) { + byte[] encryptedKey = keys.get(sourceDeviceId); + return (encryptedKey != null) ? session.processReceiving(encryptedKey) : null; + } + + public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) { + byte[] key = unpackKey(session, sourceDeviceId); + return (key != null) + ? new XmppAxolotlKeyTransportMessage(session.getFingerprint(), key, getIV()) + : null; + } + + 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/de/thedevstack/conversationsplus/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/XmppAxolotlSession.java new file mode 100644 index 00000000..47b2133a --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/XmppAxolotlSession.java @@ -0,0 +1,221 @@ +package de.thedevstack.conversationsplus.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.IdentityKey; +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 de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.entities.Account; + +public class XmppAxolotlSession { + private final SessionCipher cipher; + private final SQLiteAxolotlStore sqLiteAxolotlStore; + private final AxolotlAddress remoteAddress; + private final Account account; + private IdentityKey identityKey; + 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), + TRUSTED_X509(7), + INACTIVE_TRUSTED_X509(8); + + 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 TRUSTED_X509: + return "Trusted (X509) " + getCode(); + case INACTIVE_TRUSTED_X509: + return "Inactive (Trusted (X509)) " + 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 boolean trusted() { + return this == TRUSTED_X509 || this == TRUSTED; + } + + public boolean trustedInactive() { + return this == INACTIVE_TRUSTED_X509 || this == INACTIVE_TRUSTED; + } + } + + public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, IdentityKey identityKey) { + this(account, store, remoteAddress); + this.identityKey = identityKey; + } + + 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 identityKey == null ? null : identityKey.getFingerprint().replaceAll("\\s", ""); + } + + public IdentityKey getIdentityKey() { + return identityKey; + } + + public AxolotlAddress getRemoteAddress() { + return remoteAddress; + } + + public boolean isFresh() { + return fresh; + } + + public void setNotFresh() { + this.fresh = false; + } + + protected void setTrust(Trust trust) { + sqLiteAxolotlStore.setFingerprintTrust(getFingerprint(), trust); + } + + protected Trust getTrust() { + Trust trust = sqLiteAxolotlStore.getFingerprintTrust(getFingerprint()); + 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: + case INACTIVE_TRUSTED_X509: + case TRUSTED_X509: + try { + try { + PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey); + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); + IdentityKey msgIdentityKey = message.getIdentityKey(); + if (this.identityKey != null && !this.identityKey.equals(msgIdentityKey)) { + Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint()); + } else { + this.identityKey = msgIdentityKey; + plaintext = cipher.decrypt(message); + if (message.getPreKeyId().isPresent()) { + preKeyId = message.getPreKeyId().get(); + } + } + } catch (InvalidMessageException | InvalidVersionException e) { + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "WhisperMessage received"); + WhisperMessage message = new WhisperMessage(encryptedKey); + plaintext = cipher.decrypt(message); + } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) { + Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); + } + } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) { + Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); + } + + if (plaintext != null) { + if (trust == Trust.INACTIVE_TRUSTED) { + setTrust(Trust.TRUSTED); + } else if (trust == Trust.INACTIVE_TRUSTED_X509) { + setTrust(Trust.TRUSTED_X509); + } + } + + break; + + case COMPROMISED: + default: + // ignore + break; + } + return plaintext; + } + + @Nullable + public byte[] processSending(@NonNull byte[] outgoingMessage) { + Trust trust = getTrust(); + if (trust.trusted()) { + CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); + return ciphertextMessage.serialize(); + } else { + return null; + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/External.java b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/External.java new file mode 100644 index 00000000..dc50cbbe --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/External.java @@ -0,0 +1,30 @@ +package de.thedevstack.conversationsplus.crypto.sasl; + +import android.util.Base64; + +import java.security.SecureRandom; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xml.TagWriter; + +public class External extends SaslMechanism { + + public External(TagWriter tagWriter, Account account, SecureRandom rng) { + super(tagWriter, account, rng); + } + + @Override + public int getPriority() { + return 25; + } + + @Override + public String getMechanism() { + return "EXTERNAL"; + } + + @Override + public String getClientFirstMessage() { + return Base64.encodeToString(account.getJid().toBareJid().toString().getBytes(),Base64.NO_WRAP); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/SaslMechanism.java b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/SaslMechanism.java index ed2764c8..1f6d2bde 100644 --- a/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/SaslMechanism.java +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/SaslMechanism.java @@ -11,7 +11,7 @@ public abstract class SaslMechanism { final protected Account account; final protected SecureRandom rng; - protected static enum State { + protected enum State { INITIAL, AUTH_TEXT_SENT, RESPONSE_SENT, diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/ScramSha1.java b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/ScramSha1.java index a991b1d7..3540d2cf 100644 --- a/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/ScramSha1.java +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/sasl/ScramSha1.java @@ -21,7 +21,6 @@ public class ScramSha1 extends SaslMechanism { // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage. final private static String GS2_HEADER = "n,,"; private String clientFirstMessageBare; - private byte[] serverFirstMessage; final private String clientNonce; private byte[] serverSignature = null; private static HMac HMAC; @@ -101,7 +100,10 @@ public class ScramSha1 extends SaslMechanism { public String getResponse(final String challenge) throws AuthenticationException { switch (state) { case AUTH_TEXT_SENT: - serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT); + if (challenge == null) { + throw new AuthenticationException("challenge can not be null"); + } + byte[] serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT); final Tokenizer tokenizer = new Tokenizer(serverFirstMessage); String nonce = ""; int iterationCount = -1; |