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}