1package eu.siacs.conversations.crypto.axolotl;
2
3import android.util.Log;
4import android.util.LruCache;
5import eu.siacs.conversations.Config;
6import eu.siacs.conversations.entities.Account;
7import eu.siacs.conversations.services.XmppConnectionService;
8import eu.siacs.conversations.utils.CryptoHelper;
9import java.security.cert.X509Certificate;
10import java.util.HashSet;
11import java.util.List;
12import java.util.Set;
13import org.whispersystems.libsignal.IdentityKey;
14import org.whispersystems.libsignal.IdentityKeyPair;
15import org.whispersystems.libsignal.InvalidKeyIdException;
16import org.whispersystems.libsignal.SignalProtocolAddress;
17import org.whispersystems.libsignal.ecc.Curve;
18import org.whispersystems.libsignal.ecc.ECKeyPair;
19import org.whispersystems.libsignal.state.PreKeyRecord;
20import org.whispersystems.libsignal.state.SessionRecord;
21import org.whispersystems.libsignal.state.SignalProtocolStore;
22import org.whispersystems.libsignal.state.SignedPreKeyRecord;
23import org.whispersystems.libsignal.util.KeyHelper;
24
25public class SQLiteAxolotlStore implements SignalProtocolStore {
26
27 public static final String PREKEY_TABLENAME = "prekeys";
28 public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys";
29 public static final String SESSION_TABLENAME = "sessions";
30 public static final String IDENTITIES_TABLENAME = "identities";
31 public static final String ACCOUNT = "account";
32 public static final String DEVICE_ID = "device_id";
33 public static final String ID = "id";
34 public static final String KEY = "key";
35 public static final String FINGERPRINT = "fingerprint";
36 public static final String NAME = "name";
37 public static final String TRUSTED = "trusted"; // no longer used
38 public static final String TRUST = "trust";
39 public static final String ACTIVE = "active";
40 public static final String LAST_ACTIVATION = "last_activation";
41 public static final String OWN = "ownkey";
42 public static final String CERTIFICATE = "certificate";
43
44 public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id";
45 public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id";
46
47 private static final int NUM_TRUSTS_TO_CACHE = 100;
48
49 private final Account account;
50 private final XmppConnectionService mXmppConnectionService;
51
52 private IdentityKeyPair identityKeyPair;
53 private int localRegistrationId;
54 private int currentPreKeyId = 0;
55
56 private final HashSet<Integer> preKeysMarkedForRemoval = new HashSet<>();
57
58 private final LruCache<String, FingerprintStatus> trustCache =
59 new LruCache<String, FingerprintStatus>(NUM_TRUSTS_TO_CACHE) {
60 @Override
61 protected FingerprintStatus create(String fingerprint) {
62 return mXmppConnectionService.databaseBackend.getFingerprintStatus(
63 account, fingerprint);
64 }
65 };
66
67 private static IdentityKeyPair generateIdentityKeyPair() {
68 Log.i(
69 Config.LOGTAG,
70 AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair...");
71 ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
72 return new IdentityKeyPair(
73 new IdentityKey(identityKeyPairKeys.getPublicKey()),
74 identityKeyPairKeys.getPrivateKey());
75 }
76
77 private static int generateRegistrationId() {
78 Log.i(
79 Config.LOGTAG,
80 AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID...");
81 return KeyHelper.generateRegistrationId(true);
82 }
83
84 public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
85 this.account = account;
86 this.mXmppConnectionService = service;
87 this.localRegistrationId = loadRegistrationId();
88 this.currentPreKeyId = loadCurrentPreKeyId();
89 }
90
91 public int getCurrentPreKeyId() {
92 return currentPreKeyId;
93 }
94
95 // --------------------------------------
96 // IdentityKeyStore
97 // --------------------------------------
98
99 private IdentityKeyPair loadIdentityKeyPair() {
100 synchronized (mXmppConnectionService) {
101 IdentityKeyPair ownKey =
102 mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account);
103
104 if (ownKey != null) {
105 return ownKey;
106 } else {
107 Log.i(
108 Config.LOGTAG,
109 AxolotlService.getLogprefix(account)
110 + "Could not retrieve own IdentityKeyPair");
111 ownKey = generateIdentityKeyPair();
112 mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey);
113 }
114 return ownKey;
115 }
116 }
117
118 private int loadRegistrationId() {
119 return loadRegistrationId(false);
120 }
121
122 private int loadRegistrationId(boolean regenerate) {
123 String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
124 int reg_id;
125 if (!regenerate && regIdString != null) {
126 reg_id = Integer.valueOf(regIdString);
127 } else {
128 Log.i(
129 Config.LOGTAG,
130 AxolotlService.getLogprefix(account)
131 + "Could not retrieve axolotl registration id for account "
132 + account.getJid());
133 reg_id = generateRegistrationId();
134 boolean success =
135 this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id));
136 if (success) {
137 mXmppConnectionService.databaseBackend.updateAccount(account);
138 } else {
139 Log.e(
140 Config.LOGTAG,
141 AxolotlService.getLogprefix(account)
142 + "Failed to write new key to the database!");
143 }
144 }
145 return reg_id;
146 }
147
148 private int loadCurrentPreKeyId() {
149 String prekeyIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
150 int prekey_id;
151 if (prekeyIdString != null) {
152 prekey_id = Integer.valueOf(prekeyIdString);
153 } else {
154 Log.w(
155 Config.LOGTAG,
156 AxolotlService.getLogprefix(account)
157 + "Could not retrieve current prekey id for account "
158 + account.getJid());
159 prekey_id = 0;
160 }
161 return prekey_id;
162 }
163
164 public void regenerate() {
165 mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
166 trustCache.evictAll();
167 account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0));
168 identityKeyPair = loadIdentityKeyPair();
169 localRegistrationId = loadRegistrationId(true);
170 currentPreKeyId = 0;
171 mXmppConnectionService.updateAccountUi();
172 }
173
174 /**
175 * Get the local client's identity key pair.
176 *
177 * @return The local client's persistent identity key pair.
178 */
179 @Override
180 public IdentityKeyPair getIdentityKeyPair() {
181 if (identityKeyPair == null) {
182 identityKeyPair = loadIdentityKeyPair();
183 }
184 return identityKeyPair;
185 }
186
187 /**
188 * Return the local client's registration ID.
189 *
190 * <p>Clients should maintain a registration ID, a random number between 1 and 16380 that's
191 * generated once at install time.
192 *
193 * @return the local client's registration ID.
194 */
195 @Override
196 public int getLocalRegistrationId() {
197 return localRegistrationId;
198 }
199
200 /**
201 * Save a remote client's identity key
202 *
203 * <p>Store a remote client's identity key as trusted.
204 *
205 * @param address The address of the remote client.
206 * @param identityKey The remote client's identity key.
207 * @return true on success
208 */
209 @Override
210 public boolean saveIdentity(
211 final SignalProtocolAddress address, final IdentityKey identityKey) {
212 if (!mXmppConnectionService
213 .databaseBackend
214 .loadIdentityKeys(account, address.getName())
215 .contains(identityKey)) {
216 String fingerprint = CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize());
217 FingerprintStatus status = getFingerprintStatus(fingerprint);
218 if (status == null) {
219 if (mXmppConnectionService.getAppSettings().isBTBVEnabled()
220 && !account.getAxolotlService().hasVerifiedKeys(address.getName())) {
221 Log.d(
222 Config.LOGTAG,
223 account.getJid().asBareJid()
224 + ": blindly trusted "
225 + fingerprint
226 + " of "
227 + address.getName());
228 status = FingerprintStatus.createActiveTrusted();
229 } else {
230 status = FingerprintStatus.createActiveUndecided();
231 }
232 } else {
233 status = status.toActive();
234 }
235 mXmppConnectionService.databaseBackend.storeIdentityKey(
236 account, address.getName(), identityKey, status);
237 trustCache.remove(fingerprint);
238 }
239 return true;
240 }
241
242 /**
243 * Verify a remote client's identity key.
244 *
245 * <p>Determine whether a remote client's identity is trusted. Convention is that the TextSecure
246 * protocol is 'trust on first use.' This means that an identity key is considered 'trusted' if
247 * there is no entry for the recipient in the local store, or if it matches the saved key for a
248 * recipient in the local store. Only if it mismatches an entry in the local store is it
249 * considered 'untrusted.'
250 *
251 * @param identityKey The identity key to verify.
252 * @return true if trusted, false if untrusted.
253 */
254 @Override
255 public boolean isTrustedIdentity(
256 SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
257 return true;
258 }
259
260 public FingerprintStatus getFingerprintStatus(String fingerprint) {
261 return (fingerprint == null) ? null : trustCache.get(fingerprint);
262 }
263
264 public void setFingerprintStatus(String fingerprint, FingerprintStatus status) {
265 mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status);
266 trustCache.remove(fingerprint);
267 }
268
269 public void setFingerprintCertificate(String fingerprint, X509Certificate x509Certificate) {
270 mXmppConnectionService.databaseBackend.setIdentityKeyCertificate(
271 account, fingerprint, x509Certificate);
272 }
273
274 public X509Certificate getFingerprintCertificate(String fingerprint) {
275 return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(
276 account, fingerprint);
277 }
278
279 public Set<IdentityKey> getContactKeysWithTrust(String bareJid, FingerprintStatus status) {
280 return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status);
281 }
282
283 public long getContactNumTrustedKeys(String bareJid) {
284 return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid);
285 }
286
287 // --------------------------------------
288 // SessionStore
289 // --------------------------------------
290
291 /**
292 * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId
293 * tuple, or a new SessionRecord if one does not currently exist.
294 *
295 * <p>It is important that implementations return a copy of the current durable information. The
296 * returned SessionRecord may be modified, but those changes should not have an effect on the
297 * durable session state (what is returned by subsequent calls to this method) without the store
298 * method being called here first.
299 *
300 * @param address The name and device ID of the remote client.
301 * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or a
302 * new SessionRecord if one does not currently exist.
303 */
304 @Override
305 public SessionRecord loadSession(SignalProtocolAddress address) {
306 SessionRecord session =
307 mXmppConnectionService.databaseBackend.loadSession(this.account, address);
308 return (session != null) ? session : new SessionRecord();
309 }
310
311 /**
312 * Returns all known devices with active sessions for a recipient
313 *
314 * @param name the name of the client.
315 * @return all known sub-devices with active sessions.
316 */
317 @Override
318 public List<Integer> getSubDeviceSessions(String name) {
319 return mXmppConnectionService.databaseBackend.getSubDeviceSessions(
320 account, new SignalProtocolAddress(name, 0));
321 }
322
323 public List<String> getKnownAddresses() {
324 return mXmppConnectionService.databaseBackend.getKnownSignalAddresses(account);
325 }
326
327 /**
328 * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
329 *
330 * @param address the address of the remote client.
331 * @param record the current SessionRecord for the remote client.
332 */
333 @Override
334 public void storeSession(SignalProtocolAddress address, SessionRecord record) {
335 mXmppConnectionService.databaseBackend.storeSession(account, address, record);
336 }
337
338 /**
339 * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId
340 * tuple.
341 *
342 * @param address the address of the remote client.
343 * @return true if a {@link SessionRecord} exists, false otherwise.
344 */
345 @Override
346 public boolean containsSession(SignalProtocolAddress address) {
347 return mXmppConnectionService.databaseBackend.containsSession(account, address);
348 }
349
350 /**
351 * Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
352 *
353 * @param address the address of the remote client.
354 */
355 @Override
356 public void deleteSession(SignalProtocolAddress address) {
357 mXmppConnectionService.databaseBackend.deleteSession(account, address);
358 }
359
360 /**
361 * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
362 *
363 * @param name the name of the remote client.
364 */
365 @Override
366 public void deleteAllSessions(String name) {
367 SignalProtocolAddress address = new SignalProtocolAddress(name, 0);
368 mXmppConnectionService.databaseBackend.deleteAllSessions(account, address);
369 }
370
371 // --------------------------------------
372 // PreKeyStore
373 // --------------------------------------
374
375 /**
376 * Load a local PreKeyRecord.
377 *
378 * @param preKeyId the ID of the local PreKeyRecord.
379 * @return the corresponding PreKeyRecord.
380 * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
381 */
382 @Override
383 public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
384 PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
385 if (record == null) {
386 throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId);
387 }
388 return record;
389 }
390
391 /**
392 * Store a local PreKeyRecord.
393 *
394 * @param preKeyId the ID of the PreKeyRecord to store.
395 * @param record the PreKeyRecord.
396 */
397 @Override
398 public void storePreKey(int preKeyId, PreKeyRecord record) {
399 mXmppConnectionService.databaseBackend.storePreKey(account, record);
400 currentPreKeyId = preKeyId;
401 boolean success =
402 this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId));
403 if (success) {
404 mXmppConnectionService.databaseBackend.updateAccount(account);
405 } else {
406 Log.e(
407 Config.LOGTAG,
408 AxolotlService.getLogprefix(account)
409 + "Failed to write new prekey id to the database!");
410 }
411 }
412
413 /**
414 * @param preKeyId A PreKeyRecord ID.
415 * @return true if the store has a record for the preKeyId, otherwise false.
416 */
417 @Override
418 public boolean containsPreKey(int preKeyId) {
419 return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
420 }
421
422 /**
423 * Delete a PreKeyRecord from local storage.
424 *
425 * @param preKeyId The ID of the PreKeyRecord to remove.
426 */
427 @Override
428 public void removePreKey(int preKeyId) {
429 Log.d(Config.LOGTAG, "mark prekey for removal " + preKeyId);
430 synchronized (preKeysMarkedForRemoval) {
431 preKeysMarkedForRemoval.add(preKeyId);
432 }
433 }
434
435 public boolean flushPreKeys() {
436 Log.d(Config.LOGTAG, "flushing pre keys");
437 int count = 0;
438 synchronized (preKeysMarkedForRemoval) {
439 for (Integer preKeyId : preKeysMarkedForRemoval) {
440 count += mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
441 }
442 preKeysMarkedForRemoval.clear();
443 }
444 return count > 0;
445 }
446
447 // --------------------------------------
448 // SignedPreKeyStore
449 // --------------------------------------
450
451 /**
452 * Load a local SignedPreKeyRecord.
453 *
454 * @param signedPreKeyId the ID of the local SignedPreKeyRecord.
455 * @return the corresponding SignedPreKeyRecord.
456 * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
457 */
458 @Override
459 public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
460 SignedPreKeyRecord record =
461 mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
462 if (record == null) {
463 throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId);
464 }
465 return record;
466 }
467
468 /**
469 * Load all local SignedPreKeyRecords.
470 *
471 * @return All stored SignedPreKeyRecords.
472 */
473 @Override
474 public List<SignedPreKeyRecord> loadSignedPreKeys() {
475 return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
476 }
477
478 public int getSignedPreKeysCount() {
479 return mXmppConnectionService.databaseBackend.getSignedPreKeysCount(account);
480 }
481
482 /**
483 * Store a local SignedPreKeyRecord.
484 *
485 * @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
486 * @param record the SignedPreKeyRecord.
487 */
488 @Override
489 public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
490 mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
491 }
492
493 /**
494 * @param signedPreKeyId A SignedPreKeyRecord ID.
495 * @return true if the store has a record for the signedPreKeyId, otherwise false.
496 */
497 @Override
498 public boolean containsSignedPreKey(int signedPreKeyId) {
499 return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
500 }
501
502 /**
503 * Delete a SignedPreKeyRecord from local storage.
504 *
505 * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
506 */
507 @Override
508 public void removeSignedPreKey(int signedPreKeyId) {
509 mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
510 }
511
512 public void preVerifyFingerprint(Account account, String name, String fingerprint) {
513 mXmppConnectionService.databaseBackend.storePreVerification(
514 account, name, fingerprint, FingerprintStatus.createInactiveVerified());
515 }
516}