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