diff --git a/app/src/main/java/im/conversations/android/axolotl/AxolotlService.java b/app/src/main/java/im/conversations/android/axolotl/AxolotlService.java index 9098a1035..24612eef4 100644 --- a/app/src/main/java/im/conversations/android/axolotl/AxolotlService.java +++ b/app/src/main/java/im/conversations/android/axolotl/AxolotlService.java @@ -96,6 +96,9 @@ public class AxolotlService extends AbstractAccountService { throws NoSessionException { final var session = getExistingSession(axolotlAddress); if (session == null) { + // TODO When receiving a message that is not an OMEMOKeyExchange from a device there is + // no session with, clients SHOULD create a session with that device and notify it about + // the new session by responding with an empty OMEMO message as per Sending a message. throw new NoSessionException( String.format("No session for %s", axolotlAddress.toString())); } @@ -166,15 +169,15 @@ public class AxolotlService extends AbstractAccountService { new AxolotlAddress(from.asBareJid(), header.getSourceDevice().get())); keyWithAuthTag = session.sessionCipher.decrypt(signalMessage); } - if (keyWithAuthTag.length < 32) { - throw new OutdatedSenderException( - "Key did not contain auth tag. Sender needs to update their OMEMO client"); - } final var inDeviceList = database.axolotlDao().hasDeviceId(account, session.axolotlAddress); if (payload == null) { return new AxolotlPayload( session.axolotlAddress, session.identityKey, preKeyMessage, inDeviceList, null); } + if (keyWithAuthTag.length < 32) { + throw new OutdatedSenderException( + "Key did not contain auth tag. Sender needs to update their OMEMO client"); + } final byte[] key = new byte[16]; final byte[] authTag = new byte[16]; final byte[] iv = header.getIv(); diff --git a/app/src/main/java/im/conversations/android/axolotl/EncryptionBuilder.java b/app/src/main/java/im/conversations/android/axolotl/EncryptionBuilder.java index d96027eb7..15dbc7a6c 100644 --- a/app/src/main/java/im/conversations/android/axolotl/EncryptionBuilder.java +++ b/app/src/main/java/im/conversations/android/axolotl/EncryptionBuilder.java @@ -31,7 +31,7 @@ public class EncryptionBuilder { private Long sourceDeviceId; - private ArrayList sessions; + private final ArrayList sessions = new ArrayList<>(); private byte[] payload; @@ -76,6 +76,33 @@ public class EncryptionBuilder { return encrypted; } + public Encrypted buildKeyTransport() throws AxolotlEncryptionException { + try { + return buildKeyTransportOrThrow(); + } catch (final UntrustedIdentityException e) { + throw new AxolotlEncryptionException(e); + } + } + + private Encrypted buildKeyTransportOrThrow() throws UntrustedIdentityException { + final long sourceDeviceId = + Preconditions.checkNotNull(this.sourceDeviceId, "Specify a source device id"); + Preconditions.checkState( + this.payload == null, "A key transport message should not have a payload"); + // TODO key transport messages in twomemo (omemo:1) use 32 bytes of zeros instead of a key + // TODO if we are not using this using this for actual key transport we can do this in siacs + // omemo too (and get rid of the IV) + final var sessions = ImmutableList.copyOf(this.sessions); + final var key = generateKey(); + final var iv = generateIv(); + final var header = buildHeader(sessions, key); + header.addIv(iv); + header.setSourceDevice(sourceDeviceId); + final var encrypted = new Encrypted(); + encrypted.addExtension(header); + return encrypted; + } + public EncryptionBuilder payload(final String payload) { this.payload = payload.getBytes(StandardCharsets.UTF_8); return this; @@ -97,6 +124,7 @@ public class EncryptionBuilder { for (final AxolotlSession session : sessions) { final var cipherMessage = session.sessionCipher.encrypt(keyWithAuthTag); final var key = header.addExtension(new Key()); + key.setRemoteDeviceId(session.axolotlAddress.getDeviceId()); key.setContent(cipherMessage.serialize()); key.setIsPreKey(cipherMessage.getType() == CiphertextMessage.PREKEY_TYPE); } diff --git a/app/src/main/java/im/conversations/android/xmpp/manager/AxolotlManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/AxolotlManager.java index 84f675bf3..7e786e4e8 100644 --- a/app/src/main/java/im/conversations/android/xmpp/manager/AxolotlManager.java +++ b/app/src/main/java/im/conversations/android/xmpp/manager/AxolotlManager.java @@ -35,6 +35,7 @@ import im.conversations.android.xmpp.model.axolotl.Bundle; import im.conversations.android.xmpp.model.axolotl.DeviceList; import im.conversations.android.xmpp.model.axolotl.Encrypted; import im.conversations.android.xmpp.model.pubsub.Items; +import im.conversations.android.xmpp.model.stanza.Message; import java.util.Collection; import java.util.Collections; import java.util.Locale; @@ -503,7 +504,40 @@ public class AxolotlManager extends AbstractManager implements AxolotlService.Po "fresh session from {}/{}", axolotlAddress.getJid(), axolotlAddress.getDeviceId()); + // After receiving an OMEMOKeyExchange and successfully building a new session, the + // receiving device SHOULD automatically respond with an empty OMEMO message (as per + // Sending a message) to the source of the OMEMOKeyExchange. This is to notify the + // device that the session initiation was completed successfully and that the device can + // stop sending OMEMOKeyExchanges. + sendKeyTransportToCompleteSession(axolotlAddress); } + // TODO republish device bundle + } + + private void sendKeyTransportToCompleteSession(final AxolotlAddress axolotlAddress) { + final var existingSession = axolotlService.getExistingSession(axolotlAddress); + if (existingSession == null) { + return; + } + final Encrypted encrypted; + try { + encrypted = + new EncryptionBuilder() + .session(existingSession) + .sourceDeviceId(signalProtocolStore().getLocalRegistrationId()) + .buildKeyTransport(); + } catch (final AxolotlEncryptionException e) { + LOGGER.error("Could not create key transport message to complete session", e); + return; + } + final var message = new Message(Message.Type.NORMAL); + message.setTo(axolotlAddress.getJid()); + message.addExtension(encrypted); + LOGGER.info( + "Sending KeyTransport Message to {}/{}", + axolotlAddress.getJid(), + axolotlAddress.getDeviceId()); + connection.sendMessagePacket(message); } @Override