SQLiteAxolotlStore.java

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