Skip to content

Commit

Permalink
Merge pull request #57 from giusvale-dev/master
Browse files Browse the repository at this point in the history
Support KeyFile feature
  • Loading branch information
jorabin authored Oct 23, 2023
2 parents bb7c5e2 + e1cabe9 commit 015322f
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 10 deletions.
4 changes: 2 additions & 2 deletions kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,18 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Date;
import java.util.TimeZone;
import java.util.UUID;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;


/**
* The class provides helpers to marshal and unmarshal values of KDBX files
*/
Expand Down
30 changes: 23 additions & 7 deletions kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.DigestInputStream;
import java.util.Arrays;
import java.util.Objects;
Expand All @@ -51,9 +54,17 @@ public class KdbxKeyFile {
private static final int KEY_LEN_64 = 64;

/**
* Load a key from an InputStream
* Load a key from an InputStream, in this method the inputStrem represent the KeyFile
* <p>
* The InputStream can represent ... TODO write about the formats
* A key file is a file that contains a key (and possibly additional data, e.g. a hash that allows to verify the integrity of the key). The file extension typically is 'keyx' or 'key'.
* </p>
* Formats. KeePass supports the following key file formats:
* <ul>
* <li>XML (recommended, default). There is an XML format for key files. KeePass 2.x uses this format by default, i.e. when creating a key file in the master key dialog, an XML key file is created. The syntax and the semantics of the XML format allow to detect certain corruptions (especially such caused by faulty hardware or transfer problems), and a hash (in XML key files version 2.0 or higher) allows to verify the integrity of the key. This format is resistant to most encoding and new-line character changes (which is useful for instance when the user is opening and saving the key file or when transferring it from/to a server). Such a key file can be printed (as a backup on paper), and comments can be added in the file (with the usual XML syntax: <!-- ... -->). It is the most flexible format; new features can be added easily in the future.</li>
* <li>32 bytes. If the key file contains exactly 32 bytes, these are used as a 256-bit cryptographic key. This format requires the least disk space.</li>
* <li>Hexadecimal. If the key file contains exactly 64 hexadecimal characters (0-9 and A-F, in UTF-8/ASCII encoding, one line, no spaces), these are decoded to a 256-bit cryptographic key.</li>
* <li>Hashed. If a key file does not match any of the formats above, its content is hashed using a cryptographic hash function in order to build a key (typically a 256-bit key with SHA-256). This allows to use arbitrary files as key files.</li>
* </ul>
*
* @param inputStream the input stream holding the key, caller should close
* @return the key
Expand All @@ -70,13 +81,19 @@ public static byte[] load(InputStream inputStream) {

// if length 32 assume binary key file
if (bytesRead == KEY_LEN_32) {
return buffer;
return Arrays.copyOf(buffer, bytesRead);
}

// if length 64 may be hex encoded key file
if (bytesRead == KEY_LEN_64) {
try {
return Hex.decodeHex(ByteBuffer.wrap(buffer).asCharBuffer().array()); // (avoid creating a String)
try {
ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
Charset charSet = StandardCharsets.UTF_8;
CharBuffer charBuffer = charSet.decode(byteBuffer);
charBuffer.limit(KEY_LEN_64);
char[] hexValue = new char[charBuffer.remaining()];
charBuffer.get(hexValue);
return Hex.decodeHex(hexValue); // (avoid creating a String)
} catch (DecoderException ignored) {
// fall through it may be an XML file or just a file whose digest we want
}
Expand Down Expand Up @@ -150,7 +167,6 @@ public void close() { /* nothing */ }
throw new IllegalArgumentException("An error occurred during XML parsing: " + e.getMessage());
}
}

private static class HashMismatchException extends Exception {
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,64 @@ public void testEmptyPassword() throws Exception {
InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream);
toConsole(decryptedInputStream);
}
}

/**
* Test the hash in KeyFile (v2.0)
*/
@Test
public void testSignedKeyFile() throws Exception {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test.kdbx");
InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test.keyx");
Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile);
InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream);
toConsole(decryptedInputStream);
}

/**
* Test hash fails in KeyFile (v2.0)
*/
@Test(expected = RuntimeException.class)
public void testSignatureFails() throws Exception {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test.kdbx");
InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test_wrong_hash.keyx");
Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile);
InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream);
toConsole(decryptedInputStream);
}

/**
* Test KDBX with random KeyFile and key
*/
@Test
public void testKeyFileRandom() throws Exception {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdb_with_random_file.kdbx");
InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("random_file");
Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile);
InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream);
toConsole(decryptedInputStream);
}

/**
* Test KDBX with 64 bytes hex KeyFile and key
*/
@Test
public void testKeyFileHex64() throws Exception {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_keyfile64.kdbx");
InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("keyfile64");
Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile);
InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream);
toConsole(decryptedInputStream);
}

/**
* Test KDBX with 32 bytes KeyFile and key
*/
@Test
public void testKeyFile32() throws Exception {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_keyfile32.kdbx");
InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("keyfile32");
Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile);
InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream);
toConsole(decryptedInputStream);
}
}
Binary file added test/src/main/resources/kdb_with_random_file.kdbx
Binary file not shown.
Binary file added test/src/main/resources/kdbx_hash_test.kdbx
Binary file not shown.
12 changes: 12 additions & 0 deletions test/src/main/resources/kdbx_hash_test.keyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<KeyFile>
<Meta>
<Version>2.0</Version>
</Meta>
<Key>
<Data Hash="1CA151AE">
66F110CA E32995B5 EFA8E672 1E70C773
48F8E260 EEDD8744 93F41803 5BFDC27D
</Data>
</Key>
</KeyFile>
12 changes: 12 additions & 0 deletions test/src/main/resources/kdbx_hash_test_wrong_hash.keyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<KeyFile>
<Meta>
<Version>2.0</Version>
</Meta>
<Key>
<Data Hash="AABBCCDD">
66F110CA E32995B5 EFA8E672 1E70C773
48F8E260 EEDD8744 93F41803 5BFDC27D
</Data>
</Key>
</KeyFile>
Binary file added test/src/main/resources/kdbx_keyfile32.kdbx
Binary file not shown.
Binary file added test/src/main/resources/kdbx_keyfile64.kdbx
Binary file not shown.
1 change: 1 addition & 0 deletions test/src/main/resources/keyfile32
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
KQQWFvUqScAobSfwq4z5MwnbITIb38vm
1 change: 1 addition & 0 deletions test/src/main/resources/keyfile64
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bc63a755436af06b88027816446ba0486e8e15fac0e26dbaee82f662c77546bc
Binary file added test/src/main/resources/random_file
Binary file not shown.

0 comments on commit 015322f

Please sign in to comment.