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