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