Rename package

This commit is contained in:
AsamK 2015-12-13 11:05:08 +01:00
parent 5859e7b9f7
commit 27d9424f1e
17 changed files with 27 additions and 20 deletions

View file

@ -0,0 +1,19 @@
package org.asamk;
import org.asamk.textsecure.AttachmentInvalidException;
import org.asamk.textsecure.GroupNotFoundException;
import org.freedesktop.dbus.DBusInterface;
import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
import java.io.IOException;
import java.util.List;
public interface TextSecure extends DBusInterface {
void sendMessage(String message, List<String> attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
void sendMessage(String message, List<String> attachments, List<String> recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions;
void sendGroupMessage(String message, List<String> attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException;
}

View file

@ -0,0 +1,14 @@
package org.asamk.textsecure;
public class AttachmentInvalidException extends Exception {
private final String attachment;
public AttachmentInvalidException(String attachment, Exception e) {
super(e);
this.attachment = attachment;
}
public String getAttachment() {
return attachment;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
package org.asamk.textsecure;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
public class GroupInfo {
@JsonProperty
public final byte[] groupId;
@JsonProperty
public String name;
@JsonProperty
public Set<String> members = new HashSet<>();
@JsonProperty
public long avatarId;
public GroupInfo(byte[] groupId) {
this.groupId = groupId;
}
public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection<String> members, @JsonProperty("avatarId") long avatarId) {
this.groupId = groupId;
this.name = name;
this.members.addAll(members);
this.avatarId = avatarId;
}
}

View file

@ -0,0 +1,14 @@
package org.asamk.textsecure;
public class GroupNotFoundException extends Exception {
private final byte[] groupId;
public GroupNotFoundException(byte[] groupId) {
super();
this.groupId = groupId;
}
public byte[] getGroupId() {
return groupId;
}
}

View file

@ -0,0 +1,150 @@
package org.asamk.textsecure;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import java.util.List;
class JsonAxolotlStore implements AxolotlStore {
@JsonProperty("preKeys")
@JsonDeserialize(using = JsonPreKeyStore.JsonPreKeyStoreDeserializer.class)
@JsonSerialize(using = JsonPreKeyStore.JsonPreKeyStoreSerializer.class)
protected JsonPreKeyStore preKeyStore;
@JsonProperty("sessionStore")
@JsonDeserialize(using = JsonSessionStore.JsonSessionStoreDeserializer.class)
@JsonSerialize(using = JsonSessionStore.JsonPreKeyStoreSerializer.class)
protected JsonSessionStore sessionStore;
@JsonProperty("signedPreKeyStore")
@JsonDeserialize(using = JsonSignedPreKeyStore.JsonSignedPreKeyStoreDeserializer.class)
@JsonSerialize(using = JsonSignedPreKeyStore.JsonSignedPreKeyStoreSerializer.class)
protected JsonSignedPreKeyStore signedPreKeyStore;
@JsonProperty("identityKeyStore")
@JsonDeserialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreDeserializer.class)
@JsonSerialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreSerializer.class)
protected JsonIdentityKeyStore identityKeyStore;
public JsonAxolotlStore() {
}
public JsonAxolotlStore(JsonPreKeyStore preKeyStore, JsonSessionStore sessionStore, JsonSignedPreKeyStore signedPreKeyStore, JsonIdentityKeyStore identityKeyStore) {
this.preKeyStore = preKeyStore;
this.sessionStore = sessionStore;
this.signedPreKeyStore = signedPreKeyStore;
this.identityKeyStore = identityKeyStore;
}
public JsonAxolotlStore(IdentityKeyPair identityKeyPair, int registrationId) {
preKeyStore = new JsonPreKeyStore();
sessionStore = new JsonSessionStore();
signedPreKeyStore = new JsonSignedPreKeyStore();
this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId);
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyStore.getIdentityKeyPair();
}
@Override
public int getLocalRegistrationId() {
return identityKeyStore.getLocalRegistrationId();
}
@Override
public void saveIdentity(String name, IdentityKey identityKey) {
identityKeyStore.saveIdentity(name, identityKey);
}
@Override
public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
return identityKeyStore.isTrustedIdentity(name, identityKey);
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
return preKeyStore.loadPreKey(preKeyId);
}
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
preKeyStore.storePreKey(preKeyId, record);
}
@Override
public boolean containsPreKey(int preKeyId) {
return preKeyStore.containsPreKey(preKeyId);
}
@Override
public void removePreKey(int preKeyId) {
preKeyStore.removePreKey(preKeyId);
}
@Override
public SessionRecord loadSession(AxolotlAddress address) {
return sessionStore.loadSession(address);
}
@Override
public List<Integer> getSubDeviceSessions(String name) {
return sessionStore.getSubDeviceSessions(name);
}
@Override
public void storeSession(AxolotlAddress address, SessionRecord record) {
sessionStore.storeSession(address, record);
}
@Override
public boolean containsSession(AxolotlAddress address) {
return sessionStore.containsSession(address);
}
@Override
public void deleteSession(AxolotlAddress address) {
sessionStore.deleteSession(address);
}
@Override
public void deleteAllSessions(String name) {
sessionStore.deleteAllSessions(name);
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
return signedPreKeyStore.loadSignedPreKey(signedPreKeyId);
}
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
return signedPreKeyStore.loadSignedPreKeys();
}
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
}
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
return signedPreKeyStore.containsSignedPreKey(signedPreKeyId);
}
@Override
public void removeSignedPreKey(int signedPreKeyId) {
signedPreKeyStore.removeSignedPreKey(signedPreKeyId);
}
}

View file

@ -0,0 +1,54 @@
package org.asamk.textsecure;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class JsonGroupStore {
@JsonProperty("groups")
@JsonSerialize(using = JsonGroupStore.MapToListSerializer.class)
@JsonDeserialize(using = JsonGroupStore.GroupsDeserializer.class)
private Map<String, GroupInfo> groups = new HashMap<>();
private static final ObjectMapper jsonProcessot = new ObjectMapper();
void updateGroup(GroupInfo group) {
groups.put(Base64.encodeBytes(group.groupId), group);
}
GroupInfo getGroup(byte[] groupId) throws GroupNotFoundException {
GroupInfo g = groups.get(Base64.encodeBytes(groupId));
if (g == null) {
throw new GroupNotFoundException(groupId);
}
return g;
}
public static class MapToListSerializer extends JsonSerializer<Map<?, ?>> {
@Override
public void serialize(final Map<?, ?> value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
jgen.writeObject(value.values());
}
}
public static class GroupsDeserializer extends JsonDeserializer<Map<String, GroupInfo>> {
@Override
public Map<String, GroupInfo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
Map<String, GroupInfo> groups = new HashMap<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {
GroupInfo g = jsonProcessot.treeToValue(n, GroupInfo.class);
groups.put(Base64.encodeBytes(g.groupId), g);
}
return groups;
}
}
}

View file

@ -0,0 +1,107 @@
package org.asamk.textsecure;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.state.IdentityKeyStore;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
class JsonIdentityKeyStore implements IdentityKeyStore {
private final Map<String, IdentityKey> trustedKeys = new HashMap<>();
private final IdentityKeyPair identityKeyPair;
private final int localRegistrationId;
public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) {
this.identityKeyPair = identityKeyPair;
this.localRegistrationId = localRegistrationId;
}
public void addTrustedKeys(Map<String, IdentityKey> keyMap) {
trustedKeys.putAll(keyMap);
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyPair;
}
@Override
public int getLocalRegistrationId() {
return localRegistrationId;
}
@Override
public void saveIdentity(String name, IdentityKey identityKey) {
trustedKeys.put(name, identityKey);
}
@Override
public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
IdentityKey trusted = trustedKeys.get(name);
return (trusted == null || trusted.equals(identityKey));
}
public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer<JsonIdentityKeyStore> {
@Override
public JsonIdentityKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
try {
int localRegistrationId = node.get("registrationId").asInt();
IdentityKeyPair identityKeyPair = new IdentityKeyPair(Base64.decode(node.get("identityKey").asText()));
Map<String, IdentityKey> trustedKeyMap = new HashMap<>();
JsonNode trustedKeysNode = node.get("trustedKeys");
if (trustedKeysNode.isArray()) {
for (JsonNode trustedKey : trustedKeysNode) {
String trustedKeyName = trustedKey.get("name").asText();
try {
trustedKeyMap.put(trustedKeyName, new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0));
} catch (InvalidKeyException | IOException e) {
System.out.println(String.format("Error while decoding key for: %s", trustedKeyName));
}
}
}
JsonIdentityKeyStore keyStore = new JsonIdentityKeyStore(identityKeyPair, localRegistrationId);
keyStore.addTrustedKeys(trustedKeyMap);
return keyStore;
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
}
public static class JsonIdentityKeyStoreSerializer extends JsonSerializer<JsonIdentityKeyStore> {
@Override
public void serialize(JsonIdentityKeyStore jsonIdentityKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
json.writeStartObject();
json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId());
json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize()));
json.writeArrayFieldStart("trustedKeys");
for (Map.Entry<String, IdentityKey> trustedKey : jsonIdentityKeyStore.trustedKeys.entrySet()) {
json.writeStartObject();
json.writeStringField("name", trustedKey.getKey());
json.writeStringField("identityKey", Base64.encodeBytes(trustedKey.getValue().serialize()));
json.writeEndObject();
}
json.writeEndArray();
json.writeEndObject();
}
}
}

View file

@ -0,0 +1,97 @@
package org.asamk.textsecure;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.PreKeyStore;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
class JsonPreKeyStore implements PreKeyStore {
private final Map<Integer, byte[]> store = new HashMap<>();
public JsonPreKeyStore() {
}
public void addPreKeys(Map<Integer, byte[]> preKeys) {
store.putAll(preKeys);
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
try {
if (!store.containsKey(preKeyId)) {
throw new InvalidKeyIdException("No such prekeyrecord!");
}
return new PreKeyRecord(store.get(preKeyId));
} catch (IOException e) {
throw new AssertionError(e);
}
}
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
store.put(preKeyId, record.serialize());
}
@Override
public boolean containsPreKey(int preKeyId) {
return store.containsKey(preKeyId);
}
@Override
public void removePreKey(int preKeyId) {
store.remove(preKeyId);
}
public static class JsonPreKeyStoreDeserializer extends JsonDeserializer<JsonPreKeyStore> {
@Override
public JsonPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Map<Integer, byte[]> preKeyMap = new HashMap<>();
if (node.isArray()) {
for (JsonNode preKey : node) {
Integer preKeyId = preKey.get("id").asInt();
try {
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
} catch (IOException e) {
System.out.println(String.format("Error while decoding prekey for: %s", preKeyId));
}
}
}
JsonPreKeyStore keyStore = new JsonPreKeyStore();
keyStore.addPreKeys(preKeyMap);
return keyStore;
}
}
public static class JsonPreKeyStoreSerializer extends JsonSerializer<JsonPreKeyStore> {
@Override
public void serialize(JsonPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
json.writeStartArray();
for (Map.Entry<Integer, byte[]> preKey : jsonPreKeyStore.store.entrySet()) {
json.writeStartObject();
json.writeNumberField("id", preKey.getKey());
json.writeStringField("record", Base64.encodeBytes(preKey.getValue()));
json.writeEndObject();
}
json.writeEndArray();
}
}
}

View file

@ -0,0 +1,119 @@
package org.asamk.textsecure;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SessionStore;
import java.io.IOException;
import java.util.*;
class JsonSessionStore implements SessionStore {
private final Map<AxolotlAddress, byte[]> sessions = new HashMap<>();
public JsonSessionStore() {
}
public void addSessions(Map<AxolotlAddress, byte[]> sessions) {
this.sessions.putAll(sessions);
}
@Override
public synchronized SessionRecord loadSession(AxolotlAddress remoteAddress) {
try {
if (containsSession(remoteAddress)) {
return new SessionRecord(sessions.get(remoteAddress));
} else {
return new SessionRecord();
}
} catch (IOException e) {
throw new AssertionError(e);
}
}
@Override
public synchronized List<Integer> getSubDeviceSessions(String name) {
List<Integer> deviceIds = new LinkedList<>();
for (AxolotlAddress key : sessions.keySet()) {
if (key.getName().equals(name) &&
key.getDeviceId() != 1) {
deviceIds.add(key.getDeviceId());
}
}
return deviceIds;
}
@Override
public synchronized void storeSession(AxolotlAddress address, SessionRecord record) {
sessions.put(address, record.serialize());
}
@Override
public synchronized boolean containsSession(AxolotlAddress address) {
return sessions.containsKey(address);
}
@Override
public synchronized void deleteSession(AxolotlAddress address) {
sessions.remove(address);
}
@Override
public synchronized void deleteAllSessions(String name) {
for (AxolotlAddress key : new ArrayList<>(sessions.keySet())) {
if (key.getName().equals(name)) {
sessions.remove(key);
}
}
}
public static class JsonSessionStoreDeserializer extends JsonDeserializer<JsonSessionStore> {
@Override
public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Map<AxolotlAddress, byte[]> sessionMap = new HashMap<>();
if (node.isArray()) {
for (JsonNode session : node) {
String sessionName = session.get("name").asText();
try {
sessionMap.put(new AxolotlAddress(sessionName, session.get("deviceId").asInt()), Base64.decode(session.get("record").asText()));
} catch (IOException e) {
System.out.println(String.format("Error while decoding session for: %s", sessionName));
}
}
}
JsonSessionStore sessionStore = new JsonSessionStore();
sessionStore.addSessions(sessionMap);
return sessionStore;
}
}
public static class JsonPreKeyStoreSerializer extends JsonSerializer<JsonSessionStore> {
@Override
public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
json.writeStartArray();
for (Map.Entry<AxolotlAddress, byte[]> preKey : jsonSessionStore.sessions.entrySet()) {
json.writeStartObject();
json.writeStringField("name", preKey.getKey().getName());
json.writeNumberField("deviceId", preKey.getKey().getDeviceId());
json.writeStringField("record", Base64.encodeBytes(preKey.getValue()));
json.writeEndObject();
}
json.writeEndArray();
}
}
}

View file

@ -0,0 +1,114 @@
package org.asamk.textsecure;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
class JsonSignedPreKeyStore implements SignedPreKeyStore {
private final Map<Integer, byte[]> store = new HashMap<>();
public JsonSignedPreKeyStore() {
}
public void addSignedPreKeys(Map<Integer, byte[]> preKeys) {
store.putAll(preKeys);
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
try {
if (!store.containsKey(signedPreKeyId)) {
throw new InvalidKeyIdException("No such signedprekeyrecord! " + signedPreKeyId);
}
return new SignedPreKeyRecord(store.get(signedPreKeyId));
} catch (IOException e) {
throw new AssertionError(e);
}
}
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
try {
List<SignedPreKeyRecord> results = new LinkedList<>();
for (byte[] serialized : store.values()) {
results.add(new SignedPreKeyRecord(serialized));
}
return results;
} catch (IOException e) {
throw new AssertionError(e);
}
}
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
store.put(signedPreKeyId, record.serialize());
}
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
return store.containsKey(signedPreKeyId);
}
@Override
public void removeSignedPreKey(int signedPreKeyId) {
store.remove(signedPreKeyId);
}
public static class JsonSignedPreKeyStoreDeserializer extends JsonDeserializer<JsonSignedPreKeyStore> {
@Override
public JsonSignedPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Map<Integer, byte[]> preKeyMap = new HashMap<>();
if (node.isArray()) {
for (JsonNode preKey : node) {
Integer preKeyId = preKey.get("id").asInt();
try {
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
} catch (IOException e) {
System.out.println(String.format("Error while decoding prekey for: %s", preKeyId));
}
}
}
JsonSignedPreKeyStore keyStore = new JsonSignedPreKeyStore();
keyStore.addSignedPreKeys(preKeyMap);
return keyStore;
}
}
public static class JsonSignedPreKeyStoreSerializer extends JsonSerializer<JsonSignedPreKeyStore> {
@Override
public void serialize(JsonSignedPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
json.writeStartArray();
for (Map.Entry<Integer, byte[]> signedPreKey : jsonPreKeyStore.store.entrySet()) {
json.writeStartObject();
json.writeNumberField("id", signedPreKey.getKey());
json.writeStringField("record", Base64.encodeBytes(signedPreKey.getValue()));
json.writeEndObject();
}
json.writeEndArray();
}
}
}

View file

@ -0,0 +1,545 @@
/**
* Copyright (C) 2015 AsamK
*
* 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.asamk.textsecure;
import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.*;
import org.apache.commons.io.IOUtils;
import org.asamk.TextSecure;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException;
import org.whispersystems.textsecure.api.messages.*;
import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage;
import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException;
import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
import java.io.File;
import java.io.IOException;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static final String TEXTSECURE_BUSNAME = "org.asamk.TextSecure";
public static final String TEXTSECURE_OBJECTPATH = "/org/asamk/TextSecure";
public static void main(String[] args) {
// Workaround for BKS truststore
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
Namespace ns = parseArgs(args);
if (ns == null) {
System.exit(1);
}
final String username = ns.getString("username");
Manager m;
TextSecure ts;
DBusConnection dBusConn = null;
try {
if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
try {
m = null;
int busType;
if (ns.getBoolean("dbus_system")) {
busType = DBusConnection.SYSTEM;
} else {
busType = DBusConnection.SESSION;
}
dBusConn = DBusConnection.getConnection(busType);
ts = (TextSecure) dBusConn.getRemoteObject(
TEXTSECURE_BUSNAME, TEXTSECURE_OBJECTPATH,
TextSecure.class);
} catch (DBusException e) {
e.printStackTrace();
if (dBusConn != null) {
dBusConn.disconnect();
}
System.exit(3);
return;
}
} else {
m = new Manager(username);
ts = m;
if (m.userExists()) {
try {
m.load();
} catch (Exception e) {
System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage());
System.exit(2);
return;
}
}
}
switch (ns.getString("command")) {
case "register":
if (dBusConn != null) {
System.err.println("register is not yet implementd via dbus");
System.exit(1);
}
if (!m.userHasKeys()) {
m.createNewIdentity();
}
try {
m.register(ns.getBoolean("voice"));
} catch (IOException e) {
System.err.println("Request verify error: " + e.getMessage());
System.exit(3);
}
break;
case "verify":
if (dBusConn != null) {
System.err.println("verify is not yet implementd via dbus");
System.exit(1);
}
if (!m.userHasKeys()) {
System.err.println("User has no keys, first call register.");
System.exit(1);
}
if (m.isRegistered()) {
System.err.println("User registration is already verified");
System.exit(1);
}
try {
m.verifyAccount(ns.getString("verificationCode"));
} catch (IOException e) {
System.err.println("Verify error: " + e.getMessage());
System.exit(3);
}
break;
case "send":
if (dBusConn == null && !m.isRegistered()) {
System.err.println("User is not registered.");
System.exit(1);
}
if (ns.getBoolean("endsession")) {
if (ns.getList("recipient") == null) {
System.err.println("No recipients given");
System.err.println("Aborting sending.");
System.exit(1);
}
try {
ts.sendEndSessionMessage(ns.<String>getList("recipient"));
} catch (IOException e) {
handleIOException(e);
} catch (EncapsulatedExceptions e) {
handleEncapsulatedExceptions(e);
} catch (AssertionError e) {
handleAssertionError(e);
}
} else {
String messageText = ns.getString("message");
if (messageText == null) {
try {
messageText = IOUtils.toString(System.in);
} catch (IOException e) {
System.err.println("Failed to read message from stdin: " + e.getMessage());
System.err.println("Aborting sending.");
System.exit(1);
}
}
try {
List<String> attachments = ns.getList("attachment");
if (attachments == null) {
attachments = new ArrayList<>();
}
if (ns.getString("group") != null) {
byte[] groupId = decodeGroupId(ns.getString("group"));
ts.sendGroupMessage(messageText, attachments, groupId);
} else {
ts.sendMessage(messageText, attachments, ns.<String>getList("recipient"));
}
} catch (IOException e) {
handleIOException(e);
} catch (EncapsulatedExceptions e) {
handleEncapsulatedExceptions(e);
} catch (AssertionError e) {
handleAssertionError(e);
} catch (GroupNotFoundException e) {
handleGroupNotFoundException(e);
} catch (AttachmentInvalidException e) {
System.err.println("Failed to add attachment (\"" + e.getAttachment() + "\"): " + e.getMessage());
System.err.println("Aborting sending.");
System.exit(1);
}
}
break;
case "receive":
if (dBusConn != null) {
System.err.println("receive is not yet implementd via dbus");
System.exit(1);
}
if (!m.isRegistered()) {
System.err.println("User is not registered.");
System.exit(1);
}
int timeout = 5;
if (ns.getInt("timeout") != null) {
timeout = ns.getInt("timeout");
}
boolean returnOnTimeout = true;
if (timeout < 0) {
returnOnTimeout = false;
timeout = 3600;
}
try {
m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m));
} catch (IOException e) {
System.err.println("Error while receiving messages: " + e.getMessage());
System.exit(3);
} catch (AssertionError e) {
handleAssertionError(e);
}
break;
case "quitGroup":
if (dBusConn != null) {
System.err.println("quitGroup is not yet implementd via dbus");
System.exit(1);
}
if (!m.isRegistered()) {
System.err.println("User is not registered.");
System.exit(1);
}
try {
m.sendQuitGroupMessage(decodeGroupId(ns.getString("group")));
} catch (IOException e) {
handleIOException(e);
} catch (EncapsulatedExceptions e) {
handleEncapsulatedExceptions(e);
} catch (AssertionError e) {
handleAssertionError(e);
} catch (GroupNotFoundException e) {
handleGroupNotFoundException(e);
}
break;
case "updateGroup":
if (dBusConn != null) {
System.err.println("updateGroup is not yet implementd via dbus");
System.exit(1);
}
if (!m.isRegistered()) {
System.err.println("User is not registered.");
System.exit(1);
}
try {
byte[] groupId = null;
if (ns.getString("group") != null) {
groupId = decodeGroupId(ns.getString("group"));
}
byte[] newGroupId = m.sendUpdateGroupMessage(groupId, ns.getString("name"), ns.<String>getList("member"), ns.getString("avatar"));
if (groupId == null) {
System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\"");
}
} catch (IOException e) {
handleIOException(e);
} catch (AttachmentInvalidException e) {
System.err.println("Failed to add avatar attachment (\"" + e.getAttachment() + ") for group\": " + e.getMessage());
System.err.println("Aborting sending.");
System.exit(1);
} catch (GroupNotFoundException e) {
handleGroupNotFoundException(e);
} catch (EncapsulatedExceptions e) {
handleEncapsulatedExceptions(e);
}
break;
case "daemon":
if (dBusConn != null) {
System.err.println("Stop it.");
System.exit(1);
}
if (!m.isRegistered()) {
System.err.println("User is not registered.");
System.exit(1);
}
DBusConnection conn = null;
try {
try {
int busType;
if (ns.getBoolean("system")) {
busType = DBusConnection.SYSTEM;
} else {
busType = DBusConnection.SESSION;
}
conn = DBusConnection.getConnection(busType);
conn.requestBusName(TEXTSECURE_BUSNAME);
conn.exportObject(TEXTSECURE_OBJECTPATH, m);
} catch (DBusException e) {
e.printStackTrace();
System.exit(3);
}
try {
m.receiveMessages(3600, false, new ReceiveMessageHandler(m));
} catch (IOException e) {
System.err.println("Error while receiving messages: " + e.getMessage());
System.exit(3);
} catch (AssertionError e) {
handleAssertionError(e);
}
} finally {
if (conn != null) {
conn.disconnect();
}
}
break;
}
System.exit(0);
} finally {
if (dBusConn != null) {
dBusConn.disconnect();
}
}
}
private static void handleGroupNotFoundException(GroupNotFoundException e) {
System.err.println("Failed to send to group \"" + Base64.encodeBytes(e.getGroupId()) + "\": Unknown group");
System.err.println("Aborting sending.");
System.exit(1);
}
private static byte[] decodeGroupId(String groupId) {
try {
return Base64.decode(groupId);
} catch (IOException e) {
System.err.println("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage());
System.err.println("Aborting sending.");
System.exit(1);
return null;
}
}
private static Namespace parseArgs(String[] args) {
ArgumentParser parser = ArgumentParsers.newArgumentParser("textsecure-cli")
.defaultHelp(true)
.description("Commandline interface for TextSecure.")
.version(Manager.PROJECT_NAME + " " + Manager.PROJECT_VERSION);
parser.addArgument("-v", "--version")
.help("Show package version.")
.action(Arguments.version());
MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
mut.addArgument("-u", "--username")
.help("Specify your phone number, that will be used for verification.");
mut.addArgument("--dbus")
.help("Make request via user dbus.")
.action(Arguments.storeTrue());
mut.addArgument("--dbus-system")
.help("Make request via system dbus.")
.action(Arguments.storeTrue());
Subparsers subparsers = parser.addSubparsers()
.title("subcommands")
.dest("command")
.description("valid subcommands")
.help("additional help");
Subparser parserRegister = subparsers.addParser("register");
parserRegister.addArgument("-v", "--voice")
.help("The verification should be done over voice, not sms.")
.action(Arguments.storeTrue());
Subparser parserVerify = subparsers.addParser("verify");
parserVerify.addArgument("verificationCode")
.help("The verification code you received via sms or voice call.");
Subparser parserSend = subparsers.addParser("send");
parserSend.addArgument("-g", "--group")
.help("Specify the recipient group ID.");
parserSend.addArgument("recipient")
.help("Specify the recipients' phone number.")
.nargs("*");
parserSend.addArgument("-m", "--message")
.help("Specify the message, if missing standard input is used.");
parserSend.addArgument("-a", "--attachment")
.nargs("*")
.help("Add file as attachment");
parserSend.addArgument("-e", "--endsession")
.help("Clear session state and send end session message.")
.action(Arguments.storeTrue());
Subparser parserLeaveGroup = subparsers.addParser("quitGroup");
parserLeaveGroup.addArgument("-g", "--group")
.required(true)
.help("Specify the recipient group ID.");
Subparser parserUpdateGroup = subparsers.addParser("updateGroup");
parserUpdateGroup.addArgument("-g", "--group")
.help("Specify the recipient group ID.");
parserUpdateGroup.addArgument("-n", "--name")
.help("Specify the new group name.");
parserUpdateGroup.addArgument("-a", "--avatar")
.help("Specify a new group avatar image file");
parserUpdateGroup.addArgument("-m", "--member")
.nargs("*")
.help("Specify one or more members to add to the group");
Subparser parserReceive = subparsers.addParser("receive");
parserReceive.addArgument("-t", "--timeout")
.type(int.class)
.help("Number of seconds to wait for new messages (negative values disable timeout)");
Subparser parserDaemon = subparsers.addParser("daemon");
parserDaemon.addArgument("--system")
.action(Arguments.storeTrue())
.help("Use DBus system bus instead of user bus.");
try {
Namespace ns = parser.parseArgs(args);
if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) {
if (ns.getString("username") == null) {
parser.printUsage();
System.err.println("You need to specify a username (phone number)");
System.exit(2);
}
if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"))) {
System.err.println("Invalid username (phone number), make sure you include the country code.");
System.exit(2);
}
}
if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) {
System.err.println("You cannot specify recipients by phone number and groups a the same time");
System.exit(2);
}
return ns;
} catch (ArgumentParserException e) {
parser.handleError(e);
return null;
}
}
private static void handleAssertionError(AssertionError e) {
System.err.println("Failed to send/receive message (Assertion): " + e.getMessage());
System.err.println(e.getStackTrace());
System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README");
System.exit(1);
}
private static void handleEncapsulatedExceptions(EncapsulatedExceptions e) {
System.err.println("Failed to send (some) messages:");
for (NetworkFailureException n : e.getNetworkExceptions()) {
System.err.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage());
}
for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) {
System.err.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage());
}
for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) {
System.err.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage());
}
}
private static void handleIOException(IOException e) {
System.err.println("Failed to send message: " + e.getMessage());
}
private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
final Manager m;
public ReceiveMessageHandler(Manager m) {
this.m = m;
}
@Override
public void handleMessage(TextSecureEnvelope envelope, TextSecureContent content, GroupInfo group) {
System.out.println("Envelope from: " + envelope.getSource());
System.out.println("Timestamp: " + envelope.getTimestamp());
if (envelope.isReceipt()) {
System.out.println("Got receipt.");
} else if (envelope.isWhisperMessage() | envelope.isPreKeyWhisperMessage()) {
if (content == null) {
System.out.println("Failed to decrypt message.");
} else {
if (content.getDataMessage().isPresent()) {
TextSecureDataMessage message = content.getDataMessage().get();
System.out.println("Message timestamp: " + message.getTimestamp());
if (message.getBody().isPresent()) {
System.out.println("Body: " + message.getBody().get());
}
if (message.getGroupInfo().isPresent()) {
TextSecureGroup groupInfo = message.getGroupInfo().get();
System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
if (groupInfo.getName().isPresent()) {
System.out.println(" Name: " + groupInfo.getName().get());
} else if (group != null) {
System.out.println(" Name: " + group.name);
} else {
System.out.println(" Name: <Unknown group>");
}
System.out.println(" Type: " + groupInfo.getType());
if (groupInfo.getMembers().isPresent()) {
for (String member : groupInfo.getMembers().get()) {
System.out.println(" Member: " + member);
}
}
if (groupInfo.getAvatar().isPresent()) {
System.out.println(" Avatar:");
printAttachment(groupInfo.getAvatar().get());
}
}
if (message.isEndSession()) {
System.out.println("Is end session");
}
if (message.getAttachments().isPresent()) {
System.out.println("Attachments: ");
for (TextSecureAttachment attachment : message.getAttachments().get()) {
printAttachment(attachment);
}
}
}
if (content.getSyncMessage().isPresent()) {
TextSecureSyncMessage syncMessage = content.getSyncMessage().get();
System.out.println("Received sync message");
}
}
} else {
System.out.println("Unknown message received.");
}
System.out.println();
}
private void printAttachment(TextSecureAttachment attachment) {
System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")");
if (attachment.isPointer()) {
final TextSecureAttachmentPointer pointer = attachment.asPointer();
System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : ""));
System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "<unavailable>") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""));
File file = m.getAttachmentFile(pointer.getId());
if (file.exists()) {
System.out.println(" Stored plaintext in: " + file);
}
}
}
}
}

View file

@ -0,0 +1,602 @@
/**
* Copyright (C) 2015 AsamK
*
* 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.asamk.textsecure;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.asamk.TextSecure;
import org.whispersystems.libaxolotl.*;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.util.KeyHelper;
import org.whispersystems.libaxolotl.util.Medium;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.TextSecureAccountManager;
import org.whispersystems.textsecure.api.TextSecureMessagePipe;
import org.whispersystems.textsecure.api.TextSecureMessageReceiver;
import org.whispersystems.textsecure.api.TextSecureMessageSender;
import org.whispersystems.textsecure.api.crypto.TextSecureCipher;
import org.whispersystems.textsecure.api.messages.*;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.textsecure.api.util.InvalidNumberException;
import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
class Manager implements TextSecure {
private final static String URL = "https://textsecure-service.whispersystems.org";
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
private final static String USER_AGENT = PROJECT_NAME + " " + PROJECT_VERSION;
private final static String settingsPath = System.getProperty("user.home") + "/.config/textsecure";
private final static String dataPath = settingsPath + "/data";
private final static String attachmentsPath = settingsPath + "/attachments";
private final ObjectMapper jsonProcessot = new ObjectMapper();
private String username;
private String password;
private String signalingKey;
private int preKeyIdOffset;
private int nextSignedPreKeyId;
private boolean registered = false;
private JsonAxolotlStore axolotlStore;
private TextSecureAccountManager accountManager;
private JsonGroupStore groupStore;
public Manager(String username) {
this.username = username;
jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
public String getFileName() {
new File(dataPath).mkdirs();
return dataPath + "/" + username;
}
public boolean userExists() {
File f = new File(getFileName());
return !(!f.exists() || f.isDirectory());
}
public boolean userHasKeys() {
return axolotlStore != null;
}
private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
JsonNode node = parent.get(name);
if (node == null) {
throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
}
return node;
}
public void load() throws IOException, InvalidKeyException {
JsonNode rootNode = jsonProcessot.readTree(new File(getFileName()));
username = getNotNullNode(rootNode, "username").asText();
password = getNotNullNode(rootNode, "password").asText();
if (rootNode.has("signalingKey")) {
signalingKey = getNotNullNode(rootNode, "signalingKey").asText();
}
if (rootNode.has("preKeyIdOffset")) {
preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
} else {
preKeyIdOffset = 0;
}
if (rootNode.has("nextSignedPreKeyId")) {
nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
} else {
nextSignedPreKeyId = 0;
}
axolotlStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonAxolotlStore.class); //new JsonAxolotlStore(in.getJSONObject("axolotlStore"));
registered = getNotNullNode(rootNode, "registered").asBoolean();
JsonNode groupStoreNode = rootNode.get("groupStore");
if (groupStoreNode != null) {
groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class);
}
if (groupStore == null) {
groupStore = new JsonGroupStore();
}
accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
}
private void save() {
ObjectNode rootNode = jsonProcessot.createObjectNode();
rootNode.put("username", username)
.put("password", password)
.put("signalingKey", signalingKey)
.put("preKeyIdOffset", preKeyIdOffset)
.put("nextSignedPreKeyId", nextSignedPreKeyId)
.put("registered", registered)
.putPOJO("axolotlStore", axolotlStore)
.putPOJO("groupStore", groupStore)
;
try {
jsonProcessot.writeValue(new File(getFileName()), rootNode);
} catch (Exception e) {
System.err.println(String.format("Error saving file: %s", e.getMessage()));
}
}
public void createNewIdentity() {
IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
int registrationId = KeyHelper.generateRegistrationId(false);
axolotlStore = new JsonAxolotlStore(identityKey, registrationId);
groupStore = new JsonGroupStore();
registered = false;
save();
}
public boolean isRegistered() {
return registered;
}
public void register(boolean voiceVerication) throws IOException {
password = Util.getSecret(18);
accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
if (voiceVerication)
accountManager.requestVoiceVerificationCode();
else
accountManager.requestSmsVerificationCode();
registered = false;
save();
}
private static final int BATCH_SIZE = 100;
private List<PreKeyRecord> generatePreKeys() {
List<PreKeyRecord> records = new LinkedList<>();
for (int i = 0; i < BATCH_SIZE; i++) {
int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
ECKeyPair keyPair = Curve.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
axolotlStore.storePreKey(preKeyId, record);
records.add(record);
}
preKeyIdOffset = (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE;
save();
return records;
}
private PreKeyRecord generateLastResortPreKey() {
if (axolotlStore.containsPreKey(Medium.MAX_VALUE)) {
try {
return axolotlStore.loadPreKey(Medium.MAX_VALUE);
} catch (InvalidKeyIdException e) {
axolotlStore.removePreKey(Medium.MAX_VALUE);
}
}
ECKeyPair keyPair = Curve.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
axolotlStore.storePreKey(Medium.MAX_VALUE, record);
save();
return record;
}
private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
try {
ECKeyPair keyPair = Curve.generateKeyPair();
byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
axolotlStore.storeSignedPreKey(nextSignedPreKeyId, record);
nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
save();
return record;
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public void verifyAccount(String verificationCode) throws IOException {
verificationCode = verificationCode.replace("-", "");
signalingKey = Util.getSecret(52);
accountManager.verifyAccountWithCode(verificationCode, signalingKey, axolotlStore.getLocalRegistrationId(), false);
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
registered = true;
List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
PreKeyRecord lastResortKey = generateLastResortPreKey();
SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(axolotlStore.getIdentityKeyPair());
accountManager.setPreKeys(axolotlStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
save();
}
private static List<TextSecureAttachment> getTextSecureAttachments(List<String> attachments) throws AttachmentInvalidException {
List<TextSecureAttachment> textSecureAttachments = null;
if (attachments != null) {
textSecureAttachments = new ArrayList<>(attachments.size());
for (String attachment : attachments) {
try {
textSecureAttachments.add(createAttachment(attachment));
} catch (IOException e) {
throw new AttachmentInvalidException(attachment, e);
}
}
}
return textSecureAttachments;
}
private static TextSecureAttachmentStream createAttachment(String attachment) throws IOException {
File attachmentFile = new File(attachment);
InputStream attachmentStream = new FileInputStream(attachmentFile);
final long attachmentSize = attachmentFile.length();
String mime = Files.probeContentType(Paths.get(attachment));
return new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null);
}
@Override
public void sendGroupMessage(String messageText, List<String> attachments,
byte[] groupId)
throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText);
if (attachments != null) {
messageBuilder.withAttachments(getTextSecureAttachments(attachments));
}
if (groupId != null) {
TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.DELIVER)
.withId(groupId)
.build();
messageBuilder.asGroupMessage(group);
}
TextSecureDataMessage message = messageBuilder.build();
sendMessage(message, groupStore.getGroup(groupId).members);
}
public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.QUIT)
.withId(groupId)
.build();
TextSecureDataMessage message = TextSecureDataMessage.newBuilder()
.asGroupMessage(group)
.build();
sendMessage(message, groupStore.getGroup(groupId).members);
}
public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
GroupInfo g;
if (groupId == null) {
// Create new group
g = new GroupInfo(Util.getSecretBytes(16));
g.members.add(username);
} else {
g = groupStore.getGroup(groupId);
}
if (name != null) {
g.name = name;
}
if (members != null) {
for (String member : members) {
try {
g.members.add(canonicalizeNumber(member));
} catch (InvalidNumberException e) {
System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
System.err.println("Aborting…");
System.exit(1);
}
}
}
TextSecureGroup.Builder group = TextSecureGroup.newBuilder(TextSecureGroup.Type.UPDATE)
.withId(g.groupId)
.withName(g.name)
.withMembers(new ArrayList<>(g.members));
if (avatarFile != null) {
try {
group.withAvatar(createAttachment(avatarFile));
// TODO
g.avatarId = 0;
} catch (IOException e) {
throw new AttachmentInvalidException(avatarFile, e);
}
}
groupStore.updateGroup(g);
TextSecureDataMessage message = TextSecureDataMessage.newBuilder()
.asGroupMessage(group.build())
.build();
sendMessage(message, g.members);
return g.groupId;
}
@Override
public void sendMessage(String message, List<String> attachments, String recipient)
throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
List<String> recipients = new ArrayList<>(1);
recipients.add(recipient);
sendMessage(message, attachments, recipients);
}
@Override
public void sendMessage(String messageText, List<String> attachments,
List<String> recipients)
throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText);
if (attachments != null) {
messageBuilder.withAttachments(getTextSecureAttachments(attachments));
}
TextSecureDataMessage message = messageBuilder.build();
sendMessage(message, recipients);
}
@Override
public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
TextSecureDataMessage message = TextSecureDataMessage.newBuilder()
.asEndSessionMessage()
.build();
sendMessage(message, recipients);
}
private void sendMessage(TextSecureDataMessage message, Collection<String> recipients)
throws IOException, EncapsulatedExceptions {
TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, username, password,
axolotlStore, USER_AGENT, Optional.<TextSecureMessageSender.EventListener>absent());
Set<TextSecureAddress> recipientsTS = new HashSet<>(recipients.size());
for (String recipient : recipients) {
try {
recipientsTS.add(getPushAddress(recipient));
} catch (InvalidNumberException e) {
System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
System.err.println("Aborting sending.");
save();
return;
}
}
messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
if (message.isEndSession()) {
for (TextSecureAddress recipient : recipientsTS) {
handleEndSession(recipient.getNumber());
}
}
save();
}
private TextSecureContent decryptMessage(TextSecureEnvelope envelope) {
TextSecureCipher cipher = new TextSecureCipher(new TextSecureAddress(username), axolotlStore);
try {
return cipher.decrypt(envelope);
} catch (Exception e) {
// TODO handle all exceptions
e.printStackTrace();
return null;
}
}
private void handleEndSession(String source) {
axolotlStore.deleteAllSessions(source);
}
public interface ReceiveMessageHandler {
void handleMessage(TextSecureEnvelope envelope, TextSecureContent decryptedContent, GroupInfo group);
}
public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
TextSecureMessagePipe messagePipe = null;
try {
messagePipe = messageReceiver.createMessagePipe();
while (true) {
TextSecureEnvelope envelope;
TextSecureContent content = null;
GroupInfo group = null;
try {
envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
if (!envelope.isReceipt()) {
content = decryptMessage(envelope);
if (content != null) {
if (content.getDataMessage().isPresent()) {
TextSecureDataMessage message = content.getDataMessage().get();
if (message.getGroupInfo().isPresent()) {
TextSecureGroup groupInfo = message.getGroupInfo().get();
switch (groupInfo.getType()) {
case UPDATE:
try {
group = groupStore.getGroup(groupInfo.getGroupId());
} catch (GroupNotFoundException e) {
group = new GroupInfo(groupInfo.getGroupId());
}
if (groupInfo.getAvatar().isPresent()) {
TextSecureAttachment avatar = groupInfo.getAvatar().get();
if (avatar.isPointer()) {
long avatarId = avatar.asPointer().getId();
try {
retrieveAttachment(avatar.asPointer());
group.avatarId = avatarId;
} catch (IOException | InvalidMessageException e) {
System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage());
}
}
}
if (groupInfo.getName().isPresent()) {
group.name = groupInfo.getName().get();
}
if (groupInfo.getMembers().isPresent()) {
group.members.addAll(groupInfo.getMembers().get());
}
groupStore.updateGroup(group);
break;
case DELIVER:
try {
group = groupStore.getGroup(groupInfo.getGroupId());
} catch (GroupNotFoundException e) {
}
break;
case QUIT:
try {
group = groupStore.getGroup(groupInfo.getGroupId());
group.members.remove(envelope.getSource());
} catch (GroupNotFoundException e) {
}
break;
}
}
if (message.isEndSession()) {
handleEndSession(envelope.getSource());
}
if (message.getAttachments().isPresent()) {
for (TextSecureAttachment attachment : message.getAttachments().get()) {
if (attachment.isPointer()) {
try {
retrieveAttachment(attachment.asPointer());
} catch (IOException | InvalidMessageException e) {
System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
}
}
}
}
}
}
}
save();
handler.handleMessage(envelope, content, group);
} catch (TimeoutException e) {
if (returnOnTimeout)
return;
} catch (InvalidVersionException e) {
System.err.println("Ignoring error: " + e.getMessage());
}
}
} finally {
if (messagePipe != null)
messagePipe.shutdown();
}
}
public File getAttachmentFile(long attachmentId) {
return new File(attachmentsPath + "/" + attachmentId);
}
private File retrieveAttachment(TextSecureAttachmentPointer pointer) throws IOException, InvalidMessageException {
final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
new File(attachmentsPath).mkdirs();
File outputFile = getAttachmentFile(pointer.getId());
OutputStream output = null;
try {
output = new FileOutputStream(outputFile);
byte[] buffer = new byte[4096];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} finally {
if (output != null) {
output.close();
output = null;
}
if (!tmpFile.delete()) {
System.err.println("Failed to delete temp file: " + tmpFile);
}
}
if (pointer.getPreview().isPresent()) {
File previewFile = new File(outputFile + ".preview");
try {
output = new FileOutputStream(previewFile);
byte[] preview = pointer.getPreview().get();
output.write(preview, 0, preview.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} finally {
if (output != null) {
output.close();
}
}
}
return outputFile;
}
private String canonicalizeNumber(String number) throws InvalidNumberException {
String localNumber = username;
return PhoneNumberFormatter.formatNumber(number, localNumber);
}
private TextSecureAddress getPushAddress(String number) throws InvalidNumberException {
String e164number = canonicalizeNumber(number);
return new TextSecureAddress(e164number);
}
@Override
public boolean isRemote() {
return false;
}
}

View file

@ -0,0 +1,25 @@
package org.asamk.textsecure;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
class Util {
public static String getSecret(int size) {
byte[] secret = getSecretBytes(size);
return Base64.encodeBytes(secret);
}
public static byte[] getSecretBytes(int size) {
byte[] secret = new byte[size];
getSecureRandom().nextBytes(secret);
return secret;
}
private static SecureRandom getSecureRandom() {
try {
return SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View file

@ -0,0 +1,18 @@
package org.asamk.textsecure;
import org.whispersystems.textsecure.api.push.TrustStore;
import java.io.InputStream;
class WhisperTrustStore implements TrustStore {
@Override
public InputStream getKeyStoreInputStream() {
return WhisperTrustStore.class.getResourceAsStream("whisper.store");
}
@Override
public String getKeyStorePassword() {
return "whisper";
}
}