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