/* * [Encrypt.java] * * Summary: Encrypt a file with a one-time pad. * * Copyright: (c) 2012-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 2012-12-30 initial release. */ package com.mindprod.otp; import com.mindprod.common18.EIO; import com.mindprod.hunkio.HunkIO; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import java.awt.Container; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.security.SecureRandom; import java.text.DecimalFormat; import java.util.prefs.Preferences; import java.util.zip.Adler32; import static java.lang.System.*; /** * Encrypt a file with a one-time pad. * * @author Roedy Green, Canadian Mind Products * @version 1.0 2012-12-30 initial release. * @since 2012-12-30 */ public final class Encrypt extends JFrame { /** * height of app in pixels including top frame and bottom border. */ private static final int HEIGHT = 150; /** * Width of application in pixels, including left and right borders. */ private static final int WIDTH = 440; /** * undisplayed copyright notice * * @noinspection UnusedDeclaration */ private static final String EMBEDDED_COPYRIGHT = "Copyright: (c) 2012-2017 Roedy Green, Canadian Mind Products, http://mindprod.com"; /** * prompt */ private static final String SELECT_FILE_LONG = "Specify the file you wish to encrypt, then click Encrypt."; /** * prompt */ private static final String SELECT_FILE_SHORT = "select file to encrypt"; /** * used to format remaining bytes in pad inventory. */ private static final DecimalFormat df = new DecimalFormat( "###,##0" ); /** * used to generated random bytes disguise the original length of the file */ private static final SecureRandom wheel = new SecureRandom(); /** * buffer for reading pad bytes */ private final byte[] padChunk = new byte[ OTP.CHUNKSIZE ]; /** * holds components */ private Container contentPane; /** * click to change the Filename */ private JButton changeFilename; /** * go to start the processing. */ private JButton start; /** * instruction on how to use the program */ private JLabel instructions; /** * track progress of wipe */ private JProgressBar progressBar; /** * filename encrypted, shares space with progress bar */ private JTextField filenameFieldEncrypted; /** * filename to be processed */ private JTextField filenameFieldToEncrypt; /** * used to read PAD file */ private RandomAccessFile padRaf; /** * how many units of progress we have completed */ private int ticks; /** * Constructor */ private Encrypt() { super( "Encrypt" ); } /** * build the unencrypted part of the header * * @param padFile file we are encrypting. * @param offset offset into the pad where we start grabbing encryption bytes. * * @return unencrypted bytes for header1 */ private static byte[] buildHeader1( File padFile, long offset ) throws IOException { // 16 bit int big endian : length in bytes of the pad file name (without directory), unencrypted // n bytes Unicode-16 : name of the pad file name (without directory), unencrypted // 64 bit bit endian : offset into pad to start. final String name = padFile.getName(); // O P E N final ByteArrayOutputStream baos = new ByteArrayOutputStream( name.length() * 2 + 2 + 8 ); final DataOutputStream dos = new DataOutputStream( baos ); // W R I T E dos.writeShort( name.length() ); dos.writeChars( name ); dos.writeLong( offset ); // C L O S E dos.close(); return baos.toByteArray(); } /** * build the encrypted part of the header * * @param fileToEncrypt file we are encrypting. * * @return unencrypted bytes for header2 */ private static byte[] buildHeader2( File fileToEncrypt ) throws IOException { // 16 bit int big endian : length in bytes of the original file name (without directory), unencrypted // n bytes Unicode-16 : name of the original file name (without directory), unencrypted // 16 bit int big endian : length in bytes of length disguiser ( 0 .. 99 ) // n bytes of 0s : used to disguise the length of the original message. // n bytes : contents of the original file. final String name = fileToEncrypt.getName(); short disguiserLength = ( short ) wheel.nextInt( OTP.MAX_DISGUISE_BYTES + 1 ); // O P E N final ByteArrayOutputStream baos = new ByteArrayOutputStream( name.length() * 2 + disguiserLength + 2 + 2 ); final DataOutputStream dos = new DataOutputStream( baos ); // W R I T E dos.writeShort( name.length() ); dos.writeChars( name ); dos.writeShort( disguiserLength ); // automaticaly init to 0 byte[] disguiser = new byte[ disguiserLength ]; dos.write( disguiser ); // C L O S E dos.close(); return baos.toByteArray(); } /** * build the tail * * @param computedChecksum accumulated checksum * * @return unencrypted 4 bytes for tail */ private static byte[] buildTail( int computedChecksum ) throws IOException { // 32 bit big-endian checksum of entire unencrypted file. // O P E N final ByteArrayOutputStream baos = new ByteArrayOutputStream( 4 ); final DataOutputStream dos = new DataOutputStream( baos ); dos.writeInt( computedChecksum ); return baos.toByteArray(); } /** * Prompt user to key in a file * * @param defaultFilenameToEncrypt Current value for that directory. null or "" if none. * * @return Directory the user selected, or null if he refused to select one. */ private String askUserForAFileToEncrypt( String defaultFilenameToEncrypt ) { instruct( SELECT_FILE_LONG, false ); final Preferences userPrefs = Preferences.userRoot().node( "/com/mindprod/otp" ); final File suggestedDir = new File( userPrefs.get( "ENCRYPTDIR", "C:" ) ); final JFileChooser fc = new JFileChooser(); fc.setFileSelectionMode( JFileChooser.FILES_ONLY ); try { if ( defaultFilenameToEncrypt != null && defaultFilenameToEncrypt.length() != 0 && !defaultFilenameToEncrypt.equals( SELECT_FILE_SHORT ) ) { // use dir whatever user selected, might be outside current directory tree. final File defaultFileToDelete = new File( defaultFilenameToEncrypt ); if ( defaultFileToDelete.exists() ) { fc.setSelectedFile( defaultFileToDelete ); } else { fc.setCurrentDirectory( suggestedDir ); } } else { fc.setCurrentDirectory( suggestedDir ); } if ( fc.showOpenDialog( this ) == JFileChooser.APPROVE_OPTION ) { // save current selected directory userPrefs.put( "ENCRYPTDIR", fc.getCurrentDirectory() .getAbsolutePath() ); return fc.getSelectedFile().getCanonicalPath(); } else { return null; } } catch ( IOException e ) { return null; } } /** * allocate all the components */ private void buildComponents() { filenameFieldToEncrypt = new JTextField( SELECT_FILE_SHORT ); filenameFieldToEncrypt.setFont( OTP.FONT_FOR_FILENAME ); filenameFieldToEncrypt.setForeground( OTP.FOREGROUND_FOR_FILENAME ); filenameFieldToEncrypt.setToolTipText( "Do not edit directly. Click ..." ); filenameFieldToEncrypt.setEditable( false );// use Change button instead changeFilename = new JButton( "..." ); changeFilename.setForeground( OTP.FOREGROUND_FOR_BUTTON ); changeFilename.setToolTipText( "Change the file" ); start = new JButton( "Encrypt" ); start.setForeground( OTP.FOREGROUND_FOR_BUTTON ); progressBar = new JProgressBar( 0, 100 ); // max will be changed later progressBar.setValue( 0 ); // no wording on progress bar progressBar.setStringPainted( false ); progressBar.setForeground( OTP.FOREGROUND_FOR_PROGRESS ); filenameFieldEncrypted = new JTextField( "" ); filenameFieldEncrypted.setFont( OTP.FONT_FOR_FILENAME ); filenameFieldEncrypted.setForeground( OTP.FOREGROUND_FOR_FILENAME ); filenameFieldEncrypted.setToolTipText( "Encrypted filename" ); filenameFieldEncrypted.setEditable( false );// use Change button instead filenameFieldEncrypted.setVisible( false ); instructions = new JLabel( SELECT_FILE_LONG ); instructions.setForeground( OTP.FOREGROUND_FOR_INSTRUCTIONS ); instructions.setFont( OTP.FONT_FOR_INSTRUCTIONS ); } /** * encrypt the file. selects pad. Should run on non-EDT thread. */ void encryptFile() { try { final File fileToEncrypt = new File( filenameFieldToEncrypt.getText() ); if ( !fileToEncrypt.exists() ) { instructions.setText( "Oops: That file does not exist." ); instructions.setForeground( OTP.FOREGROUND_FOR_WARNING ); return; } if ( !fileToEncrypt.canRead() ) { instructions.setText( "Oops: The operating system refused permission to read that file." ); instructions.setForeground( OTP.FOREGROUND_FOR_WARNING ); return; } final byte[] header2 = buildHeader2( fileToEncrypt ); final long padBytesNeeded = fileToEncrypt.length() + header2.length; final PadInfo currentPadInfo = PadInfo.findBestPad( padBytesNeeded ); if ( currentPadInfo == null ) { //todo: proper notify err.println( "no pad with sufficient room" ); System.exit( 2 ); } final String padFileName = currentPadInfo.getPadFilename(); final File padFile = new File( OTP.keysDir, padFileName ); if ( !padFile.exists() ) { throw new IllegalArgumentException( "Pad file " + padFileName + " does not exist." ); } if ( !padFile.canRead() ) { throw new IllegalArgumentException( "Pad file " + padFileName + " cannot be read." ); } if ( !padFile.canWrite() ) { // canWrite true implies file exists. throw new IllegalArgumentException( "Pad file " + padFileName + " cannot be written." ); } // O P E N pad padRaf = new RandomAccessFile( padFile, "r" /* read only */ ); // O P E N file to encrypt final FileInputStream unencryptedFis = new FileInputStream( fileToEncrypt ); // O P E N encrypted file final File encryptedFile = HunkIO.createTempFile( "enc", ".encrypted", OTP.encryptedDir ); final FileOutputStream encryptedFos = new FileOutputStream( encryptedFile, false /* append */ ); final long padOffsetForHeader2 = currentPadInfo.getOffset(); final byte[] header1 = buildHeader1( padFile, padOffsetForHeader2 ); encryptedFos.write( header1 ); // compute checksum on unencryted data final Adler32 digester = new Adler32(); digester.update( header1 ); digester.update( header2 ); padRaf.seek( padOffsetForHeader2 ); final byte[] sampling = new byte[ 100 ]; int bytesRead = padRaf.read( sampling ); if ( bytesRead != sampling.length ) { err.println( "failed to read sufficient pad bytes" ); } boolean wiped = true; for ( final byte aSampling : sampling ) { if ( aSampling != 0 ) { wiped = false; break; } } if ( wiped ) { err.println( "The pad file needed to encrypt that file has been wiped." ); err.println( OTP.EVEN_WHEN_SIMULATING ); err.println( OTP.DIFFERENT_CURRENT_DIRS ); err.println( OTP.START_OVER ); System.exit( 2 ); } padRaf.seek( padOffsetForHeader2 ); xor( header2 ); encryptedFos.write( header2 ); assert padRaf.getFilePointer() == padOffsetForHeader2 + header2.length; // W R I T E final long length = fileToEncrypt.length(); // mark pad bytes as used. currentPadInfo.setOffset( padOffsetForHeader2 + padBytesNeeded ); // text of file, later encrypted contents of file. final byte[] textChunk = new byte[ OTP.CHUNKSIZE ]; final long chunks = length / OTP.CHUNKSIZE; final int lastChunkSize = ( int ) ( length % OTP.CHUNKSIZE ); final int progressTicks = 2 /* open/close */ + 5 * ( ( int ) chunks + 1 ); SwingUtilities.invokeLater( new Runnable() { public void run() { filenameFieldEncrypted.setVisible( false ); progressBar.setVisible( true ); progressBar.setMaximum( progressTicks ); progressBar.setValue( 0 ); } } ); instruct( "Encrypting " + EIO.getCanOrAbsPath( fileToEncrypt ), false ); ticks = 0; tick(); for ( int chunkNo = 0; chunkNo < chunks; chunkNo++ ) { bytesRead = unencryptedFis.read( textChunk ); if ( bytesRead != OTP.CHUNKSIZE ) { err.print( "Unable to read the file to be encrypted" ); System.exit( 2 ); } tick(); digester.update( textChunk ); tick(); xor( textChunk ); encryptedFos.write( textChunk ); tick(); } // now do final partial chunk if ( lastChunkSize != 0 ) { byte[] lastChunk = new byte[ lastChunkSize ]; bytesRead = unencryptedFis.read( lastChunk ); if ( bytesRead != lastChunkSize ) { err.print( "Unable to read the file to be encrypted" ); System.exit( 2 ); } tick(); digester.update( lastChunk ); tick(); xor( lastChunk ); encryptedFos.write( lastChunk ); tick(); } // put out the tail 4 bytes with checksum, encrypted final byte[] tail = buildTail( ( int ) digester.getValue() ); xor( tail ); encryptedFos.write( tail ); // C L O S E padRaf.close(); unencryptedFis.close(); encryptedFos.close(); tick(); OTP.wipe( "pad", padFile, padOffsetForHeader2, header2.length + length, false /* do not delete */, instructions, progressBar ); if ( OTP.WIPE_PLAIN_TEXT_FILE ) { OTP.wipe( "plain text file", fileToEncrypt, 0, fileToEncrypt.length(), true /* delete */, instructions, progressBar ); } filenameFieldEncrypted.setText( EIO.getCanOrAbsPath( encryptedFile ) ); progressBar.setVisible( false ); filenameFieldEncrypted.setVisible( true ); instruct( "Successfully encrypted", false ); } catch ( IOException e ) { instruct( e.getMessage() + " i/o failed", true ); } } /** * hook up listeners for components */ void hookListeners() { changeFilename.addActionListener( new ActionListener() { /** * Invoked when an action occurs. */ public void actionPerformed( ActionEvent e ) { String newFilename = askUserForAFileToEncrypt( filenameFieldToEncrypt.getText() ); if ( newFilename != null ) { filenameFieldToEncrypt.setText( newFilename ); } } } ); start.addActionListener( new ActionListener() { public void actionPerformed( final ActionEvent event ) { final Thread t = new Thread( new Runnable() { public void run() { encryptFile(); } } ); t.start(); } } ); } /** * Start off. Like init in Applet */ void init() { System.setProperty( "swing.aatext", "true" ); try { UIManager.setLookAndFeel( new com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel() ); } catch ( UnsupportedLookAndFeelException e ) { err.println( "Unable to set Look and Feel. Continuing with default" ); } contentPane = this.getContentPane(); contentPane.setLayout( new GridBagLayout() ); contentPane.setBackground( OTP.BACKGROUND_FOR_FRAME ); buildComponents(); hookListeners(); layoutFields(); OTP.mkDirs(); try { PadInfo.loadInventory(); PadInfo.updateInventory(); } catch ( IOException e ) { err.println( e.getMessage() + " unable to load pad inventory" ); err.println( OTP.START_OVER ); System.exit( 2 ); } catch ( ClassNotFoundException e ) { err.println( e.getMessage() + " corrupted pad inventory" ); err.println( OTP.START_OVER ); System.exit( 2 ); } long remainingbytes = PadInfo.getPadBytesRemaining(); // todo: warning if getting low out.println( "Remaining bytes in pad inventory: " + df.format( remainingbytes ) ); if ( remainingbytes < 100 * 1024 ) { err.println( "Supply of pads is getting low." ); } int disappeared = PadInfo.getPadsDisappearing(); if ( disappeared > 0 ) { err.println( disappeared + " pad filed disappeared unexpectedly. Possible tampering." ); } int appeared = PadInfo.getPadsAppearing(); if ( appeared > 0 ) { out.println( appeared + " new pad files appeared unexpectedly. If you did not put them there, " + "possible tampering." ); } // todo: proper dialog } /** * put message in the instruction slot * * @param message message to display * @param warn true if this is a warning. */ void instruct( final String message, final boolean warn ) { SwingUtilities.invokeLater( new Runnable() { public void run() { instructions.setText( message ); instructions.setForeground( warn ? OTP.FOREGROUND_FOR_WARNING : OTP.FOREGROUND_FOR_INSTRUCTIONS ); } } ); } /** * layout fields in GridBag */ private void layoutFields() { // 0_________ _1____ // file----- fc --- 0 | // -progress-- GO -- 1 | jpanel // --instructions --- 4 // JPanel p = new JPanel(); p.setLayout( new GridBagLayout() ); p.setBackground( OTP.BACKGROUND_FOR_FRAME ); p.setBorder( BorderFactory.createLineBorder( OTP.FOREGROUND_FOR_BORDER ) ); // x y w h wtx wty anchor fill T L B R padx pady p.add( filenameFieldToEncrypt, new GridBagConstraints( 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets( 5, 5, 0, 2 ), 0, 0 ) ); // x y w h wtx wty anchor fill T L B R padx pady p.add( changeFilename, new GridBagConstraints( 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 5, 2, 0, 5 ), 10, 0 ) ); // x y w h wtx wty anchor fill T L B R padx pady p.add( progressBar, new GridBagConstraints( 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets( 5, 5, 5, 2 ), 0, 0 ) ); // x y w h wtx wty anchor fill T L B R padx pady p.add( filenameFieldEncrypted, /* same slot as progress bar */ new GridBagConstraints( 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets( 5, 5, 5, 2 ), 0, 0 ) ); // x y w h wtx wty anchor fill T L B R padx pady p.add( start, new GridBagConstraints( 1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 5, 2, 5, 5 ), 0, 0 ) ); contentPane.add( p, new GridBagConstraints( 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets( 5, 5, 5, 5 ), 0, 0 ) ); // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( instructions, new GridBagConstraints( 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets( 0, 5, 5, 5 ), 0, 0 ) ); } /** * mark one unit of progress */ private void tick() { ticks++; SwingUtilities.invokeLater( new Runnable() { public void run() { progressBar.setValue( ticks ); } } ); } /** * xor a block of byte with next bytes is padRaf */ void xor( byte[] data ) throws IOException { assert data.length <= OTP.CHUNKSIZE; int bytesRead = padRaf.read( padChunk, 0, data.length ); if ( bytesRead != data.length ) { err.print( "Unable to read the pad file" ); System.exit( 2 ); } tick(); for ( int i = 0; i < data.length; i++ ) { data[ i ] = ( byte ) ( data[ i ] ^ padChunk[ i ] ); } tick(); } /** * Main class for client. This a Java Web Start, not a JApplet. * * @param args not used. */ public static void main( String[] args ) { /* start singleton OTP frame */ SwingUtilities.invokeLater( new Runnable() { /** * do all swing work on the swing thread. */ public void run() { final Encrypt theFrame = new Encrypt(); theFrame.setSize( WIDTH, HEIGHT ); theFrame.init(); theFrame.setResizable( OTP.RESIZABLE ); theFrame.setUndecorated( false ); theFrame.setDefaultCloseOperation( JFrame.DO_NOTHING_ON_CLOSE ); // allow some extra room for the frame title bar. theFrame.addWindowListener( new WindowAdapter() { /** * Handle request to shutdown. * @param event event giving details of closing. */ public void windowClosing( WindowEvent event ) { try { PadInfo.saveInventory(); } catch ( IOException e ) { err.println( e.getMessage() + " Unable to save inventory" ); System.exit( 2 ); } System.exit( 0 ); } // end WindowClosing } // end anonymous class );// end addWindowListener line // yes frame, not contentpane. theFrame.validate(); theFrame.setVisible( true ); } // end run } ); } }