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