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