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

Last change on this file since 1483 was 1483, 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. --fix previous issue with one-off messages that was just introduced.

File size: 18.6 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, mail.sender);
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 sendMail(mail, publicKey, databaseFilePath);
112 }
113 }
114
115 private static Path ensureDatabaseFile(String colleagueName, Path databaseFileDirPath) {
116 Path databaseFilePath = databaseFileDirPath.resolve(colleagueName + ".db");
117 File databaseFile = databaseFilePath.toFile();
118 if (!databaseFile.exists()) {
119 databaseFileDirPath.toFile().mkdirs();
120 String sql =
121 "CREATE TABLE EXPMAIL (" +
122 "TIME TEXT NOT NULL, " +
123 "SND TEXT NOT NULL, " +
124 "REC TEXT NOT NULL, " +
125 "MSG TEXT NOT NULL, " +
126 "MSG2 TEXT NOT NULL, " +
127 "OPTS ARRAY NOT NULL, " +
128 "OPTSVAL ARRAY NOT NULL)";
129 try {
130 Connection c = DriverManager.getConnection("jdbc:sqlite:" + databaseFile.getAbsolutePath());
131 Statement createTable = c.createStatement();
132 createTable.executeUpdate(sql);
133 createTable.close();
134 c.close();
135 } catch (SQLException e) {
136 System.err.println("Error while creating database file.");
137 e.printStackTrace();
138 }
139 }
140 return databaseFilePath;
141 }
142
143 private static Path ensureDeadDrops(String colleagueName) {
144 String me = UserSettings.UserName.get().toLowerCase();
145 String them = colleagueName.toLowerCase();
146 Path databaseFileDirPath = Paths.get(FrameIO.DEAD_DROPS_PATH).resolve(me + "+" + them);
147 if (!databaseFileDirPath.toFile().exists()) {
148 databaseFileDirPath = Paths.get(FrameIO.DEAD_DROPS_PATH).resolve(them + "+" + me);
149 }
150 return databaseFileDirPath;
151 }
152
153 private static Path ensureDeadDropsDemoMode(String colleagueName) {
154 String me = UserSettings.UserName.get().toLowerCase();
155 String them = colleagueName.toLowerCase();
156 String demoModeDeadDropsPath = FrameIO.DEAD_DROPS_PATH.replace(me, them);
157 Path databaseFileDirPath = Paths.get(demoModeDeadDropsPath).resolve(me + "+" + them);
158 if (!databaseFileDirPath.toFile().exists()) {
159 databaseFileDirPath = Paths.get(demoModeDeadDropsPath).resolve(them + "+" + me);
160 }
161 return databaseFileDirPath;
162 }
163
164 private static Path ensureDeadDrops(String colleagueName, String sender) {
165 String me = sender.toLowerCase();
166 String them = colleagueName.toLowerCase();
167 Path parent = Paths.get(FrameIO.PARENT_FOLDER).resolve("resources-" + sender).resolve("deaddrops");
168 Path databaseFileDirPath = parent.resolve(me + "+" + them);
169 if (!databaseFileDirPath.toFile().exists()) {
170 databaseFileDirPath = parent.resolve(them + "+" + me);
171 }
172 return databaseFileDirPath;
173 }
174
175 private static Path ensureDeadDropsDemoMode(String colleagueName, String sender) {
176 String me = sender.toLowerCase();
177 String them = colleagueName.toLowerCase();
178 Path parent = Paths.get(FrameIO.PARENT_FOLDER).resolve("resources-" + them).resolve("deaddrops");
179 Path databaseFileDirPath = parent.resolve(me + "+" + them);
180 if (!databaseFileDirPath.toFile().exists()) {
181 databaseFileDirPath = parent.resolve(them + "+" + me);
182 }
183 return databaseFileDirPath;
184 }
185
186 private static void sendMail(MailEntry mail, PublicKey key, Path databaseFile) {
187 try {
188 Cipher cipher = Cipher.getInstance(AsymmetricAlgorithm + AsymmetricAlgorithmParameters);
189 cipher.init(Cipher.ENCRYPT_MODE, key);
190 String sender = Base64.getEncoder().encodeToString(cipher.doFinal(mail.sender.getBytes()));
191 cipher.init(Cipher.ENCRYPT_MODE, key);
192 String rec = Base64.getEncoder().encodeToString(cipher.doFinal(mail.receiver.getBytes()));
193 cipher.init(Cipher.ENCRYPT_MODE, key);
194 String message = Base64.getEncoder().encodeToString(cipher.doFinal(mail.message.getBytes()));
195 cipher.init(Cipher.ENCRYPT_MODE, key);
196 String message2 = Base64.getEncoder().encodeToString(cipher.doFinal(mail.message2.getBytes()));
197
198 Map<String, String> options = new HashMap<String, String>();
199 for (String label: mail.options.keySet()) {
200 cipher.init(Cipher.ENCRYPT_MODE, key);
201 String labelEncrypted = Base64.getEncoder().encodeToString(cipher.doFinal(label.getBytes()));
202 cipher.init(Cipher.ENCRYPT_MODE, key);
203 String actionNameEncrypted = Base64.getEncoder().encodeToString(cipher.doFinal(mail.options.get(label).getBytes()));
204 options.put(labelEncrypted, actionNameEncrypted);
205 }
206
207 // write to mail database
208 writeToMailDatabase(mail, databaseFile, sender, rec, message, message2, options);
209 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | SQLException e) {
210 e.printStackTrace();
211 }
212 }
213
214 private static void sendMail(MailEntry mail, SecretKey key, Path databaseFile) {
215 try {
216 // Encrypt message.
217 Cipher cipher = Cipher.getInstance(SymmetricAlgorithm + SymmetricAlgorithmParameters);
218 cipher.init(Cipher.ENCRYPT_MODE, key);
219 String sender = "=" + Base64.getEncoder().encodeToString(cipher.doFinal(mail.sender.getBytes()));
220 cipher.init(Cipher.ENCRYPT_MODE, key);
221 String rec = Base64.getEncoder().encodeToString(cipher.doFinal(mail.receiver.getBytes()));
222 cipher.init(Cipher.ENCRYPT_MODE, key);
223 String message = Base64.getEncoder().encodeToString(cipher.doFinal(mail.message.getBytes()));
224 cipher.init(Cipher.ENCRYPT_MODE, key);
225 String message2 = Base64.getEncoder().encodeToString(cipher.doFinal(mail.message2.getBytes()));
226 Map<String, String> options = new HashMap<String, String>();
227 for (String label: mail.options.keySet()) {
228 cipher.init(Cipher.ENCRYPT_MODE, key);
229 String labelEncrypted = Base64.getEncoder().encodeToString(cipher.doFinal(label.getBytes()));
230 cipher.init(Cipher.ENCRYPT_MODE, key);
231 String actionNameEncrypted = Base64.getEncoder().encodeToString(cipher.doFinal(mail.options.get(label).getBytes()));
232 options.put(labelEncrypted, actionNameEncrypted);
233 }
234
235 // Write to mail database.
236 writeToMailDatabase(mail, databaseFile, sender, rec, message, message2, options);
237 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | SQLException e) {
238 e.printStackTrace();
239 }
240 }
241
242 private static void writeToMailDatabase(MailEntry mail, Path databaseFile, String sender, String rec,
243 String message, String message2, Map<String, String> options) throws SQLException {
244 Connection c = DriverManager.getConnection("jdbc:sqlite:" + databaseFile);
245 String sql = "INSERT INTO EXPMAIL (TIME,SND,REC,MSG,MSG2,OPTS,OPTSVAL) VALUES (?, ?, ?, ?, ?, ?, ?);";
246 PreparedStatement statement = c.prepareStatement(sql);
247 statement.setString(1, mail.timestamp);
248 statement.setString(2, sender);
249 statement.setString(3, rec);
250 statement.setString(4, message);
251 statement.setString(5, message2);
252 String opts = Arrays.toString(options.keySet().toArray());
253 statement.setString(6, opts);
254 String optsval = Arrays.toString(options.values().toArray());
255 statement.setString(7, optsval);
256 statement.execute();
257 statement.close();
258 c.close();
259 System.err.println("Message written to database: " + databaseFile.toString());
260 }
261
262 /**
263 * Gets the mail messages that the specified user is able to read.
264 * The caller supplies their username and private key.
265 * If the private key can decrypt a message, then it was encrypted using the users public key, and is therefore for them.
266 */
267 private static List<MailEntry> getEntries(String name, PrivateKey key) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
268 List<MailEntry> filtered = new ArrayList<MailEntry>();
269
270 for (MailEntry mail: messages) {
271 if (isEncryptedOneOffMessage(mail)) {
272 getOneOffSecureMail(name, filtered, mail);
273 } else {
274 getStandardSecureMail(name, key, filtered, mail);
275 }
276 }
277
278 return filtered;
279 }
280
281 private static void getOneOffSecureMail(String name, List<MailEntry> filtered, MailEntry mail) {
282 StringBuilder sb = new StringBuilder();
283 String sep = ":::";
284 sb.append("Read one-off secure message." + sep);
285 sb.append(mail.timestamp + sep);
286 sb.append(mail.sender.substring(1) + sep);
287 sb.append(mail.receiver + sep);
288 sb.append(mail.message + sep);
289 sb.append(mail.message2 + sep);
290 for (String k: mail.options.keySet()) {
291 sb.append(k + sep + mail.options.get(k) + sep);
292 }
293 sb.reverse().delete(0, 3).reverse();
294 Map<String, String> options = new HashMap<String, String>();
295 options.put(sb.toString(), "AuthOneOffSecureMessage");
296 String currentTime = org.expeditee.stats.Formatter.getDateTime();
297 MailEntry mailOuter = new MailEntry(currentTime, "Unknown", name, "You have received a one-off secure message.", "Check your email for more details.", options);
298
299 if (mail.deadDropSource != null) {
300 Path lastAccessedFile = mail.deadDropSource.getParent().resolve(name + ".last-accessed");
301 SimpleDateFormat format = new SimpleDateFormat("ddMMMyyyy[HH:mm]");
302 try (Scanner in = new Scanner(lastAccessedFile.toFile())) {
303 Date lastAccessedTimestamp = format.parse(in.nextLine());
304 Date mailTimestamp = format.parse(mail.timestamp);
305 if (mailTimestamp.after(lastAccessedTimestamp)) {
306 filtered.add(mailOuter);
307 }
308 } catch (FileNotFoundException e) {
309 // It may not have been created yet, then err on the safe side and add it in.
310 filtered.add(mailOuter);
311 } catch (ParseException e) {
312 // If we fail to parse, then err on the safe side and add it in.
313 filtered.add(mailOuter);
314 }
315 }
316 }
317
318 private static void getStandardSecureMail(String name, PrivateKey key, List<MailEntry> filtered, MailEntry mail)
319 throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, InvalidKeyException,
320 BadPaddingException {
321 // confirm this is a message for the requester of entries
322 String receiver = mail.receiver;
323 byte[] receiverBytes = Base64.getDecoder().decode(receiver);
324 String receiverDecrypted = null;
325 Cipher c = Cipher.getInstance(AsymmetricAlgorithm + AsymmetricAlgorithmParameters);
326 try {
327 c.init(Cipher.DECRYPT_MODE, key);
328 receiverDecrypted = new String(c.doFinal(receiverBytes));
329 } catch (InvalidKeyException | BadPaddingException e) {
330 return;
331 }
332
333 // add an unencrypted version of the message to the return list
334 if (receiverDecrypted.compareToIgnoreCase(name) == 0) {
335 c.init(Cipher.DECRYPT_MODE, key);
336 String sender = new String(c.doFinal(Base64.getDecoder().decode(mail.sender)));
337 c.init(Cipher.DECRYPT_MODE, key);
338 String message = new String(c.doFinal(Base64.getDecoder().decode(mail.message)));
339 c.init(Cipher.DECRYPT_MODE, key);
340 String message2 = new String(c.doFinal(Base64.getDecoder().decode(mail.message2)));
341
342 Map<String, String> options = new HashMap<String, String>();
343 for (String label: mail.options.keySet()) {
344 c.init(Cipher.DECRYPT_MODE, key);
345 String labelDecrypted = new String(c.doFinal(Base64.getDecoder().decode(label)));
346 c.init(Cipher.DECRYPT_MODE, key);
347 String actionNameDecrypted = new String(c.doFinal(Base64.getDecoder().decode(mail.options.get(label))));
348 options.put(labelDecrypted, actionNameDecrypted);
349 }
350
351 Path lastAccessedFile = Paths.get(FrameIO.DEAD_DROPS_PATH).resolve(UserSettings.UserName.get() + "+" + sender).resolve(name + ".last-accessed");
352 if (!lastAccessedFile.toFile().exists()) {
353 lastAccessedFile = Paths.get(FrameIO.DEAD_DROPS_PATH).resolve(sender + "+" + UserSettings.UserName.get()).resolve(name + ".last-accessed");
354 }
355 SimpleDateFormat format = new SimpleDateFormat("ddMMMyyyy[HH:mm]");
356 MailEntry mailEntry = new MailEntry(mail.timestamp, sender, receiverDecrypted, message, message2, options);
357 try (Scanner in = new Scanner(lastAccessedFile.toFile())) {
358 Date lastAccessedTimestamp = format.parse(in.nextLine());
359 Date mailTimestamp = format.parse(mail.timestamp);
360 if (mailTimestamp.after(lastAccessedTimestamp)) {
361 filtered.add(mailEntry);
362 }
363 } catch (FileNotFoundException e) {
364 // It may not have been created yet, then err on the safe side and add it in.
365 filtered.add(mailEntry);
366 } catch (ParseException e) {
367 // If we fail to parse, then err on the safe side and add it in.
368 filtered.add(mailEntry);
369 }
370
371 }
372 }
373
374 private static boolean isEncryptedOneOffMessage(MailEntry mail) {
375 return mail.sender.charAt(0) == '=';
376 }
377
378 /**
379 * Describes a piece of mail, either encrypted or decrypted.
380 */
381 public static class MailEntry {
382 public String timestamp;
383 public String sender;
384 public String receiver;
385 public String message;
386 public String message2;
387 public Map<String, String> options;
388 public Path deadDropSource;
389
390 public MailEntry(String timestamp, String sender, String rec, String message, String message2, Map<String, String> options) {
391 this.timestamp = timestamp;
392 this.sender = sender;
393 this.receiver = rec;
394 this.message = message;
395 this.message2 = message2;
396 this.options = options;
397 }
398 }
399
400 public static void checkMail(PrivateKey key) throws NoSuchAlgorithmException, NoSuchPaddingException,
401 IllegalBlockSizeException, BadPaddingException, InvalidKeyException, KeyStoreException,
402 FileNotFoundException, CertificateException, IOException, ClassNotFoundException, SQLException, ParseException {
403 MailBay.clear();
404 AuthenticatorBrowser.getInstance().loadMailDatabase();
405 List<MailEntry> mailForLoggingInUser = Mail.getEntries(UserSettings.UserName.get(), key);
406 for (MailEntry mail: mailForLoggingInUser) {
407 MailBay.addMessage(mail.timestamp, mail.sender, mail.message, mail.message2, mail.options);
408 }
409
410 // Update last read files.
411 Path deadDropPath = Paths.get(FrameIO.DEAD_DROPS_PATH);
412 for (File connectionDir: deadDropPath.toFile().listFiles()) {
413 if (connectionDir.isDirectory()) {
414 Path deaddropforcontactPath = Paths.get(connectionDir.getAbsolutePath());
415 AuthenticatorBrowser.getInstance().updateLastReadMailTime(deaddropforcontactPath);
416 }
417 }
418 }
419
420 public static void decryptOneOffSecureMessage(SecretKey key, List<String> data) {
421 byte[] topicBytes = Base64.getDecoder().decode(data.get(3));
422 String topic = new String(org.expeditee.encryption.Actions.DecryptSymmetric(topicBytes, key));
423 byte[] messageBytes = Base64.getDecoder().decode(data.get(4));
424 String message = new String(org.expeditee.encryption.Actions.DecryptSymmetric(messageBytes, key));
425 Map<String, String> options = new HashMap<String, String>();
426 for (int i = 5; i < data.size(); i+=2) {
427 byte[] optionKeyBytes = Base64.getDecoder().decode(data.get(i));
428 String k = new String(org.expeditee.encryption.Actions.DecryptSymmetric(optionKeyBytes, key));
429 byte[] optionValueBytes = Base64.getDecoder().decode(data.get(i + 1));
430 String v = new String(org.expeditee.encryption.Actions.DecryptSymmetric(optionValueBytes, key));
431 options.put(k, v);
432 }
433 MailBay.addMessage(data.get(0), "Single-use Secure (encrypted sender)", topic, message, options);
434 }
435}
Note: See TracBrowser for help on using the repository browser.