1package eu.siacs.conversations.crypto.axolotl;
2
3import android.support.annotation.Nullable;
4import android.util.Base64;
5
6import java.security.InvalidAlgorithmParameterException;
7import java.security.InvalidKeyException;
8import java.security.NoSuchAlgorithmException;
9import java.security.NoSuchProviderException;
10import java.security.SecureRandom;
11import java.util.HashSet;
12import java.util.Set;
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.xml.Element;
24import eu.siacs.conversations.xmpp.jid.Jid;
25
26public class XmppAxolotlMessage {
27 private byte[] innerKey;
28 private byte[] ciphertext;
29 private byte[] iv;
30 private final Set<XmppAxolotlMessageHeader> headers;
31 private final Jid from;
32 private final int sourceDeviceId;
33
34 public static class XmppAxolotlMessageHeader {
35 private final int recipientDeviceId;
36 private final byte[] content;
37
38 public XmppAxolotlMessageHeader(int deviceId, byte[] content) {
39 this.recipientDeviceId = deviceId;
40 this.content = content;
41 }
42
43 public XmppAxolotlMessageHeader(Element header) {
44 if("header".equals(header.getName())) {
45 this.recipientDeviceId = Integer.parseInt(header.getAttribute("rid"));
46 this.content = Base64.decode(header.getContent(),Base64.DEFAULT);
47 } else {
48 throw new IllegalArgumentException("Argument not a <header> Element!");
49 }
50 }
51
52 public int getRecipientDeviceId() {
53 return recipientDeviceId;
54 }
55
56 public byte[] getContents() {
57 return content;
58 }
59
60 public Element toXml() {
61 Element headerElement = new Element("header");
62 // TODO: generate XML
63 headerElement.setAttribute("rid", getRecipientDeviceId());
64 headerElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT));
65 return headerElement;
66 }
67 }
68
69 public static class XmppAxolotlPlaintextMessage {
70 private final AxolotlService.XmppAxolotlSession session;
71 private final String plaintext;
72 private final String fingerprint;
73
74 public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext, String fingerprint) {
75 this.session = session;
76 this.plaintext = plaintext;
77 this.fingerprint = fingerprint;
78 }
79
80 public String getPlaintext() {
81 return plaintext;
82 }
83
84 public AxolotlService.XmppAxolotlSession getSession() {
85 return session;
86 }
87
88 public String getFingerprint() {
89 return fingerprint;
90 }
91 }
92
93 public XmppAxolotlMessage(Jid from, Element axolotlMessage) {
94 this.from = from;
95 this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id"));
96 this.headers = new HashSet<>();
97 for(Element child:axolotlMessage.getChildren()) {
98 switch(child.getName()) {
99 case "header":
100 headers.add(new XmppAxolotlMessageHeader(child));
101 break;
102 case "message":
103 iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT);
104 ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT);
105 break;
106 default:
107 break;
108 }
109 }
110 }
111
112 public XmppAxolotlMessage(Jid from, int sourceDeviceId, String plaintext) throws CryptoFailedException{
113 this.from = from;
114 this.sourceDeviceId = sourceDeviceId;
115 this.headers = new HashSet<>();
116 this.encrypt(plaintext);
117 }
118
119 private void encrypt(String plaintext) throws CryptoFailedException {
120 try {
121 KeyGenerator generator = KeyGenerator.getInstance("AES");
122 generator.init(128);
123 SecretKey secretKey = generator.generateKey();
124 SecureRandom random = new SecureRandom();
125 this.iv = new byte[16];
126 random.nextBytes(iv);
127 IvParameterSpec ivSpec = new IvParameterSpec(iv);
128 Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
129 cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
130 this.innerKey = secretKey.getEncoded();
131 this.ciphertext = cipher.doFinal(plaintext.getBytes());
132 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
133 | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException
134 | InvalidAlgorithmParameterException e) {
135 throw new CryptoFailedException(e);
136 }
137 }
138
139 public Jid getFrom() {
140 return this.from;
141 }
142
143 public int getSenderDeviceId() {
144 return sourceDeviceId;
145 }
146
147 public byte[] getCiphertext() {
148 return ciphertext;
149 }
150
151 public Set<XmppAxolotlMessageHeader> getHeaders() {
152 return headers;
153 }
154
155 public void addHeader(@Nullable XmppAxolotlMessageHeader header) {
156 if (header != null) {
157 headers.add(header);
158 }
159 }
160
161 public byte[] getInnerKey(){
162 return innerKey;
163 }
164
165 public byte[] getIV() {
166 return this.iv;
167 }
168
169 public Element toXml() {
170 // TODO: generate outer XML, add in header XML
171 Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX);
172 message.setAttribute("id", sourceDeviceId);
173 for(XmppAxolotlMessageHeader header: headers) {
174 message.addChild(header.toXml());
175 }
176 Element payload = message.addChild("message");
177 payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT));
178 payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT));
179 return message;
180 }
181
182
183 public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key, String fingerprint) throws CryptoFailedException {
184 XmppAxolotlPlaintextMessage plaintextMessage = null;
185 try {
186
187 Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
188 SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
189 IvParameterSpec ivSpec = new IvParameterSpec(iv);
190
191 cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
192
193 String plaintext = new String(cipher.doFinal(ciphertext));
194 plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext, fingerprint);
195
196 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
197 | InvalidAlgorithmParameterException | IllegalBlockSizeException
198 | BadPaddingException | NoSuchProviderException e) {
199 throw new CryptoFailedException(e);
200 }
201 return plaintextMessage;
202 }
203}