1package eu.siacs.conversations.crypto.axolotl;
2
3import android.os.Bundle;
4import android.security.KeyChain;
5import android.support.annotation.NonNull;
6import android.support.annotation.Nullable;
7import android.util.Log;
8import android.util.Pair;
9
10import org.bouncycastle.jce.provider.BouncyCastleProvider;
11import org.whispersystems.libaxolotl.AxolotlAddress;
12import org.whispersystems.libaxolotl.IdentityKey;
13import org.whispersystems.libaxolotl.IdentityKeyPair;
14import org.whispersystems.libaxolotl.InvalidKeyException;
15import org.whispersystems.libaxolotl.InvalidKeyIdException;
16import org.whispersystems.libaxolotl.SessionBuilder;
17import org.whispersystems.libaxolotl.UntrustedIdentityException;
18import org.whispersystems.libaxolotl.ecc.ECPublicKey;
19import org.whispersystems.libaxolotl.state.PreKeyBundle;
20import org.whispersystems.libaxolotl.state.PreKeyRecord;
21import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
22import org.whispersystems.libaxolotl.util.KeyHelper;
23
24import java.security.PrivateKey;
25import java.security.Security;
26import java.security.Signature;
27import java.security.cert.X509Certificate;
28import java.util.Arrays;
29import java.util.HashMap;
30import java.util.HashSet;
31import java.util.List;
32import java.util.Map;
33import java.util.Random;
34import java.util.Set;
35
36import eu.siacs.conversations.Config;
37import eu.siacs.conversations.entities.Account;
38import eu.siacs.conversations.entities.Contact;
39import eu.siacs.conversations.entities.Conversation;
40import eu.siacs.conversations.entities.Message;
41import eu.siacs.conversations.parser.IqParser;
42import eu.siacs.conversations.services.XmppConnectionService;
43import eu.siacs.conversations.utils.CryptoHelper;
44import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
45import eu.siacs.conversations.xml.Element;
46import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
47import eu.siacs.conversations.xmpp.OnIqPacketReceived;
48import eu.siacs.conversations.xmpp.jid.InvalidJidException;
49import eu.siacs.conversations.xmpp.jid.Jid;
50import eu.siacs.conversations.xmpp.stanzas.IqPacket;
51
52public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
53
54 public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
55 public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
56 public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles";
57 public static final String PEP_VERIFICATION = PEP_PREFIX + ".verification";
58
59 public static final String LOGPREFIX = "AxolotlService";
60
61 public static final int NUM_KEYS_TO_PUBLISH = 100;
62 public static final int publishTriesThreshold = 3;
63
64 private final Account account;
65 private final XmppConnectionService mXmppConnectionService;
66 private final SQLiteAxolotlStore axolotlStore;
67 private final SessionMap sessions;
68 private final Map<Jid, Set<Integer>> deviceIds;
69 private final Map<String, XmppAxolotlMessage> messageCache;
70 private final FetchStatusMap fetchStatusMap;
71 private final SerialSingleThreadExecutor executor;
72 private int numPublishTriesOnEmptyPep = 0;
73 private boolean pepBroken = false;
74
75 @Override
76 public void onAdvancedStreamFeaturesAvailable(Account account) {
77 if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().pep()) {
78 publishBundlesIfNeeded(true, false);
79 } else {
80 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping OMEMO initialization");
81 }
82 }
83
84 public boolean fetchMapHasErrors(List<Jid> jids) {
85 for(Jid jid : jids) {
86 if (deviceIds.get(jid) != null) {
87 for (Integer foreignId : this.deviceIds.get(jid)) {
88 AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
89 if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
90 return true;
91 }
92 }
93 }
94 }
95 return false;
96 }
97
98 private static class AxolotlAddressMap<T> {
99 protected Map<String, Map<Integer, T>> map;
100 protected final Object MAP_LOCK = new Object();
101
102 public AxolotlAddressMap() {
103 this.map = new HashMap<>();
104 }
105
106 public void put(AxolotlAddress address, T value) {
107 synchronized (MAP_LOCK) {
108 Map<Integer, T> devices = map.get(address.getName());
109 if (devices == null) {
110 devices = new HashMap<>();
111 map.put(address.getName(), devices);
112 }
113 devices.put(address.getDeviceId(), value);
114 }
115 }
116
117 public T get(AxolotlAddress address) {
118 synchronized (MAP_LOCK) {
119 Map<Integer, T> devices = map.get(address.getName());
120 if (devices == null) {
121 return null;
122 }
123 return devices.get(address.getDeviceId());
124 }
125 }
126
127 public Map<Integer, T> getAll(AxolotlAddress address) {
128 synchronized (MAP_LOCK) {
129 Map<Integer, T> devices = map.get(address.getName());
130 if (devices == null) {
131 return new HashMap<>();
132 }
133 return devices;
134 }
135 }
136
137 public boolean hasAny(AxolotlAddress address) {
138 synchronized (MAP_LOCK) {
139 Map<Integer, T> devices = map.get(address.getName());
140 return devices != null && !devices.isEmpty();
141 }
142 }
143
144 public void clear() {
145 map.clear();
146 }
147
148 }
149
150 private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
151 private final XmppConnectionService xmppConnectionService;
152 private final Account account;
153
154 public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) {
155 super();
156 this.xmppConnectionService = service;
157 this.account = account;
158 this.fillMap(store);
159 }
160
161 private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
162 for (Integer deviceId : deviceIds) {
163 AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId);
164 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString());
165 IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey();
166 if(Config.X509_VERIFICATION) {
167 X509Certificate certificate = store.getFingerprintCertificate(identityKey.getFingerprint().replaceAll("\\s", ""));
168 if (certificate != null) {
169 Bundle information = CryptoHelper.extractCertificateInformation(certificate);
170 try {
171 final String cn = information.getString("subject_cn");
172 final Jid jid = Jid.fromString(bareJid);
173 Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn);
174 account.getRoster().getContact(jid).setCommonName(cn);
175 } catch (final InvalidJidException ignored) {
176 //ignored
177 }
178 }
179 }
180 this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey));
181 }
182 }
183
184 private void fillMap(SQLiteAxolotlStore store) {
185 List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString());
186 putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store);
187 for (Contact contact : account.getRoster().getContacts()) {
188 Jid bareJid = contact.getJid().toBareJid();
189 String address = bareJid.toString();
190 deviceIds = store.getSubDeviceSessions(address);
191 putDevicesForJid(address, deviceIds, store);
192 }
193
194 }
195
196 @Override
197 public void put(AxolotlAddress address, XmppAxolotlSession value) {
198 super.put(address, value);
199 value.setNotFresh();
200 xmppConnectionService.syncRosterToDisk(account);
201 }
202
203 public void put(XmppAxolotlSession session) {
204 this.put(session.getRemoteAddress(), session);
205 }
206 }
207
208 public enum FetchStatus {
209 PENDING,
210 SUCCESS,
211 SUCCESS_VERIFIED,
212 TIMEOUT,
213 ERROR
214 }
215
216 private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
217
218 }
219
220 public static String getLogprefix(Account account) {
221 return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): ";
222 }
223
224 public AxolotlService(Account account, XmppConnectionService connectionService) {
225 if (Security.getProvider("BC") == null) {
226 Security.addProvider(new BouncyCastleProvider());
227 }
228 this.mXmppConnectionService = connectionService;
229 this.account = account;
230 this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
231 this.deviceIds = new HashMap<>();
232 this.messageCache = new HashMap<>();
233 this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account);
234 this.fetchStatusMap = new FetchStatusMap();
235 this.executor = new SerialSingleThreadExecutor();
236 }
237
238 public String getOwnFingerprint() {
239 return axolotlStore.getIdentityKeyPair().getPublicKey().getFingerprint().replaceAll("\\s", "");
240 }
241
242 public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) {
243 return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust);
244 }
245
246 public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid) {
247 return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toString(), trust);
248 }
249
250 public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids) {
251 Set<IdentityKey> keys = new HashSet<>();
252 for(Jid jid : jids) {
253 keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toString(), trust));
254 }
255 return keys;
256 }
257
258 public long getNumTrustedKeys(Jid jid) {
259 return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString());
260 }
261
262 public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) {
263 for(Jid jid : jids) {
264 if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString()) == 0) {
265 return true;
266 }
267 }
268 return false;
269 }
270
271 private AxolotlAddress getAddressForJid(Jid jid) {
272 return new AxolotlAddress(jid.toString(), 0);
273 }
274
275 private Set<XmppAxolotlSession> findOwnSessions() {
276 AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
277 return new HashSet<>(this.sessions.getAll(ownAddress).values());
278 }
279
280 private Set<XmppAxolotlSession> findSessionsForContact(Contact contact) {
281 AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
282 return new HashSet<>(this.sessions.getAll(contactAddress).values());
283 }
284
285 private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
286 HashSet<XmppAxolotlSession> sessions = new HashSet<>();
287 for(Jid jid : conversation.getAcceptedCryptoTargets()) {
288 sessions.addAll(this.sessions.getAll(getAddressForJid(jid)).values());
289 }
290 return sessions;
291 }
292
293 public Set<String> getFingerprintsForOwnSessions() {
294 Set<String> fingerprints = new HashSet<>();
295 for (XmppAxolotlSession session : findOwnSessions()) {
296 fingerprints.add(session.getFingerprint());
297 }
298 return fingerprints;
299 }
300
301 public Set<String> getFingerprintsForContact(final Contact contact) {
302 Set<String> fingerprints = new HashSet<>();
303 for (XmppAxolotlSession session : findSessionsForContact(contact)) {
304 fingerprints.add(session.getFingerprint());
305 }
306 return fingerprints;
307 }
308
309 private boolean hasAny(Jid jid) {
310 return sessions.hasAny(getAddressForJid(jid));
311 }
312
313 public boolean isPepBroken() {
314 return this.pepBroken;
315 }
316
317 public void resetBrokenness() {
318 this.pepBroken = false;
319 numPublishTriesOnEmptyPep = 0;
320 }
321
322 public void regenerateKeys(boolean wipeOther) {
323 axolotlStore.regenerate();
324 sessions.clear();
325 fetchStatusMap.clear();
326 publishBundlesIfNeeded(true, wipeOther);
327 }
328
329 public int getOwnDeviceId() {
330 return axolotlStore.getLocalRegistrationId();
331 }
332
333 public Set<Integer> getOwnDeviceIds() {
334 return this.deviceIds.get(account.getJid().toBareJid());
335 }
336
337 private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds,
338 final XmppAxolotlSession.Trust from,
339 final XmppAxolotlSession.Trust to) {
340 for (Integer deviceId : deviceIds) {
341 AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
342 XmppAxolotlSession session = sessions.get(address);
343 if (session != null && session.getFingerprint() != null
344 && session.getTrust() == from) {
345 session.setTrust(to);
346 }
347 }
348 }
349
350 public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
351 if (jid.toBareJid().equals(account.getJid().toBareJid())) {
352 if (!deviceIds.isEmpty()) {
353 Log.d(Config.LOGTAG, getLogprefix(account) + "Received non-empty own device list. Resetting publish attempts and pepBroken status.");
354 pepBroken = false;
355 numPublishTriesOnEmptyPep = 0;
356 }
357 if (deviceIds.contains(getOwnDeviceId())) {
358 deviceIds.remove(getOwnDeviceId());
359 } else {
360 publishOwnDeviceId(deviceIds);
361 }
362 for (Integer deviceId : deviceIds) {
363 AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
364 if (sessions.get(ownDeviceAddress) == null) {
365 buildSessionFromPEP(ownDeviceAddress);
366 }
367 }
368 }
369 Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString()));
370 expiredDevices.removeAll(deviceIds);
371 setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED,
372 XmppAxolotlSession.Trust.INACTIVE_TRUSTED);
373 setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED_X509,
374 XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
375 setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED,
376 XmppAxolotlSession.Trust.INACTIVE_UNDECIDED);
377 setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED,
378 XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED);
379 Set<Integer> newDevices = new HashSet<>(deviceIds);
380 setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED,
381 XmppAxolotlSession.Trust.TRUSTED);
382 setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509,
383 XmppAxolotlSession.Trust.TRUSTED_X509);
384 setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED,
385 XmppAxolotlSession.Trust.UNDECIDED);
386 setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED,
387 XmppAxolotlSession.Trust.UNTRUSTED);
388 this.deviceIds.put(jid, deviceIds);
389 mXmppConnectionService.keyStatusUpdated(null);
390 }
391
392 public void wipeOtherPepDevices() {
393 if (pepBroken) {
394 Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... ");
395 return;
396 }
397 Set<Integer> deviceIds = new HashSet<>();
398 deviceIds.add(getOwnDeviceId());
399 IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
400 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish);
401 mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
402 @Override
403 public void onIqPacketReceived(Account account, IqPacket packet) {
404 // TODO: implement this!
405 }
406 });
407 }
408
409 public void purgeKey(final String fingerprint) {
410 axolotlStore.setFingerprintTrust(fingerprint.replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED);
411 }
412
413 public void publishOwnDeviceIdIfNeeded() {
414 if (pepBroken) {
415 Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... ");
416 return;
417 }
418 IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid());
419 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
420 @Override
421 public void onIqPacketReceived(Account account, IqPacket packet) {
422 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
423 Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids.");
424 } else {
425 Element item = mXmppConnectionService.getIqParser().getItem(packet);
426 Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
427 if (!deviceIds.contains(getOwnDeviceId())) {
428 publishOwnDeviceId(deviceIds);
429 }
430 }
431 }
432 });
433 }
434
435 public void publishOwnDeviceId(Set<Integer> deviceIds) {
436 Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds);
437 if (!deviceIdsCopy.contains(getOwnDeviceId())) {
438 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist.");
439 if (deviceIdsCopy.isEmpty()) {
440 if (numPublishTriesOnEmptyPep >= publishTriesThreshold) {
441 Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting...");
442 pepBroken = true;
443 return;
444 } else {
445 numPublishTriesOnEmptyPep++;
446 Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")");
447 }
448 } else {
449 numPublishTriesOnEmptyPep = 0;
450 }
451 deviceIdsCopy.add(getOwnDeviceId());
452 IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIdsCopy);
453 mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
454 @Override
455 public void onIqPacketReceived(Account account, IqPacket packet) {
456 if (packet.getType() == IqPacket.TYPE.ERROR) {
457 pepBroken = true;
458 Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error"));
459 }
460 }
461 });
462 }
463 }
464
465 public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord,
466 final Set<PreKeyRecord> preKeyRecords,
467 final boolean announceAfter,
468 final boolean wipe) {
469 try {
470 IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey();
471 PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias());
472 X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias());
473 Signature verifier = Signature.getInstance("sha256WithRSA");
474 verifier.initSign(x509PrivateKey,mXmppConnectionService.getRNG());
475 verifier.update(axolotlPublicKey.serialize());
476 byte[] signature = verifier.sign();
477 IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId());
478 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device "+getOwnDeviceId());
479 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
480 @Override
481 public void onIqPacketReceived(Account account, IqPacket packet) {
482 publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
483 }
484 });
485 } catch (Exception e) {
486 e.printStackTrace();
487 }
488 }
489
490 public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) {
491 if (pepBroken) {
492 Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... ");
493 return;
494 }
495 IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId());
496 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
497 @Override
498 public void onIqPacketReceived(Account account, IqPacket packet) {
499
500 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
501 return; //ignore timeout. do nothing
502 }
503
504 if (packet.getType() == IqPacket.TYPE.ERROR) {
505 Element error = packet.findChild("error");
506 if (error == null || !error.hasChild("item-not-found")) {
507 pepBroken = true;
508 Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet);
509 return;
510 }
511 }
512
513 PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
514 Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
515 boolean flush = false;
516 if (bundle == null) {
517 Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet);
518 bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
519 flush = true;
520 }
521 if (keys == null) {
522 Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet);
523 }
524 try {
525 boolean changed = false;
526 // Validate IdentityKey
527 IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
528 if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
529 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
530 changed = true;
531 }
532
533 // Validate signedPreKeyRecord + ID
534 SignedPreKeyRecord signedPreKeyRecord;
535 int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
536 try {
537 signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
538 if (flush
539 || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
540 || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
541 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
542 signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
543 axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
544 changed = true;
545 }
546 } catch (InvalidKeyIdException e) {
547 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
548 signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
549 axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
550 changed = true;
551 }
552
553 // Validate PreKeys
554 Set<PreKeyRecord> preKeyRecords = new HashSet<>();
555 if (keys != null) {
556 for (Integer id : keys.keySet()) {
557 try {
558 PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
559 if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
560 preKeyRecords.add(preKeyRecord);
561 }
562 } catch (InvalidKeyIdException ignored) {
563 }
564 }
565 }
566 int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
567 if (newKeys > 0) {
568 List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
569 axolotlStore.getCurrentPreKeyId() + 1, newKeys);
570 preKeyRecords.addAll(newRecords);
571 for (PreKeyRecord record : newRecords) {
572 axolotlStore.storePreKey(record.getId(), record);
573 }
574 changed = true;
575 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP.");
576 }
577
578
579 if (changed) {
580 if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) {
581 mXmppConnectionService.publishDisplayName(account);
582 publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
583 } else {
584 publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
585 }
586 } else {
587 Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current");
588 if (wipe) {
589 wipeOtherPepDevices();
590 } else if (announce) {
591 Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
592 publishOwnDeviceIdIfNeeded();
593 }
594 }
595 } catch (InvalidKeyException e) {
596 Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
597 }
598 }
599 });
600 }
601
602 private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord,
603 Set<PreKeyRecord> preKeyRecords,
604 final boolean announceAfter,
605 final boolean wipe) {
606 IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
607 signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
608 preKeyRecords, getOwnDeviceId());
609 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
610 mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
611 @Override
612 public void onIqPacketReceived(Account account, IqPacket packet) {
613 if (packet.getType() == IqPacket.TYPE.RESULT) {
614 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. ");
615 if (wipe) {
616 wipeOtherPepDevices();
617 } else if (announceAfter) {
618 Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
619 publishOwnDeviceIdIfNeeded();
620 }
621 } else if (packet.getType() == IqPacket.TYPE.ERROR) {
622 pepBroken = true;
623 Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error"));
624 }
625 }
626 });
627 }
628
629 public boolean isConversationAxolotlCapable(Conversation conversation) {
630 final List<Jid> jids = getCryptoTargets(conversation);
631 for(Jid jid : jids) {
632 if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) {
633 return false;
634 }
635 }
636 return jids.size() > 0;
637 }
638
639 public List<Jid> getCryptoTargets(Conversation conversation) {
640 final List<Jid> jids;
641 if (conversation.getMode() == Conversation.MODE_SINGLE) {
642 jids = Arrays.asList(conversation.getJid().toBareJid());
643 } else {
644 jids = conversation.getMucOptions().getMembers();
645 jids.remove(account.getJid().toBareJid());
646 }
647 return jids;
648 }
649
650 public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
651 return axolotlStore.getFingerprintTrust(fingerprint);
652 }
653
654 public X509Certificate getFingerprintCertificate(String fingerprint) {
655 return axolotlStore.getFingerprintCertificate(fingerprint);
656 }
657
658 public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
659 axolotlStore.setFingerprintTrust(fingerprint, trust);
660 }
661
662 private void verifySessionWithPEP(final XmppAxolotlSession session) {
663 Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep");
664 final AxolotlAddress address = session.getRemoteAddress();
665 final IdentityKey identityKey = session.getIdentityKey();
666 try {
667 IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.fromString(address.getName()), address.getDeviceId());
668 mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
669 @Override
670 public void onIqPacketReceived(Account account, IqPacket packet) {
671 Pair<X509Certificate[],byte[]> verification = mXmppConnectionService.getIqParser().verification(packet);
672 if (verification != null) {
673 try {
674 Signature verifier = Signature.getInstance("sha256WithRSA");
675 verifier.initVerify(verification.first[0]);
676 verifier.update(identityKey.serialize());
677 if (verifier.verify(verification.second)) {
678 try {
679 mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
680 String fingerprint = session.getFingerprint();
681 Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint);
682 setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509);
683 axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]);
684 fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED);
685 Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]);
686 try {
687 final String cn = information.getString("subject_cn");
688 final Jid jid = Jid.fromString(address.getName());
689 Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn);
690 account.getRoster().getContact(jid).setCommonName(cn);
691 } catch (final InvalidJidException ignored) {
692 //ignored
693 }
694 finishBuildingSessionsFromPEP(address);
695 return;
696 } catch (Exception e) {
697 Log.d(Config.LOGTAG,"could not verify certificate");
698 }
699 }
700 } catch (Exception e) {
701 Log.d(Config.LOGTAG, "error during verification " + e.getMessage());
702 }
703 } else {
704 Log.d(Config.LOGTAG,"no verification found");
705 }
706 fetchStatusMap.put(address, FetchStatus.SUCCESS);
707 finishBuildingSessionsFromPEP(address);
708 }
709 });
710 } catch (InvalidJidException e) {
711 fetchStatusMap.put(address, FetchStatus.SUCCESS);
712 finishBuildingSessionsFromPEP(address);
713 }
714 }
715
716 private void finishBuildingSessionsFromPEP(final AxolotlAddress address) {
717 AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
718 if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
719 && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
720 FetchStatus report = null;
721 if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.SUCCESS_VERIFIED)
722 | fetchStatusMap.getAll(address).containsValue(FetchStatus.SUCCESS_VERIFIED)) {
723 report = FetchStatus.SUCCESS_VERIFIED;
724 } else if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.ERROR)
725 || fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
726 report = FetchStatus.ERROR;
727 }
728 mXmppConnectionService.keyStatusUpdated(report);
729 }
730 }
731
732 private void buildSessionFromPEP(final AxolotlAddress address) {
733 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.toString());
734 if (address.getDeviceId() == getOwnDeviceId()) {
735 throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!");
736 }
737
738 try {
739 IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(
740 Jid.fromString(address.getName()), address.getDeviceId());
741 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket);
742 mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() {
743
744 @Override
745 public void onIqPacketReceived(Account account, IqPacket packet) {
746 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
747 fetchStatusMap.put(address, FetchStatus.TIMEOUT);
748 } else if (packet.getType() == IqPacket.TYPE.RESULT) {
749 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
750 final IqParser parser = mXmppConnectionService.getIqParser();
751 final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
752 final PreKeyBundle bundle = parser.bundle(packet);
753 if (preKeyBundleList.isEmpty() || bundle == null) {
754 Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet);
755 fetchStatusMap.put(address, FetchStatus.ERROR);
756 finishBuildingSessionsFromPEP(address);
757 return;
758 }
759 Random random = new Random();
760 final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
761 if (preKey == null) {
762 //should never happen
763 fetchStatusMap.put(address, FetchStatus.ERROR);
764 finishBuildingSessionsFromPEP(address);
765 return;
766 }
767
768 final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(),
769 preKey.getPreKeyId(), preKey.getPreKey(),
770 bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
771 bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
772
773 try {
774 SessionBuilder builder = new SessionBuilder(axolotlStore, address);
775 builder.process(preKeyBundle);
776 XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey());
777 sessions.put(address, session);
778 if (Config.X509_VERIFICATION) {
779 verifySessionWithPEP(session);
780 } else {
781 fetchStatusMap.put(address, FetchStatus.SUCCESS);
782 finishBuildingSessionsFromPEP(address);
783 }
784 } catch (UntrustedIdentityException | InvalidKeyException e) {
785 Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": "
786 + e.getClass().getName() + ", " + e.getMessage());
787 fetchStatusMap.put(address, FetchStatus.ERROR);
788 finishBuildingSessionsFromPEP(address);
789 }
790 } else {
791 fetchStatusMap.put(address, FetchStatus.ERROR);
792 Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error"));
793 finishBuildingSessionsFromPEP(address);
794 }
795 }
796 });
797 } catch (InvalidJidException e) {
798 Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName());
799 }
800 }
801
802 public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) {
803 Set<AxolotlAddress> addresses = new HashSet<>();
804 for(Jid jid : getCryptoTargets(conversation)) {
805 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid);
806 if (deviceIds.get(jid) != null) {
807 for (Integer foreignId : this.deviceIds.get(jid)) {
808 AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
809 if (sessions.get(address) == null) {
810 IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
811 if (identityKey != null) {
812 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
813 XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
814 sessions.put(address, session);
815 } else {
816 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + foreignId);
817 if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
818 addresses.add(address);
819 } else {
820 Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken");
821 }
822 }
823 }
824 }
825 } else {
826 Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
827 }
828 }
829 if (deviceIds.get(account.getJid().toBareJid()) != null) {
830 for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) {
831 AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId);
832 if (sessions.get(address) == null) {
833 IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
834 if (identityKey != null) {
835 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
836 XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
837 sessions.put(address, session);
838 } else {
839 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId);
840 if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
841 addresses.add(address);
842 } else {
843 Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken");
844 }
845 }
846 }
847 }
848 }
849
850 return addresses;
851 }
852
853 public boolean createSessionsIfNeeded(final Conversation conversation) {
854 Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed...");
855 boolean newSessions = false;
856 Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation);
857 for (AxolotlAddress address : addresses) {
858 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString());
859 FetchStatus status = fetchStatusMap.get(address);
860 if (status == null || status == FetchStatus.TIMEOUT) {
861 fetchStatusMap.put(address, FetchStatus.PENDING);
862 this.buildSessionFromPEP(address);
863 newSessions = true;
864 } else if (status == FetchStatus.PENDING) {
865 newSessions = true;
866 } else {
867 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString());
868 }
869 }
870
871 return newSessions;
872 }
873
874 public boolean trustedSessionVerified(final Conversation conversation) {
875 Set<XmppAxolotlSession> sessions = findSessionsForConversation(conversation);
876 sessions.addAll(findOwnSessions());
877 boolean verified = false;
878 for(XmppAxolotlSession session : sessions) {
879 if (session.getTrust().trusted()) {
880 if (session.getTrust() == XmppAxolotlSession.Trust.TRUSTED_X509) {
881 verified = true;
882 } else {
883 return false;
884 }
885 }
886 }
887 return verified;
888 }
889
890 public boolean hasPendingKeyFetches(Account account, List<Jid> jids) {
891 AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
892 if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)) {
893 return true;
894 }
895 for(Jid jid : jids) {
896 AxolotlAddress foreignAddress = new AxolotlAddress(jid.toBareJid().toString(), 0);
897 if (fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) {
898 return true;
899 }
900 }
901 return false;
902 }
903
904 @Nullable
905 private XmppAxolotlMessage buildHeader(Conversation conversation) {
906 final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
907 account.getJid().toBareJid(), getOwnDeviceId());
908
909 Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation);
910 Set<XmppAxolotlSession> ownSessions = findOwnSessions();
911 if (remoteSessions.isEmpty()) {
912 return null;
913 }
914 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
915 for (XmppAxolotlSession session : remoteSessions) {
916 Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
917 axolotlMessage.addDevice(session);
918 }
919 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements...");
920 for (XmppAxolotlSession session : ownSessions) {
921 Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
922 axolotlMessage.addDevice(session);
923 }
924
925 return axolotlMessage;
926 }
927
928 @Nullable
929 public XmppAxolotlMessage encrypt(Message message) {
930 XmppAxolotlMessage axolotlMessage = buildHeader(message.getConversation());
931
932 if (axolotlMessage != null) {
933 final String content;
934 if (message.hasFileOnRemoteHost()) {
935 content = message.getFileParams().url.toString();
936 } else {
937 content = message.getBody();
938 }
939 try {
940 axolotlMessage.encrypt(content);
941 } catch (CryptoFailedException e) {
942 Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
943 return null;
944 }
945 }
946
947 return axolotlMessage;
948 }
949
950 public void preparePayloadMessage(final Message message, final boolean delay) {
951 executor.execute(new Runnable() {
952 @Override
953 public void run() {
954 XmppAxolotlMessage axolotlMessage = encrypt(message);
955 if (axolotlMessage == null) {
956 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
957 //mXmppConnectionService.updateConversationUi();
958 } else {
959 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid());
960 messageCache.put(message.getUuid(), axolotlMessage);
961 mXmppConnectionService.resendMessage(message, delay);
962 }
963 }
964 });
965 }
966
967 public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
968 executor.execute(new Runnable() {
969 @Override
970 public void run() {
971 XmppAxolotlMessage axolotlMessage = buildHeader(conversation);
972 onMessageCreatedCallback.run(axolotlMessage);
973 }
974 });
975 }
976
977 public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
978 XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid());
979 if (axolotlMessage != null) {
980 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid());
981 messageCache.remove(message.getUuid());
982 } else {
983 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid());
984 }
985 return axolotlMessage;
986 }
987
988 private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) {
989 IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
990 return (identityKey != null)
991 ? new XmppAxolotlSession(account, axolotlStore, address, identityKey)
992 : null;
993 }
994
995 private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
996 AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
997 message.getSenderDeviceId());
998 XmppAxolotlSession session = sessions.get(senderAddress);
999 if (session == null) {
1000 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message);
1001 session = recreateUncachedSession(senderAddress);
1002 if (session == null) {
1003 session = new XmppAxolotlSession(account, axolotlStore, senderAddress);
1004 }
1005 }
1006 return session;
1007 }
1008
1009 public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) {
1010 XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
1011
1012 XmppAxolotlSession session = getReceivingSession(message);
1013 try {
1014 plaintextMessage = message.decrypt(session, getOwnDeviceId());
1015 Integer preKeyId = session.getPreKeyId();
1016 if (preKeyId != null) {
1017 publishBundlesIfNeeded(false, false);
1018 session.resetPreKeyId();
1019 }
1020 } catch (CryptoFailedException e) {
1021 Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
1022 }
1023
1024 if (session.isFresh() && plaintextMessage != null) {
1025 putFreshSession(session);
1026 }
1027
1028 return plaintextMessage;
1029 }
1030
1031 public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) {
1032 XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
1033
1034 XmppAxolotlSession session = getReceivingSession(message);
1035 keyTransportMessage = message.getParameters(session, getOwnDeviceId());
1036
1037 if (session.isFresh() && keyTransportMessage != null) {
1038 putFreshSession(session);
1039 }
1040
1041 return keyTransportMessage;
1042 }
1043
1044 private void putFreshSession(XmppAxolotlSession session) {
1045 Log.d(Config.LOGTAG,"put fresh session");
1046 sessions.put(session);
1047 if (Config.X509_VERIFICATION) {
1048 if (session.getIdentityKey() != null) {
1049 verifySessionWithPEP(session);
1050 } else {
1051 Log.e(Config.LOGTAG,account.getJid().toBareJid()+": identity key was empty after reloading for x509 verification");
1052 }
1053 }
1054 }
1055}