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