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