Implement KDBX V4 read support

This commit is contained in:
Brian Pellin
2017-03-29 22:35:30 -05:00
parent 8bcf9b8978
commit ea2c0ebd90
17 changed files with 663 additions and 98 deletions

View File

@@ -21,6 +21,7 @@ package com.keepassdroid.tests;
import static org.junit.Assert.assertArrayEquals;
import java.io.ByteArrayOutputStream;
import java.util.Calendar;
import java.util.Random;
import java.util.UUID;
@@ -191,4 +192,20 @@ public class TypesTest extends TestCase {
assertArrayEquals("UUID match failed", bUUID, eUUID);
}
public void testULongMax() throws Exception {
byte[] ulongBytes = new byte[8];
for (int i = 0; i < ulongBytes.length; i++) {
ulongBytes[i] = -1;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
LEDataOutputStream leos = new LEDataOutputStream(bos);
leos.writeLong(Types.ULONG_MAX_VALUE);
leos.close();
byte[] uLongMax = bos.toByteArray();
assertArrayEquals(ulongBytes, uLongMax);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 Brian Pellin.
* Copyright 2013-2017 Brian Pellin.
*
* This file is part of KeePassDroid.
*
@@ -38,6 +38,8 @@ import javax.crypto.NoSuchPaddingException;
import junit.framework.TestCase;
import com.keepassdroid.crypto.CipherFactory;
import com.keepassdroid.crypto.engine.AesEngine;
import com.keepassdroid.crypto.engine.CipherEngine;
import com.keepassdroid.stream.BetterCipherInputStream;
import com.keepassdroid.stream.LEDataInputStream;
@@ -54,9 +56,10 @@ public class CipherTest extends TestCase {
rand.nextBytes(iv);
rand.nextBytes(plaintext);
Cipher encrypt = CipherFactory.getInstance(CipherFactory.AES_CIPHER, Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = CipherFactory.getInstance(CipherFactory.AES_CIPHER, Cipher.DECRYPT_MODE, key, iv);
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
byte[] secrettext = encrypt.doFinal(plaintext);
byte[] decrypttext = decrypt.doFinal(secrettext);
@@ -74,10 +77,11 @@ public class CipherTest extends TestCase {
rand.nextBytes(key);
rand.nextBytes(iv);
rand.nextBytes(plaintext);
Cipher encrypt = CipherFactory.getInstance(CipherFactory.AES_CIPHER, Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = CipherFactory.getInstance(CipherFactory.AES_CIPHER, Cipher.DECRYPT_MODE, key, iv);
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
CipherOutputStream cos = new CipherOutputStream(bos, encrypt);
cos.write(plaintext);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2010-2016 Brian Pellin.
* Copyright 2010-2017 Brian Pellin.
*
* This file is part of KeePassDroid.
*
@@ -26,6 +26,7 @@ import android.content.res.AssetManager;
import android.test.AndroidTestCase;
import com.keepassdroid.crypto.CipherFactory;
import com.keepassdroid.crypto.engine.AesEngine;
import com.keepassdroid.database.PwDatabaseV4;
import com.keepassdroid.database.load.ImporterV4;
@@ -42,7 +43,7 @@ public class Kdb4Header extends AndroidTestCase {
assertEquals(6000, db.numKeyEncRounds);
assertTrue(db.dataCipher.equals(CipherFactory.AES_CIPHER));
assertTrue(db.dataCipher.equals(AesEngine.CIPHER_UUID));
is.close();

View File

@@ -32,6 +32,9 @@ import javax.crypto.spec.SecretKeySpec;
import android.os.Build;
import com.keepassdroid.crypto.engine.AesEngine;
import com.keepassdroid.crypto.engine.CipherEngine;
import com.keepassdroid.crypto.engine.TwofishEngine;
import com.keepassdroid.utils.Types;
import org.spongycastle.jce.provider.BouncyCastleProvider;
@@ -70,17 +73,8 @@ public class CipherFactory {
private static boolean hasNativeImplementation(String transformation) {
return transformation.equals("AES/CBC/PKCS5Padding");
}
public static final UUID AES_CIPHER = Types.bytestoUUID(
new byte[]{(byte)0x31, (byte)0xC1, (byte)0xF2, (byte)0xE6, (byte)0xBF, (byte)0x71, (byte)0x43, (byte)0x50,
(byte)0xBE, (byte)0x58, (byte)0x05, (byte)0x21, (byte)0x6A, (byte)0xFC, 0x5A, (byte)0xFF
});
public static final UUID TWOFISH_CIPHER = Types.bytestoUUID(
new byte[]{(byte)0xAD, (byte)0x68, (byte)0xF2, (byte)0x9F, (byte)0x57, (byte)0x6F, (byte)0x4B, (byte)0xB9,
(byte)0xA3, (byte)0x6A, (byte)0xD4, (byte)0x7A, (byte)0xF9, (byte)0x65, (byte)0x34, (byte)0x6C
});
/** Generate appropriate cipher based on KeePass 2.x UUID's
* @param uuid
* @return
@@ -89,28 +83,11 @@ public class CipherFactory {
* @throws InvalidAlgorithmParameterException
* @throws InvalidKeyException
*/
public static Cipher getInstance(UUID uuid, int opmode, byte[] key, byte[] IV) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
return getInstance(uuid, opmode, key, IV, false);
}
public static Cipher getInstance(UUID uuid, int opmode, byte[] key, byte[] IV, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
if ( uuid.equals(AES_CIPHER) ) {
Cipher cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding", androidOverride);
cipher.init(opmode, new SecretKeySpec(key, "AES"), new IvParameterSpec(IV));
return cipher;
} else if ( uuid.equals(TWOFISH_CIPHER) ) {
Cipher cipher;
if (opmode == Cipher.ENCRYPT_MODE) {
cipher = CipherFactory.getInstance("Twofish/CBC/ZeroBytePadding", androidOverride);
} else {
cipher = CipherFactory.getInstance("Twofish/CBC/NoPadding", androidOverride);
}
cipher.init(opmode, new SecretKeySpec(key, "AES"), new IvParameterSpec(IV));
return cipher;
public static CipherEngine getInstance(UUID uuid) throws NoSuchAlgorithmException {
if ( uuid.equals(AesEngine.CIPHER_UUID) ) {
return new AesEngine();
} else if ( uuid.equals(TwofishEngine.CIPHER_UUID) ) {
return new TwofishEngine();
}
throw new NoSuchAlgorithmException("UUID unrecognized.");

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2017 Brian Pellin.
*
* This file is part of KeePassDroid.
*
* KeePassDroid 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 2 of the License, or
* (at your option) any later version.
*
* KeePassDroid 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 KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.keepassdroid.crypto;
import com.keepassdroid.stream.LEDataOutputStream;
import com.keepassdroid.stream.NullOutputStream;
import java.io.IOException;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.Mac;
public class CryptoUtil {
public static byte[] resizeKey(byte[] in, int inOffset, int cbIn, int cbOut) {
if (cbOut == 0) return new byte[0];
byte[] hash;
if (cbOut <= 32) { hash = hashSha256(in, inOffset, cbIn); }
else { hash = hashSha512(in, inOffset, cbIn); }
if (cbOut == hash.length) { return hash; }
byte[] ret = new byte[cbOut];
if (cbOut < hash.length) {
System.arraycopy(hash, 0, ret, 0, cbOut);
}
else {
int pos = 0;
long r = 0;
while (pos < cbOut) {
Mac hmac;
try {
hmac = Mac.getInstance("HmacSHA256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] pbR = LEDataOutputStream.writeLongBuf(r);
byte[] part = hmac.doFinal(pbR);
int copy = Math.min(cbOut - pos, part.length);
assert(copy > 0);
System.arraycopy(part, 0, ret, pos, copy);
pos += copy;
r++;
Arrays.fill(part, (byte)0);
}
assert(pos == cbOut);
}
Arrays.fill(hash, (byte)0);
return ret;
}
public static byte[] hashSha256(byte[] data) {
return hashSha256(data, 0, data.length);
}
public static byte[] hashSha256(byte[] data, int offset, int count) {
return hashGen("SHA-256", data, offset, count);
}
public static byte[] hashSha512(byte[] data, int offset, int count) {
return hashGen("SHA-512", data, offset, count);
}
public static byte[] hashGen(String transform, byte[] data, int offset, int count) {
MessageDigest hash;
try {
hash = MessageDigest.getInstance(transform);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
NullOutputStream nos = new NullOutputStream();
DigestOutputStream dos = new DigestOutputStream(nos, hash);
try {
dos.write(data, offset, count);
dos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
return hash.digest();
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2017 Brian Pellin.
*
* This file is part of KeePassDroid.
*
* KeePassDroid 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 2 of the License, or
* (at your option) any later version.
*
* KeePassDroid 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 KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.keepassdroid.crypto.engine;
import com.keepassdroid.crypto.CipherFactory;
import com.keepassdroid.utils.Types;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class AesEngine extends CipherEngine {
public static final UUID CIPHER_UUID = Types.bytestoUUID(
new byte[]{(byte) 0x31, (byte) 0xC1, (byte) 0xF2, (byte) 0xE6, (byte) 0xBF, (byte) 0x71, (byte) 0x43, (byte) 0x50,
(byte) 0xBE, (byte) 0x58, (byte) 0x05, (byte) 0x21, (byte) 0x6A, (byte) 0xFC, 0x5A, (byte) 0xFF
});
@Override
public Cipher getCipher(int opmode, byte[] key, byte[] IV, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
Cipher cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding", androidOverride);
cipher.init(opmode, new SecretKeySpec(key, "AES"), new IvParameterSpec(IV));
return cipher;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2017 Brian Pellin.
*
* This file is part of KeePassDroid.
*
* KeePassDroid 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 2 of the License, or
* (at your option) any later version.
*
* KeePassDroid 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 KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.keepassdroid.crypto.engine;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ChaCha20Engine extends CipherEngine {
@Override
public int ivLength() {
return 12;
}
@Override
public Cipher getCipher(int opmode, byte[] key, byte[] IV, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance("Chacha");
cipher.init(opmode, new SecretKeySpec(key, "ChaCha"), new IvParameterSpec(IV));
return cipher;
}
}

View File

@@ -1,8 +1,44 @@
/*
* Copyright 2017 Brian Pellin.
*
* This file is part of KeePassDroid.
*
* KeePassDroid 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 2 of the License, or
* (at your option) any later version.
*
* KeePassDroid 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 KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.keepassdroid.crypto.engine;
/**
* Created by bpellin on 1/29/17.
*/
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
public abstract class CipherEngine {
public int keyLength() {
return 32;
}
public int ivLength() {
return 16;
}
public abstract Cipher getCipher(int opmode, byte[] key, byte[] IV, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException;
public Cipher getCipher(int opmode, byte[] key, byte[] IV) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
return getCipher(opmode, key, IV, false);
}
public class CipherEngine {
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2017 Brian Pellin.
*
* This file is part of KeePassDroid.
*
* KeePassDroid 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 2 of the License, or
* (at your option) any later version.
*
* KeePassDroid 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 KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.keepassdroid.crypto.engine;
import com.keepassdroid.crypto.CipherFactory;
import com.keepassdroid.utils.Types;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class TwofishEngine extends CipherEngine {
public static final UUID CIPHER_UUID = Types.bytestoUUID(
new byte[]{(byte)0xAD, (byte)0x68, (byte)0xF2, (byte)0x9F, (byte)0x57, (byte)0x6F, (byte)0x4B, (byte)0xB9,
(byte)0xA3, (byte)0x6A, (byte)0xD4, (byte)0x7A, (byte)0xF9, (byte)0x65, (byte)0x34, (byte)0x6C
});
@Override
public Cipher getCipher(int opmode, byte[] key, byte[] IV, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
Cipher cipher;
if (opmode == Cipher.ENCRYPT_MODE) {
cipher = CipherFactory.getInstance("Twofish/CBC/ZeroBytePadding", androidOverride);
} else {
cipher = CipherFactory.getInstance("Twofish/CBC/NoPadding", androidOverride);
}
cipher.init(opmode, new SecretKeySpec(key, "AES"), new IvParameterSpec(IV));
return cipher;
}
}

View File

@@ -92,11 +92,12 @@ public abstract class PwDatabase {
finalKey = md.digest();
}
/**
* Encrypt the master key a few times to make brute-force key-search harder
* @throws IOException
*/
private static byte[] transformMasterKey( byte[] pKeySeed, byte[] pKey, int rounds ) throws IOException
protected static byte[] transformMasterKey( byte[] pKeySeed, byte[] pKey, int rounds ) throws IOException
{
FinalKey key = FinalKeyFactory.createFinalKey();

View File

@@ -25,6 +25,7 @@ import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@@ -34,6 +35,7 @@ import java.util.UUID;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.spongycastle.crypto.engines.AESEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -44,6 +46,10 @@ import android.webkit.URLUtil;
import biz.source_code.base64Coder.Base64Coder;
import com.keepassdroid.crypto.CipherFactory;
import com.keepassdroid.crypto.CryptoUtil;
import com.keepassdroid.crypto.PwStreamCipherFactory;
import com.keepassdroid.crypto.engine.AesEngine;
import com.keepassdroid.crypto.engine.CipherEngine;
import com.keepassdroid.database.exception.InvalidKeyFileException;
import com.keepassdroid.utils.EmptyUtils;
@@ -55,8 +61,10 @@ public class PwDatabaseV4 extends PwDatabase {
private static final int DEFAULT_HISTORY_MAX_ITEMS = 10; // -1 unlimited
private static final long DEFAULT_HISTORY_MAX_SIZE = 6 * 1024 * 1024; // -1 unlimited
private static final String RECYCLEBIN_NAME = "RecycleBin";
public UUID dataCipher = CipherFactory.AES_CIPHER;
public byte[] hmacKey;
public UUID dataCipher = AesEngine.CIPHER_UUID;
public CipherEngine dataEngine = new AesEngine();
public PwCompressionAlgorithm compressionAlgorithm = PwCompressionAlgorithm.Gzip;
public long numKeyEncRounds = 6000;
public Date nameChanged = DEFAULT_NOW;
@@ -134,6 +142,29 @@ public class PwDatabaseV4 extends PwDatabase {
return md.digest(fKey);
}
@Override
public void makeFinalKey(byte[] masterSeed, byte[] masterSeed2, int numRounds) throws IOException {
byte[] transformedMasterKey = transformMasterKey(masterSeed2, masterKey, numRounds);
byte[] cmpKey = new byte[65];
System.arraycopy(masterSeed, 0, cmpKey, 0, 32);
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32);
finalKey = CryptoUtil.resizeKey(cmpKey, 0, 64, dataEngine.keyLength());
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-512");
cmpKey[64] = 1;
hmacKey = md.digest(cmpKey);
} catch (NoSuchAlgorithmException e) {
throw new IOException("No SHA-512 implementation");
} finally {
Arrays.fill(cmpKey, (byte)0);
}
}
@Override
protected String getPasswordEncoding() {
return "UTF-8";

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2010-2012 Brian Pellin.
* Copyright 2010-2017 Brian Pellin.
*
* This file is part of KeePassDroid.
*
@@ -19,23 +19,32 @@
*/
package com.keepassdroid.database;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import com.keepassdroid.database.exception.InvalidAlgorithmException;
import com.keepassdroid.database.exception.InvalidDBException;
import com.keepassdroid.database.exception.InvalidDBVersionException;
import com.keepassdroid.stream.CopyInputStream;
import com.keepassdroid.stream.HmacBlockStream;
import com.keepassdroid.stream.LEDataInputStream;
import com.keepassdroid.utils.Types;
import javax.crypto.Mac;
public class PwDbHeaderV4 extends PwDbHeader {
public static final int DBSIG_PRE2 = 0xB54BFB66;
public static final int DBSIG_2 = 0xB54BFB67;
private static final int FILE_VERSION_CRITICAL_MASK = 0xFFFF0000;
public static final int FILE_VERSION_32 = 0x00030001;
public static final int FILE_VERSION_32_3 = 0x00030001;
public static final int FILE_VERSION_32_4 = 0x00040001;
public static final int FILE_VERSION_32 = FILE_VERSION_32_4;
public class PwDbHeaderV4Fields {
public static final byte EndOfHeader = 0;
public static final byte Comment = 1;
@@ -50,11 +59,22 @@ public class PwDbHeaderV4 extends PwDbHeader {
public static final byte InnerRandomStreamID = 10;
}
public class HeaderAndHash {
public byte[] header;
public byte[] hash;
public HeaderAndHash (byte[] header, byte[] hash) {
this.header = header;
this.hash = hash;
}
}
private PwDatabaseV4 db;
public byte[] protectedStreamKey = new byte[32];
public byte[] streamStartBytes = new byte[32];
public CrsAlgorithm innerRandomStream;
public long version;
public PwDbHeaderV4(PwDatabaseV4 d) {
db = d;
@@ -67,7 +87,7 @@ public class PwDbHeaderV4 extends PwDbHeader {
* @throws IOException
* @throws InvalidDBVersionException
*/
public byte[] loadFromFile(InputStream is) throws IOException, InvalidDBVersionException {
public HeaderAndHash loadFromFile(InputStream is) throws IOException, InvalidDBVersionException {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
@@ -75,7 +95,9 @@ public class PwDbHeaderV4 extends PwDbHeader {
throw new IOException("No SHA-256 implementation");
}
DigestInputStream dis = new DigestInputStream(is, md);
ByteArrayOutputStream headerBOS = new ByteArrayOutputStream();
CopyInputStream cis = new CopyInputStream(is, headerBOS);
DigestInputStream dis = new DigestInputStream(cis, md);
LEDataInputStream lis = new LEDataInputStream(dis);
int sig1 = lis.readInt();
@@ -85,7 +107,7 @@ public class PwDbHeaderV4 extends PwDbHeader {
throw new InvalidDBVersionException();
}
long version = lis.readUInt();
version = lis.readUInt();
if ( ! validVersion(version) ) {
throw new InvalidDBVersionException();
}
@@ -94,8 +116,9 @@ public class PwDbHeaderV4 extends PwDbHeader {
while ( ! done ) {
done = readHeaderField(lis);
}
return md.digest();
byte[] hash = md.digest();
return new HeaderAndHash(headerBOS.toByteArray(), hash);
}
private boolean readHeaderField(LEDataInputStream dis) throws IOException {
@@ -221,12 +244,26 @@ public class PwDbHeaderV4 extends PwDbHeader {
*/
private boolean validVersion(long version) {
return ! ((version & FILE_VERSION_CRITICAL_MASK) > (FILE_VERSION_32 & FILE_VERSION_CRITICAL_MASK));
return ! ((version & FILE_VERSION_CRITICAL_MASK) > (FILE_VERSION_32_3 & FILE_VERSION_CRITICAL_MASK));
}
public static boolean matchesHeader(int sig1, int sig2) {
return (sig1 == PWM_DBSIG_1) && ( (sig2 == DBSIG_2) || (sig2 == DBSIG_2) );
}
public static byte[] computeHeaderHmac(byte[] header, byte[] key) throws IOException{
byte[] headerHmac;
byte[] blockKey = HmacBlockStream.GetHmacKey64(key, Types.ULONG_MAX_VALUE);
Mac hmac;
try {
hmac = Mac.getInstance("HmacSHA256");
} catch (NoSuchAlgorithmException e) {
throw new IOException("No HmacAlogirthm");
}
return hmac.doFinal(header);
}
}

View File

@@ -47,6 +47,7 @@ import biz.source_code.base64Coder.Base64Coder;
import com.keepassdroid.UpdateStatus;
import com.keepassdroid.crypto.CipherFactory;
import com.keepassdroid.crypto.PwStreamCipherFactory;
import com.keepassdroid.crypto.engine.CipherEngine;
import com.keepassdroid.database.BinaryPool;
import com.keepassdroid.database.ITimeLogger;
import com.keepassdroid.database.PwCompressionAlgorithm;
@@ -64,6 +65,7 @@ import com.keepassdroid.database.security.ProtectedBinary;
import com.keepassdroid.database.security.ProtectedString;
import com.keepassdroid.stream.BetterCipherInputStream;
import com.keepassdroid.stream.HashedBlockInputStream;
import com.keepassdroid.stream.HmacBlockInputStream;
import com.keepassdroid.stream.LEDataInputStream;
import com.keepassdroid.utils.EmptyUtils;
import com.keepassdroid.utils.MemUtil;
@@ -75,7 +77,8 @@ public class ImporterV4 extends Importer {
private PwDatabaseV4 db;
private BinaryPool binPool = new BinaryPool();
private byte[] hashOfHeader = null;
private byte[] hashOfHeader = null;
private byte[] pbHeader = null;
protected PwDatabaseV4 createDB() {
return new PwDatabaseV4();
@@ -97,49 +100,82 @@ public class ImporterV4 extends Importer {
db = createDB();
PwDbHeaderV4 header = new PwDbHeaderV4(db);
hashOfHeader = header.loadFromFile(inStream);
PwDbHeaderV4.HeaderAndHash hh = header.loadFromFile(inStream);
hashOfHeader = hh.hash;
pbHeader = hh.header;
db.setMasterKey(password, keyInputStream);
db.makeFinalKey(header.masterSeed, header.transformSeed, (int)db.numKeyEncRounds);
// Attach decryptor
Cipher cipher;
try {
cipher = CipherFactory.getInstance(db.dataCipher, Cipher.DECRYPT_MODE, db.finalKey, header.encryptionIV);
} catch (NoSuchAlgorithmException e) {
throw new IOException("Invalid algorithm.");
} catch (NoSuchPaddingException e) {
throw new IOException("Invalid algorithm.");
} catch (InvalidKeyException e) {
throw new IOException("Invalid algorithm.");
} catch (InvalidAlgorithmParameterException e) {
throw new IOException("Invalid algorithm.");
}
InputStream decrypted = new BetterCipherInputStream(inStream, cipher, 50 * 1024);
LEDataInputStream dataDecrypted = new LEDataInputStream(decrypted);
byte[] storedStartBytes = null;
try {
storedStartBytes = dataDecrypted.readBytes(32);
if ( storedStartBytes == null || storedStartBytes.length != 32 ) {
InputStream isPlain;
if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {
// Attach decryptor
CipherEngine engine;
Cipher cipher;
try {
engine = CipherFactory.getInstance(db.dataCipher);
db.dataEngine = engine;
cipher = engine.getCipher(Cipher.DECRYPT_MODE, db.finalKey, header.encryptionIV);
} catch (NoSuchAlgorithmException e) {
throw new IOException("Invalid algorithm.");
} catch (NoSuchPaddingException e) {
throw new IOException("Invalid algorithm.");
} catch (InvalidKeyException e) {
throw new IOException("Invalid algorithm.");
} catch (InvalidAlgorithmParameterException e) {
throw new IOException("Invalid algorithm.");
}
InputStream decrypted = new BetterCipherInputStream(inStream, cipher, 50 * 1024);
LEDataInputStream dataDecrypted = new LEDataInputStream(decrypted);
byte[] storedStartBytes = null;
try {
storedStartBytes = dataDecrypted.readBytes(32);
if (storedStartBytes == null || storedStartBytes.length != 32) {
throw new InvalidPasswordException();
}
} catch (IOException e) {
throw new InvalidPasswordException();
}
} catch (IOException e) {
throw new InvalidPasswordException();
if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) {
throw new InvalidPasswordException();
}
isPlain = new HashedBlockInputStream(dataDecrypted);
}
if ( ! Arrays.equals(storedStartBytes, header.streamStartBytes) ) {
throw new InvalidPasswordException();
else { // KDBX 4
LEDataInputStream isData = new LEDataInputStream(inStream);
byte[] storedHash = isData.readBytes(32);
if (!Arrays.equals(storedHash,hashOfHeader)) {
throw new InvalidDBException();
}
byte[] hmacKey = db.hmacKey;
byte[] headerHmac = PwDbHeaderV4.computeHeaderHmac(pbHeader, hmacKey);
byte[] storedHmac = isData.readBytes(32);
if (storedHmac == null || storedHmac.length != 32) {
throw new InvalidDBException();
}
// Mac doesn't match
if (! Arrays.equals(headerHmac, storedHmac)) {
throw new InvalidDBException();
}
HmacBlockInputStream hmIs = new HmacBlockInputStream(isData, true, hmacKey);
isPlain = null;
}
HashedBlockInputStream hashed = new HashedBlockInputStream(dataDecrypted);
InputStream decompressed;
InputStream isXml;
if ( db.compressionAlgorithm == PwCompressionAlgorithm.Gzip ) {
decompressed = new GZIPInputStream(hashed);
isXml = new GZIPInputStream(isPlain);
} else {
decompressed = hashed;
isXml = isPlain;
}
if ( header.protectedStreamKey == null ) {
@@ -153,7 +189,7 @@ public class ImporterV4 extends Importer {
throw new ArcFourException();
}
ReadXmlStreamed(decompressed);
ReadXmlStreamed(isXml);
return db;

View File

@@ -59,7 +59,7 @@ public class PwDbHeaderOutputV4 extends PwDbHeaderOutput {
public void output() throws IOException {
los.writeUInt(PwDbHeader.PWM_DBSIG_1);
los.writeUInt(PwDbHeaderV4.DBSIG_2);
los.writeUInt(PwDbHeaderV4.FILE_VERSION_32);
los.writeUInt(PwDbHeaderV4.FILE_VERSION_32_3);
writeHeaderField(PwDbHeaderV4Fields.CipherID, Types.UUIDtoBytes(db.dataCipher));
writeHeaderField(PwDbHeaderV4Fields.CompressionFlags, LEDataOutputStream.writeIntBuf(db.compressionAlgorithm.id));

View File

@@ -43,6 +43,7 @@ import biz.source_code.base64Coder.Base64Coder;
import com.keepassdroid.crypto.CipherFactory;
import com.keepassdroid.crypto.PwStreamCipherFactory;
import com.keepassdroid.crypto.engine.CipherEngine;
import com.keepassdroid.database.BinaryPool;
import com.keepassdroid.database.CrsAlgorithm;
import com.keepassdroid.database.EntryHandler;
@@ -245,9 +246,12 @@ public class PwDbV4Output extends PwDbOutput {
private CipherOutputStream attachStreamEncryptor(PwDbHeaderV4 header, OutputStream os) throws PwDbOutputException {
Cipher cipher;
CipherEngine engine;
try {
mPM.makeFinalKey(header.masterSeed, header.transformSeed, (int)mPM.numKeyEncRounds);
cipher = CipherFactory.getInstance(mPM.dataCipher, Cipher.ENCRYPT_MODE, mPM.finalKey, header.encryptionIV);
engine = CipherFactory.getInstance(mPM.dataCipher);
cipher = engine.getCipher(Cipher.ENCRYPT_MODE, mPM.finalKey, header.encryptionIV);
} catch (Exception e) {
throw new PwDbOutputException("Invalid algorithm.", e);
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2017 Brian Pellin.
*
* This file is part of KeePassDroid.
*
* KeePassDroid 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 2 of the License, or
* (at your option) any later version.
*
* KeePassDroid 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 KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.keepassdroid.stream;
import com.keepassdroid.utils.Types;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.Mac;
public class HmacBlockInputStream extends InputStream {
private LEDataInputStream baseStream;
private boolean verify;
private byte[] key;
private byte[] buffer;
private int bufferPos = 0;
private long blockIndex = 0;
private boolean endOfStream = false;
public HmacBlockInputStream(InputStream baseStream, boolean verify, byte[] key) {
super();
this.baseStream = new LEDataInputStream(baseStream);
this.verify = verify;
this.key = key;
buffer = null;
}
@Override
public int read() throws IOException {
if (endOfStream) return -1;
if (bufferPos == buffer.length) {
if (!readSafeBlock()) return -1;
}
int output = Types.readUByte(buffer, bufferPos);
bufferPos++;
return output;
}
@Override
public int read(byte[] outBuffer, int byteOffset, int byteCount) throws IOException {
int remaining = byteCount;
while (remaining > 0) {
if (bufferPos == buffer.length) {
if (!readSafeBlock()) {
return byteCount - remaining;
}
}
int copy = Math.min(buffer.length - bufferPos, remaining);
assert(copy > 0);
System.arraycopy(buffer, bufferPos, outBuffer, byteOffset, copy);
byteOffset += copy;
bufferPos += copy;
remaining -= copy;
}
return byteCount;
}
@Override
public int read(byte[] outBuffer) throws IOException {
return read(outBuffer, 0, outBuffer.length);
}
private boolean readSafeBlock() throws IOException {
if (endOfStream) return false;
byte[] storedHmac = baseStream.readBytes(32);
if (storedHmac == null || storedHmac.length != 32) {
throw new IOException("File corrupted");
}
byte[] pbBlockIndex = LEDataOutputStream.writeLongBuf(blockIndex);
byte[] pbBlockSize = baseStream.readBytes(4);
if (pbBlockSize == null || pbBlockSize.length != 4) {
throw new IOException("File corrupted");
}
int blockSize = LEDataInputStream.readInt(pbBlockSize, 0);
bufferPos = 0;
buffer = baseStream.readBytes(blockSize);
if (verify) {
byte[] cmpHmac;
byte[] pbBlockKey = HmacBlockStream.GetHmacKey64(key, blockIndex);
Mac hmac;
try {
hmac = Mac.getInstance("HmacSHA256");
} catch (NoSuchAlgorithmException e) {
throw new IOException("Invalid Hmac");
}
hmac.update(pbBlockIndex);
hmac.update(pbBlockSize);
if (buffer.length > 0) {
hmac.update(buffer);
}
cmpHmac = hmac.doFinal();
Arrays.fill(pbBlockKey, (byte)0);
if (!Arrays.equals(cmpHmac, storedHmac)) {
throw new IOException("Invalid Hmac");
}
}
blockIndex++;
if (blockSize == 0) {
endOfStream = true;
return false;
}
return true;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public void close() throws IOException {
baseStream.close();
}
@Override
public long skip(long byteCount) throws IOException {
return 0;
}
@Override
public int available() throws IOException {
return buffer.length - bufferPos;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2009-2011 Brian Pellin.
* Copyright 2009-2017 Brian Pellin.
*
* This file is part of KeePassDroid.
*
@@ -56,7 +56,9 @@ import com.keepassdroid.stream.LEDataOutputStream;
* @author Bill Zwicky <wrzwicky@pobox.com>
*/
public class Types {
public static long ULONG_MAX_VALUE = -1;
/** Read an unsigned byte */
public static int readUByte( byte[] buf, int offset ) {
return ((int)buf[offset] & 0xFF);