Compare commits

...

37 Commits

Author SHA1 Message Date
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
27 changed files with 2429 additions and 111 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,6 +188,7 @@ public class Contact implements ListItem, Blockable {
}
public ContentValues getContentValues() {
synchronized (this.keys) {
final ContentValues values = new ContentValues();
values.put(ACCOUNT, accountUuid);
values.put(SYSTEMNAME, systemName);
@ -198,6 +204,7 @@ public class Contact implements ListItem, Blockable {
values.put(GROUPS, groups.toString());
return values;
}
}
public int getSubscription() {
return this.subscription;
@ -281,6 +288,7 @@ public class Contact implements ListItem, Blockable {
}
public ArrayList<String> getOtrFingerprints() {
synchronized (this.keys) {
final ArrayList<String> fingerprints = new ArrayList<String>();
try {
if (this.keys.has("otr_fingerprints")) {
@ -297,8 +305,9 @@ public class Contact implements ListItem, Blockable {
}
return fingerprints;
}
}
public boolean addOtrFingerprint(String print) {
synchronized (this.keys) {
if (getOtrFingerprints().contains(print)) {
return false;
}
@ -306,7 +315,6 @@ public class Contact implements ListItem, Blockable {
JSONArray fingerprints;
if (!this.keys.has("otr_fingerprints")) {
fingerprints = new JSONArray();
} else {
fingerprints = this.keys.getJSONArray("otr_fingerprints");
}
@ -317,8 +325,10 @@ public class Contact implements ListItem, Blockable {
return false;
}
}
}
public long getPgpKeyId() {
synchronized (this.keys) {
if (this.keys.has("pgp_keyid")) {
try {
return this.keys.getLong("pgp_keyid");
@ -329,12 +339,14 @@ public class Contact implements ListItem, Blockable {
return 0;
}
}
}
public void setPgpKeyId(long keyId) {
synchronized (this.keys) {
try {
this.keys.put("pgp_keyid", keyId);
} catch (final JSONException ignored) {
}
}
}
@ -441,6 +453,7 @@ public class Contact implements ListItem, Blockable {
}
public boolean deleteOtrFingerprint(String fingerprint) {
synchronized (this.keys) {
boolean success = false;
try {
if (this.keys.has("otr_fingerprints")) {
@ -461,6 +474,7 @@ public class Contact implements ListItem, Blockable {
return false;
}
}
}
public boolean trusted() {
return getOption(Options.FROM) && getOption(Options.TO);

View File

@ -179,11 +179,11 @@ 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;
}
@ -653,4 +661,8 @@ public class Message extends AbstractEntity {
public int width = 0;
public int height = 0;
}
public void setAxolotlFingerprint(String fingerprint) {
this.axolotlFingerprint = fingerprint;
}
}

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, "
@ -39,6 +53,56 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ ") 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 PRIMARY KEY ON CONFLICT IGNORE, "
+ AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE "
+");";
private DatabaseBackend(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@ -69,12 +133,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
@ -215,6 +284,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 +385,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 +555,367 @@ 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 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());
selectionArgs.add(name);
selectionArgs.add(own?"1":"0");
String selectionString = AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.OWN + " = ? ";
if (fingerprint != null){
selectionArgs.add(fingerprint);
selectionString += "AND " +AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? ";
}
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) {
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);
db.insert(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
}
public AxolotlService.SQLiteAxolotlStore.Trust isIdentityKeyTrusted(Account account, String name, String fingerprint) {
Cursor cursor = getIdentityKeyCursor(account, name, false, 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 name, String fingerprint, AxolotlService.SQLiteAxolotlStore.Trust trust) {
SQLiteDatabase db = this.getWritableDatabase();
String[] selectionArgs = {
account.getUuid(),
name,
fingerprint
};
ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trust.ordinal());
int rows = db.update(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ? "
+ 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));
}
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,14 @@ 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);
}
break;
}
if (packet != null) {
if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
@ -943,6 +952,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 +1767,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,6 +29,7 @@ import android.widget.QuickContactBadge;
import android.widget.TextView;
import org.openintents.openpgp.util.OpenPgpUtils;
import org.whispersystems.libaxolotl.IdentityKey;
import java.util.List;
@ -376,6 +377,29 @@ 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 remove = (ImageButton) view
.findViewById(R.id.button_remove);
remove.setVisibility(View.VISIBLE);
keyTrust.setVisibility(View.VISIBLE);
keyType.setText("Axolotl Fingerprint");
key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint()));
keyTrust.setText(contact.getAccount().getAxolotlService().getFingerprintTrust(contact.getJid().toBareJid().toString(), identityKey.getFingerprint().replaceAll("\\s","")).toString());
keys.addView(view);
remove.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//confirmToDeleteFingerprint(otrFingerprint);
}
});
}
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,6 +35,7 @@ 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.entities.Account;
import eu.siacs.conversations.entities.Blockable;
@ -752,6 +754,17 @@ public class ConversationActivity extends XmppActivity
showInstallPgpDialog();
}
break;
case R.id.encryption_choice_axolotl:
Log.d(Config.LOGTAG, "Trying to enable axolotl...");
if(conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) {
Log.d(Config.LOGTAG, "Enabled axolotl for Contact " + conversation.getContact().getJid() );
conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL);
item.setChecked(true);
} else {
Log.d(Config.LOGTAG, "Contact " + conversation.getContact().getJid() + " not axolotl capable!");
showAxolotlNoSessionsDialog();
}
break;
default:
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
break;
@ -784,6 +797,11 @@ public class ConversationActivity extends XmppActivity
popup.getMenu().findItem(R.id.encryption_choice_pgp)
.setChecked(true);
break;
case Message.ENCRYPTION_AXOLOTL:
Log.d(Config.LOGTAG, "Axolotl confirmed. Setting menu item checked!");
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,13 @@
package eu.siacs.conversations.ui;
import android.app.AlertDialog;
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 +27,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 +60,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 +323,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_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 +495,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 +507,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 +518,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 +577,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

@ -266,6 +266,29 @@ public abstract class XmppActivity extends Activity {
builder.create().show();
}
public void showAxolotlNoSessionsDialog() {
Builder builder = new AlertDialog.Builder(this);
builder.setTitle("No Sessions");
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage("Your contact is not Axolotl-capable!");
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setNeutralButton("Foo",
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
builder.setPositiveButton("Bar",
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
builder.create().show();
}
abstract void onBackendConnected();
protected void registerListeners() {

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:

View File

@ -341,6 +341,107 @@
android:src="?attr/icon_copy"
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>

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"

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

@ -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>