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