mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-28 15:57:47 +00:00
Beginning of libtextsecure refactor.
1) Break out appropriate components. 2) Switch the incoming pipeline from SendReceiveService to the JobManager.
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
package org.whispersystems.textsecure.api;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.whispersystems.libaxolotl.DuplicateMessageException;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyIdException;
|
||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||
import org.whispersystems.libaxolotl.InvalidVersionException;
|
||||
import org.whispersystems.libaxolotl.LegacyMessageException;
|
||||
import org.whispersystems.libaxolotl.NoSessionException;
|
||||
import org.whispersystems.libaxolotl.UntrustedIdentityException;
|
||||
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
|
||||
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
|
||||
import org.whispersystems.libaxolotl.state.AxolotlStore;
|
||||
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
|
||||
import org.whispersystems.textsecure.crypto.AttachmentCipherInputStream;
|
||||
import org.whispersystems.textsecure.crypto.TextSecureCipher;
|
||||
import org.whispersystems.textsecure.push.IncomingEncryptedPushMessage;
|
||||
import org.whispersystems.textsecure.push.IncomingPushMessage;
|
||||
import org.whispersystems.textsecure.push.PushAddress;
|
||||
import org.whispersystems.textsecure.push.PushServiceSocket;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
|
||||
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.AttachmentPointer;
|
||||
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext.Type.DELIVER;
|
||||
|
||||
public class TextSecureMessageReceiver {
|
||||
|
||||
private final String signalingKey;
|
||||
private final AxolotlStore axolotlStore;
|
||||
private final PushServiceSocket socket;
|
||||
|
||||
|
||||
public TextSecureMessageReceiver(Context context, String signalingKey, String url,
|
||||
PushServiceSocket.TrustStore trustStore,
|
||||
String user, String password,
|
||||
AxolotlStore axolotlStore)
|
||||
{
|
||||
this.axolotlStore = axolotlStore;
|
||||
this.signalingKey = signalingKey;
|
||||
this.socket = new PushServiceSocket(context, url, trustStore, user, password);
|
||||
}
|
||||
|
||||
public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File destination)
|
||||
throws IOException, InvalidMessageException
|
||||
{
|
||||
socket.retrieveAttachment(pointer.getRelay().orNull(), pointer.getId(), destination);
|
||||
return new AttachmentCipherInputStream(destination, pointer.getKey());
|
||||
}
|
||||
|
||||
public IncomingPushMessage receiveSignal(String signal)
|
||||
throws IOException, InvalidVersionException
|
||||
{
|
||||
IncomingEncryptedPushMessage encrypted = new IncomingEncryptedPushMessage(signal, signalingKey);
|
||||
return encrypted.getIncomingPushMessage();
|
||||
}
|
||||
|
||||
public TextSecureMessage receiveMessage(long recipientId, IncomingPushMessage signal)
|
||||
throws InvalidVersionException, InvalidMessageException, NoSessionException,
|
||||
LegacyMessageException, InvalidKeyIdException, DuplicateMessageException,
|
||||
InvalidKeyException, UntrustedIdentityException
|
||||
{
|
||||
try {
|
||||
PushAddress sender = new PushAddress(recipientId, signal.getSource(), signal.getSourceDevice(), signal.getRelay());
|
||||
TextSecureCipher cipher = new TextSecureCipher(axolotlStore, sender);
|
||||
|
||||
PushMessageContent message;
|
||||
|
||||
if (signal.isPreKeyBundle()) {
|
||||
PreKeyWhisperMessage bundle = new PreKeyWhisperMessage(signal.getBody());
|
||||
message = PushMessageContent.parseFrom(cipher.decrypt(bundle));
|
||||
} else if (signal.isSecureMessage()) {
|
||||
WhisperMessage ciphertext = new WhisperMessage(signal.getBody());
|
||||
message = PushMessageContent.parseFrom(cipher.decrypt(ciphertext));
|
||||
} else if (signal.isPlaintext()) {
|
||||
message = PushMessageContent.parseFrom(signal.getBody());
|
||||
} else {
|
||||
throw new InvalidMessageException("Unknown type: " + signal.getType());
|
||||
}
|
||||
|
||||
return createTextSecureMessage(signal, message);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private TextSecureMessage createTextSecureMessage(IncomingPushMessage signal, PushMessageContent content) {
|
||||
TextSecureGroup groupInfo = createGroupInfo(signal, content);
|
||||
List<TextSecureAttachment> attachments = new LinkedList<>();
|
||||
boolean endSession = ((content.getFlags() & PushMessageContent.Flags.END_SESSION_VALUE) != 0);
|
||||
boolean secure = signal.isSecureMessage() || signal.isPreKeyBundle();
|
||||
|
||||
for (AttachmentPointer pointer : content.getAttachmentsList()) {
|
||||
attachments.add(new TextSecureAttachmentPointer(pointer.getId(),
|
||||
pointer.getContentType(),
|
||||
pointer.getKey().toByteArray(),
|
||||
signal.getRelay()));
|
||||
}
|
||||
|
||||
return new TextSecureMessage(signal.getTimestampMillis(), groupInfo, attachments,
|
||||
content.getBody(), secure, endSession);
|
||||
}
|
||||
|
||||
private TextSecureGroup createGroupInfo(IncomingPushMessage signal, PushMessageContent content) {
|
||||
if (!content.hasGroup()) return null;
|
||||
|
||||
TextSecureGroup.Type type;
|
||||
|
||||
switch (content.getGroup().getType()) {
|
||||
case DELIVER: type = TextSecureGroup.Type.DELIVER; break;
|
||||
case UPDATE: type = TextSecureGroup.Type.UPDATE; break;
|
||||
case QUIT: type = TextSecureGroup.Type.QUIT; break;
|
||||
default: type = TextSecureGroup.Type.UNKNOWN; break;
|
||||
}
|
||||
|
||||
if (content.getGroup().getType() != DELIVER) {
|
||||
String name = null;
|
||||
List<String> members = null;
|
||||
TextSecureAttachmentPointer avatar = null;
|
||||
|
||||
if (content.getGroup().hasName()) {
|
||||
name = content.getGroup().getName();
|
||||
}
|
||||
|
||||
if (content.getGroup().getMembersCount() > 0) {
|
||||
members = content.getGroup().getMembersList();
|
||||
}
|
||||
|
||||
if (content.getGroup().hasAvatar()) {
|
||||
avatar = new TextSecureAttachmentPointer(content.getGroup().getAvatar().getId(),
|
||||
content.getGroup().getAvatar().getContentType(),
|
||||
content.getGroup().getAvatar().getKey().toByteArray(),
|
||||
signal.getRelay());
|
||||
}
|
||||
|
||||
return new TextSecureGroup(type, content.getGroup().getId().toByteArray(), name, members, avatar);
|
||||
}
|
||||
|
||||
return new TextSecureGroup(content.getGroup().getId().toByteArray());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package org.whispersystems.textsecure.api;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
import org.whispersystems.libaxolotl.SessionBuilder;
|
||||
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;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentStream;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
|
||||
import org.whispersystems.textsecure.crypto.TextSecureCipher;
|
||||
import org.whispersystems.textsecure.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.textsecure.push.MismatchedDevices;
|
||||
import org.whispersystems.textsecure.push.OutgoingPushMessage;
|
||||
import org.whispersystems.textsecure.push.OutgoingPushMessageList;
|
||||
import org.whispersystems.textsecure.push.PushAddress;
|
||||
import org.whispersystems.textsecure.push.PushAttachmentData;
|
||||
import org.whispersystems.textsecure.push.PushBody;
|
||||
import org.whispersystems.textsecure.push.PushServiceSocket;
|
||||
import org.whispersystems.textsecure.push.StaleDevices;
|
||||
import org.whispersystems.textsecure.push.UnregisteredUserException;
|
||||
import org.whispersystems.textsecure.push.exceptions.EncapsulatedExceptions;
|
||||
import org.whispersystems.textsecure.push.exceptions.MismatchedDevicesException;
|
||||
import org.whispersystems.textsecure.push.exceptions.StaleDevicesException;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal.Type;
|
||||
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
|
||||
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.AttachmentPointer;
|
||||
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
|
||||
|
||||
public class TextSecureMessageSender {
|
||||
|
||||
private static final String TAG = TextSecureMessageSender.class.getSimpleName();
|
||||
|
||||
private final PushServiceSocket socket;
|
||||
private final AxolotlStore store;
|
||||
private final Optional<EventListener> eventListener;
|
||||
|
||||
public TextSecureMessageSender(Context context, String url,
|
||||
PushServiceSocket.TrustStore trustStore,
|
||||
String user, String password,
|
||||
AxolotlStore store,
|
||||
Optional<EventListener> eventListener)
|
||||
{
|
||||
this.socket = new PushServiceSocket(context, url, trustStore, user, password);
|
||||
this.store = store;
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
|
||||
public void sendMessage(PushAddress recipient, TextSecureMessage message)
|
||||
throws UntrustedIdentityException, IOException
|
||||
{
|
||||
byte[] content = createMessageContent(message);
|
||||
sendMessage(recipient, message.getTimestamp(), content);
|
||||
|
||||
if (message.isEndSession()) {
|
||||
store.deleteAllSessions(recipient.getRecipientId());
|
||||
|
||||
if (eventListener.isPresent()) {
|
||||
eventListener.get().onSecurityEvent(recipient.getRecipientId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMessage(List<PushAddress> recipients, TextSecureMessage message)
|
||||
throws IOException, EncapsulatedExceptions
|
||||
{
|
||||
byte[] content = createMessageContent(message);
|
||||
sendMessage(recipients, message.getTimestamp(), content);
|
||||
}
|
||||
|
||||
private byte[] createMessageContent(TextSecureMessage message) throws IOException {
|
||||
PushMessageContent.Builder builder = PushMessageContent.newBuilder();
|
||||
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments());
|
||||
|
||||
if (!pointers.isEmpty()) {
|
||||
builder.addAllAttachments(pointers);
|
||||
}
|
||||
|
||||
if (message.getBody().isPresent()) {
|
||||
builder.setBody(message.getBody().get());
|
||||
}
|
||||
|
||||
if (message.getGroupInfo().isPresent()) {
|
||||
builder.setGroup(createGroupContent(message.getGroupInfo().get()));
|
||||
}
|
||||
|
||||
if (message.isEndSession()) {
|
||||
builder.setFlags(PushMessageContent.Flags.END_SESSION_VALUE);
|
||||
}
|
||||
|
||||
return builder.build().toByteArray();
|
||||
}
|
||||
|
||||
private GroupContext createGroupContent(TextSecureGroup group) throws IOException {
|
||||
GroupContext.Builder builder = GroupContext.newBuilder();
|
||||
builder.setId(ByteString.copyFrom(group.getGroupId()));
|
||||
|
||||
if (group.getType() != TextSecureGroup.Type.DELIVER) {
|
||||
if (group.getType() == TextSecureGroup.Type.UPDATE) builder.setType(GroupContext.Type.UPDATE);
|
||||
else if (group.getType() == TextSecureGroup.Type.QUIT) builder.setType(GroupContext.Type.QUIT);
|
||||
else throw new AssertionError("Unknown type: " + group.getType());
|
||||
|
||||
if (group.getName().isPresent()) builder.setName(group.getName().get());
|
||||
if (group.getMembers().isPresent()) builder.addAllMembers(group.getMembers().get());
|
||||
|
||||
if (group.getAvatar().isPresent() && group.getAvatar().get().isStream()) {
|
||||
AttachmentPointer pointer = createAttachmentPointer(group.getAvatar().get().asStream());
|
||||
builder.setAvatar(pointer);
|
||||
}
|
||||
} else {
|
||||
builder.setType(GroupContext.Type.DELIVER);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void sendMessage(List<PushAddress> recipients, long timestamp, byte[] content)
|
||||
throws IOException, EncapsulatedExceptions
|
||||
{
|
||||
List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
|
||||
List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
|
||||
|
||||
for (PushAddress recipient : recipients) {
|
||||
try {
|
||||
sendMessage(recipient, timestamp, content);
|
||||
} catch (UntrustedIdentityException e) {
|
||||
Log.w(TAG, e);
|
||||
untrustedIdentities.add(e);
|
||||
} catch (UnregisteredUserException e) {
|
||||
Log.w(TAG, e);
|
||||
unregisteredUsers.add(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty()) {
|
||||
throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage(PushAddress recipient, long timestamp, byte[] content)
|
||||
throws UntrustedIdentityException, IOException
|
||||
{
|
||||
for (int i=0;i<3;i++) {
|
||||
try {
|
||||
OutgoingPushMessageList messages = getEncryptedMessages(socket, recipient, timestamp, content);
|
||||
socket.sendMessage(messages);
|
||||
|
||||
return;
|
||||
} catch (MismatchedDevicesException mde) {
|
||||
Log.w(TAG, mde);
|
||||
handleMismatchedDevices(socket, recipient, mde.getMismatchedDevices());
|
||||
} catch (StaleDevicesException ste) {
|
||||
Log.w(TAG, ste);
|
||||
handleStaleDevices(recipient, ste.getStaleDevices());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<AttachmentPointer> createAttachmentPointers(Optional<List<TextSecureAttachment>> attachments) throws IOException {
|
||||
List<AttachmentPointer> pointers = new LinkedList<>();
|
||||
|
||||
if (!attachments.isPresent() || attachments.get().isEmpty()) {
|
||||
return pointers;
|
||||
}
|
||||
|
||||
for (TextSecureAttachment attachment : attachments.get()) {
|
||||
if (attachment.isStream()) {
|
||||
pointers.add(createAttachmentPointer(attachment.asStream()));
|
||||
}
|
||||
}
|
||||
|
||||
return pointers;
|
||||
}
|
||||
|
||||
private AttachmentPointer createAttachmentPointer(TextSecureAttachmentStream attachment)
|
||||
throws IOException
|
||||
{
|
||||
byte[] attachmentKey = Util.getSecretBytes(64);
|
||||
PushAttachmentData attachmentData = new PushAttachmentData(attachment.getContentType(),
|
||||
attachment.getInputStream(),
|
||||
attachment.getLength(),
|
||||
attachmentKey);
|
||||
|
||||
long attachmentId = socket.sendAttachment(attachmentData);
|
||||
|
||||
return AttachmentPointer.newBuilder()
|
||||
.setContentType(attachment.getContentType())
|
||||
.setId(attachmentId)
|
||||
.setKey(ByteString.copyFrom(attachmentKey))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket,
|
||||
PushAddress recipient,
|
||||
long timestamp,
|
||||
byte[] plaintext)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
PushBody masterBody = getEncryptedMessage(socket, recipient, plaintext);
|
||||
|
||||
List<OutgoingPushMessage> messages = new LinkedList<>();
|
||||
messages.add(new OutgoingPushMessage(recipient, masterBody));
|
||||
|
||||
for (int deviceId : store.getSubDeviceSessions(recipient.getRecipientId())) {
|
||||
PushAddress device = new PushAddress(recipient.getRecipientId(), recipient.getNumber(), deviceId, recipient.getRelay());
|
||||
PushBody body = getEncryptedMessage(socket, device, plaintext);
|
||||
|
||||
messages.add(new OutgoingPushMessage(device, body));
|
||||
}
|
||||
|
||||
return new OutgoingPushMessageList(recipient.getNumber(), timestamp, recipient.getRelay(), messages);
|
||||
}
|
||||
|
||||
private PushBody getEncryptedMessage(PushServiceSocket socket, PushAddress recipient, byte[] plaintext)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
if (!store.containsSession(recipient.getRecipientId(), recipient.getDeviceId())) {
|
||||
try {
|
||||
List<PreKeyBundle> preKeys = socket.getPreKeys(recipient);
|
||||
|
||||
for (PreKeyBundle preKey : preKeys) {
|
||||
try {
|
||||
SessionBuilder sessionBuilder = new SessionBuilder(store, recipient.getRecipientId(), recipient.getDeviceId());
|
||||
sessionBuilder.process(preKey);
|
||||
} catch (org.whispersystems.libaxolotl.UntrustedIdentityException e) {
|
||||
throw new UntrustedIdentityException("Untrusted identity key!", recipient.getNumber(), preKey.getIdentityKey());
|
||||
}
|
||||
}
|
||||
|
||||
if (eventListener.isPresent()) {
|
||||
eventListener.get().onSecurityEvent(recipient.getRecipientId());
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
TextSecureCipher cipher = new TextSecureCipher(store, recipient);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMismatchedDevices(PushServiceSocket socket, PushAddress recipient,
|
||||
MismatchedDevices mismatchedDevices)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
try {
|
||||
for (int extraDeviceId : mismatchedDevices.getExtraDevices()) {
|
||||
store.deleteSession(recipient.getRecipientId(), extraDeviceId);
|
||||
}
|
||||
|
||||
for (int missingDeviceId : mismatchedDevices.getMissingDevices()) {
|
||||
PushAddress device = new PushAddress(recipient.getRecipientId(), recipient.getNumber(),
|
||||
missingDeviceId, recipient.getRelay());
|
||||
PreKeyBundle preKey = socket.getPreKey(device);
|
||||
|
||||
try {
|
||||
SessionBuilder sessionBuilder = new SessionBuilder(store, device.getRecipientId(), device.getDeviceId());
|
||||
sessionBuilder.process(preKey);
|
||||
} catch (org.whispersystems.libaxolotl.UntrustedIdentityException e) {
|
||||
throw new UntrustedIdentityException("Untrusted identity key!", recipient.getNumber(), preKey.getIdentityKey());
|
||||
}
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleStaleDevices(PushAddress recipient, StaleDevices staleDevices) {
|
||||
long recipientId = recipient.getRecipientId();
|
||||
|
||||
for (int staleDeviceId : staleDevices.getStaleDevices()) {
|
||||
store.deleteSession(recipientId, staleDeviceId);
|
||||
}
|
||||
}
|
||||
|
||||
public static interface EventListener {
|
||||
public void onSecurityEvent(long recipientId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.whispersystems.textsecure.api.messages;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public abstract class TextSecureAttachment {
|
||||
|
||||
private final String contentType;
|
||||
|
||||
protected TextSecureAttachment(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public abstract boolean isStream();
|
||||
public abstract boolean isPointer();
|
||||
|
||||
public TextSecureAttachmentStream asStream() {
|
||||
return (TextSecureAttachmentStream)this;
|
||||
}
|
||||
|
||||
public TextSecureAttachmentPointer asPointer() {
|
||||
return (TextSecureAttachmentPointer)this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.whispersystems.textsecure.api.messages;
|
||||
|
||||
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||
|
||||
public class TextSecureAttachmentPointer extends TextSecureAttachment {
|
||||
|
||||
private final long id;
|
||||
private final byte[] key;
|
||||
private final Optional<String> relay;
|
||||
|
||||
public TextSecureAttachmentPointer(long id, String contentType, byte[] key, String relay) {
|
||||
super(contentType);
|
||||
this.id = id;
|
||||
this.key = key;
|
||||
this.relay = Optional.fromNullable(relay);
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStream() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPointer() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public Optional<String> getRelay() {
|
||||
return relay;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.whispersystems.textsecure.api.messages;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class TextSecureAttachmentStream extends TextSecureAttachment {
|
||||
|
||||
private final InputStream inputStream;
|
||||
private final long length;
|
||||
|
||||
public TextSecureAttachmentStream(InputStream inputStream, String contentType, long length) {
|
||||
super(contentType);
|
||||
this.inputStream = inputStream;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStream() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPointer() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public InputStream getInputStream() {
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
public long getLength() {
|
||||
return length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.whispersystems.textsecure.api.messages;
|
||||
|
||||
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TextSecureGroup {
|
||||
|
||||
public enum Type {
|
||||
UNKNOWN,
|
||||
UPDATE,
|
||||
DELIVER,
|
||||
QUIT
|
||||
}
|
||||
|
||||
private final byte[] groupId;
|
||||
private final Type type;
|
||||
private final Optional<String> name;
|
||||
private final Optional<List<String>> members;
|
||||
private final Optional<TextSecureAttachment> avatar;
|
||||
|
||||
|
||||
public TextSecureGroup(byte[] groupId) {
|
||||
this(Type.DELIVER, groupId, null, null, null);
|
||||
}
|
||||
|
||||
public TextSecureGroup(Type type, byte[] groupId, String name,
|
||||
List<String> members,
|
||||
TextSecureAttachment avatar)
|
||||
{
|
||||
this.type = type;
|
||||
this.groupId = groupId;
|
||||
this.name = Optional.fromNullable(name);
|
||||
this.members = Optional.fromNullable(members);
|
||||
this.avatar = Optional.fromNullable(avatar);
|
||||
}
|
||||
|
||||
public byte[] getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Optional<String> getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Optional<List<String>> getMembers() {
|
||||
return members;
|
||||
}
|
||||
|
||||
public Optional<TextSecureAttachment> getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.whispersystems.textsecure.api.messages;
|
||||
|
||||
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TextSecureMessage {
|
||||
|
||||
private final long timestamp;
|
||||
private final Optional<List<TextSecureAttachment>> attachments;
|
||||
private final Optional<String> body;
|
||||
private final Optional<TextSecureGroup> group;
|
||||
private final boolean secure;
|
||||
private final boolean endSession;
|
||||
|
||||
public TextSecureMessage(long timestamp, String body) {
|
||||
this(timestamp, null, body);
|
||||
}
|
||||
|
||||
public TextSecureMessage(long timestamp, List<TextSecureAttachment> attachments, String body) {
|
||||
this(timestamp, null, attachments, body);
|
||||
}
|
||||
|
||||
public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body) {
|
||||
this(timestamp, group, attachments, body, true, false);
|
||||
}
|
||||
|
||||
public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body, boolean secure, boolean endSession) {
|
||||
this.timestamp = timestamp;
|
||||
this.attachments = Optional.fromNullable(attachments);
|
||||
this.body = Optional.fromNullable(body);
|
||||
this.group = Optional.fromNullable(group);
|
||||
this.secure = secure;
|
||||
this.endSession = endSession;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public Optional<List<TextSecureAttachment>> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public Optional<String> getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public Optional<TextSecureGroup> getGroupInfo() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public boolean isSecure() {
|
||||
return secure;
|
||||
}
|
||||
|
||||
public boolean isEndSession() {
|
||||
return endSession;
|
||||
}
|
||||
|
||||
public boolean isGroupUpdate() {
|
||||
return group.isPresent() && group.get().getType() != TextSecureGroup.Type.DELIVER;
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2013 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.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.libaxolotl.InvalidMacException;
|
||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Encrypts push attachments.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class AttachmentCipher {
|
||||
|
||||
static final int CIPHER_KEY_SIZE = 32;
|
||||
static final int MAC_KEY_SIZE = 32;
|
||||
|
||||
private final SecretKeySpec cipherKey;
|
||||
private final SecretKeySpec macKey;
|
||||
private final Cipher cipher;
|
||||
private final Mac mac;
|
||||
|
||||
public AttachmentCipher() {
|
||||
this.cipherKey = initializeRandomCipherKey();
|
||||
this.macKey = initializeRandomMacKey();
|
||||
this.cipher = initializeCipher();
|
||||
this.mac = initializeMac();
|
||||
}
|
||||
|
||||
public AttachmentCipher(byte[] combinedKeyMaterial) {
|
||||
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
|
||||
this.cipherKey = new SecretKeySpec(parts[0], "AES");
|
||||
this.macKey = new SecretKeySpec(parts[1], "HmacSHA256");
|
||||
this.cipher = initializeCipher();
|
||||
this.mac = initializeMac();
|
||||
}
|
||||
|
||||
public byte[] getCombinedKeyMaterial() {
|
||||
return Util.combine(this.cipherKey.getEncoded(), this.macKey.getEncoded());
|
||||
}
|
||||
|
||||
public byte[] encrypt(byte[] plaintext) {
|
||||
try {
|
||||
this.cipher.init(Cipher.ENCRYPT_MODE, this.cipherKey);
|
||||
this.mac.init(this.macKey);
|
||||
|
||||
byte[] ciphertext = this.cipher.doFinal(plaintext);
|
||||
byte[] iv = this.cipher.getIV();
|
||||
byte[] mac = this.mac.doFinal(Util.combine(iv, ciphertext));
|
||||
|
||||
return Util.combine(iv, ciphertext, mac);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] ciphertext)
|
||||
throws InvalidMacException, InvalidMessageException
|
||||
{
|
||||
try {
|
||||
if (ciphertext.length <= cipher.getBlockSize() + mac.getMacLength()) {
|
||||
throw new InvalidMessageException("Message too short!");
|
||||
}
|
||||
|
||||
byte[][] ciphertextParts = Util.split(ciphertext,
|
||||
this.cipher.getBlockSize(),
|
||||
ciphertext.length - this.cipher.getBlockSize() - this.mac.getMacLength(),
|
||||
this.mac.getMacLength());
|
||||
|
||||
this.mac.update(ciphertext, 0, ciphertext.length - mac.getMacLength());
|
||||
byte[] ourMac = this.mac.doFinal();
|
||||
|
||||
if (!Arrays.equals(ourMac, ciphertextParts[2])) {
|
||||
throw new InvalidMacException("Mac doesn't match!");
|
||||
}
|
||||
|
||||
this.cipher.init(Cipher.DECRYPT_MODE, this.cipherKey,
|
||||
new IvParameterSpec(ciphertextParts[0]));
|
||||
|
||||
return cipher.doFinal(ciphertextParts[1]);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (ParseException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Mac initializeMac() {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
return mac;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Cipher initializeCipher() {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
return cipher;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKeySpec initializeRandomCipherKey() {
|
||||
byte[] key = new byte[CIPHER_KEY_SIZE];
|
||||
Util.getSecureRandom().nextBytes(key);
|
||||
return new SecretKeySpec(key, "AES");
|
||||
}
|
||||
|
||||
private SecretKeySpec initializeRandomMacKey() {
|
||||
byte[] key = new byte[MAC_KEY_SIZE];
|
||||
Util.getSecureRandom().nextBytes(key);
|
||||
return new SecretKeySpec(key, "HmacSHA256");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -48,13 +48,15 @@ import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class AttachmentCipherInputStream extends FileInputStream {
|
||||
|
||||
private static final int BLOCK_SIZE = 16;
|
||||
private static final int BLOCK_SIZE = 16;
|
||||
private static final int CIPHER_KEY_SIZE = 32;
|
||||
private static final int MAC_KEY_SIZE = 32;
|
||||
|
||||
private Cipher cipher;
|
||||
private boolean done;
|
||||
private long totalDataSize;
|
||||
private long totalRead;
|
||||
private byte[] overflowBuffer;
|
||||
private byte[] overflowBuffer;
|
||||
|
||||
public AttachmentCipherInputStream(File file, byte[] combinedKeyMaterial)
|
||||
throws IOException, InvalidMessageException
|
||||
@@ -62,11 +64,9 @@ public class AttachmentCipherInputStream extends FileInputStream {
|
||||
super(file);
|
||||
|
||||
try {
|
||||
byte[][] parts = Util.split(combinedKeyMaterial,
|
||||
AttachmentCipher.CIPHER_KEY_SIZE,
|
||||
AttachmentCipher.MAC_KEY_SIZE);
|
||||
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
|
||||
|
||||
if (file.length() <= BLOCK_SIZE + mac.getMacLength()) {
|
||||
@@ -84,16 +84,10 @@ public class AttachmentCipherInputStream extends FileInputStream {
|
||||
this.done = false;
|
||||
this.totalRead = 0;
|
||||
this.totalDataSize = file.length() - cipher.getBlockSize() - mac.getMacLength();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidMacException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class AttachmentCipherOutputStream extends OutputStream {
|
||||
|
||||
private final Cipher cipher;
|
||||
private final Mac mac;
|
||||
private final OutputStream outputStream;
|
||||
|
||||
private long ciphertextLength = 0;
|
||||
|
||||
public AttachmentCipherOutputStream(byte[] combinedKeyMaterial,
|
||||
OutputStream outputStream)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
this.outputStream = outputStream;
|
||||
this.cipher = initializeCipher();
|
||||
this.mac = initializeMac();
|
||||
|
||||
byte[][] keyParts = Util.split(combinedKeyMaterial, 32, 32);
|
||||
|
||||
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyParts[0], "AES"));
|
||||
this.mac.init(new SecretKeySpec(keyParts[1], "HmacSHA256"));
|
||||
|
||||
mac.update(cipher.getIV());
|
||||
outputStream.write(cipher.getIV());
|
||||
ciphertextLength += cipher.getIV().length;
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer) throws IOException {
|
||||
write(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int length) throws IOException {
|
||||
byte[] ciphertext = cipher.update(buffer, offset, length);
|
||||
|
||||
if (ciphertext != null) {
|
||||
mac.update(ciphertext);
|
||||
outputStream.write(ciphertext);
|
||||
ciphertextLength += ciphertext.length;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) {
|
||||
throw new AssertionError("NYI");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
try {
|
||||
byte[] ciphertext = cipher.doFinal();
|
||||
byte[] auth = mac.doFinal(ciphertext);
|
||||
|
||||
outputStream.write(ciphertext);
|
||||
outputStream.write(auth);
|
||||
|
||||
ciphertextLength += ciphertext.length;
|
||||
ciphertextLength += auth.length;
|
||||
|
||||
outputStream.flush();
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static long getCiphertextLength(long plaintextLength) {
|
||||
return 16 + (((plaintextLength / 16) +1) * 16) + 32;
|
||||
}
|
||||
|
||||
private Mac initializeMac() {
|
||||
try {
|
||||
return Mac.getInstance("HmacSHA256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Cipher initializeCipher() {
|
||||
try {
|
||||
return Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +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.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
|
||||
public class IdentityKeyParcelable implements Parcelable {
|
||||
|
||||
public static final Parcelable.Creator<IdentityKeyParcelable> CREATOR = new Parcelable.Creator<IdentityKeyParcelable>() {
|
||||
public IdentityKeyParcelable createFromParcel(Parcel in) {
|
||||
try {
|
||||
return new IdentityKeyParcelable(in);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public IdentityKeyParcelable[] newArray(int size) {
|
||||
return new IdentityKeyParcelable[size];
|
||||
}
|
||||
};
|
||||
|
||||
private final IdentityKey identityKey;
|
||||
|
||||
public IdentityKeyParcelable(IdentityKey identityKey) {
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public IdentityKeyParcelable(Parcel in) throws InvalidKeyException {
|
||||
int serializedLength = in.readInt();
|
||||
byte[] serialized = new byte[serializedLength];
|
||||
|
||||
in.readByteArray(serialized);
|
||||
this.identityKey = new IdentityKey(serialized, 0);
|
||||
}
|
||||
|
||||
public IdentityKey get() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(identityKey.serialize().length);
|
||||
dest.writeByteArray(identityKey.serialize());
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 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.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||
import org.whispersystems.libaxolotl.ecc.Curve;
|
||||
import org.whispersystems.libaxolotl.ecc.ECPrivateKey;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Class that handles encryption for local storage.
|
||||
*
|
||||
* The protocol format is roughly:
|
||||
*
|
||||
* 1) 16 byte random IV.
|
||||
* 2) AES-CBC(plaintext)
|
||||
* 3) HMAC-SHA1 of 1 and 2
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterCipher {
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
private final Cipher encryptingCipher;
|
||||
private final Cipher decryptingCipher;
|
||||
private final Mac hmac;
|
||||
|
||||
public MasterCipher(MasterSecret masterSecret) {
|
||||
try {
|
||||
this.masterSecret = masterSecret;
|
||||
this.encryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
this.decryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
this.hmac = Mac.getInstance("HmacSHA1");
|
||||
} catch (NoSuchPaddingException nspe) {
|
||||
throw new AssertionError(nspe);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encryptKey(ECPrivateKey privateKey) {
|
||||
return encryptBytes(privateKey.serialize());
|
||||
}
|
||||
|
||||
public String encryptBody(String body) {
|
||||
return encryptAndEncodeBytes(body.getBytes());
|
||||
}
|
||||
|
||||
public String decryptBody(String body) throws InvalidMessageException {
|
||||
return new String(decodeAndDecryptBytes(body));
|
||||
}
|
||||
|
||||
public ECPrivateKey decryptKey(byte[] key)
|
||||
throws org.whispersystems.libaxolotl.InvalidKeyException
|
||||
{
|
||||
try {
|
||||
return Curve.decodePrivatePoint(decryptBytes(key));
|
||||
} catch (InvalidMessageException ime) {
|
||||
throw new org.whispersystems.libaxolotl.InvalidKeyException(ime);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decryptBytes(byte[] decodedBody) throws InvalidMessageException {
|
||||
try {
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
byte[] encryptedBody = verifyMacBody(mac, decodedBody);
|
||||
|
||||
Cipher cipher = getDecryptingCipher(masterSecret.getEncryptionKey(), encryptedBody);
|
||||
byte[] encrypted = getDecryptedBody(cipher, encryptedBody);
|
||||
|
||||
return encrypted;
|
||||
} catch (GeneralSecurityException ge) {
|
||||
throw new InvalidMessageException(ge);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encryptBytes(byte[] body) {
|
||||
try {
|
||||
Cipher cipher = getEncryptingCipher(masterSecret.getEncryptionKey());
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
|
||||
byte[] encryptedBody = getEncryptedBody(cipher, body);
|
||||
byte[] encryptedAndMacBody = getMacBody(mac, encryptedBody);
|
||||
|
||||
return encryptedAndMacBody;
|
||||
} catch (GeneralSecurityException ge) {
|
||||
Log.w("bodycipher", ge);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean verifyMacFor(String content, byte[] theirMac) {
|
||||
byte[] ourMac = getMacFor(content);
|
||||
Log.w("MasterCipher", "Our Mac: " + Hex.toString(ourMac));
|
||||
Log.w("MasterCipher", "Thr Mac: " + Hex.toString(theirMac));
|
||||
return Arrays.equals(ourMac, theirMac);
|
||||
}
|
||||
|
||||
public byte[] getMacFor(String content) {
|
||||
Log.w("MasterCipher", "Macing: " + content);
|
||||
try {
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
return mac.doFinal(content.getBytes());
|
||||
} catch (GeneralSecurityException ike) {
|
||||
throw new AssertionError(ike);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decodeAndDecryptBytes(String body) throws InvalidMessageException {
|
||||
try {
|
||||
byte[] decodedBody = Base64.decode(body);
|
||||
return decryptBytes(decodedBody);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidMessageException("Bad Base64 Encoding...", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String encryptAndEncodeBytes(byte[] bytes) {
|
||||
byte[] encryptedAndMacBody = encryptBytes(bytes);
|
||||
return Base64.encodeBytes(encryptedAndMacBody);
|
||||
}
|
||||
|
||||
private byte[] verifyMacBody(Mac hmac, byte[] encryptedAndMac) throws InvalidMessageException {
|
||||
byte[] encrypted = new byte[encryptedAndMac.length - hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMac, 0, encrypted, 0, encrypted.length);
|
||||
|
||||
byte[] remoteMac = new byte[hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMac, encryptedAndMac.length - remoteMac.length, remoteMac, 0, remoteMac.length);
|
||||
|
||||
byte[] localMac = hmac.doFinal(encrypted);
|
||||
|
||||
if (!Arrays.equals(remoteMac, localMac))
|
||||
throw new InvalidMessageException("MAC doesen't match.");
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
private byte[] getDecryptedBody(Cipher cipher, byte[] encryptedBody) throws IllegalBlockSizeException, BadPaddingException {
|
||||
return cipher.doFinal(encryptedBody, cipher.getBlockSize(), encryptedBody.length - cipher.getBlockSize());
|
||||
}
|
||||
|
||||
private byte[] getEncryptedBody(Cipher cipher, byte[] body) throws IllegalBlockSizeException, BadPaddingException {
|
||||
byte[] encrypted = cipher.doFinal(body);
|
||||
byte[] iv = cipher.getIV();
|
||||
|
||||
byte[] ivAndBody = new byte[iv.length + encrypted.length];
|
||||
System.arraycopy(iv, 0, ivAndBody, 0, iv.length);
|
||||
System.arraycopy(encrypted, 0, ivAndBody, iv.length, encrypted.length);
|
||||
|
||||
return ivAndBody;
|
||||
}
|
||||
|
||||
private Mac getMac(SecretKeySpec key) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
// Mac hmac = Mac.getInstance("HmacSHA1");
|
||||
hmac.init(key);
|
||||
|
||||
return hmac;
|
||||
}
|
||||
|
||||
private byte[] getMacBody(Mac hmac, byte[] encryptedBody) {
|
||||
byte[] mac = hmac.doFinal(encryptedBody);
|
||||
byte[] encryptedAndMac = new byte[encryptedBody.length + mac.length];
|
||||
|
||||
System.arraycopy(encryptedBody, 0, encryptedAndMac, 0, encryptedBody.length);
|
||||
System.arraycopy(mac, 0, encryptedAndMac, encryptedBody.length, mac.length);
|
||||
|
||||
return encryptedAndMac;
|
||||
}
|
||||
|
||||
private Cipher getDecryptingCipher(SecretKeySpec key, byte[] encryptedBody) throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
IvParameterSpec iv = new IvParameterSpec(encryptedBody, 0, decryptingCipher.getBlockSize());
|
||||
decryptingCipher.init(Cipher.DECRYPT_MODE, key, iv);
|
||||
|
||||
return decryptingCipher;
|
||||
}
|
||||
|
||||
private Cipher getEncryptingCipher(SecretKeySpec key) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
encryptingCipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
|
||||
return encryptingCipher;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 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.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* When a user first initializes TextSecure, a few secrets
|
||||
* are generated. These are:
|
||||
*
|
||||
* 1) A 128bit symmetric encryption key.
|
||||
* 2) A 160bit symmetric MAC key.
|
||||
* 3) An ECC keypair.
|
||||
*
|
||||
* The first two, along with the ECC keypair's private key, are
|
||||
* then encrypted on disk using PBE.
|
||||
*
|
||||
* This class represents 1 and 2.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterSecret implements Parcelable {
|
||||
|
||||
private final SecretKeySpec encryptionKey;
|
||||
private final SecretKeySpec macKey;
|
||||
|
||||
public static final Parcelable.Creator<MasterSecret> CREATOR = new Parcelable.Creator<MasterSecret>() {
|
||||
@Override
|
||||
public MasterSecret createFromParcel(Parcel in) {
|
||||
return new MasterSecret(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MasterSecret[] newArray(int size) {
|
||||
return new MasterSecret[size];
|
||||
}
|
||||
};
|
||||
|
||||
public MasterSecret(SecretKeySpec encryptionKey, SecretKeySpec macKey) {
|
||||
this.encryptionKey = encryptionKey;
|
||||
this.macKey = macKey;
|
||||
}
|
||||
|
||||
private MasterSecret(Parcel in) {
|
||||
byte[] encryptionKeyBytes = new byte[in.readInt()];
|
||||
in.readByteArray(encryptionKeyBytes);
|
||||
|
||||
byte[] macKeyBytes = new byte[in.readInt()];
|
||||
in.readByteArray(macKeyBytes);
|
||||
|
||||
this.encryptionKey = new SecretKeySpec(encryptionKeyBytes, "AES");
|
||||
this.macKey = new SecretKeySpec(macKeyBytes, "HmacSHA1");
|
||||
|
||||
// SecretKeySpec does an internal copy in its constructor.
|
||||
Arrays.fill(encryptionKeyBytes, (byte) 0x00);
|
||||
Arrays.fill(macKeyBytes, (byte)0x00);
|
||||
}
|
||||
|
||||
|
||||
public SecretKeySpec getEncryptionKey() {
|
||||
return this.encryptionKey;
|
||||
}
|
||||
|
||||
public SecretKeySpec getMacKey() {
|
||||
return this.macKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeInt(encryptionKey.getEncoded().length);
|
||||
out.writeByteArray(encryptionKey.getEncoded());
|
||||
out.writeInt(macKey.getEncoded().length);
|
||||
out.writeByteArray(macKey.getEncoded());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public MasterSecret parcelClone() {
|
||||
Parcel thisParcel = Parcel.obtain();
|
||||
Parcel thatParcel = Parcel.obtain();
|
||||
byte[] bytes = null;
|
||||
|
||||
thisParcel.writeValue(this);
|
||||
bytes = thisParcel.marshall();
|
||||
|
||||
thatParcel.unmarshall(bytes, 0, bytes.length);
|
||||
thatParcel.setDataPosition(0);
|
||||
|
||||
MasterSecret that = (MasterSecret)thatParcel.readValue(MasterSecret.class.getClassLoader());
|
||||
|
||||
thisParcel.recycle();
|
||||
thatParcel.recycle();
|
||||
|
||||
return that;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2013 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.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.thoughtcrimegson.Gson;
|
||||
|
||||
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyIdException;
|
||||
import org.whispersystems.libaxolotl.ecc.Curve;
|
||||
import org.whispersystems.libaxolotl.ecc.Curve25519;
|
||||
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
||||
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyStore;
|
||||
import org.whispersystems.libaxolotl.util.Medium;
|
||||
import org.whispersystems.textsecure.storage.TextSecurePreKeyStore;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class PreKeyUtil {
|
||||
|
||||
public static final int BATCH_SIZE = 100;
|
||||
|
||||
public static List<PreKeyRecord> generatePreKeys(Context context, MasterSecret masterSecret) {
|
||||
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context, masterSecret);
|
||||
List<PreKeyRecord> records = new LinkedList<>();
|
||||
int preKeyIdOffset = getNextPreKeyId(context);
|
||||
|
||||
for (int i=0;i<BATCH_SIZE;i++) {
|
||||
int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
|
||||
ECKeyPair keyPair = Curve25519.generateKeyPair();
|
||||
PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
|
||||
|
||||
preKeyStore.storePreKey(preKeyId, record);
|
||||
records.add(record);
|
||||
}
|
||||
|
||||
setNextPreKeyId(context, (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE);
|
||||
return records;
|
||||
}
|
||||
|
||||
public static SignedPreKeyRecord generateSignedPreKey(Context context, MasterSecret masterSecret,
|
||||
IdentityKeyPair identityKeyPair)
|
||||
{
|
||||
try {
|
||||
SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context, masterSecret);
|
||||
int signedPreKeyId = getNextSignedPreKeyId(context);
|
||||
ECKeyPair keyPair = Curve25519.generateKeyPair();
|
||||
byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
|
||||
SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
|
||||
|
||||
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
|
||||
setNextSignedPreKeyId(context, (signedPreKeyId + 1) % Medium.MAX_VALUE);
|
||||
|
||||
return record;
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static PreKeyRecord generateLastResortKey(Context context, MasterSecret masterSecret) {
|
||||
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context, masterSecret);
|
||||
|
||||
if (preKeyStore.containsPreKey(Medium.MAX_VALUE)) {
|
||||
try {
|
||||
return preKeyStore.loadPreKey(Medium.MAX_VALUE);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
preKeyStore.removePreKey(Medium.MAX_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
ECKeyPair keyPair = Curve25519.generateKeyPair();
|
||||
PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
|
||||
|
||||
preKeyStore.storePreKey(Medium.MAX_VALUE, record);
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
private static void setNextPreKeyId(Context context, int id) {
|
||||
try {
|
||||
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
|
||||
FileOutputStream fout = new FileOutputStream(nextFile);
|
||||
fout.write(new Gson().toJson(new PreKeyIndex(id)).getBytes());
|
||||
fout.close();
|
||||
} catch (IOException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setNextSignedPreKeyId(Context context, int id) {
|
||||
try {
|
||||
File nextFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME);
|
||||
FileOutputStream fout = new FileOutputStream(nextFile);
|
||||
fout.write(new Gson().toJson(new SignedPreKeyIndex(id)).getBytes());
|
||||
fout.close();
|
||||
} catch (IOException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getNextPreKeyId(Context context) {
|
||||
try {
|
||||
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
|
||||
|
||||
if (!nextFile.exists()) {
|
||||
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
|
||||
} else {
|
||||
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
|
||||
PreKeyIndex index = new Gson().fromJson(reader, PreKeyIndex.class);
|
||||
reader.close();
|
||||
return index.nextPreKeyId;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getNextSignedPreKeyId(Context context) {
|
||||
try {
|
||||
File nextFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME);
|
||||
|
||||
if (!nextFile.exists()) {
|
||||
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
|
||||
} else {
|
||||
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
|
||||
SignedPreKeyIndex index = new Gson().fromJson(reader, SignedPreKeyIndex.class);
|
||||
reader.close();
|
||||
return index.nextSignedPreKeyId;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
private static File getPreKeysDirectory(Context context) {
|
||||
return getKeysDirectory(context, TextSecurePreKeyStore.PREKEY_DIRECTORY);
|
||||
}
|
||||
|
||||
private static File getSignedPreKeysDirectory(Context context) {
|
||||
return getKeysDirectory(context, TextSecurePreKeyStore.SIGNED_PREKEY_DIRECTORY);
|
||||
}
|
||||
|
||||
private static File getKeysDirectory(Context context, String name) {
|
||||
File directory = new File(context.getFilesDir(), name);
|
||||
|
||||
if (!directory.exists())
|
||||
directory.mkdirs();
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
private static class PreKeyIndex {
|
||||
public static final String FILE_NAME = "index.dat";
|
||||
|
||||
private int nextPreKeyId;
|
||||
|
||||
public PreKeyIndex() {}
|
||||
|
||||
public PreKeyIndex(int nextPreKeyId) {
|
||||
this.nextPreKeyId = nextPreKeyId;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SignedPreKeyIndex {
|
||||
public static final String FILE_NAME = "index.dat";
|
||||
|
||||
private int nextSignedPreKeyId;
|
||||
|
||||
public SignedPreKeyIndex() {}
|
||||
|
||||
public SignedPreKeyIndex(int nextSignedPreKeyId) {
|
||||
this.nextSignedPreKeyId = nextSignedPreKeyId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 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.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
import org.whispersystems.libaxolotl.ecc.Curve;
|
||||
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class PublicKey {
|
||||
|
||||
public static final int KEY_SIZE = 3 + ECPublicKey.KEY_SIZE;
|
||||
|
||||
private final ECPublicKey publicKey;
|
||||
private int id;
|
||||
|
||||
public PublicKey(PublicKey publicKey) {
|
||||
this.id = publicKey.id;
|
||||
|
||||
// FIXME :: This not strictly an accurate copy constructor.
|
||||
this.publicKey = publicKey.publicKey;
|
||||
}
|
||||
|
||||
public PublicKey(int id, ECPublicKey publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public PublicKey(byte[] bytes, int offset) throws InvalidKeyException {
|
||||
Log.w("PublicKey", "PublicKey Length: " + (bytes.length - offset));
|
||||
|
||||
if ((bytes.length - offset) < KEY_SIZE)
|
||||
throw new InvalidKeyException("Provided bytes are too short.");
|
||||
|
||||
this.id = Conversions.byteArrayToMedium(bytes, offset);
|
||||
this.publicKey = Curve.decodePoint(bytes, offset + 3);
|
||||
}
|
||||
|
||||
public PublicKey(byte[] bytes) throws InvalidKeyException {
|
||||
this(bytes, 0);
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return publicKey.getType();
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ECPublicKey getKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public String getFingerprint() {
|
||||
return Hex.toString(getFingerprintBytes());
|
||||
}
|
||||
|
||||
public byte[] getFingerprintBytes() {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
return md.digest(serialize());
|
||||
} catch (NoSuchAlgorithmException nsae) {
|
||||
Log.w("LocalKeyPair", nsae);
|
||||
throw new IllegalArgumentException("SHA-1 isn't supported!");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] keyIdBytes = Conversions.mediumToByteArray(id);
|
||||
byte[] serializedPoint = publicKey.serialize();
|
||||
|
||||
Log.w("PublicKey", "Serializing public key point: " + Hex.toString(serializedPoint));
|
||||
|
||||
return Util.combine(keyIdBytes, serializedPoint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import org.whispersystems.libaxolotl.DuplicateMessageException;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyIdException;
|
||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||
import org.whispersystems.libaxolotl.LegacyMessageException;
|
||||
import org.whispersystems.libaxolotl.NoSessionException;
|
||||
import org.whispersystems.libaxolotl.SessionCipher;
|
||||
import org.whispersystems.libaxolotl.UntrustedIdentityException;
|
||||
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
|
||||
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
|
||||
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
|
||||
import org.whispersystems.libaxolotl.state.AxolotlStore;
|
||||
import org.whispersystems.textsecure.push.PushAddress;
|
||||
import org.whispersystems.textsecure.push.PushTransportDetails;
|
||||
|
||||
public class TextSecureCipher {
|
||||
|
||||
private final SessionCipher sessionCipher;
|
||||
private final TransportDetails transportDetails;
|
||||
|
||||
public TextSecureCipher(AxolotlStore axolotlStore, PushAddress pushAddress) {
|
||||
int sessionVersion = axolotlStore.loadSession(pushAddress.getRecipientId(),
|
||||
pushAddress.getDeviceId())
|
||||
.getSessionState().getSessionVersion();
|
||||
|
||||
this.transportDetails = new PushTransportDetails(sessionVersion);
|
||||
this.sessionCipher = new SessionCipher(axolotlStore, pushAddress.getRecipientId(),
|
||||
pushAddress.getDeviceId());
|
||||
}
|
||||
|
||||
public CiphertextMessage encrypt(byte[] unpaddedMessage) {
|
||||
return sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
|
||||
}
|
||||
|
||||
public byte[] decrypt(WhisperMessage message)
|
||||
throws DuplicateMessageException, LegacyMessageException, InvalidMessageException, NoSessionException
|
||||
{
|
||||
byte[] paddedMessage = sessionCipher.decrypt(message);
|
||||
return transportDetails.getStrippedPaddingMessageBody(paddedMessage);
|
||||
}
|
||||
|
||||
public byte[] decrypt(PreKeyWhisperMessage message)
|
||||
throws InvalidKeyException, LegacyMessageException, InvalidMessageException,
|
||||
DuplicateMessageException, InvalidKeyIdException, UntrustedIdentityException, NoSessionException
|
||||
{
|
||||
byte[] paddedMessage = sessionCipher.decrypt(message);
|
||||
return transportDetails.getStrippedPaddingMessageBody(paddedMessage);
|
||||
}
|
||||
|
||||
public int getRemoteRegistrationId() {
|
||||
return sessionCipher.getRemoteRegistrationId();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
|
||||
public class UntrustedIdentityException extends Exception {
|
||||
|
||||
private final IdentityKey identityKey;
|
||||
private final String e164number;
|
||||
|
||||
public UntrustedIdentityException(String s, String e164number, IdentityKey identityKey) {
|
||||
super(s);
|
||||
this.e164number = e164number;
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public UntrustedIdentityException(UntrustedIdentityException e) {
|
||||
this(e.getMessage(), e.getE164Number(), e.getIdentityKey());
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public String getE164Number() {
|
||||
return e164number;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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.whispersystems.textsecure.directory;
|
||||
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.MappedByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* A simple bloom filter implementation that backs the RedPhone directory.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
|
||||
public class BloomFilter {
|
||||
|
||||
private final MappedByteBuffer buffer;
|
||||
private final long length;
|
||||
private final int hashCount;
|
||||
|
||||
public BloomFilter(File bloomFilter, int hashCount)
|
||||
throws IOException
|
||||
{
|
||||
this.length = bloomFilter.length();
|
||||
this.buffer = new FileInputStream(bloomFilter).getChannel()
|
||||
.map(FileChannel.MapMode.READ_ONLY, 0, length);
|
||||
this.hashCount = hashCount;
|
||||
}
|
||||
|
||||
public int getHashCount() {
|
||||
return hashCount;
|
||||
}
|
||||
|
||||
private boolean isBitSet(long bitIndex) {
|
||||
int byteInQuestion = this.buffer.get((int)(bitIndex / 8));
|
||||
int bitOffset = (0x01 << (bitIndex % 8));
|
||||
|
||||
return (byteInQuestion & bitOffset) > 0;
|
||||
}
|
||||
|
||||
public boolean contains(String entity) {
|
||||
try {
|
||||
for (int i=0;i<this.hashCount;i++) {
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
mac.init(new SecretKeySpec((i+"").getBytes(), "HmacSHA1"));
|
||||
|
||||
byte[] hashValue = mac.doFinal(entity.getBytes());
|
||||
long bitIndex = Math.abs(Conversions.byteArrayToLong(hashValue, 0)) % (this.length * 8);
|
||||
|
||||
if (!isBitSet(bitIndex))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package org.whispersystems.textsecure.directory;
|
||||
|
||||
public class DirectoryDescriptor {
|
||||
private String version;
|
||||
private long capacity;
|
||||
private int hashCount;
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public int getHashCount() {
|
||||
return hashCount;
|
||||
}
|
||||
|
||||
public long getCapacity() {
|
||||
return capacity;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
private String url;
|
||||
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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.whispersystems.textsecure.directory;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.thoughtcrimegson.Gson;
|
||||
import com.google.thoughtcrimegson.JsonParseException;
|
||||
import com.google.thoughtcrimegson.annotations.SerializedName;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
* Handles providing lookups, serializing, and deserializing the RedPhone directory.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
|
||||
public class NumberFilter {
|
||||
|
||||
private static NumberFilter instance;
|
||||
|
||||
public synchronized static NumberFilter getInstance(Context context) {
|
||||
if (instance == null)
|
||||
instance = NumberFilter.deserializeFromFile(context);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static final String DIRECTORY_META_FILE = "directory.stat";
|
||||
|
||||
private File bloomFilter;
|
||||
private String version;
|
||||
private long capacity;
|
||||
private int hashCount;
|
||||
private Context context;
|
||||
|
||||
private NumberFilter(Context context, File bloomFilter, long capacity,
|
||||
int hashCount, String version)
|
||||
{
|
||||
this.context = context.getApplicationContext();
|
||||
this.bloomFilter = bloomFilter;
|
||||
this.capacity = capacity;
|
||||
this.hashCount = hashCount;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public synchronized boolean containsNumber(String number) {
|
||||
try {
|
||||
if (bloomFilter == null) return false;
|
||||
else if (number == null || number.length() == 0) return false;
|
||||
|
||||
return new BloomFilter(bloomFilter, hashCount).contains(number);
|
||||
} catch (IOException ioe) {
|
||||
Log.w("NumberFilter", ioe);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean containsNumbers(List<String> numbers) {
|
||||
try {
|
||||
if (bloomFilter == null) return false;
|
||||
if (numbers == null || numbers.size() == 0) return false;
|
||||
|
||||
BloomFilter filter = new BloomFilter(bloomFilter, hashCount);
|
||||
|
||||
for (String number : numbers) {
|
||||
if (!filter.contains(number)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (IOException ioe) {
|
||||
Log.w("NumberFilter", ioe);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void update(DirectoryDescriptor descriptor, File compressedData) {
|
||||
try {
|
||||
File uncompressed = File.createTempFile("directory", ".dat", context.getFilesDir());
|
||||
FileInputStream fin = new FileInputStream (compressedData);
|
||||
GZIPInputStream gin = new GZIPInputStream(fin);
|
||||
FileOutputStream out = new FileOutputStream(uncompressed);
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int read;
|
||||
|
||||
while ((read = gin.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
|
||||
out.close();
|
||||
compressedData.delete();
|
||||
|
||||
update(uncompressed, descriptor.getCapacity(), descriptor.getHashCount(), descriptor.getVersion());
|
||||
} catch (IOException ioe) {
|
||||
Log.w("NumberFilter", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void update(File bloomFilter, long capacity, int hashCount, String version)
|
||||
{
|
||||
if (this.bloomFilter != null)
|
||||
this.bloomFilter.delete();
|
||||
|
||||
this.bloomFilter = bloomFilter;
|
||||
this.capacity = capacity;
|
||||
this.hashCount = hashCount;
|
||||
this.version = version;
|
||||
|
||||
serializeToFile(context);
|
||||
}
|
||||
|
||||
private void serializeToFile(Context context) {
|
||||
if (this.bloomFilter == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
FileOutputStream fout = context.openFileOutput(DIRECTORY_META_FILE, 0);
|
||||
NumberFilterStorage storage = new NumberFilterStorage(bloomFilter.getAbsolutePath(),
|
||||
capacity, hashCount, version);
|
||||
|
||||
storage.serializeToStream(fout);
|
||||
fout.close();
|
||||
} catch (IOException ioe) {
|
||||
Log.w("NumberFilter", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
private static NumberFilter deserializeFromFile(Context context) {
|
||||
try {
|
||||
FileInputStream fis = context.openFileInput(DIRECTORY_META_FILE);
|
||||
NumberFilterStorage storage = NumberFilterStorage.fromStream(fis);
|
||||
|
||||
if (storage == null) return new NumberFilter(context, null, 0, 0, "0");
|
||||
else return new NumberFilter(context,
|
||||
new File(storage.getDataPath()),
|
||||
storage.getCapacity(),
|
||||
storage.getHashCount(),
|
||||
storage.getVersion());
|
||||
} catch (IOException ioe) {
|
||||
Log.w("NumberFilter", ioe);
|
||||
return new NumberFilter(context, null, 0, 0, "0");
|
||||
}
|
||||
}
|
||||
|
||||
private static class NumberFilterStorage {
|
||||
@SerializedName("data_path")
|
||||
private String dataPath;
|
||||
|
||||
@SerializedName("capacity")
|
||||
private long capacity;
|
||||
|
||||
@SerializedName("hash_count")
|
||||
private int hashCount;
|
||||
|
||||
@SerializedName("version")
|
||||
private String version;
|
||||
|
||||
public NumberFilterStorage(String dataPath, long capacity, int hashCount, String version) {
|
||||
this.dataPath = dataPath;
|
||||
this.capacity = capacity;
|
||||
this.hashCount = hashCount;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getDataPath() {
|
||||
return dataPath;
|
||||
}
|
||||
|
||||
public long getCapacity() {
|
||||
return capacity;
|
||||
}
|
||||
|
||||
public int getHashCount() {
|
||||
return hashCount;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void serializeToStream(OutputStream out) throws IOException {
|
||||
out.write(new Gson().toJson(this).getBytes());
|
||||
}
|
||||
|
||||
public static NumberFilterStorage fromStream(InputStream in) throws IOException {
|
||||
try {
|
||||
return new Gson().fromJson(new BufferedReader(new InputStreamReader(in)),
|
||||
NumberFilterStorage.class);
|
||||
} catch (JsonParseException jpe) {
|
||||
Log.w("NumberFilter", jpe);
|
||||
throw new IOException("JSON Parse Exception");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,15 +69,7 @@ public class IncomingEncryptedPushMessage {
|
||||
|
||||
return cipher.doFinal(ciphertext, CIPHERTEXT_OFFSET,
|
||||
ciphertext.length - VERSION_LENGTH - IV_LENGTH - MAC_SIZE);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
Log.w("IncomingEncryptedPushMessage", e);
|
||||
|
||||
@@ -10,7 +10,7 @@ public class PushAddress extends RecipientDevice {
|
||||
private final String e164number;
|
||||
private final String relay;
|
||||
|
||||
private PushAddress(long recipientId, String e164number, int deviceId, String relay) {
|
||||
public PushAddress(long recipientId, String e164number, int deviceId, String relay) {
|
||||
super(recipientId, deviceId);
|
||||
this.e164number = e164number;
|
||||
this.relay = relay;
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class PushAttachmentData {
|
||||
|
||||
private final String contentType;
|
||||
private final byte[] data;
|
||||
private final String contentType;
|
||||
private final InputStream data;
|
||||
private final long dataSize;
|
||||
private final byte[] key;
|
||||
|
||||
public PushAttachmentData(String contentType, byte[] data) {
|
||||
public PushAttachmentData(String contentType, InputStream data, long dataSize, byte[] key) {
|
||||
this.contentType = contentType;
|
||||
this.data = data;
|
||||
this.dataSize = dataSize;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
public InputStream getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public long getDataSize() {
|
||||
return dataSize;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyBundle;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.textsecure.crypto.AttachmentCipherOutputStream;
|
||||
import org.whispersystems.textsecure.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.textsecure.push.exceptions.ExpectationFailedException;
|
||||
import org.whispersystems.textsecure.push.exceptions.MismatchedDevicesException;
|
||||
@@ -47,7 +48,6 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
@@ -298,12 +298,13 @@ public class PushServiceSocket {
|
||||
|
||||
Log.w("PushServiceSocket", "Got attachment content location: " + attachmentKey.getLocation());
|
||||
|
||||
uploadExternalFile("PUT", attachmentKey.getLocation(), attachment.getData());
|
||||
uploadAttachment("PUT", attachmentKey.getLocation(), attachment.getData(),
|
||||
attachment.getDataSize(), attachment.getKey());
|
||||
|
||||
return attachmentKey.getId();
|
||||
}
|
||||
|
||||
public File retrieveAttachment(String relay, long attachmentId) throws IOException {
|
||||
public void retrieveAttachment(String relay, long attachmentId, File destination) throws IOException {
|
||||
String path = String.format(ATTACHMENT_PATH, String.valueOf(attachmentId));
|
||||
|
||||
if (!Util.isEmpty(relay)) {
|
||||
@@ -315,12 +316,7 @@ public class PushServiceSocket {
|
||||
|
||||
Log.w("PushServiceSocket", "Attachment: " + attachmentId + " is at: " + descriptor.getLocation());
|
||||
|
||||
File attachment = File.createTempFile("attachment", ".tmp", context.getFilesDir());
|
||||
attachment.deleteOnExit();
|
||||
|
||||
downloadExternalFile(descriptor.getLocation(), attachment);
|
||||
|
||||
return attachment;
|
||||
downloadExternalFile(descriptor.getLocation(), destination);
|
||||
}
|
||||
|
||||
public List<ContactTokenDetails> retrieveDirectory(Set<String> contactTokens) {
|
||||
@@ -356,7 +352,7 @@ public class PushServiceSocket {
|
||||
|
||||
try {
|
||||
if (connection.getResponseCode() != 200) {
|
||||
throw new IOException("Bad response: " + connection.getResponseCode());
|
||||
throw new NonSuccessfulResponseCodeException("Bad response: " + connection.getResponseCode());
|
||||
}
|
||||
|
||||
OutputStream output = new FileOutputStream(localDestination);
|
||||
@@ -375,20 +371,23 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
private void uploadExternalFile(String method, String url, byte[] data)
|
||||
private void uploadAttachment(String method, String url, InputStream data, long dataSize, byte[] key)
|
||||
throws IOException
|
||||
{
|
||||
URL uploadUrl = new URL(url);
|
||||
HttpsURLConnection connection = (HttpsURLConnection) uploadUrl.openConnection();
|
||||
connection.setDoOutput(true);
|
||||
connection.setFixedLengthStreamingMode((int) AttachmentCipherOutputStream.getCiphertextLength(dataSize));
|
||||
connection.setRequestMethod(method);
|
||||
connection.setRequestProperty("Content-Type", "application/octet-stream");
|
||||
connection.connect();
|
||||
|
||||
try {
|
||||
OutputStream out = connection.getOutputStream();
|
||||
out.write(data);
|
||||
out.close();
|
||||
OutputStream stream = connection.getOutputStream();
|
||||
AttachmentCipherOutputStream out = new AttachmentCipherOutputStream(key, stream);
|
||||
|
||||
Util.copy(data, out);
|
||||
out.flush();
|
||||
|
||||
if (connection.getResponseCode() != 200) {
|
||||
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
|
||||
@@ -532,14 +531,8 @@ public class PushServiceSocket {
|
||||
trustManagerFactory.init(keyStore);
|
||||
|
||||
return BlacklistingTrustManager.createFor(trustManagerFactory.getTrustManagers());
|
||||
} catch (KeyStoreException kse) {
|
||||
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException kse) {
|
||||
throw new AssertionError(kse);
|
||||
} catch (CertificateException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (IOException ioe) {
|
||||
throw new AssertionError(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.TransportDetails;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class RawTransportDetails implements TransportDetails {
|
||||
@Override
|
||||
public byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding) {
|
||||
return messageWithPadding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getPaddedMessageBody(byte[] messageBody) {
|
||||
return messageBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncodedMessage(byte[] messageWithMac) {
|
||||
return messageWithMac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getDecodedMessage(byte[] encodedMessageBytes) throws IOException {
|
||||
return encodedMessageBytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.whispersystems.textsecure.push.exceptions;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.textsecure.push.UnregisteredUserException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class EncapsulatedExceptions extends Throwable {
|
||||
|
||||
private final List<UntrustedIdentityException> untrustedIdentityExceptions;
|
||||
private final List<UnregisteredUserException> unregisteredUserExceptions;
|
||||
|
||||
public EncapsulatedExceptions(List<UntrustedIdentityException> untrustedIdentities,
|
||||
List<UnregisteredUserException> unregisteredUsers)
|
||||
{
|
||||
this.untrustedIdentityExceptions = untrustedIdentities;
|
||||
this.unregisteredUserExceptions = unregisteredUsers;
|
||||
}
|
||||
|
||||
public List<UntrustedIdentityException> getUntrustedIdentityExceptions() {
|
||||
return untrustedIdentityExceptions;
|
||||
}
|
||||
|
||||
public List<UnregisteredUserException> getUnregisteredUserExceptions() {
|
||||
return unregisteredUserExceptions;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
|
||||
public class SessionUtil {
|
||||
|
||||
public static int getSessionVersion(Context context,
|
||||
MasterSecret masterSecret,
|
||||
RecipientDevice recipient)
|
||||
{
|
||||
return
|
||||
new TextSecureSessionStore(context, masterSecret)
|
||||
.loadSession(recipient.getRecipientId(), recipient.getDeviceId())
|
||||
.getSessionState()
|
||||
.getSessionVersion();
|
||||
}
|
||||
|
||||
public static boolean hasEncryptCapableSession(Context context,
|
||||
MasterSecret masterSecret,
|
||||
CanonicalRecipient recipient)
|
||||
{
|
||||
return hasEncryptCapableSession(context, masterSecret,
|
||||
new RecipientDevice(recipient.getRecipientId(),
|
||||
RecipientDevice.DEFAULT_DEVICE_ID));
|
||||
}
|
||||
|
||||
public static boolean hasEncryptCapableSession(Context context,
|
||||
MasterSecret masterSecret,
|
||||
RecipientDevice recipientDevice)
|
||||
{
|
||||
long recipientId = recipientDevice.getRecipientId();
|
||||
int deviceId = recipientDevice.getDeviceId();
|
||||
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||
|
||||
return sessionStore.containsSession(recipientId, deviceId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.libaxolotl.InvalidKeyIdException;
|
||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyStore;
|
||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
|
||||
|
||||
public static final String PREKEY_DIRECTORY = "prekeys";
|
||||
public static final String SIGNED_PREKEY_DIRECTORY = "signed_prekeys";
|
||||
|
||||
|
||||
private static final int CURRENT_VERSION_MARKER = 1;
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
private static final String TAG = TextSecurePreKeyStore.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final MasterSecret masterSecret;
|
||||
|
||||
public TextSecurePreKeyStore(Context context, MasterSecret masterSecret) {
|
||||
this.context = context;
|
||||
this.masterSecret = masterSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
return new PreKeyRecord(loadSerializedRecord(getPreKeyFile(preKeyId)));
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new InvalidKeyIdException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
return new SignedPreKeyRecord(loadSerializedRecord(getSignedPreKeyFile(signedPreKeyId)));
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new InvalidKeyIdException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SignedPreKeyRecord> loadSignedPreKeys() {
|
||||
synchronized (FILE_LOCK) {
|
||||
File directory = getSignedPreKeyDirectory();
|
||||
List<SignedPreKeyRecord> results = new LinkedList<>();
|
||||
|
||||
for (File signedPreKeyFile : directory.listFiles()) {
|
||||
try {
|
||||
results.add(new SignedPreKeyRecord(loadSerializedRecord(signedPreKeyFile)));
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePreKey(int preKeyId, PreKeyRecord record) {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
storeSerializedRecord(getPreKeyFile(preKeyId), record.serialize());
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
storeSerializedRecord(getSignedPreKeyFile(signedPreKeyId), record.serialize());
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsPreKey(int preKeyId) {
|
||||
File record = getPreKeyFile(preKeyId);
|
||||
return record.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsSignedPreKey(int signedPreKeyId) {
|
||||
File record = getSignedPreKeyFile(signedPreKeyId);
|
||||
return record.exists();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void removePreKey(int preKeyId) {
|
||||
File record = getPreKeyFile(preKeyId);
|
||||
record.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSignedPreKey(int signedPreKeyId) {
|
||||
File record = getSignedPreKeyFile(signedPreKeyId);
|
||||
record.delete();
|
||||
}
|
||||
|
||||
private byte[] loadSerializedRecord(File recordFile)
|
||||
throws IOException, InvalidMessageException
|
||||
{
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
FileInputStream fin = new FileInputStream(recordFile);
|
||||
int recordVersion = readInteger(fin);
|
||||
|
||||
if (recordVersion != CURRENT_VERSION_MARKER) {
|
||||
throw new AssertionError("Invalid version: " + recordVersion);
|
||||
}
|
||||
|
||||
return masterCipher.decryptBytes(readBlob(fin));
|
||||
}
|
||||
|
||||
private void storeSerializedRecord(File file, byte[] serialized) throws IOException {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
RandomAccessFile recordFile = new RandomAccessFile(file, "rw");
|
||||
FileChannel out = recordFile.getChannel();
|
||||
|
||||
out.position(0);
|
||||
writeInteger(CURRENT_VERSION_MARKER, out);
|
||||
writeBlob(masterCipher.encryptBytes(serialized), out);
|
||||
out.truncate(out.position());
|
||||
recordFile.close();
|
||||
}
|
||||
|
||||
private File getPreKeyFile(int preKeyId) {
|
||||
return new File(getPreKeyDirectory(), String.valueOf(preKeyId));
|
||||
}
|
||||
|
||||
private File getSignedPreKeyFile(int signedPreKeyId) {
|
||||
return new File(getSignedPreKeyDirectory(), String.valueOf(signedPreKeyId));
|
||||
}
|
||||
|
||||
private File getPreKeyDirectory() {
|
||||
return getRecordsDirectory(PREKEY_DIRECTORY);
|
||||
}
|
||||
|
||||
private File getSignedPreKeyDirectory() {
|
||||
return getRecordsDirectory(SIGNED_PREKEY_DIRECTORY);
|
||||
}
|
||||
|
||||
private File getRecordsDirectory(String directoryName) {
|
||||
File directory = new File(context.getFilesDir(), directoryName);
|
||||
|
||||
if (!directory.exists()) {
|
||||
if (!directory.mkdirs()) {
|
||||
Log.w(TAG, "PreKey directory creation failed!");
|
||||
}
|
||||
}
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
private byte[] readBlob(FileInputStream in) throws IOException {
|
||||
int length = readInteger(in);
|
||||
byte[] blobBytes = new byte[length];
|
||||
|
||||
in.read(blobBytes, 0, blobBytes.length);
|
||||
return blobBytes;
|
||||
}
|
||||
|
||||
private void writeBlob(byte[] blobBytes, FileChannel out) throws IOException {
|
||||
writeInteger(blobBytes.length, out);
|
||||
out.write(ByteBuffer.wrap(blobBytes));
|
||||
}
|
||||
|
||||
private int readInteger(FileInputStream in) throws IOException {
|
||||
byte[] integer = new byte[4];
|
||||
in.read(integer, 0, integer.length);
|
||||
return Conversions.byteArrayToInt(integer);
|
||||
}
|
||||
|
||||
private void writeInteger(int value, FileChannel out) throws IOException {
|
||||
byte[] valueBytes = Conversions.intToByteArray(value);
|
||||
out.write(ByteBuffer.wrap(valueBytes));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||
import org.whispersystems.libaxolotl.state.SessionRecord;
|
||||
import org.whispersystems.libaxolotl.state.SessionState;
|
||||
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.whispersystems.libaxolotl.state.StorageProtos.SessionStructure;
|
||||
|
||||
public class TextSecureSessionStore implements SessionStore {
|
||||
|
||||
private static final String TAG = TextSecureSessionStore.class.getSimpleName();
|
||||
private static final String SESSIONS_DIRECTORY_V2 = "sessions-v2";
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
|
||||
private static final int SINGLE_STATE_VERSION = 1;
|
||||
private static final int ARCHIVE_STATES_VERSION = 2;
|
||||
private static final int CURRENT_VERSION = 2;
|
||||
|
||||
private final Context context;
|
||||
private final MasterSecret masterSecret;
|
||||
|
||||
public TextSecureSessionStore(Context context, MasterSecret masterSecret) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.masterSecret = masterSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionRecord loadSession(long recipientId, int deviceId) {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
MasterCipher cipher = new MasterCipher(masterSecret);
|
||||
FileInputStream in = new FileInputStream(getSessionFile(recipientId, deviceId));
|
||||
|
||||
int versionMarker = readInteger(in);
|
||||
|
||||
if (versionMarker > CURRENT_VERSION) {
|
||||
throw new AssertionError("Unknown version: " + versionMarker);
|
||||
}
|
||||
|
||||
byte[] serialized = cipher.decryptBytes(readBlob(in));
|
||||
in.close();
|
||||
|
||||
if (versionMarker == SINGLE_STATE_VERSION) {
|
||||
SessionStructure sessionStructure = SessionStructure.parseFrom(serialized);
|
||||
SessionState sessionState = new SessionState(sessionStructure);
|
||||
return new SessionRecord(sessionState);
|
||||
} else if (versionMarker == ARCHIVE_STATES_VERSION) {
|
||||
return new SessionRecord(serialized);
|
||||
} else {
|
||||
throw new AssertionError("Unknown version: " + versionMarker);
|
||||
}
|
||||
} catch (InvalidMessageException | IOException e) {
|
||||
Log.w(TAG, "No existing session information found.");
|
||||
return new SessionRecord();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeSession(long recipientId, int deviceId, SessionRecord record) {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
RandomAccessFile sessionFile = new RandomAccessFile(getSessionFile(recipientId, deviceId), "rw");
|
||||
FileChannel out = sessionFile.getChannel();
|
||||
|
||||
out.position(0);
|
||||
writeInteger(CURRENT_VERSION, out);
|
||||
writeBlob(masterCipher.encryptBytes(record.serialize()), out);
|
||||
out.truncate(out.position());
|
||||
|
||||
sessionFile.close();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsSession(long recipientId, int deviceId) {
|
||||
return getSessionFile(recipientId, deviceId).exists() &&
|
||||
loadSession(recipientId, deviceId).getSessionState().hasSenderChain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSession(long recipientId, int deviceId) {
|
||||
getSessionFile(recipientId, deviceId).delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAllSessions(long recipientId) {
|
||||
List<Integer> devices = getSubDeviceSessions(recipientId);
|
||||
|
||||
deleteSession(recipientId, RecipientDevice.DEFAULT_DEVICE_ID);
|
||||
|
||||
for (int device : devices) {
|
||||
deleteSession(recipientId, device);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getSubDeviceSessions(long recipientId) {
|
||||
List<Integer> results = new LinkedList<>();
|
||||
File parent = getSessionDirectory();
|
||||
String[] children = parent.list();
|
||||
|
||||
if (children == null) return results;
|
||||
|
||||
for (String child : children) {
|
||||
try {
|
||||
String[] parts = child.split("[.]", 2);
|
||||
long sessionRecipientId = Long.parseLong(parts[0]);
|
||||
|
||||
if (sessionRecipientId == recipientId && parts.length > 1) {
|
||||
results.add(Integer.parseInt(parts[1]));
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w("SessionRecordV2", e);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private File getSessionFile(long recipientId, int deviceId) {
|
||||
return new File(getSessionDirectory(), getSessionName(recipientId, deviceId));
|
||||
}
|
||||
|
||||
private File getSessionDirectory() {
|
||||
File directory = new File(context.getFilesDir(), SESSIONS_DIRECTORY_V2);
|
||||
|
||||
if (!directory.exists()) {
|
||||
if (!directory.mkdirs()) {
|
||||
Log.w(TAG, "Session directory creation failed!");
|
||||
}
|
||||
}
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
private String getSessionName(long recipientId, int deviceId) {
|
||||
return recipientId + (deviceId == RecipientDevice.DEFAULT_DEVICE_ID ? "" : "." + deviceId);
|
||||
}
|
||||
|
||||
private byte[] readBlob(FileInputStream in) throws IOException {
|
||||
int length = readInteger(in);
|
||||
byte[] blobBytes = new byte[length];
|
||||
|
||||
in.read(blobBytes, 0, blobBytes.length);
|
||||
return blobBytes;
|
||||
}
|
||||
|
||||
private void writeBlob(byte[] blobBytes, FileChannel out) throws IOException {
|
||||
writeInteger(blobBytes.length, out);
|
||||
out.write(ByteBuffer.wrap(blobBytes));
|
||||
}
|
||||
|
||||
private int readInteger(FileInputStream in) throws IOException {
|
||||
byte[] integer = new byte[4];
|
||||
in.read(integer, 0, integer.length);
|
||||
return Conversions.byteArrayToInt(integer);
|
||||
}
|
||||
|
||||
private void writeInteger(int value, FileChannel out) throws IOException {
|
||||
byte[] valueBytes = Conversions.intToByteArray(value);
|
||||
out.write(ByteBuffer.wrap(valueBytes));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -94,12 +94,17 @@ public class Util {
|
||||
}
|
||||
|
||||
public static String getSecret(int size) {
|
||||
byte[] secret = getSecretBytes(size);
|
||||
return Base64.encodeBytes(secret);
|
||||
}
|
||||
|
||||
public static byte[] getSecretBytes(int size) {
|
||||
try {
|
||||
byte[] secret = new byte[size];
|
||||
SecureRandom.getInstance("SHA1PRNG").nextBytes(secret);
|
||||
return Base64.encodeBytes(secret);
|
||||
} catch (NoSuchAlgorithmException nsae) {
|
||||
throw new AssertionError(nsae);
|
||||
return secret;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user