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