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