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