1package eu.siacs.conversations.crypto.axolotl;
2
3import android.util.Base64;
4
5import java.security.InvalidAlgorithmParameterException;
6import java.security.NoSuchAlgorithmException;
7import java.security.InvalidKeyException;
8import java.util.HashSet;
9import java.util.Set;
10
11import javax.crypto.BadPaddingException;
12import javax.crypto.Cipher;
13import javax.crypto.IllegalBlockSizeException;
14import javax.crypto.KeyGenerator;
15import javax.crypto.NoSuchPaddingException;
16import javax.crypto.SecretKey;
17import javax.crypto.spec.IvParameterSpec;
18import javax.crypto.spec.SecretKeySpec;
19
20import eu.siacs.conversations.entities.Contact;
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(XmppAxolotlMessageHeader header) {
150 headers.add(header);
151 }
152
153 public byte[] getInnerKey(){
154 return innerKey;
155 }
156
157 public byte[] getIV() {
158 return this.iv;
159 }
160
161 public Element toXml() {
162 // TODO: generate outer XML, add in header XML
163 Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX);
164 message.setAttribute("id", sourceDeviceId);
165 for(XmppAxolotlMessageHeader header: headers) {
166 message.addChild(header.toXml());
167 }
168 Element payload = message.addChild("message");
169 payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT));
170 payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT));
171 return message;
172 }
173
174
175 public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key, String fingerprint) {
176 XmppAxolotlPlaintextMessage plaintextMessage = null;
177 try {
178
179 Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
180 SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
181 IvParameterSpec ivSpec = new IvParameterSpec(iv);
182
183 cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
184
185 String plaintext = new String(cipher.doFinal(ciphertext));
186 plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext, fingerprint);
187
188 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
189 | InvalidAlgorithmParameterException | IllegalBlockSizeException
190 | BadPaddingException e) {
191 throw new AssertionError(e);
192 }
193 return plaintextMessage;
194 }
195}