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(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	protected String rosterVersion;
233	protected State status = State.OFFLINE;
234	protected final JSONObject keys;
235	protected String avatar;
236	protected String displayName = null;
237	protected String hostname = null;
238	protected int port = 5222;
239	protected boolean online = false;
240	private OtrService mOtrService = null;
241	private AxolotlService axolotlService = null;
242	private PgpDecryptionService pgpDecryptionService = null;
243	private XmppConnection xmppConnection = null;
244	private long mEndGracePeriod = 0L;
245	private String otrFingerprint;
246	private final Roster roster = new Roster(this);
247	private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
248	private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
249	private Presence.Status presenceStatus = Presence.Status.ONLINE;
250	private String presenceStatusMessage = null;
251
252	public Account(final Jid jid, final String password) {
253		this(java.util.UUID.randomUUID().toString(), jid,
254				password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null);
255	}
256
257	private Account(final String uuid, final Jid jid,
258					final String password, final int options, final String rosterVersion, final String keys,
259					final String avatar, String displayName, String hostname, int port,
260					final Presence.Status status, String statusMessage) {
261		this.uuid = uuid;
262		this.jid = jid;
263		if (jid.isBareJid()) {
264			this.setResource("mobile");
265		}
266		this.password = password;
267		this.options = options;
268		this.rosterVersion = rosterVersion;
269		JSONObject tmp;
270		try {
271			tmp = new JSONObject(keys);
272		} catch(JSONException e) {
273			tmp = new JSONObject();
274		}
275		this.keys = tmp;
276		this.avatar = avatar;
277		this.displayName = displayName;
278		this.hostname = hostname;
279		this.port = port;
280		this.presenceStatus = status;
281		this.presenceStatusMessage = statusMessage;
282	}
283
284	public static Account fromCursor(final Cursor cursor) {
285		Jid jid = null;
286		try {
287			jid = Jid.fromParts(cursor.getString(cursor.getColumnIndex(USERNAME)),
288					cursor.getString(cursor.getColumnIndex(SERVER)), "mobile");
289		} catch (final InvalidJidException ignored) {
290		}
291		return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
292				jid,
293				cursor.getString(cursor.getColumnIndex(PASSWORD)),
294				cursor.getInt(cursor.getColumnIndex(OPTIONS)),
295				cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
296				cursor.getString(cursor.getColumnIndex(KEYS)),
297				cursor.getString(cursor.getColumnIndex(AVATAR)),
298				cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
299				cursor.getString(cursor.getColumnIndex(HOSTNAME)),
300				cursor.getInt(cursor.getColumnIndex(PORT)),
301				Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))),
302				cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE)));
303	}
304
305	public boolean isOptionSet(final int option) {
306		return ((options & (1 << option)) != 0);
307	}
308
309	public boolean setOption(final int option, final boolean value) {
310		final int before = this.options;
311		if (value) {
312			this.options |= 1 << option;
313		} else {
314			this.options &= ~(1 << option);
315		}
316		return before != this.options;
317	}
318
319	public String getUsername() {
320		return jid.getLocalpart();
321	}
322
323	public boolean setJid(final Jid next) {
324		final Jid prev = this.jid != null ? this.jid.toBareJid() : null;
325		final boolean changed = prev == null || (next != null && !prev.equals(next.toBareJid()));
326		if (changed) {
327			final AxolotlService oldAxolotlService = this.axolotlService;
328			if (oldAxolotlService != null) {
329				oldAxolotlService.destroy();
330				this.jid = next;
331				this.axolotlService = oldAxolotlService.makeNew();
332			}
333		}
334		this.jid = next;
335		return changed;
336	}
337
338	public Jid getServer() {
339		return jid.toDomainJid();
340	}
341
342	public String getPassword() {
343		return password;
344	}
345
346	public void setPassword(final String password) {
347		this.password = password;
348	}
349
350	public void setHostname(String hostname) {
351		this.hostname = hostname;
352	}
353
354	public String getHostname() {
355		return this.hostname == null ? "" : this.hostname;
356	}
357
358	public boolean isOnion() {
359		final Jid server = getServer();
360		return server != null && server.toString().toLowerCase().endsWith(".onion");
361	}
362
363	public void setPort(int port) {
364		this.port = port;
365	}
366
367	public int getPort() {
368		return this.port;
369	}
370
371	public State getStatus() {
372		if (isOptionSet(OPTION_DISABLED)) {
373			return State.DISABLED;
374		} else {
375			return this.status;
376		}
377	}
378
379	public State getTrueStatus() {
380		return this.status;
381	}
382
383	public void setStatus(final State status) {
384		this.status = status;
385	}
386
387	public boolean errorStatus() {
388		return getStatus().isError();
389	}
390
391	public boolean hasErrorStatus() {
392		return getXmppConnection() != null
393				&& (getStatus().isError() || getStatus() == State.CONNECTING)
394				&& getXmppConnection().getAttempt() >= 3;
395	}
396
397	public void setPresenceStatus(Presence.Status status) {
398		this.presenceStatus = status;
399	}
400
401	public Presence.Status getPresenceStatus() {
402		return this.presenceStatus;
403	}
404
405	public void setPresenceStatusMessage(String message) {
406		this.presenceStatusMessage = message;
407	}
408
409	public String getPresenceStatusMessage() {
410		return this.presenceStatusMessage;
411	}
412
413	public String getResource() {
414		return jid.getResourcepart();
415	}
416
417	public boolean setResource(final String resource) {
418		final String oldResource = jid.getResourcepart();
419		if (oldResource == null || !oldResource.equals(resource)) {
420			try {
421				jid = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), resource);
422				return true;
423			} catch (final InvalidJidException ignored) {
424				return true;
425			}
426		}
427		return false;
428	}
429
430	public Jid getJid() {
431		return jid;
432	}
433
434	public JSONObject getKeys() {
435		return keys;
436	}
437
438	public String getKey(final String name) {
439		synchronized (this.keys) {
440			return this.keys.optString(name, null);
441		}
442	}
443
444	public int getKeyAsInt(final String name, int defaultValue) {
445		String key = getKey(name);
446		try {
447			return key == null ? defaultValue : Integer.parseInt(key);
448		} catch (NumberFormatException e) {
449			return defaultValue;
450		}
451	}
452
453	public boolean setKey(final String keyName, final String keyValue) {
454		synchronized (this.keys) {
455			try {
456				this.keys.put(keyName, keyValue);
457				return true;
458			} catch (final JSONException e) {
459				return false;
460			}
461		}
462	}
463
464	public boolean setPrivateKeyAlias(String alias) {
465		return setKey("private_key_alias", alias);
466	}
467
468	public String getPrivateKeyAlias() {
469		return getKey("private_key_alias");
470	}
471
472	@Override
473	public ContentValues getContentValues() {
474		final ContentValues values = new ContentValues();
475		values.put(UUID, uuid);
476		values.put(USERNAME, jid.getLocalpart());
477		values.put(SERVER, jid.getDomainpart());
478		values.put(PASSWORD, password);
479		values.put(OPTIONS, options);
480		synchronized (this.keys) {
481			values.put(KEYS, this.keys.toString());
482		}
483		values.put(ROSTERVERSION, rosterVersion);
484		values.put(AVATAR, avatar);
485		values.put(DISPLAY_NAME, displayName);
486		values.put(HOSTNAME, hostname);
487		values.put(PORT, port);
488		values.put(STATUS, presenceStatus.toShowString());
489		values.put(STATUS_MESSAGE, presenceStatusMessage);
490		return values;
491	}
492
493	public AxolotlService getAxolotlService() {
494		return axolotlService;
495	}
496
497	public void initAccountServices(final XmppConnectionService context) {
498		this.mOtrService = new OtrService(context, this);
499		this.axolotlService = new AxolotlService(this, context);
500		this.pgpDecryptionService = new PgpDecryptionService(context);
501		if (xmppConnection != null) {
502			xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
503		}
504	}
505
506	public OtrService getOtrService() {
507		return this.mOtrService;
508	}
509
510	public PgpDecryptionService getPgpDecryptionService() {
511		return this.pgpDecryptionService;
512	}
513
514	public XmppConnection getXmppConnection() {
515		return this.xmppConnection;
516	}
517
518	public void setXmppConnection(final XmppConnection connection) {
519		this.xmppConnection = connection;
520	}
521
522	public String getOtrFingerprint() {
523		if (this.otrFingerprint == null) {
524			try {
525				if (this.mOtrService == null) {
526					return null;
527				}
528				final PublicKey publicKey = this.mOtrService.getPublicKey();
529				if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
530					return null;
531				}
532				this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US);
533				return this.otrFingerprint;
534			} catch (final OtrCryptoException ignored) {
535				return null;
536			}
537		} else {
538			return this.otrFingerprint;
539		}
540	}
541
542	public String getRosterVersion() {
543		if (this.rosterVersion == null) {
544			return "";
545		} else {
546			return this.rosterVersion;
547		}
548	}
549
550	public void setRosterVersion(final String version) {
551		this.rosterVersion = version;
552	}
553
554	public int countPresences() {
555		return this.getSelfContact().getPresences().size();
556	}
557
558	public String getPgpSignature() {
559		return getKey(KEY_PGP_SIGNATURE);
560	}
561
562	public boolean setPgpSignature(String signature) {
563		return setKey(KEY_PGP_SIGNATURE, signature);
564	}
565
566	public boolean unsetPgpSignature() {
567		synchronized (this.keys) {
568			return keys.remove(KEY_PGP_SIGNATURE) != null;
569		}
570	}
571
572	public long getPgpId() {
573		synchronized (this.keys) {
574			if (keys.has(KEY_PGP_ID)) {
575				try {
576					return keys.getLong(KEY_PGP_ID);
577				} catch (JSONException e) {
578					return 0;
579				}
580			} else {
581				return 0;
582			}
583		}
584	}
585
586	public boolean setPgpSignId(long pgpID) {
587		synchronized (this.keys) {
588			try {
589				if (pgpID == 0) {
590					keys.remove(KEY_PGP_ID);
591				} else {
592					keys.put(KEY_PGP_ID, pgpID);
593				}
594			} catch (JSONException e) {
595				return false;
596			}
597			return true;
598		}
599	}
600
601	public Roster getRoster() {
602		return this.roster;
603	}
604
605	public List<Bookmark> getBookmarks() {
606		return this.bookmarks;
607	}
608
609	public void setBookmarks(final List<Bookmark> bookmarks) {
610		this.bookmarks = bookmarks;
611	}
612
613	public boolean hasBookmarkFor(final Jid conferenceJid) {
614		for (final Bookmark bookmark : this.bookmarks) {
615			final Jid jid = bookmark.getJid();
616			if (jid != null && jid.equals(conferenceJid.toBareJid())) {
617				return true;
618			}
619		}
620		return false;
621	}
622
623	public boolean setAvatar(final String filename) {
624		if (this.avatar != null && this.avatar.equals(filename)) {
625			return false;
626		} else {
627			this.avatar = filename;
628			return true;
629		}
630	}
631
632	public String getAvatar() {
633		return this.avatar;
634	}
635
636	public void activateGracePeriod(long duration) {
637		this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
638	}
639
640	public void deactivateGracePeriod() {
641		this.mEndGracePeriod = 0L;
642	}
643
644	public boolean inGracePeriod() {
645		return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
646	}
647
648	public String getShareableUri() {
649		List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
650		String uri = "xmpp:"+this.getJid().toBareJid().toString();
651		if (fingerprints.size() > 0) {
652			return XmppUri.getFingerprintUri(uri,fingerprints,';');
653		} else {
654			return uri;
655		}
656	}
657
658	public String getShareableLink() {
659		List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
660		String uri = "https://conversations.im/i/"+this.getJid().toBareJid().toString();
661		if (fingerprints.size() > 0) {
662			return XmppUri.getFingerprintUri(uri,fingerprints,'&');
663		} else {
664			return uri;
665		}
666	}
667
668	private List<XmppUri.Fingerprint> getFingerprints() {
669		ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
670		final String otr = this.getOtrFingerprint();
671		if (otr != null) {
672			fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR,otr));
673		}
674		if (axolotlService == null) {
675			return fingerprints;
676		}
677		fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,axolotlService.getOwnFingerprint().substring(2),axolotlService.getOwnDeviceId()));
678		for(XmppAxolotlSession session : axolotlService.findOwnSessions()) {
679			if (session.getTrust().isVerified() && session.getTrust().isActive()) {
680				fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,session.getFingerprint().substring(2).replaceAll("\\s",""),session.getRemoteAddress().getDeviceId()));
681			}
682		}
683		return fingerprints;
684	}
685
686	public boolean isBlocked(final ListItem contact) {
687		final Jid jid = contact.getJid();
688		return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid()));
689	}
690
691	public boolean isBlocked(final Jid jid) {
692		return jid != null && blocklist.contains(jid.toBareJid());
693	}
694
695	public Collection<Jid> getBlocklist() {
696		return this.blocklist;
697	}
698
699	public void clearBlocklist() {
700		getBlocklist().clear();
701	}
702
703	public boolean isOnlineAndConnected() {
704		return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
705	}
706}