Rename library to libtextsecure

This commit is contained in:
Moxie Marlinspike
2014-11-11 21:21:09 -08:00
parent 0d06d50a65
commit 1182052c7f
69 changed files with 3 additions and 3 deletions

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.whispersystems.textsecure"
android:versionCode="1"
android:versionName="0.1">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="16"/>
<application />
</manifest>

View File

@@ -0,0 +1,57 @@
package org.whispersystems.textsecure.push;
import android.test.AndroidTestCase;
import android.util.Base64;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
public class PushTransportDetailsTest extends AndroidTestCase {
private final PushTransportDetails transportV2 = new PushTransportDetails(2);
private final PushTransportDetails transportV3 = new PushTransportDetails(3);
public void testV3Padding() {
for (int i=0;i<159;i++) {
byte[] message = new byte[i];
assertEquals(transportV3.getPaddedMessageBody(message).length, 159);
}
for (int i=159;i<319;i++) {
byte[] message = new byte[i];
assertEquals(transportV3.getPaddedMessageBody(message).length, 319);
}
for (int i=319;i<479;i++) {
byte[] message = new byte[i];
assertEquals(transportV3.getPaddedMessageBody(message).length, 479);
}
}
public void testV2Padding() {
for (int i=0;i<480;i++) {
byte[] message = new byte[i];
assertTrue(transportV2.getPaddedMessageBody(message).length == message.length);
}
}
public void testV3Encoding() throws NoSuchAlgorithmException {
byte[] message = new byte[501];
SecureRandom.getInstance("SHA1PRNG").nextBytes(message);
byte[] padded = transportV3.getEncodedMessage(message);
assertTrue(Arrays.equals(padded, message));
}
public void testV2Encoding() throws NoSuchAlgorithmException {
byte[] message = new byte[501];
SecureRandom.getInstance("SHA1PRNG").nextBytes(message);
byte[] padded = transportV2.getEncodedMessage(message);
assertTrue(Arrays.equals(padded, message));
}
}

View File

@@ -0,0 +1,77 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.12.2'
}
}
apply plugin: 'com.android.library'
apply plugin: 'maven'
repositories {
mavenCentral()
maven {
url "https://raw.github.com/whispersystems/maven/master/gson/releases/"
}
}
dependencies {
compile 'com.google.protobuf:protobuf-java:2.5.0'
compile 'com.madgag:sc-light-jdk15on:1.47.0.2'
compile 'com.googlecode.libphonenumber:libphonenumber:6.1'
compile 'org.whispersystems:gson:2.2.4'
compile project(':libaxolotl')
}
android {
compileSdkVersion 19
buildToolsVersion '19.1.0'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
android {
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
androidTest {
java.srcDirs = ['androidTest/java']
resources.srcDirs = ['androidTest/java']
aidl.srcDirs = ['androidTest/java']
renderscript.srcDirs = ['androidTest/java']
}
}
}
}
tasks.whenTaskAdded { task ->
if (task.name.equals("lint")) {
task.enabled = false
}
}
version '0.1'
group 'org.whispersystems.textsecure'
archivesBaseName = 'libtextsecure'
uploadArchives {
repositories {
mavenDeployer {
repository(url: mavenLocal().getUrl())
}
}
}

92
libtextsecure/build.xml Normal file
View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="library" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties"/>
<!-- The ant.properties file can be created by you. It is only edited by the
'android' tool to add properties to it.
This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
The name of the output directory. Default is 'bin'.
For other overridable properties, look at the beginning of the rules
files in the SDK, at tools/ant/build.xml
Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="ant.properties"/>
<!-- if sdk.dir was not set from one of the property file, then
get it from the ANDROID_HOME env var.
This must be done before we load project.properties since
the proguard config can use sdk.dir -->
<property environment="env"/>
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
<isset property="env.ANDROID_HOME"/>
</condition>
<!-- The project.properties file is created and updated by the 'android'
tool, as well as ADT.
This contains project specific properties such as project target, and library
dependencies. Lower level build properties are stored in ant.properties
(or in .classpath for Eclipse projects).
This file is an integral part of the build system for your
application and should be checked into Version Control Systems. -->
<loadproperties srcFile="project.properties"/>
<!-- quick check on sdk.dir -->
<fail
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
unless="sdk.dir"
/>
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
-post-build
-pre-clean
-->
<import file="custom_rules.xml" optional="true"/>
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
-->
<!-- version-tag: 1 -->
<import file="${sdk.dir}/tools/ant/build.xml"/>
</project>

View File

@@ -0,0 +1,20 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@@ -0,0 +1,53 @@
package textsecure;
option java_package = "org.whispersystems.textsecure.push";
option java_outer_classname = "PushMessageProtos";
message IncomingPushMessageSignal {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
PLAINTEXT = 4;
RECEIPT = 5;
}
optional Type type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;
optional string relay = 3;
optional uint64 timestamp = 5;
optional bytes message = 6; // Contains an encrypted PushMessageContent
// repeated string destinations = 4; // No longer supported
}
message PushMessageContent {
message AttachmentPointer {
optional fixed64 id = 1;
optional string contentType = 2;
optional bytes key = 3;
}
message GroupContext {
enum Type {
UNKNOWN = 0;
UPDATE = 1;
DELIVER = 2;
QUIT = 3;
}
optional bytes id = 1;
optional Type type = 2;
optional string name = 3;
repeated string members = 4;
optional AttachmentPointer avatar = 5;
}
enum Flags {
END_SESSION = 1;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
optional uint32 flags = 4;
}

View File

@@ -0,0 +1,3 @@
all:
protoc --java_out=../src/ IncomingPushMessageSignal.proto

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -0,0 +1,76 @@
package org.whispersystems.textsecure.api;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.SignedPreKeyEntity;
import java.io.IOException;
import java.util.List;
import java.util.Set;
public class TextSecureAccountManager {
private final PushServiceSocket pushServiceSocket;
public TextSecureAccountManager(String url, PushServiceSocket.TrustStore trustStore,
String user, String password)
{
this.pushServiceSocket = new PushServiceSocket(url, trustStore, user, password);
}
public void setGcmId(Optional<String> gcmRegistrationId) throws IOException {
if (gcmRegistrationId.isPresent()) {
this.pushServiceSocket.registerGcmId(gcmRegistrationId.get());
} else {
this.pushServiceSocket.unregisterGcmId();
}
}
public void requestSmsVerificationCode() throws IOException {
this.pushServiceSocket.createAccount(false);
}
public void requestVoiceVerificationCode() throws IOException {
this.pushServiceSocket.createAccount(true);
}
public void verifyAccount(String verificationCode, String signalingKey,
boolean supportsSms, int axolotlRegistrationId)
throws IOException
{
this.pushServiceSocket.verifyAccount(verificationCode, signalingKey,
supportsSms, axolotlRegistrationId);
}
public void setPreKeys(IdentityKey identityKey, PreKeyRecord lastResortKey,
SignedPreKeyRecord signedPreKey, List<PreKeyRecord> oneTimePreKeys)
throws IOException
{
this.pushServiceSocket.registerPreKeys(identityKey, lastResortKey, signedPreKey, oneTimePreKeys);
}
public int getPreKeysCount() throws IOException {
return this.pushServiceSocket.getAvailablePreKeys();
}
public void setSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException {
this.pushServiceSocket.setCurrentSignedPreKey(signedPreKey);
}
public SignedPreKeyEntity getSignedPreKey() throws IOException {
return this.pushServiceSocket.getCurrentSignedPreKey();
}
public Optional<ContactTokenDetails> getContact(String contactToken) throws IOException {
return Optional.fromNullable(this.pushServiceSocket.getContactTokenDetails(contactToken));
}
public List<ContactTokenDetails> getContacts(Set<String> contactTokens) {
return this.pushServiceSocket.retrieveDirectory(contactTokens);
}
}

View File

@@ -0,0 +1,29 @@
package org.whispersystems.textsecure.api;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
import org.whispersystems.textsecure.push.PushServiceSocket;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class TextSecureMessageReceiver {
private final PushServiceSocket socket;
public TextSecureMessageReceiver(String url, PushServiceSocket.TrustStore trustStore,
String user, String password)
{
this.socket = new PushServiceSocket(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());
}
}

View File

@@ -0,0 +1,306 @@
package org.whispersystems.textsecure.api;
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.crypto.TextSecureCipher;
import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException;
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.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(String url, PushServiceSocket.TrustStore trustStore,
String user, String password, AxolotlStore store,
Optional<EventListener> eventListener)
{
this.socket = new PushServiceSocket(url, trustStore, user, password);
this.store = store;
this.eventListener = eventListener;
}
public void sendDeliveryReceipt(PushAddress recipient, long messageId) throws IOException {
this.socket.sendReceipt(recipient.getNumber(), messageId, recipient.getRelay());
}
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()) {
Log.w(TAG, "No attachments present...");
return pointers;
}
for (TextSecureAttachment attachment : attachments.get()) {
if (attachment.isStream()) {
Log.w(TAG, "Found attachment, creating pointer...");
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.getRecipientId(), recipient.getDeviceId());
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);
}
}

View File

@@ -0,0 +1,211 @@
/**
* 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.api.crypto;
import android.util.Log;
import org.whispersystems.libaxolotl.InvalidMacException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
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.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Class for streaming an encrypted push attachment off disk.
*
* @author Moxie Marlinspike
*/
public class AttachmentCipherInputStream extends FileInputStream {
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;
public AttachmentCipherInputStream(File file, byte[] combinedKeyMaterial)
throws IOException, InvalidMessageException
{
super(file);
try {
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
if (file.length() <= BLOCK_SIZE + mac.getMacLength()) {
throw new InvalidMessageException("Message shorter than crypto overhead!");
}
verifyMac(file, mac);
byte[] iv = new byte[BLOCK_SIZE];
readFully(iv);
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(parts[0], "AES"), new IvParameterSpec(iv));
this.done = false;
this.totalRead = 0;
this.totalDataSize = file.length() - cipher.getBlockSize() - mac.getMacLength();
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (InvalidMacException e) {
throw new InvalidMessageException(e);
}
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
if (totalRead != totalDataSize) return readIncremental(buffer, offset, length);
else if (!done) return readFinal(buffer, offset, length);
else return -1;
}
private int readFinal(byte[] buffer, int offset, int length) throws IOException {
try {
int flourish = cipher.doFinal(buffer, offset);
done = true;
return flourish;
} catch (IllegalBlockSizeException e) {
Log.w("EncryptingPartInputStream", e);
throw new IOException("Illegal block size exception!");
} catch (ShortBufferException e) {
Log.w("EncryptingPartInputStream", e);
throw new IOException("Short buffer exception!");
} catch (BadPaddingException e) {
Log.w("EncryptingPartInputStream", e);
throw new IOException("Bad padding exception!");
}
}
private int readIncremental(byte[] buffer, int offset, int length) throws IOException {
int readLength = 0;
if (null != overflowBuffer) {
if (overflowBuffer.length > length) {
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
overflowBuffer = Arrays.copyOfRange(overflowBuffer, length, overflowBuffer.length);
return length;
} else if (overflowBuffer.length == length) {
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
overflowBuffer = null;
return length;
} else {
System.arraycopy(overflowBuffer, 0, buffer, offset, overflowBuffer.length);
readLength += overflowBuffer.length;
offset += readLength;
length -= readLength;
overflowBuffer = null;
}
}
if (length + totalRead > totalDataSize)
length = (int)(totalDataSize - totalRead);
byte[] internalBuffer = new byte[length];
int read = super.read(internalBuffer, 0, internalBuffer.length <= cipher.getBlockSize() ? internalBuffer.length : internalBuffer.length - cipher.getBlockSize());
totalRead += read;
try {
int outputLen = cipher.getOutputSize(read);
if (outputLen <= length) {
readLength += cipher.update(internalBuffer, 0, read, buffer, offset);
return readLength;
}
byte[] transientBuffer = new byte[outputLen];
outputLen = cipher.update(internalBuffer, 0, read, transientBuffer, 0);
if (outputLen <= length) {
System.arraycopy(transientBuffer, 0, buffer, offset, outputLen);
readLength += outputLen;
} else {
System.arraycopy(transientBuffer, 0, buffer, offset, length);
overflowBuffer = Arrays.copyOfRange(transientBuffer, length, outputLen);
readLength += length;
}
return readLength;
} catch (ShortBufferException e) {
throw new AssertionError(e);
}
}
private void verifyMac(File file, Mac mac) throws FileNotFoundException, InvalidMacException {
try {
FileInputStream fin = new FileInputStream(file);
int remainingData = (int) file.length() - mac.getMacLength();
byte[] buffer = new byte[4096];
while (remainingData > 0) {
int read = fin.read(buffer, 0, Math.min(buffer.length, remainingData));
mac.update(buffer, 0, read);
remainingData -= read;
}
byte[] ourMac = mac.doFinal();
byte[] theirMac = new byte[mac.getMacLength()];
Util.readFully(fin, theirMac);
if (!Arrays.equals(ourMac, theirMac)) {
throw new InvalidMacException("MAC doesn't match!");
}
} catch (IOException e1) {
throw new InvalidMacException(e1);
}
}
private void readFully(byte[] buffer) throws IOException {
int offset = 0;
for (;;) {
int read = super.read(buffer, offset, buffer.length - offset);
if (read + offset < buffer.length) offset += read;
else return;
}
}
}

View File

@@ -0,0 +1,105 @@
package org.whispersystems.textsecure.api.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);
}
}
}

View File

@@ -0,0 +1,134 @@
package org.whispersystems.textsecure.api.crypto;
import android.util.Log;
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.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.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.push.PushTransportDetails;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext.Type.DELIVER;
public class TextSecureCipher {
private final SessionCipher sessionCipher;
public TextSecureCipher(AxolotlStore axolotlStore, long recipientId, int deviceId) {
this.sessionCipher = new SessionCipher(axolotlStore, recipientId, deviceId);
}
public CiphertextMessage encrypt(byte[] unpaddedMessage) {
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
return sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
}
public TextSecureMessage decrypt(TextSecureEnvelope envelope)
throws InvalidVersionException, InvalidMessageException, InvalidKeyException,
DuplicateMessageException, InvalidKeyIdException, UntrustedIdentityException,
LegacyMessageException, NoSessionException
{
try {
byte[] paddedMessage;
if (envelope.isPreKeyWhisperMessage()) {
paddedMessage = sessionCipher.decrypt(new PreKeyWhisperMessage(envelope.getMessage()));
} else if (envelope.isWhisperMessage()) {
paddedMessage = sessionCipher.decrypt(new WhisperMessage(envelope.getMessage()));
} else if (envelope.isPlaintext()) {
paddedMessage = envelope.getMessage();
} else {
throw new InvalidMessageException("Unknown type: " + envelope.getType());
}
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
PushMessageContent content = PushMessageContent.parseFrom(transportDetails.getStrippedPaddingMessageBody(paddedMessage));
return createTextSecureMessage(envelope, content);
} catch (InvalidProtocolBufferException e) {
throw new InvalidMessageException(e);
}
}
public int getRemoteRegistrationId() {
return sessionCipher.getRemoteRegistrationId();
}
private TextSecureMessage createTextSecureMessage(TextSecureEnvelope envelope, PushMessageContent content) {
TextSecureGroup groupInfo = createGroupInfo(envelope, content);
List<TextSecureAttachment> attachments = new LinkedList<>();
boolean endSession = ((content.getFlags() & PushMessageContent.Flags.END_SESSION_VALUE) != 0);
boolean secure = envelope.isWhisperMessage() || envelope.isPreKeyWhisperMessage();
for (PushMessageContent.AttachmentPointer pointer : content.getAttachmentsList()) {
attachments.add(new TextSecureAttachmentPointer(pointer.getId(),
pointer.getContentType(),
pointer.getKey().toByteArray(),
envelope.getRelay()));
}
return new TextSecureMessage(envelope.getTimestamp(), groupInfo, attachments,
content.getBody(), secure, endSession);
}
private TextSecureGroup createGroupInfo(TextSecureEnvelope envelope, 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(),
envelope.getRelay());
}
return new TextSecureGroup(type, content.getGroup().getId().toByteArray(), name, members, avatar);
}
return new TextSecureGroup(content.getGroup().getId().toByteArray());
}
}

View File

@@ -0,0 +1,28 @@
package org.whispersystems.textsecure.api.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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 true;
}
@Override
public boolean isPointer() {
return false;
}
public InputStream getInputStream() {
return inputStream;
}
public long getLength() {
return length;
}
}

View File

@@ -0,0 +1,177 @@
package org.whispersystems.textsecure.api.messages;
import android.util.Log;
import com.google.protobuf.ByteString;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Hex;
import java.io.IOException;
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;
public class TextSecureEnvelope {
private static final String TAG = TextSecureEnvelope.class.getSimpleName();
private static final int SUPPORTED_VERSION = 1;
private static final int CIPHER_KEY_SIZE = 32;
private static final int MAC_KEY_SIZE = 20;
private static final int MAC_SIZE = 10;
private static final int VERSION_OFFSET = 0;
private static final int VERSION_LENGTH = 1;
private static final int IV_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
private static final int IV_LENGTH = 16;
private static final int CIPHERTEXT_OFFSET = IV_OFFSET + IV_LENGTH;
private final IncomingPushMessageSignal signal;
public TextSecureEnvelope(String message, String signalingKey)
throws IOException, InvalidVersionException
{
byte[] ciphertext = Base64.decode(message);
if (ciphertext.length < VERSION_LENGTH || ciphertext[VERSION_OFFSET] != SUPPORTED_VERSION)
throw new InvalidVersionException("Unsupported version!");
SecretKeySpec cipherKey = getCipherKey(signalingKey);
SecretKeySpec macKey = getMacKey(signalingKey);
verifyMac(ciphertext, macKey);
this.signal = IncomingPushMessageSignal.parseFrom(getPlaintext(ciphertext, cipherKey));
}
public TextSecureEnvelope(int type, String source, int sourceDevice,
String relay, long timestamp, byte[] message)
{
this.signal = IncomingPushMessageSignal.newBuilder()
.setType(IncomingPushMessageSignal.Type.valueOf(type))
.setSource(source)
.setSourceDevice(sourceDevice)
.setRelay(relay)
.setTimestamp(timestamp)
.setMessage(ByteString.copyFrom(message))
.build();
}
public String getSource() {
return signal.getSource();
}
public int getSourceDevice() {
return signal.getSourceDevice();
}
public int getType() {
return signal.getType().getNumber();
}
public String getRelay() {
return signal.getRelay();
}
public long getTimestamp() {
return signal.getTimestamp();
}
public byte[] getMessage() {
return signal.getMessage().toByteArray();
}
public boolean isWhisperMessage() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE;
}
public boolean isPreKeyWhisperMessage() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE;
}
public boolean isPlaintext() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PLAINTEXT_VALUE;
}
public boolean isReceipt() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.RECEIPT_VALUE;
}
private byte[] getPlaintext(byte[] ciphertext, SecretKeySpec cipherKey) throws IOException {
try {
byte[] ivBytes = new byte[IV_LENGTH];
System.arraycopy(ciphertext, IV_OFFSET, ivBytes, 0, ivBytes.length);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, cipherKey, iv);
return cipher.doFinal(ciphertext, CIPHERTEXT_OFFSET,
ciphertext.length - VERSION_LENGTH - IV_LENGTH - MAC_SIZE);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
Log.w(TAG, e);
throw new IOException("Bad padding?");
}
}
private void verifyMac(byte[] ciphertext, SecretKeySpec macKey) throws IOException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(macKey);
if (ciphertext.length < MAC_SIZE + 1)
throw new IOException("Invalid MAC!");
mac.update(ciphertext, 0, ciphertext.length - MAC_SIZE);
byte[] ourMacFull = mac.doFinal();
byte[] ourMacBytes = new byte[MAC_SIZE];
System.arraycopy(ourMacFull, 0, ourMacBytes, 0, ourMacBytes.length);
byte[] theirMacBytes = new byte[MAC_SIZE];
System.arraycopy(ciphertext, ciphertext.length-MAC_SIZE, theirMacBytes, 0, theirMacBytes.length);
Log.w(TAG, "Our MAC: " + Hex.toString(ourMacBytes));
Log.w(TAG, "Thr MAC: " + Hex.toString(theirMacBytes));
if (!Arrays.equals(ourMacBytes, theirMacBytes)) {
throw new IOException("Invalid MAC compare!");
}
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
private SecretKeySpec getCipherKey(String signalingKey) throws IOException {
byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
return new SecretKeySpec(cipherKey, "AES");
}
private SecretKeySpec getMacKey(String signalingKey) throws IOException {
byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] macKey = new byte[MAC_KEY_SIZE];
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
return new SecretKeySpec(macKey, "HmacSHA256");
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,69 @@
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.body = Optional.fromNullable(body);
this.group = Optional.fromNullable(group);
this.secure = secure;
this.endSession = endSession;
if (attachments != null && !attachments.isEmpty()) {
this.attachments = Optional.of(attachments);
} else {
this.attachments = Optional.absent();
}
}
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;
}
}

View File

@@ -0,0 +1,28 @@
package org.whispersystems.textsecure.push;
public class AccountAttributes {
private String signalingKey;
private boolean supportsSms;
private int registrationId;
public AccountAttributes(String signalingKey, boolean supportsSms, int registrationId) {
this.signalingKey = signalingKey;
this.supportsSms = supportsSms;
this.registrationId = registrationId;
}
public AccountAttributes() {}
public String getSignalingKey() {
return signalingKey;
}
public boolean isSupportsSms() {
return supportsSms;
}
public int getRegistrationId() {
return registrationId;
}
}

View File

@@ -0,0 +1,33 @@
package org.whispersystems.textsecure.push;
import com.google.thoughtcrimegson.Gson;
public class ContactTokenDetails {
private String token;
private String relay;
private String number;
private boolean supportsSms;
public ContactTokenDetails() {}
public String getToken() {
return token;
}
public String getRelay() {
return relay;
}
public boolean isSupportsSms() {
return supportsSms;
}
public void setNumber(String number) {
this.number = number;
}
public String getNumber() {
return number;
}
}

View File

@@ -0,0 +1,14 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class ContactTokenDetailsList {
private List<ContactTokenDetails> contacts;
public ContactTokenDetailsList() {}
public List<ContactTokenDetails> getContacts() {
return contacts;
}
}

View File

@@ -0,0 +1,18 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class ContactTokenList {
private List<String> contacts;
public ContactTokenList(List<String> contacts) {
this.contacts = contacts;
}
public ContactTokenList() {}
public List<String> getContacts() {
return contacts;
}
}

View File

@@ -0,0 +1,17 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class MismatchedDevices {
private List<Integer> missingDevices;
private List<Integer> extraDevices;
public List<Integer> getMissingDevices() {
return missingDevices;
}
public List<Integer> getExtraDevices() {
return extraDevices;
}
}

View File

@@ -0,0 +1,50 @@
/**
* 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.push;
import org.whispersystems.textsecure.util.Base64;
public class OutgoingPushMessage {
private int type;
private int destinationDeviceId;
private int destinationRegistrationId;
private String body;
public OutgoingPushMessage(PushAddress address, PushBody body) {
this.type = body.getType();
this.destinationDeviceId = address.getDeviceId();
this.destinationRegistrationId = body.getRemoteRegistrationId();
this.body = Base64.encodeBytes(body.getBody());
}
public int getDestinationDeviceId() {
return destinationDeviceId;
}
public String getBody() {
return body;
}
public int getType() {
return type;
}
public int getDestinationRegistrationId() {
return destinationRegistrationId;
}
}

View File

@@ -0,0 +1,39 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class OutgoingPushMessageList {
private String destination;
private String relay;
private long timestamp;
private List<OutgoingPushMessage> messages;
public OutgoingPushMessageList(String destination, long timestamp, String relay,
List<OutgoingPushMessage> messages)
{
this.timestamp = timestamp;
this.destination = destination;
this.relay = relay;
this.messages = messages;
}
public String getDestination() {
return destination;
}
public List<OutgoingPushMessage> getMessages() {
return messages;
}
public String getRelay() {
return relay;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@@ -0,0 +1,69 @@
package org.whispersystems.textsecure.push;
import com.google.thoughtcrimegson.GsonBuilder;
import com.google.thoughtcrimegson.JsonDeserializationContext;
import com.google.thoughtcrimegson.JsonDeserializer;
import com.google.thoughtcrimegson.JsonElement;
import com.google.thoughtcrimegson.JsonParseException;
import com.google.thoughtcrimegson.JsonPrimitive;
import com.google.thoughtcrimegson.JsonSerializationContext;
import com.google.thoughtcrimegson.JsonSerializer;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.textsecure.util.Base64;
import java.io.IOException;
import java.lang.reflect.Type;
public class PreKeyEntity {
private int keyId;
private ECPublicKey publicKey;
public PreKeyEntity() {}
public PreKeyEntity(int keyId, ECPublicKey publicKey) {
this.keyId = keyId;
this.publicKey = publicKey;
}
public int getKeyId() {
return keyId;
}
public ECPublicKey getPublicKey() {
return publicKey;
}
public static GsonBuilder forBuilder(GsonBuilder builder) {
return builder.registerTypeAdapter(ECPublicKey.class, new ECPublicKeyJsonAdapter());
}
private static class ECPublicKeyJsonAdapter
implements JsonSerializer<ECPublicKey>, JsonDeserializer<ECPublicKey>
{
@Override
public JsonElement serialize(ECPublicKey preKeyPublic, Type type,
JsonSerializationContext jsonSerializationContext)
{
return new JsonPrimitive(Base64.encodeBytesWithoutPadding(preKeyPublic.serialize()));
}
@Override
public ECPublicKey deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext)
throws JsonParseException
{
try {
return Curve.decodePoint(Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString()), 0);
} catch (InvalidKeyException | IOException e) {
throw new JsonParseException(e);
}
}
}
}

View File

@@ -0,0 +1,65 @@
package org.whispersystems.textsecure.push;
import com.google.thoughtcrimegson.GsonBuilder;
import com.google.thoughtcrimegson.JsonDeserializationContext;
import com.google.thoughtcrimegson.JsonDeserializer;
import com.google.thoughtcrimegson.JsonElement;
import com.google.thoughtcrimegson.JsonParseException;
import com.google.thoughtcrimegson.JsonPrimitive;
import com.google.thoughtcrimegson.JsonSerializationContext;
import com.google.thoughtcrimegson.JsonSerializer;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.textsecure.util.Base64;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
public class PreKeyResponse {
private IdentityKey identityKey;
private List<PreKeyResponseItem> devices;
public IdentityKey getIdentityKey() {
return identityKey;
}
public List<PreKeyResponseItem> getDevices() {
return devices;
}
public static PreKeyResponse fromJson(String serialized) {
GsonBuilder builder = new GsonBuilder();
return PreKeyResponseItem.forBuilder(builder)
.registerTypeAdapter(IdentityKey.class, new IdentityKeyJsonAdapter())
.create().fromJson(serialized, PreKeyResponse.class);
}
public static class IdentityKeyJsonAdapter
implements JsonSerializer<IdentityKey>, JsonDeserializer<IdentityKey>
{
@Override
public JsonElement serialize(IdentityKey identityKey, Type type,
JsonSerializationContext jsonSerializationContext)
{
return new JsonPrimitive(Base64.encodeBytesWithoutPadding(identityKey.serialize()));
}
@Override
public IdentityKey deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext)
throws JsonParseException
{
try {
return new IdentityKey(Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString()), 0);
} catch (InvalidKeyException | IOException e) {
throw new JsonParseException(e);
}
}
}
}

View File

@@ -0,0 +1,31 @@
package org.whispersystems.textsecure.push;
import com.google.thoughtcrimegson.GsonBuilder;
public class PreKeyResponseItem {
private int deviceId;
private int registrationId;
private SignedPreKeyEntity signedPreKey;
private PreKeyEntity preKey;
public int getDeviceId() {
return deviceId;
}
public int getRegistrationId() {
return registrationId;
}
public SignedPreKeyEntity getSignedPreKey() {
return signedPreKey;
}
public PreKeyEntity getPreKey() {
return preKey;
}
public static GsonBuilder forBuilder(GsonBuilder builder) {
return SignedPreKeyEntity.forBuilder(builder);
}
}

View File

@@ -0,0 +1,32 @@
package org.whispersystems.textsecure.push;
import com.google.thoughtcrimegson.GsonBuilder;
import org.whispersystems.libaxolotl.IdentityKey;
import java.util.List;
public class PreKeyState {
private IdentityKey identityKey;
private List<PreKeyEntity> preKeys;
private PreKeyEntity lastResortKey;
private SignedPreKeyEntity signedPreKey;
public PreKeyState(List<PreKeyEntity> preKeys, PreKeyEntity lastResortKey,
SignedPreKeyEntity signedPreKey, IdentityKey identityKey)
{
this.preKeys = preKeys;
this.lastResortKey = lastResortKey;
this.signedPreKey = signedPreKey;
this.identityKey = identityKey;
}
public static String toJson(PreKeyState state) {
GsonBuilder builder = new GsonBuilder();
return SignedPreKeyEntity.forBuilder(builder)
.registerTypeAdapter(IdentityKey.class, new PreKeyResponse.IdentityKeyJsonAdapter())
.create().toJson(state);
}
}

View File

@@ -0,0 +1,12 @@
package org.whispersystems.textsecure.push;
public class PreKeyStatus {
private int count;
public PreKeyStatus() {}
public int getCount() {
return count;
}
}

View File

@@ -0,0 +1,24 @@
package org.whispersystems.textsecure.push;
import org.whispersystems.textsecure.storage.RecipientDevice;
public class PushAddress extends RecipientDevice {
private final String e164number;
private final String relay;
public PushAddress(long recipientId, String e164number, int deviceId, String relay) {
super(recipientId, deviceId);
this.e164number = e164number;
this.relay = relay;
}
public String getNumber() {
return e164number;
}
public String getRelay() {
return relay;
}
}

View File

@@ -0,0 +1,34 @@
package org.whispersystems.textsecure.push;
import java.io.InputStream;
public class PushAttachmentData {
private final String contentType;
private final InputStream data;
private final long dataSize;
private final byte[] key;
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 InputStream getData() {
return data;
}
public long getDataSize() {
return dataSize;
}
public byte[] getKey() {
return key;
}
}

View File

@@ -0,0 +1,63 @@
package org.whispersystems.textsecure.push;
import android.os.Parcel;
import android.os.Parcelable;
public class PushAttachmentPointer implements Parcelable {
public static final Parcelable.Creator<PushAttachmentPointer> CREATOR = new Parcelable.Creator<PushAttachmentPointer>() {
@Override
public PushAttachmentPointer createFromParcel(Parcel in) {
return new PushAttachmentPointer(in);
}
@Override
public PushAttachmentPointer[] newArray(int size) {
return new PushAttachmentPointer[size];
}
};
private final String contentType;
private final long id;
private final byte[] key;
public PushAttachmentPointer(String contentType, long id, byte[] key) {
this.contentType = contentType;
this.id = id;
this.key = key;
}
public PushAttachmentPointer(Parcel in) {
this.contentType = in.readString();
this.id = in.readLong();
int keyLength = in.readInt();
this.key = new byte[keyLength];
in.readByteArray(this.key);
}
public String getContentType() {
return contentType;
}
public long getId() {
return id;
}
public byte[] getKey() {
return key;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(contentType);
dest.writeLong(id);
dest.writeInt(this.key.length);
dest.writeByteArray(this.key);
}
}

View File

@@ -0,0 +1,26 @@
package org.whispersystems.textsecure.push;
public class PushBody {
private final int type;
private final int remoteRegistrationId;
private final byte[] body;
public PushBody(int type, int remoteRegistrationId, byte[] body) {
this.type = type;
this.remoteRegistrationId = remoteRegistrationId;
this.body = body;
}
public int getType() {
return type;
}
public byte[] getBody() {
return body;
}
public int getRemoteRegistrationId() {
return remoteRegistrationId;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class PushMessageResponse {
private List<String> success;
private List<String> failure;
public List<String> getSuccess() {
return success;
}
public List<String> getFailure() {
return failure;
}
}

View File

@@ -0,0 +1,563 @@
/**
* 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.push;
import android.util.Log;
import com.google.thoughtcrimegson.Gson;
import com.google.thoughtcrimegson.JsonParseException;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.whispersystems.libaxolotl.IdentityKey;
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.api.crypto.AttachmentCipherOutputStream;
import org.whispersystems.textsecure.push.exceptions.AuthorizationFailedException;
import org.whispersystems.textsecure.push.exceptions.ExpectationFailedException;
import org.whispersystems.textsecure.push.exceptions.MismatchedDevicesException;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.textsecure.push.exceptions.NotFoundException;
import org.whispersystems.textsecure.push.exceptions.PushNetworkException;
import org.whispersystems.textsecure.push.exceptions.RateLimitException;
import org.whispersystems.textsecure.push.exceptions.StaleDevicesException;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.BlacklistingTrustManager;
import org.whispersystems.textsecure.util.Util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
/**
*
* Network interface to the TextSecure server API.
*
* @author Moxie Marlinspike
*/
public class PushServiceSocket {
private static final String CREATE_ACCOUNT_SMS_PATH = "/v1/accounts/sms/code/%s";
private static final String CREATE_ACCOUNT_VOICE_PATH = "/v1/accounts/voice/code/%s";
private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/code/%s";
private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/";
private static final String PREKEY_METADATA_PATH = "/v2/keys/";
private static final String PREKEY_PATH = "/v2/keys/%s";
private static final String PREKEY_DEVICE_PATH = "/v2/keys/%s/%s";
private static final String SIGNED_PREKEY_PATH = "/v2/keys/signed";
private static final String DIRECTORY_TOKENS_PATH = "/v1/directory/tokens";
private static final String DIRECTORY_VERIFY_PATH = "/v1/directory/%s";
private static final String MESSAGE_PATH = "/v1/messages/%s";
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d";
private static final String ATTACHMENT_PATH = "/v1/attachments/%s";
private static final boolean ENFORCE_SSL = true;
private final String serviceUrl;
private final String localNumber;
private final String password;
private final TrustManager[] trustManagers;
public PushServiceSocket(String serviceUrl, TrustStore trustStore,
String localNumber, String password)
{
this.serviceUrl = serviceUrl;
this.localNumber = localNumber;
this.password = password;
this.trustManagers = initializeTrustManager(trustStore);
}
public void createAccount(boolean voice) throws IOException {
String path = voice ? CREATE_ACCOUNT_VOICE_PATH : CREATE_ACCOUNT_SMS_PATH;
makeRequest(String.format(path, localNumber), "GET", null);
}
public void verifyAccount(String verificationCode, String signalingKey,
boolean supportsSms, int registrationId)
throws IOException
{
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, supportsSms, registrationId);
makeRequest(String.format(VERIFY_ACCOUNT_PATH, verificationCode),
"PUT", new Gson().toJson(signalingKeyEntity));
}
public void sendReceipt(String destination, long messageId, String relay) throws IOException {
String path = String.format(RECEIPT_PATH, destination, messageId);
if (!Util.isEmpty(relay)) {
path += "?relay=" + relay;
}
makeRequest(path, "PUT", null);
}
public void registerGcmId(String gcmRegistrationId) throws IOException {
GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId);
makeRequest(REGISTER_GCM_PATH, "PUT", new Gson().toJson(registration));
}
public void unregisterGcmId() throws IOException {
makeRequest(REGISTER_GCM_PATH, "DELETE", null);
}
public void sendMessage(OutgoingPushMessageList bundle)
throws IOException
{
try {
makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", new Gson().toJson(bundle));
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(bundle.getDestination(), nfe);
}
}
public void registerPreKeys(IdentityKey identityKey,
PreKeyRecord lastResortKey,
SignedPreKeyRecord signedPreKey,
List<PreKeyRecord> records)
throws IOException
{
List<PreKeyEntity> entities = new LinkedList<>();
for (PreKeyRecord record : records) {
PreKeyEntity entity = new PreKeyEntity(record.getId(),
record.getKeyPair().getPublicKey());
entities.add(entity);
}
PreKeyEntity lastResortEntity = new PreKeyEntity(lastResortKey.getId(),
lastResortKey.getKeyPair().getPublicKey());
SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(),
signedPreKey.getKeyPair().getPublicKey(),
signedPreKey.getSignature());
makeRequest(String.format(PREKEY_PATH, ""), "PUT",
PreKeyState.toJson(new PreKeyState(entities, lastResortEntity,
signedPreKeyEntity, identityKey)));
}
public int getAvailablePreKeys() throws IOException {
String responseText = makeRequest(PREKEY_METADATA_PATH, "GET", null);
PreKeyStatus preKeyStatus = new Gson().fromJson(responseText, PreKeyStatus.class);
return preKeyStatus.getCount();
}
public List<PreKeyBundle> getPreKeys(PushAddress destination) throws IOException {
try {
String deviceId = String.valueOf(destination.getDeviceId());
if (deviceId.equals("1"))
deviceId = "*";
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(), deviceId);
if (!Util.isEmpty(destination.getRelay())) {
path = path + "?relay=" + destination.getRelay();
}
String responseText = makeRequest(path, "GET", null);
PreKeyResponse response = PreKeyResponse.fromJson(responseText);
List<PreKeyBundle> bundles = new LinkedList<>();
for (PreKeyResponseItem device : response.getDevices()) {
ECPublicKey preKey = null;
ECPublicKey signedPreKey = null;
byte[] signedPreKeySignature = null;
int preKeyId = -1;
int signedPreKeyId = -1;
if (device.getSignedPreKey() != null) {
signedPreKey = device.getSignedPreKey().getPublicKey();
signedPreKeyId = device.getSignedPreKey().getKeyId();
signedPreKeySignature = device.getSignedPreKey().getSignature();
}
if (device.getPreKey() != null) {
preKeyId = device.getPreKey().getKeyId();
preKey = device.getPreKey().getPublicKey();
}
bundles.add(new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId,
preKey, signedPreKeyId, signedPreKey, signedPreKeySignature,
response.getIdentityKey()));
}
return bundles;
} catch (JsonParseException e) {
throw new IOException(e);
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(destination.getNumber(), nfe);
}
}
public PreKeyBundle getPreKey(PushAddress destination) throws IOException {
try {
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(),
String.valueOf(destination.getDeviceId()));
if (!Util.isEmpty(destination.getRelay())) {
path = path + "?relay=" + destination.getRelay();
}
String responseText = makeRequest(path, "GET", null);
PreKeyResponse response = PreKeyResponse.fromJson(responseText);
if (response.getDevices() == null || response.getDevices().size() < 1)
throw new IOException("Empty prekey list");
PreKeyResponseItem device = response.getDevices().get(0);
ECPublicKey preKey = null;
ECPublicKey signedPreKey = null;
byte[] signedPreKeySignature = null;
int preKeyId = -1;
int signedPreKeyId = -1;
if (device.getPreKey() != null) {
preKeyId = device.getPreKey().getKeyId();
preKey = device.getPreKey().getPublicKey();
}
if (device.getSignedPreKey() != null) {
signedPreKeyId = device.getSignedPreKey().getKeyId();
signedPreKey = device.getSignedPreKey().getPublicKey();
signedPreKeySignature = device.getSignedPreKey().getSignature();
}
return new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId, preKey,
signedPreKeyId, signedPreKey, signedPreKeySignature, response.getIdentityKey());
} catch (JsonParseException e) {
throw new IOException(e);
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(destination.getNumber(), nfe);
}
}
public SignedPreKeyEntity getCurrentSignedPreKey() throws IOException {
try {
String responseText = makeRequest(SIGNED_PREKEY_PATH, "GET", null);
return SignedPreKeyEntity.fromJson(responseText);
} catch (NotFoundException e) {
Log.w("PushServiceSocket", e);
return null;
}
}
public void setCurrentSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException {
SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(),
signedPreKey.getKeyPair().getPublicKey(),
signedPreKey.getSignature());
makeRequest(SIGNED_PREKEY_PATH, "PUT", SignedPreKeyEntity.toJson(signedPreKeyEntity));
}
public long sendAttachment(PushAttachmentData attachment) throws IOException {
String response = makeRequest(String.format(ATTACHMENT_PATH, ""), "GET", null);
AttachmentDescriptor attachmentKey = new Gson().fromJson(response, AttachmentDescriptor.class);
if (attachmentKey == null || attachmentKey.getLocation() == null) {
throw new IOException("Server failed to allocate an attachment key!");
}
Log.w("PushServiceSocket", "Got attachment content location: " + attachmentKey.getLocation());
uploadAttachment("PUT", attachmentKey.getLocation(), attachment.getData(),
attachment.getDataSize(), attachment.getKey());
return attachmentKey.getId();
}
public void retrieveAttachment(String relay, long attachmentId, File destination) throws IOException {
String path = String.format(ATTACHMENT_PATH, String.valueOf(attachmentId));
if (!Util.isEmpty(relay)) {
path = path + "?relay=" + relay;
}
String response = makeRequest(path, "GET", null);
AttachmentDescriptor descriptor = new Gson().fromJson(response, AttachmentDescriptor.class);
Log.w("PushServiceSocket", "Attachment: " + attachmentId + " is at: " + descriptor.getLocation());
downloadExternalFile(descriptor.getLocation(), destination);
}
public List<ContactTokenDetails> retrieveDirectory(Set<String> contactTokens) {
try {
ContactTokenList contactTokenList = new ContactTokenList(new LinkedList<String>(contactTokens));
String response = makeRequest(DIRECTORY_TOKENS_PATH, "PUT", new Gson().toJson(contactTokenList));
ContactTokenDetailsList activeTokens = new Gson().fromJson(response, ContactTokenDetailsList.class);
return activeTokens.getContacts();
} catch (IOException ioe) {
Log.w("PushServiceSocket", ioe);
return null;
}
}
public ContactTokenDetails getContactTokenDetails(String contactToken) throws IOException {
try {
String response = makeRequest(String.format(DIRECTORY_VERIFY_PATH, contactToken), "GET", null);
return new Gson().fromJson(response, ContactTokenDetails.class);
} catch (NotFoundException nfe) {
return null;
}
}
private void downloadExternalFile(String url, File localDestination)
throws IOException
{
URL downloadUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.setRequestMethod("GET");
connection.setDoInput(true);
try {
if (connection.getResponseCode() != 200) {
throw new NonSuccessfulResponseCodeException("Bad response: " + connection.getResponseCode());
}
OutputStream output = new FileOutputStream(localDestination);
InputStream input = connection.getInputStream();
byte[] buffer = new byte[4096];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
output.close();
Log.w("PushServiceSocket", "Downloaded: " + url + " to: " + localDestination.getAbsolutePath());
} finally {
connection.disconnect();
}
}
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 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());
}
} finally {
connection.disconnect();
}
}
private String makeRequest(String urlFragment, String method, String body)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
HttpURLConnection connection = makeBaseRequest(urlFragment, method, body);
try {
String response = Util.readFully(connection.getInputStream());
connection.disconnect();
return response;
} catch (IOException ioe) {
throw new PushNetworkException(ioe);
}
}
private HttpURLConnection makeBaseRequest(String urlFragment, String method, String body)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
HttpURLConnection connection = getConnection(urlFragment, method, body);
int responseCode;
String responseMessage;
String response;
try {
responseCode = connection.getResponseCode();
responseMessage = connection.getResponseMessage();
} catch (IOException ioe) {
throw new PushNetworkException(ioe);
}
switch (responseCode) {
case 413:
connection.disconnect();
throw new RateLimitException("Rate limit exceeded: " + responseCode);
case 401:
case 403:
connection.disconnect();
throw new AuthorizationFailedException("Authorization failed!");
case 404:
connection.disconnect();
throw new NotFoundException("Not found");
case 409:
try {
response = Util.readFully(connection.getErrorStream());
} catch (IOException e) {
throw new PushNetworkException(e);
}
throw new MismatchedDevicesException(new Gson().fromJson(response, MismatchedDevices.class));
case 410:
try {
response = Util.readFully(connection.getErrorStream());
} catch (IOException e) {
throw new PushNetworkException(e);
}
throw new StaleDevicesException(new Gson().fromJson(response, StaleDevices.class));
case 417:
throw new ExpectationFailedException();
}
if (responseCode != 200 && responseCode != 204) {
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " +
responseMessage);
}
return connection;
}
private HttpURLConnection getConnection(String urlFragment, String method, String body)
throws PushNetworkException
{
try {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trustManagers, null);
URL url = new URL(String.format("%s%s", serviceUrl, urlFragment));
Log.w("PushServiceSocket", "Push service URL: " + serviceUrl);
Log.w("PushServiceSocket", "Opening URL: " + url);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
if (ENFORCE_SSL) {
((HttpsURLConnection) connection).setSSLSocketFactory(context.getSocketFactory());
((HttpsURLConnection) connection).setHostnameVerifier(new StrictHostnameVerifier());
}
connection.setRequestMethod(method);
connection.setRequestProperty("Content-Type", "application/json");
if (password != null) {
connection.setRequestProperty("Authorization", getAuthorizationHeader());
}
if (body != null) {
connection.setDoOutput(true);
}
connection.connect();
if (body != null) {
Log.w("PushServiceSocket", method + " -- " + body);
OutputStream out = connection.getOutputStream();
out.write(body.getBytes());
out.close();
}
return connection;
} catch (IOException e) {
throw new PushNetworkException(e);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (KeyManagementException e) {
throw new AssertionError(e);
}
}
private String getAuthorizationHeader() {
try {
return "Basic " + Base64.encodeBytes((localNumber + ":" + password).getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
private TrustManager[] initializeTrustManager(TrustStore trustStore) {
try {
InputStream keyStoreInputStream = trustStore.getKeyStoreInputStream();
KeyStore keyStore = KeyStore.getInstance("BKS");
keyStore.load(keyStoreInputStream, trustStore.getKeyStorePassword().toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
trustManagerFactory.init(keyStore);
return BlacklistingTrustManager.createFor(trustManagerFactory.getTrustManagers());
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException kse) {
throw new AssertionError(kse);
}
}
private static class GcmRegistrationId {
private String gcmRegistrationId;
public GcmRegistrationId() {}
public GcmRegistrationId(String gcmRegistrationId) {
this.gcmRegistrationId = gcmRegistrationId;
}
}
private static class AttachmentDescriptor {
private long id;
private String location;
public long getId() {
return id;
}
public String getLocation() {
return location;
}
}
public interface TrustStore {
public InputStream getKeyStoreInputStream();
public String getKeyStorePassword();
}
}

View File

@@ -0,0 +1,75 @@
/**
* 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.push;
import android.util.Log;
public class PushTransportDetails {
private final int messageVersion;
public PushTransportDetails(int messageVersion) {
this.messageVersion = messageVersion;
}
public byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding) {
if (messageVersion < 2) throw new AssertionError("Unknown version: " + messageVersion);
else if (messageVersion == 2) return messageWithPadding;
int paddingStart = 0;
for (int i=messageWithPadding.length-1;i>=0;i--) {
if (messageWithPadding[i] == (byte)0x80) {
paddingStart = i;
break;
} else if (messageWithPadding[i] != (byte)0x00) {
Log.w("PushTransportDetails", "Padding byte is malformed, returning unstripped padding.");
return messageWithPadding;
}
}
byte[] strippedMessage = new byte[paddingStart];
System.arraycopy(messageWithPadding, 0, strippedMessage, 0, strippedMessage.length);
return strippedMessage;
}
public byte[] getPaddedMessageBody(byte[] messageBody) {
if (messageVersion < 2) throw new AssertionError("Unknown version: " + messageVersion);
else if (messageVersion == 2) return messageBody;
// NOTE: This is dumb. We have our own padding scheme, but so does the cipher.
// The +1 -1 here is to make sure the Cipher has room to add one padding byte,
// otherwise it'll add a full 16 extra bytes.
byte[] paddedMessage = new byte[getPaddedMessageLength(messageBody.length + 1) - 1];
System.arraycopy(messageBody, 0, paddedMessage, 0, messageBody.length);
paddedMessage[messageBody.length] = (byte)0x80;
return paddedMessage;
}
private int getPaddedMessageLength(int messageLength) {
int messageLengthWithTerminator = messageLength + 1;
int messagePartCount = messageLengthWithTerminator / 160;
if (messageLengthWithTerminator % 160 != 0) {
messagePartCount++;
}
return messagePartCount * 160;
}
}

View File

@@ -0,0 +1,71 @@
package org.whispersystems.textsecure.push;
import com.google.thoughtcrimegson.GsonBuilder;
import com.google.thoughtcrimegson.JsonDeserializationContext;
import com.google.thoughtcrimegson.JsonDeserializer;
import com.google.thoughtcrimegson.JsonElement;
import com.google.thoughtcrimegson.JsonParseException;
import com.google.thoughtcrimegson.JsonPrimitive;
import com.google.thoughtcrimegson.JsonSerializationContext;
import com.google.thoughtcrimegson.JsonSerializer;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.textsecure.util.Base64;
import java.io.IOException;
import java.lang.reflect.Type;
public class SignedPreKeyEntity extends PreKeyEntity {
private byte[] signature;
public SignedPreKeyEntity() {}
public SignedPreKeyEntity(int keyId, ECPublicKey publicKey, byte[] signature) {
super(keyId, publicKey);
this.signature = signature;
}
public byte[] getSignature() {
return signature;
}
public static String toJson(SignedPreKeyEntity entity) {
GsonBuilder builder = new GsonBuilder();
return forBuilder(builder).create().toJson(entity);
}
public static SignedPreKeyEntity fromJson(String serialized) {
GsonBuilder builder = new GsonBuilder();
return forBuilder(builder).create().fromJson(serialized, SignedPreKeyEntity.class);
}
public static GsonBuilder forBuilder(GsonBuilder builder) {
return PreKeyEntity.forBuilder(builder)
.registerTypeAdapter(byte[].class, new ByteArrayJsonAdapter());
}
private static class ByteArrayJsonAdapter
implements JsonSerializer<byte[]>, JsonDeserializer<byte[]>
{
@Override
public JsonElement serialize(byte[] signature, Type type,
JsonSerializationContext jsonSerializationContext)
{
return new JsonPrimitive(Base64.encodeBytesWithoutPadding(signature));
}
@Override
public byte[] deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext)
throws JsonParseException
{
try {
return Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString());
} catch (IOException e) {
throw new JsonParseException(e);
}
}
}
}

View File

@@ -0,0 +1,12 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class StaleDevices {
private List<Integer> staleDevices;
public List<Integer> getStaleDevices() {
return staleDevices;
}
}

View File

@@ -0,0 +1,18 @@
package org.whispersystems.textsecure.push;
import java.io.IOException;
import java.util.List;
public class UnregisteredUserException extends IOException {
private final String e164number;
public UnregisteredUserException(String e164number, Exception exception) {
super(exception);
this.e164number = e164number;
}
public String getE164Number() {
return e164number;
}
}

View File

@@ -0,0 +1,7 @@
package org.whispersystems.textsecure.push.exceptions;
public class AuthorizationFailedException extends NonSuccessfulResponseCodeException {
public AuthorizationFailedException(String s) {
super(s);
}
}

View File

@@ -0,0 +1,27 @@
package org.whispersystems.textsecure.push.exceptions;
import org.whispersystems.textsecure.api.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;
}
}

View File

@@ -0,0 +1,4 @@
package org.whispersystems.textsecure.push.exceptions;
public class ExpectationFailedException extends NonSuccessfulResponseCodeException {
}

View File

@@ -0,0 +1,16 @@
package org.whispersystems.textsecure.push.exceptions;
import org.whispersystems.textsecure.push.MismatchedDevices;
public class MismatchedDevicesException extends NonSuccessfulResponseCodeException {
private final MismatchedDevices mismatchedDevices;
public MismatchedDevicesException(MismatchedDevices mismatchedDevices) {
this.mismatchedDevices = mismatchedDevices;
}
public MismatchedDevices getMismatchedDevices() {
return mismatchedDevices;
}
}

View File

@@ -0,0 +1,14 @@
package org.whispersystems.textsecure.push.exceptions;
import java.io.IOException;
public class NonSuccessfulResponseCodeException extends IOException {
public NonSuccessfulResponseCodeException() {
super();
}
public NonSuccessfulResponseCodeException(String s) {
super(s);
}
}

View File

@@ -0,0 +1,9 @@
package org.whispersystems.textsecure.push.exceptions;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
public class NotFoundException extends NonSuccessfulResponseCodeException {
public NotFoundException(String s) {
super(s);
}
}

View File

@@ -0,0 +1,13 @@
package org.whispersystems.textsecure.push.exceptions;
import java.io.IOException;
public class PushNetworkException extends IOException {
public PushNetworkException(Exception exception) {
super(exception);
}
public PushNetworkException(String s) {
super(s);
}
}

View File

@@ -0,0 +1,10 @@
package org.whispersystems.textsecure.push.exceptions;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
public class RateLimitException extends NonSuccessfulResponseCodeException {
public RateLimitException(String s) {
super(s);
}
}

View File

@@ -0,0 +1,17 @@
package org.whispersystems.textsecure.push.exceptions;
import org.whispersystems.textsecure.push.StaleDevices;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
public class StaleDevicesException extends NonSuccessfulResponseCodeException {
private final StaleDevices staleDevices;
public StaleDevicesException(StaleDevices staleDevices) {
this.staleDevices = staleDevices;
}
public StaleDevices getStaleDevices() {
return staleDevices;
}
}

View File

@@ -0,0 +1,6 @@
package org.whispersystems.textsecure.storage;
public interface CanonicalRecipient {
// public String getNumber();
public long getRecipientId();
}

View File

@@ -0,0 +1,31 @@
package org.whispersystems.textsecure.storage;
public class RecipientDevice {
public static final int DEFAULT_DEVICE_ID = 1;
private final long recipientId;
private final int deviceId;
public RecipientDevice(long recipientId, int deviceId) {
this.recipientId = recipientId;
this.deviceId = deviceId;
}
public long getRecipientId() {
return recipientId;
}
public int getDeviceId() {
return deviceId;
}
public CanonicalRecipient getRecipient() {
return new CanonicalRecipient() {
@Override
public long getRecipientId() {
return recipientId;
}
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,87 @@
/**
* 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.util;
import java.math.BigInteger;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.LinkedList;
import java.util.List;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* Trust manager that defers to a system X509 trust manager, and
* additionally rejects certificates if they have a blacklisted
* serial.
*
* @author Moxie Marlinspike
*/
public class BlacklistingTrustManager implements X509TrustManager {
private static final List<BigInteger> BLACKLIST = new LinkedList<BigInteger>() {{
add(new BigInteger("4098"));
}};
public static TrustManager[] createFor(TrustManager[] trustManagers) {
for (TrustManager trustManager : trustManagers) {
if (trustManager instanceof X509TrustManager) {
TrustManager[] results = new BlacklistingTrustManager[1];
results[0] = new BlacklistingTrustManager((X509TrustManager)trustManager);
return results;
}
}
throw new AssertionError("No X509 Trust Managers!");
}
private final X509TrustManager trustManager;
public BlacklistingTrustManager(X509TrustManager trustManager) {
this.trustManager = trustManager;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
trustManager.checkClientTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
trustManager.checkServerTrusted(chain, authType);
for (X509Certificate certificate : chain) {
for (BigInteger blacklistedSerial : BLACKLIST) {
if (certificate.getSerialNumber().equals(blacklistedSerial)) {
throw new CertificateException("Blacklisted Serial: " + certificate.getSerialNumber());
}
}
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return trustManager.getAcceptedIssuers();
}
}

View File

@@ -0,0 +1,180 @@
/**
* 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.util;
public class Conversions {
public static byte intsToByteHighAndLow(int highValue, int lowValue) {
return (byte)((highValue << 4 | lowValue) & 0xFF);
}
public static int highBitsToInt(byte value) {
return (value & 0xFF) >> 4;
}
public static int lowBitsToInt(byte value) {
return (value & 0xF);
}
public static int highBitsToMedium(int value) {
return (value >> 12);
}
public static int lowBitsToMedium(int value) {
return (value & 0xFFF);
}
public static byte[] shortToByteArray(int value) {
byte[] bytes = new byte[2];
shortToByteArray(bytes, 0, value);
return bytes;
}
public static int shortToByteArray(byte[] bytes, int offset, int value) {
bytes[offset+1] = (byte)value;
bytes[offset] = (byte)(value >> 8);
return 2;
}
public static int shortToLittleEndianByteArray(byte[] bytes, int offset, int value) {
bytes[offset] = (byte)value;
bytes[offset+1] = (byte)(value >> 8);
return 2;
}
public static byte[] mediumToByteArray(int value) {
byte[] bytes = new byte[3];
mediumToByteArray(bytes, 0, value);
return bytes;
}
public static int mediumToByteArray(byte[] bytes, int offset, int value) {
bytes[offset + 2] = (byte)value;
bytes[offset + 1] = (byte)(value >> 8);
bytes[offset] = (byte)(value >> 16);
return 3;
}
public static byte[] intToByteArray(int value) {
byte[] bytes = new byte[4];
intToByteArray(bytes, 0, value);
return bytes;
}
public static int intToByteArray(byte[] bytes, int offset, int value) {
bytes[offset + 3] = (byte)value;
bytes[offset + 2] = (byte)(value >> 8);
bytes[offset + 1] = (byte)(value >> 16);
bytes[offset] = (byte)(value >> 24);
return 4;
}
public static int intToLittleEndianByteArray(byte[] bytes, int offset, int value) {
bytes[offset] = (byte)value;
bytes[offset+1] = (byte)(value >> 8);
bytes[offset+2] = (byte)(value >> 16);
bytes[offset+3] = (byte)(value >> 24);
return 4;
}
public static byte[] longToByteArray(long l) {
byte[] bytes = new byte[8];
longToByteArray(bytes, 0, l);
return bytes;
}
public static int longToByteArray(byte[] bytes, int offset, long value) {
bytes[offset + 7] = (byte)value;
bytes[offset + 6] = (byte)(value >> 8);
bytes[offset + 5] = (byte)(value >> 16);
bytes[offset + 4] = (byte)(value >> 24);
bytes[offset + 3] = (byte)(value >> 32);
bytes[offset + 2] = (byte)(value >> 40);
bytes[offset + 1] = (byte)(value >> 48);
bytes[offset] = (byte)(value >> 56);
return 8;
}
public static int longTo4ByteArray(byte[] bytes, int offset, long value) {
bytes[offset + 3] = (byte)value;
bytes[offset + 2] = (byte)(value >> 8);
bytes[offset + 1] = (byte)(value >> 16);
bytes[offset + 0] = (byte)(value >> 24);
return 4;
}
public static int byteArrayToShort(byte[] bytes) {
return byteArrayToShort(bytes, 0);
}
public static int byteArrayToShort(byte[] bytes, int offset) {
return
(bytes[offset] & 0xff) << 8 | (bytes[offset + 1] & 0xff);
}
// The SSL patented 3-byte Value.
public static int byteArrayToMedium(byte[] bytes, int offset) {
return
(bytes[offset] & 0xff) << 16 |
(bytes[offset + 1] & 0xff) << 8 |
(bytes[offset + 2] & 0xff);
}
public static int byteArrayToInt(byte[] bytes) {
return byteArrayToInt(bytes, 0);
}
public static int byteArrayToInt(byte[] bytes, int offset) {
return
(bytes[offset] & 0xff) << 24 |
(bytes[offset + 1] & 0xff) << 16 |
(bytes[offset + 2] & 0xff) << 8 |
(bytes[offset + 3] & 0xff);
}
public static int byteArrayToIntLittleEndian(byte[] bytes, int offset) {
return
(bytes[offset + 3] & 0xff) << 24 |
(bytes[offset + 2] & 0xff) << 16 |
(bytes[offset + 1] & 0xff) << 8 |
(bytes[offset] & 0xff);
}
public static long byteArrayToLong(byte[] bytes) {
return byteArrayToLong(bytes, 0);
}
public static long byteArray4ToLong(byte[] bytes, int offset) {
return
((bytes[offset + 0] & 0xffL) << 24) |
((bytes[offset + 1] & 0xffL) << 16) |
((bytes[offset + 2] & 0xffL) << 8) |
((bytes[offset + 3] & 0xffL));
}
public static long byteArrayToLong(byte[] bytes, int offset) {
return
((bytes[offset] & 0xffL) << 56) |
((bytes[offset + 1] & 0xffL) << 48) |
((bytes[offset + 2] & 0xffL) << 40) |
((bytes[offset + 3] & 0xffL) << 32) |
((bytes[offset + 4] & 0xffL) << 24) |
((bytes[offset + 5] & 0xffL) << 16) |
((bytes[offset + 6] & 0xffL) << 8) |
((bytes[offset + 7] & 0xffL));
}
}

View File

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

View File

@@ -0,0 +1,6 @@
package org.whispersystems.textsecure.util;
public interface FutureTaskListener<V> {
public void onSuccess(V result);
public void onFailure(Throwable error);
}

View File

@@ -0,0 +1,138 @@
/**
* 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.util;
import java.io.IOException;
/**
* Utility for generating hex dumps.
*/
public class Hex {
private final static int HEX_DIGITS_START = 10;
private final static int ASCII_TEXT_START = HEX_DIGITS_START + (16*2 + (16/2));
final static String EOL = System.getProperty("line.separator");
private final static char[] HEX_DIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
public static String toString(byte[] bytes) {
return toString(bytes, 0, bytes.length);
}
public static String toString(byte[] bytes, int offset, int length) {
StringBuffer buf = new StringBuffer();
for (int i = 0; i < length; i++) {
appendHexChar(buf, bytes[offset + i]);
buf.append(' ');
}
return buf.toString();
}
public static String toStringCondensed(byte[] bytes) {
StringBuffer buf = new StringBuffer();
for (int i=0;i<bytes.length;i++) {
appendHexChar(buf, bytes[i]);
}
return buf.toString();
}
public static byte[] fromStringCondensed(String encoded) throws IOException {
final char[] data = encoded.toCharArray();
final int len = data.length;
if ((len & 0x01) != 0) {
throw new IOException("Odd number of characters.");
}
final byte[] out = new byte[len >> 1];
// two characters form the hex value.
for (int i = 0, j = 0; j < len; i++) {
int f = Character.digit(data[j], 16) << 4;
j++;
f = f | Character.digit(data[j], 16);
j++;
out[i] = (byte) (f & 0xFF);
}
return out;
}
public static String dump(byte[] bytes) {
return dump(bytes, 0, bytes.length);
}
public static String dump(byte[] bytes, int offset, int length) {
StringBuffer buf = new StringBuffer();
int lines = ((length - 1) / 16) + 1;
int lineOffset;
int lineLength;
for (int i = 0; i < lines; i++) {
lineOffset = (i * 16) + offset;
lineLength = Math.min(16, (length - (i * 16)));
appendDumpLine(buf, i, bytes, lineOffset, lineLength);
buf.append(EOL);
}
return buf.toString();
}
private static void appendDumpLine(StringBuffer buf, int line, byte[] bytes, int lineOffset, int lineLength) {
buf.append(HEX_DIGITS[(line >> 28) & 0xf]);
buf.append(HEX_DIGITS[(line >> 24) & 0xf]);
buf.append(HEX_DIGITS[(line >> 20) & 0xf]);
buf.append(HEX_DIGITS[(line >> 16) & 0xf]);
buf.append(HEX_DIGITS[(line >> 12) & 0xf]);
buf.append(HEX_DIGITS[(line >> 8) & 0xf]);
buf.append(HEX_DIGITS[(line >> 4) & 0xf]);
buf.append(HEX_DIGITS[(line ) & 0xf]);
buf.append(": ");
for (int i = 0; i < 16; i++) {
int idx = i + lineOffset;
if (i < lineLength) {
int b = bytes[idx];
appendHexChar(buf, b);
} else {
buf.append(" ");
}
if ((i % 2) == 1) {
buf.append(' ');
}
}
for (int i = 0; i < 16 && i < lineLength; i++) {
int idx = i + lineOffset;
int b = bytes[idx];
if (b >= 0x20 && b <= 0x7e) {
buf.append((char)b);
} else {
buf.append('.');
}
}
}
private static void appendHexChar(StringBuffer buf, int b) {
buf.append(HEX_DIGITS[(b >> 4) & 0xf]);
buf.append(HEX_DIGITS[b & 0xf]);
}
}

View File

@@ -0,0 +1,7 @@
package org.whispersystems.textsecure.util;
public class InvalidNumberException extends Throwable {
public InvalidNumberException(String s) {
super(s);
}
}

View File

@@ -0,0 +1,52 @@
package org.whispersystems.textsecure.util;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ListenableFutureTask<V> extends FutureTask<V> {
private final List<FutureTaskListener<V>> listeners;
public ListenableFutureTask(Callable<V> callable) {
super(callable);
this.listeners = new LinkedList<>();
}
public synchronized void addListener(FutureTaskListener<V> listener) {
if (this.isDone()) {
callback(listener);
return;
}
listeners.add(listener);
}
public synchronized boolean removeListener(FutureTaskListener<V> listener) {
return listeners.remove(listener);
}
@Override
protected synchronized void done() {
callback();
}
private void callback() {
for (FutureTaskListener<V> listener : listeners) {
callback(listener);
}
}
private void callback(FutureTaskListener<V> listener) {
if (listener != null) {
try {
listener.onSuccess(get());
} catch (ExecutionException ee) {
listener.onFailure(ee);
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
}
}

View File

@@ -0,0 +1,121 @@
package org.whispersystems.textsecure.util;
import android.util.Log;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import java.util.Locale;
/**
* Phone number formats are a pain.
*
* @author Moxie Marlinspike
*
*/
public class PhoneNumberFormatter {
public static boolean isValidNumber(String number) {
return number.matches("^\\+[0-9]{10,}");
}
private static String impreciseFormatNumber(String number, String localNumber)
throws InvalidNumberException
{
number = number.replaceAll("[^0-9+]", "");
if (number.charAt(0) == '+')
return number;
if (localNumber.charAt(0) == '+')
localNumber = localNumber.substring(1);
if (localNumber.length() == number.length() || number.length() > localNumber.length())
return "+" + number;
int difference = localNumber.length() - number.length();
return "+" + localNumber.substring(0, difference) + number;
}
public static String formatNumberInternational(String number) {
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
PhoneNumber parsedNumber = util.parse(number, null);
return util.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL);
} catch (NumberParseException e) {
Log.w("PhoneNumberFormatter", e);
return number;
}
}
public static String formatNumber(String number, String localNumber)
throws InvalidNumberException
{
if (number.contains("@")) {
throw new InvalidNumberException("Possible attempt to use email address.");
}
number = number.replaceAll("[^0-9+]", "");
if (number.length() == 0) {
throw new InvalidNumberException("No valid characters found.");
}
if (number.charAt(0) == '+')
return number;
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
PhoneNumber localNumberObject = util.parse(localNumber, null);
String localCountryCode = util.getRegionCodeForNumber(localNumberObject);
Log.w("PhoneNumberFormatter", "Got local CC: " + localCountryCode);
PhoneNumber numberObject = util.parse(number, localCountryCode);
return util.format(numberObject, PhoneNumberFormat.E164);
} catch (NumberParseException e) {
Log.w("PhoneNumberFormatter", e);
return impreciseFormatNumber(number, localNumber);
}
}
public static String getRegionDisplayName(String regionCode) {
return (regionCode == null || regionCode.equals("ZZ") || regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY))
? "Unknown country" : new Locale("", regionCode).getDisplayCountry(Locale.getDefault());
}
public static String formatE164(String countryCode, String number) {
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
int parsedCountryCode = Integer.parseInt(countryCode);
PhoneNumber parsedNumber = util.parse(number,
util.getRegionCodeForCountryCode(parsedCountryCode));
return util.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
} catch (NumberParseException npe) {
Log.w("CreateAccountActivity", npe);
} catch (NumberFormatException nfe) {
Log.w("CreateAccountActivity", nfe);
}
return "+" +
countryCode.replaceAll("[^0-9]", "").replaceAll("^0*", "") +
number.replaceAll("[^0-9]", "");
}
public static String getInternationalFormatFromE164(String e164number) {
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
PhoneNumber parsedNumber = util.parse(e164number, null);
return util.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL);
} catch (NumberParseException e) {
Log.w("PhoneNumberFormatter", e);
return e164number;
}
}
}

View File

@@ -0,0 +1,219 @@
package org.whispersystems.textsecure.util;
import android.content.Context;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableContainer;
import android.graphics.drawable.StateListDrawable;
import android.telephony.TelephonyManager;
import android.view.View;
import android.widget.EditText;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.ParseException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
public class Util {
public static byte[] combine(byte[]... elements) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (byte[] element : elements) {
baos.write(element);
}
return baos.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static byte[][] split(byte[] input, int firstLength, int secondLength) {
byte[][] parts = new byte[2][];
parts[0] = new byte[firstLength];
System.arraycopy(input, 0, parts[0], 0, firstLength);
parts[1] = new byte[secondLength];
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
return parts;
}
public static byte[][] split(byte[] input, int firstLength, int secondLength, int thirdLength)
throws ParseException
{
if (input == null || firstLength < 0 || secondLength < 0 || thirdLength < 0 ||
input.length < firstLength + secondLength + thirdLength)
{
throw new ParseException("Input too small: " + (input == null ? null : Hex.toString(input)), 0);
}
byte[][] parts = new byte[3][];
parts[0] = new byte[firstLength];
System.arraycopy(input, 0, parts[0], 0, firstLength);
parts[1] = new byte[secondLength];
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
parts[2] = new byte[thirdLength];
System.arraycopy(input, firstLength + secondLength, parts[2], 0, thirdLength);
return parts;
}
public static byte[] trim(byte[] input, int length) {
byte[] result = new byte[length];
System.arraycopy(input, 0, result, 0, result.length);
return result;
}
public static boolean isEmpty(String value) {
return value == null || value.trim().length() == 0;
}
public static boolean isEmpty(EditText value) {
return value == null || value.getText() == null || isEmpty(value.getText().toString());
}
public static boolean isEmpty(CharSequence value) {
return value == null || value.length() == 0;
}
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 secret;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public static String readFully(File file) throws IOException {
return readFully(new FileInputStream(file));
}
public static String readFully(InputStream in) throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
bout.write(buffer, 0, read);
}
in.close();
return new String(bout.toByteArray());
}
public static void readFully(InputStream in, byte[] buffer) throws IOException {
int offset = 0;
for (;;) {
int read = in.read(buffer, offset, buffer.length - offset);
if (read + offset < buffer.length) offset += read;
else return;
}
}
public static void copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
out.close();
}
public static String join(Collection<String> list, String delimiter) {
StringBuilder result = new StringBuilder();
int i=0;
for (String item : list) {
result.append(item);
if (++i < list.size())
result.append(delimiter);
}
return result.toString();
}
public static List<String> split(String source, String delimiter) {
List<String> results = new LinkedList<String>();
if (isEmpty(source)) {
return results;
}
String[] elements = source.split(delimiter);
for (String element : elements) {
results.add(element);
}
return results;
}
public static String getDeviceE164Number(Context context) {
String localNumber = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE))
.getLine1Number();
if (!org.whispersystems.textsecure.util.Util.isEmpty(localNumber) &&
!localNumber.startsWith("+"))
{
if (localNumber.length() == 10) localNumber = "+1" + localNumber;
else localNumber = "+" + localNumber;
return localNumber;
}
return null;
}
public static SecureRandom getSecureRandom() {
try {
return SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
/*
* source: http://stackoverflow.com/a/9500334
*/
public static void fixBackgroundRepeat(Drawable bg) {
if (bg != null) {
if (bg instanceof BitmapDrawable) {
BitmapDrawable bmp = (BitmapDrawable) bg;
bmp.mutate();
bmp.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
}
}
}
}

View File

@@ -0,0 +1,285 @@
/*
* Copyright 2009 ZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.whispersystems.textsecure.zxing.integration;
import android.app.AlertDialog;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
/**
* <p>A utility class which helps ease integration with Barcode Scanner via {@link Intent}s. This is a simple
* way to invoke barcode scanning and receive the result, without any need to integrate, modify, or learn the
* project's source code.</p>
*
* <h2>Initiating a barcode scan</h2>
*
* <p>Integration is essentially as easy as calling {@link #initiateScan(Activity)} and waiting
* for the result in your app.</p>
*
* <p>It does require that the Barcode Scanner application is installed. The
* {@link #initiateScan(Activity)} method will prompt the user to download the application, if needed.</p>
*
* <p>There are a few steps to using this integration. First, your {@link Activity} must implement
* the method {@link Activity#onActivityResult(int, int, Intent)} and include a line of code like this:</p>
*
* <p>{@code
* public void onActivityResult(int requestCode, int resultCode, Intent intent) {
* IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
* if (scanResult != null) {
* // handle scan result
* }
* // else continue with any other code you need in the method
* ...
* }
* }</p>
*
* <p>This is where you will handle a scan result.
* Second, just call this in response to a user action somewhere to begin the scan process:</p>
*
* <p>{@code IntentIntegrator.initiateScan(yourActivity);}</p>
*
* <p>You can use {@link #initiateScan(Activity, CharSequence, CharSequence, CharSequence, CharSequence)} or
* {@link #initiateScan(Activity, int, int, int, int)} to customize the download prompt with
* different text labels.</p>
*
* <p>Note that {@link #initiateScan(Activity)} returns an {@link AlertDialog} which is non-null if the
* user was prompted to download the application. This lets the calling app potentially manage the dialog.
* In particular, ideally, the app dismisses the dialog if it's still active in its {@link Activity#onPause()}
* method.</p>
*
* <h2>Sharing text via barcode</h2>
*
* <p>To share text, encoded as a QR Code on-screen, similarly, see {@link #shareText(Activity, CharSequence)}.</p>
*
* <p>Some code, particularly download integration, was contributed from the Anobiit application.</p>
*
* @author Sean Owen
* @author Fred Lin
* @author Isaac Potoczny-Jones
* @author Brad Drehmer
*/
public final class IntentIntegrator {
public static final int REQUEST_CODE = 0x0ba7c0de; // get it?
public static final String DEFAULT_TITLE = "Install Barcode Scanner?";
public static final String DEFAULT_MESSAGE =
"This application requires Barcode Scanner. Would you like to install it?";
public static final String DEFAULT_YES = "Yes";
public static final String DEFAULT_NO = "No";
// supported barcode formats
public static final String PRODUCT_CODE_TYPES = "UPC_A,UPC_E,EAN_8,EAN_13";
public static final String ONE_D_CODE_TYPES = PRODUCT_CODE_TYPES + ",CODE_39,CODE_93,CODE_128";
public static final String QR_CODE_TYPES = "QR_CODE";
public static final String ALL_CODE_TYPES = null;
private IntentIntegrator() {
}
/**
* See {@link #initiateScan(Activity, CharSequence, CharSequence, CharSequence, CharSequence)} --
* same, but uses default English labels.
*/
public static AlertDialog initiateScan(Activity activity) {
return initiateScan(activity, DEFAULT_TITLE, DEFAULT_MESSAGE, DEFAULT_YES, DEFAULT_NO);
}
/**
* See {@link #initiateScan(Activity, CharSequence, CharSequence, CharSequence, CharSequence)} --
* same, but takes string IDs which refer
* to the {@link Activity}'s resource bundle entries.
*/
public static AlertDialog initiateScan(Activity activity,
int stringTitle,
int stringMessage,
int stringButtonYes,
int stringButtonNo) {
return initiateScan(activity,
activity.getString(stringTitle),
activity.getString(stringMessage),
activity.getString(stringButtonYes),
activity.getString(stringButtonNo));
}
/**
* See {@link #initiateScan(Activity, CharSequence, CharSequence, CharSequence, CharSequence, CharSequence)} --
* same, but scans for all supported barcode types.
* @param stringTitle title of dialog prompting user to download Barcode Scanner
* @param stringMessage text of dialog prompting user to download Barcode Scanner
* @param stringButtonYes text of button user clicks when agreeing to download
* Barcode Scanner (e.g. "Yes")
* @param stringButtonNo text of button user clicks when declining to download
* Barcode Scanner (e.g. "No")
* @return an {@link AlertDialog} if the user was prompted to download the app,
* null otherwise
*/
public static AlertDialog initiateScan(Activity activity,
CharSequence stringTitle,
CharSequence stringMessage,
CharSequence stringButtonYes,
CharSequence stringButtonNo) {
return initiateScan(activity,
stringTitle,
stringMessage,
stringButtonYes,
stringButtonNo,
ALL_CODE_TYPES);
}
/**
* Invokes scanning.
*
* @param stringTitle title of dialog prompting user to download Barcode Scanner
* @param stringMessage text of dialog prompting user to download Barcode Scanner
* @param stringButtonYes text of button user clicks when agreeing to download
* Barcode Scanner (e.g. "Yes")
* @param stringButtonNo text of button user clicks when declining to download
* Barcode Scanner (e.g. "No")
* @param stringDesiredBarcodeFormats a comma separated list of codes you would
* like to scan for.
* @return an {@link AlertDialog} if the user was prompted to download the app,
* null otherwise
* @throws InterruptedException if timeout expires before a scan completes
*/
public static AlertDialog initiateScan(Activity activity,
CharSequence stringTitle,
CharSequence stringMessage,
CharSequence stringButtonYes,
CharSequence stringButtonNo,
CharSequence stringDesiredBarcodeFormats) {
Intent intentScan = new Intent("com.google.zxing.client.android.SCAN");
intentScan.addCategory(Intent.CATEGORY_DEFAULT);
// check which types of codes to scan for
if (stringDesiredBarcodeFormats != null) {
// set the desired barcode types
intentScan.putExtra("SCAN_FORMATS", stringDesiredBarcodeFormats);
}
try {
activity.startActivityForResult(intentScan, REQUEST_CODE);
return null;
} catch (ActivityNotFoundException e) {
return showDownloadDialog(activity, stringTitle, stringMessage, stringButtonYes, stringButtonNo);
}
}
private static AlertDialog showDownloadDialog(final Activity activity,
CharSequence stringTitle,
CharSequence stringMessage,
CharSequence stringButtonYes,
CharSequence stringButtonNo) {
AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity);
downloadDialog.setTitle(stringTitle);
downloadDialog.setMessage(stringMessage);
downloadDialog.setPositiveButton(stringButtonYes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialogInterface, int i) {
Uri uri = Uri.parse("market://search?q=pname:com.google.zxing.client.android");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
activity.startActivity(intent);
}
});
downloadDialog.setNegativeButton(stringButtonNo, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialogInterface, int i) {}
});
return downloadDialog.show();
}
/**
* <p>Call this from your {@link Activity}'s
* {@link Activity#onActivityResult(int, int, Intent)} method.</p>
*
* @return null if the event handled here was not related to {@link IntentIntegrator}, or
* else an {@link IntentResult} containing the result of the scan. If the user cancelled scanning,
* the fields will be null.
*/
public static IntentResult parseActivityResult(int requestCode, int resultCode, Intent intent) {
if (requestCode == REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
String contents = intent.getStringExtra("SCAN_RESULT");
String formatName = intent.getStringExtra("SCAN_RESULT_FORMAT");
return new IntentResult(contents, formatName);
} else {
return new IntentResult(null, null);
}
}
return null;
}
/**
* See {@link #shareText(Activity, CharSequence, CharSequence, CharSequence, CharSequence, CharSequence)} --
* same, but uses default English labels.
*/
public static void shareText(Activity activity, CharSequence text) {
shareText(activity, text, DEFAULT_TITLE, DEFAULT_MESSAGE, DEFAULT_YES, DEFAULT_NO);
}
/**
* See {@link #shareText(Activity, CharSequence, CharSequence, CharSequence, CharSequence, CharSequence)} --
* same, but takes string IDs which refer to the {@link Activity}'s resource bundle entries.
*/
public static void shareText(Activity activity,
CharSequence text,
int stringTitle,
int stringMessage,
int stringButtonYes,
int stringButtonNo) {
shareText(activity,
text,
activity.getString(stringTitle),
activity.getString(stringMessage),
activity.getString(stringButtonYes),
activity.getString(stringButtonNo));
}
/**
* Shares the given text by encoding it as a barcode, such that another user can
* scan the text off the screen of the device.
*
* @param text the text string to encode as a barcode
* @param stringTitle title of dialog prompting user to download Barcode Scanner
* @param stringMessage text of dialog prompting user to download Barcode Scanner
* @param stringButtonYes text of button user clicks when agreeing to download
* Barcode Scanner (e.g. "Yes")
* @param stringButtonNo text of button user clicks when declining to download
* Barcode Scanner (e.g. "No")
*/
public static void shareText(Activity activity,
CharSequence text,
CharSequence stringTitle,
CharSequence stringMessage,
CharSequence stringButtonYes,
CharSequence stringButtonNo) {
Intent intent = new Intent();
intent.setAction("com.google.zxing.client.android.ENCODE");
intent.putExtra("ENCODE_TYPE", "TEXT_TYPE");
intent.putExtra("ENCODE_DATA", text);
try {
activity.startActivity(intent);
} catch (ActivityNotFoundException e) {
showDownloadDialog(activity, stringTitle, stringMessage, stringButtonYes, stringButtonNo);
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2009 ZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.whispersystems.textsecure.zxing.integration;
/**
* <p>Encapsulates the result of a barcode scan invoked through {@link IntentIntegrator}.</p>
*
* @author Sean Owen
*/
public final class IntentResult {
private final String contents;
private final String formatName;
IntentResult(String contents, String formatName) {
this.contents = contents;
this.formatName = formatName;
}
/**
* @return raw content of barcode
*/
public String getContents() {
return contents;
}
/**
* @return name of format, like "QR_CODE", "UPC_A". See <code>BarcodeFormat</code> for more format names.
*/
public String getFormatName() {
return formatName;
}
}