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