1package eu.siacs.conversations.crypto;
2
3import android.util.Log;
4
5import net.java.otr4j.OtrEngineHost;
6import net.java.otr4j.OtrException;
7import net.java.otr4j.OtrPolicy;
8import net.java.otr4j.OtrPolicyImpl;
9import net.java.otr4j.crypto.OtrCryptoEngineImpl;
10import net.java.otr4j.crypto.OtrCryptoException;
11import net.java.otr4j.session.FragmenterInstructions;
12import net.java.otr4j.session.InstanceTag;
13import net.java.otr4j.session.SessionID;
14
15import org.json.JSONException;
16import org.json.JSONObject;
17
18import java.math.BigInteger;
19import java.security.KeyFactory;
20import java.security.KeyPair;
21import java.security.KeyPairGenerator;
22import java.security.NoSuchAlgorithmException;
23import java.security.PrivateKey;
24import java.security.PublicKey;
25import java.security.spec.DSAPrivateKeySpec;
26import java.security.spec.DSAPublicKeySpec;
27import java.security.spec.InvalidKeySpecException;
28
29import eu.siacs.conversations.Config;
30import eu.siacs.conversations.entities.Account;
31import eu.siacs.conversations.entities.Conversation;
32import eu.siacs.conversations.generator.MessageGenerator;
33import eu.siacs.conversations.services.XmppConnectionService;
34import eu.siacs.conversations.xmpp.chatstate.ChatState;
35import eu.siacs.conversations.xmpp.jid.InvalidJidException;
36import eu.siacs.conversations.xmpp.jid.Jid;
37import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
38
39public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
40
41 private Account account;
42 private OtrPolicy otrPolicy;
43 private KeyPair keyPair;
44 private XmppConnectionService mXmppConnectionService;
45
46 public OtrService(XmppConnectionService service, Account account) {
47 this.account = account;
48 this.otrPolicy = new OtrPolicyImpl();
49 this.otrPolicy.setAllowV1(false);
50 this.otrPolicy.setAllowV2(true);
51 this.otrPolicy.setAllowV3(true);
52 this.keyPair = loadKey(account.getKeys());
53 this.mXmppConnectionService = service;
54 }
55
56 private KeyPair loadKey(final JSONObject keys) {
57 if (keys == null) {
58 return null;
59 }
60 synchronized (keys) {
61 try {
62 BigInteger x = new BigInteger(keys.getString("otr_x"), 16);
63 BigInteger y = new BigInteger(keys.getString("otr_y"), 16);
64 BigInteger p = new BigInteger(keys.getString("otr_p"), 16);
65 BigInteger q = new BigInteger(keys.getString("otr_q"), 16);
66 BigInteger g = new BigInteger(keys.getString("otr_g"), 16);
67 KeyFactory keyFactory = KeyFactory.getInstance("DSA");
68 DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g);
69 DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g);
70 PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
71 PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
72 return new KeyPair(publicKey, privateKey);
73 } catch (JSONException e) {
74 return null;
75 } catch (NoSuchAlgorithmException e) {
76 return null;
77 } catch (InvalidKeySpecException e) {
78 return null;
79 }
80 }
81 }
82
83 private void saveKey() {
84 PublicKey publicKey = keyPair.getPublic();
85 PrivateKey privateKey = keyPair.getPrivate();
86 KeyFactory keyFactory;
87 try {
88 keyFactory = KeyFactory.getInstance("DSA");
89 DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(
90 privateKey, DSAPrivateKeySpec.class);
91 DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey,
92 DSAPublicKeySpec.class);
93 this.account.setKey("otr_x", privateKeySpec.getX().toString(16));
94 this.account.setKey("otr_g", privateKeySpec.getG().toString(16));
95 this.account.setKey("otr_p", privateKeySpec.getP().toString(16));
96 this.account.setKey("otr_q", privateKeySpec.getQ().toString(16));
97 this.account.setKey("otr_y", publicKeySpec.getY().toString(16));
98 } catch (final NoSuchAlgorithmException | InvalidKeySpecException e) {
99 e.printStackTrace();
100 }
101
102 }
103
104 @Override
105 public void askForSecret(SessionID id, InstanceTag instanceTag, String question) {
106 try {
107 final Jid jid = Jid.fromSessionID(id);
108 Conversation conversation = this.mXmppConnectionService.find(this.account,jid);
109 if (conversation!=null) {
110 conversation.smp().hint = question;
111 conversation.smp().status = Conversation.Smp.STATUS_CONTACT_REQUESTED;
112 mXmppConnectionService.updateConversationUi();
113 }
114 } catch (InvalidJidException e) {
115 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": smp in invalid session "+id.toString());
116 }
117 }
118
119 @Override
120 public void finishedSessionMessage(SessionID arg0, String arg1)
121 throws OtrException {
122
123 }
124
125 @Override
126 public String getFallbackMessage(SessionID arg0) {
127 return MessageGenerator.OTR_FALLBACK_MESSAGE;
128 }
129
130 @Override
131 public byte[] getLocalFingerprintRaw(SessionID arg0) {
132 try {
133 return getFingerprintRaw(getPublicKey());
134 } catch (OtrCryptoException e) {
135 return null;
136 }
137 }
138
139 public PublicKey getPublicKey() {
140 if (this.keyPair == null) {
141 return null;
142 }
143 return this.keyPair.getPublic();
144 }
145
146 @Override
147 public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException {
148 if (this.keyPair == null) {
149 KeyPairGenerator kg;
150 try {
151 kg = KeyPairGenerator.getInstance("DSA");
152 this.keyPair = kg.genKeyPair();
153 this.saveKey();
154 mXmppConnectionService.databaseBackend.updateAccount(account);
155 } catch (NoSuchAlgorithmException e) {
156 Log.d(Config.LOGTAG,
157 "error generating key pair " + e.getMessage());
158 }
159 }
160 return this.keyPair;
161 }
162
163 @Override
164 public String getReplyForUnreadableMessage(SessionID arg0) {
165 // TODO Auto-generated method stub
166 return null;
167 }
168
169 @Override
170 public OtrPolicy getSessionPolicy(SessionID arg0) {
171 return otrPolicy;
172 }
173
174 @Override
175 public void injectMessage(SessionID session, String body)
176 throws OtrException {
177 MessagePacket packet = new MessagePacket();
178 packet.setFrom(account.getJid());
179 if (session.getUserID().isEmpty()) {
180 packet.setAttribute("to", session.getAccountID());
181 } else {
182 packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID());
183 }
184 packet.setBody(body);
185 MessageGenerator.addMessageHints(packet);
186 try {
187 Jid jid = Jid.fromSessionID(session);
188 Conversation conversation = mXmppConnectionService.find(account,jid);
189 if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
190 if (mXmppConnectionService.sendChatStates()) {
191 packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
192 }
193 }
194 } catch (final InvalidJidException ignored) {
195
196 }
197 packet.setType(MessagePacket.TYPE_CHAT);
198 packet.addChild("encryption","urn:xmpp:eme:0")
199 .setAttribute("namespace","urn:xmpp:otr:0");
200 account.getXmppConnection().sendMessagePacket(packet);
201 }
202
203 @Override
204 public void messageFromAnotherInstanceReceived(SessionID session) {
205 sendOtrErrorMessage(session, "Message from another OTR-instance received");
206 }
207
208 @Override
209 public void multipleInstancesDetected(SessionID arg0) {
210 // TODO Auto-generated method stub
211
212 }
213
214 @Override
215 public void requireEncryptedMessage(SessionID arg0, String arg1)
216 throws OtrException {
217 // TODO Auto-generated method stub
218
219 }
220
221 @Override
222 public void showError(SessionID arg0, String arg1) throws OtrException {
223 Log.d(Config.LOGTAG,"show error");
224 }
225
226 @Override
227 public void smpAborted(SessionID id) throws OtrException {
228 setSmpStatus(id, Conversation.Smp.STATUS_NONE);
229 }
230
231 private void setSmpStatus(SessionID id, int status) {
232 try {
233 final Jid jid = Jid.fromSessionID(id);
234 Conversation conversation = this.mXmppConnectionService.find(this.account,jid);
235 if (conversation!=null) {
236 conversation.smp().status = status;
237 mXmppConnectionService.updateConversationUi();
238 }
239 } catch (final InvalidJidException ignored) {
240
241 }
242 }
243
244 @Override
245 public void smpError(SessionID id, int arg1, boolean arg2)
246 throws OtrException {
247 setSmpStatus(id, Conversation.Smp.STATUS_NONE);
248 }
249
250 @Override
251 public void unencryptedMessageReceived(SessionID arg0, String arg1)
252 throws OtrException {
253 throw new OtrException(new Exception("unencrypted message received"));
254 }
255
256 @Override
257 public void unreadableMessageReceived(SessionID session) throws OtrException {
258 Log.d(Config.LOGTAG,"unreadable message received");
259 sendOtrErrorMessage(session, "You sent me an unreadable OTR-encrypted message");
260 }
261
262 public void sendOtrErrorMessage(SessionID session, String errorText) {
263 try {
264 Jid jid = Jid.fromSessionID(session);
265 Conversation conversation = mXmppConnectionService.find(account, jid);
266 String id = conversation == null ? null : conversation.getLastReceivedOtrMessageId();
267 if (id != null) {
268 MessagePacket packet = mXmppConnectionService.getMessageGenerator()
269 .generateOtrError(jid, id, errorText);
270 packet.setFrom(account.getJid());
271 mXmppConnectionService.sendMessagePacket(account,packet);
272 Log.d(Config.LOGTAG,packet.toString());
273 Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()
274 +": unreadable OTR message in "+conversation.getName());
275 }
276 } catch (InvalidJidException e) {
277 return;
278 }
279 }
280
281 @Override
282 public void unverify(SessionID id, String arg1) {
283 setSmpStatus(id, Conversation.Smp.STATUS_FAILED);
284 }
285
286 @Override
287 public void verify(SessionID id, String fingerprint, boolean approved) {
288 Log.d(Config.LOGTAG,"OtrService.verify("+id.toString()+","+fingerprint+","+String.valueOf(approved)+")");
289 try {
290 final Jid jid = Jid.fromSessionID(id);
291 Conversation conversation = this.mXmppConnectionService.find(this.account,jid);
292 if (conversation!=null) {
293 if (approved) {
294 conversation.getContact().addOtrFingerprint(fingerprint);
295 }
296 conversation.smp().hint = null;
297 conversation.smp().status = Conversation.Smp.STATUS_VERIFIED;
298 mXmppConnectionService.updateConversationUi();
299 mXmppConnectionService.syncRosterToDisk(conversation.getAccount());
300 }
301 } catch (final InvalidJidException ignored) {
302 }
303 }
304
305 @Override
306 public FragmenterInstructions getFragmenterInstructions(SessionID sessionID) {
307 return null;
308 }
309
310}