From 596d19d2122dd2bc011aca36e89fc3e779a5551e Mon Sep 17 00:00:00 2001 From: HungryHobo Date: Sat, 5 Mar 2011 23:21:22 +0000 Subject: [PATCH] * Add command line programs Encrypt.java and Decrypt.java * EncryptedInputStream: throw a PasswordException if AESEngine.safeDecrypt() returns null --- src/i2p/bote/Configuration.java | 3 +- src/i2p/bote/Util.java | 18 ++ src/i2p/bote/email/Email.java | 2 +- src/i2p/bote/fileencryption/Decrypt.java | 116 ++++++++++ src/i2p/bote/fileencryption/Encrypt.java | 216 ++++++++++++++++++ .../fileencryption/EncryptedInputStream.java | 15 +- .../fileencryption/FileEncryptionUtil.java | 24 +- .../bote/fileencryption/PasswordCache.java | 19 +- 8 files changed, 387 insertions(+), 26 deletions(-) create mode 100644 src/i2p/bote/fileencryption/Decrypt.java create mode 100644 src/i2p/bote/fileencryption/Encrypt.java diff --git a/src/i2p/bote/Configuration.java b/src/i2p/bote/Configuration.java index 306a530f..f071ddae 100644 --- a/src/i2p/bote/Configuration.java +++ b/src/i2p/bote/Configuration.java @@ -30,6 +30,8 @@ import net.i2p.data.DataHelper; import net.i2p.util.Log; public class Configuration { + public static final String KEY_DERIVATION_PARAMETERS_FILE = "derivparams"; // name of the KDF parameter cache file, relative to I2P_BOTE_SUBDIR + private static final String I2P_BOTE_SUBDIR = "i2pbote"; // relative to the I2P app dir private static final String CONFIG_FILE_NAME = "i2pbote.config"; private static final String DEST_KEY_FILE_NAME = "local_dest.key"; @@ -39,7 +41,6 @@ public class Configuration { private static final String ADDRESS_BOOK_FILE_NAME = "addressBook"; private static final String MESSAGE_ID_CACHE_FILE = "msgidcache.txt"; private static final String PASSWORD_FILE = "password"; - private static final String KEY_DERIVATION_PARAMETERS_FILE = "derivparams"; private static final String OUTBOX_DIR = "outbox"; // relative to I2P_BOTE_SUBDIR private static final String RELAY_PKT_SUBDIR = "relay_pkt"; // relative to I2P_BOTE_SUBDIR private static final String INCOMPLETE_SUBDIR = "incomplete"; // relative to I2P_BOTE_SUBDIR diff --git a/src/i2p/bote/Util.java b/src/i2p/bote/Util.java index ab2da74e..1113a285 100644 --- a/src/i2p/bote/Util.java +++ b/src/i2p/bote/Util.java @@ -26,6 +26,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -231,6 +232,23 @@ public class Util { } } + /** + * Tests if a directory contains a file with a given name. + * @param directory + * @param filename + * @return + */ + public static boolean contains(File directory, final String filename) { + String[] matches = directory.list(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.equals(filename); + } + }); + + return matches!=null && matches.length>0; + } + /** * Creates an I2P destination with a null certificate from 384 bytes that * are read from a ByteBuffer. diff --git a/src/i2p/bote/email/Email.java b/src/i2p/bote/email/Email.java index bbf72f47..0dfa7709 100644 --- a/src/i2p/bote/email/Email.java +++ b/src/i2p/bote/email/Email.java @@ -654,7 +654,7 @@ public class Email extends MimeMessage { ByteArrayInputStream inputStream = new ByteArrayInputStream(uncompressedArray); ByteArrayOutputStream compressedStream = new ByteArrayOutputStream(); Encoder lzmaEncoder = new Encoder(); - lzmaEncoder.SetDictionarySize(1<<22); // dictionary size = 4 MBytes + lzmaEncoder.SetDictionarySize(1<<20); // dictionary size = 1 MByte lzmaEncoder.SetEndMarkerMode(true); // by using an end marker, the uncompressed size doesn't need to be stored with the compressed data lzmaEncoder.WriteCoderProperties(compressedStream); lzmaEncoder.Code(inputStream, compressedStream, -1, -1, null); diff --git a/src/i2p/bote/fileencryption/Decrypt.java b/src/i2p/bote/fileencryption/Decrypt.java new file mode 100644 index 00000000..fb86aeac --- /dev/null +++ b/src/i2p/bote/fileencryption/Decrypt.java @@ -0,0 +1,116 @@ +/** + * Copyright (C) 2009 HungryHobo@mail.i2p + * + * The GPG fingerprint for HungryHobo@mail.i2p is: + * 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12 + * + * This file is part of I2P-Bote. + * I2P-Bote is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * I2P-Bote is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with I2P-Bote. If not, see . + */ + +package i2p.bote.fileencryption; + +import i2p.bote.Util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.GeneralSecurityException; + +/** + * A command line program for decrypting I2P-Bote files. + */ +public class Decrypt { + + private Decrypt(String[] args) { + if (args.length < 1) { + printUsage(); + System.exit(1); + } + + File inputFile = new File(args[0]); + if (!inputFile.exists()) { + System.err.println("File not found: " + inputFile.getAbsolutePath()); + System.exit(1); + } + + byte[] password = promptForPassword(); + if (password == null) + System.exit(0); + + InputStream fileInputStream = null; + OutputStream output = null; + try { + fileInputStream = new FileInputStream(inputFile); + InputStream encryptedInputStream = new EncryptedInputStream(fileInputStream, password); + + if (args.length < 2) + Util.copy(encryptedInputStream, System.out); + else { + File outputFile = new File(args[1]); + output = new FileOutputStream(outputFile); + Util.copy(encryptedInputStream, output); + } + } catch (IOException e) { + System.err.println("I/O error: " + e.getLocalizedMessage()); + System.exit(1); + } catch (GeneralSecurityException e) { + System.err.println("Error: " + e.getLocalizedMessage()); + System.exit(1); + } catch (PasswordException e) { + System.err.println("Wrong password."); + System.exit(1); + } finally { + if (fileInputStream != null) + try { + fileInputStream.close(); + } catch (IOException e) { + System.err.println("Error closing input file."); + } + if (output != null) + try { + output.close(); + } catch (IOException e) { + System.err.println("Error closing output file."); + } + } + } + + private void printUsage() { + System.out.println("Syntax: Decrypt [output file]"); + System.out.println(); + System.out.println("Decrypts an input file and writes it to an output file."); + System.out.println("Existing files are overwritten without warning."); + System.out.println("If no output file is given, stdout is used instead."); + } + + private byte[] promptForPassword() { + System.out.print("Enter I2P-Bote password: "); + char[] passwordChars = System.console().readPassword(); + if (passwordChars != null) + return new String(passwordChars).getBytes(); + else + return null; + } + + /** + * @param args + */ + public static void main(String[] args) { + new Decrypt(args); + } +} \ No newline at end of file diff --git a/src/i2p/bote/fileencryption/Encrypt.java b/src/i2p/bote/fileencryption/Encrypt.java new file mode 100644 index 00000000..17230914 --- /dev/null +++ b/src/i2p/bote/fileencryption/Encrypt.java @@ -0,0 +1,216 @@ +/** + * Copyright (C) 2009 HungryHobo@mail.i2p + * + * The GPG fingerprint for HungryHobo@mail.i2p is: + * 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12 + * + * This file is part of I2P-Bote. + * I2P-Bote is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * I2P-Bote is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with I2P-Bote. If not, see . + */ + +package i2p.bote.fileencryption; + +import static i2p.bote.fileencryption.FileEncryptionConstants.SALT_LENGTH; +import i2p.bote.Configuration; +import i2p.bote.Util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +import net.i2p.I2PAppContext; +import net.i2p.data.Base64; + +/** + * A command line program for encrypting files in the I2P-Bote format. + */ +public class Encrypt { + private static final String VERBOSE_OPTION = "-v"; + private static final String DERIV_PARAMS_FILE_OPTION = "-d"; + + private boolean verbose; + private File inputFile; + private File outputFile; + private File derivParamsFile; + + private Encrypt(String[] args) { + if (args.length < 1) { + printUsage(); + System.exit(1); + } + + init(args); + + if (!inputFile.exists()) { + System.err.println("File not found: " + inputFile.getAbsolutePath()); + System.exit(1); + } + + byte[] password = promptForPassword(); + if (password == null) + System.exit(0); + + InputStream input = null; + OutputStream encryptedOutputStream = null; + try { + DerivedKey derivedKey = getDerivedKey(password, inputFile); + input = new FileInputStream(inputFile); + + OutputStream output; + if (outputFile == null) + output = System.out; + else + output = new FileOutputStream(outputFile); + + if (verbose) { + System.out.println("Parameters:"); + System.out.println(" N = " + derivedKey.scryptParams.N); + System.out.println(" r = " + derivedKey.scryptParams.r); + System.out.println(" p = " + derivedKey.scryptParams.p); + System.out.println(" Salt = " + Base64.encode(derivedKey.salt)); + } + + encryptedOutputStream = new EncryptedOutputStream(output, derivedKey); + Util.copy(input, encryptedOutputStream); + + if (verbose) + System.out.println("Encryption finished."); + } catch (IOException e) { + System.err.println("I/O error: " + e.getLocalizedMessage()); + System.exit(1); + } catch (GeneralSecurityException e) { + System.err.println("Error: " + e.getLocalizedMessage()); + System.exit(1); + } catch (PasswordException e) { + System.err.println("Wrong password."); + System.exit(1); + } finally { + if (input != null) + try { + input.close(); + } catch (IOException e) { + System.err.println("Error closing input file."); + } + if (encryptedOutputStream != null) + try { + encryptedOutputStream.close(); + } catch (IOException e) { + System.err.println("Error closing output file."); + } + } + } + + private void printUsage() { + System.out.println("Syntax: Encrypt [v] [-d file] [output file]"); + System.out.println(); + System.out.println("Encrypts an input file and writes it to an output file."); + System.out.println("Existing files are overwritten without warning."); + System.out.println("If no output file is given, stdout is used instead."); + System.out.println(); + System.out.println("Options:"); + System.out.println(" " + VERBOSE_OPTION + " Print some additional messages"); + System.out.println(" " + DERIV_PARAMS_FILE_OPTION + " file Read derivation parameters from this file"); + } + + /** + * Reads command line parameters into instance variables + */ + private void init(String[] args) { + for (int i=0; i= args.length) + System.err.println("Warning: " + DERIV_PARAMS_FILE_OPTION + " not followed by a filename, ignoring."); + else + derivParamsFile = new File(args[i]); + } + else if (inputFile == null) + inputFile = new File(arg); + else + outputFile = new File(arg); + } + + if (verbose) { + System.out.println("Input file: " + inputFile.getAbsolutePath()); + if (outputFile == null) + System.out.println("No output file given, writing to stdout."); + else + System.out.println("Output file: " + outputFile.getAbsolutePath()); + } + } + + private byte[] promptForPassword() { + System.out.print("Enter a password: "); + char[] passwordChars1 = System.console().readPassword(); + System.out.print("Enter the password again: "); + char[] passwordChars2 = System.console().readPassword(); + if (!Arrays.equals(passwordChars1, passwordChars2)) { + System.err.println("The two password don't match."); + System.exit(1); + } + if (passwordChars1 != null) + return new String(passwordChars1).getBytes(); + else + return null; + } + + private DerivedKey getDerivedKey(byte[] password, File inputFile) throws GeneralSecurityException { + if (derivParamsFile == null) { + // the derivation parameters file is in the I2P-Bote root dir, so search all parent directories + File parentDir = inputFile.getParentFile(); + while (derivParamsFile==null && parentDir!=null) { + boolean paramsFileFound = Util.contains(parentDir, Configuration.KEY_DERIVATION_PARAMETERS_FILE); + if (paramsFileFound) + derivParamsFile = new File(parentDir, Configuration.KEY_DERIVATION_PARAMETERS_FILE); + else + parentDir = parentDir.getParentFile(); + } + } + + if (derivParamsFile != null) { + if (verbose) + System.out.println("Using derivation parameters file: " + derivParamsFile.getAbsolutePath()); + try { + return FileEncryptionUtil.getEncryptionKey(password, derivParamsFile); + } + catch (IOException e) { + System.out.println("Can't create key from derivation parameters file: " + e.getLocalizedMessage()); + } + } + + // derivation parameters file not found or not readable, use default params and random salt + if (verbose) + System.out.println("No derivation parameters file available, creating new salt."); + I2PAppContext appContext = I2PAppContext.getGlobalContext(); + byte[] salt = new byte[SALT_LENGTH]; + appContext.random().nextBytes(salt); + byte[] key = FileEncryptionUtil.getEncryptionKey(password, salt, FileEncryptionConstants.KDF_PARAMETERS); + return new DerivedKey(salt, FileEncryptionConstants.KDF_PARAMETERS, key); + } + + /** + * @param args + */ + public static void main(String[] args) { + new Encrypt(args); + } +} \ No newline at end of file diff --git a/src/i2p/bote/fileencryption/EncryptedInputStream.java b/src/i2p/bote/fileencryption/EncryptedInputStream.java index 29ed62ff..d94e462d 100644 --- a/src/i2p/bote/fileencryption/EncryptedInputStream.java +++ b/src/i2p/bote/fileencryption/EncryptedInputStream.java @@ -49,23 +49,23 @@ public class EncryptedInputStream extends FilterInputStream { * @param upstream * @param passwordHolder * @throws IOException - * @throws PasswordException * @throws GeneralSecurityException + * @throws PasswordException */ - public EncryptedInputStream(InputStream upstream, PasswordHolder passwordHolder) throws IOException, PasswordException, GeneralSecurityException { + public EncryptedInputStream(InputStream upstream, PasswordHolder passwordHolder) throws IOException, GeneralSecurityException, PasswordException { super(upstream); byte[] password = passwordHolder.getPassword(); if (password == null) throw new PasswordException(); + DerivedKey cachedKey = passwordHolder.getKey(); - decryptedData = new ByteArrayInputStream(readInputStream(upstream, password, cachedKey)); + byte[] bytes = readInputStream(upstream, password, cachedKey); + decryptedData = new ByteArrayInputStream(bytes); } - public EncryptedInputStream(InputStream upstream, byte[] password) throws IOException, GeneralSecurityException { + public EncryptedInputStream(InputStream upstream, byte[] password) throws IOException, GeneralSecurityException, PasswordException { super(upstream); byte[] bytes = readInputStream(upstream, password, null); - if (bytes == null) - bytes = new byte[0]; decryptedData = new ByteArrayInputStream(bytes); } @@ -98,6 +98,9 @@ public class EncryptedInputStream extends FilterInputStream { I2PAppContext appContext = I2PAppContext.getGlobalContext(); byte[] decryptedData = appContext.aes().safeDecrypt(encryptedData, key, iv); + // null from safeDecrypt() means failure + if (decryptedData == null) + throw new PasswordException(); return decryptedData; } diff --git a/src/i2p/bote/fileencryption/FileEncryptionUtil.java b/src/i2p/bote/fileencryption/FileEncryptionUtil.java index eca8a25b..63919941 100644 --- a/src/i2p/bote/fileencryption/FileEncryptionUtil.java +++ b/src/i2p/bote/fileencryption/FileEncryptionUtil.java @@ -26,6 +26,7 @@ import static i2p.bote.fileencryption.FileEncryptionConstants.KEY_LENGTH; import static i2p.bote.fileencryption.FileEncryptionConstants.PASSWORD_FILE_PLAIN_TEXT; import i2p.bote.Util; +import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -57,6 +58,23 @@ public class FileEncryptionUtil { return key; } + static DerivedKey getEncryptionKey(byte[] password, File derivParamFile) throws GeneralSecurityException, IOException { + DataInputStream inputStream = null; + try { + inputStream = new DataInputStream(new FileInputStream(derivParamFile)); + byte[] salt = new byte[FileEncryptionConstants.SALT_LENGTH]; + inputStream.read(salt); + SCryptParameters scryptParams = new SCryptParameters(inputStream); + byte[] key = FileEncryptionUtil.getEncryptionKey(password, salt, scryptParams); + DerivedKey derivedKey = new DerivedKey(salt, scryptParams, key); + return derivedKey; + } + finally { + if (inputStream != null) + inputStream.close(); + } + } + /** * Decrypts a file with a given password and returns true if the decrypted * text is {@link FileEncryptionConstants#PASSWORD_FILE_PLAIN_TEXT}; false @@ -77,6 +95,9 @@ public class FileEncryptionUtil { byte[] decryptedText = Util.readBytes(inputStream); return Arrays.equals(PASSWORD_FILE_PLAIN_TEXT, decryptedText); } + catch (PasswordException e) { + return false; + } finally { if (inputStream != null) inputStream.close(); @@ -122,8 +143,9 @@ public class FileEncryptionUtil { * @param newKey * @throws IOException * @throws GeneralSecurityException + * @throws PasswordException */ - public static void changePassword(File file, byte[] oldPassword, DerivedKey newKey) throws IOException, GeneralSecurityException { + public static void changePassword(File file, byte[] oldPassword, DerivedKey newKey) throws IOException, GeneralSecurityException, PasswordException { InputStream inputStream = null; byte[] decryptedData = null; try { diff --git a/src/i2p/bote/fileencryption/PasswordCache.java b/src/i2p/bote/fileencryption/PasswordCache.java index 41857e4c..5c6c18ba 100644 --- a/src/i2p/bote/fileencryption/PasswordCache.java +++ b/src/i2p/bote/fileencryption/PasswordCache.java @@ -27,10 +27,8 @@ import i2p.bote.Configuration; import i2p.bote.Util; import i2p.bote.service.I2PBoteThread; -import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; @@ -91,21 +89,8 @@ public class PasswordCache extends I2PBoteThread implements PasswordHolder { // read salt + scrypt parameters from file if available File derivParamFile = configuration.getKeyDerivationParametersFile(); - if (derivParamFile.exists()) { - DataInputStream inputStream = null; - try { - inputStream = new DataInputStream(new FileInputStream(derivParamFile)); - salt = new byte[FileEncryptionConstants.SALT_LENGTH]; - inputStream.read(salt); - SCryptParameters scryptParams = new SCryptParameters(inputStream); - byte[] key = FileEncryptionUtil.getEncryptionKey(password, salt, scryptParams); - derivedKey = new DerivedKey(salt, scryptParams, key); - } - finally { - if (inputStream != null) - inputStream.close(); - } - } + if (derivParamFile.exists()) + derivedKey = FileEncryptionUtil.getEncryptionKey(password, derivParamFile); // if necessary, create a new salt and key and write the derivation parameters to the cache file if (derivedKey==null || !derivedKey.scryptParams.equals(KDF_PARAMETERS)) {