mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +00:00
Rename package
This commit is contained in:
parent
5859e7b9f7
commit
27d9424f1e
17 changed files with 27 additions and 20 deletions
19
src/main/java/org/asamk/TextSecure.java
Normal file
19
src/main/java/org/asamk/TextSecure.java
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
2135
src/main/java/org/asamk/textsecure/Base64.java
Normal file
2135
src/main/java/org/asamk/textsecure/Base64.java
Normal file
File diff suppressed because it is too large
Load diff
32
src/main/java/org/asamk/textsecure/GroupInfo.java
Normal file
32
src/main/java/org/asamk/textsecure/GroupInfo.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
150
src/main/java/org/asamk/textsecure/JsonAxolotlStore.java
Normal file
150
src/main/java/org/asamk/textsecure/JsonAxolotlStore.java
Normal 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);
|
||||
}
|
||||
}
|
54
src/main/java/org/asamk/textsecure/JsonGroupStore.java
Normal file
54
src/main/java/org/asamk/textsecure/JsonGroupStore.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
107
src/main/java/org/asamk/textsecure/JsonIdentityKeyStore.java
Normal file
107
src/main/java/org/asamk/textsecure/JsonIdentityKeyStore.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
97
src/main/java/org/asamk/textsecure/JsonPreKeyStore.java
Normal file
97
src/main/java/org/asamk/textsecure/JsonPreKeyStore.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
119
src/main/java/org/asamk/textsecure/JsonSessionStore.java
Normal file
119
src/main/java/org/asamk/textsecure/JsonSessionStore.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
114
src/main/java/org/asamk/textsecure/JsonSignedPreKeyStore.java
Normal file
114
src/main/java/org/asamk/textsecure/JsonSignedPreKeyStore.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
545
src/main/java/org/asamk/textsecure/Main.java
Normal file
545
src/main/java/org/asamk/textsecure/Main.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
602
src/main/java/org/asamk/textsecure/Manager.java
Normal file
602
src/main/java/org/asamk/textsecure/Manager.java
Normal 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;
|
||||
}
|
||||
}
|
25
src/main/java/org/asamk/textsecure/Util.java
Normal file
25
src/main/java/org/asamk/textsecure/Util.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
18
src/main/java/org/asamk/textsecure/WhisperTrustStore.java
Normal file
18
src/main/java/org/asamk/textsecure/WhisperTrustStore.java
Normal 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";
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue