diff --git a/src/main/java/de/slackspace/openkeepass/KeePassDatabase.java b/src/main/java/de/slackspace/openkeepass/KeePassDatabase.java index 39ccfc5..8f4fa85 100644 --- a/src/main/java/de/slackspace/openkeepass/KeePassDatabase.java +++ b/src/main/java/de/slackspace/openkeepass/KeePassDatabase.java @@ -321,6 +321,136 @@ public static void write(KeePassFile keePassFile, String password, String keePas } } + /** + * Encrypts a {@link KeePassFile} with the given password and writes it to + * the given file location. + *

+ * If the KeePassFile cannot be encrypted an exception will be thrown. + * + * @param keePassFile + * the keePass model which should be written + * @param password + * the password to encrypt the database + * @param stream + * the target stream where the output will be written + * @see KeePassFile + */ + public static void write(KeePassFile keePassFile, String password, OutputStream stream) { + if (stream == null) { + throw new IllegalArgumentException("You must provide a stream to write to."); + } + + write(keePassFile, password, (InputStream) null, stream); + } + + /** + * Encrypts a {@link KeePassFile} with the given password and writes it to + * the given file location. + *

+ * If the KeePassFile cannot be encrypted an exception will be thrown. + * + * @param keePassFile + * the keePass model which should be written + * @param password + * the password to encrypt the database + * @param keyFile + * the keyfile to encrypt the database as stream + * @param keePassDatabaseFile + * the target location where the database file will be written + * @see KeePassFile + */ + public static void write(KeePassFile keePassFile, String password, File keyFile, String keePassDatabaseFile) { + if (keePassDatabaseFile == null || keePassDatabaseFile.isEmpty()) { + throw new IllegalArgumentException("You must provide a non-empty path where the database should be written to."); + } + + try { + InputStream keyFileStream = null; + try { + keyFileStream = new FileInputStream(keyFile); + write(keePassFile, password, keyFileStream, new FileOutputStream(keePassDatabaseFile)); + } finally { + if (keyFileStream != null) { + try { + keyFileStream.close(); + } catch (IOException e) { + // Ignore + } + } + } + } catch (FileNotFoundException e) { + throw new KeePassDatabaseUnreadableException("Could not find database file", e); + } + } + + /** + * Encrypts a {@link KeePassFile} with the given password and writes it to + * the given file location. + *

+ * If the KeePassFile cannot be encrypted an exception will be thrown. + * + * @param keePassFile + * the keePass model which should be written + * @param password + * the password to encrypt the database + * @param keyFile + * the keyfile to encrypt the database as stream + * @param stream + * the target stream where the output will be written + * @see KeePassFile + */ + public static void write(KeePassFile keePassFile, String password, File keyFile, OutputStream stream) { + if (stream == null) { + throw new IllegalArgumentException("You must provide a stream to write to."); + } + + try { + InputStream keyFileStream = null; + try { + keyFileStream = new FileInputStream(keyFile); + write(keePassFile, password, keyFileStream, stream); + } finally { + if (keyFileStream != null) { + try { + keyFileStream.close(); + } catch (IOException e) { + // Ignore + } + } + } + } catch (FileNotFoundException e) { + throw new KeePassDatabaseUnreadableException("Could not find database file", e); + } + } + + /** + * Encrypts a {@link KeePassFile} with the given password and writes it to + * the given file location. + *

+ * If the KeePassFile cannot be encrypted an exception will be thrown. + * + * @param keePassFile + * the keePass model which should be written + * @param password + * the password to encrypt the database + * @param keyFileStream + * the keyfile to encrypt the database as stream + * @param keePassDatabaseFile + * the target location where the database file will be written + * @see KeePassFile + */ + public static void write(KeePassFile keePassFile, String password, InputStream keyFileStream, String keePassDatabaseFile) { + if (keePassDatabaseFile == null || keePassDatabaseFile.isEmpty()) { + throw new IllegalArgumentException("You must provide a non-empty path where the database should be written to."); + } + + try { + write(keePassFile, password, keyFileStream, new FileOutputStream(keePassDatabaseFile)); + } catch (FileNotFoundException e) { + throw new KeePassDatabaseUnreadableException("Could not find database file", e); + } + } + /** * Encrypts a {@link KeePassFile} with the given password and writes it to * the given stream. @@ -331,17 +461,144 @@ public static void write(KeePassFile keePassFile, String password, String keePas * the keePass model which should be written * @param password * the password to encrypt the database + * @param keyFileStream + * the keyfile to encrypt the database as stream * @param stream * the target stream where the output will be written * @see KeePassFile * */ - public static void write(KeePassFile keePassFile, String password, OutputStream stream) { + public static void write(KeePassFile keePassFile, String password, InputStream keyFileStream, OutputStream stream) { + if (stream == null) { + throw new IllegalArgumentException("You must provide a stream to write to."); + } + + new KeePassDatabaseWriter().writeKeePassFile(keePassFile, password, keyFileStream, stream); + } + + /** + * Encrypts a {@link KeePassFile} with the given password and writes it to + * the given file location. + *

+ * If the KeePassFile cannot be encrypted an exception will be thrown. + * + * @param keePassFile + * the keePass model which should be written + * @param keyFile + * the keyfile to encrypt the database as stream + * @param keePassDatabaseFile + * the target location where the database file will be written + * @see KeePassFile + */ + public static void write(KeePassFile keePassFile, File keyFile, String keePassDatabaseFile) { + if (keePassDatabaseFile == null || keePassDatabaseFile.isEmpty()) { + throw new IllegalArgumentException("You must provide a non-empty path where the database should be written to."); + } + + try { + InputStream keyFileStream = null; + try { + keyFileStream = new FileInputStream(keyFile); + write(keePassFile, null, keyFileStream, new FileOutputStream(keePassDatabaseFile)); + } finally { + if (keyFileStream != null) { + try { + keyFileStream.close(); + } catch (IOException e) { + // Ignore + } + } + } + } catch (FileNotFoundException e) { + throw new KeePassDatabaseUnreadableException("Could not find database file", e); + } + } + + /** + * Encrypts a {@link KeePassFile} with the given password and writes it to + * the given file location. + *

+ * If the KeePassFile cannot be encrypted an exception will be thrown. + * + * @param keePassFile + * the keePass model which should be written + * @param keyFile + * the keyfile to encrypt the database as stream + * @param stream + * the target stream where the output will be written + * @see KeePassFile + */ + public static void write(KeePassFile keePassFile, File keyFile, OutputStream stream) { + if (stream == null) { + throw new IllegalArgumentException("You must provide a stream to write to."); + } + + try { + InputStream keyFileStream = null; + try { + keyFileStream = new FileInputStream(keyFile); + write(keePassFile, keyFileStream, stream); + } finally { + if (keyFileStream != null) { + try { + keyFileStream.close(); + } catch (IOException e) { + // Ignore + } + } + } + } catch (FileNotFoundException e) { + throw new KeePassDatabaseUnreadableException("Could not find database file", e); + } + } + + /** + * Encrypts a {@link KeePassFile} with the given password and writes it to + * the given file location. + *

+ * If the KeePassFile cannot be encrypted an exception will be thrown. + * + * @param keePassFile + * the keePass model which should be written + * @param keyFileStream + * the keyfile to encrypt the database as stream + * @param keePassDatabaseFile + * the target location where the database file will be written + * @see KeePassFile + */ + public static void write(KeePassFile keePassFile, InputStream keyFileStream, String keePassDatabaseFile) { + if (keePassDatabaseFile == null || keePassDatabaseFile.isEmpty()) { + throw new IllegalArgumentException("You must provide a non-empty path where the database should be written to."); + } + + try { + write(keePassFile, keyFileStream, new FileOutputStream(keePassDatabaseFile)); + } catch (FileNotFoundException e) { + throw new KeePassDatabaseUnreadableException("Could not find database file", e); + } + } + + /** + * Encrypts a {@link KeePassFile} with the given password and writes it to + * the given stream. + *

+ * If the KeePassFile cannot be encrypted an exception will be thrown. + * + * @param keePassFile + * the keePass model which should be written + * @param keyFileStream + * the keyfile to encrypt the database as stream + * @param stream + * the target stream where the output will be written + * @see KeePassFile + * + */ + public static void write(KeePassFile keePassFile, InputStream keyFileStream, OutputStream stream) { if (stream == null) { throw new IllegalArgumentException("You must provide a stream to write to."); } - new KeePassDatabaseWriter().writeKeePassFile(keePassFile, password, stream); + write(keePassFile, null, keyFileStream, stream); } } diff --git a/src/main/java/de/slackspace/openkeepass/api/KeePassDatabaseWriter.java b/src/main/java/de/slackspace/openkeepass/api/KeePassDatabaseWriter.java index dec7d42..d426079 100644 --- a/src/main/java/de/slackspace/openkeepass/api/KeePassDatabaseWriter.java +++ b/src/main/java/de/slackspace/openkeepass/api/KeePassDatabaseWriter.java @@ -2,6 +2,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.zip.GZIPOutputStream; @@ -21,26 +22,35 @@ import de.slackspace.openkeepass.processor.EncryptionStrategy; import de.slackspace.openkeepass.processor.ProtectedValueProcessor; import de.slackspace.openkeepass.stream.HashedBlockOutputStream; +import de.slackspace.openkeepass.util.ByteUtils; public class KeePassDatabaseWriter { private static final String UTF_8 = "UTF-8"; public void writeKeePassFile(KeePassFile keePassFile, String password, OutputStream stream) { + writeKeePassFile(keePassFile, password, null, stream); + } + + public void writeKeePassFile(KeePassFile keePassFile, InputStream keyStream, OutputStream stream) { + writeKeePassFile(keePassFile, null, keyStream, stream); + } + + public void writeKeePassFile(KeePassFile keePassFile, String password, InputStream keyStream, OutputStream stream) { try { if (!validateKeePassFile(keePassFile)) { throw new KeePassDatabaseUnwriteableException( - "The provided keePassFile is not valid. A valid keePassFile must contain of meta and root group and the root group must at least contain one group."); + "The provided keePassFile is not valid. A valid keePassFile must contain of meta and root group and the root group must at least contain one group."); } KeePassHeader header = new KeePassHeader(new RandomGenerator()); - byte[] hashedPassword = hashPassword(password); + byte[] keyHash = getKeyHash(password, keyStream); byte[] keePassFilePayload = marshallXml(keePassFile, header); ByteArrayOutputStream streamToZip = compressStream(keePassFilePayload); ByteArrayOutputStream streamToHashBlock = hashBlockStream(streamToZip); ByteArrayOutputStream streamToEncrypt = combineHeaderAndContent(header, streamToHashBlock); - byte[] encryptedDatabase = encryptStream(header, hashedPassword, streamToEncrypt); + byte[] encryptedDatabase = encryptStream(header, keyHash, streamToEncrypt); // Write database to stream stream.write(encryptedDatabase); @@ -57,6 +67,18 @@ public void writeKeePassFile(KeePassFile keePassFile, String password, OutputStr } } + private byte[] getKeyHash(String password, InputStream keyFileStream) throws UnsupportedEncodingException { + byte[] hashedPassword = new byte[0]; + if (password != null) { + hashedPassword = hashPassword(password); + } + byte[] protectedBuffer = new byte[0]; + if (keyFileStream != null) { + protectedBuffer = new KeyFileReader().readKeyFile(keyFileStream); + } + return ByteUtils.concat(hashedPassword, protectedBuffer); + } + private byte[] hashPassword(String password) throws UnsupportedEncodingException { byte[] passwordBytes = password.getBytes(UTF_8); return Sha256.hash(passwordBytes); @@ -64,7 +86,7 @@ private byte[] hashPassword(String password) throws UnsupportedEncodingException private byte[] encryptStream(KeePassHeader header, byte[] hashedPassword, ByteArrayOutputStream streamToEncrypt) throws IOException { CryptoInformation cryptoInformation = new CryptoInformation(KeePassHeader.VERSION_SIGNATURE_LENGTH, header.getMasterSeed(), header.getTransformSeed(), - header.getEncryptionIV(), header.getTransformRounds(), header.getHeaderSize()); + header.getEncryptionIV(), header.getTransformRounds(), header.getHeaderSize()); return new Decrypter().encryptDatabase(hashedPassword, cryptoInformation, streamToEncrypt.toByteArray()); } diff --git a/src/test/java/de/slackspace/openkeepass/api/KeepassDatabaseWriterTest.java b/src/test/java/de/slackspace/openkeepass/api/KeepassDatabaseWriterTest.java index 68721a0..cabcf13 100644 --- a/src/test/java/de/slackspace/openkeepass/api/KeepassDatabaseWriterTest.java +++ b/src/test/java/de/slackspace/openkeepass/api/KeepassDatabaseWriterTest.java @@ -4,6 +4,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -110,6 +111,86 @@ public void shouldCreateNewDatabaseFile() throws IOException { Assert.assertEquals(creationDate, entryOne.getTimes().getCreationTime()); } + @Test + public void shouldCreateNewDatabaseFileWithKeyStreamAndPassword() throws IOException { + Calendar creationDate = CalendarHandler.createCalendar(2016, 5, 1); + Times times = new TimesBuilder().usageCount(5).creationTime(creationDate).build(); + Entry entryOne = new EntryBuilder("First entry").username("Carl").password("Carls secret").times(times).build(); + + KeePassFile keePassFile = new KeePassFileBuilder("testDB").addTopEntries(entryOne).build(); + + String dbFilename = tempFolder.newFile("writeNewDatabase.kdbx").getPath(); + KeePassDatabase.write(keePassFile, "abc", getClass().getResourceAsStream("/DatabaseWithKeyfile.key"), new FileOutputStream(dbFilename)); + + KeePassDatabase keePassDb = KeePassDatabase.getInstance(dbFilename); + KeePassFile database = keePassDb.openDatabase("abc", getClass().getResourceAsStream("/DatabaseWithKeyfile.key")); + Entry entryByTitle = database.getEntryByTitle("First entry"); + + Assert.assertEquals(entryOne.getTitle(), entryByTitle.getTitle()); + Assert.assertEquals(5, entryOne.getTimes().getUsageCount()); + Assert.assertEquals(creationDate, entryOne.getTimes().getCreationTime()); + } + + @Test + public void shouldCreateNewDatabaseFileWithKeyFileAndPassword() throws IOException { + Calendar creationDate = CalendarHandler.createCalendar(2016, 5, 1); + Times times = new TimesBuilder().usageCount(5).creationTime(creationDate).build(); + Entry entryOne = new EntryBuilder("First entry").username("Carl").password("Carls secret").times(times).build(); + + KeePassFile keePassFile = new KeePassFileBuilder("testDB").addTopEntries(entryOne).build(); + + String dbFilename = tempFolder.newFile("writeNewDatabase.kdbx").getPath(); + KeePassDatabase.write(keePassFile, "abc", new File("src/test/resources/DatabaseWithKeyfile.key"), dbFilename); + + KeePassDatabase keePassDb = KeePassDatabase.getInstance(dbFilename); + KeePassFile database = keePassDb.openDatabase("abc", new File("src/test/resources/DatabaseWithKeyfile.key")); + Entry entryByTitle = database.getEntryByTitle("First entry"); + + Assert.assertEquals(entryOne.getTitle(), entryByTitle.getTitle()); + Assert.assertEquals(5, entryOne.getTimes().getUsageCount()); + Assert.assertEquals(creationDate, entryOne.getTimes().getCreationTime()); + } + + @Test + public void shouldCreateNewDatabaseFileWithKeyStream() throws IOException { + Calendar creationDate = CalendarHandler.createCalendar(2016, 5, 1); + Times times = new TimesBuilder().usageCount(5).creationTime(creationDate).build(); + Entry entryOne = new EntryBuilder("First entry").username("Carl").password("Carls secret").times(times).build(); + + KeePassFile keePassFile = new KeePassFileBuilder("testDB").addTopEntries(entryOne).build(); + + String dbFilename = tempFolder.newFile("writeNewDatabase.kdbx").getPath(); + KeePassDatabase.write(keePassFile, getClass().getResourceAsStream("/DatabaseWithKeyfile.key"), new FileOutputStream(dbFilename)); + + KeePassDatabase keePassDb = KeePassDatabase.getInstance(dbFilename); + KeePassFile database = keePassDb.openDatabase(getClass().getResourceAsStream("/DatabaseWithKeyfile.key")); + Entry entryByTitle = database.getEntryByTitle("First entry"); + + Assert.assertEquals(entryOne.getTitle(), entryByTitle.getTitle()); + Assert.assertEquals(5, entryOne.getTimes().getUsageCount()); + Assert.assertEquals(creationDate, entryOne.getTimes().getCreationTime()); + } + + @Test + public void shouldCreateNewDatabaseFileWithKeyFile() throws IOException { + Calendar creationDate = CalendarHandler.createCalendar(2016, 5, 1); + Times times = new TimesBuilder().usageCount(5).creationTime(creationDate).build(); + Entry entryOne = new EntryBuilder("First entry").username("Carl").password("Carls secret").times(times).build(); + + KeePassFile keePassFile = new KeePassFileBuilder("testDB").addTopEntries(entryOne).build(); + + String dbFilename = tempFolder.newFile("writeNewDatabase.kdbx").getPath(); + KeePassDatabase.write(keePassFile, new File("src/test/resources/DatabaseWithKeyfile.key"), new FileOutputStream(dbFilename)); + + KeePassDatabase keePassDb = KeePassDatabase.getInstance(dbFilename); + KeePassFile database = keePassDb.openDatabase(new File("src/test/resources/DatabaseWithKeyfile.key")); + Entry entryByTitle = database.getEntryByTitle("First entry"); + + Assert.assertEquals(entryOne.getTitle(), entryByTitle.getTitle()); + Assert.assertEquals(5, entryOne.getTimes().getUsageCount()); + Assert.assertEquals(creationDate, entryOne.getTimes().getCreationTime()); + } + @Test public void shouldBuildKeePassFileWithTreeStructure() throws IOException { /*