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