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