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