/* * Copyright 2009-2016 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/>. * * 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.keepassdroid.database.load; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.security.DigestOutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.ShortBufferException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import android.util.Log; import com.android.keepass.R; import com.keepassdroid.UpdateStatus; import com.keepassdroid.crypto.CipherFactory; import com.keepassdroid.database.PwDatabaseV3; import com.keepassdroid.database.PwDate; import com.keepassdroid.database.PwDbHeader; import com.keepassdroid.database.PwDbHeaderV3; import com.keepassdroid.database.PwEncryptionAlgorithm; import com.keepassdroid.database.PwEntryV3; import com.keepassdroid.database.PwGroupV3; import com.keepassdroid.database.exception.InvalidAlgorithmException; import com.keepassdroid.database.exception.InvalidDBException; import com.keepassdroid.database.exception.InvalidDBSignatureException; import com.keepassdroid.database.exception.InvalidDBVersionException; import com.keepassdroid.database.exception.InvalidKeyFileException; import com.keepassdroid.database.exception.InvalidPasswordException; import com.keepassdroid.stream.LEDataInputStream; import com.keepassdroid.stream.LEDataOutputStream; import com.keepassdroid.stream.NullOutputStream; import com.keepassdroid.utils.Types; /** * Load a v3 database file. * * @author Naomaru Itoi <nao@phoneid.org> * @author Bill Zwicky <wrzwicky@pobox.com> */ public class ImporterV3 extends Importer { public ImporterV3() { super(); } protected PwDatabaseV3 createDB() { return new PwDatabaseV3(); } /** * Load a v3 database file, return contents in a new PwDatabaseV3. * * @param inStream Existing file to load. * @param password Pass phrase for infile. * @return new PwDatabaseV3 container. * * @throws IOException on any file error. * @throws InvalidKeyFileException * @throws InvalidPasswordException * @throws InvalidPasswordException on a decryption error, or possible internal bug. * @throws InvalidDBSignatureException * @throws InvalidDBVersionException * @throws IllegalBlockSizeException on a decryption error, or possible internal bug. * @throws BadPaddingException on a decryption error, or possible internal bug. * @throws NoSuchAlgorithmException on a decryption error, or possible internal bug. * @throws NoSuchPaddingException on a decryption error, or possible internal bug. * @throws InvalidAlgorithmParameterException if error decrypting main file body. * @throws ShortBufferException if error decrypting main file body. */ public PwDatabaseV3 openDatabase( InputStream inStream, String password, InputStream kfIs) throws IOException, InvalidDBException { return openDatabase(inStream, password, kfIs, new UpdateStatus()); } public PwDatabaseV3 openDatabase( InputStream inStream, String password, InputStream kfIs, UpdateStatus status ) throws IOException, InvalidDBException { PwDatabaseV3 newManager; // 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); 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(); } status.updateMessage(R.string.creating_db_key); newManager = createDB(); newManager.setMasterKey(password, kfIs); // Select algorithm if( (hdr.flags & PwDbHeaderV3.FLAG_RIJNDAEL) != 0 ) { newManager.algorithm = PwEncryptionAlgorithm.Rjindal; } else if( (hdr.flags & PwDbHeaderV3.FLAG_TWOFISH) != 0 ) { newManager.algorithm = PwEncryptionAlgorithm.Twofish; } else { throw new InvalidAlgorithmException(); } // Copy for testing newManager.copyHeader(hdr); newManager.numKeyEncRounds = hdr.numKeyEncRounds; newManager.name = "KeePass Password Manager"; // Generate transformedMasterKey from masterKey newManager.makeFinalKey(hdr.masterSeed, hdr.transformSeed, newManager.numKeyEncRounds); status.updateMessage(R.string.decrypting_db); // Initialize Rijndael algorithm Cipher cipher; try { if ( newManager.algorithm == PwEncryptionAlgorithm.Rjindal ) { cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding"); } else if ( newManager.algorithm == 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( newManager.finalKey, "AES" ), new IvParameterSpec( hdr.encryptionIV ) ); } 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(); } // Copy decrypted data for testing newManager.copyEncrypted(filebuf, PwDbHeaderV3.BUF_SIZE, encryptedPartSize); 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("KeePassDroid","Database file did not decrypt correctly. (checksum code is broken)"); throw new InvalidPasswordException(); } // Import all groups int pos = PwDbHeaderV3.BUF_SIZE; PwGroupV3 newGrp = new PwGroupV3(); 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. newGrp.populateBlankFields(newManager); newManager.groups.add(newGrp); newGrp = new PwGroupV3(); i++; } else { readGroupField(newManager, newGrp, fieldType, filebuf, pos); } pos += fieldSize; } // Import all entries PwEntryV3 newEnt = new PwEntryV3(); 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. newEnt.populateBlankFields(newManager); newManager.entries.add(newEnt); newEnt = new PwEntryV3(); i++; } else { readEntryField(newManager, newEnt, filebuf, pos); } pos += 2 + 4 + fieldSize; } newManager.constructTree(null); return newManager; } /** * KeePass's custom pad style. * * @param data buffer to pad. * @return addtional bytes to append to data[] to make * a properly padded array. */ public static byte[] makePad( byte[] data ) { //custom pad method // append 0x80 plus zeros to a multiple of 4 bytes int thisblk = 32 - data.length % 32; // bytes needed to finish blk int nextblk = 0; // 32 if we need another block // need 9 bytes; add new block if no room if( thisblk < 9 ) { nextblk = 32; } // all bytes are zeroed for free byte[] pad = new byte[ thisblk + nextblk ]; pad[0] = (byte)0x80; // write length*8 to end of final block int ix = thisblk + nextblk - 8; LEDataOutputStream.writeInt( data.length>>29, pad, ix ); bsw32( pad, ix ); ix += 4; LEDataOutputStream.writeInt( data.length<<3, pad, ix ); bsw32( pad, ix ); return pad; } public static void bsw32( byte[] ary, int offset ) { byte t = ary[offset]; ary[offset] = ary[offset+3]; ary[offset+3] = t; t = ary[offset+1]; ary[offset+1] = ary[offset+2]; ary[offset+2] = t; } /** * Parse and save one record from binary file. * @param buf * @param offset * @return If >0, * @throws UnsupportedEncodingException */ void readGroupField(PwDatabaseV3 db, PwGroupV3 grp, int fieldType, byte[] buf, int offset) throws UnsupportedEncodingException { switch( fieldType ) { case 0x0000 : // Ignore field break; case 0x0001 : grp.groupId = LEDataInputStream.readInt(buf, offset); break; case 0x0002 : grp.name = Types.readCString(buf, offset); break; case 0x0003 : grp.tCreation = new PwDate(buf, offset); break; case 0x0004 : grp.tLastMod = new PwDate(buf, offset); break; case 0x0005 : grp.tLastAccess = new PwDate(buf, offset); break; case 0x0006 : grp.tExpire = new PwDate(buf, offset); break; case 0x0007 : grp.icon = db.iconFactory.getIcon(LEDataInputStream.readInt(buf, offset)); break; case 0x0008 : grp.level = LEDataInputStream.readUShort(buf, offset); break; case 0x0009 : grp.flags = LEDataInputStream.readInt(buf, offset); break; } } 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.setUUID(Types.bytestoUUID(buf, offset)); break; case 0x0002 : ent.groupId = LEDataInputStream.readInt(buf, offset); 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.icon = db.iconFactory.getIcon(iconId); break; case 0x0004 : ent.title = Types.readCString(buf, offset); break; case 0x0005 : ent.url = Types.readCString(buf, offset); break; case 0x0006 : ent.username = Types.readCString(buf, offset); break; case 0x0007 : ent.setPassword(buf, offset, Types.strlen(buf, offset)); break; case 0x0008 : ent.additional = Types.readCString(buf, offset); break; case 0x0009 : ent.tCreation = new PwDate(buf, offset); break; case 0x000A : ent.tLastMod = new PwDate(buf, offset); break; case 0x000B : ent.tLastAccess = new PwDate(buf, offset); break; case 0x000C : ent.tExpire = new PwDate(buf, offset); break; case 0x000D : ent.binaryDesc = Types.readCString(buf, offset); break; case 0x000E : ent.setBinaryData(buf, offset, fieldSize); break; } } }