Compare commits

...

49 commits

Author SHA1 Message Date
Andreas Straub
3b279673ef Fix trust status for outgoing messages
Tag sent messages with own fingerprint, set own fingerprint as always
trusted, include own fingerprint in database trust search, explicitly
reset trust colorfilter
2015-07-15 16:34:55 +02:00
Andreas Straub
cbfd07d580 Don't merge messages with different trust statuses 2015-07-15 16:34:55 +02:00
Andreas Straub
235c9039e0 Merge pull request #1279 from SamWhited/fix_missing_refresh_icon
Add refresh icon to v21 theme / fix fingerprint copying
2015-07-10 09:49:12 +02:00
Sam Whited
625cab74cc Fix copying of axolotl keys to clipboard 2015-07-09 23:08:44 -05:00
Sam Whited
91b1225124 Add refresh icon to v21 theme 2015-07-09 22:37:40 -05:00
Andreas Straub
8b08bdb3af Disable Axolotl option if not usable
In MUCs or if contact is not axolotl capable, disable axolotl menu
option
2015-07-10 03:02:49 +02:00
Andreas Straub
36e7431595 Show trust status of messages' originating session
Shade lock icon red if message was received in a session that has not
been marked trusted by the user or fingerprint is unknown
2015-07-10 03:01:27 +02:00
Andreas Straub
7ebfe7f108 Add key trust toggle to ContactDetailsActivity
Can now toggle IdentityKey trust
2015-07-10 02:56:44 +02:00
Andreas Straub
3615b12cbb Ensure that available sessions are always used
Any time a new session is established, call syncRosterToDisk() to ensure
that on subsequent restoreFromDatabase() calls, the roster is actually
available. This is important so that initAccountServices() can properly
initialize the SessionMap. This prevents a race condition where after
adding a new account and initiating sessions with it, if the app is
killed (e.g. by reinstall) before triggering a syncRosterToDisk(),
subsequent restores will not have the roster available, leading to
missing XmppAxolotlSessions in the SessionMap cache. As a result of
this, a new session was initiated when sending a new message, and
received messages could not be tagged with the originating session's
fingerprint.

As an added sanity check, go to the database to confirm no records are
present before creating fresh XmppAxolotlSession objects (both in the
sending and receiving case).
2015-07-10 02:45:58 +02:00
Andreas Straub
cc661dc477 Fix setIdentityKeyTrust update statement 2015-07-10 02:45:58 +02:00
Andreas Straub
f0afcc4b52 Fix IdentityKey storage model
Added proper UNIQUE statement
2015-07-10 02:45:58 +02:00
Andreas Straub
26c8365a5d Fix and expand key regeneration function
Wipe session cache to prevent stale sessions being used. Wipe fetch
status cache to enable recreation of sessions. Regenerate deviceId, so
that foreign devices will talk to us again.
2015-07-10 02:45:51 +02:00
Andreas Straub
2afb36d891 Display trust status in ContactDetailsActivity 2015-07-09 14:26:19 +02:00
Andreas Straub
bdc9f9a44f Overhauled Message tagging
Messages are now tagged with the IdentityKey fingerprint of the
originating session. IdentityKeys have one of three trust states:
undecided (default), trusted, and untrusted/not yet trusted.
2015-07-09 14:23:17 +02:00
Andreas Straub
b1e719bd8b Postpone initAccountService until roster loaded
The AxolotlService depends on the roster being loaded when it is
initialized so that it can fill its in-memory SessionMap.
2015-07-09 14:18:54 +02:00
Andreas Straub
ce4b86e6d4 Fix getSubDeviceSessions SQL query 2015-07-09 14:15:59 +02:00
Andreas Straub
34f90f2eb7 Merge branch 'development' into CryptoNextAlpha
* development:
  show contacts name in non anonymous mucs. fixes #1213
2015-07-08 18:16:05 +02:00
Andreas Straub
2f487c7947 Display axolotl chat message hint 2015-07-08 18:14:28 +02:00
Andreas Straub
121919def1 Use full int range for device IDs 2015-07-08 18:13:49 +02:00
Andreas Straub
6b0d286518 Clean up unused constant 2015-07-08 17:46:03 +02:00
Andreas Straub
9e3419722b Make some fields final 2015-07-08 17:45:37 +02:00
Andreas Straub
4f6ca6fb63 Clean up logging
Add a fixed prefix to axolotl-related log messages, set log levels
sensibly.
2015-07-08 17:44:24 +02:00
Andreas Straub
d5b3557157 Add basic PEP managemend UI to EditAccountActivity
EditAccountActivity now show own fingerprint, and gives an option to
regenerate local keying material (and wipe all sessions associated with
the old keys in the process).

It also now displays a list of other own devices, and gives an option to
remove all but the current device.
2015-07-07 19:36:22 +02:00
Andreas Straub
9d780a382a Fix devicelist update handling
No longer store own device ID (so that we don't encrypt messages for
ourselves), verify that own device ID is present in update list
(otherwise republish), reflect update in UI.
2015-07-07 19:32:52 +02:00
Andreas Straub
7cdf2a9946 Refactor axolotl database recreation 2015-07-07 19:30:08 +02:00
Andreas Straub
30403a70f2 Adapt prettifyFingerprint() to axolotl FP sizes 2015-07-07 19:28:35 +02:00
Andreas Straub
4b0279a6ef Fix displaying Contact IdentityKeys
Migrate ContactDetailsActivity to use new SQL IdentityKeys storage,
remove dead code from Contact class.
2015-07-07 19:27:12 +02:00
Andreas Straub
3b8dfafecd Only cache session if successfully established
When receiving a message, only remember the XmppAxolotlSession wrapper
if the prospective session was actually established. This prevents us
from erroneously adding empty sessions that are never established using
received PreKeyWhisperMessages, which would lead to errors if we try to
use them for sending.
2015-07-05 22:54:28 +02:00
Andreas Straub
835584ae3b Return empty set on invalid PEP devicelist 2015-07-05 22:53:34 +02:00
Andreas Straub
4cc4e81b8e Trust all IdentityKeys
The trust-on-first-use policy leads to problems when receiving messages
from two different devices of a contact before sending a message to them
(as their IdentityKeys will not have been added yet). Since session
trust will be managed externally anyway, this change is not a security
problem, and will allow us to decrypt messages from yet-untrusted
sessions.
2015-07-05 22:10:43 +02:00
Andreas Straub
72619de889 Refresh PEP on session establish
We now track preKeys used to establish incoming sessions with us. On
each new established session, we remove the used prekey from PEP. We
have to do this because libaxolotl-java internally clears the used
preKey from its storage, so we will not be able to establish any future
sessions using that key.
2015-07-05 17:27:29 +02:00
Andreas Straub
12fc24dd42 Fix asynchronous axolotl message sending
XmppConnectionService.sendMessage() now dispatches messages to the
AxolotlService, where they only are prepared for sending and cached.
AxolotlService now triggers a XmppConnectionService.resendMessage(),
which then handles sending the cached message packet.

This transparently fixes, e.g., handling of messages sent while we are
offline.
2015-07-05 17:27:29 +02:00
Andreas Straub
c5596b34bc Properly track message sender
Previously, the sender was assumed to be the conversation counterpart.
This broke carboned own-device messages. We now track the sender
properly, and also set the status (sent by one of the own devices vs
received from the counterpart) accordingly.
2015-07-05 17:27:29 +02:00
Andreas Straub
9206a49b79 Rework PEP content verification
Now checks which part(s) are out of sync w/ local storage, and updates
only those, rather than assuming the entire node corrupt and
overwriting it all (especially relevant for preKey list)
2015-07-05 17:27:29 +02:00
Andreas Straub
7680a24180 Formatting fixes 2015-07-05 17:27:29 +02:00
Andreas Straub
c1116b6066 When receiving, add mock session if none exists
We need a session object in order to build a session from a
PreKeyWhisperMessage, so add an empty one when none exists on receiving
a message.

Warning: this will break right now if the session can not be constructed
from the received message.There will be an invalid session which will
break if we try to send using it.
2015-07-05 17:27:29 +02:00
Andreas Straub
ab2f85d2e8 Tag messages with originating session
This can be used later in order to display trust status of messages, as
well as for potential resending of messages in case of preKey conflicts.
2015-07-05 17:27:29 +02:00
Andreas Straub
a58d5e8ce3 Fetch bundles on-demand, encrypt in background
Bundles are now fetched on demand when a session needs to be
established. This should lessen the chance of changes to the bundles
occuring before they're used, as well as lessen the load of fetching
bundles.

Also, the message encryption is now done in a background thread, as this
can be somewhat costly if many sessions are present. This is probably
not going to be an issue in real use, but it's good practice anyway.
2015-07-05 17:27:22 +02:00
Andreas Straub
ae75c571df Use bareJid for own session retrieval 2015-07-05 17:26:29 +02:00
Andreas Straub
ba9520729f Migrate to new PEP layout
Merge prekeys into bundle node
2015-07-05 17:26:29 +02:00
Andreas Straub
287ce131d8 Formatting fixes 2015-07-05 17:26:29 +02:00
Andreas Straub
046a2d6045 Save IdentityKeys in database 2015-07-05 17:26:29 +02:00
Andreas Straub
71c0a75ec9 DatabaseBackend bugfixes
Don't leak cursors, initially create tables
2015-07-05 17:26:29 +02:00
Andreas Straub
0423852cb8 Reformat code to use tabs
This really sucks to do it like this. Sorry. :(
2015-07-05 17:26:29 +02:00
Andreas Straub
e8e126f2ce Added axolotl activation code to UI 2015-07-05 17:26:19 +02:00
Andreas Straub
61f18d4dfc Added PEP and message protocol layers
Can now fetch/retrieve from PEP, as well as encode/decode messages
2015-07-05 17:14:46 +02:00
Andreas Straub
6805abbef0 Reworked axolotl protocol layer
Numerous fixes
2015-07-05 17:09:35 +02:00
Andreas Straub
0917a75705 CryptoNext Menu entries added 2015-07-05 17:09:34 +02:00
Andreas Straub
f16b77d382 CryptoNext persistance layer mockup
Initial sketch of the peripheral storage infrastructure for the new
axolotl-based encryption scheme.
2015-07-05 17:09:34 +02:00
43 changed files with 2524 additions and 115 deletions

View file

@ -36,6 +36,7 @@ dependencies {
compile 'de.measite.minidns:minidns:0.1.3'
compile 'de.timroes.android:EnhancedListView:0.3.4'
compile 'me.leolin:ShortcutBadger:1.1.1@aar'
compile 'org.whispersystems:axolotl-android:1.3.4'
}
android {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
package eu.siacs.conversations.crypto.axolotl;
public class NoSessionsCreatedException extends Throwable{
}

View file

@ -0,0 +1,195 @@
package eu.siacs.conversations.crypto.axolotl;
import android.util.Base64;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import java.util.HashSet;
import java.util.Set;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.Jid;
public class XmppAxolotlMessage {
private byte[] innerKey;
private byte[] ciphertext;
private byte[] iv;
private final Set<XmppAxolotlMessageHeader> headers;
private final Jid from;
private final int sourceDeviceId;
public static class XmppAxolotlMessageHeader {
private final int recipientDeviceId;
private final byte[] content;
public XmppAxolotlMessageHeader(int deviceId, byte[] content) {
this.recipientDeviceId = deviceId;
this.content = content;
}
public XmppAxolotlMessageHeader(Element header) {
if("header".equals(header.getName())) {
this.recipientDeviceId = Integer.parseInt(header.getAttribute("rid"));
this.content = Base64.decode(header.getContent(),Base64.DEFAULT);
} else {
throw new IllegalArgumentException("Argument not a <header> Element!");
}
}
public int getRecipientDeviceId() {
return recipientDeviceId;
}
public byte[] getContents() {
return content;
}
public Element toXml() {
Element headerElement = new Element("header");
// TODO: generate XML
headerElement.setAttribute("rid", getRecipientDeviceId());
headerElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT));
return headerElement;
}
}
public static class XmppAxolotlPlaintextMessage {
private final AxolotlService.XmppAxolotlSession session;
private final String plaintext;
private final String fingerprint;
public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext, String fingerprint) {
this.session = session;
this.plaintext = plaintext;
this.fingerprint = fingerprint;
}
public String getPlaintext() {
return plaintext;
}
public AxolotlService.XmppAxolotlSession getSession() {
return session;
}
public String getFingerprint() {
return fingerprint;
}
}
public XmppAxolotlMessage(Jid from, Element axolotlMessage) {
this.from = from;
this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id"));
this.headers = new HashSet<>();
for(Element child:axolotlMessage.getChildren()) {
switch(child.getName()) {
case "header":
headers.add(new XmppAxolotlMessageHeader(child));
break;
case "message":
iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT);
ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT);
break;
default:
break;
}
}
}
public XmppAxolotlMessage(Jid from, int sourceDeviceId, String plaintext) {
this.from = from;
this.sourceDeviceId = sourceDeviceId;
this.headers = new HashSet<>();
this.encrypt(plaintext);
}
private void encrypt(String plaintext) {
try {
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(128);
SecretKey secretKey = generator.generateKey();
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
this.innerKey = secretKey.getEncoded();
this.iv = cipher.getIV();
this.ciphertext = cipher.doFinal(plaintext.getBytes());
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| IllegalBlockSizeException | BadPaddingException e) {
}
}
public Jid getFrom() {
return this.from;
}
public int getSenderDeviceId() {
return sourceDeviceId;
}
public byte[] getCiphertext() {
return ciphertext;
}
public Set<XmppAxolotlMessageHeader> getHeaders() {
return headers;
}
public void addHeader(XmppAxolotlMessageHeader header) {
headers.add(header);
}
public byte[] getInnerKey(){
return innerKey;
}
public byte[] getIV() {
return this.iv;
}
public Element toXml() {
// TODO: generate outer XML, add in header XML
Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX);
message.setAttribute("id", sourceDeviceId);
for(XmppAxolotlMessageHeader header: headers) {
message.addChild(header.toXml());
}
Element payload = message.addChild("message");
payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT));
payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT));
return message;
}
public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key, String fingerprint) {
XmppAxolotlPlaintextMessage plaintextMessage = null;
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
String plaintext = new String(cipher.doFinal(ciphertext));
plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext, fingerprint);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| InvalidAlgorithmParameterException | IllegalBlockSizeException
| BadPaddingException e) {
throw new AssertionError(e);
}
return plaintextMessage;
}
}

View file

@ -20,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OtrService;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
@ -122,6 +123,7 @@ public class Account extends AbstractEntity {
protected String avatar;
protected boolean online = false;
private OtrService mOtrService = null;
private AxolotlService axolotlService = null;
private XmppConnection xmppConnection = null;
private long mEndGracePeriod = 0L;
private String otrFingerprint;
@ -254,6 +256,10 @@ public class Account extends AbstractEntity {
return keys;
}
public String getKey(final String name) {
return this.keys.optString(name, null);
}
public boolean setKey(final String keyName, final String keyValue) {
try {
this.keys.put(keyName, keyValue);
@ -277,8 +283,13 @@ public class Account extends AbstractEntity {
return values;
}
public AxolotlService getAxolotlService() {
return axolotlService;
}
public void initAccountServices(final XmppConnectionService context) {
this.mOtrService = new OtrService(context, this);
this.axolotlService = new AxolotlService(this, context);
}
public OtrService getOtrService() {

View file

@ -2,15 +2,20 @@ package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
import android.util.Base64;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
@ -183,20 +188,22 @@ public class Contact implements ListItem, Blockable {
}
public ContentValues getContentValues() {
final ContentValues values = new ContentValues();
values.put(ACCOUNT, accountUuid);
values.put(SYSTEMNAME, systemName);
values.put(SERVERNAME, serverName);
values.put(JID, jid.toString());
values.put(OPTIONS, subscription);
values.put(SYSTEMACCOUNT, systemAccount);
values.put(PHOTOURI, photoUri);
values.put(KEYS, keys.toString());
values.put(AVATAR, avatar == null ? null : avatar.getFilename());
values.put(LAST_PRESENCE, lastseen.presence);
values.put(LAST_TIME, lastseen.time);
values.put(GROUPS, groups.toString());
return values;
synchronized (this.keys) {
final ContentValues values = new ContentValues();
values.put(ACCOUNT, accountUuid);
values.put(SYSTEMNAME, systemName);
values.put(SERVERNAME, serverName);
values.put(JID, jid.toString());
values.put(OPTIONS, subscription);
values.put(SYSTEMACCOUNT, systemAccount);
values.put(PHOTOURI, photoUri);
values.put(KEYS, keys.toString());
values.put(AVATAR, avatar == null ? null : avatar.getFilename());
values.put(LAST_PRESENCE, lastseen.presence);
values.put(LAST_TIME, lastseen.time);
values.put(GROUPS, groups.toString());
return values;
}
}
public int getSubscription() {
@ -281,60 +288,65 @@ public class Contact implements ListItem, Blockable {
}
public ArrayList<String> getOtrFingerprints() {
final ArrayList<String> fingerprints = new ArrayList<String>();
try {
if (this.keys.has("otr_fingerprints")) {
final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
for (int i = 0; i < prints.length(); ++i) {
final String print = prints.isNull(i) ? null : prints.getString(i);
if (print != null && !print.isEmpty()) {
fingerprints.add(prints.getString(i));
synchronized (this.keys) {
final ArrayList<String> fingerprints = new ArrayList<String>();
try {
if (this.keys.has("otr_fingerprints")) {
final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
for (int i = 0; i < prints.length(); ++i) {
final String print = prints.isNull(i) ? null : prints.getString(i);
if (print != null && !print.isEmpty()) {
fingerprints.add(prints.getString(i));
}
}
}
}
} catch (final JSONException ignored) {
} catch (final JSONException ignored) {
}
return fingerprints;
}
return fingerprints;
}
public boolean addOtrFingerprint(String print) {
if (getOtrFingerprints().contains(print)) {
return false;
}
try {
JSONArray fingerprints;
if (!this.keys.has("otr_fingerprints")) {
fingerprints = new JSONArray();
} else {
fingerprints = this.keys.getJSONArray("otr_fingerprints");
synchronized (this.keys) {
if (getOtrFingerprints().contains(print)) {
return false;
}
try {
JSONArray fingerprints;
if (!this.keys.has("otr_fingerprints")) {
fingerprints = new JSONArray();
} else {
fingerprints = this.keys.getJSONArray("otr_fingerprints");
}
fingerprints.put(print);
this.keys.put("otr_fingerprints", fingerprints);
return true;
} catch (final JSONException ignored) {
return false;
}
fingerprints.put(print);
this.keys.put("otr_fingerprints", fingerprints);
return true;
} catch (final JSONException ignored) {
return false;
}
}
public long getPgpKeyId() {
if (this.keys.has("pgp_keyid")) {
try {
return this.keys.getLong("pgp_keyid");
} catch (JSONException e) {
synchronized (this.keys) {
if (this.keys.has("pgp_keyid")) {
try {
return this.keys.getLong("pgp_keyid");
} catch (JSONException e) {
return 0;
}
} else {
return 0;
}
} else {
return 0;
}
}
public void setPgpKeyId(long keyId) {
try {
this.keys.put("pgp_keyid", keyId);
} catch (final JSONException ignored) {
synchronized (this.keys) {
try {
this.keys.put("pgp_keyid", keyId);
} catch (final JSONException ignored) {
}
}
}
@ -441,24 +453,26 @@ public class Contact implements ListItem, Blockable {
}
public boolean deleteOtrFingerprint(String fingerprint) {
boolean success = false;
try {
if (this.keys.has("otr_fingerprints")) {
JSONArray newPrints = new JSONArray();
JSONArray oldPrints = this.keys
.getJSONArray("otr_fingerprints");
for (int i = 0; i < oldPrints.length(); ++i) {
if (!oldPrints.getString(i).equals(fingerprint)) {
newPrints.put(oldPrints.getString(i));
} else {
success = true;
synchronized (this.keys) {
boolean success = false;
try {
if (this.keys.has("otr_fingerprints")) {
JSONArray newPrints = new JSONArray();
JSONArray oldPrints = this.keys
.getJSONArray("otr_fingerprints");
for (int i = 0; i < oldPrints.length(); ++i) {
if (!oldPrints.getString(i).equals(fingerprint)) {
newPrints.put(oldPrints.getString(i));
} else {
success = true;
}
}
this.keys.put("otr_fingerprints", newPrints);
}
this.keys.put("otr_fingerprints", newPrints);
return success;
} catch (JSONException e) {
return false;
}
return success;
} catch (JSONException e) {
return false;
}
}

View file

@ -179,13 +179,13 @@ public class Conversation extends AbstractEntity implements Blockable {
}
}
public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) {
public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
synchronized (this.messages) {
for (Message message : this.messages) {
if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
&& (message.getEncryption() == Message.ENCRYPTION_OTR)) {
&& (message.getEncryption() == encryptionType)) {
onMessageFound.onMessageFound(message);
}
}
}
}
}

View file

@ -8,6 +8,7 @@ import java.net.URL;
import java.util.Arrays;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.UIHelper;
@ -34,6 +35,7 @@ public class Message extends AbstractEntity {
public static final int ENCRYPTION_OTR = 2;
public static final int ENCRYPTION_DECRYPTED = 3;
public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
public static final int ENCRYPTION_AXOLOTL = 5;
public static final int TYPE_TEXT = 0;
public static final int TYPE_IMAGE = 1;
@ -52,6 +54,7 @@ public class Message extends AbstractEntity {
public static final String REMOTE_MSG_ID = "remoteMsgId";
public static final String SERVER_MSG_ID = "serverMsgId";
public static final String RELATIVE_FILE_PATH = "relativeFilePath";
public static final String FINGERPRINT = "axolotl_fingerprint";
public static final String ME_COMMAND = "/me ";
@ -73,6 +76,7 @@ public class Message extends AbstractEntity {
protected Downloadable downloadable = null;
private Message mNextMessage = null;
private Message mPreviousMessage = null;
private String axolotlFingerprint = null;
private Message() {
@ -94,6 +98,7 @@ public class Message extends AbstractEntity {
TYPE_TEXT,
null,
null,
null,
null);
this.conversation = conversation;
}
@ -101,7 +106,7 @@ public class Message extends AbstractEntity {
private Message(final String uuid, final String conversationUUid, final Jid counterpart,
final Jid trueCounterpart, final String body, final long timeSent,
final int encryption, final int status, final int type, final String remoteMsgId,
final String relativeFilePath, final String serverMsgId) {
final String relativeFilePath, final String serverMsgId, final String fingerprint) {
this.uuid = uuid;
this.conversationUuid = conversationUUid;
this.counterpart = counterpart;
@ -114,6 +119,7 @@ public class Message extends AbstractEntity {
this.remoteMsgId = remoteMsgId;
this.relativeFilePath = relativeFilePath;
this.serverMsgId = serverMsgId;
this.axolotlFingerprint = fingerprint;
}
public static Message fromCursor(Cursor cursor) {
@ -150,7 +156,8 @@ public class Message extends AbstractEntity {
cursor.getInt(cursor.getColumnIndex(TYPE)),
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)));
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
cursor.getString(cursor.getColumnIndex(FINGERPRINT)));
}
public static Message createStatusMessage(Conversation conversation, String body) {
@ -184,6 +191,7 @@ public class Message extends AbstractEntity {
values.put(REMOTE_MSG_ID, remoteMsgId);
values.put(RELATIVE_FILE_PATH, relativeFilePath);
values.put(SERVER_MSG_ID, serverMsgId);
values.put(FINGERPRINT, axolotlFingerprint);
return values;
}
@ -381,7 +389,8 @@ public class Message extends AbstractEntity {
!message.getBody().startsWith(ME_COMMAND) &&
!this.getBody().startsWith(ME_COMMAND) &&
!this.bodyIsHeart() &&
!message.bodyIsHeart()
!message.bodyIsHeart() &&
this.isTrusted() == message.isTrusted()
);
}
@ -653,4 +662,17 @@ public class Message extends AbstractEntity {
public int width = 0;
public int height = 0;
}
public void setAxolotlFingerprint(String fingerprint) {
this.axolotlFingerprint = fingerprint;
}
public String getAxolotlFingerprint() {
return axolotlFingerprint;
}
public boolean isTrusted() {
return conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint)
== AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED;
}
}

View file

@ -12,6 +12,7 @@ import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.PhoneHelper;
@ -28,7 +29,8 @@ public abstract class AbstractGenerator {
"urn:xmpp:avatar:metadata+notify",
"urn:xmpp:ping",
"jabber:iq:version",
"http://jabber.org/protocol/chatstates"};
"http://jabber.org/protocol/chatstates",
AxolotlService.PEP_DEVICE_LIST+"+notify"};
private final String[] MESSAGE_CONFIRMATION_FEATURES = {
"urn:xmpp:chat-markers:0",
"urn:xmpp:receipts"

View file

@ -1,9 +1,18 @@
package eu.siacs.conversations.generator;
import android.util.Base64;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
@ -115,6 +124,56 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket retrieveDeviceIds(final Jid to) {
final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
if(to != null) {
packet.setTo(to);
}
return packet;
}
public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) {
final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES+":"+deviceid, null);
if(to != null) {
packet.setTo(to);
}
return packet;
}
public IqPacket publishDeviceIds(final Set<Integer> ids) {
final Element item = new Element("item");
final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
for(Integer id:ids) {
final Element device = new Element("device");
device.setAttribute("id", id);
list.addChild(device);
}
return publish(AxolotlService.PEP_DEVICE_LIST, item);
}
public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
final Set<PreKeyRecord> preKeyRecords, final int deviceId) {
final Element item = new Element("item");
final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX);
final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic");
signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId());
ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey();
signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(),Base64.DEFAULT));
final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature");
signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(),Base64.DEFAULT));
final Element identityKeyElement = bundle.addChild("identityKey");
identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX);
for(PreKeyRecord preKeyRecord:preKeyRecords) {
final Element prekey = prekeys.addChild("preKeyPublic");
prekey.setAttribute("preKeyId", preKeyRecord.getId());
prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT));
}
return publish(AxolotlService.PEP_BUNDLES+":"+deviceId, item);
}
public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
final Element query = packet.query("urn:xmpp:mam:0");

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.generator;
import android.util.Log;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
@ -7,6 +9,11 @@ import java.util.TimeZone;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.NoSessionsCreatedException;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
@ -59,6 +66,22 @@ public class MessageGenerator extends AbstractGenerator {
delay.setAttribute("stamp", mDateFormat.format(date));
}
public MessagePacket generateAxolotlChat(Message message) {
return generateAxolotlChat(message, false);
}
public MessagePacket generateAxolotlChat(Message message, boolean addDelay) {
MessagePacket packet = preparePacket(message, addDelay);
AxolotlService service = message.getConversation().getAccount().getAxolotlService();
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(message.getConversation().getAccount())+"Submitting message to axolotl service for send processing...");
XmppAxolotlMessage axolotlMessage = service.encrypt(message);
if (axolotlMessage == null) {
return null;
}
packet.setAxolotlMessage(axolotlMessage.toXml());
return packet;
}
public MessagePacket generateOtrChat(Message message) {
return generateOtrChat(message, false);
}

View file

@ -1,11 +1,25 @@
package eu.siacs.conversations.parser;
import android.support.annotation.NonNull;
import android.util.Base64;
import android.util.Log;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService;
@ -71,6 +85,155 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
return super.avatarData(items);
}
public Element getItem(final IqPacket packet) {
final Element pubsub = packet.findChild("pubsub",
"http://jabber.org/protocol/pubsub");
if (pubsub == null) {
return null;
}
final Element items = pubsub.findChild("items");
if (items == null) {
return null;
}
return items.findChild("item");
}
@NonNull
public Set<Integer> deviceIds(final Element item) {
Set<Integer> deviceIds = new HashSet<>();
if (item != null) {
final Element list = item.findChild("list");
if (list != null) {
for (Element device : list.getChildren()) {
if (!device.getName().equals("device")) {
continue;
}
try {
Integer id = Integer.valueOf(device.getAttribute("id"));
deviceIds.add(id);
} catch (NumberFormatException e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered nvalid <device> node in PEP:" + device.toString()
+ ", skipping...");
continue;
}
}
}
}
return deviceIds;
}
public Integer signedPreKeyId(final Element bundle) {
final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
if(signedPreKeyPublic == null) {
return null;
}
return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
}
public ECPublicKey signedPreKeyPublic(final Element bundle) {
ECPublicKey publicKey = null;
final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
if(signedPreKeyPublic == null) {
return null;
}
try {
publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0);
} catch (InvalidKeyException e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage());
}
return publicKey;
}
public byte[] signedPreKeySignature(final Element bundle) {
final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature");
if(signedPreKeySignature == null) {
return null;
}
return Base64.decode(signedPreKeySignature.getContent(),Base64.DEFAULT);
}
public IdentityKey identityKey(final Element bundle) {
IdentityKey identityKey = null;
final Element identityKeyElement = bundle.findChild("identityKey");
if(identityKeyElement == null) {
return null;
}
try {
identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0);
} catch (InvalidKeyException e) {
Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage());
}
return identityKey;
}
public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
Element item = getItem(packet);
if (item == null) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <item> in bundle IQ packet: " + packet);
return null;
}
final Element bundleElement = item.findChild("bundle");
if(bundleElement == null) {
return null;
}
final Element prekeysElement = bundleElement.findChild("prekeys");
if(prekeysElement == null) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <prekeys> in bundle IQ packet: " + packet);
return null;
}
for(Element preKeyPublicElement : prekeysElement.getChildren()) {
if(!preKeyPublicElement.getName().equals("preKeyPublic")){
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement);
continue;
}
Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
try {
ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
preKeyRecords.put(preKeyId, preKeyPublic);
} catch (InvalidKeyException e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping...");
continue;
}
}
return preKeyRecords;
}
public PreKeyBundle bundle(final IqPacket bundle) {
Element bundleItem = getItem(bundle);
if(bundleItem == null) {
return null;
}
final Element bundleElement = bundleItem.findChild("bundle");
if(bundleElement == null) {
return null;
}
ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement);
Integer signedPreKeyId = signedPreKeyId(bundleElement);
byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
IdentityKey identityKey = identityKey(bundleElement);
if(signedPreKeyPublic == null || identityKey == null) {
return null;
}
return new PreKeyBundle(0, 0, 0, null,
signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey);
}
public List<PreKeyBundle> preKeys(final IqPacket preKeys) {
List<PreKeyBundle> bundles = new ArrayList<>();
Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
if ( preKeyPublics != null) {
for (Integer preKeyId : preKeyPublics.keySet()) {
ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId);
bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic,
0, null, null, null));
}
}
return bundles;
}
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {

View file

@ -6,7 +6,12 @@ import android.util.Pair;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionStatus;
import java.util.List;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@ -94,6 +99,20 @@ public class MessageParser extends AbstractParser implements
}
}
private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) {
Message finishedMessage = null;
AxolotlService service = conversation.getAccount().getAxolotlService();
XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(from.toBareJid(), axolotlMessage);
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage);
if(plaintextMessage != null) {
finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint());
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint());
}
return finishedMessage;
}
private class Invite {
Jid jid;
String password;
@ -170,6 +189,13 @@ public class MessageParser extends AbstractParser implements
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateAccountUi();
}
} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing...");
Element item = items.findChild("item");
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
AxolotlService axolotlService = account.getAxolotlService();
axolotlService.registerDevices(from, deviceIds);
mXmppConnectionService.updateAccountUi();
}
}
@ -232,8 +258,9 @@ public class MessageParser extends AbstractParser implements
timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis());
}
final String body = packet.getBody();
final String encrypted = packet.findChildContent("x", "jabber:x:encrypted");
final Element mucUserElement = packet.findChild("x","http://jabber.org/protocol/muc#user");
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
final Element axolotlEncrypted = packet.findChild("axolotl_message", AxolotlService.PEP_PREFIX);
int status;
final Jid counterpart;
final Jid to = packet.getTo();
@ -261,11 +288,11 @@ public class MessageParser extends AbstractParser implements
return;
}
if (extractChatState(mXmppConnectionService.find(account,from), packet)) {
if (extractChatState(mXmppConnectionService.find(account, from), packet)) {
mXmppConnectionService.updateConversationUi();
}
if ((body != null || encrypted != null) && !isMucStatusMessage) {
if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) {
Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat);
if (isTypeGroupChat) {
if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) {
@ -294,8 +321,13 @@ public class MessageParser extends AbstractParser implements
} else {
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
}
} else if (encrypted != null) {
message = new Message(conversation, encrypted, Message.ENCRYPTION_PGP, status);
} else if (pgpEncrypted != null) {
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
} else if (axolotlEncrypted != null) {
message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status);
if (message == null) {
return;
}
} else {
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
}

View file

@ -1,10 +1,31 @@
package eu.siacs.conversations.persistance;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Base64;
import android.util.Log;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@ -13,19 +34,12 @@ import eu.siacs.conversations.entities.Roster;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history";
private static final int DATABASE_VERSION = 14;
private static final int DATABASE_VERSION = 15;
private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@ -33,12 +47,66 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Contact.JID + " TEXT," + Contact.KEYS + " TEXT,"
+ Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER,"
+ Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, "
+ Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, "
+ Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, "
+ Contact.GROUPS + " TEXT, FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES "
+ Account.TABLENAME + "(" + Account.UUID
+ ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
+ Contact.JID + ") ON CONFLICT REPLACE);";
private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
+ AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME + "("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.ID + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", "
+ AxolotlService.SQLiteAxolotlStore.ID
+ ") ON CONFLICT REPLACE"
+");";
private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE "
+ AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.ID + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", "
+ AxolotlService.SQLiteAxolotlStore.ID
+ ") ON CONFLICT REPLACE"+
");";
private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE "
+ AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME + "("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", "
+ AxolotlService.SQLiteAxolotlStore.NAME + ", "
+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID
+ ") ON CONFLICT REPLACE"
+");";
private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE "
+ AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME + "("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.OWN + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", "
+ AxolotlService.SQLiteAxolotlStore.NAME + ", "
+ AxolotlService.SQLiteAxolotlStore.FINGERPRINT
+ ") ON CONFLICT IGNORE"
+");";
private DatabaseBackend(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@ -69,12 +137,17 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, "
+ Message.RELATIVE_FILE_PATH + " TEXT, "
+ Message.SERVER_MSG_ID + " TEXT, "
+ Message.FINGERPRINT + " TEXT, "
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID
+ ") ON DELETE CASCADE);");
db.execSQL(CREATE_CONTATCS_STATEMENT);
db.execSQL(CREATE_SESSIONS_STATEMENT);
db.execSQL(CREATE_PREKEYS_STATEMENT);
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
db.execSQL(CREATE_IDENTITIES_STATEMENT);
}
@Override
@ -109,12 +182,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN "
+ Conversation.ATTRIBUTES + " TEXT");
}
if (oldVersion < 9 && newVersion >= 9) {
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
+ Contact.LAST_TIME + " NUMBER");
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
+ Contact.LAST_PRESENCE + " TEXT");
}
if (oldVersion < 9 && newVersion >= 9) {
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
+ Contact.LAST_TIME + " NUMBER");
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
+ Contact.LAST_PRESENCE + " TEXT");
}
if (oldVersion < 10 && newVersion >= 10) {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ Message.RELATIVE_FILE_PATH + " TEXT");
@ -215,6 +288,11 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
cursor.close();
}
if (oldVersion < 15 && newVersion >= 15) {
recreateAxolotlDb();
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ Message.FINGERPRINT + " TEXT");
}
}
public static synchronized DatabaseBackend getInstance(Context context) {
@ -311,7 +389,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
};
Cursor cursor = db.query(Conversation.TABLENAME, null,
Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID
+ " like ? OR "+Conversation.CONTACTJID+"=?)", selectionArgs, null, null, null);
+ " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null);
if (cursor.getCount() == 0)
return null;
cursor.moveToFirst();
@ -481,4 +559,378 @@ public class DatabaseBackend extends SQLiteOpenHelper {
cursor.close();
return list;
}
private Cursor getCursorForSession(Account account, AxolotlAddress contact) {
final SQLiteDatabase db = this.getReadableDatabase();
String[] columns = null;
String[] selectionArgs = {account.getUuid(),
contact.getName(),
Integer.toString(contact.getDeviceId())};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME,
columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " = ? ",
selectionArgs,
null, null, null);
return cursor;
}
public SessionRecord loadSession(Account account, AxolotlAddress contact) {
SessionRecord session = null;
Cursor cursor = getCursorForSession(account, contact);
if(cursor.getCount() != 0) {
cursor.moveToFirst();
try {
session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (IOException e) {
cursor.close();
throw new AssertionError(e);
}
}
cursor.close();
return session;
}
public List<Integer> getSubDeviceSessions(Account account, AxolotlAddress contact) {
List<Integer> devices = new ArrayList<>();
final SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.DEVICE_ID};
String[] selectionArgs = {account.getUuid(),
contact.getName()};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME,
columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ?",
selectionArgs,
null, null, null);
while(cursor.moveToNext()) {
devices.add(cursor.getInt(
cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.DEVICE_ID)));
}
cursor.close();
return devices;
}
public boolean containsSession(Account account, AxolotlAddress contact) {
Cursor cursor = getCursorForSession(account, contact);
int count = cursor.getCount();
cursor.close();
return count != 0;
}
public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.NAME, contact.getName());
values.put(AxolotlService.SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId());
values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(),Base64.DEFAULT));
values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid());
db.insert(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, null, values);
}
public void deleteSession(Account account, AxolotlAddress contact) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(),
contact.getName(),
Integer.toString(contact.getDeviceId())};
db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " = ? ",
args);
}
public void deleteAllSessions(Account account, AxolotlAddress contact) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(), contact.getName()};
db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ?",
args);
}
private Cursor getCursorForPreKey(Account account, int preKeyId) {
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY};
String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME,
columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ AxolotlService.SQLiteAxolotlStore.ID + "=?",
selectionArgs,
null, null, null);
return cursor;
}
public PreKeyRecord loadPreKey(Account account, int preKeyId) {
PreKeyRecord record = null;
Cursor cursor = getCursorForPreKey(account, preKeyId);
if(cursor.getCount() != 0) {
cursor.moveToFirst();
try {
record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (IOException e ) {
throw new AssertionError(e);
}
}
cursor.close();
return record;
}
public boolean containsPreKey(Account account, int preKeyId) {
Cursor cursor = getCursorForPreKey(account, preKeyId);
int count = cursor.getCount();
cursor.close();
return count != 0;
}
public void storePreKey(Account account, PreKeyRecord record) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId());
values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT));
values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid());
db.insert(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, null, values);
}
public void deletePreKey(Account account, int preKeyId) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(), Integer.toString(preKeyId)};
db.delete(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ AxolotlService.SQLiteAxolotlStore.ID + "=?",
args);
}
private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) {
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY};
String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + AxolotlService.SQLiteAxolotlStore.ID + "=?",
selectionArgs,
null, null, null);
return cursor;
}
public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) {
SignedPreKeyRecord record = null;
Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId);
if(cursor.getCount() != 0) {
cursor.moveToFirst();
try {
record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (IOException e ) {
throw new AssertionError(e);
}
}
cursor.close();
return record;
}
public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) {
List<SignedPreKeyRecord> prekeys = new ArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY};
String[] selectionArgs = {account.getUuid()};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=?",
selectionArgs,
null, null, null);
while(cursor.moveToNext()) {
try {
prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)), Base64.DEFAULT)));
} catch (IOException ignored) {
}
}
cursor.close();
return prekeys;
}
public boolean containsSignedPreKey(Account account, int signedPreKeyId) {
Cursor cursor = getCursorForPreKey(account, signedPreKeyId);
int count = cursor.getCount();
cursor.close();
return count != 0;
}
public void storeSignedPreKey(Account account, SignedPreKeyRecord record) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId());
values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT));
values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid());
db.insert(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values);
}
public void deleteSignedPreKey(Account account, int signedPreKeyId) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)};
db.delete(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ AxolotlService.SQLiteAxolotlStore.ID + "=?",
args);
}
private Cursor getIdentityKeyCursor(Account account, String name, boolean own) {
return getIdentityKeyCursor(account, name, own, null);
}
private Cursor getIdentityKeyCursor(Account account, String fingerprint) {
return getIdentityKeyCursor(account, null, null, fingerprint);
}
private Cursor getIdentityKeyCursor(Account account, String name, Boolean own, String fingerprint) {
final SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.TRUSTED,
AxolotlService.SQLiteAxolotlStore.KEY};
ArrayList<String> selectionArgs = new ArrayList<>(4);
selectionArgs.add(account.getUuid());
String selectionString = AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?";
if (name != null){
selectionArgs.add(name);
selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.NAME + " = ?";
}
if (fingerprint != null){
selectionArgs.add(fingerprint);
selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ?";
}
if (own != null){
selectionArgs.add(own?"1":"0");
selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.OWN + " = ?";
}
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME,
columns,
selectionString,
selectionArgs.toArray(new String[selectionArgs.size()]),
null, null, null);
return cursor;
}
public IdentityKeyPair loadOwnIdentityKeyPair(Account account, String name) {
IdentityKeyPair identityKeyPair = null;
Cursor cursor = getIdentityKeyCursor(account, name, true);
if(cursor.getCount() != 0) {
cursor.moveToFirst();
try {
identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
}
}
cursor.close();
return identityKeyPair;
}
public Set<IdentityKey> loadIdentityKeys(Account account, String name) {
Set<IdentityKey> identityKeys = new HashSet<>();
Cursor cursor = getIdentityKeyCursor(account, name, false);
while(cursor.moveToNext()) {
try {
identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0));
} catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name);
}
}
cursor.close();
return identityKeys;
}
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) {
storeIdentityKey(account, name, own, fingerprint, base64Serialized, AxolotlService.SQLiteAxolotlStore.Trust.UNDECIDED);
}
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, AxolotlService.SQLiteAxolotlStore.Trust trusted) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid());
values.put(AxolotlService.SQLiteAxolotlStore.NAME, name);
values.put(AxolotlService.SQLiteAxolotlStore.OWN, own ? 1 : 0);
values.put(AxolotlService.SQLiteAxolotlStore.FINGERPRINT, fingerprint);
values.put(AxolotlService.SQLiteAxolotlStore.KEY, base64Serialized);
values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trusted.ordinal());
db.insert(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
}
public AxolotlService.SQLiteAxolotlStore.Trust isIdentityKeyTrusted(Account account, String fingerprint) {
Cursor cursor = getIdentityKeyCursor(account, fingerprint);
AxolotlService.SQLiteAxolotlStore.Trust trust = null;
if (cursor.getCount() > 0) {
cursor.moveToFirst();
int trustValue = cursor.getInt(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.TRUSTED));
trust = AxolotlService.SQLiteAxolotlStore.Trust.values()[trustValue];
}
cursor.close();
return trust;
}
public boolean setIdentityKeyTrust(Account account, String fingerprint, AxolotlService.SQLiteAxolotlStore.Trust trust) {
SQLiteDatabase db = this.getWritableDatabase();
String[] selectionArgs = {
account.getUuid(),
fingerprint
};
ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trust.ordinal());
int rows = db.update(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? ",
selectionArgs);
return rows == 1;
}
public void storeIdentityKey(Account account, String name, IdentityKey identityKey) {
storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
}
public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) {
storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED);
}
public void recreateAxolotlDb() {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+">>> (RE)CREATING AXOLOTL DATABASE <<<");
SQLiteDatabase db = this.getWritableDatabase();
db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME);
db.execSQL(CREATE_SESSIONS_STATEMENT);
db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME);
db.execSQL(CREATE_PREKEYS_STATEMENT);
db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME);
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME);
db.execSQL(CREATE_IDENTITIES_STATEMENT);
}
public void wipeAxolotlDb(Account account) {
String accountName = account.getUuid();
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<");
SQLiteDatabase db = this.getWritableDatabase();
String[] deleteArgs= {
accountName
};
db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
db.delete(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
db.delete(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
db.delete(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
}
}

View file

@ -272,7 +272,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
syncDirtyContacts(account);
scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode());
account.getAxolotlService().publishOwnDeviceIdIfNeeded();
account.getAxolotlService().publishBundlesIfNeeded();
scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
} else if (account.getStatus() == Account.State.OFFLINE) {
resetSendingToWaiting(account);
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
@ -590,9 +593,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
this.accounts = databaseBackend.getAccounts();
for (final Account account : this.accounts) {
account.initAccountServices(this);
}
restoreFromDatabase();
getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
@ -698,7 +698,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
message.getConversation().endOtrIfNeeded();
message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
new Conversation.OnMessageFound() {
@Override
public void onMessageFound(Message message) {
markMessage(message,Message.STATUS_SEND_FAILED);
@ -752,6 +753,15 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
break;
case Message.ENCRYPTION_AXOLOTL:
message.setStatus(Message.STATUS_WAITING);
packet = account.getAxolotlService().fetchPacketFromCache(message);
if (packet == null && account.isOnlineAndConnected()) {
account.getAxolotlService().prepareMessage(message);
message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", ""));
}
break;
}
if (packet != null) {
if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
@ -779,6 +789,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
conversation.startOtrSession(message.getCounterpart().getResourcepart(), false);
}
break;
case Message.ENCRYPTION_AXOLOTL:
message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", ""));
break;
}
}
@ -943,6 +956,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
Log.d(Config.LOGTAG,"restoring roster");
for(Account account : accounts) {
databaseBackend.readRoster(account.getRoster());
account.initAccountServices(XmppConnectionService.this);
}
getBitmapCache().evictAll();
Looper.prepare();
@ -1757,7 +1771,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
account.getJid().toBareJid() + " otr session established with "
+ conversation.getJid() + "/"
+ otrSession.getSessionID().getUserID());
conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
@Override
public void onMessageFound(Message message) {

View file

@ -29,11 +29,13 @@ import android.widget.QuickContactBadge;
import android.widget.TextView;
import org.openintents.openpgp.util.OpenPgpUtils;
import org.whispersystems.libaxolotl.IdentityKey;
import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.ListItem;
@ -362,13 +364,13 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
View view = inflater.inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
ImageButton remove = (ImageButton) view
ImageButton removeButton = (ImageButton) view
.findViewById(R.id.button_remove);
remove.setVisibility(View.VISIBLE);
removeButton.setVisibility(View.VISIBLE);
keyType.setText("OTR Fingerprint");
key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
keys.addView(view);
remove.setOnClickListener(new OnClickListener() {
removeButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
@ -376,6 +378,57 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
});
}
for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys(
contact.getAccount(), contact.getJid().toBareJid().toString())) {
hasKeys = true;
View view = inflater.inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
TextView keyTrust = (TextView) view.findViewById(R.id.key_trust);
ImageButton removeButton = (ImageButton) view
.findViewById(R.id.button_remove);
ImageButton trustButton = (ImageButton) view
.findViewById(R.id.button_trust);
final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", "");
final Jid bareJid = contactJid.toBareJid();
AxolotlService.SQLiteAxolotlStore.Trust trust = contact.getAccount().getAxolotlService()
.getFingerprintTrust(fingerprint);
switch (trust) {
case TRUSTED:
removeButton.setVisibility(View.VISIBLE);
//Log.d(Config.LOGTAG, AxolotlService.getLogprefix(contact.getAccount()) + "Setting remove button visible!");
break;
case UNDECIDED:
case UNTRUSTED:
//Log.d(Config.LOGTAG, AxolotlService.getLogprefix(contact.getAccount()) + "Setting trust button visible!");
trustButton.setVisibility(View.VISIBLE);
break;
}
keyType.setText("Axolotl Fingerprint");
key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint()));
keyTrust.setText(trust.toString());
keyTrust.setVisibility(View.VISIBLE);
keys.addView(view);
removeButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
axolotlService.setFingerprintTrust(fingerprint,
AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED);
refreshUi();
xmppConnectionService.updateConversationUi();
}
});
trustButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
axolotlService.setFingerprintTrust(fingerprint,
AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED);
refreshUi();
xmppConnectionService.updateConversationUi();
}
});
}
if (contact.getPgpKeyId() != 0) {
hasKeys = true;
View view = inflater.inflate(R.layout.contact_key, keys, false);

View file

@ -16,6 +16,7 @@ import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v4.widget.SlidingPaneLayout;
import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -34,7 +35,9 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
import eu.siacs.conversations.entities.Contact;
@ -752,6 +755,12 @@ public class ConversationActivity extends XmppActivity
showInstallPgpDialog();
}
break;
case R.id.encryption_choice_axolotl:
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount())
+ "Enabled axolotl for Contact " + conversation.getContact().getJid());
conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL);
item.setChecked(true);
break;
default:
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
break;
@ -764,15 +773,20 @@ public class ConversationActivity extends XmppActivity
});
popup.inflate(R.menu.encryption_choices);
MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr);
MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl);
MenuItem none = popup.getMenu().findItem(
R.id.encryption_choice_none);
if (conversation.getMode() == Conversation.MODE_MULTI) {
otr.setEnabled(false);
axolotl.setEnabled(false);
} else {
if (forceEncryption()) {
none.setVisible(false);
}
}
if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) {
axolotl.setEnabled(false);
}
switch (conversation.getNextEncryption(forceEncryption())) {
case Message.ENCRYPTION_NONE:
none.setChecked(true);
@ -784,6 +798,10 @@ public class ConversationActivity extends XmppActivity
popup.getMenu().findItem(R.id.encryption_choice_pgp)
.setChecked(true);
break;
case Message.ENCRYPTION_AXOLOTL:
popup.getMenu().findItem(R.id.encryption_choice_axolotl)
.setChecked(true);
break;
default:
popup.getMenu().findItem(R.id.encryption_choice_none)
.setChecked(true);

View file

@ -302,6 +302,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
sendOtrMessage(message);
} else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) {
sendPgpMessage(message);
} else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_AXOLOTL) {
sendAxolotlMessage(message);
} else {
sendPlainTextMessage(message);
}
@ -322,6 +324,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
case Message.ENCRYPTION_OTR:
mEditMessage.setHint(getString(R.string.send_otr_message));
break;
case Message.ENCRYPTION_AXOLOTL:
mEditMessage.setHint(getString(R.string.send_axolotl_message));
break;
case Message.ENCRYPTION_PGP:
mEditMessage.setHint(getString(R.string.send_pgp_message));
break;
@ -1114,6 +1119,14 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
builder.create().show();
}
protected void sendAxolotlMessage(final Message message) {
final ConversationActivity activity = (ConversationActivity) getActivity();
final XmppConnectionService xmppService = activity.xmppConnectionService;
//message.setCounterpart(conversation.getNextCounterpart());
xmppService.sendMessage(message);
messageSent();
}
protected void sendOtrMessage(final Message message) {
final ConversationActivity activity = (ConversationActivity) getActivity();
final XmppConnectionService xmppService = activity.xmppConnectionService;

View file

@ -1,9 +1,12 @@
package eu.siacs.conversations.ui;
import android.app.AlertDialog.Builder;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
@ -23,6 +26,8 @@ import android.widget.TableLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.util.Set;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
@ -54,9 +59,16 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
private TextView mServerInfoPep;
private TextView mSessionEst;
private TextView mOtrFingerprint;
private TextView mAxolotlFingerprint;
private TextView mAxolotlDevicelist;
private ImageView mAvatar;
private RelativeLayout mOtrFingerprintBox;
private RelativeLayout mAxolotlFingerprintBox;
private RelativeLayout mAxolotlDevicelistBox;
private ImageButton mOtrFingerprintToClipboardButton;
private ImageButton mAxolotlFingerprintToClipboardButton;
private ImageButton mWipeAxolotlPepButton;
private ImageButton mRegenerateAxolotlKeyButton;
private Jid jidToEdit;
private Account mAccount;
@ -310,6 +322,13 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint);
this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box);
this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard);
this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint);
this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box);
this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_axolotl_to_clipboard);
this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key);
this.mAxolotlDevicelist = (TextView) findViewById(R.id.axolotl_devicelist);
this.mAxolotlDevicelistBox = (RelativeLayout) findViewById(R.id.axolotl_devices_box);
this.mWipeAxolotlPepButton = (ImageButton) findViewById(R.id.action_wipe_axolotl_pep);
this.mSaveButton = (Button) findViewById(R.id.save_button);
this.mCancelButton = (Button) findViewById(R.id.cancel_button);
this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener);
@ -475,10 +494,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
} else {
this.mServerInfoPep.setText(R.string.server_info_unavailable);
}
final String fingerprint = this.mAccount.getOtrFingerprint();
if (fingerprint != null) {
final String otrFingerprint = this.mAccount.getOtrFingerprint();
if (otrFingerprint != null) {
this.mOtrFingerprintBox.setVisibility(View.VISIBLE);
this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(fingerprint));
this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
this.mOtrFingerprintToClipboardButton
.setVisibility(View.VISIBLE);
this.mOtrFingerprintToClipboardButton
@ -487,7 +506,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
@Override
public void onClick(final View v) {
if (copyTextToClipboard(fingerprint, R.string.otr_fingerprint)) {
if (copyTextToClipboard(otrFingerprint, R.string.otr_fingerprint)) {
Toast.makeText(
EditAccountActivity.this,
R.string.toast_message_otr_fingerprint,
@ -498,6 +517,55 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
} else {
this.mOtrFingerprintBox.setVisibility(View.GONE);
}
final Set<Integer> ownDevices = this.mAccount.getAxolotlService().getOwnDeviceIds();
if (ownDevices != null && !ownDevices.isEmpty()) {
this.mAxolotlDevicelistBox.setVisibility(View.VISIBLE);
this.mAxolotlDevicelist.setText(TextUtils.join(", ", ownDevices));
this.mWipeAxolotlPepButton
.setVisibility(View.VISIBLE);
this.mWipeAxolotlPepButton
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
showWipePepDialog();
}
});
} else {
this.mAxolotlDevicelistBox.setVisibility(View.GONE);
}
final String axolotlFingerprint = this.mAccount.getAxolotlService().getOwnPublicKey().getFingerprint();
if (axolotlFingerprint != null) {
this.mAxolotlFingerprintBox.setVisibility(View.VISIBLE);
this.mAxolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(axolotlFingerprint));
this.mAxolotlFingerprintToClipboardButton
.setVisibility(View.VISIBLE);
this.mAxolotlFingerprintToClipboardButton
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
if (copyTextToClipboard(axolotlFingerprint, R.string.axolotl_fingerprint)) {
Toast.makeText(
EditAccountActivity.this,
R.string.toast_message_axolotl_fingerprint,
Toast.LENGTH_SHORT).show();
}
}
});
this.mRegenerateAxolotlKeyButton
.setVisibility(View.VISIBLE);
this.mRegenerateAxolotlKeyButton
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
showRegenerateAxolotlKeyDialog();
}
});
} else {
this.mAxolotlFingerprintBox.setVisibility(View.GONE);
}
} else {
if (this.mAccount.errorStatus()) {
this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId()));
@ -508,4 +576,36 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mStats.setVisibility(View.GONE);
}
}
public void showRegenerateAxolotlKeyDialog() {
Builder builder = new Builder(this);
builder.setTitle("Regenerate Key");
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage("Are you sure you want to regenerate your Identity Key? (This will also wipe all established sessions and contact Identity Keys)");
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton("Yes",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mAccount.getAxolotlService().regenerateKeys();
}
});
builder.create().show();
}
public void showWipePepDialog() {
Builder builder = new Builder(this);
builder.setTitle("Wipe PEP");
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage("Are you sure you want to wipe all other devices from the PEP device ID list?");
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton("Yes",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mAccount.getAxolotlService().wipeOtherPepDevices();
}
});
builder.create().show();
}
}

View file

@ -3,6 +3,7 @@ package eu.siacs.conversations.ui.adapter;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.text.Spannable;
@ -26,6 +27,7 @@ import android.widget.Toast;
import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@ -154,6 +156,17 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.indicator.setVisibility(View.GONE);
} else {
viewHolder.indicator.setVisibility(View.VISIBLE);
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
AxolotlService.SQLiteAxolotlStore.Trust trust = message.getConversation()
.getAccount().getAxolotlService().getFingerprintTrust(
message.getAxolotlFingerprint());
if(trust == null || trust != AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED) {
viewHolder.indicator.setColorFilter(Color.RED);
} else {
viewHolder.indicator.clearColorFilter();
}
}
}
String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(),

View file

@ -94,11 +94,10 @@ public final class CryptoHelper {
if (fingerprint.length() < 40) {
return fingerprint;
}
StringBuilder builder = new StringBuilder(fingerprint);
builder.insert(8, " ");
builder.insert(17, " ");
builder.insert(26, " ");
builder.insert(35, " ");
StringBuilder builder = new StringBuilder(fingerprint.replaceAll("\\s",""));
for(int i=8;i<builder.length();i+=9) {
builder.insert(i, ' ');
}
return builder.toString();
}

View file

@ -21,6 +21,11 @@ public class Element {
this.name = name;
}
public Element(String name, String xmlns) {
this.name = name;
this.setAttribute("xmlns", xmlns);
}
public Element addChild(Element child) {
this.content = null;
children.add(child);

View file

@ -29,6 +29,11 @@ public class MessagePacket extends AbstractStanza {
this.children.add(0, body);
}
public void setAxolotlMessage(Element axolotlMessage) {
this.children.remove(findChild("body"));
this.children.add(0, axolotlMessage);
}
public void setType(int type) {
switch (type) {
case TYPE_CHAT:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -342,6 +342,107 @@
android:visibility="visible"
android:contentDescription="@string/copy_otr_clipboard_description"/>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/axolotl_fingerprint_box"
android:layout_marginTop="32dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/axolotl_actions"
android:orientation="vertical">
<TextView
android:id="@+id/axolotl_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody"
android:typeface="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeInfo"
android:text="@string/axolotl_fingerprint"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/axolotl_actions"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:orientation="vertical">
<ImageButton
android:id="@+id/action_copy_axolotl_to_clipboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:selectableItemBackground"
android:padding="@dimen/image_button_padding"
android:src="?attr/icon_copy"
android:visibility="visible"
android:contentDescription="@string/copy_axolotl_clipboard_description"/>
<ImageButton
android:id="@+id/action_regenerate_axolotl_key"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:selectableItemBackground"
android:padding="@dimen/image_button_padding"
android:src="?attr/icon_refresh"
android:visibility="visible"
android:contentDescription="@string/regenerate_axolotl_key"/>
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/axolotl_devices_box"
android:layout_marginTop="32dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/action_wipe_axolotl_pep"
android:orientation="vertical">
<TextView
android:id="@+id/axolotl_devicelist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody"
android:typeface="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeInfo"
android:text="@string/axolotl_devicelist"/>
</LinearLayout>
<ImageButton
android:id="@+id/action_wipe_axolotl_pep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:background="?android:selectableItemBackground"
android:padding="@dimen/image_button_padding"
android:src="?attr/icon_remove"
android:visibility="visible"
android:contentDescription="@string/wipe_axolotl_pep"/>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>

View file

@ -3,18 +3,18 @@
android:layout_width="wrap_content"
android:layout_height="match_parent" >
<LinearLayout
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/button_remove"
android:orientation="vertical"
android:padding="8dp" >
<TextView
android:id="@+id/key"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody"
android:typeface="monospace" />
@ -23,9 +23,21 @@
android:id="@+id/key_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/key"
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeInfo"/>
</LinearLayout>
<TextView
android:id="@+id/key_trust"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_below="@+id/key"
android:visibility="gone"
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeInfo"/>
</RelativeLayout>
<ImageButton
android:id="@+id/button_remove"
@ -38,4 +50,14 @@
android:src="?attr/icon_remove"
android:visibility="invisible" />
<ImageButton
android:id="@+id/button_trust"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:background="?android:selectableItemBackground"
android:padding="@dimen/image_button_padding"
android:src="?attr/icon_done"
android:visibility="invisible" />
</RelativeLayout>

View file

@ -11,6 +11,9 @@
<item
android:id="@+id/encryption_choice_pgp"
android:title="@string/encryption_choice_pgp"/>
<item
android:id="@+id/encryption_choice_axolotl"
android:title="@string/encryption_choice_axolotl"/>
</group>
</menu>

View file

@ -18,8 +18,10 @@
<item name="attr/icon_download">@drawable/ic_file_download_white_24dp</item>
<item name="attr/icon_edit">@drawable/ic_edit_white_24dp</item>
<item name="attr/icon_edit_dark">@drawable/ic_edit_grey600_24dp</item>
<item name="attr/icon_done">@drawable/ic_done_black_24dp</item>
<item name="attr/icon_group">@drawable/ic_group_white_24dp</item>
<item name="attr/icon_new">@drawable/ic_add_white_24dp</item>
<item name="attr/icon_refresh">@drawable/ic_refresh_grey600_24dp</item>
<item name="attr/icon_new_attachment">@drawable/ic_attach_file_white_24dp</item>
<item name="attr/icon_not_secure">@drawable/ic_lock_open_white_24dp</item>
<item name="attr/icon_remove">@drawable/ic_delete_grey600_24dp</item>

View file

@ -14,6 +14,7 @@
<attr name="icon_download" format="reference"/>
<attr name="icon_edit" format="reference"/>
<attr name="icon_edit_dark" format="reference"/>
<attr name="icon_done" format="reference"/>
<attr name="icon_group" format="reference"/>
<attr name="icon_new" format="reference"/>
<attr name="icon_new_attachment" format="reference"/>

View file

@ -80,6 +80,7 @@
<string name="choose_presence">Choose presence to contact</string>
<string name="send_plain_text_message">Send plain text message</string>
<string name="send_otr_message">Send OTR encrypted message</string>
<string name="send_axolotl_message">Send Axolotl encrypted message</string>
<string name="send_pgp_message">Send OpenPGP encrypted message</string>
<string name="your_nick_has_been_changed">Your nickname has been changed</string>
<string name="send_unencrypted">Send unencrypted</string>
@ -154,6 +155,7 @@
<string name="encryption_choice_none">Plain text</string>
<string name="encryption_choice_otr">OTR</string>
<string name="encryption_choice_pgp">OpenPGP</string>
<string name="encryption_choice_axolotl">Axolotl</string>
<string name="mgmt_account_edit">Edit account</string>
<string name="mgmt_account_delete">Delete account</string>
<string name="mgmt_account_disable">Temporarily disable</string>
@ -206,6 +208,8 @@
<string name="reception_failed">Reception failed</string>
<string name="your_fingerprint">Your fingerprint</string>
<string name="otr_fingerprint">OTR fingerprint</string>
<string name="axolotl_fingerprint">Axolotl fingerprint</string>
<string name="axolotl_devicelist">Other own Axolotl Devices</string>
<string name="verify">Verify</string>
<string name="decrypt">Decrypt</string>
<string name="conferences">Conferences</string>
@ -312,6 +316,7 @@
<string name="pref_conference_name">Conference name</string>
<string name="pref_conference_name_summary">Use rooms subject instead of JID to identify conferences</string>
<string name="toast_message_otr_fingerprint">OTR fingerprint copied to clipboard!</string>
<string name="toast_message_axolotl_fingerprint">Axolotl fingerprint copied to clipboard!</string>
<string name="conference_banned">You are banned from this conference</string>
<string name="conference_members_only">This conference is members only</string>
<string name="conference_kicked">You have been kicked from this conference</string>
@ -379,6 +384,9 @@
<string name="reset">Reset</string>
<string name="account_image_description">Account avatar</string>
<string name="copy_otr_clipboard_description">Copy OTR fingerprint to clipboard</string>
<string name="copy_axolotl_clipboard_description">Copy Axolotl fingerprint to clipboard</string>
<string name="regenerate_axolotl_key">Copy Axolotl fingerprint to clipboard</string>
<string name="wipe_axolotl_pep">Wipe other devices from PEP</string>
<string name="fetching_history_from_server">Fetching history from server</string>
<string name="no_more_history_on_server">No more history on server</string>
<string name="updating">Updating…</string>

View file

@ -18,6 +18,7 @@
<item name="attr/icon_download">@drawable/ic_action_download</item>
<item name="attr/icon_edit">@drawable/ic_action_edit</item>
<item name="attr/icon_edit_dark">@drawable/ic_action_edit_dark</item>
<item name="attr/icon_done">@drawable/ic_action_done</item>
<item name="attr/icon_group">@drawable/ic_action_group</item>
<item name="attr/icon_new">@drawable/ic_action_new</item>