/* * 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/>. * */ package com.keepassdroid.database; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import android.os.DropBoxManager.Entry; import com.keepassdroid.crypto.finalkey.FinalKey; import com.keepassdroid.crypto.finalkey.FinalKeyFactory; import com.keepassdroid.database.exception.InvalidKeyFileException; import com.keepassdroid.database.exception.KeyFileEmptyException; import com.keepassdroid.stream.NullOutputStream; import com.keepassdroid.utils.Util; public abstract class PwDatabase { public byte masterKey[] = new byte[32]; public byte[] finalKey; public String name = "KeePass database"; public PwGroup rootGroup; public PwIconFactory iconFactory = new PwIconFactory(); public Map<PwGroupId, PwGroup> groups = new HashMap<PwGroupId, PwGroup>(); public Map<UUID, PwEntry> entries = new HashMap<UUID, PwEntry>(); private static boolean isKDBExtension(String filename) { if (filename == null) { return false; } int extIdx = filename.lastIndexOf("."); if (extIdx == -1) return false; return filename.substring(extIdx, filename.length()).equalsIgnoreCase(".kdb"); } public static PwDatabase getNewDBInstance(String filename) { if (isKDBExtension(filename)) { return new PwDatabaseV3(); } else { return new PwDatabaseV4(); } } public void makeFinalKey(byte[] masterSeed, byte[] masterSeed2, int numRounds) throws IOException { // Write checksum Checksum MessageDigest md = null; try { md = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { throw new IOException("SHA-256 not implemented here."); } NullOutputStream nos = new NullOutputStream(); DigestOutputStream dos = new DigestOutputStream(nos, md); byte[] transformedMasterKey = transformMasterKey(masterSeed2, masterKey, numRounds); dos.write(masterSeed); dos.write(transformedMasterKey); finalKey = md.digest(); } /** * Encrypt the master key a few times to make brute-force key-search harder * @throws IOException */ protected static byte[] transformMasterKey( byte[] pKeySeed, byte[] pKey, int rounds ) throws IOException { FinalKey key = FinalKeyFactory.createFinalKey(); return key.transformMasterKey(pKeySeed, pKey, rounds); } public abstract byte[] getMasterKey(String key, InputStream keyInputStream) throws InvalidKeyFileException, IOException; public void setMasterKey(String key, InputStream keyInputStream) throws InvalidKeyFileException, IOException { assert(key != null); masterKey = getMasterKey(key, keyInputStream); } protected byte[] getCompositeKey(String key, InputStream keyInputStream) throws InvalidKeyFileException, IOException { assert(key != null && keyInputStream != null); byte[] fileKey = getFileKey(keyInputStream); byte[] passwordKey = getPasswordKey(key); MessageDigest md; try { md = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { throw new IOException("SHA-256 not supported"); } md.update(passwordKey); return md.digest(fileKey); } protected byte[] getFileKey(InputStream keyInputStream) throws InvalidKeyFileException, IOException { assert(keyInputStream != null); ByteArrayOutputStream bos = new ByteArrayOutputStream(); Util.copyStream(keyInputStream, bos); byte[] keyData = bos.toByteArray(); ByteArrayInputStream bis = new ByteArrayInputStream(keyData); byte[] key = loadXmlKeyFile(bis); if ( key != null ) { return key; } long fileSize = keyData.length; if ( fileSize == 0 ) { throw new KeyFileEmptyException(); } else if ( fileSize == 32 ) { return keyData; } else if ( fileSize == 64 ) { byte[] hex = new byte[64]; try { return hexStringToByteArray(new String(keyData)); } catch (IndexOutOfBoundsException e) { // Key is not base 64, treat it as binary data } } MessageDigest md; try { md = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { throw new IOException("SHA-256 not supported"); } //SHA256Digest md = new SHA256Digest(); byte[] buffer = new byte[2048]; int offset = 0; try { md.update(keyData); } catch (Exception e) { System.out.println(e.toString()); } return md.digest(); } protected abstract byte[] loadXmlKeyFile(InputStream keyInputStream); public static byte[] hexStringToByteArray(String s) { int len = s.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16)); } return data; } public boolean validatePasswordEncoding(String key) { String encoding = getPasswordEncoding(); byte[] bKey; try { bKey = key.getBytes(encoding); } catch (UnsupportedEncodingException e) { return false; } String reencoded; try { reencoded = new String(bKey, encoding); } catch (UnsupportedEncodingException e) { return false; } return key.equals(reencoded); } protected abstract String getPasswordEncoding(); public byte[] getPasswordKey(String key) throws IOException { assert(key!=null); if ( key.length() == 0 ) throw new IllegalArgumentException( "Key cannot be empty." ); MessageDigest md; try { md = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { throw new IOException("SHA-256 not supported"); } byte[] bKey; try { bKey = key.getBytes(getPasswordEncoding()); } catch (UnsupportedEncodingException e) { assert false; bKey = key.getBytes(); } md.update(bKey, 0, bKey.length ); return md.digest(); } public abstract List<PwGroup> getGrpRoots(); public abstract List<PwGroup> getGroups(); public abstract List<PwEntry> getEntries(); public abstract long getNumRounds(); public abstract void setNumRounds(long rounds) throws NumberFormatException; public abstract boolean appSettingsEnabled(); public abstract PwEncryptionAlgorithm getEncAlgorithm(); public void addGroupTo(PwGroup newGroup, PwGroup parent) { // Add group to parent group if ( parent == null ) { parent = rootGroup; } parent.childGroups.add(newGroup); newGroup.setParent(parent); groups.put(newGroup.getId(), newGroup); parent.touch(true, true); } public void removeGroupFrom(PwGroup remove, PwGroup parent) { // Remove group from parent group parent.childGroups.remove(remove); groups.remove(remove.getId()); } public void addEntryTo(PwEntry newEntry, PwGroup parent) { // Add entry to parent if (parent != null) { parent.childEntries.add(newEntry); } newEntry.setParent(parent); entries.put(newEntry.getUUID(), newEntry); } public void removeEntryFrom(PwEntry remove, PwGroup parent) { // Remove entry for parent if (parent != null) { parent.childEntries.remove(remove); } entries.remove(remove.getUUID()); } public abstract PwGroupId newGroupId(); /** * Determine if an id number is already in use * * @param id * ID number to check for * @return True if the ID is used, false otherwise */ protected boolean isGroupIdUsed(PwGroupId id) { List<PwGroup> groups = getGroups(); for (int i = 0; i < groups.size(); i++) { PwGroup group =groups.get(i); if (group.getId().equals(id)) { return true; } } return false; } public abstract PwGroup createGroup(); public abstract boolean isBackup(PwGroup group); public void populateGlobals(PwGroup currentGroup) { List<PwGroup> childGroups = currentGroup.childGroups; List<PwEntry> childEntries = currentGroup.childEntries; for (int i = 0; i < childEntries.size(); i++ ) { PwEntry cur = childEntries.get(i); entries.put(cur.getUUID(), cur); } for (int i = 0; i < childGroups.size(); i++ ) { PwGroup cur = childGroups.get(i); groups.put(cur.getId(), cur); populateGlobals(cur); } } public boolean canRecycle(PwGroup group) { return false; } public boolean canRecycle(PwEntry entry) { return false; } public void recycle(PwEntry entry) { // Assume calls to this are protected by calling inRecyleBin throw new RuntimeException("Call not valid for .kdb databases."); } public void undoRecycle(PwEntry entry, PwGroup origParent) { throw new RuntimeException("Call not valid for .kdb databases."); } public void deleteEntry(PwEntry entry) { PwGroup parent = entry.getParent(); removeEntryFrom(entry, parent); parent.touch(false, true); } public void undoDeleteEntry(PwEntry entry, PwGroup origParent) { addEntryTo(entry, origParent); } public PwGroup getRecycleBin() { return null; } public boolean isGroupSearchable(PwGroup group, boolean omitBackup) { return group != null; } /** * Initialize a newly created database */ public abstract void initNew(String dbPath); }