1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.os.SystemClock;
6import android.util.Pair;
7
8import eu.siacs.conversations.crypto.PgpDecryptionService;
9
10import net.java.otr4j.crypto.OtrCryptoEngineImpl;
11import net.java.otr4j.crypto.OtrCryptoException;
12
13import org.json.JSONException;
14import org.json.JSONObject;
15
16import java.security.PublicKey;
17import java.security.interfaces.DSAPublicKey;
18import java.util.ArrayList;
19import java.util.Collection;
20import java.util.HashSet;
21import java.util.List;
22import java.util.concurrent.CopyOnWriteArrayList;
23import java.util.concurrent.CopyOnWriteArraySet;
24
25import eu.siacs.conversations.R;
26import eu.siacs.conversations.crypto.OtrService;
27import eu.siacs.conversations.crypto.axolotl.AxolotlService;
28import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
29import eu.siacs.conversations.services.XmppConnectionService;
30import eu.siacs.conversations.utils.XmppUri;
31import eu.siacs.conversations.xmpp.XmppConnection;
32import eu.siacs.conversations.xmpp.jid.InvalidJidException;
33import eu.siacs.conversations.xmpp.jid.Jid;
34
35public class Account extends AbstractEntity {
36
37 public static final String TABLENAME = "accounts";
38
39 public static final String USERNAME = "username";
40 public static final String SERVER = "server";
41 public static final String PASSWORD = "password";
42 public static final String OPTIONS = "options";
43 public static final String ROSTERVERSION = "rosterversion";
44 public static final String KEYS = "keys";
45 public static final String AVATAR = "avatar";
46 public static final String DISPLAY_NAME = "display_name";
47 public static final String HOSTNAME = "hostname";
48 public static final String PORT = "port";
49 public static final String STATUS = "status";
50 public static final String STATUS_MESSAGE = "status_message";
51
52 public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
53
54 public static final int OPTION_USETLS = 0;
55 public static final int OPTION_DISABLED = 1;
56 public static final int OPTION_REGISTER = 2;
57 public static final int OPTION_USECOMPRESSION = 3;
58 public static final int OPTION_MAGIC_CREATE = 4;
59 public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>();
60
61 public boolean httpUploadAvailable(long filesize) {
62 return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize);
63 }
64
65 public boolean httpUploadAvailable() {
66 return httpUploadAvailable(0);
67 }
68
69 public void setDisplayName(String displayName) {
70 this.displayName = displayName;
71 }
72
73 public String getDisplayName() {
74 return displayName;
75 }
76
77 public XmppConnection.Identity getServerIdentity() {
78 if (xmppConnection == null) {
79 return XmppConnection.Identity.UNKNOWN;
80 } else {
81 return xmppConnection.getServerIdentity();
82 }
83 }
84
85 public Contact getSelfContact() {
86 return getRoster().getContact(jid);
87 }
88
89 public boolean hasPendingPgpIntent(Conversation conversation) {
90 return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation);
91 }
92
93 public boolean isPgpDecryptionServiceConnected() {
94 return pgpDecryptionService != null && pgpDecryptionService.isConnected();
95 }
96
97 public boolean setShowErrorNotification(boolean newValue) {
98 boolean oldValue = showErrorNotification();
99 setKey("show_error",Boolean.toString(newValue));
100 return newValue != oldValue;
101 }
102
103 public boolean showErrorNotification() {
104 String key = getKey("show_error");
105 return key == null || Boolean.parseBoolean(key);
106 }
107
108 public enum State {
109 DISABLED,
110 OFFLINE,
111 CONNECTING,
112 ONLINE,
113 NO_INTERNET,
114 UNAUTHORIZED(true),
115 SERVER_NOT_FOUND(true),
116 REGISTRATION_FAILED(true),
117 REGISTRATION_CONFLICT(true),
118 REGISTRATION_SUCCESSFUL,
119 REGISTRATION_NOT_SUPPORTED(true),
120 SECURITY_ERROR(true),
121 INCOMPATIBLE_SERVER(true),
122 TOR_NOT_AVAILABLE(true),
123 BIND_FAILURE(true),
124 HOST_UNKNOWN(true),
125 REGISTRATION_PLEASE_WAIT(true),
126 STREAM_ERROR(true),
127 POLICY_VIOLATION(true),
128 REGISTRATION_PASSWORD_TOO_WEAK(true),
129 PAYMENT_REQUIRED(true),
130 MISSING_INTERNET_PERMISSION(true);
131
132 private final boolean isError;
133
134 public boolean isError() {
135 return this.isError;
136 }
137
138 State(final boolean isError) {
139 this.isError = isError;
140 }
141
142 State() {
143 this(false);
144 }
145
146 public int getReadableId() {
147 switch (this) {
148 case DISABLED:
149 return R.string.account_status_disabled;
150 case ONLINE:
151 return R.string.account_status_online;
152 case CONNECTING:
153 return R.string.account_status_connecting;
154 case OFFLINE:
155 return R.string.account_status_offline;
156 case UNAUTHORIZED:
157 return R.string.account_status_unauthorized;
158 case SERVER_NOT_FOUND:
159 return R.string.account_status_not_found;
160 case NO_INTERNET:
161 return R.string.account_status_no_internet;
162 case REGISTRATION_FAILED:
163 return R.string.account_status_regis_fail;
164 case REGISTRATION_CONFLICT:
165 return R.string.account_status_regis_conflict;
166 case REGISTRATION_SUCCESSFUL:
167 return R.string.account_status_regis_success;
168 case REGISTRATION_NOT_SUPPORTED:
169 return R.string.account_status_regis_not_sup;
170 case SECURITY_ERROR:
171 return R.string.account_status_security_error;
172 case INCOMPATIBLE_SERVER:
173 return R.string.account_status_incompatible_server;
174 case TOR_NOT_AVAILABLE:
175 return R.string.account_status_tor_unavailable;
176 case BIND_FAILURE:
177 return R.string.account_status_bind_failure;
178 case HOST_UNKNOWN:
179 return R.string.account_status_host_unknown;
180 case POLICY_VIOLATION:
181 return R.string.account_status_policy_violation;
182 case REGISTRATION_PLEASE_WAIT:
183 return R.string.registration_please_wait;
184 case REGISTRATION_PASSWORD_TOO_WEAK:
185 return R.string.registration_password_too_weak;
186 case STREAM_ERROR:
187 return R.string.account_status_stream_error;
188 case PAYMENT_REQUIRED:
189 return R.string.payment_required;
190 case MISSING_INTERNET_PERMISSION:
191 return R.string.missing_internet_permission;
192 default:
193 return R.string.account_status_unknown;
194 }
195 }
196 }
197
198 public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<>();
199 public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<>();
200
201 private static final String KEY_PGP_SIGNATURE = "pgp_signature";
202 private static final String KEY_PGP_ID = "pgp_id";
203
204 protected Jid jid;
205 protected String password;
206 protected int options = 0;
207 protected String rosterVersion;
208 protected State status = State.OFFLINE;
209 protected final JSONObject keys;
210 protected String avatar;
211 protected String displayName = null;
212 protected String hostname = null;
213 protected int port = 5222;
214 protected boolean online = false;
215 private OtrService mOtrService = null;
216 private AxolotlService axolotlService = null;
217 private PgpDecryptionService pgpDecryptionService = null;
218 private XmppConnection xmppConnection = null;
219 private long mEndGracePeriod = 0L;
220 private String otrFingerprint;
221 private final Roster roster = new Roster(this);
222 private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
223 private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
224 private Presence.Status presenceStatus = Presence.Status.ONLINE;
225 private String presenceStatusMessage = null;
226
227 public Account(final Jid jid, final String password) {
228 this(java.util.UUID.randomUUID().toString(), jid,
229 password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null);
230 }
231
232 private Account(final String uuid, final Jid jid,
233 final String password, final int options, final String rosterVersion, final String keys,
234 final String avatar, String displayName, String hostname, int port,
235 final Presence.Status status, String statusMessage) {
236 this.uuid = uuid;
237 this.jid = jid;
238 if (jid.isBareJid()) {
239 this.setResource("mobile");
240 }
241 this.password = password;
242 this.options = options;
243 this.rosterVersion = rosterVersion;
244 JSONObject tmp;
245 try {
246 tmp = new JSONObject(keys);
247 } catch(JSONException e) {
248 tmp = new JSONObject();
249 }
250 this.keys = tmp;
251 this.avatar = avatar;
252 this.displayName = displayName;
253 this.hostname = hostname;
254 this.port = port;
255 this.presenceStatus = status;
256 this.presenceStatusMessage = statusMessage;
257 }
258
259 public static Account fromCursor(final Cursor cursor) {
260 Jid jid = null;
261 try {
262 jid = Jid.fromParts(cursor.getString(cursor.getColumnIndex(USERNAME)),
263 cursor.getString(cursor.getColumnIndex(SERVER)), "mobile");
264 } catch (final InvalidJidException ignored) {
265 }
266 return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
267 jid,
268 cursor.getString(cursor.getColumnIndex(PASSWORD)),
269 cursor.getInt(cursor.getColumnIndex(OPTIONS)),
270 cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
271 cursor.getString(cursor.getColumnIndex(KEYS)),
272 cursor.getString(cursor.getColumnIndex(AVATAR)),
273 cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
274 cursor.getString(cursor.getColumnIndex(HOSTNAME)),
275 cursor.getInt(cursor.getColumnIndex(PORT)),
276 Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))),
277 cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE)));
278 }
279
280 public boolean isOptionSet(final int option) {
281 return ((options & (1 << option)) != 0);
282 }
283
284 public void setOption(final int option, final boolean value) {
285 if (value) {
286 this.options |= 1 << option;
287 } else {
288 this.options &= ~(1 << option);
289 }
290 }
291
292 public String getUsername() {
293 return jid.getLocalpart();
294 }
295
296 public boolean setJid(final Jid next) {
297 final Jid prev = this.jid != null ? this.jid.toBareJid() : null;
298 this.jid = next;
299 return prev == null || (next != null && !prev.equals(next.toBareJid()));
300 }
301
302 public Jid getServer() {
303 return jid.toDomainJid();
304 }
305
306 public String getPassword() {
307 return password;
308 }
309
310 public void setPassword(final String password) {
311 this.password = password;
312 }
313
314 public void setHostname(String hostname) {
315 this.hostname = hostname;
316 }
317
318 public String getHostname() {
319 return this.hostname == null ? "" : this.hostname;
320 }
321
322 public boolean isOnion() {
323 final Jid server = getServer();
324 return server != null && server.toString().toLowerCase().endsWith(".onion");
325 }
326
327 public void setPort(int port) {
328 this.port = port;
329 }
330
331 public int getPort() {
332 return this.port;
333 }
334
335 public State getStatus() {
336 if (isOptionSet(OPTION_DISABLED)) {
337 return State.DISABLED;
338 } else {
339 return this.status;
340 }
341 }
342
343 public void setStatus(final State status) {
344 this.status = status;
345 }
346
347 public boolean errorStatus() {
348 return getStatus().isError();
349 }
350
351 public boolean hasErrorStatus() {
352 return getXmppConnection() != null
353 && (getStatus().isError() || getStatus() == State.CONNECTING)
354 && getXmppConnection().getAttempt() >= 3;
355 }
356
357 public void setPresenceStatus(Presence.Status status) {
358 this.presenceStatus = status;
359 }
360
361 public Presence.Status getPresenceStatus() {
362 return this.presenceStatus;
363 }
364
365 public void setPresenceStatusMessage(String message) {
366 this.presenceStatusMessage = message;
367 }
368
369 public String getPresenceStatusMessage() {
370 return this.presenceStatusMessage;
371 }
372
373 public String getResource() {
374 return jid.getResourcepart();
375 }
376
377 public boolean setResource(final String resource) {
378 final String oldResource = jid.getResourcepart();
379 if (oldResource == null || !oldResource.equals(resource)) {
380 try {
381 jid = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), resource);
382 return true;
383 } catch (final InvalidJidException ignored) {
384 return true;
385 }
386 }
387 return false;
388 }
389
390 public Jid getJid() {
391 return jid;
392 }
393
394 public JSONObject getKeys() {
395 return keys;
396 }
397
398 public String getKey(final String name) {
399 synchronized (this.keys) {
400 return this.keys.optString(name, null);
401 }
402 }
403
404 public int getKeyAsInt(final String name, int defaultValue) {
405 String key = getKey(name);
406 try {
407 return key == null ? defaultValue : Integer.parseInt(key);
408 } catch (NumberFormatException e) {
409 return defaultValue;
410 }
411 }
412
413 public boolean setKey(final String keyName, final String keyValue) {
414 synchronized (this.keys) {
415 try {
416 this.keys.put(keyName, keyValue);
417 return true;
418 } catch (final JSONException e) {
419 return false;
420 }
421 }
422 }
423
424 public boolean setPrivateKeyAlias(String alias) {
425 return setKey("private_key_alias", alias);
426 }
427
428 public String getPrivateKeyAlias() {
429 return getKey("private_key_alias");
430 }
431
432 @Override
433 public ContentValues getContentValues() {
434 final ContentValues values = new ContentValues();
435 values.put(UUID, uuid);
436 values.put(USERNAME, jid.getLocalpart());
437 values.put(SERVER, jid.getDomainpart());
438 values.put(PASSWORD, password);
439 values.put(OPTIONS, options);
440 synchronized (this.keys) {
441 values.put(KEYS, this.keys.toString());
442 }
443 values.put(ROSTERVERSION, rosterVersion);
444 values.put(AVATAR, avatar);
445 values.put(DISPLAY_NAME, displayName);
446 values.put(HOSTNAME, hostname);
447 values.put(PORT, port);
448 values.put(STATUS, presenceStatus.toShowString());
449 values.put(STATUS_MESSAGE, presenceStatusMessage);
450 return values;
451 }
452
453 public AxolotlService getAxolotlService() {
454 return axolotlService;
455 }
456
457 public void initAccountServices(final XmppConnectionService context) {
458 this.mOtrService = new OtrService(context, this);
459 this.axolotlService = new AxolotlService(this, context);
460 this.pgpDecryptionService = new PgpDecryptionService(context);
461 if (xmppConnection != null) {
462 xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
463 }
464 }
465
466 public OtrService getOtrService() {
467 return this.mOtrService;
468 }
469
470 public PgpDecryptionService getPgpDecryptionService() {
471 return this.pgpDecryptionService;
472 }
473
474 public XmppConnection getXmppConnection() {
475 return this.xmppConnection;
476 }
477
478 public void setXmppConnection(final XmppConnection connection) {
479 this.xmppConnection = connection;
480 }
481
482 public String getOtrFingerprint() {
483 if (this.otrFingerprint == null) {
484 try {
485 if (this.mOtrService == null) {
486 return null;
487 }
488 final PublicKey publicKey = this.mOtrService.getPublicKey();
489 if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
490 return null;
491 }
492 this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey);
493 return this.otrFingerprint;
494 } catch (final OtrCryptoException ignored) {
495 return null;
496 }
497 } else {
498 return this.otrFingerprint;
499 }
500 }
501
502 public String getRosterVersion() {
503 if (this.rosterVersion == null) {
504 return "";
505 } else {
506 return this.rosterVersion;
507 }
508 }
509
510 public void setRosterVersion(final String version) {
511 this.rosterVersion = version;
512 }
513
514 public int countPresences() {
515 return this.getSelfContact().getPresences().size();
516 }
517
518 public String getPgpSignature() {
519 return getKey(KEY_PGP_SIGNATURE);
520 }
521
522 public boolean setPgpSignature(String signature) {
523 return setKey(KEY_PGP_SIGNATURE, signature);
524 }
525
526 public boolean unsetPgpSignature() {
527 synchronized (this.keys) {
528 return keys.remove(KEY_PGP_SIGNATURE) != null;
529 }
530 }
531
532 public long getPgpId() {
533 synchronized (this.keys) {
534 if (keys.has(KEY_PGP_ID)) {
535 try {
536 return keys.getLong(KEY_PGP_ID);
537 } catch (JSONException e) {
538 return 0;
539 }
540 } else {
541 return 0;
542 }
543 }
544 }
545
546 public boolean setPgpSignId(long pgpID) {
547 synchronized (this.keys) {
548 try {
549 keys.put(KEY_PGP_ID, pgpID);
550 } catch (JSONException e) {
551 return false;
552 }
553 return true;
554 }
555 }
556
557 public Roster getRoster() {
558 return this.roster;
559 }
560
561 public List<Bookmark> getBookmarks() {
562 return this.bookmarks;
563 }
564
565 public void setBookmarks(final List<Bookmark> bookmarks) {
566 this.bookmarks = bookmarks;
567 }
568
569 public boolean hasBookmarkFor(final Jid conferenceJid) {
570 for (final Bookmark bookmark : this.bookmarks) {
571 final Jid jid = bookmark.getJid();
572 if (jid != null && jid.equals(conferenceJid.toBareJid())) {
573 return true;
574 }
575 }
576 return false;
577 }
578
579 public boolean setAvatar(final String filename) {
580 if (this.avatar != null && this.avatar.equals(filename)) {
581 return false;
582 } else {
583 this.avatar = filename;
584 return true;
585 }
586 }
587
588 public String getAvatar() {
589 return this.avatar;
590 }
591
592 public void activateGracePeriod(long duration) {
593 this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
594 }
595
596 public void deactivateGracePeriod() {
597 this.mEndGracePeriod = 0L;
598 }
599
600 public boolean inGracePeriod() {
601 return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
602 }
603
604 public String getShareableUri() {
605 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
606 String uri = "xmpp:"+this.getJid().toBareJid().toString();
607 if (fingerprints.size() > 0) {
608 StringBuilder builder = new StringBuilder(uri);
609 builder.append('?');
610 for(int i = 0; i < fingerprints.size(); ++i) {
611 XmppUri.FingerprintType type = fingerprints.get(i).type;
612 if (type == XmppUri.FingerprintType.OMEMO) {
613 builder.append(XmppUri.OMEMO_URI_PARAM);
614 builder.append(fingerprints.get(i).deviceId);
615 } else if (type == XmppUri.FingerprintType.OTR) {
616 builder.append(XmppUri.OTR_URI_PARAM);
617 }
618 builder.append('=');
619 builder.append(fingerprints.get(i).fingerprint);
620 if (i != fingerprints.size() -1) {
621 builder.append(';');
622 }
623 }
624 return builder.toString();
625 } else {
626 return uri;
627 }
628 }
629
630 private List<XmppUri.Fingerprint> getFingerprints() {
631 ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
632 final String otr = this.getOtrFingerprint();
633 if (otr != null) {
634 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR,otr));
635 }
636 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,axolotlService.getOwnFingerprint().substring(2),axolotlService.getOwnDeviceId()));
637 for(XmppAxolotlSession session : axolotlService.findOwnSessions()) {
638 if (session.getTrust().isVerified() && session.getTrust().isActive()) {
639 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,session.getFingerprint().substring(2).replaceAll("\\s",""),session.getRemoteAddress().getDeviceId()));
640 }
641 }
642 return fingerprints;
643 }
644
645 public boolean isBlocked(final ListItem contact) {
646 final Jid jid = contact.getJid();
647 return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid()));
648 }
649
650 public boolean isBlocked(final Jid jid) {
651 return jid != null && blocklist.contains(jid.toBareJid());
652 }
653
654 public Collection<Jid> getBlocklist() {
655 return this.blocklist;
656 }
657
658 public void clearBlocklist() {
659 getBlocklist().clear();
660 }
661
662 public boolean isOnlineAndConnected() {
663 return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
664 }
665}