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