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