source: trunk/src/org/expeditee/auth/mail/Mail.java@ 1478

Last change on this file since 1478 was 1478, checked in by bnemhaus, 4 years ago

Introducing java property 'expeditee.demo-mode', when set to true. With it enabled, any expeditee mail sent will also go to the recipient if they are on the same computer.

File size: 18.7 KB
Line 
1package org.expeditee.auth.mail;
2
3import java.io.File;
4import java.io.FileNotFoundException;
5import java.io.IOException;
6import java.nio.file.Path;
7import java.nio.file.Paths;
8import java.security.InvalidKeyException;
9import java.security.KeyStoreException;
10import java.security.NoSuchAlgorithmException;
11import java.security.PrivateKey;
12import java.security.PublicKey;
13import java.security.cert.CertificateException;
14import java.security.spec.InvalidKeySpecException;
15import java.sql.Connection;
16import java.sql.DriverManager;
17import java.sql.PreparedStatement;
18import java.sql.SQLException;
19import java.sql.Statement;
20import java.text.ParseException;
21import java.text.SimpleDateFormat;
22import java.util.ArrayList;
23import java.util.Arrays;
24import java.util.Base64;
25import java.util.Date;
26import java.util.HashMap;
27import java.util.List;
28import java.util.Map;
29import java.util.Scanner;
30
31import javax.crypto.BadPaddingException;
32import javax.crypto.Cipher;
33import javax.crypto.IllegalBlockSizeException;
34import javax.crypto.NoSuchPaddingException;
35import javax.crypto.SecretKey;
36import javax.crypto.spec.SecretKeySpec;
37
38import org.expeditee.auth.AuthenticatorBrowser;
39import org.expeditee.auth.mail.gui.MailBay;
40import org.expeditee.encryption.CryptographyConstants;
41import org.expeditee.gui.FrameIO;
42import org.expeditee.settings.UserSettings;
43
44public class Mail implements CryptographyConstants {
45
46 private static List<MailEntry> messages = new ArrayList<MailEntry>();
47
48 /**
49 * Add a piece of mail, used during initialisation.
50 */
51 public static void addEntry(MailEntry mail) {
52 messages.add(mail);
53 }
54
55 public static void clear() {
56 messages.clear();
57 }
58
59 public static void sendOneOffMail(MailEntry mail, String colleagueName, byte[] key) {
60 // Ensure dead drop area is set up.
61 Path databaseFileDirPath = ensureDeadDrops(colleagueName, mail.sender);
62
63 // Ensure the database file exists.
64 Path databaseFilePath = ensureDatabaseFile(colleagueName, databaseFileDirPath);
65
66 // Create secret key.
67 SecretKey secretKey = new SecretKeySpec(key, SymmetricAlgorithm);
68
69 // Send message
70 sendMail(mail, secretKey, databaseFilePath);
71
72 // Do it all again if demo mode is on (ensures sync without having to move db files)
73 if (Boolean.getBoolean("expeditee.demo-mode")) {
74 databaseFileDirPath = ensureDeadDropsDemoMode(colleagueName);
75 databaseFilePath = ensureDatabaseFile(colleagueName, databaseFileDirPath);
76 sendMail(mail, secretKey, databaseFilePath);
77 }
78 }
79
80 public static void sendMail(MailEntry mail, String colleagueName) {
81 // Ensure dead drop area is set up.
82 Path databaseFileDirPath = ensureDeadDrops(colleagueName);
83
84 // Ensure the database file exists.
85 Path databaseFilePath = ensureDatabaseFile(colleagueName, databaseFileDirPath);
86
87 // Obtain public key
88 PublicKey publicKey = null;
89 try {
90 publicKey = AuthenticatorBrowser.getInstance().getPublicKey(colleagueName);
91 } catch (InvalidKeySpecException | NoSuchAlgorithmException | KeyStoreException | CertificateException
92 | ClassNotFoundException | IOException | SQLException e) {
93 System.err.println("Error while sending message. Unable to obtain public key for colleague " +
94 colleagueName + ". Exception message: " + e.getMessage());
95 return;
96 }
97
98 // Check we got public key
99 if (publicKey == null) {
100 System.err.println("Error while sending message. Unable to obtain public key for colleague. Have you exchanged contact details?");
101 return;
102 }
103
104 // Send message
105 sendMail(mail, publicKey, databaseFilePath);
106
107 // Do it all again if demo mode is on (ensures sync without having to move db files)
108 if (Boolean.getBoolean("expeditee.demo-mode")) {
109 databaseFileDirPath = ensureDeadDropsDemoMode(colleagueName);
110 databaseFilePath = ensureDatabaseFile(colleagueName, databaseFileDirPath);
111 try {
112 publicKey = AuthenticatorBrowser.getInstance().getPublicKey(colleagueName);
113 } catch (InvalidKeySpecException | NoSuchAlgorithmException | KeyStoreException | CertificateException
114 | ClassNotFoundException | IOException | SQLException e) {
115 System.err.println("Error while sending message. Unable to obtain public key for colleague " +
116 colleagueName + ". Exception message: " + e.getMessage());
117 return;
118 }
119 if (publicKey == null) {
120 System.err.println("(Demo mode) Error while sending message. Unable to obtain public key for colleague. Have you exchanged contact details?");
121 return;
122 }
123
124 sendMail(mail, publicKey, databaseFilePath);
125 }
126 }
127
128 private static Path ensureDatabaseFile(String colleagueName, Path databaseFileDirPath) {
129 Path databaseFilePath = databaseFileDirPath.resolve(colleagueName + ".db");
130 File databaseFile = databaseFilePath.toFile();
131 if (!databaseFile.exists()) {
132 databaseFileDirPath.toFile().mkdirs();
133 String sql =
134 "CREATE TABLE EXPMAIL (" +
135 "TIME TEXT NOT NULL, " +
136 "SND TEXT NOT NULL, " +
137 "REC TEXT NOT NULL, " +
138 "MSG TEXT NOT NULL, " +
139 "MSG2 TEXT NOT NULL, " +
140 "OPTS ARRAY NOT NULL, " +
141 "OPTSVAL ARRAY NOT NULL)";
142 try {
143 Connection c = DriverManager.getConnection("jdbc:sqlite:" + databaseFile.getAbsolutePath());
144 Statement createTable = c.createStatement();
145 createTable.executeUpdate(sql);
146 createTable.close();
147 c.close();
148 } catch (SQLException e) {
149 System.err.println("Error while creating database file.");
150 e.printStackTrace();
151 }
152 }
153 return databaseFilePath;
154 }
155
156 private static Path ensureDeadDrops(String colleagueName) {
157 String me = UserSettings.UserName.get().toLowerCase();
158 String them = colleagueName.toLowerCase();
159 Path databaseFileDirPath = Paths.get(FrameIO.DEAD_DROPS_PATH).resolve(me + "+" + them);
160 if (!databaseFileDirPath.toFile().exists()) {
161 databaseFileDirPath = Paths.get(FrameIO.DEAD_DROPS_PATH).resolve(them + "+" + me);
162 }
163 return databaseFileDirPath;
164 }
165
166 private static Path ensureDeadDropsDemoMode(String colleagueName) {
167 String me = UserSettings.UserName.get().toLowerCase();
168 String them = colleagueName.toLowerCase();
169 String demoModeDeadDropsPath = FrameIO.DEAD_DROPS_PATH.replace(me, them);
170 Path databaseFileDirPath = Paths.get(demoModeDeadDropsPath).resolve(me + "+" + them);
171 if (!databaseFileDirPath.toFile().exists()) {
172 databaseFileDirPath = Paths.get(demoModeDeadDropsPath).resolve(them + "+" + me);
173 }
174 return databaseFileDirPath;
175 }
176
177 private static Path ensureDeadDrops(String colleagueName, String sender) {
178 String me = sender.toLowerCase();
179 String them = colleagueName.toLowerCase();
180 Path parent = Paths.get(FrameIO.PARENT_FOLDER).resolve("resources-" + sender).resolve("deaddrops");
181 Path databaseFileDirPath = parent.resolve(me + "+" + them);
182 if (!databaseFileDirPath.toFile().exists()) {
183 databaseFileDirPath = parent.resolve(them + "+" + me);
184 }
185 return databaseFileDirPath;
186 }
187
188 private static void sendMail(MailEntry mail, PublicKey key, Path databaseFile) {
189 try {
190 Cipher cipher = Cipher.getInstance(AsymmetricAlgorithm + AsymmetricAlgorithmParameters);
191 cipher.init(Cipher.ENCRYPT_MODE, key);
192 String sender = Base64.getEncoder().encodeToString(cipher.doFinal(mail.sender.getBytes()));
193 cipher.init(Cipher.ENCRYPT_MODE, key);
194 String rec = Base64.getEncoder().encodeToString(cipher.doFinal(mail.receiver.getBytes()));
195 cipher.init(Cipher.ENCRYPT_MODE, key);
196 String message = Base64.getEncoder().encodeToString(cipher.doFinal(mail.message.getBytes()));
197 cipher.init(Cipher.ENCRYPT_MODE, key);
198 String message2 = Base64.getEncoder().encodeToString(cipher.doFinal(mail.message2.getBytes()));
199
200 Map<String, String> options = new HashMap<String, String>();
201 for (String label: mail.options.keySet()) {
202 cipher.init(Cipher.ENCRYPT_MODE, key);
203 String labelEncrypted = Base64.getEncoder().encodeToString(cipher.doFinal(label.getBytes()));
204 cipher.init(Cipher.ENCRYPT_MODE, key);
205 String actionNameEncrypted = Base64.getEncoder().encodeToString(cipher.doFinal(mail.options.get(label).getBytes()));
206 options.put(labelEncrypted, actionNameEncrypted);
207 }
208
209 // write to mail database
210 writeToMailDatabase(mail, databaseFile, sender, rec, message, message2, options);
211 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | SQLException e) {
212 e.printStackTrace();
213 }
214 }
215
216 private static void sendMail(MailEntry mail, SecretKey key, Path databaseFile) {
217 try {
218 // Encrypt message.
219 Cipher cipher = Cipher.getInstance(SymmetricAlgorithm + SymmetricAlgorithmParameters);
220 cipher.init(Cipher.ENCRYPT_MODE, key);
221 String sender = "=" + Base64.getEncoder().encodeToString(cipher.doFinal(mail.sender.getBytes()));
222 cipher.init(Cipher.ENCRYPT_MODE, key);
223 String rec = Base64.getEncoder().encodeToString(cipher.doFinal(mail.receiver.getBytes()));
224 cipher.init(Cipher.ENCRYPT_MODE, key);
225 String message = Base64.getEncoder().encodeToString(cipher.doFinal(mail.message.getBytes()));
226 cipher.init(Cipher.ENCRYPT_MODE, key);
227 String message2 = Base64.getEncoder().encodeToString(cipher.doFinal(mail.message2.getBytes()));
228 Map<String, String> options = new HashMap<String, String>();
229 for (String label: mail.options.keySet()) {
230 cipher.init(Cipher.ENCRYPT_MODE, key);
231 String labelEncrypted = Base64.getEncoder().encodeToString(cipher.doFinal(label.getBytes()));
232 cipher.init(Cipher.ENCRYPT_MODE, key);
233 String actionNameEncrypted = Base64.getEncoder().encodeToString(cipher.doFinal(mail.options.get(label).getBytes()));
234 options.put(labelEncrypted, actionNameEncrypted);
235 }
236
237 // Write to mail database.
238 writeToMailDatabase(mail, databaseFile, sender, rec, message, message2, options);
239 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | SQLException e) {
240 e.printStackTrace();
241 }
242 }
243
244 private static void writeToMailDatabase(MailEntry mail, Path databaseFile, String sender, String rec,
245 String message, String message2, Map<String, String> options) throws SQLException {
246 Connection c = DriverManager.getConnection("jdbc:sqlite:" + databaseFile);
247 String sql = "INSERT INTO EXPMAIL (TIME,SND,REC,MSG,MSG2,OPTS,OPTSVAL) VALUES (?, ?, ?, ?, ?, ?, ?);";
248 PreparedStatement statement = c.prepareStatement(sql);
249 statement.setString(1, mail.timestamp);
250 statement.setString(2, sender);
251 statement.setString(3, rec);
252 statement.setString(4, message);
253 statement.setString(5, message2);
254 String opts = Arrays.toString(options.keySet().toArray());
255 statement.setString(6, opts);
256 String optsval = Arrays.toString(options.values().toArray());
257 statement.setString(7, optsval);
258 statement.execute();
259 statement.close();
260 c.close();
261 System.err.println("Message written to database: " + databaseFile.toString());
262 }
263
264 /**
265 * Gets the mail messages that the specified user is able to read.
266 * The caller supplies their username and private key.
267 * If the private key can decrypt a message, then it was encrypted using the users public key, and is therefore for them.
268 */
269 private static List<MailEntry> getEntries(String name, PrivateKey key) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
270 List<MailEntry> filtered = new ArrayList<MailEntry>();
271
272 for (MailEntry mail: messages) {
273 if (isEncryptedOneOffMessage(mail)) {
274 getOneOffSecureMail(name, filtered, mail);
275 } else {
276 getStandardSecureMail(name, key, filtered, mail);
277 }
278 }
279
280 return filtered;
281 }
282
283 private static void getOneOffSecureMail(String name, List<MailEntry> filtered, MailEntry mail) {
284 StringBuilder sb = new StringBuilder();
285 String sep = ":::";
286 sb.append("Read one-off secure message." + sep);
287 sb.append(mail.timestamp + sep);
288 sb.append(mail.sender.substring(1) + sep);
289 sb.append(mail.receiver + sep);
290 sb.append(mail.message + sep);
291 sb.append(mail.message2 + sep);
292 for (String k: mail.options.keySet()) {
293 sb.append(k + sep + mail.options.get(k) + sep);
294 }
295 sb.reverse().delete(0, 3).reverse();
296 Map<String, String> options = new HashMap<String, String>();
297 options.put(sb.toString(), "AuthOneOffSecureMessage");
298 String currentTime = org.expeditee.stats.Formatter.getDateTime();
299 MailEntry mailOuter = new MailEntry(currentTime, "Unknown", name, "You have received a one-off secure message.", "Check your email for more details.", options);
300
301 if (mail.deadDropSource != null) {
302 Path lastAccessedFile = mail.deadDropSource.getParent().resolve(name + ".last-accessed");
303 SimpleDateFormat format = new SimpleDateFormat("ddMMMyyyy[HH:mm]");
304 try (Scanner in = new Scanner(lastAccessedFile.toFile())) {
305 Date lastAccessedTimestamp = format.parse(in.nextLine());
306 Date mailTimestamp = format.parse(mail.timestamp);
307 if (mailTimestamp.after(lastAccessedTimestamp)) {
308 filtered.add(mailOuter);
309 }
310 } catch (FileNotFoundException e) {
311 // It may not have been created yet, then err on the safe side and add it in.
312 filtered.add(mailOuter);
313 } catch (ParseException e) {
314 // If we fail to parse, then err on the safe side and add it in.
315 filtered.add(mailOuter);
316 }
317 }
318 }
319
320 private static void getStandardSecureMail(String name, PrivateKey key, List<MailEntry> filtered, MailEntry mail)
321 throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, InvalidKeyException,
322 BadPaddingException {
323 // confirm this is a message for the requester of entries
324 String receiver = mail.receiver;
325 byte[] receiverBytes = Base64.getDecoder().decode(receiver);
326 String receiverDecrypted = null;
327 Cipher c = Cipher.getInstance(AsymmetricAlgorithm + AsymmetricAlgorithmParameters);
328 try {
329 c.init(Cipher.DECRYPT_MODE, key);
330 receiverDecrypted = new String(c.doFinal(receiverBytes));
331 } catch (InvalidKeyException | BadPaddingException e) {
332 return;
333 }
334
335 // add an unencrypted version of the message to the return list
336 if (receiverDecrypted.compareToIgnoreCase(name) == 0) {
337 c.init(Cipher.DECRYPT_MODE, key);
338 String sender = new String(c.doFinal(Base64.getDecoder().decode(mail.sender)));
339 c.init(Cipher.DECRYPT_MODE, key);
340 String message = new String(c.doFinal(Base64.getDecoder().decode(mail.message)));
341 c.init(Cipher.DECRYPT_MODE, key);
342 String message2 = new String(c.doFinal(Base64.getDecoder().decode(mail.message2)));
343
344 Map<String, String> options = new HashMap<String, String>();
345 for (String label: mail.options.keySet()) {
346 c.init(Cipher.DECRYPT_MODE, key);
347 String labelDecrypted = new String(c.doFinal(Base64.getDecoder().decode(label)));
348 c.init(Cipher.DECRYPT_MODE, key);
349 String actionNameDecrypted = new String(c.doFinal(Base64.getDecoder().decode(mail.options.get(label))));
350 options.put(labelDecrypted, actionNameDecrypted);
351 }
352
353 Path lastAccessedFile = Paths.get(FrameIO.DEAD_DROPS_PATH).resolve(UserSettings.UserName.get() + "+" + sender).resolve(name + ".last-accessed");
354 if (!lastAccessedFile.toFile().exists()) {
355 lastAccessedFile = Paths.get(FrameIO.DEAD_DROPS_PATH).resolve(sender + "+" + UserSettings.UserName.get()).resolve(name + ".last-accessed");
356 }
357 SimpleDateFormat format = new SimpleDateFormat("ddMMMyyyy[HH:mm]");
358 MailEntry mailEntry = new MailEntry(mail.timestamp, sender, receiverDecrypted, message, message2, options);
359 try (Scanner in = new Scanner(lastAccessedFile.toFile())) {
360 Date lastAccessedTimestamp = format.parse(in.nextLine());
361 Date mailTimestamp = format.parse(mail.timestamp);
362 if (mailTimestamp.after(lastAccessedTimestamp)) {
363 filtered.add(mailEntry);
364 }
365 } catch (FileNotFoundException e) {
366 // It may not have been created yet, then err on the safe side and add it in.
367 filtered.add(mailEntry);
368 } catch (ParseException e) {
369 // If we fail to parse, then err on the safe side and add it in.
370 filtered.add(mailEntry);
371 }
372
373 }
374 }
375
376 private static boolean isEncryptedOneOffMessage(MailEntry mail) {
377 return mail.sender.charAt(0) == '=';
378 }
379
380 /**
381 * Describes a piece of mail, either encrypted or decrypted.
382 */
383 public static class MailEntry {
384 public String timestamp;
385 public String sender;
386 public String receiver;
387 public String message;
388 public String message2;
389 public Map<String, String> options;
390 public Path deadDropSource;
391
392 public MailEntry(String timestamp, String sender, String rec, String message, String message2, Map<String, String> options) {
393 this.timestamp = timestamp;
394 this.sender = sender;
395 this.receiver = rec;
396 this.message = message;
397 this.message2 = message2;
398 this.options = options;
399 }
400 }
401
402 public static void checkMail(PrivateKey key) throws NoSuchAlgorithmException, NoSuchPaddingException,
403 IllegalBlockSizeException, BadPaddingException, InvalidKeyException, KeyStoreException,
404 FileNotFoundException, CertificateException, IOException, ClassNotFoundException, SQLException, ParseException {
405 MailBay.clear();
406 AuthenticatorBrowser.getInstance().loadMailDatabase();
407 List<MailEntry> mailForLoggingInUser = Mail.getEntries(UserSettings.UserName.get(), key);
408 for (MailEntry mail: mailForLoggingInUser) {
409 MailBay.addMessage(mail.timestamp, mail.sender, mail.message, mail.message2, mail.options);
410 }
411
412 // Update last read files.
413 Path deadDropPath = Paths.get(FrameIO.DEAD_DROPS_PATH);
414 for (File connectionDir: deadDropPath.toFile().listFiles()) {
415 if (connectionDir.isDirectory()) {
416 Path deaddropforcontactPath = Paths.get(connectionDir.getAbsolutePath());
417 AuthenticatorBrowser.getInstance().updateLastReadMailTime(deaddropforcontactPath);
418 }
419 }
420 }
421
422 public static void decryptOneOffSecureMessage(SecretKey key, List<String> data) {
423 byte[] topicBytes = Base64.getDecoder().decode(data.get(3));
424 String topic = new String(org.expeditee.encryption.Actions.DecryptSymmetric(topicBytes, key));
425 byte[] messageBytes = Base64.getDecoder().decode(data.get(4));
426 String message = new String(org.expeditee.encryption.Actions.DecryptSymmetric(messageBytes, key));
427 Map<String, String> options = new HashMap<String, String>();
428 for (int i = 5; i < data.size(); i+=2) {
429 byte[] optionKeyBytes = Base64.getDecoder().decode(data.get(i));
430 String k = new String(org.expeditee.encryption.Actions.DecryptSymmetric(optionKeyBytes, key));
431 byte[] optionValueBytes = Base64.getDecoder().decode(data.get(i + 1));
432 String v = new String(org.expeditee.encryption.Actions.DecryptSymmetric(optionValueBytes, key));
433 options.put(k, v);
434 }
435 MailBay.addMessage(data.get(0), "Single-use Secure (encrypted sender)", topic, message, options);
436 }
437}
Note: See TracBrowser for help on using the repository browser.