copy commits

This commit is contained in:
Christian S 2015-08-02 08:54:53 +02:00
commit 2d1abca302
58 changed files with 1915 additions and 1395 deletions

View file

@ -1,8 +1,13 @@
###Changelog ###Changelog
####Version 1.6.0
* new multi-end-to-multi-end encryption method
* show unexpected encryption changes as red chat bubbles
* always notify in private/non-anonymous conferences
####Version 1.5.2 ####Version 1.5.2
* added new message bubbles * added new message bubbles
* added subtitles to chatviews in ActionBar to display typing info in single chats and participants names in conferences * added subtitles to chatviews in ActionBar to display typing info in single chats and participant names in conferences
* some bug fixes * some bug fixes
####Version 1.5.1 ####Version 1.5.1

View file

@ -39,24 +39,24 @@ support these extensions; therefore to get the most out of Conversations you
should consider either switching to an XMPP server that does or — even better — should consider either switching to an XMPP server that does or — even better —
run your own XMPP server for you and your friends. These XEP's are: run your own XMPP server for you and your friends. These XEP's are:
* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Will be used to transfer * [XEP-0065: SOCKS5 Bytestreams](http://xmpp.org/extensions/xep-0065.html) (or mod_proxy65). Will be used to transfer
files if both parties are behind a firewall (NAT). files if both parties are behind a firewall (NAT).
* XEP-0163: Personal Eventing Protocol for avatars * [XEP-0163: Personal Eventing Protocol](http://xmpp.org/extensions/xep-0163.html) for avatars
* XEP-0191: Blocking command lets you blacklist spammers or block contacts * [XEP-0191: Blocking command](http://xmpp.org/extensions/xep-0191.html) lets you blacklist spammers or block contacts
without removing them from your roster. without removing them from your roster.
* XEP-0198: Stream Management allows XMPP to survive small network outages and * [XEP-0198: Stream Management](http://xmpp.org/extensions/xep-0198.html) allows XMPP to survive small network outages and
changes of the underlying TCP connection. changes of the underlying TCP connection.
* XEP-0280: Message Carbons which automatically syncs the messages you send to * [XEP-0280: Message Carbons](http://xmpp.org/extensions/xep-0280.html) which automatically syncs the messages you send to
your desktop client and thus allows you to switch seamlessly from your mobile your desktop client and thus allows you to switch seamlessly from your mobile
client to your desktop client and back within one conversation. client to your desktop client and back within one conversation.
* XEP-0237: Roster Versioning mainly to save bandwidth on poor mobile connections * [XEP-0237: Roster Versioning](http://xmpp.org/extensions/xep-0237.html) mainly to save bandwidth on poor mobile connections
* XEP-0313: Message Archive Management synchronize message history with the * [XEP-0313: Message Archive Management](http://xmpp.org/extensions/xep-0313.html) synchronize message history with the
server. Catch up with messages that were sent while Conversations was server. Catch up with messages that were sent while Conversations was
offline. offline.
* XEP-0352: Client State Indication lets the server know whether or not * [XEP-0352: Client State Indication](http://xmpp.org/extensions/xep-0352.html) lets the server know whether or not
Conversations is in the background. Allows the server to save bandwidth by Conversations is in the background. Allows the server to save bandwidth by
withholding unimportant packages. withholding unimportant packages.
* XEP-xxxx: HttpUpload allows you to share files in conferences and with offline * [XEP-xxxx: HTTP File Upload](http://xmpp.org/extensions/inbox/http-upload.html) allows you to share files in conferences and with offline
contacts. Requires an [additional component](https://github.com/siacs/HttpUploadComponent) contacts. Requires an [additional component](https://github.com/siacs/HttpUploadComponent)
on your server. on your server.

View file

@ -0,0 +1,165 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="36"
height="26"
id="svg2"
version="1.1"
inkscape:version="0.48.5 r10040"
sodipodi:docname="message_bubble_received.svg">
<defs
id="defs4">
<filter
x="-0.25"
y="-0.25"
width="1.5"
height="1.5"
inkscape:label="Drop Shadow"
id="filter3811"
color-interpolation-filters="sRGB">
<feFlood
flood-opacity="0.25"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood3813" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite3815" />
<feGaussianBlur
stdDeviation="0.5"
result="blur"
id="feGaussianBlur3817" />
<feOffset
dx="0"
dy="1"
result="offset"
id="feOffset3819" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite3821" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="25.745257"
inkscape:cy="9.618802"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="989"
inkscape:window-height="755"
inkscape:window-x="22"
inkscape:window-y="16"
inkscape:window-maximized="0"
showguides="true"
inkscape:guide-bbox="true"
guidecolor="#000000"
guideopacity="0.49803922">
<inkscape:grid
type="xygrid"
id="grid2985"
empspacing="4"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="1px"
spacingy="1px"
originx="0px"
originy="0px"
color="#0000ff"
opacity="0.03137255" />
<sodipodi:guide
orientation="1,0"
position="20,26"
id="guide3060" />
<sodipodi:guide
orientation="1,0"
position="24,26"
id="guide3062" />
<sodipodi:guide
orientation="0,1"
position="36,22"
id="guide3064" />
<sodipodi:guide
orientation="0,1"
position="36,6"
id="guide3066" />
<sodipodi:guide
orientation="1,0"
position="26,0"
id="guide3068" />
<sodipodi:guide
orientation="1,0"
position="18,0"
id="guide3070" />
<sodipodi:guide
orientation="0,1"
position="0,10"
id="guide3074" />
<sodipodi:guide
orientation="0,1"
position="0,8"
id="guide3076" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer"
inkscape:groupmode="layer"
id="layer"
transform="translate(0,-2)">
<g
id="g3759"
style="fill:#c64545;fill-opacity:1;stroke:none;fill-rule:nonzero;filter:url(#filter3811)">
<path
style="display:none"
d="m 8,6 c 2,2 4,6 4,10 L 16,6 z"
id="path3805"
inkscape:connector-curvature="0"
transform="translate(0,2)"
sodipodi:nodetypes="cccc" />
<path
inkscape:connector-curvature="0"
id="path2989"
d="M 4,4 16,16 16,4 z"
sodipodi:nodetypes="cccc" />
<rect
ry="2"
y="4"
x="12"
height="20"
width="20"
id="rect2987" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -44,6 +44,7 @@ images = {
'md_switch_thumb_on_normal.svg' => ['switch_thumb_on_normal', 48], 'md_switch_thumb_on_normal.svg' => ['switch_thumb_on_normal', 48],
'md_switch_thumb_on_pressed.svg' => ['switch_thumb_on_pressed', 48], 'md_switch_thumb_on_pressed.svg' => ['switch_thumb_on_pressed', 48],
'message_bubble_received.svg' => ['message_bubble_received.9', 0], 'message_bubble_received.svg' => ['message_bubble_received.9', 0],
'message_bubble_received_warning.svg' => ['message_bubble_received_warning.9', 0],
'message_bubble_sent.svg' => ['message_bubble_sent.9', 0], 'message_bubble_sent.svg' => ['message_bubble_sent.9', 0],
} }

View file

@ -48,8 +48,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion 14
targetSdkVersion 21 targetSdkVersion 21
versionCode 80 versionCode 81
versionName "1.5.2" versionName "1.6.0-beta"
} }
compileOptions { compileOptions {

View file

@ -6,28 +6,15 @@ import android.util.Log;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.whispersystems.libaxolotl.AxolotlAddress; import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair; import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException; import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.SessionBuilder; import org.whispersystems.libaxolotl.SessionBuilder;
import org.whispersystems.libaxolotl.SessionCipher;
import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.ecc.ECPublicKey; import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.libaxolotl.state.PreKeyBundle; import org.whispersystems.libaxolotl.state.PreKeyBundle;
import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.util.KeyHelper; import org.whispersystems.libaxolotl.util.KeyHelper;
@ -73,547 +60,6 @@ public class AxolotlService {
private final FetchStatusMap fetchStatusMap; private final FetchStatusMap fetchStatusMap;
private final SerialSingleThreadExecutor executor; private final SerialSingleThreadExecutor executor;
public static class SQLiteAxolotlStore implements AxolotlStore {
public static final String PREKEY_TABLENAME = "prekeys";
public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys";
public static final String SESSION_TABLENAME = "sessions";
public static final String IDENTITIES_TABLENAME = "identities";
public static final String ACCOUNT = "account";
public static final String DEVICE_ID = "device_id";
public static final String ID = "id";
public static final String KEY = "key";
public static final String FINGERPRINT = "fingerprint";
public static final String NAME = "name";
public static final String TRUSTED = "trusted";
public static final String OWN = "ownkey";
public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id";
public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id";
private final Account account;
private final XmppConnectionService mXmppConnectionService;
private IdentityKeyPair identityKeyPair;
private int localRegistrationId;
private int currentPreKeyId = 0;
public enum Trust {
UNDECIDED(0),
TRUSTED(1),
UNTRUSTED(2),
COMPROMISED(3),
INACTIVE(4);
private static final Map<Integer, Trust> trustsByValue = new HashMap<>();
static {
for (Trust trust : Trust.values()) {
trustsByValue.put(trust.getCode(), trust);
}
}
private final int code;
Trust(int code){
this.code = code;
}
public int getCode() {
return this.code;
}
public String toString() {
switch(this){
case UNDECIDED:
return "Trust undecided "+getCode();
case TRUSTED:
return "Trusted "+getCode();
case COMPROMISED:
return "Compromised "+getCode();
case INACTIVE:
return "Inactive "+getCode();
case UNTRUSTED:
default:
return "Untrusted "+getCode();
}
}
public static Trust fromBoolean(Boolean trusted) {
return trusted?TRUSTED:UNTRUSTED;
}
public static Trust fromCode(int code) {
return trustsByValue.get(code);
}
};
private static IdentityKeyPair generateIdentityKeyPair() {
Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl IdentityKeyPair...");
ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()),
identityKeyPairKeys.getPrivateKey());
return ownKey;
}
private static int generateRegistrationId() {
Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl registration ID...");
int reg_id = KeyHelper.generateRegistrationId(true);
return reg_id;
}
public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
this.account = account;
this.mXmppConnectionService = service;
this.localRegistrationId = loadRegistrationId();
this.currentPreKeyId = loadCurrentPreKeyId();
for (SignedPreKeyRecord record : loadSignedPreKeys()) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got Axolotl signed prekey record:" + record.getId());
}
}
public int getCurrentPreKeyId() {
return currentPreKeyId;
}
// --------------------------------------
// IdentityKeyStore
// --------------------------------------
private IdentityKeyPair loadIdentityKeyPair() {
String ownName = account.getJid().toBareJid().toString();
IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account,
ownName);
if (ownKey != null) {
return ownKey;
} else {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl key for account " + ownName);
ownKey = generateIdentityKeyPair();
mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey);
}
return ownKey;
}
private int loadRegistrationId() {
return loadRegistrationId(false);
}
private int loadRegistrationId(boolean regenerate) {
String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
int reg_id;
if (!regenerate && regIdString != null) {
reg_id = Integer.valueOf(regIdString);
} else {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl registration id for account " + account.getJid());
reg_id = generateRegistrationId();
boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id));
if (success) {
mXmppConnectionService.databaseBackend.updateAccount(account);
} else {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to write new key to the database!");
}
}
return reg_id;
}
private int loadCurrentPreKeyId() {
String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
int reg_id;
if (regIdString != null) {
reg_id = Integer.valueOf(regIdString);
} else {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve current prekey id for account " + account.getJid());
reg_id = 0;
}
return reg_id;
}
public void regenerate() {
mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0));
identityKeyPair = loadIdentityKeyPair();
localRegistrationId = loadRegistrationId(true);
currentPreKeyId = 0;
mXmppConnectionService.updateAccountUi();
}
/**
* Get the local client's identity key pair.
*
* @return The local client's persistent identity key pair.
*/
@Override
public IdentityKeyPair getIdentityKeyPair() {
if(identityKeyPair == null) {
identityKeyPair = loadIdentityKeyPair();
}
return identityKeyPair;
}
/**
* Return the local client's registration ID.
* <p/>
* Clients should maintain a registration ID, a random number
* between 1 and 16380 that's generated once at install time.
*
* @return the local client's registration ID.
*/
@Override
public int getLocalRegistrationId() {
return localRegistrationId;
}
/**
* Save a remote client's identity key
* <p/>
* Store a remote client's identity key as trusted.
*
* @param name The name of the remote client.
* @param identityKey The remote client's identity key.
*/
@Override
public void saveIdentity(String name, IdentityKey identityKey) {
if(!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
}
}
/**
* Verify a remote client's identity key.
* <p/>
* Determine whether a remote client's identity is trusted. Convention is
* that the TextSecure protocol is 'trust on first use.' This means that
* an identity key is considered 'trusted' if there is no entry for the recipient
* in the local store, or if it matches the saved key for a recipient in the local
* store. Only if it mismatches an entry in the local store is it considered
* 'untrusted.'
*
* @param name The name of the remote client.
* @param identityKey The identity key to verify.
* @return true if trusted, false if untrusted.
*/
@Override
public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
return true;
}
public Trust getFingerprintTrust(String fingerprint) {
return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint);
}
public void setFingerprintTrust(String fingerprint, Trust trust) {
mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust);
}
public Set<IdentityKey> getContactUndecidedKeys(String bareJid, Trust trust) {
return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust);
}
public long getContactNumTrustedKeys(String bareJid) {
return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid);
}
// --------------------------------------
// SessionStore
// --------------------------------------
/**
* Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple,
* or a new SessionRecord if one does not currently exist.
* <p/>
* It is important that implementations return a copy of the current durable information. The
* returned SessionRecord may be modified, but those changes should not have an effect on the
* durable session state (what is returned by subsequent calls to this method) without the
* store method being called here first.
*
* @param address The name and device ID of the remote client.
* @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or
* a new SessionRecord if one does not currently exist.
*/
@Override
public SessionRecord loadSession(AxolotlAddress address) {
SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address);
return (session != null) ? session : new SessionRecord();
}
/**
* Returns all known devices with active sessions for a recipient
*
* @param name the name of the client.
* @return all known sub-devices with active sessions.
*/
@Override
public List<Integer> getSubDeviceSessions(String name) {
return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account,
new AxolotlAddress(name, 0));
}
/**
* Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
*
* @param address the address of the remote client.
* @param record the current SessionRecord for the remote client.
*/
@Override
public void storeSession(AxolotlAddress address, SessionRecord record) {
mXmppConnectionService.databaseBackend.storeSession(account, address, record);
}
/**
* Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple.
*
* @param address the address of the remote client.
* @return true if a {@link SessionRecord} exists, false otherwise.
*/
@Override
public boolean containsSession(AxolotlAddress address) {
return mXmppConnectionService.databaseBackend.containsSession(account, address);
}
/**
* Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
*
* @param address the address of the remote client.
*/
@Override
public void deleteSession(AxolotlAddress address) {
mXmppConnectionService.databaseBackend.deleteSession(account, address);
}
/**
* Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
*
* @param name the name of the remote client.
*/
@Override
public void deleteAllSessions(String name) {
mXmppConnectionService.databaseBackend.deleteAllSessions(account,
new AxolotlAddress(name, 0));
}
// --------------------------------------
// PreKeyStore
// --------------------------------------
/**
* Load a local PreKeyRecord.
*
* @param preKeyId the ID of the local PreKeyRecord.
* @return the corresponding PreKeyRecord.
* @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
*/
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
if (record == null) {
throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId);
}
return record;
}
/**
* Store a local PreKeyRecord.
*
* @param preKeyId the ID of the PreKeyRecord to store.
* @param record the PreKeyRecord.
*/
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
mXmppConnectionService.databaseBackend.storePreKey(account, record);
currentPreKeyId = preKeyId;
boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId));
if (success) {
mXmppConnectionService.databaseBackend.updateAccount(account);
} else {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to write new prekey id to the database!");
}
}
/**
* @param preKeyId A PreKeyRecord ID.
* @return true if the store has a record for the preKeyId, otherwise false.
*/
@Override
public boolean containsPreKey(int preKeyId) {
return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
}
/**
* Delete a PreKeyRecord from local storage.
*
* @param preKeyId The ID of the PreKeyRecord to remove.
*/
@Override
public void removePreKey(int preKeyId) {
mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
}
// --------------------------------------
// SignedPreKeyStore
// --------------------------------------
/**
* Load a local SignedPreKeyRecord.
*
* @param signedPreKeyId the ID of the local SignedPreKeyRecord.
* @return the corresponding SignedPreKeyRecord.
* @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
*/
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
if (record == null) {
throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId);
}
return record;
}
/**
* Load all local SignedPreKeyRecords.
*
* @return All stored SignedPreKeyRecords.
*/
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
}
/**
* Store a local SignedPreKeyRecord.
*
* @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
* @param record the SignedPreKeyRecord.
*/
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
}
/**
* @param signedPreKeyId A SignedPreKeyRecord ID.
* @return true if the store has a record for the signedPreKeyId, otherwise false.
*/
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
}
/**
* Delete a SignedPreKeyRecord from local storage.
*
* @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
*/
@Override
public void removeSignedPreKey(int signedPreKeyId) {
mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
}
}
public static class XmppAxolotlSession {
private final SessionCipher cipher;
private Integer preKeyId = null;
private final SQLiteAxolotlStore sqLiteAxolotlStore;
private final AxolotlAddress remoteAddress;
private final Account account;
private String fingerprint = null;
public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, String fingerprint) {
this(account, store, remoteAddress);
this.fingerprint = fingerprint;
}
public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) {
this.cipher = new SessionCipher(store, remoteAddress);
this.remoteAddress = remoteAddress;
this.sqLiteAxolotlStore = store;
this.account = account;
}
public Integer getPreKeyId() {
return preKeyId;
}
public void resetPreKeyId() {
preKeyId = null;
}
public String getFingerprint() {
return fingerprint;
}
protected void setTrust(SQLiteAxolotlStore.Trust trust) {
sqLiteAxolotlStore.setFingerprintTrust(fingerprint, trust);
}
protected SQLiteAxolotlStore.Trust getTrust() {
return sqLiteAxolotlStore.getFingerprintTrust(fingerprint);
}
@Nullable
public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) {
byte[] plaintext = null;
SQLiteAxolotlStore.Trust trust = getTrust();
switch (trust) {
case INACTIVE:
case UNDECIDED:
case UNTRUSTED:
case TRUSTED:
try {
try {
PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents());
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId());
String fingerprint = message.getIdentityKey().getFingerprint().replaceAll("\\s", "");
if (this.fingerprint != null && !this.fingerprint.equals(fingerprint)) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Had session with fingerprint "+ this.fingerprint+", received message with fingerprint "+fingerprint);
} else {
this.fingerprint = fingerprint;
plaintext = cipher.decrypt(message);
if (message.getPreKeyId().isPresent()) {
preKeyId = message.getPreKeyId().get();
}
}
} catch (InvalidMessageException | InvalidVersionException e) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"WhisperMessage received");
WhisperMessage message = new WhisperMessage(incomingHeader.getContents());
plaintext = cipher.decrypt(message);
} catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage());
}
} catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage());
}
if (plaintext != null && trust == SQLiteAxolotlStore.Trust.INACTIVE) {
setTrust(SQLiteAxolotlStore.Trust.TRUSTED);
}
break;
case COMPROMISED:
default:
// ignore
break;
}
return plaintext;
}
@Nullable
public XmppAxolotlMessage.XmppAxolotlMessageHeader processSending(@NonNull byte[] outgoingMessage) {
SQLiteAxolotlStore.Trust trust = getTrust();
if (trust == SQLiteAxolotlStore.Trust.TRUSTED) {
CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
XmppAxolotlMessage.XmppAxolotlMessageHeader header =
new XmppAxolotlMessage.XmppAxolotlMessageHeader(remoteAddress.getDeviceId(),
ciphertextMessage.serialize());
return header;
} else {
return null;
}
}
}
private static class AxolotlAddressMap<T> { private static class AxolotlAddressMap<T> {
protected Map<String, Map<Integer, T>> map; protected Map<String, Map<Integer, T>> map;
protected final Object MAP_LOCK = new Object(); protected final Object MAP_LOCK = new Object();
@ -704,8 +150,13 @@ public class AxolotlService {
@Override @Override
public void put(AxolotlAddress address, XmppAxolotlSession value) { public void put(AxolotlAddress address, XmppAxolotlSession value) {
super.put(address, value); super.put(address, value);
value.setNotFresh();
xmppConnectionService.syncRosterToDisk(account); xmppConnectionService.syncRosterToDisk(account);
} }
public void put(XmppAxolotlSession session) {
this.put(session.getRemoteAddress(), session);
}
} }
private static enum FetchStatus { private static enum FetchStatus {
@ -740,12 +191,12 @@ public class AxolotlService {
return axolotlStore.getIdentityKeyPair().getPublicKey(); return axolotlStore.getIdentityKeyPair().getPublicKey();
} }
public Set<IdentityKey> getKeysWithTrust(SQLiteAxolotlStore.Trust trust) { public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) {
return axolotlStore.getContactUndecidedKeys(account.getJid().toBareJid().toString(), trust); return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust);
} }
public Set<IdentityKey> getKeysWithTrust(SQLiteAxolotlStore.Trust trust, Contact contact) { public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact) {
return axolotlStore.getContactUndecidedKeys(contact.getJid().toBareJid().toString(), trust); return axolotlStore.getContactKeysWithTrust(contact.getJid().toBareJid().toString(), trust);
} }
public long getNumTrustedKeys(Contact contact) { public long getNumTrustedKeys(Contact contact) {
@ -782,7 +233,7 @@ public class AxolotlService {
} }
public int getOwnDeviceId() { public int getOwnDeviceId() {
return axolotlStore.loadRegistrationId(); return axolotlStore.getLocalRegistrationId();
} }
public Set<Integer> getOwnDeviceIds() { public Set<Integer> getOwnDeviceIds() {
@ -790,8 +241,8 @@ public class AxolotlService {
} }
private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds, private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds,
final SQLiteAxolotlStore.Trust from, final XmppAxolotlSession.Trust from,
final SQLiteAxolotlStore.Trust to) { final XmppAxolotlSession.Trust to) {
for (Integer deviceId : deviceIds) { for (Integer deviceId : deviceIds) {
AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId); AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
XmppAxolotlSession session = sessions.get(address); XmppAxolotlSession session = sessions.get(address);
@ -810,17 +261,25 @@ public class AxolotlService {
for (Integer deviceId : deviceIds) { for (Integer deviceId : deviceIds) {
AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId); AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
if (sessions.get(ownDeviceAddress) == null) { if (sessions.get(ownDeviceAddress) == null) {
buildSessionFromPEP(null, ownDeviceAddress, false); buildSessionFromPEP(ownDeviceAddress);
} }
} }
} }
Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString())); Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString()));
expiredDevices.removeAll(deviceIds); expiredDevices.removeAll(deviceIds);
setTrustOnSessions(jid, expiredDevices, SQLiteAxolotlStore.Trust.TRUSTED, setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED,
SQLiteAxolotlStore.Trust.INACTIVE); XmppAxolotlSession.Trust.INACTIVE_TRUSTED);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED,
XmppAxolotlSession.Trust.INACTIVE_UNDECIDED);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED,
XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED);
Set<Integer> newDevices = new HashSet<>(deviceIds); Set<Integer> newDevices = new HashSet<>(deviceIds);
setTrustOnSessions(jid, newDevices, SQLiteAxolotlStore.Trust.INACTIVE, setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED,
SQLiteAxolotlStore.Trust.TRUSTED); XmppAxolotlSession.Trust.TRUSTED);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED,
XmppAxolotlSession.Trust.UNDECIDED);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED,
XmppAxolotlSession.Trust.UNTRUSTED);
this.deviceIds.put(jid, deviceIds); this.deviceIds.put(jid, deviceIds);
mXmppConnectionService.keyStatusUpdated(); mXmppConnectionService.keyStatusUpdated();
publishOwnDeviceIdIfNeeded(); publishOwnDeviceIdIfNeeded();
@ -840,7 +299,7 @@ public class AxolotlService {
} }
public void purgeKey(IdentityKey identityKey) { public void purgeKey(IdentityKey identityKey) {
axolotlStore.setFingerprintTrust(identityKey.getFingerprint().replaceAll("\\s",""), SQLiteAxolotlStore.Trust.COMPROMISED); axolotlStore.setFingerprintTrust(identityKey.getFingerprint().replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED);
} }
public void publishOwnDeviceIdIfNeeded() { public void publishOwnDeviceIdIfNeeded() {
@ -967,15 +426,16 @@ public class AxolotlService {
return sessions.hasAny(address) || return sessions.hasAny(address) ||
(deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty()); (deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
} }
public SQLiteAxolotlStore.Trust getFingerprintTrust(String fingerprint) {
public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
return axolotlStore.getFingerprintTrust(fingerprint); return axolotlStore.getFingerprintTrust(fingerprint);
} }
public void setFingerprintTrust(String fingerprint, SQLiteAxolotlStore.Trust trust) { public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
axolotlStore.setFingerprintTrust(fingerprint, trust); axolotlStore.setFingerprintTrust(fingerprint, trust);
} }
private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address, final boolean flushWaitingQueueAfterFetch) { private void buildSessionFromPEP(final AxolotlAddress address) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.getDeviceId()); Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.getDeviceId());
try { try {
@ -987,15 +447,6 @@ public class AxolotlService {
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0); AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
&& !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) { && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
if (flushWaitingQueueAfterFetch && conversation != null) {
conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_AXOLOTL,
new Conversation.OnMessageFound() {
@Override
public void onMessageFound(Message message) {
processSending(message,false);
}
});
}
mXmppConnectionService.keyStatusUpdated(); mXmppConnectionService.keyStatusUpdated();
} }
} }
@ -1090,7 +541,7 @@ public class AxolotlService {
return addresses; return addresses;
} }
public boolean createSessionsIfNeeded(final Conversation conversation, final boolean flushWaitingQueueAfterFetch) { public boolean createSessionsIfNeeded(final Conversation conversation) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed...");
boolean newSessions = false; boolean newSessions = false;
Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation); Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation);
@ -1099,7 +550,9 @@ public class AxolotlService {
FetchStatus status = fetchStatusMap.get(address); FetchStatus status = fetchStatusMap.get(address);
if (status == null || status == FetchStatus.ERROR) { if (status == null || status == FetchStatus.ERROR) {
fetchStatusMap.put(address, FetchStatus.PENDING); fetchStatusMap.put(address, FetchStatus.PENDING);
this.buildSessionFromPEP(conversation, address, flushWaitingQueueAfterFetch); this.buildSessionFromPEP(address);
newSessions = true;
} else if (status == FetchStatus.PENDING) {
newSessions = true; newSessions = true;
} else { } else {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString()); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString());
@ -1117,49 +570,53 @@ public class AxolotlService {
} }
@Nullable
private XmppAxolotlMessage buildHeader(Contact contact) {
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
contact.getJid().toBareJid(), getOwnDeviceId());
Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact);
Set<XmppAxolotlSession> ownSessions = findOwnSessions();
if (contactSessions.isEmpty()) {
return null;
}
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
for (XmppAxolotlSession session : contactSessions) {
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
axolotlMessage.addDevice(session);
}
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements...");
for (XmppAxolotlSession session : ownSessions) {
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
axolotlMessage.addDevice(session);
}
return axolotlMessage;
}
@Nullable @Nullable
public XmppAxolotlMessage encrypt(Message message) { public XmppAxolotlMessage encrypt(Message message) {
XmppAxolotlMessage axolotlMessage = buildHeader(message.getContact());
if (axolotlMessage != null) {
final String content; final String content;
if (message.hasFileOnRemoteHost()) { if (message.hasFileOnRemoteHost()) {
content = message.getFileParams().url.toString(); content = message.getFileParams().url.toString();
} else { } else {
content = message.getBody(); content = message.getBody();
} }
final XmppAxolotlMessage axolotlMessage;
try { try {
axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(), axolotlMessage.encrypt(content);
getOwnDeviceId(), content);
} catch (CryptoFailedException e) { } catch (CryptoFailedException e) {
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
return null; return null;
} }
if(findSessionsforContact(message.getContact()).isEmpty()) {
return null;
}
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl foreign headers...");
for (XmppAxolotlSession session : findSessionsforContact(message.getContact())) {
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.remoteAddress.toString());
//if(!session.isTrusted()) {
// TODO: handle this properly
// continue;
// }
axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey()));
}
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl own headers...");
for (XmppAxolotlSession session : findOwnSessions()) {
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.remoteAddress.toString());
// if(!session.isTrusted()) {
// TODO: handle this properly
// continue;
// }
axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey()));
} }
return axolotlMessage; return axolotlMessage;
} }
private void processSending(final Message message, final boolean delay) { public void preparePayloadMessage(final Message message, final boolean delay) {
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -1176,13 +633,14 @@ public class AxolotlService {
}); });
} }
public void prepareMessage(final Message message,final boolean delay) { public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) {
if (!messageCache.containsKey(message.getUuid())) { executor.execute(new Runnable() {
boolean newSessions = createSessionsIfNeeded(message.getConversation(), true); @Override
if (!newSessions) { public void run() {
this.processSending(message,delay); XmppAxolotlMessage axolotlMessage = buildHeader(contact);
} onMessageCreatedCallback.run(axolotlMessage);
} }
});
} }
public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
@ -1196,51 +654,60 @@ public class AxolotlService {
return axolotlMessage; return axolotlMessage;
} }
public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) {
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
return (identityKey != null)
? new XmppAxolotlSession(account, axolotlStore, address,
identityKey.getFingerprint().replaceAll("\\s", ""))
: null;
}
private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(), AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
message.getSenderDeviceId()); message.getSenderDeviceId());
boolean newSession = false;
XmppAxolotlSession session = sessions.get(senderAddress); XmppAxolotlSession session = sessions.get(senderAddress);
if (session == null) { if (session == null) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message);
// TODO: handle this properly session = recreateUncachedSession(senderAddress);
IdentityKey identityKey = axolotlStore.loadSession(senderAddress).getSessionState().getRemoteIdentityKey(); if (session == null) {
if ( identityKey != null ) {
session = new XmppAxolotlSession(account, axolotlStore, senderAddress, identityKey.getFingerprint().replaceAll("\\s", ""));
} else {
session = new XmppAxolotlSession(account, axolotlStore, senderAddress); session = new XmppAxolotlSession(account, axolotlStore, senderAddress);
} }
newSession = true; }
return session;
} }
for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) {
if (header.getRecipientDeviceId() == getOwnDeviceId()) { XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found axolotl header matching own device ID, processing...");
byte[] payloadKey = session.processReceiving(header); XmppAxolotlSession session = getReceivingSession(message);
if (payloadKey != null) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got payload key from axolotl header. Decrypting message...");
try { try {
plaintextMessage = message.decrypt(session, payloadKey, session.getFingerprint()); plaintextMessage = message.decrypt(session, getOwnDeviceId());
} catch (CryptoFailedException e) {
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
break;
}
}
Integer preKeyId = session.getPreKeyId(); Integer preKeyId = session.getPreKeyId();
if (preKeyId != null) { if (preKeyId != null) {
publishBundlesIfNeeded(); publishBundlesIfNeeded();
session.resetPreKeyId(); session.resetPreKeyId();
} }
break; } catch (CryptoFailedException e) {
} Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
} }
if (newSession && plaintextMessage != null) { if (session.isFresh() && plaintextMessage != null) {
sessions.put(senderAddress, session); sessions.put(session);
} }
return plaintextMessage; return plaintextMessage;
} }
public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) {
XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage = null;
XmppAxolotlSession session = getReceivingSession(message);
keyTransportMessage = message.getParameters(session, getOwnDeviceId());
if (session.isFresh() && keyTransportMessage != null) {
sessions.put(session);
}
return keyTransportMessage;
}
} }

View file

@ -0,0 +1,5 @@
package eu.siacs.conversations.crypto.axolotl;
public interface OnMessageCreatedCallback {
void run(XmppAxolotlMessage message);
}

View file

@ -0,0 +1,421 @@
package eu.siacs.conversations.crypto.axolotl;
import android.util.Log;
import android.util.LruCache;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.util.KeyHelper;
import java.util.List;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService;
public class SQLiteAxolotlStore implements AxolotlStore {
public static final String PREKEY_TABLENAME = "prekeys";
public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys";
public static final String SESSION_TABLENAME = "sessions";
public static final String IDENTITIES_TABLENAME = "identities";
public static final String ACCOUNT = "account";
public static final String DEVICE_ID = "device_id";
public static final String ID = "id";
public static final String KEY = "key";
public static final String FINGERPRINT = "fingerprint";
public static final String NAME = "name";
public static final String TRUSTED = "trusted";
public static final String OWN = "ownkey";
public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id";
public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id";
private static final int NUM_TRUSTS_TO_CACHE = 100;
private final Account account;
private final XmppConnectionService mXmppConnectionService;
private IdentityKeyPair identityKeyPair;
private int localRegistrationId;
private int currentPreKeyId = 0;
private final LruCache<String, XmppAxolotlSession.Trust> trustCache =
new LruCache<String, XmppAxolotlSession.Trust>(NUM_TRUSTS_TO_CACHE) {
@Override
protected XmppAxolotlSession.Trust create(String fingerprint) {
return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint);
}
};
private static IdentityKeyPair generateIdentityKeyPair() {
Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair...");
ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
return new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()),
identityKeyPairKeys.getPrivateKey());
}
private static int generateRegistrationId() {
Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID...");
return KeyHelper.generateRegistrationId(true);
}
public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
this.account = account;
this.mXmppConnectionService = service;
this.localRegistrationId = loadRegistrationId();
this.currentPreKeyId = loadCurrentPreKeyId();
for (SignedPreKeyRecord record : loadSignedPreKeys()) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got Axolotl signed prekey record:" + record.getId());
}
}
public int getCurrentPreKeyId() {
return currentPreKeyId;
}
// --------------------------------------
// IdentityKeyStore
// --------------------------------------
private IdentityKeyPair loadIdentityKeyPair() {
String ownName = account.getJid().toBareJid().toString();
IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account,
ownName);
if (ownKey != null) {
return ownKey;
} else {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl key for account " + ownName);
ownKey = generateIdentityKeyPair();
mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey);
}
return ownKey;
}
private int loadRegistrationId() {
return loadRegistrationId(false);
}
private int loadRegistrationId(boolean regenerate) {
String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
int reg_id;
if (!regenerate && regIdString != null) {
reg_id = Integer.valueOf(regIdString);
} else {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid());
reg_id = generateRegistrationId();
boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id));
if (success) {
mXmppConnectionService.databaseBackend.updateAccount(account);
} else {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new key to the database!");
}
}
return reg_id;
}
private int loadCurrentPreKeyId() {
String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
int reg_id;
if (regIdString != null) {
reg_id = Integer.valueOf(regIdString);
} else {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid());
reg_id = 0;
}
return reg_id;
}
public void regenerate() {
mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
trustCache.evictAll();
account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0));
identityKeyPair = loadIdentityKeyPair();
localRegistrationId = loadRegistrationId(true);
currentPreKeyId = 0;
mXmppConnectionService.updateAccountUi();
}
/**
* Get the local client's identity key pair.
*
* @return The local client's persistent identity key pair.
*/
@Override
public IdentityKeyPair getIdentityKeyPair() {
if (identityKeyPair == null) {
identityKeyPair = loadIdentityKeyPair();
}
return identityKeyPair;
}
/**
* Return the local client's registration ID.
* <p/>
* Clients should maintain a registration ID, a random number
* between 1 and 16380 that's generated once at install time.
*
* @return the local client's registration ID.
*/
@Override
public int getLocalRegistrationId() {
return localRegistrationId;
}
/**
* Save a remote client's identity key
* <p/>
* Store a remote client's identity key as trusted.
*
* @param name The name of the remote client.
* @param identityKey The remote client's identity key.
*/
@Override
public void saveIdentity(String name, IdentityKey identityKey) {
if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
}
}
/**
* Verify a remote client's identity key.
* <p/>
* Determine whether a remote client's identity is trusted. Convention is
* that the TextSecure protocol is 'trust on first use.' This means that
* an identity key is considered 'trusted' if there is no entry for the recipient
* in the local store, or if it matches the saved key for a recipient in the local
* store. Only if it mismatches an entry in the local store is it considered
* 'untrusted.'
*
* @param name The name of the remote client.
* @param identityKey The identity key to verify.
* @return true if trusted, false if untrusted.
*/
@Override
public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
return true;
}
public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
return (fingerprint == null)? null : trustCache.get(fingerprint);
}
public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust);
trustCache.remove(fingerprint);
}
public Set<IdentityKey> getContactKeysWithTrust(String bareJid, XmppAxolotlSession.Trust trust) {
return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust);
}
public long getContactNumTrustedKeys(String bareJid) {
return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid);
}
// --------------------------------------
// SessionStore
// --------------------------------------
/**
* Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple,
* or a new SessionRecord if one does not currently exist.
* <p/>
* It is important that implementations return a copy of the current durable information. The
* returned SessionRecord may be modified, but those changes should not have an effect on the
* durable session state (what is returned by subsequent calls to this method) without the
* store method being called here first.
*
* @param address The name and device ID of the remote client.
* @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or
* a new SessionRecord if one does not currently exist.
*/
@Override
public SessionRecord loadSession(AxolotlAddress address) {
SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address);
return (session != null) ? session : new SessionRecord();
}
/**
* Returns all known devices with active sessions for a recipient
*
* @param name the name of the client.
* @return all known sub-devices with active sessions.
*/
@Override
public List<Integer> getSubDeviceSessions(String name) {
return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account,
new AxolotlAddress(name, 0));
}
/**
* Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
*
* @param address the address of the remote client.
* @param record the current SessionRecord for the remote client.
*/
@Override
public void storeSession(AxolotlAddress address, SessionRecord record) {
mXmppConnectionService.databaseBackend.storeSession(account, address, record);
}
/**
* Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple.
*
* @param address the address of the remote client.
* @return true if a {@link SessionRecord} exists, false otherwise.
*/
@Override
public boolean containsSession(AxolotlAddress address) {
return mXmppConnectionService.databaseBackend.containsSession(account, address);
}
/**
* Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
*
* @param address the address of the remote client.
*/
@Override
public void deleteSession(AxolotlAddress address) {
mXmppConnectionService.databaseBackend.deleteSession(account, address);
}
/**
* Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
*
* @param name the name of the remote client.
*/
@Override
public void deleteAllSessions(String name) {
AxolotlAddress address = new AxolotlAddress(name, 0);
mXmppConnectionService.databaseBackend.deleteAllSessions(account,
address);
}
// --------------------------------------
// PreKeyStore
// --------------------------------------
/**
* Load a local PreKeyRecord.
*
* @param preKeyId the ID of the local PreKeyRecord.
* @return the corresponding PreKeyRecord.
* @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
*/
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
if (record == null) {
throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId);
}
return record;
}
/**
* Store a local PreKeyRecord.
*
* @param preKeyId the ID of the PreKeyRecord to store.
* @param record the PreKeyRecord.
*/
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
mXmppConnectionService.databaseBackend.storePreKey(account, record);
currentPreKeyId = preKeyId;
boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId));
if (success) {
mXmppConnectionService.databaseBackend.updateAccount(account);
} else {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new prekey id to the database!");
}
}
/**
* @param preKeyId A PreKeyRecord ID.
* @return true if the store has a record for the preKeyId, otherwise false.
*/
@Override
public boolean containsPreKey(int preKeyId) {
return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
}
/**
* Delete a PreKeyRecord from local storage.
*
* @param preKeyId The ID of the PreKeyRecord to remove.
*/
@Override
public void removePreKey(int preKeyId) {
mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
}
// --------------------------------------
// SignedPreKeyStore
// --------------------------------------
/**
* Load a local SignedPreKeyRecord.
*
* @param signedPreKeyId the ID of the local SignedPreKeyRecord.
* @return the corresponding SignedPreKeyRecord.
* @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
*/
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
if (record == null) {
throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId);
}
return record;
}
/**
* Load all local SignedPreKeyRecords.
*
* @return All stored SignedPreKeyRecords.
*/
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
}
/**
* Store a local SignedPreKeyRecord.
*
* @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
* @param record the SignedPreKeyRecord.
*/
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
}
/**
* @param signedPreKeyId A SignedPreKeyRecord ID.
* @return true if the store has a record for the signedPreKeyId, otherwise false.
*/
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
}
/**
* Delete a SignedPreKeyRecord from local storage.
*
* @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
*/
@Override
public void removeSignedPreKey(int signedPreKeyId) {
mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
}
}

View file

@ -1,15 +1,16 @@
package eu.siacs.conversations.crypto.axolotl; package eu.siacs.conversations.crypto.axolotl;
import android.support.annotation.Nullable;
import android.util.Base64; import android.util.Base64;
import android.util.Log;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException; import java.security.NoSuchProviderException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.HashSet; import java.util.HashMap;
import java.util.Set; import java.util.List;
import java.util.Map;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
@ -20,59 +21,35 @@ import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jid.Jid;
public class XmppAxolotlMessage { public class XmppAxolotlMessage {
public static final String CONTAINERTAG = "encrypted";
public static final String HEADER = "header";
public static final String SOURCEID = "sid";
public static final String KEYTAG = "key";
public static final String REMOTEID = "rid";
public static final String IVTAG = "iv";
public static final String PAYLOAD = "payload";
private static final String KEYTYPE = "AES";
private static final String CIPHERMODE = "AES/GCM/NoPadding";
private static final String PROVIDER = "BC";
private byte[] innerKey; private byte[] innerKey;
private byte[] ciphertext; private byte[] ciphertext = null;
private byte[] iv; private byte[] iv = null;
private final Set<XmppAxolotlMessageHeader> headers; private final Map<Integer, byte[]> keys;
private final Jid from; private final Jid from;
private final int sourceDeviceId; 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 { public static class XmppAxolotlPlaintextMessage {
private final AxolotlService.XmppAxolotlSession session;
private final String plaintext; private final String plaintext;
private final String fingerprint; private final String fingerprint;
public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext, String fingerprint) { public XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) {
this.session = session;
this.plaintext = plaintext; this.plaintext = plaintext;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
} }
@ -81,51 +58,105 @@ public class XmppAxolotlMessage {
return plaintext; return plaintext;
} }
public AxolotlService.XmppAxolotlSession getSession() {
return session;
}
public String getFingerprint() { public String getFingerprint() {
return fingerprint; return fingerprint;
} }
} }
public XmppAxolotlMessage(Jid from, Element axolotlMessage) { public static class XmppAxolotlKeyTransportMessage {
private final String fingerprint;
private final byte[] key;
private final byte[] iv;
public XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) {
this.fingerprint = fingerprint;
this.key = key;
this.iv = iv;
}
public String getFingerprint() {
return fingerprint;
}
public byte[] getKey() {
return key;
}
public byte[] getIv() {
return iv;
}
}
private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException {
this.from = from; this.from = from;
this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id")); Element header = axolotlMessage.findChild(HEADER);
this.headers = new HashSet<>(); this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID));
for(Element child:axolotlMessage.getChildren()) { List<Element> keyElements = header.getChildren();
switch(child.getName()) { this.keys = new HashMap<>(keyElements.size());
case "header": for (Element keyElement : keyElements) {
headers.add(new XmppAxolotlMessageHeader(child)); switch (keyElement.getName()) {
case KEYTAG:
try {
Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
byte[] key = Base64.decode(keyElement.getContent(), Base64.DEFAULT);
this.keys.put(recipientId, key);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(e);
}
break; break;
case "message": case IVTAG:
iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT); if (this.iv != null) {
ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT); throw new IllegalArgumentException("Duplicate iv entry");
}
iv = Base64.decode(keyElement.getContent(), Base64.DEFAULT);
break; break;
default: default:
Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString());
break; break;
} }
} }
Element payloadElement = axolotlMessage.findChild(PAYLOAD);
if (payloadElement != null) {
ciphertext = Base64.decode(payloadElement.getContent(), Base64.DEFAULT);
}
} }
public XmppAxolotlMessage(Jid from, int sourceDeviceId, String plaintext) throws CryptoFailedException{ public XmppAxolotlMessage(Jid from, int sourceDeviceId) {
this.from = from; this.from = from;
this.sourceDeviceId = sourceDeviceId; this.sourceDeviceId = sourceDeviceId;
this.headers = new HashSet<>(); this.keys = new HashMap<>();
this.encrypt(plaintext); this.iv = generateIv();
this.innerKey = generateKey();
} }
private void encrypt(String plaintext) throws CryptoFailedException { public static XmppAxolotlMessage fromElement(Element element, Jid from) {
return new XmppAxolotlMessage(element, from);
}
private static byte[] generateKey() {
try { try {
KeyGenerator generator = KeyGenerator.getInstance("AES"); KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE);
generator.init(128); generator.init(128);
SecretKey secretKey = generator.generateKey(); return generator.generateKey().getEncoded();
} catch (NoSuchAlgorithmException e) {
Log.e(Config.LOGTAG, e.getMessage());
return null;
}
}
private static byte[] generateIv() {
SecureRandom random = new SecureRandom(); SecureRandom random = new SecureRandom();
this.iv = new byte[16]; byte[] iv = new byte[16];
random.nextBytes(iv); random.nextBytes(iv);
return iv;
}
public void encrypt(String plaintext) throws CryptoFailedException {
try {
SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(iv); IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC"); Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
this.innerKey = secretKey.getEncoded(); this.innerKey = secretKey.getEncoded();
this.ciphertext = cipher.doFinal(plaintext.getBytes()); this.ciphertext = cipher.doFinal(plaintext.getBytes());
@ -148,13 +179,10 @@ public class XmppAxolotlMessage {
return ciphertext; return ciphertext;
} }
public Set<XmppAxolotlMessageHeader> getHeaders() { public void addDevice(XmppAxolotlSession session) {
return headers; byte[] key = session.processSending(innerKey);
} if (key != null) {
keys.put(session.getRemoteAddress().getDeviceId(), key);
public void addHeader(@Nullable XmppAxolotlMessageHeader header) {
if (header != null) {
headers.add(header);
} }
} }
@ -166,38 +194,56 @@ public class XmppAxolotlMessage {
return this.iv; return this.iv;
} }
public Element toXml() { public Element toElement() {
// TODO: generate outer XML, add in header XML Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX);
Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX); Element headerElement = encryptionElement.addChild(HEADER);
message.setAttribute("id", sourceDeviceId); headerElement.setAttribute(SOURCEID, sourceDeviceId);
for(XmppAxolotlMessageHeader header: headers) { for (Map.Entry<Integer, byte[]> keyEntry : keys.entrySet()) {
message.addChild(header.toXml()); Element keyElement = new Element(KEYTAG);
keyElement.setAttribute(REMOTEID, keyEntry.getKey());
keyElement.setContent(Base64.encodeToString(keyEntry.getValue(), Base64.DEFAULT));
headerElement.addChild(keyElement);
} }
Element payload = message.addChild("message"); headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.DEFAULT));
payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT)); if (ciphertext != null) {
Element payload = encryptionElement.addChild(PAYLOAD);
payload.setContent(Base64.encodeToString(ciphertext, Base64.DEFAULT)); payload.setContent(Base64.encodeToString(ciphertext, Base64.DEFAULT));
return message; }
return encryptionElement;
} }
private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) {
byte[] encryptedKey = keys.get(sourceDeviceId);
return (encryptedKey != null) ? session.processReceiving(encryptedKey) : null;
}
public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key, String fingerprint) throws CryptoFailedException { public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) {
byte[] key = unpackKey(session, sourceDeviceId);
return (key != null)
? new XmppAxolotlKeyTransportMessage(session.getFingerprint(), key, getIV())
: null;
}
public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
XmppAxolotlPlaintextMessage plaintextMessage = null; XmppAxolotlPlaintextMessage plaintextMessage = null;
byte[] key = unpackKey(session, sourceDeviceId);
if (key != null) {
try { try {
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC"); SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv); IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
String plaintext = new String(cipher.doFinal(ciphertext)); String plaintext = new String(cipher.doFinal(ciphertext));
plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext, fingerprint); plaintextMessage = new XmppAxolotlPlaintextMessage(plaintext, session.getFingerprint());
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| InvalidAlgorithmParameterException | IllegalBlockSizeException | InvalidAlgorithmParameterException | IllegalBlockSizeException
| BadPaddingException | NoSuchProviderException e) { | BadPaddingException | NoSuchProviderException e) {
throw new CryptoFailedException(e); throw new CryptoFailedException(e);
} }
}
return plaintextMessage; return plaintextMessage;
} }
} }

View file

@ -0,0 +1,196 @@
package eu.siacs.conversations.crypto.axolotl;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.SessionCipher;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import java.util.HashMap;
import java.util.Map;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
public class XmppAxolotlSession {
private final SessionCipher cipher;
private final SQLiteAxolotlStore sqLiteAxolotlStore;
private final AxolotlAddress remoteAddress;
private final Account account;
private String fingerprint = null;
private Integer preKeyId = null;
private boolean fresh = true;
public enum Trust {
UNDECIDED(0),
TRUSTED(1),
UNTRUSTED(2),
COMPROMISED(3),
INACTIVE_TRUSTED(4),
INACTIVE_UNDECIDED(5),
INACTIVE_UNTRUSTED(6);
private static final Map<Integer, Trust> trustsByValue = new HashMap<>();
static {
for (Trust trust : Trust.values()) {
trustsByValue.put(trust.getCode(), trust);
}
}
private final int code;
Trust(int code) {
this.code = code;
}
public int getCode() {
return this.code;
}
public String toString() {
switch (this) {
case UNDECIDED:
return "Trust undecided " + getCode();
case TRUSTED:
return "Trusted " + getCode();
case COMPROMISED:
return "Compromised " + getCode();
case INACTIVE_TRUSTED:
return "Inactive (Trusted)" + getCode();
case INACTIVE_UNDECIDED:
return "Inactive (Undecided)" + getCode();
case INACTIVE_UNTRUSTED:
return "Inactive (Untrusted)" + getCode();
case UNTRUSTED:
default:
return "Untrusted " + getCode();
}
}
public static Trust fromBoolean(Boolean trusted) {
return trusted ? TRUSTED : UNTRUSTED;
}
public static Trust fromCode(int code) {
return trustsByValue.get(code);
}
}
public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, String fingerprint) {
this(account, store, remoteAddress);
this.fingerprint = fingerprint;
}
public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) {
this.cipher = new SessionCipher(store, remoteAddress);
this.remoteAddress = remoteAddress;
this.sqLiteAxolotlStore = store;
this.account = account;
}
public Integer getPreKeyId() {
return preKeyId;
}
public void resetPreKeyId() {
preKeyId = null;
}
public String getFingerprint() {
return fingerprint;
}
public AxolotlAddress getRemoteAddress() {
return remoteAddress;
}
public boolean isFresh() {
return fresh;
}
public void setNotFresh() {
this.fresh = false;
}
protected void setTrust(Trust trust) {
sqLiteAxolotlStore.setFingerprintTrust(fingerprint, trust);
}
protected Trust getTrust() {
Trust trust = sqLiteAxolotlStore.getFingerprintTrust(fingerprint);
return (trust == null) ? Trust.UNDECIDED : trust;
}
@Nullable
public byte[] processReceiving(byte[] encryptedKey) {
byte[] plaintext = null;
Trust trust = getTrust();
switch (trust) {
case INACTIVE_TRUSTED:
case UNDECIDED:
case UNTRUSTED:
case TRUSTED:
try {
try {
PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey);
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId());
String fingerprint = message.getIdentityKey().getFingerprint().replaceAll("\\s", "");
if (this.fingerprint != null && !this.fingerprint.equals(fingerprint)) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.fingerprint + ", received message with fingerprint " + fingerprint);
} else {
this.fingerprint = fingerprint;
plaintext = cipher.decrypt(message);
if (message.getPreKeyId().isPresent()) {
preKeyId = message.getPreKeyId().get();
}
}
} catch (InvalidMessageException | InvalidVersionException e) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received");
WhisperMessage message = new WhisperMessage(encryptedKey);
plaintext = cipher.decrypt(message);
} catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
}
} catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
}
if (plaintext != null && trust == Trust.INACTIVE_TRUSTED) {
setTrust(Trust.TRUSTED);
}
break;
case COMPROMISED:
default:
// ignore
break;
}
return plaintext;
}
@Nullable
public byte[] processSending(@NonNull byte[] outgoingMessage) {
Trust trust = getTrust();
if (trust == Trust.TRUSTED) {
CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
return ciphertextMessage.serialize();
} else {
return null;
}
}
}

View file

@ -563,43 +563,52 @@ public class Conversation extends AbstractEntity implements Blockable {
return this.nextCounterpart; return this.nextCounterpart;
} }
public int getLatestEncryption() { private int getMostRecentlyUsedOutgoingEncryption() {
int latestEncryption = this.getLatestMessage().getEncryption(); synchronized (this.messages) {
if ((latestEncryption == Message.ENCRYPTION_DECRYPTED) for(int i = this.messages.size() -1; i >= 0; --i) {
|| (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) { final Message m = this.messages.get(0);
if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
final int e = m.getEncryption();
if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
return Message.ENCRYPTION_PGP; return Message.ENCRYPTION_PGP;
} else { } else {
return latestEncryption; return e;
} }
} }
}
}
return Message.ENCRYPTION_NONE;
}
public int getNextEncryption(boolean force) { private int getMostRecentlyUsedIncomingEncryption() {
synchronized (this.messages) {
for(int i = this.messages.size() -1; i >= 0; --i) {
final Message m = this.messages.get(0);
if (m.getStatus() == Message.STATUS_RECEIVED) {
final int e = m.getEncryption();
if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
return Message.ENCRYPTION_PGP;
} else {
return e;
}
}
}
}
return Message.ENCRYPTION_NONE;
}
public int getNextEncryption() {
int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1); int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
if (next == -1) { if (next == -1) {
int latest = this.getLatestEncryption(); int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
if (latest == Message.ENCRYPTION_NONE) { if (outgoing == Message.ENCRYPTION_NONE) {
if (force && getMode() == MODE_SINGLE) { return this.getMostRecentlyUsedIncomingEncryption();
return Message.ENCRYPTION_OTR;
} else if (getContact().getPresences().size() == 1) {
if (getContact().getOtrFingerprints().size() >= 1) {
return Message.ENCRYPTION_OTR;
} else { } else {
return latest; return outgoing;
}
} else {
return latest;
}
} else {
return latest;
} }
} }
if (next == Message.ENCRYPTION_NONE && force
&& getMode() == MODE_SINGLE) {
return Message.ENCRYPTION_OTR;
} else {
return next; return next;
} }
}
public void setNextEncryption(int encryption) { public void setNextEncryption(int encryption) {
this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption)); this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));

View file

@ -1,26 +1,7 @@
package eu.siacs.conversations.entities; package eu.siacs.conversations.entities;
import android.util.Log;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.MimeUtils;
public class DownloadableFile extends File { public class DownloadableFile extends File {
@ -29,8 +10,7 @@ public class DownloadableFile extends File {
private long expectedSize = 0; private long expectedSize = 0;
private String sha1sum; private String sha1sum;
private Key aeskey; private byte[] aeskey;
private String mime;
private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf }; 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
@ -44,16 +24,8 @@ public class DownloadableFile extends File {
} }
public long getExpectedSize() { public long getExpectedSize() {
if (this.aeskey != null) {
if (this.expectedSize == 0) {
return 0;
} else {
return (this.expectedSize / 16 + 1) * 16;
}
} else {
return this.expectedSize; return this.expectedSize;
} }
}
public String getMimeType() { public String getMimeType() {
String path = this.getAbsolutePath(); String path = this.getAbsolutePath();
@ -78,91 +50,38 @@ public class DownloadableFile extends File {
this.sha1sum = sum; this.sha1sum = sum;
} }
public void setKey(byte[] key) { public void setKeyAndIv(byte[] keyIvCombo) {
if (key.length == 48) { if (keyIvCombo.length == 48) {
byte[] secretKey = new byte[32]; byte[] secretKey = new byte[32];
byte[] iv = new byte[16]; byte[] iv = new byte[16];
System.arraycopy(key, 0, iv, 0, 16); System.arraycopy(keyIvCombo, 0, iv, 0, 16);
System.arraycopy(key, 16, secretKey, 0, 32); System.arraycopy(keyIvCombo, 16, secretKey, 0, 32);
this.aeskey = new SecretKeySpec(secretKey, "AES"); this.aeskey = secretKey;
this.iv = iv; this.iv = iv;
} else if (key.length >= 32) { } else if (keyIvCombo.length >= 32) {
byte[] secretKey = new byte[32]; byte[] secretKey = new byte[32];
System.arraycopy(key, 0, secretKey, 0, 32); System.arraycopy(keyIvCombo, 0, secretKey, 0, 32);
this.aeskey = new SecretKeySpec(secretKey, "AES"); this.aeskey = secretKey;
} else if (key.length >= 16) { } else if (keyIvCombo.length >= 16) {
byte[] secretKey = new byte[16]; byte[] secretKey = new byte[16];
System.arraycopy(key, 0, secretKey, 0, 16); System.arraycopy(keyIvCombo, 0, secretKey, 0, 16);
this.aeskey = new SecretKeySpec(secretKey, "AES"); this.aeskey = secretKey;
} }
} }
public Key getKey() { public void setKey(byte[] key) {
this.aeskey = key;
}
public void setIv(byte[] iv) {
this.iv = iv;
}
public byte[] getKey() {
return this.aeskey; return this.aeskey;
} }
public InputStream createInputStream() { public byte[] getIv() {
if (this.getKey() == null) { return this.iv;
try {
return new FileInputStream(this);
} catch (FileNotFoundException e) {
return null;
}
} else {
try {
IvParameterSpec ips = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips);
Log.d(Config.LOGTAG, "opening encrypted input stream");
return new CipherInputStream(new FileInputStream(this), cipher);
} catch (NoSuchAlgorithmException e) {
Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
return null;
} catch (NoSuchPaddingException e) {
Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
return null;
} catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
return null;
} catch (InvalidAlgorithmParameterException e) {
Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
return null;
} catch (FileNotFoundException e) {
return null;
}
}
}
public OutputStream createOutputStream() {
if (this.getKey() == null) {
try {
return new FileOutputStream(this);
} catch (FileNotFoundException e) {
return null;
}
} else {
try {
IvParameterSpec ips = new IvParameterSpec(this.iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips);
Log.d(Config.LOGTAG, "opening encrypted output stream");
return new CipherOutputStream(new FileOutputStream(this),
cipher);
} catch (NoSuchAlgorithmException e) {
Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
return null;
} catch (NoSuchPaddingException e) {
Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
return null;
} catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
return null;
} catch (InvalidAlgorithmParameterException e) {
Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
return null;
} catch (FileNotFoundException e) {
return null;
}
}
} }
} }

View file

@ -8,7 +8,7 @@ import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
@ -51,6 +51,7 @@ public class Message extends AbstractEntity {
public static final String ENCRYPTION = "encryption"; public static final String ENCRYPTION = "encryption";
public static final String STATUS = "status"; public static final String STATUS = "status";
public static final String TYPE = "type"; public static final String TYPE = "type";
public static final String CARBON = "carbon";
public static final String REMOTE_MSG_ID = "remoteMsgId"; public static final String REMOTE_MSG_ID = "remoteMsgId";
public static final String SERVER_MSG_ID = "serverMsgId"; public static final String SERVER_MSG_ID = "serverMsgId";
public static final String RELATIVE_FILE_PATH = "relativeFilePath"; public static final String RELATIVE_FILE_PATH = "relativeFilePath";
@ -68,6 +69,7 @@ public class Message extends AbstractEntity {
protected int encryption; protected int encryption;
protected int status; protected int status;
protected int type; protected int type;
protected boolean carbon = false;
protected String relativeFilePath; protected String relativeFilePath;
protected boolean read = true; protected boolean read = true;
protected String remoteMsgId = null; protected String remoteMsgId = null;
@ -85,8 +87,11 @@ public class Message extends AbstractEntity {
public Message(Conversation conversation, String body, int encryption) { public Message(Conversation conversation, String body, int encryption) {
this(conversation, body, encryption, STATUS_UNSEND); this(conversation, body, encryption, STATUS_UNSEND);
} }
public Message(Conversation conversation, String body, int encryption, int status) { public Message(Conversation conversation, String body, int encryption, int status) {
this(conversation, body, encryption, status, false);
}
public Message(Conversation conversation, String body, int encryption, int status, boolean carbon) {
this(java.util.UUID.randomUUID().toString(), this(java.util.UUID.randomUUID().toString(),
conversation.getUuid(), conversation.getUuid(),
conversation.getJid() == null ? null : conversation.getJid().toBareJid(), conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
@ -96,6 +101,7 @@ public class Message extends AbstractEntity {
encryption, encryption,
status, status,
TYPE_TEXT, TYPE_TEXT,
false,
null, null,
null, null,
null, null,
@ -105,8 +111,9 @@ public class Message extends AbstractEntity {
private Message(final String uuid, final String conversationUUid, final Jid counterpart, private Message(final String uuid, final String conversationUUid, final Jid counterpart,
final Jid trueCounterpart, final String body, final long timeSent, final Jid trueCounterpart, final String body, final long timeSent,
final int encryption, final int status, final int type, final String remoteMsgId, final int encryption, final int status, final int type, final boolean carbon,
final String relativeFilePath, final String serverMsgId, final String fingerprint) { final String remoteMsgId, final String relativeFilePath,
final String serverMsgId, final String fingerprint) {
this.uuid = uuid; this.uuid = uuid;
this.conversationUuid = conversationUUid; this.conversationUuid = conversationUUid;
this.counterpart = counterpart; this.counterpart = counterpart;
@ -116,6 +123,7 @@ public class Message extends AbstractEntity {
this.encryption = encryption; this.encryption = encryption;
this.status = status; this.status = status;
this.type = type; this.type = type;
this.carbon = carbon;
this.remoteMsgId = remoteMsgId; this.remoteMsgId = remoteMsgId;
this.relativeFilePath = relativeFilePath; this.relativeFilePath = relativeFilePath;
this.serverMsgId = serverMsgId; this.serverMsgId = serverMsgId;
@ -154,6 +162,7 @@ public class Message extends AbstractEntity {
cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
cursor.getInt(cursor.getColumnIndex(STATUS)), cursor.getInt(cursor.getColumnIndex(STATUS)),
cursor.getInt(cursor.getColumnIndex(TYPE)), cursor.getInt(cursor.getColumnIndex(TYPE)),
cursor.getInt(cursor.getColumnIndex(CARBON))>0,
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
@ -188,6 +197,7 @@ public class Message extends AbstractEntity {
values.put(ENCRYPTION, encryption); values.put(ENCRYPTION, encryption);
values.put(STATUS, status); values.put(STATUS, status);
values.put(TYPE, type); values.put(TYPE, type);
values.put(CARBON, carbon ? 1 : 0);
values.put(REMOTE_MSG_ID, remoteMsgId); values.put(REMOTE_MSG_ID, remoteMsgId);
values.put(RELATIVE_FILE_PATH, relativeFilePath); values.put(RELATIVE_FILE_PATH, relativeFilePath);
values.put(SERVER_MSG_ID, serverMsgId); values.put(SERVER_MSG_ID, serverMsgId);
@ -312,6 +322,14 @@ public class Message extends AbstractEntity {
this.type = type; this.type = type;
} }
public boolean isCarbon() {
return carbon;
}
public void setCarbon(boolean carbon) {
this.carbon = carbon;
}
public void setTrueCounterpart(Jid trueCounterpart) { public void setTrueCounterpart(Jid trueCounterpart) {
this.trueCounterpart = trueCounterpart; this.trueCounterpart = trueCounterpart;
} }
@ -416,11 +434,14 @@ public class Message extends AbstractEntity {
} }
public String getMergedBody() { public String getMergedBody() {
final Message next = this.next(); StringBuilder body = new StringBuilder(this.body.trim());
if (this.mergeable(next)) { Message current = this;
return getBody().trim() + MERGE_SEPARATOR + next.getMergedBody(); while(current.mergeable(current.next())) {
current = current.next();
body.append(MERGE_SEPARATOR);
body.append(current.getBody().trim());
} }
return getBody().trim(); return body.toString();
} }
public boolean hasMeCommand() { public boolean hasMeCommand() {
@ -428,20 +449,23 @@ public class Message extends AbstractEntity {
} }
public int getMergedStatus() { public int getMergedStatus() {
final Message next = this.next(); int status = this.status;
if (this.mergeable(next)) { Message current = this;
return next.getStatus(); while(current.mergeable(current.next())) {
current = current.next();
status = current.status;
} }
return getStatus(); return status;
} }
public long getMergedTimeSent() { public long getMergedTimeSent() {
Message next = this.next(); long time = this.timeSent;
if (this.mergeable(next)) { Message current = this;
return next.getMergedTimeSent(); while(current.mergeable(current.next())) {
} else { current = current.next();
return getTimeSent(); time = current.timeSent;
} }
return time;
} }
public boolean wasMergedIntoPrevious() { public boolean wasMergedIntoPrevious() {
@ -683,6 +707,37 @@ public class Message extends AbstractEntity {
public boolean isTrusted() { public boolean isTrusted() {
return conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint) return conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint)
== AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED; == XmppAxolotlSession.Trust.TRUSTED;
}
private int getPreviousEncryption() {
for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
continue;
}
return iterator.getEncryption();
}
return ENCRYPTION_NONE;
}
private int getNextEncryption() {
for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
continue;
}
return iterator.getEncryption();
}
return conversation.getNextEncryption();
}
public boolean isValidInSession() {
int pastEncryption = this.getPreviousEncryption();
int futureEncryption = this.getNextEncryption();
boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
|| futureEncryption == ENCRYPTION_NONE
|| pastEncryption != futureEncryption;
return inUnencryptedSession || this.getEncryption() == pastEncryption;
} }
} }

View file

@ -254,12 +254,15 @@ public class IqGenerator extends AbstractGenerator {
return packet; return packet;
} }
public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file) { public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET); IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(host); packet.setTo(host);
Element request = packet.addChild("request",Xmlns.HTTP_UPLOAD); Element request = packet.addChild("request",Xmlns.HTTP_UPLOAD);
request.addChild("filename").setContent(file.getName()); request.addChild("filename").setContent(file.getName());
request.addChild("size").setContent(String.valueOf(file.getExpectedSize())); request.addChild("size").setContent(String.valueOf(file.getExpectedSize()));
if (mime != null) {
request.addChild("content-type", mime);
}
return packet; return packet;
} }
} }

View file

@ -63,7 +63,7 @@ public class MessageGenerator extends AbstractGenerator {
if (axolotlMessage == null) { if (axolotlMessage == null) {
return null; return null;
} }
packet.setAxolotlMessage(axolotlMessage.toXml()); packet.setAxolotlMessage(axolotlMessage.toElement());
return packet; return packet;
} }

View file

@ -2,7 +2,6 @@ package eu.siacs.conversations.http;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
@ -21,6 +20,8 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
@ -83,7 +84,7 @@ public class HttpDownloadConnection implements Transferable {
this.file = mXmppConnectionService.getFileBackend().getFile(message, false); this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
String reference = mUrl.getRef(); String reference = mUrl.getRef();
if (reference != null && reference.length() == 96) { if (reference != null && reference.length() == 96) {
this.file.setKey(CryptoHelper.hexToBytes(reference)); this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
} }
if ((this.message.getEncryption() == Message.ENCRYPTION_OTR if ((this.message.getEncryption() == Message.ENCRYPTION_OTR
@ -187,6 +188,8 @@ public class HttpDownloadConnection implements Transferable {
private boolean interactive = false; private boolean interactive = false;
private OutputStream os;
public FileDownloader(boolean interactive) { public FileDownloader(boolean interactive) {
this.interactive = interactive; this.interactive = interactive;
} }
@ -199,14 +202,16 @@ public class HttpDownloadConnection implements Transferable {
updateImageBounds(); updateImageBounds();
finish(); finish();
} catch (SSLHandshakeException e) { } catch (SSLHandshakeException e) {
FileBackend.close(os);
changeStatus(STATUS_OFFER); changeStatus(STATUS_OFFER);
} catch (IOException e) { } catch (IOException e) {
FileBackend.close(os);
mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host); mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host);
cancel(); cancel();
} }
} }
private void download() throws SSLHandshakeException, IOException { private void download() throws IOException {
HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
if (connection instanceof HttpsURLConnection) { if (connection instanceof HttpsURLConnection) {
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
@ -215,10 +220,7 @@ public class HttpDownloadConnection implements Transferable {
BufferedInputStream is = new BufferedInputStream(connection.getInputStream()); BufferedInputStream is = new BufferedInputStream(connection.getInputStream());
file.getParentFile().mkdirs(); file.getParentFile().mkdirs();
file.createNewFile(); file.createNewFile();
OutputStream os = file.createOutputStream(); os = AbstractConnectionManager.createOutputStream(file,true);
if (os == null) {
throw new IOException();
}
long transmitted = 0; long transmitted = 0;
long expected = file.getExpectedSize(); long expected = file.getExpectedSize();
int count = -1; int count = -1;

View file

@ -1,7 +1,10 @@
package eu.siacs.conversations.http; package eu.siacs.conversations.http;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -18,6 +21,7 @@ import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UiCallback; import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
@ -37,13 +41,15 @@ public class HttpUploadConnection implements Transferable {
private Account account; private Account account;
private DownloadableFile file; private DownloadableFile file;
private Message message; private Message message;
private String mime;
private URL mGetUrl; private URL mGetUrl;
private URL mPutUrl; private URL mPutUrl;
private byte[] key = null; private byte[] key = null;
private long transmitted = 0; private long transmitted = 0;
private long expected = 1;
private InputStream mFileInputStream;
public HttpUploadConnection(HttpConnectionManager httpConnectionManager) { public HttpUploadConnection(HttpConnectionManager httpConnectionManager) {
this.mHttpConnectionManager = httpConnectionManager; this.mHttpConnectionManager = httpConnectionManager;
@ -67,7 +73,7 @@ public class HttpUploadConnection implements Transferable {
@Override @Override
public int getProgress() { public int getProgress() {
return (int) ((((double) transmitted) / expected) * 100); return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
} }
@Override @Override
@ -79,6 +85,7 @@ public class HttpUploadConnection implements Transferable {
mHttpConnectionManager.finishUploadConnection(this); mHttpConnectionManager.finishUploadConnection(this);
message.setTransferable(null); message.setTransferable(null);
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
FileBackend.close(mFileInputStream);
} }
public void init(Message message, boolean delay) { public void init(Message message, boolean delay) {
@ -87,19 +94,20 @@ public class HttpUploadConnection implements Transferable {
mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
this.account = message.getConversation().getAccount(); this.account = message.getConversation().getAccount();
this.file = mXmppConnectionService.getFileBackend().getFile(message, false); this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
this.file.setExpectedSize(this.file.getSize()); this.mime = this.file.getMimeType();
this.delayed = delay; this.delayed = delay;
if (Config.ENCRYPT_ON_HTTP_UPLOADED if (Config.ENCRYPT_ON_HTTP_UPLOADED
|| message.getEncryption() == Message.ENCRYPTION_AXOLOTL || message.getEncryption() == Message.ENCRYPTION_AXOLOTL
|| message.getEncryption() == Message.ENCRYPTION_OTR) { || message.getEncryption() == Message.ENCRYPTION_OTR) {
this.key = new byte[48]; this.key = new byte[48];
mXmppConnectionService.getRNG().nextBytes(this.key); mXmppConnectionService.getRNG().nextBytes(this.key);
this.file.setKey(this.key); this.file.setKeyAndIv(this.key);
} }
Pair<InputStream,Integer> pair = AbstractConnectionManager.createInputStream(file,true);
this.file.setExpectedSize(pair.second);
this.mFileInputStream = pair.first;
Jid host = account.getXmppConnection().findDiscoItemByFeature(Xmlns.HTTP_UPLOAD); Jid host = account.getXmppConnection().findDiscoItemByFeature(Xmlns.HTTP_UPLOAD);
IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file); IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file,mime);
mXmppConnectionService.sendIqPacket(account, request, new OnIqPacketReceived() { mXmppConnectionService.sendIqPacket(account, request, new OnIqPacketReceived() {
@Override @Override
public void onIqPacketReceived(Account account, IqPacket packet) { public void onIqPacketReceived(Account account, IqPacket packet) {
@ -134,7 +142,6 @@ public class HttpUploadConnection implements Transferable {
private void upload() { private void upload() {
OutputStream os = null; OutputStream os = null;
InputStream is = null;
HttpURLConnection connection = null; HttpURLConnection connection = null;
try { try {
Log.d(Config.LOGTAG, "uploading to " + mPutUrl.toString()); Log.d(Config.LOGTAG, "uploading to " + mPutUrl.toString());
@ -144,30 +151,31 @@ public class HttpUploadConnection implements Transferable {
} }
connection.setRequestMethod("PUT"); connection.setRequestMethod("PUT");
connection.setFixedLengthStreamingMode((int) file.getExpectedSize()); connection.setFixedLengthStreamingMode((int) file.getExpectedSize());
connection.setRequestProperty("Content-Type", mime == null ? "application/octet-stream" : mime);
connection.setDoOutput(true); connection.setDoOutput(true);
connection.connect(); connection.connect();
os = connection.getOutputStream(); os = connection.getOutputStream();
is = file.createInputStream();
transmitted = 0; transmitted = 0;
expected = file.getExpectedSize();
int count = -1; int count = -1;
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
while (((count = is.read(buffer)) != -1) && !canceled) { while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) {
transmitted += count; transmitted += count;
os.write(buffer, 0, count); os.write(buffer, 0, count);
mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateConversationUi();
} }
os.flush(); os.flush();
os.close(); os.close();
is.close(); mFileInputStream.close();
int code = connection.getResponseCode(); int code = connection.getResponseCode();
if (code == 200 || code == 201) { if (code == 200 || code == 201) {
Log.d(Config.LOGTAG, "finished uploading file"); Log.d(Config.LOGTAG, "finished uploading file");
Message.FileParams params = message.getFileParams();
if (key != null) { if (key != null) {
mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key)); mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key));
} }
mXmppConnectionService.getFileBackend().updateFileParams(message, mGetUrl); mXmppConnectionService.getFileBackend().updateFileParams(message, mGetUrl);
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(file));
mXmppConnectionService.sendBroadcast(intent);
message.setTransferable(null); message.setTransferable(null);
message.setCounterpart(message.getConversation().getJid().toBareJid()); message.setCounterpart(message.getConversation().getJid().toBareJid());
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
@ -194,10 +202,11 @@ public class HttpUploadConnection implements Transferable {
fail(); fail();
} }
} catch (IOException e) { } catch (IOException e) {
Log.d(Config.LOGTAG, e.getMessage()); e.printStackTrace();
Log.d(Config.LOGTAG,"http upload failed "+e.getMessage());
fail(); fail();
} finally { } finally {
FileBackend.close(is); FileBackend.close(mFileInputStream);
FileBackend.close(os); FileBackend.close(os);
if (connection != null) { if (connection != null) {
connection.disconnect(); connection.disconnect();

View file

@ -73,11 +73,9 @@ public class MessageParser extends AbstractParser implements
body = otrSession.transformReceiving(body); body = otrSession.transformReceiving(body);
SessionStatus status = otrSession.getSessionStatus(); SessionStatus status = otrSession.getSessionStatus();
if (body == null && status == SessionStatus.ENCRYPTED) { if (body == null && status == SessionStatus.ENCRYPTED) {
conversation.setNextEncryption(Message.ENCRYPTION_OTR);
mXmppConnectionService.onOtrSessionEstablished(conversation); mXmppConnectionService.onOtrSessionEstablished(conversation);
return null; return null;
} else if (body == null && status == SessionStatus.FINISHED) { } else if (body == null && status == SessionStatus.FINISHED) {
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
conversation.resetOtrSession(); conversation.resetOtrSession();
mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateConversationUi();
return null; return null;
@ -101,8 +99,8 @@ public class MessageParser extends AbstractParser implements
private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) { private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) {
Message finishedMessage = null; Message finishedMessage = null;
AxolotlService service = conversation.getAccount().getAxolotlService(); AxolotlService service = conversation.getAccount().getAxolotlService();
XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(from.toBareJid(), axolotlMessage); XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid());
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage);
if(plaintextMessage != null) { if(plaintextMessage != null) {
finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status); finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint()); finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint());
@ -202,6 +200,13 @@ public class MessageParser extends AbstractParser implements
if (packet.getType() == MessagePacket.TYPE_ERROR) { if (packet.getType() == MessagePacket.TYPE_ERROR) {
Jid from = packet.getFrom(); Jid from = packet.getFrom();
if (from != null) { if (from != null) {
Element error = packet.findChild("error");
String text = error == null ? null : error.findChildContent("text");
if (text != null) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + text);
} else if (error != null) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + error);
}
Message message = mXmppConnectionService.markMessage(account, Message message = mXmppConnectionService.markMessage(account,
from.toBareJid(), from.toBareJid(),
packet.getId(), packet.getId(),
@ -223,6 +228,7 @@ public class MessageParser extends AbstractParser implements
final MessagePacket packet; final MessagePacket packet;
Long timestamp = null; Long timestamp = null;
final boolean isForwarded; final boolean isForwarded;
boolean isCarbon = false;
String serverMsgId = null; String serverMsgId = null;
final Element fin = original.findChild("fin", "urn:xmpp:mam:0"); final Element fin = original.findChild("fin", "urn:xmpp:mam:0");
if (fin != null) { if (fin != null) {
@ -253,7 +259,8 @@ public class MessageParser extends AbstractParser implements
return; return;
} }
timestamp = f != null ? f.second : null; timestamp = f != null ? f.second : null;
isForwarded = f != null; isCarbon = f != null;
isForwarded = isCarbon;
} else { } else {
packet = original; packet = original;
isForwarded = false; isForwarded = false;
@ -265,7 +272,7 @@ public class MessageParser extends AbstractParser implements
final String body = packet.getBody(); final String body = packet.getBody();
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 String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
final Element axolotlEncrypted = packet.findChild("axolotl_message", AxolotlService.PEP_PREFIX); final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
int status; int status;
final Jid counterpart; final Jid counterpart;
final Jid to = packet.getTo(); final Jid to = packet.getTo();
@ -339,6 +346,7 @@ public class MessageParser extends AbstractParser implements
message.setCounterpart(counterpart); message.setCounterpart(counterpart);
message.setRemoteMsgId(remoteMsgId); message.setRemoteMsgId(remoteMsgId);
message.setServerMsgId(serverMsgId); message.setServerMsgId(serverMsgId);
message.setCarbon(isCarbon);
message.setTime(timestamp); message.setTime(timestamp);
message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0"); message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
if (conversation.getMode() == Conversation.MODE_MULTI) { if (conversation.getMode() == Conversation.MODE_MULTI) {

View file

@ -27,6 +27,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
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;
@ -40,7 +42,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null; private static DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history"; private static final String DATABASE_NAME = "history";
private static final int DATABASE_VERSION = 15; private static final int DATABASE_VERSION = 16;
private static String CREATE_CONTATCS_STATEMENT = "create table " private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@ -55,56 +57,56 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Contact.JID + ") ON CONFLICT REPLACE);"; + Contact.JID + ") ON CONFLICT REPLACE);";
private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE " private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
+ AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME + "(" + SQLiteAxolotlStore.PREKEY_TABLENAME + "("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.ID + " INTEGER, " + SQLiteAxolotlStore.ID + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ AxolotlService.SQLiteAxolotlStore.ID + SQLiteAxolotlStore.ID
+ ") ON CONFLICT REPLACE" + ") ON CONFLICT REPLACE"
+");"; +");";
private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE " private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE "
+ AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "(" + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.ID + " INTEGER, " + SQLiteAxolotlStore.ID + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ AxolotlService.SQLiteAxolotlStore.ID + SQLiteAxolotlStore.ID
+ ") ON CONFLICT REPLACE"+ + ") ON CONFLICT REPLACE"+
");"; ");";
private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE " private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE "
+ AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME + "(" + SQLiteAxolotlStore.SESSION_TABLENAME + "("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " + SQLiteAxolotlStore.NAME + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " + SQLiteAxolotlStore.DEVICE_ID + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ AxolotlService.SQLiteAxolotlStore.NAME + ", " + SQLiteAxolotlStore.NAME + ", "
+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID + SQLiteAxolotlStore.DEVICE_ID
+ ") ON CONFLICT REPLACE" + ") ON CONFLICT REPLACE"
+");"; +");";
private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE " private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE "
+ AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME + "(" + SQLiteAxolotlStore.IDENTITIES_TABLENAME + "("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " + SQLiteAxolotlStore.NAME + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.OWN + " INTEGER, " + SQLiteAxolotlStore.OWN + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " TEXT, " + SQLiteAxolotlStore.FINGERPRINT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, " + SQLiteAxolotlStore.TRUSTED + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ AxolotlService.SQLiteAxolotlStore.NAME + ", " + SQLiteAxolotlStore.NAME + ", "
+ AxolotlService.SQLiteAxolotlStore.FINGERPRINT + SQLiteAxolotlStore.FINGERPRINT
+ ") ON CONFLICT IGNORE" + ") ON CONFLICT IGNORE"
+");"; +");";
@ -139,6 +141,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.RELATIVE_FILE_PATH + " TEXT, " + Message.RELATIVE_FILE_PATH + " TEXT, "
+ Message.SERVER_MSG_ID + " TEXT, " + Message.SERVER_MSG_ID + " TEXT, "
+ Message.FINGERPRINT + " TEXT, " + Message.FINGERPRINT + " TEXT, "
+ Message.CARBON + " INTEGER, "
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES " + Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID + Conversation.TABLENAME + "(" + Conversation.UUID
@ -294,6 +297,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ Message.FINGERPRINT + " TEXT"); + Message.FINGERPRINT + " TEXT");
} }
if (oldVersion < 16 && newVersion >= 16) {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ Message.CARBON + " INTEGER");
}
} }
public static synchronized DatabaseBackend getInstance(Context context) { public static synchronized DatabaseBackend getInstance(Context context) {
@ -567,11 +574,11 @@ public class DatabaseBackend extends SQLiteOpenHelper {
String[] selectionArgs = {account.getUuid(), String[] selectionArgs = {account.getUuid(),
contact.getName(), contact.getName(),
Integer.toString(contact.getDeviceId())}; Integer.toString(contact.getDeviceId())};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
columns, columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " + SQLiteAxolotlStore.NAME + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " = ? ", + SQLiteAxolotlStore.DEVICE_ID + " = ? ",
selectionArgs, selectionArgs,
null, null, null); null, null, null);
@ -584,7 +591,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if(cursor.getCount() != 0) { if(cursor.getCount() != 0) {
cursor.moveToFirst(); cursor.moveToFirst();
try { try {
session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (IOException e) { } catch (IOException e) {
cursor.close(); cursor.close();
throw new AssertionError(e); throw new AssertionError(e);
@ -597,19 +604,19 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public List<Integer> getSubDeviceSessions(Account account, AxolotlAddress contact) { public List<Integer> getSubDeviceSessions(Account account, AxolotlAddress contact) {
List<Integer> devices = new ArrayList<>(); List<Integer> devices = new ArrayList<>();
final SQLiteDatabase db = this.getReadableDatabase(); final SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.DEVICE_ID}; String[] columns = {SQLiteAxolotlStore.DEVICE_ID};
String[] selectionArgs = {account.getUuid(), String[] selectionArgs = {account.getUuid(),
contact.getName()}; contact.getName()};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
columns, columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ?", + SQLiteAxolotlStore.NAME + " = ?",
selectionArgs, selectionArgs,
null, null, null); null, null, null);
while(cursor.moveToNext()) { while(cursor.moveToNext()) {
devices.add(cursor.getInt( devices.add(cursor.getInt(
cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.DEVICE_ID))); cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID)));
} }
cursor.close(); cursor.close();
@ -626,11 +633,11 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) { public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) {
SQLiteDatabase db = this.getWritableDatabase(); SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.NAME, contact.getName()); values.put(SQLiteAxolotlStore.NAME, contact.getName());
values.put(AxolotlService.SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId());
values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(),Base64.DEFAULT)); values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(),Base64.DEFAULT));
values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
db.insert(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, null, values); db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values);
} }
public void deleteSession(Account account, AxolotlAddress contact) { public void deleteSession(Account account, AxolotlAddress contact) {
@ -638,30 +645,30 @@ public class DatabaseBackend extends SQLiteOpenHelper {
String[] args = {account.getUuid(), String[] args = {account.getUuid(),
contact.getName(), contact.getName(),
Integer.toString(contact.getDeviceId())}; Integer.toString(contact.getDeviceId())};
db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " + SQLiteAxolotlStore.NAME + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " = ? ", + SQLiteAxolotlStore.DEVICE_ID + " = ? ",
args); args);
} }
public void deleteAllSessions(Account account, AxolotlAddress contact) { public void deleteAllSessions(Account account, AxolotlAddress contact) {
SQLiteDatabase db = this.getWritableDatabase(); SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(), contact.getName()}; String[] args = {account.getUuid(), contact.getName()};
db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ?", + SQLiteAxolotlStore.NAME + " = ?",
args); args);
} }
private Cursor getCursorForPreKey(Account account, int preKeyId) { private Cursor getCursorForPreKey(Account account, int preKeyId) {
SQLiteDatabase db = this.getReadableDatabase(); SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; String[] columns = {SQLiteAxolotlStore.KEY};
String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)}; String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, Cursor cursor = db.query(SQLiteAxolotlStore.PREKEY_TABLENAME,
columns, columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ AxolotlService.SQLiteAxolotlStore.ID + "=?", + SQLiteAxolotlStore.ID + "=?",
selectionArgs, selectionArgs,
null, null, null); null, null, null);
@ -674,7 +681,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if(cursor.getCount() != 0) { if(cursor.getCount() != 0) {
cursor.moveToFirst(); cursor.moveToFirst();
try { try {
record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (IOException e ) { } catch (IOException e ) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@ -693,28 +700,28 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public void storePreKey(Account account, PreKeyRecord record) { public void storePreKey(Account account, PreKeyRecord record) {
SQLiteDatabase db = this.getWritableDatabase(); SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId()); values.put(SQLiteAxolotlStore.ID, record.getId());
values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT)); values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT));
values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
db.insert(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, null, values); db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values);
} }
public void deletePreKey(Account account, int preKeyId) { public void deletePreKey(Account account, int preKeyId) {
SQLiteDatabase db = this.getWritableDatabase(); SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(), Integer.toString(preKeyId)}; String[] args = {account.getUuid(), Integer.toString(preKeyId)};
db.delete(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ AxolotlService.SQLiteAxolotlStore.ID + "=?", + SQLiteAxolotlStore.ID + "=?",
args); args);
} }
private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) { private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) {
SQLiteDatabase db = this.getReadableDatabase(); SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; String[] columns = {SQLiteAxolotlStore.KEY};
String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)}; String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
columns, columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + AxolotlService.SQLiteAxolotlStore.ID + "=?", SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
selectionArgs, selectionArgs,
null, null, null); null, null, null);
@ -727,7 +734,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if(cursor.getCount() != 0) { if(cursor.getCount() != 0) {
cursor.moveToFirst(); cursor.moveToFirst();
try { try {
record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (IOException e ) { } catch (IOException e ) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@ -739,17 +746,17 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) { public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) {
List<SignedPreKeyRecord> prekeys = new ArrayList<>(); List<SignedPreKeyRecord> prekeys = new ArrayList<>();
SQLiteDatabase db = this.getReadableDatabase(); SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; String[] columns = {SQLiteAxolotlStore.KEY};
String[] selectionArgs = {account.getUuid()}; String[] selectionArgs = {account.getUuid()};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
columns, columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=?", SQLiteAxolotlStore.ACCOUNT + "=?",
selectionArgs, selectionArgs,
null, null, null); null, null, null);
while(cursor.moveToNext()) { while(cursor.moveToNext()) {
try { try {
prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)), Base64.DEFAULT))); prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)));
} catch (IOException ignored) { } catch (IOException ignored) {
} }
} }
@ -767,18 +774,18 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public void storeSignedPreKey(Account account, SignedPreKeyRecord record) { public void storeSignedPreKey(Account account, SignedPreKeyRecord record) {
SQLiteDatabase db = this.getWritableDatabase(); SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId()); values.put(SQLiteAxolotlStore.ID, record.getId());
values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT)); values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT));
values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
db.insert(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values); db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values);
} }
public void deleteSignedPreKey(Account account, int signedPreKeyId) { public void deleteSignedPreKey(Account account, int signedPreKeyId) {
SQLiteDatabase db = this.getWritableDatabase(); SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)}; String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)};
db.delete(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ AxolotlService.SQLiteAxolotlStore.ID + "=?", + SQLiteAxolotlStore.ID + "=?",
args); args);
} }
@ -792,24 +799,24 @@ public class DatabaseBackend extends SQLiteOpenHelper {
private Cursor getIdentityKeyCursor(Account account, String name, Boolean own, String fingerprint) { private Cursor getIdentityKeyCursor(Account account, String name, Boolean own, String fingerprint) {
final SQLiteDatabase db = this.getReadableDatabase(); final SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.TRUSTED, String[] columns = {SQLiteAxolotlStore.TRUSTED,
AxolotlService.SQLiteAxolotlStore.KEY}; SQLiteAxolotlStore.KEY};
ArrayList<String> selectionArgs = new ArrayList<>(4); ArrayList<String> selectionArgs = new ArrayList<>(4);
selectionArgs.add(account.getUuid()); selectionArgs.add(account.getUuid());
String selectionString = AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?"; String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?";
if (name != null){ if (name != null){
selectionArgs.add(name); selectionArgs.add(name);
selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.NAME + " = ?"; selectionString += " AND " + SQLiteAxolotlStore.NAME + " = ?";
} }
if (fingerprint != null){ if (fingerprint != null){
selectionArgs.add(fingerprint); selectionArgs.add(fingerprint);
selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ?"; selectionString += " AND " + SQLiteAxolotlStore.FINGERPRINT + " = ?";
} }
if (own != null){ if (own != null){
selectionArgs.add(own?"1":"0"); selectionArgs.add(own?"1":"0");
selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.OWN + " = ?"; selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?";
} }
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME,
columns, columns,
selectionString, selectionString,
selectionArgs.toArray(new String[selectionArgs.size()]), selectionArgs.toArray(new String[selectionArgs.size()]),
@ -824,7 +831,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if(cursor.getCount() != 0) { if(cursor.getCount() != 0) {
cursor.moveToFirst(); cursor.moveToFirst();
try { try {
identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
} }
@ -838,18 +845,18 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return loadIdentityKeys(account, name, null); return loadIdentityKeys(account, name, null);
} }
public Set<IdentityKey> loadIdentityKeys(Account account, String name, AxolotlService.SQLiteAxolotlStore.Trust trust) { public Set<IdentityKey> loadIdentityKeys(Account account, String name, XmppAxolotlSession.Trust trust) {
Set<IdentityKey> identityKeys = new HashSet<>(); Set<IdentityKey> identityKeys = new HashSet<>();
Cursor cursor = getIdentityKeyCursor(account, name, false); Cursor cursor = getIdentityKeyCursor(account, name, false);
while(cursor.moveToNext()) { while(cursor.moveToNext()) {
if ( trust != null && if ( trust != null &&
cursor.getInt(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.TRUSTED)) cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED))
!= trust.getCode()) { != trust.getCode()) {
continue; continue;
} }
try { try {
identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0)); identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0));
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name);
} }
@ -864,55 +871,55 @@ public class DatabaseBackend extends SQLiteOpenHelper {
String[] args = { String[] args = {
account.getUuid(), account.getUuid(),
name, name,
String.valueOf(AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED.getCode()) String.valueOf(XmppAxolotlSession.Trust.TRUSTED.getCode())
}; };
return DatabaseUtils.queryNumEntries(db, AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?" SQLiteAxolotlStore.ACCOUNT + " = ?"
+ " AND " + AxolotlService.SQLiteAxolotlStore.NAME + " = ?" + " AND " + SQLiteAxolotlStore.NAME + " = ?"
+ " AND " + AxolotlService.SQLiteAxolotlStore.TRUSTED + " = ?", + " AND " + SQLiteAxolotlStore.TRUSTED + " = ?",
args args
); );
} }
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) { private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) {
storeIdentityKey(account, name, own, fingerprint, base64Serialized, AxolotlService.SQLiteAxolotlStore.Trust.UNDECIDED); storeIdentityKey(account, name, own, fingerprint, base64Serialized, XmppAxolotlSession.Trust.UNDECIDED);
} }
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, AxolotlService.SQLiteAxolotlStore.Trust trusted) { private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, XmppAxolotlSession.Trust trusted) {
SQLiteDatabase db = this.getWritableDatabase(); SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
values.put(AxolotlService.SQLiteAxolotlStore.NAME, name); values.put(SQLiteAxolotlStore.NAME, name);
values.put(AxolotlService.SQLiteAxolotlStore.OWN, own ? 1 : 0); values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0);
values.put(AxolotlService.SQLiteAxolotlStore.FINGERPRINT, fingerprint); values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
values.put(AxolotlService.SQLiteAxolotlStore.KEY, base64Serialized); values.put(SQLiteAxolotlStore.KEY, base64Serialized);
values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trusted.getCode()); values.put(SQLiteAxolotlStore.TRUSTED, trusted.getCode());
db.insert(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
} }
public AxolotlService.SQLiteAxolotlStore.Trust isIdentityKeyTrusted(Account account, String fingerprint) { public XmppAxolotlSession.Trust isIdentityKeyTrusted(Account account, String fingerprint) {
Cursor cursor = getIdentityKeyCursor(account, fingerprint); Cursor cursor = getIdentityKeyCursor(account, fingerprint);
AxolotlService.SQLiteAxolotlStore.Trust trust = null; XmppAxolotlSession.Trust trust = null;
if (cursor.getCount() > 0) { if (cursor.getCount() > 0) {
cursor.moveToFirst(); cursor.moveToFirst();
int trustValue = cursor.getInt(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.TRUSTED)); int trustValue = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED));
trust = AxolotlService.SQLiteAxolotlStore.Trust.fromCode(trustValue); trust = XmppAxolotlSession.Trust.fromCode(trustValue);
} }
cursor.close(); cursor.close();
return trust; return trust;
} }
public boolean setIdentityKeyTrust(Account account, String fingerprint, AxolotlService.SQLiteAxolotlStore.Trust trust) { public boolean setIdentityKeyTrust(Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
SQLiteDatabase db = this.getWritableDatabase(); SQLiteDatabase db = this.getWritableDatabase();
String[] selectionArgs = { String[] selectionArgs = {
account.getUuid(), account.getUuid(),
fingerprint fingerprint
}; };
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trust.getCode()); values.put(SQLiteAxolotlStore.TRUSTED, trust.getCode());
int rows = db.update(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? ", + SQLiteAxolotlStore.FINGERPRINT + " = ? ",
selectionArgs); selectionArgs);
return rows == 1; return rows == 1;
} }
@ -922,7 +929,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
} }
public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) { public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) {
storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED); storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), XmppAxolotlSession.Trust.TRUSTED);
} }
public void recreateAxolotlDb() { public void recreateAxolotlDb() {
@ -931,13 +938,13 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public void recreateAxolotlDb(SQLiteDatabase db) { public void recreateAxolotlDb(SQLiteDatabase db) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+">>> (RE)CREATING AXOLOTL DATABASE <<<"); Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+">>> (RE)CREATING AXOLOTL DATABASE <<<");
db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME); db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME);
db.execSQL(CREATE_SESSIONS_STATEMENT); db.execSQL(CREATE_SESSIONS_STATEMENT);
db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME); db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME);
db.execSQL(CREATE_PREKEYS_STATEMENT); db.execSQL(CREATE_PREKEYS_STATEMENT);
db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME);
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME); db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.IDENTITIES_TABLENAME);
db.execSQL(CREATE_IDENTITIES_STATEMENT); db.execSQL(CREATE_IDENTITIES_STATEMENT);
} }
@ -948,17 +955,17 @@ public class DatabaseBackend extends SQLiteOpenHelper {
String[] deleteArgs= { String[] deleteArgs= {
accountName accountName
}; };
db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs); deleteArgs);
db.delete(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs); deleteArgs);
db.delete(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs); deleteArgs);
db.delete(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, db.delete(SQLiteAxolotlStore.IDENTITIES_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs); deleteArgs);
} }
} }

View file

@ -1,5 +1,33 @@
package eu.siacs.conversations.services; package eu.siacs.conversations.services;
import android.util.Log;
import android.util.Pair;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.DownloadableFile;
public class AbstractConnectionManager { public class AbstractConnectionManager {
protected XmppConnectionService mXmppConnectionService; protected XmppConnectionService mXmppConnectionService;
@ -20,4 +48,73 @@ public class AbstractConnectionManager {
return 524288; return 524288;
} }
} }
public static Pair<InputStream,Integer> createInputStream(DownloadableFile file, boolean gcm) {
FileInputStream is;
int size;
try {
is = new FileInputStream(file);
size = (int) file.getSize();
if (file.getKey() == null) {
return new Pair<InputStream,Integer>(is,size);
}
} catch (FileNotFoundException e) {
return null;
}
try {
if (gcm) {
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
InputStream cis = new org.bouncycastle.crypto.io.CipherInputStream(is, cipher);
return new Pair<>(cis, cipher.getOutputSize(size));
} else {
IvParameterSpec ips = new IvParameterSpec(file.getIv());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips);
Log.d(Config.LOGTAG, "opening encrypted input stream");
return new Pair<InputStream,Integer>(new CipherInputStream(is, cipher),(size / 16 + 1) * 16);
}
} catch (InvalidKeyException e) {
return null;
} catch (NoSuchAlgorithmException e) {
return null;
} catch (NoSuchPaddingException e) {
return null;
} catch (InvalidAlgorithmParameterException e) {
return null;
}
}
public static OutputStream createOutputStream(DownloadableFile file, boolean gcm) {
FileOutputStream os;
try {
os = new FileOutputStream(file);
if (file.getKey() == null) {
return os;
}
} catch (FileNotFoundException e) {
return null;
}
try {
if (gcm) {
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
return new org.bouncycastle.crypto.io.CipherOutputStream(os, cipher);
} else {
IvParameterSpec ips = new IvParameterSpec(file.getIv());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips);
Log.d(Config.LOGTAG, "opening encrypted output stream");
return new CipherOutputStream(os, cipher);
}
} catch (InvalidKeyException e) {
return null;
} catch (NoSuchAlgorithmException e) {
return null;
} catch (NoSuchPaddingException e) {
return null;
} catch (InvalidAlgorithmParameterException e) {
return null;
}
}
} }

View file

@ -330,9 +330,10 @@ public class NotificationService {
private Message getImage(final Iterable<Message> messages) { private Message getImage(final Iterable<Message> messages) {
for (final Message message : messages) { for (final Message message : messages) {
if (message.getType() == Message.TYPE_IMAGE if (message.getType() != Message.TYPE_TEXT
&& message.getTransferable() == null && message.getTransferable() == null
&& message.getEncryption() != Message.ENCRYPTION_PGP) { && message.getEncryption() != Message.ENCRYPTION_PGP
&& message.getFileParams().height > 0) {
return message; return message;
} }
} }

View file

@ -349,7 +349,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void attachLocationToConversation(final Conversation conversation, public void attachLocationToConversation(final Conversation conversation,
final Uri uri, final Uri uri,
final UiCallback<Message> callback) { final UiCallback<Message> callback) {
int encryption = conversation.getNextEncryption(forceEncryption()); int encryption = conversation.getNextEncryption();
if (encryption == Message.ENCRYPTION_PGP) { if (encryption == Message.ENCRYPTION_PGP) {
encryption = Message.ENCRYPTION_DECRYPTED; encryption = Message.ENCRYPTION_DECRYPTED;
} }
@ -368,12 +368,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
final Uri uri, final Uri uri,
final UiCallback<Message> callback) { final UiCallback<Message> callback) {
final Message message; final Message message;
if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
message = new Message(conversation, "", message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
Message.ENCRYPTION_DECRYPTED);
} else { } else {
message = new Message(conversation, "", message = new Message(conversation, "", conversation.getNextEncryption());
conversation.getNextEncryption(forceEncryption()));
} }
message.setCounterpart(conversation.getNextCounterpart()); message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_FILE); message.setType(Message.TYPE_FILE);
@ -409,12 +407,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void attachImageToConversation(final Conversation conversation, public void attachImageToConversation(final Conversation conversation,
final Uri uri, final UiCallback<Message> callback) { final Uri uri, final UiCallback<Message> callback) {
final Message message; final Message message;
if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
message = new Message(conversation, "", message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
Message.ENCRYPTION_DECRYPTED);
} else { } else {
message = new Message(conversation, "", message = new Message(conversation, "",conversation.getNextEncryption());
conversation.getNextEncryption(forceEncryption()));
} }
message.setCounterpart(conversation.getNextCounterpart()); message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_IMAGE); message.setType(Message.TYPE_IMAGE);
@ -424,7 +420,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void run() { public void run() {
try { try {
getFileBackend().copyImageToPrivateStorage(message, uri); getFileBackend().copyImageToPrivateStorage(message, uri);
if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
getPgpEngine().encrypt(message, callback); getPgpEngine().encrypt(message, callback);
} else { } else {
callback.success(message); callback.success(message);
@ -759,6 +755,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
} }
break; break;
case Message.ENCRYPTION_AXOLOTL: case Message.ENCRYPTION_AXOLOTL:
message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", ""));
if (message.needsUploading()) { if (message.needsUploading()) {
if (account.httpUploadAvailable() || message.fixCounterpart()) { if (account.httpUploadAvailable() || message.fixCounterpart()) {
this.sendFileMessage(message,delay); this.sendFileMessage(message,delay);
@ -768,8 +765,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
} else { } else {
XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message); XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message);
if (axolotlMessage == null) { if (axolotlMessage == null) {
account.getAxolotlService().prepareMessage(message,delay); account.getAxolotlService().preparePayloadMessage(message, delay);
message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", ""));
} else { } else {
packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage); packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
} }
@ -2533,8 +2529,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
} }
} }
for (final Message msg : messages) { for (final Message msg : messages) {
msg.setTime(System.currentTimeMillis());
markMessage(msg, Message.STATUS_WAITING); markMessage(msg, Message.STATUS_WAITING);
this.resendMessage(msg,true); this.resendMessage(msg,false);
} }
} }

View file

@ -111,6 +111,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
private LinearLayout keys; private LinearLayout keys;
private LinearLayout tags; private LinearLayout tags;
private boolean showDynamicTags; private boolean showDynamicTags;
private String messageFingerprint;
private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() { private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
@ -193,6 +194,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
} catch (final InvalidJidException ignored) { } catch (final InvalidJidException ignored) {
} }
} }
this.messageFingerprint = getIntent().getStringExtra("fingerprint");
setContentView(R.layout.activity_contact_details); setContentView(R.layout.activity_contact_details);
contactJidTv = (TextView) findViewById(R.id.details_contactjid); contactJidTv = (TextView) findViewById(R.id.details_contactjid);
@ -386,7 +388,8 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
} }
for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys( for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys(
contact.getAccount(), contact.getJid().toBareJid().toString())) { contact.getAccount(), contact.getJid().toBareJid().toString())) {
hasKeys |= addFingerprintRow(keys, contact.getAccount(), identityKey); boolean highlight = identityKey.getFingerprint().replaceAll("\\s", "").equals(messageFingerprint);
hasKeys |= addFingerprintRow(keys, contact.getAccount(), identityKey, highlight);
} }
if (contact.getPgpKeyId() != 0) { if (contact.getPgpKeyId() != 0) {
hasKeys = true; hasKeys = true;

View file

@ -39,7 +39,7 @@ import de.timroes.android.listview.EnhancedListView;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.AxolotlService.SQLiteAxolotlStore.Trust; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Blockable;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
@ -402,7 +402,7 @@ public class ConversationActivity extends XmppActivity
} else { } else {
menuAdd.setVisible(!isConversationsOverviewHideable()); menuAdd.setVisible(!isConversationsOverviewHideable());
if (this.getSelectedConversation() != null) { if (this.getSelectedConversation() != null) {
if (this.getSelectedConversation().getNextEncryption(forceEncryption()) != Message.ENCRYPTION_NONE) { if (this.getSelectedConversation().getNextEncryption() != Message.ENCRYPTION_NONE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
menuSecure.setIcon(R.drawable.ic_lock_white_24dp); menuSecure.setIcon(R.drawable.ic_lock_white_24dp);
} else { } else {
@ -515,7 +515,7 @@ public class ConversationActivity extends XmppActivity
break; break;
} }
final Conversation conversation = getSelectedConversation(); final Conversation conversation = getSelectedConversation();
final int encryption = conversation.getNextEncryption(forceEncryption()); final int encryption = conversation.getNextEncryption();
if (encryption == Message.ENCRYPTION_PGP) { if (encryption == Message.ENCRYPTION_PGP) {
if (hasPgp()) { if (hasPgp()) {
if (conversation.getContact().getPgpKeyId() != 0) { if (conversation.getContact().getPgpKeyId() != 0) {
@ -792,6 +792,7 @@ public class ConversationActivity extends XmppActivity
xmppConnectionService.databaseBackend.updateConversation(conversation); xmppConnectionService.databaseBackend.updateConversation(conversation);
fragment.updateChatMsgHint(); fragment.updateChatMsgHint();
invalidateOptionsMenu(); invalidateOptionsMenu();
refreshUi();
return true; return true;
} }
}); });
@ -803,15 +804,10 @@ public class ConversationActivity extends XmppActivity
if (conversation.getMode() == Conversation.MODE_MULTI) { if (conversation.getMode() == Conversation.MODE_MULTI) {
otr.setEnabled(false); otr.setEnabled(false);
axolotl.setEnabled(false); axolotl.setEnabled(false);
} else { } else if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) {
if (forceEncryption()) {
none.setVisible(false);
}
}
if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) {
axolotl.setEnabled(false); axolotl.setEnabled(false);
} }
switch (conversation.getNextEncryption(forceEncryption())) { switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_NONE: case Message.ENCRYPTION_NONE:
none.setChecked(true); none.setChecked(true);
break; break;
@ -822,8 +818,7 @@ public class ConversationActivity extends XmppActivity
pgp.setChecked(true); pgp.setChecked(true);
break; break;
case Message.ENCRYPTION_AXOLOTL: case Message.ENCRYPTION_AXOLOTL:
popup.getMenu().findItem(R.id.encryption_choice_axolotl) axolotl.setChecked(true);
.setChecked(true);
break; break;
default: default:
none.setChecked(true); none.setChecked(true);
@ -836,8 +831,7 @@ public class ConversationActivity extends XmppActivity
protected void muteConversationDialog(final Conversation conversation) { protected void muteConversationDialog(final Conversation conversation) {
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.disable_notifications); builder.setTitle(R.string.disable_notifications);
final int[] durations = getResources().getIntArray( final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
R.array.mute_options_durations);
builder.setItems(R.array.mute_options_descriptions, builder.setItems(R.array.mute_options_descriptions,
new OnClickListener() { new OnClickListener() {
@ -1269,10 +1263,6 @@ public class ConversationActivity extends XmppActivity
}); });
} }
public boolean forceEncryption() {
return getPreferences().getBoolean("force_encryption", false);
}
public boolean useSendButtonToIndicateStatus() { public boolean useSendButtonToIndicateStatus() {
return getPreferences().getBoolean("send_button_status", false); return getPreferences().getBoolean("send_button_status", false);
} }
@ -1287,12 +1277,12 @@ public class ConversationActivity extends XmppActivity
protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) { protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService(); AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
boolean hasPendingKeys = !axolotlService.getKeysWithTrust(Trust.UNDECIDED, boolean hasPendingKeys = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED,
mSelectedConversation.getContact()).isEmpty() mSelectedConversation.getContact()).isEmpty()
|| !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty(); || !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
boolean hasNoTrustedKeys = axolotlService.getNumTrustedKeys(mSelectedConversation.getContact()) == 0; boolean hasNoTrustedKeys = axolotlService.getNumTrustedKeys(mSelectedConversation.getContact()) == 0;
if( hasPendingKeys || hasNoTrustedKeys) { if( hasPendingKeys || hasNoTrustedKeys) {
axolotlService.createSessionsIfNeeded(mSelectedConversation, false); axolotlService.createSessionsIfNeeded(mSelectedConversation);
Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class); Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class);
intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString()); intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString());
intent.putExtra("account", mSelectedConversation.getAccount().getJid().toBareJid().toString()); intent.putExtra("account", mSelectedConversation.getAccount().getJid().toBareJid().toString());

View file

@ -293,22 +293,26 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
if (body.length() == 0 || this.conversation == null) { if (body.length() == 0 || this.conversation == null) {
return; return;
} }
Message message = new Message(conversation, body, conversation.getNextEncryption(activity.forceEncryption())); Message message = new Message(conversation, body, conversation.getNextEncryption());
if (conversation.getMode() == Conversation.MODE_MULTI) { if (conversation.getMode() == Conversation.MODE_MULTI) {
if (conversation.getNextCounterpart() != null) { if (conversation.getNextCounterpart() != null) {
message.setCounterpart(conversation.getNextCounterpart()); message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_PRIVATE); message.setType(Message.TYPE_PRIVATE);
} }
} }
if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) { switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_OTR:
sendOtrMessage(message); sendOtrMessage(message);
} else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) { break;
case Message.ENCRYPTION_PGP:
sendPgpMessage(message); sendPgpMessage(message);
} else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_AXOLOTL) { break;
case Message.ENCRYPTION_AXOLOTL:
if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) { if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) {
sendAxolotlMessage(message); sendAxolotlMessage(message);
} }
} else { break;
default:
sendPlainTextMessage(message); sendPlainTextMessage(message);
} }
} }
@ -320,7 +324,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
R.string.send_private_message_to, R.string.send_private_message_to,
conversation.getNextCounterpart().getResourcepart())); conversation.getNextCounterpart().getResourcepart()));
} else { } else {
switch (conversation.getNextEncryption(activity.forceEncryption())) { switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_NONE: case Message.ENCRYPTION_NONE:
mEditMessage mEditMessage
.setHint(getString(R.string.send_plain_text_message)); .setHint(getString(R.string.send_plain_text_message));
@ -392,12 +396,13 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
highlightInConference(user); highlightInConference(user);
} }
} else { } else {
activity.switchToContactDetails(message.getContact()); activity.switchToContactDetails(message.getContact(), message.getAxolotlFingerprint());
} }
} else { } else {
Account account = message.getConversation().getAccount(); Account account = message.getConversation().getAccount();
Intent intent = new Intent(activity, EditAccountActivity.class); Intent intent = new Intent(activity, EditAccountActivity.class);
intent.putExtra("jid", account.getJid().toBareJid().toString()); intent.putExtra("jid", account.getJid().toBareJid().toString());
intent.putExtra("fingerprint", message.getAxolotlFingerprint());
startActivity(intent); startActivity(intent);
} }
} }
@ -832,7 +837,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
} catch (final NoSuchElementException ignored) { } catch (final NoSuchElementException ignored) {
} }
activity.xmppConnectionService.updateConversationUi(); activity.refreshUi();
} }
}); });
} }
@ -1210,11 +1215,11 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) { if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) {
final String body = mEditMessage.getText().toString(); final String body = mEditMessage.getText().toString();
Message message = new Message(conversation, body, conversation.getNextEncryption(activity.forceEncryption())); Message message = new Message(conversation, body, conversation.getNextEncryption());
sendAxolotlMessage(message); sendAxolotlMessage(message);
} else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) { } else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) {
int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID); int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID);
activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption(activity.forceEncryption())); activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption());
} }
} }
} }

View file

@ -74,6 +74,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
private Jid jidToEdit; private Jid jidToEdit;
private Account mAccount; private Account mAccount;
private String messageFingerprint;
private boolean mFetchingAvatar = false; private boolean mFetchingAvatar = false;
@ -388,6 +389,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
} catch (final InvalidJidException | NullPointerException ignored) { } catch (final InvalidJidException | NullPointerException ignored) {
this.jidToEdit = null; this.jidToEdit = null;
} }
this.messageFingerprint = getIntent().getStringExtra("fingerprint");
if (this.jidToEdit != null) { if (this.jidToEdit != null) {
this.mRegisterNew.setVisibility(View.GONE); this.mRegisterNew.setVisibility(View.GONE);
if (getActionBar() != null) { if (getActionBar() != null) {
@ -571,7 +573,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
if(ownKey.equals(identityKey)) { if(ownKey.equals(identityKey)) {
continue; continue;
} }
hasKeys |= addFingerprintRow(keys, mAccount, identityKey); boolean highlight = identityKey.getFingerprint().replaceAll("\\s", "").equals(messageFingerprint);
hasKeys |= addFingerprintRow(keys, mAccount, identityKey, highlight);
} }
if (hasKeys) { if (hasKeys) {
keysCard.setVisibility(View.VISIBLE); keysCard.setVisibility(View.VISIBLE);

View file

@ -16,7 +16,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService.SQLiteAxolotlStore.Trust; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
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;
@ -118,8 +118,8 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
boolean hasForeignKeys = false; boolean hasForeignKeys = false;
for(final IdentityKey identityKey : ownKeysToTrust.keySet()) { for(final IdentityKey identityKey : ownKeysToTrust.keySet()) {
hasOwnKeys = true; hasOwnKeys = true;
addFingerprintRowWithListeners(ownKeys, contact.getAccount(), identityKey, addFingerprintRowWithListeners(ownKeys, contact.getAccount(), identityKey, false,
Trust.fromBoolean(ownKeysToTrust.get(identityKey)), false, XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(identityKey)), false,
new CompoundButton.OnCheckedChangeListener() { new CompoundButton.OnCheckedChangeListener() {
@Override @Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
@ -134,8 +134,8 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
} }
for(final IdentityKey identityKey : foreignKeysToTrust.keySet()) { for(final IdentityKey identityKey : foreignKeysToTrust.keySet()) {
hasForeignKeys = true; hasForeignKeys = true;
addFingerprintRowWithListeners(foreignKeys, contact.getAccount(), identityKey, addFingerprintRowWithListeners(foreignKeys, contact.getAccount(), identityKey, false,
Trust.fromBoolean(foreignKeysToTrust.get(identityKey)), false, XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(identityKey)), false,
new CompoundButton.OnCheckedChangeListener() { new CompoundButton.OnCheckedChangeListener() {
@Override @Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
@ -171,11 +171,11 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
} }
private void getFingerprints(final Account account) { private void getFingerprints(final Account account) {
Set<IdentityKey> ownKeysSet = account.getAxolotlService().getKeysWithTrust(Trust.UNDECIDED); Set<IdentityKey> ownKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
Set<IdentityKey> foreignKeysSet = account.getAxolotlService().getKeysWithTrust(Trust.UNDECIDED, contact); Set<IdentityKey> foreignKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, contact);
if (hasNoTrustedKeys) { if (hasNoTrustedKeys) {
ownKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(Trust.UNTRUSTED)); ownKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED));
foreignKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(Trust.UNTRUSTED, contact)); foreignKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, contact));
} }
for(final IdentityKey identityKey : ownKeysSet) { for(final IdentityKey identityKey : ownKeysSet) {
if(!ownKeysToTrust.containsKey(identityKey)) { if(!ownKeysToTrust.containsKey(identityKey)) {
@ -226,12 +226,12 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
for(IdentityKey identityKey:ownKeysToTrust.keySet()) { for(IdentityKey identityKey:ownKeysToTrust.keySet()) {
contact.getAccount().getAxolotlService().setFingerprintTrust( contact.getAccount().getAxolotlService().setFingerprintTrust(
identityKey.getFingerprint().replaceAll("\\s", ""), identityKey.getFingerprint().replaceAll("\\s", ""),
Trust.fromBoolean(ownKeysToTrust.get(identityKey))); XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(identityKey)));
} }
for(IdentityKey identityKey:foreignKeysToTrust.keySet()) { for(IdentityKey identityKey:foreignKeysToTrust.keySet()) {
contact.getAccount().getAxolotlService().setFingerprintTrust( contact.getAccount().getAxolotlService().setFingerprintTrust(
identityKey.getFingerprint().replaceAll("\\s", ""), identityKey.getFingerprint().replaceAll("\\s", ""),
Trust.fromBoolean(foreignKeysToTrust.get(identityKey))); XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(identityKey)));
} }
} }

View file

@ -70,7 +70,7 @@ import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
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;
@ -351,7 +351,7 @@ public abstract class XmppActivity extends Activity {
mPrimaryTextColor = getResources().getColor(R.color.black87); mPrimaryTextColor = getResources().getColor(R.color.black87);
mSecondaryTextColor = getResources().getColor(R.color.black54); mSecondaryTextColor = getResources().getColor(R.color.black54);
mTertiaryTextColor = getResources().getColor(R.color.black12); mTertiaryTextColor = getResources().getColor(R.color.black12);
mColorRed = getResources().getColor(R.color.red500); mColorRed = getResources().getColor(R.color.red800);
mColorOrange = getResources().getColor(R.color.orange500); mColorOrange = getResources().getColor(R.color.orange500);
mColorGreen = getResources().getColor(R.color.green500); mColorGreen = getResources().getColor(R.color.green500);
mPrimaryColor = getResources().getColor(R.color.green500); mPrimaryColor = getResources().getColor(R.color.green500);
@ -424,10 +424,15 @@ public abstract class XmppActivity extends Activity {
} }
public void switchToContactDetails(Contact contact) { public void switchToContactDetails(Contact contact) {
switchToContactDetails(contact, null);
}
public void switchToContactDetails(Contact contact, String messageFingerprint) {
Intent intent = new Intent(this, ContactDetailsActivity.class); Intent intent = new Intent(this, ContactDetailsActivity.class);
intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
intent.putExtra("account", contact.getAccount().getJid().toBareJid().toString()); intent.putExtra("account", contact.getAccount().getJid().toBareJid().toString());
intent.putExtra("contact", contact.getJid().toString()); intent.putExtra("contact", contact.getJid().toString());
intent.putExtra("fingerprint", messageFingerprint);
startActivity(intent); startActivity(intent);
} }
@ -608,32 +613,25 @@ public abstract class XmppActivity extends Activity {
builder.create().show(); builder.create().show();
} }
protected boolean addFingerprintRow(LinearLayout keys, final Account account, IdentityKey identityKey) { protected boolean addFingerprintRow(LinearLayout keys, final Account account, IdentityKey identityKey, boolean highlight) {
final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", "");
final AxolotlService.SQLiteAxolotlStore.Trust trust = account.getAxolotlService() final XmppAxolotlSession.Trust trust = account.getAxolotlService()
.getFingerprintTrust(fingerprint); .getFingerprintTrust(fingerprint);
return addFingerprintRowWithListeners(keys, account, identityKey, trust, true, return addFingerprintRowWithListeners(keys, account, identityKey, highlight, trust, true,
new CompoundButton.OnCheckedChangeListener() { new CompoundButton.OnCheckedChangeListener() {
@Override @Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked != (trust == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED)) {
account.getAxolotlService().setFingerprintTrust(fingerprint, account.getAxolotlService().setFingerprintTrust(fingerprint,
(isChecked) ? AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED : (isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); XmppAxolotlSession.Trust.UNTRUSTED);
}
refreshUi();
xmppConnectionService.updateAccountUi();
xmppConnectionService.updateConversationUi();
} }
}, },
new View.OnClickListener() { new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
account.getAxolotlService().setFingerprintTrust(fingerprint, account.getAxolotlService().setFingerprintTrust(fingerprint,
AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); XmppAxolotlSession.Trust.UNTRUSTED);
refreshUi(); v.setEnabled(true);
xmppConnectionService.updateAccountUi();
xmppConnectionService.updateConversationUi();
} }
} }
@ -642,12 +640,13 @@ public abstract class XmppActivity extends Activity {
protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account, protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
final IdentityKey identityKey, final IdentityKey identityKey,
AxolotlService.SQLiteAxolotlStore.Trust trust, boolean highlight,
XmppAxolotlSession.Trust trust,
boolean showTag, boolean showTag,
CompoundButton.OnCheckedChangeListener CompoundButton.OnCheckedChangeListener
onCheckedChangeListener, onCheckedChangeListener,
View.OnClickListener onClickListener) { View.OnClickListener onClickListener) {
if (trust == AxolotlService.SQLiteAxolotlStore.Trust.COMPROMISED) { if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
return false; return false;
} }
View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false); View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
@ -668,7 +667,7 @@ public abstract class XmppActivity extends Activity {
switch (trust) { switch (trust) {
case UNTRUSTED: case UNTRUSTED:
case TRUSTED: case TRUSTED:
trustToggle.setChecked(trust == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED, false); trustToggle.setChecked(trust == XmppAxolotlSession.Trust.TRUSTED, false);
trustToggle.setEnabled(true); trustToggle.setEnabled(true);
key.setTextColor(getPrimaryTextColor()); key.setTextColor(getPrimaryTextColor());
keyType.setTextColor(getSecondaryTextColor()); keyType.setTextColor(getSecondaryTextColor());
@ -679,7 +678,15 @@ public abstract class XmppActivity extends Activity {
key.setTextColor(getPrimaryTextColor()); key.setTextColor(getPrimaryTextColor());
keyType.setTextColor(getSecondaryTextColor()); keyType.setTextColor(getSecondaryTextColor());
break; break;
case INACTIVE: case INACTIVE_UNTRUSTED:
case INACTIVE_UNDECIDED:
trustToggle.setOnClickListener(null);
trustToggle.setChecked(false, false);
trustToggle.setEnabled(false);
key.setTextColor(getTertiaryTextColor());
keyType.setTextColor(getTertiaryTextColor());
break;
case INACTIVE_TRUSTED:
trustToggle.setOnClickListener(null); trustToggle.setOnClickListener(null);
trustToggle.setChecked(true, false); trustToggle.setChecked(true, false);
trustToggle.setEnabled(false); trustToggle.setEnabled(false);
@ -693,6 +700,12 @@ public abstract class XmppActivity extends Activity {
} else { } else {
keyType.setVisibility(View.GONE); keyType.setVisibility(View.GONE);
} }
if (highlight) {
keyType.setTextColor(getResources().getColor(R.color.accent));
keyType.setText(getString(R.string.axolotl_fingerprint_selected_message));
} else {
keyType.setText(getString(R.string.axolotl_fingerprint));
}
key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint()));
keys.addView(view); keys.addView(view);

View file

@ -27,7 +27,7 @@ import android.widget.Toast;
import java.util.List; import java.util.List;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
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;
@ -96,19 +96,15 @@ public class MessageAdapter extends ArrayAdapter<Message> {
return this.getItemViewType(getItem(position)); return this.getItemViewType(getItem(position));
} }
private int getMessageTextColor(Message message) { private int getMessageTextColor(int type, boolean primary) {
int type = this.getItemViewType(message);
if (type == SENT) { if (type == SENT) {
return activity.getResources().getColor(R.color.black87); return activity.getResources().getColor(primary ? R.color.black87 : R.color.black54);
} else if (type == RECEIVED) { } else {
return activity.getResources().getColor(R.color.white); return activity.getResources().getColor(primary ? R.color.white : R.color.white70);
}
} }
return activity.getPrimaryTextColor(); private void displayStatus(ViewHolder viewHolder, Message message, int type) {
}
private void displayStatus(ViewHolder viewHolder, Message message) {
String filesize = null; String filesize = null;
String info = null; String info = null;
boolean error = false; boolean error = false;
@ -163,24 +159,37 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} }
break; break;
} }
if (error) { if (error && type == SENT) {
viewHolder.time.setTextColor(activity.getWarningTextColor()); viewHolder.time.setTextColor(activity.getWarningTextColor());
} else { } else {
viewHolder.time.setTextColor(this.getMessageTextColor(message)); viewHolder.time.setTextColor(this.getMessageTextColor(type,false));
} }
if (message.getEncryption() == Message.ENCRYPTION_NONE) { if (message.getEncryption() == Message.ENCRYPTION_NONE) {
viewHolder.indicator.setVisibility(View.GONE); viewHolder.indicator.setVisibility(View.GONE);
} else { } else {
viewHolder.indicator.setVisibility(View.VISIBLE); viewHolder.indicator.setVisibility(View.VISIBLE);
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
AxolotlService.SQLiteAxolotlStore.Trust trust = message.getConversation() XmppAxolotlSession.Trust trust = message.getConversation()
.getAccount().getAxolotlService().getFingerprintTrust( .getAccount().getAxolotlService().getFingerprintTrust(
message.getAxolotlFingerprint()); message.getAxolotlFingerprint());
if(trust == null || trust != AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED) { if(trust == null || trust != XmppAxolotlSession.Trust.TRUSTED) {
viewHolder.indicator.setColorFilter(Color.RED); viewHolder.indicator.setColorFilter(activity.getWarningTextColor());
viewHolder.indicator.setAlpha(1.0f);
} else { } else {
viewHolder.indicator.clearColorFilter(); viewHolder.indicator.clearColorFilter();
if (type == SENT) {
viewHolder.indicator.setAlpha(0.57f);
} else {
viewHolder.indicator.setAlpha(0.7f);
}
}
} else {
viewHolder.indicator.clearColorFilter();
if (type == SENT) {
viewHolder.indicator.setAlpha(0.57f);
} else {
viewHolder.indicator.setAlpha(0.7f);
} }
} }
} }
@ -214,19 +223,19 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} }
} }
private void displayInfoMessage(ViewHolder viewHolder, String text) { private void displayInfoMessage(ViewHolder viewHolder, String text, int type) {
if (viewHolder.download_button != null) { if (viewHolder.download_button != null) {
viewHolder.download_button.setVisibility(View.GONE); viewHolder.download_button.setVisibility(View.GONE);
} }
viewHolder.image.setVisibility(View.GONE); viewHolder.image.setVisibility(View.GONE);
viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody.setVisibility(View.VISIBLE);
viewHolder.messageBody.setText(text); viewHolder.messageBody.setText(text);
viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor()); viewHolder.messageBody.setTextColor(getMessageTextColor(type,false));
viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); viewHolder.messageBody.setTypeface(null, Typeface.ITALIC);
viewHolder.messageBody.setTextIsSelectable(false); viewHolder.messageBody.setTextIsSelectable(false);
} }
private void displayDecryptionFailed(ViewHolder viewHolder) { private void displayDecryptionFailed(ViewHolder viewHolder, int type) {
if (viewHolder.download_button != null) { if (viewHolder.download_button != null) {
viewHolder.download_button.setVisibility(View.GONE); viewHolder.download_button.setVisibility(View.GONE);
} }
@ -234,7 +243,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody.setVisibility(View.VISIBLE);
viewHolder.messageBody.setText(getContext().getString( viewHolder.messageBody.setText(getContext().getString(
R.string.decryption_failed)); R.string.decryption_failed));
viewHolder.messageBody.setTextColor(activity.getWarningTextColor()); viewHolder.messageBody.setTextColor(getMessageTextColor(type,false));
viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
viewHolder.messageBody.setTextIsSelectable(false); viewHolder.messageBody.setTextIsSelectable(false);
} }
@ -252,7 +261,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.messageBody.setText(span); viewHolder.messageBody.setText(span);
} }
private void displayTextMessage(final ViewHolder viewHolder, final Message message) { private void displayTextMessage(final ViewHolder viewHolder, final Message message, int type) {
if (viewHolder.download_button != null) { if (viewHolder.download_button != null) {
viewHolder.download_button.setVisibility(View.GONE); viewHolder.download_button.setVisibility(View.GONE);
} }
@ -310,7 +319,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else { } else {
viewHolder.messageBody.setText(""); viewHolder.messageBody.setText("");
} }
viewHolder.messageBody.setTextColor(this.getMessageTextColor(message)); viewHolder.messageBody.setTextColor(this.getMessageTextColor(type,true));
viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
viewHolder.messageBody.setTextIsSelectable(true); viewHolder.messageBody.setTextIsSelectable(true);
} }
@ -519,7 +528,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else if (transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) { } else if (transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message))); displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)));
} else { } else {
displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first); displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first,type);
} }
} else if (message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { } else if (message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
displayImageMessage(viewHolder, message); displayImageMessage(viewHolder, message);
@ -531,10 +540,9 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} }
} else if (message.getEncryption() == Message.ENCRYPTION_PGP) { } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
if (activity.hasPgp()) { if (activity.hasPgp()) {
displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message)); displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message),type);
} else { } else {
displayInfoMessage(viewHolder, displayInfoMessage(viewHolder,activity.getString(R.string.install_openkeychain),type);
activity.getString(R.string.install_openkeychain));
if (viewHolder != null) { if (viewHolder != null) {
viewHolder.message_box viewHolder.message_box
.setOnClickListener(new OnClickListener() { .setOnClickListener(new OnClickListener() {
@ -547,7 +555,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} }
} }
} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
displayDecryptionFailed(viewHolder); displayDecryptionFailed(viewHolder,type);
} else { } else {
if (GeoHelper.isGeoUri(message.getBody())) { if (GeoHelper.isGeoUri(message.getBody())) {
displayLocationMessage(viewHolder,message); displayLocationMessage(viewHolder,message);
@ -556,11 +564,19 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else if (message.treatAsDownloadable() == Message.Decision.MUST) { } else if (message.treatAsDownloadable() == Message.Decision.MUST) {
displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message))); displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)));
} else { } else {
displayTextMessage(viewHolder, message); displayTextMessage(viewHolder, message, type);
} }
} }
displayStatus(viewHolder, message); if (type == RECEIVED) {
if(message.isValidInSession()) {
viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received);
} else {
viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning);
}
}
displayStatus(viewHolder, message, type);
return view; return view;
} }

View file

@ -38,17 +38,14 @@ public class DNSHelper {
public static Bundle getSRVRecord(final Jid jid) throws IOException { public static Bundle getSRVRecord(final Jid jid) throws IOException {
final String host = jid.getDomainpart(); final String host = jid.getDomainpart();
String dns[] = client.findDNS(); String dns[] = client.findDNS();
for (int i = 0; i < dns.length; ++i) {
if (dns != null) { InetAddress ip = InetAddress.getByName(dns[i]);
for (String dnsserver : dns) {
InetAddress ip = InetAddress.getByName(dnsserver);
Bundle b = queryDNS(host, ip); Bundle b = queryDNS(host, ip);
if (b.containsKey("values")) { if (b.containsKey("values") || i == dns.length - 1) {
return b; return b;
} }
} }
} return null;
return queryDNS(host, InetAddress.getByName("8.8.8.8"));
} }
public static Bundle queryDNS(String host, InetAddress dnsServer) { public static Bundle queryDNS(String host, InetAddress dnsServer) {
@ -132,7 +129,6 @@ public class DNSHelper {
} catch (SocketTimeoutException e) { } catch (SocketTimeoutException e) {
bundle.putString("error", "timeout"); bundle.putString("error", "timeout");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace();
bundle.putString("error", "unhandled"); bundle.putString("error", "unhandled");
} }
return bundle; return bundle;

View file

@ -164,6 +164,9 @@ public class XmppConnection implements Runnable {
} }
} else { } else {
final Bundle result = DNSHelper.getSRVRecord(account.getServer()); final Bundle result = DNSHelper.getSRVRecord(account.getServer());
if (result == null) {
throw new IOException("unhandled exception in DNS resolver");
}
final ArrayList<Parcelable> values = result.getParcelableArrayList("values"); final ArrayList<Parcelable> values = result.getParcelableArrayList("values");
if ("timeout".equals(result.getString("error"))) { if ("timeout".equals(result.getString("error"))) {
throw new IOException("timeout in dns"); throw new IOException("timeout in dns");

View file

@ -2,9 +2,11 @@ package eu.siacs.conversations.xmpp.jingle;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
@ -14,13 +16,19 @@ import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.OnMessageCreatedCallback;
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.DownloadableFile; import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jid.Jid;
@ -66,8 +74,13 @@ public class JingleConnection implements Transferable {
private boolean acceptedAutomatically = false; private boolean acceptedAutomatically = false;
private XmppAxolotlMessage mXmppAxolotlMessage;
private JingleTransport transport = null; private JingleTransport transport = null;
private OutputStream mFileOutputStream;
private InputStream mFileInputStream;
private OnIqPacketReceived responseListener = new OnIqPacketReceived() { private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
@Override @Override
@ -113,6 +126,14 @@ public class JingleConnection implements Transferable {
} }
}; };
public InputStream getFileInputStream() {
return this.mFileInputStream;
}
public OutputStream getFileOutputStream() {
return this.mFileOutputStream;
}
private OnProxyActivated onProxyActivated = new OnProxyActivated() { private OnProxyActivated onProxyActivated = new OnProxyActivated() {
@Override @Override
@ -194,7 +215,22 @@ public class JingleConnection implements Transferable {
mXmppConnectionService.sendIqPacket(account,response,null); mXmppConnectionService.sendIqPacket(account,response,null);
} }
public void init(Message message) { public void init(final Message message) {
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
Conversation conversation = message.getConversation();
conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation.getContact(), new OnMessageCreatedCallback() {
@Override
public void run(XmppAxolotlMessage xmppAxolotlMessage) {
init(message, xmppAxolotlMessage);
}
});
} else {
init(message, null);
}
}
private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) {
this.mXmppAxolotlMessage = xmppAxolotlMessage;
this.contentCreator = "initiator"; this.contentCreator = "initiator";
this.contentName = this.mJingleConnectionManager.nextRandomId(); this.contentName = this.mJingleConnectionManager.nextRandomId();
this.message = message; this.message = message;
@ -238,8 +274,7 @@ public class JingleConnection implements Transferable {
}); });
mergeCandidate(candidate); mergeCandidate(candidate);
} else { } else {
Log.d(Config.LOGTAG, Log.d(Config.LOGTAG,"no primary candidate of our own was found");
"no primary candidate of our own was found");
sendInitRequest(); sendInitRequest();
} }
} }
@ -267,13 +302,16 @@ public class JingleConnection implements Transferable {
this.contentCreator = content.getAttribute("creator"); this.contentCreator = content.getAttribute("creator");
this.contentName = content.getAttribute("name"); this.contentName = content.getAttribute("name");
this.transportId = content.getTransportId(); this.transportId = content.getTransportId();
this.mergeCandidates(JingleCandidate.parse(content.socks5transport() this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
.getChildren()));
this.fileOffer = packet.getJingleContent().getFileOffer(); this.fileOffer = packet.getJingleContent().getFileOffer();
mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null); mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null);
if (fileOffer != null) { if (fileOffer != null) {
Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
if (encrypted != null) {
this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().toBareJid());
}
Element fileSize = fileOffer.findChild("size"); Element fileSize = fileOffer.findChild("size");
Element fileNameElement = fileOffer.findChild("name"); Element fileNameElement = fileOffer.findChild("name");
if (fileNameElement != null) { if (fileNameElement != null) {
@ -319,10 +357,8 @@ public class JingleConnection implements Transferable {
message.setBody(Long.toString(size)); message.setBody(Long.toString(size));
conversation.add(message); conversation.add(message);
mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateConversationUi();
if (size < this.mJingleConnectionManager if (size < this.mJingleConnectionManager.getAutoAcceptFileSize()) {
.getAutoAcceptFileSize()) { Log.d(Config.LOGTAG, "auto accepting file from "+ packet.getFrom());
Log.d(Config.LOGTAG, "auto accepting file from "
+ packet.getFrom());
this.acceptedAutomatically = true; this.acceptedAutomatically = true;
this.sendAccept(); this.sendAccept();
} else { } else {
@ -333,22 +369,32 @@ public class JingleConnection implements Transferable {
+ " allowed size:" + " allowed size:"
+ this.mJingleConnectionManager + this.mJingleConnectionManager
.getAutoAcceptFileSize()); .getAutoAcceptFileSize());
this.mXmppConnectionService.getNotificationService() this.mXmppConnectionService.getNotificationService().push(message);
.push(message);
} }
this.file = this.mXmppConnectionService.getFileBackend() this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
.getFile(message, false); if (mXmppAxolotlMessage != null) {
if (message.getEncryption() == Message.ENCRYPTION_OTR) { XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage);
if (transportMessage != null) {
message.setEncryption(Message.ENCRYPTION_AXOLOTL);
this.file.setKey(transportMessage.getKey());
this.file.setIv(transportMessage.getIv());
message.setAxolotlFingerprint(transportMessage.getFingerprint());
} else {
Log.d(Config.LOGTAG,"could not process KeyTransportMessage");
}
} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
byte[] key = conversation.getSymmetricKey(); byte[] key = conversation.getSymmetricKey();
if (key == null) { if (key == null) {
this.sendCancel(); this.sendCancel();
this.fail(); this.fail();
return; return;
} else { } else {
this.file.setKey(key); this.file.setKeyAndIv(key);
} }
} }
this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file,message.getEncryption() == Message.ENCRYPTION_AXOLOTL);
this.file.setExpectedSize(size); this.file.setExpectedSize(size);
Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
} else { } else {
this.sendCancel(); this.sendCancel();
this.fail(); this.fail();
@ -364,19 +410,30 @@ public class JingleConnection implements Transferable {
Content content = new Content(this.contentCreator, this.contentName); Content content = new Content(this.contentCreator, this.contentName);
if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
content.setTransportId(this.transportId); content.setTransportId(this.transportId);
this.file = this.mXmppConnectionService.getFileBackend().getFile( this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
message, false); Pair<InputStream,Integer> pair;
if (message.getEncryption() == Message.ENCRYPTION_OTR) { if (message.getEncryption() == Message.ENCRYPTION_OTR) {
Conversation conversation = this.message.getConversation(); Conversation conversation = this.message.getConversation();
if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) { if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not set symmetric key"); Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not set symmetric key");
cancel(); cancel();
} }
this.file.setKeyAndIv(conversation.getSymmetricKey());
pair = AbstractConnectionManager.createInputStream(this.file,false);
this.file.setExpectedSize(pair.second);
content.setFileOffer(this.file, true); content.setFileOffer(this.file, true);
this.file.setKey(conversation.getSymmetricKey()); } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
this.file.setKey(mXmppAxolotlMessage.getInnerKey());
this.file.setIv(mXmppAxolotlMessage.getIV());
pair = AbstractConnectionManager.createInputStream(this.file,true);
this.file.setExpectedSize(pair.second);
content.setFileOffer(this.file, false).addChild(mXmppAxolotlMessage.toElement());
} else { } else {
pair = AbstractConnectionManager.createInputStream(this.file,false);
this.file.setExpectedSize(pair.second);
content.setFileOffer(this.file, false); content.setFileOffer(this.file, false);
} }
this.mFileInputStream = pair.first;
this.transportId = this.mJingleConnectionManager.nextRandomId(); this.transportId = this.mJingleConnectionManager.nextRandomId();
content.setTransportId(this.transportId); content.setTransportId(this.transportId);
content.socks5transport().setChildren(getCandidatesAsElements()); content.socks5transport().setChildren(getCandidatesAsElements());
@ -748,6 +805,8 @@ public class JingleConnection implements Transferable {
if (this.transport != null && this.transport instanceof JingleInbandTransport) { if (this.transport != null && this.transport instanceof JingleInbandTransport) {
this.transport.disconnect(); this.transport.disconnect();
} }
FileBackend.close(mFileInputStream);
FileBackend.close(mFileOutputStream);
if (this.message != null) { if (this.message != null) {
if (this.responder.equals(account.getJid())) { if (this.responder.equals(account.getJid())) {
this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED)); this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));

View file

@ -93,7 +93,7 @@ public class JingleInbandTransport extends JingleTransport {
digest.reset(); digest.reset();
file.getParentFile().mkdirs(); file.getParentFile().mkdirs();
file.createNewFile(); file.createNewFile();
this.fileOutputStream = file.createOutputStream(); this.fileOutputStream = connection.getFileOutputStream();
if (this.fileOutputStream == null) { if (this.fileOutputStream == null) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not create output stream"); Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not create output stream");
callback.onFileTransferAborted(); callback.onFileTransferAborted();
@ -112,15 +112,11 @@ public class JingleInbandTransport extends JingleTransport {
this.onFileTransmissionStatusChanged = callback; this.onFileTransmissionStatusChanged = callback;
this.file = file; this.file = file;
try { try {
if (this.file.getKey() != null) { this.remainingSize = this.file.getExpectedSize();
this.remainingSize = (this.file.getSize() / 16 + 1) * 16;
} else {
this.remainingSize = this.file.getSize();
}
this.fileSize = this.remainingSize; this.fileSize = this.remainingSize;
this.digest = MessageDigest.getInstance("SHA-1"); this.digest = MessageDigest.getInstance("SHA-1");
this.digest.reset(); this.digest.reset();
fileInputStream = this.file.createInputStream(); fileInputStream = connection.getFileInputStream();
if (fileInputStream == null) { if (fileInputStream == null) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could no create input stream"); Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could no create input stream");
callback.onFileTransferAborted(); callback.onFileTransferAborted();

View file

@ -106,13 +106,13 @@ public class JingleSocks5Transport extends JingleTransport {
try { try {
MessageDigest digest = MessageDigest.getInstance("SHA-1"); MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.reset(); digest.reset();
fileInputStream = file.createInputStream(); fileInputStream = connection.getFileInputStream();
if (fileInputStream == null) { if (fileInputStream == null) {
Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create input stream"); Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create input stream");
callback.onFileTransferAborted(); callback.onFileTransferAborted();
return; return;
} }
long size = file.getSize(); long size = file.getExpectedSize();
long transmitted = 0; long transmitted = 0;
int count; int count;
byte[] buffer = new byte[8192]; byte[] buffer = new byte[8192];
@ -157,7 +157,7 @@ public class JingleSocks5Transport extends JingleTransport {
socket.setSoTimeout(30000); socket.setSoTimeout(30000);
file.getParentFile().mkdirs(); file.getParentFile().mkdirs();
file.createNewFile(); file.createNewFile();
fileOutputStream = file.createOutputStream(); fileOutputStream = connection.getFileOutputStream();
if (fileOutputStream == null) { if (fileOutputStream == null) {
callback.onFileTransferAborted(); callback.onFileTransferAborted();
Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create output stream"); Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create output stream");

View file

@ -1,5 +1,31 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import android.util.Log;
import android.util.Pair;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.DownloadableFile;
public abstract class JingleTransport { public abstract class JingleTransport {

View file

@ -25,17 +25,18 @@ public class Content extends Element {
this.transportId = sid; this.transportId = sid;
} }
public void setFileOffer(DownloadableFile actualFile, boolean otr) { public Element setFileOffer(DownloadableFile actualFile, boolean otr) {
Element description = this.addChild("description", Element description = this.addChild("description",
"urn:xmpp:jingle:apps:file-transfer:3"); "urn:xmpp:jingle:apps:file-transfer:3");
Element offer = description.addChild("offer"); Element offer = description.addChild("offer");
Element file = offer.addChild("file"); Element file = offer.addChild("file");
file.addChild("size").setContent(Long.toString(actualFile.getSize())); file.addChild("size").setContent(Long.toString(actualFile.getExpectedSize()));
if (otr) { if (otr) {
file.addChild("name").setContent(actualFile.getName() + ".otr"); file.addChild("name").setContent(actualFile.getName() + ".otr");
} else { } else {
file.addChild("name").setContent(actualFile.getName()); file.addChild("name").setContent(actualFile.getName());
} }
return file;
} }
public Element getFileOffer() { public Element getFileOffer() {

View file

@ -39,6 +39,9 @@ public class IqPacket extends AbstractStanza {
public TYPE getType() { public TYPE getType() {
final String type = getAttribute("type"); final String type = getAttribute("type");
if (type == null) {
return TYPE.INVALID;
}
switch (type) { switch (type) {
case "error": case "error":
return TYPE.ERROR; return TYPE.ERROR;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 B

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 B

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 929 B

After

Width:  |  Height:  |  Size: 929 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 857 B

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -28,6 +28,7 @@
android:textColor="@color/black54" android:textColor="@color/black54"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_below="@+id/key" android:layout_below="@+id/key"
android:maxLines="1"
android:textSize="?attr/TextSizeInfo"/> android:textSize="?attr/TextSizeInfo"/>
<TextView <TextView

View file

@ -91,7 +91,6 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:text="@string/sending" android:text="@string/sending"
android:textColor="@color/white70" android:textColor="@color/white70"
android:alpha="0.70"
android:textSize="?attr/TextSizeInfo" /> android:textSize="?attr/TextSizeInfo" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View file

@ -81,7 +81,6 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:text="@string/sending" android:text="@string/sending"
android:textColor="@color/black54" android:textColor="@color/black54"
android:alpha="0.54"
android:textSize="?attr/TextSizeInfo" /> android:textSize="?attr/TextSizeInfo" />
<ImageView <ImageView

View file

@ -13,5 +13,6 @@
<color name="grey200">#ffeeeeee</color> <color name="grey200">#ffeeeeee</color>
<color name="grey800">#ff424242</color> <color name="grey800">#ff424242</color>
<color name="red500">#fff44336</color> <color name="red500">#fff44336</color>
<color name="red800">#ffc62828</color>
<color name="orange500">#ffff9800</color> <color name="orange500">#ffff9800</color>
</resources> </resources>

View file

@ -80,7 +80,7 @@
<string name="choose_presence">Choose presence to contact</string> <string name="choose_presence">Choose presence to contact</string>
<string name="send_plain_text_message">Send plain text message</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_otr_message">Send OTR encrypted message</string>
<string name="send_axolotl_message">Send Axolotl encrypted message</string> <string name="send_axolotl_message">Send Multi-End encrypted message</string>
<string name="send_pgp_message">Send OpenPGP 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="your_nick_has_been_changed">Your nickname has been changed</string>
<string name="send_unencrypted">Send unencrypted</string> <string name="send_unencrypted">Send unencrypted</string>
@ -155,7 +155,7 @@
<string name="encryption_choice_none">Plain text</string> <string name="encryption_choice_none">Plain text</string>
<string name="encryption_choice_otr">OTR</string> <string name="encryption_choice_otr">OTR</string>
<string name="encryption_choice_pgp">OpenPGP</string> <string name="encryption_choice_pgp">OpenPGP</string>
<string name="encryption_choice_axolotl">Axolotl</string> <string name="encryption_choice_axolotl">Multi-End</string>
<string name="mgmt_account_edit">Edit account</string> <string name="mgmt_account_edit">Edit account</string>
<string name="mgmt_account_delete">Delete account</string> <string name="mgmt_account_delete">Delete account</string>
<string name="mgmt_account_disable">Temporarily disable</string> <string name="mgmt_account_disable">Temporarily disable</string>
@ -208,13 +208,14 @@
<string name="reception_failed">Reception failed</string> <string name="reception_failed">Reception failed</string>
<string name="your_fingerprint">Your fingerprint</string> <string name="your_fingerprint">Your fingerprint</string>
<string name="otr_fingerprint">OTR fingerprint</string> <string name="otr_fingerprint">OTR fingerprint</string>
<string name="axolotl_fingerprint">Axolotl fingerprint</string> <string name="axolotl_fingerprint">Multi-End fingerprint</string>
<string name="this_device_axolotl_fingerprint">Own Axolotl fingerprint</string> <string name="axolotl_fingerprint_selected_message">Multi-End fingerprint of message</string>
<string name="this_device_axolotl_fingerprint">Own Multi-End fingerprint</string>
<string name="other_devices">Other devices</string> <string name="other_devices">Other devices</string>
<string name="trust_keys">Trust Axolotl Keys</string> <string name="trust_keys">Trust Multi-End Keys</string>
<string name="fetching_keys">Fetching keys...</string> <string name="fetching_keys">Fetching keys...</string>
<string name="done">Done</string> <string name="done">Done</string>
<string name="axolotl_devicelist">Other own Axolotl Devices</string> <string name="axolotl_devicelist">Other own Multi-End Devices</string>
<string name="verify">Verify</string> <string name="verify">Verify</string>
<string name="decrypt">Decrypt</string> <string name="decrypt">Decrypt</string>
<string name="conferences">Conferences</string> <string name="conferences">Conferences</string>
@ -321,7 +322,7 @@
<string name="pref_conference_name">Conference name</string> <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="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_otr_fingerprint">OTR fingerprint copied to clipboard!</string>
<string name="toast_message_axolotl_fingerprint">Axolotl fingerprint copied to clipboard!</string> <string name="toast_message_axolotl_fingerprint">Multi-End fingerprint copied to clipboard!</string>
<string name="conference_banned">You are banned from this conference</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_members_only">This conference is members only</string>
<string name="conference_kicked">You have been kicked from this conference</string> <string name="conference_kicked">You have been kicked from this conference</string>
@ -388,11 +389,11 @@
<string name="reset">Reset</string> <string name="reset">Reset</string>
<string name="account_image_description">Account avatar</string> <string name="account_image_description">Account avatar</string>
<string name="copy_otr_clipboard_description">Copy OTR fingerprint to clipboard</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="copy_axolotl_clipboard_description">Copy Multi-End fingerprint to clipboard</string>
<string name="regenerate_axolotl_key">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="wipe_axolotl_pep">Wipe other devices from PEP</string>
<string name="clear_other_devices">Clear devices</string> <string name="clear_other_devices">Clear devices</string>
<string name="clear_other_devices_desc">Are you sure you want to clear all other devices from the axolotl announcement? The next time your devices connect, they will reannounce themselves, but they might not receive messages sent in the meantime.</string> <string name="clear_other_devices_desc">Are you sure you want to clear all other devices from the Multi-End announcement? The next time your devices connect, they will reannounce themselves, but they might not receive messages sent in the meantime.</string>
<string name="purge_key">Purge key</string> <string name="purge_key">Purge key</string>
<string name="purge_key_desc_part1">Are you sure you want to purge this key?</string> <string name="purge_key_desc_part1">Are you sure you want to purge this key?</string>
<string name="purge_key_desc_part2">It will irreversibly be considered compromised, and you can never build a session with it again.</string> <string name="purge_key_desc_part2">It will irreversibly be considered compromised, and you can never build a session with it again.</string>
@ -473,7 +474,6 @@
<string name="received_location">Received location</string> <string name="received_location">Received location</string>
<string name="title_undo_swipe_out_conversation">Conversation closed</string> <string name="title_undo_swipe_out_conversation">Conversation closed</string>
<string name="title_undo_swipe_out_muc">Left conference</string> <string name="title_undo_swipe_out_muc">Left conference</string>
<string name="pref_certificate_options">Certificate options</string>
<string name="pref_dont_trust_system_cas_title">Dont trust system CAs</string> <string name="pref_dont_trust_system_cas_title">Dont trust system CAs</string>
<string name="pref_dont_trust_system_cas_summary">All certificates must be manually approved</string> <string name="pref_dont_trust_system_cas_summary">All certificates must be manually approved</string>
<string name="pref_remove_trusted_certificates_title">Remove certificates</string> <string name="pref_remove_trusted_certificates_title">Remove certificates</string>

View file

@ -37,8 +37,8 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:title="@string/pref_notification_settings" android:key="notifications"
android:key="notifications"> android:title="@string/pref_notification_settings">
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="true" android:defaultValue="true"
android:key="show_notification" android:key="show_notification"
@ -46,9 +46,9 @@
android:title="@string/pref_notifications"/> android:title="@string/pref_notifications"/>
<PreferenceScreen <PreferenceScreen
android:dependency="show_notification" android:dependency="show_notification"
android:key="quiet_hours"
android:summary="@string/pref_quiet_hours_summary" android:summary="@string/pref_quiet_hours_summary"
android:title="@string/title_pref_quiet_hours" android:title="@string/title_pref_quiet_hours">
android:key="quiet_hours">
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="enable_quiet_hours" android:key="enable_quiet_hours"
@ -106,13 +106,13 @@
android:summary="@string/pref_use_send_button_to_indicate_status_summary" android:summary="@string/pref_use_send_button_to_indicate_status_summary"
android:title="@string/pref_use_send_button_to_indicate_status"/> android:title="@string/pref_use_send_button_to_indicate_status"/>
<ListPreference <ListPreference
android:key="quick_action"
android:defaultValue="recent" android:defaultValue="recent"
android:dialogTitle="@string/choose_quick_action"
android:entries="@array/quick_actions" android:entries="@array/quick_actions"
android:entryValues="@array/quick_action_values" android:entryValues="@array/quick_action_values"
android:key="quick_action"
android:summary="@string/pref_quick_action_summary" android:summary="@string/pref_quick_action_summary"
android:title="@string/pref_quick_action" android:title="@string/pref_quick_action"/>
android:dialogTitle="@string/choose_quick_action"/>
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="show_dynamic_tags" android:key="show_dynamic_tags"
@ -120,46 +120,39 @@
android:title="@string/pref_show_dynamic_tags"/> android:title="@string/pref_show_dynamic_tags"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:title="@string/pref_advanced_options" android:key="advanced"
android:key="advanced"> android:title="@string/pref_advanced_options">
<PreferenceScreen <PreferenceScreen
android:key="expert"
android:summary="@string/pref_expert_options_summary" android:summary="@string/pref_expert_options_summary"
android:title="@string/pref_expert_options" android:title="@string/pref_expert_options">
android:key="expert">
<PreferenceCategory android:title="@string/pref_encryption_settings"> <PreferenceCategory android:title="@string/pref_encryption_settings">
<CheckBoxPreference
android:defaultValue="false"
android:key="force_encryption"
android:summary="@string/pref_force_encryption_summary"
android:title="@string/pref_force_encryption" />
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="dont_save_encrypted" android:key="dont_save_encrypted"
android:summary="@string/pref_dont_save_encrypted_summary" android:summary="@string/pref_dont_save_encrypted_summary"
android:title="@string/pref_dont_save_encrypted"/> android:title="@string/pref_dont_save_encrypted"/>
<CheckBoxPreference
android:defaultValue="false"
android:key="dont_trust_system_cas"
android:summary="@string/pref_dont_trust_system_cas_summary"
android:title="@string/pref_dont_trust_system_cas_title"/>
<Preference
android:key="remove_trusted_certificates"
android:summary="@string/pref_remove_trusted_certificates_summary"
android:title="@string/pref_remove_trusted_certificates_title"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/pref_input_options"> <PreferenceCategory android:title="@string/pref_input_options">
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="enter_is_send" android:key="enter_is_send"
android:title="@string/pref_enter_is_send" android:summary="@string/pref_enter_is_send_summary"
android:summary="@string/pref_enter_is_send_summary" /> android:title="@string/pref_enter_is_send"/>
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="display_enter_key" android:key="display_enter_key"
android:title="@string/pref_display_enter_key" android:summary="@string/pref_display_enter_key_summary"
android:summary="@string/pref_display_enter_key_summary" /> android:title="@string/pref_display_enter_key"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/pref_certificate_options">
<CheckBoxPreference
android:defaultValue="false"
android:key="dont_trust_system_cas"
android:title="@string/pref_dont_trust_system_cas_title"
android:summary="@string/pref_dont_trust_system_cas_summary" />
<Preference
android:key="remove_trusted_certificates"
android:title="@string/pref_remove_trusted_certificates_title"
android:summary="@string/pref_remove_trusted_certificates_summary" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/pref_expert_options_other"> <PreferenceCategory android:title="@string/pref_expert_options_other">
<CheckBoxPreference <CheckBoxPreference
@ -170,8 +163,8 @@
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="true" android:defaultValue="true"
android:key="keep_foreground_service" android:key="keep_foreground_service"
android:title="@string/pref_keep_foreground_service" android:summary="@string/pref_keep_foreground_service_summary"
android:summary="@string/pref_keep_foreground_service_summary" /> android:title="@string/pref_keep_foreground_service"/>
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>