Kotlinized importers

This commit is contained in:
J-Jamet
2019-06-02 14:04:36 +02:00
parent 9907af8135
commit 8890296beb
5 changed files with 1370 additions and 1479 deletions

View File

@@ -31,7 +31,7 @@ abstract class Importer<PwDb : 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.
* @return new PwDatabase container.
*
@@ -39,6 +39,6 @@ abstract class Importer<PwDb : PwDatabase<*, *>> {
* @throws InvalidDBException on database error.
*/
@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
}

View File

@@ -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;
}
}
}

View File

@@ -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