]> nmode's Git Repositories - signal-cli/blob - src/main/java/cli/Manager.java
758dabc3f6e8149b4204af9dac2eb76815ac7970
[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.LinkedList;
48 import java.util.List;
49 import java.util.concurrent.TimeUnit;
50 import java.util.concurrent.TimeoutException;
51
52 class Manager {
53 private final static String URL = "https://textsecure-service.whispersystems.org";
54 private final static TrustStore TRUST_STORE = new WhisperTrustStore();
55
56 public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
57 public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
58 private final static String USER_AGENT = PROJECT_NAME + " " + PROJECT_VERSION;
59
60 private final static String settingsPath = System.getProperty("user.home") + "/.config/textsecure";
61 private final static String dataPath = settingsPath + "/data";
62 private final static String attachmentsPath = settingsPath + "/attachments";
63
64 private final ObjectMapper jsonProcessot = new ObjectMapper();
65 private String username;
66 private String password;
67 private String signalingKey;
68 private int preKeyIdOffset;
69 private int nextSignedPreKeyId;
70
71 private boolean registered = false;
72
73 private JsonAxolotlStore axolotlStore;
74 private TextSecureAccountManager accountManager;
75 private JsonGroupStore groupStore;
76
77 public Manager(String username) {
78 this.username = username;
79 jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
80 jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
81 jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
82 jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
83 }
84
85 public String getFileName() {
86 new File(dataPath).mkdirs();
87 return dataPath + "/" + username;
88 }
89
90 public boolean userExists() {
91 File f = new File(getFileName());
92 return !(!f.exists() || f.isDirectory());
93 }
94
95 public boolean userHasKeys() {
96 return axolotlStore != null;
97 }
98
99 private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
100 JsonNode node = parent.get(name);
101 if (node == null) {
102 throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
103 }
104
105 return node;
106 }
107
108 public void load() throws IOException, InvalidKeyException {
109 JsonNode rootNode = jsonProcessot.readTree(new File(getFileName()));
110
111 username = getNotNullNode(rootNode, "username").asText();
112 password = getNotNullNode(rootNode, "password").asText();
113 if (rootNode.has("signalingKey")) {
114 signalingKey = getNotNullNode(rootNode, "signalingKey").asText();
115 }
116 if (rootNode.has("preKeyIdOffset")) {
117 preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
118 } else {
119 preKeyIdOffset = 0;
120 }
121 if (rootNode.has("nextSignedPreKeyId")) {
122 nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
123 } else {
124 nextSignedPreKeyId = 0;
125 }
126 axolotlStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonAxolotlStore.class); //new JsonAxolotlStore(in.getJSONObject("axolotlStore"));
127 registered = getNotNullNode(rootNode, "registered").asBoolean();
128 JsonNode groupStoreNode = rootNode.get("groupStore");
129 if (groupStoreNode != null) {
130 groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class);
131 }
132 accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
133 }
134
135 public void save() {
136 ObjectNode rootNode = jsonProcessot.createObjectNode();
137 rootNode.put("username", username)
138 .put("password", password)
139 .put("signalingKey", signalingKey)
140 .put("preKeyIdOffset", preKeyIdOffset)
141 .put("nextSignedPreKeyId", nextSignedPreKeyId)
142 .put("registered", registered)
143 .putPOJO("axolotlStore", axolotlStore)
144 .putPOJO("groupStore", groupStore)
145 ;
146 try {
147 jsonProcessot.writeValue(new File(getFileName()), rootNode);
148 } catch (Exception e) {
149 System.err.println(String.format("Error saving file: %s", e.getMessage()));
150 }
151 }
152
153 public void createNewIdentity() {
154 IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
155 int registrationId = KeyHelper.generateRegistrationId(false);
156 axolotlStore = new JsonAxolotlStore(identityKey, registrationId);
157 groupStore = new JsonGroupStore();
158 registered = false;
159 }
160
161 public boolean isRegistered() {
162 return registered;
163 }
164
165 public void register(boolean voiceVerication) throws IOException {
166 password = Util.getSecret(18);
167
168 accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
169
170 if (voiceVerication)
171 accountManager.requestVoiceVerificationCode();
172 else
173 accountManager.requestSmsVerificationCode();
174
175 registered = false;
176 }
177
178 private static final int BATCH_SIZE = 100;
179
180 private List<PreKeyRecord> generatePreKeys() {
181 List<PreKeyRecord> records = new LinkedList<>();
182
183 for (int i = 0; i < BATCH_SIZE; i++) {
184 int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
185 ECKeyPair keyPair = Curve.generateKeyPair();
186 PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
187
188 axolotlStore.storePreKey(preKeyId, record);
189 records.add(record);
190 }
191
192 preKeyIdOffset = (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE;
193 return records;
194 }
195
196 private PreKeyRecord generateLastResortPreKey() {
197 if (axolotlStore.containsPreKey(Medium.MAX_VALUE)) {
198 try {
199 return axolotlStore.loadPreKey(Medium.MAX_VALUE);
200 } catch (InvalidKeyIdException e) {
201 axolotlStore.removePreKey(Medium.MAX_VALUE);
202 }
203 }
204
205 ECKeyPair keyPair = Curve.generateKeyPair();
206 PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
207
208 axolotlStore.storePreKey(Medium.MAX_VALUE, record);
209
210 return record;
211 }
212
213 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
214 try {
215 ECKeyPair keyPair = Curve.generateKeyPair();
216 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
217 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
218
219 axolotlStore.storeSignedPreKey(nextSignedPreKeyId, record);
220 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
221
222 return record;
223 } catch (InvalidKeyException e) {
224 throw new AssertionError(e);
225 }
226 }
227
228 public void verifyAccount(String verificationCode) throws IOException {
229 verificationCode = verificationCode.replace("-", "");
230 signalingKey = Util.getSecret(52);
231 accountManager.verifyAccountWithCode(verificationCode, signalingKey, axolotlStore.getLocalRegistrationId(), false);
232
233 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
234 registered = true;
235
236 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
237
238 PreKeyRecord lastResortKey = generateLastResortPreKey();
239
240 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(axolotlStore.getIdentityKeyPair());
241
242 accountManager.setPreKeys(axolotlStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
243 }
244
245 public void sendMessage(List<TextSecureAddress> recipients, TextSecureDataMessage message)
246 throws IOException, EncapsulatedExceptions {
247 TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, username, password,
248 axolotlStore, USER_AGENT, Optional.<TextSecureMessageSender.EventListener>absent());
249 messageSender.sendMessage(recipients, message);
250 }
251
252 public TextSecureContent decryptMessage(TextSecureEnvelope envelope) {
253 TextSecureCipher cipher = new TextSecureCipher(new TextSecureAddress(username), axolotlStore);
254 try {
255 return cipher.decrypt(envelope);
256 } catch (Exception e) {
257 // TODO handle all exceptions
258 e.printStackTrace();
259 return null;
260 }
261 }
262
263 public void handleEndSession(String source) {
264 axolotlStore.deleteAllSessions(source);
265 }
266
267 public interface ReceiveMessageHandler {
268 void handleMessage(TextSecureEnvelope envelope, TextSecureContent decryptedContent, GroupInfo group);
269 }
270
271 public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
272 final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
273 TextSecureMessagePipe messagePipe = null;
274
275 try {
276 messagePipe = messageReceiver.createMessagePipe();
277
278 while (true) {
279 TextSecureEnvelope envelope;
280 TextSecureContent content = null;
281 GroupInfo group = null;
282 try {
283 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
284 if (!envelope.isReceipt()) {
285 content = decryptMessage(envelope);
286 if (content != null) {
287 if (content.getDataMessage().isPresent()) {
288 TextSecureDataMessage message = content.getDataMessage().get();
289 if (message.getGroupInfo().isPresent()) {
290 TextSecureGroup groupInfo = message.getGroupInfo().get();
291 switch (groupInfo.getType()) {
292 case UPDATE:
293 group = new GroupInfo(groupInfo.getGroupId(), groupInfo.getName().get(), groupInfo.getMembers().get(), groupInfo.getAvatar().get().asPointer().getId());
294 groupStore.updateGroup(group);
295 break;
296 case DELIVER:
297 group = groupStore.getGroup(groupInfo.getGroupId());
298 break;
299 case QUIT:
300 group = groupStore.getGroup(groupInfo.getGroupId());
301 if (group != null) {
302 group.members.remove(envelope.getSource());
303 }
304 break;
305 }
306 }
307 }
308 }
309 }
310 handler.handleMessage(envelope, content, group);
311 } catch (TimeoutException e) {
312 if (returnOnTimeout)
313 return;
314 } catch (InvalidVersionException e) {
315 System.err.println("Ignoring error: " + e.getMessage());
316 }
317 save();
318 }
319 } finally {
320 if (messagePipe != null)
321 messagePipe.shutdown();
322 }
323 }
324
325 public File retrieveAttachment(TextSecureAttachmentPointer pointer) throws IOException, InvalidMessageException {
326 final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
327
328 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
329 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
330
331 new File(attachmentsPath).mkdirs();
332 File outputFile = new File(attachmentsPath + "/" + pointer.getId());
333 OutputStream output = null;
334 try {
335 output = new FileOutputStream(outputFile);
336 byte[] buffer = new byte[4096];
337 int read;
338
339 while ((read = input.read(buffer)) != -1) {
340 output.write(buffer, 0, read);
341 }
342 } catch (FileNotFoundException e) {
343 e.printStackTrace();
344 return null;
345 } finally {
346 if (output != null) {
347 output.close();
348 output = null;
349 }
350 if (!tmpFile.delete()) {
351 System.err.println("Failed to delete temp file: " + tmpFile);
352 }
353 }
354 if (pointer.getPreview().isPresent()) {
355 File previewFile = new File(outputFile + ".preview");
356 try {
357 output = new FileOutputStream(previewFile);
358 byte[] preview = pointer.getPreview().get();
359 output.write(preview, 0, preview.length);
360 } catch (FileNotFoundException e) {
361 e.printStackTrace();
362 return null;
363 } finally {
364 if (output != null) {
365 output.close();
366 }
367 }
368 }
369 return outputFile;
370 }
371
372 private String canonicalizeNumber(String number) throws InvalidNumberException {
373 String localNumber = username;
374 return PhoneNumberFormatter.formatNumber(number, localNumber);
375 }
376
377 TextSecureAddress getPushAddress(String number) throws InvalidNumberException {
378 String e164number = canonicalizeNumber(number);
379 return new TextSecureAddress(e164number);
380 }
381
382 GroupInfo getGroupInfo(byte[] groupId) {
383 return groupStore.getGroup(groupId);
384 }
385 }