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