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