mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Kotlinized importers
This commit is contained in:
@@ -31,7 +31,7 @@ abstract class Importer<PwDb : PwDatabase<*, *>> {
|
|||||||
/**
|
/**
|
||||||
* Load a versioned database file, return contents in a new PwDatabase.
|
* Load a versioned database file, return contents in a new PwDatabase.
|
||||||
*
|
*
|
||||||
* @param inStream Existing file to load.
|
* @param databaseInputStream Existing file to load.
|
||||||
* @param password Pass phrase for infile.
|
* @param password Pass phrase for infile.
|
||||||
* @return new PwDatabase container.
|
* @return new PwDatabase container.
|
||||||
*
|
*
|
||||||
@@ -39,6 +39,6 @@ abstract class Importer<PwDb : PwDatabase<*, *>> {
|
|||||||
* @throws InvalidDBException on database error.
|
* @throws InvalidDBException on database error.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class, InvalidDBException::class)
|
@Throws(IOException::class, InvalidDBException::class)
|
||||||
abstract fun openDatabase(inStream: InputStream, password: String?, keyInputStream: InputStream?, updater: ProgressTaskUpdater?): PwDb
|
abstract fun openDatabase(databaseInputStream: InputStream, password: String?, keyInputStream: InputStream?, progressTaskUpdater: ProgressTaskUpdater?): PwDb
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,338 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePass DX.
|
|
||||||
*
|
|
||||||
* KeePass DX 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.
|
|
||||||
*
|
|
||||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
|
|
||||||
Derived from
|
|
||||||
|
|
||||||
KeePass for J2ME
|
|
||||||
|
|
||||||
Copyright 2007 Naomaru Itoi <nao@phoneid.org>
|
|
||||||
|
|
||||||
This file was derived from
|
|
||||||
|
|
||||||
Java clone of KeePass - A KeePass file viewer for Java
|
|
||||||
Copyright 2006 Bill Zwicky <billzwicky@users.sourceforge.net>
|
|
||||||
|
|
||||||
This program 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; version 2
|
|
||||||
|
|
||||||
This program 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 this program; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.kunzisoft.keepass.database.load;
|
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.util.Log;
|
|
||||||
import com.kunzisoft.keepass.R;
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory;
|
|
||||||
import com.kunzisoft.keepass.database.element.*;
|
|
||||||
import com.kunzisoft.keepass.database.exception.*;
|
|
||||||
import com.kunzisoft.keepass.stream.LEDataInputStream;
|
|
||||||
import com.kunzisoft.keepass.stream.NullOutputStream;
|
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater;
|
|
||||||
import com.kunzisoft.keepass.utils.Types;
|
|
||||||
|
|
||||||
import javax.crypto.*;
|
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.security.*;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a v3 database file.
|
|
||||||
*
|
|
||||||
* @author Naomaru Itoi <nao@phoneid.org>
|
|
||||||
* @author Bill Zwicky <wrzwicky@pobox.com>
|
|
||||||
*/
|
|
||||||
public class ImporterV3 extends Importer<PwDatabaseV3> {
|
|
||||||
|
|
||||||
private static final String TAG = ImporterV3.class.getName();
|
|
||||||
|
|
||||||
private PwDatabaseV3 databaseToOpen;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public PwDatabaseV3 openDatabase(@NonNull InputStream inStream,
|
|
||||||
String password,
|
|
||||||
InputStream kfIs,
|
|
||||||
ProgressTaskUpdater progressTaskUpdater)
|
|
||||||
throws IOException, InvalidDBException {
|
|
||||||
|
|
||||||
// Load entire file, most of it's encrypted.
|
|
||||||
int fileSize = inStream.available();
|
|
||||||
byte[] filebuf = new byte[fileSize + 16]; // Pad with a blocksize (Twofish uses 128 bits), since Android 4.3 tries to write more to the buffer
|
|
||||||
inStream.read(filebuf, 0, fileSize); // TODO remove
|
|
||||||
inStream.close();
|
|
||||||
|
|
||||||
// Parse header (unencrypted)
|
|
||||||
if( fileSize < PwDbHeaderV3.BUF_SIZE )
|
|
||||||
throw new IOException( "File too short for header" );
|
|
||||||
PwDbHeaderV3 hdr = new PwDbHeaderV3();
|
|
||||||
hdr.loadFromFile(filebuf, 0 );
|
|
||||||
|
|
||||||
if( (hdr.signature1 != PwDbHeader.PWM_DBSIG_1) || (hdr.signature2 != PwDbHeaderV3.DBSIG_2) ) {
|
|
||||||
throw new InvalidDBSignatureException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !hdr.matchesVersion() ) {
|
|
||||||
throw new InvalidDBVersionException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressTaskUpdater != null)
|
|
||||||
progressTaskUpdater.updateMessage(R.string.retrieving_db_key);
|
|
||||||
databaseToOpen = new PwDatabaseV3();
|
|
||||||
databaseToOpen.retrieveMasterKey(password, kfIs);
|
|
||||||
|
|
||||||
// Select algorithm
|
|
||||||
if( (hdr.flags & PwDbHeaderV3.FLAG_RIJNDAEL) != 0 ) {
|
|
||||||
databaseToOpen.setEncryptionAlgorithm(PwEncryptionAlgorithm.AESRijndael);
|
|
||||||
} else if( (hdr.flags & PwDbHeaderV3.FLAG_TWOFISH) != 0 ) {
|
|
||||||
databaseToOpen.setEncryptionAlgorithm(PwEncryptionAlgorithm.Twofish);
|
|
||||||
} else {
|
|
||||||
throw new InvalidAlgorithmException();
|
|
||||||
}
|
|
||||||
|
|
||||||
databaseToOpen.setNumberKeyEncryptionRounds(hdr.numKeyEncRounds);
|
|
||||||
|
|
||||||
// Generate transformedMasterKey from masterKey
|
|
||||||
databaseToOpen.makeFinalKey(hdr.getMasterSeed(), hdr.transformSeed, databaseToOpen.getNumberKeyEncryptionRounds());
|
|
||||||
|
|
||||||
if (progressTaskUpdater != null)
|
|
||||||
progressTaskUpdater.updateMessage(R.string.decrypting_db);
|
|
||||||
// Initialize Rijndael algorithm
|
|
||||||
Cipher cipher;
|
|
||||||
try {
|
|
||||||
if ( databaseToOpen.getEncryptionAlgorithm() == PwEncryptionAlgorithm.AESRijndael) {
|
|
||||||
cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding");
|
|
||||||
} else if ( databaseToOpen.getEncryptionAlgorithm() == PwEncryptionAlgorithm.Twofish ) {
|
|
||||||
cipher = CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING");
|
|
||||||
} else {
|
|
||||||
throw new IOException( "Encryption algorithm is not supported" );
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (NoSuchAlgorithmException e1) {
|
|
||||||
throw new IOException("No such algorithm");
|
|
||||||
} catch (NoSuchPaddingException e1) {
|
|
||||||
throw new IOException("No such pdading");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
cipher.init( Cipher.DECRYPT_MODE, new SecretKeySpec( databaseToOpen.getFinalKey(), "AES" ), new IvParameterSpec(hdr.getEncryptionIV()) );
|
|
||||||
} catch (InvalidKeyException e1) {
|
|
||||||
throw new IOException("Invalid key");
|
|
||||||
} catch (InvalidAlgorithmParameterException e1) {
|
|
||||||
throw new IOException("Invalid algorithm parameter.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt! The first bytes aren't encrypted (that's the header)
|
|
||||||
int encryptedPartSize;
|
|
||||||
try {
|
|
||||||
encryptedPartSize = cipher.doFinal(filebuf, PwDbHeaderV3.BUF_SIZE, fileSize - PwDbHeaderV3.BUF_SIZE, filebuf, PwDbHeaderV3.BUF_SIZE );
|
|
||||||
} catch (ShortBufferException e1) {
|
|
||||||
throw new IOException("Buffer too short");
|
|
||||||
} catch (IllegalBlockSizeException e1) {
|
|
||||||
throw new IOException("Invalid block size");
|
|
||||||
} catch (BadPaddingException e1) {
|
|
||||||
throw new InvalidPasswordException();
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageDigest md = null;
|
|
||||||
try {
|
|
||||||
md = MessageDigest.getInstance("SHA-256");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new IOException("No SHA-256 algorithm");
|
|
||||||
}
|
|
||||||
NullOutputStream nos = new NullOutputStream();
|
|
||||||
DigestOutputStream dos = new DigestOutputStream(nos, md);
|
|
||||||
dos.write(filebuf, PwDbHeaderV3.BUF_SIZE, encryptedPartSize);
|
|
||||||
dos.close();
|
|
||||||
byte[] hash = md.digest();
|
|
||||||
|
|
||||||
if( ! Arrays.equals(hash, hdr.contentsHash) ) {
|
|
||||||
|
|
||||||
Log.w(TAG,"Database file did not decrypt correctly. (checksum code is broken)");
|
|
||||||
throw new InvalidPasswordException();
|
|
||||||
}
|
|
||||||
|
|
||||||
// New manual root because V3 contains multiple root groups (here available with getRootGroups())
|
|
||||||
PwGroupV3 newRoot = databaseToOpen.createGroup();
|
|
||||||
newRoot.setLevel(-1);
|
|
||||||
databaseToOpen.setRootGroup(newRoot);
|
|
||||||
|
|
||||||
// Import all groups
|
|
||||||
int pos = PwDbHeaderV3.BUF_SIZE;
|
|
||||||
PwGroupV3 newGrp = databaseToOpen.createGroup();
|
|
||||||
for( int i = 0; i < hdr.numGroups; ) {
|
|
||||||
int fieldType = LEDataInputStream.readUShort( filebuf, pos );
|
|
||||||
pos += 2;
|
|
||||||
int fieldSize = LEDataInputStream.readInt( filebuf, pos );
|
|
||||||
pos += 4;
|
|
||||||
|
|
||||||
if( fieldType == 0xFFFF ) {
|
|
||||||
// End-Group record. Save group and count it.
|
|
||||||
databaseToOpen.addGroupIndex(newGrp);
|
|
||||||
newGrp = databaseToOpen.createGroup();
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
readGroupField(databaseToOpen, newGrp, fieldType, filebuf, pos);
|
|
||||||
}
|
|
||||||
pos += fieldSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import all entries
|
|
||||||
PwEntryV3 newEnt = databaseToOpen.createEntry();
|
|
||||||
for( int i = 0; i < hdr.numEntries; ) {
|
|
||||||
int fieldType = LEDataInputStream.readUShort( filebuf, pos );
|
|
||||||
int fieldSize = LEDataInputStream.readInt( filebuf, pos + 2 );
|
|
||||||
|
|
||||||
if( fieldType == 0xFFFF ) {
|
|
||||||
// End-Group record. Save group and count it.
|
|
||||||
databaseToOpen.addEntryIndex(newEnt);
|
|
||||||
newEnt = databaseToOpen.createEntry();
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
readEntryField(databaseToOpen, newEnt, filebuf, pos);
|
|
||||||
}
|
|
||||||
pos += 2 + 4 + fieldSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
databaseToOpen.constructTreeFromIndex();
|
|
||||||
|
|
||||||
return databaseToOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse and save one record from binary file.
|
|
||||||
* @param buf
|
|
||||||
* @param offset
|
|
||||||
* @return If >0,
|
|
||||||
* @throws UnsupportedEncodingException
|
|
||||||
*/
|
|
||||||
private void readGroupField(PwDatabaseV3 db, PwGroupV3 grp, int fieldType, byte[] buf, int offset) throws UnsupportedEncodingException {
|
|
||||||
switch( fieldType ) {
|
|
||||||
case 0x0000 :
|
|
||||||
// Ignore field
|
|
||||||
break;
|
|
||||||
case 0x0001 :
|
|
||||||
grp.setGroupId(LEDataInputStream.readInt(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x0002 :
|
|
||||||
grp.setTitle(Types.readCString(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x0003 :
|
|
||||||
grp.setCreationTime(new PwDate(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x0004 :
|
|
||||||
grp.setLastModificationTime(new PwDate(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x0005 :
|
|
||||||
grp.setLastAccessTime(new PwDate(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x0006 :
|
|
||||||
grp.setExpiryTime(new PwDate(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x0007 :
|
|
||||||
grp.setIcon(db.getIconFactory().getIcon(LEDataInputStream.readInt(buf, offset)));
|
|
||||||
break;
|
|
||||||
case 0x0008 :
|
|
||||||
grp.setLevel(LEDataInputStream.readUShort(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x0009 :
|
|
||||||
grp.setFlags(LEDataInputStream.readInt(buf, offset));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void readEntryField(PwDatabaseV3 db, PwEntryV3 ent, byte[] buf, int offset) throws UnsupportedEncodingException {
|
|
||||||
int fieldType = LEDataInputStream.readUShort(buf, offset);
|
|
||||||
offset += 2;
|
|
||||||
int fieldSize = LEDataInputStream.readInt(buf, offset);
|
|
||||||
offset += 4;
|
|
||||||
|
|
||||||
switch( fieldType ) {
|
|
||||||
case 0x0000 :
|
|
||||||
// Ignore field
|
|
||||||
break;
|
|
||||||
case 0x0001 :
|
|
||||||
ent.setNodeId(new PwNodeIdUUID(Types.bytestoUUID(buf, offset)));
|
|
||||||
break;
|
|
||||||
case 0x0002 :
|
|
||||||
PwGroupV3 pwGroupV3 = databaseToOpen.createGroup();
|
|
||||||
pwGroupV3.setNodeId(new PwNodeIdInt(LEDataInputStream.readInt(buf, offset)));
|
|
||||||
ent.setParent(pwGroupV3);
|
|
||||||
break;
|
|
||||||
case 0x0003 :
|
|
||||||
int iconId = LEDataInputStream.readInt(buf, offset);
|
|
||||||
|
|
||||||
// Clean up after bug that set icon ids to -1
|
|
||||||
if (iconId == -1) {
|
|
||||||
iconId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ent.setIcon(db.getIconFactory().getIcon(iconId));
|
|
||||||
break;
|
|
||||||
case 0x0004 :
|
|
||||||
ent.setTitle(Types.readCString(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x0005 :
|
|
||||||
ent.setUrl(Types.readCString(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x0006 :
|
|
||||||
ent.setUsername(Types.readCString(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x0007 :
|
|
||||||
ent.setPassword(buf, offset, Types.strlen(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x0008 :
|
|
||||||
ent.setNotes(Types.readCString(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x0009 :
|
|
||||||
ent.setCreationTime(new PwDate(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x000A :
|
|
||||||
ent.setLastModificationTime(new PwDate(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x000B :
|
|
||||||
ent.setLastAccessTime(new PwDate(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x000C :
|
|
||||||
ent.setExpiryTime(new PwDate(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x000D :
|
|
||||||
ent.setBinaryDesc(Types.readCString(buf, offset));
|
|
||||||
break;
|
|
||||||
case 0x000E :
|
|
||||||
ent.setBinaryData(buf, offset, fieldSize);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePass DX.
|
||||||
|
*
|
||||||
|
* KeePass DX 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.
|
||||||
|
*
|
||||||
|
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
|
||||||
|
Derived from
|
||||||
|
|
||||||
|
KeePass for J2ME
|
||||||
|
|
||||||
|
Copyright 2007 Naomaru Itoi <nao@phoneid.org>
|
||||||
|
|
||||||
|
This file was derived from
|
||||||
|
|
||||||
|
Java clone of KeePass - A KeePass file viewer for Java
|
||||||
|
Copyright 2006 Bill Zwicky <billzwicky@users.sourceforge.net>
|
||||||
|
|
||||||
|
This program 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; version 2
|
||||||
|
|
||||||
|
This program 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 this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kunzisoft.keepass.database.load
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||||
|
import com.kunzisoft.keepass.database.element.*
|
||||||
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
|
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||||
|
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||||
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
|
import com.kunzisoft.keepass.utils.Types
|
||||||
|
|
||||||
|
import javax.crypto.*
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.UnsupportedEncodingException
|
||||||
|
import java.security.*
|
||||||
|
import java.util.Arrays
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a v3 database file.
|
||||||
|
*
|
||||||
|
* @author Naomaru Itoi <nao></nao>@phoneid.org>
|
||||||
|
* @author Bill Zwicky <wrzwicky></wrzwicky>@pobox.com>
|
||||||
|
*/
|
||||||
|
class ImporterV3 : Importer<PwDatabaseV3>() {
|
||||||
|
|
||||||
|
private lateinit var mDatabaseToOpen: PwDatabaseV3
|
||||||
|
|
||||||
|
@Throws(IOException::class, InvalidDBException::class)
|
||||||
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
|
password: String?,
|
||||||
|
keyInputStream: InputStream?,
|
||||||
|
progressTaskUpdater: ProgressTaskUpdater?): PwDatabaseV3 {
|
||||||
|
|
||||||
|
// Load entire file, most of it's encrypted.
|
||||||
|
val fileSize = databaseInputStream.available()
|
||||||
|
val filebuf = ByteArray(fileSize + 16) // Pad with a blocksize (Twofish uses 128 bits), since Android 4.3 tries to write more to the buffer
|
||||||
|
databaseInputStream.read(filebuf, 0, fileSize) // TODO remove
|
||||||
|
databaseInputStream.close()
|
||||||
|
|
||||||
|
// Parse header (unencrypted)
|
||||||
|
if (fileSize < PwDbHeaderV3.BUF_SIZE)
|
||||||
|
throw IOException("File too short for header")
|
||||||
|
val hdr = PwDbHeaderV3()
|
||||||
|
hdr.loadFromFile(filebuf, 0)
|
||||||
|
|
||||||
|
if (hdr.signature1 != PwDbHeader.PWM_DBSIG_1 || hdr.signature2 != PwDbHeaderV3.DBSIG_2) {
|
||||||
|
throw InvalidDBSignatureException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hdr.matchesVersion()) {
|
||||||
|
throw InvalidDBVersionException()
|
||||||
|
}
|
||||||
|
|
||||||
|
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
||||||
|
mDatabaseToOpen = PwDatabaseV3()
|
||||||
|
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
|
||||||
|
|
||||||
|
// Select algorithm
|
||||||
|
if (hdr.flags and PwDbHeaderV3.FLAG_RIJNDAEL != 0) {
|
||||||
|
mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.AESRijndael
|
||||||
|
} else if (hdr.flags and PwDbHeaderV3.FLAG_TWOFISH != 0) {
|
||||||
|
mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.Twofish
|
||||||
|
} else {
|
||||||
|
throw InvalidAlgorithmException()
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong()
|
||||||
|
|
||||||
|
// Generate transformedMasterKey from masterKey
|
||||||
|
mDatabaseToOpen.makeFinalKey(hdr.masterSeed, hdr.transformSeed, mDatabaseToOpen.numberKeyEncryptionRounds)
|
||||||
|
|
||||||
|
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
|
||||||
|
// Initialize Rijndael algorithm
|
||||||
|
val cipher: Cipher
|
||||||
|
try {
|
||||||
|
if (mDatabaseToOpen.encryptionAlgorithm === PwEncryptionAlgorithm.AESRijndael) {
|
||||||
|
cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
} else if (mDatabaseToOpen.encryptionAlgorithm === PwEncryptionAlgorithm.Twofish) {
|
||||||
|
cipher = CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
|
||||||
|
} else {
|
||||||
|
throw IOException("Encryption algorithm is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e1: NoSuchAlgorithmException) {
|
||||||
|
throw IOException("No such algorithm")
|
||||||
|
} catch (e1: NoSuchPaddingException) {
|
||||||
|
throw IOException("No such pdading")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(mDatabaseToOpen.finalKey, "AES"), IvParameterSpec(hdr.encryptionIV))
|
||||||
|
} catch (e1: InvalidKeyException) {
|
||||||
|
throw IOException("Invalid key")
|
||||||
|
} catch (e1: InvalidAlgorithmParameterException) {
|
||||||
|
throw IOException("Invalid algorithm parameter.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt! The first bytes aren't encrypted (that's the header)
|
||||||
|
val encryptedPartSize: Int
|
||||||
|
try {
|
||||||
|
encryptedPartSize = cipher.doFinal(filebuf, PwDbHeaderV3.BUF_SIZE, fileSize - PwDbHeaderV3.BUF_SIZE, filebuf, PwDbHeaderV3.BUF_SIZE)
|
||||||
|
} catch (e1: ShortBufferException) {
|
||||||
|
throw IOException("Buffer too short")
|
||||||
|
} catch (e1: IllegalBlockSizeException) {
|
||||||
|
throw IOException("Invalid block size")
|
||||||
|
} catch (e1: BadPaddingException) {
|
||||||
|
throw InvalidPasswordException()
|
||||||
|
}
|
||||||
|
|
||||||
|
var md: MessageDigest? = null
|
||||||
|
try {
|
||||||
|
md = MessageDigest.getInstance("SHA-256")
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
throw IOException("No SHA-256 algorithm")
|
||||||
|
}
|
||||||
|
|
||||||
|
val nos = NullOutputStream()
|
||||||
|
val dos = DigestOutputStream(nos, md)
|
||||||
|
dos.write(filebuf, PwDbHeaderV3.BUF_SIZE, encryptedPartSize)
|
||||||
|
dos.close()
|
||||||
|
val hash = md!!.digest()
|
||||||
|
|
||||||
|
if (!Arrays.equals(hash, hdr.contentsHash)) {
|
||||||
|
|
||||||
|
Log.w(TAG, "Database file did not decrypt correctly. (checksum code is broken)")
|
||||||
|
throw InvalidPasswordException()
|
||||||
|
}
|
||||||
|
|
||||||
|
// New manual root because V3 contains multiple root groups (here available with getRootGroups())
|
||||||
|
val newRoot = mDatabaseToOpen.createGroup()
|
||||||
|
newRoot.level = -1
|
||||||
|
mDatabaseToOpen.rootGroup = newRoot
|
||||||
|
|
||||||
|
// Import all groups
|
||||||
|
var pos = PwDbHeaderV3.BUF_SIZE
|
||||||
|
var newGrp = mDatabaseToOpen.createGroup()
|
||||||
|
run {
|
||||||
|
var i = 0
|
||||||
|
while (i < hdr.numGroups) {
|
||||||
|
val fieldType = LEDataInputStream.readUShort(filebuf, pos)
|
||||||
|
pos += 2
|
||||||
|
val fieldSize = LEDataInputStream.readInt(filebuf, pos)
|
||||||
|
pos += 4
|
||||||
|
|
||||||
|
if (fieldType == 0xFFFF) {
|
||||||
|
// End-Group record. Save group and count it.
|
||||||
|
mDatabaseToOpen.addGroupIndex(newGrp)
|
||||||
|
newGrp = mDatabaseToOpen.createGroup()
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
readGroupField(mDatabaseToOpen, newGrp, fieldType, filebuf, pos)
|
||||||
|
}
|
||||||
|
pos += fieldSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import all entries
|
||||||
|
var newEnt = mDatabaseToOpen.createEntry()
|
||||||
|
var i = 0
|
||||||
|
while (i < hdr.numEntries) {
|
||||||
|
val fieldType = LEDataInputStream.readUShort(filebuf, pos)
|
||||||
|
val fieldSize = LEDataInputStream.readInt(filebuf, pos + 2)
|
||||||
|
|
||||||
|
if (fieldType == 0xFFFF) {
|
||||||
|
// End-Group record. Save group and count it.
|
||||||
|
mDatabaseToOpen.addEntryIndex(newEnt)
|
||||||
|
newEnt = mDatabaseToOpen.createEntry()
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
readEntryField(mDatabaseToOpen, newEnt, filebuf, pos)
|
||||||
|
}
|
||||||
|
pos += 2 + 4 + fieldSize
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseToOpen.constructTreeFromIndex()
|
||||||
|
|
||||||
|
return mDatabaseToOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and save one record from binary file.
|
||||||
|
* @param buf
|
||||||
|
* @param offset
|
||||||
|
* @return If >0,
|
||||||
|
* @throws UnsupportedEncodingException
|
||||||
|
*/
|
||||||
|
@Throws(UnsupportedEncodingException::class)
|
||||||
|
private fun readGroupField(db: PwDatabaseV3, grp: PwGroupV3, fieldType: Int, buf: ByteArray, offset: Int) {
|
||||||
|
when (fieldType) {
|
||||||
|
0x0000 -> {
|
||||||
|
}
|
||||||
|
0x0001 -> grp.setGroupId(LEDataInputStream.readInt(buf, offset))
|
||||||
|
0x0002 -> grp.title = Types.readCString(buf, offset)
|
||||||
|
0x0003 -> grp.creationTime = PwDate(buf, offset)
|
||||||
|
0x0004 -> grp.lastModificationTime = PwDate(buf, offset)
|
||||||
|
0x0005 -> grp.lastAccessTime = PwDate(buf, offset)
|
||||||
|
0x0006 -> grp.expiryTime = PwDate(buf, offset)
|
||||||
|
0x0007 -> grp.icon = db.iconFactory.getIcon(LEDataInputStream.readInt(buf, offset))
|
||||||
|
0x0008 -> grp.level = LEDataInputStream.readUShort(buf, offset)
|
||||||
|
0x0009 -> grp.flags = LEDataInputStream.readInt(buf, offset)
|
||||||
|
}// Ignore field
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(UnsupportedEncodingException::class)
|
||||||
|
private fun readEntryField(db: PwDatabaseV3, ent: PwEntryV3, buf: ByteArray, offset: Int) {
|
||||||
|
var offsetMutable = offset
|
||||||
|
val fieldType = LEDataInputStream.readUShort(buf, offsetMutable)
|
||||||
|
offsetMutable += 2
|
||||||
|
val fieldSize = LEDataInputStream.readInt(buf, offsetMutable)
|
||||||
|
offsetMutable += 4
|
||||||
|
|
||||||
|
when (fieldType) {
|
||||||
|
0x0000 -> {
|
||||||
|
}
|
||||||
|
0x0001 -> ent.nodeId = PwNodeIdUUID(Types.bytestoUUID(buf, offsetMutable))
|
||||||
|
0x0002 -> {
|
||||||
|
val pwGroupV3 = mDatabaseToOpen.createGroup()
|
||||||
|
pwGroupV3.nodeId = PwNodeIdInt(LEDataInputStream.readInt(buf, offsetMutable))
|
||||||
|
ent.parent = pwGroupV3
|
||||||
|
}
|
||||||
|
0x0003 -> {
|
||||||
|
var iconId = LEDataInputStream.readInt(buf, offsetMutable)
|
||||||
|
|
||||||
|
// Clean up after bug that set icon ids to -1
|
||||||
|
if (iconId == -1) {
|
||||||
|
iconId = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.icon = db.iconFactory.getIcon(iconId)
|
||||||
|
}
|
||||||
|
0x0004 -> ent.title = Types.readCString(buf, offsetMutable)
|
||||||
|
0x0005 -> ent.url = Types.readCString(buf, offsetMutable)
|
||||||
|
0x0006 -> ent.username = Types.readCString(buf, offsetMutable)
|
||||||
|
0x0007 -> ent.setPassword(buf, offsetMutable, Types.strlen(buf, offsetMutable))
|
||||||
|
0x0008 -> ent.notes = Types.readCString(buf, offsetMutable)
|
||||||
|
0x0009 -> ent.creationTime = PwDate(buf, offsetMutable)
|
||||||
|
0x000A -> ent.lastModificationTime = PwDate(buf, offsetMutable)
|
||||||
|
0x000B -> ent.lastAccessTime = PwDate(buf, offsetMutable)
|
||||||
|
0x000C -> ent.expiryTime = PwDate(buf, offsetMutable)
|
||||||
|
0x000D -> ent.binaryDesc = Types.readCString(buf, offsetMutable)
|
||||||
|
0x000E -> ent.setBinaryData(buf, offsetMutable, fieldSize)
|
||||||
|
}// Ignore field
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = ImporterV3::class.java.name
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
1071
app/src/main/java/com/kunzisoft/keepass/database/load/ImporterV4.kt
Normal file
1071
app/src/main/java/com/kunzisoft/keepass/database/load/ImporterV4.kt
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user