/* * [Decrypt.java] * * Summary: Decrypt 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 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.ByteArrayInputStream; import java.io.DataInputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.text.DecimalFormat; import java.util.prefs.Preferences; import java.util.zip.Adler32; import static java.lang.System.*; /** * Decrypt 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 Decrypt extends JFrame { /** * height of app in pixels including top frame and bottom border. */ private static final int HEIGHT = 150; // todo: make sure only one instance running. /** * 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 decrypt, then click Decrypt."; /** * prompt */ private static final String SELECT_FILE_SHORT = "select file to decrypt"; /** * format remaining bytes in pad inventory. */ private static final DecimalFormat df = new DecimalFormat( "###,##0" ); /** * buffer for reading pad bytes */ private final byte[] padChunk = new byte[ OTP.CHUNKSIZE ]; /** * computer checksum of all bytes including unencrypetd header1 and header 2. */ private Adler32 digester; /** * holds components */ private Container contentPane; /** * file after decryption */ private File decryptedFile; /** * file to decrypt */ private File encryptedFile; /** * one time pad */ private File padFile; /** * 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 decryptedFilenameField; /** * filename to be processed */ private JTextField encryptedFilenameField; /** * used to read PAD file */ private RandomAccessFile padRaf; /** * one time pad */ private String padFilename; /** * length of header1 */ private int header1Length; /** * length of header2 including disguiser bytes */ private int header2Length; /** * how many units of progress we have completed */ private int ticks; /** * start of payload */ private long offsetOfPayload; /** * start of bytes for encrypted header2 */ private long padOffsetForHeader2; /** * start of payload */ private long padOffsetForPayload; /** * Constructor */ private Decrypt() { super( "Decrypt" ); } /** * read a UTF-16 string of known length from a DataInputStream * * @param dis DataInputStream * @param length how many chars long * * @return String * @throws EOFException */ private static String readString( DataInputStream dis, int length ) throws IOException { char[] c = new char[ length ]; for ( int i = 0; i < length; i++ ) { c[ i ] = dis.readChar(); } return String.valueOf( c ); } /** * Prompt user to key in a file * * @param defaultFilenameToDecrypt Current value for that directory. null or "" if none. * * @return Directory the user selected, or null if he refused to select one. */ private String askUserForAFilenameToDecrypt( String defaultFilenameToDecrypt ) { instruct( SELECT_FILE_LONG, false ); final Preferences userPrefs = Preferences.userRoot().node( "/com/mindprod/otp" ); final File suggestedDir = new File( userPrefs.get( "DECRYPTDIR", "C:" ) ); final JFileChooser fc = new JFileChooser(); fc.setFileSelectionMode( JFileChooser.FILES_ONLY ); try { if ( defaultFilenameToDecrypt != null && defaultFilenameToDecrypt.length() != 0 && !defaultFilenameToDecrypt.equals( SELECT_FILE_SHORT ) ) { // use dir selected, possibly outside current directory tree final File defaultFileToDelete = new File( defaultFilenameToDecrypt ); 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( "DECRYPTDIR", fc.getCurrentDirectory() .getAbsolutePath() ); return fc.getSelectedFile().getCanonicalPath(); } else { return null; } } catch ( IOException e ) { return null; } } /** * allocate all the components */ private void buildComponents() { encryptedFilenameField = new JTextField( SELECT_FILE_SHORT ); encryptedFilenameField.setFont( OTP.FONT_FOR_FILENAME ); encryptedFilenameField.setForeground( OTP.FOREGROUND_FOR_FILENAME ); encryptedFilenameField.setToolTipText( "Do not edit directly. Click ..." ); encryptedFilenameField.setEditable( false );// use Change button instead changeFilename = new JButton( "..." ); changeFilename.setForeground( OTP.FOREGROUND_FOR_BUTTON ); changeFilename.setToolTipText( "Change the file" ); start = new JButton( "Decrypt" ); 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 ); decryptedFilenameField = new JTextField( "" ); decryptedFilenameField.setFont( OTP.FONT_FOR_FILENAME ); decryptedFilenameField.setForeground( OTP.FOREGROUND_FOR_FILENAME ); decryptedFilenameField.setToolTipText( "Decrypted filename" ); decryptedFilenameField.setEditable( false );// use Change button instead decryptedFilenameField.setVisible( false ); instructions = new JLabel( SELECT_FILE_LONG ); instructions.setForeground( OTP.FOREGROUND_FOR_INSTRUCTIONS ); instructions.setFont( OTP.FONT_FOR_INSTRUCTIONS ); } /** * computer Adlerian digest for two headers */ private void computeDigestForHeaders() throws IOException { // O P E N file to decrypt final FileInputStream fis = new FileInputStream( encryptedFile ); // 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. // We now know the precise length of everything. byte[] header1 = new byte[ header1Length ]; // R E A D int bytesRead = fis.read( header1 ); if ( bytesRead != header1.length ) { err.println( "failed to read encrypted file" ); } digester.update( header1 ); // read header2 byte[] header2 = new byte[ header2Length ]; // R E A D bytesRead = fis.read( header2 ); if ( bytesRead != header2.length ) { err.println( "failed to read encrypted file" ); } padRaf.seek( padOffsetForHeader2 ); xor( header2 ); digester.update( header2 ); } /** * decrypt the file. Should run on non-EDT thread. */ void decryptFile() { try { encryptedFile = new File( encryptedFilenameField.getText() ); if ( !encryptedFile.exists() ) { instructions.setText( "Oops: That file does not exist." ); instructions.setForeground( OTP.FOREGROUND_FOR_WARNING ); return; } if ( !encryptedFile.canRead() ) { instructions.setText( "Oops: The operating system refused permission to read that file." ); instructions.setForeground( OTP.FOREGROUND_FOR_WARNING ); return; } // O P E N pad readHeaders(); digester = new Adler32(); computeDigestForHeaders(); 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." ); } padRaf.seek( padOffsetForPayload ); // O P E N file to decrypt final FileInputStream encryptedFis = new FileInputStream( encryptedFile ); if ( offsetOfPayload != encryptedFis.skip( offsetOfPayload ) ) { err.println( "unable to skip through encryted file." ); System.exit( 2 ); } final FileOutputStream decryptedFos = new FileOutputStream( decryptedFile, false /* append */ ); // text of file, later encrypted contents of file. final byte[] textChunk = new byte[ OTP.CHUNKSIZE ]; final long length = encryptedFile.length() - offsetOfPayload - 4; /* not counting tail */ 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() { decryptedFilenameField.setVisible( false ); progressBar.setVisible( true ); progressBar.setMaximum( progressTicks ); progressBar.setValue( 0 ); } } ); instruct( "Decrypting " + EIO.getCanOrAbsPath( encryptedFile ), false ); ticks = 0; tick(); int bytesRead; for ( int chunkNo = 0; chunkNo < chunks; chunkNo++ ) { bytesRead = encryptedFis.read( textChunk ); if ( bytesRead != OTP.CHUNKSIZE ) { err.print( "Unable to read the file to be decrypted" ); System.exit( 2 ); } tick(); xor( textChunk ); digester.update( textChunk ); tick(); decryptedFos.write( textChunk ); tick(); } // now do final partial chunk final byte[] lastChunk = new byte[ lastChunkSize ]; bytesRead = encryptedFis.read( lastChunk ); if ( bytesRead != lastChunkSize ) { err.print( "Unable to read the file to be encrypted" ); System.exit( 2 ); } tick(); xor( lastChunk ); digester.update( lastChunk ); tick(); decryptedFos.write( lastChunk ); tick(); readTail( ( int ) digester.getValue(), encryptedFis ); // C L O S E padRaf.close(); encryptedFis.close(); decryptedFos.close(); tick(); OTP.wipe( "pad", padFile, padOffsetForHeader2, padOffsetForPayload - padOffsetForHeader2 + length, false /* do not delete */, instructions, progressBar ); if ( OTP.WIPE_ENCRYPTED_FILE ) { OTP.wipe( "encrypted file", encryptedFile, 0, encryptedFile.length(), true /* delete */, instructions, progressBar ); } decryptedFilenameField.setText( EIO.getCanOrAbsPath( decryptedFile ) ); progressBar.setVisible( false ); decryptedFilenameField.setVisible( true ); instruct( "Successfully decrypted", false ); } catch ( IOException e ) { err.println( e.getMessage() + "Cannot decrypt file" ); System.exit( 2 ); } } /** * hook up listeners for components */ void hookListeners() { changeFilename.addActionListener( new ActionListener() { /** * Invoked when an action occurs. */ public void actionPerformed( ActionEvent e ) { String newFilename = askUserForAFilenameToDecrypt( encryptedFilenameField.getText() ); if ( newFilename != null ) { encryptedFilenameField.setText( newFilename ); } } } ); start.addActionListener( new ActionListener() { public void actionPerformed( final ActionEvent event ) { final Thread t = new Thread( new Runnable() { public void run() { decryptFile(); } } ); 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" ); System.exit( 2 ); } catch ( ClassNotFoundException e ) { err.println( e.getMessage() + " corrupted pad inventory" ); 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 instuction 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( encryptedFilenameField, 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( decryptedFilenameField, /* 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 ) ); } /** * read the unencrypted and encrypted headers */ private void readHeaders() throws IOException { // O P E N file to decrypt DataInputStream encryptedFis = EIO.getDataInputStream( encryptedFile, 4 * 1024 ); // 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. // R E A D int padFilenameLength = encryptedFis.readShort(); padFilename = readString( encryptedFis, padFilenameLength ); padFile = new File( OTP.keysDir, padFilename ); padOffsetForHeader2 = encryptedFis.readLong(); padRaf = new RandomAccessFile( padFile, "r" /* read only */ ); padRaf.seek( padOffsetForHeader2 ); header1Length = 2 + padFilenameLength * 2 + 8; // start of encrypted header2 final long offsetOfHeader2 = header1Length; // 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. // we don't know how long this is, so we estimate. // O P E N pad 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 decrypt 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 ); } int estimatedHeader2Size = ( int ) Math.min( 100, encryptedFile.length() - offsetOfHeader2 ); byte[] header2 = new byte[ estimatedHeader2Size ]; /* should not be longer than file */ bytesRead = encryptedFis.read( header2 ); if ( bytesRead != estimatedHeader2Size ) { err.println( "cannot read encrypted file" ); System.exit( 2 ); } encryptedFis.close(); padRaf.seek( padOffsetForHeader2 ); // decrypt header 2 xor( header2 ); // O P E N final ByteArrayInputStream bais = new ByteArrayInputStream( header2 ); final DataInputStream dis = new DataInputStream( bais ); // R E A D int unencryptedFilenameLength = dis.readShort(); final String decryptedFilename = readString( dis, unencryptedFilenameLength ); // put decrypted file clear dir, not same dir as encrypted as we did before. decryptedFile = new File( OTP.clearDir, decryptedFilename ); short disguiserLength = dis.readShort(); // there is no fis.getFilePointer() and we cannot ask how many bytes we have read. header2Length = 2 + unencryptedFilenameLength * 2 + 2 + disguiserLength; // C L O S E dis.close(); // we don't need to actually read or decrypt the disguise bytes, just bypass them. padOffsetForPayload = padOffsetForHeader2 + header2Length; offsetOfPayload = offsetOfHeader2 + header2Length; } /** * read the tail containing the 32-bit checksum. */ private void readTail( int computedChecksum, FileInputStream encryptedFis ) throws IOException { // O P E N file to decrypt final byte[] block = new byte[ 4 ]; int bytesRead = encryptedFis.read( block ); if ( bytesRead != 4 ) { err.println( "cannot read encrypted file" ); System.exit( 2 ); } // decrypt block xor( block ); final ByteArrayInputStream bais = new ByteArrayInputStream( block ); final DataInputStream dis = new DataInputStream( bais ); int expectedChecksum = dis.readInt(); if ( expectedChecksum != computedChecksum ) { err.println( "checksum failure. Likely tampering or damage to file or pads." ); System.exit( 2 ); } } /** * 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 ) { /* The singleton frame */ SwingUtilities.invokeLater( new Runnable() { /** * do all swing work on the swing thread. */ public void run() { final Decrypt theFrame = new Decrypt(); 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 } ); } }