Account.java

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