1package eu.siacs.conversations.crypto.axolotl;
2
3import android.util.Base64;
4import android.util.Log;
5import android.util.SparseArray;
6
7
8import java.security.InvalidAlgorithmParameterException;
9import java.security.InvalidKeyException;
10import java.security.NoSuchAlgorithmException;
11import java.security.NoSuchProviderException;
12import java.security.SecureRandom;
13import java.util.HashMap;
14import java.util.List;
15import java.util.Map;
16
17import javax.crypto.BadPaddingException;
18import javax.crypto.Cipher;
19import javax.crypto.IllegalBlockSizeException;
20import javax.crypto.KeyGenerator;
21import javax.crypto.NoSuchPaddingException;
22import javax.crypto.SecretKey;
23import javax.crypto.spec.IvParameterSpec;
24import javax.crypto.spec.SecretKeySpec;
25
26import eu.siacs.conversations.Config;
27import eu.siacs.conversations.utils.Compatibility;
28import eu.siacs.conversations.xml.Element;
29import rocks.xmpp.addr.Jid;
30
31public class XmppAxolotlMessage {
32 public static final String CONTAINERTAG = "encrypted";
33 public static final String HEADER = "header";
34 public static final String SOURCEID = "sid";
35 public static final String KEYTAG = "key";
36 public static final String REMOTEID = "rid";
37 public static final String IVTAG = "iv";
38 public static final String PAYLOAD = "payload";
39
40 private static final String KEYTYPE = "AES";
41 private static final String CIPHERMODE = "AES/GCM/NoPadding";
42 private static final String PROVIDER = "BC";
43
44 private byte[] innerKey;
45 private byte[] ciphertext = null;
46 private byte[] authtagPlusInnerKey = null;
47 private byte[] iv = null;
48 private final SparseArray<XmppAxolotlSession.AxolotlKey> keys;
49 private final Jid from;
50 private final int sourceDeviceId;
51
52 public static class XmppAxolotlPlaintextMessage {
53 private final String plaintext;
54 private final String fingerprint;
55
56 public XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) {
57 this.plaintext = plaintext;
58 this.fingerprint = fingerprint;
59 }
60
61 public String getPlaintext() {
62 return plaintext;
63 }
64
65
66 public String getFingerprint() {
67 return fingerprint;
68 }
69 }
70
71 public static class XmppAxolotlKeyTransportMessage {
72 private final String fingerprint;
73 private final byte[] key;
74 private final byte[] iv;
75
76 public XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) {
77 this.fingerprint = fingerprint;
78 this.key = key;
79 this.iv = iv;
80 }
81
82 public String getFingerprint() {
83 return fingerprint;
84 }
85
86 public byte[] getKey() {
87 return key;
88 }
89
90 public byte[] getIv() {
91 return iv;
92 }
93 }
94
95 public static int parseSourceId(final Element axolotlMessage) throws IllegalArgumentException {
96 final Element header = axolotlMessage.findChild(HEADER);
97 if (header == null) {
98 throw new IllegalArgumentException("No header found");
99 }
100 try {
101 return Integer.parseInt(header.getAttribute(SOURCEID));
102 } catch (NumberFormatException e) {
103 throw new IllegalArgumentException("invalid source id");
104 }
105 }
106
107 private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException {
108 this.from = from;
109 Element header = axolotlMessage.findChild(HEADER);
110 try {
111 this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID));
112 } catch (NumberFormatException e) {
113 throw new IllegalArgumentException("invalid source id");
114 }
115 List<Element> keyElements = header.getChildren();
116 this.keys = new SparseArray<>();
117 for (Element keyElement : keyElements) {
118 switch (keyElement.getName()) {
119 case KEYTAG:
120 try {
121 Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
122 byte[] key = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT);
123 boolean isPreKey =keyElement.getAttributeAsBoolean("prekey");
124 this.keys.put(recipientId, new XmppAxolotlSession.AxolotlKey(key,isPreKey));
125 } catch (NumberFormatException e) {
126 throw new IllegalArgumentException("invalid remote id");
127 }
128 break;
129 case IVTAG:
130 if (this.iv != null) {
131 throw new IllegalArgumentException("Duplicate iv entry");
132 }
133 iv = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT);
134 break;
135 default:
136 Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString());
137 break;
138 }
139 }
140 Element payloadElement = axolotlMessage.findChild(PAYLOAD);
141 if (payloadElement != null) {
142 ciphertext = Base64.decode(payloadElement.getContent().trim(), Base64.DEFAULT);
143 }
144 }
145
146 public XmppAxolotlMessage(Jid from, int sourceDeviceId) {
147 this.from = from;
148 this.sourceDeviceId = sourceDeviceId;
149 this.keys = new SparseArray<>();
150 this.iv = generateIv();
151 this.innerKey = generateKey();
152 }
153
154 public static XmppAxolotlMessage fromElement(Element element, Jid from) {
155 return new XmppAxolotlMessage(element, from);
156 }
157
158 private static byte[] generateKey() {
159 try {
160 KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE);
161 generator.init(128);
162 return generator.generateKey().getEncoded();
163 } catch (NoSuchAlgorithmException e) {
164 Log.e(Config.LOGTAG, e.getMessage());
165 return null;
166 }
167 }
168
169 private static byte[] generateIv() {
170 SecureRandom random = new SecureRandom();
171 byte[] iv = new byte[16];
172 random.nextBytes(iv);
173 return iv;
174 }
175
176 public boolean hasPayload() {
177 return ciphertext != null;
178 }
179
180 public void encrypt(String plaintext) throws CryptoFailedException {
181 try {
182 SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE);
183 IvParameterSpec ivSpec = new IvParameterSpec(iv);
184 Cipher cipher = Compatibility.twentyTwo() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
185 cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
186 this.ciphertext = cipher.doFinal(Config.OMEMO_PADDING ? getPaddedBytes(plaintext) : plaintext.getBytes());
187 if (Config.PUT_AUTH_TAG_INTO_KEY && this.ciphertext != null) {
188 this.authtagPlusInnerKey = new byte[16+16];
189 byte[] ciphertext = new byte[this.ciphertext.length - 16];
190 System.arraycopy(this.ciphertext,0,ciphertext,0,ciphertext.length);
191 System.arraycopy(this.ciphertext,ciphertext.length,authtagPlusInnerKey,16,16);
192 System.arraycopy(this.innerKey,0,authtagPlusInnerKey,0,this.innerKey.length);
193 this.ciphertext = ciphertext;
194 }
195 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
196 | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException
197 | InvalidAlgorithmParameterException e) {
198 throw new CryptoFailedException(e);
199 }
200 }
201
202 private static byte[] getPaddedBytes(String plaintext) {
203 int plainLength = plaintext.getBytes().length;
204 int pad = Math.max(64,(plainLength / 32 + 1) * 32) - plainLength;
205 SecureRandom random = new SecureRandom();
206 int left = random.nextInt(pad);
207 int right = pad - left;
208 StringBuilder builder = new StringBuilder(plaintext);
209 for(int i = 0; i < left; ++i) {
210 builder.insert(0,random.nextBoolean() ? "\t" : " ");
211 }
212 for(int i = 0; i < right; ++i) {
213 builder.append(random.nextBoolean() ? "\t" : " ");
214 }
215 return builder.toString().getBytes();
216 }
217
218 public Jid getFrom() {
219 return this.from;
220 }
221
222 int getSenderDeviceId() {
223 return sourceDeviceId;
224 }
225
226 void addDevice(XmppAxolotlSession session) {
227 addDevice(session, false);
228 }
229
230 void addDevice(XmppAxolotlSession session, boolean ignoreSessionTrust) {
231 XmppAxolotlSession.AxolotlKey key;
232 if (authtagPlusInnerKey != null) {
233 key = session.processSending(authtagPlusInnerKey, ignoreSessionTrust);
234 } else {
235 key = session.processSending(innerKey, ignoreSessionTrust);
236 }
237 if (key != null) {
238 keys.put(session.getRemoteAddress().getDeviceId(), key);
239 }
240 }
241
242 public byte[] getInnerKey() {
243 return innerKey;
244 }
245
246 public byte[] getIV() {
247 return this.iv;
248 }
249
250 public Element toElement() {
251 Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX);
252 Element headerElement = encryptionElement.addChild(HEADER);
253 headerElement.setAttribute(SOURCEID, sourceDeviceId);
254 for(int i = 0; i < keys.size(); ++i) {
255 Element keyElement = new Element(KEYTAG);
256 keyElement.setAttribute(REMOTEID, keys.keyAt(i));
257 if (keys.valueAt(i).prekey) {
258 keyElement.setAttribute("prekey","true");
259 }
260 keyElement.setContent(Base64.encodeToString(keys.valueAt(i).key, Base64.NO_WRAP));
261 headerElement.addChild(keyElement);
262 }
263 headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.NO_WRAP));
264 if (ciphertext != null) {
265 Element payload = encryptionElement.addChild(PAYLOAD);
266 payload.setContent(Base64.encodeToString(ciphertext, Base64.NO_WRAP));
267 }
268 return encryptionElement;
269 }
270
271 private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
272 XmppAxolotlSession.AxolotlKey encryptedKey = keys.get(sourceDeviceId);
273 if (encryptedKey == null) {
274 throw new NotEncryptedForThisDeviceException();
275 }
276 return session.processReceiving(encryptedKey);
277 }
278
279 public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
280 return new XmppAxolotlKeyTransportMessage(session.getFingerprint(), unpackKey(session, sourceDeviceId), getIV());
281 }
282
283 public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
284 XmppAxolotlPlaintextMessage plaintextMessage = null;
285 byte[] key = unpackKey(session, sourceDeviceId);
286 if (key != null) {
287 try {
288 if (key.length >= 32) {
289 int authtaglength = key.length - 16;
290 Log.d(Config.LOGTAG,"found auth tag as part of omemo key");
291 byte[] newCipherText = new byte[key.length - 16 + ciphertext.length];
292 byte[] newKey = new byte[16];
293 System.arraycopy(ciphertext, 0, newCipherText, 0, ciphertext.length);
294 System.arraycopy(key, 16, newCipherText, ciphertext.length, authtaglength);
295 System.arraycopy(key,0,newKey,0,newKey.length);
296 ciphertext = newCipherText;
297 key = newKey;
298 }
299
300 Cipher cipher = Compatibility.twentyTwo() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
301 SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
302 IvParameterSpec ivSpec = new IvParameterSpec(iv);
303
304 cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
305
306 String plaintext = new String(cipher.doFinal(ciphertext));
307 plaintextMessage = new XmppAxolotlPlaintextMessage(Config.OMEMO_PADDING ? plaintext.trim() : plaintext, session.getFingerprint());
308
309 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
310 | InvalidAlgorithmParameterException | IllegalBlockSizeException
311 | BadPaddingException | NoSuchProviderException e) {
312 throw new CryptoFailedException(e);
313 }
314 }
315 return plaintextMessage;
316 }
317}