libtextsecure javadoc and minor API refactoring

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-02-27 15:35:18 -08:00
parent fcde642563
commit 004f050741
18 changed files with 452 additions and 73 deletions

View File

@ -30,19 +30,40 @@ import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
import org.whispersystems.textsecure.api.push.TrustStore; import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.internal.crypto.ProvisioningCipher; import org.whispersystems.textsecure.internal.crypto.ProvisioningCipher;
import org.whispersystems.textsecure.internal.push.PushServiceSocket; import org.whispersystems.textsecure.internal.push.PushServiceSocket;
import org.whispersystems.textsecure.internal.util.Base64;
import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider; import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider;
import org.whispersystems.textsecure.internal.util.Util;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import static org.whispersystems.textsecure.internal.push.ProvisioningProtos.ProvisionMessage; import static org.whispersystems.textsecure.internal.push.ProvisioningProtos.ProvisionMessage;
/**
* The main interface for creating, registering, and
* managing a TextSecure account.
*
* @author Moxie Marlinspike
*/
public class TextSecureAccountManager { public class TextSecureAccountManager {
private final PushServiceSocket pushServiceSocket; private final PushServiceSocket pushServiceSocket;
private final String user; private final String user;
/**
* Construct a TextSecureAccountManager.
*
* @param url The URL for the TextSecure server.
* @param trustStore The {@link org.whispersystems.textsecure.api.push.TrustStore} for the TextSecure server's TLS certificate.
* @param user A TextSecure phone number.
* @param password A TextSecure password.
*/
public TextSecureAccountManager(String url, TrustStore trustStore, public TextSecureAccountManager(String url, TrustStore trustStore,
String user, String password) String user, String password)
{ {
@ -50,6 +71,12 @@ public class TextSecureAccountManager {
this.user = user; this.user = user;
} }
/**
* Register/Unregister a Google Cloud Messaging registration ID.
*
* @param gcmRegistrationId The GCM id to register. A call with an absent value will unregister.
* @throws IOException
*/
public void setGcmId(Optional<String> gcmRegistrationId) throws IOException { public void setGcmId(Optional<String> gcmRegistrationId) throws IOException {
if (gcmRegistrationId.isPresent()) { if (gcmRegistrationId.isPresent()) {
this.pushServiceSocket.registerGcmId(gcmRegistrationId.get()); this.pushServiceSocket.registerGcmId(gcmRegistrationId.get());
@ -58,14 +85,42 @@ public class TextSecureAccountManager {
} }
} }
/**
* Request an SMS verification code. On success, the server will send
* an SMS verification code to this TextSecure user.
*
* @throws IOException
*/
public void requestSmsVerificationCode() throws IOException { public void requestSmsVerificationCode() throws IOException {
this.pushServiceSocket.createAccount(false); this.pushServiceSocket.createAccount(false);
} }
/**
* Request a Voice verification code. On success, the server will
* make a voice call to this TextSecure user.
*
* @throws IOException
*/
public void requestVoiceVerificationCode() throws IOException { public void requestVoiceVerificationCode() throws IOException {
this.pushServiceSocket.createAccount(true); this.pushServiceSocket.createAccount(true);
} }
/**
* Verify a TextSecure account.
*
* @param verificationCode The verification code received via SMS or Voice
* (see {@link #requestSmsVerificationCode} and
* {@link #requestVoiceVerificationCode}).
* @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key,
* concatenated.
* @param supportsSms Indicate whether this client is capable of supporting encrypted SMS.
* @param axolotlRegistrationId A random 14-bit number that identifies this TextSecure install.
* This value should remain consistent across registrations for the
* same install, but probabilistically differ across registrations
* for separate installs.
*
* @throws IOException
*/
public void verifyAccount(String verificationCode, String signalingKey, public void verifyAccount(String verificationCode, String signalingKey,
boolean supportsSms, int axolotlRegistrationId) boolean supportsSms, int axolotlRegistrationId)
throws IOException throws IOException
@ -74,6 +129,17 @@ public class TextSecureAccountManager {
supportsSms, axolotlRegistrationId); supportsSms, axolotlRegistrationId);
} }
/**
* Register an identity key, last resort key, signed prekey, and list of one time prekeys
* with the server.
*
* @param identityKey The client's long-term identity keypair.
* @param lastResortKey The client's "last resort" prekey.
* @param signedPreKey The client's signed prekey.
* @param oneTimePreKeys The client's list of one-time prekeys.
*
* @throws IOException
*/
public void setPreKeys(IdentityKey identityKey, PreKeyRecord lastResortKey, public void setPreKeys(IdentityKey identityKey, PreKeyRecord lastResortKey,
SignedPreKeyRecord signedPreKey, List<PreKeyRecord> oneTimePreKeys) SignedPreKeyRecord signedPreKey, List<PreKeyRecord> oneTimePreKeys)
throws IOException throws IOException
@ -81,26 +147,68 @@ public class TextSecureAccountManager {
this.pushServiceSocket.registerPreKeys(identityKey, lastResortKey, signedPreKey, oneTimePreKeys); this.pushServiceSocket.registerPreKeys(identityKey, lastResortKey, signedPreKey, oneTimePreKeys);
} }
/**
* @return The server's count of currently available (eg. unused) prekeys for this user.
* @throws IOException
*/
public int getPreKeysCount() throws IOException { public int getPreKeysCount() throws IOException {
return this.pushServiceSocket.getAvailablePreKeys(); return this.pushServiceSocket.getAvailablePreKeys();
} }
/**
* Set the client's signed prekey.
*
* @param signedPreKey The client's new signed prekey.
* @throws IOException
*/
public void setSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException { public void setSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException {
this.pushServiceSocket.setCurrentSignedPreKey(signedPreKey); this.pushServiceSocket.setCurrentSignedPreKey(signedPreKey);
} }
/**
* @return The server's view of the client's current signed prekey.
* @throws IOException
*/
public SignedPreKeyEntity getSignedPreKey() throws IOException { public SignedPreKeyEntity getSignedPreKey() throws IOException {
return this.pushServiceSocket.getCurrentSignedPreKey(); return this.pushServiceSocket.getCurrentSignedPreKey();
} }
public Optional<ContactTokenDetails> getContact(String contactToken) throws IOException { /**
return Optional.fromNullable(this.pushServiceSocket.getContactTokenDetails(contactToken)); * Checks whether a contact is currently registered with the server.
*
* @param e164number The contact to check.
* @return An optional ContactTokenDetails, present if registered, absent if not.
* @throws IOException
*/
public Optional<ContactTokenDetails> getContact(String e164number) throws IOException {
String contactToken = createDirectoryServerToken(e164number);
ContactTokenDetails contactTokenDetails = this.pushServiceSocket.getContactTokenDetails(contactToken);
if (contactTokenDetails != null) {
contactTokenDetails.setNumber(e164number);
}
return Optional.fromNullable(contactTokenDetails);
} }
public List<ContactTokenDetails> getContacts(Set<String> contactTokens) /**
* Checks which contacts in a set are registered with the server.
*
* @param e164numbers The contacts to check.
* @return A list of ContactTokenDetails for the registered users.
* @throws IOException
*/
public List<ContactTokenDetails> getContacts(Set<String> e164numbers)
throws IOException throws IOException
{ {
return this.pushServiceSocket.retrieveDirectory(contactTokens); Map<String, String> contactTokensMap = createDirectoryServerTokenMap(e164numbers);
List<ContactTokenDetails> activeTokens = this.pushServiceSocket.retrieveDirectory(contactTokensMap.keySet());
for (ContactTokenDetails activeToken : activeTokens) {
activeToken.setNumber(contactTokensMap.get(activeToken.getToken()));
}
return activeTokens;
} }
public String getNewDeviceVerificationCode() throws IOException { public String getNewDeviceVerificationCode() throws IOException {
@ -125,4 +233,24 @@ public class TextSecureAccountManager {
this.pushServiceSocket.sendProvisioningMessage(deviceIdentifier, ciphertext); this.pushServiceSocket.sendProvisioningMessage(deviceIdentifier, ciphertext);
} }
private String createDirectoryServerToken(String e164number) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA1");
byte[] token = Util.trim(digest.digest(e164number.getBytes()), 10);
return Base64.encodeBytesWithoutPadding(token);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private Map<String, String> createDirectoryServerTokenMap(Collection<String> e164numbers) {
Map<String,String> tokenMap = new HashMap<>(e164numbers.size());
for (String number : e164numbers) {
tokenMap.put(createDirectoryServerToken(number), number);
}
return tokenMap;
}
} }

View File

@ -12,24 +12,59 @@ import java.util.concurrent.TimeoutException;
import static org.whispersystems.textsecure.internal.websocket.WebSocketProtos.WebSocketRequestMessage; import static org.whispersystems.textsecure.internal.websocket.WebSocketProtos.WebSocketRequestMessage;
import static org.whispersystems.textsecure.internal.websocket.WebSocketProtos.WebSocketResponseMessage; import static org.whispersystems.textsecure.internal.websocket.WebSocketProtos.WebSocketResponseMessage;
/**
* A TextSecureMessagePipe represents a dedicated connection
* to the TextSecure server, which the server can push messages
* down.
*/
public class TextSecureMessagePipe { public class TextSecureMessagePipe {
private final WebSocketConnection websocket; private final WebSocketConnection websocket;
private final CredentialsProvider credentialsProvider; private final CredentialsProvider credentialsProvider;
public TextSecureMessagePipe(WebSocketConnection websocket, CredentialsProvider credentialsProvider) { TextSecureMessagePipe(WebSocketConnection websocket, CredentialsProvider credentialsProvider) {
this.websocket = websocket; this.websocket = websocket;
this.credentialsProvider = credentialsProvider; this.credentialsProvider = credentialsProvider;
this.websocket.connect(); this.websocket.connect();
} }
/**
* A blocking call that reads a message off the pipe. When this
* call returns, the message has been acknowledged and will not
* be retransmitted.
*
* @param timeout The timeout to wait for.
* @param unit The timeout time unit.
* @return A new message.
*
* @throws InvalidVersionException
* @throws IOException
* @throws TimeoutException
*/
public TextSecureEnvelope read(long timeout, TimeUnit unit) public TextSecureEnvelope read(long timeout, TimeUnit unit)
throws InvalidVersionException, IOException, TimeoutException throws InvalidVersionException, IOException, TimeoutException
{ {
return read(timeout, unit, new NullMessagePipeCallback()); return read(timeout, unit, new NullMessagePipeCallback());
} }
/**
* A blocking call that reads a message off the pipe (see {@link #read(long, java.util.concurrent.TimeUnit)}
*
* Unlike {@link #read(long, java.util.concurrent.TimeUnit)}, this method allows you
* to specify a callback that will be called before the received message is acknowledged.
* This allows you to write the received message to durable storage before acknowledging
* receipt of it to the server.
*
* @param timeout The timeout to wait for.
* @param unit The timeout time unit.
* @param callback A callback that will be called before the message receipt is
* acknowledged to the server.
* @return The message read (same as the message sent through the callback).
* @throws TimeoutException
* @throws IOException
* @throws InvalidVersionException
*/
public TextSecureEnvelope read(long timeout, TimeUnit unit, MessagePipeCallback callback) public TextSecureEnvelope read(long timeout, TimeUnit unit, MessagePipeCallback callback)
throws TimeoutException, IOException, InvalidVersionException throws TimeoutException, IOException, InvalidVersionException
{ {
@ -51,6 +86,9 @@ public class TextSecureMessagePipe {
} }
} }
/**
* Close this connection to the server.
*/
public void shutdown() { public void shutdown() {
websocket.disconnect(); websocket.disconnect();
} }
@ -75,6 +113,10 @@ public class TextSecureMessagePipe {
} }
} }
/**
* For receiving a callback when a new message has been
* received.
*/
public static interface MessagePipeCallback { public static interface MessagePipeCallback {
public void onMessage(TextSecureEnvelope envelope); public void onMessage(TextSecureEnvelope envelope);
} }

View File

@ -17,25 +17,23 @@
package org.whispersystems.textsecure.api; package org.whispersystems.textsecure.api;
import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.textsecure.api.crypto.AttachmentCipherInputStream; import org.whispersystems.textsecure.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer; import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
import org.whispersystems.textsecure.api.push.TrustStore; import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.api.util.CredentialsProvider; import org.whispersystems.textsecure.api.util.CredentialsProvider;
import org.whispersystems.textsecure.internal.push.PushServiceSocket; import org.whispersystems.textsecure.internal.push.PushServiceSocket;
import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider; import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider;
import org.whispersystems.textsecure.internal.websocket.WebSocketConnection; import org.whispersystems.textsecure.internal.websocket.WebSocketConnection;
import org.whispersystems.textsecure.internal.websocket.WebSocketProtos;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.whispersystems.textsecure.internal.websocket.WebSocketProtos.WebSocketRequestMessage;
/**
* The primary interface for receiving TextSecure messages.
*
* @author Moxie Marlinspike
*/
public class TextSecureMessageReceiver { public class TextSecureMessageReceiver {
private final PushServiceSocket socket; private final PushServiceSocket socket;
@ -43,12 +41,30 @@ public class TextSecureMessageReceiver {
private final String url; private final String url;
private final CredentialsProvider credentialsProvider; private final CredentialsProvider credentialsProvider;
/**
* Construct a TextSecureMessageReceiver.
*
* @param url The URL of the TextSecure server.
* @param trustStore The {@link org.whispersystems.textsecure.api.push.TrustStore} containing
* the server's TLS signing certificate.
* @param user The TextSecure user's username (eg. phone number).
* @param password The TextSecure user's password.
* @param signalingKey The 52 byte signaling key assigned to this user at registration.
*/
public TextSecureMessageReceiver(String url, TrustStore trustStore, public TextSecureMessageReceiver(String url, TrustStore trustStore,
String user, String password, String signalingKey) String user, String password, String signalingKey)
{ {
this(url, trustStore, new StaticCredentialsProvider(user, password, signalingKey)); this(url, trustStore, new StaticCredentialsProvider(user, password, signalingKey));
} }
/**
* Construct a TextSecureMessageReceiver.
*
* @param url The URL of the TextSecure server.
* @param trustStore The {@link org.whispersystems.textsecure.api.push.TrustStore} containing
* the server's TLS signing certificate.
* @param credentials The TextSecure user's credentials.
*/
public TextSecureMessageReceiver(String url, TrustStore trustStore, CredentialsProvider credentials) { public TextSecureMessageReceiver(String url, TrustStore trustStore, CredentialsProvider credentials) {
this.url = url; this.url = url;
this.trustStore = trustStore; this.trustStore = trustStore;
@ -56,6 +72,17 @@ public class TextSecureMessageReceiver {
this.socket = new PushServiceSocket(url, trustStore, credentials); this.socket = new PushServiceSocket(url, trustStore, credentials);
} }
/**
* Retrieves a TextSecure attachment.
*
* @param pointer The {@link org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer}
* received in a {@link org.whispersystems.textsecure.api.messages.TextSecureMessage}.
* @param destination The download destination for this attachment.
*
* @return An InputStream that streams the plaintext attachment contents.
* @throws IOException
* @throws InvalidMessageException
*/
public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File destination) public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File destination)
throws IOException, InvalidMessageException throws IOException, InvalidMessageException
{ {
@ -63,6 +90,13 @@ public class TextSecureMessageReceiver {
return new AttachmentCipherInputStream(destination, pointer.getKey()); return new AttachmentCipherInputStream(destination, pointer.getKey());
} }
/**
* Creates a pipe for receiving TextSecure messages.
*
* Callers must call {@link TextSecureMessagePipe#shutdown()} when finished with the pipe.
*
* @return A TextSecureMessagePipe for receiving TextSecure messages.
*/
public TextSecureMessagePipe createMessagePipe() { public TextSecureMessagePipe createMessagePipe() {
WebSocketConnection webSocket = new WebSocketConnection(url, trustStore, credentialsProvider); WebSocketConnection webSocket = new WebSocketConnection(url, trustStore, credentialsProvider);
return new TextSecureMessagePipe(webSocket, credentialsProvider); return new TextSecureMessagePipe(webSocket, credentialsProvider);

View File

@ -61,6 +61,11 @@ import static org.whispersystems.textsecure.internal.push.PushMessageProtos.Push
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.AttachmentPointer; import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.AttachmentPointer;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext; import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext;
/**
* The main interface for sending TextSecure messages.
*
* @author Moxie Marlinspike
*/
public class TextSecureMessageSender { public class TextSecureMessageSender {
private static final String TAG = TextSecureMessageSender.class.getSimpleName(); private static final String TAG = TextSecureMessageSender.class.getSimpleName();
@ -70,6 +75,18 @@ public class TextSecureMessageSender {
private final PushAddress syncAddress; private final PushAddress syncAddress;
private final Optional<EventListener> eventListener; private final Optional<EventListener> eventListener;
/**
* Construct a TextSecureMessageSender.
*
* @param url The URL of the TextSecure server.
* @param trustStore The trust store containing the TextSecure server's signing TLS certificate.
* @param user The TextSecure username (eg phone number).
* @param password The TextSecure user's password.
* @param userId The axolotl recipient id for the local TextSecure user.
* @param store The AxolotlStore.
* @param eventListener An optional event listener, which fires whenever sessions are
* setup or torn down for a recipient.
*/
public TextSecureMessageSender(String url, TrustStore trustStore, public TextSecureMessageSender(String url, TrustStore trustStore,
String user, String password, String user, String password,
long userId, AxolotlStore store, long userId, AxolotlStore store,
@ -81,10 +98,25 @@ public class TextSecureMessageSender {
this.eventListener = eventListener; this.eventListener = eventListener;
} }
/**
* Send a delivery receipt for a received message. It is not necessary to call this
* when receiving messages through {@link org.whispersystems.textsecure.api.TextSecureMessagePipe}.
* @param recipient The sender of the received message you're acknowledging.
* @param messageId The message id of the received message you're acknowledging.
* @throws IOException
*/
public void sendDeliveryReceipt(PushAddress recipient, long messageId) throws IOException { public void sendDeliveryReceipt(PushAddress recipient, long messageId) throws IOException {
this.socket.sendReceipt(recipient.getNumber(), messageId, recipient.getRelay()); this.socket.sendReceipt(recipient.getNumber(), messageId, recipient.getRelay());
} }
/**
* Send a message to a single recipient.
*
* @param recipient The message's destination.
* @param message The message.
* @throws UntrustedIdentityException
* @throws IOException
*/
public void sendMessage(PushAddress recipient, TextSecureMessage message) public void sendMessage(PushAddress recipient, TextSecureMessage message)
throws UntrustedIdentityException, IOException throws UntrustedIdentityException, IOException
{ {
@ -106,6 +138,14 @@ public class TextSecureMessageSender {
} }
} }
/**
* Send a message to a group.
*
* @param recipients The group members.
* @param message The group message.
* @throws IOException
* @throws EncapsulatedExceptions
*/
public void sendMessage(List<PushAddress> recipients, TextSecureMessage message) public void sendMessage(List<PushAddress> recipients, TextSecureMessage message)
throws IOException, EncapsulatedExceptions throws IOException, EncapsulatedExceptions
{ {

View File

@ -44,6 +44,11 @@ import java.util.List;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent; import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext.Type.DELIVER; import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext.Type.DELIVER;
/**
* This is used to decrypt received {@link org.whispersystems.textsecure.api.messages.TextSecureEnvelope}s.
*
* @author Moxie Marlinspike
*/
public class TextSecureCipher { public class TextSecureCipher {
private final SessionCipher sessionCipher; private final SessionCipher sessionCipher;
@ -57,6 +62,20 @@ public class TextSecureCipher {
return sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage)); return sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
} }
/**
* Decrypt a received {@link org.whispersystems.textsecure.api.messages.TextSecureEnvelope}
*
* @param envelope The received TextSecureEnvelope
* @return a decrypted TextSecureMessage
* @throws InvalidVersionException
* @throws InvalidMessageException
* @throws InvalidKeyException
* @throws DuplicateMessageException
* @throws InvalidKeyIdException
* @throws UntrustedIdentityException
* @throws LegacyMessageException
* @throws NoSessionException
*/
public TextSecureMessage decrypt(TextSecureEnvelope envelope) public TextSecureMessage decrypt(TextSecureEnvelope envelope)
throws InvalidVersionException, InvalidMessageException, InvalidKeyException, throws InvalidVersionException, InvalidMessageException, InvalidKeyException,
DuplicateMessageException, InvalidKeyIdException, UntrustedIdentityException, DuplicateMessageException, InvalidKeyIdException, UntrustedIdentityException,

View File

@ -18,6 +18,13 @@ package org.whispersystems.textsecure.api.messages;
import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.libaxolotl.util.guava.Optional;
/**
* Represents a received TextSecureMessage attachment "handle." This
* is a pointer to the actual attachment content, which needs to be
* retrieved using {@link org.whispersystems.textsecure.api.TextSecureMessageReceiver#retrieveAttachment(TextSecureAttachmentPointer, java.io.File)}
*
* @author Moxie Marlinspike
*/
public class TextSecureAttachmentPointer extends TextSecureAttachment { public class TextSecureAttachmentPointer extends TextSecureAttachment {
private final long id; private final long id;

View File

@ -18,6 +18,9 @@ package org.whispersystems.textsecure.api.messages;
import java.io.InputStream; import java.io.InputStream;
/**
* Represents a local TextSecureAttachment to be sent.
*/
public class TextSecureAttachmentStream extends TextSecureAttachment { public class TextSecureAttachmentStream extends TextSecureAttachment {
private final InputStream inputStream; private final InputStream inputStream;

View File

@ -39,6 +39,14 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
/**
* This class represents an encrypted TextSecure envelope.
*
* The envelope contains the wrapping information, such as the sender, the
* message timestamp, the encrypted message type, etc.
*
* @author Moxie Marlinspike
*/
public class TextSecureEnvelope { public class TextSecureEnvelope {
private static final String TAG = TextSecureEnvelope.class.getSimpleName(); private static final String TAG = TextSecureEnvelope.class.getSimpleName();
@ -56,12 +64,29 @@ public class TextSecureEnvelope {
private final IncomingPushMessageSignal signal; private final IncomingPushMessageSignal signal;
/**
* Construct an envelope from a serialized, Base64 encoded TextSecureEnvelope, encrypted
* with a signaling key.
*
* @param message The serialized TextSecureEnvelope, base64 encoded and encrypted.
* @param signalingKey The signaling key.
* @throws IOException
* @throws InvalidVersionException
*/
public TextSecureEnvelope(String message, String signalingKey) public TextSecureEnvelope(String message, String signalingKey)
throws IOException, InvalidVersionException throws IOException, InvalidVersionException
{ {
this(Base64.decode(message), signalingKey); this(Base64.decode(message), signalingKey);
} }
/**
* Construct an envelope from a serialized TextSecureEnvelope, encrypted with a signaling key.
*
* @param ciphertext The serialized and encrypted TextSecureEnvelope.
* @param signalingKey The signaling key.
* @throws InvalidVersionException
* @throws IOException
*/
public TextSecureEnvelope(byte[] ciphertext, String signalingKey) public TextSecureEnvelope(byte[] ciphertext, String signalingKey)
throws InvalidVersionException, IOException throws InvalidVersionException, IOException
{ {
@ -89,42 +114,72 @@ public class TextSecureEnvelope {
.build(); .build();
} }
/**
* @return The envelope's sender.
*/
public String getSource() { public String getSource() {
return signal.getSource(); return signal.getSource();
} }
/**
* @return The envelope's sender device ID.
*/
public int getSourceDevice() { public int getSourceDevice() {
return signal.getSourceDevice(); return signal.getSourceDevice();
} }
/**
* @return The envelope content type.
*/
public int getType() { public int getType() {
return signal.getType().getNumber(); return signal.getType().getNumber();
} }
/**
* @return The federated server this envelope came from.
*/
public String getRelay() { public String getRelay() {
return signal.getRelay(); return signal.getRelay();
} }
/**
* @return The timestamp this envelope was sent.
*/
public long getTimestamp() { public long getTimestamp() {
return signal.getTimestamp(); return signal.getTimestamp();
} }
/**
* @return The envelope's containing message.
*/
public byte[] getMessage() { public byte[] getMessage() {
return signal.getMessage().toByteArray(); return signal.getMessage().toByteArray();
} }
/**
* @return true if the containing message is a {@link org.whispersystems.libaxolotl.protocol.WhisperMessage}
*/
public boolean isWhisperMessage() { public boolean isWhisperMessage() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE; return signal.getType().getNumber() == IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE;
} }
/**
* @return true if the containing message is a {@link org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage}
*/
public boolean isPreKeyWhisperMessage() { public boolean isPreKeyWhisperMessage() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE; return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE;
} }
/**
* @return true if the containing message is plaintext.
*/
public boolean isPlaintext() { public boolean isPlaintext() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PLAINTEXT_VALUE; return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PLAINTEXT_VALUE;
} }
/**
* @return true if the containing message is a delivery receipt.
*/
public boolean isReceipt() { public boolean isReceipt() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.RECEIPT_VALUE; return signal.getType().getNumber() == IncomingPushMessageSignal.Type.RECEIPT_VALUE;
} }

View File

@ -20,6 +20,19 @@ import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.List; import java.util.List;
/**
* Group information to include in TextSecureMessages destined to groups.
*
* This class represents a "context" that is included with textsecure messages
* to make them group messages. There are three types of context:
*
* 1) Update -- Sent when either creating a group, or updating the properties
* of a group (such as the avatar icon, membership list, or title).
* 2) Deliver -- Sent when a message is to be delivered to an existing group.
* 3) Quit -- Sent when the sender wishes to leave an existing group.
*
* @author Moxie Marlinspike
*/
public class TextSecureGroup { public class TextSecureGroup {
public enum Type { public enum Type {
@ -36,10 +49,22 @@ public class TextSecureGroup {
private final Optional<TextSecureAttachment> avatar; private final Optional<TextSecureAttachment> avatar;
/**
* Construct a DELIVER group context.
* @param groupId
*/
public TextSecureGroup(byte[] groupId) { public TextSecureGroup(byte[] groupId) {
this(Type.DELIVER, groupId, null, null, null); this(Type.DELIVER, groupId, null, null, null);
} }
/**
* Construct a group context.
* @param type The group message type (update, deliver, quit).
* @param groupId The group ID.
* @param name The group title.
* @param members The group membership list.
* @param avatar The group avatar icon.
*/
public TextSecureGroup(Type type, byte[] groupId, String name, public TextSecureGroup(Type type, byte[] groupId, String name,
List<String> members, List<String> members,
TextSecureAttachment avatar) TextSecureAttachment avatar)

View File

@ -20,6 +20,9 @@ import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.List; import java.util.List;
/**
* Represents a decrypted text secure message.
*/
public class TextSecureMessage { public class TextSecureMessage {
private final long timestamp; private final long timestamp;
@ -29,18 +32,49 @@ public class TextSecureMessage {
private final boolean secure; private final boolean secure;
private final boolean endSession; private final boolean endSession;
/**
* Construct a TextSecureMessage with a body and no attachments.
*
* @param timestamp The sent timestamp.
* @param body The message contents.
*/
public TextSecureMessage(long timestamp, String body) { public TextSecureMessage(long timestamp, String body) {
this(timestamp, null, body); this(timestamp, null, body);
} }
/**
* Construct a TextSecureMessage with a body and list of attachments.
*
* @param timestamp The sent timestamp.
* @param attachments The attachments.
* @param body The message contents.
*/
public TextSecureMessage(long timestamp, List<TextSecureAttachment> attachments, String body) { public TextSecureMessage(long timestamp, List<TextSecureAttachment> attachments, String body) {
this(timestamp, null, attachments, body); this(timestamp, null, attachments, body);
} }
/**
* Construct a TextSecure group message with attachments and body.
*
* @param timestamp The sent timestamp.
* @param group The group information.
* @param attachments The attachments.
* @param body The message contents.
*/
public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body) { public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body) {
this(timestamp, group, attachments, body, true, false); this(timestamp, group, attachments, body, true, false);
} }
/**
* Construct a TextSecureMessage.
*
* @param timestamp The sent timestamp.
* @param group The group information (or null if none).
* @param attachments The attachments (or null if none).
* @param body The message contents.
* @param secure Flag indicating whether this message is to be encrypted.
* @param endSession Flag indicating whether this message should close a session.
*/
public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body, boolean secure, boolean endSession) { public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body, boolean secure, boolean endSession) {
this.timestamp = timestamp; this.timestamp = timestamp;
this.body = Optional.fromNullable(body); this.body = Optional.fromNullable(body);
@ -55,18 +89,30 @@ public class TextSecureMessage {
} }
} }
/**
* @return The message timestamp.
*/
public long getTimestamp() { public long getTimestamp() {
return timestamp; return timestamp;
} }
/**
* @return The message attachments (if any).
*/
public Optional<List<TextSecureAttachment>> getAttachments() { public Optional<List<TextSecureAttachment>> getAttachments() {
return attachments; return attachments;
} }
/**
* @return The message body (if any).
*/
public Optional<String> getBody() { public Optional<String> getBody() {
return body; return body;
} }
/**
* @return The message group info (if any).
*/
public Optional<TextSecureGroup> getGroupInfo() { public Optional<TextSecureGroup> getGroupInfo() {
return group; return group;
} }

View File

@ -16,6 +16,9 @@
*/ */
package org.whispersystems.textsecure.api.push; package org.whispersystems.textsecure.api.push;
/**
* A class that represents a contact's registration state.
*/
public class ContactTokenDetails { public class ContactTokenDetails {
private String token; private String token;
@ -25,14 +28,23 @@ public class ContactTokenDetails {
public ContactTokenDetails() {} public ContactTokenDetails() {}
/**
* @return The "anonymized" token (truncated hash) that's transmitted to the server.
*/
public String getToken() { public String getToken() {
return token; return token;
} }
/**
* @return The federated server this contact is registered with, or null if on your server.
*/
public String getRelay() { public String getRelay() {
return relay; return relay;
} }
/**
* @return Whether this contact supports receiving encrypted SMS.
*/
public boolean isSupportsSms() { public boolean isSupportsSms() {
return supportsSms; return supportsSms;
} }
@ -41,6 +53,9 @@ public class ContactTokenDetails {
this.number = number; this.number = number;
} }
/**
* @return This contact's username (e164 formatted number).
*/
public String getNumber() { public String getNumber() {
return number; return number;
} }

View File

@ -16,6 +16,9 @@
*/ */
package org.whispersystems.textsecure.api.push; package org.whispersystems.textsecure.api.push;
/**
* A class representing a message destination or origin.
*/
public class PushAddress { public class PushAddress {
public static final int DEFAULT_DEVICE_ID = 1; public static final int DEFAULT_DEVICE_ID = 1;
@ -24,6 +27,13 @@ public class PushAddress {
private final String e164number; private final String e164number;
private final String relay; private final String relay;
/**
* Construct a PushAddress.
*
* @param recipientId The axolotl recipient ID of this destination.
* @param e164number The TextSecure username of this destination (eg e164 representation of a phone number).
* @param relay The TextSecure federated server this user is registered with (if not your own server).
*/
public PushAddress(long recipientId, String e164number, String relay) { public PushAddress(long recipientId, String e164number, String relay) {
this.recipientId = recipientId; this.recipientId = recipientId;
this.e164number = e164number; this.e164number = e164number;

View File

@ -18,6 +18,10 @@ package org.whispersystems.textsecure.api.push;
import java.io.InputStream; import java.io.InputStream;
/**
* A class that represents a Java {@link java.security.KeyStore} and
* its associated password.
*/
public interface TrustStore { public interface TrustStore {
public InputStream getKeyStoreInputStream(); public InputStream getKeyStoreInputStream();
public String getKeyStorePassword(); public String getKeyStorePassword();

View File

@ -50,6 +50,13 @@ public class Util {
return parts; return parts;
} }
public static byte[] trim(byte[] input, int length) {
byte[] result = new byte[length];
System.arraycopy(input, 0, result, 0, result.length);
return result;
}
public static boolean isEmpty(String value) { public static boolean isEmpty(String value) {
return value == null || value.trim().length() == 0; return value == null || value.trim().length() == 0;
} }

View File

@ -118,8 +118,7 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
messageSender.sendMessage(address, new TextSecureMessage(message.getDateSent(), null, messageSender.sendMessage(address, new TextSecureMessage(message.getDateSent(), null,
null, null, true, true)); null, null, true, true));
} else { } else {
messageSender.sendMessage(address, new TextSecureMessage(message.getDateSent(), null, messageSender.sendMessage(address, new TextSecureMessage(message.getDateSent(), message.getBody().getBody()));
message.getBody().getBody()));
} }
return true; return true;

View File

@ -45,7 +45,6 @@ import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.TextSecureAccountManager; import org.whispersystems.textsecure.api.TextSecureAccountManager;
import org.whispersystems.textsecure.api.push.ContactTokenDetails; import org.whispersystems.textsecure.api.push.ContactTokenDetails;
import org.thoughtcrime.securesms.util.DirectoryUtil;
import org.whispersystems.textsecure.api.util.InvalidNumberException; import org.whispersystems.textsecure.api.util.InvalidNumberException;
import java.io.IOException; import java.io.IOException;
@ -283,8 +282,7 @@ public class MessageSender {
} catch (NotInDirectoryException e) { } catch (NotInDirectoryException e) {
try { try {
TextSecureAccountManager accountManager = TextSecureCommunicationFactory.createManager(context); TextSecureAccountManager accountManager = TextSecureCommunicationFactory.createManager(context);
String contactToken = DirectoryUtil.getDirectoryServerToken(destination); Optional<ContactTokenDetails> registeredUser = accountManager.getContact(destination);
Optional<ContactTokenDetails> registeredUser = accountManager.getContact(contactToken);
if (!registeredUser.isPresent()) { if (!registeredUser.isPresent()) {
registeredUser = Optional.of(new ContactTokenDetails()); registeredUser = Optional.of(new ContactTokenDetails());

View File

@ -15,7 +15,6 @@ import org.whispersystems.textsecure.api.util.InvalidNumberException;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
public class DirectoryHelper { public class DirectoryHelper {
@ -67,13 +66,12 @@ public class DirectoryHelper {
{ {
TextSecureDirectory directory = TextSecureDirectory.getInstance(context); TextSecureDirectory directory = TextSecureDirectory.getInstance(context);
Set<String> eligibleContactNumbers = directory.getPushEligibleContactNumbers(localNumber); Set<String> eligibleContactNumbers = directory.getPushEligibleContactNumbers(localNumber);
Map<String, String> tokenMap = DirectoryUtil.getDirectoryServerTokenMap(eligibleContactNumbers); List<ContactTokenDetails> activeTokens = accountManager.getContacts(eligibleContactNumbers);
List<ContactTokenDetails> activeTokens = accountManager.getContacts(tokenMap.keySet());
if (activeTokens != null) { if (activeTokens != null) {
for (ContactTokenDetails activeToken : activeTokens) { for (ContactTokenDetails activeToken : activeTokens) {
eligibleContactNumbers.remove(tokenMap.get(activeToken.getToken())); eligibleContactNumbers.remove(activeToken.getNumber());
activeToken.setNumber(tokenMap.get(activeToken.getToken())); activeToken.setNumber(activeToken.getNumber());
} }
directory.setNumbers(activeTokens, eligibleContactNumbers); directory.setNumbers(activeTokens, eligibleContactNumbers);

View File

@ -1,51 +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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class DirectoryUtil {
public static String getDirectoryServerToken(String e164number) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA1");
byte[] token = Util.trim(digest.digest(e164number.getBytes()), 10);
return Base64.encodeBytesWithoutPadding(token);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
/**
* Get a mapping of directory server tokens to their requested number.
* @param e164numbers
* @return map with token as key, E164 number as value
*/
public static Map<String, String> getDirectoryServerTokenMap(Collection<String> e164numbers) {
final Map<String,String> tokenMap = new HashMap<String,String>(e164numbers.size());
for (String number : e164numbers) {
tokenMap.put(getDirectoryServerToken(number), number);
}
return tokenMap;
}
}