Account.java

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