From d783cec97084a12873ca62b5fcd64620056ec01b Mon Sep 17 00:00:00 2001 From: Christian Schneppe Date: Sat, 19 Nov 2016 23:07:54 +0100 Subject: reformat code --- .../de/pixart/messenger/crypto/OtrService.java | 486 ++--- .../messenger/crypto/PgpDecryptionService.java | 24 +- .../java/de/pixart/messenger/crypto/PgpEngine.java | 536 ++--- .../messenger/crypto/XmppDomainVerifier.java | 188 +- .../messenger/crypto/axolotl/AxolotlService.java | 2090 ++++++++++---------- .../crypto/axolotl/CryptoFailedException.java | 6 +- .../crypto/axolotl/FingerprintStatus.java | 6 +- .../crypto/axolotl/NoSessionsCreatedException.java | 2 +- .../crypto/axolotl/OnMessageCreatedCallback.java | 2 +- .../crypto/axolotl/SQLiteAxolotlStore.java | 834 ++++---- .../crypto/axolotl/XmppAxolotlMessage.java | 446 ++--- .../crypto/axolotl/XmppAxolotlSession.java | 218 +- .../de/pixart/messenger/crypto/sasl/Anonymous.java | 30 +- .../de/pixart/messenger/crypto/sasl/DigestMd5.java | 140 +- .../de/pixart/messenger/crypto/sasl/External.java | 36 +- .../de/pixart/messenger/crypto/sasl/Plain.java | 32 +- .../messenger/crypto/sasl/SaslMechanism.java | 107 +- .../de/pixart/messenger/crypto/sasl/ScramSha1.java | 423 ++-- .../de/pixart/messenger/crypto/sasl/Tokenizer.java | 120 +- 19 files changed, 2865 insertions(+), 2861 deletions(-) (limited to 'src/main/java/de/pixart/messenger/crypto') diff --git a/src/main/java/de/pixart/messenger/crypto/OtrService.java b/src/main/java/de/pixart/messenger/crypto/OtrService.java index 18f9670d8..459f49c38 100644 --- a/src/main/java/de/pixart/messenger/crypto/OtrService.java +++ b/src/main/java/de/pixart/messenger/crypto/OtrService.java @@ -38,25 +38,25 @@ import de.pixart.messenger.xmpp.stanzas.MessagePacket; public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { - private Account account; - private OtrPolicy otrPolicy; - private KeyPair keyPair; - private XmppConnectionService mXmppConnectionService; - - public OtrService(XmppConnectionService service, Account account) { - this.account = account; - this.otrPolicy = new OtrPolicyImpl(); - this.otrPolicy.setAllowV1(false); - this.otrPolicy.setAllowV2(true); - this.otrPolicy.setAllowV3(true); - this.keyPair = loadKey(account.getKeys()); - this.mXmppConnectionService = service; - } - - private KeyPair loadKey(final JSONObject keys) { - if (keys == null) { - return null; - } + private Account account; + private OtrPolicy otrPolicy; + private KeyPair keyPair; + private XmppConnectionService mXmppConnectionService; + + public OtrService(XmppConnectionService service, Account account) { + this.account = account; + this.otrPolicy = new OtrPolicyImpl(); + this.otrPolicy.setAllowV1(false); + this.otrPolicy.setAllowV2(true); + this.otrPolicy.setAllowV3(true); + this.keyPair = loadKey(account.getKeys()); + this.mXmppConnectionService = service; + } + + private KeyPair loadKey(final JSONObject keys) { + if (keys == null) { + return null; + } synchronized (keys) { try { BigInteger x = new BigInteger(keys.getString("otr_x"), 16); @@ -78,232 +78,232 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { return null; } } - } - - private void saveKey() { - PublicKey publicKey = keyPair.getPublic(); - PrivateKey privateKey = keyPair.getPrivate(); - KeyFactory keyFactory; - try { - keyFactory = KeyFactory.getInstance("DSA"); - DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec( - privateKey, DSAPrivateKeySpec.class); - DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, - DSAPublicKeySpec.class); - this.account.setKey("otr_x", privateKeySpec.getX().toString(16)); - this.account.setKey("otr_g", privateKeySpec.getG().toString(16)); - this.account.setKey("otr_p", privateKeySpec.getP().toString(16)); - this.account.setKey("otr_q", privateKeySpec.getQ().toString(16)); - this.account.setKey("otr_y", publicKeySpec.getY().toString(16)); - } catch (final NoSuchAlgorithmException | InvalidKeySpecException e) { - e.printStackTrace(); - } + } + + private void saveKey() { + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + KeyFactory keyFactory; + try { + keyFactory = KeyFactory.getInstance("DSA"); + DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec( + privateKey, DSAPrivateKeySpec.class); + DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, + DSAPublicKeySpec.class); + this.account.setKey("otr_x", privateKeySpec.getX().toString(16)); + this.account.setKey("otr_g", privateKeySpec.getG().toString(16)); + this.account.setKey("otr_p", privateKeySpec.getP().toString(16)); + this.account.setKey("otr_q", privateKeySpec.getQ().toString(16)); + this.account.setKey("otr_y", publicKeySpec.getY().toString(16)); + } catch (final NoSuchAlgorithmException | InvalidKeySpecException e) { + e.printStackTrace(); + } + + } + + @Override + public void askForSecret(SessionID id, InstanceTag instanceTag, String question) { + try { + final Jid jid = Jid.fromSessionID(id); + Conversation conversation = this.mXmppConnectionService.find(this.account, jid); + if (conversation != null) { + conversation.smp().hint = question; + conversation.smp().status = Conversation.Smp.STATUS_CONTACT_REQUESTED; + mXmppConnectionService.updateConversationUi(); + } + } catch (InvalidJidException e) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": smp in invalid session " + id.toString()); + } + } + + @Override + public void finishedSessionMessage(SessionID arg0, String arg1) + throws OtrException { + + } + + @Override + public String getFallbackMessage(SessionID arg0) { + return MessageGenerator.OTR_FALLBACK_MESSAGE; + } + + @Override + public byte[] getLocalFingerprintRaw(SessionID arg0) { + try { + return getFingerprintRaw(getPublicKey()); + } catch (OtrCryptoException e) { + return null; + } + } + + public PublicKey getPublicKey() { + if (this.keyPair == null) { + return null; + } + return this.keyPair.getPublic(); + } + + @Override + public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException { + if (this.keyPair == null) { + KeyPairGenerator kg; + try { + kg = KeyPairGenerator.getInstance("DSA"); + this.keyPair = kg.genKeyPair(); + this.saveKey(); + mXmppConnectionService.databaseBackend.updateAccount(account); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, + "error generating key pair " + e.getMessage()); + } + } + return this.keyPair; + } + + @Override + public String getReplyForUnreadableMessage(SessionID arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public OtrPolicy getSessionPolicy(SessionID arg0) { + return otrPolicy; + } + + @Override + public void injectMessage(SessionID session, String body) + throws OtrException { + MessagePacket packet = new MessagePacket(); + packet.setFrom(account.getJid()); + if (session.getUserID().isEmpty()) { + packet.setAttribute("to", session.getAccountID()); + } else { + packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID()); + } + packet.setBody(body); + MessageGenerator.addMessageHints(packet); + try { + Jid jid = Jid.fromSessionID(session); + Conversation conversation = mXmppConnectionService.find(account, jid); + if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + if (mXmppConnectionService.sendChatStates()) { + packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); + } + } + } catch (final InvalidJidException ignored) { + + } + + packet.setType(MessagePacket.TYPE_CHAT); + account.getXmppConnection().sendMessagePacket(packet); + } + + @Override + public void messageFromAnotherInstanceReceived(SessionID session) { + sendOtrErrorMessage(session, "Message from another OTR-instance received"); + } + + @Override + public void multipleInstancesDetected(SessionID arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void requireEncryptedMessage(SessionID arg0, String arg1) + throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void showError(SessionID arg0, String arg1) throws OtrException { + Log.d(Config.LOGTAG, "show error"); + } + + @Override + public void smpAborted(SessionID id) throws OtrException { + setSmpStatus(id, Conversation.Smp.STATUS_NONE); + } + + private void setSmpStatus(SessionID id, int status) { + try { + final Jid jid = Jid.fromSessionID(id); + Conversation conversation = this.mXmppConnectionService.find(this.account, jid); + if (conversation != null) { + conversation.smp().status = status; + mXmppConnectionService.updateConversationUi(); + } + } catch (final InvalidJidException ignored) { + + } + } + + @Override + public void smpError(SessionID id, int arg1, boolean arg2) + throws OtrException { + setSmpStatus(id, Conversation.Smp.STATUS_NONE); + } + + @Override + public void unencryptedMessageReceived(SessionID arg0, String arg1) + throws OtrException { + throw new OtrException(new Exception("unencrypted message received")); + } + + @Override + public void unreadableMessageReceived(SessionID session) throws OtrException { + Log.d(Config.LOGTAG, "unreadable message received"); + sendOtrErrorMessage(session, "You sent me an unreadable OTR-encrypted message"); + } + + public void sendOtrErrorMessage(SessionID session, String errorText) { + try { + Jid jid = Jid.fromSessionID(session); + Conversation conversation = mXmppConnectionService.find(account, jid); + String id = conversation == null ? null : conversation.getLastReceivedOtrMessageId(); + if (id != null) { + MessagePacket packet = mXmppConnectionService.getMessageGenerator() + .generateOtrError(jid, id, errorText); + packet.setFrom(account.getJid()); + mXmppConnectionService.sendMessagePacket(account, packet); + Log.d(Config.LOGTAG, packet.toString()); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": unreadable OTR message in " + conversation.getName()); + } + } catch (InvalidJidException e) { + return; + } + } + @Override + public void unverify(SessionID id, String arg1) { + setSmpStatus(id, Conversation.Smp.STATUS_FAILED); } - @Override - public void askForSecret(SessionID id, InstanceTag instanceTag, String question) { - try { - final Jid jid = Jid.fromSessionID(id); - Conversation conversation = this.mXmppConnectionService.find(this.account,jid); - if (conversation!=null) { - conversation.smp().hint = question; - conversation.smp().status = Conversation.Smp.STATUS_CONTACT_REQUESTED; - mXmppConnectionService.updateConversationUi(); - } - } catch (InvalidJidException e) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": smp in invalid session "+id.toString()); - } - } - - @Override - public void finishedSessionMessage(SessionID arg0, String arg1) - throws OtrException { - - } - - @Override - public String getFallbackMessage(SessionID arg0) { - return MessageGenerator.OTR_FALLBACK_MESSAGE; - } - - @Override - public byte[] getLocalFingerprintRaw(SessionID arg0) { - try { - return getFingerprintRaw(getPublicKey()); - } catch (OtrCryptoException e) { - return null; - } - } - - public PublicKey getPublicKey() { - if (this.keyPair == null) { - return null; - } - return this.keyPair.getPublic(); - } - - @Override - public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException { - if (this.keyPair == null) { - KeyPairGenerator kg; - try { - kg = KeyPairGenerator.getInstance("DSA"); - this.keyPair = kg.genKeyPair(); - this.saveKey(); - mXmppConnectionService.databaseBackend.updateAccount(account); - } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, - "error generating key pair " + e.getMessage()); - } - } - return this.keyPair; - } - - @Override - public String getReplyForUnreadableMessage(SessionID arg0) { - // TODO Auto-generated method stub - return null; - } - - @Override - public OtrPolicy getSessionPolicy(SessionID arg0) { - return otrPolicy; - } - - @Override - public void injectMessage(SessionID session, String body) - throws OtrException { - MessagePacket packet = new MessagePacket(); - packet.setFrom(account.getJid()); - if (session.getUserID().isEmpty()) { - packet.setAttribute("to", session.getAccountID()); - } else { - packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID()); - } - packet.setBody(body); - MessageGenerator.addMessageHints(packet); - try { - Jid jid = Jid.fromSessionID(session); - Conversation conversation = mXmppConnectionService.find(account,jid); - if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { - if (mXmppConnectionService.sendChatStates()) { - packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); - } - } - } catch (final InvalidJidException ignored) { - - } - - packet.setType(MessagePacket.TYPE_CHAT); - account.getXmppConnection().sendMessagePacket(packet); - } - - @Override - public void messageFromAnotherInstanceReceived(SessionID session) { - sendOtrErrorMessage(session, "Message from another OTR-instance received"); - } - - @Override - public void multipleInstancesDetected(SessionID arg0) { - // TODO Auto-generated method stub - - } - - @Override - public void requireEncryptedMessage(SessionID arg0, String arg1) - throws OtrException { - // TODO Auto-generated method stub - - } - - @Override - public void showError(SessionID arg0, String arg1) throws OtrException { - Log.d(Config.LOGTAG,"show error"); - } - - @Override - public void smpAborted(SessionID id) throws OtrException { - setSmpStatus(id, Conversation.Smp.STATUS_NONE); - } - - private void setSmpStatus(SessionID id, int status) { - try { - final Jid jid = Jid.fromSessionID(id); - Conversation conversation = this.mXmppConnectionService.find(this.account,jid); - if (conversation!=null) { - conversation.smp().status = status; - mXmppConnectionService.updateConversationUi(); - } - } catch (final InvalidJidException ignored) { - - } - } - - @Override - public void smpError(SessionID id, int arg1, boolean arg2) - throws OtrException { - setSmpStatus(id, Conversation.Smp.STATUS_NONE); - } - - @Override - public void unencryptedMessageReceived(SessionID arg0, String arg1) - throws OtrException { - throw new OtrException(new Exception("unencrypted message received")); - } - - @Override - public void unreadableMessageReceived(SessionID session) throws OtrException { - Log.d(Config.LOGTAG,"unreadable message received"); - sendOtrErrorMessage(session, "You sent me an unreadable OTR-encrypted message"); - } - - public void sendOtrErrorMessage(SessionID session, String errorText) { - try { - Jid jid = Jid.fromSessionID(session); - Conversation conversation = mXmppConnectionService.find(account, jid); - String id = conversation == null ? null : conversation.getLastReceivedOtrMessageId(); - if (id != null) { - MessagePacket packet = mXmppConnectionService.getMessageGenerator() - .generateOtrError(jid, id, errorText); - packet.setFrom(account.getJid()); - mXmppConnectionService.sendMessagePacket(account,packet); - Log.d(Config.LOGTAG,packet.toString()); - Log.d(Config.LOGTAG,account.getJid().toBareJid().toString() - +": unreadable OTR message in "+conversation.getName()); - } - } catch (InvalidJidException e) { - return; - } - } - - @Override - public void unverify(SessionID id, String arg1) { - setSmpStatus(id, Conversation.Smp.STATUS_FAILED); - } - - @Override - public void verify(SessionID id, String fingerprint, boolean approved) { - Log.d(Config.LOGTAG,"OtrService.verify("+id.toString()+","+fingerprint+","+String.valueOf(approved)+")"); - try { - final Jid jid = Jid.fromSessionID(id); - Conversation conversation = this.mXmppConnectionService.find(this.account,jid); - if (conversation!=null) { - if (approved) { - conversation.getContact().addOtrFingerprint(fingerprint); - } - conversation.smp().hint = null; - conversation.smp().status = Conversation.Smp.STATUS_VERIFIED; - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); - } - } catch (final InvalidJidException ignored) { - } - } - - @Override - public FragmenterInstructions getFragmenterInstructions(SessionID sessionID) { - return null; - } + @Override + public void verify(SessionID id, String fingerprint, boolean approved) { + Log.d(Config.LOGTAG, "OtrService.verify(" + id.toString() + "," + fingerprint + "," + String.valueOf(approved) + ")"); + try { + final Jid jid = Jid.fromSessionID(id); + Conversation conversation = this.mXmppConnectionService.find(this.account, jid); + if (conversation != null) { + if (approved) { + conversation.getContact().addOtrFingerprint(fingerprint); + } + conversation.smp().hint = null; + conversation.smp().status = Conversation.Smp.STATUS_VERIFIED; + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); + } + } catch (final InvalidJidException ignored) { + } + } + + @Override + public FragmenterInstructions getFragmenterInstructions(SessionID sessionID) { + return null; + } } diff --git a/src/main/java/de/pixart/messenger/crypto/PgpDecryptionService.java b/src/main/java/de/pixart/messenger/crypto/PgpDecryptionService.java index ffa853fda..4b0e78764 100644 --- a/src/main/java/de/pixart/messenger/crypto/PgpDecryptionService.java +++ b/src/main/java/de/pixart/messenger/crypto/PgpDecryptionService.java @@ -28,9 +28,9 @@ public class PgpDecryptionService { private final XmppConnectionService mXmppConnectionService; private OpenPgpApi openPgpApi = null; - protected final ArrayDeque messages = new ArrayDeque(); + protected final ArrayDeque messages = new ArrayDeque(); protected final HashSet pendingNotifications = new HashSet<>(); - Message currentMessage; + Message currentMessage; private PendingIntent pendingIntent; @@ -38,7 +38,7 @@ public class PgpDecryptionService { this.mXmppConnectionService = service; } - public synchronized boolean decrypt(final Message message, boolean notify) { + public synchronized boolean decrypt(final Message message, boolean notify) { messages.add(message); if (notify && pendingIntent == null) { pendingNotifications.add(message); @@ -48,10 +48,10 @@ public class PgpDecryptionService { continueDecryption(); return notify; } - } + } public synchronized void decrypt(final List list) { - for(Message message : list) { + for (Message message : list) { if (message.getEncryption() == Message.ENCRYPTION_PGP) { messages.add(message); } @@ -69,19 +69,19 @@ public class PgpDecryptionService { this.pendingNotifications.remove(message); } - protected synchronized void decryptNext() { - if (pendingIntent == null + protected synchronized void decryptNext() { + if (pendingIntent == null && getOpenPgpApi() != null - && (currentMessage = messages.poll()) != null) { - new Thread(new Runnable() { + && (currentMessage = messages.poll()) != null) { + new Thread(new Runnable() { @Override public void run() { executeApi(currentMessage); decryptNext(); } }).start(); - } - } + } + } public synchronized void continueDecryption(boolean resetPending) { if (resetPending) { @@ -200,7 +200,7 @@ public class PgpDecryptionService { if (pendingIntent == null) { return false; } else { - for(Message message : messages) { + for (Message message : messages) { if (message.getConversation() == conversation) { return true; } diff --git a/src/main/java/de/pixart/messenger/crypto/PgpEngine.java b/src/main/java/de/pixart/messenger/crypto/PgpEngine.java index 38d097ec3..c906c7514 100644 --- a/src/main/java/de/pixart/messenger/crypto/PgpEngine.java +++ b/src/main/java/de/pixart/messenger/crypto/PgpEngine.java @@ -29,290 +29,290 @@ import de.pixart.messenger.services.XmppConnectionService; import de.pixart.messenger.ui.UiCallback; public class PgpEngine { - private OpenPgpApi api; - private XmppConnectionService mXmppConnectionService; + private OpenPgpApi api; + private XmppConnectionService mXmppConnectionService; - public PgpEngine(OpenPgpApi api, XmppConnectionService service) { - this.api = api; - this.mXmppConnectionService = service; - } + public PgpEngine(OpenPgpApi api, XmppConnectionService service) { + this.api = api; + this.mXmppConnectionService = service; + } - public void encrypt(final Message message, final UiCallback callback) { - Intent params = new Intent(); - params.setAction(OpenPgpApi.ACTION_ENCRYPT); - 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, conversation.getMucOptions().getPgpKeyIds()); - } + public void encrypt(final Message message, final UiCallback callback) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_ENCRYPT); + 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, conversation.getMucOptions().getPgpKeyIds()); + } - if (!message.needsUploading()) { - params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - String body; - if (message.hasFileOnRemoteHost()) { - body = message.getFileParams().url.toString(); - } else { - body = message.getBody(); - } - InputStream is = new ByteArrayInputStream(body.getBytes()); - final OutputStream os = new ByteArrayOutputStream(); - api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + if (!message.needsUploading()) { + params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + String body; + if (message.hasFileOnRemoteHost()) { + body = message.getFileParams().url.toString(); + } else { + body = message.getBody(); + } + InputStream is = new ByteArrayInputStream(body.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)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - try { - os.flush(); - StringBuilder encryptedMessageBody = new StringBuilder(); - String[] lines = os.toString().split("\n"); - for (int i = 2; i < lines.length - 1; ++i) { - if (!lines[i].contains("Version")) { - encryptedMessageBody.append(lines[i].trim()); - } - } - message.setEncryptedBody(encryptedMessageBody - .toString()); - callback.success(message); - } catch (IOException e) { - callback.error(R.string.openpgp_error, message); - } + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + try { + os.flush(); + StringBuilder encryptedMessageBody = new StringBuilder(); + String[] lines = os.toString().split("\n"); + for (int i = 2; i < lines.length - 1; ++i) { + if (!lines[i].contains("Version")) { + encryptedMessageBody.append(lines[i].trim()); + } + } + message.setEncryptedBody(encryptedMessageBody + .toString()); + callback.success(message); + } catch (IOException e) { + callback.error(R.string.openpgp_error, message); + } - break; - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT), - message); - break; - case OpenPgpApi.RESULT_CODE_ERROR: - logError(conversation.getAccount(), (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); - callback.error(R.string.openpgp_error, message); - break; - } - } - }); - } else { - try { - DownloadableFile inputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, true); - DownloadableFile outputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, false); - outputFile.getParentFile().mkdirs(); - outputFile.createNewFile(); - final InputStream is = new FileInputStream(inputFile); - final OutputStream os = new FileOutputStream(outputFile); - api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + break; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + break; + case OpenPgpApi.RESULT_CODE_ERROR: + logError(conversation.getAccount(), (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); + callback.error(R.string.openpgp_error, message); + break; + } + } + }); + } else { + try { + DownloadableFile inputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, true); + DownloadableFile outputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, false); + outputFile.getParentFile().mkdirs(); + outputFile.createNewFile(); + 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) { - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, - OpenPgpApi.RESULT_CODE_ERROR)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - try { - os.flush(); - } catch (IOException ignored) { - //ignored - } - FileBackend.close(os); - callback.success(message); - break; - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequried( - (PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT), - message); - break; - case OpenPgpApi.RESULT_CODE_ERROR: - logError(conversation.getAccount(), (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); - callback.error(R.string.openpgp_error, message); - break; - } - } - }); - } catch (final IOException e) { - callback.error(R.string.openpgp_error, message); - } - } - } + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + try { + os.flush(); + } catch (IOException ignored) { + //ignored + } + FileBackend.close(os); + callback.success(message); + break; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried( + (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + break; + case OpenPgpApi.RESULT_CODE_ERROR: + logError(conversation.getAccount(), (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); + callback.error(R.string.openpgp_error, message); + break; + } + } + }); + } catch (final IOException e) { + callback.error(R.string.openpgp_error, message); + } + } + } - public long fetchKeyId(Account account, String status, String signature) { - if ((signature == null) || (api == null)) { - return 0; - } - if (status == null) { - status = ""; - } - final StringBuilder pgpSig = new StringBuilder(); - pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----"); - pgpSig.append('\n'); - pgpSig.append('\n'); - pgpSig.append(status); - pgpSig.append('\n'); - pgpSig.append("-----BEGIN PGP SIGNATURE-----"); - pgpSig.append('\n'); - pgpSig.append('\n'); - pgpSig.append(signature.replace("\n", "").trim()); - pgpSig.append('\n'); - pgpSig.append("-----END PGP SIGNATURE-----"); - Intent params = new Intent(); - params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); - params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes()); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - Intent result = api.executeApi(params, is, os); - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, - OpenPgpApi.RESULT_CODE_ERROR)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - OpenPgpSignatureResult sigResult = result - .getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE); - if (sigResult != null) { - return sigResult.getKeyId(); - } else { - return 0; - } - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - return 0; - case OpenPgpApi.RESULT_CODE_ERROR: - logError(account, (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); - return 0; - } - return 0; - } + public long fetchKeyId(Account account, String status, String signature) { + if ((signature == null) || (api == null)) { + return 0; + } + if (status == null) { + status = ""; + } + final StringBuilder pgpSig = new StringBuilder(); + pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----"); + pgpSig.append('\n'); + pgpSig.append('\n'); + pgpSig.append(status); + pgpSig.append('\n'); + pgpSig.append("-----BEGIN PGP SIGNATURE-----"); + pgpSig.append('\n'); + pgpSig.append('\n'); + pgpSig.append(signature.replace("\n", "").trim()); + pgpSig.append('\n'); + pgpSig.append("-----END PGP SIGNATURE-----"); + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes()); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Intent result = api.executeApi(params, is, os); + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + OpenPgpSignatureResult sigResult = result + .getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE); + if (sigResult != null) { + return sigResult.getKeyId(); + } else { + return 0; + } + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + return 0; + case OpenPgpApi.RESULT_CODE_ERROR: + logError(account, (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); + return 0; + } + return 0; + } - public void chooseKey(final Account account, final UiCallback callback) { - Intent p = new Intent(); - p.setAction(OpenPgpApi.ACTION_GET_SIGN_KEY_ID); - api.executeApiAsync(p, null, null, new IOpenPgpCallback() { + public void chooseKey(final Account account, final UiCallback 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: - logError(account, (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); - callback.error(R.string.openpgp_error, account); - } - } - }); - } + @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: + logError(account, (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); + callback.error(R.string.openpgp_error, account); + } + } + }); + } - public void generateSignature(final Account account, String status, - final UiCallback callback) { - if (account.getPgpId() == 0) { - return; - } - Intent params = new Intent(); - params.setAction(OpenPgpApi.ACTION_CLEARTEXT_SIGN); - params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - params.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, account.getPgpId()); - InputStream is = new ByteArrayInputStream(status.getBytes()); - final OutputStream os = new ByteArrayOutputStream(); - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": signing status message \""+status+"\""); - api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + public void generateSignature(final Account account, String status, + final UiCallback callback) { + if (account.getPgpId() == 0) { + return; + } + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_CLEARTEXT_SIGN); + params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + params.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, account.getPgpId()); + InputStream is = new ByteArrayInputStream(status.getBytes()); + final OutputStream os = new ByteArrayOutputStream(); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": signing status message \"" + status + "\""); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { - @Override - public void onReturn(Intent result) { - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - StringBuilder signatureBuilder = new StringBuilder(); - try { - os.flush(); - String[] lines = os.toString().split("\n"); - boolean sig = false; - for (String line : lines) { - if (sig) { - if (line.contains("END PGP SIGNATURE")) { - sig = false; - } else { - if (!line.contains("Version")) { - signatureBuilder.append(line.trim()); - } - } - } - if (line.contains("BEGIN PGP SIGNATURE")) { - sig = true; - } - } - } catch (IOException e) { - callback.error(R.string.openpgp_error, account); - return; - } - account.setPgpSignature(signatureBuilder.toString()); - 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: - OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); - if (error != null && "signing subkey not found!".equals(error.getMessage())) { - callback.error(0,account); - } else { - logError(account, error); - callback.error(R.string.unable_to_connect_to_keychain, null); - } + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + StringBuilder signatureBuilder = new StringBuilder(); + try { + os.flush(); + String[] lines = os.toString().split("\n"); + boolean sig = false; + for (String line : lines) { + if (sig) { + if (line.contains("END PGP SIGNATURE")) { + sig = false; + } else { + if (!line.contains("Version")) { + signatureBuilder.append(line.trim()); + } + } + } + if (line.contains("BEGIN PGP SIGNATURE")) { + sig = true; + } + } + } catch (IOException e) { + callback.error(R.string.openpgp_error, account); + return; + } + account.setPgpSignature(signatureBuilder.toString()); + 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: + OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); + if (error != null && "signing subkey not found!".equals(error.getMessage())) { + callback.error(0, account); + } else { + logError(account, error); + callback.error(R.string.unable_to_connect_to_keychain, null); + } } - } - }); - } + } + }); + } - public void hasKey(final Contact contact, final UiCallback callback) { - Intent params = new Intent(); - params.setAction(OpenPgpApi.ACTION_GET_KEY); - params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); - api.executeApiAsync(params, null, null, new IOpenPgpCallback() { + public void hasKey(final Contact contact, final UiCallback callback) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); + api.executeApiAsync(params, null, null, new IOpenPgpCallback() { - @Override - public void onReturn(Intent result) { - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - callback.success(contact); - return; - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT), - contact); - return; - case OpenPgpApi.RESULT_CODE_ERROR: - logError(contact.getAccount(), (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); - callback.error(R.string.openpgp_error, contact); + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + callback.success(contact); + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + contact); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + logError(contact.getAccount(), (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); + callback.error(R.string.openpgp_error, contact); } - } - }); - } + } + }); + } - private static void logError(Account account, OpenPgpError error) { - if (error != null) { - Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": OpenKeychain error '"+error.getMessage()+"' code="+error.getErrorId()); - } else { - Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": OpenKeychain error with no message"); - } - } + private static void logError(Account account, OpenPgpError error) { + if (error != null) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": OpenKeychain error '" + error.getMessage() + "' code=" + error.getErrorId()); + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": OpenKeychain error with no message"); + } + } - public PendingIntent getIntentForKey(Contact contact) { - return getIntentForKey(contact.getPgpKeyId()); - } + public PendingIntent getIntentForKey(Contact contact) { + return getIntentForKey(contact.getPgpKeyId()); + } - public PendingIntent getIntentForKey(long pgpKeyId) { - Intent params = new Intent(); - params.setAction(OpenPgpApi.ACTION_GET_KEY); - params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId); - Intent result = api.executeApi(params, null, null); - return (PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); - } + public PendingIntent getIntentForKey(long pgpKeyId) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId); + Intent result = api.executeApi(params, null, null); + return (PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); + } } diff --git a/src/main/java/de/pixart/messenger/crypto/XmppDomainVerifier.java b/src/main/java/de/pixart/messenger/crypto/XmppDomainVerifier.java index 250516daa..d305a33d5 100644 --- a/src/main/java/de/pixart/messenger/crypto/XmppDomainVerifier.java +++ b/src/main/java/de/pixart/messenger/crypto/XmppDomainVerifier.java @@ -26,102 +26,102 @@ import javax.net.ssl.SSLSession; public class XmppDomainVerifier implements HostnameVerifier { - private static final String LOGTAG = "XmppDomainVerifier"; + 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"; + 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> alternativeNames = certificate.getSubjectAlternativeNames(); - List xmppAddrs = new ArrayList<>(); - List srvNames = new ArrayList<>(); - List domains = new ArrayList<>(); - if (alternativeNames != null) { - for (List san : alternativeNames) { - Integer type = (Integer) san.get(0); - if (type == 0) { - Pair 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; - } - } + @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> alternativeNames = certificate.getSubjectAlternativeNames(); + List xmppAddrs = new ArrayList<>(); + List srvNames = new ArrayList<>(); + List domains = new ArrayList<>(); + if (alternativeNames != null) { + for (List san : alternativeNames) { + Integer type = (Integer) san.get(0); + if (type == 0) { + Pair 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 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 Pair 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 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; - } + private static boolean matchDomain(String needle, List 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/pixart/messenger/crypto/axolotl/AxolotlService.java b/src/main/java/de/pixart/messenger/crypto/axolotl/AxolotlService.java index 532fbe470..1ed410fdb 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/AxolotlService.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/AxolotlService.java @@ -51,1049 +51,1049 @@ import de.pixart.messenger.xmpp.stanzas.IqPacket; public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { - public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; - public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; - public static final String PEP_DEVICE_LIST_NOTIFY = PEP_DEVICE_LIST + "+notify"; - public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles"; - public static final String PEP_VERIFICATION = PEP_PREFIX + ".verification"; - - public static final String LOGPREFIX = "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> deviceIds; - private final Map messageCache; - private final FetchStatusMap fetchStatusMap; - private final SerialSingleThreadExecutor executor; - private int numPublishTriesOnEmptyPep = 0; - private boolean pepBroken = false; - - @Override - public void onAdvancedStreamFeaturesAvailable(Account account) { - if (Config.supportOmemo() - && account.getXmppConnection() != null - && account.getXmppConnection().getFeatures().pep()) { - publishBundlesIfNeeded(true, false); - } else { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping OMEMO initialization"); - } - } - - public boolean fetchMapHasErrors(List jids) { - for(Jid jid : jids) { - 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; - } - - public void preVerifyFingerprint(Contact contact, String fingerprint) { - axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().toBareJid().toPreppedString(), fingerprint); - } - - public void preVerifyFingerprint(Account account, String fingerprint) { - axolotlStore.preVerifyFingerprint(account, account.getJid().toBareJid().toPreppedString(), fingerprint); - } - - private static class AxolotlAddressMap { - protected Map> 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 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 devices = map.get(address.getName()); - if (devices == null) { - return null; - } - return devices.get(address.getDeviceId()); - } - } - - public Map getAll(AxolotlAddress address) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - if (devices == null) { - return new HashMap<>(); - } - return devices; - } - } - - public boolean hasAny(AxolotlAddress address) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - return devices != null && !devices.isEmpty(); - } - } - - public void clear() { - map.clear(); - } - - } - - private static class SessionMap extends AxolotlAddressMap { - 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 deviceIds, SQLiteAxolotlStore store) { - for (Integer deviceId : deviceIds) { - AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString()); - 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 deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toPreppedString()); - putDevicesForJid(account.getJid().toBareJid().toPreppedString(), 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); //TODO why? - } - - public void put(XmppAxolotlSession session) { - this.put(session.getRemoteAddress(), session); - } - } - - public enum FetchStatus { - PENDING, - SUCCESS, - SUCCESS_VERIFIED, - TIMEOUT, - ERROR - } - - private static class FetchStatusMap extends AxolotlAddressMap { - - public void clearErrorFor(Jid jid) { - synchronized (MAP_LOCK) { - Map devices = this.map.get(jid.toBareJid().toPreppedString()); - if (devices == null) { - return; - } - for(Map.Entry entry : devices.entrySet()) { - if (entry.getValue() == FetchStatus.ERROR) { - Log.d(Config.LOGTAG,"resetting error for "+jid.toBareJid()+"("+entry.getKey()+")"); - entry.setValue(FetchStatus.TIMEOUT); - } - } - } - } - } - - public static String getLogprefix(Account account) { - return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): "; - } - - public AxolotlService(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(); - } - - public String getOwnFingerprint() { - return axolotlStore.getIdentityKeyPair().getPublicKey().getFingerprint().replaceAll("\\s", ""); - } - - public Set getKeysWithTrust(FingerprintStatus status) { - return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toPreppedString(), status); - } - - public Set getKeysWithTrust(FingerprintStatus status, Jid jid) { - return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toPreppedString(), status); - } - - public Set getKeysWithTrust(FingerprintStatus status, List jids) { - Set keys = new HashSet<>(); - for(Jid jid : jids) { - keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toPreppedString(), status)); - } - return keys; - } - - public long getNumTrustedKeys(Jid jid) { - return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString()); - } - - public boolean anyTargetHasNoTrustedKeys(List jids) { - for(Jid jid : jids) { - if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString()) == 0) { - return true; - } - } - return false; - } - - private AxolotlAddress getAddressForJid(Jid jid) { - return new AxolotlAddress(jid.toPreppedString(), 0); - } - - public Set findOwnSessions() { - AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid()); - return new HashSet<>(this.sessions.getAll(ownAddress).values()); - } - - private Set findSessionsForContact(Contact contact) { - AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); - return new HashSet<>(this.sessions.getAll(contactAddress).values()); - } - - private Set findSessionsForConversation(Conversation conversation) { - HashSet sessions = new HashSet<>(); - for(Jid jid : conversation.getAcceptedCryptoTargets()) { - sessions.addAll(this.sessions.getAll(getAddressForJid(jid)).values()); - } - return sessions; - } - - public Set getFingerprintsForOwnSessions() { - Set fingerprints = new HashSet<>(); - for (XmppAxolotlSession session : findOwnSessions()) { - fingerprints.add(session.getFingerprint()); - } - return fingerprints; - } - - public Set getFingerprintsForContact(final Contact contact) { - Set fingerprints = new HashSet<>(); - for (XmppAxolotlSession session : findSessionsForContact(contact)) { - fingerprints.add(session.getFingerprint()); - } - return fingerprints; - } - - private boolean hasAny(Jid jid) { - return sessions.hasAny(getAddressForJid(jid)); - } - - public boolean isPepBroken() { - return this.pepBroken; - } - - public void resetBrokenness() { - this.pepBroken = false; - numPublishTriesOnEmptyPep = 0; - } - - public void clearErrorsInFetchStatusMap(Jid jid) { - fetchStatusMap.clearErrorFor(jid); - } - - public void regenerateKeys(boolean wipeOther) { - axolotlStore.regenerate(); - sessions.clear(); - fetchStatusMap.clear(); - publishBundlesIfNeeded(true, wipeOther); - } - - public int getOwnDeviceId() { - return axolotlStore.getLocalRegistrationId(); - } - - public Set getOwnDeviceIds() { - return this.deviceIds.get(account.getJid().toBareJid()); - } - - public void registerDevices(final Jid jid, @NonNull final Set 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 attempts 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().toPreppedString(), deviceId); - if (sessions.get(ownDeviceAddress) == null) { - buildSessionFromPEP(ownDeviceAddress); - } - } - } - Set expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toPreppedString())); - expiredDevices.removeAll(deviceIds); - for (Integer deviceId : expiredDevices) { - AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId); - XmppAxolotlSession session = sessions.get(address); - if (session != null && session.getFingerprint() != null) { - if (session.getTrust().isActive()) { - session.setTrust(session.getTrust().toInactive()); - } - } - } - Set newDevices = new HashSet<>(deviceIds); - for (Integer deviceId : newDevices) { - AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId); - XmppAxolotlSession session = sessions.get(address); - if (session != null && session.getFingerprint() != null) { - if (!session.getTrust().isActive()) { - session.setTrust(session.getTrust().toActive()); - } - } - } - this.deviceIds.put(jid, deviceIds); - mXmppConnectionService.keyStatusUpdated(null); - } - - public void wipeOtherPepDevices() { - if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... "); - return; - } - Set deviceIds = new HashSet<>(); - deviceIds.add(getOwnDeviceId()); - IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - // TODO: implement this! - } - }); - } - - public void purgeKey(final String fingerprint) { - axolotlStore.setFingerprintStatus(fingerprint.replaceAll("\\s", ""), FingerprintStatus.createCompromised()); - } - - 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 deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - if (!deviceIds.contains(getOwnDeviceId())) { - publishOwnDeviceId(deviceIds); - } - } - } - }); - } - - public void publishOwnDeviceId(Set deviceIds) { - Set deviceIdsCopy = new HashSet<>(deviceIds); - if (!deviceIdsCopy.contains(getOwnDeviceId())) { - Log.d(Config.LOGTAG, AxolotlService.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.ERROR) { - pepBroken = true; - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error")); - } - } - }); - } - } - - public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord, - final Set 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, AxolotlService.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(); - } - } - - 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, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet); - return; - } - } - - PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); - Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); - boolean flush = false; - if (bundle == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet); - bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); - flush = true; - } - if (keys == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet); - } - try { - boolean changed = false; - // Validate IdentityKey - IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); - if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); - 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, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); - signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - changed = true; - } - } catch (InvalidKeyIdException e) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); - signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - changed = true; - } - - // Validate PreKeys - Set 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 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, AxolotlService.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, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); - } - } - }); - } - - private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord, - Set preKeyRecords, - final boolean announceAfter, - final boolean wipe) { - IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( - signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), - preKeyRecords, getOwnDeviceId()); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. "); - if (wipe) { - wipeOtherPepDevices(); - } else if (announceAfter) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); - publishOwnDeviceIdIfNeeded(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - pepBroken = true; - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error")); - } - } - }); - } - - public enum AxolotlCapability { - FULL, - MISSING_PRESENCE, - MISSING_KEYS, - WRONG_CONFIGURATION, - NO_MEMBERS - } - - public boolean isConversationAxolotlCapable(Conversation conversation) { - return isConversationAxolotlCapableDetailed(conversation).first == AxolotlCapability.FULL; - } - - public Pair isConversationAxolotlCapableDetailed(Conversation conversation) { - if (conversation.getMode() == Conversation.MODE_SINGLE || (conversation.getMucOptions().membersOnly() && conversation.getMucOptions().nonanonymous())) { - final List jids = getCryptoTargets(conversation); - for(Jid jid : jids) { - if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) { - if (conversation.getAccount().getRoster().getContact(jid).mutualPresenceSubscription()) { - return new Pair<>(AxolotlCapability.MISSING_KEYS,jid); - } else { - return new Pair<>(AxolotlCapability.MISSING_PRESENCE,jid); - } - } - } - if (jids.size() > 0) { - return new Pair<>(AxolotlCapability.FULL, null); - } else { - return new Pair<>(AxolotlCapability.NO_MEMBERS, null); - } - } else { - return new Pair<>(AxolotlCapability.WRONG_CONFIGURATION, null); - } - } - - public List getCryptoTargets(Conversation conversation) { - final List jids; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - jids = Arrays.asList(conversation.getJid().toBareJid()); - } else { - jids = conversation.getMucOptions().getMembers(); - } - return jids; - } - - public FingerprintStatus getFingerprintTrust(String fingerprint) { - return axolotlStore.getFingerprintStatus(fingerprint); - } - - public X509Certificate getFingerprintCertificate(String fingerprint) { - return axolotlStore.getFingerprintCertificate(fingerprint); - } - - public void setFingerprintTrust(String fingerprint, FingerprintStatus status) { - axolotlStore.setFingerprintStatus(fingerprint, status); - } - - 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 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, FingerprintStatus.createActiveVerified(true)); - 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().toPreppedString(), 0); - Map own = fetchStatusMap.getAll(ownAddress); - Map remote = fetchStatusMap.getAll(address); - if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) { - FetchStatus report = null; - if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) { - report = FetchStatus.SUCCESS; - } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) { - report = FetchStatus.SUCCESS_VERIFIED; - } else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) { - report = FetchStatus.ERROR; - } - mXmppConnectionService.keyStatusUpdated(report); - } - } - - private void buildSessionFromPEP(final AxolotlAddress address) { - Log.i(Config.LOGTAG, AxolotlService.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, AxolotlService.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, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final List preKeyBundleList = parser.preKeys(packet); - final PreKeyBundle bundle = parser.bundle(packet); - if (preKeyBundleList.isEmpty() || bundle == null) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet); - 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 { - FingerprintStatus status = getFingerprintTrust(bundle.getIdentityKey().getFingerprint().replaceAll("\\s","")); - boolean verified = status != null && status.isVerified(); - fetchStatusMap.put(address, verified ? FetchStatus.SUCCESS_VERIFIED : FetchStatus.SUCCESS); - finishBuildingSessionsFromPEP(address); - } - } catch (UntrustedIdentityException | InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": " - + e.getClass().getName() + ", " + e.getMessage()); - fetchStatusMap.put(address, FetchStatus.ERROR); - 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, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName()); - } - } - - public Set findDevicesWithoutSession(final Conversation conversation) { - Set addresses = new HashSet<>(); - for(Jid jid : getCryptoTargets(conversation)) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid); - if (deviceIds.get(jid) != null) { - for (Integer foreignId : this.deviceIds.get(jid)) { - AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId); - if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - if (identityKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); - sessions.put(address, session); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + 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, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); - } - } - if (deviceIds.get(account.getJid().toBareJid()) != null) { - for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) { - AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), ownId); - if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - if (identityKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); - sessions.put(address, session); - } else { - Log.d(Config.LOGTAG, AxolotlService.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; - } - - public boolean createSessionsIfNeeded(final Conversation conversation) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); - boolean newSessions = false; - Set addresses = findDevicesWithoutSession(conversation); - for (AxolotlAddress address : addresses) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString()); - FetchStatus status = fetchStatusMap.get(address); - if (status == null || status == FetchStatus.TIMEOUT) { - fetchStatusMap.put(address, FetchStatus.PENDING); - this.buildSessionFromPEP(address); - newSessions = true; - } else if (status == FetchStatus.PENDING) { - newSessions = true; - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString()); - } - } - - return newSessions; - } - - public boolean trustedSessionVerified(final Conversation conversation) { - Set sessions = findSessionsForConversation(conversation); - sessions.addAll(findOwnSessions()); - boolean verified = false; - for(XmppAxolotlSession session : sessions) { - if (session.getTrust().isTrustedAndActive()) { - if (session.getTrust().getTrust() == FingerprintStatus.Trust.VERIFIED_X509) { - verified = true; - } else { - return false; - } - } - } - return verified; - } - - public boolean hasPendingKeyFetches(Account account, List jids) { - AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), 0); - if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)) { - return true; - } - for(Jid jid : jids) { - AxolotlAddress foreignAddress = new AxolotlAddress(jid.toBareJid().toPreppedString(), 0); - if (fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) { - return true; - } - } - return false; - } - - @Nullable - private XmppAxolotlMessage buildHeader(Conversation conversation) { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage( - account.getJid().toBareJid(), getOwnDeviceId()); - - Set remoteSessions = findSessionsForConversation(conversation); - Set ownSessions = findOwnSessions(); - if (remoteSessions.isEmpty()) { - return null; - } - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements..."); - for (XmppAxolotlSession session : remoteSessions) { - Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString()); - axolotlMessage.addDevice(session); - } - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements..."); - for (XmppAxolotlSession session : ownSessions) { - Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString()); - axolotlMessage.addDevice(session); - } - - return axolotlMessage; - } - - @Nullable - public XmppAxolotlMessage encrypt(Message message) { - XmppAxolotlMessage axolotlMessage = buildHeader(message.getConversation()); - - 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; - } - - 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, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); - messageCache.put(message.getUuid(), axolotlMessage); - mXmppConnectionService.resendMessage(message, delay); - } - } - }); - } - - public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) { - executor.execute(new Runnable() { - @Override - public void run() { - XmppAxolotlMessage axolotlMessage = buildHeader(conversation); - onMessageCreatedCallback.run(axolotlMessage); - } - }); - } - - public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { - XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid()); - if (axolotlMessage != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid()); - messageCache.remove(message.getUuid()); - } else { - Log.d(Config.LOGTAG, AxolotlService.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, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message); - session = recreateUncachedSession(senderAddress); - if (session == null) { - session = new XmppAxolotlSession(account, axolotlStore, senderAddress); - } - } - return session; - } - - 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; - } - - 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"); - } - } - } + public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; + public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; + public static final String PEP_DEVICE_LIST_NOTIFY = PEP_DEVICE_LIST + "+notify"; + public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles"; + public static final String PEP_VERIFICATION = PEP_PREFIX + ".verification"; + + public static final String LOGPREFIX = "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> deviceIds; + private final Map messageCache; + private final FetchStatusMap fetchStatusMap; + private final SerialSingleThreadExecutor executor; + private int numPublishTriesOnEmptyPep = 0; + private boolean pepBroken = false; + + @Override + public void onAdvancedStreamFeaturesAvailable(Account account) { + if (Config.supportOmemo() + && account.getXmppConnection() != null + && account.getXmppConnection().getFeatures().pep()) { + publishBundlesIfNeeded(true, false); + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": skipping OMEMO initialization"); + } + } + + public boolean fetchMapHasErrors(List jids) { + for (Jid jid : jids) { + 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; + } + + public void preVerifyFingerprint(Contact contact, String fingerprint) { + axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().toBareJid().toPreppedString(), fingerprint); + } + + public void preVerifyFingerprint(Account account, String fingerprint) { + axolotlStore.preVerifyFingerprint(account, account.getJid().toBareJid().toPreppedString(), fingerprint); + } + + private static class AxolotlAddressMap { + protected Map> 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 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 devices = map.get(address.getName()); + if (devices == null) { + return null; + } + return devices.get(address.getDeviceId()); + } + } + + public Map getAll(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if (devices == null) { + return new HashMap<>(); + } + return devices; + } + } + + public boolean hasAny(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + return devices != null && !devices.isEmpty(); + } + } + + public void clear() { + map.clear(); + } + + } + + private static class SessionMap extends AxolotlAddressMap { + 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 deviceIds, SQLiteAxolotlStore store) { + for (Integer deviceId : deviceIds) { + AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString()); + 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 deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toPreppedString()); + putDevicesForJid(account.getJid().toBareJid().toPreppedString(), 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); //TODO why? + } + + public void put(XmppAxolotlSession session) { + this.put(session.getRemoteAddress(), session); + } + } + + public enum FetchStatus { + PENDING, + SUCCESS, + SUCCESS_VERIFIED, + TIMEOUT, + ERROR + } + + private static class FetchStatusMap extends AxolotlAddressMap { + + public void clearErrorFor(Jid jid) { + synchronized (MAP_LOCK) { + Map devices = this.map.get(jid.toBareJid().toPreppedString()); + if (devices == null) { + return; + } + for (Map.Entry entry : devices.entrySet()) { + if (entry.getValue() == FetchStatus.ERROR) { + Log.d(Config.LOGTAG, "resetting error for " + jid.toBareJid() + "(" + entry.getKey() + ")"); + entry.setValue(FetchStatus.TIMEOUT); + } + } + } + } + } + + public static String getLogprefix(Account account) { + return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): "; + } + + public AxolotlService(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(); + } + + public String getOwnFingerprint() { + return axolotlStore.getIdentityKeyPair().getPublicKey().getFingerprint().replaceAll("\\s", ""); + } + + public Set getKeysWithTrust(FingerprintStatus status) { + return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toPreppedString(), status); + } + + public Set getKeysWithTrust(FingerprintStatus status, Jid jid) { + return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toPreppedString(), status); + } + + public Set getKeysWithTrust(FingerprintStatus status, List jids) { + Set keys = new HashSet<>(); + for (Jid jid : jids) { + keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toPreppedString(), status)); + } + return keys; + } + + public long getNumTrustedKeys(Jid jid) { + return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString()); + } + + public boolean anyTargetHasNoTrustedKeys(List jids) { + for (Jid jid : jids) { + if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString()) == 0) { + return true; + } + } + return false; + } + + private AxolotlAddress getAddressForJid(Jid jid) { + return new AxolotlAddress(jid.toPreppedString(), 0); + } + + public Set findOwnSessions() { + AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid()); + return new HashSet<>(this.sessions.getAll(ownAddress).values()); + } + + private Set findSessionsForContact(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + return new HashSet<>(this.sessions.getAll(contactAddress).values()); + } + + private Set findSessionsForConversation(Conversation conversation) { + HashSet sessions = new HashSet<>(); + for (Jid jid : conversation.getAcceptedCryptoTargets()) { + sessions.addAll(this.sessions.getAll(getAddressForJid(jid)).values()); + } + return sessions; + } + + public Set getFingerprintsForOwnSessions() { + Set fingerprints = new HashSet<>(); + for (XmppAxolotlSession session : findOwnSessions()) { + fingerprints.add(session.getFingerprint()); + } + return fingerprints; + } + + public Set getFingerprintsForContact(final Contact contact) { + Set fingerprints = new HashSet<>(); + for (XmppAxolotlSession session : findSessionsForContact(contact)) { + fingerprints.add(session.getFingerprint()); + } + return fingerprints; + } + + private boolean hasAny(Jid jid) { + return sessions.hasAny(getAddressForJid(jid)); + } + + public boolean isPepBroken() { + return this.pepBroken; + } + + public void resetBrokenness() { + this.pepBroken = false; + numPublishTriesOnEmptyPep = 0; + } + + public void clearErrorsInFetchStatusMap(Jid jid) { + fetchStatusMap.clearErrorFor(jid); + } + + public void regenerateKeys(boolean wipeOther) { + axolotlStore.regenerate(); + sessions.clear(); + fetchStatusMap.clear(); + publishBundlesIfNeeded(true, wipeOther); + } + + public int getOwnDeviceId() { + return axolotlStore.getLocalRegistrationId(); + } + + public Set getOwnDeviceIds() { + return this.deviceIds.get(account.getJid().toBareJid()); + } + + public void registerDevices(final Jid jid, @NonNull final Set 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 attempts 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().toPreppedString(), deviceId); + if (sessions.get(ownDeviceAddress) == null) { + buildSessionFromPEP(ownDeviceAddress); + } + } + } + Set expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toPreppedString())); + expiredDevices.removeAll(deviceIds); + for (Integer deviceId : expiredDevices) { + AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId); + XmppAxolotlSession session = sessions.get(address); + if (session != null && session.getFingerprint() != null) { + if (session.getTrust().isActive()) { + session.setTrust(session.getTrust().toInactive()); + } + } + } + Set newDevices = new HashSet<>(deviceIds); + for (Integer deviceId : newDevices) { + AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId); + XmppAxolotlSession session = sessions.get(address); + if (session != null && session.getFingerprint() != null) { + if (!session.getTrust().isActive()) { + session.setTrust(session.getTrust().toActive()); + } + } + } + this.deviceIds.put(jid, deviceIds); + mXmppConnectionService.keyStatusUpdated(null); + } + + public void wipeOtherPepDevices() { + if (pepBroken) { + Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... "); + return; + } + Set deviceIds = new HashSet<>(); + deviceIds.add(getOwnDeviceId()); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + } + }); + } + + public void purgeKey(final String fingerprint) { + axolotlStore.setFingerprintStatus(fingerprint.replaceAll("\\s", ""), FingerprintStatus.createCompromised()); + } + + 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 deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + if (!deviceIds.contains(getOwnDeviceId())) { + publishOwnDeviceId(deviceIds); + } + } + } + }); + } + + public void publishOwnDeviceId(Set deviceIds) { + Set deviceIdsCopy = new HashSet<>(deviceIds); + if (!deviceIdsCopy.contains(getOwnDeviceId())) { + Log.d(Config.LOGTAG, AxolotlService.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.ERROR) { + pepBroken = true; + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error")); + } + } + }); + } + } + + public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord, + final Set 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, AxolotlService.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(); + } + } + + 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, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet); + return; + } + } + + PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); + Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); + boolean flush = false; + if (bundle == null) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet); + bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); + flush = true; + } + if (keys == null) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet); + } + try { + boolean changed = false; + // Validate IdentityKey + IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); + if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); + 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, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } + } catch (InvalidKeyIdException e) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } + + // Validate PreKeys + Set 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 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, AxolotlService.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, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + } + } + }); + } + + private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord, + Set preKeyRecords, + final boolean announceAfter, + final boolean wipe) { + IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( + signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), + preKeyRecords, getOwnDeviceId()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. "); + if (wipe) { + wipeOtherPepDevices(); + } else if (announceAfter) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); + publishOwnDeviceIdIfNeeded(); + } + } else if (packet.getType() == IqPacket.TYPE.ERROR) { + pepBroken = true; + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error")); + } + } + }); + } + + public enum AxolotlCapability { + FULL, + MISSING_PRESENCE, + MISSING_KEYS, + WRONG_CONFIGURATION, + NO_MEMBERS + } + + public boolean isConversationAxolotlCapable(Conversation conversation) { + return isConversationAxolotlCapableDetailed(conversation).first == AxolotlCapability.FULL; + } + + public Pair isConversationAxolotlCapableDetailed(Conversation conversation) { + if (conversation.getMode() == Conversation.MODE_SINGLE || (conversation.getMucOptions().membersOnly() && conversation.getMucOptions().nonanonymous())) { + final List jids = getCryptoTargets(conversation); + for (Jid jid : jids) { + if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) { + if (conversation.getAccount().getRoster().getContact(jid).mutualPresenceSubscription()) { + return new Pair<>(AxolotlCapability.MISSING_KEYS, jid); + } else { + return new Pair<>(AxolotlCapability.MISSING_PRESENCE, jid); + } + } + } + if (jids.size() > 0) { + return new Pair<>(AxolotlCapability.FULL, null); + } else { + return new Pair<>(AxolotlCapability.NO_MEMBERS, null); + } + } else { + return new Pair<>(AxolotlCapability.WRONG_CONFIGURATION, null); + } + } + + public List getCryptoTargets(Conversation conversation) { + final List jids; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + jids = Arrays.asList(conversation.getJid().toBareJid()); + } else { + jids = conversation.getMucOptions().getMembers(); + } + return jids; + } + + public FingerprintStatus getFingerprintTrust(String fingerprint) { + return axolotlStore.getFingerprintStatus(fingerprint); + } + + public X509Certificate getFingerprintCertificate(String fingerprint) { + return axolotlStore.getFingerprintCertificate(fingerprint); + } + + public void setFingerprintTrust(String fingerprint, FingerprintStatus status) { + axolotlStore.setFingerprintStatus(fingerprint, status); + } + + 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 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, FingerprintStatus.createActiveVerified(true)); + 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().toPreppedString(), 0); + Map own = fetchStatusMap.getAll(ownAddress); + Map remote = fetchStatusMap.getAll(address); + if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) { + FetchStatus report = null; + if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) { + report = FetchStatus.SUCCESS; + } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) { + report = FetchStatus.SUCCESS_VERIFIED; + } else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) { + report = FetchStatus.ERROR; + } + mXmppConnectionService.keyStatusUpdated(report); + } + } + + private void buildSessionFromPEP(final AxolotlAddress address) { + Log.i(Config.LOGTAG, AxolotlService.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, AxolotlService.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, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final List preKeyBundleList = parser.preKeys(packet); + final PreKeyBundle bundle = parser.bundle(packet); + if (preKeyBundleList.isEmpty() || bundle == null) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet); + 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 { + FingerprintStatus status = getFingerprintTrust(bundle.getIdentityKey().getFingerprint().replaceAll("\\s", "")); + boolean verified = status != null && status.isVerified(); + fetchStatusMap.put(address, verified ? FetchStatus.SUCCESS_VERIFIED : FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + } + } catch (UntrustedIdentityException | InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": " + + e.getClass().getName() + ", " + e.getMessage()); + fetchStatusMap.put(address, FetchStatus.ERROR); + 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, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName()); + } + } + + public Set findDevicesWithoutSession(final Conversation conversation) { + Set addresses = new HashSet<>(); + for (Jid jid : getCryptoTargets(conversation)) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid); + if (deviceIds.get(jid) != null) { + for (Integer foreignId : this.deviceIds.get(jid)) { + AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId); + if (sessions.get(address) == null) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if (identityKey != null) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + 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, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); + } + } + if (deviceIds.get(account.getJid().toBareJid()) != null) { + for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) { + AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), ownId); + if (sessions.get(address) == null) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if (identityKey != null) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlService.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; + } + + public boolean createSessionsIfNeeded(final Conversation conversation) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); + boolean newSessions = false; + Set addresses = findDevicesWithoutSession(conversation); + for (AxolotlAddress address : addresses) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString()); + FetchStatus status = fetchStatusMap.get(address); + if (status == null || status == FetchStatus.TIMEOUT) { + fetchStatusMap.put(address, FetchStatus.PENDING); + this.buildSessionFromPEP(address); + newSessions = true; + } else if (status == FetchStatus.PENDING) { + newSessions = true; + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString()); + } + } + + return newSessions; + } + + public boolean trustedSessionVerified(final Conversation conversation) { + Set sessions = findSessionsForConversation(conversation); + sessions.addAll(findOwnSessions()); + boolean verified = false; + for (XmppAxolotlSession session : sessions) { + if (session.getTrust().isTrustedAndActive()) { + if (session.getTrust().getTrust() == FingerprintStatus.Trust.VERIFIED_X509) { + verified = true; + } else { + return false; + } + } + } + return verified; + } + + public boolean hasPendingKeyFetches(Account account, List jids) { + AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), 0); + if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)) { + return true; + } + for (Jid jid : jids) { + AxolotlAddress foreignAddress = new AxolotlAddress(jid.toBareJid().toPreppedString(), 0); + if (fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) { + return true; + } + } + return false; + } + + @Nullable + private XmppAxolotlMessage buildHeader(Conversation conversation) { + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage( + account.getJid().toBareJid(), getOwnDeviceId()); + + Set remoteSessions = findSessionsForConversation(conversation); + Set ownSessions = findOwnSessions(); + if (remoteSessions.isEmpty()) { + return null; + } + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements..."); + for (XmppAxolotlSession session : remoteSessions) { + Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString()); + axolotlMessage.addDevice(session); + } + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements..."); + for (XmppAxolotlSession session : ownSessions) { + Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString()); + axolotlMessage.addDevice(session); + } + + return axolotlMessage; + } + + @Nullable + public XmppAxolotlMessage encrypt(Message message) { + XmppAxolotlMessage axolotlMessage = buildHeader(message.getConversation()); + + 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; + } + + 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, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); + messageCache.put(message.getUuid(), axolotlMessage); + mXmppConnectionService.resendMessage(message, delay); + } + } + }); + } + + public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) { + executor.execute(new Runnable() { + @Override + public void run() { + XmppAxolotlMessage axolotlMessage = buildHeader(conversation); + onMessageCreatedCallback.run(axolotlMessage); + } + }); + } + + public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { + XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid()); + if (axolotlMessage != null) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid()); + messageCache.remove(message.getUuid()); + } else { + Log.d(Config.LOGTAG, AxolotlService.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, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message); + session = recreateUncachedSession(senderAddress); + if (session == null) { + session = new XmppAxolotlSession(account, axolotlStore, senderAddress); + } + } + return session; + } + + 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; + } + + 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/pixart/messenger/crypto/axolotl/CryptoFailedException.java b/src/main/java/de/pixart/messenger/crypto/axolotl/CryptoFailedException.java index b29d7cb30..da847393b 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/CryptoFailedException.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/CryptoFailedException.java @@ -1,7 +1,7 @@ package de.pixart.messenger.crypto.axolotl; public class CryptoFailedException extends Exception { - public CryptoFailedException(Exception e){ - super(e); - } + public CryptoFailedException(Exception e) { + super(e); + } } diff --git a/src/main/java/de/pixart/messenger/crypto/axolotl/FingerprintStatus.java b/src/main/java/de/pixart/messenger/crypto/axolotl/FingerprintStatus.java index db67617b9..3be469b5a 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/FingerprintStatus.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/FingerprintStatus.java @@ -32,8 +32,8 @@ public class FingerprintStatus { public ContentValues toContentValues() { final ContentValues contentValues = new ContentValues(); - contentValues.put(SQLiteAxolotlStore.TRUST,trust.toString()); - contentValues.put(SQLiteAxolotlStore.ACTIVE,active ? 1 : 0); + contentValues.put(SQLiteAxolotlStore.TRUST, trust.toString()); + contentValues.put(SQLiteAxolotlStore.ACTIVE, active ? 1 : 0); return contentValues; } @@ -41,7 +41,7 @@ public class FingerprintStatus { final FingerprintStatus status = new FingerprintStatus(); try { status.trust = Trust.valueOf(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.TRUST))); - } catch(IllegalArgumentException e) { + } catch (IllegalArgumentException e) { status.trust = Trust.UNTRUSTED; } status.active = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.ACTIVE)) > 0; diff --git a/src/main/java/de/pixart/messenger/crypto/axolotl/NoSessionsCreatedException.java b/src/main/java/de/pixart/messenger/crypto/axolotl/NoSessionsCreatedException.java index 5d0a7547a..92635a364 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/NoSessionsCreatedException.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/NoSessionsCreatedException.java @@ -1,4 +1,4 @@ package de.pixart.messenger.crypto.axolotl; -public class NoSessionsCreatedException extends Throwable{ +public class NoSessionsCreatedException extends Throwable { } diff --git a/src/main/java/de/pixart/messenger/crypto/axolotl/OnMessageCreatedCallback.java b/src/main/java/de/pixart/messenger/crypto/axolotl/OnMessageCreatedCallback.java index a6daeb196..088831661 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/OnMessageCreatedCallback.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/OnMessageCreatedCallback.java @@ -1,5 +1,5 @@ package de.pixart.messenger.crypto.axolotl; public interface OnMessageCreatedCallback { - void run(XmppAxolotlMessage message); + void run(XmppAxolotlMessage message); } diff --git a/src/main/java/de/pixart/messenger/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/de/pixart/messenger/crypto/axolotl/SQLiteAxolotlStore.java index 2980bf7cc..c45e5045e 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/SQLiteAxolotlStore.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/SQLiteAxolotlStore.java @@ -25,421 +25,421 @@ import de.pixart.messenger.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"; //no longer used - public static final String TRUST = "trust"; - public static final String ACTIVE = "active"; - 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 trustCache = - new LruCache(NUM_TRUSTS_TO_CACHE) { - @Override - protected FingerprintStatus create(String fingerprint) { - return mXmppConnectionService.databaseBackend.getFingerprintStatus(account, fingerprint); - } - }; - - private static IdentityKeyPair generateIdentityKeyPair() { - Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair..."); - ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); - return new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), - identityKeyPairKeys.getPrivateKey()); - } - - private static int generateRegistrationId() { - Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID..."); - return KeyHelper.generateRegistrationId(true); - } - - public SQLiteAxolotlStore(Account account, XmppConnectionService service) { - this.account = account; - this.mXmppConnectionService = service; - this.localRegistrationId = loadRegistrationId(); - this.currentPreKeyId = loadCurrentPreKeyId(); - for (SignedPreKeyRecord record : loadSignedPreKeys()) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got Axolotl signed prekey record:" + record.getId()); - } - } - - public int getCurrentPreKeyId() { - return currentPreKeyId; - } - - // -------------------------------------- - // IdentityKeyStore - // -------------------------------------- - - private IdentityKeyPair loadIdentityKeyPair() { - synchronized (mXmppConnectionService) { - IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account); - - if (ownKey != null) { - return ownKey; - } else { - Log.i(Config.LOGTAG, AxolotlService.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, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid()); - reg_id = generateRegistrationId(); - boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); - if (success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new key to the database!"); - } - } - return reg_id; - } - - private int loadCurrentPreKeyId() { - String prekeyIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); - int prekey_id; - if (prekeyIdString != null) { - prekey_id = Integer.valueOf(prekeyIdString); - } else { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid()); - prekey_id = 0; - } - return prekey_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. - *

- * 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 - *

- * 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)) { - String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); - FingerprintStatus status = getFingerprintStatus(fingerprint); - if (status == null) { - status = FingerprintStatus.createActiveUndecided(); //default for new keys - } else { - status = status.toActive(); - } - mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey, status); - trustCache.remove(fingerprint); - } - } - - /** - * Verify a remote client's identity key. - *

- * 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 FingerprintStatus getFingerprintStatus(String fingerprint) { - return (fingerprint == null)? null : trustCache.get(fingerprint); - } - - public void setFingerprintStatus(String fingerprint, FingerprintStatus status) { - mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status); - 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 getContactKeysWithTrust(String bareJid, FingerprintStatus status) { - return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status); - } - - 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. - *

- * 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 getSubDeviceSessions(String name) { - return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, - new AxolotlAddress(name, 0)); - } - - /** - * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @param record the current SessionRecord for the remote client. - */ - @Override - public void storeSession(AxolotlAddress address, SessionRecord record) { - mXmppConnectionService.databaseBackend.storeSession(account, address, record); - } - - /** - * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @return true if a {@link SessionRecord} exists, false otherwise. - */ - @Override - public boolean containsSession(AxolotlAddress address) { - return mXmppConnectionService.databaseBackend.containsSession(account, address); - } - - /** - * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - */ - @Override - public void deleteSession(AxolotlAddress address) { - mXmppConnectionService.databaseBackend.deleteSession(account, address); - } - - /** - * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. - * - * @param name the name of the remote client. - */ - @Override - public void deleteAllSessions(String name) { - AxolotlAddress address = new AxolotlAddress(name, 0); - mXmppConnectionService.databaseBackend.deleteAllSessions(account, - address); - } - - // -------------------------------------- - // PreKeyStore - // -------------------------------------- - - /** - * Load a local PreKeyRecord. - * - * @param preKeyId the ID of the local PreKeyRecord. - * @return the corresponding PreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. - */ - @Override - public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { - PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); - if (record == null) { - throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); - } - return record; - } - - /** - * Store a local PreKeyRecord. - * - * @param preKeyId the ID of the PreKeyRecord to store. - * @param record the PreKeyRecord. - */ - @Override - public void storePreKey(int preKeyId, PreKeyRecord record) { - mXmppConnectionService.databaseBackend.storePreKey(account, record); - currentPreKeyId = preKeyId; - boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId)); - if (success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new prekey id to the database!"); - } - } - - /** - * @param preKeyId A PreKeyRecord ID. - * @return true if the store has a record for the preKeyId, otherwise false. - */ - @Override - public boolean containsPreKey(int preKeyId) { - return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); - } - - /** - * Delete a PreKeyRecord from local storage. - * - * @param preKeyId The ID of the PreKeyRecord to remove. - */ - @Override - public void removePreKey(int preKeyId) { - mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); - } - - // -------------------------------------- - // SignedPreKeyStore - // -------------------------------------- - - /** - * Load a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the local SignedPreKeyRecord. - * @return the corresponding SignedPreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. - */ - @Override - public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { - SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); - if (record == null) { - throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); - } - return record; - } - - /** - * Load all local SignedPreKeyRecords. - * - * @return All stored SignedPreKeyRecords. - */ - @Override - public List loadSignedPreKeys() { - return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); - } - - /** - * Store a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. - * @param record the SignedPreKeyRecord. - */ - @Override - public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { - mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); - } - - /** - * @param signedPreKeyId A SignedPreKeyRecord ID. - * @return true if the store has a record for the signedPreKeyId, otherwise false. - */ - @Override - public boolean containsSignedPreKey(int signedPreKeyId) { - return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); - } - - /** - * Delete a SignedPreKeyRecord from local storage. - * - * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. - */ - @Override - public void removeSignedPreKey(int signedPreKeyId) { - mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); - } - - public void preVerifyFingerprint(Account account, String name, String fingerprint) { - mXmppConnectionService.databaseBackend.storePreVerification(account,name,fingerprint,FingerprintStatus.createInactiveVerified()); - } + 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"; //no longer used + public static final String TRUST = "trust"; + public static final String ACTIVE = "active"; + 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 trustCache = + new LruCache(NUM_TRUSTS_TO_CACHE) { + @Override + protected FingerprintStatus create(String fingerprint) { + return mXmppConnectionService.databaseBackend.getFingerprintStatus(account, fingerprint); + } + }; + + private static IdentityKeyPair generateIdentityKeyPair() { + Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair..."); + ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); + return new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), + identityKeyPairKeys.getPrivateKey()); + } + + private static int generateRegistrationId() { + Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID..."); + return KeyHelper.generateRegistrationId(true); + } + + public SQLiteAxolotlStore(Account account, XmppConnectionService service) { + this.account = account; + this.mXmppConnectionService = service; + this.localRegistrationId = loadRegistrationId(); + this.currentPreKeyId = loadCurrentPreKeyId(); + for (SignedPreKeyRecord record : loadSignedPreKeys()) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got Axolotl signed prekey record:" + record.getId()); + } + } + + public int getCurrentPreKeyId() { + return currentPreKeyId; + } + + // -------------------------------------- + // IdentityKeyStore + // -------------------------------------- + + private IdentityKeyPair loadIdentityKeyPair() { + synchronized (mXmppConnectionService) { + IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account); + + if (ownKey != null) { + return ownKey; + } else { + Log.i(Config.LOGTAG, AxolotlService.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, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid()); + reg_id = generateRegistrationId(); + boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); + if (success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new key to the database!"); + } + } + return reg_id; + } + + private int loadCurrentPreKeyId() { + String prekeyIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); + int prekey_id; + if (prekeyIdString != null) { + prekey_id = Integer.valueOf(prekeyIdString); + } else { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid()); + prekey_id = 0; + } + return prekey_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. + *

+ * 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 + *

+ * 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)) { + String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); + FingerprintStatus status = getFingerprintStatus(fingerprint); + if (status == null) { + status = FingerprintStatus.createActiveUndecided(); //default for new keys + } else { + status = status.toActive(); + } + mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey, status); + trustCache.remove(fingerprint); + } + } + + /** + * Verify a remote client's identity key. + *

+ * 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 FingerprintStatus getFingerprintStatus(String fingerprint) { + return (fingerprint == null) ? null : trustCache.get(fingerprint); + } + + public void setFingerprintStatus(String fingerprint, FingerprintStatus status) { + mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status); + 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 getContactKeysWithTrust(String bareJid, FingerprintStatus status) { + return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status); + } + + 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. + *

+ * 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 getSubDeviceSessions(String name) { + return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, + new AxolotlAddress(name, 0)); + } + + /** + * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @param record the current SessionRecord for the remote client. + */ + @Override + public void storeSession(AxolotlAddress address, SessionRecord record) { + mXmppConnectionService.databaseBackend.storeSession(account, address, record); + } + + /** + * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @return true if a {@link SessionRecord} exists, false otherwise. + */ + @Override + public boolean containsSession(AxolotlAddress address) { + return mXmppConnectionService.databaseBackend.containsSession(account, address); + } + + /** + * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + */ + @Override + public void deleteSession(AxolotlAddress address) { + mXmppConnectionService.databaseBackend.deleteSession(account, address); + } + + /** + * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. + * + * @param name the name of the remote client. + */ + @Override + public void deleteAllSessions(String name) { + AxolotlAddress address = new AxolotlAddress(name, 0); + mXmppConnectionService.databaseBackend.deleteAllSessions(account, + address); + } + + // -------------------------------------- + // PreKeyStore + // -------------------------------------- + + /** + * Load a local PreKeyRecord. + * + * @param preKeyId the ID of the local PreKeyRecord. + * @return the corresponding PreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. + */ + @Override + public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { + PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); + if (record == null) { + throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); + } + return record; + } + + /** + * Store a local PreKeyRecord. + * + * @param preKeyId the ID of the PreKeyRecord to store. + * @param record the PreKeyRecord. + */ + @Override + public void storePreKey(int preKeyId, PreKeyRecord record) { + mXmppConnectionService.databaseBackend.storePreKey(account, record); + currentPreKeyId = preKeyId; + boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId)); + if (success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new prekey id to the database!"); + } + } + + /** + * @param preKeyId A PreKeyRecord ID. + * @return true if the store has a record for the preKeyId, otherwise false. + */ + @Override + public boolean containsPreKey(int preKeyId) { + return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); + } + + /** + * Delete a PreKeyRecord from local storage. + * + * @param preKeyId The ID of the PreKeyRecord to remove. + */ + @Override + public void removePreKey(int preKeyId) { + mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); + } + + // -------------------------------------- + // SignedPreKeyStore + // -------------------------------------- + + /** + * Load a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the local SignedPreKeyRecord. + * @return the corresponding SignedPreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. + */ + @Override + public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { + SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); + if (record == null) { + throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); + } + return record; + } + + /** + * Load all local SignedPreKeyRecords. + * + * @return All stored SignedPreKeyRecords. + */ + @Override + public List loadSignedPreKeys() { + return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); + } + + /** + * Store a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. + * @param record the SignedPreKeyRecord. + */ + @Override + public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { + mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); + } + + /** + * @param signedPreKeyId A SignedPreKeyRecord ID. + * @return true if the store has a record for the signedPreKeyId, otherwise false. + */ + @Override + public boolean containsSignedPreKey(int signedPreKeyId) { + return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); + } + + /** + * Delete a SignedPreKeyRecord from local storage. + * + * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. + */ + @Override + public void removeSignedPreKey(int signedPreKeyId) { + mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); + } + + public void preVerifyFingerprint(Account account, String name, String fingerprint) { + mXmppConnectionService.databaseBackend.storePreVerification(account, name, fingerprint, FingerprintStatus.createInactiveVerified()); + } } diff --git a/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlMessage.java index 431f81d9b..f49eefb5a 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlMessage.java @@ -26,227 +26,227 @@ import de.pixart.messenger.xml.Element; import de.pixart.messenger.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 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); - try { - this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("invalid source id"); - } - List 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().trim(), Base64.DEFAULT); - this.keys.put(recipientId, key); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("invalid remote id"); - } - break; - case IVTAG: - if (this.iv != null) { - throw new IllegalArgumentException("Duplicate iv entry"); - } - iv = Base64.decode(keyElement.getContent().trim(), 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().trim(), 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.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 keyEntry : keys.entrySet()) { - Element keyElement = new Element(KEYTAG); - keyElement.setAttribute(REMOTEID, keyEntry.getKey()); - keyElement.setContent(Base64.encodeToString(keyEntry.getValue(), Base64.NO_WRAP)); - headerElement.addChild(keyElement); - } - headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.NO_WRAP)); - if (ciphertext != null) { - Element payload = encryptionElement.addChild(PAYLOAD); - payload.setContent(Base64.encodeToString(ciphertext, Base64.NO_WRAP)); - } - 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; - } + 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 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); + try { + this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid source id"); + } + List 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().trim(), Base64.DEFAULT); + this.keys.put(recipientId, key); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid remote id"); + } + break; + case IVTAG: + if (this.iv != null) { + throw new IllegalArgumentException("Duplicate iv entry"); + } + iv = Base64.decode(keyElement.getContent().trim(), 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().trim(), 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.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 keyEntry : keys.entrySet()) { + Element keyElement = new Element(KEYTAG); + keyElement.setAttribute(REMOTEID, keyEntry.getKey()); + keyElement.setContent(Base64.encodeToString(keyEntry.getValue(), Base64.NO_WRAP)); + headerElement.addChild(keyElement); + } + headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.NO_WRAP)); + if (ciphertext != null) { + Element payload = encryptionElement.addChild(PAYLOAD); + payload.setContent(Base64.encodeToString(ciphertext, Base64.NO_WRAP)); + } + 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/pixart/messenger/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlSession.java index 566afef9a..27647fb66 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlSession.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlSession.java @@ -23,113 +23,113 @@ import de.pixart.messenger.Config; import de.pixart.messenger.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 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(FingerprintStatus status) { - sqLiteAxolotlStore.setFingerprintStatus(getFingerprint(), status); - } - - public FingerprintStatus getTrust() { - FingerprintStatus status = sqLiteAxolotlStore.getFingerprintStatus(getFingerprint()); - return (status == null) ? FingerprintStatus.createActiveUndecided() : status; - } - - @Nullable - public byte[] processReceiving(byte[] encryptedKey) { - byte[] plaintext = null; - FingerprintStatus status = getTrust(); - if (!status.isCompromised()) { - try { - try { - PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey); - if (!message.getPreKeyId().isPresent()) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage did not contain a PreKeyId"); - return null; - } - Log.i(Config.LOGTAG, AxolotlService.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, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint()); - } else { - this.identityKey = msgIdentityKey; - plaintext = cipher.decrypt(message); - preKeyId = message.getPreKeyId().get(); - } - } catch (InvalidMessageException | InvalidVersionException e) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received"); - WhisperMessage message = new WhisperMessage(encryptedKey); - plaintext = cipher.decrypt(message); - } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); - } - } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); - } - - if (plaintext != null) { - if (!status.isActive()) { - setTrust(status.toActive()); - } - } - } - return plaintext; - } - - @Nullable - public byte[] processSending(@NonNull byte[] outgoingMessage) { - FingerprintStatus status = getTrust(); - if (status.isTrustedAndActive()) { - CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); - return ciphertextMessage.serialize(); - } else { - return null; - } - } + 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 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(FingerprintStatus status) { + sqLiteAxolotlStore.setFingerprintStatus(getFingerprint(), status); + } + + public FingerprintStatus getTrust() { + FingerprintStatus status = sqLiteAxolotlStore.getFingerprintStatus(getFingerprint()); + return (status == null) ? FingerprintStatus.createActiveUndecided() : status; + } + + @Nullable + public byte[] processReceiving(byte[] encryptedKey) { + byte[] plaintext = null; + FingerprintStatus status = getTrust(); + if (!status.isCompromised()) { + try { + try { + PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey); + if (!message.getPreKeyId().isPresent()) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage did not contain a PreKeyId"); + return null; + } + Log.i(Config.LOGTAG, AxolotlService.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, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint()); + } else { + this.identityKey = msgIdentityKey; + plaintext = cipher.decrypt(message); + preKeyId = message.getPreKeyId().get(); + } + } catch (InvalidMessageException | InvalidVersionException e) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received"); + WhisperMessage message = new WhisperMessage(encryptedKey); + plaintext = cipher.decrypt(message); + } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); + } + } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); + } + + if (plaintext != null) { + if (!status.isActive()) { + setTrust(status.toActive()); + } + } + } + return plaintext; + } + + @Nullable + public byte[] processSending(@NonNull byte[] outgoingMessage) { + FingerprintStatus status = getTrust(); + if (status.isTrustedAndActive()) { + CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); + return ciphertextMessage.serialize(); + } else { + return null; + } + } } diff --git a/src/main/java/de/pixart/messenger/crypto/sasl/Anonymous.java b/src/main/java/de/pixart/messenger/crypto/sasl/Anonymous.java index 7e4e98bc6..93c3de64b 100644 --- a/src/main/java/de/pixart/messenger/crypto/sasl/Anonymous.java +++ b/src/main/java/de/pixart/messenger/crypto/sasl/Anonymous.java @@ -7,22 +7,22 @@ import de.pixart.messenger.xml.TagWriter; public class Anonymous extends SaslMechanism { - public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) { - super(tagWriter, account, rng); - } + public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) { + super(tagWriter, account, rng); + } - @Override - public int getPriority() { - return 0; - } + @Override + public int getPriority() { + return 0; + } - @Override - public String getMechanism() { - return "ANONYMOUS"; - } + @Override + public String getMechanism() { + return "ANONYMOUS"; + } - @Override - public String getClientFirstMessage() { - return ""; - } + @Override + public String getClientFirstMessage() { + return ""; + } } diff --git a/src/main/java/de/pixart/messenger/crypto/sasl/DigestMd5.java b/src/main/java/de/pixart/messenger/crypto/sasl/DigestMd5.java index 09ac4865a..e974d8cd4 100644 --- a/src/main/java/de/pixart/messenger/crypto/sasl/DigestMd5.java +++ b/src/main/java/de/pixart/messenger/crypto/sasl/DigestMd5.java @@ -13,79 +13,79 @@ import de.pixart.messenger.utils.CryptoHelper; import de.pixart.messenger.xml.TagWriter; public class DigestMd5 extends SaslMechanism { - public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - } + public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + super(tagWriter, account, rng); + } - @Override - public int getPriority() { - return 10; - } + @Override + public int getPriority() { + return 10; + } - @Override - public String getMechanism() { - return "DIGEST-MD5"; - } + @Override + public String getMechanism() { + return "DIGEST-MD5"; + } - private State state = State.INITIAL; + private State state = State.INITIAL; - @Override - public String getResponse(final String challenge) throws AuthenticationException { - switch (state) { - case INITIAL: - state = State.RESPONSE_SENT; - final String encodedResponse; - try { - final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); - String nonce = ""; - for (final String token : tokenizer) { - final String[] parts = token.split("=", 2); - if (parts[0].equals("nonce")) { - nonce = parts[1].replace("\"", ""); - } else if (parts[0].equals("rspauth")) { - return ""; - } - } - final String digestUri = "xmpp/" + account.getServer(); - final String nonceCount = "00000001"; - final String x = account.getUsername() + ":" + account.getServer() + ":" - + account.getPassword(); - final MessageDigest md = MessageDigest.getInstance("MD5"); - final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); - final String cNonce = new BigInteger(100, rng).toString(32); - final byte[] a1 = CryptoHelper.concatenateByteArrays(y, - (":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset())); - final String a2 = "AUTHENTICATE:" + digestUri; - final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); - final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset - .defaultCharset()))); - final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce - + ":auth:" + ha2; - final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset - .defaultCharset()))); - final String saslString = "username=\"" + account.getUsername() - + "\",realm=\"" + account.getServer() + "\",nonce=\"" - + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount - + ",qop=auth,digest-uri=\"" + digestUri + "\",response=" - + response + ",charset=utf-8"; - encodedResponse = Base64.encodeToString( - saslString.getBytes(Charset.defaultCharset()), - Base64.NO_WRAP); - } catch (final NoSuchAlgorithmException e) { - throw new AuthenticationException(e); - } + @Override + public String getResponse(final String challenge) throws AuthenticationException { + switch (state) { + case INITIAL: + state = State.RESPONSE_SENT; + final String encodedResponse; + try { + final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); + String nonce = ""; + for (final String token : tokenizer) { + final String[] parts = token.split("=", 2); + if (parts[0].equals("nonce")) { + nonce = parts[1].replace("\"", ""); + } else if (parts[0].equals("rspauth")) { + return ""; + } + } + final String digestUri = "xmpp/" + account.getServer(); + final String nonceCount = "00000001"; + final String x = account.getUsername() + ":" + account.getServer() + ":" + + account.getPassword(); + final MessageDigest md = MessageDigest.getInstance("MD5"); + final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); + final String cNonce = new BigInteger(100, rng).toString(32); + final byte[] a1 = CryptoHelper.concatenateByteArrays(y, + (":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset())); + final String a2 = "AUTHENTICATE:" + digestUri; + final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); + final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset + .defaultCharset()))); + final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + + ":auth:" + ha2; + final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset + .defaultCharset()))); + final String saslString = "username=\"" + account.getUsername() + + "\",realm=\"" + account.getServer() + "\",nonce=\"" + + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount + + ",qop=auth,digest-uri=\"" + digestUri + "\",response=" + + response + ",charset=utf-8"; + encodedResponse = Base64.encodeToString( + saslString.getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } catch (final NoSuchAlgorithmException e) { + throw new AuthenticationException(e); + } - return encodedResponse; - case RESPONSE_SENT: - state = State.VALID_SERVER_RESPONSE; - break; - case VALID_SERVER_RESPONSE: - if (challenge==null) { - return null; //everything is fine - } - default: - throw new InvalidStateException(state); - } - return null; - } + return encodedResponse; + case RESPONSE_SENT: + state = State.VALID_SERVER_RESPONSE; + break; + case VALID_SERVER_RESPONSE: + if (challenge == null) { + return null; //everything is fine + } + default: + throw new InvalidStateException(state); + } + return null; + } } diff --git a/src/main/java/de/pixart/messenger/crypto/sasl/External.java b/src/main/java/de/pixart/messenger/crypto/sasl/External.java index a1a79a0a8..b80d174f0 100644 --- a/src/main/java/de/pixart/messenger/crypto/sasl/External.java +++ b/src/main/java/de/pixart/messenger/crypto/sasl/External.java @@ -9,22 +9,22 @@ import de.pixart.messenger.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); - } + 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/pixart/messenger/crypto/sasl/Plain.java b/src/main/java/de/pixart/messenger/crypto/sasl/Plain.java index d0aa90b49..27de45247 100644 --- a/src/main/java/de/pixart/messenger/crypto/sasl/Plain.java +++ b/src/main/java/de/pixart/messenger/crypto/sasl/Plain.java @@ -8,23 +8,23 @@ import de.pixart.messenger.entities.Account; import de.pixart.messenger.xml.TagWriter; public class Plain extends SaslMechanism { - public Plain(final TagWriter tagWriter, final Account account) { - super(tagWriter, account, null); - } + public Plain(final TagWriter tagWriter, final Account account) { + super(tagWriter, account, null); + } - @Override - public int getPriority() { - return 10; - } + @Override + public int getPriority() { + return 10; + } - @Override - public String getMechanism() { - return "PLAIN"; - } + @Override + public String getMechanism() { + return "PLAIN"; + } - @Override - public String getClientFirstMessage() { - final String sasl = '\u0000' + account.getUsername() + '\u0000' + account.getPassword(); - return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); - } + @Override + public String getClientFirstMessage() { + final String sasl = '\u0000' + account.getUsername() + '\u0000' + account.getPassword(); + return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); + } } diff --git a/src/main/java/de/pixart/messenger/crypto/sasl/SaslMechanism.java b/src/main/java/de/pixart/messenger/crypto/sasl/SaslMechanism.java index 19e8f3591..8b8883b9f 100644 --- a/src/main/java/de/pixart/messenger/crypto/sasl/SaslMechanism.java +++ b/src/main/java/de/pixart/messenger/crypto/sasl/SaslMechanism.java @@ -7,56 +7,59 @@ import de.pixart.messenger.xml.TagWriter; public abstract class SaslMechanism { - final protected TagWriter tagWriter; - final protected Account account; - final protected SecureRandom rng; - - protected enum State { - INITIAL, - AUTH_TEXT_SENT, - RESPONSE_SENT, - VALID_SERVER_RESPONSE, - } - - public static class AuthenticationException extends Exception { - public AuthenticationException(final String message) { - super(message); - } - - public AuthenticationException(final Exception inner) { - super(inner); - } - } - - public static class InvalidStateException extends AuthenticationException { - public InvalidStateException(final String message) { - super(message); - } - - public InvalidStateException(final State state) { - this("Invalid state: " + state.toString()); - } - } - - public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - this.tagWriter = tagWriter; - this.account = account; - this.rng = rng; - } - - /** - * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another - * mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade - * attacks). - * @return An arbitrary int representing the priority - */ - public abstract int getPriority(); - - public abstract String getMechanism(); - public String getClientFirstMessage() { - return ""; - } - public String getResponse(final String challenge) throws AuthenticationException { - return ""; - } + final protected TagWriter tagWriter; + final protected Account account; + final protected SecureRandom rng; + + protected enum State { + INITIAL, + AUTH_TEXT_SENT, + RESPONSE_SENT, + VALID_SERVER_RESPONSE, + } + + public static class AuthenticationException extends Exception { + public AuthenticationException(final String message) { + super(message); + } + + public AuthenticationException(final Exception inner) { + super(inner); + } + } + + public static class InvalidStateException extends AuthenticationException { + public InvalidStateException(final String message) { + super(message); + } + + public InvalidStateException(final State state) { + this("Invalid state: " + state.toString()); + } + } + + public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + this.tagWriter = tagWriter; + this.account = account; + this.rng = rng; + } + + /** + * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another + * mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade + * attacks). + * + * @return An arbitrary int representing the priority + */ + public abstract int getPriority(); + + public abstract String getMechanism(); + + public String getClientFirstMessage() { + return ""; + } + + public String getResponse(final String challenge) throws AuthenticationException { + return ""; + } } diff --git a/src/main/java/de/pixart/messenger/crypto/sasl/ScramSha1.java b/src/main/java/de/pixart/messenger/crypto/sasl/ScramSha1.java index abff542d4..d21cad37d 100644 --- a/src/main/java/de/pixart/messenger/crypto/sasl/ScramSha1.java +++ b/src/main/java/de/pixart/messenger/crypto/sasl/ScramSha1.java @@ -18,221 +18,222 @@ import de.pixart.messenger.utils.CryptoHelper; import de.pixart.messenger.xml.TagWriter; 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; - final private String clientNonce; - private byte[] serverSignature = null; - private static HMac HMAC; - private static Digest DIGEST; - private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); - private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); - - public static class KeyPair { - final public byte[] clientKey; - final public byte[] serverKey; - - public KeyPair(final byte[] clientKey, final byte[] serverKey) { - this.clientKey = clientKey; - this.serverKey = serverKey; - } - } - - private static final LruCache CACHE; - - static { - DIGEST = new SHA1Digest(); - HMAC = new HMac(new SHA1Digest()); - CACHE = new LruCache(10) { - protected KeyPair create(final String k) { - // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". - // Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()' - // is applied to prevent commas in the strings breaking things. - final String[] kparts = k.split(",", 4); - try { - final byte[] saltedPassword, serverKey, clientKey; - saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(), - Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3])); - serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); - clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); - - return new KeyPair(clientKey, serverKey); - } catch (final InvalidKeyException | NumberFormatException e) { - return null; - } - } - }; - } - - private State state = State.INITIAL; - - public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - - // This nonce should be different for each authentication attempt. - clientNonce = new BigInteger(100, this.rng).toString(32); - clientFirstMessageBare = ""; - } - - @Override - public int getPriority() { - return 20; - } - - @Override - public String getMechanism() { - return "SCRAM-SHA-1"; - } - - @Override - public String getClientFirstMessage() { - if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { - clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + - ",r=" + this.clientNonce; - state = State.AUTH_TEXT_SENT; - } - return Base64.encodeToString( - (GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()), - Base64.NO_WRAP); - } - - @Override - public String getResponse(final String challenge) throws AuthenticationException { - switch (state) { - case AUTH_TEXT_SENT: - 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; - String salt = ""; - for (final String token : tokenizer) { - if (token.charAt(1) == '=') { - switch (token.charAt(0)) { - case 'i': - try { - iterationCount = Integer.parseInt(token.substring(2)); - } catch (final NumberFormatException e) { - throw new AuthenticationException(e); - } - break; - case 's': - salt = token.substring(2); - break; - case 'r': - nonce = token.substring(2); - break; - case 'm': - /* + // 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; + final private String clientNonce; + private byte[] serverSignature = null; + private static HMac HMAC; + private static Digest DIGEST; + private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); + private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); + + public static class KeyPair { + final public byte[] clientKey; + final public byte[] serverKey; + + public KeyPair(final byte[] clientKey, final byte[] serverKey) { + this.clientKey = clientKey; + this.serverKey = serverKey; + } + } + + private static final LruCache CACHE; + + static { + DIGEST = new SHA1Digest(); + HMAC = new HMac(new SHA1Digest()); + CACHE = new LruCache(10) { + protected KeyPair create(final String k) { + // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". + // Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()' + // is applied to prevent commas in the strings breaking things. + final String[] kparts = k.split(",", 4); + try { + final byte[] saltedPassword, serverKey, clientKey; + saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(), + Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3])); + serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); + clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); + + return new KeyPair(clientKey, serverKey); + } catch (final InvalidKeyException | NumberFormatException e) { + return null; + } + } + }; + } + + private State state = State.INITIAL; + + public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + super(tagWriter, account, rng); + + // This nonce should be different for each authentication attempt. + clientNonce = new BigInteger(100, this.rng).toString(32); + clientFirstMessageBare = ""; + } + + @Override + public int getPriority() { + return 20; + } + + @Override + public String getMechanism() { + return "SCRAM-SHA-1"; + } + + @Override + public String getClientFirstMessage() { + if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { + clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + + ",r=" + this.clientNonce; + state = State.AUTH_TEXT_SENT; + } + return Base64.encodeToString( + (GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } + + @Override + public String getResponse(final String challenge) throws AuthenticationException { + switch (state) { + case AUTH_TEXT_SENT: + 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; + String salt = ""; + for (final String token : tokenizer) { + if (token.charAt(1) == '=') { + switch (token.charAt(0)) { + case 'i': + try { + iterationCount = Integer.parseInt(token.substring(2)); + } catch (final NumberFormatException e) { + throw new AuthenticationException(e); + } + break; + case 's': + salt = token.substring(2); + break; + case 'r': + nonce = token.substring(2); + break; + case 'm': + /* * RFC 5802: * m: This attribute is reserved for future extensibility. In this * version of SCRAM, its presence in a client or a server message * MUST cause authentication failure when the attribute is parsed by * the other end. */ - throw new AuthenticationException("Server sent reserved token: `m'"); - } - } - } - - if (iterationCount < 0) { - throw new AuthenticationException("Server did not send iteration count"); - } - if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { - throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce); - } - if (salt.isEmpty()) { - throw new AuthenticationException("Server sent empty salt"); - } - - final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString( - GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce; - final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' - + clientFinalMessageWithoutProof).getBytes(); - - // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". - final KeyPair keys = CACHE.get( - CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + "," - + CryptoHelper.bytesToHex(account.getPassword().getBytes()) + "," - + CryptoHelper.bytesToHex(salt.getBytes()) + "," - + String.valueOf(iterationCount) - ); - if (keys == null) { - throw new AuthenticationException("Invalid keys generated"); - } - final byte[] clientSignature; - try { - serverSignature = hmac(keys.serverKey, authMessage); - final byte[] storedKey = digest(keys.clientKey); - - clientSignature = hmac(storedKey, authMessage); - - } catch (final InvalidKeyException e) { - throw new AuthenticationException(e); - } - - final byte[] clientProof = new byte[keys.clientKey.length]; - - for (int i = 0; i < clientProof.length; i++) { - clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); - } - - - final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + - Base64.encodeToString(clientProof, Base64.NO_WRAP); - state = State.RESPONSE_SENT; - return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); - case RESPONSE_SENT: - try { - final String clientCalculatedServerFinalMessage = "v=" + - Base64.encodeToString(serverSignature, Base64.NO_WRAP); - if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) { - throw new Exception(); - }; - state = State.VALID_SERVER_RESPONSE; - return ""; - } catch(Exception e) { - throw new AuthenticationException("Server final message does not match calculated final message"); - } - default: - throw new InvalidStateException(state); - } - } - - public static synchronized byte[] hmac(final byte[] key, final byte[] input) - throws InvalidKeyException { - HMAC.init(new KeyParameter(key)); - HMAC.update(input, 0, input.length); - final byte[] out = new byte[HMAC.getMacSize()]; - HMAC.doFinal(out, 0); - return out; - } - - public static synchronized byte[] digest(byte[] bytes) { - DIGEST.reset(); - DIGEST.update(bytes, 0, bytes.length); - final byte[] out = new byte[DIGEST.getDigestSize()]; - DIGEST.doFinal(out, 0); - return out; - } - - /* - * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the - * pseudorandom function (PRF) and with dkLen == output length of - * HMAC() == output length of H(). - */ - private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations) - throws InvalidKeyException { - byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE)); - byte[] out = u.clone(); - for (int i = 1; i < iterations; i++) { - u = hmac(key, u); - for (int j = 0; j < u.length; j++) { - out[j] ^= u[j]; - } - } - return out; - } + throw new AuthenticationException("Server sent reserved token: `m'"); + } + } + } + + if (iterationCount < 0) { + throw new AuthenticationException("Server did not send iteration count"); + } + if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { + throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce); + } + if (salt.isEmpty()) { + throw new AuthenticationException("Server sent empty salt"); + } + + final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString( + GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce; + final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' + + clientFinalMessageWithoutProof).getBytes(); + + // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". + final KeyPair keys = CACHE.get( + CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + "," + + CryptoHelper.bytesToHex(account.getPassword().getBytes()) + "," + + CryptoHelper.bytesToHex(salt.getBytes()) + "," + + String.valueOf(iterationCount) + ); + if (keys == null) { + throw new AuthenticationException("Invalid keys generated"); + } + final byte[] clientSignature; + try { + serverSignature = hmac(keys.serverKey, authMessage); + final byte[] storedKey = digest(keys.clientKey); + + clientSignature = hmac(storedKey, authMessage); + + } catch (final InvalidKeyException e) { + throw new AuthenticationException(e); + } + + final byte[] clientProof = new byte[keys.clientKey.length]; + + for (int i = 0; i < clientProof.length; i++) { + clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); + } + + + final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + + Base64.encodeToString(clientProof, Base64.NO_WRAP); + state = State.RESPONSE_SENT; + return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); + case RESPONSE_SENT: + try { + final String clientCalculatedServerFinalMessage = "v=" + + Base64.encodeToString(serverSignature, Base64.NO_WRAP); + if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) { + throw new Exception(); + } + ; + state = State.VALID_SERVER_RESPONSE; + return ""; + } catch (Exception e) { + throw new AuthenticationException("Server final message does not match calculated final message"); + } + default: + throw new InvalidStateException(state); + } + } + + public static synchronized byte[] hmac(final byte[] key, final byte[] input) + throws InvalidKeyException { + HMAC.init(new KeyParameter(key)); + HMAC.update(input, 0, input.length); + final byte[] out = new byte[HMAC.getMacSize()]; + HMAC.doFinal(out, 0); + return out; + } + + public static synchronized byte[] digest(byte[] bytes) { + DIGEST.reset(); + DIGEST.update(bytes, 0, bytes.length); + final byte[] out = new byte[DIGEST.getDigestSize()]; + DIGEST.doFinal(out, 0); + return out; + } + + /* + * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the + * pseudorandom function (PRF) and with dkLen == output length of + * HMAC() == output length of H(). + */ + private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations) + throws InvalidKeyException { + byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE)); + byte[] out = u.clone(); + for (int i = 1; i < iterations; i++) { + u = hmac(key, u); + for (int j = 0; j < u.length; j++) { + out[j] ^= u[j]; + } + } + return out; + } } diff --git a/src/main/java/de/pixart/messenger/crypto/sasl/Tokenizer.java b/src/main/java/de/pixart/messenger/crypto/sasl/Tokenizer.java index 01cd07929..67e4418dd 100644 --- a/src/main/java/de/pixart/messenger/crypto/sasl/Tokenizer.java +++ b/src/main/java/de/pixart/messenger/crypto/sasl/Tokenizer.java @@ -10,69 +10,69 @@ import java.util.NoSuchElementException; * A tokenizer for GS2 header strings */ public final class Tokenizer implements Iterator, Iterable { - private final List parts; - private int index; + private final List parts; + private int index; - public Tokenizer(final byte[] challenge) { - final String challengeString = new String(challenge); - parts = new ArrayList<>(Arrays.asList(challengeString.split(","))); - // Trim parts. - for (int i = 0; i < parts.size(); i++) { - parts.set(i, parts.get(i).trim()); - } - index = 0; - } + public Tokenizer(final byte[] challenge) { + final String challengeString = new String(challenge); + parts = new ArrayList<>(Arrays.asList(challengeString.split(","))); + // Trim parts. + for (int i = 0; i < parts.size(); i++) { + parts.set(i, parts.get(i).trim()); + } + index = 0; + } - /** - * Returns true if there is at least one more element, false otherwise. - * - * @see #next - */ - @Override - public boolean hasNext() { - return parts.size() != index + 1; - } + /** + * Returns true if there is at least one more element, false otherwise. + * + * @see #next + */ + @Override + public boolean hasNext() { + return parts.size() != index + 1; + } - /** - * Returns the next object and advances the iterator. - * - * @return the next object. - * @throws java.util.NoSuchElementException if there are no more elements. - * @see #hasNext - */ - @Override - public String next() { - if (hasNext()) { - return parts.get(index++); - } else { - throw new NoSuchElementException("No such element. Size is: " + parts.size()); - } - } + /** + * Returns the next object and advances the iterator. + * + * @return the next object. + * @throws java.util.NoSuchElementException if there are no more elements. + * @see #hasNext + */ + @Override + public String next() { + if (hasNext()) { + return parts.get(index++); + } else { + throw new NoSuchElementException("No such element. Size is: " + parts.size()); + } + } - /** - * Removes the last object returned by {@code next} from the collection. - * This method can only be called once between each call to {@code next}. - * - * @throws UnsupportedOperationException if removing is not supported by the collection being - * iterated. - * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has - * already been called after the last call to {@code next}. - */ - @Override - public void remove() { - if(index <= 0) { - throw new IllegalStateException("You can't delete an element before first next() method call"); - } - parts.remove(--index); - } + /** + * Removes the last object returned by {@code next} from the collection. + * This method can only be called once between each call to {@code next}. + * + * @throws UnsupportedOperationException if removing is not supported by the collection being + * iterated. + * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has + * already been called after the last call to {@code next}. + */ + @Override + public void remove() { + if (index <= 0) { + throw new IllegalStateException("You can't delete an element before first next() method call"); + } + parts.remove(--index); + } - /** - * Returns an {@link java.util.Iterator} for the elements in this object. - * - * @return An {@code Iterator} instance. - */ - @Override - public Iterator iterator() { - return parts.iterator(); - } + /** + * Returns an {@link java.util.Iterator} for the elements in this object. + * + * @return An {@code Iterator} instance. + */ + @Override + public Iterator iterator() { + return parts.iterator(); + } } -- cgit v1.2.3