/*
* 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 2 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 .
*
*/
package com.keepassdroid.database;
import android.webkit.URLUtil;
import com.keepassdroid.collections.VariantDictionary;
import com.keepassdroid.crypto.CryptoUtil;
import com.keepassdroid.crypto.engine.AesEngine;
import com.keepassdroid.crypto.engine.CipherEngine;
import com.keepassdroid.crypto.keyDerivation.AesKdf;
import com.keepassdroid.crypto.keyDerivation.KdfEngine;
import com.keepassdroid.crypto.keyDerivation.KdfFactory;
import com.keepassdroid.crypto.keyDerivation.KdfParameters;
import com.keepassdroid.database.exception.InvalidKeyFileException;
import com.keepassdroid.utils.EmptyUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import java.io.IOException;
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;
import java.util.Map;
import java.util.UUID;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import biz.source_code.base64Coder.Base64Coder;
public class PwDatabaseV4 extends PwDatabase {
public static final int DEFAULT_ROUNDS = 6000;
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 byte[] hmacKey;
public UUID dataCipher = AesEngine.CIPHER_UUID;
public CipherEngine dataEngine = new AesEngine();
public PwCompressionAlgorithm compressionAlgorithm = PwCompressionAlgorithm.Gzip;
// TODO: Refactor me away to get directly from kdfParameters
public long numKeyEncRounds = 6000;
public Date nameChanged = new Date();
public Date settingsChanged = new Date();
public String description = "";
public Date descriptionChanged = new Date();
public String defaultUserName = "";
public Date defaultUserNameChanged = new Date();
public Date keyLastChanged = new Date();
public long keyChangeRecDays = -1;
public long keyChangeForceDays = 1;
public boolean keyChangeForceOnce = false;
public long maintenanceHistoryDays = 365;
public String color = "";
public boolean recycleBinEnabled = true;
public UUID recycleBinUUID = UUID_ZERO;
public Date recycleBinChanged = new Date();
public UUID entryTemplatesGroup = UUID_ZERO;
public Date entryTemplatesGroupChanged = new Date();
public int historyMaxItems = DEFAULT_HISTORY_MAX_ITEMS;
public long historyMaxSize = DEFAULT_HISTORY_MAX_SIZE;
public UUID lastSelectedGroup = UUID_ZERO;
public UUID lastTopVisibleGroup = UUID_ZERO;
public MemoryProtectionConfig memoryProtection = new MemoryProtectionConfig();
public List deletedObjects = new ArrayList<>();
public List customIcons = new ArrayList<>();
public Map customData = new HashMap<>();
public KdfParameters kdfParameters = KdfFactory.getDefaultParameters();
public VariantDictionary publicCustomData = new VariantDictionary();
public BinaryPool binPool = new BinaryPool();
public String localizedAppName = "KeePassDroid";
public class MemoryProtectionConfig {
public boolean protectTitle = false;
public boolean protectUserName = false;
public boolean protectPassword = false;
public boolean protectUrl = false;
public boolean protectNotes = false;
public boolean autoEnableVisualHiding = false;
public boolean GetProtection(String field) {
if ( field.equalsIgnoreCase(PwDefsV4.TITLE_FIELD)) return protectTitle;
if ( field.equalsIgnoreCase(PwDefsV4.USERNAME_FIELD)) return protectUserName;
if ( field.equalsIgnoreCase(PwDefsV4.PASSWORD_FIELD)) return protectPassword;
if ( field.equalsIgnoreCase(PwDefsV4.URL_FIELD)) return protectUrl;
if ( field.equalsIgnoreCase(PwDefsV4.NOTES_FIELD)) return protectNotes;
return false;
}
}
@Override
public byte[] getMasterKey(String key, InputStream keyInputStream)
throws InvalidKeyFileException, IOException {
assert(key != null);
byte[] fKey = new byte[]{};
if ( key.length() > 0 && keyInputStream != null) {
return getCompositeKey(key, keyInputStream);
} else if ( key.length() > 0 ) {
fKey = getPasswordKey(key);
} else if ( keyInputStream != null) {
fKey = getFileKey(keyInputStream);
}
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IOException("No SHA-256 implementation");
}
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);
}
}
public void makeFinalKey(byte[] masterSeed, KdfParameters kdfP) throws IOException {
makeFinalKey(masterSeed, kdfP, 0);
}
public void makeFinalKey(byte[] masterSeed, KdfParameters kdfP, long roundsFix)
throws IOException {
KdfEngine kdfEngine = KdfFactory.get(kdfP.kdfUUID);
if (kdfEngine == null) {
throw new IOException("Unknown key derivation function");
}
// Set to 6000 rounds to open corrupted database
if (roundsFix > 0 && kdfP.kdfUUID.equals(AesKdf.CIPHER_UUID)) {
kdfP.setUInt32(AesKdf.ParamRounds, roundsFix);
numKeyEncRounds = roundsFix;
}
byte[] transformedMasterKey = kdfEngine.transform(masterKey, kdfP);
if (transformedMasterKey.length != 32) {
transformedMasterKey = CryptoUtil.hashSha256(transformedMasterKey);
}
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";
}
private static final String RootElementName = "KeyFile";
//private static final String MetaElementName = "Meta";
//private static final String VersionElementName = "Version";
private static final String KeyElementName = "Key";
private static final String KeyDataElementName = "Data";
@Override
protected byte[] loadXmlKeyFile(InputStream keyInputStream) {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(keyInputStream);
Element el = doc.getDocumentElement();
if (el == null || ! el.getNodeName().equalsIgnoreCase(RootElementName)) {
return null;
}
NodeList children = el.getChildNodes();
if (children.getLength() < 2) {
return null;
}
for ( int i = 0; i < children.getLength(); i++ ) {
Node child = children.item(i);
if ( child.getNodeName().equalsIgnoreCase(KeyElementName) ) {
NodeList keyChildren = child.getChildNodes();
for ( int j = 0; j < keyChildren.getLength(); j++ ) {
Node keyChild = keyChildren.item(j);
if ( keyChild.getNodeName().equalsIgnoreCase(KeyDataElementName) ) {
NodeList children2 = keyChild.getChildNodes();
for ( int k = 0; k < children2.getLength(); k++) {
Node text = children2.item(k);
if (text.getNodeType() == Node.TEXT_NODE) {
Text txt = (Text) text;
return Base64Coder.decode(txt.getNodeValue());
}
}
}
}
}
}
} catch (Exception e) {
return null;
}
return null;
}
@Override
public List getGroups() {
List list = new ArrayList();
PwGroupV4 root = (PwGroupV4) rootGroup;
buildChildGroupsRecursive(root, list);
return list;
}
private static void buildChildGroupsRecursive(PwGroupV4 root, List list) {
list.add(root);
for ( int i = 0; i < root.numbersOfChildGroups(); i++) {
PwGroupV4 child = (PwGroupV4) root.getChildGroupAt(i);
buildChildGroupsRecursive(child, list);
}
}
@Override
public List getGrpRoots() {
return rootGroup.getChildGroups();
}
@Override
public List getEntries() {
List list = new ArrayList();
PwGroupV4 root = (PwGroupV4) rootGroup;
buildChildEntriesRecursive(root, list);
return list;
}
private static void buildChildEntriesRecursive(PwGroupV4 root, List list) {
for ( int i = 0; i < root.numbersOfChildEntries(); i++ ) {
list.add(root.getChildEntryAt(i));
}
for ( int i = 0; i < root.numbersOfChildGroups(); i++ ) {
PwGroupV4 child = (PwGroupV4) root.getChildGroupAt(i);
buildChildEntriesRecursive(child, list);
}
}
@Override
public long getNumRounds() {
return numKeyEncRounds;
}
@Override
public void setNumRounds(long rounds) throws NumberFormatException {
numKeyEncRounds = rounds;
}
@Override
public boolean algorithmSettingsEnabled() {
return false;
}
@Override
public PwEncryptionAlgorithm getEncAlgorithm() {
return PwEncryptionAlgorithm.Rjindal;
}
@Override
public PwGroupIdV4 newGroupId() {
PwGroupIdV4 id = new PwGroupIdV4(UUID_ZERO);
while (true) {
id = new PwGroupIdV4(UUID.randomUUID());
if (!isGroupIdUsed(id)) break;
}
return id;
}
@Override
public PwGroup createGroup() {
return new PwGroupV4();
}
@Override
public boolean isBackup(PwGroup group) {
if (!recycleBinEnabled) {
return false;
}
return group.isContainedIn(getRecycleBin());
}
@Override
public void populateGlobals(PwGroup currentGroup) {
groups.put(rootGroup.getId(), rootGroup);
super.populateGlobals(currentGroup);
}
/** Ensure that the recycle bin tree exists, if enabled and create it
* if it doesn't exist
*
*/
private void ensureRecycleBin() {
if (getRecycleBin() == null) {
// Create recycle bin
PwGroupV4 recycleBin = new PwGroupV4(RECYCLEBIN_NAME, iconFactory.getIcon(PwIconStandard.TRASH_BIN));
recycleBin.setEnableAutoType(false);
recycleBin.setEnableSearching(false);
recycleBin.setExpanded(false);
addGroupTo(recycleBin, rootGroup);
recycleBinUUID = recycleBin.getUUID();
}
}
@Override
public boolean isRecycleBinAvailable() {
return true;
}
@Override
public boolean isRecycleBinEnable() {
return recycleBinEnabled;
}
@Override
public boolean canRecycle(PwGroup group) {
if (!recycleBinEnabled) {
return false;
}
PwGroup recycle = getRecycleBin();
return (recycle == null) || (!group.isContainedIn(recycle));
}
@Override
public boolean canRecycle(PwEntry entry) {
if (!recycleBinEnabled) {
return false;
}
PwGroup parent = entry.getParent();
return (parent != null) && canRecycle(parent);
}
@Override
public void recycle(PwGroup group) {
ensureRecycleBin();
PwGroup parent = group.getParent();
removeGroupFrom(group, parent);
parent.touch(false, true);
PwGroup recycleBin = getRecycleBin();
addGroupTo(group, recycleBin);
group.touch(false, true);
// TODO ? group.touchLocation();
}
@Override
public void recycle(PwEntry entry) {
ensureRecycleBin();
PwGroup parent = entry.getParent();
removeEntryFrom(entry, parent);
parent.touch(false, true);
PwGroup recycleBin = getRecycleBin();
addEntryTo(entry, recycleBin);
entry.touch(false, true);
entry.touchLocation();
}
@Override
public void undoRecycle(PwGroup group, PwGroup origParent) {
PwGroup recycleBin = getRecycleBin();
removeGroupFrom(group, recycleBin);
addGroupTo(group, origParent);
}
@Override
public void undoRecycle(PwEntry entry, PwGroup origParent) {
PwGroup recycleBin = getRecycleBin();
removeEntryFrom(entry, recycleBin);
addEntryTo(entry, origParent);
}
@Override
public void deleteEntry(PwEntry entry) {
super.deleteEntry(entry);
deletedObjects.add(new PwDeletedObject(entry.getUUID()));
}
@Override
public void undoDeleteEntry(PwEntry entry, PwGroup origParent) {
super.undoDeleteEntry(entry, origParent);
// TODO undo delete entry
deletedObjects.remove(entry);
}
@Override
public PwGroupV4 getRecycleBin() {
if (recycleBinUUID == null) {
return null;
}
PwGroupId recycleId = new PwGroupIdV4(recycleBinUUID);
return (PwGroupV4) groups.get(recycleId);
}
@Override
public boolean isGroupSearchable(PwGroup group, boolean omitBackup) {
if (!super.isGroupSearchable(group, omitBackup)) {
return false;
}
PwGroupV4 g = (PwGroupV4) group;
return g.isSearchEnabled();
}
@Override
public boolean validatePasswordEncoding(String key) {
return true;
}
@Override
public void initNew(String dbPath) {
String filename = URLUtil.guessFileName(dbPath, null, null);
rootGroup = new PwGroupV4(dbNameFromPath(dbPath), iconFactory.getIcon(PwIconStandard.FOLDER));
groups.put(rootGroup.getId(), rootGroup);
}
private String dbNameFromPath(String dbPath) {
String filename = URLUtil.guessFileName(dbPath, null, null);
if (EmptyUtils.isNullOrEmpty(filename)) {
return "KeePass Database";
}
int lastExtDot = filename.lastIndexOf(".");
if (lastExtDot == -1) {
return filename;
}
return filename.substring(0, lastExtDot);
}
private class GroupHasCustomData extends GroupHandler {
public boolean hasCustomData = false;
@Override
public boolean operate(PwGroup group) {
if (group == null) {
return true;
}
PwGroupV4 g4 = (PwGroupV4) group;
if (g4.containsCustomData()) {
hasCustomData = true;
return false;
}
return true;
}
}
private class EntryHasCustomData extends EntryHandler {
public boolean hasCustomData = false;
@Override
public boolean operate(PwEntry entry) {
if (entry == null) {
return true;
}
PwEntryV4 e4 = (PwEntryV4)entry;
if (e4.containsCustomData()) {
hasCustomData = true;
return false;
}
return true;
}
}
public int getMinKdbxVersion() {
if (!AesKdf.CIPHER_UUID.equals(kdfParameters.kdfUUID)) {
return PwDbHeaderV4.FILE_VERSION_32;
}
if (publicCustomData.size() > 0) {
return PwDbHeaderV4.FILE_VERSION_32;
}
EntryHasCustomData entryHandler = new EntryHasCustomData();
GroupHasCustomData groupHandler = new GroupHasCustomData();
if (rootGroup == null ) {
return PwDbHeaderV4.FILE_VERSION_32_3;
}
rootGroup.preOrderTraverseTree(groupHandler, entryHandler);
if (groupHandler.hasCustomData || entryHandler.hasCustomData) {
return PwDbHeaderV4.FILE_VERSION_32;
}
return PwDbHeaderV4.FILE_VERSION_32_3;
}
}