From 64b1dbfdae02cbb400fa9f970063c9b404a4ee93 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Wed, 4 Mar 2015 18:53:57 -0800 Subject: [PATCH] Simplify API, add builders. --- .../api/TextSecureMessageSender.java | 30 +++------- .../api/crypto/TextSecureCipher.java | 36 ++++++++---- .../api/messages/TextSecureAttachment.java | 38 +++++++++++++ .../api/messages/TextSecureEnvelope.java | 11 ++++ .../api/messages/TextSecureGroup.java | 52 ++++++++++++++++++ .../api/messages/TextSecureMessage.java | 55 +++++++++++++++++++ .../internal/push/OutgoingPushMessage.java | 14 +++-- .../textsecure/internal/push/PushBody.java | 42 -------------- 8 files changed, 199 insertions(+), 79 deletions(-) delete mode 100644 java/src/main/java/org/whispersystems/textsecure/internal/push/PushBody.java diff --git a/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageSender.java b/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageSender.java index 6160feffaa..c0a3e764ac 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageSender.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageSender.java @@ -23,7 +23,6 @@ import org.whispersystems.libaxolotl.AxolotlAddress; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.SessionBuilder; import org.whispersystems.libaxolotl.logging.Log; -import org.whispersystems.libaxolotl.protocol.CiphertextMessage; import org.whispersystems.libaxolotl.state.AxolotlStore; import org.whispersystems.libaxolotl.state.PreKeyBundle; import org.whispersystems.libaxolotl.util.guava.Optional; @@ -35,18 +34,17 @@ import org.whispersystems.textsecure.api.messages.TextSecureGroup; import org.whispersystems.textsecure.api.messages.TextSecureMessage; import org.whispersystems.textsecure.api.push.TextSecureAddress; import org.whispersystems.textsecure.api.push.TrustStore; +import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException; import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException; +import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException; import org.whispersystems.textsecure.internal.push.MismatchedDevices; import org.whispersystems.textsecure.internal.push.OutgoingPushMessage; import org.whispersystems.textsecure.internal.push.OutgoingPushMessageList; import org.whispersystems.textsecure.internal.push.PushAttachmentData; -import org.whispersystems.textsecure.internal.push.PushBody; import org.whispersystems.textsecure.internal.push.PushServiceSocket; import org.whispersystems.textsecure.internal.push.SendMessageResponse; import org.whispersystems.textsecure.internal.push.StaleDevices; -import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException; -import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.textsecure.internal.push.exceptions.MismatchedDevicesException; import org.whispersystems.textsecure.internal.push.exceptions.StaleDevicesException; import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider; @@ -56,7 +54,6 @@ import java.io.IOException; import java.util.LinkedList; import java.util.List; -import static org.whispersystems.textsecure.internal.push.PushMessageProtos.IncomingPushMessageSignal.Type; import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent; import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.AttachmentPointer; import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext; @@ -304,22 +301,21 @@ public class TextSecureMessageSender { List messages = new LinkedList<>(); if (!recipient.equals(syncAddress)) { - PushBody masterBody = getEncryptedMessage(socket, recipient, TextSecureAddress.DEFAULT_DEVICE_ID, plaintext); - messages.add(new OutgoingPushMessage(recipient, TextSecureAddress.DEFAULT_DEVICE_ID, masterBody)); + messages.add(getEncryptedMessage(socket, recipient, TextSecureAddress.DEFAULT_DEVICE_ID, plaintext)); } for (int deviceId : store.getSubDeviceSessions(recipient.getNumber())) { - PushBody body = getEncryptedMessage(socket, recipient, deviceId, plaintext); - messages.add(new OutgoingPushMessage(recipient, deviceId, body)); + messages.add(getEncryptedMessage(socket, recipient, deviceId, plaintext)); } return new OutgoingPushMessageList(recipient.getNumber(), timestamp, recipient.getRelay().orNull(), messages); } - private PushBody getEncryptedMessage(PushServiceSocket socket, TextSecureAddress recipient, int deviceId, byte[] plaintext) + private OutgoingPushMessage getEncryptedMessage(PushServiceSocket socket, TextSecureAddress recipient, int deviceId, byte[] plaintext) throws IOException, UntrustedIdentityException { - AxolotlAddress axolotlAddress = new AxolotlAddress(recipient.getNumber(), deviceId); + AxolotlAddress axolotlAddress = new AxolotlAddress(recipient.getNumber(), deviceId); + TextSecureCipher cipher = new TextSecureCipher(store); if (!store.containsSession(axolotlAddress)) { try { @@ -343,17 +339,7 @@ public class TextSecureMessageSender { } } - TextSecureCipher cipher = new TextSecureCipher(store, axolotlAddress); - CiphertextMessage message = cipher.encrypt(plaintext); - int remoteRegistrationId = cipher.getRemoteRegistrationId(); - - if (message.getType() == CiphertextMessage.PREKEY_TYPE) { - return new PushBody(Type.PREKEY_BUNDLE_VALUE, remoteRegistrationId, message.serialize()); - } else if (message.getType() == CiphertextMessage.WHISPER_TYPE) { - return new PushBody(Type.CIPHERTEXT_VALUE, remoteRegistrationId, message.serialize()); - } else { - throw new AssertionError("Unknown ciphertext type: " + message.getType()); - } + return cipher.encrypt(axolotlAddress, plaintext); } private void handleMismatchedDevices(PushServiceSocket socket, TextSecureAddress recipient, diff --git a/java/src/main/java/org/whispersystems/textsecure/api/crypto/TextSecureCipher.java b/java/src/main/java/org/whispersystems/textsecure/api/crypto/TextSecureCipher.java index 9c7cef8ecf..cb94f0998b 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/crypto/TextSecureCipher.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/crypto/TextSecureCipher.java @@ -37,11 +37,15 @@ import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer; import org.whispersystems.textsecure.api.messages.TextSecureEnvelope; import org.whispersystems.textsecure.api.messages.TextSecureGroup; import org.whispersystems.textsecure.api.messages.TextSecureMessage; +import org.whispersystems.textsecure.internal.push.OutgoingPushMessage; +import org.whispersystems.textsecure.internal.push.PushMessageProtos; import org.whispersystems.textsecure.internal.push.PushTransportDetails; +import org.whispersystems.textsecure.internal.util.Base64; import java.util.LinkedList; import java.util.List; +import static org.whispersystems.textsecure.internal.push.PushMessageProtos.IncomingPushMessageSignal.Type; import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent; import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext.Type.DELIVER; @@ -52,15 +56,28 @@ import static org.whispersystems.textsecure.internal.push.PushMessageProtos.Push */ public class TextSecureCipher { - private final SessionCipher sessionCipher; + private final AxolotlStore axolotlStore; - public TextSecureCipher(AxolotlStore axolotlStore, AxolotlAddress destination) { - this.sessionCipher = new SessionCipher(axolotlStore, destination); + public TextSecureCipher(AxolotlStore axolotlStore) { + this.axolotlStore = axolotlStore; } - public CiphertextMessage encrypt(byte[] unpaddedMessage) { - PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion()); - return sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage)); + public OutgoingPushMessage encrypt(AxolotlAddress destination, byte[] unpaddedMessage) { + SessionCipher sessionCipher = new SessionCipher(axolotlStore, destination); + PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion()); + CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage)); + int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(); + String body = Base64.encodeBytes(message.serialize()); + + int type; + + switch (message.getType()) { + case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break; + case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break; + default: throw new AssertionError("Bad type: " + message.getType()); + } + + return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body); } /** @@ -83,6 +100,9 @@ public class TextSecureCipher { LegacyMessageException, NoSessionException { try { + AxolotlAddress sourceAddress = new AxolotlAddress(envelope.getSource(), envelope.getSourceDevice()); + SessionCipher sessionCipher = new SessionCipher(axolotlStore, sourceAddress); + byte[] paddedMessage; if (envelope.isPreKeyWhisperMessage()) { @@ -104,10 +124,6 @@ public class TextSecureCipher { } } - public int getRemoteRegistrationId() { - return sessionCipher.getRemoteRegistrationId(); - } - private TextSecureMessage createTextSecureMessage(TextSecureEnvelope envelope, PushMessageContent content) { TextSecureGroup groupInfo = createGroupInfo(envelope, content); List attachments = new LinkedList<>(); diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachment.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachment.java index c58b709f62..03b6437a6e 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachment.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachment.java @@ -16,6 +16,8 @@ */ package org.whispersystems.textsecure.api.messages; +import java.io.InputStream; + public abstract class TextSecureAttachment { private final String contentType; @@ -38,4 +40,40 @@ public abstract class TextSecureAttachment { public TextSecureAttachmentPointer asPointer() { return (TextSecureAttachmentPointer)this; } + + public static Builder newStreamBuilder() { + return new Builder(); + } + + public static class Builder { + + private InputStream inputStream; + private String contentType; + private long length; + + private Builder() {} + + public Builder withStream(InputStream inputStream) { + this.inputStream = inputStream; + return this; + } + + public Builder withContentType(String contentType) { + this.contentType = contentType; + return this; + } + + public Builder withLength(long length) { + this.length = length; + return this; + } + + public TextSecureAttachmentStream build() { + if (inputStream == null) throw new IllegalArgumentException("Must specify stream!"); + if (contentType == null) throw new IllegalArgumentException("No content type specified!"); + if (length == 0) throw new IllegalArgumentException("No length specified!"); + + return new TextSecureAttachmentStream(inputStream, contentType, length); + } + } } diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureEnvelope.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureEnvelope.java index 6b722f0cb3..9e1588752a 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureEnvelope.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureEnvelope.java @@ -20,6 +20,8 @@ import com.google.protobuf.ByteString; import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.libaxolotl.logging.Log; +import org.whispersystems.libaxolotl.util.guava.Optional; +import org.whispersystems.textsecure.api.push.TextSecureAddress; import org.whispersystems.textsecure.internal.push.PushMessageProtos.IncomingPushMessageSignal; import org.whispersystems.textsecure.internal.util.Base64; import org.whispersystems.textsecure.internal.util.Hex; @@ -127,6 +129,15 @@ public class TextSecureEnvelope { return signal.getSourceDevice(); } + /** + * @return The envelope's sender as a TextSecureAddress. + */ + public TextSecureAddress getSourceAddress() { + return new TextSecureAddress(signal.getSource(), + signal.hasRelay() ? Optional.fromNullable(signal.getRelay()) : + Optional.absent()); + } + /** * @return The envelope content type. */ diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureGroup.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureGroup.java index 83d6a755d2..07c3f95c6c 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureGroup.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureGroup.java @@ -96,4 +96,56 @@ public class TextSecureGroup { return avatar; } + public static Builder newUpdateBuilder() { + return new Builder(Type.UPDATE); + } + + public static Builder newBuilder(Type type) { + return new Builder(type); + } + + public static class Builder { + + private Type type; + private byte[] id; + private String name; + private List members; + private TextSecureAttachment avatar; + + private Builder(Type type) { + this.type = type; + } + + public Builder withId(byte[] id) { + this.id = id; + return this; + } + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Builder withMembers(List members) { + this.members = members; + return this; + } + + public Builder withAvatar(TextSecureAttachment avatar) { + this.avatar = avatar; + return this; + } + + public TextSecureGroup build() { + if (id == null) throw new IllegalArgumentException("No group ID specified!"); + + if (type == Type.UPDATE && name == null && members == null && avatar == null) { + throw new IllegalArgumentException("Group update with no updates!"); + } + + return new TextSecureGroup(type, id, name, members, avatar); + } + + } + } diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureMessage.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureMessage.java index ef31e1e391..f6044a3d2c 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureMessage.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureMessage.java @@ -94,6 +94,10 @@ public class TextSecureMessage { } } + public static Builder newBuilder() { + return new Builder(); + } + /** * @return The message timestamp. */ @@ -133,4 +137,55 @@ public class TextSecureMessage { public boolean isGroupUpdate() { return group.isPresent() && group.get().getType() != TextSecureGroup.Type.DELIVER; } + + public static class Builder { + + private List attachments = new LinkedList<>(); + private long timestamp; + private TextSecureGroup group; + private String body; + private boolean endSession; + + private Builder() {} + + public Builder withTimestamp(long timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder asGroupMessage(TextSecureGroup group) { + this.group = group; + return this; + } + + public Builder withAttachment(TextSecureAttachment attachment) { + this.attachments.add(attachment); + return this; + } + + public Builder withAttachments(List attachments) { + this.attachments.addAll(attachments); + return this; + } + + public Builder withBody(String body) { + this.body = body; + return this; + } + + public Builder asEndSessionMessage() { + this.endSession = true; + return this; + } + + public Builder asEndSessionMessage(boolean endSession) { + this.endSession = endSession; + return this; + } + + public TextSecureMessage build() { + if (timestamp == 0) timestamp = System.currentTimeMillis(); + return new TextSecureMessage(timestamp, group, attachments, body, true, endSession); + } + } } diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/push/OutgoingPushMessage.java b/java/src/main/java/org/whispersystems/textsecure/internal/push/OutgoingPushMessage.java index 66cd6c0217..6ce3645324 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/push/OutgoingPushMessage.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/push/OutgoingPushMessage.java @@ -33,11 +33,15 @@ public class OutgoingPushMessage { @JsonProperty private String body; - public OutgoingPushMessage(TextSecureAddress address, int deviceId, PushBody body) { - this.type = body.getType(); - this.destinationDeviceId = deviceId; - this.destinationRegistrationId = body.getRemoteRegistrationId(); - this.body = Base64.encodeBytes(body.getBody()); + public OutgoingPushMessage(int type, + int destinationDeviceId, + int destinationRegistrationId, + String body) + { + this.type = type; + this.destinationDeviceId = destinationDeviceId; + this.destinationRegistrationId = destinationRegistrationId; + this.body = body; } public int getDestinationDeviceId() { diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/push/PushBody.java b/java/src/main/java/org/whispersystems/textsecure/internal/push/PushBody.java deleted file mode 100644 index 3805aa455c..0000000000 --- a/java/src/main/java/org/whispersystems/textsecure/internal/push/PushBody.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (C) 2014 Open Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.whispersystems.textsecure.internal.push; - -public class PushBody { - - private final int type; - private final int remoteRegistrationId; - private final byte[] body; - - public PushBody(int type, int remoteRegistrationId, byte[] body) { - this.type = type; - this.remoteRegistrationId = remoteRegistrationId; - this.body = body; - } - - public int getType() { - return type; - } - - public byte[] getBody() { - return body; - } - - public int getRemoteRegistrationId() { - return remoteRegistrationId; - } -}