Added PEP and message protocol layers
Can now fetch/retrieve from PEP, as well as encode/decode messages
This commit is contained in:
parent
f73aa1a200
commit
77619b55e4
|
@ -41,16 +41,26 @@ import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Contact;
|
import eu.siacs.conversations.entities.Contact;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
|
import eu.siacs.conversations.parser.IqParser;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
|
||||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||||
|
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||||
|
|
||||||
public class AxolotlService {
|
public class AxolotlService {
|
||||||
|
|
||||||
|
public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
|
||||||
|
public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
|
||||||
|
public static final String PEP_PREKEYS = PEP_PREFIX + ".prekeys";
|
||||||
|
public static final String PEP_BUNDLE = PEP_PREFIX + ".bundle";
|
||||||
|
|
||||||
private final Account account;
|
private final Account account;
|
||||||
private final XmppConnectionService mXmppConnectionService;
|
private final XmppConnectionService mXmppConnectionService;
|
||||||
private final SQLiteAxolotlStore axolotlStore;
|
private final SQLiteAxolotlStore axolotlStore;
|
||||||
private final SessionMap sessions;
|
private final SessionMap sessions;
|
||||||
|
private final BundleMap bundleCache;
|
||||||
private int ownDeviceId;
|
private int ownDeviceId;
|
||||||
|
|
||||||
public static class SQLiteAxolotlStore implements AxolotlStore {
|
public static class SQLiteAxolotlStore implements AxolotlStore {
|
||||||
|
@ -571,6 +581,8 @@ public class AxolotlService {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class BundleMap extends AxolotlAddressMap<PreKeyBundle> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public AxolotlService(Account account, XmppConnectionService connectionService) {
|
public AxolotlService(Account account, XmppConnectionService connectionService) {
|
||||||
|
@ -578,6 +590,7 @@ public class AxolotlService {
|
||||||
this.account = account;
|
this.account = account;
|
||||||
this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
|
this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
|
||||||
this.sessions = new SessionMap(axolotlStore, account);
|
this.sessions = new SessionMap(axolotlStore, account);
|
||||||
|
this.bundleCache = new BundleMap();
|
||||||
this.ownDeviceId = axolotlStore.getLocalRegistrationId();
|
this.ownDeviceId = axolotlStore.getLocalRegistrationId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -618,7 +631,202 @@ public class AxolotlService {
|
||||||
return ownDeviceId;
|
return ownDeviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void fetchBundleIfNeeded(final Contact contact, final Integer deviceId) {
|
||||||
|
final AxolotlAddress address = new AxolotlAddress(contact.getJid().toString(), deviceId);
|
||||||
|
if (sessions.get(address) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (bundleCache) {
|
||||||
|
PreKeyBundle bundle = bundleCache.get(address);
|
||||||
|
if (bundle == null) {
|
||||||
|
bundle = new PreKeyBundle(0, deviceId, 0, null, 0, null, null, null);
|
||||||
|
bundleCache.put(address, bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(bundle.getPreKey() == null) {
|
||||||
|
Log.d(Config.LOGTAG, "No preKey in cache, fetching...");
|
||||||
|
IqPacket prekeysPacket = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(contact.getJid(), deviceId);
|
||||||
|
mXmppConnectionService.sendIqPacket(account, prekeysPacket, new OnIqPacketReceived() {
|
||||||
|
@Override
|
||||||
|
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||||
|
synchronized (bundleCache) {
|
||||||
|
Log.d(Config.LOGTAG, "Received preKey IQ packet, processing...");
|
||||||
|
final IqParser parser = mXmppConnectionService.getIqParser();
|
||||||
|
final PreKeyBundle bundle = bundleCache.get(address);
|
||||||
|
final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
|
||||||
|
if (preKeyBundleList.isEmpty()) {
|
||||||
|
Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Random random = new Random();
|
||||||
|
final PreKeyBundle newBundle = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
|
||||||
|
if (bundle == null || newBundle == null) {
|
||||||
|
//should never happen
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(),
|
||||||
|
bundle.getDeviceId(), newBundle.getPreKeyId(), newBundle.getPreKey(),
|
||||||
|
bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
|
||||||
|
bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
|
||||||
|
|
||||||
|
bundleCache.put(address, mergedBundle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(bundle.getIdentityKey() == null) {
|
||||||
|
Log.d(Config.LOGTAG, "No bundle in cache, fetching...");
|
||||||
|
IqPacket bundlePacket = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(contact.getJid(), deviceId);
|
||||||
|
mXmppConnectionService.sendIqPacket(account, bundlePacket, new OnIqPacketReceived() {
|
||||||
|
@Override
|
||||||
|
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||||
|
synchronized (bundleCache) {
|
||||||
|
Log.d(Config.LOGTAG, "Received bundle IQ packet, processing...");
|
||||||
|
final IqParser parser = mXmppConnectionService.getIqParser();
|
||||||
|
final PreKeyBundle bundle = bundleCache.get(address);
|
||||||
|
final PreKeyBundle newBundle = parser.bundle(packet);
|
||||||
|
if( bundle == null || newBundle == null ) {
|
||||||
|
Log.d(Config.LOGTAG, "bundle IQ packet invalid: " + packet);
|
||||||
|
//should never happen
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(),
|
||||||
|
bundle.getDeviceId(), bundle.getPreKeyId(), bundle.getPreKey(),
|
||||||
|
newBundle.getSignedPreKeyId(), newBundle.getSignedPreKey(),
|
||||||
|
newBundle.getSignedPreKeySignature(), newBundle.getIdentityKey());
|
||||||
|
|
||||||
|
axolotlStore.saveIdentity(contact.getJid().toBareJid().toString(), newBundle.getIdentityKey());
|
||||||
|
bundleCache.put(address, mergedBundle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishOwnDeviceIdIfNeeded() {
|
||||||
|
IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid());
|
||||||
|
mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
|
||||||
|
@Override
|
||||||
|
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||||
|
Element item = mXmppConnectionService.getIqParser().getItem(packet);
|
||||||
|
List<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
|
||||||
|
if(deviceIds == null) {
|
||||||
|
deviceIds = new ArrayList<>();
|
||||||
|
}
|
||||||
|
if(!deviceIds.contains(getOwnDeviceId())) {
|
||||||
|
Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing...");
|
||||||
|
deviceIds.add(getOwnDeviceId());
|
||||||
|
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
|
||||||
|
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
|
||||||
|
@Override
|
||||||
|
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||||
|
// TODO: implement this!
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishBundleIfNeeded() {
|
||||||
|
IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(account.getJid().toBareJid(), ownDeviceId);
|
||||||
|
mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
|
||||||
|
@Override
|
||||||
|
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||||
|
PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
|
||||||
|
if(bundle == null) {
|
||||||
|
Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing...");
|
||||||
|
int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
|
||||||
|
try {
|
||||||
|
SignedPreKeyRecord signedPreKeyRecord = KeyHelper.generateSignedPreKey(
|
||||||
|
axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1);
|
||||||
|
axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
|
||||||
|
IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundle(
|
||||||
|
signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
|
||||||
|
ownDeviceId);
|
||||||
|
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
|
||||||
|
@Override
|
||||||
|
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||||
|
// TODO: implement this!
|
||||||
|
Log.d(Config.LOGTAG, "Published bundle, got: " + packet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishPreKeysIfNeeded() {
|
||||||
|
IqPacket packet = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(account.getJid().toBareJid(), ownDeviceId);
|
||||||
|
mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
|
||||||
|
@Override
|
||||||
|
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||||
|
Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
|
||||||
|
if(keys == null || keys.isEmpty()) {
|
||||||
|
Log.d(Config.LOGTAG, "Prekeys " + getOwnDeviceId() + " not in PEP. Publishing...");
|
||||||
|
List<PreKeyRecord> preKeyRecords = KeyHelper.generatePreKeys(
|
||||||
|
axolotlStore.getCurrentPreKeyId(), 100);
|
||||||
|
for(PreKeyRecord record : preKeyRecords) {
|
||||||
|
axolotlStore.storePreKey(record.getId(), record);
|
||||||
|
}
|
||||||
|
IqPacket publish = mXmppConnectionService.getIqGenerator().publishPreKeys(
|
||||||
|
preKeyRecords, ownDeviceId);
|
||||||
|
|
||||||
|
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
|
||||||
|
@Override
|
||||||
|
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||||
|
Log.d(Config.LOGTAG, "Published prekeys, got: " + packet);
|
||||||
|
// TODO: implement this!
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isContactAxolotlCapable(Contact contact) {
|
||||||
|
AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0);
|
||||||
|
return sessions.hasAny(address) || bundleCache.hasAny(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initiateSynchronousSession(Contact contact) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException {
|
private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException {
|
||||||
|
Log.d(Config.LOGTAG, "Creating axolotl sessions if needed...");
|
||||||
|
AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0);
|
||||||
|
for(Integer deviceId: bundleCache.getAll(address).keySet()) {
|
||||||
|
Log.d(Config.LOGTAG, "Processing device ID: " + deviceId);
|
||||||
|
AxolotlAddress remoteAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), deviceId);
|
||||||
|
if(sessions.get(remoteAddress) == null) {
|
||||||
|
Log.d(Config.LOGTAG, "Building new sesstion for " + deviceId);
|
||||||
|
SessionBuilder builder = new SessionBuilder(this.axolotlStore, remoteAddress);
|
||||||
|
try {
|
||||||
|
builder.process(bundleCache.get(remoteAddress));
|
||||||
|
XmppAxolotlSession session = new XmppAxolotlSession(this.axolotlStore, remoteAddress);
|
||||||
|
sessions.put(remoteAddress, session);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": InvalidKeyException, " +e.getMessage());
|
||||||
|
} catch (UntrustedIdentityException e) {
|
||||||
|
Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": UntrustedIdentityException, " +e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(Config.LOGTAG, "Already have session for " + deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!this.hasAny(contact)) {
|
||||||
|
Log.e(Config.LOGTAG, "No Axolotl sessions available!");
|
||||||
|
throw new NoSessionsCreatedException(); // FIXME: proper error handling
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException {
|
public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.utils.PhoneHelper;
|
import eu.siacs.conversations.utils.PhoneHelper;
|
||||||
|
|
||||||
|
@ -28,7 +29,8 @@ public abstract class AbstractGenerator {
|
||||||
"urn:xmpp:avatar:metadata+notify",
|
"urn:xmpp:avatar:metadata+notify",
|
||||||
"urn:xmpp:ping",
|
"urn:xmpp:ping",
|
||||||
"jabber:iq:version",
|
"jabber:iq:version",
|
||||||
"http://jabber.org/protocol/chatstates"};
|
"http://jabber.org/protocol/chatstates",
|
||||||
|
AxolotlService.PEP_DEVICE_LIST+"+notify"};
|
||||||
private final String[] MESSAGE_CONFIRMATION_FEATURES = {
|
private final String[] MESSAGE_CONFIRMATION_FEATURES = {
|
||||||
"urn:xmpp:chat-markers:0",
|
"urn:xmpp:chat-markers:0",
|
||||||
"urn:xmpp:receipts"
|
"urn:xmpp:receipts"
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
package eu.siacs.conversations.generator;
|
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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.entities.DownloadableFile;
|
import eu.siacs.conversations.entities.DownloadableFile;
|
||||||
|
@ -115,6 +123,68 @@ public class IqGenerator extends AbstractGenerator {
|
||||||
return packet;
|
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 retrieveBundleForDevice(final Jid to, final int deviceid) {
|
||||||
|
final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLE+":"+deviceid, null);
|
||||||
|
if(to != null) {
|
||||||
|
packet.setTo(to);
|
||||||
|
}
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IqPacket retrievePreKeysForDevice(final Jid to, final int deviceId) {
|
||||||
|
final IqPacket packet = retrieve(AxolotlService.PEP_PREKEYS+":"+deviceId, null);
|
||||||
|
if(to != null) {
|
||||||
|
packet.setTo(to);
|
||||||
|
}
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IqPacket publishDeviceIds(final List<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 publishBundle(final SignedPreKeyRecord signedPreKeyRecord, IdentityKey identityKey, 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));
|
||||||
|
|
||||||
|
return publish(AxolotlService.PEP_BUNDLE+":"+deviceId, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IqPacket publishPreKeys(final List<PreKeyRecord> prekeyList, final int deviceId) {
|
||||||
|
final Element item = new Element("item");
|
||||||
|
final Element prekeys = item.addChild("prekeys", AxolotlService.PEP_PREFIX);
|
||||||
|
for(PreKeyRecord preKeyRecord:prekeyList) {
|
||||||
|
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_PREKEYS+":"+deviceId, item);
|
||||||
|
}
|
||||||
|
|
||||||
public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
|
public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
|
||||||
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||||
final Element query = packet.query("urn:xmpp:mam:0");
|
final Element query = packet.query("urn:xmpp:mam:0");
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package eu.siacs.conversations.generator;
|
package eu.siacs.conversations.generator;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -7,6 +9,11 @@ import java.util.TimeZone;
|
||||||
|
|
||||||
import net.java.otr4j.OtrException;
|
import net.java.otr4j.OtrException;
|
||||||
import net.java.otr4j.session.Session;
|
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.Account;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
|
@ -59,6 +66,20 @@ public class MessageGenerator extends AbstractGenerator {
|
||||||
delay.setAttribute("stamp", mDateFormat.format(date));
|
delay.setAttribute("stamp", mDateFormat.format(date));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MessagePacket generateAxolotlChat(Message message) throws NoSessionsCreatedException{
|
||||||
|
return generateAxolotlChat(message, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessagePacket generateAxolotlChat(Message message, boolean addDelay) throws NoSessionsCreatedException{
|
||||||
|
MessagePacket packet = preparePacket(message, addDelay);
|
||||||
|
AxolotlService service = message.getConversation().getAccount().getAxolotlService();
|
||||||
|
Log.d(Config.LOGTAG, "Submitting message to axolotl service for send processing...");
|
||||||
|
XmppAxolotlMessage axolotlMessage = service.processSending(message.getContact(),
|
||||||
|
message.getBody());
|
||||||
|
packet.setAxolotlMessage(axolotlMessage.toXml());
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
public MessagePacket generateOtrChat(Message message) {
|
public MessagePacket generateOtrChat(Message message) {
|
||||||
return generateOtrChat(message, false);
|
return generateOtrChat(message, false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
package eu.siacs.conversations.parser;
|
package eu.siacs.conversations.parser;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
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.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
@ -71,6 +81,152 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
||||||
return super.avatarData(items);
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Integer> deviceIds(final Element item) {
|
||||||
|
List<Integer> deviceIds = new ArrayList<>();
|
||||||
|
if (item == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final Element list = item.findChild("list");
|
||||||
|
if(list == null) {
|
||||||
|
return 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, "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, "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,"Invalid identityKey in PEP: "+e.getMessage());
|
||||||
|
}
|
||||||
|
return identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
|
||||||
|
Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
|
||||||
|
Element prekeysItem = getItem(packet);
|
||||||
|
if (prekeysItem == null) {
|
||||||
|
Log.d(Config.LOGTAG, "Couldn't find <item> in preKeyPublic IQ packet: " + packet);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final Element prekeysElement = prekeysItem.findChild("prekeys");
|
||||||
|
if(prekeysElement == null) {
|
||||||
|
Log.d(Config.LOGTAG, "Couldn't find <prekeys> in preKeyPublic IQ packet: " + packet);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for(Element preKeyPublicElement : prekeysElement.getChildren()) {
|
||||||
|
if(!preKeyPublicElement.getName().equals("preKeyPublic")){
|
||||||
|
Log.d(Config.LOGTAG, "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, "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
|
@Override
|
||||||
public void onIqPacketReceived(final Account account, final IqPacket packet) {
|
public void onIqPacketReceived(final Account account, final IqPacket packet) {
|
||||||
if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {
|
if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {
|
||||||
|
|
|
@ -6,7 +6,11 @@ import android.util.Pair;
|
||||||
import net.java.otr4j.session.Session;
|
import net.java.otr4j.session.Session;
|
||||||
import net.java.otr4j.session.SessionStatus;
|
import net.java.otr4j.session.SessionStatus;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
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.Account;
|
||||||
import eu.siacs.conversations.entities.Contact;
|
import eu.siacs.conversations.entities.Contact;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
|
@ -94,6 +98,18 @@ public class MessageParser extends AbstractParser implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation) {
|
||||||
|
Message finishedMessage = null;
|
||||||
|
AxolotlService service = conversation.getAccount().getAxolotlService();
|
||||||
|
XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(conversation.getContact(), axolotlMessage);
|
||||||
|
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage);
|
||||||
|
if(plaintextMessage != null) {
|
||||||
|
finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, Message.STATUS_RECEIVED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return finishedMessage;
|
||||||
|
}
|
||||||
|
|
||||||
private class Invite {
|
private class Invite {
|
||||||
Jid jid;
|
Jid jid;
|
||||||
String password;
|
String password;
|
||||||
|
@ -170,6 +186,18 @@ public class MessageParser extends AbstractParser implements
|
||||||
mXmppConnectionService.updateConversationUi();
|
mXmppConnectionService.updateConversationUi();
|
||||||
mXmppConnectionService.updateAccountUi();
|
mXmppConnectionService.updateAccountUi();
|
||||||
}
|
}
|
||||||
|
} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
|
||||||
|
Log.d(Config.LOGTAG, "Received PEP device list update from "+ from + ", processing...");
|
||||||
|
Element item = items.findChild("item");
|
||||||
|
List<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
|
||||||
|
AxolotlService axolotlService = account.getAxolotlService();
|
||||||
|
if(account.getJid().toBareJid().equals(from)) {
|
||||||
|
} else {
|
||||||
|
Contact contact = account.getRoster().getContact(from);
|
||||||
|
for (Integer deviceId : deviceIds) {
|
||||||
|
axolotlService.fetchBundleIfNeeded(contact, deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,8 +260,9 @@ public class MessageParser extends AbstractParser implements
|
||||||
timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis());
|
timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
final String body = packet.getBody();
|
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 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;
|
int status;
|
||||||
final Jid counterpart;
|
final Jid counterpart;
|
||||||
final Jid to = packet.getTo();
|
final Jid to = packet.getTo();
|
||||||
|
@ -261,11 +290,11 @@ public class MessageParser extends AbstractParser implements
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extractChatState(mXmppConnectionService.find(account,from), packet)) {
|
if (extractChatState(mXmppConnectionService.find(account, from), packet)) {
|
||||||
mXmppConnectionService.updateConversationUi();
|
mXmppConnectionService.updateConversationUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((body != null || encrypted != null) && !isMucStatusMessage) {
|
if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) {
|
||||||
Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat);
|
Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat);
|
||||||
if (isTypeGroupChat) {
|
if (isTypeGroupChat) {
|
||||||
if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) {
|
if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) {
|
||||||
|
@ -294,8 +323,13 @@ public class MessageParser extends AbstractParser implements
|
||||||
} else {
|
} else {
|
||||||
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
|
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
|
||||||
}
|
}
|
||||||
} else if (encrypted != null) {
|
} else if (pgpEncrypted != null) {
|
||||||
message = new Message(conversation, encrypted, Message.ENCRYPTION_PGP, status);
|
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
|
||||||
|
} else if (axolotlEncrypted != null) {
|
||||||
|
message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation);
|
||||||
|
if (message == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
|
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,11 @@ public class Element {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Element(String name, String xmlns) {
|
||||||
|
this.name = name;
|
||||||
|
this.setAttribute("xmlns", xmlns);
|
||||||
|
}
|
||||||
|
|
||||||
public Element addChild(Element child) {
|
public Element addChild(Element child) {
|
||||||
this.content = null;
|
this.content = null;
|
||||||
children.add(child);
|
children.add(child);
|
||||||
|
|
|
@ -29,6 +29,11 @@ public class MessagePacket extends AbstractStanza {
|
||||||
this.children.add(0, body);
|
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) {
|
public void setType(int type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TYPE_CHAT:
|
case TYPE_CHAT:
|
||||||
|
|
Loading…
Reference in New Issue