Ignore:
Timestamp:
01/29/20 13:20:24 (4 years ago)
Author:
bnemhaus
Message:

Revised implementation of authenticated Expeditee mail. Motivated by bugs relating to messages not being marked as read and incorrect counting of new messages for users, the Expeditee mail system has been rewritten. The new code not only does not exhibit the previous bugs but is also better engineered. Whilst the MailBay is static (which is in line with the MessageBay), the Mail class is no longer static and must be initialised for each user as they log in.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/expeditee/auth/mail/Mail.java

    r1483 r1504  
    33import java.io.File;
    44import java.io.FileNotFoundException;
     5import java.io.FileWriter;
    56import java.io.IOException;
    67import java.nio.file.Path;
    78import java.nio.file.Paths;
    89import java.security.InvalidKeyException;
     10import java.security.Key;
     11import java.security.KeyFactory;
    912import java.security.KeyStoreException;
    1013import java.security.NoSuchAlgorithmException;
     
    1316import java.security.cert.CertificateException;
    1417import java.security.spec.InvalidKeySpecException;
     18import java.security.spec.PKCS8EncodedKeySpec;
    1519import java.sql.Connection;
    1620import java.sql.DriverManager;
    1721import java.sql.PreparedStatement;
     22import java.sql.ResultSet;
    1823import java.sql.SQLException;
    1924import java.sql.Statement;
     
    2833import java.util.Map;
    2934import java.util.Scanner;
     35import java.util.function.Supplier;
    3036
    3137import javax.crypto.BadPaddingException;
     
    3440import javax.crypto.NoSuchPaddingException;
    3541import javax.crypto.SecretKey;
    36 import javax.crypto.spec.SecretKeySpec;
    3742
    3843import org.expeditee.auth.AuthenticatorBrowser;
    39 import org.expeditee.auth.mail.gui.MailBay;
    4044import org.expeditee.encryption.CryptographyConstants;
    4145import org.expeditee.gui.FrameIO;
    42 import org.expeditee.settings.UserSettings;
     46import org.expeditee.gui.MessageBay;
     47import org.expeditee.items.Text;
     48import org.expeditee.settings.identity.secrets.KeyList;
    4349
    4450public class Mail implements CryptographyConstants {
     51        private String user;
     52        public static final SimpleDateFormat FORMAT = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z");
     53
     54        public Mail(String user) {
     55                this.user = user.toLowerCase();
     56        }
    4557       
    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
     58        public void sendMail(MailEntry mail, String colleagueName) {
     59                // Get database file to place mail into
     60                Path databaseFilePath = getDatabaseFilePath(colleagueName);
     61
     62                // Obtain colleagues public key
    8863                PublicKey publicKey = null;
    8964                try {
     
    9166                } catch (InvalidKeySpecException | NoSuchAlgorithmException | KeyStoreException | CertificateException
    9267                                | 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());
     68                        System.err.println("Error while sending message.  Unable to obtain public key for colleague "
     69                                        + colleagueName + ".  Exception message: " + e.getMessage());
    9570                        return;
    9671                }
    97                
     72
    9873                // Check we got public key
    9974                if (publicKey == null) {
    100                         System.err.println("Error while sending message.  Unable to obtain public key for colleague.  Have you exchanged contact details?");
     75                        System.err.println("Error while sending message.  " + "Unable to obtain public key for colleague.  "
     76                                        + "Have you exchanged contact details?");
    10177                        return;
    10278                }
     79
     80                // Send message
     81                MailEntry encryptedMailEntryForSending = mail.encryptMailEntry(publicKey, () -> {
     82                        try {
     83                                return Cipher.getInstance(AsymmetricAlgorithm + AsymmetricAlgorithmParameters);
     84                        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
     85                                e.printStackTrace();
     86                                return null;
     87                        }
     88                }, "");
     89                encryptedMailEntryForSending.writeToMailDatabase(databaseFilePath);
     90
     91                // Do it all again if demo mode is on (ensures sync without having to move db
     92                // files)
     93                if (Boolean.getBoolean("expeditee.demo-mode")) {
     94                        databaseFilePath = getDatabaseFilePath(colleagueName, true);
     95                        encryptedMailEntryForSending.writeToMailDatabase(databaseFilePath);
     96                }
     97        }
     98
     99        public void sendOneOffMail(MailEntry mail, String colleagueName, SecretKey key) {
     100                // Get database file to place mail into
     101                Path databaseFilePath = getDatabaseFilePath(colleagueName);
     102
     103                // Send message
     104                MailEntry encryptedMailEntryForSending = mail.encryptMailEntry(key, () -> {
     105                        try {
     106                                return Cipher.getInstance(SymmetricAlgorithm + SymmetricAlgorithmParameters);
     107                        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
     108                                e.printStackTrace();
     109                                return null;
     110                        }
     111                }, MailEntry.SINGLE_USE_SENDER_TAG);
     112                encryptedMailEntryForSending.writeToMailDatabase(databaseFilePath);
     113
     114                // Do it all again if demo mode is on (ensures sync without having to move db
     115                // files)
     116                if (Boolean.getBoolean("expeditee.demo-mode")) {
     117                        databaseFilePath = getDatabaseFilePath(colleagueName, true);
     118                        encryptedMailEntryForSending.writeToMailDatabase(databaseFilePath);
     119                }
     120        }
     121
     122        public List<MailEntry> checkMail() {
     123                PrivateKey key = getPrivateKey();
     124                List<Path> databasePaths = getDatabaseInboxFiles();
     125                List<MailEntry> receivedMailEncrypted = loadMailFromDatabases(databasePaths);
     126                List<MailEntry> receivedMail = decryptMailEntries(receivedMailEncrypted, key);
     127                // TODO: delete processed messages from database files
     128                touchLastAccessedFiles(databasePaths);
     129                return receivedMail;
     130        }
     131
     132        public class MailEntry {
     133                private static final String SINGLE_USE_SENDER_TAG = "=";
     134                private String timestamp;
     135                private String sender;
     136                private String receiver;
     137                private String subject;
     138                private String message;
     139                private Map<String, String> options;
     140                private Path source;
     141
     142                public MailEntry(String timestamp, String sender, String receiver, String subject, String message,
     143                                Map<String, String> options) {
     144                        this.timestamp = timestamp;
     145                        this.sender = sender;
     146                        this.receiver = receiver;
     147                        this.subject = subject;
     148                        this.message = message;
     149                        this.options = options;
     150                }
     151
     152                //@formatter:off
     153                public String getTimestamp() { return timestamp; }
     154                public String getSender() { return sender; }
     155                public String getReceiver() { return receiver; }
     156                public String getSubject() { return subject; }
     157                public String getMessage() { return message; }
     158                public Map<String, String> getOptionsTextActionMap() { return options; }
     159                public Path getDatabaseSource() { return source; }
     160                public void setDatabaseSource(Path source) { this.source = source; }
     161                public boolean isSingleUseEncryption() {
     162                        return sender.startsWith(MailEntry.SINGLE_USE_SENDER_TAG);
     163                }
     164                //@formatter:on
     165
     166                private MailEntry encryptMailEntry(Key key, Supplier<Cipher> generateCipher, String markSender) {
     167                        if (markSender == null)
     168                                markSender = "";
     169                        try {
     170                                // Encrypt sender, receiver, subject and message
     171                                Cipher cipher = generateCipher.get();
     172                                cipher.init(Cipher.ENCRYPT_MODE, key);
     173                                String sender = markSender + Base64.getEncoder().encodeToString(cipher.doFinal(getSender().toLowerCase().getBytes()));
     174                                cipher.init(Cipher.ENCRYPT_MODE, key);
     175                                String receiver = Base64.getEncoder().encodeToString(cipher.doFinal(getReceiver().getBytes()));
     176                                cipher.init(Cipher.ENCRYPT_MODE, key);
     177                                String subject = Base64.getEncoder().encodeToString(cipher.doFinal(getSubject().getBytes()));
     178                                cipher.init(Cipher.ENCRYPT_MODE, key);
     179                                String message = Base64.getEncoder().encodeToString(cipher.doFinal(getMessage().getBytes()));
     180
     181                                // Encrypt reply options to be provided to receiver
     182                                Map<String, String> optionsTextActionMap = new HashMap<String, String>();
     183                                Map<String, String> toEncOptionsTextActionMap = getOptionsTextActionMap();
     184                                for (String text : toEncOptionsTextActionMap.keySet()) {
     185                                        cipher.init(Cipher.ENCRYPT_MODE, key);
     186                                        String textEncrypted = Base64.getEncoder().encodeToString(cipher.doFinal(text.getBytes()));
     187                                        cipher.init(Cipher.ENCRYPT_MODE, key);
     188                                        String toEncAction = toEncOptionsTextActionMap.get(text);
     189                                        String actionEncrypted = Base64.getEncoder().encodeToString(cipher.doFinal(toEncAction.getBytes()));
     190                                        optionsTextActionMap.put(textEncrypted, actionEncrypted);
     191                                }
     192
     193                                return new MailEntry(getTimestamp(), sender, receiver, subject, message, optionsTextActionMap);
     194                        } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
     195                                e.printStackTrace();
     196                                return null;
     197                        }
     198                }
     199
     200                private void writeToMailDatabase(Path databaseFile) {
     201                        try {
     202                                Connection c = DriverManager.getConnection("jdbc:sqlite:" + databaseFile);
     203                                String sql = "INSERT INTO EXPMAIL (TIME,SND,REC,SUB,MSG,OPTS,OPTSVAL) VALUES (?, ?, ?, ?, ?, ?, ?);";
     204                                PreparedStatement statement = c.prepareStatement(sql);
     205                                statement.setString(1, getTimestamp());
     206                                statement.setString(2, getSender());
     207                                statement.setString(3, getReceiver());
     208                                statement.setString(4, getSubject());
     209                                statement.setString(5, message);
     210                                String opts = Arrays.toString(getOptionsTextActionMap().keySet().toArray());
     211                                statement.setString(6, opts);
     212                                String optsVal = Arrays.toString(getOptionsTextActionMap().values().toArray());
     213                                statement.setString(7, optsVal);
     214                                statement.execute();
     215                                statement.close();
     216                        } catch (SQLException e) {
     217                                e.printStackTrace();
     218                        }
     219                }
     220
     221                private MailEntry decryptMailEntry(Key key, Supplier<Cipher> generateCipher, String senderMarkFilter) {
     222                        Cipher cipher = generateCipher.get();
     223                        try {
     224                                // Confirm this is a message is intended for this recipient
     225                                cipher.init(Cipher.DECRYPT_MODE, key);
     226                                byte[] receiverBytes = Base64.getDecoder().decode(getReceiver());
     227                                String receiver = new String(cipher.doFinal(receiverBytes));
     228                                if (!receiver.toLowerCase().equals(user)) {
     229                                        return null;
     230                                }
     231
     232                                // Decrypt remainder of message
     233                                cipher.init(Cipher.DECRYPT_MODE, key);
     234                                byte[] senderBytes = Base64.getDecoder().decode(getSender());
     235                                String sender = new String(cipher.doFinal(senderBytes));
     236                                cipher.init(Cipher.DECRYPT_MODE, key);
     237                                byte[] subjectBytes = Base64.getDecoder().decode(getSubject());
     238                                String subject = new String(cipher.doFinal(subjectBytes));
     239
     240                                cipher.init(Cipher.DECRYPT_MODE, key);
     241                                byte[] messageBytes = Base64.getDecoder().decode(getMessage());
     242                                String message = new String(cipher.doFinal(messageBytes));
     243
     244                                Map<String, String> options = new HashMap<String, String>();
     245                                for (String text : getOptionsTextActionMap().keySet()) {
     246                                        cipher.init(Cipher.DECRYPT_MODE, key);
     247                                        byte[] textBytes = Base64.getDecoder().decode(text);
     248                                        String k = new String(cipher.doFinal(textBytes));
     249                                        cipher.init(Cipher.DECRYPT_MODE, key);
     250                                        byte[] actionBytes = Base64.getDecoder().decode(getOptionsTextActionMap().get(text));
     251                                        String v = new String(cipher.doFinal(actionBytes));
     252                                        options.put(k, v);
     253                                }
     254
     255                                return new MailEntry(getTimestamp(), sender, receiver, subject, message, options);
     256                        } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException e) {
     257                                e.printStackTrace();
     258                                return null;
     259                        }
     260                }
     261
     262                public String toString() {
     263                        StringBuilder sb = new StringBuilder("MailEntry { ");
     264                       
     265                        sb.append("[timestamp: " + getTimestamp() + "], ");
     266                        sb.append("[sender: " + getSender() + "], ");
     267                        sb.append("[receiver: " + getReceiver() + "], ");
     268                        sb.append("[subject: " + getSubject() + "], ");
     269                        sb.append("[message: " + getMessage() + "], ");
     270                       
     271                        sb.append(" }");
     272                        return sb.toString();
     273                }
     274        }
     275
     276        private Path getDatabaseFilePath(String colleagueName) {
     277                return getDatabaseFilePath(colleagueName, false);
     278        }
     279
     280        private Path getDatabaseFilePath(String colleagueName, boolean demoMode) {
     281                String deadDropsPath = Paths.get(FrameIO.PARENT_FOLDER).resolve("resources-" + user).resolve("deaddrops").toAbsolutePath().toString();//FrameIO.DEAD_DROPS_PATH;
    103282               
    104                 // Send message
    105                 sendMail(mail, publicKey, databaseFilePath);
     283//              if (deadDropsPath == null) {
     284//                      deadDropsPath = Paths.get(FrameIO.PARENT_FOLDER).resolve("resources-" + colleagueName).resolve("deadrops").toAbsolutePath().toString();
     285//              } else if (demoMode) {
     286//                      deadDropsPath = deadDropsPath.replace(user, colleagueName);
     287//              }
    106288               
    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();
     289                if (demoMode) {
     290                        deadDropsPath = deadDropsPath.replace(user, colleagueName);
     291                }
     292
     293                Path databaseFileDirectoryPath = Paths.get(deadDropsPath).resolve(user + "+" + colleagueName);
     294                if (!databaseFileDirectoryPath.toFile().exists()) {
     295                        databaseFileDirectoryPath = Paths.get(deadDropsPath).resolve(colleagueName + "+" + user);
     296                }
     297               
     298                if (!databaseFileDirectoryPath.toFile().exists()) {
     299                        databaseFileDirectoryPath.toFile().mkdirs();
     300                }
     301
     302                Path databaseFilePath = databaseFileDirectoryPath.resolve(colleagueName + ".db");
     303                if (!databaseFilePath.toFile().exists()) {
     304                        createMailDatabaseFile(databaseFilePath);
     305                }
     306                return databaseFilePath;
     307        }
     308
     309        private void createMailDatabaseFile(Path databaseFilePath) {
     310                databaseFilePath.getParent().toFile().mkdir();
     311                //@formatter:off
     312                String sql =
     313                                "CREATE TABLE EXPMAIL (" +
     314                                "TIME           TEXT                                    NOT NULL, " +
     315                                "SND            TEXT                                    NOT NULL, " +
     316                                "REC            TEXT                                    NOT NULL, " +
     317                                "SUB            TEXT                                    NOT NULL, " +
     318                                "MSG            TEXT                                    NOT NULL, " +
     319                                "OPTS           ARRAY                                   NOT NULL, " +
     320                                "OPTSVAL        ARRAY                                   NOT NULL)";
     321                //@formatter:on
     322                try (Connection c = DriverManager.getConnection("jdbc:sqlite:" + databaseFilePath.toAbsolutePath())) {
     323                        Statement createTable = c.createStatement();
     324                        createTable.executeUpdate(sql);
     325                        createTable.close();
     326                } catch (SQLException e) {
     327                        System.err.println("Error while creating database file.");
     328                        e.printStackTrace();
     329                }
     330        }
     331
     332        private PrivateKey getPrivateKey() {
     333                Text keyItem = KeyList.PrivateKey.get();
     334                String noKeyMessage = "No private key present: your communication with other Expeditee users will be limited until this is resolved.";
     335                if (keyItem.getData() != null) {
     336                        // Check mail.
     337                        String keyEncoded = keyItem.getData().get(0);
     338                        byte[] keyBytes = Base64.getDecoder().decode(keyEncoded);
     339                        try {
     340                                return KeyFactory.getInstance(AsymmetricAlgorithm).generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
     341                        } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
     342                                MessageBay.errorMessage(noKeyMessage);
     343                                MessageBay.errorMessage("See exception stack trace for more information.");
     344                                e.printStackTrace();
     345                                return null;
     346                        }
     347                } else {
     348                        MessageBay.errorMessage(noKeyMessage);
     349                        return null;
     350                }
     351        }
     352
     353        private List<Path> getDatabaseInboxFiles() {
     354                List<Path> databaseFiles = new ArrayList<Path>();
     355                Path deadDropPath = Paths.get(FrameIO.DEAD_DROPS_PATH);
     356                File deadDropDirectory = deadDropPath.toFile();
     357                if (!deadDropDirectory.exists()) { return databaseFiles; }
     358                for (File partnershipDirectoryCanditate : deadDropDirectory.listFiles()) {
     359                        if (partnershipDirectoryCanditate.isDirectory()) {
     360                                Path partnershipDirectory = Paths.get(partnershipDirectoryCanditate.getAbsolutePath());
     361                                Path dbFile = partnershipDirectory.resolve(user + ".db");
     362                                if (dbFile.toFile().exists()) {
     363                                        databaseFiles.add(dbFile);
     364                                }
     365                        }
     366                }
     367                return databaseFiles;
     368        }
     369
     370        private List<MailEntry> loadMailFromDatabases(List<Path> databasePaths) {
     371                List<MailEntry> mail = new ArrayList<MailEntry>();
     372
     373                for (Path dbPath : databasePaths) {
     374                        try {
     375                                mail.addAll(loadMailFromDatabase(dbPath));
    135376                        } 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()));
     377                                e.printStackTrace();
     378                        }
     379                }
     380
     381                return mail;
     382        }
     383
     384        private List<MailEntry> loadMailFromDatabase(Path dbPath) throws SQLException {
     385                List<MailEntry> mail = new ArrayList<MailEntry>();
     386
     387                Connection c = DriverManager.getConnection("jdbc:sqlite:" + dbPath.toAbsolutePath());
     388                String sql = "SELECT * FROM EXPMAIL";
     389                PreparedStatement query = c.prepareStatement(sql);
     390                ResultSet results = query.executeQuery();
     391
     392                while (results.next()) {
     393                        String timestamp = results.getString("TIME");
    197394                       
    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);
     395                        Path lastAccessedPath = dbPath.getParent().resolve(user + ".last-accessed");
     396                        try (Scanner in = new Scanner(lastAccessedPath.toFile())) {
     397                                Date lastAccessedTimestamp = FORMAT.parse(in.nextLine());
     398                                Date mailTimestamp = FORMAT.parse(timestamp);
     399                                if (mailTimestamp.before(lastAccessedTimestamp)) {
     400                                        continue;
     401                                }
     402                        } catch (FileNotFoundException | ParseException e) {
     403                                // It may not have been created yet, then err on the safe side and add it in.
     404                                // If it fails to parse then we again err on the safe side.
     405                        } catch (RuntimeException e) {
     406                                e.printStackTrace();
     407                                continue;
    205408                        }
    206409                       
    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();
     410                        String from = results.getString("SND");
     411                        String to = results.getString("REC");
     412                        String subject = results.getString("SUB");
     413                        String message = results.getString("MSG");
     414                        String[] opts = results.getString("opts").split(",");
     415                        opts[0] = opts[0].replace("[", "");
     416                        opts[opts.length - 1] = opts[opts.length - 1].replace("]", "");
     417                        String[] optsVal = results.getString("optsval").split(",");
     418                        optsVal[0] = optsVal[0].replace("[", "");
     419                        optsVal[optsVal.length - 1] = optsVal[optsVal.length - 1].replace("]", "");
     420                        Map<String, String> options = new HashMap<String, String>();
     421                        for (int i = 0; i < opts.length; i++) {
     422                                String key = opts[i].trim();
     423                                String val = optsVal[i].trim();
     424                                options.put(key, val);
     425                        }
     426
     427                        MailEntry mailEntry = new MailEntry(timestamp, from, to, subject, message, options);
     428                        mailEntry.setDatabaseSource(dbPath);
     429                        mail.add(mailEntry);
     430                }
     431
     432                results.close();
     433                query.close();
    258434                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);
     435
     436                return mail;
     437        }
     438
     439        private List<MailEntry> decryptMailEntries(List<MailEntry> encryptedMailMessages, PrivateKey key) {
     440                List<MailEntry> mail = new ArrayList<MailEntry>();
     441
     442                for (MailEntry encMail : encryptedMailMessages) {
     443                        if (encMail.isSingleUseEncryption()) {
     444                                MailEntry oneOffSecureSubstitute = generateOneOffSecureSubstitute(encMail);
     445                                if (oneOffSecureSubstitute != null) {
     446                                        mail.add(oneOffSecureSubstitute);
     447                                }
    273448                        } 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) {
     449                                MailEntry decryptMailEntry = encMail.decryptMailEntry(key, () -> {
     450                                        try {
     451                                                return Cipher.getInstance(AsymmetricAlgorithm + AsymmetricAlgorithmParameters);
     452                                        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
     453                                                e.printStackTrace();
     454                                                return null;
     455                                        }
     456                                }, "");
     457                                if (decryptMailEntry != null) {
     458                                        mail.add(decryptMailEntry);
     459                                }
     460                        }
     461                }
     462
     463                return mail;
     464        }
     465
     466        private void touchLastAccessedFiles(List<Path> databasePaths) {
     467                for (Path databasePath : databasePaths) {
     468                        Path lastAccessedFilePath = databasePath.getParent().resolve(user + ".last-accessed");
     469                        try (FileWriter out = new FileWriter(lastAccessedFilePath.toFile())) {
     470                                out.write(Mail.FORMAT.format(new Date()) + System.getProperty("line.separator"));
     471                        } catch (IOException e) {
     472                                e.printStackTrace();
     473                        }
     474                }
     475        }
     476
     477        private MailEntry generateOneOffSecureSubstitute(MailEntry toWrap) {
    282478                StringBuilder sb = new StringBuilder();
    283479                String sep = ":::";
    284480                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);
     481                sb.append(toWrap.timestamp + sep);
     482                sb.append(toWrap.sender.substring(1) + sep);
     483                sb.append(toWrap.receiver + sep);
     484                sb.append(toWrap.subject + sep);
     485                sb.append(toWrap.message + sep);
     486                for (String k: toWrap.options.keySet()) {
     487                        sb.append(k + sep + toWrap.options.get(k) + sep);
    292488                }
    293489                sb.reverse().delete(0, 3).reverse();
    294490                Map<String, String> options = new HashMap<String, String>();
    295491                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);
     492                MailEntry mailOuter = new MailEntry(toWrap.timestamp, "Unknown", user, "You have received a one-off secure message.",
     493                                "Check your email for more details.", options);
     494                return mailOuter;
     495        }
     496
     497        public String getUser() {
     498                return user;
    434499        }
    435500}
Note: See TracChangeset for help on using the changeset viewer.