]> nmode's Git Repositories - signal-cli/blob - src/main/java/cli/Manager.java
a0fb5b01e67faded00d17d65b25aed6759dc17ae
[signal-cli] / src / main / java / cli / Manager.java
1 /**
2 * Copyright (C) 2015 AsamK
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17 package cli;
18
19 import com.fasterxml.jackson.annotation.JsonAutoDetect;
20 import com.fasterxml.jackson.annotation.PropertyAccessor;
21 import com.fasterxml.jackson.databind.DeserializationFeature;
22 import com.fasterxml.jackson.databind.JsonNode;
23 import com.fasterxml.jackson.databind.ObjectMapper;
24 import com.fasterxml.jackson.databind.SerializationFeature;
25 import com.fasterxml.jackson.databind.node.ObjectNode;
26 import org.whispersystems.libaxolotl.*;
27 import org.whispersystems.libaxolotl.ecc.Curve;
28 import org.whispersystems.libaxolotl.ecc.ECKeyPair;
29 import org.whispersystems.libaxolotl.state.PreKeyRecord;
30 import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
31 import org.whispersystems.libaxolotl.util.KeyHelper;
32 import org.whispersystems.libaxolotl.util.Medium;
33 import org.whispersystems.libaxolotl.util.guava.Optional;
34 import org.whispersystems.textsecure.api.TextSecureAccountManager;
35 import org.whispersystems.textsecure.api.TextSecureMessagePipe;
36 import org.whispersystems.textsecure.api.TextSecureMessageReceiver;
37 import org.whispersystems.textsecure.api.TextSecureMessageSender;
38 import org.whispersystems.textsecure.api.crypto.TextSecureCipher;
39 import org.whispersystems.textsecure.api.messages.*;
40 import org.whispersystems.textsecure.api.push.TextSecureAddress;
41 import org.whispersystems.textsecure.api.push.TrustStore;
42 import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
43 import org.whispersystems.textsecure.api.util.InvalidNumberException;
44 import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
45
46 import java.io.*;
47 import java.util.*;
48 import java.util.concurrent.TimeUnit;
49 import java.util.concurrent.TimeoutException;
50
51 class Manager {
52 private final static String URL = "https://textsecure-service.whispersystems.org";
53 private final static TrustStore TRUST_STORE = new WhisperTrustStore();
54
55 public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
56 public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
57 private final static String USER_AGENT = PROJECT_NAME + " " + PROJECT_VERSION;
58
59 private final static String settingsPath = System.getProperty("user.home") + "/.config/textsecure";
60 private final static String dataPath = settingsPath + "/data";
61 private final static String attachmentsPath = settingsPath + "/attachments";
62
63 private final ObjectMapper jsonProcessot = new ObjectMapper();
64 private String username;
65 private String password;
66 private String signalingKey;
67 private int preKeyIdOffset;
68 private int nextSignedPreKeyId;
69
70 private boolean registered = false;
71
72 private JsonAxolotlStore axolotlStore;
73 private TextSecureAccountManager accountManager;
74 private JsonGroupStore groupStore;
75
76 public Manager(String username) {
77 this.username = username;
78 jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
79 jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
80 jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
81 jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
82 }
83
84 public String getFileName() {
85 new File(dataPath).mkdirs();
86 return dataPath + "/" + username;
87 }
88
89 public boolean userExists() {
90 File f = new File(getFileName());
91 return !(!f.exists() || f.isDirectory());
92 }
93
94 public boolean userHasKeys() {
95 return axolotlStore != null;
96 }
97
98 private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
99 JsonNode node = parent.get(name);
100 if (node == null) {
101 throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
102 }
103
104 return node;
105 }
106
107 public void load() throws IOException, InvalidKeyException {
108 JsonNode rootNode = jsonProcessot.readTree(new File(getFileName()));
109
110 username = getNotNullNode(rootNode, "username").asText();
111 password = getNotNullNode(rootNode, "password").asText();
112 if (rootNode.has("signalingKey")) {
113 signalingKey = getNotNullNode(rootNode, "signalingKey").asText();
114 }
115 if (rootNode.has("preKeyIdOffset")) {
116 preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
117 } else {
118 preKeyIdOffset = 0;
119 }
120 if (rootNode.has("nextSignedPreKeyId")) {
121 nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
122 } else {
123 nextSignedPreKeyId = 0;
124 }
125 axolotlStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonAxolotlStore.class); //new JsonAxolotlStore(in.getJSONObject("axolotlStore"));
126 registered = getNotNullNode(rootNode, "registered").asBoolean();
127 JsonNode groupStoreNode = rootNode.get("groupStore");
128 if (groupStoreNode != null) {
129 groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class);
130 }
131 accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
132 }
133
134 public void save() {
135 ObjectNode rootNode = jsonProcessot.createObjectNode();
136 rootNode.put("username", username)
137 .put("password", password)
138 .put("signalingKey", signalingKey)
139 .put("preKeyIdOffset", preKeyIdOffset)
140 .put("nextSignedPreKeyId", nextSignedPreKeyId)
141 .put("registered", registered)
142 .putPOJO("axolotlStore", axolotlStore)
143 .putPOJO("groupStore", groupStore)
144 ;
145 try {
146 jsonProcessot.writeValue(new File(getFileName()), rootNode);
147 } catch (Exception e) {
148 System.err.println(String.format("Error saving file: %s", e.getMessage()));
149 }
150 }
151
152 public void createNewIdentity() {
153 IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
154 int registrationId = KeyHelper.generateRegistrationId(false);
155 axolotlStore = new JsonAxolotlStore(identityKey, registrationId);
156 groupStore = new JsonGroupStore();
157 registered = false;
158 }
159
160 public boolean isRegistered() {
161 return registered;
162 }
163
164 public void register(boolean voiceVerication) throws IOException {
165 password = Util.getSecret(18);
166
167 accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
168
169 if (voiceVerication)
170 accountManager.requestVoiceVerificationCode();
171 else
172 accountManager.requestSmsVerificationCode();
173
174 registered = false;
175 }
176
177 private static final int BATCH_SIZE = 100;
178
179 private List<PreKeyRecord> generatePreKeys() {
180 List<PreKeyRecord> records = new LinkedList<>();
181
182 for (int i = 0; i < BATCH_SIZE; i++) {
183 int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
184 ECKeyPair keyPair = Curve.generateKeyPair();
185 PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
186
187 axolotlStore.storePreKey(preKeyId, record);
188 records.add(record);
189 }
190
191 preKeyIdOffset = (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE;
192 return records;
193 }
194
195 private PreKeyRecord generateLastResortPreKey() {
196 if (axolotlStore.containsPreKey(Medium.MAX_VALUE)) {
197 try {
198 return axolotlStore.loadPreKey(Medium.MAX_VALUE);
199 } catch (InvalidKeyIdException e) {
200 axolotlStore.removePreKey(Medium.MAX_VALUE);
201 }
202 }
203
204 ECKeyPair keyPair = Curve.generateKeyPair();
205 PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
206
207 axolotlStore.storePreKey(Medium.MAX_VALUE, record);
208
209 return record;
210 }
211
212 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
213 try {
214 ECKeyPair keyPair = Curve.generateKeyPair();
215 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
216 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
217
218 axolotlStore.storeSignedPreKey(nextSignedPreKeyId, record);
219 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
220
221 return record;
222 } catch (InvalidKeyException e) {
223 throw new AssertionError(e);
224 }
225 }
226
227 public void verifyAccount(String verificationCode) throws IOException {
228 verificationCode = verificationCode.replace("-", "");
229 signalingKey = Util.getSecret(52);
230 accountManager.verifyAccountWithCode(verificationCode, signalingKey, axolotlStore.getLocalRegistrationId(), false);
231
232 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
233 registered = true;
234
235 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
236
237 PreKeyRecord lastResortKey = generateLastResortPreKey();
238
239 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(axolotlStore.getIdentityKeyPair());
240
241 accountManager.setPreKeys(axolotlStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
242 }
243
244 public void sendMessage(List<String> recipients, TextSecureDataMessage message)
245 throws IOException, EncapsulatedExceptions {
246 TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, username, password,
247 axolotlStore, USER_AGENT, Optional.<TextSecureMessageSender.EventListener>absent());
248
249 Set<TextSecureAddress> recipientsTS = new HashSet<>(recipients.size());
250 for (String recipient : recipients) {
251 try {
252 recipientsTS.add(getPushAddress(recipient));
253 } catch (InvalidNumberException e) {
254 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
255 System.err.println("Aborting sending.");
256 return;
257 }
258 }
259
260 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
261
262 if (message.isEndSession()) {
263 for (TextSecureAddress recipient : recipientsTS) {
264 handleEndSession(recipient.getNumber());
265 }
266 }
267 }
268
269 private TextSecureContent decryptMessage(TextSecureEnvelope envelope) {
270 TextSecureCipher cipher = new TextSecureCipher(new TextSecureAddress(username), axolotlStore);
271 try {
272 return cipher.decrypt(envelope);
273 } catch (Exception e) {
274 // TODO handle all exceptions
275 e.printStackTrace();
276 return null;
277 }
278 }
279
280 private void handleEndSession(String source) {
281 axolotlStore.deleteAllSessions(source);
282 }
283
284 public interface ReceiveMessageHandler {
285 void handleMessage(TextSecureEnvelope envelope, TextSecureContent decryptedContent, GroupInfo group);
286 }
287
288 public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
289 final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
290 TextSecureMessagePipe messagePipe = null;
291
292 try {
293 messagePipe = messageReceiver.createMessagePipe();
294
295 while (true) {
296 TextSecureEnvelope envelope;
297 TextSecureContent content = null;
298 GroupInfo group = null;
299 try {
300 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
301 if (!envelope.isReceipt()) {
302 content = decryptMessage(envelope);
303 if (content != null) {
304 if (content.getDataMessage().isPresent()) {
305 TextSecureDataMessage message = content.getDataMessage().get();
306 if (message.getGroupInfo().isPresent()) {
307 TextSecureGroup groupInfo = message.getGroupInfo().get();
308 switch (groupInfo.getType()) {
309 case UPDATE:
310 group = groupStore.getGroup(groupInfo.getGroupId());
311 if (group == null) {
312 group = new GroupInfo(groupInfo.getGroupId());
313 }
314
315 if (groupInfo.getAvatar().isPresent()) {
316 TextSecureAttachment avatar = groupInfo.getAvatar().get();
317 if (avatar.isPointer()) {
318 long avatarId = avatar.asPointer().getId();
319 try {
320 retrieveAttachment(avatar.asPointer());
321 group.avatarId = avatarId;
322 } catch (IOException | InvalidMessageException e) {
323 System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage());
324 }
325 }
326 }
327
328 if (groupInfo.getName().isPresent()) {
329 group.name = groupInfo.getName().get();
330 }
331
332 if (groupInfo.getMembers().isPresent()) {
333 group.members.addAll(groupInfo.getMembers().get());
334 }
335
336 groupStore.updateGroup(group);
337 break;
338 case DELIVER:
339 group = groupStore.getGroup(groupInfo.getGroupId());
340 break;
341 case QUIT:
342 group = groupStore.getGroup(groupInfo.getGroupId());
343 if (group != null) {
344 group.members.remove(envelope.getSource());
345 }
346 break;
347 }
348 }
349 if (message.isEndSession()) {
350 handleEndSession(envelope.getSource());
351 }
352 if (message.getAttachments().isPresent()) {
353 for (TextSecureAttachment attachment : message.getAttachments().get()) {
354 if (attachment.isPointer()) {
355 try {
356 retrieveAttachment(attachment.asPointer());
357 } catch (IOException | InvalidMessageException e) {
358 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
359 }
360 }
361 }
362 }
363 }
364 }
365 }
366 handler.handleMessage(envelope, content, group);
367 } catch (TimeoutException e) {
368 if (returnOnTimeout)
369 return;
370 } catch (InvalidVersionException e) {
371 System.err.println("Ignoring error: " + e.getMessage());
372 }
373 save();
374 }
375 } finally {
376 if (messagePipe != null)
377 messagePipe.shutdown();
378 }
379 }
380
381 public File getAttachmentFile(long attachmentId) {
382 return new File(attachmentsPath + "/" + attachmentId);
383 }
384
385 private File retrieveAttachment(TextSecureAttachmentPointer pointer) throws IOException, InvalidMessageException {
386 final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
387
388 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
389 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
390
391 new File(attachmentsPath).mkdirs();
392 File outputFile = getAttachmentFile(pointer.getId());
393 OutputStream output = null;
394 try {
395 output = new FileOutputStream(outputFile);
396 byte[] buffer = new byte[4096];
397 int read;
398
399 while ((read = input.read(buffer)) != -1) {
400 output.write(buffer, 0, read);
401 }
402 } catch (FileNotFoundException e) {
403 e.printStackTrace();
404 return null;
405 } finally {
406 if (output != null) {
407 output.close();
408 output = null;
409 }
410 if (!tmpFile.delete()) {
411 System.err.println("Failed to delete temp file: " + tmpFile);
412 }
413 }
414 if (pointer.getPreview().isPresent()) {
415 File previewFile = new File(outputFile + ".preview");
416 try {
417 output = new FileOutputStream(previewFile);
418 byte[] preview = pointer.getPreview().get();
419 output.write(preview, 0, preview.length);
420 } catch (FileNotFoundException e) {
421 e.printStackTrace();
422 return null;
423 } finally {
424 if (output != null) {
425 output.close();
426 }
427 }
428 }
429 return outputFile;
430 }
431
432 public String canonicalizeNumber(String number) throws InvalidNumberException {
433 String localNumber = username;
434 return PhoneNumberFormatter.formatNumber(number, localNumber);
435 }
436
437 private TextSecureAddress getPushAddress(String number) throws InvalidNumberException {
438 String e164number = canonicalizeNumber(number);
439 return new TextSecureAddress(e164number);
440 }
441
442 public GroupInfo getGroupInfo(byte[] groupId) {
443 return groupStore.getGroup(groupId);
444 }
445
446 public void setGroupInfo(GroupInfo group) {
447 groupStore.updateGroup(group);
448 }
449
450 public String getUsername() {
451 return username;
452 }
453 }