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