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