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