From d95188e986bbde4cb65b9ba8e2d2c0dc13127666 Mon Sep 17 00:00:00 2001 From: Giuseppe Valente Date: Sun, 3 Sep 2023 17:34:38 +0200 Subject: [PATCH 1/4] #41 Added support for hash check in the KeyFile (v. 2) --- .../linguafranca/pwdb/kdbx/KdbxKeyFile.java | 19 +++++++++++++++- .../pwdb/kdbx/KdbxKeyFileTest.java | 21 ++++++++++++++++++ test/src/main/resources/kdbx_hash_test.kdbx | Bin 0 -> 1870 bytes test/src/main/resources/kdbx_hash_test.keyx | 12 ++++++++++ .../resources/kdbx_hash_test_wrong_hash.keyx | 12 ++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100755 test/src/main/resources/kdbx_hash_test.kdbx create mode 100755 test/src/main/resources/kdbx_hash_test.keyx create mode 100755 test/src/main/resources/kdbx_hash_test_wrong_hash.keyx diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index 9d2b52e5..c855d302 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -18,6 +18,7 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; +import org.linguafranca.pwdb.security.Encryption; import org.w3c.dom.Document; import javax.xml.parsers.DocumentBuilder; @@ -26,6 +27,7 @@ import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; import java.io.InputStream; +import java.security.MessageDigest; /** * Class has a static method to load a key from a KDBX XML Key File @@ -52,7 +54,22 @@ public static byte[] load(InputStream inputStream) { return null; } if (version.equals("2.0")) { - return Hex.decodeHex(data.replaceAll("\\s","")); + + byte[] hexData = Hex.decodeHex(data.replaceAll("\\s","")); + + MessageDigest md = Encryption.getSha256MessageDigestInstance(); + byte[] computedHash = md.digest(hexData); + + String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, XPathConstants.STRING); + byte[] verifiedHash = Hex.decodeHex(hashToCheck); + + for(int i = 0; i < verifiedHash.length; i++) { + if(computedHash[i] != verifiedHash[i]) { + return null; + } + } + return hexData; + } return Base64.decodeBase64(data.getBytes()); } catch (Exception e) { diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java index aa6b4716..1bed6fa1 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java @@ -98,4 +98,25 @@ 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(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); + } } \ No newline at end of file diff --git a/test/src/main/resources/kdbx_hash_test.kdbx b/test/src/main/resources/kdbx_hash_test.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..42ee092524719035c1ab2427312b07bd417d8b31 GIT binary patch literal 1870 zcmV-U2eJ4A*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZa{K9bk zh%qk@b&|*Qb#enZbzY{H(6R!31mp__Kl8Cba3hC zi(Ej4Tr2nOlxv9w2moN}00000000LN0HJ5@*1<+*MjPUg%UjX`rU)PaB1{Z6xW> zH>i+-FGuKF(EaQTwm@reMFbK(0R_ozsuRo|vH!`~84cpldwaa=zc6(w(y#i3CFPS- zE8R|hQtIP!m1AyNIV%F&xYKWA`#|C?)#o{ZMoOO);--nM!FK9}T(iM7jA0RRH6Z-? zn!{{K4Nl;ezbyCFQ()O+OcP_Dm$1fvrqaT8zjE_~^W^;KfN z@($WVSEn!~2Z@k%yu0a4lD`7IME$j-xV@##~wWOf6u7Tg8z}!o68AqLTc=kud@t-5Y z0D+lpeDRivtzE?@cVe$7A(-B2m}AnL3OJ-`n<>*|`VYY_Pgftdegrr0{sBP(nz~gg z`zn4)%Eb@Rg0Sg8!2JJpO8Jj(lTz=Rulw<4mP4uoBRV);w_$@qmM#clUZ-jw6001A zVS#Tazp-Y(+xxFsb+?55_x5}|cmTKN#eDR-sT_*p;yUPk2AYfVr zxj<(VlG)1yxi=vjpjbW4{wFM0|6UK${Z_{6UDE>c{!n%**dS6=I||nc^%!9J!(5o_ z?5xSOIFXu*y$kD*4MHq!Xk-|EI4{8Gkc_$?vr`?*YOFQ@8qvui3Y~-Ir+e?F9VirE zV#RBm<78Q^eF@UHc{hpdD$^ZIU*7Oi>`)KcD+0{({4F2AUZ=c{sWvxw4Xzp+#_iv`Tgt{WyS$PzGd~!g=HsHZxf->O%^{y`{9a`im6DxlN}H z+8^uDzP=od3ENN|i07G{UA9gHV|f(ktTFmw*2@*>NP6M&wpr&i{k&-^_)Uh(iuc|S zaoa<%)xQiBiZ_u>4V&t!xRR9XqY)Dx9r z_c}o?GcTOIbO(fBB@WLp1RnLU(i@nE3poeB{Zp(Q-v5wldqq1`lEeEj*-ZvJ(A>?B zDT=Yw09riCztmG^5)* zpdT!tgE{da*V$9jK6RHdOzb)5!JOr<>4~ivB~LsbxSq|FjQ&m!&C#`-!rtoof5Zz* zY5ks5oZc=|dk6W;C?!ENbB91Q{mjrh%az7T)%yz=@2T`^i)ao@O4b?!=>xcm4%aJ0;O;!T8=FcNRm}+QyK~@guPsiF0FnPbhCAy{vFbHlAv%7e znBq^W2Uol5@A=j5)23qf^4A(WPEj&9ReE}ULKK2ejTrex#qXzCk!?njh1dsN zw4!GTo`u1!&~Ss5s$__4us4Nvk$=7 zD^KSbF9TB5MC^1vgUXan6psqnFt&DYnqo%b&^TQ$Xk9X5K7R + + + 2.0 + + + + 66F110CA E32995B5 EFA8E672 1E70C773 + 48F8E260 EEDD8744 93F41803 5BFDC27D + + + \ No newline at end of file diff --git a/test/src/main/resources/kdbx_hash_test_wrong_hash.keyx b/test/src/main/resources/kdbx_hash_test_wrong_hash.keyx new file mode 100755 index 00000000..5c7b37e2 --- /dev/null +++ b/test/src/main/resources/kdbx_hash_test_wrong_hash.keyx @@ -0,0 +1,12 @@ + + + + 2.0 + + + + 66F110CA E32995B5 EFA8E672 1E70C773 + 48F8E260 EEDD8744 93F41803 5BFDC27D + + + \ No newline at end of file From 7d84ced58d723e8c20764f4c30e14ce98043d81f Mon Sep 17 00:00:00 2001 From: Giuseppe Valente Date: Wed, 6 Sep 2023 00:05:14 +0200 Subject: [PATCH 2/4] #41 PR update the review code Signed-off-by: Giuseppe Valente --- .../main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java | 8 ++++---- .../java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index c855d302..f3ad7dbd 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -28,6 +28,7 @@ import javax.xml.xpath.XPathFactory; import java.io.InputStream; import java.security.MessageDigest; +import java.util.Arrays; /** * Class has a static method to load a key from a KDBX XML Key File @@ -63,10 +64,9 @@ public static byte[] load(InputStream inputStream) { String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, XPathConstants.STRING); byte[] verifiedHash = Hex.decodeHex(hashToCheck); - for(int i = 0; i < verifiedHash.length; i++) { - if(computedHash[i] != verifiedHash[i]) { - return null; - } + boolean isHashVerified = Arrays.equals(Arrays.copyOf(computedHash, verifiedHash.length), verifiedHash); + if(!isHashVerified) { + throw new IllegalStateException("Hash mismatch error"); } return hexData; diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java index 1bed6fa1..4b8ea5f2 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java @@ -111,6 +111,9 @@ public void testSignedKeyFile() throws Exception { 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"); From 1cc52f7a3dfd6a0d8da555dcc50cceb7f3686029 Mon Sep 17 00:00:00 2001 From: Giuseppe Valente Date: Wed, 6 Sep 2023 23:12:05 +0200 Subject: [PATCH 3/4] #41 closes: implemented all features --- .../org/linguafranca/pwdb/kdbx/Helpers.java | 42 +++++++++ .../linguafranca/pwdb/kdbx/KdbxKeyFile.java | 81 +++++++++++++----- .../pwdb/kdbx/KdbxKeyFileTest.java | 38 +++++++- .../main/resources/kdb_with_random_file.kdbx | Bin 0 -> 1870 bytes test/src/main/resources/kdbx_keyfile32.kdbx | Bin 0 -> 1870 bytes test/src/main/resources/kdbx_keyfile64.kdbx | Bin 0 -> 1870 bytes test/src/main/resources/keyfile32 | 1 + test/src/main/resources/keyfile64 | 1 + test/src/main/resources/random_file | Bin 0 -> 3072 bytes 9 files changed, 139 insertions(+), 24 deletions(-) create mode 100755 test/src/main/resources/kdb_with_random_file.kdbx create mode 100755 test/src/main/resources/kdbx_keyfile32.kdbx create mode 100755 test/src/main/resources/kdbx_keyfile64.kdbx create mode 100755 test/src/main/resources/keyfile32 create mode 100755 test/src/main/resources/keyfile64 create mode 100755 test/src/main/resources/random_file diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index 35f989f8..e66a063a 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.text.SimpleDateFormat; @@ -32,9 +33,14 @@ import java.util.Date; import java.util.TimeZone; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + /** * The class provides helpers to marshal and unmarshal values of KDBX files */ @@ -201,4 +207,40 @@ public static byte[] toBytes(int value, ByteOrder byteOrder) { .putInt(value); return longBuffer; } + + /** + * Check if the data is an XML file. + * + * @param data the file to check + * @return true if and only if the data is an XML parsable + */ + public static boolean checkIfKeyFileIsXml(byte[] data) { + try { + InputStream is = new ByteArrayInputStream(data); + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + documentBuilder.parse(is); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Check if the data is a valid hex + * + * @param data the data to check + * @return true if and only if the key contained in the KeyFile is an Hex format + * @throws IllegalAccessException if the length of the data is not equal to 64 + */ + public static boolean isValidHexString(byte[] data) { + + if(data == null) { + return false; + } + + final Pattern hexPattern = Pattern.compile("\\p{XDigit}+"); + final Matcher matcher = hexPattern.matcher(new String(data)); + return matcher.matches(); + } + } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index f3ad7dbd..ea5c7269 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -21,15 +21,21 @@ import org.linguafranca.pwdb.security.Encryption; import org.w3c.dom.Document; +import com.google.common.io.ByteStreams; + import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; + +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.security.MessageDigest; import java.util.Arrays; + + /** * Class has a static method to load a key from a KDBX XML Key File * @@ -46,32 +52,61 @@ public class KdbxKeyFile { * @return the key */ public static byte[] load(InputStream inputStream) { + try { - DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document doc = documentBuilder.parse(inputStream); - String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, XPathConstants.STRING); - String data = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); - if (data == null) { - return null; - } - if (version.equals("2.0")) { - - byte[] hexData = Hex.decodeHex(data.replaceAll("\\s","")); - - MessageDigest md = Encryption.getSha256MessageDigestInstance(); - byte[] computedHash = md.digest(hexData); - - String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, XPathConstants.STRING); - byte[] verifiedHash = Hex.decodeHex(hashToCheck); - - boolean isHashVerified = Arrays.equals(Arrays.copyOf(computedHash, verifiedHash.length), verifiedHash); - if(!isHashVerified) { - throw new IllegalStateException("Hash mismatch error"); + byte[] inputBytes = ByteStreams.toByteArray(inputStream); + if (inputBytes.length == 32) { + //32 bytes KeyFile + return inputBytes; + } else if (inputBytes.length == 64) { + //64 bytes KeyFile (only Hexadecimal values) + if (Helpers.isValidHexString(inputBytes)) { + byte[] keyFile = org.bouncycastle.util.encoders.Hex.decode(inputBytes); + return keyFile; + } else { + throw new IllegalStateException("KeyFile contains not allowed characters"); + } + } else { + + //Standard KeyFile + if (Helpers.checkIfKeyFileIsXml(inputBytes)) { + + InputStream is = new ByteArrayInputStream(inputBytes); + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document doc = documentBuilder.parse(is); + String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, + XPathConstants.STRING); + String data = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); + if (data == null) { + return null; + } + if (version.equals("2.0")) { + + byte[] hexData = Hex.decodeHex(data.replaceAll("\\s", "")); + + MessageDigest md = Encryption.getSha256MessageDigestInstance(); + byte[] computedHash = md.digest(hexData); + + String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, + XPathConstants.STRING); + byte[] verifiedHash = Hex.decodeHex(hashToCheck); + + boolean isHashVerified = Arrays.equals(Arrays.copyOf(computedHash, verifiedHash.length), + verifiedHash); + if (!isHashVerified) { + throw new IllegalStateException("Hash mismatch error"); + } + return hexData; + } + return Base64.decodeBase64(data.getBytes()); + } else { + //Any file compute the hash + MessageDigest md = Encryption.getSha256MessageDigestInstance(); + byte[] keyFile = md.digest(inputBytes); + return keyFile; } - return hexData; - } - return Base64.decodeBase64(data.getBytes()); + } catch (Exception e) { throw new RuntimeException("Key File input stream cannot be null"); } diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java index 4b8ea5f2..31b8801f 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java @@ -122,4 +122,40 @@ public void testSignatureFails() throws Exception { InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); toConsole(decryptedInputStream); } -} \ No newline at end of file + + /** + * 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); + } +} diff --git a/test/src/main/resources/kdb_with_random_file.kdbx b/test/src/main/resources/kdb_with_random_file.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..d46ac89f688252821f95da9170816cda1ed8e668 GIT binary patch literal 1870 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 IoE4#%+lJSJbpQYW literal 0 HcmV?d00001 diff --git a/test/src/main/resources/kdbx_keyfile64.kdbx b/test/src/main/resources/kdbx_keyfile64.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..04adb6154f06813bd77237d177cb48d6123f2f71 GIT binary patch literal 1870 zcmV-U2eJ4A*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZal6>zn zSuwo=BD1&tX3>_1HtL_(Q}k||{HEZCKkDVe1t0)gu@FWW=3Zm;-DUm$HF4UXsj?)yi1 z*jiwDv_w5L zQO@V>I0SnN1ONg60000401XNa3ec3HZ-uJ@E+K@a-7Eqti84tN-k`6<}n=TfN#1iaGVrvMJWw`w|4Qa+soRb#&qg_&i;1WY# zdO~27xPCL=|3|inHaH!!8RSp#QMOy+V)ut7CNBBN{VO+ip=lxGTr3-un4J&=`F zO@cwI8@O4S{|#fa56JtiASAA!*JISL^O3Qm29Xz02Yu--veAytrkY0YNbg}6Y7Yc> z)toSCnjvMTyvyIulw=2mqckc|oKugfk{PX)F6IC&LBQ^EiBF5SXym`YFQC2TzwvI) z5u(TfrST(=^X^oQV+r4Sujh4R@9ChUJ`g8R{J3@6L>1o4za4zeW?r)p_wu%0VW(Gqb z^~P@j0|;1PG%mgtfv}96sqw0F5~GjoxMF@U{er$EP-X-$;SZ+YhJu382aQL&Q>Ndb zhsl>I87!^)=58V1DMQ;5kK(QNIRmRiBN|H&o3xiKyQ4M>dt9tYPNUs4pw6e%hFnCV z*tLYQcdrRxa$+kL_%)dZ-n%k1n?#iwgN!+T$F$#@Y<6hbgmm(tKjU1k<=GUIdx6$R}CSq&@iplxu=nHo}RLtrr02AM?JU~s` zlBz#l4T0N&k@BJ{Qs{axl~Gw(8BG2vz{icu`DuGFFB;i2697|${q8vc56O1#^`8|= zjS)etWOY~7`4dRZLO~*EUP}M%XS9mS>n61mooLkB@Q>d@471oMO&QWaQjcd&h`mrE z5`^rLU}g%oIoeSoPp@V_FlE~kh7RTR)<`IjQ_KKg?5%SBfWr(?lbbmGUV)Cv4Gm=k zISHWWD5~e~<)>$$3F&$qck1DjH4p}nj z{tXWe7>)r`liE}2ueAsA;NZkbFWyE`A1}$HP~sc`@SSYbR=4 zv(kw!ml*57TJBGC=~znF@;jpA_SJ4;l0jwA#-XI*rYhuqYp($JJ;?#>LB9n5;|2~n zGo1m$q;$mSn45v*gP;?X$pU9sCeWcKolN92vDM6`sKyrFO;NzulwewOixy-f1$9;_HPoqbc35w{mHdLYn~iX183j~=FyIET?!!9 I8g}V263F3(TmS$7 literal 0 HcmV?d00001 diff --git a/test/src/main/resources/keyfile32 b/test/src/main/resources/keyfile32 new file mode 100755 index 00000000..70b28d84 --- /dev/null +++ b/test/src/main/resources/keyfile32 @@ -0,0 +1 @@ +KQQWFvUqScAobSfwq4z5MwnbITIb38vm \ No newline at end of file diff --git a/test/src/main/resources/keyfile64 b/test/src/main/resources/keyfile64 new file mode 100755 index 00000000..d41b5e88 --- /dev/null +++ b/test/src/main/resources/keyfile64 @@ -0,0 +1 @@ +bc63a755436af06b88027816446ba0486e8e15fac0e26dbaee82f662c77546bc \ No newline at end of file diff --git a/test/src/main/resources/random_file b/test/src/main/resources/random_file new file mode 100755 index 0000000000000000000000000000000000000000..a572bb478e4ff8b64b6726b70a0c86d461c58570 GIT binary patch literal 3072 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 Date: Sat, 23 Sep 2023 18:14:06 +0200 Subject: [PATCH 4/4] #41 code review for PR --- .../org/linguafranca/pwdb/kdbx/Helpers.java | 44 +----- .../linguafranca/pwdb/kdbx/KdbxKeyFile.java | 148 ++++++++++++------ 2 files changed, 99 insertions(+), 93 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index e66a063a..d789bd15 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -23,23 +23,17 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; + 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.regex.Matcher; -import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; /** * The class provides helpers to marshal and unmarshal values of KDBX files @@ -207,40 +201,4 @@ public static byte[] toBytes(int value, ByteOrder byteOrder) { .putInt(value); return longBuffer; } - - /** - * Check if the data is an XML file. - * - * @param data the file to check - * @return true if and only if the data is an XML parsable - */ - public static boolean checkIfKeyFileIsXml(byte[] data) { - try { - InputStream is = new ByteArrayInputStream(data); - DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - documentBuilder.parse(is); - return true; - } catch (Exception e) { - return false; - } - } - - /** - * Check if the data is a valid hex - * - * @param data the data to check - * @return true if and only if the key contained in the KeyFile is an Hex format - * @throws IllegalAccessException if the length of the data is not equal to 64 - */ - public static boolean isValidHexString(byte[] data) { - - if(data == null) { - return false; - } - - final Pattern hexPattern = Pattern.compile("\\p{XDigit}+"); - final Matcher matcher = hexPattern.matcher(new String(data)); - return matcher.matches(); - } - } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index ea5c7269..0c3c1157 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -16,26 +16,33 @@ package org.linguafranca.pwdb.kdbx; +import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; import org.linguafranca.pwdb.security.Encryption; import org.w3c.dom.Document; +import org.xml.sax.SAXException; + -import com.google.common.io.ByteStreams; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.PushbackInputStream; import java.security.MessageDigest; import java.util.Arrays; - - /** * Class has a static method to load a key from a KDBX XML Key File * @@ -45,70 +52,111 @@ public class KdbxKeyFile { private static XPath xpath = XPathFactory.newInstance().newXPath(); + private static int BUFFER_SIZE = 65; + private static int KEY_LEN_32 = 32; + private static int KEY_LEN_64 = 64; /** * Load a key from an InputStream with a KDBX XML key file. + * * @param inputStream the input stream holding the key * @return the key */ public static byte[] load(InputStream inputStream) { - try { - byte[] inputBytes = ByteStreams.toByteArray(inputStream); - if (inputBytes.length == 32) { - //32 bytes KeyFile - return inputBytes; - } else if (inputBytes.length == 64) { - //64 bytes KeyFile (only Hexadecimal values) - if (Helpers.isValidHexString(inputBytes)) { - byte[] keyFile = org.bouncycastle.util.encoders.Hex.decode(inputBytes); - return keyFile; - } else { - throw new IllegalStateException("KeyFile contains not allowed characters"); - } - } else { - //Standard KeyFile - if (Helpers.checkIfKeyFileIsXml(inputBytes)) { - - InputStream is = new ByteArrayInputStream(inputBytes); - DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document doc = documentBuilder.parse(is); - String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, - XPathConstants.STRING); - String data = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); - if (data == null) { - return null; - } - if (version.equals("2.0")) { + PushbackInputStream pis = new PushbackInputStream(inputStream, BUFFER_SIZE); + byte[] buffer = new byte[65]; + int bytesRead = pis.read(buffer); - byte[] hexData = Hex.decodeHex(data.replaceAll("\\s", "")); - - MessageDigest md = Encryption.getSha256MessageDigestInstance(); - byte[] computedHash = md.digest(hexData); - - String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, - XPathConstants.STRING); - byte[] verifiedHash = Hex.decodeHex(hashToCheck); - - boolean isHashVerified = Arrays.equals(Arrays.copyOf(computedHash, verifiedHash.length), - verifiedHash); - if (!isHashVerified) { - throw new IllegalStateException("Hash mismatch error"); - } - return hexData; - } - return Base64.decodeBase64(data.getBytes()); + if (bytesRead == KEY_LEN_32) { + return Arrays.copyOf(buffer, bytesRead); + } else if (bytesRead == KEY_LEN_64) { + byte[] keyFile = Hex.decodeHex(new String(Arrays.copyOf(buffer, bytesRead))); + return keyFile; + } else { + if (isXML(buffer)) { + pis.unread(buffer); // Push back the buffer + return computeXmlKeyFile(pis); } else { - //Any file compute the hash + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(buffer, 0, bytesRead); // Insert the first 65 bytes in the OutputStream + buffer = new byte[1024]; // Increase the buffer + while ((bytesRead = pis.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + // Compute the SHA256 of the InputStream MessageDigest md = Encryption.getSha256MessageDigestInstance(); - byte[] keyFile = md.digest(inputBytes); + byte[] keyFile = md.digest(outputStream.toByteArray()); return keyFile; } } + } catch (IOException | DecoderException e) { + throw new IllegalArgumentException(e); + } + } + /** + * Check if the data is an XML + * @param data the data to ckeck + * @return true if the data is an XML + */ + private static boolean isXML(byte[] data) { + + try { + XMLInputFactory factory = XMLInputFactory.newFactory(); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + XMLStreamReader reader = factory.createXMLStreamReader(bais); + // Attempt to read the start element of the XML document. + if (reader.hasNext()) { + int eventType = reader.next(); + if (eventType == XMLStreamReader.START_ELEMENT) { + return true; // The InputStream contains valid XML + } + } + // If we reach this point, it's not valid XML. + return false; } catch (Exception e) { - throw new RuntimeException("Key File input stream cannot be null"); + // An exception occurred, so it's not valid XML. + return false; + } + } + + /** + * Read the InputStream (keyx file) and compute the hash (SHA2-256) to build a key + * + * @param is The KeyFile as an InputStream + * @return the computed byte array (keyFile) to compute the MasterKey + */ + private static byte[] computeXmlKeyFile(InputStream is) { + + try { + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document doc = documentBuilder.parse(new PushbackInputStream(is)); // This function close the input stream, we + // need to create another one + String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, XPathConstants.STRING); + String data = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); + if (data == null) { + return null; + } + if (version.equals("2.0")) { + byte[] hexData = Hex.decodeHex(data.replaceAll("\\s", "")); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); + byte[] computedHash = md.digest(hexData); + String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, XPathConstants.STRING); + byte[] verifiedHash = Hex.decodeHex(hashToCheck); + + boolean isHashVerified = Arrays.equals(Arrays.copyOf(computedHash, verifiedHash.length), + verifiedHash); + if (!isHashVerified) { + throw new IllegalStateException("Hash mismatch error"); + } + return hexData; + } + return Base64.decodeBase64(data.getBytes()); + + } catch(IOException | SAXException | ParserConfigurationException | XPathExpressionException | DecoderException e) { + throw new IllegalArgumentException("An error occours during XML parsing: " + e.getMessage()); } } }