XmppAxolotlSession.java

  1package eu.siacs.conversations.crypto.axolotl;
  2
  3import android.support.annotation.NonNull;
  4import android.support.annotation.Nullable;
  5import android.util.Log;
  6
  7import org.whispersystems.libsignal.SignalProtocolAddress;
  8import org.whispersystems.libsignal.DuplicateMessageException;
  9import org.whispersystems.libsignal.IdentityKey;
 10import org.whispersystems.libsignal.InvalidKeyException;
 11import org.whispersystems.libsignal.InvalidKeyIdException;
 12import org.whispersystems.libsignal.InvalidMessageException;
 13import org.whispersystems.libsignal.InvalidVersionException;
 14import org.whispersystems.libsignal.LegacyMessageException;
 15import org.whispersystems.libsignal.NoSessionException;
 16import org.whispersystems.libsignal.SessionCipher;
 17import org.whispersystems.libsignal.UntrustedIdentityException;
 18import org.whispersystems.libsignal.protocol.CiphertextMessage;
 19import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
 20import org.whispersystems.libsignal.protocol.SignalMessage;
 21import org.whispersystems.libsignal.util.guava.Optional;
 22
 23import java.util.Iterator;
 24import java.util.List;
 25
 26import eu.siacs.conversations.Config;
 27import eu.siacs.conversations.entities.Account;
 28import eu.siacs.conversations.utils.CryptoHelper;
 29
 30public class XmppAxolotlSession implements Comparable<XmppAxolotlSession> {
 31	private final SessionCipher cipher;
 32	private final SQLiteAxolotlStore sqLiteAxolotlStore;
 33	private final SignalProtocolAddress remoteAddress;
 34	private final Account account;
 35	private IdentityKey identityKey;
 36	private Integer preKeyId = null;
 37	private boolean fresh = true;
 38
 39	public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, SignalProtocolAddress remoteAddress, IdentityKey identityKey) {
 40		this(account, store, remoteAddress);
 41		this.identityKey = identityKey;
 42	}
 43
 44	public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, SignalProtocolAddress remoteAddress) {
 45		this.cipher = new SessionCipher(store, remoteAddress);
 46		this.remoteAddress = remoteAddress;
 47		this.sqLiteAxolotlStore = store;
 48		this.account = account;
 49	}
 50
 51	public Integer getPreKeyIdAndReset() {
 52		final Integer preKeyId = this.preKeyId;
 53		this.preKeyId = null;
 54		return preKeyId;
 55	}
 56
 57	public String getFingerprint() {
 58		return identityKey == null ? null : CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize());
 59	}
 60
 61	public IdentityKey getIdentityKey() {
 62		return identityKey;
 63	}
 64
 65	public SignalProtocolAddress getRemoteAddress() {
 66		return remoteAddress;
 67	}
 68
 69	public boolean isFresh() {
 70		return fresh;
 71	}
 72
 73	public void setNotFresh() {
 74		this.fresh = false;
 75	}
 76
 77	protected void setTrust(FingerprintStatus status) {
 78		sqLiteAxolotlStore.setFingerprintStatus(getFingerprint(), status);
 79	}
 80
 81	public FingerprintStatus getTrust() {
 82		FingerprintStatus status = sqLiteAxolotlStore.getFingerprintStatus(getFingerprint());
 83		return (status == null) ? FingerprintStatus.createActiveUndecided() : status;
 84	}
 85
 86	@Nullable
 87	byte[] processReceiving(List<AxolotlKey> possibleKeys) throws CryptoFailedException {
 88		byte[] plaintext = null;
 89		FingerprintStatus status = getTrust();
 90		if (!status.isCompromised()) {
 91			Iterator<AxolotlKey> iterator = possibleKeys.iterator();
 92			while (iterator.hasNext()) {
 93				AxolotlKey encryptedKey = iterator.next();
 94				try {
 95					if (encryptedKey.prekey) {
 96						PreKeySignalMessage preKeySignalMessage = new PreKeySignalMessage(encryptedKey.key);
 97						Optional<Integer> optionalPreKeyId = preKeySignalMessage.getPreKeyId();
 98						IdentityKey identityKey = preKeySignalMessage.getIdentityKey();
 99						if (!optionalPreKeyId.isPresent()) {
100							if (iterator.hasNext()) {
101								continue;
102							}
103							throw new CryptoFailedException("PreKeyWhisperMessage did not contain a PreKeyId");
104						}
105						preKeyId = optionalPreKeyId.get();
106						if (this.identityKey != null && !this.identityKey.equals(identityKey)) {
107							if (iterator.hasNext()) {
108								continue;
109							}
110							throw new CryptoFailedException("Received PreKeyWhisperMessage but preexisting identity key changed.");
111						}
112						this.identityKey = identityKey;
113						plaintext = cipher.decrypt(preKeySignalMessage);
114					} else {
115						SignalMessage signalMessage = new SignalMessage(encryptedKey.key);
116						try {
117							plaintext = cipher.decrypt(signalMessage);
118						} catch (InvalidMessageException | NoSessionException e) {
119							if (iterator.hasNext()) {
120								Log.w(Config.LOGTAG,account.getJid().asBareJid()+": ignoring crypto exception because possible keys left to try",e);
121								continue;
122							}
123							throw new BrokenSessionException(this.remoteAddress, e);
124						}
125						preKeyId = null; //better safe than sorry because we use that to do special after prekey handling
126					}
127				} catch (InvalidVersionException | InvalidKeyException | LegacyMessageException | InvalidMessageException | DuplicateMessageException | InvalidKeyIdException | UntrustedIdentityException e) {
128					if (iterator.hasNext()) {
129						Log.w(Config.LOGTAG,account.getJid().asBareJid()+": ignoring crypto exception because possible keys left to try",e);
130						continue;
131					}
132					throw new CryptoFailedException("Error decrypting SignalMessage", e);
133				}
134				if (iterator.hasNext()) {
135					break;
136				}
137			}
138			if (!status.isActive()) {
139				setTrust(status.toActive());
140				//TODO: also (re)add to device list?
141			}
142		} else {
143			throw new CryptoFailedException("not encrypting omemo message from fingerprint "+getFingerprint()+" because it was marked as compromised");
144		}
145		return plaintext;
146	}
147
148	@Nullable
149	public AxolotlKey processSending(@NonNull byte[] outgoingMessage, boolean ignoreSessionTrust) {
150		FingerprintStatus status = getTrust();
151		if (ignoreSessionTrust || status.isTrustedAndActive()) {
152			try {
153				CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
154				return new AxolotlKey(getRemoteAddress().getDeviceId(), ciphertextMessage.serialize(),ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE);
155			} catch (UntrustedIdentityException e) {
156				return null;
157			}
158		} else {
159			return null;
160		}
161	}
162
163	public Account getAccount() {
164		return account;
165	}
166
167	@Override
168	public int compareTo(XmppAxolotlSession o) {
169		return getTrust().compareTo(o.getTrust());
170	}
171
172	public static class AxolotlKey {
173
174
175		public final byte[] key;
176		public final boolean prekey;
177		public final int deviceId;
178
179		public AxolotlKey(int deviceId, byte[] key, boolean prekey) {
180			this.deviceId = deviceId;
181			this.key = key;
182			this.prekey = prekey;
183		}
184	}
185}