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