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

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