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(final Jid jid, final String password) {
166 this(java.util.UUID.randomUUID().toString(), jid,
167 password, 0, null, "", null, null, null, 5222);
168 }
169
170 private Account(final String uuid, final Jid jid,
171 final String password, final int options, final String rosterVersion, final String keys,
172 final String avatar, String displayName, String hostname, int port) {
173 this.uuid = uuid;
174 this.jid = jid;
175 if (jid.isBareJid()) {
176 this.setResource("mobile");
177 }
178 this.password = password;
179 this.options = options;
180 this.rosterVersion = rosterVersion;
181 try {
182 this.keys = new JSONObject(keys);
183 } catch (final JSONException ignored) {
184 this.keys = new JSONObject();
185 }
186 this.avatar = avatar;
187 this.displayName = displayName;
188 this.hostname = hostname;
189 this.port = port;
190 }
191
192 public static Account fromCursor(final Cursor cursor) {
193 Jid jid = null;
194 try {
195 jid = Jid.fromParts(cursor.getString(cursor.getColumnIndex(USERNAME)),
196 cursor.getString(cursor.getColumnIndex(SERVER)), "mobile");
197 } catch (final InvalidJidException ignored) {
198 }
199 return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
200 jid,
201 cursor.getString(cursor.getColumnIndex(PASSWORD)),
202 cursor.getInt(cursor.getColumnIndex(OPTIONS)),
203 cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
204 cursor.getString(cursor.getColumnIndex(KEYS)),
205 cursor.getString(cursor.getColumnIndex(AVATAR)),
206 cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
207 cursor.getString(cursor.getColumnIndex(HOSTNAME)),
208 cursor.getInt(cursor.getColumnIndex(PORT)));
209 }
210
211 public boolean isOptionSet(final int option) {
212 return ((options & (1 << option)) != 0);
213 }
214
215 public void setOption(final int option, final boolean value) {
216 if (value) {
217 this.options |= 1 << option;
218 } else {
219 this.options &= ~(1 << option);
220 }
221 }
222
223 public String getUsername() {
224 return jid.getLocalpart();
225 }
226
227 public void setJid(final Jid jid) {
228 this.jid = jid;
229 }
230
231 public Jid getServer() {
232 return jid.toDomainJid();
233 }
234
235 public String getPassword() {
236 return password;
237 }
238
239 public void setPassword(final String password) {
240 this.password = password;
241 }
242
243 public void setHostname(String hostname) {
244 this.hostname = hostname;
245 }
246
247 public String getHostname() {
248 return this.hostname == null ? "" : this.hostname;
249 }
250
251 public boolean isOnion() {
252 return getServer().toString().toLowerCase().endsWith(".onion");
253 }
254
255 public void setPort(int port) {
256 this.port = port;
257 }
258
259 public int getPort() {
260 return this.port;
261 }
262
263 public State getStatus() {
264 if (isOptionSet(OPTION_DISABLED)) {
265 return State.DISABLED;
266 } else {
267 return this.status;
268 }
269 }
270
271 public void setStatus(final State status) {
272 this.status = status;
273 }
274
275 public boolean errorStatus() {
276 return getStatus().isError();
277 }
278
279 public boolean hasErrorStatus() {
280 return getXmppConnection() != null && getStatus().isError() && getXmppConnection().getAttempt() >= 3;
281 }
282
283 public String getResource() {
284 return jid.getResourcepart();
285 }
286
287 public boolean setResource(final String resource) {
288 final String oldResource = jid.getResourcepart();
289 if (oldResource == null || !oldResource.equals(resource)) {
290 try {
291 jid = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), resource);
292 return true;
293 } catch (final InvalidJidException ignored) {
294 return true;
295 }
296 }
297 return false;
298 }
299
300 public Jid getJid() {
301 return jid;
302 }
303
304 public JSONObject getKeys() {
305 return keys;
306 }
307
308 public String getKey(final String name) {
309 return this.keys.optString(name, null);
310 }
311
312 public boolean setKey(final String keyName, final String keyValue) {
313 try {
314 this.keys.put(keyName, keyValue);
315 return true;
316 } catch (final JSONException e) {
317 return false;
318 }
319 }
320
321 public boolean setPrivateKeyAlias(String alias) {
322 return setKey("private_key_alias", alias);
323 }
324
325 public String getPrivateKeyAlias() {
326 return getKey("private_key_alias");
327 }
328
329 @Override
330 public ContentValues getContentValues() {
331 final ContentValues values = new ContentValues();
332 values.put(UUID, uuid);
333 values.put(USERNAME, jid.getLocalpart());
334 values.put(SERVER, jid.getDomainpart());
335 values.put(PASSWORD, password);
336 values.put(OPTIONS, options);
337 values.put(KEYS, this.keys.toString());
338 values.put(ROSTERVERSION, rosterVersion);
339 values.put(AVATAR, avatar);
340 values.put(DISPLAY_NAME, displayName);
341 values.put(HOSTNAME, hostname);
342 values.put(PORT, port);
343 return values;
344 }
345
346 public AxolotlService getAxolotlService() {
347 return axolotlService;
348 }
349
350 public void initAccountServices(final XmppConnectionService context) {
351 this.mOtrService = new OtrService(context, this);
352 this.axolotlService = new AxolotlService(this, context);
353 if (xmppConnection != null) {
354 xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
355 }
356 this.pgpDecryptionService = new PgpDecryptionService(context);
357 }
358
359 public OtrService getOtrService() {
360 return this.mOtrService;
361 }
362
363 public PgpDecryptionService getPgpDecryptionService() {
364 return pgpDecryptionService;
365 }
366
367 public XmppConnection getXmppConnection() {
368 return this.xmppConnection;
369 }
370
371 public void setXmppConnection(final XmppConnection connection) {
372 this.xmppConnection = connection;
373 }
374
375 public String getOtrFingerprint() {
376 if (this.otrFingerprint == null) {
377 try {
378 if (this.mOtrService == null) {
379 return null;
380 }
381 final PublicKey publicKey = this.mOtrService.getPublicKey();
382 if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
383 return null;
384 }
385 this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey);
386 return this.otrFingerprint;
387 } catch (final OtrCryptoException ignored) {
388 return null;
389 }
390 } else {
391 return this.otrFingerprint;
392 }
393 }
394
395 public String getRosterVersion() {
396 if (this.rosterVersion == null) {
397 return "";
398 } else {
399 return this.rosterVersion;
400 }
401 }
402
403 public void setRosterVersion(final String version) {
404 this.rosterVersion = version;
405 }
406
407 public int countPresences() {
408 return this.getRoster().getContact(this.getJid().toBareJid()).getPresences().size();
409 }
410
411 public String getPgpSignature() {
412 try {
413 if (keys.has(KEY_PGP_SIGNATURE) && !"null".equals(keys.getString(KEY_PGP_SIGNATURE))) {
414 return keys.getString(KEY_PGP_SIGNATURE);
415 } else {
416 return null;
417 }
418 } catch (final JSONException e) {
419 return null;
420 }
421 }
422
423 public boolean setPgpSignature(String signature) {
424 try {
425 keys.put(KEY_PGP_SIGNATURE, signature);
426 } catch (JSONException e) {
427 return false;
428 }
429 return true;
430 }
431
432 public boolean unsetPgpSignature() {
433 try {
434 keys.put(KEY_PGP_SIGNATURE, JSONObject.NULL);
435 } catch (JSONException e) {
436 return false;
437 }
438 return true;
439 }
440
441 public long getPgpId() {
442 if (keys.has(KEY_PGP_ID)) {
443 try {
444 return keys.getLong(KEY_PGP_ID);
445 } catch (JSONException e) {
446 return -1;
447 }
448 } else {
449 return -1;
450 }
451 }
452
453 public boolean setPgpSignId(long pgpID) {
454 try {
455 keys.put(KEY_PGP_ID, pgpID);
456 } catch (JSONException e) {
457 return false;
458 }
459 return true;
460 }
461
462 public Roster getRoster() {
463 return this.roster;
464 }
465
466 public List<Bookmark> getBookmarks() {
467 return this.bookmarks;
468 }
469
470 public void setBookmarks(final List<Bookmark> bookmarks) {
471 this.bookmarks = bookmarks;
472 }
473
474 public boolean hasBookmarkFor(final Jid conferenceJid) {
475 for (final Bookmark bookmark : this.bookmarks) {
476 final Jid jid = bookmark.getJid();
477 if (jid != null && jid.equals(conferenceJid.toBareJid())) {
478 return true;
479 }
480 }
481 return false;
482 }
483
484 public boolean setAvatar(final String filename) {
485 if (this.avatar != null && this.avatar.equals(filename)) {
486 return false;
487 } else {
488 this.avatar = filename;
489 return true;
490 }
491 }
492
493 public String getAvatar() {
494 return this.avatar;
495 }
496
497 public void activateGracePeriod() {
498 this.mEndGracePeriod = SystemClock.elapsedRealtime()
499 + (Config.CARBON_GRACE_PERIOD * 1000);
500 }
501
502 public void deactivateGracePeriod() {
503 this.mEndGracePeriod = 0L;
504 }
505
506 public boolean inGracePeriod() {
507 return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
508 }
509
510 public String getShareableUri() {
511 final String fingerprint = this.getOtrFingerprint();
512 if (fingerprint != null) {
513 return "xmpp:" + this.getJid().toBareJid().toString() + "?otr-fingerprint="+fingerprint;
514 } else {
515 return "xmpp:" + this.getJid().toBareJid().toString();
516 }
517 }
518
519 public boolean isBlocked(final ListItem contact) {
520 final Jid jid = contact.getJid();
521 return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid()));
522 }
523
524 public boolean isBlocked(final Jid jid) {
525 return jid != null && blocklist.contains(jid.toBareJid());
526 }
527
528 public Collection<Jid> getBlocklist() {
529 return this.blocklist;
530 }
531
532 public void clearBlocklist() {
533 getBlocklist().clear();
534 }
535
536 public boolean isOnlineAndConnected() {
537 return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
538 }
539}