1package eu.siacs.conversations.crypto.axolotl;
2
3import android.util.Log;
4
5import org.whispersystems.libaxolotl.AxolotlAddress;
6import org.whispersystems.libaxolotl.IdentityKey;
7import org.whispersystems.libaxolotl.IdentityKeyPair;
8import org.whispersystems.libaxolotl.InvalidKeyException;
9import org.whispersystems.libaxolotl.InvalidKeyIdException;
10import org.whispersystems.libaxolotl.ecc.Curve;
11import org.whispersystems.libaxolotl.ecc.ECKeyPair;
12import org.whispersystems.libaxolotl.state.AxolotlStore;
13import org.whispersystems.libaxolotl.state.PreKeyRecord;
14import org.whispersystems.libaxolotl.state.SessionRecord;
15import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
16import org.whispersystems.libaxolotl.util.KeyHelper;
17
18import java.util.ArrayList;
19import java.util.HashMap;
20import java.util.List;
21import java.util.Map;
22
23import eu.siacs.conversations.Config;
24import eu.siacs.conversations.entities.Account;
25import eu.siacs.conversations.entities.Conversation;
26import eu.siacs.conversations.entities.Message;
27import eu.siacs.conversations.services.XmppConnectionService;
28import eu.siacs.conversations.xmpp.jid.InvalidJidException;
29import eu.siacs.conversations.xmpp.jid.Jid;
30
31public class AxolotlService {
32
33 private Account account;
34 private XmppConnectionService mXmppConnectionService;
35 private SQLiteAxolotlStore axolotlStore;
36 private Map<Jid,XmppAxolotlSession> sessions;
37
38 public static class SQLiteAxolotlStore implements AxolotlStore {
39
40 public static final String PREKEY_TABLENAME = "prekeys";
41 public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys";
42 public static final String SESSION_TABLENAME = "signed_prekeys";
43 public static final String NAME = "name";
44 public static final String DEVICE_ID = "device_id";
45 public static final String ID = "id";
46 public static final String KEY = "key";
47 public static final String ACCOUNT = "account";
48
49 public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key";
50 public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id";
51
52 private final Account account;
53 private final XmppConnectionService mXmppConnectionService;
54
55 private final IdentityKeyPair identityKeyPair;
56 private final int localRegistrationId;
57
58
59 private static IdentityKeyPair generateIdentityKeyPair() {
60 Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair...");
61 ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
62 IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()),
63 identityKeyPairKeys.getPrivateKey());
64 return ownKey;
65 }
66
67 private static int generateRegistrationId() {
68 Log.d(Config.LOGTAG, "Generating axolotl registration ID...");
69 int reg_id = KeyHelper.generateRegistrationId(false);
70 return reg_id;
71 }
72
73 public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
74 this.account = account;
75 this.mXmppConnectionService = service;
76 this.identityKeyPair = loadIdentityKeyPair();
77 this.localRegistrationId = loadRegistrationId();
78 }
79
80 // --------------------------------------
81 // IdentityKeyStore
82 // --------------------------------------
83
84 private IdentityKeyPair loadIdentityKeyPair() {
85 String serializedKey = this.account.getKey(JSONKEY_IDENTITY_KEY_PAIR);
86 IdentityKeyPair ownKey;
87 if( serializedKey != null ) {
88 try {
89 ownKey = new IdentityKeyPair(serializedKey.getBytes());
90 } catch (InvalidKeyException e) {
91 Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage());
92 return null;
93 }
94 } else {
95 Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid());
96 ownKey = generateIdentityKeyPair();
97 boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, new String(ownKey.serialize()));
98 if(success) {
99 mXmppConnectionService.databaseBackend.updateAccount(account);
100 } else {
101 Log.e(Config.LOGTAG, "Failed to write new key to the database!");
102 }
103 }
104 return ownKey;
105 }
106
107 private int loadRegistrationId() {
108 String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
109 int reg_id;
110 if (regIdString != null) {
111 reg_id = Integer.valueOf(regIdString);
112 } else {
113 Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid());
114 reg_id = generateRegistrationId();
115 boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID,""+reg_id);
116 if(success) {
117 mXmppConnectionService.databaseBackend.updateAccount(account);
118 } else {
119 Log.e(Config.LOGTAG, "Failed to write new key to the database!");
120 }
121 }
122 return reg_id;
123 }
124
125 /**
126 * Get the local client's identity key pair.
127 *
128 * @return The local client's persistent identity key pair.
129 */
130 @Override
131 public IdentityKeyPair getIdentityKeyPair() {
132 return identityKeyPair;
133 }
134
135 /**
136 * Return the local client's registration ID.
137 * <p/>
138 * Clients should maintain a registration ID, a random number
139 * between 1 and 16380 that's generated once at install time.
140 *
141 * @return the local client's registration ID.
142 */
143 @Override
144 public int getLocalRegistrationId() {
145 return localRegistrationId;
146 }
147
148 /**
149 * Save a remote client's identity key
150 * <p/>
151 * Store a remote client's identity key as trusted.
152 *
153 * @param name The name of the remote client.
154 * @param identityKey The remote client's identity key.
155 */
156 @Override
157 public void saveIdentity(String name, IdentityKey identityKey) {
158 try {
159 Jid contactJid = Jid.fromString(name);
160 Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid);
161 if (conversation != null) {
162 conversation.getContact().addAxolotlIdentityKey(identityKey, false);
163 mXmppConnectionService.updateConversationUi();
164 mXmppConnectionService.syncRosterToDisk(conversation.getAccount());
165 }
166 } catch (final InvalidJidException e) {
167 Log.e(Config.LOGTAG, "Failed to save identityKey for contact name " + name + ": " + e.toString());
168 }
169 }
170
171 /**
172 * Verify a remote client's identity key.
173 * <p/>
174 * Determine whether a remote client's identity is trusted. Convention is
175 * that the TextSecure protocol is 'trust on first use.' This means that
176 * an identity key is considered 'trusted' if there is no entry for the recipient
177 * in the local store, or if it matches the saved key for a recipient in the local
178 * store. Only if it mismatches an entry in the local store is it considered
179 * 'untrusted.'
180 *
181 * @param name The name of the remote client.
182 * @param identityKey The identity key to verify.
183 * @return true if trusted, false if untrusted.
184 */
185 @Override
186 public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
187 try {
188 Jid contactJid = Jid.fromString(name);
189 Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid);
190 if (conversation != null) {
191 List<IdentityKey> trustedKeys = conversation.getContact().getTrustedAxolotlIdentityKeys();
192 return trustedKeys.contains(identityKey);
193 } else {
194 return false;
195 }
196 } catch (final InvalidJidException e) {
197 Log.e(Config.LOGTAG, "Failed to save identityKey for contact name" + name + ": " + e.toString());
198 return false;
199 }
200 }
201
202 // --------------------------------------
203 // SessionStore
204 // --------------------------------------
205
206 /**
207 * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple,
208 * or a new SessionRecord if one does not currently exist.
209 * <p/>
210 * It is important that implementations return a copy of the current durable information. The
211 * returned SessionRecord may be modified, but those changes should not have an effect on the
212 * durable session state (what is returned by subsequent calls to this method) without the
213 * store method being called here first.
214 *
215 * @param address The name and device ID of the remote client.
216 * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or
217 * a new SessionRecord if one does not currently exist.
218 */
219 @Override
220 public SessionRecord loadSession(AxolotlAddress address) {
221 SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address);
222 return (session!=null)?session:new SessionRecord();
223 }
224
225 /**
226 * Returns all known devices with active sessions for a recipient
227 *
228 * @param name the name of the client.
229 * @return all known sub-devices with active sessions.
230 */
231 @Override
232 public List<Integer> getSubDeviceSessions(String name) {
233 return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account,
234 new AxolotlAddress(name,0));
235 }
236
237 /**
238 * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
239 *
240 * @param address the address of the remote client.
241 * @param record the current SessionRecord for the remote client.
242 */
243 @Override
244 public void storeSession(AxolotlAddress address, SessionRecord record) {
245 mXmppConnectionService.databaseBackend.storeSession(account, address, record);
246 }
247
248 /**
249 * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple.
250 *
251 * @param address the address of the remote client.
252 * @return true if a {@link SessionRecord} exists, false otherwise.
253 */
254 @Override
255 public boolean containsSession(AxolotlAddress address) {
256 return mXmppConnectionService.databaseBackend.containsSession(account, address);
257 }
258
259 /**
260 * Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
261 *
262 * @param address the address of the remote client.
263 */
264 @Override
265 public void deleteSession(AxolotlAddress address) {
266 mXmppConnectionService.databaseBackend.deleteSession(account, address);
267 }
268
269 /**
270 * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
271 *
272 * @param name the name of the remote client.
273 */
274 @Override
275 public void deleteAllSessions(String name) {
276 mXmppConnectionService.databaseBackend.deleteAllSessions(account,
277 new AxolotlAddress(name,0));
278 }
279
280 // --------------------------------------
281 // PreKeyStore
282 // --------------------------------------
283
284 /**
285 * Load a local PreKeyRecord.
286 *
287 * @param preKeyId the ID of the local PreKeyRecord.
288 * @return the corresponding PreKeyRecord.
289 * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
290 */
291 @Override
292 public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
293 PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
294 if(record == null) {
295 throw new InvalidKeyIdException("No such PreKeyRecord!");
296 }
297 return record;
298 }
299
300 /**
301 * Store a local PreKeyRecord.
302 *
303 * @param preKeyId the ID of the PreKeyRecord to store.
304 * @param record the PreKeyRecord.
305 */
306 @Override
307 public void storePreKey(int preKeyId, PreKeyRecord record) {
308 mXmppConnectionService.databaseBackend.storePreKey(account, record);
309 }
310
311 /**
312 * @param preKeyId A PreKeyRecord ID.
313 * @return true if the store has a record for the preKeyId, otherwise false.
314 */
315 @Override
316 public boolean containsPreKey(int preKeyId) {
317 return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
318 }
319
320 /**
321 * Delete a PreKeyRecord from local storage.
322 *
323 * @param preKeyId The ID of the PreKeyRecord to remove.
324 */
325 @Override
326 public void removePreKey(int preKeyId) {
327 mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
328 }
329
330 // --------------------------------------
331 // SignedPreKeyStore
332 // --------------------------------------
333
334 /**
335 * Load a local SignedPreKeyRecord.
336 *
337 * @param signedPreKeyId the ID of the local SignedPreKeyRecord.
338 * @return the corresponding SignedPreKeyRecord.
339 * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
340 */
341 @Override
342 public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
343 SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
344 if(record == null) {
345 throw new InvalidKeyIdException("No such PreKeyRecord!");
346 }
347 return record;
348 }
349
350 /**
351 * Load all local SignedPreKeyRecords.
352 *
353 * @return All stored SignedPreKeyRecords.
354 */
355 @Override
356 public List<SignedPreKeyRecord> loadSignedPreKeys() {
357 return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
358 }
359
360 /**
361 * Store a local SignedPreKeyRecord.
362 *
363 * @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
364 * @param record the SignedPreKeyRecord.
365 */
366 @Override
367 public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
368 mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
369 }
370
371 /**
372 * @param signedPreKeyId A SignedPreKeyRecord ID.
373 * @return true if the store has a record for the signedPreKeyId, otherwise false.
374 */
375 @Override
376 public boolean containsSignedPreKey(int signedPreKeyId) {
377 return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
378 }
379
380 /**
381 * Delete a SignedPreKeyRecord from local storage.
382 *
383 * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
384 */
385 @Override
386 public void removeSignedPreKey(int signedPreKeyId) {
387 mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
388 }
389 }
390
391 private static class XmppAxolotlSession {
392 private List<Message> untrustedMessages;
393 private AxolotlStore axolotlStore;
394
395 public XmppAxolotlSession(SQLiteAxolotlStore axolotlStore) {
396 this.untrustedMessages = new ArrayList<>();
397 this.axolotlStore = axolotlStore;
398 }
399
400 public void trust() {
401 for (Message message : this.untrustedMessages) {
402 message.trust();
403 }
404 this.untrustedMessages = null;
405 }
406
407 public boolean isTrusted() {
408 return (this.untrustedMessages == null);
409 }
410
411 public String processReceiving(XmppAxolotlMessage incomingMessage) {
412 return null;
413 }
414
415 public XmppAxolotlMessage processSending(String outgoingMessage) {
416 return null;
417 }
418 }
419
420 public AxolotlService(Account account, XmppConnectionService connectionService) {
421 this.mXmppConnectionService = connectionService;
422 this.account = account;
423 this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
424 this.sessions = new HashMap<>();
425 }
426
427 public void trustSession(Jid counterpart) {
428 XmppAxolotlSession session = sessions.get(counterpart);
429 if(session != null) {
430 session.trust();
431 }
432 }
433
434 public boolean isTrustedSession(Jid counterpart) {
435 XmppAxolotlSession session = sessions.get(counterpart);
436 return session != null && session.isTrusted();
437 }
438
439
440}